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
+52 -19
View File
@@ -83,6 +83,7 @@ import type { ActorIdentity } from "./actors/controller.ts";
import { SRGBColorSpace, VideoTexture } from "three";
import {
CALIFORNIA_AIR_ROUTE,
createAircraftPoseSnapshot,
type AircraftActionSnapshot,
} from "./aircraft/index.ts";
import {
@@ -254,6 +255,8 @@ let cityId = "california";
*/
let wantedCity = "california";
let office: OfficeScene | null = null;
/** The pack used by `office`; kept separate from the currently selected door. */
let builtOfficeId: string | null = null;
let inside = false;
let markers: Marker[] = SAMPLE_MARKERS;
let realtimeClient: RealtimeClient | null = null;
@@ -265,6 +268,7 @@ let realtimePageActive = true;
/** Page-scoped wire identities; auth subject never enters the spatial protocol. */
const realtimeActorId = crypto.randomUUID();
const realtimeVehicleId = crypto.randomUUID();
const realtimeAircraftId = crypto.randomUUID();
/**
* The buildings you can walk into, as procedural glyphs on the city.
@@ -773,13 +777,7 @@ async function mountCity(id: string) {
weatherWatch = null;
poseEditor?.destroy();
poseEditor = null;
stopWatchingOccupancy();
disposeOfficeScreenUi();
officePlan?.dispose();
officePlan = null;
office?.dispose();
office = null;
officeAtmosphere = null;
disposeLoadedOffice();
inside = false;
minimap?.dispose();
minimap = null;
@@ -1185,6 +1183,12 @@ function syncJourneyVehicle(now: number): void {
*/
async function enterOffice() {
if (!city) return;
// A city can have more than one authored door. Leaving one office parks its
// scene for a cheap re-entry, but clicking a different door must not reopen
// that cached building under the new office id. Tear down every resource
// owned by the old room before loading the requested one; the parked city
// actor, journey identity and local profile deliberately live elsewhere.
if (office && builtOfficeId !== officeId) disposeLoadedOffice();
if (!office) {
const built = await loadOffice();
// The chunk arrived after the user had already left for the other city, or
@@ -1272,6 +1276,7 @@ async function enterOffice() {
: { onPlacePick: (place) => showDetail(place ? place.label : null) }),
...(createOfficePeers ? { realtimePeers: { create: createOfficePeers } } : {}),
});
builtOfficeId = officeId;
office.onViewChange(() => renderLegend());
officePlan = buildOfficePlan(createOfficeMinimap, office);
}
@@ -1410,6 +1415,18 @@ function leaveOffice() {
renderLegend();
}
/** Dispose everything whose coordinates or subscriptions belong to one office. */
function disposeLoadedOffice(): void {
stopWatchingOccupancy();
disposeOfficeScreenUi();
officePlan?.dispose();
officePlan = null;
office?.dispose();
office = null;
builtOfficeId = null;
officeAtmosphere = null;
}
/**
* Fetch Spaces.
*
@@ -1634,14 +1651,8 @@ async function switchOffice(id: string) {
const previous = officeId;
officeId = id;
// The roster belongs to the building you have left.
stopWatchingOccupancy();
disposeOfficeScreenUi();
officePlan?.dispose();
officePlan = null;
office?.dispose();
office = null;
officeAtmosphere = null;
// The roster, screen session, plan and scene all belong to the building left.
disposeLoadedOffice();
entering = true;
try {
@@ -2015,6 +2026,16 @@ function realtimePose(): EntityPoseSnapshot | null {
};
}
const aircraft = city?.aircraftActive() ? city.aircraftState() : null;
if (aircraft) {
return createAircraftPoseSnapshot(
{ aircraftId: realtimeAircraftId, pilotActorId: realtimeActorId },
aircraft,
0,
timestampMs,
);
}
const vehicle = journey.vehicle !== null ? city?.vehicleState() : null;
if (vehicle) {
const heading = vehicle.headingDeg * Math.PI / 180;
@@ -2080,7 +2101,11 @@ function realtimePose(): EntityPoseSnapshot | null {
function ownRealtimeEntity(snapshot: EntityPoseSnapshot): boolean {
if (snapshot.entity === "actor") return snapshot.actorId === realtimeClient?.state().actorId;
return snapshot.driverActorId === realtimeClient?.state().actorId || snapshot.vehicleId === realtimeVehicleId;
if (snapshot.entity === "vehicle") {
return snapshot.driverActorId === realtimeClient?.state().actorId || snapshot.vehicleId === realtimeVehicleId;
}
return snapshot.pilotActorId === realtimeClient?.state().actorId ||
snapshot.aircraftId === realtimeAircraftId;
}
function clearRealtimePeers(): void {
@@ -2107,7 +2132,11 @@ function applyRealtimeMessage(message: ServerRealtimeMessage): void {
}
if (message.type === "pose-delta") {
for (const id of message.removedEntityIds) {
if (id !== `actor:${realtimeClient?.state().actorId}` && id !== `vehicle:${realtimeVehicleId}`) {
if (
id !== `actor:${realtimeClient?.state().actorId}` &&
id !== `vehicle:${realtimeVehicleId}` &&
id !== `aircraft:${realtimeAircraftId}`
) {
target?.removeRemoteEntity(id);
}
}
@@ -2804,9 +2833,13 @@ canvas.addEventListener("click", () => {
const marker = hoveredMarker;
if (!marker) return;
const id = officeIdOf(marker);
if (id === null) return;
if (id === null || entering) return;
entering = true;
officeId = id;
void building(`Opening ${marker.label}`, () => enterOffice());
void building(`Opening ${marker.label}`, () => enterOffice()).finally(() => {
entering = false;
renderLegend();
});
});
// ---- Panels, plan and overlays ----------------------------------------------