@scenography/plugin-fpc 0.2.0 → 0.3.1
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/dist/index.cjs +22 -4
- package/dist/index.cjs.map +1 -1
- package/dist/index.d.cts +3 -0
- package/dist/index.d.mts +3 -0
- package/dist/index.mjs +22 -4
- package/dist/index.mjs.map +1 -1
- package/package.json +4 -4
package/dist/index.cjs
CHANGED
|
@@ -160,10 +160,15 @@ const movementFromTouch = (state, phi, delta) => {
|
|
|
160
160
|
return movementFromAxis(state.moveForward, -state.moveStrafe, phi, delta);
|
|
161
161
|
};
|
|
162
162
|
/** Integrate the look joystick into the look angles, mutating `look` in place. */
|
|
163
|
-
const updateLookFromTouch = (state, look, delta
|
|
163
|
+
const updateLookFromTouch = (state, look, delta, invert = {
|
|
164
|
+
x: false,
|
|
165
|
+
y: false
|
|
166
|
+
}) => {
|
|
164
167
|
if (state.lookX === 0 && state.lookY === 0) return;
|
|
165
|
-
|
|
166
|
-
|
|
168
|
+
const sx = invert.x ? -1 : 1;
|
|
169
|
+
const sy = invert.y ? -1 : 1;
|
|
170
|
+
look.phi -= state.lookX * sx * LOOK_SPEED * delta;
|
|
171
|
+
look.theta = clamp(look.theta + state.lookY * sy * LOOK_SPEED * delta, -MAX_PITCH, MAX_PITCH);
|
|
167
172
|
};
|
|
168
173
|
//#endregion
|
|
169
174
|
//#region src/FreeRoamControls.ts
|
|
@@ -193,6 +198,10 @@ var FreeRoamControls = class {
|
|
|
193
198
|
rotation = new three.Quaternion();
|
|
194
199
|
translation = new three.Vector3(0, 170, 0);
|
|
195
200
|
enabled = true;
|
|
201
|
+
lookInvert = {
|
|
202
|
+
x: false,
|
|
203
|
+
y: false
|
|
204
|
+
};
|
|
196
205
|
boundary;
|
|
197
206
|
groundHeight;
|
|
198
207
|
start;
|
|
@@ -270,6 +279,13 @@ var FreeRoamControls = class {
|
|
|
270
279
|
setEnabled(enabled) {
|
|
271
280
|
this.enabled = enabled;
|
|
272
281
|
}
|
|
282
|
+
/** Invert the analog look per axis (the visitor's control preference). */
|
|
283
|
+
setLookInvert(x, y) {
|
|
284
|
+
this.lookInvert = {
|
|
285
|
+
x,
|
|
286
|
+
y
|
|
287
|
+
};
|
|
288
|
+
}
|
|
273
289
|
/** Current pose: world position plus yaw (`phi`) and pitch (`theta`) in radians. */
|
|
274
290
|
getPose() {
|
|
275
291
|
return {
|
|
@@ -293,7 +309,7 @@ var FreeRoamControls = class {
|
|
|
293
309
|
}
|
|
294
310
|
if (this.mouse.dragging) updateLookFromMouse(this.mouse, this.look);
|
|
295
311
|
this.look.phi += yawFromKeyboard(this.keyboard, delta);
|
|
296
|
-
updateLookFromTouch(this.input, this.look, delta);
|
|
312
|
+
updateLookFromTouch(this.input, this.look, delta, this.lookInvert);
|
|
297
313
|
this.rotation = quaternionFromLook(this.look);
|
|
298
314
|
const step = movementFromKeyboard(this.keyboard, this.look.phi, delta).add(movementFromTouch(this.input, this.look.phi, delta));
|
|
299
315
|
this.translation = resolveMove(this.translation, step, this.colliders, this.playerRadius, this.boundary);
|
|
@@ -343,6 +359,8 @@ const fpc = () => ({
|
|
|
343
359
|
calibrateEyeHeight(scene);
|
|
344
360
|
return {
|
|
345
361
|
update: (deltaMs) => {
|
|
362
|
+
const flags = ctx.controlFlags();
|
|
363
|
+
controls.setLookInvert(flags.invertLookX, flags.invertLookY);
|
|
346
364
|
controls.setColliders(capabilities.get(_scenography_engine.COLLIDERS) ?? []);
|
|
347
365
|
controls.update(deltaMs / 1e3);
|
|
348
366
|
},
|
package/dist/index.cjs.map
CHANGED
|
@@ -1 +1 @@
|
|
|
1
|
-
{"version":3,"file":"index.cjs","names":["Vector3","Quaternion","Vector3","Vector2","Quaternion","Vector3","Vector3","Quaternion","Vector3","TERRAIN","Vector3","ROAM_CONTROL","COLLIDERS"],"sources":["../src/collision.ts","../src/headBob.ts","../src/keyboard.ts","../src/mouse.ts","../src/touch.ts","../src/FreeRoamControls.ts","../src/plugin.ts"],"sourcesContent":["import { Vector3 } from \"three\";\nimport type { Vec2 } from \"@scenography/scene\";\nimport type { Collider } from \"@scenography/engine\";\n\n/** Whether `(x, z)` lies inside the XZ polygon (even-odd ray casting). */\nexport const insidePolygon = (x: number, z: number, polygon: Vec2[]): boolean => {\n let inside = false;\n for (let i = 0, j = polygon.length - 1; i < polygon.length; j = i++) {\n const a = polygon[i];\n const b = polygon[j];\n if (!a || !b) continue; // unreachable given the loop bounds; satisfies strict indexing\n if (a[1] > z !== b[1] > z && x < ((b[0] - a[0]) * (z - a[1])) / (b[1] - a[1]) + a[0]) {\n inside = !inside;\n }\n }\n return inside;\n};\n\n/** Whether a circle of `radius` centered at `(x, z)` overlaps any collider box. */\nconst hits = (x: number, z: number, radius: number, colliders: Collider[]): boolean => {\n const r2 = radius * radius;\n for (const c of colliders) {\n const px = x - c.cx;\n const pz = z - c.cz;\n // Project onto the box axes: along its length and along its thickness normal.\n const along = px * c.dx + pz * c.dz;\n const perp = px * -c.dz + pz * c.dx;\n // Distance from the point to the closest point on the box.\n const ddAlong = along - Math.max(-c.halfLength, Math.min(c.halfLength, along));\n const ddPerp = perp - Math.max(-c.halfThickness, Math.min(c.halfThickness, perp));\n if (ddAlong * ddAlong + ddPerp * ddPerp < r2) return true;\n }\n return false;\n};\n\n/**\n * Resolve a desired XZ step against wall colliders and an optional walkable-area\n * boundary, one axis at a time so the camera slides along an obstacle instead of\n * sticking when it can't move straight.\n */\nexport const resolveMove = (\n current: Vector3,\n step: Vector3,\n colliders: Collider[],\n radius: number,\n boundary?: Vec2[],\n): Vector3 => {\n let { x, z } = current;\n const free = (nx: number, nz: number): boolean =>\n !hits(nx, nz, radius, colliders) && (!boundary || insidePolygon(nx, nz, boundary));\n if (step.x !== 0 && free(x + step.x, z)) x += step.x;\n if (step.z !== 0 && free(x, z + step.z)) z += step.z;\n return new Vector3(x, current.y, z);\n};\n","/** State for the subtle vertical bob applied while walking. */\nexport interface HeadBobState {\n active: boolean;\n timer: number;\n}\n\nexport const createHeadBobState = (): HeadBobState => ({ active: false, timer: 0 });\n\n/**\n * Advance the head-bob timer by `elapsed` seconds, snapping to half-wavelength\n * steps so the bob always settles back to neutral when movement stops.\n */\nexport const updateHeadBob = (state: HeadBobState, elapsed: number): void => {\n if (!state.active) return;\n\n const wavelength = Math.PI;\n const nextStep = 1 + Math.floor(((state.timer + 1e-6) * 10) / wavelength);\n const nextStepTime = (nextStep * wavelength) / 10;\n state.timer = Math.min(state.timer + elapsed, nextStepTime);\n\n if (state.timer === nextStepTime) state.active = false;\n};\n\n/** Vertical offset to add to the camera, in world units. */\nexport const headBobOffset = (state: HeadBobState): number => Math.sin(state.timer * 10) * 1.5;\n","import { Quaternion, Vector3 } from \"three\";\n\n/** Set of currently-pressed key codes. */\nexport type KeyboardState = Record<string, boolean>;\n\nexport const createKeyboardState = (): KeyboardState => ({});\n\n/** Codes the controls consume; preventDefault'd so the arrows don't scroll. */\nconst MOVEMENT_KEYS = new Set([\n \"KeyW\",\n \"KeyA\",\n \"KeyS\",\n \"KeyD\",\n \"ArrowUp\",\n \"ArrowDown\",\n \"ArrowLeft\",\n \"ArrowRight\",\n]);\n\nexport const onKeyDown = (state: KeyboardState, event: KeyboardEvent): void => {\n state[event.code] = true;\n if (MOVEMENT_KEYS.has(event.code)) event.preventDefault();\n};\n\nexport const onKeyUp = (state: KeyboardState, event: KeyboardEvent): void => {\n state[event.code] = false;\n};\n\n/** Yaw turn rate for the Q/E keys, in radians per second. */\nconst KEY_TURN_SPEED = 1.8;\n\n/**\n * Turn a forward/strafe pair (each in `-1..1`) into a per-frame translation\n * oriented by the current yaw (`phi`). Shared by keyboard and touch input.\n */\nexport const movementFromAxis = (\n forwardVelocity: number,\n strafeVelocity: number,\n phi: number,\n delta: number,\n boosted = false,\n): Vector3 => {\n const factor = boosted ? 60 : 20;\n const yaw = new Quaternion().setFromAxisAngle(new Vector3(0, 1, 0), phi);\n\n const forward = new Vector3(0, 0, -10)\n .applyQuaternion(yaw)\n .multiplyScalar(forwardVelocity * delta * factor);\n const left = new Vector3(-10, 0, 0)\n .applyQuaternion(yaw)\n .multiplyScalar(strafeVelocity * delta * factor);\n\n return forward.add(left);\n};\n\n/**\n * Compute the per-frame translation from WASD or arrow-key input, oriented by\n * the current yaw (`phi`). Holding Shift acts as a speed booster.\n */\nexport const movementFromKeyboard = (state: KeyboardState, phi: number, delta: number): Vector3 => {\n const has = (...codes: string[]): number => (codes.some((c) => state[c]) ? 1 : 0);\n const forwardVelocity = has(\"KeyW\", \"ArrowUp\") - has(\"KeyS\", \"ArrowDown\");\n const strafeVelocity = has(\"KeyA\", \"ArrowLeft\") - has(\"KeyD\", \"ArrowRight\");\n const boosted = Boolean(state[\"ShiftLeft\"] || state[\"ShiftRight\"]);\n\n return movementFromAxis(forwardVelocity, strafeVelocity, phi, delta, boosted);\n};\n\n/**\n * Per-frame yaw delta (radians) from the Q/E turn keys. Q turns left\n * (increasing yaw), E turns right.\n */\nexport const yawFromKeyboard = (state: KeyboardState, delta: number): number => {\n const turn = (state[\"KeyQ\"] ? 1 : 0) - (state[\"KeyE\"] ? 1 : 0);\n return turn * KEY_TURN_SPEED * delta;\n};\n","import { Quaternion, Vector2, Vector3 } from \"three\";\n\n/** Mutable pointer state accumulated between frames. */\nexport interface MouseState {\n /** Whether the primary button is held (drag-to-look). */\n dragging: boolean;\n pos: Vector2;\n prevPos: Vector2;\n delta: Vector2;\n}\n\n/** Look angles driven by the pointer. */\nexport interface LookState {\n phi: number;\n theta: number;\n}\n\nconst PHI_SPEED = 8;\nconst THETA_SPEED = 5;\n\nexport const createMouseState = (): MouseState => ({\n dragging: false,\n pos: new Vector2(),\n prevPos: new Vector2(),\n delta: new Vector2(),\n});\n\nexport const clamp = (x: number, a: number, b: number): number => Math.min(Math.max(x, a), b);\n\n/** Maximum up/down look angle (radians) — keeps the camera from flipping over. */\nexport const MAX_PITCH = Math.PI / 3;\n\n/** Build the look quaternion from the current yaw/pitch angles. */\nexport const quaternionFromLook = (look: LookState): Quaternion => {\n const qy = new Quaternion().setFromAxisAngle(new Vector3(0, 1, 0), look.phi);\n const qx = new Quaternion().setFromAxisAngle(new Vector3(1, 0, 0), look.theta);\n return new Quaternion().multiply(qy).multiply(qx);\n};\n\nexport const onMouseDown = (state: MouseState, event: MouseEvent): void => {\n state.dragging = event.button === 0;\n};\n\nexport const onMouseUp = (state: MouseState): void => {\n state.dragging = false;\n};\n\nexport const onMouseMove = (state: MouseState, event: MouseEvent): void => {\n state.pos.set(event.pageX - window.innerWidth / 2, event.pageY - window.innerHeight / 2);\n state.delta.subVectors(state.pos, state.prevPos);\n};\n\nexport const commitMouseFrame = (state: MouseState): void => {\n state.prevPos.copy(state.pos);\n state.delta.set(0, 0);\n};\n\n/**\n * Integrate the pointer delta into the look angles. Mutates `look` in place.\n * Yaw is unbounded; pitch is clamped to avoid flipping over.\n */\nexport const updateLookFromMouse = (mouse: MouseState, look: LookState): void => {\n const xh = mouse.delta.x / window.innerWidth;\n const yh = mouse.delta.y / window.innerHeight;\n\n look.phi += -xh * PHI_SPEED;\n look.theta = clamp(look.theta + -yh * THETA_SPEED, -MAX_PITCH, MAX_PITCH);\n};\n","import { Vector3 } from \"three\";\n\nimport { movementFromAxis } from \"./keyboard\";\nimport { clamp, MAX_PITCH, type LookState } from \"./mouse\";\n\n/**\n * The shape of the shared analog input fpc reads (the kernel's `InputBus`): the\n * move/look axes a joystick feeds. Structural, so any producer with these fields\n * works. fpc interprets them as walking + turning; another camera might not.\n */\nexport interface AnalogInput {\n /** Forward (`+1`) / backward (`-1`) movement. */\n moveForward: number;\n /** Strafe right (`+1`) / left (`-1`). */\n moveStrafe: number;\n /** Look right (`+1`) / left (`-1`). */\n lookX: number;\n /** Look up (`+1`) / down (`-1`). */\n lookY: number;\n}\n\n/** Look turn rate driven by the joystick, in radians per second at full deflection. */\nconst LOOK_SPEED = 2.2;\n\n/** Per-frame translation from the analog move axes, oriented by yaw (`phi`). */\nexport const movementFromTouch = (state: AnalogInput, phi: number, delta: number): Vector3 => {\n if (state.moveForward === 0 && state.moveStrafe === 0) return new Vector3();\n // Strafe sign matches the keyboard helper (left strafe is +1 there).\n return movementFromAxis(state.moveForward, -state.moveStrafe, phi, delta);\n};\n\n/** Integrate the look joystick into the look angles, mutating `look` in place. */\nexport const updateLookFromTouch = (state: AnalogInput, look: LookState, delta: number): void => {\n if (state.lookX === 0 && state.lookY === 0) return;\n look.phi -= state.lookX * LOOK_SPEED * delta;\n look.theta = clamp(look.theta + state.lookY * LOOK_SPEED * delta, -MAX_PITCH, MAX_PITCH);\n};\n","import { Quaternion, Vector3, type PerspectiveCamera } from \"three\";\nimport type { Vec2 } from \"@scenography/scene\";\nimport type { Collider } from \"@scenography/engine\";\n\nimport { resolveMove } from \"./collision\";\nimport { createHeadBobState, headBobOffset, updateHeadBob } from \"./headBob\";\nimport {\n createKeyboardState,\n movementFromKeyboard,\n onKeyDown,\n onKeyUp,\n yawFromKeyboard,\n} from \"./keyboard\";\nimport {\n commitMouseFrame,\n createMouseState,\n onMouseDown,\n onMouseMove,\n onMouseUp,\n quaternionFromLook,\n updateLookFromMouse,\n type LookState,\n} from \"./mouse\";\nimport { movementFromTouch, updateLookFromTouch, type AnalogInput } from \"./touch\";\n\nexport interface FreeRoamOptions {\n /** Wall collision boxes the camera can't pass through. */\n colliders?: Collider[];\n /** Camera collision radius in world units. Defaults to `25`. */\n playerRadius?: number;\n /** Starting position. */\n position?: Vector3;\n /** Starting yaw in radians. */\n yaw?: number;\n /** Walkable-area polygon (XZ ring); the camera can't step outside it. */\n boundary?: Vec2[];\n /** Ground height at `(x, z)`; when set the camera follows the terrain. */\n groundHeight?: (x: number, z: number) => number;\n /** Eye height above the ground. Defaults to the start Y over the start ground. */\n eyeHeight?: number;\n}\n\n/**\n * First-person, free-roaming camera controls: drag-to-look with the mouse, WASD\n * to walk, a subtle head-bob while moving, wall collision and terrain following.\n * The shared analog input (`AnalogInput`, the kernel's InputBus) is folded in on\n * top of keyboard/mouse, so on-screen joysticks drive it too. It keeps the\n * camera upright (no roll) and clamps pitch — the first-person contract.\n *\n * Framework-agnostic: construct it with a camera, the canvas and the input,\n * call `attach()`, then `update(delta)` once per frame.\n */\nexport class FreeRoamControls {\n private colliders: Collider[];\n private readonly playerRadius: number;\n\n private readonly camera: PerspectiveCamera;\n private readonly domElement: HTMLElement;\n private readonly input: AnalogInput;\n private readonly mouse = createMouseState();\n private readonly keyboard = createKeyboardState();\n private readonly headBob = createHeadBobState();\n private readonly look: LookState = { phi: 0, theta: 0 };\n private rotation = new Quaternion();\n private translation = new Vector3(0, 170, 0);\n private enabled = true;\n private boundary: Vec2[] | undefined;\n private groundHeight: ((x: number, z: number) => number) | null;\n private readonly start: Vector3;\n private eyeHeight: number;\n\n private readonly handlers: Array<[EventTarget, string, EventListener]> = [];\n\n constructor(\n camera: PerspectiveCamera,\n domElement: HTMLElement,\n input: AnalogInput,\n options: FreeRoamOptions = {},\n ) {\n this.camera = camera;\n this.domElement = domElement;\n this.input = input;\n this.colliders = options.colliders ?? [];\n this.playerRadius = options.playerRadius ?? 25;\n if (options.position) this.translation.copy(options.position);\n if (options.yaw !== undefined) this.look.phi = options.yaw;\n this.rotation.setFromAxisAngle(new Vector3(0, 1, 0), this.look.phi);\n this.boundary = options.boundary;\n this.groundHeight = options.groundHeight ?? null;\n this.start = this.translation.clone();\n // Eye height = start Y above the ground under the start position.\n const groundAtStart = this.groundHeight?.(this.start.x, this.start.z) ?? 0;\n this.eyeHeight = options.eyeHeight ?? this.start.y - groundAtStart;\n }\n\n /** Current eye height above the ground (world units). */\n getEyeHeight(): number {\n return this.eyeHeight;\n }\n\n /** Fix the eye height above the ground (world units). */\n setEyeHeight(eyeHeight: number): void {\n this.eyeHeight = eyeHeight;\n }\n\n /**\n * Re-derive the default eye height (start Y over the ground under the start\n * position). Call once asynchronous ground data (a floor heightmap) has\n * loaded: the value captured at construction was measured against the\n * not-yet-displaced floor.\n */\n recalibrateEyeHeight(): void {\n const ground = this.groundHeight?.(this.start.x, this.start.z) ?? 0;\n this.eyeHeight = this.start.y - ground;\n }\n\n attach(): void {\n const bind = (target: EventTarget, type: string, listener: EventListener): void => {\n target.addEventListener(type, listener);\n this.handlers.push([target, type, listener]);\n };\n\n bind(this.domElement, \"mousedown\", (e) => onMouseDown(this.mouse, e as MouseEvent));\n bind(window, \"mousemove\", (e) => onMouseMove(this.mouse, e as MouseEvent));\n bind(window, \"mouseup\", () => onMouseUp(this.mouse));\n bind(window, \"keydown\", (e) => onKeyDown(this.keyboard, e as KeyboardEvent));\n bind(window, \"keyup\", (e) => onKeyUp(this.keyboard, e as KeyboardEvent));\n }\n\n detach(): void {\n for (const [target, type, listener] of this.handlers)\n target.removeEventListener(type, listener);\n this.handlers.length = 0;\n }\n\n /** Replace the wall collision set (e.g. after the scene's walls change). */\n setColliders(colliders: Collider[]): void {\n this.colliders = colliders;\n }\n\n /** Replace the walkable-area boundary polygon (or clear it with `undefined`). */\n setBoundary(boundary: Vec2[] | undefined): void {\n this.boundary = boundary;\n }\n\n /** Replace the ground-height function (or clear it to stop following terrain). */\n setGroundHeight(groundHeight: ((x: number, z: number) => number) | null): void {\n this.groundHeight = groundHeight;\n }\n\n /**\n * Enable/disable user input. While disabled, `update()` leaves the camera\n * untouched so another driver (e.g. a camera tween) can own it; re-enable after\n * syncing the pose with `setPose` so free-roam continues from there.\n */\n setEnabled(enabled: boolean): void {\n this.enabled = enabled;\n }\n\n /** Current pose: world position plus yaw (`phi`) and pitch (`theta`) in radians. */\n getPose(): { position: Vector3; phi: number; theta: number } {\n return { position: this.translation.clone(), phi: this.look.phi, theta: this.look.theta };\n }\n\n /** Force the pose (e.g. at the end of a camera tween) so input resumes from it. */\n setPose(position: Vector3, phi: number, theta: number): void {\n this.translation.copy(position);\n this.look.phi = phi;\n this.look.theta = theta;\n this.rotation = quaternionFromLook(this.look);\n }\n\n /** Advance the camera by `delta` seconds of input. */\n update(delta: number): void {\n // Disabled: another driver owns the camera this frame. Drop any accumulated\n // pointer delta so look doesn't jump when control is handed back.\n if (!this.enabled) {\n commitMouseFrame(this.mouse);\n return;\n }\n // Accumulate look from every source into the angles, then derive rotation.\n if (this.mouse.dragging) updateLookFromMouse(this.mouse, this.look);\n this.look.phi += yawFromKeyboard(this.keyboard, delta);\n updateLookFromTouch(this.input, this.look, delta);\n this.rotation = quaternionFromLook(this.look);\n\n const step = movementFromKeyboard(this.keyboard, this.look.phi, delta).add(\n movementFromTouch(this.input, this.look.phi, delta),\n );\n this.translation = resolveMove(\n this.translation,\n step,\n this.colliders,\n this.playerRadius,\n this.boundary,\n );\n // Follow the terrain: keep the eye a fixed height above the ground below.\n if (this.groundHeight) {\n this.translation.y = this.groundHeight(this.translation.x, this.translation.z) + this.eyeHeight;\n }\n if (step.x !== 0 || step.z !== 0) this.headBob.active = true;\n updateHeadBob(this.headBob, delta);\n\n this.camera.quaternion.copy(this.rotation);\n this.camera.position.copy(this.translation);\n this.camera.position.y += headBobOffset(this.headBob);\n\n const target = new Vector3(0, 0, -1)\n .applyQuaternion(this.rotation)\n .multiplyScalar(100)\n .add(this.translation);\n this.camera.lookAt(target);\n\n commitMouseFrame(this.mouse);\n }\n}\n","import {\n COLLIDERS,\n ROAM_CONTROL,\n TERRAIN,\n type Collider,\n type RoamControl,\n type SystemPlugin,\n type TerrainCapability,\n} from \"@scenography/engine\";\nimport type { Scene } from \"@scenography/scene\";\nimport { Vector3 } from \"three\";\n\nimport { FreeRoamControls } from \"./FreeRoamControls\";\n\n/**\n * First-person camera — a *system* plugin. It drives the kernel camera from\n * WASD/drag plus the shared analog input (joysticks), keeps it at `eyeHeight`\n * over the terrain, and blocks it against wall colliders. Consumes the `terrain`\n * and `colliders` capabilities (live, so it tracks async heightmaps and hot wall\n * edits) and publishes `roamControl` so a framing/tour system can suspend it.\n */\nexport const fpc = (): SystemPlugin => ({\n name: \"fpc\",\n setup: (ctx) => {\n const { camera, canvas, input, capabilities, scene } = ctx;\n const terrainHeight = (x: number, z: number): number =>\n capabilities.get<TerrainCapability>(TERRAIN)?.heightAt(x, z) ?? 0;\n // Follow terrain only when some plugin provides it; otherwise keep a fixed Y.\n const groundHeight = (): ((x: number, z: number) => number) | undefined =>\n capabilities.get<TerrainCapability>(TERRAIN) ? terrainHeight : undefined;\n\n const start = scene.camera.start;\n const controls = new FreeRoamControls(camera, canvas, input, {\n position: new Vector3(start.position[0], start.position[1], start.position[2]),\n yaw: start.yaw,\n boundary: scene.boundary,\n eyeHeight: scene.camera.eyeHeight,\n groundHeight: groundHeight(),\n });\n controls.attach();\n capabilities.provide(ROAM_CONTROL, controls satisfies RoamControl);\n\n // Eye height: an explicit `camera.eyeHeight` wins. Otherwise it was derived\n // from the start Y over the ground — but a floor heightmap loads async, so\n // recalibrate once the terrain is real, or the visitor walks in the air.\n const calibrateEyeHeight = (s: Scene): void => {\n if (s.camera.eyeHeight !== undefined) controls.setEyeHeight(s.camera.eyeHeight);\n else {\n const terrain = capabilities.get<TerrainCapability>(TERRAIN);\n if (terrain) void terrain.ready.then(() => controls.recalibrateEyeHeight());\n }\n };\n calibrateEyeHeight(scene);\n\n return {\n update: (deltaMs) => {\n controls.setColliders(capabilities.get<Collider[]>(COLLIDERS) ?? []);\n controls.update(deltaMs / 1000);\n },\n setScene: (s) => {\n controls.setBoundary(s.boundary);\n controls.setGroundHeight(groundHeight() ?? null);\n calibrateEyeHeight(s);\n },\n dispose: () => {\n controls.detach();\n capabilities.revoke(ROAM_CONTROL);\n },\n };\n },\n});\n"],"mappings":"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"}
|
|
1
|
+
{"version":3,"file":"index.cjs","names":["Vector3","Quaternion","Vector3","Vector2","Quaternion","Vector3","Vector3","Quaternion","Vector3","TERRAIN","Vector3","ROAM_CONTROL","COLLIDERS"],"sources":["../src/collision.ts","../src/headBob.ts","../src/keyboard.ts","../src/mouse.ts","../src/touch.ts","../src/FreeRoamControls.ts","../src/plugin.ts"],"sourcesContent":["import { Vector3 } from \"three\";\nimport type { Vec2 } from \"@scenography/scene\";\nimport type { Collider } from \"@scenography/engine\";\n\n/** Whether `(x, z)` lies inside the XZ polygon (even-odd ray casting). */\nexport const insidePolygon = (x: number, z: number, polygon: Vec2[]): boolean => {\n let inside = false;\n for (let i = 0, j = polygon.length - 1; i < polygon.length; j = i++) {\n const a = polygon[i];\n const b = polygon[j];\n if (!a || !b) continue; // unreachable given the loop bounds; satisfies strict indexing\n if (a[1] > z !== b[1] > z && x < ((b[0] - a[0]) * (z - a[1])) / (b[1] - a[1]) + a[0]) {\n inside = !inside;\n }\n }\n return inside;\n};\n\n/** Whether a circle of `radius` centered at `(x, z)` overlaps any collider box. */\nconst hits = (x: number, z: number, radius: number, colliders: Collider[]): boolean => {\n const r2 = radius * radius;\n for (const c of colliders) {\n const px = x - c.cx;\n const pz = z - c.cz;\n // Project onto the box axes: along its length and along its thickness normal.\n const along = px * c.dx + pz * c.dz;\n const perp = px * -c.dz + pz * c.dx;\n // Distance from the point to the closest point on the box.\n const ddAlong = along - Math.max(-c.halfLength, Math.min(c.halfLength, along));\n const ddPerp = perp - Math.max(-c.halfThickness, Math.min(c.halfThickness, perp));\n if (ddAlong * ddAlong + ddPerp * ddPerp < r2) return true;\n }\n return false;\n};\n\n/**\n * Resolve a desired XZ step against wall colliders and an optional walkable-area\n * boundary, one axis at a time so the camera slides along an obstacle instead of\n * sticking when it can't move straight.\n */\nexport const resolveMove = (\n current: Vector3,\n step: Vector3,\n colliders: Collider[],\n radius: number,\n boundary?: Vec2[],\n): Vector3 => {\n let { x, z } = current;\n const free = (nx: number, nz: number): boolean =>\n !hits(nx, nz, radius, colliders) && (!boundary || insidePolygon(nx, nz, boundary));\n if (step.x !== 0 && free(x + step.x, z)) x += step.x;\n if (step.z !== 0 && free(x, z + step.z)) z += step.z;\n return new Vector3(x, current.y, z);\n};\n","/** State for the subtle vertical bob applied while walking. */\nexport interface HeadBobState {\n active: boolean;\n timer: number;\n}\n\nexport const createHeadBobState = (): HeadBobState => ({ active: false, timer: 0 });\n\n/**\n * Advance the head-bob timer by `elapsed` seconds, snapping to half-wavelength\n * steps so the bob always settles back to neutral when movement stops.\n */\nexport const updateHeadBob = (state: HeadBobState, elapsed: number): void => {\n if (!state.active) return;\n\n const wavelength = Math.PI;\n const nextStep = 1 + Math.floor(((state.timer + 1e-6) * 10) / wavelength);\n const nextStepTime = (nextStep * wavelength) / 10;\n state.timer = Math.min(state.timer + elapsed, nextStepTime);\n\n if (state.timer === nextStepTime) state.active = false;\n};\n\n/** Vertical offset to add to the camera, in world units. */\nexport const headBobOffset = (state: HeadBobState): number => Math.sin(state.timer * 10) * 1.5;\n","import { Quaternion, Vector3 } from \"three\";\n\n/** Set of currently-pressed key codes. */\nexport type KeyboardState = Record<string, boolean>;\n\nexport const createKeyboardState = (): KeyboardState => ({});\n\n/** Codes the controls consume; preventDefault'd so the arrows don't scroll. */\nconst MOVEMENT_KEYS = new Set([\n \"KeyW\",\n \"KeyA\",\n \"KeyS\",\n \"KeyD\",\n \"ArrowUp\",\n \"ArrowDown\",\n \"ArrowLeft\",\n \"ArrowRight\",\n]);\n\nexport const onKeyDown = (state: KeyboardState, event: KeyboardEvent): void => {\n state[event.code] = true;\n if (MOVEMENT_KEYS.has(event.code)) event.preventDefault();\n};\n\nexport const onKeyUp = (state: KeyboardState, event: KeyboardEvent): void => {\n state[event.code] = false;\n};\n\n/** Yaw turn rate for the Q/E keys, in radians per second. */\nconst KEY_TURN_SPEED = 1.8;\n\n/**\n * Turn a forward/strafe pair (each in `-1..1`) into a per-frame translation\n * oriented by the current yaw (`phi`). Shared by keyboard and touch input.\n */\nexport const movementFromAxis = (\n forwardVelocity: number,\n strafeVelocity: number,\n phi: number,\n delta: number,\n boosted = false,\n): Vector3 => {\n const factor = boosted ? 60 : 20;\n const yaw = new Quaternion().setFromAxisAngle(new Vector3(0, 1, 0), phi);\n\n const forward = new Vector3(0, 0, -10)\n .applyQuaternion(yaw)\n .multiplyScalar(forwardVelocity * delta * factor);\n const left = new Vector3(-10, 0, 0)\n .applyQuaternion(yaw)\n .multiplyScalar(strafeVelocity * delta * factor);\n\n return forward.add(left);\n};\n\n/**\n * Compute the per-frame translation from WASD or arrow-key input, oriented by\n * the current yaw (`phi`). Holding Shift acts as a speed booster.\n */\nexport const movementFromKeyboard = (state: KeyboardState, phi: number, delta: number): Vector3 => {\n const has = (...codes: string[]): number => (codes.some((c) => state[c]) ? 1 : 0);\n const forwardVelocity = has(\"KeyW\", \"ArrowUp\") - has(\"KeyS\", \"ArrowDown\");\n const strafeVelocity = has(\"KeyA\", \"ArrowLeft\") - has(\"KeyD\", \"ArrowRight\");\n const boosted = Boolean(state[\"ShiftLeft\"] || state[\"ShiftRight\"]);\n\n return movementFromAxis(forwardVelocity, strafeVelocity, phi, delta, boosted);\n};\n\n/**\n * Per-frame yaw delta (radians) from the Q/E turn keys. Q turns left\n * (increasing yaw), E turns right.\n */\nexport const yawFromKeyboard = (state: KeyboardState, delta: number): number => {\n const turn = (state[\"KeyQ\"] ? 1 : 0) - (state[\"KeyE\"] ? 1 : 0);\n return turn * KEY_TURN_SPEED * delta;\n};\n","import { Quaternion, Vector2, Vector3 } from \"three\";\n\n/** Mutable pointer state accumulated between frames. */\nexport interface MouseState {\n /** Whether the primary button is held (drag-to-look). */\n dragging: boolean;\n pos: Vector2;\n prevPos: Vector2;\n delta: Vector2;\n}\n\n/** Look angles driven by the pointer. */\nexport interface LookState {\n phi: number;\n theta: number;\n}\n\nconst PHI_SPEED = 8;\nconst THETA_SPEED = 5;\n\nexport const createMouseState = (): MouseState => ({\n dragging: false,\n pos: new Vector2(),\n prevPos: new Vector2(),\n delta: new Vector2(),\n});\n\nexport const clamp = (x: number, a: number, b: number): number => Math.min(Math.max(x, a), b);\n\n/** Maximum up/down look angle (radians) — keeps the camera from flipping over. */\nexport const MAX_PITCH = Math.PI / 3;\n\n/** Build the look quaternion from the current yaw/pitch angles. */\nexport const quaternionFromLook = (look: LookState): Quaternion => {\n const qy = new Quaternion().setFromAxisAngle(new Vector3(0, 1, 0), look.phi);\n const qx = new Quaternion().setFromAxisAngle(new Vector3(1, 0, 0), look.theta);\n return new Quaternion().multiply(qy).multiply(qx);\n};\n\nexport const onMouseDown = (state: MouseState, event: MouseEvent): void => {\n state.dragging = event.button === 0;\n};\n\nexport const onMouseUp = (state: MouseState): void => {\n state.dragging = false;\n};\n\nexport const onMouseMove = (state: MouseState, event: MouseEvent): void => {\n state.pos.set(event.pageX - window.innerWidth / 2, event.pageY - window.innerHeight / 2);\n state.delta.subVectors(state.pos, state.prevPos);\n};\n\nexport const commitMouseFrame = (state: MouseState): void => {\n state.prevPos.copy(state.pos);\n state.delta.set(0, 0);\n};\n\n/**\n * Integrate the pointer delta into the look angles. Mutates `look` in place.\n * Yaw is unbounded; pitch is clamped to avoid flipping over.\n */\nexport const updateLookFromMouse = (mouse: MouseState, look: LookState): void => {\n const xh = mouse.delta.x / window.innerWidth;\n const yh = mouse.delta.y / window.innerHeight;\n\n look.phi += -xh * PHI_SPEED;\n look.theta = clamp(look.theta + -yh * THETA_SPEED, -MAX_PITCH, MAX_PITCH);\n};\n","import { Vector3 } from \"three\";\n\nimport { movementFromAxis } from \"./keyboard\";\nimport { clamp, MAX_PITCH, type LookState } from \"./mouse\";\n\n/**\n * The shape of the shared analog input fpc reads (the kernel's `InputBus`): the\n * move/look axes a joystick feeds. Structural, so any producer with these fields\n * works. fpc interprets them as walking + turning; another camera might not.\n */\nexport interface AnalogInput {\n /** Forward (`+1`) / backward (`-1`) movement. */\n moveForward: number;\n /** Strafe right (`+1`) / left (`-1`). */\n moveStrafe: number;\n /** Look right (`+1`) / left (`-1`). */\n lookX: number;\n /** Look up (`+1`) / down (`-1`). */\n lookY: number;\n}\n\n/** Look turn rate driven by the joystick, in radians per second at full deflection. */\nconst LOOK_SPEED = 2.2;\n\n/** Per-frame translation from the analog move axes, oriented by yaw (`phi`). */\nexport const movementFromTouch = (state: AnalogInput, phi: number, delta: number): Vector3 => {\n if (state.moveForward === 0 && state.moveStrafe === 0) return new Vector3();\n // Strafe sign matches the keyboard helper (left strafe is +1 there).\n return movementFromAxis(state.moveForward, -state.moveStrafe, phi, delta);\n};\n\n/** Per-axis inversion of the analog look, for visitors who feel it backwards. */\nexport interface LookInvert {\n /** Invert the horizontal (turn) axis. */\n x: boolean;\n /** Invert the vertical (pitch) axis. */\n y: boolean;\n}\n\n/** Integrate the look joystick into the look angles, mutating `look` in place. */\nexport const updateLookFromTouch = (\n state: AnalogInput,\n look: LookState,\n delta: number,\n invert: LookInvert = { x: false, y: false },\n): void => {\n if (state.lookX === 0 && state.lookY === 0) return;\n const sx = invert.x ? -1 : 1;\n const sy = invert.y ? -1 : 1;\n look.phi -= state.lookX * sx * LOOK_SPEED * delta;\n look.theta = clamp(look.theta + state.lookY * sy * LOOK_SPEED * delta, -MAX_PITCH, MAX_PITCH);\n};\n","import { Quaternion, Vector3, type PerspectiveCamera } from \"three\";\nimport type { Vec2 } from \"@scenography/scene\";\nimport type { Collider } from \"@scenography/engine\";\n\nimport { resolveMove } from \"./collision\";\nimport { createHeadBobState, headBobOffset, updateHeadBob } from \"./headBob\";\nimport {\n createKeyboardState,\n movementFromKeyboard,\n onKeyDown,\n onKeyUp,\n yawFromKeyboard,\n} from \"./keyboard\";\nimport {\n commitMouseFrame,\n createMouseState,\n onMouseDown,\n onMouseMove,\n onMouseUp,\n quaternionFromLook,\n updateLookFromMouse,\n type LookState,\n} from \"./mouse\";\nimport { movementFromTouch, updateLookFromTouch, type AnalogInput, type LookInvert } from \"./touch\";\n\nexport interface FreeRoamOptions {\n /** Wall collision boxes the camera can't pass through. */\n colliders?: Collider[];\n /** Camera collision radius in world units. Defaults to `25`. */\n playerRadius?: number;\n /** Starting position. */\n position?: Vector3;\n /** Starting yaw in radians. */\n yaw?: number;\n /** Walkable-area polygon (XZ ring); the camera can't step outside it. */\n boundary?: Vec2[];\n /** Ground height at `(x, z)`; when set the camera follows the terrain. */\n groundHeight?: (x: number, z: number) => number;\n /** Eye height above the ground. Defaults to the start Y over the start ground. */\n eyeHeight?: number;\n}\n\n/**\n * First-person, free-roaming camera controls: drag-to-look with the mouse, WASD\n * to walk, a subtle head-bob while moving, wall collision and terrain following.\n * The shared analog input (`AnalogInput`, the kernel's InputBus) is folded in on\n * top of keyboard/mouse, so on-screen joysticks drive it too. It keeps the\n * camera upright (no roll) and clamps pitch — the first-person contract.\n *\n * Framework-agnostic: construct it with a camera, the canvas and the input,\n * call `attach()`, then `update(delta)` once per frame.\n */\nexport class FreeRoamControls {\n private colliders: Collider[];\n private readonly playerRadius: number;\n\n private readonly camera: PerspectiveCamera;\n private readonly domElement: HTMLElement;\n private readonly input: AnalogInput;\n private readonly mouse = createMouseState();\n private readonly keyboard = createKeyboardState();\n private readonly headBob = createHeadBobState();\n private readonly look: LookState = { phi: 0, theta: 0 };\n private rotation = new Quaternion();\n private translation = new Vector3(0, 170, 0);\n private enabled = true;\n private lookInvert: LookInvert = { x: false, y: false };\n private boundary: Vec2[] | undefined;\n private groundHeight: ((x: number, z: number) => number) | null;\n private readonly start: Vector3;\n private eyeHeight: number;\n\n private readonly handlers: Array<[EventTarget, string, EventListener]> = [];\n\n constructor(\n camera: PerspectiveCamera,\n domElement: HTMLElement,\n input: AnalogInput,\n options: FreeRoamOptions = {},\n ) {\n this.camera = camera;\n this.domElement = domElement;\n this.input = input;\n this.colliders = options.colliders ?? [];\n this.playerRadius = options.playerRadius ?? 25;\n if (options.position) this.translation.copy(options.position);\n if (options.yaw !== undefined) this.look.phi = options.yaw;\n this.rotation.setFromAxisAngle(new Vector3(0, 1, 0), this.look.phi);\n this.boundary = options.boundary;\n this.groundHeight = options.groundHeight ?? null;\n this.start = this.translation.clone();\n // Eye height = start Y above the ground under the start position.\n const groundAtStart = this.groundHeight?.(this.start.x, this.start.z) ?? 0;\n this.eyeHeight = options.eyeHeight ?? this.start.y - groundAtStart;\n }\n\n /** Current eye height above the ground (world units). */\n getEyeHeight(): number {\n return this.eyeHeight;\n }\n\n /** Fix the eye height above the ground (world units). */\n setEyeHeight(eyeHeight: number): void {\n this.eyeHeight = eyeHeight;\n }\n\n /**\n * Re-derive the default eye height (start Y over the ground under the start\n * position). Call once asynchronous ground data (a floor heightmap) has\n * loaded: the value captured at construction was measured against the\n * not-yet-displaced floor.\n */\n recalibrateEyeHeight(): void {\n const ground = this.groundHeight?.(this.start.x, this.start.z) ?? 0;\n this.eyeHeight = this.start.y - ground;\n }\n\n attach(): void {\n const bind = (target: EventTarget, type: string, listener: EventListener): void => {\n target.addEventListener(type, listener);\n this.handlers.push([target, type, listener]);\n };\n\n bind(this.domElement, \"mousedown\", (e) => onMouseDown(this.mouse, e as MouseEvent));\n bind(window, \"mousemove\", (e) => onMouseMove(this.mouse, e as MouseEvent));\n bind(window, \"mouseup\", () => onMouseUp(this.mouse));\n bind(window, \"keydown\", (e) => onKeyDown(this.keyboard, e as KeyboardEvent));\n bind(window, \"keyup\", (e) => onKeyUp(this.keyboard, e as KeyboardEvent));\n }\n\n detach(): void {\n for (const [target, type, listener] of this.handlers)\n target.removeEventListener(type, listener);\n this.handlers.length = 0;\n }\n\n /** Replace the wall collision set (e.g. after the scene's walls change). */\n setColliders(colliders: Collider[]): void {\n this.colliders = colliders;\n }\n\n /** Replace the walkable-area boundary polygon (or clear it with `undefined`). */\n setBoundary(boundary: Vec2[] | undefined): void {\n this.boundary = boundary;\n }\n\n /** Replace the ground-height function (or clear it to stop following terrain). */\n setGroundHeight(groundHeight: ((x: number, z: number) => number) | null): void {\n this.groundHeight = groundHeight;\n }\n\n /**\n * Enable/disable user input. While disabled, `update()` leaves the camera\n * untouched so another driver (e.g. a camera tween) can own it; re-enable after\n * syncing the pose with `setPose` so free-roam continues from there.\n */\n setEnabled(enabled: boolean): void {\n this.enabled = enabled;\n }\n\n /** Invert the analog look per axis (the visitor's control preference). */\n setLookInvert(x: boolean, y: boolean): void {\n this.lookInvert = { x, y };\n }\n\n /** Current pose: world position plus yaw (`phi`) and pitch (`theta`) in radians. */\n getPose(): { position: Vector3; phi: number; theta: number } {\n return { position: this.translation.clone(), phi: this.look.phi, theta: this.look.theta };\n }\n\n /** Force the pose (e.g. at the end of a camera tween) so input resumes from it. */\n setPose(position: Vector3, phi: number, theta: number): void {\n this.translation.copy(position);\n this.look.phi = phi;\n this.look.theta = theta;\n this.rotation = quaternionFromLook(this.look);\n }\n\n /** Advance the camera by `delta` seconds of input. */\n update(delta: number): void {\n // Disabled: another driver owns the camera this frame. Drop any accumulated\n // pointer delta so look doesn't jump when control is handed back.\n if (!this.enabled) {\n commitMouseFrame(this.mouse);\n return;\n }\n // Accumulate look from every source into the angles, then derive rotation.\n if (this.mouse.dragging) updateLookFromMouse(this.mouse, this.look);\n this.look.phi += yawFromKeyboard(this.keyboard, delta);\n updateLookFromTouch(this.input, this.look, delta, this.lookInvert);\n this.rotation = quaternionFromLook(this.look);\n\n const step = movementFromKeyboard(this.keyboard, this.look.phi, delta).add(\n movementFromTouch(this.input, this.look.phi, delta),\n );\n this.translation = resolveMove(\n this.translation,\n step,\n this.colliders,\n this.playerRadius,\n this.boundary,\n );\n // Follow the terrain: keep the eye a fixed height above the ground below.\n if (this.groundHeight) {\n this.translation.y = this.groundHeight(this.translation.x, this.translation.z) + this.eyeHeight;\n }\n if (step.x !== 0 || step.z !== 0) this.headBob.active = true;\n updateHeadBob(this.headBob, delta);\n\n this.camera.quaternion.copy(this.rotation);\n this.camera.position.copy(this.translation);\n this.camera.position.y += headBobOffset(this.headBob);\n\n const target = new Vector3(0, 0, -1)\n .applyQuaternion(this.rotation)\n .multiplyScalar(100)\n .add(this.translation);\n this.camera.lookAt(target);\n\n commitMouseFrame(this.mouse);\n }\n}\n","import {\n COLLIDERS,\n ROAM_CONTROL,\n TERRAIN,\n type Collider,\n type RoamControl,\n type SystemPlugin,\n type TerrainCapability,\n} from \"@scenography/engine\";\nimport type { Scene } from \"@scenography/scene\";\nimport { Vector3 } from \"three\";\n\nimport { FreeRoamControls } from \"./FreeRoamControls\";\n\n/**\n * First-person camera — a *system* plugin. It drives the kernel camera from\n * WASD/drag plus the shared analog input (joysticks), keeps it at `eyeHeight`\n * over the terrain, and blocks it against wall colliders. Consumes the `terrain`\n * and `colliders` capabilities (live, so it tracks async heightmaps and hot wall\n * edits) and publishes `roamControl` so a framing/tour system can suspend it.\n */\nexport const fpc = (): SystemPlugin => ({\n name: \"fpc\",\n setup: (ctx) => {\n const { camera, canvas, input, capabilities, scene } = ctx;\n const terrainHeight = (x: number, z: number): number =>\n capabilities.get<TerrainCapability>(TERRAIN)?.heightAt(x, z) ?? 0;\n // Follow terrain only when some plugin provides it; otherwise keep a fixed Y.\n const groundHeight = (): ((x: number, z: number) => number) | undefined =>\n capabilities.get<TerrainCapability>(TERRAIN) ? terrainHeight : undefined;\n\n const start = scene.camera.start;\n const controls = new FreeRoamControls(camera, canvas, input, {\n position: new Vector3(start.position[0], start.position[1], start.position[2]),\n yaw: start.yaw,\n boundary: scene.boundary,\n eyeHeight: scene.camera.eyeHeight,\n groundHeight: groundHeight(),\n });\n controls.attach();\n capabilities.provide(ROAM_CONTROL, controls satisfies RoamControl);\n\n // Eye height: an explicit `camera.eyeHeight` wins. Otherwise it was derived\n // from the start Y over the ground — but a floor heightmap loads async, so\n // recalibrate once the terrain is real, or the visitor walks in the air.\n const calibrateEyeHeight = (s: Scene): void => {\n if (s.camera.eyeHeight !== undefined) controls.setEyeHeight(s.camera.eyeHeight);\n else {\n const terrain = capabilities.get<TerrainCapability>(TERRAIN);\n if (terrain) void terrain.ready.then(() => controls.recalibrateEyeHeight());\n }\n };\n calibrateEyeHeight(scene);\n\n return {\n update: (deltaMs) => {\n const flags = ctx.controlFlags();\n controls.setLookInvert(flags.invertLookX, flags.invertLookY);\n controls.setColliders(capabilities.get<Collider[]>(COLLIDERS) ?? []);\n controls.update(deltaMs / 1000);\n },\n setScene: (s) => {\n controls.setBoundary(s.boundary);\n controls.setGroundHeight(groundHeight() ?? null);\n calibrateEyeHeight(s);\n },\n dispose: () => {\n controls.detach();\n capabilities.revoke(ROAM_CONTROL);\n },\n };\n },\n});\n"],"mappings":"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"}
|
package/dist/index.d.cts
CHANGED
|
@@ -69,6 +69,7 @@ declare class FreeRoamControls {
|
|
|
69
69
|
private rotation;
|
|
70
70
|
private translation;
|
|
71
71
|
private enabled;
|
|
72
|
+
private lookInvert;
|
|
72
73
|
private boundary;
|
|
73
74
|
private groundHeight;
|
|
74
75
|
private readonly start;
|
|
@@ -100,6 +101,8 @@ declare class FreeRoamControls {
|
|
|
100
101
|
* syncing the pose with `setPose` so free-roam continues from there.
|
|
101
102
|
*/
|
|
102
103
|
setEnabled(enabled: boolean): void;
|
|
104
|
+
/** Invert the analog look per axis (the visitor's control preference). */
|
|
105
|
+
setLookInvert(x: boolean, y: boolean): void;
|
|
103
106
|
/** Current pose: world position plus yaw (`phi`) and pitch (`theta`) in radians. */
|
|
104
107
|
getPose(): {
|
|
105
108
|
position: Vector3;
|
package/dist/index.d.mts
CHANGED
|
@@ -69,6 +69,7 @@ declare class FreeRoamControls {
|
|
|
69
69
|
private rotation;
|
|
70
70
|
private translation;
|
|
71
71
|
private enabled;
|
|
72
|
+
private lookInvert;
|
|
72
73
|
private boundary;
|
|
73
74
|
private groundHeight;
|
|
74
75
|
private readonly start;
|
|
@@ -100,6 +101,8 @@ declare class FreeRoamControls {
|
|
|
100
101
|
* syncing the pose with `setPose` so free-roam continues from there.
|
|
101
102
|
*/
|
|
102
103
|
setEnabled(enabled: boolean): void;
|
|
104
|
+
/** Invert the analog look per axis (the visitor's control preference). */
|
|
105
|
+
setLookInvert(x: boolean, y: boolean): void;
|
|
103
106
|
/** Current pose: world position plus yaw (`phi`) and pitch (`theta`) in radians. */
|
|
104
107
|
getPose(): {
|
|
105
108
|
position: Vector3;
|
package/dist/index.mjs
CHANGED
|
@@ -159,10 +159,15 @@ const movementFromTouch = (state, phi, delta) => {
|
|
|
159
159
|
return movementFromAxis(state.moveForward, -state.moveStrafe, phi, delta);
|
|
160
160
|
};
|
|
161
161
|
/** Integrate the look joystick into the look angles, mutating `look` in place. */
|
|
162
|
-
const updateLookFromTouch = (state, look, delta
|
|
162
|
+
const updateLookFromTouch = (state, look, delta, invert = {
|
|
163
|
+
x: false,
|
|
164
|
+
y: false
|
|
165
|
+
}) => {
|
|
163
166
|
if (state.lookX === 0 && state.lookY === 0) return;
|
|
164
|
-
|
|
165
|
-
|
|
167
|
+
const sx = invert.x ? -1 : 1;
|
|
168
|
+
const sy = invert.y ? -1 : 1;
|
|
169
|
+
look.phi -= state.lookX * sx * LOOK_SPEED * delta;
|
|
170
|
+
look.theta = clamp(look.theta + state.lookY * sy * LOOK_SPEED * delta, -MAX_PITCH, MAX_PITCH);
|
|
166
171
|
};
|
|
167
172
|
//#endregion
|
|
168
173
|
//#region src/FreeRoamControls.ts
|
|
@@ -192,6 +197,10 @@ var FreeRoamControls = class {
|
|
|
192
197
|
rotation = new Quaternion();
|
|
193
198
|
translation = new Vector3(0, 170, 0);
|
|
194
199
|
enabled = true;
|
|
200
|
+
lookInvert = {
|
|
201
|
+
x: false,
|
|
202
|
+
y: false
|
|
203
|
+
};
|
|
195
204
|
boundary;
|
|
196
205
|
groundHeight;
|
|
197
206
|
start;
|
|
@@ -269,6 +278,13 @@ var FreeRoamControls = class {
|
|
|
269
278
|
setEnabled(enabled) {
|
|
270
279
|
this.enabled = enabled;
|
|
271
280
|
}
|
|
281
|
+
/** Invert the analog look per axis (the visitor's control preference). */
|
|
282
|
+
setLookInvert(x, y) {
|
|
283
|
+
this.lookInvert = {
|
|
284
|
+
x,
|
|
285
|
+
y
|
|
286
|
+
};
|
|
287
|
+
}
|
|
272
288
|
/** Current pose: world position plus yaw (`phi`) and pitch (`theta`) in radians. */
|
|
273
289
|
getPose() {
|
|
274
290
|
return {
|
|
@@ -292,7 +308,7 @@ var FreeRoamControls = class {
|
|
|
292
308
|
}
|
|
293
309
|
if (this.mouse.dragging) updateLookFromMouse(this.mouse, this.look);
|
|
294
310
|
this.look.phi += yawFromKeyboard(this.keyboard, delta);
|
|
295
|
-
updateLookFromTouch(this.input, this.look, delta);
|
|
311
|
+
updateLookFromTouch(this.input, this.look, delta, this.lookInvert);
|
|
296
312
|
this.rotation = quaternionFromLook(this.look);
|
|
297
313
|
const step = movementFromKeyboard(this.keyboard, this.look.phi, delta).add(movementFromTouch(this.input, this.look.phi, delta));
|
|
298
314
|
this.translation = resolveMove(this.translation, step, this.colliders, this.playerRadius, this.boundary);
|
|
@@ -342,6 +358,8 @@ const fpc = () => ({
|
|
|
342
358
|
calibrateEyeHeight(scene);
|
|
343
359
|
return {
|
|
344
360
|
update: (deltaMs) => {
|
|
361
|
+
const flags = ctx.controlFlags();
|
|
362
|
+
controls.setLookInvert(flags.invertLookX, flags.invertLookY);
|
|
345
363
|
controls.setColliders(capabilities.get(COLLIDERS) ?? []);
|
|
346
364
|
controls.update(deltaMs / 1e3);
|
|
347
365
|
},
|
package/dist/index.mjs.map
CHANGED
|
@@ -1 +1 @@
|
|
|
1
|
-
{"version":3,"file":"index.mjs","names":[],"sources":["../src/collision.ts","../src/headBob.ts","../src/keyboard.ts","../src/mouse.ts","../src/touch.ts","../src/FreeRoamControls.ts","../src/plugin.ts"],"sourcesContent":["import { Vector3 } from \"three\";\nimport type { Vec2 } from \"@scenography/scene\";\nimport type { Collider } from \"@scenography/engine\";\n\n/** Whether `(x, z)` lies inside the XZ polygon (even-odd ray casting). */\nexport const insidePolygon = (x: number, z: number, polygon: Vec2[]): boolean => {\n let inside = false;\n for (let i = 0, j = polygon.length - 1; i < polygon.length; j = i++) {\n const a = polygon[i];\n const b = polygon[j];\n if (!a || !b) continue; // unreachable given the loop bounds; satisfies strict indexing\n if (a[1] > z !== b[1] > z && x < ((b[0] - a[0]) * (z - a[1])) / (b[1] - a[1]) + a[0]) {\n inside = !inside;\n }\n }\n return inside;\n};\n\n/** Whether a circle of `radius` centered at `(x, z)` overlaps any collider box. */\nconst hits = (x: number, z: number, radius: number, colliders: Collider[]): boolean => {\n const r2 = radius * radius;\n for (const c of colliders) {\n const px = x - c.cx;\n const pz = z - c.cz;\n // Project onto the box axes: along its length and along its thickness normal.\n const along = px * c.dx + pz * c.dz;\n const perp = px * -c.dz + pz * c.dx;\n // Distance from the point to the closest point on the box.\n const ddAlong = along - Math.max(-c.halfLength, Math.min(c.halfLength, along));\n const ddPerp = perp - Math.max(-c.halfThickness, Math.min(c.halfThickness, perp));\n if (ddAlong * ddAlong + ddPerp * ddPerp < r2) return true;\n }\n return false;\n};\n\n/**\n * Resolve a desired XZ step against wall colliders and an optional walkable-area\n * boundary, one axis at a time so the camera slides along an obstacle instead of\n * sticking when it can't move straight.\n */\nexport const resolveMove = (\n current: Vector3,\n step: Vector3,\n colliders: Collider[],\n radius: number,\n boundary?: Vec2[],\n): Vector3 => {\n let { x, z } = current;\n const free = (nx: number, nz: number): boolean =>\n !hits(nx, nz, radius, colliders) && (!boundary || insidePolygon(nx, nz, boundary));\n if (step.x !== 0 && free(x + step.x, z)) x += step.x;\n if (step.z !== 0 && free(x, z + step.z)) z += step.z;\n return new Vector3(x, current.y, z);\n};\n","/** State for the subtle vertical bob applied while walking. */\nexport interface HeadBobState {\n active: boolean;\n timer: number;\n}\n\nexport const createHeadBobState = (): HeadBobState => ({ active: false, timer: 0 });\n\n/**\n * Advance the head-bob timer by `elapsed` seconds, snapping to half-wavelength\n * steps so the bob always settles back to neutral when movement stops.\n */\nexport const updateHeadBob = (state: HeadBobState, elapsed: number): void => {\n if (!state.active) return;\n\n const wavelength = Math.PI;\n const nextStep = 1 + Math.floor(((state.timer + 1e-6) * 10) / wavelength);\n const nextStepTime = (nextStep * wavelength) / 10;\n state.timer = Math.min(state.timer + elapsed, nextStepTime);\n\n if (state.timer === nextStepTime) state.active = false;\n};\n\n/** Vertical offset to add to the camera, in world units. */\nexport const headBobOffset = (state: HeadBobState): number => Math.sin(state.timer * 10) * 1.5;\n","import { Quaternion, Vector3 } from \"three\";\n\n/** Set of currently-pressed key codes. */\nexport type KeyboardState = Record<string, boolean>;\n\nexport const createKeyboardState = (): KeyboardState => ({});\n\n/** Codes the controls consume; preventDefault'd so the arrows don't scroll. */\nconst MOVEMENT_KEYS = new Set([\n \"KeyW\",\n \"KeyA\",\n \"KeyS\",\n \"KeyD\",\n \"ArrowUp\",\n \"ArrowDown\",\n \"ArrowLeft\",\n \"ArrowRight\",\n]);\n\nexport const onKeyDown = (state: KeyboardState, event: KeyboardEvent): void => {\n state[event.code] = true;\n if (MOVEMENT_KEYS.has(event.code)) event.preventDefault();\n};\n\nexport const onKeyUp = (state: KeyboardState, event: KeyboardEvent): void => {\n state[event.code] = false;\n};\n\n/** Yaw turn rate for the Q/E keys, in radians per second. */\nconst KEY_TURN_SPEED = 1.8;\n\n/**\n * Turn a forward/strafe pair (each in `-1..1`) into a per-frame translation\n * oriented by the current yaw (`phi`). Shared by keyboard and touch input.\n */\nexport const movementFromAxis = (\n forwardVelocity: number,\n strafeVelocity: number,\n phi: number,\n delta: number,\n boosted = false,\n): Vector3 => {\n const factor = boosted ? 60 : 20;\n const yaw = new Quaternion().setFromAxisAngle(new Vector3(0, 1, 0), phi);\n\n const forward = new Vector3(0, 0, -10)\n .applyQuaternion(yaw)\n .multiplyScalar(forwardVelocity * delta * factor);\n const left = new Vector3(-10, 0, 0)\n .applyQuaternion(yaw)\n .multiplyScalar(strafeVelocity * delta * factor);\n\n return forward.add(left);\n};\n\n/**\n * Compute the per-frame translation from WASD or arrow-key input, oriented by\n * the current yaw (`phi`). Holding Shift acts as a speed booster.\n */\nexport const movementFromKeyboard = (state: KeyboardState, phi: number, delta: number): Vector3 => {\n const has = (...codes: string[]): number => (codes.some((c) => state[c]) ? 1 : 0);\n const forwardVelocity = has(\"KeyW\", \"ArrowUp\") - has(\"KeyS\", \"ArrowDown\");\n const strafeVelocity = has(\"KeyA\", \"ArrowLeft\") - has(\"KeyD\", \"ArrowRight\");\n const boosted = Boolean(state[\"ShiftLeft\"] || state[\"ShiftRight\"]);\n\n return movementFromAxis(forwardVelocity, strafeVelocity, phi, delta, boosted);\n};\n\n/**\n * Per-frame yaw delta (radians) from the Q/E turn keys. Q turns left\n * (increasing yaw), E turns right.\n */\nexport const yawFromKeyboard = (state: KeyboardState, delta: number): number => {\n const turn = (state[\"KeyQ\"] ? 1 : 0) - (state[\"KeyE\"] ? 1 : 0);\n return turn * KEY_TURN_SPEED * delta;\n};\n","import { Quaternion, Vector2, Vector3 } from \"three\";\n\n/** Mutable pointer state accumulated between frames. */\nexport interface MouseState {\n /** Whether the primary button is held (drag-to-look). */\n dragging: boolean;\n pos: Vector2;\n prevPos: Vector2;\n delta: Vector2;\n}\n\n/** Look angles driven by the pointer. */\nexport interface LookState {\n phi: number;\n theta: number;\n}\n\nconst PHI_SPEED = 8;\nconst THETA_SPEED = 5;\n\nexport const createMouseState = (): MouseState => ({\n dragging: false,\n pos: new Vector2(),\n prevPos: new Vector2(),\n delta: new Vector2(),\n});\n\nexport const clamp = (x: number, a: number, b: number): number => Math.min(Math.max(x, a), b);\n\n/** Maximum up/down look angle (radians) — keeps the camera from flipping over. */\nexport const MAX_PITCH = Math.PI / 3;\n\n/** Build the look quaternion from the current yaw/pitch angles. */\nexport const quaternionFromLook = (look: LookState): Quaternion => {\n const qy = new Quaternion().setFromAxisAngle(new Vector3(0, 1, 0), look.phi);\n const qx = new Quaternion().setFromAxisAngle(new Vector3(1, 0, 0), look.theta);\n return new Quaternion().multiply(qy).multiply(qx);\n};\n\nexport const onMouseDown = (state: MouseState, event: MouseEvent): void => {\n state.dragging = event.button === 0;\n};\n\nexport const onMouseUp = (state: MouseState): void => {\n state.dragging = false;\n};\n\nexport const onMouseMove = (state: MouseState, event: MouseEvent): void => {\n state.pos.set(event.pageX - window.innerWidth / 2, event.pageY - window.innerHeight / 2);\n state.delta.subVectors(state.pos, state.prevPos);\n};\n\nexport const commitMouseFrame = (state: MouseState): void => {\n state.prevPos.copy(state.pos);\n state.delta.set(0, 0);\n};\n\n/**\n * Integrate the pointer delta into the look angles. Mutates `look` in place.\n * Yaw is unbounded; pitch is clamped to avoid flipping over.\n */\nexport const updateLookFromMouse = (mouse: MouseState, look: LookState): void => {\n const xh = mouse.delta.x / window.innerWidth;\n const yh = mouse.delta.y / window.innerHeight;\n\n look.phi += -xh * PHI_SPEED;\n look.theta = clamp(look.theta + -yh * THETA_SPEED, -MAX_PITCH, MAX_PITCH);\n};\n","import { Vector3 } from \"three\";\n\nimport { movementFromAxis } from \"./keyboard\";\nimport { clamp, MAX_PITCH, type LookState } from \"./mouse\";\n\n/**\n * The shape of the shared analog input fpc reads (the kernel's `InputBus`): the\n * move/look axes a joystick feeds. Structural, so any producer with these fields\n * works. fpc interprets them as walking + turning; another camera might not.\n */\nexport interface AnalogInput {\n /** Forward (`+1`) / backward (`-1`) movement. */\n moveForward: number;\n /** Strafe right (`+1`) / left (`-1`). */\n moveStrafe: number;\n /** Look right (`+1`) / left (`-1`). */\n lookX: number;\n /** Look up (`+1`) / down (`-1`). */\n lookY: number;\n}\n\n/** Look turn rate driven by the joystick, in radians per second at full deflection. */\nconst LOOK_SPEED = 2.2;\n\n/** Per-frame translation from the analog move axes, oriented by yaw (`phi`). */\nexport const movementFromTouch = (state: AnalogInput, phi: number, delta: number): Vector3 => {\n if (state.moveForward === 0 && state.moveStrafe === 0) return new Vector3();\n // Strafe sign matches the keyboard helper (left strafe is +1 there).\n return movementFromAxis(state.moveForward, -state.moveStrafe, phi, delta);\n};\n\n/** Integrate the look joystick into the look angles, mutating `look` in place. */\nexport const updateLookFromTouch = (state: AnalogInput, look: LookState, delta: number): void => {\n if (state.lookX === 0 && state.lookY === 0) return;\n look.phi -= state.lookX * LOOK_SPEED * delta;\n look.theta = clamp(look.theta + state.lookY * LOOK_SPEED * delta, -MAX_PITCH, MAX_PITCH);\n};\n","import { Quaternion, Vector3, type PerspectiveCamera } from \"three\";\nimport type { Vec2 } from \"@scenography/scene\";\nimport type { Collider } from \"@scenography/engine\";\n\nimport { resolveMove } from \"./collision\";\nimport { createHeadBobState, headBobOffset, updateHeadBob } from \"./headBob\";\nimport {\n createKeyboardState,\n movementFromKeyboard,\n onKeyDown,\n onKeyUp,\n yawFromKeyboard,\n} from \"./keyboard\";\nimport {\n commitMouseFrame,\n createMouseState,\n onMouseDown,\n onMouseMove,\n onMouseUp,\n quaternionFromLook,\n updateLookFromMouse,\n type LookState,\n} from \"./mouse\";\nimport { movementFromTouch, updateLookFromTouch, type AnalogInput } from \"./touch\";\n\nexport interface FreeRoamOptions {\n /** Wall collision boxes the camera can't pass through. */\n colliders?: Collider[];\n /** Camera collision radius in world units. Defaults to `25`. */\n playerRadius?: number;\n /** Starting position. */\n position?: Vector3;\n /** Starting yaw in radians. */\n yaw?: number;\n /** Walkable-area polygon (XZ ring); the camera can't step outside it. */\n boundary?: Vec2[];\n /** Ground height at `(x, z)`; when set the camera follows the terrain. */\n groundHeight?: (x: number, z: number) => number;\n /** Eye height above the ground. Defaults to the start Y over the start ground. */\n eyeHeight?: number;\n}\n\n/**\n * First-person, free-roaming camera controls: drag-to-look with the mouse, WASD\n * to walk, a subtle head-bob while moving, wall collision and terrain following.\n * The shared analog input (`AnalogInput`, the kernel's InputBus) is folded in on\n * top of keyboard/mouse, so on-screen joysticks drive it too. It keeps the\n * camera upright (no roll) and clamps pitch — the first-person contract.\n *\n * Framework-agnostic: construct it with a camera, the canvas and the input,\n * call `attach()`, then `update(delta)` once per frame.\n */\nexport class FreeRoamControls {\n private colliders: Collider[];\n private readonly playerRadius: number;\n\n private readonly camera: PerspectiveCamera;\n private readonly domElement: HTMLElement;\n private readonly input: AnalogInput;\n private readonly mouse = createMouseState();\n private readonly keyboard = createKeyboardState();\n private readonly headBob = createHeadBobState();\n private readonly look: LookState = { phi: 0, theta: 0 };\n private rotation = new Quaternion();\n private translation = new Vector3(0, 170, 0);\n private enabled = true;\n private boundary: Vec2[] | undefined;\n private groundHeight: ((x: number, z: number) => number) | null;\n private readonly start: Vector3;\n private eyeHeight: number;\n\n private readonly handlers: Array<[EventTarget, string, EventListener]> = [];\n\n constructor(\n camera: PerspectiveCamera,\n domElement: HTMLElement,\n input: AnalogInput,\n options: FreeRoamOptions = {},\n ) {\n this.camera = camera;\n this.domElement = domElement;\n this.input = input;\n this.colliders = options.colliders ?? [];\n this.playerRadius = options.playerRadius ?? 25;\n if (options.position) this.translation.copy(options.position);\n if (options.yaw !== undefined) this.look.phi = options.yaw;\n this.rotation.setFromAxisAngle(new Vector3(0, 1, 0), this.look.phi);\n this.boundary = options.boundary;\n this.groundHeight = options.groundHeight ?? null;\n this.start = this.translation.clone();\n // Eye height = start Y above the ground under the start position.\n const groundAtStart = this.groundHeight?.(this.start.x, this.start.z) ?? 0;\n this.eyeHeight = options.eyeHeight ?? this.start.y - groundAtStart;\n }\n\n /** Current eye height above the ground (world units). */\n getEyeHeight(): number {\n return this.eyeHeight;\n }\n\n /** Fix the eye height above the ground (world units). */\n setEyeHeight(eyeHeight: number): void {\n this.eyeHeight = eyeHeight;\n }\n\n /**\n * Re-derive the default eye height (start Y over the ground under the start\n * position). Call once asynchronous ground data (a floor heightmap) has\n * loaded: the value captured at construction was measured against the\n * not-yet-displaced floor.\n */\n recalibrateEyeHeight(): void {\n const ground = this.groundHeight?.(this.start.x, this.start.z) ?? 0;\n this.eyeHeight = this.start.y - ground;\n }\n\n attach(): void {\n const bind = (target: EventTarget, type: string, listener: EventListener): void => {\n target.addEventListener(type, listener);\n this.handlers.push([target, type, listener]);\n };\n\n bind(this.domElement, \"mousedown\", (e) => onMouseDown(this.mouse, e as MouseEvent));\n bind(window, \"mousemove\", (e) => onMouseMove(this.mouse, e as MouseEvent));\n bind(window, \"mouseup\", () => onMouseUp(this.mouse));\n bind(window, \"keydown\", (e) => onKeyDown(this.keyboard, e as KeyboardEvent));\n bind(window, \"keyup\", (e) => onKeyUp(this.keyboard, e as KeyboardEvent));\n }\n\n detach(): void {\n for (const [target, type, listener] of this.handlers)\n target.removeEventListener(type, listener);\n this.handlers.length = 0;\n }\n\n /** Replace the wall collision set (e.g. after the scene's walls change). */\n setColliders(colliders: Collider[]): void {\n this.colliders = colliders;\n }\n\n /** Replace the walkable-area boundary polygon (or clear it with `undefined`). */\n setBoundary(boundary: Vec2[] | undefined): void {\n this.boundary = boundary;\n }\n\n /** Replace the ground-height function (or clear it to stop following terrain). */\n setGroundHeight(groundHeight: ((x: number, z: number) => number) | null): void {\n this.groundHeight = groundHeight;\n }\n\n /**\n * Enable/disable user input. While disabled, `update()` leaves the camera\n * untouched so another driver (e.g. a camera tween) can own it; re-enable after\n * syncing the pose with `setPose` so free-roam continues from there.\n */\n setEnabled(enabled: boolean): void {\n this.enabled = enabled;\n }\n\n /** Current pose: world position plus yaw (`phi`) and pitch (`theta`) in radians. */\n getPose(): { position: Vector3; phi: number; theta: number } {\n return { position: this.translation.clone(), phi: this.look.phi, theta: this.look.theta };\n }\n\n /** Force the pose (e.g. at the end of a camera tween) so input resumes from it. */\n setPose(position: Vector3, phi: number, theta: number): void {\n this.translation.copy(position);\n this.look.phi = phi;\n this.look.theta = theta;\n this.rotation = quaternionFromLook(this.look);\n }\n\n /** Advance the camera by `delta` seconds of input. */\n update(delta: number): void {\n // Disabled: another driver owns the camera this frame. Drop any accumulated\n // pointer delta so look doesn't jump when control is handed back.\n if (!this.enabled) {\n commitMouseFrame(this.mouse);\n return;\n }\n // Accumulate look from every source into the angles, then derive rotation.\n if (this.mouse.dragging) updateLookFromMouse(this.mouse, this.look);\n this.look.phi += yawFromKeyboard(this.keyboard, delta);\n updateLookFromTouch(this.input, this.look, delta);\n this.rotation = quaternionFromLook(this.look);\n\n const step = movementFromKeyboard(this.keyboard, this.look.phi, delta).add(\n movementFromTouch(this.input, this.look.phi, delta),\n );\n this.translation = resolveMove(\n this.translation,\n step,\n this.colliders,\n this.playerRadius,\n this.boundary,\n );\n // Follow the terrain: keep the eye a fixed height above the ground below.\n if (this.groundHeight) {\n this.translation.y = this.groundHeight(this.translation.x, this.translation.z) + this.eyeHeight;\n }\n if (step.x !== 0 || step.z !== 0) this.headBob.active = true;\n updateHeadBob(this.headBob, delta);\n\n this.camera.quaternion.copy(this.rotation);\n this.camera.position.copy(this.translation);\n this.camera.position.y += headBobOffset(this.headBob);\n\n const target = new Vector3(0, 0, -1)\n .applyQuaternion(this.rotation)\n .multiplyScalar(100)\n .add(this.translation);\n this.camera.lookAt(target);\n\n commitMouseFrame(this.mouse);\n }\n}\n","import {\n COLLIDERS,\n ROAM_CONTROL,\n TERRAIN,\n type Collider,\n type RoamControl,\n type SystemPlugin,\n type TerrainCapability,\n} from \"@scenography/engine\";\nimport type { Scene } from \"@scenography/scene\";\nimport { Vector3 } from \"three\";\n\nimport { FreeRoamControls } from \"./FreeRoamControls\";\n\n/**\n * First-person camera — a *system* plugin. It drives the kernel camera from\n * WASD/drag plus the shared analog input (joysticks), keeps it at `eyeHeight`\n * over the terrain, and blocks it against wall colliders. Consumes the `terrain`\n * and `colliders` capabilities (live, so it tracks async heightmaps and hot wall\n * edits) and publishes `roamControl` so a framing/tour system can suspend it.\n */\nexport const fpc = (): SystemPlugin => ({\n name: \"fpc\",\n setup: (ctx) => {\n const { camera, canvas, input, capabilities, scene } = ctx;\n const terrainHeight = (x: number, z: number): number =>\n capabilities.get<TerrainCapability>(TERRAIN)?.heightAt(x, z) ?? 0;\n // Follow terrain only when some plugin provides it; otherwise keep a fixed Y.\n const groundHeight = (): ((x: number, z: number) => number) | undefined =>\n capabilities.get<TerrainCapability>(TERRAIN) ? terrainHeight : undefined;\n\n const start = scene.camera.start;\n const controls = new FreeRoamControls(camera, canvas, input, {\n position: new Vector3(start.position[0], start.position[1], start.position[2]),\n yaw: start.yaw,\n boundary: scene.boundary,\n eyeHeight: scene.camera.eyeHeight,\n groundHeight: groundHeight(),\n });\n controls.attach();\n capabilities.provide(ROAM_CONTROL, controls satisfies RoamControl);\n\n // Eye height: an explicit `camera.eyeHeight` wins. Otherwise it was derived\n // from the start Y over the ground — but a floor heightmap loads async, so\n // recalibrate once the terrain is real, or the visitor walks in the air.\n const calibrateEyeHeight = (s: Scene): void => {\n if (s.camera.eyeHeight !== undefined) controls.setEyeHeight(s.camera.eyeHeight);\n else {\n const terrain = capabilities.get<TerrainCapability>(TERRAIN);\n if (terrain) void terrain.ready.then(() => controls.recalibrateEyeHeight());\n }\n };\n calibrateEyeHeight(scene);\n\n return {\n update: (deltaMs) => {\n controls.setColliders(capabilities.get<Collider[]>(COLLIDERS) ?? []);\n controls.update(deltaMs / 1000);\n },\n setScene: (s) => {\n controls.setBoundary(s.boundary);\n controls.setGroundHeight(groundHeight() ?? null);\n calibrateEyeHeight(s);\n },\n dispose: () => {\n controls.detach();\n capabilities.revoke(ROAM_CONTROL);\n },\n };\n },\n});\n"],"mappings":"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"}
|
|
1
|
+
{"version":3,"file":"index.mjs","names":[],"sources":["../src/collision.ts","../src/headBob.ts","../src/keyboard.ts","../src/mouse.ts","../src/touch.ts","../src/FreeRoamControls.ts","../src/plugin.ts"],"sourcesContent":["import { Vector3 } from \"three\";\nimport type { Vec2 } from \"@scenography/scene\";\nimport type { Collider } from \"@scenography/engine\";\n\n/** Whether `(x, z)` lies inside the XZ polygon (even-odd ray casting). */\nexport const insidePolygon = (x: number, z: number, polygon: Vec2[]): boolean => {\n let inside = false;\n for (let i = 0, j = polygon.length - 1; i < polygon.length; j = i++) {\n const a = polygon[i];\n const b = polygon[j];\n if (!a || !b) continue; // unreachable given the loop bounds; satisfies strict indexing\n if (a[1] > z !== b[1] > z && x < ((b[0] - a[0]) * (z - a[1])) / (b[1] - a[1]) + a[0]) {\n inside = !inside;\n }\n }\n return inside;\n};\n\n/** Whether a circle of `radius` centered at `(x, z)` overlaps any collider box. */\nconst hits = (x: number, z: number, radius: number, colliders: Collider[]): boolean => {\n const r2 = radius * radius;\n for (const c of colliders) {\n const px = x - c.cx;\n const pz = z - c.cz;\n // Project onto the box axes: along its length and along its thickness normal.\n const along = px * c.dx + pz * c.dz;\n const perp = px * -c.dz + pz * c.dx;\n // Distance from the point to the closest point on the box.\n const ddAlong = along - Math.max(-c.halfLength, Math.min(c.halfLength, along));\n const ddPerp = perp - Math.max(-c.halfThickness, Math.min(c.halfThickness, perp));\n if (ddAlong * ddAlong + ddPerp * ddPerp < r2) return true;\n }\n return false;\n};\n\n/**\n * Resolve a desired XZ step against wall colliders and an optional walkable-area\n * boundary, one axis at a time so the camera slides along an obstacle instead of\n * sticking when it can't move straight.\n */\nexport const resolveMove = (\n current: Vector3,\n step: Vector3,\n colliders: Collider[],\n radius: number,\n boundary?: Vec2[],\n): Vector3 => {\n let { x, z } = current;\n const free = (nx: number, nz: number): boolean =>\n !hits(nx, nz, radius, colliders) && (!boundary || insidePolygon(nx, nz, boundary));\n if (step.x !== 0 && free(x + step.x, z)) x += step.x;\n if (step.z !== 0 && free(x, z + step.z)) z += step.z;\n return new Vector3(x, current.y, z);\n};\n","/** State for the subtle vertical bob applied while walking. */\nexport interface HeadBobState {\n active: boolean;\n timer: number;\n}\n\nexport const createHeadBobState = (): HeadBobState => ({ active: false, timer: 0 });\n\n/**\n * Advance the head-bob timer by `elapsed` seconds, snapping to half-wavelength\n * steps so the bob always settles back to neutral when movement stops.\n */\nexport const updateHeadBob = (state: HeadBobState, elapsed: number): void => {\n if (!state.active) return;\n\n const wavelength = Math.PI;\n const nextStep = 1 + Math.floor(((state.timer + 1e-6) * 10) / wavelength);\n const nextStepTime = (nextStep * wavelength) / 10;\n state.timer = Math.min(state.timer + elapsed, nextStepTime);\n\n if (state.timer === nextStepTime) state.active = false;\n};\n\n/** Vertical offset to add to the camera, in world units. */\nexport const headBobOffset = (state: HeadBobState): number => Math.sin(state.timer * 10) * 1.5;\n","import { Quaternion, Vector3 } from \"three\";\n\n/** Set of currently-pressed key codes. */\nexport type KeyboardState = Record<string, boolean>;\n\nexport const createKeyboardState = (): KeyboardState => ({});\n\n/** Codes the controls consume; preventDefault'd so the arrows don't scroll. */\nconst MOVEMENT_KEYS = new Set([\n \"KeyW\",\n \"KeyA\",\n \"KeyS\",\n \"KeyD\",\n \"ArrowUp\",\n \"ArrowDown\",\n \"ArrowLeft\",\n \"ArrowRight\",\n]);\n\nexport const onKeyDown = (state: KeyboardState, event: KeyboardEvent): void => {\n state[event.code] = true;\n if (MOVEMENT_KEYS.has(event.code)) event.preventDefault();\n};\n\nexport const onKeyUp = (state: KeyboardState, event: KeyboardEvent): void => {\n state[event.code] = false;\n};\n\n/** Yaw turn rate for the Q/E keys, in radians per second. */\nconst KEY_TURN_SPEED = 1.8;\n\n/**\n * Turn a forward/strafe pair (each in `-1..1`) into a per-frame translation\n * oriented by the current yaw (`phi`). Shared by keyboard and touch input.\n */\nexport const movementFromAxis = (\n forwardVelocity: number,\n strafeVelocity: number,\n phi: number,\n delta: number,\n boosted = false,\n): Vector3 => {\n const factor = boosted ? 60 : 20;\n const yaw = new Quaternion().setFromAxisAngle(new Vector3(0, 1, 0), phi);\n\n const forward = new Vector3(0, 0, -10)\n .applyQuaternion(yaw)\n .multiplyScalar(forwardVelocity * delta * factor);\n const left = new Vector3(-10, 0, 0)\n .applyQuaternion(yaw)\n .multiplyScalar(strafeVelocity * delta * factor);\n\n return forward.add(left);\n};\n\n/**\n * Compute the per-frame translation from WASD or arrow-key input, oriented by\n * the current yaw (`phi`). Holding Shift acts as a speed booster.\n */\nexport const movementFromKeyboard = (state: KeyboardState, phi: number, delta: number): Vector3 => {\n const has = (...codes: string[]): number => (codes.some((c) => state[c]) ? 1 : 0);\n const forwardVelocity = has(\"KeyW\", \"ArrowUp\") - has(\"KeyS\", \"ArrowDown\");\n const strafeVelocity = has(\"KeyA\", \"ArrowLeft\") - has(\"KeyD\", \"ArrowRight\");\n const boosted = Boolean(state[\"ShiftLeft\"] || state[\"ShiftRight\"]);\n\n return movementFromAxis(forwardVelocity, strafeVelocity, phi, delta, boosted);\n};\n\n/**\n * Per-frame yaw delta (radians) from the Q/E turn keys. Q turns left\n * (increasing yaw), E turns right.\n */\nexport const yawFromKeyboard = (state: KeyboardState, delta: number): number => {\n const turn = (state[\"KeyQ\"] ? 1 : 0) - (state[\"KeyE\"] ? 1 : 0);\n return turn * KEY_TURN_SPEED * delta;\n};\n","import { Quaternion, Vector2, Vector3 } from \"three\";\n\n/** Mutable pointer state accumulated between frames. */\nexport interface MouseState {\n /** Whether the primary button is held (drag-to-look). */\n dragging: boolean;\n pos: Vector2;\n prevPos: Vector2;\n delta: Vector2;\n}\n\n/** Look angles driven by the pointer. */\nexport interface LookState {\n phi: number;\n theta: number;\n}\n\nconst PHI_SPEED = 8;\nconst THETA_SPEED = 5;\n\nexport const createMouseState = (): MouseState => ({\n dragging: false,\n pos: new Vector2(),\n prevPos: new Vector2(),\n delta: new Vector2(),\n});\n\nexport const clamp = (x: number, a: number, b: number): number => Math.min(Math.max(x, a), b);\n\n/** Maximum up/down look angle (radians) — keeps the camera from flipping over. */\nexport const MAX_PITCH = Math.PI / 3;\n\n/** Build the look quaternion from the current yaw/pitch angles. */\nexport const quaternionFromLook = (look: LookState): Quaternion => {\n const qy = new Quaternion().setFromAxisAngle(new Vector3(0, 1, 0), look.phi);\n const qx = new Quaternion().setFromAxisAngle(new Vector3(1, 0, 0), look.theta);\n return new Quaternion().multiply(qy).multiply(qx);\n};\n\nexport const onMouseDown = (state: MouseState, event: MouseEvent): void => {\n state.dragging = event.button === 0;\n};\n\nexport const onMouseUp = (state: MouseState): void => {\n state.dragging = false;\n};\n\nexport const onMouseMove = (state: MouseState, event: MouseEvent): void => {\n state.pos.set(event.pageX - window.innerWidth / 2, event.pageY - window.innerHeight / 2);\n state.delta.subVectors(state.pos, state.prevPos);\n};\n\nexport const commitMouseFrame = (state: MouseState): void => {\n state.prevPos.copy(state.pos);\n state.delta.set(0, 0);\n};\n\n/**\n * Integrate the pointer delta into the look angles. Mutates `look` in place.\n * Yaw is unbounded; pitch is clamped to avoid flipping over.\n */\nexport const updateLookFromMouse = (mouse: MouseState, look: LookState): void => {\n const xh = mouse.delta.x / window.innerWidth;\n const yh = mouse.delta.y / window.innerHeight;\n\n look.phi += -xh * PHI_SPEED;\n look.theta = clamp(look.theta + -yh * THETA_SPEED, -MAX_PITCH, MAX_PITCH);\n};\n","import { Vector3 } from \"three\";\n\nimport { movementFromAxis } from \"./keyboard\";\nimport { clamp, MAX_PITCH, type LookState } from \"./mouse\";\n\n/**\n * The shape of the shared analog input fpc reads (the kernel's `InputBus`): the\n * move/look axes a joystick feeds. Structural, so any producer with these fields\n * works. fpc interprets them as walking + turning; another camera might not.\n */\nexport interface AnalogInput {\n /** Forward (`+1`) / backward (`-1`) movement. */\n moveForward: number;\n /** Strafe right (`+1`) / left (`-1`). */\n moveStrafe: number;\n /** Look right (`+1`) / left (`-1`). */\n lookX: number;\n /** Look up (`+1`) / down (`-1`). */\n lookY: number;\n}\n\n/** Look turn rate driven by the joystick, in radians per second at full deflection. */\nconst LOOK_SPEED = 2.2;\n\n/** Per-frame translation from the analog move axes, oriented by yaw (`phi`). */\nexport const movementFromTouch = (state: AnalogInput, phi: number, delta: number): Vector3 => {\n if (state.moveForward === 0 && state.moveStrafe === 0) return new Vector3();\n // Strafe sign matches the keyboard helper (left strafe is +1 there).\n return movementFromAxis(state.moveForward, -state.moveStrafe, phi, delta);\n};\n\n/** Per-axis inversion of the analog look, for visitors who feel it backwards. */\nexport interface LookInvert {\n /** Invert the horizontal (turn) axis. */\n x: boolean;\n /** Invert the vertical (pitch) axis. */\n y: boolean;\n}\n\n/** Integrate the look joystick into the look angles, mutating `look` in place. */\nexport const updateLookFromTouch = (\n state: AnalogInput,\n look: LookState,\n delta: number,\n invert: LookInvert = { x: false, y: false },\n): void => {\n if (state.lookX === 0 && state.lookY === 0) return;\n const sx = invert.x ? -1 : 1;\n const sy = invert.y ? -1 : 1;\n look.phi -= state.lookX * sx * LOOK_SPEED * delta;\n look.theta = clamp(look.theta + state.lookY * sy * LOOK_SPEED * delta, -MAX_PITCH, MAX_PITCH);\n};\n","import { Quaternion, Vector3, type PerspectiveCamera } from \"three\";\nimport type { Vec2 } from \"@scenography/scene\";\nimport type { Collider } from \"@scenography/engine\";\n\nimport { resolveMove } from \"./collision\";\nimport { createHeadBobState, headBobOffset, updateHeadBob } from \"./headBob\";\nimport {\n createKeyboardState,\n movementFromKeyboard,\n onKeyDown,\n onKeyUp,\n yawFromKeyboard,\n} from \"./keyboard\";\nimport {\n commitMouseFrame,\n createMouseState,\n onMouseDown,\n onMouseMove,\n onMouseUp,\n quaternionFromLook,\n updateLookFromMouse,\n type LookState,\n} from \"./mouse\";\nimport { movementFromTouch, updateLookFromTouch, type AnalogInput, type LookInvert } from \"./touch\";\n\nexport interface FreeRoamOptions {\n /** Wall collision boxes the camera can't pass through. */\n colliders?: Collider[];\n /** Camera collision radius in world units. Defaults to `25`. */\n playerRadius?: number;\n /** Starting position. */\n position?: Vector3;\n /** Starting yaw in radians. */\n yaw?: number;\n /** Walkable-area polygon (XZ ring); the camera can't step outside it. */\n boundary?: Vec2[];\n /** Ground height at `(x, z)`; when set the camera follows the terrain. */\n groundHeight?: (x: number, z: number) => number;\n /** Eye height above the ground. Defaults to the start Y over the start ground. */\n eyeHeight?: number;\n}\n\n/**\n * First-person, free-roaming camera controls: drag-to-look with the mouse, WASD\n * to walk, a subtle head-bob while moving, wall collision and terrain following.\n * The shared analog input (`AnalogInput`, the kernel's InputBus) is folded in on\n * top of keyboard/mouse, so on-screen joysticks drive it too. It keeps the\n * camera upright (no roll) and clamps pitch — the first-person contract.\n *\n * Framework-agnostic: construct it with a camera, the canvas and the input,\n * call `attach()`, then `update(delta)` once per frame.\n */\nexport class FreeRoamControls {\n private colliders: Collider[];\n private readonly playerRadius: number;\n\n private readonly camera: PerspectiveCamera;\n private readonly domElement: HTMLElement;\n private readonly input: AnalogInput;\n private readonly mouse = createMouseState();\n private readonly keyboard = createKeyboardState();\n private readonly headBob = createHeadBobState();\n private readonly look: LookState = { phi: 0, theta: 0 };\n private rotation = new Quaternion();\n private translation = new Vector3(0, 170, 0);\n private enabled = true;\n private lookInvert: LookInvert = { x: false, y: false };\n private boundary: Vec2[] | undefined;\n private groundHeight: ((x: number, z: number) => number) | null;\n private readonly start: Vector3;\n private eyeHeight: number;\n\n private readonly handlers: Array<[EventTarget, string, EventListener]> = [];\n\n constructor(\n camera: PerspectiveCamera,\n domElement: HTMLElement,\n input: AnalogInput,\n options: FreeRoamOptions = {},\n ) {\n this.camera = camera;\n this.domElement = domElement;\n this.input = input;\n this.colliders = options.colliders ?? [];\n this.playerRadius = options.playerRadius ?? 25;\n if (options.position) this.translation.copy(options.position);\n if (options.yaw !== undefined) this.look.phi = options.yaw;\n this.rotation.setFromAxisAngle(new Vector3(0, 1, 0), this.look.phi);\n this.boundary = options.boundary;\n this.groundHeight = options.groundHeight ?? null;\n this.start = this.translation.clone();\n // Eye height = start Y above the ground under the start position.\n const groundAtStart = this.groundHeight?.(this.start.x, this.start.z) ?? 0;\n this.eyeHeight = options.eyeHeight ?? this.start.y - groundAtStart;\n }\n\n /** Current eye height above the ground (world units). */\n getEyeHeight(): number {\n return this.eyeHeight;\n }\n\n /** Fix the eye height above the ground (world units). */\n setEyeHeight(eyeHeight: number): void {\n this.eyeHeight = eyeHeight;\n }\n\n /**\n * Re-derive the default eye height (start Y over the ground under the start\n * position). Call once asynchronous ground data (a floor heightmap) has\n * loaded: the value captured at construction was measured against the\n * not-yet-displaced floor.\n */\n recalibrateEyeHeight(): void {\n const ground = this.groundHeight?.(this.start.x, this.start.z) ?? 0;\n this.eyeHeight = this.start.y - ground;\n }\n\n attach(): void {\n const bind = (target: EventTarget, type: string, listener: EventListener): void => {\n target.addEventListener(type, listener);\n this.handlers.push([target, type, listener]);\n };\n\n bind(this.domElement, \"mousedown\", (e) => onMouseDown(this.mouse, e as MouseEvent));\n bind(window, \"mousemove\", (e) => onMouseMove(this.mouse, e as MouseEvent));\n bind(window, \"mouseup\", () => onMouseUp(this.mouse));\n bind(window, \"keydown\", (e) => onKeyDown(this.keyboard, e as KeyboardEvent));\n bind(window, \"keyup\", (e) => onKeyUp(this.keyboard, e as KeyboardEvent));\n }\n\n detach(): void {\n for (const [target, type, listener] of this.handlers)\n target.removeEventListener(type, listener);\n this.handlers.length = 0;\n }\n\n /** Replace the wall collision set (e.g. after the scene's walls change). */\n setColliders(colliders: Collider[]): void {\n this.colliders = colliders;\n }\n\n /** Replace the walkable-area boundary polygon (or clear it with `undefined`). */\n setBoundary(boundary: Vec2[] | undefined): void {\n this.boundary = boundary;\n }\n\n /** Replace the ground-height function (or clear it to stop following terrain). */\n setGroundHeight(groundHeight: ((x: number, z: number) => number) | null): void {\n this.groundHeight = groundHeight;\n }\n\n /**\n * Enable/disable user input. While disabled, `update()` leaves the camera\n * untouched so another driver (e.g. a camera tween) can own it; re-enable after\n * syncing the pose with `setPose` so free-roam continues from there.\n */\n setEnabled(enabled: boolean): void {\n this.enabled = enabled;\n }\n\n /** Invert the analog look per axis (the visitor's control preference). */\n setLookInvert(x: boolean, y: boolean): void {\n this.lookInvert = { x, y };\n }\n\n /** Current pose: world position plus yaw (`phi`) and pitch (`theta`) in radians. */\n getPose(): { position: Vector3; phi: number; theta: number } {\n return { position: this.translation.clone(), phi: this.look.phi, theta: this.look.theta };\n }\n\n /** Force the pose (e.g. at the end of a camera tween) so input resumes from it. */\n setPose(position: Vector3, phi: number, theta: number): void {\n this.translation.copy(position);\n this.look.phi = phi;\n this.look.theta = theta;\n this.rotation = quaternionFromLook(this.look);\n }\n\n /** Advance the camera by `delta` seconds of input. */\n update(delta: number): void {\n // Disabled: another driver owns the camera this frame. Drop any accumulated\n // pointer delta so look doesn't jump when control is handed back.\n if (!this.enabled) {\n commitMouseFrame(this.mouse);\n return;\n }\n // Accumulate look from every source into the angles, then derive rotation.\n if (this.mouse.dragging) updateLookFromMouse(this.mouse, this.look);\n this.look.phi += yawFromKeyboard(this.keyboard, delta);\n updateLookFromTouch(this.input, this.look, delta, this.lookInvert);\n this.rotation = quaternionFromLook(this.look);\n\n const step = movementFromKeyboard(this.keyboard, this.look.phi, delta).add(\n movementFromTouch(this.input, this.look.phi, delta),\n );\n this.translation = resolveMove(\n this.translation,\n step,\n this.colliders,\n this.playerRadius,\n this.boundary,\n );\n // Follow the terrain: keep the eye a fixed height above the ground below.\n if (this.groundHeight) {\n this.translation.y = this.groundHeight(this.translation.x, this.translation.z) + this.eyeHeight;\n }\n if (step.x !== 0 || step.z !== 0) this.headBob.active = true;\n updateHeadBob(this.headBob, delta);\n\n this.camera.quaternion.copy(this.rotation);\n this.camera.position.copy(this.translation);\n this.camera.position.y += headBobOffset(this.headBob);\n\n const target = new Vector3(0, 0, -1)\n .applyQuaternion(this.rotation)\n .multiplyScalar(100)\n .add(this.translation);\n this.camera.lookAt(target);\n\n commitMouseFrame(this.mouse);\n }\n}\n","import {\n COLLIDERS,\n ROAM_CONTROL,\n TERRAIN,\n type Collider,\n type RoamControl,\n type SystemPlugin,\n type TerrainCapability,\n} from \"@scenography/engine\";\nimport type { Scene } from \"@scenography/scene\";\nimport { Vector3 } from \"three\";\n\nimport { FreeRoamControls } from \"./FreeRoamControls\";\n\n/**\n * First-person camera — a *system* plugin. It drives the kernel camera from\n * WASD/drag plus the shared analog input (joysticks), keeps it at `eyeHeight`\n * over the terrain, and blocks it against wall colliders. Consumes the `terrain`\n * and `colliders` capabilities (live, so it tracks async heightmaps and hot wall\n * edits) and publishes `roamControl` so a framing/tour system can suspend it.\n */\nexport const fpc = (): SystemPlugin => ({\n name: \"fpc\",\n setup: (ctx) => {\n const { camera, canvas, input, capabilities, scene } = ctx;\n const terrainHeight = (x: number, z: number): number =>\n capabilities.get<TerrainCapability>(TERRAIN)?.heightAt(x, z) ?? 0;\n // Follow terrain only when some plugin provides it; otherwise keep a fixed Y.\n const groundHeight = (): ((x: number, z: number) => number) | undefined =>\n capabilities.get<TerrainCapability>(TERRAIN) ? terrainHeight : undefined;\n\n const start = scene.camera.start;\n const controls = new FreeRoamControls(camera, canvas, input, {\n position: new Vector3(start.position[0], start.position[1], start.position[2]),\n yaw: start.yaw,\n boundary: scene.boundary,\n eyeHeight: scene.camera.eyeHeight,\n groundHeight: groundHeight(),\n });\n controls.attach();\n capabilities.provide(ROAM_CONTROL, controls satisfies RoamControl);\n\n // Eye height: an explicit `camera.eyeHeight` wins. Otherwise it was derived\n // from the start Y over the ground — but a floor heightmap loads async, so\n // recalibrate once the terrain is real, or the visitor walks in the air.\n const calibrateEyeHeight = (s: Scene): void => {\n if (s.camera.eyeHeight !== undefined) controls.setEyeHeight(s.camera.eyeHeight);\n else {\n const terrain = capabilities.get<TerrainCapability>(TERRAIN);\n if (terrain) void terrain.ready.then(() => controls.recalibrateEyeHeight());\n }\n };\n calibrateEyeHeight(scene);\n\n return {\n update: (deltaMs) => {\n const flags = ctx.controlFlags();\n controls.setLookInvert(flags.invertLookX, flags.invertLookY);\n controls.setColliders(capabilities.get<Collider[]>(COLLIDERS) ?? []);\n controls.update(deltaMs / 1000);\n },\n setScene: (s) => {\n controls.setBoundary(s.boundary);\n controls.setGroundHeight(groundHeight() ?? null);\n calibrateEyeHeight(s);\n },\n dispose: () => {\n controls.detach();\n capabilities.revoke(ROAM_CONTROL);\n },\n };\n },\n});\n"],"mappings":"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"}
|
package/package.json
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "@scenography/plugin-fpc",
|
|
3
|
-
"version": "0.
|
|
3
|
+
"version": "0.3.1",
|
|
4
4
|
"description": "First-person camera plugin for scenography: WASD + drag/touch free-roam with head-bob, wall collision and terrain following. A system plugin that drives the kernel camera.",
|
|
5
5
|
"license": "MIT",
|
|
6
6
|
"files": [
|
|
@@ -23,16 +23,16 @@
|
|
|
23
23
|
}
|
|
24
24
|
},
|
|
25
25
|
"dependencies": {
|
|
26
|
-
"@scenography/scene": "0.
|
|
26
|
+
"@scenography/scene": "0.3.1"
|
|
27
27
|
},
|
|
28
28
|
"devDependencies": {
|
|
29
29
|
"@types/three": "^0.160.0",
|
|
30
30
|
"three": "^0.160.0",
|
|
31
|
-
"@scenography/engine": "0.
|
|
31
|
+
"@scenography/engine": "0.3.1"
|
|
32
32
|
},
|
|
33
33
|
"peerDependencies": {
|
|
34
34
|
"three": ">=0.160.0",
|
|
35
|
-
"@scenography/engine": "0.
|
|
35
|
+
"@scenography/engine": "0.3.1"
|
|
36
36
|
},
|
|
37
37
|
"scripts": {
|
|
38
38
|
"build": "tsdown",
|