@waica/engine 0.25.0 → 0.27.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 +77 -1
- package/dist/anchored-pieces.d.ts +3 -2
- package/dist/anchored-pieces.js +12 -8
- package/dist/animation/clip-player.d.ts +7 -0
- package/dist/animation/clip-player.js +9 -0
- package/dist/animation/clip-target.d.ts +38 -0
- package/dist/animation/clip-target.js +9 -0
- package/dist/animation/pose-hash.d.ts +10 -0
- package/dist/animation/pose-hash.js +43 -0
- 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 +14 -3
- package/dist/components/animated-sprite.js +16 -2
- package/dist/components/animator.d.ts +98 -0
- package/dist/components/animator.js +197 -0
- 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 +2 -1
- package/dist/components/light.js +1 -0
- package/dist/components/model.d.ts +71 -0
- package/dist/components/model.js +128 -0
- package/dist/components/particle-emitter.d.ts +2 -1
- package/dist/components/particle-emitter.js +1 -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 +2 -1
- package/dist/components/sprite.js +1 -0
- 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 +1 -0
- package/dist/entity-transform.d.ts +18 -0
- package/dist/entity-transform.js +17 -0
- package/dist/game.d.ts +51 -15
- package/dist/game.js +83 -70
- package/dist/index.d.ts +34 -4
- package/dist/index.js +15 -1
- package/dist/lit-frame.js +10 -3
- 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/runtime-bridge.d.ts +1 -1
- package/dist/runtime-inspection.d.ts +9 -0
- package/dist/runtime-inspection.js +8 -2
- package/dist/runtime-lighting-snapshot.d.ts +27 -1
- package/dist/runtime-lighting-snapshot.js +31 -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-lights-3d.d.ts +17 -0
- package/dist/scene-lights-3d.js +27 -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 +12 -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/state/state-machine.d.ts +32 -8
- package/dist/state/state-machine.js +88 -27
- package/package.json +2 -1
|
@@ -0,0 +1,46 @@
|
|
|
1
|
+
import * as THREE from 'three/webgpu';
|
|
2
|
+
import { Component, type ComponentSpace } from '../component.js';
|
|
3
|
+
import type { Vec3Json } from '../scene-camera-3d.js';
|
|
4
|
+
/**
|
|
5
|
+
* The 3D scene's sunlight (ADR 0027, `CONTEXT.md` Sun): a directional light
|
|
6
|
+
* that shines along `direction`, in world space — where its entity stands, or
|
|
7
|
+
* how it is rotated, does not matter. Color is `0xrrggbb`; no shadows. A Sun
|
|
8
|
+
* in a 2D scene creates nothing. The three light lives in the Game's scene,
|
|
9
|
+
* moves with its props, and leaves with the entity.
|
|
10
|
+
*/
|
|
11
|
+
export declare class Sun extends Component {
|
|
12
|
+
static componentName: string;
|
|
13
|
+
static space: ComponentSpace;
|
|
14
|
+
static params: {
|
|
15
|
+
direction: {
|
|
16
|
+
label: string;
|
|
17
|
+
kind: "vector3";
|
|
18
|
+
};
|
|
19
|
+
color: {
|
|
20
|
+
label: string;
|
|
21
|
+
kind: "color";
|
|
22
|
+
};
|
|
23
|
+
intensity: {
|
|
24
|
+
label: string;
|
|
25
|
+
min: number;
|
|
26
|
+
step: number;
|
|
27
|
+
};
|
|
28
|
+
};
|
|
29
|
+
static transient: string[];
|
|
30
|
+
/** The three light, null in a 2D scene or before the component is ready. */
|
|
31
|
+
light: THREE.DirectionalLight | null;
|
|
32
|
+
private _direction;
|
|
33
|
+
private _color;
|
|
34
|
+
private _intensity;
|
|
35
|
+
/** Where the light travels, as a world-space vector `[x, y, z]`; any length but zero. */
|
|
36
|
+
get direction(): Vec3Json;
|
|
37
|
+
set direction(value: Vec3Json);
|
|
38
|
+
get color(): number;
|
|
39
|
+
set color(value: number);
|
|
40
|
+
get intensity(): number;
|
|
41
|
+
set intensity(value: number);
|
|
42
|
+
onReady(): void;
|
|
43
|
+
onDestroy(): void;
|
|
44
|
+
/** Puts the light on the opposite side of the origin from where it travels, targeting the origin. */
|
|
45
|
+
private aim;
|
|
46
|
+
}
|
|
@@ -0,0 +1,74 @@
|
|
|
1
|
+
import * as THREE from 'three/webgpu';
|
|
2
|
+
import { Component } from '../component.js';
|
|
3
|
+
const DEFAULT_DIRECTION = [-1, -2, -1];
|
|
4
|
+
/** Scene JSON is untyped at runtime: only an array of three finite numbers, not all zero, aims a light. */
|
|
5
|
+
const finiteDirection = (value) => Array.isArray(value) && value.length === 3 && value.every(Number.isFinite) && value.some((component) => component !== 0);
|
|
6
|
+
/**
|
|
7
|
+
* The 3D scene's sunlight (ADR 0027, `CONTEXT.md` Sun): a directional light
|
|
8
|
+
* that shines along `direction`, in world space — where its entity stands, or
|
|
9
|
+
* how it is rotated, does not matter. Color is `0xrrggbb`; no shadows. A Sun
|
|
10
|
+
* in a 2D scene creates nothing. The three light lives in the Game's scene,
|
|
11
|
+
* moves with its props, and leaves with the entity.
|
|
12
|
+
*/
|
|
13
|
+
export class Sun extends Component {
|
|
14
|
+
static componentName = 'Sun';
|
|
15
|
+
static space = '3d';
|
|
16
|
+
static params = {
|
|
17
|
+
direction: { label: 'Direction', kind: 'vector3' },
|
|
18
|
+
color: { label: 'Color', kind: 'color' },
|
|
19
|
+
intensity: { label: 'Intensity', min: 0, step: 0.1 },
|
|
20
|
+
};
|
|
21
|
+
static transient = ['light'];
|
|
22
|
+
/** The three light, null in a 2D scene or before the component is ready. */
|
|
23
|
+
light = null;
|
|
24
|
+
_direction = [...DEFAULT_DIRECTION];
|
|
25
|
+
_color = 0xffffff;
|
|
26
|
+
_intensity = 1;
|
|
27
|
+
/** Where the light travels, as a world-space vector `[x, y, z]`; any length but zero. */
|
|
28
|
+
get direction() {
|
|
29
|
+
return [...this._direction];
|
|
30
|
+
}
|
|
31
|
+
set direction(value) {
|
|
32
|
+
// A zero or unreadable direction keeps the last good one: there is nothing to aim along.
|
|
33
|
+
if (!finiteDirection(value))
|
|
34
|
+
return;
|
|
35
|
+
this._direction = [...value];
|
|
36
|
+
this.aim();
|
|
37
|
+
}
|
|
38
|
+
get color() {
|
|
39
|
+
return this._color;
|
|
40
|
+
}
|
|
41
|
+
set color(value) {
|
|
42
|
+
this._color = value;
|
|
43
|
+
this.light?.color.setHex(value);
|
|
44
|
+
}
|
|
45
|
+
get intensity() {
|
|
46
|
+
return this._intensity;
|
|
47
|
+
}
|
|
48
|
+
set intensity(value) {
|
|
49
|
+
this._intensity = value;
|
|
50
|
+
if (this.light)
|
|
51
|
+
this.light.intensity = value;
|
|
52
|
+
}
|
|
53
|
+
onReady() {
|
|
54
|
+
if (this.game.space !== '3d')
|
|
55
|
+
return;
|
|
56
|
+
const light = new THREE.DirectionalLight(this._color, this._intensity);
|
|
57
|
+
this.light = light;
|
|
58
|
+
// The target joins the scene too, or three never updates its world matrix.
|
|
59
|
+
this.game.scene.add(light, light.target);
|
|
60
|
+
this.aim();
|
|
61
|
+
}
|
|
62
|
+
onDestroy() {
|
|
63
|
+
this.light?.removeFromParent();
|
|
64
|
+
this.light?.target.removeFromParent();
|
|
65
|
+
}
|
|
66
|
+
/** Puts the light on the opposite side of the origin from where it travels, targeting the origin. */
|
|
67
|
+
aim() {
|
|
68
|
+
if (!this.light)
|
|
69
|
+
return;
|
|
70
|
+
const [x, y, z] = this._direction;
|
|
71
|
+
this.light.position.set(-x, -y, -z).normalize();
|
|
72
|
+
this.light.target.position.set(0, 0, 0);
|
|
73
|
+
}
|
|
74
|
+
}
|
|
@@ -1,10 +1,11 @@
|
|
|
1
|
-
import { Component } from '../component.js';
|
|
1
|
+
import { Component, type ComponentSpace } from '../component.js';
|
|
2
2
|
import { SOLID_SOURCE_SYMBOL, type SolidSource } from '../scene-solids.js';
|
|
3
3
|
import { type TilemapCell, type TilemapCellBounds } from '../tilemap-grid.js';
|
|
4
4
|
import { Solid } from './solid.js';
|
|
5
5
|
/** One authorable cell map rendered as a single merged geometry. */
|
|
6
6
|
export declare class Tilemap extends Component implements SolidSource {
|
|
7
7
|
static componentName: string;
|
|
8
|
+
static space: ComponentSpace;
|
|
8
9
|
static params: {
|
|
9
10
|
color: {
|
|
10
11
|
label: string;
|
|
@@ -11,6 +11,7 @@ import { TRANSPARENT_TEXEL_ALPHA } from '../render-layers.js';
|
|
|
11
11
|
/** One authorable cell map rendered as a single merged geometry. */
|
|
12
12
|
export class Tilemap extends Component {
|
|
13
13
|
static componentName = 'Tilemap';
|
|
14
|
+
static space = '2d';
|
|
14
15
|
static params = {
|
|
15
16
|
color: { label: 'color' },
|
|
16
17
|
cols: { label: 'tileset columns', min: 1, step: 1 },
|
|
@@ -0,0 +1,18 @@
|
|
|
1
|
+
import type { Entity } from './entity.js';
|
|
2
|
+
/** A scene entity's position: `[x, y]` (z is 0) or `[x, y, z]`. */
|
|
3
|
+
export type PositionJson = [number, number] | [number, number, number];
|
|
4
|
+
/** The transform a scene or prefab entity declares, applied when it spawns. */
|
|
5
|
+
export interface TransformJson {
|
|
6
|
+
/** `[x, y]` (z = 0) or `[x, y, z]`; in a 3D scene z is a world axis. */
|
|
7
|
+
position?: PositionJson;
|
|
8
|
+
/** Euler angles in degrees, order XYZ, applied to the entity's node (3D scenes). */
|
|
9
|
+
rotation?: [number, number, number];
|
|
10
|
+
/** Per-axis scale of the entity's node. */
|
|
11
|
+
scale?: [number, number, number];
|
|
12
|
+
}
|
|
13
|
+
/**
|
|
14
|
+
* Applies a declared transform to a freshly spawned entity. A two-number
|
|
15
|
+
* position behaves exactly as it always did (z = 0); `rotation` is converted
|
|
16
|
+
* from degrees and `scale` replaces the node's unit scale.
|
|
17
|
+
*/
|
|
18
|
+
export declare function applyTransformJson(entity: Entity, json: TransformJson): void;
|
|
@@ -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
|
+
}
|
package/dist/game.d.ts
CHANGED
|
@@ -1,19 +1,27 @@
|
|
|
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';
|
|
14
20
|
import { GamePost } from './post-effects.js';
|
|
15
21
|
import { type RenderBackend } from './render-readiness.js';
|
|
16
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';
|
|
17
25
|
import { GameLighting } from './scene-lighting.js';
|
|
18
26
|
import { type SpatialQuery } from './spatial-query.js';
|
|
19
27
|
import { Stats, type StatValue } from './stats.js';
|
|
@@ -33,7 +41,7 @@ export interface GameOptions {
|
|
|
33
41
|
/** Fixed resolution (from the project's game.json); absent = fill the canvas. */
|
|
34
42
|
resolution?: GameResolution;
|
|
35
43
|
/** Control overrides (action → key and `Gamepad:` codes) on top of the defaults. */
|
|
36
|
-
bindings?: InputBindings
|
|
44
|
+
bindings?: Readonly<InputBindings>;
|
|
37
45
|
/** Radial stick dead zone of `game.input`, in [0, 1); absent or invalid means 0.2 (ADR 0023). */
|
|
38
46
|
gamepadDeadZone?: number;
|
|
39
47
|
/** Initial stat values (points, lives…) from the project's stats.json. */
|
|
@@ -53,11 +61,20 @@ export interface GameOptions {
|
|
|
53
61
|
* the real backend either way; `game.assets` always exists.
|
|
54
62
|
*/
|
|
55
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;
|
|
56
73
|
}
|
|
57
74
|
export type UpdateFn = (dt: number) => void;
|
|
58
75
|
export interface SpawnPrefabOptions {
|
|
59
76
|
name?: string;
|
|
60
|
-
position?:
|
|
77
|
+
position?: PositionJson;
|
|
61
78
|
}
|
|
62
79
|
/** The Project's scenes by name (a file's stem), plus the registry shared by all of them. */
|
|
63
80
|
export interface SceneCatalog {
|
|
@@ -72,7 +89,6 @@ export type ParamOverrides = Record<string, Record<string, Record<string, number
|
|
|
72
89
|
*/
|
|
73
90
|
export declare class Game {
|
|
74
91
|
readonly scene: THREE.Scene<THREE.Object3DEventMap>;
|
|
75
|
-
readonly camera: THREE.OrthographicCamera;
|
|
76
92
|
readonly input: Input;
|
|
77
93
|
readonly pointer: Pointer;
|
|
78
94
|
readonly query: SpatialQuery;
|
|
@@ -100,6 +116,8 @@ export declare class Game {
|
|
|
100
116
|
readonly lighting: GameLighting;
|
|
101
117
|
/** The live scene's Post Effects (vignette, color grade); they die with their scene. */
|
|
102
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;
|
|
103
121
|
/** Registry retained by loadScene for runtime prefab spawning. */
|
|
104
122
|
registry: SceneRegistry | null;
|
|
105
123
|
paramOverrides: ParamOverrides;
|
|
@@ -131,8 +149,17 @@ export declare class Game {
|
|
|
131
149
|
private readonly baseViewHeight;
|
|
132
150
|
private viewHeight;
|
|
133
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;
|
|
134
159
|
private renderSort;
|
|
135
160
|
private sceneProjection;
|
|
161
|
+
/** The Ambient Light of a 3D scene, fed by `lighting.ambient` (ADR 0027). */
|
|
162
|
+
private readonly ambientLight3d;
|
|
136
163
|
/** Timestamp of the last animation frame; null until the loop's first frame seeds it. */
|
|
137
164
|
private lastTime;
|
|
138
165
|
/** Seconds of elapsed time not yet worth a whole Simulation Step (ADR 0014). */
|
|
@@ -153,7 +180,9 @@ export declare class Game {
|
|
|
153
180
|
constructor(options: GameOptions);
|
|
154
181
|
/**
|
|
155
182
|
* Resolves once the renderer can draw; rejects, naming both Render
|
|
156
|
-
* 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).
|
|
157
186
|
* Frames before it simulate but draw nothing, so a host awaits it before
|
|
158
187
|
* its first frame (ADR 0025).
|
|
159
188
|
*/
|
|
@@ -211,6 +240,8 @@ export declare class Game {
|
|
|
211
240
|
* draw order stays layer-banded with spawn-order ties.
|
|
212
241
|
*/
|
|
213
242
|
setSceneRender(json?: SceneRenderJson): void;
|
|
243
|
+
/** Adopts a scene's simulation block (gravity); called by loadScene beside setSceneRender. */
|
|
244
|
+
setSceneSimulation(json?: SceneSimulationJson): void;
|
|
214
245
|
/**
|
|
215
246
|
* Adopts a scene's camera block: jumps to its framing and, while
|
|
216
247
|
* simulating, follows/clamps per its settings. Called by loadScene.
|
|
@@ -222,7 +253,21 @@ export declare class Game {
|
|
|
222
253
|
removeEntity(entity: Entity): void;
|
|
223
254
|
/** The scene's render projection; null keeps logical and render space identical. */
|
|
224
255
|
get projection(): 'isometric' | null;
|
|
225
|
-
/**
|
|
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. */
|
|
226
271
|
get view(): number;
|
|
227
272
|
setViewHeight(value: number): void;
|
|
228
273
|
/** Shuts the game down completely (loop, input, GPU). */
|
|
@@ -279,14 +324,5 @@ export declare class Game {
|
|
|
279
324
|
private componentUpdateSchedule;
|
|
280
325
|
private updateSceneCamera;
|
|
281
326
|
private renderPoint;
|
|
282
|
-
/**
|
|
283
|
-
* The audio listener's position (CA-8) in logical coordinates. The camera
|
|
284
|
-
* itself only ever holds render-space coordinates (see `updateSceneCamera`,
|
|
285
|
-
* `setSceneCamera`), so under `projection: 'isometric'` this is the exact
|
|
286
|
-
* inverse of `renderPoint` — without it, distance-based attenuation would
|
|
287
|
-
* measure render-space distance instead of real game distance.
|
|
288
|
-
*/
|
|
289
|
-
private audioListenerPosition;
|
|
290
|
-
private dispatchCollisions;
|
|
291
327
|
private resize;
|
|
292
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,19 +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';
|
|
15
17
|
import { GamePost } from './post-effects.js';
|
|
16
18
|
import { activeRuntimeBridgeHook, EngineRuntimeBridge, } from './runtime-bridge.js';
|
|
17
19
|
import { RuntimeInspector } from './runtime-inspection.js';
|
|
18
20
|
import { FrameComposer } from './lit-frame.js';
|
|
19
|
-
import {
|
|
20
|
-
import { projectIsometric, unprojectIsometric } from './projection.js';
|
|
21
|
+
import { projectIsometric } from './projection.js';
|
|
21
22
|
import { canvasBackground, drawStraightToCanvas, mapUvPerVertex } from './render-output.js';
|
|
22
23
|
import { RenderReadiness } from './render-readiness.js';
|
|
23
24
|
import { applyYSort } from './render-sort.js';
|
|
24
25
|
import { loadScene, registryEntry, spawnFromJson, } from './scene.js';
|
|
25
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';
|
|
26
30
|
import { GameLighting } from './scene-lighting.js';
|
|
31
|
+
import { runSimulationPhases } from './simulation-phases.js';
|
|
27
32
|
import { createSpriteBatches } from './sprite-batches.js';
|
|
28
33
|
import { createSpatialQuery } from './spatial-query.js';
|
|
29
34
|
import { Stats } from './stats.js';
|
|
@@ -44,7 +49,6 @@ function componentsInOrder(order, byName) {
|
|
|
44
49
|
*/
|
|
45
50
|
export class Game {
|
|
46
51
|
scene = new THREE.Scene();
|
|
47
|
-
camera;
|
|
48
52
|
input;
|
|
49
53
|
pointer;
|
|
50
54
|
query;
|
|
@@ -72,6 +76,8 @@ export class Game {
|
|
|
72
76
|
lighting = new GameLighting();
|
|
73
77
|
/** The live scene's Post Effects (vignette, color grade); they die with their scene. */
|
|
74
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;
|
|
75
81
|
/** Registry retained by loadScene for runtime prefab spawning. */
|
|
76
82
|
registry = null;
|
|
77
83
|
paramOverrides = {};
|
|
@@ -103,8 +109,21 @@ export class Game {
|
|
|
103
109
|
baseViewHeight;
|
|
104
110
|
viewHeight;
|
|
105
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
|
+
};
|
|
106
123
|
renderSort = null;
|
|
107
124
|
sceneProjection = null;
|
|
125
|
+
/** The Ambient Light of a 3D scene, fed by `lighting.ambient` (ADR 0027). */
|
|
126
|
+
ambientLight3d = new SceneAmbientLight3d(this.scene);
|
|
108
127
|
/** Timestamp of the last animation frame; null until the loop's first frame seeds it. */
|
|
109
128
|
lastTime = null;
|
|
110
129
|
/** Seconds of elapsed time not yet worth a whole Simulation Step (ADR 0014). */
|
|
@@ -147,11 +166,13 @@ export class Game {
|
|
|
147
166
|
});
|
|
148
167
|
this.assets = new AssetLoader({
|
|
149
168
|
backend: options.textures,
|
|
169
|
+
models: options.models,
|
|
150
170
|
// Same late lookup as audio's: preload() resolves through whatever
|
|
151
171
|
// catalog is registered at call time, which outlives every scene.
|
|
152
172
|
resolveAsset: (uri) => this.sceneCatalog?.registry.resolveAsset?.(uri) ?? uri,
|
|
153
173
|
});
|
|
154
174
|
this.spriteBatches = createSpriteBatches(this, this.assets);
|
|
175
|
+
this.physics = new PhysicsHost(this, options.physics);
|
|
155
176
|
this.cameraEffects = new CameraEffects({
|
|
156
177
|
host: () => canvas.parentElement ?? document.body,
|
|
157
178
|
// One screen pixel in world units: the shake snaps to it (never the base).
|
|
@@ -170,14 +191,10 @@ export class Game {
|
|
|
170
191
|
activeRuntimeBridgeHook()?.fail?.({ code: 'render-backend-failed', message: error.message });
|
|
171
192
|
});
|
|
172
193
|
this.scene.background = canvasBackground(background);
|
|
173
|
-
this.camera = new THREE.OrthographicCamera();
|
|
174
|
-
this.camera.position.z = 10;
|
|
175
|
-
// Emissive drawables sit on their own layer; an unlit frame draws it in place.
|
|
176
|
-
this.camera.layers.enable(EMISSIVE_LAYER);
|
|
177
194
|
const { renderer, spriteBatches, resolution } = this;
|
|
178
195
|
this.frame = new FrameComposer({ game: this, renderer, spriteBatches, resolution });
|
|
179
196
|
this.pointer = new Pointer(canvas, {
|
|
180
|
-
camera: this.camera,
|
|
197
|
+
camera: () => this.camera,
|
|
181
198
|
resolution: this.resolution,
|
|
182
199
|
projection: () => this.sceneProjection,
|
|
183
200
|
entities: this.entities,
|
|
@@ -188,12 +205,14 @@ export class Game {
|
|
|
188
205
|
}
|
|
189
206
|
/**
|
|
190
207
|
* Resolves once the renderer can draw; rejects, naming both Render
|
|
191
|
-
* 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).
|
|
192
211
|
* Frames before it simulate but draw nothing, so a host awaits it before
|
|
193
212
|
* its first frame (ADR 0025).
|
|
194
213
|
*/
|
|
195
214
|
ready() {
|
|
196
|
-
return this.readiness.promise;
|
|
215
|
+
return this.physics.whenReady(this.readiness.promise);
|
|
197
216
|
}
|
|
198
217
|
/** The Render Backend the renderer settled on; null until ready() resolved. */
|
|
199
218
|
get backend() {
|
|
@@ -251,6 +270,8 @@ export class Game {
|
|
|
251
270
|
// Pointer holds that array by reference, so it must never be reassigned.
|
|
252
271
|
for (const entity of [...this.entities])
|
|
253
272
|
entity.destroy();
|
|
273
|
+
// After the cascade removed every body: the world goes with its scene.
|
|
274
|
+
this.physics.unloadScene();
|
|
254
275
|
// Drains transferred by the destruction cascade above are scene-scoped too.
|
|
255
276
|
sceneDrainsOf(this).clear();
|
|
256
277
|
// After the cascade freed every slot: the batches go with their scene.
|
|
@@ -259,6 +280,8 @@ export class Game {
|
|
|
259
280
|
this.renderSort = null;
|
|
260
281
|
this.sceneProjection = null;
|
|
261
282
|
this.sceneCamera = null;
|
|
283
|
+
this.sceneSpace = '2d';
|
|
284
|
+
this.cameraRig.reset();
|
|
262
285
|
this.liveSceneName = null;
|
|
263
286
|
this.setViewHeight(this.baseViewHeight);
|
|
264
287
|
}
|
|
@@ -348,11 +371,13 @@ export class Game {
|
|
|
348
371
|
* draw order stays layer-banded with spawn-order ties.
|
|
349
372
|
*/
|
|
350
373
|
setSceneRender(json) {
|
|
351
|
-
|
|
374
|
+
const { space, sort, projection } = resolveRenderPolicy(json);
|
|
375
|
+
this.sceneSpace = space;
|
|
376
|
+
this.renderSort = sort;
|
|
352
377
|
this.spriteBatches.enabled = json?.batch !== false;
|
|
353
378
|
this.lighting.loadScene(json);
|
|
354
379
|
this.post.loadScene(json);
|
|
355
|
-
|
|
380
|
+
this.physics.enterScene(space);
|
|
356
381
|
if (projection === this.sceneProjection)
|
|
357
382
|
return;
|
|
358
383
|
this.sceneProjection = projection;
|
|
@@ -362,26 +387,28 @@ export class Game {
|
|
|
362
387
|
component.onProjectionChange?.(projection);
|
|
363
388
|
}
|
|
364
389
|
}
|
|
390
|
+
/** Adopts a scene's simulation block (gravity); called by loadScene beside setSceneRender. */
|
|
391
|
+
setSceneSimulation(json) {
|
|
392
|
+
this.physics.setSimulation(resolveSceneSimulation(json));
|
|
393
|
+
}
|
|
365
394
|
/**
|
|
366
395
|
* Adopts a scene's camera block: jumps to its framing and, while
|
|
367
396
|
* simulating, follows/clamps per its settings. Called by loadScene.
|
|
368
397
|
*/
|
|
369
398
|
setSceneCamera(json) {
|
|
370
|
-
// No
|
|
371
|
-
|
|
372
|
-
|
|
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)
|
|
373
402
|
return;
|
|
374
|
-
}
|
|
375
|
-
this.sceneCamera = resolveSceneCamera(json);
|
|
376
403
|
// With a follow target the declared position is moot: start centered on
|
|
377
404
|
// the target so play begins framed like the editor shows it.
|
|
378
|
-
const followed =
|
|
405
|
+
const followed = cam.follow ? this.find(cam.follow) : undefined;
|
|
379
406
|
const center = followed
|
|
380
407
|
? this.renderPoint(followed.position.x, followed.position.y)
|
|
381
|
-
: { x:
|
|
408
|
+
: { x: cam.position[0], y: cam.position[1] };
|
|
382
409
|
this.camera.position.x = center.x;
|
|
383
410
|
this.camera.position.y = center.y;
|
|
384
|
-
this.setViewHeight(
|
|
411
|
+
this.setViewHeight(cam.zoom);
|
|
385
412
|
}
|
|
386
413
|
start() {
|
|
387
414
|
if (this.disposed)
|
|
@@ -438,11 +465,35 @@ export class Game {
|
|
|
438
465
|
get projection() {
|
|
439
466
|
return this.sceneProjection;
|
|
440
467
|
}
|
|
441
|
-
/**
|
|
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. */
|
|
442
490
|
get view() {
|
|
443
491
|
return this.viewHeight;
|
|
444
492
|
}
|
|
445
493
|
setViewHeight(value) {
|
|
494
|
+
// A perspective camera frames by its lens, not by a visible height.
|
|
495
|
+
if (this.sceneSpace === '3d')
|
|
496
|
+
return;
|
|
446
497
|
this.viewHeight = Math.min(Math.max(value, 2), 80);
|
|
447
498
|
this.resize();
|
|
448
499
|
}
|
|
@@ -462,6 +513,7 @@ export class Game {
|
|
|
462
513
|
this.time.cancelAll();
|
|
463
514
|
for (const entity of [...this.entities])
|
|
464
515
|
entity.destroy();
|
|
516
|
+
this.physics.dispose();
|
|
465
517
|
sceneDrainsOf(this).clear();
|
|
466
518
|
this.spriteBatches.unload();
|
|
467
519
|
// After the entities: their clones go with the cascade above, the
|
|
@@ -555,7 +607,7 @@ export class Game {
|
|
|
555
607
|
// Positional audio (CA-8): recomputed every frame, on this same pass —
|
|
556
608
|
// never a second walk of `this.entities`, since `this.audio` already
|
|
557
609
|
// holds direct references to whichever entities are tracked.
|
|
558
|
-
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);
|
|
559
611
|
this.renderSurface();
|
|
560
612
|
}
|
|
561
613
|
finally {
|
|
@@ -581,7 +633,7 @@ export class Game {
|
|
|
581
633
|
for (const component of schedule)
|
|
582
634
|
component.onUpdate?.(SIMULATION_STEP);
|
|
583
635
|
}
|
|
584
|
-
this
|
|
636
|
+
runSimulationPhases(this);
|
|
585
637
|
this.updateSceneCamera(SIMULATION_STEP);
|
|
586
638
|
advanceCameraEffects(this.cameraEffects);
|
|
587
639
|
this.finishStep();
|
|
@@ -625,11 +677,13 @@ export class Game {
|
|
|
625
677
|
if (this.renderSort === 'y')
|
|
626
678
|
applyYSort(this.entities, sceneDrainsOf(this).participants());
|
|
627
679
|
this.ui.setActive(this.simulate);
|
|
680
|
+
this.ambientLight3d.sync(this.sceneSpace, this.lighting.ambient);
|
|
628
681
|
// The shake offset exists only while drawing (ADR 0020): Anchored Pieces
|
|
629
682
|
// and the render see the drawn center, then the base comes back exactly,
|
|
630
683
|
// so follow smoothing never starts from its own jitter.
|
|
631
684
|
const { x, y } = this.camera.position;
|
|
632
|
-
|
|
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;
|
|
633
687
|
this.camera.position.x = x + shake.x;
|
|
634
688
|
this.camera.position.y = y + shake.y;
|
|
635
689
|
try {
|
|
@@ -688,48 +742,13 @@ export class Game {
|
|
|
688
742
|
return componentsInOrder(result.order, byName);
|
|
689
743
|
}
|
|
690
744
|
updateSceneCamera(dt) {
|
|
691
|
-
|
|
692
|
-
if (!cam)
|
|
745
|
+
if (!this.sceneCamera)
|
|
693
746
|
return;
|
|
694
|
-
|
|
695
|
-
const provider = followed?.components.find((c) => isCameraVelocityProvider(c));
|
|
696
|
-
const velocity = provider?.getCameraVelocity();
|
|
697
|
-
const target = followed
|
|
698
|
-
? this.renderPoint(followed.position.x, followed.position.y)
|
|
699
|
-
: null;
|
|
700
|
-
const renderVelocity = velocity
|
|
701
|
-
? this.renderPoint(velocity.vx, velocity.vy)
|
|
702
|
-
: { x: 0, y: 0 };
|
|
703
|
-
const next = stepSceneCamera(cam, {
|
|
704
|
-
x: this.camera.position.x,
|
|
705
|
-
y: this.camera.position.y,
|
|
706
|
-
halfW: (this.camera.right - this.camera.left) / 2,
|
|
707
|
-
halfH: this.viewHeight / 2,
|
|
708
|
-
target,
|
|
709
|
-
vx: renderVelocity.x,
|
|
710
|
-
vy: renderVelocity.y,
|
|
711
|
-
dt,
|
|
712
|
-
});
|
|
713
|
-
this.camera.position.x = next.x;
|
|
714
|
-
this.camera.position.y = next.y;
|
|
747
|
+
this.cameraRig.follow(this.sceneCamera, this.followHost, dt);
|
|
715
748
|
}
|
|
716
749
|
renderPoint(x, y) {
|
|
717
750
|
return this.sceneProjection === 'isometric' ? projectIsometric(x, y) : { x, y };
|
|
718
751
|
}
|
|
719
|
-
/**
|
|
720
|
-
* The audio listener's position (CA-8) in logical coordinates. The camera
|
|
721
|
-
* itself only ever holds render-space coordinates (see `updateSceneCamera`,
|
|
722
|
-
* `setSceneCamera`), so under `projection: 'isometric'` this is the exact
|
|
723
|
-
* inverse of `renderPoint` — without it, distance-based attenuation would
|
|
724
|
-
* measure render-space distance instead of real game distance.
|
|
725
|
-
*/
|
|
726
|
-
audioListenerPosition() {
|
|
727
|
-
const { x, y } = this.camera.position;
|
|
728
|
-
return this.sceneProjection === 'isometric' ? unprojectIsometric(x, y) : { x, y };
|
|
729
|
-
}
|
|
730
|
-
dispatchCollisions() {
|
|
731
|
-
dispatchHitboxCollisions(this);
|
|
732
|
-
}
|
|
733
752
|
resize() {
|
|
734
753
|
const canvas = this.renderer.domElement;
|
|
735
754
|
const { clientWidth: w, clientHeight: h } = canvas;
|
|
@@ -747,13 +766,7 @@ export class Game {
|
|
|
747
766
|
this.renderer.setViewport(vx, vy, vw, vh);
|
|
748
767
|
this.renderer.setScissor(vx, vy, vw, vh);
|
|
749
768
|
}
|
|
750
|
-
|
|
751
|
-
const halfW = halfH * aspect;
|
|
752
|
-
this.camera.left = -halfW;
|
|
753
|
-
this.camera.right = halfW;
|
|
754
|
-
this.camera.top = halfH;
|
|
755
|
-
this.camera.bottom = -halfH;
|
|
756
|
-
this.camera.updateProjectionMatrix();
|
|
769
|
+
this.cameraRig.fit(aspect, this.viewHeight);
|
|
757
770
|
layoutCameraEffects(this.cameraEffects, gameViewport(w, h, this.resolution));
|
|
758
771
|
}
|
|
759
772
|
}
|