feat: complete actor handoff and piloted aircraft presence
This commit is contained in:
@@ -96,7 +96,9 @@ function predictPose(pose: SpatialPose, velocity: PoseVelocity, seconds: number)
|
||||
}
|
||||
|
||||
function id(snapshot: EntityPoseSnapshot): string {
|
||||
return snapshot.entity === "actor" ? `actor:${snapshot.actorId}` : `vehicle:${snapshot.vehicleId}`;
|
||||
if (snapshot.entity === "actor") return `actor:${snapshot.actorId}`;
|
||||
if (snapshot.entity === "vehicle") return `vehicle:${snapshot.vehicleId}`;
|
||||
return `aircraft:${snapshot.aircraftId}`;
|
||||
}
|
||||
|
||||
export class PoseInterpolationBuffer {
|
||||
|
||||
@@ -114,13 +114,26 @@ export function isEntityPoseSnapshot(value: unknown): value is EntityPoseSnapsho
|
||||
(value.kind === "humanoid" || value.kind === "dog" || value.kind === "crow")
|
||||
);
|
||||
}
|
||||
if (value.entity === "vehicle") {
|
||||
return (
|
||||
string(value.vehicleId) &&
|
||||
value.kind === "model-x" &&
|
||||
(value.driverActorId === null || string(value.driverActorId)) &&
|
||||
finite(value.steering) && value.steering >= -1 && value.steering <= 1 &&
|
||||
finite(value.wheelRadians) && Math.abs(value.wheelRadians) <= 1_000_000
|
||||
);
|
||||
}
|
||||
return (
|
||||
value.entity === "vehicle" &&
|
||||
string(value.vehicleId) &&
|
||||
value.kind === "model-x" &&
|
||||
(value.driverActorId === null || string(value.driverActorId)) &&
|
||||
finite(value.steering) && value.steering >= -1 && value.steering <= 1 &&
|
||||
finite(value.wheelRadians) && Math.abs(value.wheelRadians) <= 1_000_000
|
||||
value.entity === "aircraft" &&
|
||||
string(value.aircraftId) &&
|
||||
value.kind === "electric-vtail" &&
|
||||
string(value.pilotActorId) &&
|
||||
finite(value.rollDeg) && value.rollDeg >= -90 && value.rollDeg <= 90 &&
|
||||
finite(value.throttle) && value.throttle >= 0 && value.throttle <= 1 &&
|
||||
finite(value.rollInput) && value.rollInput >= -1 && value.rollInput <= 1 &&
|
||||
finite(value.pitchInput) && value.pitchInput >= -1 && value.pitchInput <= 1 &&
|
||||
finite(value.yawInput) && value.yawInput >= -1 && value.yawInput <= 1 &&
|
||||
finite(value.fanRadians) && Math.abs(value.fanRadians) <= 1_000_000
|
||||
);
|
||||
}
|
||||
|
||||
@@ -208,7 +221,9 @@ export function validateServerRealtimeMessage(
|
||||
}
|
||||
|
||||
function entityId(snapshot: EntityPoseSnapshot): string {
|
||||
return snapshot.entity === "actor" ? `actor:${snapshot.actorId}` : `vehicle:${snapshot.vehicleId}`;
|
||||
if (snapshot.entity === "actor") return `actor:${snapshot.actorId}`;
|
||||
if (snapshot.entity === "vehicle") return `vehicle:${snapshot.vehicleId}`;
|
||||
return `aircraft:${snapshot.aircraftId}`;
|
||||
}
|
||||
|
||||
function localCellKey(pose: SpatialPose): string | null {
|
||||
|
||||
@@ -33,6 +33,13 @@ import {
|
||||
setModelXWheelRotation,
|
||||
type ModelXRig,
|
||||
} from "../assets/vehicles/index.ts";
|
||||
import {
|
||||
buildElectricAircraft,
|
||||
disposeElectricAircraft,
|
||||
setAircraftControlSurfaces,
|
||||
setAircraftFanRotation,
|
||||
type ElectricAircraftRig,
|
||||
} from "../aircraft/asset.ts";
|
||||
import { PoseInterpolationBuffer } from "./interpolation.ts";
|
||||
import { isEntityPoseSnapshot } from "./protocol.ts";
|
||||
import type {
|
||||
@@ -79,13 +86,15 @@ type PeerVisual =
|
||||
| { kind: "humanoid"; rig: HumanoidRig }
|
||||
| { kind: "dog"; rig: DogRig }
|
||||
| { kind: "crow"; rig: CrowRig }
|
||||
| { kind: "model-x"; rig: ModelXRig };
|
||||
| { kind: "model-x"; rig: ModelXRig }
|
||||
| { kind: "electric-vtail"; rig: ElectricAircraftRig };
|
||||
|
||||
interface Prototypes {
|
||||
humanoid: HumanoidRig;
|
||||
dog: DogRig;
|
||||
crow: CrowRig;
|
||||
vehicle: ModelXRig;
|
||||
aircraft: ElectricAircraftRig;
|
||||
}
|
||||
|
||||
interface Peer {
|
||||
@@ -101,7 +110,9 @@ interface Peer {
|
||||
}
|
||||
|
||||
export function scenePeerId(snapshot: EntityPoseSnapshot): string {
|
||||
return snapshot.entity === "actor" ? `actor:${snapshot.actorId}` : `vehicle:${snapshot.vehicleId}`;
|
||||
if (snapshot.entity === "actor") return `actor:${snapshot.actorId}`;
|
||||
if (snapshot.entity === "vehicle") return `vehicle:${snapshot.vehicleId}`;
|
||||
return `aircraft:${snapshot.aircraftId}`;
|
||||
}
|
||||
|
||||
function poseSpaceKey(pose: SpatialPose): string {
|
||||
@@ -130,10 +141,36 @@ function makePrototypes(): Prototypes {
|
||||
dog: buildDog(),
|
||||
crow: buildCrow(),
|
||||
vehicle,
|
||||
aircraft: buildElectricAircraft(),
|
||||
};
|
||||
}
|
||||
|
||||
function childGroup(root: THREE.Group, name: string): THREE.Group {
|
||||
const value = root.getObjectByName(name);
|
||||
if (!(value instanceof THREE.Group)) throw new Error(`aircraft prototype is missing ${name}`);
|
||||
return value;
|
||||
}
|
||||
|
||||
function cloneAircraft(prototype: ElectricAircraftRig): ElectricAircraftRig {
|
||||
const root = prototype.root.clone(true);
|
||||
return {
|
||||
root,
|
||||
leftAileron: childGroup(root, "electric-aircraft.aileron-left"),
|
||||
rightAileron: childGroup(root, "electric-aircraft.aileron-right"),
|
||||
leftVTail: childGroup(root, "electric-aircraft.v-tail-left"),
|
||||
rightVTail: childGroup(root, "electric-aircraft.v-tail-right"),
|
||||
fans: [
|
||||
childGroup(root, "electric-aircraft.fan-left"),
|
||||
childGroup(root, "electric-aircraft.fan-right"),
|
||||
],
|
||||
ownsMaterials: false,
|
||||
};
|
||||
}
|
||||
|
||||
function cloneVisual(snapshot: EntityPoseSnapshot, prototypes: Prototypes): PeerVisual {
|
||||
if (snapshot.entity === "aircraft") {
|
||||
return { kind: "electric-vtail", rig: cloneAircraft(prototypes.aircraft) };
|
||||
}
|
||||
if (snapshot.entity === "vehicle") {
|
||||
const rig = cloneModelX(prototypes.vehicle);
|
||||
rig.root.name = "generic-black-ev";
|
||||
@@ -157,7 +194,7 @@ function replaceVisual(peer: Peer, snapshot: EntityPoseSnapshot, prototypes: Pro
|
||||
}
|
||||
|
||||
function animateActor(
|
||||
visual: Exclude<PeerVisual, { kind: "model-x" }>,
|
||||
visual: Extract<PeerVisual, { kind: "humanoid" | "dog" | "crow" }>,
|
||||
sample: BufferedPoseSample,
|
||||
): void {
|
||||
const speed = Math.hypot(sample.velocity.xMps, sample.velocity.zMps);
|
||||
@@ -262,7 +299,9 @@ export function createScenePeers(options: ScenePeersOptions): ScenePeers {
|
||||
peer.root.rotation.set(
|
||||
THREE.MathUtils.degToRad(pose.pitchDeg),
|
||||
-THREE.MathUtils.degToRad(pose.headingDeg),
|
||||
0,
|
||||
peer.latest.entity === "aircraft"
|
||||
? -THREE.MathUtils.degToRad(peer.latest.rollDeg)
|
||||
: 0,
|
||||
);
|
||||
if (peer.visual.kind === "model-x") {
|
||||
const latest = peer.latest.entity === "vehicle" ? peer.latest : null;
|
||||
@@ -272,6 +311,16 @@ export function createScenePeers(options: ScenePeersOptions): ScenePeers {
|
||||
// wheel geometry rolls forward with negative local-X rotation.
|
||||
setModelXWheelRotation(peer.visual.rig, -latest.wheelRadians);
|
||||
}
|
||||
} else if (peer.visual.kind === "electric-vtail") {
|
||||
const latest = peer.latest.entity === "aircraft" ? peer.latest : null;
|
||||
if (latest) {
|
||||
setAircraftControlSurfaces(peer.visual.rig, {
|
||||
roll: latest.rollInput,
|
||||
pitch: latest.pitchInput,
|
||||
yaw: latest.yawInput,
|
||||
});
|
||||
setAircraftFanRotation(peer.visual.rig, latest.fanRadians);
|
||||
}
|
||||
} else {
|
||||
animateActor(peer.visual, sample);
|
||||
}
|
||||
@@ -353,6 +402,7 @@ export function createScenePeers(options: ScenePeersOptions): ScenePeers {
|
||||
disposeDog(prototypes.dog);
|
||||
disposeCrow(prototypes.crow);
|
||||
disposeModelX(prototypes.vehicle);
|
||||
disposeElectricAircraft(prototypes.aircraft);
|
||||
disposed = true;
|
||||
},
|
||||
};
|
||||
|
||||
+16
-1
@@ -2,6 +2,7 @@
|
||||
|
||||
export type RealtimeActorKind = "humanoid" | "dog" | "crow";
|
||||
export type RealtimeVehicleKind = "model-x";
|
||||
export type RealtimeAircraftKind = "electric-vtail";
|
||||
export type RealtimeMemberRole = "visitor" | "member" | "admin";
|
||||
export type RealtimeRevocationReason =
|
||||
| "signed-out"
|
||||
@@ -104,7 +105,21 @@ export interface VehiclePoseSnapshot extends EntityPoseSnapshotBase {
|
||||
wheelRadians: number;
|
||||
}
|
||||
|
||||
export type EntityPoseSnapshot = ActorPoseSnapshot | VehiclePoseSnapshot;
|
||||
/** Authoritative state needed to reproduce a piloted aircraft on another client. */
|
||||
export interface AircraftPoseSnapshot extends EntityPoseSnapshotBase {
|
||||
entity: "aircraft";
|
||||
aircraftId: string;
|
||||
kind: RealtimeAircraftKind;
|
||||
pilotActorId: string;
|
||||
rollDeg: number;
|
||||
throttle: number;
|
||||
rollInput: number;
|
||||
pitchInput: number;
|
||||
yawInput: number;
|
||||
fanRadians: number;
|
||||
}
|
||||
|
||||
export type EntityPoseSnapshot = ActorPoseSnapshot | VehiclePoseSnapshot | AircraftPoseSnapshot;
|
||||
|
||||
export interface JoinRequest {
|
||||
type: "join-request";
|
||||
|
||||
Reference in New Issue
Block a user