feat: add playable flight and office screen sharing
This commit is contained in:
@@ -2,9 +2,10 @@
|
||||
* The office-facing half of walk mode: a numeric `WalkerController` wearing an
|
||||
* original actor rig and publishing a third-person camera pose.
|
||||
*
|
||||
* There is deliberately no identity, webcam, keyboard or network code here.
|
||||
* There is deliberately no identity, webcam capture, keyboard or network code here.
|
||||
* Callers choose humanoid or anonymous dog, translate their own input device to
|
||||
* a normalized planar action, and decide when walk mode is active.
|
||||
* a normalized planar action, and decide when walk mode is active. A caller may
|
||||
* attach an already-created face texture; ownership stays with that caller.
|
||||
*/
|
||||
|
||||
import * as THREE from "three";
|
||||
@@ -88,6 +89,12 @@ export interface OfficeWalker {
|
||||
setAction(action: WalkerAction): WalkerAction;
|
||||
tick(elapsedSeconds: number): OfficeWalkerState;
|
||||
reset(spawn?: WalkerSpawn): OfficeWalkerState;
|
||||
/** Rebuild only the procedural skin, retaining this root and all walker/camera state. */
|
||||
setAppearance(appearance: OfficeActorAppearance): OfficeWalkerState;
|
||||
/** Attach a caller-owned texture. False for an anonymous dog. */
|
||||
attachFaceTexture(texture: THREE.Texture): boolean;
|
||||
/** Detach the current face without disposing the caller's texture. */
|
||||
clearFaceTexture(): void;
|
||||
/** A defensive scene-space chase-camera pose for the current actor state. */
|
||||
followPose(): Pose;
|
||||
dispose(): void;
|
||||
@@ -98,8 +105,7 @@ type Actor =
|
||||
| { kind: "anonymous-dog"; rig: DogRig };
|
||||
|
||||
export function createOfficeWalker(plan: Plan, options: OfficeWalkerOptions): OfficeWalker {
|
||||
const appearance = options.actor ?? { kind: "humanoid" };
|
||||
const actor = buildActor(appearance);
|
||||
let actor = buildActor(options.actor ?? { kind: "humanoid" });
|
||||
const controller: WalkerController = createWalker(plan, options);
|
||||
const baseCamera = actor.kind === "humanoid" ? HUMANOID_CAMERA : DOG_CAMERA;
|
||||
const camera = {
|
||||
@@ -112,15 +118,22 @@ export function createOfficeWalker(plan: Plan, options: OfficeWalkerOptions): Of
|
||||
let enabled = options.active ?? false;
|
||||
let desired: WalkerAction = { x: 0, z: 0 };
|
||||
let gait = 0;
|
||||
let faceTexture: THREE.Texture | null = null;
|
||||
let disposed = false;
|
||||
const root = new THREE.Group();
|
||||
root.name = "office-walker-actor";
|
||||
root.userData.kind = "playable-actor";
|
||||
root.userData.forwardAxis = "-Z";
|
||||
root.userData.actorType = actor.rig.root.userData.actorType;
|
||||
root.add(actor.rig.root);
|
||||
const view = { position: new THREE.Vector3() };
|
||||
|
||||
function sync(state: WalkerState, elapsedSeconds = 0, travelled = 0): void {
|
||||
const level = plan.level(state.levelId);
|
||||
if (!level) throw new Error(`office walker lost level "${state.levelId}"`);
|
||||
actor.rig.root.position.set(state.position.x, level.floorY, state.position.z);
|
||||
view.position.copy(actor.rig.root.position);
|
||||
actor.rig.root.rotation.y = Math.atan2(-state.facing.x, -state.facing.z);
|
||||
root.position.set(state.position.x, level.floorY, state.position.z);
|
||||
view.position.copy(root.position);
|
||||
root.rotation.y = Math.atan2(-state.facing.x, -state.facing.z);
|
||||
|
||||
const speed = elapsedSeconds > 0 ? travelled / elapsedSeconds : 0;
|
||||
const targetGait = enabled && speed > 1e-5 ? Math.min(1, speed / (options.speed ?? 1.6)) : 0;
|
||||
@@ -146,10 +159,24 @@ export function createOfficeWalker(plan: Plan, options: OfficeWalkerOptions): Of
|
||||
};
|
||||
}
|
||||
|
||||
function replaceActor(appearance: OfficeActorAppearance): void {
|
||||
const previous = actor;
|
||||
actor = buildActor(appearance);
|
||||
root.add(actor.rig.root);
|
||||
if (actor.kind === "humanoid" && faceTexture) applyOfficeFaceTexture(actor.rig, faceTexture);
|
||||
if (previous.kind === "humanoid") applyOfficeFaceTexture(previous.rig, null);
|
||||
if (actor.kind !== "humanoid") faceTexture = null;
|
||||
previous.rig.root.removeFromParent();
|
||||
if (previous.kind === "humanoid") disposeHumanoid(previous.rig);
|
||||
else disposeDog(previous.rig);
|
||||
root.userData.actorType = actor.rig.root.userData.actorType;
|
||||
sync(controller.state());
|
||||
}
|
||||
|
||||
sync(controller.state());
|
||||
|
||||
return {
|
||||
root: actor.rig.root,
|
||||
root,
|
||||
view,
|
||||
state: snapshot,
|
||||
active: () => enabled,
|
||||
@@ -178,6 +205,24 @@ export function createOfficeWalker(plan: Plan, options: OfficeWalkerOptions): Of
|
||||
sync(state);
|
||||
return snapshot();
|
||||
},
|
||||
setAppearance(appearance) {
|
||||
if (disposed) return snapshot();
|
||||
replaceActor(appearance);
|
||||
return snapshot();
|
||||
},
|
||||
attachFaceTexture(texture) {
|
||||
if (disposed || actor.kind !== "humanoid") return false;
|
||||
if (!texture || texture.isTexture !== true) {
|
||||
throw new RangeError("office walker: face texture must be a Three.js Texture");
|
||||
}
|
||||
faceTexture = texture;
|
||||
applyOfficeFaceTexture(actor.rig, texture);
|
||||
return true;
|
||||
},
|
||||
clearFaceTexture() {
|
||||
faceTexture = null;
|
||||
if (!disposed && actor.kind === "humanoid") applyOfficeFaceTexture(actor.rig, null);
|
||||
},
|
||||
followPose() {
|
||||
const state = controller.state();
|
||||
const level = plan.level(state.levelId);
|
||||
@@ -198,7 +243,9 @@ export function createOfficeWalker(plan: Plan, options: OfficeWalkerOptions): Of
|
||||
dispose() {
|
||||
if (disposed) return;
|
||||
disposed = true;
|
||||
actor.rig.root.removeFromParent();
|
||||
root.removeFromParent();
|
||||
if (actor.kind === "humanoid") applyOfficeFaceTexture(actor.rig, null);
|
||||
faceTexture = null;
|
||||
if (actor.kind === "humanoid") disposeHumanoid(actor.rig);
|
||||
else disposeDog(actor.rig);
|
||||
},
|
||||
@@ -228,6 +275,18 @@ function buildActor(appearance: OfficeActorAppearance): Actor {
|
||||
};
|
||||
}
|
||||
|
||||
const DEFAULT_OFFICE_FACE_COLOR = 0x18242b;
|
||||
|
||||
function applyOfficeFaceTexture(rig: HumanoidRig, texture: THREE.Texture | null): void {
|
||||
const material = rig.face.material;
|
||||
if (!(material instanceof THREE.MeshBasicMaterial)) {
|
||||
throw new Error("office walker: humanoid face must use one MeshBasicMaterial");
|
||||
}
|
||||
material.map = texture;
|
||||
material.color.set(texture ? 0xffffff : DEFAULT_OFFICE_FACE_COLOR);
|
||||
material.needsUpdate = true;
|
||||
}
|
||||
|
||||
function positive(value: number, name: string): number {
|
||||
if (!(value > 0) || !Number.isFinite(value)) throw new RangeError(`${name} must be finite and positive`);
|
||||
return value;
|
||||
|
||||
Reference in New Issue
Block a user