feat: add playable actors and seamless journey state
This commit is contained in:
+56
-1
@@ -64,6 +64,8 @@ import type {
|
||||
ScenePalette,
|
||||
} from "./types.ts";
|
||||
import { World, type FieldProgress } from "./world.ts";
|
||||
import { createSceneActor, type SceneActorOptions } from "../actors/sceneActor.ts";
|
||||
import type { ActorActionSnapshot, ActorControllerSnapshot } from "../actors/controller.ts";
|
||||
|
||||
export interface SceneOptions {
|
||||
city: City;
|
||||
@@ -79,6 +81,10 @@ export interface SceneOptions {
|
||||
markers?: Marker[];
|
||||
/** Optional deterministic road traffic for a state/corridor-scale board. */
|
||||
roadTraffic?: RoadTrafficOptions;
|
||||
/** Optional possessed procedural actor for play mode; inactive by default. */
|
||||
actor?: SceneActorOptions;
|
||||
/** Geographic spawn anchor for that actor; defaults to the board centre. */
|
||||
actorAnchor?: { lat: number; lng: number };
|
||||
flights?: FlightSource;
|
||||
/**
|
||||
* Element sets to propagate, if this deployment has any.
|
||||
@@ -175,6 +181,10 @@ export interface SceneHandle {
|
||||
vehicleState(): Readonly<VehicleControllerState> | null;
|
||||
setVehicleCamera(mode: VehicleCameraMode): void;
|
||||
vehicleCamera(): VehicleCameraMode | null;
|
||||
setActorActions(actions: Partial<ActorActionSnapshot>): void;
|
||||
actorState(): Readonly<ActorControllerSnapshot> | null;
|
||||
setActorActive(active: boolean): void;
|
||||
actorActive(): boolean;
|
||||
setMarkers(markers: Marker[]): void;
|
||||
/** Take this city off the stage and release everything it built. */
|
||||
dispose(): void;
|
||||
@@ -254,6 +264,7 @@ export async function createScene(
|
||||
Math.hypot(eastX, southZ),
|
||||
);
|
||||
|
||||
const orbitMinDistance = Math.max(4, boardSpan * 0.02);
|
||||
const kit = createSceneKit({
|
||||
scene,
|
||||
dom: stage.renderer.domElement,
|
||||
@@ -272,7 +283,7 @@ export async function createScene(
|
||||
* the invariant: nothing should be cut off before it has fully faded out.
|
||||
*/
|
||||
far: boardSpan * 4,
|
||||
minDistance: Math.max(4, boardSpan * 0.02),
|
||||
minDistance: orbitMinDistance,
|
||||
/**
|
||||
* Far enough out to be **above the satellites**, which is what the extra
|
||||
* half-span buys.
|
||||
@@ -344,6 +355,22 @@ export async function createScene(
|
||||
? createRoadTrafficLayer(world, kit.camera, kit.controls, options.roadTraffic)
|
||||
: null;
|
||||
if (roadTraffic) scene.add(roadTraffic.group);
|
||||
const actorAnchor = options.actorAnchor ?? city.center;
|
||||
const [actorX, actorZ] = world.project(actorAnchor.lat, actorAnchor.lng);
|
||||
const sceneActor = options.actor
|
||||
? createSceneActor({
|
||||
...options.actor,
|
||||
sceneUnitsPerMetre: options.actor.sceneUnitsPerMetre ?? 1 / world.metresPerUnit,
|
||||
visualSceneUnitsPerMetre: options.actor.visualSceneUnitsPerMetre ??
|
||||
(city.id === "california" ? 0.025 : 1 / world.metresPerUnit),
|
||||
sceneOrigin: options.actor.sceneOrigin ?? {
|
||||
x: actorX,
|
||||
y: world.groundAt(actorAnchor.lat, actorAnchor.lng),
|
||||
z: actorZ,
|
||||
},
|
||||
})
|
||||
: null;
|
||||
if (sceneActor) scene.add(sceneActor.root);
|
||||
|
||||
let flightLayer: FlightLayer | null = null;
|
||||
let flightTimer = 0;
|
||||
@@ -408,6 +435,15 @@ export async function createScene(
|
||||
function flyTo(chapterId: string) {
|
||||
const ch = chapterById[chapterId];
|
||||
if (!ch) return;
|
||||
// Named viewpoints are observe/vehicle destinations. Possessing an actor
|
||||
// is an explicit UI action, so a chapter selection always hands the camera
|
||||
// back before it moves anywhere else.
|
||||
sceneActor?.setActive(false);
|
||||
kit.controls.minDistance = orbitMinDistance;
|
||||
if (kit.camera.near !== 0.1) {
|
||||
kit.camera.near = 0.1;
|
||||
kit.camera.updateProjectionMatrix();
|
||||
}
|
||||
const route = options.roadTraffic?.pack.routes.find((candidate) => candidate.id === chapterId);
|
||||
if (route && roadTraffic) {
|
||||
roadTraffic.setRoute(route.id);
|
||||
@@ -446,7 +482,11 @@ export async function createScene(
|
||||
// bug.
|
||||
onExit: () => kit.resetPick(),
|
||||
tick(dt) {
|
||||
const actorPlaying = sceneActor?.active() ?? false;
|
||||
kit.controls.enabled = !actorPlaying;
|
||||
kit.tick(dt);
|
||||
sceneActor?.tick(dt);
|
||||
if (actorPlaying && sceneActor) kit.setPose(sceneActor.followPose());
|
||||
roadTraffic?.tick(dt);
|
||||
clouds.tick(dt);
|
||||
if (options.flights && flightLayer) {
|
||||
@@ -538,10 +578,25 @@ export async function createScene(
|
||||
vehicleState: () => roadTraffic?.hero() ?? null,
|
||||
setVehicleCamera: (mode) => roadTraffic?.setCameraMode(mode),
|
||||
vehicleCamera: () => roadTraffic?.cameraMode() ?? null,
|
||||
setActorActions: (actions) => { sceneActor?.setActions(actions); },
|
||||
actorState: () => sceneActor?.state() ?? null,
|
||||
setActorActive(active) {
|
||||
sceneActor?.setActive(active);
|
||||
if (active) roadTraffic?.setFollowing(false);
|
||||
// State boards compress one real metre to a few hundredths of a scene
|
||||
// unit. Their possessed actor and chase camera are therefore closer than
|
||||
// the map camera's 0.1 near plane; lower it only for play mode so the
|
||||
// procedural rig is not clipped away, then restore the depth precision.
|
||||
kit.camera.near = active ? 0.001 : 0.1;
|
||||
kit.controls.minDistance = active ? 0.001 : orbitMinDistance;
|
||||
kit.camera.updateProjectionMatrix();
|
||||
},
|
||||
actorActive: () => sceneActor?.active() ?? false,
|
||||
setMarkers(markers) {
|
||||
markerLayer.setMarkers(markers);
|
||||
},
|
||||
dispose() {
|
||||
sceneActor?.dispose();
|
||||
/**
|
||||
* Off the stage, then released — and the stage itself is left running.
|
||||
*
|
||||
|
||||
Reference in New Issue
Block a user