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

Merged opened this

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

Merged

scout/cs:scout/174-bomb-planted-where-the-message-line-and → main 1 commit fork

  • Merged. scout/174-bomb-planted-where-the-message-line-and was merged into main as 9ba7609.

Review

Sign in to review this change.
3 files changed +74 −6
README.md +4 −1
@@ -177,7 +177,10 @@ wants one, but the running server never reads it.
177177 <kbd>Shift</kbd> walk, mouse aims and fires, the right mouse button or
178178 <kbd>Q</kbd> scopes on the AWP, <kbd>R</kbd> reload,
179179 <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
181184 time, <kbd>1</kbd>/<kbd>2</kbd>/<kbd>3</kbd> switch guns and <kbd>4</kbd> takes
182185 out the HE grenade if you bought one (fire throws it, and the hand goes back to
183186 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";
22 import { GRENADE } from "./grenades.ts";
33 import { teleported } from "./interpolate.ts";
44 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";
66 import { REPEAT_GAP, SIDE_GAP } from "./callout.ts";
77 import { halfTime, SHORT, winReward, type Rules } from "./match.ts";
88 import { GAIT_CYCLE, STEP_STRIDE } from "./physics.ts";
@@ -10,8 +10,8 @@ import { WEAPONS, type WeaponId } from "./weapons.ts";
1010 import { snapshot, type Saved } from "./saved.ts";
1111 import {
1212 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,
1515 type Input, type Player, type World,
1616 } from "./world.ts";
1717
@@ -110,6 +110,56 @@ test("the terrorists plant, and a planted bomb keeps the round alive past the cl
110110 expect(soundsNamed(world, "plant")).toBe(1);
111111 });
112112
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
113163 test("a defusing CT wins the round, and the money follows the rules", () => {
114164 const world = createWorld(SHORT, "me", "ct", 5, 13);
115165 run(world, SHORT.freezeTime + 0.2);
src/client/world.ts +17 −2
@@ -525,7 +525,14 @@ export function startRound(world: World): void {
525525 }
526526 }
527527
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 */
529536 function announce(world: World, winner: Team, reason: rules.EndReason): void {
530537 for (const p of world.players) {
531538 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,
865872 victim.hasBomb = false;
866873 world.bomb.state = "dropped";
867874 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.
868879 }
869880 // The gun they were holding stays where they fell. A knife and a grenade are
870881 // 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 {
10901101 rules.plantBomb(world.match);
10911102 bomb.beepAt = 0;
10921103 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}`);
10941109 }
10951110 } else {
10961111 bomb.plantProgress = 0;

↑↓ navigate ↵ open ⌘↵ its app esc close

Keyboard shortcuts

Docs