@vgai/engine 0.5.13 → 0.5.14
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/adapter/adapter-module.d.ts +447 -0
- package/dist/adapter/adapter-module.d.ts.map +1 -0
- package/dist/adapter/adapter-module.js +308 -0
- package/dist/adapter/authoring.d.ts +357 -10
- package/dist/adapter/authoring.d.ts.map +1 -1
- package/dist/adapter/authoring.js +87 -1
- package/dist/adapter/constraint.d.ts +9 -3
- package/dist/adapter/constraint.d.ts.map +1 -1
- package/dist/adapter/finders/finder-result.d.ts +28 -0
- package/dist/adapter/finders/finder-result.d.ts.map +1 -0
- package/dist/adapter/finders/finder-result.js +10 -0
- package/dist/adapter/finders/index.d.ts +38 -0
- package/dist/adapter/finders/index.d.ts.map +1 -0
- package/dist/adapter/finders/index.js +41 -0
- package/dist/adapter/finders/prefabs-from-stories.d.ts +64 -0
- package/dist/adapter/finders/prefabs-from-stories.d.ts.map +1 -0
- package/dist/adapter/finders/prefabs-from-stories.js +92 -0
- package/dist/adapter/finders/scenes-from-entrypoint-selection.d.ts +67 -0
- package/dist/adapter/finders/scenes-from-entrypoint-selection.d.ts.map +1 -0
- package/dist/adapter/finders/scenes-from-entrypoint-selection.js +226 -0
- package/dist/adapter/first-party-systems.d.ts.map +1 -1
- package/dist/adapter/first-party-systems.js +29 -0
- package/dist/adapter/index.d.ts +7 -3
- package/dist/adapter/index.d.ts.map +1 -1
- package/dist/adapter/index.js +5 -2
- package/dist/adapter/ingest/contract-system-adapters.d.ts +24 -1
- package/dist/adapter/ingest/contract-system-adapters.d.ts.map +1 -1
- package/dist/adapter/ingest/contract-system-adapters.js +100 -34
- package/dist/adapter/ingest/game-contract.d.ts +188 -2
- package/dist/adapter/ingest/game-contract.d.ts.map +1 -1
- package/dist/adapter/ingest/game-contract.js +107 -0
- package/dist/adapter/ingest/mount-readiness.d.ts +84 -0
- package/dist/adapter/ingest/mount-readiness.d.ts.map +1 -0
- package/dist/adapter/ingest/mount-readiness.js +81 -0
- package/dist/adapter/ingest/scene-capture.d.ts +91 -2
- package/dist/adapter/ingest/scene-capture.d.ts.map +1 -1
- package/dist/adapter/ingest/scene-capture.js +110 -18
- package/dist/adapter/ingest/structural-ids.d.ts +11 -5
- package/dist/adapter/ingest/structural-ids.d.ts.map +1 -1
- package/dist/adapter/ingest/structural-ids.js +18 -9
- package/dist/adapter/ingest/upstream-pin.d.ts +4 -2
- package/dist/adapter/ingest/upstream-pin.d.ts.map +1 -1
- package/dist/adapter/ingest/upstream-pin.js +4 -2
- package/dist/adapter/ingest/visible-capture-window.d.ts +116 -0
- package/dist/adapter/ingest/visible-capture-window.d.ts.map +1 -0
- package/dist/adapter/ingest/visible-capture-window.js +141 -0
- package/dist/adapter/loop-gate-report.d.ts +3 -4
- package/dist/adapter/loop-gate-report.d.ts.map +1 -1
- package/dist/adapter/loop-gate-report.js +3 -4
- package/dist/adapter/root-adapter.d.ts +22 -2
- package/dist/adapter/root-adapter.d.ts.map +1 -1
- package/dist/adapter/setup-three-root-adapter.d.ts.map +1 -1
- package/dist/adapter/setup-three-root-adapter.js +15 -0
- package/dist/adapter/system-adapter.d.ts +161 -2
- package/dist/adapter/system-adapter.d.ts.map +1 -1
- package/dist/adapter/system-adapter.js +18 -1
- package/dist/animation/camera-ownership.d.ts +4 -2
- package/dist/animation/camera-ownership.d.ts.map +1 -1
- package/dist/animation/camera-ownership.js +58 -1
- package/dist/animation/runtime-inspection.d.ts +70 -0
- package/dist/animation/runtime-inspection.d.ts.map +1 -0
- package/dist/animation/runtime-inspection.js +25 -0
- package/dist/animation/xstate-animation-binding.d.ts +9 -1
- package/dist/animation/xstate-animation-binding.d.ts.map +1 -1
- package/dist/animation/xstate-animation-binding.js +48 -1
- package/dist/asset-formats/material.d.ts +1 -1
- package/dist/asset-formats/mesh.d.ts +1 -1
- package/dist/asset-formats/particles.d.ts +13 -0
- package/dist/asset-formats/particles.d.ts.map +1 -1
- package/dist/asset-formats/particles.js +28 -0
- package/dist/behavior/xstate-inspection.d.ts +24 -0
- package/dist/behavior/xstate-inspection.d.ts.map +1 -0
- package/dist/behavior/xstate-inspection.js +25 -0
- package/dist/core/game-loop.d.ts +7 -5
- package/dist/core/game-loop.d.ts.map +1 -1
- package/dist/core/game-loop.js +39 -18
- package/dist/core/types.d.ts +11 -12
- package/dist/core/types.d.ts.map +1 -1
- package/dist/data/data-asset.js +1 -1
- package/dist/dev/instruments.d.ts.map +1 -1
- package/dist/dev/instruments.js +5 -1
- package/dist/dev/render-debug-adapter.d.ts +19 -2
- package/dist/dev/render-debug-adapter.d.ts.map +1 -1
- package/dist/dev/render-debug-adapter.js +10 -0
- package/dist/ecs/user-data.d.ts +10 -2
- package/dist/ecs/user-data.d.ts.map +1 -1
- package/dist/ecs/user-data.js +6 -2
- package/dist/input/binding-identity.d.ts +10 -0
- package/dist/input/binding-identity.d.ts.map +1 -0
- package/dist/input/binding-identity.js +36 -0
- package/dist/input/input-manager.d.ts +18 -185
- package/dist/input/input-manager.d.ts.map +1 -1
- package/dist/input/input-manager.js +78 -68
- package/dist/manifest/load.d.ts +14 -0
- package/dist/manifest/load.d.ts.map +1 -1
- package/dist/manifest/load.js +6 -1
- package/dist/manifest/schema.d.ts +18 -0
- package/dist/manifest/schema.d.ts.map +1 -1
- package/dist/manifest/schema.js +64 -7
- package/dist/pixi/authoring.d.ts +27 -0
- package/dist/pixi/authoring.d.ts.map +1 -1
- package/dist/pixi/authoring.js +10 -3
- package/dist/pixi/index.d.ts +1 -1
- package/dist/pixi/index.d.ts.map +1 -1
- package/dist/pixi/index.js +1 -1
- package/dist/pixi/ingest.d.ts +8 -0
- package/dist/pixi/ingest.d.ts.map +1 -1
- package/dist/pixi/ingest.js +10 -1
- package/dist/pixi/render-pass-bracket.d.ts +56 -0
- package/dist/pixi/render-pass-bracket.d.ts.map +1 -0
- package/dist/pixi/render-pass-bracket.js +98 -0
- package/dist/pixi/scene-capture.d.ts +32 -1
- package/dist/pixi/scene-capture.d.ts.map +1 -1
- package/dist/pixi/scene-capture.js +21 -9
- package/dist/pixi/system-adapters.d.ts +29 -0
- package/dist/pixi/system-adapters.d.ts.map +1 -1
- package/dist/pixi/system-adapters.js +27 -0
- package/dist/render/particles-factory.d.ts +16 -2
- package/dist/render/particles-factory.d.ts.map +1 -1
- package/dist/render/particles-factory.js +88 -5
- package/dist/render/soft-particle-depth.d.ts +149 -0
- package/dist/render/soft-particle-depth.d.ts.map +1 -0
- package/dist/render/soft-particle-depth.js +305 -0
- package/dist/render/voxel-surface.d.ts +117 -0
- package/dist/render/voxel-surface.d.ts.map +1 -0
- package/dist/render/voxel-surface.js +263 -0
- package/dist/runtime/create-runtime.js +7 -0
- package/dist/runtime/debug-bridge.d.ts +15 -15
- package/dist/runtime/debug-bridge.d.ts.map +1 -1
- package/dist/runtime/debug-bridge.js +27 -26
- package/dist/runtime/game.d.ts.map +1 -1
- package/dist/runtime/game.js +1 -2
- package/dist/setup/setup-particles.d.ts.map +1 -1
- package/dist/setup/setup-particles.js +9 -0
- package/dist/world3d-react/r3f-adapter.d.ts.map +1 -1
- package/dist/world3d-react/r3f-adapter.js +42 -3
- package/dist/world3d-react/rapier-physics-bridge.d.ts.map +1 -1
- package/dist/world3d-react/rapier-physics-bridge.js +201 -11
- package/dist-config/config.js.map +1 -1
- package/package.json +4 -2
- package/schemas/engine-capabilities.json +12 -12
- package/schemas/vgai-project.schema.json +41 -2
- package/src/adapter/adapter-module.ts +607 -0
- package/src/adapter/authoring.ts +440 -10
- package/src/adapter/constraint.ts +10 -3
- package/src/adapter/finders/finder-result.ts +29 -0
- package/src/adapter/finders/index.ts +62 -0
- package/src/adapter/finders/prefabs-from-stories.ts +142 -0
- package/src/adapter/finders/scenes-from-entrypoint-selection.ts +304 -0
- package/src/adapter/first-party-systems.ts +30 -0
- package/src/adapter/index.ts +32 -1
- package/src/adapter/ingest/contract-system-adapters.ts +116 -32
- package/src/adapter/ingest/game-contract.ts +265 -2
- package/src/adapter/ingest/mount-readiness.ts +140 -0
- package/src/adapter/ingest/scene-capture.ts +199 -22
- package/src/adapter/ingest/structural-ids.ts +21 -9
- package/src/adapter/ingest/upstream-pin.ts +4 -2
- package/src/adapter/ingest/visible-capture-window.ts +213 -0
- package/src/adapter/loop-gate-report.ts +3 -4
- package/src/adapter/root-adapter.ts +22 -2
- package/src/adapter/setup-three-root-adapter.ts +16 -0
- package/src/adapter/system-adapter.ts +188 -2
- package/src/animation/camera-ownership.ts +68 -3
- package/src/animation/runtime-inspection.ts +104 -0
- package/src/animation/xstate-animation-binding.ts +53 -1
- package/src/asset-formats/particles.ts +31 -0
- package/src/behavior/xstate-inspection.ts +51 -0
- package/src/core/game-loop.ts +39 -17
- package/src/core/types.ts +11 -12
- package/src/data/data-asset.ts +1 -1
- package/src/dev/instruments.ts +5 -1
- package/src/dev/render-debug-adapter.ts +22 -2
- package/src/ecs/user-data.ts +10 -2
- package/src/input/binding-identity.ts +37 -0
- package/src/input/input-manager.ts +93 -68
- package/src/manifest/load.ts +23 -2
- package/src/manifest/schema.ts +76 -7
- package/src/pixi/authoring.ts +37 -5
- package/src/pixi/index.ts +2 -0
- package/src/pixi/ingest.ts +15 -2
- package/src/pixi/render-pass-bracket.ts +117 -0
- package/src/pixi/scene-capture.ts +63 -13
- package/src/pixi/system-adapters.ts +41 -0
- package/src/render/particles-factory.ts +89 -6
- package/src/render/soft-particle-depth.ts +336 -0
- package/src/render/voxel-surface.ts +358 -0
- package/src/runtime/create-runtime.ts +7 -0
- package/src/runtime/debug-bridge.ts +32 -31
- package/src/runtime/game.ts +1 -2
- package/src/setup/setup-particles.ts +9 -0
- package/src/world3d-react/r3f-adapter.tsx +48 -2
- package/src/world3d-react/rapier-physics-bridge.tsx +249 -11
|
@@ -0,0 +1,51 @@
|
|
|
1
|
+
import type * as THREE from 'three';
|
|
2
|
+
import type { AnyStateMachine } from 'xstate';
|
|
3
|
+
import { deleteUserData, getUserData, setUserData } from '../ecs/user-data';
|
|
4
|
+
|
|
5
|
+
/** Minimal structural actor surface needed by the live behavior debugger. */
|
|
6
|
+
export interface InspectableXStateActor {
|
|
7
|
+
readonly getSnapshot: () => {
|
|
8
|
+
readonly value: unknown;
|
|
9
|
+
readonly context: unknown;
|
|
10
|
+
readonly machine?: AnyStateMachine;
|
|
11
|
+
};
|
|
12
|
+
readonly subscribe: (next: (snapshot: { readonly value: unknown }) => void) => {
|
|
13
|
+
unsubscribe: () => void;
|
|
14
|
+
};
|
|
15
|
+
readonly logic?: AnyStateMachine;
|
|
16
|
+
}
|
|
17
|
+
|
|
18
|
+
export interface XStateBehaviorInspection {
|
|
19
|
+
readonly actor: InspectableXStateActor;
|
|
20
|
+
readonly machine: AnyStateMachine;
|
|
21
|
+
}
|
|
22
|
+
|
|
23
|
+
let version = 0;
|
|
24
|
+
const listeners = new Set<() => void>();
|
|
25
|
+
|
|
26
|
+
function notify(): void {
|
|
27
|
+
version++;
|
|
28
|
+
for (const listener of listeners) listener();
|
|
29
|
+
}
|
|
30
|
+
|
|
31
|
+
export function subscribeXStateBehaviorInspections(listener: () => void): () => void {
|
|
32
|
+
listeners.add(listener);
|
|
33
|
+
return () => listeners.delete(listener);
|
|
34
|
+
}
|
|
35
|
+
|
|
36
|
+
export function xstateBehaviorInspectionsVersion(): number {
|
|
37
|
+
return version;
|
|
38
|
+
}
|
|
39
|
+
|
|
40
|
+
export function attachXStateBehaviorInspection(
|
|
41
|
+
owner: THREE.Object3D,
|
|
42
|
+
inspection: XStateBehaviorInspection,
|
|
43
|
+
): () => void {
|
|
44
|
+
setUserData(owner, '_xstateBehavior', inspection);
|
|
45
|
+
notify();
|
|
46
|
+
return () => {
|
|
47
|
+
if (getUserData(owner, '_xstateBehavior') !== inspection) return;
|
|
48
|
+
deleteUserData(owner, '_xstateBehavior');
|
|
49
|
+
notify();
|
|
50
|
+
};
|
|
51
|
+
}
|
package/src/core/game-loop.ts
CHANGED
|
@@ -1,6 +1,12 @@
|
|
|
1
1
|
import { clampTimeScale, paceFrame, TIME_SCALE_RANGE } from './frame-pacing';
|
|
2
2
|
import type { GameLoopConfig, GameLoopLiveness } from './types';
|
|
3
3
|
|
|
4
|
+
/** A visible rAF chain that has produced no callback for this long is not
|
|
5
|
+
* honestly "running" merely because its callback remains armed. One second
|
|
6
|
+
* is far beyond an ordinary dropped frame and short enough for status to
|
|
7
|
+
* report browser starvation while it is still actionable. */
|
|
8
|
+
const LOOP_STARVATION_MS = 1_000;
|
|
9
|
+
|
|
4
10
|
/**
|
|
5
11
|
* Fixed-timestep game loop with accumulator pattern — SIM at a fixed rate,
|
|
6
12
|
* PRESENTATION at the display's rate (WO-P13, `docs/WORK.md` §2 P13, which
|
|
@@ -56,19 +62,24 @@ export function createGameLoop(config: GameLoopConfig) {
|
|
|
56
62
|
// Idle throttle (T2.1): a hidden tab stops the loop outright; becoming
|
|
57
63
|
// visible again restarts it with the accumulator clock resynced to "now" —
|
|
58
64
|
// deliberately no catch-up burst for the wall-clock time spent hidden.
|
|
59
|
-
let
|
|
65
|
+
let visibilityPaused = false;
|
|
60
66
|
let visibilityListenerAttached = false;
|
|
61
67
|
|
|
62
|
-
|
|
68
|
+
/** Reconcile the gate against the CURRENT document bit, not merely the
|
|
69
|
+
* last event. Browsers can miss/coalesce visibilitychange around window
|
|
70
|
+
* occlusion and reloads; reading liveness is itself a recovery opportunity,
|
|
71
|
+
* so a stale event can never leave this flag permanently wedged. */
|
|
72
|
+
function reconcileVisibilityGate() {
|
|
73
|
+
if (externalDrive || !visibilityListenerAttached) return;
|
|
63
74
|
if (typeof document === 'undefined') return;
|
|
64
75
|
if (document.hidden) {
|
|
65
76
|
if (running) {
|
|
66
77
|
running = false;
|
|
67
78
|
cancelAnimationFrame(rafId);
|
|
68
|
-
|
|
79
|
+
visibilityPaused = true;
|
|
69
80
|
}
|
|
70
|
-
} else if (
|
|
71
|
-
|
|
81
|
+
} else if (visibilityPaused) {
|
|
82
|
+
visibilityPaused = false;
|
|
72
83
|
running = true;
|
|
73
84
|
lastTime = performance.now();
|
|
74
85
|
accumulator = 0;
|
|
@@ -76,6 +87,10 @@ export function createGameLoop(config: GameLoopConfig) {
|
|
|
76
87
|
}
|
|
77
88
|
}
|
|
78
89
|
|
|
90
|
+
function handleVisibilityChange() {
|
|
91
|
+
reconcileVisibilityGate();
|
|
92
|
+
}
|
|
93
|
+
|
|
79
94
|
function frame(currentTime: number) {
|
|
80
95
|
if (!running) return;
|
|
81
96
|
rafId = requestAnimationFrame(frame);
|
|
@@ -107,13 +122,13 @@ export function createGameLoop(config: GameLoopConfig) {
|
|
|
107
122
|
start() {
|
|
108
123
|
if (running) return;
|
|
109
124
|
running = true;
|
|
110
|
-
// A direct start() call can land while the loop is auto-
|
|
111
|
-
// (`
|
|
125
|
+
// A direct start() call can land while the loop is auto-loop-starved
|
|
126
|
+
// (`visibilityPaused` true, `running` false, no rAF pending). Without this
|
|
112
127
|
// reset, a later visibilitychange-to-visible would see stale
|
|
113
|
-
// `
|
|
128
|
+
// `visibilityPaused === true` and spawn a SECOND rAF chain on top of the
|
|
114
129
|
// one this call is about to start (doubled update()/render() overhead
|
|
115
130
|
// until the next stop() — no sim-speed effect, but wasted work).
|
|
116
|
-
|
|
131
|
+
visibilityPaused = false;
|
|
117
132
|
lastTime = performance.now();
|
|
118
133
|
accumulator = 0;
|
|
119
134
|
|
|
@@ -137,7 +152,7 @@ export function createGameLoop(config: GameLoopConfig) {
|
|
|
137
152
|
}
|
|
138
153
|
if (typeof document !== 'undefined' && document.hidden) {
|
|
139
154
|
running = false;
|
|
140
|
-
|
|
155
|
+
visibilityPaused = true;
|
|
141
156
|
return;
|
|
142
157
|
}
|
|
143
158
|
|
|
@@ -146,7 +161,7 @@ export function createGameLoop(config: GameLoopConfig) {
|
|
|
146
161
|
|
|
147
162
|
stop() {
|
|
148
163
|
running = false;
|
|
149
|
-
|
|
164
|
+
visibilityPaused = false;
|
|
150
165
|
cancelAnimationFrame(rafId);
|
|
151
166
|
|
|
152
167
|
if (visibilityListenerAttached && typeof document !== 'undefined') {
|
|
@@ -156,22 +171,29 @@ export function createGameLoop(config: GameLoopConfig) {
|
|
|
156
171
|
},
|
|
157
172
|
|
|
158
173
|
get isRunning() {
|
|
174
|
+
reconcileVisibilityGate();
|
|
159
175
|
return running;
|
|
160
176
|
},
|
|
161
177
|
|
|
162
178
|
/**
|
|
163
179
|
* Truthful liveness (issue #175 — see `GameLoopLiveness`'s doc comment
|
|
164
180
|
* in `core/types.ts` for the full contract). Unlike `isRunning` above,
|
|
165
|
-
* this
|
|
166
|
-
* `
|
|
167
|
-
*
|
|
168
|
-
* so
|
|
181
|
+
* this distinguishes a starved rAF loop from an actually-stopped loop.
|
|
182
|
+
* `visibilityPaused` records the deliberate `document.hidden` gate, and
|
|
183
|
+
* every read first reconciles that flag against the CURRENT document bit
|
|
184
|
+
* so a missed visibilitychange cannot wedge it. A visible, armed chain
|
|
185
|
+
* also reports starvation when no real frame callback has arrived within
|
|
186
|
+
* {@link LOOP_STARVATION_MS}; "armed" alone is not evidence of progress. An
|
|
169
187
|
* `externalDrive` loop never installs the visibility listener at all
|
|
170
|
-
* (see `start()` above), so `
|
|
188
|
+
* (see `start()` above), so `visibilityPaused` stays permanently `false` for
|
|
171
189
|
* it — it only ever reports `'running'` or `'stopped'`.
|
|
172
190
|
*/
|
|
173
191
|
get liveness(): GameLoopLiveness {
|
|
174
|
-
|
|
192
|
+
reconcileVisibilityGate();
|
|
193
|
+
if (visibilityPaused) return 'loop-starved';
|
|
194
|
+
if (running && !externalDrive && performance.now() - lastTime > LOOP_STARVATION_MS) {
|
|
195
|
+
return 'loop-starved';
|
|
196
|
+
}
|
|
175
197
|
return running ? 'running' : 'stopped';
|
|
176
198
|
},
|
|
177
199
|
|
package/src/core/types.ts
CHANGED
|
@@ -110,18 +110,17 @@ export interface GameLoopConfig {
|
|
|
110
110
|
* debug-registry.ts`) can name the union without importing `game-loop.ts`
|
|
111
111
|
* as a value.
|
|
112
112
|
*
|
|
113
|
-
* - `'running'` —
|
|
114
|
-
*
|
|
115
|
-
*
|
|
116
|
-
* - `'
|
|
117
|
-
*
|
|
118
|
-
*
|
|
113
|
+
* - `'running'` — a real rAF callback arrived recently (or, for an
|
|
114
|
+
* `externalDrive` loop, `start()` has been called — see that mode's own
|
|
115
|
+
* liveness note below).
|
|
116
|
+
* - `'loop-starved'` — the host loop has made no recent rAF progress: either
|
|
117
|
+
* the T2.1 visibility gate deliberately parked it, or an armed visible-page
|
|
118
|
+
* callback has not arrived within the starvation threshold. This reports
|
|
119
|
+
* the LOOP measurement, never a conclusion about where the browser tab is.
|
|
119
120
|
* - `'stopped'` — `stop()` was called, or `start()` was never called.
|
|
120
121
|
*
|
|
121
|
-
* `isRunning`
|
|
122
|
-
* `
|
|
123
|
-
*
|
|
124
|
-
* store-level play state. Read `liveness` wherever "is this game actually
|
|
125
|
-
* advancing" matters.
|
|
122
|
+
* `isRunning` says whether the rAF chain is armed; it can therefore remain
|
|
123
|
+
* `true` while a visible browser starves callbacks. Read `liveness` wherever
|
|
124
|
+
* "is this host loop actually advancing" matters.
|
|
126
125
|
*/
|
|
127
|
-
export type GameLoopLiveness = 'running' | '
|
|
126
|
+
export type GameLoopLiveness = 'running' | 'loop-starved' | 'stopped';
|
package/src/data/data-asset.ts
CHANGED
|
@@ -94,7 +94,7 @@ export function defineData<S extends z.ZodType>(
|
|
|
94
94
|
parsed = parseDataJson(schema, next, sourcePath);
|
|
95
95
|
} catch (err) {
|
|
96
96
|
// Keep the last good value alive — see the JSDoc on DataHandle.hotSwap.
|
|
97
|
-
// biome-ignore lint/suspicious/noConsole: deliberate, greppable — a rejected live-tune edit must be loud without crashing the running game
|
|
97
|
+
// biome-ignore lint/suspicious/noConsole: deliberate, greppable — a rejected live-tune edit must be loud without crashing the running game
|
|
98
98
|
console.error(`[data] hot edit rejected, keeping previous values.\n${String(err)}`);
|
|
99
99
|
return;
|
|
100
100
|
}
|
package/src/dev/instruments.ts
CHANGED
|
@@ -50,6 +50,7 @@
|
|
|
50
50
|
*/
|
|
51
51
|
|
|
52
52
|
import { z } from 'zod';
|
|
53
|
+
import { nodeKeyedPhysics } from '../adapter/system-adapter';
|
|
53
54
|
import { clampTimeScale, TIME_SCALE_RANGE } from '../core/frame-pacing';
|
|
54
55
|
import type { Game } from '../runtime/game';
|
|
55
56
|
|
|
@@ -175,7 +176,10 @@ export function createDevInstruments(game: Game): DevInstrumentSet {
|
|
|
175
176
|
// below, which is also the only thing that calls the adapter.
|
|
176
177
|
let collidersDrawn = false;
|
|
177
178
|
const colliderDraw = () => {
|
|
178
|
-
|
|
179
|
+
// Node-id keyed only — `setDebugDrawEnabled` draws THREE wireframes into
|
|
180
|
+
// the world, which a canvas mount's display-keyed carrier has no analogue
|
|
181
|
+
// for and does not declare.
|
|
182
|
+
const adapter = nodeKeyedPhysics(game.systemAdapters.physics);
|
|
179
183
|
return adapter?.setDebugDrawEnabled ? adapter : null;
|
|
180
184
|
};
|
|
181
185
|
|
|
@@ -18,6 +18,12 @@
|
|
|
18
18
|
* `annotation: null` (counted as unattributed) — never a fabricated guess. The
|
|
19
19
|
* wrappers are all restored when the capture resolves, rejects, times out, or
|
|
20
20
|
* the adapter is disposed.
|
|
21
|
+
*
|
|
22
|
+
* That attribution is a THREE capability, not a property of frame capture: a
|
|
23
|
+
* surface whose renderer has no per-draw annotation seam omits `scene` entirely
|
|
24
|
+
* (the Pixi ingest lane does — `editor/src/ingest/ingest-render-debug.ts`), and
|
|
25
|
+
* every draw is then recorded unattributed. The capture itself is GL-level and
|
|
26
|
+
* unchanged either way; only the names beside the draws go away.
|
|
21
27
|
*/
|
|
22
28
|
|
|
23
29
|
import type * as THREE from 'three';
|
|
@@ -44,8 +50,19 @@ export function frameCaptureContextFor(
|
|
|
44
50
|
export interface RenderDebugAdapterDeps {
|
|
45
51
|
/** The armed-on-demand frame capture over the mount's real context. */
|
|
46
52
|
capture: WebGLFrameCapture;
|
|
47
|
-
/**
|
|
48
|
-
|
|
53
|
+
/**
|
|
54
|
+
* The live three scene root traversed to attribute draws to the objects that
|
|
55
|
+
* issue them.
|
|
56
|
+
*
|
|
57
|
+
* OMITTED on a surface whose renderer publishes no per-draw annotation seam —
|
|
58
|
+
* the Pixi lane, where display objects have no `onBeforeRender` and the
|
|
59
|
+
* batcher merges many display objects into one draw, so there is no object to
|
|
60
|
+
* name for a given draw at all. Then every draw is recorded with
|
|
61
|
+
* `annotation: null` and lands in `totals.unattributed`: the honest "this
|
|
62
|
+
* seam cannot say who drew this", never a guess derived from the scene's
|
|
63
|
+
* shape.
|
|
64
|
+
*/
|
|
65
|
+
scene?: THREE.Object3D;
|
|
49
66
|
/** Memory snapshot supplier (renderer.info-backed). When omitted, the
|
|
50
67
|
* adapter's `memorySnapshot` is absent (honest capability degradation). */
|
|
51
68
|
memory?: () => RenderMemorySnapshot;
|
|
@@ -127,6 +144,9 @@ export function createRenderDebugAdapter(deps: RenderDebugAdapterDeps): RenderDe
|
|
|
127
144
|
|
|
128
145
|
function wrapOnBeforeRender(): (() => void)[] {
|
|
129
146
|
const restores: (() => void)[] = [];
|
|
147
|
+
// No scene ⇒ no annotation seam on this surface (see `scene` above): wrap
|
|
148
|
+
// nothing, restore nothing, and let every draw record `annotation: null`.
|
|
149
|
+
if (!scene) return restores;
|
|
130
150
|
scene.traverse((object) => {
|
|
131
151
|
if (!isRenderable(object)) return;
|
|
132
152
|
const original = object.onBeforeRender;
|
package/src/ecs/user-data.ts
CHANGED
|
@@ -46,8 +46,10 @@
|
|
|
46
46
|
* mixer; see xstate-animation-binding.ts).
|
|
47
47
|
* - `_availableClips` — string[] of clip names discovered on the GLTF
|
|
48
48
|
* (inspector dropdown; same names as `_animClips`' keys).
|
|
49
|
-
* - `
|
|
50
|
-
*
|
|
49
|
+
* - `_animationRuntime` — format-neutral live native mixer/action inspection.
|
|
50
|
+
* - `_xstateBehavior` — optional live XState behavior actor inspection.
|
|
51
|
+
* - `_xstateAnimation` — compatibility handle for the optional XState→Three
|
|
52
|
+
* bridge; animation UI never discovers through it.
|
|
51
53
|
*
|
|
52
54
|
* Disposal contract:
|
|
53
55
|
* - `__sharedGeometry` — `true` when a mesh's geometry is shared/cached and MUST
|
|
@@ -118,7 +120,9 @@ import type { ParticleSystem } from 'three.quarks';
|
|
|
118
120
|
import type { ConstraintMark } from '../adapter/constraint';
|
|
119
121
|
import type { RapierEditableBody } from '../adapter/rapier-physics-adapter';
|
|
120
122
|
import type { ReflectionProbeMark } from '../adapter/reflection-probe';
|
|
123
|
+
import type { AnimationRuntimeInspection } from '../animation/runtime-inspection';
|
|
121
124
|
import type { XStateAnimationBinding } from '../animation/xstate-animation-binding';
|
|
125
|
+
import type { XStateBehaviorInspection } from '../behavior/xstate-inspection';
|
|
122
126
|
|
|
123
127
|
/** The kinds of editor helper objects tagged via `editorHelperType`. */
|
|
124
128
|
export type EditorHelperType =
|
|
@@ -165,6 +169,8 @@ export interface UserDataSchema {
|
|
|
165
169
|
_animMixer: THREE.AnimationMixer;
|
|
166
170
|
_animClips: Map<string, THREE.AnimationClip>;
|
|
167
171
|
_availableClips: string[];
|
|
172
|
+
_animationRuntime: AnimationRuntimeInspection;
|
|
173
|
+
_xstateBehavior: XStateBehaviorInspection;
|
|
168
174
|
_xstateAnimation: XStateAnimationBinding;
|
|
169
175
|
__sharedGeometry: boolean;
|
|
170
176
|
__shadeOrig: THREE.Material | THREE.Material[];
|
|
@@ -222,6 +228,8 @@ export const UserDataKeys = {
|
|
|
222
228
|
_animMixer: '_animMixer',
|
|
223
229
|
_animClips: '_animClips',
|
|
224
230
|
_availableClips: '_availableClips',
|
|
231
|
+
_animationRuntime: '_animationRuntime',
|
|
232
|
+
_xstateBehavior: '_xstateBehavior',
|
|
225
233
|
_xstateAnimation: '_xstateAnimation',
|
|
226
234
|
__sharedGeometry: '__sharedGeometry',
|
|
227
235
|
__shadeOrig: '__shadeOrig',
|
|
@@ -0,0 +1,37 @@
|
|
|
1
|
+
import type { InputBinding } from './input-types';
|
|
2
|
+
|
|
3
|
+
/**
|
|
4
|
+
* Whether two bindings name the same physical input.
|
|
5
|
+
*
|
|
6
|
+
* This is the native identity rule used by both runtime rebinding and the
|
|
7
|
+
* editor's `.inputmap.json` document. Deadzones do not change identity, while
|
|
8
|
+
* opposite directions on one gamepad axis remain independent controls.
|
|
9
|
+
*/
|
|
10
|
+
export function inputBindingsCollide(a: InputBinding, b: InputBinding): boolean {
|
|
11
|
+
if (a.type !== b.type) return false;
|
|
12
|
+
switch (a.type) {
|
|
13
|
+
case 'key':
|
|
14
|
+
return a.code === (b as typeof a).code;
|
|
15
|
+
case 'mouse_button':
|
|
16
|
+
return a.button === (b as typeof a).button;
|
|
17
|
+
case 'mouse_move':
|
|
18
|
+
return true;
|
|
19
|
+
case 'gamepad_button':
|
|
20
|
+
return a.button === (b as typeof a).button;
|
|
21
|
+
case 'gamepad_axis': {
|
|
22
|
+
const other = b as typeof a;
|
|
23
|
+
return a.axis === other.axis && a.direction === other.direction;
|
|
24
|
+
}
|
|
25
|
+
case 'gamepad_axis_pair': {
|
|
26
|
+
const other = b as typeof a;
|
|
27
|
+
return a.xAxis === other.xAxis && a.yAxis === other.yAxis;
|
|
28
|
+
}
|
|
29
|
+
case 'touch_button':
|
|
30
|
+
case 'touch_stick':
|
|
31
|
+
case 'test_axis':
|
|
32
|
+
case 'test_vector2':
|
|
33
|
+
case 'test_pointer_delta':
|
|
34
|
+
case 'test_pointer_position':
|
|
35
|
+
return a.sourceId === (b as typeof a).sourceId;
|
|
36
|
+
}
|
|
37
|
+
}
|
|
@@ -1,5 +1,6 @@
|
|
|
1
1
|
import { AssetParseError } from '../asset-parse-error';
|
|
2
2
|
import { resolveUrl } from '../loader';
|
|
3
|
+
import { inputBindingsCollide } from './binding-identity';
|
|
3
4
|
import type {
|
|
4
5
|
ActionValueOf,
|
|
5
6
|
ActionValueSource,
|
|
@@ -235,7 +236,7 @@ const INPUT_TRACE_CAP = 2000;
|
|
|
235
236
|
* map this class serializes is a valid document for B2 to read back.
|
|
236
237
|
* - Conflicts: `findConflicts(proposed, excludeActionName?)` scans every
|
|
237
238
|
* OTHER registered action's bindings for one that's structurally
|
|
238
|
-
* equivalent to `proposed` (see
|
|
239
|
+
* equivalent to `proposed` (see `inputBindingsCollide` — same
|
|
239
240
|
* physical input identity: same key code, same mouse button, the same
|
|
240
241
|
* gamepad button/axis/axis-pair, the same touch `sourceId`, etc.) and
|
|
241
242
|
* returns the (possibly empty) list as plain `BindingConflict` data —
|
|
@@ -261,15 +262,28 @@ const INPUT_TRACE_CAP = 2000;
|
|
|
261
262
|
*
|
|
262
263
|
* - `getLastActiveDevice()` returns the `PromptDevice` family of whichever
|
|
263
264
|
* input arrived most recently — keyboard on `keydown`, mouse on
|
|
264
|
-
* `mousedown`/a real `mousemove`, gamepad on a button press or
|
|
265
|
-
*
|
|
266
|
-
* `setTouchButton(_, true)` or `setTouchStick`
|
|
267
|
-
*
|
|
265
|
+
* `mousedown`/a real `mousemove`, gamepad on a button press or an axis
|
|
266
|
+
* past ITS OWN DECLARED DEADZONE (checked in `poll()`), touch on
|
|
267
|
+
* `setTouchButton(_, true)` or a `setTouchStick` past the deadzone that
|
|
268
|
+
* stick's binding declares. `null` before any input has arrived. The
|
|
269
|
+
* off-rest threshold is always the game's declaration, never a constant
|
|
270
|
+
* this class picked — see `declaredDeadzone`.
|
|
268
271
|
* - `getPrompt(actionName)` (device omitted) resolves against it directly,
|
|
269
272
|
* falling back to `'keyboard'` before any input has arrived — a HUD can
|
|
270
273
|
* call it every frame with no extra bookkeeping and the label switches on
|
|
271
274
|
* its own.
|
|
272
275
|
*/
|
|
276
|
+
/**
|
|
277
|
+
* The documented deadzone defaults, one home each. Both are stated in
|
|
278
|
+
* `input/schema.ts`'s own `.describe()` text — spelled here so every reader
|
|
279
|
+
* (the value getters AND the active-device tracker) uses the SAME number, which
|
|
280
|
+
* is the drift the tracker's old hardcoded 0.3 was.
|
|
281
|
+
*/
|
|
282
|
+
const GAMEPAD_AXIS_DEFAULT_DEADZONE = 0.15;
|
|
283
|
+
/** `touch_stick`'s documented default: none. A virtual stick reports a value
|
|
284
|
+
* only because a finger is on it, so it has no at-rest jitter floor to clear. */
|
|
285
|
+
const TOUCH_STICK_DEFAULT_DEADZONE = 0;
|
|
286
|
+
|
|
273
287
|
export class InputManager {
|
|
274
288
|
private actions = new Map<string, InputBinding[]>();
|
|
275
289
|
// F1 — each registered/loaded action's declared value shape (defaults to
|
|
@@ -344,20 +358,44 @@ export class InputManager {
|
|
|
344
358
|
// the player switches controllers, without the caller having to guess or
|
|
345
359
|
// track this itself. Updated at the same points raw input arrives: keydown
|
|
346
360
|
// (`'keyboard'`), mousedown/mousemove (`'mouse'`), a gamepad button press or
|
|
347
|
-
//
|
|
361
|
+
// an axis past its declared deadzone (`'gamepad'`, both detected in
|
|
348
362
|
// `poll()` — a raw resting stick reports small non-zero noise on some
|
|
349
|
-
// pads, so this
|
|
350
|
-
// axis"), and a touch button press / stick pushed past rest
|
|
363
|
+
// pads, so this checks the threshold the GAME declared for that axis, not
|
|
364
|
+
// "any non-zero axis"), and a touch button press / stick pushed past rest
|
|
351
365
|
// (`'touch'` — `setTouchButton`/`setTouchStick`). `null` until the very
|
|
352
366
|
// first input of any kind arrives. See `getLastActiveDevice`/`getPrompt`.
|
|
353
367
|
private lastActiveDevice: PromptDevice | null = null;
|
|
354
|
-
/**
|
|
355
|
-
*
|
|
356
|
-
*
|
|
357
|
-
*
|
|
358
|
-
*
|
|
359
|
-
|
|
360
|
-
|
|
368
|
+
/**
|
|
369
|
+
* "Is this control off its rest position" for the active-device tracker,
|
|
370
|
+
* answered from what the GAME DECLARED rather than from a threshold this
|
|
371
|
+
* class invented.
|
|
372
|
+
*
|
|
373
|
+
* It used to be `Math.hypot(v.x, v.y) > 0.3` — a constant nobody could see or
|
|
374
|
+
* change, sitting on top of a deadzone the game had already stated. A game
|
|
375
|
+
* that declares a 0.5 deadzone on its move stick considers everything under
|
|
376
|
+
* 0.5 to be rest; reporting "the player switched to gamepad" at 0.31 makes a
|
|
377
|
+
* HUD's prompt flip to a control the game itself is ignoring. Zero inference
|
|
378
|
+
* (ARCHITECTURE-CORE §The editor protocol) says the fix is to read the
|
|
379
|
+
* declaration, and it is already here in `this.actions`.
|
|
380
|
+
*
|
|
381
|
+
* SMALLEST wins when several bindings name one source: the first magnitude at
|
|
382
|
+
* which ANY of the game's own bindings treats the control as live is the
|
|
383
|
+
* moment the player has grabbed it.
|
|
384
|
+
*/
|
|
385
|
+
private declaredDeadzone(
|
|
386
|
+
matches: (binding: InputBinding) => number | undefined,
|
|
387
|
+
documentedDefault: number,
|
|
388
|
+
): number {
|
|
389
|
+
let smallest: number | undefined;
|
|
390
|
+
for (const bindings of this.actions.values()) {
|
|
391
|
+
for (const binding of bindings) {
|
|
392
|
+
const declared = matches(binding);
|
|
393
|
+
if (declared !== undefined && (smallest === undefined || declared < smallest)) {
|
|
394
|
+
smallest = declared;
|
|
395
|
+
}
|
|
396
|
+
}
|
|
397
|
+
}
|
|
398
|
+
return smallest ?? documentedDefault;
|
|
361
399
|
}
|
|
362
400
|
// Task 1.4 — action-level virtual input
|
|
363
401
|
// for a synthetic player, promoted from hollowstone's `VirtualInput`. Held
|
|
@@ -859,15 +897,30 @@ export class InputManager {
|
|
|
859
897
|
// above. isPressed/isJustPressed/isJustReleased apply a binding's own
|
|
860
898
|
// threshold to these raw values (current vs. `gamepadAxesPrev`) on read.
|
|
861
899
|
const currentAxes = new Map<string, number>();
|
|
900
|
+
// F4 — the deadzone THIS GAME declares on each axis, resolved once per poll
|
|
901
|
+
// (see `declaredDeadzone`): an axis reading past the game's own rest
|
|
902
|
+
// threshold is unambiguous real gamepad input; anything under it is the
|
|
903
|
+
// resting noise every pad emits, and the game is already ignoring it.
|
|
904
|
+
const axisDeadzone = new Map<number, number>();
|
|
862
905
|
for (let i = 0; i < this.gamepads.length; i++) {
|
|
863
906
|
const gp = this.gamepads[i];
|
|
864
907
|
if (!gp) continue;
|
|
865
908
|
for (let a = 0; a < gp.axes.length; a++) {
|
|
866
909
|
const value = gp.axes[a] ?? 0;
|
|
867
910
|
currentAxes.set(`${i}:${a}`, value);
|
|
868
|
-
|
|
869
|
-
|
|
870
|
-
|
|
911
|
+
let deadzone = axisDeadzone.get(a);
|
|
912
|
+
if (deadzone === undefined) {
|
|
913
|
+
deadzone = this.declaredDeadzone(
|
|
914
|
+
(binding) =>
|
|
915
|
+
(binding.type === 'gamepad_axis' && binding.axis === a) ||
|
|
916
|
+
(binding.type === 'gamepad_axis_pair' && (binding.xAxis === a || binding.yAxis === a))
|
|
917
|
+
? (binding.deadzone ?? GAMEPAD_AXIS_DEFAULT_DEADZONE)
|
|
918
|
+
: undefined,
|
|
919
|
+
GAMEPAD_AXIS_DEFAULT_DEADZONE,
|
|
920
|
+
);
|
|
921
|
+
axisDeadzone.set(a, deadzone);
|
|
922
|
+
}
|
|
923
|
+
if (Math.abs(value) > deadzone) this.lastActiveDevice = 'gamepad';
|
|
871
924
|
}
|
|
872
925
|
}
|
|
873
926
|
this.gamepadAxesCurrent = currentAxes;
|
|
@@ -1011,7 +1064,7 @@ export class InputManager {
|
|
|
1011
1064
|
}
|
|
1012
1065
|
break;
|
|
1013
1066
|
case 'gamepad_axis': {
|
|
1014
|
-
const dz = binding.deadzone ??
|
|
1067
|
+
const dz = binding.deadzone ?? GAMEPAD_AXIS_DEFAULT_DEADZONE;
|
|
1015
1068
|
for (const gp of this.gamepads) {
|
|
1016
1069
|
if (!gp) continue;
|
|
1017
1070
|
const val = gp.axes[binding.axis] ?? 0;
|
|
@@ -1056,7 +1109,7 @@ export class InputManager {
|
|
|
1056
1109
|
// Edge-tracked via raw axis snapshots (poll()): true only on the
|
|
1057
1110
|
// first frame the thresholded (direction+deadzone) boolean flips
|
|
1058
1111
|
// from off→on (across any connected gamepad).
|
|
1059
|
-
const dz = binding.deadzone ??
|
|
1112
|
+
const dz = binding.deadzone ?? GAMEPAD_AXIS_DEFAULT_DEADZONE;
|
|
1060
1113
|
for (let i = 0; i < this.gamepads.length; i++) {
|
|
1061
1114
|
const key = `${i}:${binding.axis}`;
|
|
1062
1115
|
const now = this.gamepadAxesCurrent.get(key) ?? 0;
|
|
@@ -1102,7 +1155,7 @@ export class InputManager {
|
|
|
1102
1155
|
case 'gamepad_axis': {
|
|
1103
1156
|
// The mirror of isJustPressed: true only on the first frame the
|
|
1104
1157
|
// thresholded boolean flips from on→off.
|
|
1105
|
-
const dz = binding.deadzone ??
|
|
1158
|
+
const dz = binding.deadzone ?? GAMEPAD_AXIS_DEFAULT_DEADZONE;
|
|
1106
1159
|
for (let i = 0; i < this.gamepads.length; i++) {
|
|
1107
1160
|
const key = `${i}:${binding.axis}`;
|
|
1108
1161
|
const now = this.gamepadAxesCurrent.get(key) ?? 0;
|
|
@@ -1155,7 +1208,7 @@ export class InputManager {
|
|
|
1155
1208
|
}
|
|
1156
1209
|
break;
|
|
1157
1210
|
case 'gamepad_axis': {
|
|
1158
|
-
const dz = binding.deadzone ??
|
|
1211
|
+
const dz = binding.deadzone ?? GAMEPAD_AXIS_DEFAULT_DEADZONE;
|
|
1159
1212
|
for (let i = 0; i < this.gamepads.length; i++) {
|
|
1160
1213
|
const gp = this.gamepads[i];
|
|
1161
1214
|
if (!gp) continue;
|
|
@@ -1240,7 +1293,7 @@ export class InputManager {
|
|
|
1240
1293
|
if (!bindings) return out;
|
|
1241
1294
|
for (const binding of bindings) {
|
|
1242
1295
|
if (binding.type === 'gamepad_axis') {
|
|
1243
|
-
const dz = binding.deadzone ??
|
|
1296
|
+
const dz = binding.deadzone ?? GAMEPAD_AXIS_DEFAULT_DEADZONE;
|
|
1244
1297
|
for (let i = 0; i < this.gamepads.length; i++) {
|
|
1245
1298
|
const gp = this.gamepads[i];
|
|
1246
1299
|
if (!gp) continue;
|
|
@@ -1292,7 +1345,7 @@ export class InputManager {
|
|
|
1292
1345
|
private contributeGamepadAxisPair(
|
|
1293
1346
|
binding: Extract<InputBinding, { type: 'gamepad_axis_pair' }>,
|
|
1294
1347
|
): { value: Vector2; source: ActionValueSource }[] {
|
|
1295
|
-
const dz = binding.deadzone ??
|
|
1348
|
+
const dz = binding.deadzone ?? GAMEPAD_AXIS_DEFAULT_DEADZONE;
|
|
1296
1349
|
const out: { value: Vector2; source: ActionValueSource }[] = [];
|
|
1297
1350
|
for (let i = 0; i < this.gamepads.length; i++) {
|
|
1298
1351
|
const gp = this.gamepads[i];
|
|
@@ -1350,7 +1403,10 @@ export class InputManager {
|
|
|
1350
1403
|
case 'touch_stick': {
|
|
1351
1404
|
const raw = this.touchStickValues.get(binding.sourceId) ?? { x: 0, y: 0 };
|
|
1352
1405
|
out.push({
|
|
1353
|
-
value: InputManager.rescaleVector2(
|
|
1406
|
+
value: InputManager.rescaleVector2(
|
|
1407
|
+
raw,
|
|
1408
|
+
binding.deadzone ?? TOUCH_STICK_DEFAULT_DEADZONE,
|
|
1409
|
+
),
|
|
1354
1410
|
source: { bindingType: 'touch_stick', sourceId: binding.sourceId },
|
|
1355
1411
|
});
|
|
1356
1412
|
break;
|
|
@@ -1715,46 +1771,6 @@ export class InputManager {
|
|
|
1715
1771
|
for (const actionName of this.defaultBindings.keys()) this.resetBindings(actionName);
|
|
1716
1772
|
}
|
|
1717
1773
|
|
|
1718
|
-
/**
|
|
1719
|
-
* Whether `a` and `b` are the SAME physical input (F3's structural-conflict
|
|
1720
|
-
* identity check — see `findConflicts`). Deliberately narrower than deep
|
|
1721
|
-
* equality: `deadzone` never participates (two bindings on the same key
|
|
1722
|
-
* with different deadzones are still the same physical control), and a
|
|
1723
|
-
* `gamepad_axis`'s `direction` DOES participate — opposite directions of one
|
|
1724
|
-
* physical axis (e.g. "accelerate" on positive, "brake" on negative) are a
|
|
1725
|
-
* legitimate, non-conflicting pair, not a collision.
|
|
1726
|
-
*/
|
|
1727
|
-
private static bindingsCollide(a: InputBinding, b: InputBinding): boolean {
|
|
1728
|
-
if (a.type !== b.type) return false;
|
|
1729
|
-
switch (a.type) {
|
|
1730
|
-
case 'key':
|
|
1731
|
-
return a.code === (b as typeof a).code;
|
|
1732
|
-
case 'mouse_button':
|
|
1733
|
-
return a.button === (b as typeof a).button;
|
|
1734
|
-
case 'mouse_move':
|
|
1735
|
-
return true; // there is only one physical mouse-move source
|
|
1736
|
-
case 'gamepad_button':
|
|
1737
|
-
return a.button === (b as typeof a).button;
|
|
1738
|
-
case 'gamepad_axis': {
|
|
1739
|
-
const other = b as typeof a;
|
|
1740
|
-
return a.axis === other.axis && a.direction === other.direction;
|
|
1741
|
-
}
|
|
1742
|
-
case 'gamepad_axis_pair': {
|
|
1743
|
-
const other = b as typeof a;
|
|
1744
|
-
return a.xAxis === other.xAxis && a.yAxis === other.yAxis;
|
|
1745
|
-
}
|
|
1746
|
-
case 'touch_button':
|
|
1747
|
-
case 'touch_stick':
|
|
1748
|
-
case 'test_axis':
|
|
1749
|
-
case 'test_vector2':
|
|
1750
|
-
case 'test_pointer_delta':
|
|
1751
|
-
case 'test_pointer_position':
|
|
1752
|
-
return a.sourceId === (b as typeof a).sourceId;
|
|
1753
|
-
default:
|
|
1754
|
-
return false;
|
|
1755
|
-
}
|
|
1756
|
-
}
|
|
1757
|
-
|
|
1758
1774
|
/**
|
|
1759
1775
|
* Every EXISTING binding, on every OTHER registered action, that is
|
|
1760
1776
|
* structurally the same physical input as `proposed` (F3 AC "conflicts are
|
|
@@ -1769,7 +1785,7 @@ export class InputManager {
|
|
|
1769
1785
|
for (const [actionName, bindings] of this.actions) {
|
|
1770
1786
|
if (actionName === excludeActionName) continue;
|
|
1771
1787
|
bindings.forEach((binding, bindingIndex) => {
|
|
1772
|
-
if (
|
|
1788
|
+
if (inputBindingsCollide(proposed, binding)) {
|
|
1773
1789
|
conflicts.push({ actionName, bindingIndex, binding: structuredClone(binding) });
|
|
1774
1790
|
}
|
|
1775
1791
|
});
|
|
@@ -1995,8 +2011,17 @@ export class InputManager {
|
|
|
1995
2011
|
*/
|
|
1996
2012
|
setTouchStick(sourceId: string, value: Vector2): void {
|
|
1997
2013
|
this.touchStickValues.set(sourceId, value);
|
|
1998
|
-
// F4 —
|
|
1999
|
-
|
|
2014
|
+
// F4 — same rule as poll()'s gamepad axes (see `declaredDeadzone`): the
|
|
2015
|
+
// deadzone the game declared on THIS virtual stick decides whether the knob
|
|
2016
|
+
// has left rest, not a number this class picked.
|
|
2017
|
+
const deadzone = this.declaredDeadzone(
|
|
2018
|
+
(binding) =>
|
|
2019
|
+
binding.type === 'touch_stick' && binding.sourceId === sourceId
|
|
2020
|
+
? (binding.deadzone ?? TOUCH_STICK_DEFAULT_DEADZONE)
|
|
2021
|
+
: undefined,
|
|
2022
|
+
TOUCH_STICK_DEFAULT_DEADZONE,
|
|
2023
|
+
);
|
|
2024
|
+
if (Math.hypot(value.x, value.y) > deadzone) this.lastActiveDevice = 'touch';
|
|
2000
2025
|
}
|
|
2001
2026
|
|
|
2002
2027
|
/** Clear a virtual stick's value (e.g. simulating the touch point lifting —
|