1
0

feat: complete actor handoff and piloted aircraft presence

This commit is contained in:
2026-08-11 22:17:06 -07:00
parent 655c383061
commit 3326d2e6d0
20 changed files with 907 additions and 41 deletions
+173
View File
@@ -0,0 +1,173 @@
import assert from "node:assert/strict";
import { describe, it } from "node:test";
import * as THREE from "three";
import { createSceneActor, type ActorIdentity } from "../actors/index.ts";
import { createOfficeWalker, type OfficeActorAppearance } from "../interiors/officeWalker.ts";
import { Plan } from "../interiors/plan.ts";
import type { Level, Office, Room } from "../interiors/types.ts";
import { createJourney, journeyReducer, type JourneyActor } from "../journey/index.ts";
import {
actorKindForPresence,
createDefaultLocalProfile,
resolveHumanoidAppearance,
} from "../profile/index.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 officePlan(): Plan {
const level: Level = {
id: "ground",
name: "Ground",
elevation: 0,
wallHeight: 3,
wallThickness: 0.12,
floorplan: {
rooms: [FLOOR],
walls: [{
id: "dividing-wall",
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 office: Office = {
id: "handoff-office",
name: "Handoff Office",
levels: [level],
viewpoints: [],
};
return new Plan(office, { warn: false });
}
function materialColor(root: THREE.Object3D, meshName: string): string {
const mesh = root.getObjectByName(meshName);
assert.ok(mesh instanceof THREE.Mesh, `${meshName} exists`);
assert.ok(mesh.material instanceof THREE.MeshStandardMaterial, `${meshName} has a standard material`);
return `#${mesh.material.color.getHexString()}`;
}
describe("city and office actor acceptance handoff", () => {
it("hands an anonymous crow to an office dog, walks through doors, and returns to the same crow", () => {
const journeyActor: JourneyActor = {
id: "anonymous",
kind: "crow",
signedIn: false,
profile: { displayName: "Guest" },
};
const identity: ActorIdentity = {
id: journeyActor.id,
displayName: journeyActor.profile.displayName,
authenticated: false,
profile: {},
};
let journey = createJourney({ actor: journeyActor });
journey = journeyReducer(journey, { type: "navigate-to-city", city: "bay-area" });
assert.equal(actorKindForPresence(false, "outdoors"), "crow");
const cityActor = createSceneActor({
kind: "crow",
mode: "flight",
identity,
position: { x: 12, y: 30, z: -8 },
active: true,
fixedStepSeconds: 0.1,
minFlightAltitude: 5,
maxFlightAltitude: 60,
});
cityActor.setActions({ forward: 1, climb: 0.2 });
cityActor.tick(0.5);
cityActor.setActive(false);
const parkedCityState = cityActor.state();
journey = journeyReducer(journey, { type: "enter-office", officeId: "lumbridge-hq" });
assert.equal(journey.location.scale, "office");
assert.deepEqual(journey.actor, journeyActor, "entering never rewrites the serializable identity");
assert.equal(actorKindForPresence(false, "office"), "dog");
const walker = createOfficeWalker(officePlan(), {
levelId: "ground",
position: { x: 4, z: 4 },
actor: { kind: "anonymous-dog" },
active: true,
speed: 2,
fixedStep: 0.1,
});
walker.setAction({ x: 1, z: 0 });
for (let step = 0; step < 10; step += 1) walker.tick(0.1);
assert.ok(walker.state().position.x > 5.5, "the possessed dog passes through the authored door gap");
walker.reset({ levelId: "ground", position: { x: 4, z: 1 } });
walker.setAction({ x: 1, z: 0 });
for (let step = 0; step < 10; step += 1) walker.tick(0.1);
assert.ok(walker.state().position.x < 5, "the same controller cannot cross the solid wall");
journey = journeyReducer(journey, { type: "leave-office" });
assert.equal(journey.location.scale, "bay-area");
assert.deepEqual(journey.actor, journeyActor);
assert.deepEqual(cityActor.state(), parkedCityState, "the outdoor actor stays parked during the office visit");
assert.deepEqual(cityActor.state().identity, identity);
cityActor.setActive(true);
cityActor.setActions({ forward: 1 });
cityActor.tick(0.2);
assert.ok(cityActor.state().distanceM > parkedCityState.distanceM, "the returned crow is playable");
walker.dispose();
cityActor.dispose();
});
it("renders one signed-in humanoid appearance on both sides of the office door", () => {
const profile = createDefaultLocalProfile("member-acceptance", "Morgan");
const appearance = resolveHumanoidAppearance(profile.appearance);
const identity: ActorIdentity = {
id: "member-acceptance",
displayName: profile.displayName,
authenticated: true,
profile: {
appearance: {
skinTone: appearance.skinTone,
primaryColor: appearance.outfitColor,
accentColor: appearance.accentColor,
hairColor: appearance.hairColor,
bodyShape: appearance.bodyShape,
},
},
};
const officeAppearance: OfficeActorAppearance = {
kind: "humanoid",
skinTone: appearance.skinTone,
outfitColor: appearance.outfitColor,
accentColor: appearance.accentColor,
hairColor: appearance.hairColor,
bodyShape: appearance.bodyShape,
};
assert.equal(actorKindForPresence(true, "outdoors"), "humanoid");
assert.equal(actorKindForPresence(true, "office"), "humanoid");
const cityActor = createSceneActor({ kind: "humanoid", identity });
const walker = createOfficeWalker(officePlan(), {
levelId: "ground",
position: { x: 2, z: 2 },
actor: officeAppearance,
});
assert.equal(cityActor.state().identity.id, "member-acceptance");
assert.equal(cityActor.state().identity.displayName, "Morgan");
assert.ok(cityActor.root.getObjectByName("humanoid"));
assert.equal(walker.root.userData.actorType, "humanoid");
assert.equal(materialColor(cityActor.root, "humanoid.chest"), appearance.outfitColor);
assert.equal(materialColor(walker.root, "humanoid.chest"), appearance.outfitColor);
assert.equal(materialColor(cityActor.root, "humanoid.chest.accent"), appearance.accentColor);
assert.equal(materialColor(walker.root, "humanoid.chest.accent"), appearance.accentColor);
assert.equal(materialColor(cityActor.root, "humanoid.hair"), appearance.hairColor);
assert.equal(materialColor(walker.root, "humanoid.hair"), appearance.hairColor);
walker.dispose();
cityActor.dispose();
});
});
+78
View File
@@ -0,0 +1,78 @@
import assert from "node:assert/strict";
import { describe, it } from "node:test";
import {
AircraftController,
createAircraftPoseSnapshot,
} from "../aircraft/index.ts";
import {
PoseInterpolationBuffer,
isEntityPoseSnapshot,
} from "../realtime/index.ts";
describe("playable aircraft realtime bridge", () => {
it("publishes a strict geographic snapshot with deterministic flight axes", () => {
const controller = new AircraftController({
initialHeadingDeg: 90,
initialSpeedMps: 60,
initialAltitudeM: 1_500,
});
const state = controller.snapshot();
const snapshot = createAircraftPoseSnapshot(
{ aircraftId: "aircraft-page-1", pilotActorId: "actor-page-1" },
state,
7,
10_000,
);
assert.equal(isEntityPoseSnapshot(snapshot), true);
assert.equal(snapshot.entity, "aircraft");
assert.equal(snapshot.pose.space, "geographic");
assert.ok(Math.abs(snapshot.velocity.xMps - state.speedMps) < 1e-10);
assert.ok(Math.abs(snapshot.velocity.zMps) < 1e-10);
assert.equal(snapshot.velocity.yMps, state.verticalSpeedMps);
});
it("feeds aircraft samples through the shared deterministic interpolation buffer", () => {
const controller = new AircraftController({ initialHeadingDeg: 0, initialSpeedMps: 50 });
const first = createAircraftPoseSnapshot(
{ aircraftId: "aircraft-page-1", pilotActorId: "actor-page-1" },
controller.snapshot(),
1,
1_000,
);
controller.tick(1, { throttle: 1 });
const second = createAircraftPoseSnapshot(
{ aircraftId: "aircraft-page-1", pilotActorId: "actor-page-1" },
controller.snapshot(),
2,
2_000,
);
const buffer = new PoseInterpolationBuffer({ interpolationDelayMs: 0 });
assert.equal(buffer.push(first), true);
assert.equal(buffer.push(second), true);
const sample = buffer.sample(1_500);
assert.equal(sample?.mode, "interpolated");
assert.equal(sample?.pose.space, "geographic");
if (sample?.pose.space === "geographic" && first.pose.space === "geographic" && second.pose.space === "geographic") {
assert.ok(sample.pose.lat >= Math.min(first.pose.lat, second.pose.lat));
assert.ok(sample.pose.lat <= Math.max(first.pose.lat, second.pose.lat));
}
});
it("rejects non-wire-safe identity and aircraft controls", () => {
const controller = new AircraftController();
assert.throws(() => createAircraftPoseSnapshot(
{ aircraftId: "", pilotActorId: "actor-page-1" },
controller.snapshot(),
1,
1_000,
), RangeError);
const valid = createAircraftPoseSnapshot(
{ aircraftId: "aircraft-page-1", pilotActorId: "actor-page-1" },
controller.snapshot(),
1,
1_000,
);
assert.equal(isEntityPoseSnapshot({ ...valid, throttle: 1.01 }), false);
assert.equal(isEntityPoseSnapshot({ ...valid, rollDeg: Number.NaN }), false);
});
});
+37
View File
@@ -5,6 +5,7 @@ import {
createScenePeers,
scenePeerId,
type ActorPoseSnapshot,
type AircraftPoseSnapshot,
type EntityPoseSnapshot,
type InterestCell,
type ScenePeersOptions,
@@ -48,6 +49,25 @@ function vehicle(sequence: number, timestampMs: number, lat: number): VehiclePos
};
}
function aircraft(sequence: number, timestampMs: number, lat: number): AircraftPoseSnapshot {
return {
entity: "aircraft",
aircraftId: "evtol-1",
kind: "electric-vtail",
pilotActorId: "pilot-1",
sequence,
timestampMs,
pose: { space: "geographic", lat, lng: -121, altitudeM: 1_500, headingDeg: 30, pitchDeg: 5 },
velocity: { xMps: 30, yMps: 2, zMps: 52, yawDegPerSec: 4 },
rollDeg: 18,
throttle: 0.72,
rollInput: 0.4,
pitchInput: -0.2,
yawInput: 0.1,
fanRadians: 2.4,
};
}
function fixture(over: Partial<ScenePeersOptions> = {}) {
return createScenePeers({
project: (lat, lng) => [lng * 2, -lat * 3],
@@ -120,6 +140,23 @@ describe("remote scene peers", () => {
peers.dispose();
});
it("renders a remote piloted aircraft with authoritative bank and rig state", () => {
const peers = fixture();
const snapshot = aircraft(1, 1_000, 37.7);
assert.equal(scenePeerId(snapshot), "aircraft:evtol-1");
assert.equal(peers.upsert(snapshot), true);
assert.equal(peers.tick(1_000), 1);
const root = peers.root.children[0] as THREE.Group;
assert.deepEqual(root.position.toArray(), [-242, 155, -113.10000000000001]);
assert.ok(Math.abs(root.rotation.x - 5 * Math.PI / 180) < 1e-12);
assert.ok(Math.abs(root.rotation.y + 30 * Math.PI / 180) < 1e-12);
assert.ok(Math.abs(root.rotation.z + 18 * Math.PI / 180) < 1e-12);
assert.ok(root.getObjectByName("electric-aircraft"));
assert.equal(root.getObjectByName("electric-aircraft.fan-left")?.rotation.z, 2.4);
assert.ok(Math.abs((root.getObjectByName("electric-aircraft.aileron-left")?.rotation.x ?? 0) - 0.18) < 1e-12);
peers.dispose();
});
it("shares prototype resources while kind changes retain the entity root", () => {
const peers = fixture();
peers.upsert(actor("one", "humanoid", 1, 1_000, 0));