Split The code is public; the app answers only to the crew.
Killed by whom, with what — but not how close you came: the death notice has no killer health #238
Check · Review · Land · Live done — Landed and live in production.
scout/cs:scout/204-killed-by-whom-with-what-but-not-how-clo → main 1 commit fork
8 files
changed +64
−10
README.md +5 −2
| @@ -495,8 +495,11 @@ be sitting on the radar, the health or the ammo. | ||
| 495 | 495 | |
| 496 | 496 | When you die the camera moves to a teammate who is still up and the HUD becomes |
| 497 | 497 | theirs — their health, armour, ammo and gun, no crosshair — over a line naming |
| 498 | who killed you and with what. Clicking, or the fire key, moves on to the next | |
| 499 | teammate. Whoever dies stays on the floor where they fell, lying the way they | |
| 498 | who killed you, with what, and how much health they had left, so a lost duel | |
| 499 | says whether you were outplayed or one bullet away. That last number is a | |
| 500 | fact about the moment you died, not a live reading of the killer, and it is | |
| 501 | silent when nobody else's death caused yours (a fall, the bomb, your own | |
| 502 | grenade). Clicking, or the fire key, moves on to the next teammate. Whoever dies stays on the floor where they fell, lying the way they | |
| 500 | 503 | were facing and tinted their side, until the next round starts — so the rifle |
| 501 | 504 | dropped in a doorway has a body under it and a fight leaves a record of itself. |
| 502 | 505 | A body is decoration and nothing more: bullets go through it, it blocks nobody, |
src/client/bots.test.ts +2 −2
| @@ -559,7 +559,7 @@ test("a call outranks the rotation a counter-terrorist would have made on its ow | ||
| 559 | 559 | fallen.pos = [...DE_FORGE.sites.B] as [number, number, number]; |
| 560 | 560 | world.feed.push({ |
| 561 | 561 | at: world.time, killer: "them", killerId: 9, victim: fallen.name, victimId: fallen.id, |
| 562 | weapon: "ak47", headshot: false, killerTeam: "t", | |
| 562 | weapon: "ak47", headshot: false, killerTeam: "t", killerHealth: 100, | |
| 563 | 563 | }); |
| 564 | 564 | |
| 565 | 565 | const standing = cts.filter((p) => p.alive); |
| @@ -741,7 +741,7 @@ test("a call turns a rotating defender round inside its repath window", () => { | ||
| 741 | 741 | fallen.pos = [...DE_FORGE.sites.B] as [number, number, number]; |
| 742 | 742 | world.feed.push({ |
| 743 | 743 | at: world.time, killer: "them", killerId: 9, victim: fallen.name, victimId: fallen.id, |
| 744 | weapon: "ak47", headshot: false, killerTeam: "t", | |
| 744 | weapon: "ak47", headshot: false, killerTeam: "t", killerHealth: 100, | |
| 745 | 745 | }); |
| 746 | 746 | thinkAll(world); |
| 747 | 747 | const rotated = defenders(world).filter((p) => p.alive && p.brain?.site === "A"); |
src/client/hud.test.ts +17 −3
| @@ -255,7 +255,7 @@ const spectatedWorld = (): World => { | ||
| 255 | 255 | world.me.health = 0; |
| 256 | 256 | world.feed.push({ |
| 257 | 257 | at: world.time, killer: "Krait", killerId: 9, victim: world.me.name, victimId: world.me.id, |
| 258 | weapon: "ak47", headshot: true, killerTeam: "t", | |
| 258 | weapon: "ak47", headshot: true, killerTeam: "t", killerHealth: 23, | |
| 259 | 259 | }); |
| 260 | 260 | return world; |
| 261 | 261 | }; |
| @@ -274,6 +274,7 @@ test("dead, the HUD is the watched player's: their numbers, no crosshair, and wh | ||
| 274 | 274 | expect(texts.join("|")).toContain(`Watching ${mate.name}`); |
| 275 | 275 | expect(texts.join("|")).toContain("Krait killed you with AK-47"); |
| 276 | 276 | expect(texts.join("|")).toContain("headshot"); |
| 277 | expect(texts.join("|")).toContain("23 hp left"); // how close you came | |
| 277 | 278 | // The crosshair is four strokes out from the middle; a spectator gets none. |
| 278 | 279 | expect(frame.calls.filter((c) => c.name === "moveTo" && c.args[1] === H / 2).length).toBe(0); |
| 279 | 280 | }); |
| @@ -282,7 +283,7 @@ test("a fall that killed you says so, rather than naming you as your own killer" | ||
| 282 | 283 | const world = spectatedWorld(); |
| 283 | 284 | world.feed.push({ |
| 284 | 285 | at: world.time, killer: world.me.name, killerId: world.me.id, victim: world.me.name, victimId: world.me.id, |
| 285 | weapon: "fall", headshot: false, killerTeam: "ct", | |
| 286 | weapon: "fall", headshot: false, killerTeam: "ct", killerHealth: null, | |
| 286 | 287 | }); |
| 287 | 288 | const mate = world.players.find((p) => p.alive && p.team === "ct") as Player; |
| 288 | 289 | const frame = recorder(); |
| @@ -292,6 +293,19 @@ test("a fall that killed you says so, rather than naming you as your own killer" | ||
| 292 | 293 | expect(texts).toContain(`${world.me.name} Fall ${world.me.name}`); |
| 293 | 294 | }); |
| 294 | 295 | |
| 296 | test("a self-kill says nothing about health: there is no one else's hp to report", () => { | |
| 297 | const world = spectatedWorld(); | |
| 298 | world.feed.push({ | |
| 299 | at: world.time, killer: world.me.name, killerId: world.me.id, victim: world.me.name, victimId: world.me.id, | |
| 300 | weapon: "bomb", headshot: false, killerTeam: "ct", killerHealth: null, | |
| 301 | }); | |
| 302 | const mate = world.players.find((p) => p.alive && p.team === "ct") as Player; | |
| 303 | const frame = recorder(); | |
| 304 | draw(frame, world, mate); | |
| 305 | const texts = frame.calls.filter((c) => c.name === "fillText").map((c) => c.text).join("|"); | |
| 306 | expect(texts).not.toContain("hp left"); | |
| 307 | }); | |
| 308 | ||
| 295 | 309 | test("a spectator does not inherit the dead player's hit markers", () => { |
| 296 | 310 | const world = spectatedWorld(); |
| 297 | 311 | const mate = world.players.find((p) => p.alive && p.team === "ct") as Player; |
| @@ -723,7 +737,7 @@ test("reduced motion leaves the hit marker, the kill feed and the crosshair exac | ||
| 723 | 737 | world.effects.push({ kind: "hitmark", at: world.time - 0.1, headshot: true, killed: true }); |
| 724 | 738 | world.feed.push({ |
| 725 | 739 | at: world.time, killer: "me", killerId: world.me.id, killerTeam: "ct", victim: "them", |
| 726 | weapon: "ak47", headshot: true, victimId: 5, | |
| 740 | weapon: "ak47", headshot: true, victimId: 5, killerHealth: 80, | |
| 727 | 741 | }); |
| 728 | 742 | // Every call, argument for argument and alpha for alpha: the whole of the |
| 729 | 743 | // rest of the HUD is information, and a setting that moved any of it would |
src/client/hud.ts +2 −1
| @@ -606,8 +606,9 @@ export function drawHud( | ||
| 606 | 606 | if (notice !== null) { |
| 607 | 607 | ctx.font = mid; |
| 608 | 608 | ctx.fillStyle = "#ff8f86"; |
| 609 | const hp = notice.killerHealth === null ? "" : `, ${Math.round(clamp(notice.killerHealth, 0, 100))} hp left`; | |
| 609 | 610 | const how = notice.weapon === "fall" ? "You fell too far" : |
| 610 | `${notice.killer} killed you with ${causeName(notice.weapon)}` + (notice.headshot ? " — headshot" : ""); | |
| 611 | `${notice.killer} killed you with ${causeName(notice.weapon)}` + (notice.headshot ? " — headshot" : "") + hp; | |
| 611 | 612 | ctx.fillText(how, width / 2, top + Math.round(30 * scale)); |
| 612 | 613 | } |
| 613 | 614 | ctx.font = small; |
src/client/saved.test.ts +13
| @@ -82,6 +82,19 @@ test("snapshot reads only what world.ts already exports, and parses back", () => | ||
| 82 | 82 | expect(parseSaved(JSON.stringify(saved))).toEqual(saved); |
| 83 | 83 | }); |
| 84 | 84 | |
| 85 | test("the kill feed, killer health included, is not part of a snapshot", () => { | |
| 86 | const world: World = createWorld(SHORT, "scout", "ct", 5, 9); | |
| 87 | const before = snapshot(world, crypto.randomUUID()); | |
| 88 | world.feed.push({ | |
| 89 | at: world.time, killer: "them", killerId: 1, victim: world.me.name, victimId: world.me.id, | |
| 90 | weapon: "ak47", headshot: false, killerTeam: "t", killerHealth: 23, | |
| 91 | }); | |
| 92 | const after = snapshot(world, before.matchId); | |
| 93 | expect(after).toEqual(before); | |
| 94 | expect(JSON.stringify(after)).not.toMatch(/killerHealth/); | |
| 95 | expect(parseSaved(JSON.stringify(after))).toEqual(after); | |
| 96 | }); | |
| 97 | ||
| 85 | 98 | test("snapshot carries nothing mid-round: no positions, no health, no bomb", () => { |
| 86 | 99 | const world: World = createWorld(SHORT, "scout", "ct", 5, 8); |
| 87 | 100 | const saved = snapshot(world, crypto.randomUUID()); |
src/client/spectate.test.ts +2 −2
| @@ -56,12 +56,12 @@ test("the kill feed says who killed you, and nothing when it has no line for you | ||
| 56 | 56 | const bot = world.players[1] as Player; |
| 57 | 57 | world.feed.push({ |
| 58 | 58 | at: world.time, killer: bot.name, killerId: bot.id, victim: world.me.name, victimId: world.me.id, |
| 59 | weapon: "ak47", headshot: true, killerTeam: bot.team, | |
| 59 | weapon: "ak47", headshot: true, killerTeam: bot.team, killerHealth: 42, | |
| 60 | 60 | }); |
| 61 | 61 | // A namesake killed somewhere else does not become your killer: ids decide. |
| 62 | 62 | world.feed.push({ |
| 63 | 63 | at: world.time, killer: "someone", killerId: 8, victim: world.me.name, victimId: 7, |
| 64 | weapon: "usp", headshot: false, killerTeam: "ct", | |
| 64 | weapon: "usp", headshot: false, killerTeam: "ct", killerHealth: 100, | |
| 65 | 65 | }); |
| 66 | 66 | const notice = deathNotice(world, world.me); |
| 67 | 67 | expect(notice?.killer).toBe(bot.name); |
src/client/world.test.ts +15
| @@ -732,6 +732,7 @@ test("a bomb kill runs the same path as any other: a hurt, a named feed line, a | ||
| 732 | 732 | // Nobody planted this for a kill: the victim stands in for the killer, |
| 733 | 733 | // the same way a suicide by your own grenade does. |
| 734 | 734 | expect(entry?.killerId).toBe(world.me.id); |
| 735 | expect(entry?.killerHealth).toBe(null); // no one else's hp to report on a suicide | |
| 735 | 736 | |
| 736 | 737 | expect(world.bodies.length).toBe(1); |
| 737 | 738 | }); |
| @@ -1820,6 +1821,20 @@ test("a bot's knife in your back ends it in one, and is credited like any kill", | ||
| 1820 | 1821 | expect(damageDealt(world, bot.id)[0].entry.damage).toBe(100); |
| 1821 | 1822 | }); |
| 1822 | 1823 | |
| 1824 | test("the feed keeps the killer's health as it was at the kill, not as it is now", () => { | |
| 1825 | const world = createWorld(SHORT, "me", "ct", 5, 402); | |
| 1826 | const bot = world.players.find((p) => p.team === "t") as Player; | |
| 1827 | bot.health = 23; | |
| 1828 | knifeRange(world, bot, world.me, 0); | |
| 1829 | expect(shoot(world, bot)).toBe(true); | |
| 1830 | expect(world.me.alive).toBe(false); | |
| 1831 | const line = world.feed[world.feed.length - 1]; | |
| 1832 | expect(line.killerId).toBe(bot.id); | |
| 1833 | expect(line.killerHealth).toBe(23); | |
| 1834 | bot.health = 5; // hurt after the fact, in another fight entirely | |
| 1835 | expect(world.feed[world.feed.length - 1].killerHealth).toBe(23); | |
| 1836 | }); | |
| 1837 | ||
| 1823 | 1838 | test("the same bot stabbing you in the chest leaves you standing", () => { |
| 1824 | 1839 | const world = createWorld(SHORT, "me", "ct", 5, 403); |
| 1825 | 1840 | const bot = world.players.find((p) => p.team === "t") as Player; |
src/client/world.ts +8
| @@ -157,6 +157,13 @@ export type FeedEntry = { | ||
| 157 | 157 | weapon: Cause; |
| 158 | 158 | headshot: boolean; |
| 159 | 159 | killerTeam: Team; |
| 160 | /** | |
| 161 | * The killer's health the instant the kill landed, taken once here and | |
| 162 | * never re-read: a corpse's feed line must not change as the world moves | |
| 163 | * on. Null when the killer is the victim (a fall, a bomb, your own | |
| 164 | * grenade) — there is no "how close" to a death nobody else caused. | |
| 165 | */ | |
| 166 | killerHealth: number | null; | |
| 160 | 167 | }; |
| 161 | 168 | |
| 162 | 169 | /** One pairing in the round's damage ledger: what one player did to another. */ |
| @@ -990,6 +997,7 @@ function onKill(world: World, killer: Player, victim: Player, weapon: Cause, hea | ||
| 990 | 997 | killer: killer.name, killerId: killer.id, |
| 991 | 998 | victim: victim.name, victimId: victim.id, |
| 992 | 999 | weapon, headshot, killerTeam: killer.team, |
| 1000 | killerHealth: killer === victim ? null : killer.health, | |
| 993 | 1001 | }); |
| 994 | 1002 | if (world.feed.length > 5) world.feed.shift(); |
| 995 | 1003 | if (victim.hasBomb) { |