@vgai/engine 0.5.42 → 0.5.44
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 +28 -29
- package/dist/adapter/adapter-module.d.ts.map +1 -1
- package/dist/adapter/adapter-module.js +11 -34
- package/dist/adapter/authoring-seam-contract.d.ts +5 -0
- package/dist/adapter/authoring-seam-contract.d.ts.map +1 -1
- package/dist/adapter/authoring-seam-contract.js +1 -0
- package/dist/adapter/authoring.d.ts +16 -0
- package/dist/adapter/authoring.d.ts.map +1 -1
- package/dist/adapter/finders/finder-result.d.ts +2 -2
- package/dist/adapter/finders/finder-result.d.ts.map +1 -1
- package/dist/adapter/finders/index.d.ts +15 -2
- package/dist/adapter/finders/index.d.ts.map +1 -1
- package/dist/adapter/finders/index.js +38 -10
- package/dist/adapter/finders/prefabs-from-stories.d.ts.map +1 -1
- package/dist/adapter/finders/registry.d.ts +45 -0
- package/dist/adapter/finders/registry.d.ts.map +1 -0
- package/dist/adapter/finders/registry.js +56 -0
- package/dist/adapter/ingest/scene-capture.d.ts.map +1 -1
- package/dist/adapter/ingest/scene-capture.js +50 -20
- package/dist/adapter/system-adapter.d.ts +3 -1
- package/dist/adapter/system-adapter.d.ts.map +1 -1
- package/dist/canvas-react/pixi-react-root-factory.d.ts.map +1 -1
- package/dist/canvas-react/pixi-react-root-factory.js +21 -11
- package/dist/data/vite-plugin-data.d.ts +1 -1
- package/dist/data/vite-plugin-data.d.ts.map +1 -1
- package/dist/data/vite-plugin-data.js +2 -2
- package/dist/index.d.ts +1 -1
- package/dist/index.d.ts.map +1 -1
- package/dist/index.js +1 -1
- package/dist/input/host-pointer.d.ts +18 -0
- package/dist/input/host-pointer.d.ts.map +1 -1
- package/dist/input/host-pointer.js +54 -5
- package/dist/input/input-manager.d.ts +83 -1
- package/dist/input/input-manager.d.ts.map +1 -1
- package/dist/input/input-manager.js +357 -8
- package/dist/manifest/configuration-kinds.d.ts +97 -0
- package/dist/manifest/configuration-kinds.d.ts.map +1 -0
- package/dist/manifest/configuration-kinds.js +148 -0
- package/dist/manifest/index.d.ts +2 -1
- package/dist/manifest/index.d.ts.map +1 -1
- package/dist/manifest/index.js +4 -3
- package/dist/manifest/kind-modules.d.ts +18 -0
- package/dist/manifest/kind-modules.d.ts.map +1 -0
- package/dist/manifest/kind-modules.js +58 -0
- package/dist/manifest/load-file.d.ts +3 -3
- package/dist/manifest/load-file.d.ts.map +1 -1
- package/dist/manifest/load-file.js +4 -4
- package/dist/manifest/load.d.ts +17 -12
- package/dist/manifest/load.d.ts.map +1 -1
- package/dist/manifest/load.js +29 -19
- package/dist/manifest/runtime-environment.d.ts +11 -0
- package/dist/manifest/runtime-environment.d.ts.map +1 -0
- package/dist/manifest/runtime-environment.js +44 -0
- package/dist/manifest/schema.d.ts +10 -9
- package/dist/manifest/schema.d.ts.map +1 -1
- package/dist/manifest/schema.js +32 -27
- package/dist/react/use-data.d.ts +1 -1
- package/dist/react/use-data.js +1 -1
- package/dist/render/matcap-texture.d.ts +5 -0
- package/dist/render/matcap-texture.d.ts.map +1 -0
- package/dist/render/matcap-texture.js +82 -0
- package/dist/render/soft-particle-depth.d.ts +14 -8
- package/dist/render/soft-particle-depth.d.ts.map +1 -1
- package/dist/render/soft-particle-depth.js +37 -14
- package/dist/render/viewport-shading.d.ts +11 -1
- package/dist/render/viewport-shading.d.ts.map +1 -1
- package/dist/render/viewport-shading.js +25 -0
- package/dist/runtime/create-runtime.js +6 -5
- package/dist/runtime/mount-manifest.d.ts.map +1 -1
- package/dist/runtime/mount-manifest.js +11 -2
- package/dist/settings/schema.d.ts +99 -0
- package/dist/settings/schema.d.ts.map +1 -0
- package/dist/settings/schema.js +110 -0
- package/dist/world3d-react/r3f-root-factory.d.ts.map +1 -1
- package/dist/world3d-react/r3f-root-factory.js +34 -36
- package/dist/world3d-react/rapier-physics-bridge.d.ts.map +1 -1
- package/dist/world3d-react/rapier-physics-bridge.js +21 -6
- package/dist-config/config.js +1 -1
- package/dist-config/config.js.map +2 -2
- package/package.json +2 -1
- package/schemas/engine-capabilities.json +4 -4
- package/schemas/vgai-project.schema.json +41 -36
- package/schemas/vgai-settings.schema.json +112 -0
- package/src/adapter/adapter-module.ts +33 -51
- package/src/adapter/authoring-seam-contract.ts +1 -0
- package/src/adapter/authoring.ts +16 -0
- package/src/adapter/finders/finder-result.ts +2 -2
- package/src/adapter/finders/index.ts +68 -12
- package/src/adapter/finders/prefabs-from-stories.ts +2 -2
- package/src/adapter/finders/registry.ts +95 -0
- package/src/adapter/finders/scenes-from-entrypoint-selection.ts +4 -4
- package/src/adapter/ingest/scene-capture.ts +48 -19
- package/src/adapter/system-adapter.ts +3 -1
- package/src/canvas-react/pixi-react-root-factory.tsx +21 -11
- package/src/data/vite-plugin-data.ts +4 -2
- package/src/index.ts +1 -1
- package/src/input/host-pointer.ts +84 -5
- package/src/input/input-manager.ts +404 -7
- package/src/manifest/configuration-kinds.ts +241 -0
- package/src/manifest/index.ts +4 -3
- package/src/manifest/kind-modules.ts +58 -0
- package/src/manifest/load-file.ts +11 -5
- package/src/manifest/load.ts +53 -22
- package/src/manifest/runtime-environment.ts +48 -0
- package/src/manifest/schema.ts +39 -33
- package/src/react/use-data.ts +1 -1
- package/src/render/matcap-texture.ts +92 -0
- package/src/render/soft-particle-depth.ts +43 -14
- package/src/render/viewport-shading.ts +33 -1
- package/src/runtime/create-runtime.ts +6 -5
- package/src/runtime/mount-manifest.ts +13 -2
- package/src/settings/schema.ts +127 -0
- package/src/world3d-react/r3f-root-factory.tsx +45 -39
- package/src/world3d-react/rapier-physics-bridge.tsx +22 -8
package/src/index.ts
CHANGED
|
@@ -32,7 +32,7 @@ export type {
|
|
|
32
32
|
ResolvedGameManifest,
|
|
33
33
|
RootAdapter,
|
|
34
34
|
} from './manifest';
|
|
35
|
-
export {
|
|
35
|
+
export { loadGameManifest } from './manifest';
|
|
36
36
|
export type { GameSession } from './runtime/create-runtime';
|
|
37
37
|
export type {
|
|
38
38
|
MountEntry,
|
|
@@ -72,6 +72,8 @@ export interface HostPointerTouch {
|
|
|
72
72
|
* differently is holding a button nothing will ever release.
|
|
73
73
|
*/
|
|
74
74
|
readonly phase: 'start' | 'move' | 'end';
|
|
75
|
+
/** True only when the browser ended this contact with `pointercancel`. */
|
|
76
|
+
readonly canceled: boolean;
|
|
75
77
|
/**
|
|
76
78
|
* DENSE finger index — the lowest slot free when this finger went down, and
|
|
77
79
|
* free again the moment it lifts. NOT the browser's `pointerId`, which never
|
|
@@ -80,6 +82,22 @@ export interface HostPointerTouch {
|
|
|
80
82
|
readonly index: number;
|
|
81
83
|
/** Position in the design space (see {@link HostPointerOptions.designSize}). */
|
|
82
84
|
readonly position: Vector2;
|
|
85
|
+
/** Position before design-resolution scaling, in surface CSS pixels. */
|
|
86
|
+
readonly screenPosition: Vector2;
|
|
87
|
+
/** Delta since this contact's preceding sample, in design pixels. */
|
|
88
|
+
readonly relative: Vector2;
|
|
89
|
+
/** Delta since this contact's preceding sample, in surface CSS pixels. */
|
|
90
|
+
readonly screenRelative: Vector2;
|
|
91
|
+
/** Design-pixel velocity derived from the browser event timestamps. */
|
|
92
|
+
readonly velocity: Vector2;
|
|
93
|
+
/** Surface CSS-pixel velocity derived from the browser event timestamps. */
|
|
94
|
+
readonly screenVelocity: Vector2;
|
|
95
|
+
/** Native PointerEvent pressure in the normalized [0, 1] range. */
|
|
96
|
+
readonly pressure: number;
|
|
97
|
+
/** Native PointerEvent tilt, in degrees. Touch contacts normally report zero. */
|
|
98
|
+
readonly tilt: Vector2;
|
|
99
|
+
/** Touch pointers are never an inverted pen; retained for Godot's event value contract. */
|
|
100
|
+
readonly penInverted: false;
|
|
83
101
|
}
|
|
84
102
|
|
|
85
103
|
/** What {@link createHostPointer} needs. */
|
|
@@ -141,6 +159,14 @@ export function createHostPointer(options: HostPointerOptions): HostPointer {
|
|
|
141
159
|
const touches: HostPointerTouch[] = [];
|
|
142
160
|
/** Browser `pointerId` -> dense finger index, for fingers currently down. */
|
|
143
161
|
const fingerOfPointer = new Map<number, number>();
|
|
162
|
+
const sampleOfPointer = new Map<
|
|
163
|
+
number,
|
|
164
|
+
{
|
|
165
|
+
readonly position: Vector2;
|
|
166
|
+
readonly screenPosition: Vector2;
|
|
167
|
+
readonly timestamp: number;
|
|
168
|
+
}
|
|
169
|
+
>();
|
|
144
170
|
const freeFinger = (): number => {
|
|
145
171
|
const taken = new Set(fingerOfPointer.values());
|
|
146
172
|
let index = 0;
|
|
@@ -152,11 +178,52 @@ export function createHostPointer(options: HostPointerOptions): HostPointer {
|
|
|
152
178
|
if (touches.length > TOUCH_QUEUE_MAX) touches.shift();
|
|
153
179
|
};
|
|
154
180
|
|
|
155
|
-
const queueTouch = (event: PointerEvent, point: Vector2): void => {
|
|
181
|
+
const queueTouch = (event: PointerEvent, point: Vector2, screenPoint: Vector2): void => {
|
|
182
|
+
const previous = sampleOfPointer.get(event.pointerId);
|
|
183
|
+
const relative =
|
|
184
|
+
previous === undefined
|
|
185
|
+
? { x: 0, y: 0 }
|
|
186
|
+
: { x: point.x - previous.position.x, y: point.y - previous.position.y };
|
|
187
|
+
const screenRelative =
|
|
188
|
+
previous === undefined
|
|
189
|
+
? { x: 0, y: 0 }
|
|
190
|
+
: {
|
|
191
|
+
x: screenPoint.x - previous.screenPosition.x,
|
|
192
|
+
y: screenPoint.y - previous.screenPosition.y,
|
|
193
|
+
};
|
|
194
|
+
const elapsedSeconds =
|
|
195
|
+
previous === undefined ? 0 : Math.max(0, event.timeStamp - previous.timestamp) / 1000;
|
|
196
|
+
const velocity =
|
|
197
|
+
elapsedSeconds > 0
|
|
198
|
+
? { x: relative.x / elapsedSeconds, y: relative.y / elapsedSeconds }
|
|
199
|
+
: { x: 0, y: 0 };
|
|
200
|
+
const screenVelocity =
|
|
201
|
+
elapsedSeconds > 0
|
|
202
|
+
? { x: screenRelative.x / elapsedSeconds, y: screenRelative.y / elapsedSeconds }
|
|
203
|
+
: { x: 0, y: 0 };
|
|
204
|
+
const common = {
|
|
205
|
+
position: point,
|
|
206
|
+
screenPosition: screenPoint,
|
|
207
|
+
relative,
|
|
208
|
+
screenRelative,
|
|
209
|
+
velocity,
|
|
210
|
+
screenVelocity,
|
|
211
|
+
pressure: Number.isFinite(event.pressure) ? Math.max(0, Math.min(1, event.pressure)) : 0,
|
|
212
|
+
tilt: {
|
|
213
|
+
x: Number.isFinite(event.tiltX) ? event.tiltX : 0,
|
|
214
|
+
y: Number.isFinite(event.tiltY) ? event.tiltY : 0,
|
|
215
|
+
},
|
|
216
|
+
penInverted: false as const,
|
|
217
|
+
};
|
|
156
218
|
if (event.type === 'pointerdown') {
|
|
157
219
|
const index = freeFinger();
|
|
158
220
|
fingerOfPointer.set(event.pointerId, index);
|
|
159
|
-
|
|
221
|
+
sampleOfPointer.set(event.pointerId, {
|
|
222
|
+
position: point,
|
|
223
|
+
screenPosition: screenPoint,
|
|
224
|
+
timestamp: event.timeStamp,
|
|
225
|
+
});
|
|
226
|
+
queue({ phase: 'start', canceled: false, index, ...common });
|
|
160
227
|
return;
|
|
161
228
|
}
|
|
162
229
|
// A move or a release from a pointer that never went down on this surface
|
|
@@ -165,10 +232,21 @@ export function createHostPointer(options: HostPointerOptions): HostPointer {
|
|
|
165
232
|
if (index === undefined) return;
|
|
166
233
|
if (event.type === 'pointerup' || event.type === 'pointercancel') {
|
|
167
234
|
fingerOfPointer.delete(event.pointerId);
|
|
168
|
-
|
|
235
|
+
sampleOfPointer.delete(event.pointerId);
|
|
236
|
+
queue({
|
|
237
|
+
phase: 'end',
|
|
238
|
+
canceled: event.type === 'pointercancel',
|
|
239
|
+
index,
|
|
240
|
+
...common,
|
|
241
|
+
});
|
|
169
242
|
return;
|
|
170
243
|
}
|
|
171
|
-
|
|
244
|
+
sampleOfPointer.set(event.pointerId, {
|
|
245
|
+
position: point,
|
|
246
|
+
screenPosition: screenPoint,
|
|
247
|
+
timestamp: event.timeStamp,
|
|
248
|
+
});
|
|
249
|
+
queue({ phase: 'move', canceled: false, index, ...common });
|
|
172
250
|
};
|
|
173
251
|
|
|
174
252
|
/** A client-space event as a point in the design space, via the surface rect
|
|
@@ -188,6 +266,7 @@ export function createHostPointer(options: HostPointerOptions): HostPointer {
|
|
|
188
266
|
if (surface === null) return;
|
|
189
267
|
const rect = surface.getBoundingClientRect();
|
|
190
268
|
if (rect.width <= 0 || rect.height <= 0) return;
|
|
269
|
+
const screenPoint = { x: event.clientX - rect.left, y: event.clientY - rect.top };
|
|
191
270
|
const point = toDesignSpace(event, rect);
|
|
192
271
|
|
|
193
272
|
if (event.type === 'pointermove') {
|
|
@@ -199,7 +278,7 @@ export function createHostPointer(options: HostPointerOptions): HostPointer {
|
|
|
199
278
|
}
|
|
200
279
|
// A mouse or a pen is not a finger. A control that answered them here would
|
|
201
280
|
// steal the clicks of a player who cannot see it.
|
|
202
|
-
if (event.pointerType === 'touch') queueTouch(event, point);
|
|
281
|
+
if (event.pointerType === 'touch') queueTouch(event, point, screenPoint);
|
|
203
282
|
};
|
|
204
283
|
|
|
205
284
|
const KINDS = ['pointerdown', 'pointermove', 'pointerup', 'pointercancel'] as const;
|
|
@@ -81,6 +81,41 @@ export class InputTickError extends Error {
|
|
|
81
81
|
}
|
|
82
82
|
}
|
|
83
83
|
|
|
84
|
+
/** One named digital-pair axis whose interpolation is owned by {@link InputManager}.
|
|
85
|
+
*
|
|
86
|
+
* Foreign runtimes translate their authored axis table into this neutral carrier, then keep their
|
|
87
|
+
* public API at the compat boundary. The manager owns the live values because the values are a
|
|
88
|
+
* derived view of its action state, not a second device/input store. */
|
|
89
|
+
export interface SmoothedInputAxisDefinition {
|
|
90
|
+
readonly name: string;
|
|
91
|
+
readonly negativeAction: string;
|
|
92
|
+
readonly positiveAction: string;
|
|
93
|
+
/** Units per second when returning to zero. */
|
|
94
|
+
readonly gravity: number;
|
|
95
|
+
/** Magnitudes below this threshold read as zero without changing retained interpolation. */
|
|
96
|
+
readonly dead: number;
|
|
97
|
+
/** Units per second when approaching a pressed direction. */
|
|
98
|
+
readonly sensitivity: number;
|
|
99
|
+
/** Reset to zero before accelerating when the newly pressed direction opposes the old value. */
|
|
100
|
+
readonly snap: boolean;
|
|
101
|
+
readonly invert: boolean;
|
|
102
|
+
}
|
|
103
|
+
|
|
104
|
+
/** Scene-owned handle over an InputManager-owned set of smoothed axes. */
|
|
105
|
+
export interface SmoothedInputAxisSet {
|
|
106
|
+
advance(deltaTime: number): void;
|
|
107
|
+
getAxis(name: string): number;
|
|
108
|
+
getAxisRaw(name: string): number;
|
|
109
|
+
snapshot(): Record<string, number>;
|
|
110
|
+
dispose(): void;
|
|
111
|
+
}
|
|
112
|
+
|
|
113
|
+
interface SmoothedInputAxisSetState {
|
|
114
|
+
readonly definitions: ReadonlyMap<string, SmoothedInputAxisDefinition>;
|
|
115
|
+
readonly values: Map<string, number>;
|
|
116
|
+
disposed: boolean;
|
|
117
|
+
}
|
|
118
|
+
|
|
84
119
|
/** Shallow equality for a resolved action value (boolean/number/Vector2) —
|
|
85
120
|
* D15/T-D15.5's recording tap uses this to emit DELTAS only (the trace
|
|
86
121
|
* format's "deltas only" line), never a full snapshot every tick. `prev`
|
|
@@ -415,6 +450,8 @@ export class InputManager {
|
|
|
415
450
|
// level values feeding the same max-magnitude/sum combine every real
|
|
416
451
|
// binding uses (collectScalarContributions/collectVector2Contributions).
|
|
417
452
|
private virtualDigitalHeld = new Set<string>();
|
|
453
|
+
/** Processed strength attached to a held virtual digital action. */
|
|
454
|
+
private virtualDigitalStrength = new Map<string, number>();
|
|
418
455
|
private virtualDigitalTapQueued = new Set<string>();
|
|
419
456
|
private virtualDigitalTapActive = new Set<string>();
|
|
420
457
|
/** pulse-runner friction #4 — the release-edge mirror of
|
|
@@ -428,6 +465,9 @@ export class InputManager {
|
|
|
428
465
|
private virtualDigitalJustReleased = new Set<string>();
|
|
429
466
|
private virtualScalarValues = new Map<string, number>();
|
|
430
467
|
private virtualVector2Values = new Map<string, Vector2>();
|
|
468
|
+
/** Derived digital-pair axes owned by mounted foreign runtimes. Their public GetAxis-shaped
|
|
469
|
+
* calls remain in compat; only interpolation state and action reads live beside their source. */
|
|
470
|
+
private smoothedAxisSets = new Set<SmoothedInputAxisSetState>();
|
|
431
471
|
// D15/T-D15.5 — tick-indexed input scheduling + post-gate recording.
|
|
432
472
|
//
|
|
433
473
|
// `currentTick` is a FALLBACK, self-incrementing counter, used only when a
|
|
@@ -524,8 +564,61 @@ export class InputManager {
|
|
|
524
564
|
private pointerLocked = false;
|
|
525
565
|
/** Element currently wired for click-to-pointer-lock, and its listener. */
|
|
526
566
|
private pointerLockElement: HTMLElement | null = null;
|
|
567
|
+
/** One report per refusal reason: a refused lock is the whole story of
|
|
568
|
+
* "the mouse doesn't aim", and it was silent. */
|
|
569
|
+
private reportedPointerLockRefusals = new Set<string>();
|
|
570
|
+
private reportPointerLockRefused(reason: string): void {
|
|
571
|
+
if (this.reportedPointerLockRefusals.has(reason)) return;
|
|
572
|
+
this.reportedPointerLockRefusals.add(reason);
|
|
573
|
+
// biome-ignore lint/suspicious/noConsole: the game's own input intent failed in the browser — an error the editor console and a tester's evidence must both carry
|
|
574
|
+
console.error(
|
|
575
|
+
`Pointer lock was refused, so mouse look and mouse aim cannot engage: ${reason}. ` +
|
|
576
|
+
'The browser grants it only to a user gesture on a page it trusts — a page inside a ' +
|
|
577
|
+
'frame needs allow="pointer-lock", and some extensions block it.',
|
|
578
|
+
);
|
|
579
|
+
}
|
|
580
|
+
/** A retry already scheduled for Chrome's post-exit cooldown, if any. */
|
|
581
|
+
private pointerLockRetry: ReturnType<typeof setTimeout> | null = null;
|
|
527
582
|
private onPointerLockClick = () => {
|
|
528
|
-
|
|
583
|
+
// Chrome answers with a promise that REJECTS when the lock is refused (a
|
|
584
|
+
// sandboxed/automated document, a policy, a frame without the permission).
|
|
585
|
+
// Three testers in a row reported "the mouse doesn't aim" on the hosted
|
|
586
|
+
// editor while automation could not lock at all — and the refusal was
|
|
587
|
+
// caught silently (runhuman passes 122–126). It is reported now, once
|
|
588
|
+
// per reason, as the error it is.
|
|
589
|
+
const request = this.pointerLockElement?.requestPointerLock() as unknown;
|
|
590
|
+
if (request instanceof Promise) {
|
|
591
|
+
request.catch((error: unknown) => {
|
|
592
|
+
const reason = error instanceof Error ? error.message : String(error);
|
|
593
|
+
// CHROME'S COOLDOWN IS NOT A REFUSAL. After Escape ends a lock, a
|
|
594
|
+
// request in the next ~1.25 s is rejected with "cannot be acquired
|
|
595
|
+
// immediately after the user has exited the lock" — and a player who
|
|
596
|
+
// clicks back into the game right away gets no aim and no second
|
|
597
|
+
// chance (runhuman pass 130: "I can't move the camera with the
|
|
598
|
+
// mouse"). The click's activation still stands for a few seconds, so
|
|
599
|
+
// the request is retried once when the cooldown has passed.
|
|
600
|
+
if (/exited the lock/i.test(reason) && this.pointerLockRetry === null) {
|
|
601
|
+
this.pointerLockRetry = setTimeout(() => {
|
|
602
|
+
this.pointerLockRetry = null;
|
|
603
|
+
const doc = typeof document !== 'undefined' ? document : null;
|
|
604
|
+
const activation =
|
|
605
|
+
typeof navigator !== 'undefined' ? navigator.userActivation : undefined;
|
|
606
|
+
if (!this.pointerLockElement || doc?.pointerLockElement === this.pointerLockElement) {
|
|
607
|
+
return;
|
|
608
|
+
}
|
|
609
|
+
if (activation?.isActive) {
|
|
610
|
+
this.onPointerLockClick();
|
|
611
|
+
} else {
|
|
612
|
+
// The cooldown outlived the click's activation: the retry
|
|
613
|
+
// cannot run, and a silent end here is "mouse dead, no error".
|
|
614
|
+
this.reportPointerLockRefused(`${reason} (the retry found no user activation left)`);
|
|
615
|
+
}
|
|
616
|
+
}, 1_300);
|
|
617
|
+
return;
|
|
618
|
+
}
|
|
619
|
+
this.reportPointerLockRefused(reason);
|
|
620
|
+
});
|
|
621
|
+
}
|
|
529
622
|
};
|
|
530
623
|
private onPointerLockChange = () => {
|
|
531
624
|
const doc = typeof document !== 'undefined' ? document : null;
|
|
@@ -654,6 +747,7 @@ export class InputManager {
|
|
|
654
747
|
}
|
|
655
748
|
}
|
|
656
749
|
this.virtualDigitalHeld.clear();
|
|
750
|
+
this.virtualDigitalStrength.clear();
|
|
657
751
|
this.virtualDigitalTapQueued.clear();
|
|
658
752
|
this.virtualDigitalTapActive.clear();
|
|
659
753
|
this.virtualScalarValues.clear();
|
|
@@ -799,6 +893,116 @@ export class InputManager {
|
|
|
799
893
|
return [...this.actions.keys()];
|
|
800
894
|
}
|
|
801
895
|
|
|
896
|
+
/**
|
|
897
|
+
* Create a retained, action-backed set of digital axes. Foreign runtimes keep their public
|
|
898
|
+
* GetAxis-shaped API in compat, while interpolation and the live action reads stay in the input
|
|
899
|
+
* owner instead of becoming a parallel compat input store.
|
|
900
|
+
*/
|
|
901
|
+
createSmoothedAxisSet(definitions: readonly SmoothedInputAxisDefinition[]): SmoothedInputAxisSet {
|
|
902
|
+
const byName = new Map<string, SmoothedInputAxisDefinition>();
|
|
903
|
+
for (const source of definitions) {
|
|
904
|
+
const definition = { ...source };
|
|
905
|
+
if (definition.name.length === 0) {
|
|
906
|
+
throw new Error('InputManager.createSmoothedAxisSet: axis names must not be empty');
|
|
907
|
+
}
|
|
908
|
+
if (byName.has(definition.name)) {
|
|
909
|
+
throw new Error(
|
|
910
|
+
`InputManager.createSmoothedAxisSet: duplicate axis ${JSON.stringify(definition.name)}`,
|
|
911
|
+
);
|
|
912
|
+
}
|
|
913
|
+
if (definition.negativeAction.length === 0 || definition.positiveAction.length === 0) {
|
|
914
|
+
throw new Error(
|
|
915
|
+
`InputManager.createSmoothedAxisSet: axis ${JSON.stringify(definition.name)} requires ` +
|
|
916
|
+
'both negative and positive actions',
|
|
917
|
+
);
|
|
918
|
+
}
|
|
919
|
+
for (const [field, value] of [
|
|
920
|
+
['gravity', definition.gravity],
|
|
921
|
+
['dead', definition.dead],
|
|
922
|
+
['sensitivity', definition.sensitivity],
|
|
923
|
+
] as const) {
|
|
924
|
+
if (!Number.isFinite(value) || value < 0) {
|
|
925
|
+
throw new Error(
|
|
926
|
+
`InputManager.createSmoothedAxisSet: ${definition.name}.${field} must be a finite ` +
|
|
927
|
+
`non-negative number, received ${String(value)}`,
|
|
928
|
+
);
|
|
929
|
+
}
|
|
930
|
+
}
|
|
931
|
+
byName.set(definition.name, Object.freeze(definition));
|
|
932
|
+
}
|
|
933
|
+
|
|
934
|
+
const state: SmoothedInputAxisSetState = {
|
|
935
|
+
definitions: byName,
|
|
936
|
+
values: new Map(),
|
|
937
|
+
disposed: false,
|
|
938
|
+
};
|
|
939
|
+
this.smoothedAxisSets.add(state);
|
|
940
|
+
|
|
941
|
+
const assertLive = (): void => {
|
|
942
|
+
if (state.disposed || this.disposed) {
|
|
943
|
+
throw new Error('InputManager smoothed axis set has been disposed');
|
|
944
|
+
}
|
|
945
|
+
};
|
|
946
|
+
const definitionOf = (name: string): SmoothedInputAxisDefinition => {
|
|
947
|
+
assertLive();
|
|
948
|
+
const definition = state.definitions.get(name);
|
|
949
|
+
if (definition === undefined) {
|
|
950
|
+
throw new Error(`InputManager smoothed axis set has no axis ${JSON.stringify(name)}`);
|
|
951
|
+
}
|
|
952
|
+
return definition;
|
|
953
|
+
};
|
|
954
|
+
const rawValue = (definition: SmoothedInputAxisDefinition): number => {
|
|
955
|
+
const negative = this.isPressed(definition.negativeAction) ? -1 : 0;
|
|
956
|
+
const positive = this.isPressed(definition.positiveAction) ? 1 : 0;
|
|
957
|
+
return (negative + positive) * (definition.invert ? -1 : 1);
|
|
958
|
+
};
|
|
959
|
+
const getAxis = (name: string): number => {
|
|
960
|
+
const definition = definitionOf(name);
|
|
961
|
+
const value = state.values.get(name) ?? 0;
|
|
962
|
+
return Math.abs(value) < definition.dead ? 0 : value;
|
|
963
|
+
};
|
|
964
|
+
|
|
965
|
+
return {
|
|
966
|
+
advance: (deltaTime) => {
|
|
967
|
+
assertLive();
|
|
968
|
+
if (!Number.isFinite(deltaTime) || deltaTime < 0) {
|
|
969
|
+
throw new Error(
|
|
970
|
+
`InputManager smoothed axis deltaTime must be finite and non-negative, received ` +
|
|
971
|
+
String(deltaTime),
|
|
972
|
+
);
|
|
973
|
+
}
|
|
974
|
+
for (const definition of state.definitions.values()) {
|
|
975
|
+
const target = rawValue(definition);
|
|
976
|
+
const current = state.values.get(definition.name) ?? 0;
|
|
977
|
+
let next: number;
|
|
978
|
+
if (target === 0) {
|
|
979
|
+
const step = definition.gravity * deltaTime;
|
|
980
|
+
next = current > 0 ? Math.max(0, current - step) : Math.min(0, current + step);
|
|
981
|
+
} else {
|
|
982
|
+
const from = definition.snap && current * target < 0 ? 0 : current;
|
|
983
|
+
const step = definition.sensitivity * deltaTime;
|
|
984
|
+
next = target > 0 ? Math.min(target, from + step) : Math.max(target, from - step);
|
|
985
|
+
}
|
|
986
|
+
state.values.set(definition.name, next);
|
|
987
|
+
}
|
|
988
|
+
},
|
|
989
|
+
getAxis,
|
|
990
|
+
getAxisRaw: (name) => rawValue(definitionOf(name)),
|
|
991
|
+
snapshot: () => {
|
|
992
|
+
assertLive();
|
|
993
|
+
const snapshot: Record<string, number> = {};
|
|
994
|
+
for (const name of state.definitions.keys()) snapshot[name] = getAxis(name);
|
|
995
|
+
return snapshot;
|
|
996
|
+
},
|
|
997
|
+
dispose: () => {
|
|
998
|
+
if (state.disposed) return;
|
|
999
|
+
state.disposed = true;
|
|
1000
|
+
state.values.clear();
|
|
1001
|
+
this.smoothedAxisSets.delete(state);
|
|
1002
|
+
},
|
|
1003
|
+
};
|
|
1004
|
+
}
|
|
1005
|
+
|
|
802
1006
|
/**
|
|
803
1007
|
* Poll gamepads and virtual look stick (call at start of frame).
|
|
804
1008
|
*
|
|
@@ -1014,9 +1218,11 @@ export class InputManager {
|
|
|
1014
1218
|
if (typeof entry.value === 'boolean') {
|
|
1015
1219
|
if (entry.value) {
|
|
1016
1220
|
this.virtualDigitalHeld.add(entry.action);
|
|
1221
|
+
this.virtualDigitalStrength.set(entry.action, 1);
|
|
1017
1222
|
this.virtualDigitalTapActive.add(entry.action);
|
|
1018
1223
|
} else {
|
|
1019
1224
|
this.virtualDigitalHeld.delete(entry.action);
|
|
1225
|
+
this.virtualDigitalStrength.delete(entry.action);
|
|
1020
1226
|
}
|
|
1021
1227
|
} else {
|
|
1022
1228
|
this.applyVirtualActionValue(entry.action, entry.value);
|
|
@@ -1114,6 +1320,110 @@ export class InputManager {
|
|
|
1114
1320
|
return false;
|
|
1115
1321
|
}
|
|
1116
1322
|
|
|
1323
|
+
/**
|
|
1324
|
+
* Processed strength for a declared digital action without creating a
|
|
1325
|
+
* second action-state store. Digital keys/buttons/touches contribute 1,
|
|
1326
|
+
* while a directional gamepad axis contributes Godot's deadzone-rescaled
|
|
1327
|
+
* magnitude. Multiple bindings combine by greatest strength, matching the
|
|
1328
|
+
* action state that {@link isPressed} reads as a boolean.
|
|
1329
|
+
*/
|
|
1330
|
+
getDigitalStrength(actionName: string): number {
|
|
1331
|
+
this.assertValueType(actionName, 'digital', 'getDigitalStrength');
|
|
1332
|
+
if (
|
|
1333
|
+
this.machineInputActive &&
|
|
1334
|
+
(this.virtualDigitalHeld.has(actionName) || this.virtualDigitalTapActive.has(actionName))
|
|
1335
|
+
) {
|
|
1336
|
+
return this.virtualDigitalStrength.get(actionName) ?? 1;
|
|
1337
|
+
}
|
|
1338
|
+
if (!this.inputActive) return 0;
|
|
1339
|
+
const bindings = this.actions.get(actionName);
|
|
1340
|
+
if (!bindings) return 0;
|
|
1341
|
+
|
|
1342
|
+
let strength = 0;
|
|
1343
|
+
for (const binding of bindings) {
|
|
1344
|
+
switch (binding.type) {
|
|
1345
|
+
case 'key':
|
|
1346
|
+
if (this.keysDown.has(binding.code)) return 1;
|
|
1347
|
+
break;
|
|
1348
|
+
case 'mouse_button':
|
|
1349
|
+
if (this.mouseButtons.has(binding.button)) return 1;
|
|
1350
|
+
break;
|
|
1351
|
+
case 'gamepad_button':
|
|
1352
|
+
for (const gp of this.gamepads) {
|
|
1353
|
+
const button = gp?.buttons[binding.button];
|
|
1354
|
+
// Pinned InputEventJoypadButton::action_match reports 1 for a
|
|
1355
|
+
// pressed button and 0 otherwise; GamepadButton.value is not its
|
|
1356
|
+
// action strength.
|
|
1357
|
+
if (button?.pressed) return 1;
|
|
1358
|
+
}
|
|
1359
|
+
break;
|
|
1360
|
+
case 'gamepad_axis': {
|
|
1361
|
+
const deadzone = binding.deadzone ?? GAMEPAD_AXIS_DEFAULT_DEADZONE;
|
|
1362
|
+
for (const gp of this.gamepads) {
|
|
1363
|
+
if (!gp) continue;
|
|
1364
|
+
const raw = gp.axes[binding.axis] ?? 0;
|
|
1365
|
+
const directional = binding.direction === 'positive' ? raw : -raw;
|
|
1366
|
+
if (directional < deadzone) continue;
|
|
1367
|
+
strength = Math.max(
|
|
1368
|
+
strength,
|
|
1369
|
+
deadzone === 1
|
|
1370
|
+
? 1
|
|
1371
|
+
: Math.min(1, Math.max(0, (directional - deadzone) / (1 - deadzone))),
|
|
1372
|
+
);
|
|
1373
|
+
}
|
|
1374
|
+
break;
|
|
1375
|
+
}
|
|
1376
|
+
case 'touch_button':
|
|
1377
|
+
if (this.touchButtonsDown.has(binding.sourceId)) return 1;
|
|
1378
|
+
break;
|
|
1379
|
+
}
|
|
1380
|
+
}
|
|
1381
|
+
return strength;
|
|
1382
|
+
}
|
|
1383
|
+
|
|
1384
|
+
/** Raw (pre-deadzone) strength for a declared digital action. */
|
|
1385
|
+
getDigitalRawStrength(actionName: string): number {
|
|
1386
|
+
this.assertValueType(actionName, 'digital', 'getDigitalRawStrength');
|
|
1387
|
+
if (
|
|
1388
|
+
this.machineInputActive &&
|
|
1389
|
+
(this.virtualDigitalHeld.has(actionName) || this.virtualDigitalTapActive.has(actionName))
|
|
1390
|
+
) {
|
|
1391
|
+
return this.virtualDigitalStrength.get(actionName) ?? 1;
|
|
1392
|
+
}
|
|
1393
|
+
if (!this.inputActive) return 0;
|
|
1394
|
+
const bindings = this.actions.get(actionName);
|
|
1395
|
+
if (!bindings) return 0;
|
|
1396
|
+
|
|
1397
|
+
let strength = 0;
|
|
1398
|
+
for (const binding of bindings) {
|
|
1399
|
+
switch (binding.type) {
|
|
1400
|
+
case 'key':
|
|
1401
|
+
if (this.keysDown.has(binding.code)) return 1;
|
|
1402
|
+
break;
|
|
1403
|
+
case 'mouse_button':
|
|
1404
|
+
if (this.mouseButtons.has(binding.button)) return 1;
|
|
1405
|
+
break;
|
|
1406
|
+
case 'gamepad_button':
|
|
1407
|
+
if (this.gamepads.some((gp) => gp?.buttons[binding.button]?.pressed)) return 1;
|
|
1408
|
+
break;
|
|
1409
|
+
case 'gamepad_axis':
|
|
1410
|
+
for (const gp of this.gamepads) {
|
|
1411
|
+
if (!gp) continue;
|
|
1412
|
+
const raw = gp.axes[binding.axis] ?? 0;
|
|
1413
|
+
strength = Math.max(
|
|
1414
|
+
strength,
|
|
1415
|
+
Math.min(1, Math.max(0, binding.direction === 'positive' ? raw : -raw)),
|
|
1416
|
+
);
|
|
1417
|
+
}
|
|
1418
|
+
break;
|
|
1419
|
+
case 'touch_button':
|
|
1420
|
+
if (this.touchButtonsDown.has(binding.sourceId)) return 1;
|
|
1421
|
+
break;
|
|
1422
|
+
}
|
|
1423
|
+
}
|
|
1424
|
+
return strength;
|
|
1425
|
+
}
|
|
1426
|
+
|
|
1117
1427
|
/** Was an action pressed this frame (not held from previous)? */
|
|
1118
1428
|
isJustPressed(actionName: string): boolean {
|
|
1119
1429
|
this.assertValueType(actionName, 'digital', 'isJustPressed');
|
|
@@ -1798,6 +2108,17 @@ export class InputManager {
|
|
|
1798
2108
|
return this.gamepads.some((pad) => pad !== null);
|
|
1799
2109
|
}
|
|
1800
2110
|
|
|
2111
|
+
/** Connected W3C Gamepad indices in native slot order, gated by the same play/focus state as
|
|
2112
|
+
* every other raw device poll. Foreign-engine adapters use their own collection value type. */
|
|
2113
|
+
getConnectedGamepadIndices(): number[] {
|
|
2114
|
+
if (!this.inputActive) return [];
|
|
2115
|
+
const indices: number[] = [];
|
|
2116
|
+
for (let index = 0; index < this.gamepads.length; index += 1) {
|
|
2117
|
+
if (this.gamepads[index] !== null) indices.push(index);
|
|
2118
|
+
}
|
|
2119
|
+
return indices;
|
|
2120
|
+
}
|
|
2121
|
+
|
|
1801
2122
|
isDeviceGamepadButtonPressed(button: number): boolean {
|
|
1802
2123
|
if (!this.inputActive) return false;
|
|
1803
2124
|
const index = this.firstConnectedGamepadIndex();
|
|
@@ -1805,6 +2126,15 @@ export class InputManager {
|
|
|
1805
2126
|
return this.gamepadButtonsDown.has(`${index}:${button}`);
|
|
1806
2127
|
}
|
|
1807
2128
|
|
|
2129
|
+
/** Raw button state for one explicit native Gamepad slot. */
|
|
2130
|
+
isDeviceGamepadButtonPressedAt(index: number, button: number): boolean {
|
|
2131
|
+
if (!this.inputActive) return false;
|
|
2132
|
+
if (!Number.isSafeInteger(index) || index < 0 || !Number.isSafeInteger(button) || button < 0) {
|
|
2133
|
+
return false;
|
|
2134
|
+
}
|
|
2135
|
+
return this.gamepads[index]?.buttons[button]?.pressed ?? false;
|
|
2136
|
+
}
|
|
2137
|
+
|
|
1808
2138
|
isDeviceGamepadButtonJustPressed(button: number): boolean {
|
|
1809
2139
|
if (!this.inputActive) return false;
|
|
1810
2140
|
const index = this.firstConnectedGamepadIndex();
|
|
@@ -1841,6 +2171,15 @@ export class InputManager {
|
|
|
1841
2171
|
return this.gamepads[index]?.axes[axis] ?? 0;
|
|
1842
2172
|
}
|
|
1843
2173
|
|
|
2174
|
+
/** Raw axis value for one explicit native Gamepad slot. */
|
|
2175
|
+
getDeviceGamepadAxisAt(index: number, axis: number): number {
|
|
2176
|
+
if (!this.inputActive) return 0;
|
|
2177
|
+
if (!Number.isSafeInteger(index) || index < 0 || !Number.isSafeInteger(axis) || axis < 0) {
|
|
2178
|
+
return 0;
|
|
2179
|
+
}
|
|
2180
|
+
return this.gamepads[index]?.axes[axis] ?? 0;
|
|
2181
|
+
}
|
|
2182
|
+
|
|
1844
2183
|
private firstConnectedGamepadIndex(): number {
|
|
1845
2184
|
for (let i = 0; i < this.gamepads.length; i++) {
|
|
1846
2185
|
if (this.gamepads[i] !== null) return i;
|
|
@@ -2266,6 +2605,31 @@ export class InputManager {
|
|
|
2266
2605
|
return { delivered: true };
|
|
2267
2606
|
}
|
|
2268
2607
|
|
|
2608
|
+
/**
|
|
2609
|
+
* Godot's `Input.action_press(action, strength)` door for a digital action.
|
|
2610
|
+
* This extends the existing virtual held/edge state with its clamped
|
|
2611
|
+
* processed strength instead of maintaining a parallel Godot action map.
|
|
2612
|
+
*/
|
|
2613
|
+
setVirtualDigitalStrength(
|
|
2614
|
+
action: string,
|
|
2615
|
+
strength: number,
|
|
2616
|
+
): { delivered: boolean; reason?: string } {
|
|
2617
|
+
this.assertActionExists(action, 'setVirtualDigitalStrength');
|
|
2618
|
+
this.assertValueType(action, 'digital', 'setVirtualDigitalStrength');
|
|
2619
|
+
if (!Number.isFinite(strength)) {
|
|
2620
|
+
throw new Error('InputManager.setVirtualDigitalStrength: strength must be finite.');
|
|
2621
|
+
}
|
|
2622
|
+
if (!this.machineInputActive) {
|
|
2623
|
+
return { delivered: false, reason: 'input-gated (input disabled by the editor)' };
|
|
2624
|
+
}
|
|
2625
|
+
const clamped = Math.min(1, Math.max(0, strength));
|
|
2626
|
+
// `Input::action_press` sets api_pressed=true even when its clamped
|
|
2627
|
+
// strength is zero; only `action_release` clears the pressed state.
|
|
2628
|
+
this.applyVirtualActionValue(action, true);
|
|
2629
|
+
this.virtualDigitalStrength.set(action, clamped);
|
|
2630
|
+
return { delivered: true };
|
|
2631
|
+
}
|
|
2632
|
+
|
|
2269
2633
|
/**
|
|
2270
2634
|
* The write half of {@link setVirtualAction} — extracted so D15/T-D15.5's
|
|
2271
2635
|
* `scheduleActionAtTick` can apply a non-boolean (scalar/vector2) queued
|
|
@@ -2315,6 +2679,7 @@ export class InputManager {
|
|
|
2315
2679
|
} else {
|
|
2316
2680
|
if (this.virtualDigitalHeld.has(action)) this.virtualDigitalJustReleased.add(action);
|
|
2317
2681
|
this.virtualDigitalHeld.delete(action);
|
|
2682
|
+
this.virtualDigitalStrength.delete(action);
|
|
2318
2683
|
}
|
|
2319
2684
|
break;
|
|
2320
2685
|
case 'scalar':
|
|
@@ -2440,16 +2805,34 @@ export class InputManager {
|
|
|
2440
2805
|
return { version: 1, ticks: this.inputTrace.slice() };
|
|
2441
2806
|
}
|
|
2442
2807
|
|
|
2443
|
-
/**
|
|
2808
|
+
/**
|
|
2809
|
+
* Request pointer lock for FPS-style mouse control.
|
|
2810
|
+
*
|
|
2811
|
+
* Wires `element` for click-to-lock (so a lock released by Escape comes back
|
|
2812
|
+
* on the next click) AND, when the call itself is inside a user gesture,
|
|
2813
|
+
* requests the lock right now. Games call this FROM a click handler
|
|
2814
|
+
* (third-person's Player: canvas click → `requestPointerLock(canvas)`), and
|
|
2815
|
+
* with only the listener wired that first click did nothing — the lock
|
|
2816
|
+
* arrived one click late, with no feedback, which every hosted-editor tester
|
|
2817
|
+
* read as "the mouse doesn't work" (runhuman passes 113–116). Outside a
|
|
2818
|
+
* gesture the browser would refuse and log, so the immediate request is
|
|
2819
|
+
* gated on `navigator.userActivation`.
|
|
2820
|
+
*/
|
|
2444
2821
|
requestPointerLock(element: HTMLElement) {
|
|
2445
2822
|
// Idempotent: never stack duplicate click listeners. Rewire if the element
|
|
2446
2823
|
// changed, and track it so dispose() can remove the listener.
|
|
2447
|
-
if (this.pointerLockElement
|
|
2448
|
-
|
|
2449
|
-
|
|
2824
|
+
if (this.pointerLockElement !== element) {
|
|
2825
|
+
if (this.pointerLockElement) {
|
|
2826
|
+
this.pointerLockElement.removeEventListener('click', this.onPointerLockClick);
|
|
2827
|
+
}
|
|
2828
|
+
this.pointerLockElement = element;
|
|
2829
|
+
element.addEventListener('click', this.onPointerLockClick);
|
|
2830
|
+
}
|
|
2831
|
+
const doc = typeof document !== 'undefined' ? document : null;
|
|
2832
|
+
const activation = typeof navigator !== 'undefined' ? navigator.userActivation : undefined;
|
|
2833
|
+
if (doc && doc.pointerLockElement !== element && activation?.isActive) {
|
|
2834
|
+
this.onPointerLockClick();
|
|
2450
2835
|
}
|
|
2451
|
-
this.pointerLockElement = element;
|
|
2452
|
-
element.addEventListener('click', this.onPointerLockClick);
|
|
2453
2836
|
}
|
|
2454
2837
|
|
|
2455
2838
|
/**
|
|
@@ -2465,6 +2848,10 @@ export class InputManager {
|
|
|
2465
2848
|
* holding lock on a different element is not ours to drop.
|
|
2466
2849
|
*/
|
|
2467
2850
|
exitPointerLock() {
|
|
2851
|
+
if (this.pointerLockRetry !== null) {
|
|
2852
|
+
clearTimeout(this.pointerLockRetry);
|
|
2853
|
+
this.pointerLockRetry = null;
|
|
2854
|
+
}
|
|
2468
2855
|
const el = this.pointerLockElement;
|
|
2469
2856
|
if (el) {
|
|
2470
2857
|
el.removeEventListener('click', this.onPointerLockClick);
|
|
@@ -2480,6 +2867,16 @@ export class InputManager {
|
|
|
2480
2867
|
dispose() {
|
|
2481
2868
|
if (this.disposed) return;
|
|
2482
2869
|
this.disposed = true;
|
|
2870
|
+
// A manager torn down with a key under the hand (Stop while W is held)
|
|
2871
|
+
// kept that key in `keysDown` for good — its listeners went away before
|
|
2872
|
+
// the release arrived, and nothing else clears it. A disposed manager
|
|
2873
|
+
// holds nothing.
|
|
2874
|
+
this.flushHeldState();
|
|
2875
|
+
for (const axes of this.smoothedAxisSets) {
|
|
2876
|
+
axes.disposed = true;
|
|
2877
|
+
axes.values.clear();
|
|
2878
|
+
}
|
|
2879
|
+
this.smoothedAxisSets.clear();
|
|
2483
2880
|
if (typeof window !== 'undefined') {
|
|
2484
2881
|
window.removeEventListener('keydown', this.onKeyDown);
|
|
2485
2882
|
window.removeEventListener('keyup', this.onKeyUp);
|