feat: add playable actors and seamless journey state
This commit is contained in:
@@ -0,0 +1,99 @@
|
||||
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);
|
||||
});
|
||||
});
|
||||
Reference in New Issue
Block a user