- 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
88 lines
3.8 KiB
TypeScript
88 lines
3.8 KiB
TypeScript
import { describe, expect, test } from "vitest";
|
|
import { buildFloorPlan, findPath } from "../../src/renderer/src/pages/office/floor-plan";
|
|
import type { FloorPlan, Rect, Vec } from "../../src/renderer/src/pages/office/office-types";
|
|
|
|
function segmentIntersectsRect(a: Vec, b: Vec, rect: Rect): boolean {
|
|
if (a.x === b.x) {
|
|
const [y1, y2] = [Math.min(a.y, b.y), Math.max(a.y, b.y)];
|
|
return a.x > rect.x && a.x < rect.x + rect.w && y2 > rect.y && y1 < rect.y + rect.h;
|
|
}
|
|
const [x1, x2] = [Math.min(a.x, b.x), Math.max(a.x, b.x)];
|
|
return a.y > rect.y && a.y < rect.y + rect.h && x2 > rect.x && x1 < rect.x + rect.w;
|
|
}
|
|
|
|
function expectWalkablePath(plan: FloorPlan, path: Vec[]): void {
|
|
expect(path.length).toBeGreaterThanOrEqual(2);
|
|
for (let i = 0; i + 1 < path.length; i++) {
|
|
const from = path[i];
|
|
const to = path[i + 1];
|
|
expect(from.x === to.x || from.y === to.y).toBe(true);
|
|
for (const collider of plan.colliders) {
|
|
expect(segmentIntersectsRect(from, to, collider)).toBe(false);
|
|
}
|
|
}
|
|
}
|
|
|
|
describe("office floor plan", () => {
|
|
test("provides the four rooms of a real office", () => {
|
|
const plan = buildFloorPlan(5);
|
|
expect(plan.rooms.map((room) => room.kind)).toEqual(["ceo", "meeting", "snack", "restroom"]);
|
|
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", () => {
|
|
const small = buildFloorPlan(3);
|
|
const large = buildFloorPlan(63);
|
|
expect(small.desks.length).toBeGreaterThanOrEqual(3);
|
|
expect(large.desks.length).toBeGreaterThanOrEqual(63);
|
|
expect(large.world.h).toBeGreaterThan(small.world.h);
|
|
expect(large.world.w).toBe(small.world.w);
|
|
});
|
|
|
|
test("desk seats and amenity stands never sit inside a collider", () => {
|
|
const plan = buildFloorPlan(28);
|
|
const standing = (p: Vec): Rect => ({ x: p.x - 7, y: p.y - 7, w: 14, h: 14 });
|
|
const spots = [plan.ceoSeat, ...plan.desks.map((d) => d.seat), ...plan.amenities.map((a) => a.stand)];
|
|
for (const spot of spots) {
|
|
const body = standing(spot);
|
|
for (const collider of plan.colliders) {
|
|
const overlap =
|
|
body.x < collider.x + collider.w &&
|
|
body.x + body.w > collider.x &&
|
|
body.y < collider.y + collider.h &&
|
|
body.y + body.h > collider.y;
|
|
expect(overlap).toBe(false);
|
|
}
|
|
}
|
|
});
|
|
|
|
test("every desk, the CEO seat, and every amenity is reachable from the entrance", () => {
|
|
const plan = buildFloorPlan(17);
|
|
const targets = [plan.ceoSeat, ...plan.desks.map((d) => d.seat), ...plan.amenities.map((a) => a.stand)];
|
|
for (const target of targets) {
|
|
expectWalkablePath(plan, findPath(plan, plan.entrance, target));
|
|
}
|
|
});
|
|
|
|
test("routes between desks and amenities across the whole office", () => {
|
|
const plan = buildFloorPlan(20);
|
|
const lastDesk = plan.desks[plan.desks.length - 1];
|
|
const coffee = plan.amenities.find((a) => a.kind === "coffee")!;
|
|
const restroom = plan.amenities.find((a) => a.kind === "restroom")!;
|
|
expectWalkablePath(plan, findPath(plan, lastDesk.seat, coffee.stand));
|
|
expectWalkablePath(plan, findPath(plan, coffee.stand, restroom.stand));
|
|
expectWalkablePath(plan, findPath(plan, restroom.stand, plan.ceoSeat));
|
|
});
|
|
|
|
test("re-routes from an arbitrary mid-walk position", () => {
|
|
const plan = buildFloorPlan(10);
|
|
const midCorridor = { x: 500, y: 320 };
|
|
const firstDesk = plan.desks[0];
|
|
expectWalkablePath(plan, findPath(plan, midCorridor, firstDesk.seat));
|
|
});
|
|
});
|