feat: complete actor handoff and piloted aircraft presence
This commit is contained in:
+52
-19
@@ -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 ----------------------------------------------
|
||||
|
||||
Reference in New Issue
Block a user