Public: anyone can read this; the crew can change it.

forked from trevato/cs · proposes

1 shot deag. the deag should 1 shot headshot pretty much everywhere #196

Merged opened this in answer to

Check · Review · Land · Live done — Landed and live in production.

Merged

vesper/cs-engine:vesper/194-1-shot-deag-the-deag-should-1-shot-heads → main 1 commit fork

  • Merged. vesper/194-1-shot-deag-the-deag-should-1-shot-heads was merged into main as 3d4126a.

Review

Sign in to review this change.
7 files changed +139 −65
README.md +12
@@ -159,6 +159,18 @@ the bomb still ticking it watches whoever is left alive rather than your own
159159 body. `src/client/spectate.ts` is that camera, and nothing else in the game
160160 reads it.
161161
162 The guns are in `src/client/weapons.ts` and follow CS's shape: damage falls off
163 with range, armour eats a share of it, a helmet is the only thing that helps a
164 head, and a rifle held down climbs a pattern you can learn. One number is
165 deliberately not CS's. **The Desert Eagle one-taps.** A headshot through a
166 helmet kills at every distance a bullet in this game travels — 110 health at
167 8,192 units, which is longer than the diagonal of any map it generates — because
168 the Deagle carries at 0.955 per 500 units rather than CS's 0.81, which ran out
169 of kill about 2,000 units from the barrel and left the $700 pistol doing 66 to a
170 head across a map that is 5,120 units a side. It is still two in the chest at
171 any range, and no other gun reaches like that: an AK headshot stops killing well
172 short of the far wall, and the AWP is what the far wall is for.
173
162174 At `/range` the same controls drive the second game, on its own bindings:
163175 <kbd>WASD</kbd> move, <kbd>Space</kbd> jump, <kbd>Ctrl</kbd> crouch,
164176 <kbd>Shift</kbd> walk, <kbd>F</kbd> or the mouse fires, <kbd>R</kbd> reload,
src/client/combat.test.ts +16 −1
@@ -2,7 +2,7 @@ import { expect, test } from "bun:test";
22 import { applyHit, hitboxes, traceBullet, type Actor } from "./combat.ts";
33 import type { Box } from "./level.ts";
44 import type { Vec3 } from "../engine/index.ts";
5 import { WEAPONS } from "./weapons.ts";
5 import { BULLET_RANGE, WEAPONS } from "./weapons.ts";
66
77 const actor = (over: Partial<Actor> = {}): Actor => ({
88 id: 1, team: "t", pos: [500, 0, 0], yaw: 180, duckAmount: 0,
@@ -50,6 +50,21 @@ test("two AK headshots are one too many", () => {
5050 expect(target.alive).toBe(false);
5151 });
5252
53 test("a Deagle one-taps a helmeted head from the far corner of the map", () => {
54 // A bullet fired the whole length one can be fired, at a head with a helmet
55 // and a full vest: the trace says head, the hit says killed, and the body is
56 // down in one. This is the end of the wire `weapons.test.ts` measures.
57 const far = actor({ pos: [BULLET_RANGE - 500, 0, 0], armor: 100, helmet: true });
58 const hit = traceBullet([0, 0, 66], [BULLET_RANGE, 0, 66], [], [far], 0);
59 expect(hit.kind).toBe("actor");
60 if (hit.kind !== "actor") return;
61 expect(hit.group).toBe("head");
62 expect(hit.distance).toBeGreaterThan(7000);
63 const result = applyHit(far, WEAPONS.deagle, hit.group, hit.distance);
64 expect(result).toEqual({ damage: 100, killed: true, headshot: true });
65 expect(far.alive).toBe(false);
66 });
67
5368 test("damage credited never exceeds the health that was there", () => {
5469 const target = actor({ health: 12 });
5570 const hit = applyHit(target, WEAPONS.ak47, "chest", 100);
src/client/weapons.test.ts +23 −1
@@ -1,6 +1,7 @@
11 import { expect, test } from "bun:test";
22 import {
3 blindSeconds, buyable, computeDamage, FLASH, HE, inaccuracy, isThrown, recoilOffset, WEAPONS,
3 blindSeconds, BULLET_RANGE, buyable, computeDamage, FLASH, HE, inaccuracy, isThrown, recoilOffset,
4 WEAPONS,
45 } from "./weapons.ts";
56
67 test("an AK chest shot through armour is survivable, a headshot is not", () => {
@@ -34,6 +35,27 @@ test("damage falls off with range", () => {
3435 expect(far).toBeLessThan(near * 0.7);
3536 });
3637
38 test("a Deagle headshot kills a helmeted head at every range a bullet reaches", () => {
39 const deagle = WEAPONS.deagle;
40 const survived: number[] = [];
41 for (let d = 0; d <= BULLET_RANGE; d += 128)
42 if (computeDamage(deagle, d, "head", 100, true).health < 100) survived.push(d);
43 expect(survived).toEqual([]); // a failure names the ranges that lived
44 // The far end is the worst of them, and it still has room to spare.
45 expect(computeDamage(deagle, BULLET_RANGE, "head", 100, true).health).toBe(110);
46 // And bare, which is only easier.
47 expect(computeDamage(deagle, BULLET_RANGE, "head", 0, false).health).toBeGreaterThanOrEqual(100);
48 });
49
50 test("carrying that far is the Deagle's alone, and it is not a body-shot gun", () => {
51 const far = BULLET_RANGE;
52 for (const id of ["glock", "usp", "mp9", "ak47", "m4a4"] as const)
53 expect(computeDamage(WEAPONS[id], far, "head", 100, true).health).toBeLessThan(100);
54 // Two in the chest at any range, never one: the head is what the gun is for.
55 for (const d of [0, 1000, 4000, far])
56 expect(computeDamage(WEAPONS.deagle, d, "chest", 100, true).health).toBeLessThan(100);
57 });
58
3759 test("an AWP body shot kills through armour", () => {
3860 expect(computeDamage(WEAPONS.awp, 1000, "chest", 100, true).health).toBeGreaterThanOrEqual(100);
3961 });
src/client/weapons.ts +28 −1
@@ -96,7 +96,10 @@ const WEAPON_LIST: Weapon[] = [
9696 { id: "knife", name: "Knife", kind: "knife", team: "both", price: 0, damage: 55, armorPenetration: 0.85, rangeModifier: 1, rpm: 240, magazine: 0, reserve: 0, reloadTime: 0, killReward: 1500, carrySpeed: 250, spread: 0, moveSpread: 0, recoil: 0, automatic: true },
9797 { id: "glock", name: "Glock-18", kind: "pistol", team: "t", price: 200, damage: 30, armorPenetration: 0.47, rangeModifier: 0.75, rpm: 400, magazine: 20, reserve: 120, reloadTime: 2.1, killReward: 300, carrySpeed: 240, spread: 0.22, moveSpread: 1.6, recoil: 0.7, automatic: false },
9898 { id: "usp", name: "USP-S", kind: "pistol", team: "ct", price: 200, damage: 35, armorPenetration: 0.505, rangeModifier: 0.79, rpm: 352, magazine: 12, reserve: 24, reloadTime: 2.2, killReward: 300, carrySpeed: 240, spread: 0.18, moveSpread: 1.5, recoil: 0.75, automatic: false },
99 { id: "deagle", name: "Desert Eagle", kind: "pistol", team: "both", price: 700, damage: 63, armorPenetration: 0.925, rangeModifier: 0.81, rpm: 267, magazine: 7, reserve: 35, reloadTime: 2.2, killReward: 300, carrySpeed: 230, spread: 0.25, moveSpread: 2.6, recoil: 2.2, automatic: false },
99 // The Deagle carries: `rangeModifier` is 0.955 rather than CS's 0.81 so that
100 // a headshot through a helmet kills at every distance a bullet in this game
101 // can travel. See `BULLET_RANGE` below for the arithmetic.
102 { id: "deagle", name: "Desert Eagle", kind: "pistol", team: "both", price: 700, damage: 63, armorPenetration: 0.925, rangeModifier: 0.955, rpm: 267, magazine: 7, reserve: 35, reloadTime: 2.2, killReward: 300, carrySpeed: 230, spread: 0.25, moveSpread: 2.6, recoil: 2.2, automatic: false },
100103 { id: "mp9", name: "MP9", kind: "smg", team: "both", price: 1250, damage: 26, armorPenetration: 0.6, rangeModifier: 0.74, rpm: 857, magazine: 30, reserve: 120, reloadTime: 2.1, killReward: 600, carrySpeed: 240, spread: 0.3, moveSpread: 1.1, recoil: 0.6, automatic: true },
101104 { id: "ak47", name: "AK-47", kind: "rifle", team: "t", price: 2700, damage: 36, armorPenetration: 0.775, rangeModifier: 0.98, rpm: 600, magazine: 30, reserve: 90, reloadTime: 2.5, killReward: 300, carrySpeed: 215, spread: 0.16, moveSpread: 3.4, recoil: 1.5, automatic: true },
102105 { id: "m4a4", name: "M4A4", kind: "rifle", team: "ct", price: 3100, damage: 33, armorPenetration: 0.7, rangeModifier: 0.97, rpm: 666, magazine: 30, reserve: 90, reloadTime: 3.1, killReward: 300, carrySpeed: 225, spread: 0.15, moveSpread: 3.2, recoil: 1.25, automatic: true },
@@ -119,6 +122,30 @@ export const WEAPONS: Record<WeaponId, Weapon> = Object.fromEntries(
119122 export const buyable = (team: "t" | "ct"): Weapon[] =>
120123 WEAPON_LIST.filter((w) => w.kind !== "knife" && (w.team === "both" || w.team === team));
121124
125 /**
126 * How far a bullet travels before it stops, in units: CS's 8,192, which is
127 * longer than the diagonal of any map this game generates (40 cells of 128
128 * units is 5,120 a side, so 7,241 corner to corner) and therefore the longest
129 * shot anybody can take.
130 *
131 * It lives here because it is a number about guns and because the Deagle's
132 * promise is stated against it: *a Deagle headshot kills a helmeted body at
133 * any range a bullet reaches*. Working it through `computeDamage` at the far
134 * end of that range —
135 *
136 * 63 × 4 (head) × 0.955^(8192/500) × 0.925 (armour) = 110 health
137 *
138 * — which is over 100, and every shorter shot hits harder than that one. At
139 * CS's own 0.81 the same shot stopped killing past about 2,000 units and was
140 * down to 66 health at 3,000, which is a corridor and a half: a Deagle that
141 * stops one-tapping halfway across the map is not the gun anybody is aiming
142 * with, and the dial that says how well a round carries is this one.
143 *
144 * `src/client/world.ts` traces every bullet to exactly this distance, so the
145 * claim is not an approximation of the simulation — it is the simulation.
146 */
147 export const BULLET_RANGE = 8192;
148
122149 export const ARMOR_PRICE = 650;
123150 export const HELMET_PRICE = 350;
124151 export const DEFUSE_KIT_PRICE = 400;
src/client/world.ts +2 −4
@@ -30,7 +30,7 @@ import {
3030 import * as rules from "./match.ts";
3131 import { CS_HULL, CS_MOVE } from "./movement.ts";
3232 import {
33 WEAPONS, inaccuracy, recoilOffset, RECOIL_SHARE,
33 BULLET_RANGE, WEAPONS, inaccuracy, recoilOffset, RECOIL_SHARE,
3434 type ThrownId, type WeaponId, type Weapon,
3535 } from "./weapons.ts";
3636 import { botBuy, botThink, type BotBrain } from "./bots.ts";
@@ -331,8 +331,6 @@ function checkRoundEnd(world: World): void {
331331
332332 // --- shooting -------------------------------------------------------------
333333
334 const RANGE = 8192;
335
336334 export function currentWeapon(p: Player): Weapon {
337335 return WEAPONS[p.weapon] ?? WEAPONS.knife;
338336 }
@@ -387,7 +385,7 @@ export function shoot(world: World, p: Player): boolean {
387385 p.punch = [p.punch[0] + (rx - p.punch[0]) * 0.5, p.punch[1] + (ry - p.punch[1]) * 0.5];
388386
389387 const from = eyePos(p);
390 const range = w.kind === "knife" ? 64 : RANGE;
388 const range = w.kind === "knife" ? 64 : BULLET_RANGE;
391389 const dir = angleVectors(yaw, pitch).forward;
392390 const to: Vec3 = [from[0] + dir[0] * range, from[1] + dir[1] * range, from[2] + dir[2] * range];
393391 const hit = traceBullet(from, to, world.level.boxes, world.players, p.id);
static/app.6bbbaf11e0.js added +58

Diff not shown — a line 72,599 characters wide (limit 2,000). +58

View file
static/app.be118e74ac.js deleted −58

Diff not shown — a line 72,602 characters wide (limit 2,000). −58

View file

↑↓ navigate ↵ open ⌘↵ its app esc close

Keyboard shortcuts

Docs