1
0

feat: add playable flight and office screen sharing

This commit is contained in:
2026-08-11 19:46:47 -07:00
parent c0c7fcf974
commit 16dc85a6f8
13 changed files with 1355 additions and 45 deletions
+48 -2
View File
@@ -70,6 +70,14 @@ import type {
ActorControllerSnapshot,
ActorIdentity,
} from "../actors/controller.ts";
import {
createSceneAircraft,
type SceneAircraftOptions,
} from "../aircraft/sceneAircraft.ts";
import type {
AircraftActionSnapshot,
AircraftControllerSnapshot,
} from "../aircraft/controller.ts";
export interface SceneOptions {
city: City;
@@ -89,6 +97,8 @@ export interface SceneOptions {
actor?: SceneActorOptions;
/** Geographic spawn anchor for that actor; defaults to the board centre. */
actorAnchor?: { lat: number; lng: number };
/** Optional possessed fixed-wing aircraft; the city supplies its projection and terrain. */
aircraft?: Omit<SceneAircraftOptions, "project" | "groundAt">;
flights?: FlightSource;
/**
* Element sets to propagate, if this deployment has any.
@@ -191,6 +201,10 @@ export interface SceneHandle {
setActorIdentity(identity: ActorIdentity): void;
setActorActive(active: boolean): void;
actorActive(): boolean;
setAircraftActions(actions: Partial<AircraftActionSnapshot>): void;
aircraftState(): Readonly<AircraftControllerSnapshot> | null;
setAircraftActive(active: boolean): void;
aircraftActive(): boolean;
setMarkers(markers: Marker[]): void;
/** Take this city off the stage and release everything it built. */
dispose(): void;
@@ -377,6 +391,16 @@ export async function createScene(
})
: null;
if (sceneActor) scene.add(sceneActor.root);
const sceneAircraft = options.aircraft
? createSceneAircraft({
...options.aircraft,
project: (lat, lng) => world.project(lat, lng),
groundAt: (lat, lng) => world.groundAt(lat, lng),
altitudeSceneUnitsPerMetre: options.aircraft.altitudeSceneUnitsPerMetre ??
1 / world.metresPerUnit,
})
: null;
if (sceneAircraft) scene.add(sceneAircraft.root);
let flightLayer: FlightLayer | null = null;
let flightTimer = 0;
@@ -445,6 +469,7 @@ export async function createScene(
// is an explicit UI action, so a chapter selection always hands the camera
// back before it moves anywhere else.
sceneActor?.setActive(false);
sceneAircraft?.setActive(false);
kit.controls.minDistance = orbitMinDistance;
if (kit.camera.near !== 0.1) {
kit.camera.near = 0.1;
@@ -489,10 +514,13 @@ export async function createScene(
onExit: () => kit.resetPick(),
tick(dt) {
const actorPlaying = sceneActor?.active() ?? false;
kit.controls.enabled = !actorPlaying;
const aircraftPlaying = sceneAircraft?.active() ?? false;
kit.controls.enabled = !actorPlaying && !aircraftPlaying;
kit.tick(dt);
sceneActor?.tick(dt);
if (actorPlaying && sceneActor) kit.setPose(sceneActor.followPose());
sceneAircraft?.tick(dt);
if (aircraftPlaying && sceneAircraft) kit.setPose(sceneAircraft.followPose());
roadTraffic?.tick(dt);
clouds.tick(dt);
if (options.flights && flightLayer) {
@@ -543,6 +571,7 @@ export async function createScene(
nightLights.dispose();
markerLayer.dispose();
roadTraffic?.dispose();
sceneAircraft?.dispose();
kit.dispose();
scene.traverse((obj) => {
const mesh = obj as THREE.Mesh;
@@ -589,7 +618,10 @@ export async function createScene(
setActorIdentity: (identity) => { sceneActor?.setIdentity(identity); },
setActorActive(active) {
sceneActor?.setActive(active);
if (active) roadTraffic?.setFollowing(false);
if (active) {
sceneAircraft?.setActive(false);
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
@@ -599,11 +631,25 @@ export async function createScene(
kit.camera.updateProjectionMatrix();
},
actorActive: () => sceneActor?.active() ?? false,
setAircraftActions: (actions) => { sceneAircraft?.setActions(actions); },
aircraftState: () => sceneAircraft?.state() ?? null,
setAircraftActive(active) {
sceneAircraft?.setActive(active);
if (active) {
sceneActor?.setActive(false);
roadTraffic?.setFollowing(false);
}
kit.camera.near = active ? 0.01 : 0.1;
kit.controls.minDistance = active ? 0.01 : orbitMinDistance;
kit.camera.updateProjectionMatrix();
},
aircraftActive: () => sceneAircraft?.active() ?? false,
setMarkers(markers) {
markerLayer.setMarkers(markers);
},
dispose() {
sceneActor?.dispose();
sceneAircraft?.dispose();
/**
* Off the stage, then released — and the stage itself is left running.
*