1
0

feat: add playable actors and seamless journey state

This commit is contained in:
2026-08-11 19:10:14 -07:00
parent 4e313c4a79
commit a2a52bfdae
19 changed files with 2778 additions and 24 deletions
+33 -8
View File
@@ -29,13 +29,12 @@
* station said so is a nice idea and a bad dependency for a room that has to
* render with no network at all.
*
* ### Orbit dollhouse is the only navigation mode
* ### Orbit dollhouse remains the default navigation mode
*
* Walk mode is not built here. `Plan` already produces the collision segments it
* will need, which is the point of doing the wall split once, but v1 orbits: the
* ceilings come off, the walls between you and what you are looking at go
* translucent, and the existing camera, flight and picking machinery is reused
* verbatim.
* An optional `OfficeWalker` can temporarily possess a local actor and publish a
* chase-camera pose. It is inactive by default; without one, or while inactive,
* the ceilings, occlusion fading, named views and orbit controls behave exactly
* as before.
*
* ### Two depths, and the public one is the architecture without the people
*
@@ -83,6 +82,11 @@ import { Plan, type Depth, type PlanOptions } from "./plan.ts";
import { createPresenceLayer, type PresenceLayer, type PresencePalette } from "./presence.ts";
import { createShell, type Shell, type WallInfo } from "./shell.ts";
import { createLuminaires, type Luminaires, type Walker } from "./luminaires.ts";
import {
createOfficeWalker,
type OfficeWalker,
type OfficeWalkerOptions,
} from "./officeWalker.ts";
import {
createRobotLayer,
type RobotLayer,
@@ -210,6 +214,8 @@ export interface OfficeSceneOptions {
* withhold, so a stranger gets them too.
*/
robots?: readonly RobotSpec[];
/** Optional local walk actor. Constructed inactive unless `walker.active` says otherwise. */
walker?: OfficeWalkerOptions;
/** Defaults to false — the lid comes off, because that is the whole view. */
showCeilings?: boolean;
/** Fade the walls you are looking through. Defaults to true. */
@@ -230,6 +236,8 @@ export interface OfficeScene extends StageScene {
* print the "no presence" badge and whether to offer a sign-in.
*/
depth: Depth;
/** Local walk-mode actor, or null when this scene was built as dollhouse-only. */
walker: OfficeWalker | null;
/** The pack's viewpoints, as the thing a legend prints and `flyTo` is keyed on. */
views: View[];
flyTo(viewId: string): void;
@@ -486,6 +494,8 @@ export function createOfficeScene(office: Office, options: OfficeSceneOptions):
options.robots && options.robots.length > 0
? createRobotLayer(plan, { materials, robots: options.robots })
: null;
const officeWalker = options.walker ? createOfficeWalker(plan, options.walker) : null;
if (officeWalker) scene.add(officeWalker.root);
if (robots) {
scene.add(robots.group);
/**
@@ -496,7 +506,12 @@ export function createOfficeScene(office: Office, options: OfficeSceneOptions):
* through a reference taken at setup. Calling it every frame would allocate
* nothing extra but would imply the array were a snapshot, which it is not.
*/
luminaires.setWalkers(robots.robots());
}
if (robots || officeWalker) {
luminaires.setWalkers([
...(robots?.robots() ?? NO_ROBOTS),
...(officeWalker ? [officeWalker.view] : []),
]);
}
// A public office has no presence layer, rather than an empty one. The
// difference is not cosmetic: an empty `PresenceLayer` is a `THREE.Group`
@@ -749,13 +764,18 @@ export function createOfficeScene(office: Office, options: OfficeSceneOptions):
controls: kit.controls,
plan,
depth,
walker: officeWalker,
views,
// A public office anchors nothing, because it has nobody to anchor. The
// empty map is this scene's own rather than a shared module-level one: an
// HTML overlay that writes into what it was handed should not be able to
// reach across into another office.
anchors: presence?.anchors ?? new Map<string, THREE.Vector3>(),
flyTo,
flyTo(viewId) {
officeWalker?.setActive(false);
kit.controls.enabled = true;
flyTo(viewId);
},
current: () => currentView,
onViewChange(fn) {
viewListeners.push(fn);
@@ -799,7 +819,11 @@ export function createOfficeScene(office: Office, options: OfficeSceneOptions):
onExit: () => kit.resetPick(),
tick(dt) {
if (disposed) return;
const walking = officeWalker?.active() ?? false;
kit.controls.enabled = !walking;
kit.tick(dt);
officeWalker?.tick(dt);
if (walking && officeWalker) kit.setPose(officeWalker.followPose());
updateOcclusion();
// Robots first: the lights above them should respond to where they are
// *now*, not to where they were last frame.
@@ -807,6 +831,7 @@ export function createOfficeScene(office: Office, options: OfficeSceneOptions):
luminaires.tick(dt);
},
dispose() {
officeWalker?.dispose();
robots?.dispose();
luminaires.dispose();
if (horizonPlane) {