Office life: everyone roams, meetings convene in pairs, card follows agent

- The detail card now rides beside the clicked agent (flipping and
  clamping at viewport edges via a pure cardPositionFor helper) instead
  of pinning to the top-right corner; it follows a walking agent
- Every agent - the CEO and busy agents included - periodically visits
  the coffee machine, snack bar, and restroom; work status no longer
  recalls a walker mid-break
- The office convenes meetings around the meeting table every few
  minutes: at least two settled attendees walk over, dwell together on
  distinct table spots, and head back; a solo office never meets
- Meeting spots ship as lane-wired amenities so pathing stays correct
  by construction; floor-plan invariants (collider-free, reachable)
  cover them automatically
- e2e asserts the card opens beside the sprite, not in a corner
This commit is contained in:
ZacharyZhang-NY committed 2026-07-08 21:40:53 -04:00
1 parent e55b8b85b0
commit c3924af9ac
10 files changed
+206 -36

No files matched your search

+32
View File
@@ -0,0 +1,32 @@
import { describe, expect, test } from "vitest";
import { cardPositionFor, CARD_W } from "../../src/renderer/src/pages/office/agent-card-position";
describe("agent detail card placement", () => {
test("sits to the right of the sprite, vertically centered on it", () => {
const position = cardPositionFor({ x: 400, y: 300 }, { x: 0, y: 0 }, 1200, 800, 170);
expect(position.left).toBe(418);
expect(position.top).toBe(300 - 16 - 85);
});
test("flips to the left when the right edge would leave the viewport", () => {
const position = cardPositionFor({ x: 1100, y: 300 }, { x: 0, y: 0 }, 1200, 800, 170);
expect(position.left).toBe(1100 - CARD_W - 18);
expect(position.left + CARD_W).toBeLessThanOrEqual(1200 - 8);
});
test("clamps fully on screen near every edge", () => {
for (const agent of [{ x: 5, y: 5 }, { x: 5, y: 795 }, { x: 1195, y: 5 }, { x: 1195, y: 795 }]) {
const position = cardPositionFor(agent, { x: 0, y: 0 }, 1200, 800, 170);
expect(position.left).toBeGreaterThanOrEqual(8);
expect(position.left + CARD_W).toBeLessThanOrEqual(1192);
expect(position.top).toBeGreaterThanOrEqual(8);
expect(position.top + 170).toBeLessThanOrEqual(792);
}
});
test("accounts for the camera offset", () => {
const onScreen = cardPositionFor({ x: 700, y: 500 }, { x: 300, y: 200 }, 1200, 800, 170);
const noCamera = cardPositionFor({ x: 400, y: 300 }, { x: 0, y: 0 }, 1200, 800, 170);
expect(onScreen).toEqual(noCamera);
});
});
+2
View File
@@ -30,6 +30,8 @@ describe("office floor plan", () => {
expect(plan.amenities.filter((a) => a.kind === "restroom").length).toBeGreaterThanOrEqual(2);
expect(plan.amenities.some((a) => a.kind === "coffee")).toBe(true);
expect(plan.amenities.some((a) => a.kind === "snack")).toBe(true);
// Meetings need at least two attendees, so the table has spots for them.
expect(plan.amenities.filter((a) => a.kind === "meeting").length).toBeGreaterThanOrEqual(2);
});
test("desk field grows by rows so no head-count is ever cut off", () => {
+53 -11
View File
@@ -121,30 +121,72 @@ describe("office simulation", () => {
expect(agent.carrying).toBe("coffee");
});
test("working agents never leave their desks", () => {
test("working agents (and the CEO) take breaks too", () => {
const sim = new OfficeSimulation(() => 0);
sim.sync([agentInput("a", "staff", "active")]);
sim.sync([agentInput("ceo", "c_suite", "active")]);
const agent = sim.agents[0];
for (let i = 0; i < 600; i++) sim.step(16, IDLE);
expect(agent.behavior.kind).toBe("seated");
expect(agent.x).toBe(agent.seat.x);
expect(agent.y).toBe(agent.seat.y);
let leftTheDesk = false;
for (let i = 0; i < 3000 && !leftTheDesk; i++) {
sim.step(16, IDLE);
if (agent.behavior.kind !== "seated") leftTheDesk = true;
}
expect(leftTheDesk).toBe(true);
});
test("an agent called to work abandons the break and returns to their desk", () => {
test("a snapshot refresh mid-break never recalls the walker", () => {
const sim = new OfficeSimulation(() => 0);
sim.sync([agentInput("a")]);
const agent = sim.agents[0];
for (let i = 0; i < 3000 && agent.behavior.kind === "seated"; i++) sim.step(16, IDLE);
for (let i = 0; i < 30; i++) sim.step(16, IDLE);
expect(agent.behavior.kind).toBe("walking");
expect(agent.x === agent.seat.x && agent.y === agent.seat.y).toBe(false);
// Becoming busy does not cancel the human ritual of a coffee run.
sim.sync([agentInput("a", "staff", "active")]);
for (let i = 0; i < 6000 && sim.agents[0].behavior.kind !== "seated"; i++) sim.step(16, IDLE);
const refreshed = sim.agents[0];
expect(refreshed.behavior.kind).toBe("walking");
let dwelled = false;
for (let i = 0; i < 6000 && !dwelled; i++) {
sim.step(16, IDLE);
if (refreshed.behavior.kind === "dwelling") dwelled = true;
}
expect(dwelled).toBe(true);
});
test("meetings convene at least two attendees around the table, then everyone returns", () => {
const sim = new OfficeSimulation(() => 0);
sim.sync([agentInput("a", "staff", "active"), agentInput("b"), agentInput("ceo", "c_suite", "active")]);
// Nobody has settled yet: a meeting cannot be convened.
expect(sim.conveneMeeting()).toBe(0);
for (const agent of sim.agents) agent.settledMs = 30_000;
const attendees = sim.conveneMeeting();
expect(attendees).toBeGreaterThanOrEqual(2);
const walkers = sim.agents.filter((agent) => agent.behavior.kind === "walking");
expect(walkers).toHaveLength(attendees);
const spots = new Set(
walkers.map((agent) => (agent.behavior.kind === "walking" ? `${agent.behavior.spot?.stand.x}` : "")),
);
expect(spots.size).toBe(attendees);
// Everyone reaches the meeting room, dwells together, and heads home.
let overlapped = false;
for (let i = 0; i < 12000 && !overlapped; i++) {
sim.step(16, IDLE);
const dwellingInMeeting = sim.agents.filter(
(agent) => agent.behavior.kind === "dwelling" && agent.behavior.spot.kind === "meeting",
);
if (dwellingInMeeting.length >= 2) overlapped = true;
}
expect(overlapped).toBe(true);
});
test("a solo office never convenes a meeting", () => {
const sim = new OfficeSimulation(() => 0);
sim.sync([agentInput("a")]);
sim.agents[0].settledMs = 30_000;
expect(sim.conveneMeeting()).toBe(0);
expect(sim.agents[0].behavior.kind).toBe("seated");
expect(sim.agents[0].x).toBe(sim.agents[0].seat.x);
expect(sim.agents[0].y).toBe(sim.agents[0].seat.y);
});
test("clicks hit the agent drawn at that spot, frontmost first", () => {