@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
package/dist/camera.js
CHANGED
|
@@ -1,4 +1,5 @@
|
|
|
1
1
|
import * as THREE from 'three/webgpu';
|
|
2
|
+
import { resolvePerspectiveCamera, } from './scene-camera-3d.js';
|
|
2
3
|
export const CAMERA_DEFAULTS = {
|
|
3
4
|
position: [0, 0],
|
|
4
5
|
zoom: 12,
|
|
@@ -9,8 +10,9 @@ export const CAMERA_DEFAULTS = {
|
|
|
9
10
|
lookaheadY: 0,
|
|
10
11
|
smoothing: 6,
|
|
11
12
|
};
|
|
12
|
-
/** Fills a scene's camera block with the engine defaults. */
|
|
13
13
|
export function resolveSceneCamera(json) {
|
|
14
|
+
if (json?.kind === 'perspective')
|
|
15
|
+
return resolvePerspectiveCamera(json);
|
|
14
16
|
return {
|
|
15
17
|
position: json?.position ?? CAMERA_DEFAULTS.position,
|
|
16
18
|
zoom: json?.zoom ?? CAMERA_DEFAULTS.zoom,
|
package/dist/component.d.ts
CHANGED
|
@@ -8,12 +8,21 @@ export interface ParamSpec {
|
|
|
8
8
|
max?: number;
|
|
9
9
|
step?: number;
|
|
10
10
|
/** Specialized editor control for values that are otherwise plain JSON. */
|
|
11
|
-
kind?: 'string-list' | 'vector2' | 'color' | 'texture';
|
|
11
|
+
kind?: 'string-list' | 'vector2' | 'vector3' | 'color' | 'texture' | 'model';
|
|
12
12
|
/** Allowed values for a string param; rendered as a dropdown. Takes precedence over ref. */
|
|
13
13
|
options?: string[];
|
|
14
14
|
/** Project value this string param names; rendered and validated as a typed reference. */
|
|
15
15
|
ref?: 'prefab' | 'stat' | 'action' | 'clip' | 'sound' | 'ui';
|
|
16
16
|
}
|
|
17
|
+
/**
|
|
18
|
+
* The scene space a component belongs to (ADR 0027): `'2d'` components draw,
|
|
19
|
+
* collide or light the orthographic world, move or query over its x/y plane,
|
|
20
|
+
* or need a sibling that does; `'3d'` ones need a perspective camera;
|
|
21
|
+
* `'both'` run anywhere. A component that declares nothing is `'both'`.
|
|
22
|
+
*/
|
|
23
|
+
export type ComponentSpace = '2d' | '3d' | 'both';
|
|
24
|
+
/** The values of `ComponentSpace`, for the checks that read a marker from data (the project component protocol, conformance). */
|
|
25
|
+
export declare const COMPONENT_SPACES: readonly ComponentSpace[];
|
|
17
26
|
export interface ComponentClass<T extends Component = Component> {
|
|
18
27
|
new (): T;
|
|
19
28
|
/**
|
|
@@ -27,6 +36,8 @@ export interface ComponentClass<T extends Component = Component> {
|
|
|
27
36
|
params?: Record<string, ParamSpec>;
|
|
28
37
|
/** Sibling component updates that must complete before this one when present. */
|
|
29
38
|
updateAfter?: readonly string[];
|
|
39
|
+
/** The scene space this component belongs to; absent means both. */
|
|
40
|
+
space?: ComponentSpace;
|
|
30
41
|
/**
|
|
31
42
|
* Instance fields holding runtime state rather than authorable defaults.
|
|
32
43
|
* Excluded from authoringDefaults(); a subclass that does not redeclare
|
|
@@ -47,6 +58,25 @@ export interface SolidContact {
|
|
|
47
58
|
readonly axis: 'x' | 'y';
|
|
48
59
|
readonly normal: ContactNormal;
|
|
49
60
|
}
|
|
61
|
+
/** A 3D contact between two entities' solid Colliders, read after the physics step. */
|
|
62
|
+
export interface BodyContact {
|
|
63
|
+
/** The entity whose component receives the hook. */
|
|
64
|
+
readonly entity: Entity;
|
|
65
|
+
/** The entity it is in contact with. */
|
|
66
|
+
readonly other: Entity;
|
|
67
|
+
/** Unit normal in world space, pointing from `entity` to `other`. */
|
|
68
|
+
readonly normal: {
|
|
69
|
+
readonly x: number;
|
|
70
|
+
readonly y: number;
|
|
71
|
+
readonly z: number;
|
|
72
|
+
};
|
|
73
|
+
/** A world-space point on the contact. */
|
|
74
|
+
readonly point: {
|
|
75
|
+
readonly x: number;
|
|
76
|
+
readonly y: number;
|
|
77
|
+
readonly z: number;
|
|
78
|
+
};
|
|
79
|
+
}
|
|
50
80
|
/**
|
|
51
81
|
* A pluggable piece of an entity. User behaviors and engine ones are the
|
|
52
82
|
* same thing: Component subclasses with public props.
|
|
@@ -56,6 +86,7 @@ export declare abstract class Component {
|
|
|
56
86
|
static displayName?: string;
|
|
57
87
|
static params?: Record<string, ParamSpec>;
|
|
58
88
|
static updateAfter?: readonly string[];
|
|
89
|
+
static space?: ComponentSpace;
|
|
59
90
|
static transient?: readonly string[];
|
|
60
91
|
entity: Entity;
|
|
61
92
|
game: Game;
|
|
@@ -67,8 +98,13 @@ export declare abstract class Component {
|
|
|
67
98
|
onUpdate?(dt: number): void;
|
|
68
99
|
/** Runs after the scene changes between identity and projected rendering. */
|
|
69
100
|
onProjectionChange?(projection: 'isometric' | null): void;
|
|
70
|
-
/**
|
|
101
|
+
/**
|
|
102
|
+
* Runs on overlap when this entity's Hitbox (2D) or sensor Collider (3D)
|
|
103
|
+
* mask names the other's layer.
|
|
104
|
+
*/
|
|
71
105
|
onCollide?(other: Entity): void;
|
|
106
|
+
/** Runs once per Simulation Step for each entity this entity's solid Collider is in contact with (3D, issue #159). */
|
|
107
|
+
onBodyContact?(contact: BodyContact): void;
|
|
72
108
|
/** Runs when this entity's DynamicBody physically contacts a Solid. */
|
|
73
109
|
onContact?(contact: SolidContact): void;
|
|
74
110
|
/**
|
package/dist/component.js
CHANGED
|
@@ -1,3 +1,5 @@
|
|
|
1
|
+
/** The values of `ComponentSpace`, for the checks that read a marker from data (the project component protocol, conformance). */
|
|
2
|
+
export const COMPONENT_SPACES = ['2d', '3d', 'both'];
|
|
1
3
|
/**
|
|
2
4
|
* A pluggable piece of an entity. User behaviors and engine ones are the
|
|
3
5
|
* same thing: Component subclasses with public props.
|
|
@@ -7,6 +9,7 @@ export class Component {
|
|
|
7
9
|
static displayName;
|
|
8
10
|
static params;
|
|
9
11
|
static updateAfter;
|
|
12
|
+
static space;
|
|
10
13
|
static transient;
|
|
11
14
|
entity;
|
|
12
15
|
game;
|
|
@@ -1,5 +1,6 @@
|
|
|
1
|
-
import { Component } from '../component.js';
|
|
1
|
+
import { Component, type ComponentSpace } from '../component.js';
|
|
2
2
|
import { type ClipDef } from '../animation/clip-player.js';
|
|
3
|
+
import type { ClipTarget } from '../animation/clip-target.js';
|
|
3
4
|
import { type SheetCell, type SheetDef } from '../animation/sheet.js';
|
|
4
5
|
import type { YSortParticipant } from '../render-sort.js';
|
|
5
6
|
/**
|
|
@@ -12,8 +13,9 @@ import type { YSortParticipant } from '../render-sort.js';
|
|
|
12
13
|
* per frame, anchored bottom-center — width/height size the sheet's largest
|
|
13
14
|
* frame and smaller ones keep their feet planted.
|
|
14
15
|
*/
|
|
15
|
-
export declare class AnimatedSprite extends Component implements YSortParticipant {
|
|
16
|
+
export declare class AnimatedSprite extends Component implements YSortParticipant, ClipTarget {
|
|
16
17
|
static componentName: string;
|
|
18
|
+
static space: ComponentSpace;
|
|
17
19
|
static updateAfter: readonly string[];
|
|
18
20
|
static params: {
|
|
19
21
|
offsetX: {
|
|
@@ -95,6 +97,10 @@ export declare class AnimatedSprite extends Component implements YSortParticipan
|
|
|
95
97
|
initialClip?: string;
|
|
96
98
|
/** Clip currently playing. */
|
|
97
99
|
current?: string;
|
|
100
|
+
/** The names a StateMachine can ask for (the Clip Target seam, issue #166). */
|
|
101
|
+
get clipNames(): readonly string[];
|
|
102
|
+
/** True once a non-looping clip has held its last frame for its whole duration; false after `play`. */
|
|
103
|
+
get clipFinished(): boolean;
|
|
98
104
|
private readonly player;
|
|
99
105
|
private sheets;
|
|
100
106
|
private texs;
|
|
@@ -104,7 +110,12 @@ export declare class AnimatedSprite extends Component implements YSortParticipan
|
|
|
104
110
|
private frameScaleX;
|
|
105
111
|
private frameScaleY;
|
|
106
112
|
onReady(): void;
|
|
107
|
-
/**
|
|
113
|
+
/**
|
|
114
|
+
* Switches clip. Repeating the running clip's name doesn't restart it; once
|
|
115
|
+
* a non-looping clip has finished it does (issue #166 CA-3), so a state
|
|
116
|
+
* re-entered after its one-shot clip ended replays it instead of reporting
|
|
117
|
+
* `clip:end` on every step.
|
|
118
|
+
*/
|
|
108
119
|
play(name: string): void;
|
|
109
120
|
onUpdate(dt: number): void;
|
|
110
121
|
onDestroy(): void;
|
|
@@ -21,6 +21,7 @@ const clampAnchor = (value) => Math.min(1, Math.max(0, value));
|
|
|
21
21
|
*/
|
|
22
22
|
export class AnimatedSprite extends Component {
|
|
23
23
|
static componentName = 'AnimatedSprite';
|
|
24
|
+
static space = '2d';
|
|
24
25
|
static updateAfter = ['StateMachine'];
|
|
25
26
|
static params = {
|
|
26
27
|
offsetX: { label: 'x offset' },
|
|
@@ -150,6 +151,14 @@ export class AnimatedSprite extends Component {
|
|
|
150
151
|
initialClip;
|
|
151
152
|
/** Clip currently playing. */
|
|
152
153
|
current;
|
|
154
|
+
/** The names a StateMachine can ask for (the Clip Target seam, issue #166). */
|
|
155
|
+
get clipNames() {
|
|
156
|
+
return Object.keys(this.clips);
|
|
157
|
+
}
|
|
158
|
+
/** True once a non-looping clip has held its last frame for its whole duration; false after `play`. */
|
|
159
|
+
get clipFinished() {
|
|
160
|
+
return this.player.finished;
|
|
161
|
+
}
|
|
153
162
|
player = new ClipPlayer();
|
|
154
163
|
sheets = [];
|
|
155
164
|
texs = [];
|
|
@@ -205,9 +214,14 @@ export class AnimatedSprite extends Component {
|
|
|
205
214
|
if (this.initialClip)
|
|
206
215
|
this.play(this.initialClip);
|
|
207
216
|
}
|
|
208
|
-
/**
|
|
217
|
+
/**
|
|
218
|
+
* Switches clip. Repeating the running clip's name doesn't restart it; once
|
|
219
|
+
* a non-looping clip has finished it does (issue #166 CA-3), so a state
|
|
220
|
+
* re-entered after its one-shot clip ended replays it instead of reporting
|
|
221
|
+
* `clip:end` on every step.
|
|
222
|
+
*/
|
|
209
223
|
play(name) {
|
|
210
|
-
if (this.current === name)
|
|
224
|
+
if (this.current === name && !this.player.finished)
|
|
211
225
|
return;
|
|
212
226
|
const clip = this.clips[name];
|
|
213
227
|
if (!clip)
|
|
@@ -0,0 +1,98 @@
|
|
|
1
|
+
import type { ClipTarget } from '../animation/clip-target.js';
|
|
2
|
+
import { Component, type ComponentSpace } from '../component.js';
|
|
3
|
+
/** Per-clip overrides, keyed by the glTF's clip name: a one-shot (`loop: false`) and a playback rate. */
|
|
4
|
+
export interface AnimatorClipOverride {
|
|
5
|
+
/** Defaults to true; false plays the clip once and holds its last pose. */
|
|
6
|
+
loop?: boolean;
|
|
7
|
+
/** Playback rate; 1 by default, 2 finishes in half the time. */
|
|
8
|
+
speed?: number;
|
|
9
|
+
}
|
|
10
|
+
/** What `inspect_runtime` shows of an Animator. */
|
|
11
|
+
export interface AnimatorState {
|
|
12
|
+
loaded: boolean;
|
|
13
|
+
/** The playing clip; null before the first play (an undefined would project as an unsupported marker). */
|
|
14
|
+
current: string | null;
|
|
15
|
+
/** The current action's time in seconds (0 with none). */
|
|
16
|
+
time: number;
|
|
17
|
+
/** False before the model loaded or with no clip playing. */
|
|
18
|
+
playing: boolean;
|
|
19
|
+
/** The pose fingerprint of the rig (see poseHash); empty with no skeleton. */
|
|
20
|
+
poseHash: string;
|
|
21
|
+
}
|
|
22
|
+
/**
|
|
23
|
+
* Plays the glTF clips of the Model beside it (issue #166, ADR 0029): the
|
|
24
|
+
* 3D Clip Target. One AnimationMixer over the Model's own clone, one action
|
|
25
|
+
* per clip, advanced by the Simulation Step in onUpdate — nothing runs per
|
|
26
|
+
* render frame, so the pose is deterministic and visible step by step. A
|
|
27
|
+
* StateMachine finds it like it finds an AnimatedSprite and plays the clip of
|
|
28
|
+
* each state; `clip:end` fires when a `loop: false` clip finishes. A `play`
|
|
29
|
+
* before the model loaded is remembered and applied when it settles;
|
|
30
|
+
* otherwise `initialClip` plays then. In a 2D scene it creates nothing.
|
|
31
|
+
*/
|
|
32
|
+
export declare class Animator extends Component implements ClipTarget {
|
|
33
|
+
static componentName: string;
|
|
34
|
+
static space: ComponentSpace;
|
|
35
|
+
static updateAfter: readonly string[];
|
|
36
|
+
static params: {
|
|
37
|
+
initialClip: {
|
|
38
|
+
label: string;
|
|
39
|
+
};
|
|
40
|
+
fade: {
|
|
41
|
+
label: string;
|
|
42
|
+
min: number;
|
|
43
|
+
max: number;
|
|
44
|
+
step: number;
|
|
45
|
+
};
|
|
46
|
+
};
|
|
47
|
+
static transient: string[];
|
|
48
|
+
/** The clip that plays once the model settles, when nothing asked for one before. */
|
|
49
|
+
initialClip: string;
|
|
50
|
+
/** Crossfade between clips, in seconds; 0 cuts. */
|
|
51
|
+
fade: number;
|
|
52
|
+
/**
|
|
53
|
+
* Per-clip overrides by glTF clip name (authored in JSON, like
|
|
54
|
+
* AnimatedSprite.clips). Read when the actions are built, as the model
|
|
55
|
+
* settles: a change after that applies on the next build (a Model rebuild).
|
|
56
|
+
*/
|
|
57
|
+
clips: Record<string, AnimatorClipOverride>;
|
|
58
|
+
/** The clip playing now; undefined before the first play. */
|
|
59
|
+
current: string | undefined;
|
|
60
|
+
private mixer;
|
|
61
|
+
private readonly actions;
|
|
62
|
+
private active;
|
|
63
|
+
/** A play asked for before the model loaded. */
|
|
64
|
+
private requested;
|
|
65
|
+
private finished;
|
|
66
|
+
private root;
|
|
67
|
+
private readonly warned;
|
|
68
|
+
private attached;
|
|
69
|
+
/** The Model handle the mixer was built over, or is waiting on: a Model rebuild replaces it (review of PR #168). */
|
|
70
|
+
private handle;
|
|
71
|
+
/** The clip names, computed once per build: the StateMachine reads them every step. */
|
|
72
|
+
private names;
|
|
73
|
+
/** True while clips are expected but not listed yet (the glb is loading); false once built, or once no Model can provide them. */
|
|
74
|
+
private pending;
|
|
75
|
+
get clipNames(): readonly string[];
|
|
76
|
+
get clipFinished(): boolean;
|
|
77
|
+
get clipsPending(): boolean;
|
|
78
|
+
onReady(): void;
|
|
79
|
+
/** Plays a clip: a no-op for the running one, a restart for a finished one, a crossfade from any other over `fade` seconds. */
|
|
80
|
+
play(name: string): void;
|
|
81
|
+
onUpdate(dt: number): void;
|
|
82
|
+
onDestroy(): void;
|
|
83
|
+
inspectState(): AnimatorState;
|
|
84
|
+
/**
|
|
85
|
+
* Takes the sibling Model's handle and builds the mixer when it settles. A
|
|
86
|
+
* Model later in the component order is not there at onReady, so the first
|
|
87
|
+
* onUpdate tries once more (`final`); a primitive Model warns at once, a
|
|
88
|
+
* missing one once it is certain, and either way exactly once.
|
|
89
|
+
*/
|
|
90
|
+
private attach;
|
|
91
|
+
/** Forgets the Model's old clone and keeps the clip it played as the request for the next build. */
|
|
92
|
+
private detach;
|
|
93
|
+
/** Stops and drops the mixer, its actions and the clone they ran over. */
|
|
94
|
+
private release;
|
|
95
|
+
private build;
|
|
96
|
+
private configure;
|
|
97
|
+
private warnOnce;
|
|
98
|
+
}
|
|
@@ -0,0 +1,197 @@
|
|
|
1
|
+
import * as THREE from 'three/webgpu';
|
|
2
|
+
import { poseHash } from '../animation/pose-hash.js';
|
|
3
|
+
import { Component } from '../component.js';
|
|
4
|
+
import { reportRejection } from '../report-rejection.js';
|
|
5
|
+
import { Model } from './model.js';
|
|
6
|
+
/**
|
|
7
|
+
* Plays the glTF clips of the Model beside it (issue #166, ADR 0029): the
|
|
8
|
+
* 3D Clip Target. One AnimationMixer over the Model's own clone, one action
|
|
9
|
+
* per clip, advanced by the Simulation Step in onUpdate — nothing runs per
|
|
10
|
+
* render frame, so the pose is deterministic and visible step by step. A
|
|
11
|
+
* StateMachine finds it like it finds an AnimatedSprite and plays the clip of
|
|
12
|
+
* each state; `clip:end` fires when a `loop: false` clip finishes. A `play`
|
|
13
|
+
* before the model loaded is remembered and applied when it settles;
|
|
14
|
+
* otherwise `initialClip` plays then. In a 2D scene it creates nothing.
|
|
15
|
+
*/
|
|
16
|
+
export class Animator extends Component {
|
|
17
|
+
static componentName = 'Animator';
|
|
18
|
+
static space = '3d';
|
|
19
|
+
static updateAfter = ['StateMachine'];
|
|
20
|
+
static params = {
|
|
21
|
+
initialClip: { label: 'Initial clip' },
|
|
22
|
+
fade: { label: 'Fade (s)', min: 0, max: 1, step: 0.05 },
|
|
23
|
+
};
|
|
24
|
+
static transient = ['current', 'mixer', 'actions', 'active', 'requested', 'finished', 'root', 'warned', 'attached', 'handle', 'names', 'pending'];
|
|
25
|
+
/** The clip that plays once the model settles, when nothing asked for one before. */
|
|
26
|
+
initialClip = '';
|
|
27
|
+
/** Crossfade between clips, in seconds; 0 cuts. */
|
|
28
|
+
fade = 0.15;
|
|
29
|
+
/**
|
|
30
|
+
* Per-clip overrides by glTF clip name (authored in JSON, like
|
|
31
|
+
* AnimatedSprite.clips). Read when the actions are built, as the model
|
|
32
|
+
* settles: a change after that applies on the next build (a Model rebuild).
|
|
33
|
+
*/
|
|
34
|
+
clips = {};
|
|
35
|
+
/** The clip playing now; undefined before the first play. */
|
|
36
|
+
current;
|
|
37
|
+
mixer = null;
|
|
38
|
+
actions = new Map();
|
|
39
|
+
active = null;
|
|
40
|
+
/** A play asked for before the model loaded. */
|
|
41
|
+
requested;
|
|
42
|
+
finished = false;
|
|
43
|
+
root = null;
|
|
44
|
+
warned = new Set();
|
|
45
|
+
attached = false;
|
|
46
|
+
/** The Model handle the mixer was built over, or is waiting on: a Model rebuild replaces it (review of PR #168). */
|
|
47
|
+
handle = null;
|
|
48
|
+
/** The clip names, computed once per build: the StateMachine reads them every step. */
|
|
49
|
+
names = [];
|
|
50
|
+
/** True while clips are expected but not listed yet (the glb is loading); false once built, or once no Model can provide them. */
|
|
51
|
+
pending = false;
|
|
52
|
+
get clipNames() {
|
|
53
|
+
return this.names;
|
|
54
|
+
}
|
|
55
|
+
get clipFinished() {
|
|
56
|
+
return this.finished;
|
|
57
|
+
}
|
|
58
|
+
get clipsPending() {
|
|
59
|
+
return this.pending;
|
|
60
|
+
}
|
|
61
|
+
onReady() {
|
|
62
|
+
if (this.game.space !== '3d')
|
|
63
|
+
return;
|
|
64
|
+
this.pending = true;
|
|
65
|
+
this.attach();
|
|
66
|
+
}
|
|
67
|
+
/** Plays a clip: a no-op for the running one, a restart for a finished one, a crossfade from any other over `fade` seconds. */
|
|
68
|
+
play(name) {
|
|
69
|
+
if (this.game.space !== '3d')
|
|
70
|
+
return;
|
|
71
|
+
if (!this.mixer) {
|
|
72
|
+
this.requested = name;
|
|
73
|
+
return;
|
|
74
|
+
}
|
|
75
|
+
const action = this.actions.get(name);
|
|
76
|
+
if (!action) {
|
|
77
|
+
this.warnOnce(name, `[waica] "${this.entity.name}": no clip "${name}"`);
|
|
78
|
+
return;
|
|
79
|
+
}
|
|
80
|
+
if (action === this.active && !this.finished)
|
|
81
|
+
return;
|
|
82
|
+
const previous = this.active;
|
|
83
|
+
this.active = action;
|
|
84
|
+
this.current = name;
|
|
85
|
+
this.finished = false;
|
|
86
|
+
action.reset().setEffectiveWeight(1).play();
|
|
87
|
+
if (!previous || previous === action)
|
|
88
|
+
return;
|
|
89
|
+
if (this.fade <= 0)
|
|
90
|
+
previous.stop();
|
|
91
|
+
else
|
|
92
|
+
previous.crossFadeTo(action, this.fade, false);
|
|
93
|
+
}
|
|
94
|
+
onUpdate(dt) {
|
|
95
|
+
if (this.game.space !== '3d')
|
|
96
|
+
return;
|
|
97
|
+
// A rebuilt Model (a new src or shape) draws a new clone: the mixer over
|
|
98
|
+
// the old one would animate a graph nobody draws, so it is torn down and
|
|
99
|
+
// the clip it was playing is asked for again on the new one.
|
|
100
|
+
if (this.attached && (this.entity.get(Model)?.handle ?? null) !== this.handle)
|
|
101
|
+
this.detach();
|
|
102
|
+
if (!this.attached)
|
|
103
|
+
this.attach(true);
|
|
104
|
+
this.mixer?.update(dt);
|
|
105
|
+
}
|
|
106
|
+
onDestroy() {
|
|
107
|
+
this.release();
|
|
108
|
+
this.pending = false;
|
|
109
|
+
}
|
|
110
|
+
inspectState() {
|
|
111
|
+
return {
|
|
112
|
+
loaded: this.mixer !== null,
|
|
113
|
+
current: this.current ?? null,
|
|
114
|
+
time: this.active?.time ?? 0,
|
|
115
|
+
playing: this.active !== null && this.active.isRunning(),
|
|
116
|
+
poseHash: this.root ? poseHash(this.root) : '',
|
|
117
|
+
};
|
|
118
|
+
}
|
|
119
|
+
/**
|
|
120
|
+
* Takes the sibling Model's handle and builds the mixer when it settles. A
|
|
121
|
+
* Model later in the component order is not there at onReady, so the first
|
|
122
|
+
* onUpdate tries once more (`final`); a primitive Model warns at once, a
|
|
123
|
+
* missing one once it is certain, and either way exactly once.
|
|
124
|
+
*/
|
|
125
|
+
attach(final = false) {
|
|
126
|
+
const model = this.entity.get(Model);
|
|
127
|
+
const handle = model?.handle;
|
|
128
|
+
if (!handle) {
|
|
129
|
+
if ((model && !model.src) || final) {
|
|
130
|
+
this.attached = true;
|
|
131
|
+
this.pending = false;
|
|
132
|
+
this.warnOnce('model', `[waica] "${this.entity.name}": Animator needs a Model with a src beside it; nothing plays`);
|
|
133
|
+
}
|
|
134
|
+
return;
|
|
135
|
+
}
|
|
136
|
+
this.attached = true;
|
|
137
|
+
this.handle = handle;
|
|
138
|
+
reportRejection(handle.settled.then((outcome) => {
|
|
139
|
+
// Only the handle still attached builds: a Model rebuilt meanwhile has a newer one.
|
|
140
|
+
if (outcome === 'loaded' && this.entity.alive && !this.mixer && this.handle === handle)
|
|
141
|
+
this.build(handle);
|
|
142
|
+
}), 'animator model settle');
|
|
143
|
+
}
|
|
144
|
+
/** Forgets the Model's old clone and keeps the clip it played as the request for the next build. */
|
|
145
|
+
detach() {
|
|
146
|
+
this.requested = this.current ?? this.requested;
|
|
147
|
+
this.release();
|
|
148
|
+
this.attached = false;
|
|
149
|
+
this.pending = true;
|
|
150
|
+
}
|
|
151
|
+
/** Stops and drops the mixer, its actions and the clone they ran over. */
|
|
152
|
+
release() {
|
|
153
|
+
this.mixer?.stopAllAction();
|
|
154
|
+
if (this.mixer && this.root)
|
|
155
|
+
this.mixer.uncacheRoot(this.root);
|
|
156
|
+
this.mixer = null;
|
|
157
|
+
this.active = null;
|
|
158
|
+
this.current = undefined;
|
|
159
|
+
this.finished = false;
|
|
160
|
+
this.actions.clear();
|
|
161
|
+
this.names = [];
|
|
162
|
+
this.root = null;
|
|
163
|
+
this.handle = null;
|
|
164
|
+
}
|
|
165
|
+
build(handle) {
|
|
166
|
+
const mixer = new THREE.AnimationMixer(handle.root);
|
|
167
|
+
this.mixer = mixer;
|
|
168
|
+
this.root = handle.root;
|
|
169
|
+
for (const clip of handle.animations)
|
|
170
|
+
this.actions.set(clip.name, this.configure(mixer.clipAction(clip), clip.name));
|
|
171
|
+
this.names = [...this.actions.keys()];
|
|
172
|
+
this.pending = false;
|
|
173
|
+
mixer.addEventListener('finished', (event) => {
|
|
174
|
+
if (event.action === this.active)
|
|
175
|
+
this.finished = true;
|
|
176
|
+
});
|
|
177
|
+
const first = this.requested ?? this.initialClip;
|
|
178
|
+
this.requested = undefined;
|
|
179
|
+
if (first)
|
|
180
|
+
this.play(first);
|
|
181
|
+
}
|
|
182
|
+
configure(action, name) {
|
|
183
|
+
const override = this.clips[name];
|
|
184
|
+
if (override?.loop === false) {
|
|
185
|
+
action.setLoop(THREE.LoopOnce, 1);
|
|
186
|
+
action.clampWhenFinished = true;
|
|
187
|
+
}
|
|
188
|
+
action.timeScale = override?.speed ?? 1;
|
|
189
|
+
return action;
|
|
190
|
+
}
|
|
191
|
+
warnOnce(key, message) {
|
|
192
|
+
if (this.warned.has(key))
|
|
193
|
+
return;
|
|
194
|
+
this.warned.add(key);
|
|
195
|
+
console.warn(message);
|
|
196
|
+
}
|
|
197
|
+
}
|
|
@@ -0,0 +1,89 @@
|
|
|
1
|
+
import { Component, type ComponentSpace } from '../component.js';
|
|
2
|
+
import type { Vec3Json } from '../scene-camera-3d.js';
|
|
3
|
+
export type ColliderShape = 'box' | 'sphere' | 'capsule';
|
|
4
|
+
export declare const COLLIDER_SHAPES: readonly ColliderShape[];
|
|
5
|
+
/**
|
|
6
|
+
* The 3D scene's collision shape (ADR 0028), Rapier's model: alone on an
|
|
7
|
+
* entity it is a fixed wall or floor, created at the entity's position and
|
|
8
|
+
* rotation scaled by `entity.scale`, and moving the entity later does not
|
|
9
|
+
* move it. With a `RigidBody` beside it, it is the body that falls, bounces
|
|
10
|
+
* or walks. `sensor: true` makes it a trigger instead — it detects and never
|
|
11
|
+
* pushes — with the 2D Hitbox's `layer` and `collidesWith` semantics (ADR
|
|
12
|
+
* 0016); with the default mask (`['*']`) a sensor is not told about fixed
|
|
13
|
+
* solid colliders (the level's walls and floors), as a Hitbox never sees a
|
|
14
|
+
* Solid — a mask that names their layer still is. `size` is a box's full
|
|
15
|
+
* extents; `radius` a sphere's or capsule's; `height` a capsule's total
|
|
16
|
+
* end-to-end height, at least twice its radius; `offset` is in the entity's
|
|
17
|
+
* local frame. A Collider in a 2D scene creates nothing; one whose params
|
|
18
|
+
* Rapier cannot take creates no body and warns once (`validate_project`
|
|
19
|
+
* reports them as `invalid-collider-param`).
|
|
20
|
+
*/
|
|
21
|
+
export declare class Collider extends Component {
|
|
22
|
+
static componentName: string;
|
|
23
|
+
static space: ComponentSpace;
|
|
24
|
+
static params: {
|
|
25
|
+
shape: {
|
|
26
|
+
label: string;
|
|
27
|
+
options: ColliderShape[];
|
|
28
|
+
};
|
|
29
|
+
size: {
|
|
30
|
+
label: string;
|
|
31
|
+
kind: "vector3";
|
|
32
|
+
};
|
|
33
|
+
radius: {
|
|
34
|
+
label: string;
|
|
35
|
+
min: number;
|
|
36
|
+
step: number;
|
|
37
|
+
};
|
|
38
|
+
height: {
|
|
39
|
+
label: string;
|
|
40
|
+
min: number;
|
|
41
|
+
step: number;
|
|
42
|
+
};
|
|
43
|
+
offset: {
|
|
44
|
+
label: string;
|
|
45
|
+
kind: "vector3";
|
|
46
|
+
};
|
|
47
|
+
sensor: {
|
|
48
|
+
label: string;
|
|
49
|
+
};
|
|
50
|
+
layer: {
|
|
51
|
+
label: string;
|
|
52
|
+
};
|
|
53
|
+
collidesWith: {
|
|
54
|
+
label: string;
|
|
55
|
+
kind: "string-list";
|
|
56
|
+
};
|
|
57
|
+
friction: {
|
|
58
|
+
label: string;
|
|
59
|
+
min: number;
|
|
60
|
+
max: number;
|
|
61
|
+
step: number;
|
|
62
|
+
};
|
|
63
|
+
restitution: {
|
|
64
|
+
label: string;
|
|
65
|
+
min: number;
|
|
66
|
+
max: number;
|
|
67
|
+
step: number;
|
|
68
|
+
};
|
|
69
|
+
};
|
|
70
|
+
shape: ColliderShape;
|
|
71
|
+
/** A box's full extents `[x, y, z]`. */
|
|
72
|
+
size: Vec3Json;
|
|
73
|
+
/** A sphere's or capsule's radius. */
|
|
74
|
+
radius: number;
|
|
75
|
+
/** A capsule's total end-to-end height. */
|
|
76
|
+
height: number;
|
|
77
|
+
/** Where the shape sits in the entity's local frame. */
|
|
78
|
+
offset: Vec3Json;
|
|
79
|
+
/** A trigger: detects overlaps, never pushes or is pushed. */
|
|
80
|
+
sensor: boolean;
|
|
81
|
+
/** One exact, case-sensitive `^[a-z][a-z0-9-]*$` membership name. */
|
|
82
|
+
layer: string;
|
|
83
|
+
/** Outgoing layer interest for `onCollide`; `'*'` means every valid layer and `[]` means none. */
|
|
84
|
+
collidesWith: string[];
|
|
85
|
+
friction: number;
|
|
86
|
+
restitution: number;
|
|
87
|
+
onReady(): void;
|
|
88
|
+
onDestroy(): void;
|
|
89
|
+
}
|
|
@@ -0,0 +1,57 @@
|
|
|
1
|
+
import { Component } from '../component.js';
|
|
2
|
+
export const COLLIDER_SHAPES = ['box', 'sphere', 'capsule'];
|
|
3
|
+
/**
|
|
4
|
+
* The 3D scene's collision shape (ADR 0028), Rapier's model: alone on an
|
|
5
|
+
* entity it is a fixed wall or floor, created at the entity's position and
|
|
6
|
+
* rotation scaled by `entity.scale`, and moving the entity later does not
|
|
7
|
+
* move it. With a `RigidBody` beside it, it is the body that falls, bounces
|
|
8
|
+
* or walks. `sensor: true` makes it a trigger instead — it detects and never
|
|
9
|
+
* pushes — with the 2D Hitbox's `layer` and `collidesWith` semantics (ADR
|
|
10
|
+
* 0016); with the default mask (`['*']`) a sensor is not told about fixed
|
|
11
|
+
* solid colliders (the level's walls and floors), as a Hitbox never sees a
|
|
12
|
+
* Solid — a mask that names their layer still is. `size` is a box's full
|
|
13
|
+
* extents; `radius` a sphere's or capsule's; `height` a capsule's total
|
|
14
|
+
* end-to-end height, at least twice its radius; `offset` is in the entity's
|
|
15
|
+
* local frame. A Collider in a 2D scene creates nothing; one whose params
|
|
16
|
+
* Rapier cannot take creates no body and warns once (`validate_project`
|
|
17
|
+
* reports them as `invalid-collider-param`).
|
|
18
|
+
*/
|
|
19
|
+
export class Collider extends Component {
|
|
20
|
+
static componentName = 'Collider';
|
|
21
|
+
static space = '3d';
|
|
22
|
+
static params = {
|
|
23
|
+
shape: { label: 'Shape', options: [...COLLIDER_SHAPES] },
|
|
24
|
+
size: { label: 'Size', kind: 'vector3' },
|
|
25
|
+
radius: { label: 'Radius', min: 0.01, step: 0.05 },
|
|
26
|
+
height: { label: 'Height', min: 0.01, step: 0.05 },
|
|
27
|
+
offset: { label: 'Offset', kind: 'vector3' },
|
|
28
|
+
sensor: { label: 'Sensor' },
|
|
29
|
+
layer: { label: 'Collision Layer' },
|
|
30
|
+
collidesWith: { label: 'Collision Mask', kind: 'string-list' },
|
|
31
|
+
friction: { label: 'Friction', min: 0, max: 1, step: 0.05 },
|
|
32
|
+
restitution: { label: 'Restitution', min: 0, max: 1, step: 0.05 },
|
|
33
|
+
};
|
|
34
|
+
shape = 'box';
|
|
35
|
+
/** A box's full extents `[x, y, z]`. */
|
|
36
|
+
size = [1, 1, 1];
|
|
37
|
+
/** A sphere's or capsule's radius. */
|
|
38
|
+
radius = 0.5;
|
|
39
|
+
/** A capsule's total end-to-end height. */
|
|
40
|
+
height = 1.8;
|
|
41
|
+
/** Where the shape sits in the entity's local frame. */
|
|
42
|
+
offset = [0, 0, 0];
|
|
43
|
+
/** A trigger: detects overlaps, never pushes or is pushed. */
|
|
44
|
+
sensor = false;
|
|
45
|
+
/** One exact, case-sensitive `^[a-z][a-z0-9-]*$` membership name. */
|
|
46
|
+
layer = 'default';
|
|
47
|
+
/** Outgoing layer interest for `onCollide`; `'*'` means every valid layer and `[]` means none. */
|
|
48
|
+
collidesWith = ['*'];
|
|
49
|
+
friction = 0.5;
|
|
50
|
+
restitution = 0;
|
|
51
|
+
onReady() {
|
|
52
|
+
this.game.physics.attach(this.entity);
|
|
53
|
+
}
|
|
54
|
+
onDestroy() {
|
|
55
|
+
this.game.physics.detach(this.entity);
|
|
56
|
+
}
|
|
57
|
+
}
|
|
@@ -1,11 +1,12 @@
|
|
|
1
1
|
import { type CollisionPoint, type CollisionShape } from '../collision-shape.js';
|
|
2
|
-
import { Component } from '../component.js';
|
|
2
|
+
import { Component, type ComponentSpace } from '../component.js';
|
|
3
3
|
/**
|
|
4
4
|
* Reusable linear body: integrates its public velocity and resolves only
|
|
5
5
|
* against static Solid geometry. Hitbox trigger overlaps remain independent.
|
|
6
6
|
*/
|
|
7
7
|
export declare class DynamicBody extends Component {
|
|
8
8
|
static componentName: string;
|
|
9
|
+
static space: ComponentSpace;
|
|
9
10
|
static params: {
|
|
10
11
|
vx: {
|
|
11
12
|
label: string;
|
|
@@ -1,4 +1,4 @@
|
|
|
1
|
-
import { Component } from '../component.js';
|
|
1
|
+
import { Component, type ComponentSpace } from '../component.js';
|
|
2
2
|
import { type CollisionPoint, type CollisionShape } from '../collision-shape.js';
|
|
3
3
|
/**
|
|
4
4
|
* Trigger collider. A pair is tested when either Hitbox's Collision Mask
|
|
@@ -8,6 +8,7 @@ import { type CollisionPoint, type CollisionShape } from '../collision-shape.js'
|
|
|
8
8
|
*/
|
|
9
9
|
export declare class Hitbox extends Component {
|
|
10
10
|
static componentName: string;
|
|
11
|
+
static space: ComponentSpace;
|
|
11
12
|
static params: {
|
|
12
13
|
layer: {
|
|
13
14
|
label: string;
|