import assert from "node:assert/strict"; import { describe, it } from "node:test"; import * as THREE from "three"; import { createOfficeWalker } from "../interiors/officeWalker.ts"; import { Plan } from "../interiors/plan.ts"; import type { Level, Office, Room, Wall } from "../interiors/types.ts"; const FLOOR: Room = { id: "floor", name: "Floor", floor: "floor" as never, outline: [{ x: 0, z: 0 }, { x: 10, z: 0 }, { x: 10, z: 8 }, { x: 0, z: 8 }], }; function makePlan(walls: Wall[] = [], elevation = 2.5): Plan { const level: Level = { id: "ground", name: "Ground", elevation, wallHeight: 3, wallThickness: 0.1, floorplan: { rooms: [FLOOR], walls }, }; const office: Office = { id: "actor-test", name: "Actor Test", levels: [level], viewpoints: [] }; return new Plan(office, { warn: false }); } describe("office walker actor adapter", () => { it("is inert by default and becomes a floor-aware humanoid when activated", () => { const actor = createOfficeWalker(makePlan(), { levelId: "ground", position: { x: 2, z: 2 }, speed: 1, fixedStep: 0.1, }); assert.equal(actor.state().actor, "humanoid"); assert.equal(actor.root.position.y, 2.5); actor.setAction({ x: 2, z: 0 }); actor.tick(0.2); assert.deepEqual(actor.state().position, { x: 2, z: 2 }); actor.setActive(true); assert.deepEqual(actor.action(), { x: 0, z: 0 }, "activation never replays stale input"); actor.setAction({ x: 2, z: 0 }); actor.tick(0.2); assert.ok(actor.state().position.x > 2.19); assert.equal(actor.view.position.y, 2.5); assert.ok(Math.abs(actor.root.rotation.y + Math.PI / 2) < 1e-12); actor.dispose(); }); it("builds an anonymous dog and publishes a defensive chase-camera contract", () => { const actor = createOfficeWalker(makePlan(), { levelId: "ground", position: { x: 4, z: 4 }, actor: { kind: "anonymous-dog", coatColor: 0x222222 }, active: true, }); assert.equal(actor.root.userData.actorType, "anonymous-dog"); const pose = actor.followPose(); assert.ok(pose.position.z > 4, "camera is behind a -Z-facing actor"); assert.ok(pose.target.z < 4, "camera aims ahead of the actor"); pose.position.x = 999; assert.notEqual(actor.followPose().position.x, 999); actor.dispose(); }); it("uses resolved door gaps while retaining wall collision and reset state", () => { const plan = makePlan([{ id: "divider", from: { x: 5, z: 0 }, to: { x: 5, z: 8 }, openings: [{ kind: "door", start: 3.4, width: 1.2, sill: 0, head: 2.1 }], }]); const actor = createOfficeWalker(plan, { levelId: "ground", position: { x: 4, z: 4 }, speed: 2, fixedStep: 0.1, active: true, }); actor.setAction({ x: 1, z: 0 }); for (let index = 0; index < 10; index += 1) actor.tick(0.1); assert.ok(actor.state().position.x > 5.5); const reset = actor.reset({ levelId: "ground", position: { x: 2, z: 2 } }); assert.deepEqual(reset.position, { x: 2, z: 2 }); assert.deepEqual(reset.action, { x: 0, z: 0 }); assert.equal(reset.distance, 0); actor.dispose(); }); it("removes and disposes its actor root idempotently", () => { const actor = createOfficeWalker(makePlan(), { levelId: "ground", position: { x: 2, z: 2 } }); const parent = new THREE.Group(); parent.add(actor.root); actor.dispose(); actor.dispose(); assert.equal(actor.root.parent, null); }); });