@waica/engine 0.28.0 → 0.29.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/README.md +29 -8
- package/dist/anchored-pieces.js +21 -5
- package/dist/archetype.d.ts +15 -0
- package/dist/archetype.js +41 -1
- package/dist/camera-projection.d.ts +6 -5
- package/dist/camera-projection.js +8 -7
- package/dist/camera-rig.d.ts +32 -3
- package/dist/camera-rig.js +75 -1
- package/dist/components/animator.d.ts +6 -0
- package/dist/components/animator.js +12 -0
- package/dist/entity-update-schedules.d.ts +21 -0
- package/dist/entity-update-schedules.js +67 -0
- package/dist/game.d.ts +13 -11
- package/dist/game.js +40 -74
- package/dist/index.d.ts +7 -2
- package/dist/index.js +2 -0
- package/dist/input-mouse.d.ts +83 -0
- package/dist/input-mouse.js +205 -0
- package/dist/input.d.ts +26 -5
- package/dist/input.js +58 -10
- package/dist/orbit-camera.d.ts +68 -0
- package/dist/orbit-camera.js +92 -0
- package/dist/physics-3d/world-queries.d.ts +10 -1
- package/dist/physics-3d/world-queries.js +6 -2
- package/dist/runtime-bridge.d.ts +12 -2
- package/dist/runtime-bridge.js +12 -1
- package/dist/runtime-inspection.js +3 -2
- package/dist/runtime-view-snapshot.d.ts +5 -8
- package/dist/runtime-view-snapshot.js +13 -10
- package/dist/scene-camera-3d.d.ts +14 -15
- package/dist/scene-camera-3d.js +14 -10
- package/dist/scene-camera-orbit.d.ts +72 -0
- package/dist/scene-camera-orbit.js +155 -0
- package/dist/scene-field-issues.d.ts +16 -0
- package/dist/scene-field-issues.js +11 -0
- package/dist/scene.js +5 -3
- package/dist/spatial-query-3d.d.ts +2 -2
- package/dist/spatial-query-3d.js +13 -4
- package/package.json +1 -1
package/dist/input.d.ts
CHANGED
|
@@ -1,6 +1,7 @@
|
|
|
1
|
+
import { type MouseInputOptions, type MouseMovement } from './input-mouse.js';
|
|
1
2
|
export type ActionName = string;
|
|
2
3
|
export type InjectedActionOperation = 'press' | 'hold' | 'release';
|
|
3
|
-
/** Action → source codes: `KeyboardEvent.code` values
|
|
4
|
+
/** Action → source codes: `KeyboardEvent.code` values, `Gamepad:<control>` codes (ADR 0023) and `Mouse:Left`/`Mouse:Right`. */
|
|
4
5
|
export type InputBindings = Record<string, string[]>;
|
|
5
6
|
/** Neutral engine baseline; archetypes own their action vocabulary. */
|
|
6
7
|
export declare const DEFAULT_BINDINGS: Readonly<InputBindings>;
|
|
@@ -15,14 +16,16 @@ export type InjectedValueCheck = {
|
|
|
15
16
|
* takes a value, a finite number in (0, 1]; an absent value always passes.
|
|
16
17
|
*/
|
|
17
18
|
export declare function checkInjectedValue(operation: InjectedActionOperation, value: unknown): InjectedValueCheck;
|
|
18
|
-
export interface InputOptions {
|
|
19
|
+
export interface InputOptions extends MouseInputOptions {
|
|
19
20
|
/** Radial stick dead zone in [0, 1); absent or invalid means 0.2. */
|
|
20
21
|
gamepadDeadZone?: number;
|
|
21
22
|
}
|
|
22
23
|
/**
|
|
23
|
-
* Action-based input with archetype default bindings. Sources are keys
|
|
24
|
-
* player 1's standard gamepad (ADR 0023)
|
|
25
|
-
*
|
|
24
|
+
* Action-based input with archetype default bindings. Sources are keys,
|
|
25
|
+
* player 1's standard gamepad (ADR 0023) and the mouse buttons on the game
|
|
26
|
+
* canvas; every Action has a 0..1 value, the max across its sources, and is
|
|
27
|
+
* held at 0.5 or above. The mouse also reports its locked movement and the
|
|
28
|
+
* wheel per Simulation Step (issue #181). Touch is still TODO.
|
|
26
29
|
*/
|
|
27
30
|
export declare class Input {
|
|
28
31
|
private readonly bindings;
|
|
@@ -40,6 +43,8 @@ export declare class Input {
|
|
|
40
43
|
private readonly disarmed;
|
|
41
44
|
/** Actions held when the last step closed: a press is a step that starts from none of them (CA-5). */
|
|
42
45
|
private heldAtStepEnd;
|
|
46
|
+
/** Mouse buttons press `Mouse:` codes into the same sets keys use, so they behave exactly as keys. */
|
|
47
|
+
private readonly mouse;
|
|
43
48
|
/** Installs exactly the action map supplied by the active archetype/project. */
|
|
44
49
|
constructor(bindings?: Readonly<InputBindings>, options?: InputOptions);
|
|
45
50
|
/** Is the action held this frame (some source at 0.5 or above)? */
|
|
@@ -84,6 +89,20 @@ export declare class Input {
|
|
|
84
89
|
* before component updates.
|
|
85
90
|
*/
|
|
86
91
|
pollGamepad(): void;
|
|
92
|
+
/** Freezes the mouse movement and wheel since the last step; the Game calls this beside pollGamepad(). */
|
|
93
|
+
pollMouse(): void;
|
|
94
|
+
/** The mouse movement of this Simulation Step while the pointer is locked to the canvas, in CSS px; zero between steps. */
|
|
95
|
+
mouseMovement(): MouseMovement;
|
|
96
|
+
/** The wheel delta of this Simulation Step over the canvas, in CSS px (positive scrolls down); zero between steps. */
|
|
97
|
+
wheelDelta(): number;
|
|
98
|
+
/** Adds a mouse movement to the next step as if the pointer were locked (the Runtime Bridge's `look`). */
|
|
99
|
+
injectLook(dx: number, dy: number): void;
|
|
100
|
+
/** Gives the pointer back if it is locked to the game canvas; a lock elsewhere is not the game's. */
|
|
101
|
+
releasePointerLock(): void;
|
|
102
|
+
/** Asks the browser to lock the pointer to the game canvas. */
|
|
103
|
+
requestPointerLock(): void;
|
|
104
|
+
/** Is the pointer locked to the game canvas? */
|
|
105
|
+
get pointerLocked(): boolean;
|
|
87
106
|
/**
|
|
88
107
|
* Marks this frame's press of the action as spent, so consumers that
|
|
89
108
|
* honor consumption (state-machine 'input:' triggers) ignore it. One
|
|
@@ -102,5 +121,7 @@ export declare class Input {
|
|
|
102
121
|
private releaseAll;
|
|
103
122
|
private onVisibilityChange;
|
|
104
123
|
private onKeyDown;
|
|
124
|
+
/** A key or mouse button went down. */
|
|
125
|
+
private press;
|
|
105
126
|
private onKeyUp;
|
|
106
127
|
}
|
package/dist/input.js
CHANGED
|
@@ -1,4 +1,5 @@
|
|
|
1
1
|
import { ACTION_HELD_THRESHOLD, connectedGamepads, GAMEPAD_CODE_PREFIX, gamepadControl, gamepadValues, GamepadSlot, resolveGamepadDeadZone, } from './gamepad.js';
|
|
2
|
+
import { isUnknownMouseCode, MouseInput } from './input-mouse.js';
|
|
2
3
|
/** Neutral engine baseline; archetypes own their action vocabulary. */
|
|
3
4
|
export const DEFAULT_BINDINGS = {};
|
|
4
5
|
/**
|
|
@@ -16,9 +17,11 @@ export function checkInjectedValue(operation, value) {
|
|
|
16
17
|
return { value };
|
|
17
18
|
}
|
|
18
19
|
/**
|
|
19
|
-
* Action-based input with archetype default bindings. Sources are keys
|
|
20
|
-
* player 1's standard gamepad (ADR 0023)
|
|
21
|
-
*
|
|
20
|
+
* Action-based input with archetype default bindings. Sources are keys,
|
|
21
|
+
* player 1's standard gamepad (ADR 0023) and the mouse buttons on the game
|
|
22
|
+
* canvas; every Action has a 0..1 value, the max across its sources, and is
|
|
23
|
+
* held at 0.5 or above. The mouse also reports its locked movement and the
|
|
24
|
+
* wheel per Simulation Step (issue #181). Touch is still TODO.
|
|
22
25
|
*/
|
|
23
26
|
export class Input {
|
|
24
27
|
bindings = new Map();
|
|
@@ -36,17 +39,25 @@ export class Input {
|
|
|
36
39
|
disarmed = new Set();
|
|
37
40
|
/** Actions held when the last step closed: a press is a step that starts from none of them (CA-5). */
|
|
38
41
|
heldAtStepEnd = new Set();
|
|
42
|
+
/** Mouse buttons press `Mouse:` codes into the same sets keys use, so they behave exactly as keys. */
|
|
43
|
+
mouse;
|
|
39
44
|
/** Installs exactly the action map supplied by the active archetype/project. */
|
|
40
45
|
constructor(bindings = DEFAULT_BINDINGS, options = {}) {
|
|
41
46
|
const warned = new Set();
|
|
42
47
|
for (const [action, codes] of Object.entries(bindings)) {
|
|
43
48
|
this.bindings.set(action, new Set(codes));
|
|
44
49
|
for (const code of codes)
|
|
45
|
-
|
|
50
|
+
warnUnknownCode(code, warned);
|
|
46
51
|
}
|
|
47
52
|
this.deadZone = resolveGamepadDeadZone(options.gamepadDeadZone, (message) => {
|
|
48
53
|
console.warn(message);
|
|
49
54
|
});
|
|
55
|
+
const keyLike = {
|
|
56
|
+
press: (code) => this.press(code),
|
|
57
|
+
release: (code) => this.down.delete(code),
|
|
58
|
+
binds: (code) => [...this.bindings.values()].some((codes) => codes.has(code)),
|
|
59
|
+
};
|
|
60
|
+
this.mouse = new MouseInput(keyLike, options);
|
|
50
61
|
window.addEventListener('keydown', this.onKeyDown);
|
|
51
62
|
window.addEventListener('keyup', this.onKeyUp);
|
|
52
63
|
window.addEventListener('blur', this.releaseAll);
|
|
@@ -161,6 +172,34 @@ export class Input {
|
|
|
161
172
|
}
|
|
162
173
|
this.padValues = raw;
|
|
163
174
|
}
|
|
175
|
+
/** Freezes the mouse movement and wheel since the last step; the Game calls this beside pollGamepad(). */
|
|
176
|
+
pollMouse() {
|
|
177
|
+
this.mouse.poll();
|
|
178
|
+
}
|
|
179
|
+
/** The mouse movement of this Simulation Step while the pointer is locked to the canvas, in CSS px; zero between steps. */
|
|
180
|
+
mouseMovement() {
|
|
181
|
+
return this.mouse.movement();
|
|
182
|
+
}
|
|
183
|
+
/** The wheel delta of this Simulation Step over the canvas, in CSS px (positive scrolls down); zero between steps. */
|
|
184
|
+
wheelDelta() {
|
|
185
|
+
return this.mouse.wheelDelta();
|
|
186
|
+
}
|
|
187
|
+
/** Adds a mouse movement to the next step as if the pointer were locked (the Runtime Bridge's `look`). */
|
|
188
|
+
injectLook(dx, dy) {
|
|
189
|
+
this.mouse.inject(dx, dy);
|
|
190
|
+
}
|
|
191
|
+
/** Gives the pointer back if it is locked to the game canvas; a lock elsewhere is not the game's. */
|
|
192
|
+
releasePointerLock() {
|
|
193
|
+
this.mouse.releaseLock();
|
|
194
|
+
}
|
|
195
|
+
/** Asks the browser to lock the pointer to the game canvas. */
|
|
196
|
+
requestPointerLock() {
|
|
197
|
+
this.mouse.requestLock();
|
|
198
|
+
}
|
|
199
|
+
/** Is the pointer locked to the game canvas? */
|
|
200
|
+
get pointerLocked() {
|
|
201
|
+
return this.mouse.locked;
|
|
202
|
+
}
|
|
164
203
|
/**
|
|
165
204
|
* Marks this frame's press of the action as spent, so consumers that
|
|
166
205
|
* honor consumption (state-machine 'input:' triggers) ignore it. One
|
|
@@ -180,12 +219,14 @@ export class Input {
|
|
|
180
219
|
this.injectedJustDown.clear();
|
|
181
220
|
this.injectedPresses.clear();
|
|
182
221
|
this.used.clear();
|
|
222
|
+
this.mouse.endStep();
|
|
183
223
|
this.heldAtStepEnd = new Set(this.heldActions());
|
|
184
224
|
}
|
|
185
225
|
dispose() {
|
|
186
226
|
this.injectedHolds.clear();
|
|
187
227
|
this.injectedJustDown.clear();
|
|
188
228
|
this.injectedPresses.clear();
|
|
229
|
+
this.mouse.dispose();
|
|
189
230
|
window.removeEventListener('keydown', this.onKeyDown);
|
|
190
231
|
window.removeEventListener('keyup', this.onKeyUp);
|
|
191
232
|
window.removeEventListener('blur', this.releaseAll);
|
|
@@ -220,17 +261,24 @@ export class Input {
|
|
|
220
261
|
onKeyDown = (e) => {
|
|
221
262
|
if (e.repeat)
|
|
222
263
|
return;
|
|
223
|
-
this.
|
|
224
|
-
this.justDown.add(e.code);
|
|
264
|
+
this.press(e.code);
|
|
225
265
|
};
|
|
266
|
+
/** A key or mouse button went down. */
|
|
267
|
+
press(code) {
|
|
268
|
+
this.down.add(code);
|
|
269
|
+
this.justDown.add(code);
|
|
270
|
+
}
|
|
226
271
|
onKeyUp = (e) => {
|
|
227
272
|
this.down.delete(e.code);
|
|
228
273
|
};
|
|
229
274
|
}
|
|
230
|
-
/** An unknown `Gamepad:` code never matches; it is reported once per code per Input. */
|
|
231
|
-
function
|
|
232
|
-
if (
|
|
275
|
+
/** An unknown `Gamepad:` or `Mouse:` code never matches; it is reported once per code per Input. */
|
|
276
|
+
function warnUnknownCode(code, warned) {
|
|
277
|
+
if (warned.has(code))
|
|
278
|
+
return;
|
|
279
|
+
const unknownPad = code.startsWith(GAMEPAD_CODE_PREFIX) && !gamepadControl(code);
|
|
280
|
+
if (!unknownPad && !isUnknownMouseCode(code))
|
|
233
281
|
return;
|
|
234
282
|
warned.add(code);
|
|
235
|
-
console.warn(`[waica] unknown gamepad binding "${code}": it never matches.`);
|
|
283
|
+
console.warn(`[waica] unknown ${unknownPad ? 'gamepad' : 'mouse'} binding "${code}": it never matches.`);
|
|
236
284
|
}
|
|
@@ -0,0 +1,68 @@
|
|
|
1
|
+
import type * as THREE from 'three/webgpu';
|
|
2
|
+
import type { Entity } from './entity.js';
|
|
3
|
+
import type { Input } from './input.js';
|
|
4
|
+
import type { ResolvedOrbit } from './scene-camera-orbit.js';
|
|
5
|
+
/** How far in front of what blocks the view the camera stops, and how close to the look point it may come (CA-9). */
|
|
6
|
+
export declare const ORBIT_COLLISION_MARGIN = 0.2;
|
|
7
|
+
export declare const ORBIT_MIN_DISTANCE = 0.5;
|
|
8
|
+
/** World units of distance per CSS pixel of wheel (one ~100 px notch is one unit). */
|
|
9
|
+
export declare const ORBIT_WHEEL_ZOOM = 0.01;
|
|
10
|
+
/** A point or direction in world space. */
|
|
11
|
+
export interface OrbitPoint {
|
|
12
|
+
x: number;
|
|
13
|
+
y: number;
|
|
14
|
+
z: number;
|
|
15
|
+
}
|
|
16
|
+
/** The ray from the look point toward the camera, which skips the followed entity's own colliders. */
|
|
17
|
+
export interface OrbitRay {
|
|
18
|
+
origin: OrbitPoint;
|
|
19
|
+
direction: OrbitPoint;
|
|
20
|
+
maxDistance: number;
|
|
21
|
+
exclude: Entity;
|
|
22
|
+
}
|
|
23
|
+
/** What orbiting reads from the Game each Simulation Step. */
|
|
24
|
+
export interface OrbitHost {
|
|
25
|
+
find(name: string): Entity | undefined;
|
|
26
|
+
input(): Input;
|
|
27
|
+
/** The distance to the first fixed collider along the ray (level geometry: sensors, characters and dynamic bodies never block it); null when nothing is hit. */
|
|
28
|
+
rayDistance(ray: OrbitRay): number | null;
|
|
29
|
+
}
|
|
30
|
+
/** The orbit's state as the Runtime Snapshot's `view` reports it (CA-10). */
|
|
31
|
+
export interface OrbitView {
|
|
32
|
+
follow: string;
|
|
33
|
+
yaw: number;
|
|
34
|
+
pitch: number;
|
|
35
|
+
distance: number;
|
|
36
|
+
distanceActual: number;
|
|
37
|
+
}
|
|
38
|
+
/**
|
|
39
|
+
* A perspective camera orbiting a followed entity (issue #181, ADR 0031): the
|
|
40
|
+
* locked mouse, the orbit Actions and the wheel turn and zoom it once per
|
|
41
|
+
* Simulation Step, and a ray from the look point pulls it in front of a wall
|
|
42
|
+
* without changing the chosen distance. With no input the orbit holds.
|
|
43
|
+
*/
|
|
44
|
+
export declare class OrbitCamera {
|
|
45
|
+
readonly settings: ResolvedOrbit;
|
|
46
|
+
yaw: number;
|
|
47
|
+
pitch: number;
|
|
48
|
+
/** The chosen distance: the wheel's, clamped to [minDistance, maxDistance]. */
|
|
49
|
+
distance: number;
|
|
50
|
+
/** Where the camera actually sits: the chosen distance, or less in front of a wall. */
|
|
51
|
+
distanceActual: number;
|
|
52
|
+
constructor(settings: ResolvedOrbit);
|
|
53
|
+
/** One Simulation Step: turn and zoom by this step's input, then place `camera`. No followed entity: nothing moves, and false. */
|
|
54
|
+
step(camera: THREE.PerspectiveCamera, host: OrbitHost, dt: number): boolean;
|
|
55
|
+
/** Places `camera` on the orbit as it stands, with no input (a scene load); false, and nothing moves, with no followed entity. */
|
|
56
|
+
place(camera: THREE.PerspectiveCamera, host: OrbitHost): boolean;
|
|
57
|
+
/** The camera at the chosen distance from the look point, or in front of what the ray hits (CA-9). */
|
|
58
|
+
private placeAround;
|
|
59
|
+
/** The orbit for the Runtime Snapshot's view. */
|
|
60
|
+
view(): OrbitView;
|
|
61
|
+
/** The step's locked mouse movement times `sensitivity`, plus the orbit Actions at `orbitSpeed`; pitch clamped. */
|
|
62
|
+
private turn;
|
|
63
|
+
/** The orbit Actions as -1..1 turns: right minus left for yaw, down minus up for pitch. */
|
|
64
|
+
private actionTurn;
|
|
65
|
+
private lookPoint;
|
|
66
|
+
/** The unit direction from the look point to the camera: yaw 0 puts it on +z, yaw 90 on -x. */
|
|
67
|
+
private awayFromLook;
|
|
68
|
+
}
|
|
@@ -0,0 +1,92 @@
|
|
|
1
|
+
/** How far in front of what blocks the view the camera stops, and how close to the look point it may come (CA-9). */
|
|
2
|
+
export const ORBIT_COLLISION_MARGIN = 0.2;
|
|
3
|
+
export const ORBIT_MIN_DISTANCE = 0.5;
|
|
4
|
+
/** World units of distance per CSS pixel of wheel (one ~100 px notch is one unit). */
|
|
5
|
+
export const ORBIT_WHEEL_ZOOM = 0.01;
|
|
6
|
+
const RAD = Math.PI / 180;
|
|
7
|
+
const clamp = (value, low, high) => Math.min(Math.max(value, low), high);
|
|
8
|
+
/** A yaw in degrees wrapped to (-180, 180], so two equal views report the same number. */
|
|
9
|
+
const wrapYaw = (degrees) => degrees - 360 * Math.ceil((degrees - 180) / 360);
|
|
10
|
+
/**
|
|
11
|
+
* A perspective camera orbiting a followed entity (issue #181, ADR 0031): the
|
|
12
|
+
* locked mouse, the orbit Actions and the wheel turn and zoom it once per
|
|
13
|
+
* Simulation Step, and a ray from the look point pulls it in front of a wall
|
|
14
|
+
* without changing the chosen distance. With no input the orbit holds.
|
|
15
|
+
*/
|
|
16
|
+
export class OrbitCamera {
|
|
17
|
+
settings;
|
|
18
|
+
yaw;
|
|
19
|
+
pitch;
|
|
20
|
+
/** The chosen distance: the wheel's, clamped to [minDistance, maxDistance]. */
|
|
21
|
+
distance;
|
|
22
|
+
/** Where the camera actually sits: the chosen distance, or less in front of a wall. */
|
|
23
|
+
distanceActual;
|
|
24
|
+
constructor(settings) {
|
|
25
|
+
this.settings = settings;
|
|
26
|
+
this.yaw = wrapYaw(settings.yaw);
|
|
27
|
+
this.pitch = clamp(settings.pitch, settings.minPitch, settings.maxPitch);
|
|
28
|
+
this.distance = clamp(settings.distance, settings.minDistance, settings.maxDistance);
|
|
29
|
+
this.distanceActual = this.distance;
|
|
30
|
+
}
|
|
31
|
+
/** One Simulation Step: turn and zoom by this step's input, then place `camera`. No followed entity: nothing moves, and false. */
|
|
32
|
+
step(camera, host, dt) {
|
|
33
|
+
const followed = host.find(this.settings.follow);
|
|
34
|
+
if (!followed)
|
|
35
|
+
return false;
|
|
36
|
+
this.turn(host.input(), dt);
|
|
37
|
+
this.distance = clamp(this.distance + host.input().wheelDelta() * ORBIT_WHEEL_ZOOM, this.settings.minDistance, this.settings.maxDistance);
|
|
38
|
+
this.placeAround(camera, host, followed);
|
|
39
|
+
return true;
|
|
40
|
+
}
|
|
41
|
+
/** Places `camera` on the orbit as it stands, with no input (a scene load); false, and nothing moves, with no followed entity. */
|
|
42
|
+
place(camera, host) {
|
|
43
|
+
const followed = host.find(this.settings.follow);
|
|
44
|
+
if (!followed)
|
|
45
|
+
return false;
|
|
46
|
+
this.placeAround(camera, host, followed);
|
|
47
|
+
return true;
|
|
48
|
+
}
|
|
49
|
+
/** The camera at the chosen distance from the look point, or in front of what the ray hits (CA-9). */
|
|
50
|
+
placeAround(camera, host, followed) {
|
|
51
|
+
const look = this.lookPoint(followed);
|
|
52
|
+
const away = this.awayFromLook();
|
|
53
|
+
const hit = host.rayDistance({ origin: look, direction: away, maxDistance: this.distance, exclude: followed });
|
|
54
|
+
this.distanceActual =
|
|
55
|
+
hit === null ? this.distance : Math.min(this.distance, Math.max(hit - ORBIT_COLLISION_MARGIN, ORBIT_MIN_DISTANCE));
|
|
56
|
+
camera.position.set(look.x + away.x * this.distanceActual, look.y + away.y * this.distanceActual, look.z + away.z * this.distanceActual);
|
|
57
|
+
camera.lookAt(look.x, look.y, look.z);
|
|
58
|
+
camera.updateMatrixWorld();
|
|
59
|
+
}
|
|
60
|
+
/** The orbit for the Runtime Snapshot's view. */
|
|
61
|
+
view() {
|
|
62
|
+
const { yaw, pitch, distance, distanceActual } = this;
|
|
63
|
+
return { follow: this.settings.follow, yaw, pitch, distance, distanceActual };
|
|
64
|
+
}
|
|
65
|
+
/** The step's locked mouse movement times `sensitivity`, plus the orbit Actions at `orbitSpeed`; pitch clamped. */
|
|
66
|
+
turn(input, dt) {
|
|
67
|
+
const { sensitivity, orbitSpeed, invertY, minPitch, maxPitch } = this.settings;
|
|
68
|
+
const mouse = input.mouseMovement();
|
|
69
|
+
const actions = this.actionTurn(input);
|
|
70
|
+
this.yaw = wrapYaw(this.yaw + mouse.x * sensitivity + actions.yaw * orbitSpeed * dt);
|
|
71
|
+
const mousePitch = (invertY ? -mouse.y : mouse.y) * sensitivity;
|
|
72
|
+
this.pitch = clamp(this.pitch + mousePitch + actions.pitch * orbitSpeed * dt, minPitch, maxPitch);
|
|
73
|
+
}
|
|
74
|
+
/** The orbit Actions as -1..1 turns: right minus left for yaw, down minus up for pitch. */
|
|
75
|
+
actionTurn(input) {
|
|
76
|
+
const { orbitLeftAction, orbitRightAction, orbitUpAction, orbitDownAction } = this.settings;
|
|
77
|
+
return {
|
|
78
|
+
yaw: input.value(orbitRightAction) - input.value(orbitLeftAction),
|
|
79
|
+
pitch: input.value(orbitDownAction) - input.value(orbitUpAction),
|
|
80
|
+
};
|
|
81
|
+
}
|
|
82
|
+
lookPoint(followed) {
|
|
83
|
+
const { x, y, z } = followed.position;
|
|
84
|
+
return { x, y: y + this.settings.lookHeight, z };
|
|
85
|
+
}
|
|
86
|
+
/** The unit direction from the look point to the camera: yaw 0 puts it on +z, yaw 90 on -x. */
|
|
87
|
+
awayFromLook() {
|
|
88
|
+
const yaw = this.yaw * RAD;
|
|
89
|
+
const pitch = this.pitch * RAD;
|
|
90
|
+
return { x: -Math.sin(yaw) * Math.cos(pitch), y: Math.sin(pitch), z: Math.cos(yaw) * Math.cos(pitch) };
|
|
91
|
+
}
|
|
92
|
+
}
|
|
@@ -6,15 +6,24 @@ export interface Point3d {
|
|
|
6
6
|
y: number;
|
|
7
7
|
z: number;
|
|
8
8
|
}
|
|
9
|
-
/**
|
|
9
|
+
/**
|
|
10
|
+
* Which colliders `game.query.area` reads: the sensors (the default, ADR
|
|
11
|
+
* 0015's trigger domain) or every collider, solid ones included — what a
|
|
12
|
+
* melee strike hits (issue #181).
|
|
13
|
+
*/
|
|
14
|
+
export type QueryColliders3d = 'sensors' | 'all';
|
|
15
|
+
/** A volume `game.query.area` tests colliders against; a box may turn by `yaw` degrees about +y. */
|
|
10
16
|
export type QueryVolume3d = {
|
|
11
17
|
shape: 'box';
|
|
12
18
|
center: Point3d;
|
|
13
19
|
size: Point3d;
|
|
20
|
+
yaw?: number;
|
|
21
|
+
colliders?: QueryColliders3d;
|
|
14
22
|
} | {
|
|
15
23
|
shape: 'sphere';
|
|
16
24
|
center: Point3d;
|
|
17
25
|
radius: number;
|
|
26
|
+
colliders?: QueryColliders3d;
|
|
18
27
|
};
|
|
19
28
|
/** One collider a ray reaches: whose it is, how far, where and which way its surface faces. */
|
|
20
29
|
export interface WorldRayHit {
|
|
@@ -1,4 +1,8 @@
|
|
|
1
|
-
|
|
1
|
+
/** The rotation of a box turned `yaw` degrees about +y (none for a sphere or an unturned box). */
|
|
2
|
+
function rotationOf(volume) {
|
|
3
|
+
const half = volume.shape === 'box' ? ((volume.yaw ?? 0) * Math.PI) / 360 : 0;
|
|
4
|
+
return { x: 0, y: Math.sin(half), z: 0, w: Math.cos(half) };
|
|
5
|
+
}
|
|
2
6
|
/**
|
|
3
7
|
* Rapier's predicate for a query: only colliders whose body record `accepts`.
|
|
4
8
|
* It runs inside Rapier's borrow of the world, so `accepts` must be a plain
|
|
@@ -42,7 +46,7 @@ export function recordsInVolume(world, volume, accepts) {
|
|
|
42
46
|
? new R.Cuboid(volume.size.x / 2, volume.size.y / 2, volume.size.z / 2)
|
|
43
47
|
: new R.Ball(volume.radius);
|
|
44
48
|
const found = [];
|
|
45
|
-
raw.intersectionsWithShape(volume.center,
|
|
49
|
+
raw.intersectionsWithShape(volume.center, rotationOf(volume), shape, (collider) => {
|
|
46
50
|
const record = world.recordOfCollider(collider.handle);
|
|
47
51
|
if (record)
|
|
48
52
|
found.push(record);
|
package/dist/runtime-bridge.d.ts
CHANGED
|
@@ -25,9 +25,10 @@ export type RuntimeMode = 'paused' | 'real-time';
|
|
|
25
25
|
* 0025) announces that every metadata carries `backend`: the Render Backend
|
|
26
26
|
* once the renderer is ready, null before — a Run Session waits for it — and
|
|
27
27
|
* that a renderer that fails to initialize reports through the activation's
|
|
28
|
-
* `fail`.
|
|
28
|
+
* `fail`. `look` (issue #181) announces the `look` operation: a mouse delta
|
|
29
|
+
* the next Simulation Step reads as locked mouse movement.
|
|
29
30
|
*/
|
|
30
|
-
export declare const RUNTIME_BRIDGE_CAPABILITIES: readonly ['click', 'scene', 'fixed-step', 'assets', 'camera-effects', 'analog-actions', 'render-backend'];
|
|
31
|
+
export declare const RUNTIME_BRIDGE_CAPABILITIES: readonly ['click', 'scene', 'fixed-step', 'assets', 'camera-effects', 'analog-actions', 'render-backend', 'look'];
|
|
31
32
|
export interface RuntimeMetadata {
|
|
32
33
|
bridgeVersion: typeof RUNTIME_BRIDGE_PROTOCOL_VERSION;
|
|
33
34
|
engineVersion: string;
|
|
@@ -63,6 +64,12 @@ export type RuntimeControlRequest = {
|
|
|
63
64
|
} | {
|
|
64
65
|
operation: 'scene';
|
|
65
66
|
scene: string;
|
|
67
|
+
}
|
|
68
|
+
/** Adds `dx`, `dy` CSS px of mouse movement to the next Simulation Step, as if the pointer were locked. */
|
|
69
|
+
| {
|
|
70
|
+
operation: 'look';
|
|
71
|
+
dx: number;
|
|
72
|
+
dy: number;
|
|
66
73
|
};
|
|
67
74
|
export interface RuntimeControlResult extends RuntimeMetadata {
|
|
68
75
|
heldActions: string[];
|
|
@@ -108,6 +115,8 @@ export interface RuntimeBridgeHost {
|
|
|
108
115
|
resume(onStep: () => void): void;
|
|
109
116
|
pause(): void;
|
|
110
117
|
injectAction(action: string, operation: 'press' | 'hold' | 'release', value?: number): boolean;
|
|
118
|
+
/** Adds a mouse movement to the next Simulation Step, as if the pointer were locked. */
|
|
119
|
+
look(dx: number, dy: number): void;
|
|
111
120
|
availableActions(): string[];
|
|
112
121
|
heldActions(): string[];
|
|
113
122
|
actionValues(): Record<string, number>;
|
|
@@ -137,6 +146,7 @@ export declare class EngineRuntimeBridge implements RuntimeBridge {
|
|
|
137
146
|
private injectAction;
|
|
138
147
|
/** Advances whole Simulation Steps while paused. */
|
|
139
148
|
private step;
|
|
149
|
+
private look;
|
|
140
150
|
private loadScene;
|
|
141
151
|
/** Counts one Simulation Step, whoever ran it (paused stepping or real-time playback). */
|
|
142
152
|
private advance;
|
package/dist/runtime-bridge.js
CHANGED
|
@@ -24,7 +24,8 @@ export const RUNTIME_BRIDGE_SYMBOL = Symbol.for('@waica/runtime-bridge/v1');
|
|
|
24
24
|
* 0025) announces that every metadata carries `backend`: the Render Backend
|
|
25
25
|
* once the renderer is ready, null before — a Run Session waits for it — and
|
|
26
26
|
* that a renderer that fails to initialize reports through the activation's
|
|
27
|
-
* `fail`.
|
|
27
|
+
* `fail`. `look` (issue #181) announces the `look` operation: a mouse delta
|
|
28
|
+
* the next Simulation Step reads as locked mouse movement.
|
|
28
29
|
*/
|
|
29
30
|
export const RUNTIME_BRIDGE_CAPABILITIES = [
|
|
30
31
|
'click',
|
|
@@ -34,6 +35,7 @@ export const RUNTIME_BRIDGE_CAPABILITIES = [
|
|
|
34
35
|
'camera-effects',
|
|
35
36
|
'analog-actions',
|
|
36
37
|
'render-backend',
|
|
38
|
+
'look',
|
|
37
39
|
];
|
|
38
40
|
export class RuntimeBridgeOperationError extends Error {
|
|
39
41
|
code;
|
|
@@ -122,6 +124,9 @@ export class EngineRuntimeBridge {
|
|
|
122
124
|
case 'scene':
|
|
123
125
|
this.loadScene(request.scene);
|
|
124
126
|
break;
|
|
127
|
+
case 'look':
|
|
128
|
+
this.look(request);
|
|
129
|
+
break;
|
|
125
130
|
default: {
|
|
126
131
|
const unsupported = request;
|
|
127
132
|
throw new RuntimeBridgeOperationError('runtime-operation-failed', `Unsupported runtime control operation "${unsupported.operation}".`);
|
|
@@ -160,6 +165,12 @@ export class EngineRuntimeBridge {
|
|
|
160
165
|
this.host.step(() => this.advance());
|
|
161
166
|
}
|
|
162
167
|
}
|
|
168
|
+
look(request) {
|
|
169
|
+
if (!Number.isFinite(request.dx) || !Number.isFinite(request.dy)) {
|
|
170
|
+
throw new RuntimeBridgeOperationError('runtime-operation-failed', 'dx and dy must be finite numbers.');
|
|
171
|
+
}
|
|
172
|
+
this.host.look(request.dx, request.dy);
|
|
173
|
+
}
|
|
163
174
|
loadScene(scene) {
|
|
164
175
|
if (this.host.loadScene(scene))
|
|
165
176
|
return;
|
|
@@ -1,4 +1,4 @@
|
|
|
1
|
-
import {
|
|
1
|
+
import { viewSnapshot } from './runtime-view-snapshot.js';
|
|
2
2
|
import { anchoredPiecesOf } from './ui.js';
|
|
3
3
|
import { componentClassOf } from './component-registry.js';
|
|
4
4
|
import { capLights, lightingSnapshot, postSnapshot, } from './runtime-lighting-snapshot.js';
|
|
@@ -295,7 +295,8 @@ export class RuntimeInspector {
|
|
|
295
295
|
audio: this.audioSnapshot(),
|
|
296
296
|
time: this.timeSnapshot(),
|
|
297
297
|
ui: this.uiSnapshot(projectionIssues),
|
|
298
|
-
camera
|
|
298
|
+
// The shake offset applies in both spaces since a perspective camera shakes too (issue #181 CA-11).
|
|
299
|
+
camera: this.game.cameraEffects.state,
|
|
299
300
|
space: this.game.space,
|
|
300
301
|
view: viewSnapshot(this.game),
|
|
301
302
|
lighting: lightingSnapshot(this.game, (entity) => this.idFor(entity)),
|
|
@@ -1,5 +1,5 @@
|
|
|
1
|
-
import type { CameraEffectsState } from './camera-effects.js';
|
|
2
1
|
import type { Game } from './game.js';
|
|
2
|
+
import type { OrbitView } from './orbit-camera.js';
|
|
3
3
|
import type { Vec3Json } from './scene-camera-3d.js';
|
|
4
4
|
/**
|
|
5
5
|
* The view the scene is drawn from (issue #154 CA-16), beside the Camera
|
|
@@ -10,16 +10,13 @@ export type RuntimeSnapshotView = {
|
|
|
10
10
|
kind: 'orthographic';
|
|
11
11
|
position: [number, number];
|
|
12
12
|
zoom: number;
|
|
13
|
-
} |
|
|
13
|
+
} | PerspectiveView | (PerspectiveView & OrbitView);
|
|
14
|
+
/** A perspective camera's pose; a following one adds its whole orbit (issue #181 CA-10), so `'follow' in view` narrows to it. */
|
|
15
|
+
type PerspectiveView = {
|
|
14
16
|
kind: 'perspective';
|
|
15
17
|
position: Vec3Json;
|
|
16
18
|
target: Vec3Json;
|
|
17
19
|
fov: number;
|
|
18
20
|
};
|
|
19
21
|
export declare function viewSnapshot(game: Game): RuntimeSnapshotView;
|
|
20
|
-
|
|
21
|
-
* `game.cameraEffects.state` as the snapshot reports it. A perspective camera
|
|
22
|
-
* does not shake (spec decision 24: shake is a no-op in a 3D scene), so a 3D
|
|
23
|
-
* scene reports no offset even though the effect's own state keeps decaying.
|
|
24
|
-
*/
|
|
25
|
-
export declare function cameraEffectsSnapshot(game: Game): CameraEffectsState;
|
|
22
|
+
export {};
|
|
@@ -13,19 +13,22 @@ export function viewSnapshot(game) {
|
|
|
13
13
|
}
|
|
14
14
|
camera.updateMatrixWorld();
|
|
15
15
|
const ahead = camera.getWorldDirection(new THREE.Vector3()).multiplyScalar(game.cameraRig.lookDistance);
|
|
16
|
-
|
|
16
|
+
const pose = {
|
|
17
17
|
kind: 'perspective',
|
|
18
18
|
position: vector(camera.position),
|
|
19
19
|
target: vector(camera.position.clone().add(ahead)),
|
|
20
20
|
fov: camera.fov,
|
|
21
21
|
};
|
|
22
|
-
|
|
23
|
-
|
|
24
|
-
|
|
25
|
-
|
|
26
|
-
|
|
27
|
-
|
|
28
|
-
|
|
29
|
-
|
|
30
|
-
|
|
22
|
+
const orbit = game.cameraRig.orbitView;
|
|
23
|
+
if (!orbit)
|
|
24
|
+
return pose;
|
|
25
|
+
// A following camera's orbit (issue #181 CA-10): the chosen distance and the placed one.
|
|
26
|
+
return {
|
|
27
|
+
...pose,
|
|
28
|
+
follow: orbit.follow,
|
|
29
|
+
yaw: clean(orbit.yaw),
|
|
30
|
+
pitch: clean(orbit.pitch),
|
|
31
|
+
distance: clean(orbit.distance),
|
|
32
|
+
distanceActual: clean(orbit.distanceActual),
|
|
33
|
+
};
|
|
31
34
|
}
|
|
@@ -1,11 +1,15 @@
|
|
|
1
1
|
import type * as THREE from 'three/webgpu';
|
|
2
|
+
import { type OrbitCameraJson, type ResolvedOrbit } from './scene-camera-orbit.js';
|
|
3
|
+
import { type SceneFieldIssue } from './scene-field-issues.js';
|
|
2
4
|
/** A world-space point or direction as scene JSON stores it. */
|
|
3
5
|
export type Vec3Json = [number, number, number];
|
|
4
6
|
/**
|
|
5
|
-
* A scene's perspective camera (3D scenes, ADR 0027).
|
|
6
|
-
* code may move it
|
|
7
|
+
* A scene's perspective camera (3D scenes, ADR 0027). Without `follow` it is
|
|
8
|
+
* fixed by the scene and game code may move it: Y is up and the camera looks
|
|
9
|
+
* from `position` at `target`. With `follow` it orbits that entity once per
|
|
10
|
+
* Simulation Step (issue #181, ADR 0031; the orbit fields are OrbitCameraJson's).
|
|
7
11
|
*/
|
|
8
|
-
export interface PerspectiveSceneCameraJson {
|
|
12
|
+
export interface PerspectiveSceneCameraJson extends OrbitCameraJson {
|
|
9
13
|
kind: 'perspective';
|
|
10
14
|
/** Where the camera sits. Default `[0, 5, 10]`. */
|
|
11
15
|
position?: Vec3Json;
|
|
@@ -18,7 +22,6 @@ export interface PerspectiveSceneCameraJson {
|
|
|
18
22
|
/** Far clipping plane, above `near`. Default 1000. */
|
|
19
23
|
far?: number;
|
|
20
24
|
zoom?: never;
|
|
21
|
-
follow?: never;
|
|
22
25
|
deadzoneWidth?: never;
|
|
23
26
|
deadzoneHeight?: never;
|
|
24
27
|
lookahead?: never;
|
|
@@ -33,6 +36,8 @@ export interface ResolvedPerspectiveCamera {
|
|
|
33
36
|
fov: number;
|
|
34
37
|
near: number;
|
|
35
38
|
far: number;
|
|
39
|
+
/** The orbit around `follow`; absent for a fixed camera. */
|
|
40
|
+
orbit?: ResolvedOrbit;
|
|
36
41
|
}
|
|
37
42
|
export declare const PERSPECTIVE_DEFAULTS: {
|
|
38
43
|
readonly position: Vec3Json;
|
|
@@ -49,18 +54,12 @@ export declare function isPerspectiveCameraJson(json: {
|
|
|
49
54
|
export declare function resolvePerspectiveCamera(json?: PerspectiveSceneCameraJson): ResolvedPerspectiveCamera;
|
|
50
55
|
/** Points `camera` as the resolved block says, at the given aspect ratio. */
|
|
51
56
|
export declare function placePerspectiveCamera(camera: THREE.PerspectiveCamera, resolved: ResolvedPerspectiveCamera, aspect: number): void;
|
|
52
|
-
|
|
53
|
-
export interface SceneFieldIssue {
|
|
54
|
-
field: string;
|
|
55
|
-
message: string;
|
|
56
|
-
}
|
|
57
|
-
/** A JSON value as a message quotes it: strings in quotes, everything else as `String()` shows it. */
|
|
58
|
-
export declare const shown: (value: unknown) => string;
|
|
59
|
-
export declare function isFiniteNumber(value: unknown): value is number;
|
|
57
|
+
export { isFiniteNumber, shown, type SceneFieldIssue } from './scene-field-issues.js';
|
|
60
58
|
/**
|
|
61
59
|
* Every field of a perspective camera block outside its range: `fov` in
|
|
62
|
-
* (0, 180), `near` above 0, `far` above `near`,
|
|
63
|
-
* three finite numbers, and none of the
|
|
64
|
-
* that is not a perspective block
|
|
60
|
+
* (0, 180), `near` above 0, `far` above `near`, `position` and `target`
|
|
61
|
+
* three finite numbers, the orbit fields (orbitCameraIssues), and none of the
|
|
62
|
+
* orthographic block's other fields. Anything that is not a perspective block
|
|
63
|
+
* reports none.
|
|
65
64
|
*/
|
|
66
65
|
export declare function perspectiveCameraIssues(camera: unknown): SceneFieldIssue[];
|