Split The code is public; the app answers only to the crew.
Bomb planted — *where*: the message line and the kill feed name the place, in the map's own words #188
Check · Review · Land · Live done — Landed and live in production.
scout/cs:scout/174-bomb-planted-where-the-message-line-and → main 1 commit fork
3 files
changed +74
−6
README.md +4 −1
| @@ -177,7 +177,10 @@ wants one, but the running server never reads it. | ||
| 177 | 177 | <kbd>Shift</kbd> walk, mouse aims and fires, the right mouse button or |
| 178 | 178 | <kbd>Q</kbd> scopes on the AWP, <kbd>R</kbd> reload, |
| 179 | 179 | <kbd>E</kbd> plants, defuses or takes the gun you are standing on (the HUD says |
| 180 | which gun when it is one you have to ask for), <kbd>B</kbd> opens the buy menu in the freeze | |
| 180 | which gun when it is one you have to ask for). Planting puts a line on the | |
| 181 | message log naming the site it went down on — `Bomb planted — B site` — in | |
| 182 | the same words `placeAt` gives a callout, so a plant you did not see is still | |
| 183 | a place to run to rather than a beep to go hunting for. <kbd>B</kbd> opens the buy menu in the freeze | |
| 181 | 184 | time, <kbd>1</kbd>/<kbd>2</kbd>/<kbd>3</kbd> switch guns and <kbd>4</kbd> takes |
| 182 | 185 | out the HE grenade if you bought one (fire throws it, and the hand goes back to |
| 183 | 186 | the gun once it is gone), <kbd>Z</kbd> opens the radio, <kbd>Tab</kbd> shows |
src/client/world.test.ts +53 −3
| @@ -2,7 +2,7 @@ import { expect, test } from "bun:test"; | ||
| 2 | 2 | import { GRENADE } from "./grenades.ts"; |
| 3 | 3 | import { teleported } from "./interpolate.ts"; |
| 4 | 4 | import { dist } from "./math.ts"; |
| 5 | import { DE_FORGE, MAP_NAMES, MAPS, siteAt } from "./map.ts"; | |
| 5 | import { DE_FORGE, MAP_NAMES, MAPS, placeAt, siteAt } from "./map.ts"; | |
| 6 | 6 | import { REPEAT_GAP, SIDE_GAP } from "./callout.ts"; |
| 7 | 7 | import { halfTime, SHORT, winReward, type Rules } from "./match.ts"; |
| 8 | 8 | import { GAIT_CYCLE, STEP_STRIDE } from "./physics.ts"; |
| @@ -10,8 +10,8 @@ import { WEAPONS, type WeaponId } from "./weapons.ts"; | ||
| 10 | 10 | import { snapshot, type Saved } from "./saved.ts"; |
| 11 | 11 | import { |
| 12 | 12 | aimAt, applyInput, bodyCentre, BOMB_BLAST_RADIUS, buy, callout, createWorld, currentWeapon, damageDealt, |
| 13 | damageTaken, ENVIRONMENT, eyePos, flinch, grenadeCount, grenadeSlot, MAX_GRENADES, newBomb, nextGrenade, | |
| 14 | NO_INPUT, offeredPickup, radioCall, restoreWorld, shoot, startRound, update, | |
| 13 | damageTaken, ENVIRONMENT, eyePos, finishRound, flinch, grenadeCount, grenadeSlot, MAX_GRENADES, newBomb, | |
| 14 | nextGrenade, NO_INPUT, offeredPickup, radioCall, restoreWorld, shoot, startRound, update, | |
| 15 | 15 | type Input, type Player, type World, |
| 16 | 16 | } from "./world.ts"; |
| 17 | 17 | |
| @@ -110,6 +110,56 @@ test("the terrorists plant, and a planted bomb keeps the round alive past the cl | ||
| 110 | 110 | expect(soundsNamed(world, "plant")).toBe(1); |
| 111 | 111 | }); |
| 112 | 112 | |
| 113 | /** Hold `use` on a site until the bomb goes down, keeping the planter alive to see it through. */ | |
| 114 | function plant(world: World, pos: [number, number, number]): void { | |
| 115 | for (const p of world.players) p.hasBomb = false; | |
| 116 | world.me.hasBomb = true; | |
| 117 | world.me.pos = pos; | |
| 118 | for (let t = 0; t < world.match.rules.plantTime + 0.3; t += TICK) { | |
| 119 | update(world, TICK, { ...NO_INPUT, use: true }); | |
| 120 | world.me.health = 100; | |
| 121 | } | |
| 122 | } | |
| 123 | ||
| 124 | test("a bomb planted on each site of each map names that site, in the map's own words", () => { | |
| 125 | for (const name of MAP_NAMES) { | |
| 126 | const map = MAPS[name]; | |
| 127 | for (const site of ["A", "B"] as const) { | |
| 128 | const world = createWorld(SHORT, "me", "t", 5, 41, "normal", map); | |
| 129 | run(world, SHORT.freezeTime + 0.2); | |
| 130 | plant(world, [map.sites[site][0], map.sites[site][1], 2]); | |
| 131 | expect(world.bomb.state).toBe("planted"); | |
| 132 | const place = placeAt(map, world.bomb.pos); | |
| 133 | expect(place).not.toBeNull(); | |
| 134 | expect(world.messages[world.messages.length - 1]?.text).toBe(`Bomb planted — ${place}`); | |
| 135 | } | |
| 136 | } | |
| 137 | }); | |
| 138 | ||
| 139 | test("the plant message is said once, not once a tick", () => { | |
| 140 | const world = createWorld(SHORT, "me", "t", 5, 42); | |
| 141 | run(world, SHORT.freezeTime + 0.2); | |
| 142 | plant(world, [DE_FORGE.sites.A[0], DE_FORGE.sites.A[1], 2]); | |
| 143 | // Held well past the plant finishing: the site is already "planted", so | |
| 144 | // holding `use` longer must not push a second line. | |
| 145 | run(world, 1, { ...NO_INPUT, use: true }); | |
| 146 | expect(world.messages.filter((m) => m.text.startsWith("Bomb planted")).length).toBe(1); | |
| 147 | }); | |
| 148 | ||
| 149 | test("a round restart replaces the plant line with the round's own result", () => { | |
| 150 | const world = createWorld(SHORT, "me", "t", 5, 43); | |
| 151 | run(world, SHORT.freezeTime + 0.2); | |
| 152 | plant(world, [DE_FORGE.sites.A[0], DE_FORGE.sites.A[1], 2]); | |
| 153 | expect(world.messages[world.messages.length - 1]?.text).toBe("Bomb planted — A site"); | |
| 154 | finishRound(world, "t", "detonated"); | |
| 155 | // The round's own announcement already overwrites the display, and the | |
| 156 | // freeze that follows holds that line rather than the plant's. | |
| 157 | expect(world.messages[world.messages.length - 1]?.text).not.toBe("Bomb planted — A site"); | |
| 158 | run(world, world.match.rules.endDelay + world.match.rules.freezeTime + 0.2); | |
| 159 | expect(world.match.phase).toBe("live"); | |
| 160 | expect(world.messages[world.messages.length - 1]?.text).not.toBe("Bomb planted — A site"); | |
| 161 | }); | |
| 162 | ||
| 113 | 163 | test("a defusing CT wins the round, and the money follows the rules", () => { |
| 114 | 164 | const world = createWorld(SHORT, "me", "ct", 5, 13); |
| 115 | 165 | run(world, SHORT.freezeTime + 0.2); |
src/client/world.ts +17 −2
| @@ -525,7 +525,14 @@ export function startRound(world: World): void { | ||
| 525 | 525 | } |
| 526 | 526 | } |
| 527 | 527 | |
| 528 | /** Pay everyone for the round and put a line on the screen about it. */ | |
| 528 | /** | |
| 529 | * Pay everyone for the round and put a line on the screen about it. | |
| 530 | * | |
| 531 | * No place here, on purpose: a defuse happens right where the plant message | |
| 532 | * already named, and the round is over by the time this line is read — there | |
| 533 | * is nowhere left to run to. Naming it twice would not tell anyone anything | |
| 534 | * the plant line did not. | |
| 535 | */ | |
| 529 | 536 | function announce(world: World, winner: Team, reason: rules.EndReason): void { |
| 530 | 537 | for (const p of world.players) { |
| 531 | 538 | const reward = rules.roundReward(world.match, p.team, winner, reason, world.bombPlanted); |
| @@ -865,6 +872,10 @@ function onKill(world: World, killer: Player, victim: Player, weapon: WeaponId, | ||
| 865 | 872 | victim.hasBomb = false; |
| 866 | 873 | world.bomb.state = "dropped"; |
| 867 | 874 | world.bomb.pos = [victim.pos[0], victim.pos[1], victim.pos[2] + 4]; |
| 875 | // No place here either: there was never a message line for a drop, only | |
| 876 | // the kill feed entry above, which already names who fell and where the | |
| 877 | // fight was. Giving the drop a line of its own is a feature this issue | |
| 878 | // did not ask for, not a place this issue left unnamed. | |
| 868 | 879 | } |
| 869 | 880 | // The gun they were holding stays where they fell. A knife and a grenade are |
| 870 | 881 | // never pickups, so someone killed holding one leaves their primary instead |
| @@ -1090,7 +1101,11 @@ function updateBomb(world: World, p: Player, input: Input, dt: number): void { | ||
| 1090 | 1101 | rules.plantBomb(world.match); |
| 1091 | 1102 | bomb.beepAt = 0; |
| 1092 | 1103 | world.effects.push({ kind: "sound", at: world.time, pos: bomb.pos, name: "plant", by: p.id }); |
| 1093 | say(world, `The bomb has been planted at ${site}.`); | |
| 1104 | // One lookup, here at the moment of the plant, not one a tick: the | |
| 1105 | // place named is `placeAt`'s own word for it, the same vocabulary a | |
| 1106 | // callout uses, so a plant nobody saw is still a place to run to. | |
| 1107 | const place = placeAt(world.map, bomb.pos); | |
| 1108 | say(world, place === null ? "Bomb planted" : `Bomb planted — ${place}`); | |
| 1094 | 1109 | } |
| 1095 | 1110 | } else { |
| 1096 | 1111 | bomb.plantProgress = 0; |
new above
offline