@waica/engine 0.6.0 → 0.7.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/dist/animation/directional.d.ts +48 -0
- package/dist/animation/directional.js +68 -0
- package/dist/archetype.d.ts +3 -0
- package/dist/camera.d.ts +19 -0
- package/dist/camera.js +10 -0
- package/dist/components/animated-sprite.d.ts +8 -1
- package/dist/components/animated-sprite.js +19 -2
- package/dist/components/sprite.d.ts +4 -1
- package/dist/components/sprite.js +5 -0
- package/dist/game.d.ts +9 -1
- package/dist/game.js +32 -3
- package/dist/index.d.ts +7 -3
- package/dist/index.js +3 -1
- package/dist/render-sort.d.ts +31 -0
- package/dist/render-sort.js +39 -0
- package/dist/scene.d.ts +10 -0
- package/dist/scene.js +1 -0
- package/dist/state/hooks.d.ts +1 -1
- package/dist/state/hooks.js +3 -1
- package/dist/state/state-machine.d.ts +18 -0
- package/dist/state/state-machine.js +57 -1
- package/package.json +1 -1
|
@@ -0,0 +1,48 @@
|
|
|
1
|
+
import type { AnimationContract } from './contract.js';
|
|
2
|
+
/**
|
|
3
|
+
* Directional animation contract: the archetype declares which facing
|
|
4
|
+
* directions exist, how clips are named per direction (`<state>-<dir>`),
|
|
5
|
+
* and how a missing direction degrades — including mirroring, so west can
|
|
6
|
+
* reuse east art flipped. Extends the base AnimationContract thesis to
|
|
7
|
+
* genres where characters face more than one way (top-down, isometric).
|
|
8
|
+
*/
|
|
9
|
+
export interface DirectionalFallback<Dir extends string = string> {
|
|
10
|
+
dir: Dir;
|
|
11
|
+
/** Mirror the resolved clip horizontally (e.g. west plays east flipped). */
|
|
12
|
+
flip?: boolean;
|
|
13
|
+
}
|
|
14
|
+
export interface DirectionalAnimation<Dir extends string = string> {
|
|
15
|
+
/** Declared facing directions, e.g. ['n', 's', 'e', 'w'] — 8 for isometric. */
|
|
16
|
+
directions: readonly Dir[];
|
|
17
|
+
/** Directional degradation: a missing facing resolves to another, optionally mirrored. */
|
|
18
|
+
fallbacks?: Partial<Record<Dir, DirectionalFallback<Dir>>>;
|
|
19
|
+
/** State-level degradation chain applied when no directional clip resolves. */
|
|
20
|
+
contract: AnimationContract;
|
|
21
|
+
}
|
|
22
|
+
export interface ResolvedDirectionalClip {
|
|
23
|
+
clip: string | undefined;
|
|
24
|
+
flip: boolean;
|
|
25
|
+
}
|
|
26
|
+
/**
|
|
27
|
+
* Resolves state × facing to a playable clip: the exact `<state>-<dir>`
|
|
28
|
+
* clip, else the declared directional fallback chain (accumulating flips).
|
|
29
|
+
* On a dead end, walks the base AnimationContract's state fallback chain
|
|
30
|
+
* (cycle-safe) and, for each candidate state in turn, retries the
|
|
31
|
+
* directional chain against it before trying its bare name. An invalid
|
|
32
|
+
* facing, or a chain that dead-ends everywhere, resolves to nothing rather
|
|
33
|
+
* than guessing — the caller's name-based fallback path handles that.
|
|
34
|
+
*/
|
|
35
|
+
export declare function resolveDirectionalClip(animation: DirectionalAnimation, available: Iterable<string>, state: string, facing: string): ResolvedDirectionalClip;
|
|
36
|
+
/** Installs (or clears, with null) the active directional contract. */
|
|
37
|
+
export declare function installDirectionalAnimation(animation: DirectionalAnimation | null): void;
|
|
38
|
+
/** The active directional contract, if an archetype installed one. */
|
|
39
|
+
export declare function installedDirectionalAnimation(): DirectionalAnimation | null;
|
|
40
|
+
/**
|
|
41
|
+
* The explicit seam a driving component (a motor) opts into so StateMachine
|
|
42
|
+
* can resolve directional clips: it reports the entity's current facing as
|
|
43
|
+
* one of the contract's declared directions.
|
|
44
|
+
*/
|
|
45
|
+
export interface AnimationFacingProvider {
|
|
46
|
+
getAnimationFacing(): string;
|
|
47
|
+
}
|
|
48
|
+
export declare function isAnimationFacingProvider(value: unknown): value is AnimationFacingProvider;
|
|
@@ -0,0 +1,68 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Walks the exact `<state>-<dir>` clip, then the declared directional
|
|
3
|
+
* fallback chain (accumulating flips), cycle-safe. Undefined at a dead end —
|
|
4
|
+
* callers decide what to try next, instead of this reaching for anything else.
|
|
5
|
+
*/
|
|
6
|
+
function directionalChain(animation, available, state, facing) {
|
|
7
|
+
const seen = new Set();
|
|
8
|
+
let dir = facing;
|
|
9
|
+
let flip = false;
|
|
10
|
+
while (dir && !seen.has(dir)) {
|
|
11
|
+
const candidate = `${state}-${dir}`;
|
|
12
|
+
if (available.has(candidate))
|
|
13
|
+
return { clip: candidate, flip };
|
|
14
|
+
seen.add(dir);
|
|
15
|
+
const fallback = animation.fallbacks?.[dir];
|
|
16
|
+
if (!fallback)
|
|
17
|
+
break;
|
|
18
|
+
if (fallback.flip)
|
|
19
|
+
flip = !flip;
|
|
20
|
+
dir = fallback.dir;
|
|
21
|
+
}
|
|
22
|
+
return undefined;
|
|
23
|
+
}
|
|
24
|
+
/**
|
|
25
|
+
* Resolves state × facing to a playable clip: the exact `<state>-<dir>`
|
|
26
|
+
* clip, else the declared directional fallback chain (accumulating flips).
|
|
27
|
+
* On a dead end, walks the base AnimationContract's state fallback chain
|
|
28
|
+
* (cycle-safe) and, for each candidate state in turn, retries the
|
|
29
|
+
* directional chain against it before trying its bare name. An invalid
|
|
30
|
+
* facing, or a chain that dead-ends everywhere, resolves to nothing rather
|
|
31
|
+
* than guessing — the caller's name-based fallback path handles that.
|
|
32
|
+
*/
|
|
33
|
+
export function resolveDirectionalClip(animation, available, state, facing) {
|
|
34
|
+
if (!animation.directions.includes(facing))
|
|
35
|
+
return { clip: undefined, flip: false };
|
|
36
|
+
const set = new Set(available);
|
|
37
|
+
const direct = directionalChain(animation, set, state, facing);
|
|
38
|
+
if (direct)
|
|
39
|
+
return direct;
|
|
40
|
+
const seenStates = new Set();
|
|
41
|
+
let candidate = state;
|
|
42
|
+
while (candidate && !seenStates.has(candidate)) {
|
|
43
|
+
seenStates.add(candidate);
|
|
44
|
+
if (candidate !== state) {
|
|
45
|
+
const viaDirection = directionalChain(animation, set, candidate, facing);
|
|
46
|
+
if (viaDirection)
|
|
47
|
+
return viaDirection;
|
|
48
|
+
}
|
|
49
|
+
if (set.has(candidate))
|
|
50
|
+
return { clip: candidate, flip: false };
|
|
51
|
+
candidate = animation.contract.fallbacks[candidate];
|
|
52
|
+
}
|
|
53
|
+
return { clip: undefined, flip: false };
|
|
54
|
+
}
|
|
55
|
+
let installed = null;
|
|
56
|
+
/** Installs (or clears, with null) the active directional contract. */
|
|
57
|
+
export function installDirectionalAnimation(animation) {
|
|
58
|
+
installed = animation;
|
|
59
|
+
}
|
|
60
|
+
/** The active directional contract, if an archetype installed one. */
|
|
61
|
+
export function installedDirectionalAnimation() {
|
|
62
|
+
return installed;
|
|
63
|
+
}
|
|
64
|
+
export function isAnimationFacingProvider(value) {
|
|
65
|
+
return (typeof value === 'object' &&
|
|
66
|
+
value !== null &&
|
|
67
|
+
typeof value.getAnimationFacing === 'function');
|
|
68
|
+
}
|
package/dist/archetype.d.ts
CHANGED
|
@@ -1,3 +1,4 @@
|
|
|
1
|
+
import type { DirectionalAnimation } from './animation/directional.js';
|
|
1
2
|
import type { InputBindings } from './input.js';
|
|
2
3
|
import type { PrefabJson, SceneEntityJson, SceneJson, SceneRegistry } from './scene.js';
|
|
3
4
|
import type { ArchetypeBundle } from './state/hooks.js';
|
|
@@ -30,6 +31,8 @@ export interface ArchetypeManifest {
|
|
|
30
31
|
bindings: Readonly<InputBindings>;
|
|
31
32
|
actionLabels: Readonly<Record<string, string>>;
|
|
32
33
|
bundle: ArchetypeBundle;
|
|
34
|
+
/** Directional animation contract, for genres where characters face around. */
|
|
35
|
+
animation?: DirectionalAnimation;
|
|
33
36
|
}
|
|
34
37
|
/** Browser manifest enriched with URLs produced by an asset-aware bundler. */
|
|
35
38
|
export interface BrowserArchetypeManifest extends ArchetypeManifest {
|
package/dist/camera.d.ts
CHANGED
|
@@ -21,6 +21,8 @@ export interface SceneCameraJson {
|
|
|
21
21
|
deadzoneWidth?: number;
|
|
22
22
|
deadzoneHeight?: number;
|
|
23
23
|
lookahead?: number;
|
|
24
|
+
/** Vertical lookahead in world units; 0 (the default) disables it. */
|
|
25
|
+
lookaheadY?: number;
|
|
24
26
|
smoothing?: number;
|
|
25
27
|
limits?: CameraLimitsJson;
|
|
26
28
|
}
|
|
@@ -31,6 +33,7 @@ export interface ResolvedSceneCamera {
|
|
|
31
33
|
deadzoneWidth: number;
|
|
32
34
|
deadzoneHeight: number;
|
|
33
35
|
lookahead: number;
|
|
36
|
+
lookaheadY: number;
|
|
34
37
|
smoothing: number;
|
|
35
38
|
limits: CameraLimitsJson | null;
|
|
36
39
|
}
|
|
@@ -41,10 +44,24 @@ export declare const CAMERA_DEFAULTS: {
|
|
|
41
44
|
readonly deadzoneWidth: 2;
|
|
42
45
|
readonly deadzoneHeight: 2.5;
|
|
43
46
|
readonly lookahead: 1.5;
|
|
47
|
+
readonly lookaheadY: 0;
|
|
44
48
|
readonly smoothing: 6;
|
|
45
49
|
};
|
|
46
50
|
/** Fills a scene's camera block with the engine defaults. */
|
|
47
51
|
export declare function resolveSceneCamera(json?: SceneCameraJson): ResolvedSceneCamera;
|
|
52
|
+
/** Two-axis velocity a followed entity reports for camera lookahead. */
|
|
53
|
+
export interface CameraVelocity {
|
|
54
|
+
vx: number;
|
|
55
|
+
vy: number;
|
|
56
|
+
}
|
|
57
|
+
/**
|
|
58
|
+
* The explicit seam a component opts into so the scene camera can read the
|
|
59
|
+
* followed entity's velocity — the engine never guesses from field names.
|
|
60
|
+
*/
|
|
61
|
+
export interface CameraVelocityProvider {
|
|
62
|
+
getCameraVelocity(): CameraVelocity;
|
|
63
|
+
}
|
|
64
|
+
export declare function isCameraVelocityProvider(value: unknown): value is CameraVelocityProvider;
|
|
48
65
|
export interface CameraStepInput {
|
|
49
66
|
/** Current camera center. */
|
|
50
67
|
x: number;
|
|
@@ -59,6 +76,8 @@ export interface CameraStepInput {
|
|
|
59
76
|
} | null;
|
|
60
77
|
/** Followed entity's horizontal velocity, for lookahead. */
|
|
61
78
|
vx: number;
|
|
79
|
+
/** Followed entity's vertical velocity, for lookaheadY; omitted = 0. */
|
|
80
|
+
vy?: number;
|
|
62
81
|
dt: number;
|
|
63
82
|
}
|
|
64
83
|
/**
|
package/dist/camera.js
CHANGED
|
@@ -6,6 +6,7 @@ export const CAMERA_DEFAULTS = {
|
|
|
6
6
|
deadzoneWidth: 2,
|
|
7
7
|
deadzoneHeight: 2.5,
|
|
8
8
|
lookahead: 1.5,
|
|
9
|
+
lookaheadY: 0,
|
|
9
10
|
smoothing: 6,
|
|
10
11
|
};
|
|
11
12
|
/** Fills a scene's camera block with the engine defaults. */
|
|
@@ -17,6 +18,7 @@ export function resolveSceneCamera(json) {
|
|
|
17
18
|
deadzoneWidth: json?.deadzoneWidth ?? CAMERA_DEFAULTS.deadzoneWidth,
|
|
18
19
|
deadzoneHeight: json?.deadzoneHeight ?? CAMERA_DEFAULTS.deadzoneHeight,
|
|
19
20
|
lookahead: json?.lookahead ?? CAMERA_DEFAULTS.lookahead,
|
|
21
|
+
lookaheadY: json?.lookaheadY ?? CAMERA_DEFAULTS.lookaheadY,
|
|
20
22
|
smoothing: json?.smoothing ?? CAMERA_DEFAULTS.smoothing,
|
|
21
23
|
limits: json?.limits ?? null,
|
|
22
24
|
};
|
|
@@ -28,6 +30,11 @@ function clampAxis(center, halfView, min, max) {
|
|
|
28
30
|
return (min + max) / 2;
|
|
29
31
|
return Math.min(Math.max(center, min + halfView), max - halfView);
|
|
30
32
|
}
|
|
33
|
+
export function isCameraVelocityProvider(value) {
|
|
34
|
+
return (typeof value === 'object' &&
|
|
35
|
+
value !== null &&
|
|
36
|
+
typeof value.getCameraVelocity === 'function');
|
|
37
|
+
}
|
|
31
38
|
/**
|
|
32
39
|
* One simulation step of the camera: deadzone-follow with lookahead and
|
|
33
40
|
* exponential smoothing, then limits. Pure — returns the next center.
|
|
@@ -48,6 +55,9 @@ export function stepSceneCamera(cam, input) {
|
|
|
48
55
|
wantY = input.target.y - Math.sign(dy) * halfDzH;
|
|
49
56
|
if (Math.abs(input.vx) > 1)
|
|
50
57
|
wantX += Math.sign(input.vx) * cam.lookahead;
|
|
58
|
+
const vy = input.vy ?? 0;
|
|
59
|
+
if (Math.abs(vy) > 1)
|
|
60
|
+
wantY += Math.sign(vy) * cam.lookaheadY;
|
|
51
61
|
x = THREE.MathUtils.damp(x, wantX, cam.smoothing, input.dt);
|
|
52
62
|
y = THREE.MathUtils.damp(y, wantY, cam.smoothing, input.dt);
|
|
53
63
|
}
|
|
@@ -1,6 +1,7 @@
|
|
|
1
1
|
import { Component } from '../component.js';
|
|
2
2
|
import { type ClipDef } from '../animation/clip-player.js';
|
|
3
3
|
import { type SheetCell, type SheetDef } from '../animation/sheet.js';
|
|
4
|
+
import type { YSortParticipant } from '../render-sort.js';
|
|
4
5
|
/**
|
|
5
6
|
* Sprite animated from one or more spritesheets. The main sheet is the
|
|
6
7
|
* top-level texture/cols/rows (or explicit cells); extraSheets append after
|
|
@@ -10,7 +11,7 @@ import { type SheetCell, type SheetDef } from '../animation/sheet.js';
|
|
|
10
11
|
* so the quad rescales per frame, anchored bottom-center — width/height size
|
|
11
12
|
* the sheet's largest frame and smaller ones keep their feet planted.
|
|
12
13
|
*/
|
|
13
|
-
export declare class AnimatedSprite extends Component {
|
|
14
|
+
export declare class AnimatedSprite extends Component implements YSortParticipant {
|
|
14
15
|
static componentName: string;
|
|
15
16
|
static updateAfter: readonly string[];
|
|
16
17
|
static params: {
|
|
@@ -59,6 +60,12 @@ export declare class AnimatedSprite extends Component {
|
|
|
59
60
|
private _layer;
|
|
60
61
|
get layer(): number;
|
|
61
62
|
set layer(value: number);
|
|
63
|
+
/** Y-sort pass hook: overrides the layer-derived z for this frame. */
|
|
64
|
+
setSortZ(z: number): void;
|
|
65
|
+
/** Mirrored state, readable by Runtime Snapshots; write via setFlipX. */
|
|
66
|
+
flipX: boolean;
|
|
67
|
+
/** Mirrors the quad horizontally — directional clips reuse east art for west. */
|
|
68
|
+
setFlipX(value: boolean): void;
|
|
62
69
|
clips: Record<string, ClipDef>;
|
|
63
70
|
initialClip?: string;
|
|
64
71
|
/** Clip currently playing. */
|
|
@@ -22,6 +22,7 @@ export class AnimatedSprite extends Component {
|
|
|
22
22
|
};
|
|
23
23
|
static transient = [
|
|
24
24
|
'current',
|
|
25
|
+
'flipX',
|
|
25
26
|
'player',
|
|
26
27
|
'sheets',
|
|
27
28
|
'texs',
|
|
@@ -93,6 +94,20 @@ export class AnimatedSprite extends Component {
|
|
|
93
94
|
if (this.mesh)
|
|
94
95
|
this.mesh.position.z = value * 0.01;
|
|
95
96
|
}
|
|
97
|
+
/** Y-sort pass hook: overrides the layer-derived z for this frame. */
|
|
98
|
+
setSortZ(z) {
|
|
99
|
+
if (this.mesh)
|
|
100
|
+
this.mesh.position.z = z;
|
|
101
|
+
}
|
|
102
|
+
/** Mirrored state, readable by Runtime Snapshots; write via setFlipX. */
|
|
103
|
+
flipX = false;
|
|
104
|
+
/** Mirrors the quad horizontally — directional clips reuse east art for west. */
|
|
105
|
+
setFlipX(value) {
|
|
106
|
+
if (this.flipX === value)
|
|
107
|
+
return;
|
|
108
|
+
this.flipX = value;
|
|
109
|
+
this.syncQuad();
|
|
110
|
+
}
|
|
96
111
|
clips = {};
|
|
97
112
|
initialClip;
|
|
98
113
|
/** Clip currently playing. */
|
|
@@ -171,9 +186,11 @@ export class AnimatedSprite extends Component {
|
|
|
171
186
|
syncQuad() {
|
|
172
187
|
if (!this.mesh)
|
|
173
188
|
return;
|
|
174
|
-
this.mesh.scale.set(this._width * this.frameScaleX, this._height * this.frameScaleY, 1);
|
|
189
|
+
this.mesh.scale.set(this._width * this.frameScaleX * (this.flipX ? -1 : 1), this._height * this.frameScaleY, 1);
|
|
175
190
|
// Bottom-center anchor: a shrunk frame keeps its feet on the quad's floor.
|
|
176
|
-
|
|
191
|
+
// The offset mirrors along with the art, or a flipped sprite with a
|
|
192
|
+
// nonzero offsetX would shift to the wrong side.
|
|
193
|
+
this.mesh.position.x = this.flipX ? -this._offsetX : this._offsetX;
|
|
177
194
|
this.mesh.position.y = this._offsetY - (this._height * (1 - this.frameScaleY)) / 2;
|
|
178
195
|
}
|
|
179
196
|
applyFrame() {
|
|
@@ -1,10 +1,11 @@
|
|
|
1
1
|
import { Component } from '../component.js';
|
|
2
|
+
import type { YSortParticipant } from '../render-sort.js';
|
|
2
3
|
export type SpriteShape = 'rectangle' | 'circle';
|
|
3
4
|
/**
|
|
4
5
|
* Textured or flat-color quad. In the unified pipeline, a 2D sprite is a
|
|
5
6
|
* plane in front of the orthographic camera (see DESIGN.md §6, decision 2).
|
|
6
7
|
*/
|
|
7
|
-
export declare class Sprite extends Component {
|
|
8
|
+
export declare class Sprite extends Component implements YSortParticipant {
|
|
8
9
|
static componentName: string;
|
|
9
10
|
static params: {
|
|
10
11
|
offsetX: {
|
|
@@ -42,6 +43,8 @@ export declare class Sprite extends Component {
|
|
|
42
43
|
private _layer;
|
|
43
44
|
get layer(): number;
|
|
44
45
|
set layer(value: number);
|
|
46
|
+
/** Y-sort pass hook: overrides the layer-derived z for this frame. */
|
|
47
|
+
setSortZ(z: number): void;
|
|
45
48
|
private _shape;
|
|
46
49
|
get shape(): SpriteShape;
|
|
47
50
|
set shape(value: SpriteShape);
|
|
@@ -71,6 +71,11 @@ export class Sprite extends Component {
|
|
|
71
71
|
if (this.mesh)
|
|
72
72
|
this.mesh.position.z = value * 0.01;
|
|
73
73
|
}
|
|
74
|
+
/** Y-sort pass hook: overrides the layer-derived z for this frame. */
|
|
75
|
+
setSortZ(z) {
|
|
76
|
+
if (this.mesh)
|
|
77
|
+
this.mesh.position.z = z;
|
|
78
|
+
}
|
|
74
79
|
_shape = 'rectangle';
|
|
75
80
|
get shape() {
|
|
76
81
|
return this._shape;
|
package/dist/game.d.ts
CHANGED
|
@@ -4,7 +4,7 @@ import type { Component } from './component.js';
|
|
|
4
4
|
import { Entity } from './entity.js';
|
|
5
5
|
import { Emitter } from './events.js';
|
|
6
6
|
import { Input, type InputBindings } from './input.js';
|
|
7
|
-
import { type SceneRegistry } from './scene.js';
|
|
7
|
+
import { type SceneRegistry, type SceneRenderJson } from './scene.js';
|
|
8
8
|
import { Stats, type StatValue } from './stats.js';
|
|
9
9
|
import { GameUi } from './ui.js';
|
|
10
10
|
/** Fixed game resolution: the view keeps this aspect, letterboxed. */
|
|
@@ -61,6 +61,7 @@ export declare class Game {
|
|
|
61
61
|
private readonly resolution;
|
|
62
62
|
private viewHeight;
|
|
63
63
|
private sceneCamera;
|
|
64
|
+
private renderSort;
|
|
64
65
|
private lastTime;
|
|
65
66
|
private runtimeBridge;
|
|
66
67
|
constructor(options: GameOptions);
|
|
@@ -76,6 +77,11 @@ export declare class Game {
|
|
|
76
77
|
applyParamOverrides(entity: Entity, component: Component): void;
|
|
77
78
|
/** Registers a function that runs once per frame. Returns the unsubscribe. */
|
|
78
79
|
onUpdate(fn: UpdateFn): () => void;
|
|
80
|
+
/**
|
|
81
|
+
* Adopts a scene's render block. Called by loadScene; without a block the
|
|
82
|
+
* draw order stays layer-banded with spawn-order ties.
|
|
83
|
+
*/
|
|
84
|
+
setSceneRender(json?: SceneRenderJson): void;
|
|
79
85
|
/**
|
|
80
86
|
* Adopts a scene's camera block: jumps to its framing and, while
|
|
81
87
|
* simulating, follows/clamps per its settings. Called by loadScene.
|
|
@@ -94,6 +100,8 @@ export declare class Game {
|
|
|
94
100
|
private tick;
|
|
95
101
|
private runFrame;
|
|
96
102
|
private unregisterRuntimeBridge;
|
|
103
|
+
/** Under y-sort, re-derives every participant's z from layer band + entity Y. */
|
|
104
|
+
private applyYSort;
|
|
97
105
|
private renderSurface;
|
|
98
106
|
private componentUpdateSchedule;
|
|
99
107
|
private updateSceneCamera;
|
package/dist/game.js
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
import * as THREE from 'three';
|
|
2
2
|
import { collisionOverlap } from './collision-shape.js';
|
|
3
|
-
import { resolveSceneCamera, stepSceneCamera } from './camera.js';
|
|
3
|
+
import { isCameraVelocityProvider, resolveSceneCamera, stepSceneCamera, } from './camera.js';
|
|
4
4
|
import { resolveComponentUpdateSchedule } from './component-update-schedule.js';
|
|
5
5
|
import { Hitbox } from './components/hitbox.js';
|
|
6
6
|
import { Entity } from './entity.js';
|
|
@@ -8,6 +8,7 @@ import { Emitter } from './events.js';
|
|
|
8
8
|
import { Input } from './input.js';
|
|
9
9
|
import { activeRuntimeBridgeHook, EngineRuntimeBridge, } from './runtime-bridge.js';
|
|
10
10
|
import { RuntimeInspector } from './runtime-inspection.js';
|
|
11
|
+
import { isYSortParticipant, ySortZ } from './render-sort.js';
|
|
11
12
|
import { registryEntry, spawnFromJson } from './scene.js';
|
|
12
13
|
import { Stats } from './stats.js';
|
|
13
14
|
import { GameUi } from './ui.js';
|
|
@@ -40,6 +41,7 @@ export class Game {
|
|
|
40
41
|
resolution;
|
|
41
42
|
viewHeight;
|
|
42
43
|
sceneCamera = null;
|
|
44
|
+
renderSort = null;
|
|
43
45
|
lastTime = 0;
|
|
44
46
|
runtimeBridge = null;
|
|
45
47
|
constructor(options) {
|
|
@@ -107,6 +109,13 @@ export class Game {
|
|
|
107
109
|
this.updateFns.add(fn);
|
|
108
110
|
return () => this.updateFns.delete(fn);
|
|
109
111
|
}
|
|
112
|
+
/**
|
|
113
|
+
* Adopts a scene's render block. Called by loadScene; without a block the
|
|
114
|
+
* draw order stays layer-banded with spawn-order ties.
|
|
115
|
+
*/
|
|
116
|
+
setSceneRender(json) {
|
|
117
|
+
this.renderSort = json?.sort === 'y' ? 'y' : null;
|
|
118
|
+
}
|
|
110
119
|
/**
|
|
111
120
|
* Adopts a scene's camera block: jumps to its framing and, while
|
|
112
121
|
* simulating, follows/clamps per its settings. Called by loadScene.
|
|
@@ -216,7 +225,25 @@ export class Game {
|
|
|
216
225
|
this.runtimeBridge?.unregister();
|
|
217
226
|
this.runtimeBridge = null;
|
|
218
227
|
};
|
|
228
|
+
/** Under y-sort, re-derives every participant's z from layer band + entity Y. */
|
|
229
|
+
applyYSort() {
|
|
230
|
+
const participants = [];
|
|
231
|
+
const entries = [];
|
|
232
|
+
for (const entity of this.entities) {
|
|
233
|
+
for (const component of entity.components) {
|
|
234
|
+
if (isYSortParticipant(component)) {
|
|
235
|
+
participants.push(component);
|
|
236
|
+
entries.push({ layer: component.layer, y: entity.position.y });
|
|
237
|
+
}
|
|
238
|
+
}
|
|
239
|
+
}
|
|
240
|
+
const z = ySortZ(entries);
|
|
241
|
+
for (const [index, participant] of participants.entries())
|
|
242
|
+
participant.setSortZ(z[index]);
|
|
243
|
+
}
|
|
219
244
|
renderSurface() {
|
|
245
|
+
if (this.renderSort === 'y')
|
|
246
|
+
this.applyYSort();
|
|
220
247
|
this.ui.setActive(this.simulate);
|
|
221
248
|
if (this.resolution) {
|
|
222
249
|
// Letterbox bars: clear the whole canvas, then render inside the scissor.
|
|
@@ -262,14 +289,16 @@ export class Game {
|
|
|
262
289
|
if (!cam)
|
|
263
290
|
return;
|
|
264
291
|
const followed = cam.follow ? this.find(cam.follow) : undefined;
|
|
265
|
-
const
|
|
292
|
+
const provider = followed?.components.find((c) => isCameraVelocityProvider(c));
|
|
293
|
+
const velocity = provider?.getCameraVelocity();
|
|
266
294
|
const next = stepSceneCamera(cam, {
|
|
267
295
|
x: this.camera.position.x,
|
|
268
296
|
y: this.camera.position.y,
|
|
269
297
|
halfW: (this.camera.right - this.camera.left) / 2,
|
|
270
298
|
halfH: this.viewHeight / 2,
|
|
271
299
|
target: followed ? { x: followed.position.x, y: followed.position.y } : null,
|
|
272
|
-
vx:
|
|
300
|
+
vx: velocity?.vx ?? 0,
|
|
301
|
+
vy: velocity?.vy ?? 0,
|
|
273
302
|
dt,
|
|
274
303
|
});
|
|
275
304
|
this.camera.position.x = next.x;
|
package/dist/index.d.ts
CHANGED
|
@@ -1,7 +1,11 @@
|
|
|
1
1
|
export { Game } from './game.js';
|
|
2
2
|
export type { GameOptions, GameResolution, SpawnPrefabOptions, UpdateFn, ParamOverrides, } from './game.js';
|
|
3
|
-
export {
|
|
4
|
-
export type {
|
|
3
|
+
export { installDirectionalAnimation, installedDirectionalAnimation, isAnimationFacingProvider, resolveDirectionalClip, } from './animation/directional.js';
|
|
4
|
+
export type { AnimationFacingProvider, DirectionalAnimation, DirectionalFallback, ResolvedDirectionalClip, } from './animation/directional.js';
|
|
5
|
+
export { isYSortParticipant, ySortZ } from './render-sort.js';
|
|
6
|
+
export type { YSortEntry, YSortParticipant } from './render-sort.js';
|
|
7
|
+
export { CAMERA_DEFAULTS, isCameraVelocityProvider, resolveSceneCamera, stepSceneCamera } from './camera.js';
|
|
8
|
+
export type { SceneCameraJson, CameraLimitsJson, CameraVelocity, CameraVelocityProvider, ResolvedSceneCamera, } from './camera.js';
|
|
5
9
|
export { Entity } from './entity.js';
|
|
6
10
|
export { Component } from './component.js';
|
|
7
11
|
export { authoringDefaults } from './authoring-defaults.js';
|
|
@@ -32,7 +36,7 @@ export { COLLISION_SHAPES, DEFAULT_COLLISION_POLYGON, collisionBounds, collision
|
|
|
32
36
|
export type { CollisionBody, CollisionBounds, CollisionPoint, CollisionShape, } from './collision-shape.js';
|
|
33
37
|
export { Emitter } from './events.js';
|
|
34
38
|
export { loadScene, spawnFromJson, resolveEntityComponents, resolveProps } from './scene.js';
|
|
35
|
-
export type { SceneJson, SceneEntityJson, SceneComponentJson, SceneRegistry, PrefabJson } from './scene.js';
|
|
39
|
+
export type { SceneJson, SceneEntityJson, SceneComponentJson, SceneRegistry, SceneRenderJson, PrefabJson, } from './scene.js';
|
|
36
40
|
export { ClipPlayer } from './animation/clip-player.js';
|
|
37
41
|
export type { ClipDef } from './animation/clip-player.js';
|
|
38
42
|
export { sheetCell, sheetFrameCount, locateFrame } from './animation/sheet.js';
|
package/dist/index.js
CHANGED
|
@@ -1,5 +1,7 @@
|
|
|
1
1
|
export { Game } from './game.js';
|
|
2
|
-
export {
|
|
2
|
+
export { installDirectionalAnimation, installedDirectionalAnimation, isAnimationFacingProvider, resolveDirectionalClip, } from './animation/directional.js';
|
|
3
|
+
export { isYSortParticipant, ySortZ } from './render-sort.js';
|
|
4
|
+
export { CAMERA_DEFAULTS, isCameraVelocityProvider, resolveSceneCamera, stepSceneCamera } from './camera.js';
|
|
3
5
|
export { Entity } from './entity.js';
|
|
4
6
|
export { Component } from './component.js';
|
|
5
7
|
export { authoringDefaults } from './authoring-defaults.js';
|
|
@@ -0,0 +1,31 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Y-sort draw ordering: an opt-in render mode (scene JSON `render.sort: 'y'`)
|
|
3
|
+
* for top-down scenes where "lower on screen" means "closer to the camera".
|
|
4
|
+
*/
|
|
5
|
+
export interface YSortEntry {
|
|
6
|
+
/** The sprite's layer — the primary draw-order band, exactly as without y-sort. */
|
|
7
|
+
layer: number;
|
|
8
|
+
/** The owning entity's world Y — the sort key. Sprite offsets don't shift it. */
|
|
9
|
+
y: number;
|
|
10
|
+
}
|
|
11
|
+
/**
|
|
12
|
+
* The explicit seam a component opts into to participate in y-sort: it
|
|
13
|
+
* exposes its draw-order layer and accepts the per-frame z the pass derives.
|
|
14
|
+
* Both stock sprite classes implement it; a custom renderable can too.
|
|
15
|
+
*/
|
|
16
|
+
export interface YSortParticipant {
|
|
17
|
+
readonly layer: number;
|
|
18
|
+
/** Y-sort pass hook: overrides the layer-derived z for this frame. */
|
|
19
|
+
setSortZ(z: number): void;
|
|
20
|
+
}
|
|
21
|
+
export declare function isYSortParticipant(value: unknown): value is YSortParticipant;
|
|
22
|
+
/**
|
|
23
|
+
* Z per entry under y-sort. Each layer keeps its 0.01 band; within a band,
|
|
24
|
+
* lower Y gets a higher z (renders in front), and exact Y ties keep input
|
|
25
|
+
* order. Offsets stay strictly inside (layer, layer + 1) × 0.01 for integer
|
|
26
|
+
* layers — but a fractional layer (e.g. 0.5) can sit closer than that to the
|
|
27
|
+
* next one present, so each band is capped at the gap to the next distinct
|
|
28
|
+
* layer above it, never wider than 0.01. Integer layers are always >= 1
|
|
29
|
+
* apart, so their band is exactly the old fixed 0.01.
|
|
30
|
+
*/
|
|
31
|
+
export declare function ySortZ(entries: readonly YSortEntry[]): number[];
|
|
@@ -0,0 +1,39 @@
|
|
|
1
|
+
export function isYSortParticipant(value) {
|
|
2
|
+
return (typeof value === 'object' &&
|
|
3
|
+
value !== null &&
|
|
4
|
+
typeof value.layer === 'number' &&
|
|
5
|
+
typeof value.setSortZ === 'function');
|
|
6
|
+
}
|
|
7
|
+
/**
|
|
8
|
+
* Z per entry under y-sort. Each layer keeps its 0.01 band; within a band,
|
|
9
|
+
* lower Y gets a higher z (renders in front), and exact Y ties keep input
|
|
10
|
+
* order. Offsets stay strictly inside (layer, layer + 1) × 0.01 for integer
|
|
11
|
+
* layers — but a fractional layer (e.g. 0.5) can sit closer than that to the
|
|
12
|
+
* next one present, so each band is capped at the gap to the next distinct
|
|
13
|
+
* layer above it, never wider than 0.01. Integer layers are always >= 1
|
|
14
|
+
* apart, so their band is exactly the old fixed 0.01.
|
|
15
|
+
*/
|
|
16
|
+
export function ySortZ(entries) {
|
|
17
|
+
const byLayer = new Map();
|
|
18
|
+
for (const [index, entry] of entries.entries()) {
|
|
19
|
+
const group = byLayer.get(entry.layer);
|
|
20
|
+
if (group)
|
|
21
|
+
group.push(index);
|
|
22
|
+
else
|
|
23
|
+
byLayer.set(entry.layer, [index]);
|
|
24
|
+
}
|
|
25
|
+
const layers = [...byLayer.keys()].sort((a, b) => a - b);
|
|
26
|
+
const z = new Array(entries.length);
|
|
27
|
+
for (const [i, layer] of layers.entries()) {
|
|
28
|
+
const indices = byLayer.get(layer);
|
|
29
|
+
const next = layers[i + 1];
|
|
30
|
+
const width = next === undefined ? 0.01 : Math.min(0.01, (next - layer) * 0.01);
|
|
31
|
+
// Stable sort: back-to-front is descending Y, ties keep input order.
|
|
32
|
+
const ordered = [...indices].sort((a, b) => entries[b].y - entries[a].y);
|
|
33
|
+
const step = width / (ordered.length + 1);
|
|
34
|
+
for (const [rank, index] of ordered.entries()) {
|
|
35
|
+
z[index] = layer * 0.01 + (rank + 1) * step;
|
|
36
|
+
}
|
|
37
|
+
}
|
|
38
|
+
return z;
|
|
39
|
+
}
|
package/dist/scene.d.ts
CHANGED
|
@@ -28,10 +28,20 @@ export interface PrefabJson {
|
|
|
28
28
|
type: 'character' | 'object' | 'tile';
|
|
29
29
|
components: SceneComponentJson[];
|
|
30
30
|
}
|
|
31
|
+
/** Scene-wide render options (v3). */
|
|
32
|
+
export interface SceneRenderJson {
|
|
33
|
+
/**
|
|
34
|
+
* 'y' orders same-layer sprites by their entity's world Y — lower Y renders
|
|
35
|
+
* in front (top-down depth). Absent: spawn order breaks same-layer ties.
|
|
36
|
+
*/
|
|
37
|
+
sort?: 'y';
|
|
38
|
+
}
|
|
31
39
|
export interface SceneJson {
|
|
32
40
|
waicaScene: 1 | 2 | 3;
|
|
33
41
|
/** The scene's built-in camera (v3); absent = the host keeps control. */
|
|
34
42
|
camera?: SceneCameraJson;
|
|
43
|
+
/** Draw-order policy (v3); absent = layer bands with spawn-order ties. */
|
|
44
|
+
render?: SceneRenderJson;
|
|
35
45
|
entities: SceneEntityJson[];
|
|
36
46
|
/** UI pieces (src/ui/*.html) mounted visible when the scene loads. */
|
|
37
47
|
ui?: string[];
|
package/dist/scene.js
CHANGED
|
@@ -82,6 +82,7 @@ export function spawnFromJson(game, json, registry) {
|
|
|
82
82
|
/** Loads a full scene into the game. */
|
|
83
83
|
export function loadScene(game, scene, registry) {
|
|
84
84
|
game.registry = registry;
|
|
85
|
+
game.setSceneRender(scene.render);
|
|
85
86
|
for (const entityJson of scene.entities)
|
|
86
87
|
spawnFromJson(game, entityJson, registry);
|
|
87
88
|
// After the spawns: with a follow target, the camera starts centered on it.
|
package/dist/state/hooks.d.ts
CHANGED
|
@@ -63,7 +63,7 @@ export interface ArchetypeBundle {
|
|
|
63
63
|
/** Extra named logic sets that are not themselves roles. */
|
|
64
64
|
logicSets?: Readonly<Record<string, StateLogic>>;
|
|
65
65
|
}
|
|
66
|
-
/** Clears role definitions
|
|
66
|
+
/** Clears role definitions, every named logic set and the directional contract. */
|
|
67
67
|
export declare function resetRegistries(): void;
|
|
68
68
|
/**
|
|
69
69
|
* Replaces the active registry contents with one archetype's complete bundle.
|
package/dist/state/hooks.js
CHANGED
|
@@ -1,3 +1,4 @@
|
|
|
1
|
+
import { installDirectionalAnimation } from '../animation/directional.js';
|
|
1
2
|
const sets = new Map();
|
|
2
3
|
/**
|
|
3
4
|
* Registers state code under a logic-set name. A role's name is also its
|
|
@@ -8,10 +9,11 @@ export function defineStates(name, states) {
|
|
|
8
9
|
sets.set(name, { ...sets.get(name), ...states });
|
|
9
10
|
}
|
|
10
11
|
const roles = new Map();
|
|
11
|
-
/** Clears role definitions
|
|
12
|
+
/** Clears role definitions, every named logic set and the directional contract. */
|
|
12
13
|
export function resetRegistries() {
|
|
13
14
|
roles.clear();
|
|
14
15
|
sets.clear();
|
|
16
|
+
installDirectionalAnimation(null);
|
|
15
17
|
}
|
|
16
18
|
/**
|
|
17
19
|
* Replaces the active registry contents with one archetype's complete bundle.
|
|
@@ -66,6 +66,8 @@ export declare class StateMachine extends Component {
|
|
|
66
66
|
private readonly instanceHooks;
|
|
67
67
|
private readonly signals;
|
|
68
68
|
private readonly warnedClips;
|
|
69
|
+
/** Facing last used to resolve a directional clip — detects a turn mid-state. */
|
|
70
|
+
private lastFacing;
|
|
69
71
|
onReady(): void;
|
|
70
72
|
/** Adds instance-level hooks on top of the logic set — the escape hatch. */
|
|
71
73
|
on(state: string, hooks: StateHooks): void;
|
|
@@ -81,4 +83,20 @@ export declare class StateMachine extends Component {
|
|
|
81
83
|
private runCollision;
|
|
82
84
|
private enter;
|
|
83
85
|
private playClip;
|
|
86
|
+
/**
|
|
87
|
+
* Directional resolution: with an installed contract AND a sibling that
|
|
88
|
+
* reports facing, plays state × facing (mirroring included). Returns false
|
|
89
|
+
* to keep the name-based path — no contract, no facing, no playable clip.
|
|
90
|
+
*/
|
|
91
|
+
private playDirectionalClip;
|
|
92
|
+
private facingProvider;
|
|
93
|
+
/**
|
|
94
|
+
* playDirectionalClip only runs at enter() time (via playClip), so a
|
|
95
|
+
* facing change that doesn't cross a state boundary — turning while
|
|
96
|
+
* still walking, say — never re-resolves the clip on its own. Catch that
|
|
97
|
+
* here every frame: same state, new facing, re-run the resolution.
|
|
98
|
+
* AnimatedSprite.play() no-ops on an unchanged clip name, so this is
|
|
99
|
+
* cheap when facing hasn't moved the resolved clip.
|
|
100
|
+
*/
|
|
101
|
+
private reresolveOnFacingChange;
|
|
84
102
|
}
|
|
@@ -1,3 +1,4 @@
|
|
|
1
|
+
import { installedDirectionalAnimation, isAnimationFacingProvider, resolveDirectionalClip, } from '../animation/directional.js';
|
|
1
2
|
import { Component } from '../component.js';
|
|
2
3
|
import { AnimatedSprite } from '../components/animated-sprite.js';
|
|
3
4
|
import { closestLogicSet, logicSet, registeredLogicSets, } from './hooks.js';
|
|
@@ -47,7 +48,14 @@ export class StateMachine extends Component {
|
|
|
47
48
|
static params = {
|
|
48
49
|
role: { label: 'Role' },
|
|
49
50
|
};
|
|
50
|
-
static transient = [
|
|
51
|
+
static transient = [
|
|
52
|
+
'current',
|
|
53
|
+
'elapsed',
|
|
54
|
+
'instanceHooks',
|
|
55
|
+
'signals',
|
|
56
|
+
'warnedClips',
|
|
57
|
+
'lastFacing',
|
|
58
|
+
];
|
|
51
59
|
/** The character's role — names the logic set providing its state code. */
|
|
52
60
|
role = '';
|
|
53
61
|
/** Starting state; defaults to the first declared state. */
|
|
@@ -60,6 +68,8 @@ export class StateMachine extends Component {
|
|
|
60
68
|
instanceHooks = new Map();
|
|
61
69
|
signals = new Set();
|
|
62
70
|
warnedClips = new Set();
|
|
71
|
+
/** Facing last used to resolve a directional clip — detects a turn mid-state. */
|
|
72
|
+
lastFacing;
|
|
63
73
|
onReady() {
|
|
64
74
|
if (this.role && !logicSet(this.role)) {
|
|
65
75
|
const sets = registeredLogicSets();
|
|
@@ -111,6 +121,7 @@ export class StateMachine extends Component {
|
|
|
111
121
|
this.enter(edge.to);
|
|
112
122
|
}
|
|
113
123
|
this.signals.clear();
|
|
124
|
+
this.reresolveOnFacingChange();
|
|
114
125
|
}
|
|
115
126
|
onCollide(other) {
|
|
116
127
|
// The state that was active when the contact happened owns it: a
|
|
@@ -167,6 +178,8 @@ export class StateMachine extends Component {
|
|
|
167
178
|
if (!sprite)
|
|
168
179
|
return;
|
|
169
180
|
const clip = this.states[state]?.clip ?? state;
|
|
181
|
+
if (this.playDirectionalClip(sprite, clip))
|
|
182
|
+
return;
|
|
170
183
|
if (sprite.clips[clip]) {
|
|
171
184
|
sprite.play(clip);
|
|
172
185
|
}
|
|
@@ -176,4 +189,47 @@ export class StateMachine extends Component {
|
|
|
176
189
|
`keeping "${sprite.current ?? 'none'}"`);
|
|
177
190
|
}
|
|
178
191
|
}
|
|
192
|
+
/**
|
|
193
|
+
* Directional resolution: with an installed contract AND a sibling that
|
|
194
|
+
* reports facing, plays state × facing (mirroring included). Returns false
|
|
195
|
+
* to keep the name-based path — no contract, no facing, no playable clip.
|
|
196
|
+
*/
|
|
197
|
+
playDirectionalClip(sprite, clip) {
|
|
198
|
+
const animation = installedDirectionalAnimation();
|
|
199
|
+
if (!animation)
|
|
200
|
+
return false;
|
|
201
|
+
const provider = this.facingProvider();
|
|
202
|
+
if (!provider)
|
|
203
|
+
return false;
|
|
204
|
+
const facing = provider.getAnimationFacing();
|
|
205
|
+
this.lastFacing = facing;
|
|
206
|
+
const resolved = resolveDirectionalClip(animation, Object.keys(sprite.clips), clip, facing);
|
|
207
|
+
if (!resolved.clip || !sprite.clips[resolved.clip])
|
|
208
|
+
return false;
|
|
209
|
+
sprite.setFlipX(resolved.flip);
|
|
210
|
+
sprite.play(resolved.clip);
|
|
211
|
+
return true;
|
|
212
|
+
}
|
|
213
|
+
facingProvider() {
|
|
214
|
+
return this.entity.components.find((c) => isAnimationFacingProvider(c));
|
|
215
|
+
}
|
|
216
|
+
/**
|
|
217
|
+
* playDirectionalClip only runs at enter() time (via playClip), so a
|
|
218
|
+
* facing change that doesn't cross a state boundary — turning while
|
|
219
|
+
* still walking, say — never re-resolves the clip on its own. Catch that
|
|
220
|
+
* here every frame: same state, new facing, re-run the resolution.
|
|
221
|
+
* AnimatedSprite.play() no-ops on an unchanged clip name, so this is
|
|
222
|
+
* cheap when facing hasn't moved the resolved clip.
|
|
223
|
+
*/
|
|
224
|
+
reresolveOnFacingChange() {
|
|
225
|
+
if (!this.current || !installedDirectionalAnimation())
|
|
226
|
+
return;
|
|
227
|
+
const sprite = this.entity.get(AnimatedSprite);
|
|
228
|
+
if (!sprite)
|
|
229
|
+
return;
|
|
230
|
+
const provider = this.facingProvider();
|
|
231
|
+
if (!provider || provider.getAnimationFacing() === this.lastFacing)
|
|
232
|
+
return;
|
|
233
|
+
this.playDirectionalClip(sprite, this.states[this.current]?.clip ?? this.current);
|
|
234
|
+
}
|
|
179
235
|
}
|