Animate the whip: a crack at the strike point and a flinch on the target

Every whip click now lands visibly, wherever the target is standing:

- simulation.whip() always registers a WhipCrackEffect at the struck
  agent and starts their hit reaction; step() ages both away. The
  discipline rule is unchanged - only snack-bar/restroom slackers are
  sent hustling home; desk, CEO office, and meeting stay safe
- office-renderer draws the strike: a lash sweeping in from the upper
  right that collapses into an impact starburst with an expanding ring
  (~450ms), while the struck agent shakes with anime-style pain sparks
  over their head (~600ms), fading as the flinch drains
- The office e2e now collects pageerror during the run and lets the
  effect play out after a whip-mode click, so a throwing frame in the
  rAF loop fails the suite instead of dying silently
This commit is contained in:
ZacharyZhang-NY committed 2026-07-08 23:41:33 -04:00
1 parent 1f89ea7089
commit c50f17190c
6 files changed
+137 -8

No files matched your search

+24
View File
@@ -239,6 +239,30 @@ describe("office simulation", () => {
expect(sim.whip("nobody")).toBe(false);
});
test("every whip strike plays a crack and a flinch, wherever the target is", () => {
const sim = new OfficeSimulation(() => 0);
sim.sync([agentInput("a")]);
const agent = sim.agents[0];
// Struck at the desk: no discipline effect, but the animation still fires.
expect(sim.whip("a")).toBe(false);
expect(agent.behavior.kind).toBe("seated");
expect(agent.hitReactMs).toBeGreaterThan(0);
expect(sim.effects).toHaveLength(1);
expect(sim.effects[0]).toMatchObject({ x: agent.x, ageMs: 0 });
// A second strike stacks another crack; a missing target adds nothing.
sim.whip("a");
expect(sim.effects).toHaveLength(2);
sim.whip("nobody");
expect(sim.effects).toHaveLength(2);
// Both the crack flash and the flinch drain away with time.
for (let i = 0; i < 50; i++) sim.step(16, IDLE);
expect(sim.effects).toHaveLength(0);
expect(agent.hitReactMs).toBe(0);
});
test("clicks hit the agent drawn at that spot, frontmost first", () => {
const sim = new OfficeSimulation();
sim.sync([agentInput("a"), agentInput("b")]);