@waica/engine 0.24.0 → 0.26.0
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 +79 -1
- package/dist/anchored-pieces.d.ts +3 -2
- package/dist/anchored-pieces.js +12 -8
- package/dist/archetype.d.ts +2 -2
- package/dist/assets/asset-loader.d.ts +28 -5
- package/dist/assets/asset-loader.js +63 -7
- package/dist/assets/model-backend.d.ts +21 -0
- package/dist/assets/model-backend.js +21 -0
- package/dist/assets/model-cache.d.ts +47 -0
- package/dist/assets/model-cache.js +117 -0
- package/dist/audio/audio-subsystem.d.ts +6 -1
- package/dist/audio/audio-subsystem.js +8 -15
- package/dist/audio/placement.d.ts +41 -0
- package/dist/audio/placement.js +21 -0
- package/dist/audio/spatial.d.ts +20 -0
- package/dist/audio/spatial.js +26 -0
- package/dist/audio/types.d.ts +7 -1
- package/dist/camera-projection.d.ts +38 -0
- package/dist/camera-projection.js +44 -0
- package/dist/camera-rig.d.ts +48 -0
- package/dist/camera-rig.js +98 -0
- package/dist/camera.d.ts +20 -3
- package/dist/camera.js +3 -1
- package/dist/component.d.ts +38 -2
- package/dist/component.js +3 -0
- package/dist/components/animated-sprite.d.ts +8 -1
- package/dist/components/animated-sprite.js +16 -1
- package/dist/components/collider.d.ts +89 -0
- package/dist/components/collider.js +57 -0
- package/dist/components/dynamic-body.d.ts +2 -1
- package/dist/components/dynamic-body.js +1 -0
- package/dist/components/hitbox.d.ts +2 -1
- package/dist/components/hitbox.js +1 -0
- package/dist/components/light.d.ts +69 -0
- package/dist/components/light.js +60 -0
- package/dist/components/model.d.ts +63 -0
- package/dist/components/model.js +117 -0
- package/dist/components/particle-emitter.d.ts +8 -1
- package/dist/components/particle-emitter.js +14 -0
- package/dist/components/point-light.d.ts +75 -0
- package/dist/components/point-light.js +102 -0
- package/dist/components/rigid-body.d.ts +114 -0
- package/dist/components/rigid-body.js +83 -0
- package/dist/components/solid.d.ts +2 -1
- package/dist/components/solid.js +1 -0
- package/dist/components/sprite.d.ts +8 -1
- package/dist/components/sprite.js +16 -1
- package/dist/components/sun.d.ts +46 -0
- package/dist/components/sun.js +74 -0
- package/dist/components/tilemap.d.ts +2 -1
- package/dist/components/tilemap.js +7 -0
- package/dist/entity-transform.d.ts +18 -0
- package/dist/entity-transform.js +17 -0
- package/dist/frame-quad.d.ts +21 -0
- package/dist/frame-quad.js +50 -0
- package/dist/game.d.ts +59 -15
- package/dist/game.js +100 -75
- package/dist/index.d.ts +39 -4
- package/dist/index.js +18 -1
- package/dist/light-field.d.ts +78 -0
- package/dist/light-field.js +145 -0
- package/dist/light-map-material.d.ts +40 -0
- package/dist/light-map-material.js +203 -0
- package/dist/light-map.d.ts +46 -0
- package/dist/light-map.js +132 -0
- package/dist/lit-frame.d.ts +48 -0
- package/dist/lit-frame.js +174 -0
- package/dist/occluder-grid.d.ts +19 -0
- package/dist/occluder-grid.js +50 -0
- package/dist/particle-batch.js +3 -2
- package/dist/physics-3d/body-contacts.d.ts +10 -0
- package/dist/physics-3d/body-contacts.js +59 -0
- package/dist/physics-3d/body-sync.d.ts +52 -0
- package/dist/physics-3d/body-sync.js +153 -0
- package/dist/physics-3d/character-controller.d.ts +44 -0
- package/dist/physics-3d/character-controller.js +126 -0
- package/dist/physics-3d/physics-host.d.ts +86 -0
- package/dist/physics-3d/physics-host.js +174 -0
- package/dist/physics-3d/physics-world.d.ts +65 -0
- package/dist/physics-3d/physics-world.js +158 -0
- package/dist/physics-3d/rapier-module.d.ts +17 -0
- package/dist/physics-3d/rapier-module.js +19 -0
- package/dist/physics-3d/sensor-dispatch.d.ts +10 -0
- package/dist/physics-3d/sensor-dispatch.js +77 -0
- package/dist/physics-3d/world-queries.d.ts +39 -0
- package/dist/physics-3d/world-queries.js +63 -0
- package/dist/pointer-raycast.d.ts +17 -0
- package/dist/pointer-raycast.js +48 -0
- package/dist/pointer.d.ts +27 -6
- package/dist/pointer.js +43 -17
- package/dist/post-effects.d.ts +33 -0
- package/dist/post-effects.js +48 -0
- package/dist/post-pass.d.ts +23 -0
- package/dist/post-pass.js +93 -0
- package/dist/render-layers.d.ts +18 -0
- package/dist/render-layers.js +19 -0
- package/dist/render-output.d.ts +7 -0
- package/dist/render-output.js +17 -1
- package/dist/runtime-bridge.d.ts +1 -1
- package/dist/runtime-inspection.d.ts +14 -0
- package/dist/runtime-inspection.js +12 -2
- package/dist/runtime-lighting-snapshot.d.ts +67 -0
- package/dist/runtime-lighting-snapshot.js +77 -0
- package/dist/runtime-physics-snapshot.d.ts +41 -0
- package/dist/runtime-physics-snapshot.js +45 -0
- package/dist/runtime-view-snapshot.d.ts +25 -0
- package/dist/runtime-view-snapshot.js +30 -0
- package/dist/scene-camera-3d.d.ts +66 -0
- package/dist/scene-camera-3d.js +117 -0
- package/dist/scene-lighting.d.ts +52 -0
- package/dist/scene-lighting.js +107 -0
- package/dist/scene-lights-3d.d.ts +17 -0
- package/dist/scene-lights-3d.js +27 -0
- package/dist/scene-render-options.d.ts +51 -0
- package/dist/scene-render-options.js +126 -0
- package/dist/scene-simulation.d.ts +22 -0
- package/dist/scene-simulation.js +29 -0
- package/dist/scene-space.d.ts +50 -0
- package/dist/scene-space.js +103 -0
- package/dist/scene.d.ts +41 -2
- package/dist/scene.js +11 -4
- package/dist/simulation-phases.d.ts +9 -0
- package/dist/simulation-phases.js +19 -0
- package/dist/spatial-query-3d.d.ts +48 -0
- package/dist/spatial-query-3d.js +96 -0
- package/dist/spatial-query.d.ts +18 -4
- package/dist/spatial-query.js +57 -1
- package/dist/sprite-batch.js +5 -1
- package/package.json +2 -1
|
@@ -0,0 +1,17 @@
|
|
|
1
|
+
import * as THREE from 'three/webgpu';
|
|
2
|
+
/**
|
|
3
|
+
* Applies a declared transform to a freshly spawned entity. A two-number
|
|
4
|
+
* position behaves exactly as it always did (z = 0); `rotation` is converted
|
|
5
|
+
* from degrees and `scale` replaces the node's unit scale.
|
|
6
|
+
*/
|
|
7
|
+
export function applyTransformJson(entity, json) {
|
|
8
|
+
const { position, rotation, scale } = json;
|
|
9
|
+
if (position)
|
|
10
|
+
entity.position.set(position[0], position[1], position[2] ?? 0);
|
|
11
|
+
if (rotation) {
|
|
12
|
+
const [x, y, z] = rotation;
|
|
13
|
+
entity.node.rotation.set(THREE.MathUtils.degToRad(x), THREE.MathUtils.degToRad(y), THREE.MathUtils.degToRad(z), 'XYZ');
|
|
14
|
+
}
|
|
15
|
+
if (scale)
|
|
16
|
+
entity.node.scale.set(scale[0], scale[1], scale[2]);
|
|
17
|
+
}
|
|
@@ -0,0 +1,21 @@
|
|
|
1
|
+
import * as THREE from 'three/webgpu';
|
|
2
|
+
/** Whether `value` is the scene of a FrameQuad. */
|
|
3
|
+
export declare function isFrameQuad(value: unknown): boolean;
|
|
4
|
+
/**
|
|
5
|
+
* A full-viewport pass drawn as an ordinary scene with its own camera — not
|
|
6
|
+
* a THREE.QuadMesh: on the WebGPU backend a QuadMesh drawn to the canvas
|
|
7
|
+
* marks that render a full-screen pass, and the next render to the canvas in
|
|
8
|
+
* the same frame then loses everything drawn before it (seen in the issue
|
|
9
|
+
* #78 e2e). The lit frame draws its Emissive drawables after its multiply
|
|
10
|
+
* quad, so its quad must be ordinary.
|
|
11
|
+
*/
|
|
12
|
+
export declare class FrameQuad {
|
|
13
|
+
readonly material: THREE.Material;
|
|
14
|
+
private readonly scene;
|
|
15
|
+
private readonly camera;
|
|
16
|
+
private readonly mesh;
|
|
17
|
+
constructor(material: THREE.Material);
|
|
18
|
+
/** Draws the quad into whatever is bound now, inside the current viewport. */
|
|
19
|
+
render(renderer: THREE.WebGPURenderer): void;
|
|
20
|
+
dispose(): void;
|
|
21
|
+
}
|
|
@@ -0,0 +1,50 @@
|
|
|
1
|
+
import * as THREE from 'three/webgpu';
|
|
2
|
+
/** The scenes FrameQuad draws, so a test can tell them from any other render. */
|
|
3
|
+
const quadScenes = new WeakSet();
|
|
4
|
+
/** Whether `value` is the scene of a FrameQuad. */
|
|
5
|
+
export function isFrameQuad(value) {
|
|
6
|
+
return typeof value === 'object' && value !== null && quadScenes.has(value);
|
|
7
|
+
}
|
|
8
|
+
/**
|
|
9
|
+
* Two triangles covering the viewport, with three's QuadMesh UV convention
|
|
10
|
+
* (v = 1 at the bottom), so a render target samples upright on both Render
|
|
11
|
+
* Backends — three flips a render target's v itself on WebGL2.
|
|
12
|
+
*/
|
|
13
|
+
function quadGeometry() {
|
|
14
|
+
const geometry = new THREE.BufferGeometry();
|
|
15
|
+
geometry.setAttribute('position', new THREE.Float32BufferAttribute([-1, -1, 0, 1, -1, 0, 1, 1, 0, -1, 1, 0], 3));
|
|
16
|
+
geometry.setAttribute('uv', new THREE.Float32BufferAttribute([0, 1, 1, 1, 1, 0, 0, 0], 2));
|
|
17
|
+
geometry.setIndex([0, 1, 2, 0, 2, 3]);
|
|
18
|
+
return geometry;
|
|
19
|
+
}
|
|
20
|
+
/**
|
|
21
|
+
* A full-viewport pass drawn as an ordinary scene with its own camera — not
|
|
22
|
+
* a THREE.QuadMesh: on the WebGPU backend a QuadMesh drawn to the canvas
|
|
23
|
+
* marks that render a full-screen pass, and the next render to the canvas in
|
|
24
|
+
* the same frame then loses everything drawn before it (seen in the issue
|
|
25
|
+
* #78 e2e). The lit frame draws its Emissive drawables after its multiply
|
|
26
|
+
* quad, so its quad must be ordinary.
|
|
27
|
+
*/
|
|
28
|
+
export class FrameQuad {
|
|
29
|
+
material;
|
|
30
|
+
scene = new THREE.Scene();
|
|
31
|
+
camera = new THREE.OrthographicCamera(-1, 1, 1, -1, 0, 1);
|
|
32
|
+
mesh;
|
|
33
|
+
constructor(material) {
|
|
34
|
+
this.material = material;
|
|
35
|
+
this.mesh = new THREE.Mesh(quadGeometry(), material);
|
|
36
|
+
this.mesh.position.z = -0.5;
|
|
37
|
+
this.mesh.frustumCulled = false;
|
|
38
|
+
this.scene.add(this.mesh);
|
|
39
|
+
this.scene.matrixWorldAutoUpdate = true;
|
|
40
|
+
quadScenes.add(this.scene);
|
|
41
|
+
}
|
|
42
|
+
/** Draws the quad into whatever is bound now, inside the current viewport. */
|
|
43
|
+
render(renderer) {
|
|
44
|
+
renderer.render(this.scene, this.camera);
|
|
45
|
+
}
|
|
46
|
+
dispose() {
|
|
47
|
+
this.mesh.geometry.dispose();
|
|
48
|
+
this.material.dispose();
|
|
49
|
+
}
|
|
50
|
+
}
|
package/dist/game.d.ts
CHANGED
|
@@ -1,18 +1,28 @@
|
|
|
1
1
|
import * as THREE from 'three/webgpu';
|
|
2
2
|
import { AssetLoader } from './assets/asset-loader.js';
|
|
3
|
+
import type { ModelBackend } from './assets/model-backend.js';
|
|
3
4
|
import type { TextureBackend } from './assets/texture-backend.js';
|
|
4
5
|
import type { AudioBackend } from './audio/backend.js';
|
|
5
6
|
import { AudioSubsystem } from './audio/audio-subsystem.js';
|
|
6
|
-
import {
|
|
7
|
+
import type { SceneCameraJson } from './camera.js';
|
|
8
|
+
import { type GameCamera, type WorldPoint } from './camera-projection.js';
|
|
9
|
+
import { CameraRig } from './camera-rig.js';
|
|
7
10
|
import { CameraEffects } from './camera-effects.js';
|
|
8
11
|
import type { Component } from './component.js';
|
|
9
12
|
import { Entity } from './entity.js';
|
|
13
|
+
import type { PositionJson } from './entity-transform.js';
|
|
10
14
|
import { Emitter } from './events.js';
|
|
11
15
|
import { GameTime } from './game-time.js';
|
|
12
16
|
import { Input, type InputBindings } from './input.js';
|
|
17
|
+
import { PhysicsHost } from './physics-3d/physics-host.js';
|
|
18
|
+
import type { PhysicsBackend } from './physics-3d/rapier-module.js';
|
|
13
19
|
import { Pointer } from './pointer.js';
|
|
20
|
+
import { GamePost } from './post-effects.js';
|
|
14
21
|
import { type RenderBackend } from './render-readiness.js';
|
|
15
22
|
import { type SceneJson, type SceneRegistry, type SceneRenderJson } from './scene.js';
|
|
23
|
+
import { type SceneSimulationJson } from './scene-simulation.js';
|
|
24
|
+
import { type SceneSpace } from './scene-space.js';
|
|
25
|
+
import { GameLighting } from './scene-lighting.js';
|
|
16
26
|
import { type SpatialQuery } from './spatial-query.js';
|
|
17
27
|
import { Stats, type StatValue } from './stats.js';
|
|
18
28
|
import { GameUi } from './ui.js';
|
|
@@ -31,7 +41,7 @@ export interface GameOptions {
|
|
|
31
41
|
/** Fixed resolution (from the project's game.json); absent = fill the canvas. */
|
|
32
42
|
resolution?: GameResolution;
|
|
33
43
|
/** Control overrides (action → key and `Gamepad:` codes) on top of the defaults. */
|
|
34
|
-
bindings?: InputBindings
|
|
44
|
+
bindings?: Readonly<InputBindings>;
|
|
35
45
|
/** Radial stick dead zone of `game.input`, in [0, 1); absent or invalid means 0.2 (ADR 0023). */
|
|
36
46
|
gamepadDeadZone?: number;
|
|
37
47
|
/** Initial stat values (points, lives…) from the project's stats.json. */
|
|
@@ -51,11 +61,20 @@ export interface GameOptions {
|
|
|
51
61
|
* the real backend either way; `game.assets` always exists.
|
|
52
62
|
*/
|
|
53
63
|
textures?: TextureBackend;
|
|
64
|
+
/** Replaces the real `GLTFLoader` implementation behind `game.assets.model` (the same seam, for glTF). */
|
|
65
|
+
models?: ModelBackend;
|
|
66
|
+
/**
|
|
67
|
+
* Where Rapier comes from for a 3D scene (ADR 0013's seam, applied to
|
|
68
|
+
* physics): resolves to the initialized module. Defaults to the real
|
|
69
|
+
* dynamic import, which only a 3D scene triggers; a project's own tests
|
|
70
|
+
* pass the module they initialized once, or a rejecting stub.
|
|
71
|
+
*/
|
|
72
|
+
physics?: PhysicsBackend;
|
|
54
73
|
}
|
|
55
74
|
export type UpdateFn = (dt: number) => void;
|
|
56
75
|
export interface SpawnPrefabOptions {
|
|
57
76
|
name?: string;
|
|
58
|
-
position?:
|
|
77
|
+
position?: PositionJson;
|
|
59
78
|
}
|
|
60
79
|
/** The Project's scenes by name (a file's stem), plus the registry shared by all of them. */
|
|
61
80
|
export interface SceneCatalog {
|
|
@@ -70,7 +89,6 @@ export type ParamOverrides = Record<string, Record<string, Record<string, number
|
|
|
70
89
|
*/
|
|
71
90
|
export declare class Game {
|
|
72
91
|
readonly scene: THREE.Scene<THREE.Object3DEventMap>;
|
|
73
|
-
readonly camera: THREE.OrthographicCamera;
|
|
74
92
|
readonly input: Input;
|
|
75
93
|
readonly pointer: Pointer;
|
|
76
94
|
readonly query: SpatialQuery;
|
|
@@ -94,6 +112,12 @@ export declare class Game {
|
|
|
94
112
|
readonly cameraEffects: CameraEffects;
|
|
95
113
|
/** Simulated scheduling: `after`, `every`, `tween`, `now`. See ADR 0017. */
|
|
96
114
|
readonly time: GameTime;
|
|
115
|
+
/** The live scene's Ambient Light and Lights (ADR 0026); it dies with its scene. */
|
|
116
|
+
readonly lighting: GameLighting;
|
|
117
|
+
/** The live scene's Post Effects (vignette, color grade); they die with their scene. */
|
|
118
|
+
readonly post: GamePost;
|
|
119
|
+
/** The live 3D scene's Rapier world and bodies (ADR 0028); package-internal, read by the physics components and the Runtime Snapshot. */
|
|
120
|
+
readonly physics: PhysicsHost;
|
|
97
121
|
/** Registry retained by loadScene for runtime prefab spawning. */
|
|
98
122
|
registry: SceneRegistry | null;
|
|
99
123
|
paramOverrides: ParamOverrides;
|
|
@@ -108,6 +132,8 @@ export declare class Game {
|
|
|
108
132
|
private readonly resizeObserver;
|
|
109
133
|
/** Scene-scoped Sprite Batches (ADR 0024); `render.batch: false` opts a scene out. */
|
|
110
134
|
private readonly spriteBatches;
|
|
135
|
+
/** Off, lit or through the Post Effects: how each frame is drawn (ADR 0026). */
|
|
136
|
+
private readonly frame;
|
|
111
137
|
private readonly updateFns;
|
|
112
138
|
private readonly invalidUpdateCompositions;
|
|
113
139
|
/**
|
|
@@ -123,8 +149,17 @@ export declare class Game {
|
|
|
123
149
|
private readonly baseViewHeight;
|
|
124
150
|
private viewHeight;
|
|
125
151
|
private sceneCamera;
|
|
152
|
+
/** The orthographic camera of 2D scenes and the perspective one of 3D scenes (ADR 0027); read by the Runtime Snapshot, not part of the game-facing API. */
|
|
153
|
+
readonly cameraRig: CameraRig;
|
|
154
|
+
private sceneSpace;
|
|
155
|
+
/** A 3D scene's stereo pan, reading the live camera at every call (built once, not per frame). */
|
|
156
|
+
private readonly screenPan;
|
|
157
|
+
/** What following an entity reads from the Game, built once rather than per Simulation Step. */
|
|
158
|
+
private readonly followHost;
|
|
126
159
|
private renderSort;
|
|
127
160
|
private sceneProjection;
|
|
161
|
+
/** The Ambient Light of a 3D scene, fed by `lighting.ambient` (ADR 0027). */
|
|
162
|
+
private readonly ambientLight3d;
|
|
128
163
|
/** Timestamp of the last animation frame; null until the loop's first frame seeds it. */
|
|
129
164
|
private lastTime;
|
|
130
165
|
/** Seconds of elapsed time not yet worth a whole Simulation Step (ADR 0014). */
|
|
@@ -145,7 +180,9 @@ export declare class Game {
|
|
|
145
180
|
constructor(options: GameOptions);
|
|
146
181
|
/**
|
|
147
182
|
* Resolves once the renderer can draw; rejects, naming both Render
|
|
148
|
-
* Backends, when neither initializes.
|
|
183
|
+
* Backends, when neither initializes. While the live scene is 3D it also
|
|
184
|
+
* waits for the physics module, and rejects naming its package if that
|
|
185
|
+
* fails to load (ADR 0028).
|
|
149
186
|
* Frames before it simulate but draw nothing, so a host awaits it before
|
|
150
187
|
* its first frame (ADR 0025).
|
|
151
188
|
*/
|
|
@@ -203,6 +240,8 @@ export declare class Game {
|
|
|
203
240
|
* draw order stays layer-banded with spawn-order ties.
|
|
204
241
|
*/
|
|
205
242
|
setSceneRender(json?: SceneRenderJson): void;
|
|
243
|
+
/** Adopts a scene's simulation block (gravity); called by loadScene beside setSceneRender. */
|
|
244
|
+
setSceneSimulation(json?: SceneSimulationJson): void;
|
|
206
245
|
/**
|
|
207
246
|
* Adopts a scene's camera block: jumps to its framing and, while
|
|
208
247
|
* simulating, follows/clamps per its settings. Called by loadScene.
|
|
@@ -214,7 +253,21 @@ export declare class Game {
|
|
|
214
253
|
removeEntity(entity: Entity): void;
|
|
215
254
|
/** The scene's render projection; null keeps logical and render space identical. */
|
|
216
255
|
get projection(): 'isometric' | null;
|
|
217
|
-
/**
|
|
256
|
+
/** The live scene's space (`render.space`); `'2d'` with no scene (ADR 0027). */
|
|
257
|
+
get space(): SceneSpace;
|
|
258
|
+
/** The camera the live scene draws with: orthographic in 2D scenes, perspective in 3D ones. */
|
|
259
|
+
get camera(): GameCamera;
|
|
260
|
+
/**
|
|
261
|
+
* Where a logical point falls on the canvas, in CSS px from its top-left
|
|
262
|
+
* corner inside the letterbox; null when a perspective camera cannot see it
|
|
263
|
+
* (behind its near plane). The one projection Anchored Pieces, spatial
|
|
264
|
+
* audio and the editor share (CA-6).
|
|
265
|
+
*/
|
|
266
|
+
worldToScreen(point: WorldPoint): {
|
|
267
|
+
x: number;
|
|
268
|
+
y: number;
|
|
269
|
+
} | null;
|
|
270
|
+
/** Visible world height (2D camera zoom); a perspective camera does not use it. */
|
|
218
271
|
get view(): number;
|
|
219
272
|
setViewHeight(value: number): void;
|
|
220
273
|
/** Shuts the game down completely (loop, input, GPU). */
|
|
@@ -271,14 +324,5 @@ export declare class Game {
|
|
|
271
324
|
private componentUpdateSchedule;
|
|
272
325
|
private updateSceneCamera;
|
|
273
326
|
private renderPoint;
|
|
274
|
-
/**
|
|
275
|
-
* The audio listener's position (CA-8) in logical coordinates. The camera
|
|
276
|
-
* itself only ever holds render-space coordinates (see `updateSceneCamera`,
|
|
277
|
-
* `setSceneCamera`), so under `projection: 'isometric'` this is the exact
|
|
278
|
-
* inverse of `renderPoint` — without it, distance-based attenuation would
|
|
279
|
-
* measure render-space distance instead of real game distance.
|
|
280
|
-
*/
|
|
281
|
-
private audioListenerPosition;
|
|
282
|
-
private dispatchCollisions;
|
|
283
327
|
private resize;
|
|
284
328
|
}
|
package/dist/game.js
CHANGED
|
@@ -2,8 +2,9 @@ import * as THREE from 'three/webgpu';
|
|
|
2
2
|
import { gameViewport } from './anchored-pieces.js';
|
|
3
3
|
import { AssetLoader } from './assets/asset-loader.js';
|
|
4
4
|
import { AudioSubsystem } from './audio/audio-subsystem.js';
|
|
5
|
-
import {
|
|
6
|
-
import {
|
|
5
|
+
import { listenerPosition, screenPanOf } from './audio/spatial.js';
|
|
6
|
+
import { worldToNormalized } from './camera-projection.js';
|
|
7
|
+
import { CameraRig } from './camera-rig.js';
|
|
7
8
|
import { advanceCameraEffects, CameraEffects, layoutCameraEffects } from './camera-effects.js';
|
|
8
9
|
import { implementsOnUpdate, resolveComponentUpdateSchedule } from './component-update-schedule.js';
|
|
9
10
|
import { Entity } from './entity.js';
|
|
@@ -11,15 +12,23 @@ import { Emitter } from './events.js';
|
|
|
11
12
|
import { consumeSimulationSteps, MAX_CHAINED_HOPS, SIMULATION_STEP, snapElapsedToStep, } from './fixed-step.js';
|
|
12
13
|
import { advanceGameTime, GameTime } from './game-time.js';
|
|
13
14
|
import { Input } from './input.js';
|
|
15
|
+
import { PhysicsHost } from './physics-3d/physics-host.js';
|
|
14
16
|
import { Pointer } from './pointer.js';
|
|
17
|
+
import { GamePost } from './post-effects.js';
|
|
15
18
|
import { activeRuntimeBridgeHook, EngineRuntimeBridge, } from './runtime-bridge.js';
|
|
16
19
|
import { RuntimeInspector } from './runtime-inspection.js';
|
|
17
|
-
import {
|
|
20
|
+
import { FrameComposer } from './lit-frame.js';
|
|
21
|
+
import { projectIsometric } from './projection.js';
|
|
18
22
|
import { canvasBackground, drawStraightToCanvas, mapUvPerVertex } from './render-output.js';
|
|
19
23
|
import { RenderReadiness } from './render-readiness.js';
|
|
20
24
|
import { applyYSort } from './render-sort.js';
|
|
21
25
|
import { loadScene, registryEntry, spawnFromJson, } from './scene.js';
|
|
22
26
|
import { sceneDrainsOf } from './scene-drains.js';
|
|
27
|
+
import { resolveSceneSimulation } from './scene-simulation.js';
|
|
28
|
+
import { SceneAmbientLight3d } from './scene-lights-3d.js';
|
|
29
|
+
import { resolveRenderPolicy } from './scene-space.js';
|
|
30
|
+
import { GameLighting } from './scene-lighting.js';
|
|
31
|
+
import { runSimulationPhases } from './simulation-phases.js';
|
|
23
32
|
import { createSpriteBatches } from './sprite-batches.js';
|
|
24
33
|
import { createSpatialQuery } from './spatial-query.js';
|
|
25
34
|
import { Stats } from './stats.js';
|
|
@@ -40,7 +49,6 @@ function componentsInOrder(order, byName) {
|
|
|
40
49
|
*/
|
|
41
50
|
export class Game {
|
|
42
51
|
scene = new THREE.Scene();
|
|
43
|
-
camera;
|
|
44
52
|
input;
|
|
45
53
|
pointer;
|
|
46
54
|
query;
|
|
@@ -64,6 +72,12 @@ export class Game {
|
|
|
64
72
|
cameraEffects;
|
|
65
73
|
/** Simulated scheduling: `after`, `every`, `tween`, `now`. See ADR 0017. */
|
|
66
74
|
time = new GameTime();
|
|
75
|
+
/** The live scene's Ambient Light and Lights (ADR 0026); it dies with its scene. */
|
|
76
|
+
lighting = new GameLighting();
|
|
77
|
+
/** The live scene's Post Effects (vignette, color grade); they die with their scene. */
|
|
78
|
+
post = new GamePost();
|
|
79
|
+
/** The live 3D scene's Rapier world and bodies (ADR 0028); package-internal, read by the physics components and the Runtime Snapshot. */
|
|
80
|
+
physics;
|
|
67
81
|
/** Registry retained by loadScene for runtime prefab spawning. */
|
|
68
82
|
registry = null;
|
|
69
83
|
paramOverrides = {};
|
|
@@ -78,6 +92,8 @@ export class Game {
|
|
|
78
92
|
resizeObserver;
|
|
79
93
|
/** Scene-scoped Sprite Batches (ADR 0024); `render.batch: false` opts a scene out. */
|
|
80
94
|
spriteBatches;
|
|
95
|
+
/** Off, lit or through the Post Effects: how each frame is drawn (ADR 0026). */
|
|
96
|
+
frame;
|
|
81
97
|
updateFns = new Set();
|
|
82
98
|
invalidUpdateCompositions = new WeakMap();
|
|
83
99
|
/**
|
|
@@ -93,8 +109,21 @@ export class Game {
|
|
|
93
109
|
baseViewHeight;
|
|
94
110
|
viewHeight;
|
|
95
111
|
sceneCamera = null;
|
|
112
|
+
/** The orthographic camera of 2D scenes and the perspective one of 3D scenes (ADR 0027); read by the Runtime Snapshot, not part of the game-facing API. */
|
|
113
|
+
cameraRig = new CameraRig();
|
|
114
|
+
sceneSpace = '2d';
|
|
115
|
+
/** A 3D scene's stereo pan, reading the live camera at every call (built once, not per frame). */
|
|
116
|
+
screenPan = screenPanOf(() => this.camera);
|
|
117
|
+
/** What following an entity reads from the Game, built once rather than per Simulation Step. */
|
|
118
|
+
followHost = {
|
|
119
|
+
find: (name) => this.find(name),
|
|
120
|
+
renderPoint: (x, y) => this.renderPoint(x, y),
|
|
121
|
+
viewHeight: () => this.viewHeight,
|
|
122
|
+
};
|
|
96
123
|
renderSort = null;
|
|
97
124
|
sceneProjection = null;
|
|
125
|
+
/** The Ambient Light of a 3D scene, fed by `lighting.ambient` (ADR 0027). */
|
|
126
|
+
ambientLight3d = new SceneAmbientLight3d(this.scene);
|
|
98
127
|
/** Timestamp of the last animation frame; null until the loop's first frame seeds it. */
|
|
99
128
|
lastTime = null;
|
|
100
129
|
/** Seconds of elapsed time not yet worth a whole Simulation Step (ADR 0014). */
|
|
@@ -137,11 +166,13 @@ export class Game {
|
|
|
137
166
|
});
|
|
138
167
|
this.assets = new AssetLoader({
|
|
139
168
|
backend: options.textures,
|
|
169
|
+
models: options.models,
|
|
140
170
|
// Same late lookup as audio's: preload() resolves through whatever
|
|
141
171
|
// catalog is registered at call time, which outlives every scene.
|
|
142
172
|
resolveAsset: (uri) => this.sceneCatalog?.registry.resolveAsset?.(uri) ?? uri,
|
|
143
173
|
});
|
|
144
174
|
this.spriteBatches = createSpriteBatches(this, this.assets);
|
|
175
|
+
this.physics = new PhysicsHost(this, options.physics);
|
|
145
176
|
this.cameraEffects = new CameraEffects({
|
|
146
177
|
host: () => canvas.parentElement ?? document.body,
|
|
147
178
|
// One screen pixel in world units: the shake snaps to it (never the base).
|
|
@@ -160,10 +191,10 @@ export class Game {
|
|
|
160
191
|
activeRuntimeBridgeHook()?.fail?.({ code: 'render-backend-failed', message: error.message });
|
|
161
192
|
});
|
|
162
193
|
this.scene.background = canvasBackground(background);
|
|
163
|
-
|
|
164
|
-
this.
|
|
194
|
+
const { renderer, spriteBatches, resolution } = this;
|
|
195
|
+
this.frame = new FrameComposer({ game: this, renderer, spriteBatches, resolution });
|
|
165
196
|
this.pointer = new Pointer(canvas, {
|
|
166
|
-
camera: this.camera,
|
|
197
|
+
camera: () => this.camera,
|
|
167
198
|
resolution: this.resolution,
|
|
168
199
|
projection: () => this.sceneProjection,
|
|
169
200
|
entities: this.entities,
|
|
@@ -174,12 +205,14 @@ export class Game {
|
|
|
174
205
|
}
|
|
175
206
|
/**
|
|
176
207
|
* Resolves once the renderer can draw; rejects, naming both Render
|
|
177
|
-
* Backends, when neither initializes.
|
|
208
|
+
* Backends, when neither initializes. While the live scene is 3D it also
|
|
209
|
+
* waits for the physics module, and rejects naming its package if that
|
|
210
|
+
* fails to load (ADR 0028).
|
|
178
211
|
* Frames before it simulate but draw nothing, so a host awaits it before
|
|
179
212
|
* its first frame (ADR 0025).
|
|
180
213
|
*/
|
|
181
214
|
ready() {
|
|
182
|
-
return this.readiness.promise;
|
|
215
|
+
return this.physics.whenReady(this.readiness.promise);
|
|
183
216
|
}
|
|
184
217
|
/** The Render Backend the renderer settled on; null until ready() resolved. */
|
|
185
218
|
get backend() {
|
|
@@ -228,6 +261,8 @@ export class Game {
|
|
|
228
261
|
// destroyed below, so an owner's own destroy() cancellation is a no-op.
|
|
229
262
|
this.time.cancelSceneScoped();
|
|
230
263
|
this.cameraEffects.unloadScene();
|
|
264
|
+
this.lighting.unloadScene();
|
|
265
|
+
this.post.unloadScene();
|
|
231
266
|
// An explicit unload means "no scene": a swap queued earlier this frame
|
|
232
267
|
// would otherwise flush next frame and resurrect one.
|
|
233
268
|
this.pendingSceneLoad = null;
|
|
@@ -235,6 +270,8 @@ export class Game {
|
|
|
235
270
|
// Pointer holds that array by reference, so it must never be reassigned.
|
|
236
271
|
for (const entity of [...this.entities])
|
|
237
272
|
entity.destroy();
|
|
273
|
+
// After the cascade removed every body: the world goes with its scene.
|
|
274
|
+
this.physics.unloadScene();
|
|
238
275
|
// Drains transferred by the destruction cascade above are scene-scoped too.
|
|
239
276
|
sceneDrainsOf(this).clear();
|
|
240
277
|
// After the cascade freed every slot: the batches go with their scene.
|
|
@@ -243,6 +280,8 @@ export class Game {
|
|
|
243
280
|
this.renderSort = null;
|
|
244
281
|
this.sceneProjection = null;
|
|
245
282
|
this.sceneCamera = null;
|
|
283
|
+
this.sceneSpace = '2d';
|
|
284
|
+
this.cameraRig.reset();
|
|
246
285
|
this.liveSceneName = null;
|
|
247
286
|
this.setViewHeight(this.baseViewHeight);
|
|
248
287
|
}
|
|
@@ -332,9 +371,13 @@ export class Game {
|
|
|
332
371
|
* draw order stays layer-banded with spawn-order ties.
|
|
333
372
|
*/
|
|
334
373
|
setSceneRender(json) {
|
|
335
|
-
|
|
374
|
+
const { space, sort, projection } = resolveRenderPolicy(json);
|
|
375
|
+
this.sceneSpace = space;
|
|
376
|
+
this.renderSort = sort;
|
|
336
377
|
this.spriteBatches.enabled = json?.batch !== false;
|
|
337
|
-
|
|
378
|
+
this.lighting.loadScene(json);
|
|
379
|
+
this.post.loadScene(json);
|
|
380
|
+
this.physics.enterScene(space);
|
|
338
381
|
if (projection === this.sceneProjection)
|
|
339
382
|
return;
|
|
340
383
|
this.sceneProjection = projection;
|
|
@@ -344,26 +387,28 @@ export class Game {
|
|
|
344
387
|
component.onProjectionChange?.(projection);
|
|
345
388
|
}
|
|
346
389
|
}
|
|
390
|
+
/** Adopts a scene's simulation block (gravity); called by loadScene beside setSceneRender. */
|
|
391
|
+
setSceneSimulation(json) {
|
|
392
|
+
this.physics.setSimulation(resolveSceneSimulation(json));
|
|
393
|
+
}
|
|
347
394
|
/**
|
|
348
395
|
* Adopts a scene's camera block: jumps to its framing and, while
|
|
349
396
|
* simulating, follows/clamps per its settings. Called by loadScene.
|
|
350
397
|
*/
|
|
351
398
|
setSceneCamera(json) {
|
|
352
|
-
// No
|
|
353
|
-
|
|
354
|
-
|
|
399
|
+
// No orthographic block (none, or a 3D scene): leave the camera to the host.
|
|
400
|
+
const cam = (this.sceneCamera = this.cameraRig.adopt(json, this.sceneSpace));
|
|
401
|
+
if (!cam)
|
|
355
402
|
return;
|
|
356
|
-
}
|
|
357
|
-
this.sceneCamera = resolveSceneCamera(json);
|
|
358
403
|
// With a follow target the declared position is moot: start centered on
|
|
359
404
|
// the target so play begins framed like the editor shows it.
|
|
360
|
-
const followed =
|
|
405
|
+
const followed = cam.follow ? this.find(cam.follow) : undefined;
|
|
361
406
|
const center = followed
|
|
362
407
|
? this.renderPoint(followed.position.x, followed.position.y)
|
|
363
|
-
: { x:
|
|
408
|
+
: { x: cam.position[0], y: cam.position[1] };
|
|
364
409
|
this.camera.position.x = center.x;
|
|
365
410
|
this.camera.position.y = center.y;
|
|
366
|
-
this.setViewHeight(
|
|
411
|
+
this.setViewHeight(cam.zoom);
|
|
367
412
|
}
|
|
368
413
|
start() {
|
|
369
414
|
if (this.disposed)
|
|
@@ -420,11 +465,35 @@ export class Game {
|
|
|
420
465
|
get projection() {
|
|
421
466
|
return this.sceneProjection;
|
|
422
467
|
}
|
|
423
|
-
/**
|
|
468
|
+
/** The live scene's space (`render.space`); `'2d'` with no scene (ADR 0027). */
|
|
469
|
+
get space() {
|
|
470
|
+
return this.sceneSpace;
|
|
471
|
+
}
|
|
472
|
+
/** The camera the live scene draws with: orthographic in 2D scenes, perspective in 3D ones. */
|
|
473
|
+
get camera() {
|
|
474
|
+
return this.cameraRig.camera;
|
|
475
|
+
}
|
|
476
|
+
/**
|
|
477
|
+
* Where a logical point falls on the canvas, in CSS px from its top-left
|
|
478
|
+
* corner inside the letterbox; null when a perspective camera cannot see it
|
|
479
|
+
* (behind its near plane). The one projection Anchored Pieces, spatial
|
|
480
|
+
* audio and the editor share (CA-6).
|
|
481
|
+
*/
|
|
482
|
+
worldToScreen(point) {
|
|
483
|
+
const { clientWidth: w, clientHeight: h } = this.renderer.domElement;
|
|
484
|
+
const view = gameViewport(w, h, this.resolution);
|
|
485
|
+
const render = this.renderPoint(point.x, point.y);
|
|
486
|
+
const at = worldToNormalized(this.camera, { x: render.x, y: render.y, z: point.z });
|
|
487
|
+
return at && { x: view.x + at.nx * view.width, y: view.y + at.ny * view.height };
|
|
488
|
+
}
|
|
489
|
+
/** Visible world height (2D camera zoom); a perspective camera does not use it. */
|
|
424
490
|
get view() {
|
|
425
491
|
return this.viewHeight;
|
|
426
492
|
}
|
|
427
493
|
setViewHeight(value) {
|
|
494
|
+
// A perspective camera frames by its lens, not by a visible height.
|
|
495
|
+
if (this.sceneSpace === '3d')
|
|
496
|
+
return;
|
|
428
497
|
this.viewHeight = Math.min(Math.max(value, 2), 80);
|
|
429
498
|
this.resize();
|
|
430
499
|
}
|
|
@@ -444,11 +513,13 @@ export class Game {
|
|
|
444
513
|
this.time.cancelAll();
|
|
445
514
|
for (const entity of [...this.entities])
|
|
446
515
|
entity.destroy();
|
|
516
|
+
this.physics.dispose();
|
|
447
517
|
sceneDrainsOf(this).clear();
|
|
448
518
|
this.spriteBatches.unload();
|
|
449
519
|
// After the entities: their clones go with the cascade above, the
|
|
450
520
|
// cached bases go here, exactly once (ADR 0019).
|
|
451
521
|
this.assets.dispose();
|
|
522
|
+
this.frame.dispose();
|
|
452
523
|
this.readiness.disposeRenderer();
|
|
453
524
|
}
|
|
454
525
|
/**
|
|
@@ -536,7 +607,7 @@ export class Game {
|
|
|
536
607
|
// Positional audio (CA-8): recomputed every frame, on this same pass —
|
|
537
608
|
// never a second walk of `this.entities`, since `this.audio` already
|
|
538
609
|
// holds direct references to whichever entities are tracked.
|
|
539
|
-
this.audio.updatePlacements(this.
|
|
610
|
+
this.audio.updatePlacements(listenerPosition(this.sceneSpace, this.camera, this.sceneProjection), (x, y) => this.renderPoint(x, y), this.sceneSpace === '3d' ? this.screenPan : undefined);
|
|
540
611
|
this.renderSurface();
|
|
541
612
|
}
|
|
542
613
|
finally {
|
|
@@ -562,7 +633,7 @@ export class Game {
|
|
|
562
633
|
for (const component of schedule)
|
|
563
634
|
component.onUpdate?.(SIMULATION_STEP);
|
|
564
635
|
}
|
|
565
|
-
this
|
|
636
|
+
runSimulationPhases(this);
|
|
566
637
|
this.updateSceneCamera(SIMULATION_STEP);
|
|
567
638
|
advanceCameraEffects(this.cameraEffects);
|
|
568
639
|
this.finishStep();
|
|
@@ -606,11 +677,13 @@ export class Game {
|
|
|
606
677
|
if (this.renderSort === 'y')
|
|
607
678
|
applyYSort(this.entities, sceneDrainsOf(this).participants());
|
|
608
679
|
this.ui.setActive(this.simulate);
|
|
680
|
+
this.ambientLight3d.sync(this.sceneSpace, this.lighting.ambient);
|
|
609
681
|
// The shake offset exists only while drawing (ADR 0020): Anchored Pieces
|
|
610
682
|
// and the render see the drawn center, then the base comes back exactly,
|
|
611
683
|
// so follow smoothing never starts from its own jitter.
|
|
612
684
|
const { x, y } = this.camera.position;
|
|
613
|
-
|
|
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;
|
|
614
687
|
this.camera.position.x = x + shake.x;
|
|
615
688
|
this.camera.position.y = y + shake.y;
|
|
616
689
|
try {
|
|
@@ -618,14 +691,7 @@ export class Game {
|
|
|
618
691
|
// Before ready() the frame simulated, but three cannot draw yet.
|
|
619
692
|
if (!this.readiness.isReady)
|
|
620
693
|
return;
|
|
621
|
-
|
|
622
|
-
// Letterbox bars: clear the whole canvas, then render inside the scissor.
|
|
623
|
-
this.renderer.setScissorTest(false);
|
|
624
|
-
this.renderer.setClearColor(0x000000, 1);
|
|
625
|
-
this.renderer.clear(true, false, false);
|
|
626
|
-
this.renderer.setScissorTest(true);
|
|
627
|
-
}
|
|
628
|
-
this.spriteBatches.drawFrame(this.scene, this.camera, () => this.renderer.render(this.scene, this.camera));
|
|
694
|
+
this.frame.draw();
|
|
629
695
|
}
|
|
630
696
|
finally {
|
|
631
697
|
this.camera.position.x = x;
|
|
@@ -676,48 +742,13 @@ export class Game {
|
|
|
676
742
|
return componentsInOrder(result.order, byName);
|
|
677
743
|
}
|
|
678
744
|
updateSceneCamera(dt) {
|
|
679
|
-
|
|
680
|
-
if (!cam)
|
|
745
|
+
if (!this.sceneCamera)
|
|
681
746
|
return;
|
|
682
|
-
|
|
683
|
-
const provider = followed?.components.find((c) => isCameraVelocityProvider(c));
|
|
684
|
-
const velocity = provider?.getCameraVelocity();
|
|
685
|
-
const target = followed
|
|
686
|
-
? this.renderPoint(followed.position.x, followed.position.y)
|
|
687
|
-
: null;
|
|
688
|
-
const renderVelocity = velocity
|
|
689
|
-
? this.renderPoint(velocity.vx, velocity.vy)
|
|
690
|
-
: { x: 0, y: 0 };
|
|
691
|
-
const next = stepSceneCamera(cam, {
|
|
692
|
-
x: this.camera.position.x,
|
|
693
|
-
y: this.camera.position.y,
|
|
694
|
-
halfW: (this.camera.right - this.camera.left) / 2,
|
|
695
|
-
halfH: this.viewHeight / 2,
|
|
696
|
-
target,
|
|
697
|
-
vx: renderVelocity.x,
|
|
698
|
-
vy: renderVelocity.y,
|
|
699
|
-
dt,
|
|
700
|
-
});
|
|
701
|
-
this.camera.position.x = next.x;
|
|
702
|
-
this.camera.position.y = next.y;
|
|
747
|
+
this.cameraRig.follow(this.sceneCamera, this.followHost, dt);
|
|
703
748
|
}
|
|
704
749
|
renderPoint(x, y) {
|
|
705
750
|
return this.sceneProjection === 'isometric' ? projectIsometric(x, y) : { x, y };
|
|
706
751
|
}
|
|
707
|
-
/**
|
|
708
|
-
* The audio listener's position (CA-8) in logical coordinates. The camera
|
|
709
|
-
* itself only ever holds render-space coordinates (see `updateSceneCamera`,
|
|
710
|
-
* `setSceneCamera`), so under `projection: 'isometric'` this is the exact
|
|
711
|
-
* inverse of `renderPoint` — without it, distance-based attenuation would
|
|
712
|
-
* measure render-space distance instead of real game distance.
|
|
713
|
-
*/
|
|
714
|
-
audioListenerPosition() {
|
|
715
|
-
const { x, y } = this.camera.position;
|
|
716
|
-
return this.sceneProjection === 'isometric' ? unprojectIsometric(x, y) : { x, y };
|
|
717
|
-
}
|
|
718
|
-
dispatchCollisions() {
|
|
719
|
-
dispatchHitboxCollisions(this);
|
|
720
|
-
}
|
|
721
752
|
resize() {
|
|
722
753
|
const canvas = this.renderer.domElement;
|
|
723
754
|
const { clientWidth: w, clientHeight: h } = canvas;
|
|
@@ -735,13 +766,7 @@ export class Game {
|
|
|
735
766
|
this.renderer.setViewport(vx, vy, vw, vh);
|
|
736
767
|
this.renderer.setScissor(vx, vy, vw, vh);
|
|
737
768
|
}
|
|
738
|
-
|
|
739
|
-
const halfW = halfH * aspect;
|
|
740
|
-
this.camera.left = -halfW;
|
|
741
|
-
this.camera.right = halfW;
|
|
742
|
-
this.camera.top = halfH;
|
|
743
|
-
this.camera.bottom = -halfH;
|
|
744
|
-
this.camera.updateProjectionMatrix();
|
|
769
|
+
this.cameraRig.fit(aspect, this.viewHeight);
|
|
745
770
|
layoutCameraEffects(this.cameraEffects, gameViewport(w, h, this.resolution));
|
|
746
771
|
}
|
|
747
772
|
}
|