@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/game.d.ts
CHANGED
|
@@ -135,15 +135,8 @@ export declare class Game {
|
|
|
135
135
|
/** Off, lit or through the Post Effects: how each frame is drawn (ADR 0026). */
|
|
136
136
|
private readonly frame;
|
|
137
137
|
private readonly updateFns;
|
|
138
|
-
|
|
139
|
-
|
|
140
|
-
* componentUpdateSchedule's result for the last composition signature seen
|
|
141
|
-
* per entity: the resolve (Tarjan SCC + Kahn sort) it's built from is pure
|
|
142
|
-
* for a fixed composition, so a signature match skips it entirely. Only
|
|
143
|
-
* ever holds a successful resolution — an invalid composition is never
|
|
144
|
-
* cached, since invalidUpdateCompositions already dedupes its console.error.
|
|
145
|
-
*/
|
|
146
|
-
private readonly updateScheduleCache;
|
|
138
|
+
/** Each entity's Component Update Schedule (ADR 0004), resolved once per composition. */
|
|
139
|
+
private readonly updateSchedules;
|
|
147
140
|
private readonly resolution;
|
|
148
141
|
/** The constructor's viewHeight — unloadScene() restores it. */
|
|
149
142
|
private readonly baseViewHeight;
|
|
@@ -247,12 +240,20 @@ export declare class Game {
|
|
|
247
240
|
* simulating, follows/clamps per its settings. Called by loadScene.
|
|
248
241
|
*/
|
|
249
242
|
setSceneCamera(json?: SceneCameraJson): void;
|
|
243
|
+
/** Puts a following perspective camera on its orbit; loadScene calls it after the spawns, so the first frame is framed (issue #181). */
|
|
244
|
+
placeSceneCamera(): void;
|
|
250
245
|
start(): void;
|
|
251
246
|
stop(): void;
|
|
252
247
|
/** Internal: called by Entity.destroy(). Its Anchored Pieces go (or freeze) with it. */
|
|
253
248
|
removeEntity(entity: Entity): void;
|
|
254
249
|
/** The scene's render projection; null keeps logical and render space identical. */
|
|
255
250
|
get projection(): 'isometric' | null;
|
|
251
|
+
/**
|
|
252
|
+
* The yaw of the scene's orbit camera in degrees — 0 looks toward -z, 90
|
|
253
|
+
* toward +x — or null when the scene camera follows nothing (issue #181);
|
|
254
|
+
* what a camera-relative mover rotates its input by.
|
|
255
|
+
*/
|
|
256
|
+
get cameraYaw(): number | null;
|
|
256
257
|
/** The live scene's space (`render.space`); `'2d'` with no scene (ADR 0027). */
|
|
257
258
|
get space(): SceneSpace;
|
|
258
259
|
/** The camera the live scene draws with: orthographic in 2D scenes, perspective in 3D ones, or a host's view camera over a 3D one (`cameraRig.setViewCamera`). */
|
|
@@ -309,7 +310,7 @@ export declare class Game {
|
|
|
309
310
|
*/
|
|
310
311
|
private runFrame;
|
|
311
312
|
/**
|
|
312
|
-
* One Simulation Step: the pad
|
|
313
|
+
* One Simulation Step: the pad and the mouse are read first (issue #75 CA-6, issue #181 CA-2), then
|
|
313
314
|
* game.time's start-of-step pass (ADR 0017, CA-3), then the Component Update Schedule (ADR 0004) in full,
|
|
314
315
|
* collisions, the scene camera and the host's callbacks, every one of
|
|
315
316
|
* them handed exactly SIMULATION_STEP (CA-1); then the input frame ends.
|
|
@@ -321,7 +322,8 @@ export declare class Game {
|
|
|
321
322
|
private flushPendingSceneLoad;
|
|
322
323
|
private unregisterRuntimeBridge;
|
|
323
324
|
private renderSurface;
|
|
324
|
-
|
|
325
|
+
/** True while a simulating scene's camera orbits a followed entity: what the pointer lock is for. */
|
|
326
|
+
private get orbitPlays();
|
|
325
327
|
private updateSceneCamera;
|
|
326
328
|
private renderPoint;
|
|
327
329
|
private resize;
|
package/dist/game.js
CHANGED
|
@@ -6,9 +6,10 @@ import { listenerPosition, screenPanOf } from './audio/spatial.js';
|
|
|
6
6
|
import { worldToNormalized } from './camera-projection.js';
|
|
7
7
|
import { CameraRig } from './camera-rig.js';
|
|
8
8
|
import { advanceCameraEffects, CameraEffects, layoutCameraEffects } from './camera-effects.js';
|
|
9
|
-
import { implementsOnUpdate, resolveComponentUpdateSchedule } from './component-update-schedule.js';
|
|
10
9
|
import { Entity } from './entity.js';
|
|
10
|
+
import { EntityUpdateSchedules } from './entity-update-schedules.js';
|
|
11
11
|
import { Emitter } from './events.js';
|
|
12
|
+
import { RigidBody } from './components/rigid-body.js';
|
|
12
13
|
import { consumeSimulationSteps, MAX_CHAINED_HOPS, SIMULATION_STEP, snapElapsedToStep, } from './fixed-step.js';
|
|
13
14
|
import { advanceGameTime, GameTime } from './game-time.js';
|
|
14
15
|
import { Input } from './input.js';
|
|
@@ -34,15 +35,6 @@ import { createSpatialQuery } from './spatial-query.js';
|
|
|
34
35
|
import { Stats } from './stats.js';
|
|
35
36
|
import { anchoredPiecesOf, GameUi } from './ui.js';
|
|
36
37
|
import { componentClassOf } from './component-registry.js';
|
|
37
|
-
/** Maps a resolved update order back to the entity's components; the order only names components it was built from. */
|
|
38
|
-
function componentsInOrder(order, byName) {
|
|
39
|
-
return order.map((name) => {
|
|
40
|
-
const component = byName.get(name);
|
|
41
|
-
if (!component)
|
|
42
|
-
throw new Error(`Component update schedule names "${name}", which is not on the entity`);
|
|
43
|
-
return component;
|
|
44
|
-
});
|
|
45
|
-
}
|
|
46
38
|
/**
|
|
47
39
|
* Engine core: loop, unified 2D/3D three scene, orthographic camera,
|
|
48
40
|
* entities with components, and input. See DESIGN.md.
|
|
@@ -95,15 +87,8 @@ export class Game {
|
|
|
95
87
|
/** Off, lit or through the Post Effects: how each frame is drawn (ADR 0026). */
|
|
96
88
|
frame;
|
|
97
89
|
updateFns = new Set();
|
|
98
|
-
|
|
99
|
-
|
|
100
|
-
* componentUpdateSchedule's result for the last composition signature seen
|
|
101
|
-
* per entity: the resolve (Tarjan SCC + Kahn sort) it's built from is pure
|
|
102
|
-
* for a fixed composition, so a signature match skips it entirely. Only
|
|
103
|
-
* ever holds a successful resolution — an invalid composition is never
|
|
104
|
-
* cached, since invalidUpdateCompositions already dedupes its console.error.
|
|
105
|
-
*/
|
|
106
|
-
updateScheduleCache = new WeakMap();
|
|
90
|
+
/** Each entity's Component Update Schedule (ADR 0004), resolved once per composition. */
|
|
91
|
+
updateSchedules = new EntityUpdateSchedules();
|
|
107
92
|
resolution;
|
|
108
93
|
/** The constructor's viewHeight — unloadScene() restores it. */
|
|
109
94
|
baseViewHeight;
|
|
@@ -119,6 +104,9 @@ export class Game {
|
|
|
119
104
|
find: (name) => this.find(name),
|
|
120
105
|
renderPoint: (x, y) => this.renderPoint(x, y),
|
|
121
106
|
viewHeight: () => this.viewHeight,
|
|
107
|
+
input: () => this.input,
|
|
108
|
+
// Fixed level geometry only (a Collider with no RigidBody): a character or a crate crossing behind the Player never moves the camera.
|
|
109
|
+
rayDistance: ({ origin, direction, maxDistance, exclude }) => this.query.ray(origin, direction, maxDistance, { exclude, without: [RigidBody] })?.distance ?? null,
|
|
122
110
|
};
|
|
123
111
|
renderSort = null;
|
|
124
112
|
sceneProjection = null;
|
|
@@ -146,7 +134,14 @@ export class Game {
|
|
|
146
134
|
this.baseViewHeight = viewHeight;
|
|
147
135
|
this.viewHeight = viewHeight;
|
|
148
136
|
this.resolution = options.resolution ?? null;
|
|
149
|
-
this.input = new Input(options.bindings, {
|
|
137
|
+
this.input = new Input(options.bindings, {
|
|
138
|
+
gamepadDeadZone: options.gamepadDeadZone,
|
|
139
|
+
canvas,
|
|
140
|
+
// A click takes the pointer lock only while an orbit camera plays (never in the editor's edit mode).
|
|
141
|
+
lockOnClick: () => this.orbitPlays,
|
|
142
|
+
// The wheel zooms the orbit; any other scene leaves the page's scroll alone (CA-2 as amended).
|
|
143
|
+
preventsWheelScroll: () => this.orbitPlays,
|
|
144
|
+
});
|
|
150
145
|
this.query = createSpatialQuery(this);
|
|
151
146
|
this.stats = new Stats(options.stats);
|
|
152
147
|
this.ui = new GameUi(this.stats, () => canvas.parentElement ?? document.body);
|
|
@@ -410,6 +405,10 @@ export class Game {
|
|
|
410
405
|
this.camera.position.y = center.y;
|
|
411
406
|
this.setViewHeight(cam.zoom);
|
|
412
407
|
}
|
|
408
|
+
/** Puts a following perspective camera on its orbit; loadScene calls it after the spawns, so the first frame is framed (issue #181). */
|
|
409
|
+
placeSceneCamera() {
|
|
410
|
+
this.cameraRig.placeOrbit(this.followHost);
|
|
411
|
+
}
|
|
413
412
|
start() {
|
|
414
413
|
if (this.disposed)
|
|
415
414
|
return;
|
|
@@ -422,6 +421,7 @@ export class Game {
|
|
|
422
421
|
resume: (onStep) => this.resumeRuntime(onStep),
|
|
423
422
|
pause: () => this.stop(),
|
|
424
423
|
injectAction: (action, operation, value) => this.input.injectAction(action, operation, value),
|
|
424
|
+
look: (dx, dy) => this.input.injectLook(dx, dy),
|
|
425
425
|
availableActions: () => this.input.availableActions(),
|
|
426
426
|
heldActions: () => this.input.heldActions(),
|
|
427
427
|
actionValues: () => this.input.actionValues(),
|
|
@@ -465,6 +465,14 @@ export class Game {
|
|
|
465
465
|
get projection() {
|
|
466
466
|
return this.sceneProjection;
|
|
467
467
|
}
|
|
468
|
+
/**
|
|
469
|
+
* The yaw of the scene's orbit camera in degrees — 0 looks toward -z, 90
|
|
470
|
+
* toward +x — or null when the scene camera follows nothing (issue #181);
|
|
471
|
+
* what a camera-relative mover rotates its input by.
|
|
472
|
+
*/
|
|
473
|
+
get cameraYaw() {
|
|
474
|
+
return this.cameraRig.orbitView?.yaw ?? null;
|
|
475
|
+
}
|
|
468
476
|
/** The live scene's space (`render.space`); `'2d'` with no scene (ADR 0027). */
|
|
469
477
|
get space() {
|
|
470
478
|
return this.sceneSpace;
|
|
@@ -604,6 +612,9 @@ export class Game {
|
|
|
604
612
|
this.finishStep();
|
|
605
613
|
}
|
|
606
614
|
this.audio.setActive(this.simulate);
|
|
615
|
+
// The pointer lock belongs to a playing orbit: a pause, a swap or an unload gives the cursor back.
|
|
616
|
+
if (!this.orbitPlays)
|
|
617
|
+
this.input.releasePointerLock();
|
|
607
618
|
// Positional audio (CA-8): recomputed every frame, on this same pass —
|
|
608
619
|
// never a second walk of `this.entities`, since `this.audio` already
|
|
609
620
|
// holds direct references to whichever entities are tracked.
|
|
@@ -615,19 +626,20 @@ export class Game {
|
|
|
615
626
|
}
|
|
616
627
|
}
|
|
617
628
|
/**
|
|
618
|
-
* One Simulation Step: the pad
|
|
629
|
+
* One Simulation Step: the pad and the mouse are read first (issue #75 CA-6, issue #181 CA-2), then
|
|
619
630
|
* game.time's start-of-step pass (ADR 0017, CA-3), then the Component Update Schedule (ADR 0004) in full,
|
|
620
631
|
* collisions, the scene camera and the host's callbacks, every one of
|
|
621
632
|
* them handed exactly SIMULATION_STEP (CA-1); then the input frame ends.
|
|
622
633
|
*/
|
|
623
634
|
simulateStep() {
|
|
624
635
|
this.input.pollGamepad();
|
|
636
|
+
this.input.pollMouse();
|
|
625
637
|
advanceGameTime(this.time);
|
|
626
638
|
// Existing drains advance before entity updates, so one detached during
|
|
627
639
|
// this step begins on the next Simulation Step (ADR 0022).
|
|
628
640
|
sceneDrainsOf(this).advance(SIMULATION_STEP);
|
|
629
641
|
for (const entity of [...this.entities]) {
|
|
630
|
-
const schedule = this.
|
|
642
|
+
const schedule = this.updateSchedules.of(entity, this.registry?.components);
|
|
631
643
|
if (!schedule)
|
|
632
644
|
continue;
|
|
633
645
|
for (const component of schedule)
|
|
@@ -681,11 +693,7 @@ export class Game {
|
|
|
681
693
|
// The shake offset exists only while drawing (ADR 0020): Anchored Pieces
|
|
682
694
|
// and the render see the drawn center, then the base comes back exactly,
|
|
683
695
|
// so follow smoothing never starts from its own jitter.
|
|
684
|
-
const
|
|
685
|
-
// A perspective camera does not shake: the offset is in orthographic world units.
|
|
686
|
-
const shake = this.sceneSpace === '3d' ? { x: 0, y: 0 } : this.cameraEffects.state.shake;
|
|
687
|
-
this.camera.position.x = x + shake.x;
|
|
688
|
-
this.camera.position.y = y + shake.y;
|
|
696
|
+
const restoreBase = this.cameraRig.shake(this.cameraEffects.state.shake, this.viewHeight);
|
|
689
697
|
try {
|
|
690
698
|
anchoredPiecesOf(this.ui).place();
|
|
691
699
|
// Before ready() the frame simulated, but three cannot draw yet.
|
|
@@ -694,57 +702,15 @@ export class Game {
|
|
|
694
702
|
this.frame.draw();
|
|
695
703
|
}
|
|
696
704
|
finally {
|
|
697
|
-
|
|
698
|
-
this.camera.position.y = y;
|
|
705
|
+
restoreBase();
|
|
699
706
|
}
|
|
700
707
|
}
|
|
701
|
-
|
|
702
|
-
|
|
703
|
-
|
|
704
|
-
const names = [];
|
|
705
|
-
const signatureParts = [];
|
|
706
|
-
for (const component of components) {
|
|
707
|
-
const Class = componentClassOf(component);
|
|
708
|
-
const name = Class.componentName;
|
|
709
|
-
names.push(name);
|
|
710
|
-
byName.set(name, component);
|
|
711
|
-
signatureParts.push(`${name}:${implementsOnUpdate(Class) ? 'updates' : 'passive'}:` +
|
|
712
|
-
[...new Set(Class.updateAfter ?? [])].sort().join(','));
|
|
713
|
-
}
|
|
714
|
-
const signature = signatureParts.sort().join('|');
|
|
715
|
-
// The resolve below (duplicate check + Tarjan SCC + Kahn sort) is pure
|
|
716
|
-
// for a fixed composition, and this method now runs once per entity per
|
|
717
|
-
// Simulation Step rather than once per rendered frame: skip it entirely
|
|
718
|
-
// when nothing about this entity's components changed since last time.
|
|
719
|
-
const cached = this.updateScheduleCache.get(entity);
|
|
720
|
-
if (cached && cached.signature === signature) {
|
|
721
|
-
return componentsInOrder(cached.order, byName);
|
|
722
|
-
}
|
|
723
|
-
const registry = {
|
|
724
|
-
...(this.registry?.components ?? {}),
|
|
725
|
-
};
|
|
726
|
-
for (const component of components) {
|
|
727
|
-
const Class = componentClassOf(component);
|
|
728
|
-
registry[Class.componentName] = Class;
|
|
729
|
-
}
|
|
730
|
-
const result = resolveComponentUpdateSchedule(names, registry);
|
|
731
|
-
if (!result.ok) {
|
|
732
|
-
this.updateScheduleCache.delete(entity);
|
|
733
|
-
if (this.invalidUpdateCompositions.get(entity) !== signature) {
|
|
734
|
-
this.invalidUpdateCompositions.set(entity, signature);
|
|
735
|
-
console.error(`[waica] invalid component update schedule for "${entity.name}": ` +
|
|
736
|
-
result.issues.map((issue) => issue.cause).join(' '));
|
|
737
|
-
}
|
|
738
|
-
return null;
|
|
739
|
-
}
|
|
740
|
-
this.invalidUpdateCompositions.delete(entity);
|
|
741
|
-
this.updateScheduleCache.set(entity, { signature, order: result.order });
|
|
742
|
-
return componentsInOrder(result.order, byName);
|
|
708
|
+
/** True while a simulating scene's camera orbits a followed entity: what the pointer lock is for. */
|
|
709
|
+
get orbitPlays() {
|
|
710
|
+
return this.simulate && this.cameraRig.orbiting;
|
|
743
711
|
}
|
|
744
712
|
updateSceneCamera(dt) {
|
|
745
|
-
|
|
746
|
-
return;
|
|
747
|
-
this.cameraRig.follow(this.sceneCamera, this.followHost, dt);
|
|
713
|
+
this.cameraRig.step(this.sceneCamera, this.followHost, dt);
|
|
748
714
|
}
|
|
749
715
|
renderPoint(x, y) {
|
|
750
716
|
return this.sceneProjection === 'isometric' ? projectIsometric(x, y) : { x, y };
|
package/dist/index.d.ts
CHANGED
|
@@ -30,6 +30,9 @@ export type { EasingName, TimerHandle, TimerOptions, TweenOptions } from './game
|
|
|
30
30
|
export type { SceneCameraJson, OrthographicSceneCameraJson, CameraLimitsJson, CameraVelocity, CameraVelocityProvider, ResolvedSceneCamera, } from './camera.js';
|
|
31
31
|
export { isPerspectiveCameraJson, PERSPECTIVE_DEFAULTS, perspectiveCameraIssues, placePerspectiveCamera, resolvePerspectiveCamera, } from './scene-camera-3d.js';
|
|
32
32
|
export type { PerspectiveSceneCameraJson, ResolvedPerspectiveCamera, SceneFieldIssue, Vec3Json, } from './scene-camera-3d.js';
|
|
33
|
+
export { ORBIT_DEFAULTS, resolveOrbit } from './scene-camera-orbit.js';
|
|
34
|
+
export type { OrbitCameraJson, ResolvedOrbit } from './scene-camera-orbit.js';
|
|
35
|
+
export type { OrbitView } from './orbit-camera.js';
|
|
33
36
|
export { isPerspectiveCamera, worldToNormalized } from './camera-projection.js';
|
|
34
37
|
export type { GameCamera, NormalizedPoint, ViewCamera, WorldPoint } from './camera-projection.js';
|
|
35
38
|
export { componentSpaceMismatch, componentSpaceOf, entityTransformIssues, resolveSceneSpace, SCENE_SPACES, sceneSpaceIssues, } from './scene-space.js';
|
|
@@ -47,6 +50,7 @@ export { resolveComponentUpdateSchedule } from './component-update-schedule.js';
|
|
|
47
50
|
export type { ComponentUpdateCycleIssue, ComponentUpdateRegistry, ComponentUpdateScheduleIssue, ComponentUpdateScheduleResult, DuplicateComponentUpdateIssue, InvalidComponentUpdateConstraintIssue, InvalidComponentUpdateSchedule, InvalidUpdateConstraintReason, ValidComponentUpdateSchedule, } from './component-update-schedule.js';
|
|
48
51
|
export { Input, DEFAULT_BINDINGS } from './input.js';
|
|
49
52
|
export type { ActionName, InjectedActionOperation, InputBindings, InputOptions } from './input.js';
|
|
53
|
+
export type { MouseMovement } from './input-mouse.js';
|
|
50
54
|
export { ACTION_HELD_THRESHOLD, firstStandardPadValues, GAMEPAD_CODE_PREFIX, gamepadControl, } from './gamepad.js';
|
|
51
55
|
export type { GamepadControl } from './gamepad.js';
|
|
52
56
|
export { Pointer } from './pointer.js';
|
|
@@ -59,7 +63,8 @@ export type { RenderBackend } from './render-readiness.js';
|
|
|
59
63
|
export { RUNTIME_PROJECTION_LIMITS } from './runtime-inspection.js';
|
|
60
64
|
export type { RuntimeSnapshotLight, RuntimeSnapshotLighting, RuntimeSnapshotPointLight, RuntimeSnapshotSun, RuntimeSnapshotPost, } from './runtime-lighting-snapshot.js';
|
|
61
65
|
export type { ProjectedValue, ProjectionIssue, ProjectionMarker, ProjectionMarkerKind, RuntimeComponentSnapshot, RuntimeEntitySnapshot, RuntimeSnapshot, RuntimeSnapshotAudio, RuntimeSnapshotCamera, RuntimeSnapshotFilters, RuntimeSnapshotTime, RuntimeSnapshotUi, RuntimeTransformSnapshot, } from './runtime-inspection.js';
|
|
62
|
-
export type { ArchetypeArt, ArchetypeManifest, BrowserArchetypeManifest, EntityTemplate, } from './archetype.js';
|
|
66
|
+
export type { ArchetypeArt, ArchetypeManifest, BrowserArchetypeManifest, EntityTemplate, SceneArchetypeNeeds, } from './archetype.js';
|
|
67
|
+
export { sceneArchetypeNeeds } from './archetype.js';
|
|
63
68
|
export { Stats } from './stats.js';
|
|
64
69
|
export type { StatValue } from './stats.js';
|
|
65
70
|
export { GameUi } from './ui.js';
|
|
@@ -106,7 +111,7 @@ export type { TilemapCell, TilemapCellBounds, TilemapGridSpec, } from './tilemap
|
|
|
106
111
|
export { COLLISION_SHAPES, DEFAULT_COLLISION_POLYGON, collisionBounds, collisionOverlap, collisionVertices, resolveCollisionPoints, } from './collision-shape.js';
|
|
107
112
|
export type { CollisionBody, CollisionBounds, CollisionPoint, CollisionShape, } from './collision-shape.js';
|
|
108
113
|
export type { EntityWith, NearestQueryContext, NearestSpatialQueryFilter, RayHit, SpatialQuery, SpatialQueryFilter, } from './spatial-query.js';
|
|
109
|
-
export type { Point3d, QueryVolume3d, RayHit3d } from './spatial-query-3d.js';
|
|
114
|
+
export type { Point3d, QueryColliders3d, QueryVolume3d, RayHit3d } from './spatial-query-3d.js';
|
|
110
115
|
export { Emitter } from './events.js';
|
|
111
116
|
export { loadScene, spawnFromJson, resolveEntityComponents, resolveProps } from './scene.js';
|
|
112
117
|
export type { SceneJson, SceneEntityJson, SceneComponentJson, SceneRegistry, SceneRenderJson, SceneLightingJson, ScenePostJson, PrefabJson, } from './scene.js';
|
package/dist/index.js
CHANGED
|
@@ -13,6 +13,7 @@ export { SIMULATION_STEP, SIMULATION_TIME_EPSILON } from './fixed-step.js';
|
|
|
13
13
|
export { CameraEffects } from './camera-effects.js';
|
|
14
14
|
export { GameTime, advanceGameTime } from './game-time.js';
|
|
15
15
|
export { isPerspectiveCameraJson, PERSPECTIVE_DEFAULTS, perspectiveCameraIssues, placePerspectiveCamera, resolvePerspectiveCamera, } from './scene-camera-3d.js';
|
|
16
|
+
export { ORBIT_DEFAULTS, resolveOrbit } from './scene-camera-orbit.js';
|
|
16
17
|
export { isPerspectiveCamera, worldToNormalized } from './camera-projection.js';
|
|
17
18
|
export { componentSpaceMismatch, componentSpaceOf, entityTransformIssues, resolveSceneSpace, SCENE_SPACES, sceneSpaceIssues, } from './scene-space.js';
|
|
18
19
|
export { applyTransformJson } from './entity-transform.js';
|
|
@@ -27,6 +28,7 @@ export { Pointer } from './pointer.js';
|
|
|
27
28
|
export { raycastPick } from './pointer-raycast.js';
|
|
28
29
|
export { RUNTIME_BRIDGE_CAPABILITIES, RUNTIME_BRIDGE_PROTOCOL_VERSION, RUNTIME_BRIDGE_SYMBOL, RuntimeBridgeOperationError, } from './runtime-bridge.js';
|
|
29
30
|
export { RUNTIME_PROJECTION_LIMITS } from './runtime-inspection.js';
|
|
31
|
+
export { sceneArchetypeNeeds } from './archetype.js';
|
|
30
32
|
export { Stats } from './stats.js';
|
|
31
33
|
export { GameUi } from './ui.js';
|
|
32
34
|
export { Sprite } from './components/sprite.js';
|
|
@@ -0,0 +1,83 @@
|
|
|
1
|
+
/** The prefix of a mouse button's binding code, beside keys and `Gamepad:` codes (issue #181). */
|
|
2
|
+
export declare const MOUSE_CODE_PREFIX = "Mouse:";
|
|
3
|
+
/** A mouse movement or wheel reading, in CSS pixels. */
|
|
4
|
+
export interface MouseMovement {
|
|
5
|
+
readonly x: number;
|
|
6
|
+
readonly y: number;
|
|
7
|
+
}
|
|
8
|
+
/** What the mouse reports to the Input it belongs to: a button code going down or up, and whether an Action binds a code. */
|
|
9
|
+
export interface MouseButtonSink {
|
|
10
|
+
press(code: string): void;
|
|
11
|
+
release(code: string): void;
|
|
12
|
+
binds(code: string): boolean;
|
|
13
|
+
}
|
|
14
|
+
export interface MouseInputOptions {
|
|
15
|
+
/** The game canvas: buttons, the wheel and the pointer lock are read on it. Without one, only injected movement counts. */
|
|
16
|
+
canvas?: HTMLCanvasElement;
|
|
17
|
+
/** Asked on a press over the canvas while it is unlocked: true asks for the lock with that press, which presses nothing once granted (CA-3) and goes through when refused. */
|
|
18
|
+
lockOnClick?: () => boolean;
|
|
19
|
+
/** Asked on each wheel over the canvas: true keeps the page from scrolling (an orbit camera zooms with it); false by default (CA-2). */
|
|
20
|
+
preventsWheelScroll?: () => boolean;
|
|
21
|
+
}
|
|
22
|
+
/** True for a `Mouse:` code no button reports, which therefore never matches. */
|
|
23
|
+
export declare function isUnknownMouseCode(code: string): boolean;
|
|
24
|
+
/**
|
|
25
|
+
* The mouse as an Input source (issue #181): its buttons as `Mouse:` binding
|
|
26
|
+
* codes, its movement while the pointer is locked to the game canvas, the
|
|
27
|
+
* wheel over the canvas, and the pointer lock itself. Movement and wheel
|
|
28
|
+
* accumulate between Simulation Steps, are frozen by `poll()` at a step's
|
|
29
|
+
* start and read zero again after `endStep()`.
|
|
30
|
+
*/
|
|
31
|
+
export declare class MouseInput {
|
|
32
|
+
private readonly buttons;
|
|
33
|
+
private pendingMove;
|
|
34
|
+
private pendingWheel;
|
|
35
|
+
private move;
|
|
36
|
+
private wheel;
|
|
37
|
+
private readonly preventsWheelScroll;
|
|
38
|
+
private readonly canvas;
|
|
39
|
+
private readonly lockOnClick;
|
|
40
|
+
/** Set by dispose(): a lock the browser grants after it is given straight back. */
|
|
41
|
+
private disposed;
|
|
42
|
+
/** The codes whose press asked for the lock, awaiting the browser's answer: true while still held. */
|
|
43
|
+
private readonly askedLock;
|
|
44
|
+
constructor(buttons: MouseButtonSink, options?: MouseInputOptions);
|
|
45
|
+
/** Is the pointer locked to the game canvas right now? */
|
|
46
|
+
get locked(): boolean;
|
|
47
|
+
/** The locked movement of the current Simulation Step; zero between steps. */
|
|
48
|
+
movement(): MouseMovement;
|
|
49
|
+
/** The wheel delta of the current Simulation Step, in CSS px (positive scrolls down); zero between steps. */
|
|
50
|
+
wheelDelta(): number;
|
|
51
|
+
/** Freezes what accumulated since the last step as this step's movement and wheel. */
|
|
52
|
+
poll(): void;
|
|
53
|
+
/** The step is over: its movement and wheel read zero until the next poll. */
|
|
54
|
+
endStep(): void;
|
|
55
|
+
/** Adds a movement as if the pointer were locked (the Runtime Bridge's `look`, CA-4). */
|
|
56
|
+
inject(dx: number, dy: number): void;
|
|
57
|
+
/** Exits the pointer lock when the game canvas holds it; a lock on any other element is left alone. */
|
|
58
|
+
releaseLock(): void;
|
|
59
|
+
/** Asks the browser to lock the pointer to the game canvas; a refusal (or no Pointer Lock API) leaves it unlocked. */
|
|
60
|
+
requestLock(): void;
|
|
61
|
+
/**
|
|
62
|
+
* The presses that asked for a lock the browser refused (or cannot give)
|
|
63
|
+
* reach their `Mouse:` codes after all (maintainer decision on issue #181):
|
|
64
|
+
* still held, or as a tap when the button was let go before the answer.
|
|
65
|
+
*/
|
|
66
|
+
private pressRefused;
|
|
67
|
+
/** Stops listening, and releases the pointer lock if this canvas holds it (a Game that goes away, the editor's Play stopping). */
|
|
68
|
+
dispose(): void;
|
|
69
|
+
/** `document.pointerLockElement`, which a browser without the Pointer Lock API (or a test DOM) lacks. */
|
|
70
|
+
private lockElement;
|
|
71
|
+
private onPointerDown;
|
|
72
|
+
private onPointerUp;
|
|
73
|
+
/**
|
|
74
|
+
* A button going down or up while another is held fires `pointermove`, not
|
|
75
|
+
* `pointerdown`/`pointerup` (Pointer Events' chorded buttons). Its bit in
|
|
76
|
+
* `buttons` tells which; a press counts over the canvas only, like the first.
|
|
77
|
+
*/
|
|
78
|
+
private onPointerChord;
|
|
79
|
+
/** The browser menu would swallow the release of a bound `Mouse:Right`; other games keep their menu. */
|
|
80
|
+
private onContextMenu;
|
|
81
|
+
private onWheel;
|
|
82
|
+
private onMouseMove;
|
|
83
|
+
}
|
|
@@ -0,0 +1,205 @@
|
|
|
1
|
+
/** The prefix of a mouse button's binding code, beside keys and `Gamepad:` codes (issue #181). */
|
|
2
|
+
export const MOUSE_CODE_PREFIX = 'Mouse:';
|
|
3
|
+
/** `PointerEvent.button` → the binding code of the buttons an Action can bind. */
|
|
4
|
+
const MOUSE_BUTTON_CODES = new Map([
|
|
5
|
+
[0, 'Mouse:Left'],
|
|
6
|
+
[2, 'Mouse:Right'],
|
|
7
|
+
]);
|
|
8
|
+
const KNOWN_MOUSE_CODES = new Set(MOUSE_BUTTON_CODES.values());
|
|
9
|
+
/** `PointerEvent.button` → its bit in `PointerEvent.buttons`, which tells a chorded press from a chorded release. */
|
|
10
|
+
const MOUSE_BUTTON_BITS = new Map([
|
|
11
|
+
[0, 1],
|
|
12
|
+
[2, 2],
|
|
13
|
+
]);
|
|
14
|
+
/** Wheel deltas reported in lines or pages, in CSS pixels (the units of `deltaMode` 0). */
|
|
15
|
+
const WHEEL_LINE_PX = 16;
|
|
16
|
+
const WHEEL_PAGE_PX = 800;
|
|
17
|
+
/** Touch and pen pointers are not the mouse: `Mouse:` codes ignore them (no mobile support, issue #181). */
|
|
18
|
+
const fromMouse = (event) => event.pointerType !== 'touch' && event.pointerType !== 'pen';
|
|
19
|
+
/** True for a `Mouse:` code no button reports, which therefore never matches. */
|
|
20
|
+
export function isUnknownMouseCode(code) {
|
|
21
|
+
return code.startsWith(MOUSE_CODE_PREFIX) && !KNOWN_MOUSE_CODES.has(code);
|
|
22
|
+
}
|
|
23
|
+
function wheelPixels(event) {
|
|
24
|
+
if (event.deltaMode === 1)
|
|
25
|
+
return event.deltaY * WHEEL_LINE_PX;
|
|
26
|
+
if (event.deltaMode === 2)
|
|
27
|
+
return event.deltaY * WHEEL_PAGE_PX;
|
|
28
|
+
return event.deltaY;
|
|
29
|
+
}
|
|
30
|
+
/**
|
|
31
|
+
* The mouse as an Input source (issue #181): its buttons as `Mouse:` binding
|
|
32
|
+
* codes, its movement while the pointer is locked to the game canvas, the
|
|
33
|
+
* wheel over the canvas, and the pointer lock itself. Movement and wheel
|
|
34
|
+
* accumulate between Simulation Steps, are frozen by `poll()` at a step's
|
|
35
|
+
* start and read zero again after `endStep()`.
|
|
36
|
+
*/
|
|
37
|
+
export class MouseInput {
|
|
38
|
+
buttons;
|
|
39
|
+
pendingMove = { x: 0, y: 0 };
|
|
40
|
+
pendingWheel = 0;
|
|
41
|
+
move = { x: 0, y: 0 };
|
|
42
|
+
wheel = 0;
|
|
43
|
+
preventsWheelScroll;
|
|
44
|
+
canvas;
|
|
45
|
+
lockOnClick;
|
|
46
|
+
/** Set by dispose(): a lock the browser grants after it is given straight back. */
|
|
47
|
+
disposed = false;
|
|
48
|
+
/** The codes whose press asked for the lock, awaiting the browser's answer: true while still held. */
|
|
49
|
+
askedLock = new Map();
|
|
50
|
+
constructor(buttons, options = {}) {
|
|
51
|
+
this.buttons = buttons;
|
|
52
|
+
this.canvas = options.canvas ?? null;
|
|
53
|
+
this.lockOnClick = options.lockOnClick ?? (() => false);
|
|
54
|
+
this.preventsWheelScroll = options.preventsWheelScroll ?? (() => false);
|
|
55
|
+
this.canvas?.addEventListener('pointerdown', this.onPointerDown);
|
|
56
|
+
this.canvas?.addEventListener('wheel', this.onWheel, { passive: false });
|
|
57
|
+
this.canvas?.addEventListener('contextmenu', this.onContextMenu);
|
|
58
|
+
// A release anywhere counts, so a button let go off the canvas never stays held.
|
|
59
|
+
window.addEventListener('pointerup', this.onPointerUp);
|
|
60
|
+
window.addEventListener('pointermove', this.onPointerChord);
|
|
61
|
+
document.addEventListener('mousemove', this.onMouseMove);
|
|
62
|
+
}
|
|
63
|
+
/** Is the pointer locked to the game canvas right now? */
|
|
64
|
+
get locked() {
|
|
65
|
+
return this.canvas !== null && this.lockElement() === this.canvas;
|
|
66
|
+
}
|
|
67
|
+
/** The locked movement of the current Simulation Step; zero between steps. */
|
|
68
|
+
movement() {
|
|
69
|
+
return this.move;
|
|
70
|
+
}
|
|
71
|
+
/** The wheel delta of the current Simulation Step, in CSS px (positive scrolls down); zero between steps. */
|
|
72
|
+
wheelDelta() {
|
|
73
|
+
return this.wheel;
|
|
74
|
+
}
|
|
75
|
+
/** Freezes what accumulated since the last step as this step's movement and wheel. */
|
|
76
|
+
poll() {
|
|
77
|
+
this.move = { ...this.pendingMove };
|
|
78
|
+
this.wheel = this.pendingWheel;
|
|
79
|
+
this.pendingMove = { x: 0, y: 0 };
|
|
80
|
+
this.pendingWheel = 0;
|
|
81
|
+
}
|
|
82
|
+
/** The step is over: its movement and wheel read zero until the next poll. */
|
|
83
|
+
endStep() {
|
|
84
|
+
this.move = { x: 0, y: 0 };
|
|
85
|
+
this.wheel = 0;
|
|
86
|
+
}
|
|
87
|
+
/** Adds a movement as if the pointer were locked (the Runtime Bridge's `look`, CA-4). */
|
|
88
|
+
inject(dx, dy) {
|
|
89
|
+
this.pendingMove.x += dx;
|
|
90
|
+
this.pendingMove.y += dy;
|
|
91
|
+
}
|
|
92
|
+
/** Exits the pointer lock when the game canvas holds it; a lock on any other element is left alone. */
|
|
93
|
+
releaseLock() {
|
|
94
|
+
if (this.locked && 'exitPointerLock' in document)
|
|
95
|
+
document.exitPointerLock();
|
|
96
|
+
}
|
|
97
|
+
/** Asks the browser to lock the pointer to the game canvas; a refusal (or no Pointer Lock API) leaves it unlocked. */
|
|
98
|
+
requestLock() {
|
|
99
|
+
const canvas = this.canvas;
|
|
100
|
+
if (!canvas || !('requestPointerLock' in canvas)) {
|
|
101
|
+
this.pressRefused();
|
|
102
|
+
return;
|
|
103
|
+
}
|
|
104
|
+
try {
|
|
105
|
+
Promise.resolve(canvas.requestPointerLock()).then(() => {
|
|
106
|
+
// The press that asked took the lock, and presses nothing (CA-3).
|
|
107
|
+
this.askedLock.clear();
|
|
108
|
+
// Granted after this Input went away (the editor's Play stopped meanwhile): the lock is no longer the game's.
|
|
109
|
+
if (this.disposed)
|
|
110
|
+
this.releaseLock();
|
|
111
|
+
}, () => {
|
|
112
|
+
// Refused (no user gesture, a sandboxed iframe, the cooldown after Esc): the press goes through; the next click asks again.
|
|
113
|
+
this.pressRefused();
|
|
114
|
+
});
|
|
115
|
+
}
|
|
116
|
+
catch {
|
|
117
|
+
// Older browsers throw synchronously instead of rejecting; same outcome.
|
|
118
|
+
this.pressRefused();
|
|
119
|
+
}
|
|
120
|
+
}
|
|
121
|
+
/**
|
|
122
|
+
* The presses that asked for a lock the browser refused (or cannot give)
|
|
123
|
+
* reach their `Mouse:` codes after all (maintainer decision on issue #181):
|
|
124
|
+
* still held, or as a tap when the button was let go before the answer.
|
|
125
|
+
*/
|
|
126
|
+
pressRefused() {
|
|
127
|
+
for (const [code, held] of this.askedLock) {
|
|
128
|
+
this.buttons.press(code);
|
|
129
|
+
if (!held)
|
|
130
|
+
this.buttons.release(code);
|
|
131
|
+
}
|
|
132
|
+
this.askedLock.clear();
|
|
133
|
+
}
|
|
134
|
+
/** Stops listening, and releases the pointer lock if this canvas holds it (a Game that goes away, the editor's Play stopping). */
|
|
135
|
+
dispose() {
|
|
136
|
+
this.disposed = true;
|
|
137
|
+
this.releaseLock();
|
|
138
|
+
this.canvas?.removeEventListener('pointerdown', this.onPointerDown);
|
|
139
|
+
this.canvas?.removeEventListener('wheel', this.onWheel);
|
|
140
|
+
window.removeEventListener('pointerup', this.onPointerUp);
|
|
141
|
+
document.removeEventListener('mousemove', this.onMouseMove);
|
|
142
|
+
this.canvas?.removeEventListener('contextmenu', this.onContextMenu);
|
|
143
|
+
window.removeEventListener('pointermove', this.onPointerChord);
|
|
144
|
+
}
|
|
145
|
+
/** `document.pointerLockElement`, which a browser without the Pointer Lock API (or a test DOM) lacks. */
|
|
146
|
+
lockElement() {
|
|
147
|
+
return 'pointerLockElement' in document ? document.pointerLockElement : null;
|
|
148
|
+
}
|
|
149
|
+
onPointerDown = (event) => {
|
|
150
|
+
if (!fromMouse(event))
|
|
151
|
+
return;
|
|
152
|
+
const code = MOUSE_BUTTON_CODES.get(event.button);
|
|
153
|
+
if (!this.locked && this.lockOnClick()) {
|
|
154
|
+
// This press asks for the lock (G-17): it reaches its code only if the browser refuses.
|
|
155
|
+
if (code)
|
|
156
|
+
this.askedLock.set(code, true);
|
|
157
|
+
this.requestLock();
|
|
158
|
+
return;
|
|
159
|
+
}
|
|
160
|
+
if (code)
|
|
161
|
+
this.buttons.press(code);
|
|
162
|
+
};
|
|
163
|
+
onPointerUp = (event) => {
|
|
164
|
+
if (!fromMouse(event))
|
|
165
|
+
return;
|
|
166
|
+
const code = MOUSE_BUTTON_CODES.get(event.button);
|
|
167
|
+
if (!code)
|
|
168
|
+
return;
|
|
169
|
+
// Let go before the lock's answer: a refusal then delivers a tap.
|
|
170
|
+
if (this.askedLock.has(code))
|
|
171
|
+
this.askedLock.set(code, false);
|
|
172
|
+
this.buttons.release(code);
|
|
173
|
+
};
|
|
174
|
+
/**
|
|
175
|
+
* A button going down or up while another is held fires `pointermove`, not
|
|
176
|
+
* `pointerdown`/`pointerup` (Pointer Events' chorded buttons). Its bit in
|
|
177
|
+
* `buttons` tells which; a press counts over the canvas only, like the first.
|
|
178
|
+
*/
|
|
179
|
+
onPointerChord = (event) => {
|
|
180
|
+
const bit = MOUSE_BUTTON_BITS.get(event.button);
|
|
181
|
+
if (bit === undefined || !fromMouse(event))
|
|
182
|
+
return;
|
|
183
|
+
if ((event.buttons & bit) === 0)
|
|
184
|
+
this.onPointerUp(event);
|
|
185
|
+
else if (event.target === this.canvas)
|
|
186
|
+
this.onPointerDown(event);
|
|
187
|
+
};
|
|
188
|
+
/** The browser menu would swallow the release of a bound `Mouse:Right`; other games keep their menu. */
|
|
189
|
+
onContextMenu = (event) => {
|
|
190
|
+
if (this.buttons.binds('Mouse:Right'))
|
|
191
|
+
event.preventDefault();
|
|
192
|
+
};
|
|
193
|
+
onWheel = (event) => {
|
|
194
|
+
// Over the game canvas only, and only while something zooms with it (an orbit camera, issue #181): otherwise the page still scrolls.
|
|
195
|
+
if (this.preventsWheelScroll())
|
|
196
|
+
event.preventDefault();
|
|
197
|
+
this.pendingWheel += wheelPixels(event);
|
|
198
|
+
};
|
|
199
|
+
onMouseMove = (event) => {
|
|
200
|
+
if (!this.locked)
|
|
201
|
+
return;
|
|
202
|
+
this.pendingMove.x += event.movementX;
|
|
203
|
+
this.pendingMove.y += event.movementY;
|
|
204
|
+
};
|
|
205
|
+
}
|