hayao 0.3.0 → 0.4.1
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/README.md +10 -0
- package/bin/create-hayao.mjs +1 -1
- package/dist/anim/blend.d.ts +68 -0
- package/dist/anim/clip.d.ts +87 -0
- package/dist/anim/ik.d.ts +40 -0
- package/dist/anim/skeleton.d.ts +64 -0
- package/dist/app/browser.d.ts +33 -3
- package/dist/app/game.d.ts +15 -11
- package/dist/audio/audio.d.ts +18 -1
- package/dist/audio/synth.d.ts +7 -0
- package/dist/audio/zzfx.d.ts +14 -0
- package/dist/core/clock.d.ts +1 -1
- package/dist/core/projection.d.ts +36 -0
- package/dist/core/rng.d.ts +9 -0
- package/dist/hayao.global.js +190 -5
- package/dist/index.d.ts +20 -1
- package/dist/index.js +4159 -979
- package/dist/index.js.map +4 -4
- package/dist/index.min.js +190 -5
- package/dist/input/actions.d.ts +60 -6
- package/dist/input/gamepad.d.ts +101 -0
- package/dist/input/source.d.ts +85 -4
- package/dist/logic/coroutine.d.ts +68 -0
- package/dist/render/canvas.d.ts +3 -7
- package/dist/render/canvas2d-core.d.ts +13 -0
- package/dist/render/commands.d.ts +55 -2
- package/dist/render/lightRun.d.ts +35 -0
- package/dist/render/paint.d.ts +8 -0
- package/dist/render/renderer.d.ts +25 -0
- package/dist/render/svg.d.ts +2 -1
- package/dist/render/svgString.d.ts +8 -3
- package/dist/render/webgl.d.ts +176 -0
- package/dist/scene/clipPlayer.d.ts +58 -0
- package/dist/scene/ikTarget.d.ts +25 -0
- package/dist/scene/iso.d.ts +73 -0
- package/dist/scene/light.d.ts +80 -0
- package/dist/scene/node.d.ts +81 -2
- package/dist/scene/nodes.d.ts +34 -0
- package/dist/scene/particles.d.ts +19 -0
- package/dist/scene/shadow2d.d.ts +25 -0
- package/dist/scene/skeletonDebug.d.ts +28 -0
- package/dist/scene/verletChain.d.ts +76 -0
- package/dist/ui/touch.d.ts +51 -0
- package/dist/verify/dom.d.ts +26 -0
- package/dist/verify/gates.d.ts +25 -0
- package/dist/world.d.ts +72 -7
- package/docs/API.md +138 -27
- package/docs/CONVENTIONS.md +346 -2
- package/docs/EMBED.md +5 -0
- package/docs/QUICKSTART.md +13 -1
- package/docs/VERIFICATION.md +34 -4
- package/package.json +2 -1
|
@@ -0,0 +1,28 @@
|
|
|
1
|
+
import { type Transform } from '../core/math';
|
|
2
|
+
import type { DrawCommand } from '../render/commands';
|
|
3
|
+
import { Node, type NodeConfig } from './node';
|
|
4
|
+
export interface SkeletonDebugConfig extends NodeConfig {
|
|
5
|
+
/** The rig root to visualize. Defaults to the node's parent. */
|
|
6
|
+
rig?: Node;
|
|
7
|
+
/** Bone segment color. Default '#e0483b'. */
|
|
8
|
+
boneColor?: string;
|
|
9
|
+
/** Joint pivot color. Default '#f4d35e'. */
|
|
10
|
+
pivotColor?: string;
|
|
11
|
+
/** Joint circle radius, px. Default 3. */
|
|
12
|
+
pivotRadius?: number;
|
|
13
|
+
/** Bone stroke width, px. Default 2. */
|
|
14
|
+
strokeWidth?: number;
|
|
15
|
+
}
|
|
16
|
+
/** Draws a rig's bones + pivots as transient overlay commands. Cosmetic. */
|
|
17
|
+
export declare class SkeletonDebug extends Node {
|
|
18
|
+
readonly type = "SkeletonDebug";
|
|
19
|
+
rig: Node | null;
|
|
20
|
+
boneColor: string;
|
|
21
|
+
pivotColor: string;
|
|
22
|
+
pivotRadius: number;
|
|
23
|
+
strokeWidth: number;
|
|
24
|
+
constructor(config?: SkeletonDebugConfig);
|
|
25
|
+
protected onReady(): void;
|
|
26
|
+
protected draw(out: DrawCommand[], _world: Transform): void;
|
|
27
|
+
private emitFor;
|
|
28
|
+
}
|
|
@@ -0,0 +1,76 @@
|
|
|
1
|
+
import { type Transform, type Vec2 } from '../core/math';
|
|
2
|
+
import type { DrawCommand } from '../render/commands';
|
|
3
|
+
import { Node, type NodeConfig } from './node';
|
|
4
|
+
export interface VerletChainConfig extends NodeConfig {
|
|
5
|
+
/** Number of segments (points = segments + 1). */
|
|
6
|
+
segments: number;
|
|
7
|
+
/** Total rest length of the chain, px. */
|
|
8
|
+
length: number;
|
|
9
|
+
/** Acceleration applied to free points, px/s² in WORLD space (+y is down). */
|
|
10
|
+
gravity?: Vec2;
|
|
11
|
+
/** Per-step velocity retention 0..1 (1 = undamped). Default 0.985. */
|
|
12
|
+
damping?: number;
|
|
13
|
+
/** Constraint-relaxation passes per step. Default 3. */
|
|
14
|
+
iterations?: number;
|
|
15
|
+
/** Pin the tail too (set `tailTarget` for where). Default false. */
|
|
16
|
+
pinTail?: boolean;
|
|
17
|
+
/** Stroke color for draw(). Default '#333'. */
|
|
18
|
+
stroke?: string;
|
|
19
|
+
/** Stroke width at the head, px. Default 3. */
|
|
20
|
+
strokeWidth?: number;
|
|
21
|
+
/** Taper the stroke toward the tail (draws per-segment polys). Default false. */
|
|
22
|
+
taper?: boolean;
|
|
23
|
+
}
|
|
24
|
+
/**
|
|
25
|
+
* The rope node. Read `points` (node-local) to attach child sprites along the
|
|
26
|
+
* chain, `segmentAngle(i)` to orient them, and call `impulse(v)` to flick it.
|
|
27
|
+
*/
|
|
28
|
+
export declare class VerletChain extends Node {
|
|
29
|
+
readonly type: string;
|
|
30
|
+
gravity: Vec2;
|
|
31
|
+
damping: number;
|
|
32
|
+
iterations: number;
|
|
33
|
+
pinTail: boolean;
|
|
34
|
+
stroke: string;
|
|
35
|
+
strokeWidth: number;
|
|
36
|
+
taper: boolean;
|
|
37
|
+
/**
|
|
38
|
+
* Where the tail is pinned when `pinTail` is true, in NODE-LOCAL space
|
|
39
|
+
* (same space as `points`). null leaves the tail free even with pinTail set.
|
|
40
|
+
*/
|
|
41
|
+
tailTarget: Vec2 | null;
|
|
42
|
+
readonly segments: number;
|
|
43
|
+
/** Rest length of one segment. */
|
|
44
|
+
readonly segmentLength: number;
|
|
45
|
+
private px;
|
|
46
|
+
private py;
|
|
47
|
+
private prevx;
|
|
48
|
+
private prevy;
|
|
49
|
+
/** Node-local mirror of the sim points, updated after each step. */
|
|
50
|
+
private readonly local;
|
|
51
|
+
private initialized;
|
|
52
|
+
private impX;
|
|
53
|
+
private impY;
|
|
54
|
+
constructor(config: VerletChainConfig);
|
|
55
|
+
/**
|
|
56
|
+
* Chain points in NODE-LOCAL space, head first (`points[0]` ≈ {0,0}).
|
|
57
|
+
* The Vec2 objects are reused across steps — read, don't retain copies-by-ref.
|
|
58
|
+
*/
|
|
59
|
+
get points(): readonly Vec2[];
|
|
60
|
+
/**
|
|
61
|
+
* Direction of segment i (0..segments-1) in node-local space, radians
|
|
62
|
+
* (0 = +x, y-down screen convention). Map child sprites to segments with
|
|
63
|
+
* `sprite.pos = chain.points[i]; sprite.rotation = chain.segmentAngle(i)`.
|
|
64
|
+
*/
|
|
65
|
+
segmentAngle(i: number): number;
|
|
66
|
+
/** Flick the chain: add a velocity (world px/s) to every free point next step. */
|
|
67
|
+
impulse(v: Vec2): void;
|
|
68
|
+
protected onProcess(dt: number): void;
|
|
69
|
+
/**
|
|
70
|
+
* Stroked open poly of the chain. `points` are node-local and `world` is the
|
|
71
|
+
* self-inclusive world transform handed to draw(), so commands emit with it
|
|
72
|
+
* directly. With `taper`, each segment is its own 2-point poly at a linearly
|
|
73
|
+
* decreasing width (segments are few, so this stays cheap); otherwise one poly.
|
|
74
|
+
*/
|
|
75
|
+
protected draw(out: DrawCommand[], world: Transform): void;
|
|
76
|
+
}
|
|
@@ -0,0 +1,51 @@
|
|
|
1
|
+
import type { GameHandle } from '../app/browser';
|
|
2
|
+
/** A virtual stick. Give it 4 directional actions, and/or an analog axis prefix. */
|
|
3
|
+
export interface TouchStick {
|
|
4
|
+
/** Directional actions held while tilted past the deadzone. Array = [up,down,left,right]. */
|
|
5
|
+
dirs?: [string, string, string, string] | {
|
|
6
|
+
up: string;
|
|
7
|
+
down: string;
|
|
8
|
+
left: string;
|
|
9
|
+
right: string;
|
|
10
|
+
};
|
|
11
|
+
/**
|
|
12
|
+
* Emit live analog axes `${prefix}x`, `${prefix}y` (−1..1) and `${prefix}angle`
|
|
13
|
+
* (radians), quantized to `buckets`. NOTE: these are LIVE axes (feel), not part
|
|
14
|
+
* of the input log — for replay-exact analog aim, thread them through
|
|
15
|
+
* `world.step(actions, axes)` (see docs/CONVENTIONS.md §Pointer).
|
|
16
|
+
*/
|
|
17
|
+
prefix?: string;
|
|
18
|
+
/** Axis quantization levels (default 32). */
|
|
19
|
+
buckets?: number;
|
|
20
|
+
/** Dead-zone as a fraction of the stick radius, 0..1 (default 0.28). */
|
|
21
|
+
deadzone?: number;
|
|
22
|
+
}
|
|
23
|
+
export interface TouchButton {
|
|
24
|
+
action: string;
|
|
25
|
+
/** Glyph/label drawn on the button (default the action name). */
|
|
26
|
+
label?: string;
|
|
27
|
+
/** Hold the action while pressed (default true); false = one-shot tap via press(). */
|
|
28
|
+
hold?: boolean;
|
|
29
|
+
}
|
|
30
|
+
export interface TouchControlsLayout {
|
|
31
|
+
/** Left-corner stick (movement). Array shorthand → [up,down,left,right]. */
|
|
32
|
+
left?: TouchStick | [string, string, string, string];
|
|
33
|
+
/** Right-corner stick (aim). */
|
|
34
|
+
right?: TouchStick | [string, string, string, string];
|
|
35
|
+
/** Action buttons, stacked at the lower-right. */
|
|
36
|
+
buttons?: TouchButton[];
|
|
37
|
+
/** Only mount when the device has a coarse (touch) pointer (default true). */
|
|
38
|
+
touchOnly?: boolean;
|
|
39
|
+
}
|
|
40
|
+
export declare class TouchControls {
|
|
41
|
+
private handle;
|
|
42
|
+
private root;
|
|
43
|
+
private disposers;
|
|
44
|
+
/** Every action this control set can hold — released together on dispose/reset. */
|
|
45
|
+
private owned;
|
|
46
|
+
constructor(handle: GameHandle, layout: TouchControlsLayout);
|
|
47
|
+
private addStick;
|
|
48
|
+
private addButtons;
|
|
49
|
+
private hold;
|
|
50
|
+
dispose(): void;
|
|
51
|
+
}
|
|
@@ -0,0 +1,26 @@
|
|
|
1
|
+
import { type GameHandle } from '../app/browser';
|
|
2
|
+
import type { GameDefinition } from '../app/game';
|
|
3
|
+
export interface DomHarness {
|
|
4
|
+
/** The live game handle (world, input, pointer, renderer, viewport…). */
|
|
5
|
+
handle: GameHandle;
|
|
6
|
+
/** Press a finger/pointer down at design (x,y) with a stable id (default 1). */
|
|
7
|
+
touchDown(x: number, y: number, id?: number): void;
|
|
8
|
+
/** Move a tracked finger/pointer to design (x,y). */
|
|
9
|
+
touchMove(x: number, y: number, id?: number): void;
|
|
10
|
+
/** Lift a finger/pointer (default id 1). */
|
|
11
|
+
touchUp(id?: number): void;
|
|
12
|
+
/**
|
|
13
|
+
* Sample the live pointer + keyboard input into the sim, then run `n` fixed
|
|
14
|
+
* steps — the same order `runBrowser` uses, minus wall-clock. Held/virtual
|
|
15
|
+
* actions and quantized axes flow through exactly as in production.
|
|
16
|
+
*/
|
|
17
|
+
step(n?: number, axes?: Record<string, number>): void;
|
|
18
|
+
dispose(): void;
|
|
19
|
+
}
|
|
20
|
+
/**
|
|
21
|
+
* Boot a game's real host wiring under the DOM for a host-layer test. The loop is
|
|
22
|
+
* frozen (`isHeld`) so stepping is deterministic and test-driven — call `step()`
|
|
23
|
+
* after firing input. Attach a `TouchControls` to `harness.handle` to prove a
|
|
24
|
+
* virtual gamepad end-to-end.
|
|
25
|
+
*/
|
|
26
|
+
export declare function bootDom(def: GameDefinition, mount?: HTMLElement): DomHarness;
|
package/dist/verify/gates.d.ts
CHANGED
|
@@ -92,6 +92,23 @@ export interface TelegraphFrame {
|
|
|
92
92
|
* Feed a per-frame timeline (one entry per sim frame) for a single threat.
|
|
93
93
|
*/
|
|
94
94
|
export declare function telegraphIssues(timeline: readonly TelegraphFrame[], minFrames: number, label?: string): string[];
|
|
95
|
+
export interface LightingOptions {
|
|
96
|
+
/**
|
|
97
|
+
* Minimum effective ambient level (0 = pitch black, 1 = fully lit) the avatar
|
|
98
|
+
* must sit in — either from the ambient base or a pool falling on it (default
|
|
99
|
+
* 0.35). Below this the avatar reads as a silhouette in shadow.
|
|
100
|
+
*/
|
|
101
|
+
minLitLevel?: number;
|
|
102
|
+
}
|
|
103
|
+
/**
|
|
104
|
+
* Lit-readability gate. Given the rendered display list (which must include the
|
|
105
|
+
* LightLayer's run) and the avatar's WORLD position + fill, compute the light
|
|
106
|
+
* level reaching the avatar: the ambient base, lifted toward 1 by any pool whose
|
|
107
|
+
* radius covers it (scaled by that pool's peak brightness). If the avatar ends up
|
|
108
|
+
* below `minLitLevel`, it is a silhouette lost in the dark — flagged. When the
|
|
109
|
+
* frame carries no parseable light run, there is nothing to darken: pass (empty).
|
|
110
|
+
*/
|
|
111
|
+
export declare function lightingIssues(commands: DrawCommand[], avatarFill: string, avatarWorld: Vec2, opts?: LightingOptions): string[];
|
|
95
112
|
export interface CameraOptions {
|
|
96
113
|
/** Fixed timestep between samples in seconds (default 1/60). */
|
|
97
114
|
dt?: number;
|
|
@@ -127,6 +144,12 @@ export interface FeelSpec {
|
|
|
127
144
|
};
|
|
128
145
|
/** True for scrolling games — enables the camera gate (needs sampled positions). */
|
|
129
146
|
scrolls?: boolean;
|
|
147
|
+
/**
|
|
148
|
+
* True for games with a LightLayer — enables the lit-readability gate (needs a
|
|
149
|
+
* rendered frame carrying the light run + the avatar's world position). Uses
|
|
150
|
+
* `avatarFill` for the color.
|
|
151
|
+
*/
|
|
152
|
+
lit?: boolean;
|
|
130
153
|
}
|
|
131
154
|
/** Runtime inputs the audit/verify computes and hands to the aggregator. */
|
|
132
155
|
export interface FeelContext {
|
|
@@ -140,6 +163,8 @@ export interface FeelContext {
|
|
|
140
163
|
dt?: number;
|
|
141
164
|
/** Background fallback if the spec omits one. */
|
|
142
165
|
background?: string;
|
|
166
|
+
/** The avatar's WORLD position in the rendered frame — enables the lit-readability gate. */
|
|
167
|
+
avatarWorld?: Vec2;
|
|
143
168
|
}
|
|
144
169
|
export interface FeelReport {
|
|
145
170
|
ok: boolean;
|
package/dist/world.d.ts
CHANGED
|
@@ -1,7 +1,7 @@
|
|
|
1
1
|
import { Clock, type ClockConfig } from './core/clock';
|
|
2
2
|
import { EventBus } from './core/events';
|
|
3
3
|
import { Rng } from './core/rng';
|
|
4
|
-
import { InputState } from './input/actions';
|
|
4
|
+
import { InputState, type AxisFrame } from './input/actions';
|
|
5
5
|
import { Node, type WorldContext } from './scene/node';
|
|
6
6
|
import type { Camera2D } from './scene/nodes';
|
|
7
7
|
import { type Transform, type Vec2 } from './core/math';
|
|
@@ -14,12 +14,18 @@ export interface WorldConfig {
|
|
|
14
14
|
height?: number;
|
|
15
15
|
/** Resolved tuning values (see app/tuning.ts). Sim state: hashed + snapshotted. */
|
|
16
16
|
tuning?: Record<string, number | string>;
|
|
17
|
+
/**
|
|
18
|
+
* Dev guard: during each step(), `Math.random`/`Date.now` are wrapped so a
|
|
19
|
+
* stray call inside the sim warns ONCE (with a stack hint) instead of
|
|
20
|
+
* silently breaking determinism. Never throws; safe to leave on in tests.
|
|
21
|
+
*/
|
|
22
|
+
guardDeterminism?: boolean;
|
|
17
23
|
}
|
|
18
24
|
/** Default engine event map; games extend it with their own keys. */
|
|
19
25
|
export interface CoreEvents {
|
|
20
26
|
[key: string]: unknown;
|
|
21
27
|
}
|
|
22
|
-
export declare class World implements WorldContext {
|
|
28
|
+
export declare class World<TState extends Record<string, unknown> = Record<string, unknown>> implements WorldContext {
|
|
23
29
|
readonly rng: Rng;
|
|
24
30
|
readonly clock: Clock;
|
|
25
31
|
readonly input: InputState;
|
|
@@ -31,15 +37,32 @@ export declare class World implements WorldContext {
|
|
|
31
37
|
* `hash()` and `snapshot()`, so hidden state here cannot escape determinism
|
|
32
38
|
* checks the way ad-hoc module variables can.
|
|
33
39
|
*/
|
|
34
|
-
state:
|
|
40
|
+
state: TState;
|
|
35
41
|
readonly width: number;
|
|
36
42
|
readonly height: number;
|
|
43
|
+
/**
|
|
44
|
+
* Pause switch. While true, only `pauseMode: 'always'` subtrees update —
|
|
45
|
+
* rendering, input sampling, and the clock keep ticking, so a pause menu
|
|
46
|
+
* (an 'always' subtree) stays live. Sim state: hashed + snapshotted, but
|
|
47
|
+
* only when true, so pinned hashes of unpaused games are unchanged.
|
|
48
|
+
*/
|
|
49
|
+
paused: boolean;
|
|
50
|
+
/**
|
|
51
|
+
* Sim-time multiplier: the tree receives `clock.dt * timeScale` (slow-mo /
|
|
52
|
+
* fast-forward), while `'always'` subtrees get the unscaled dt so UI keeps
|
|
53
|
+
* realtime feel. The clock itself ticks normally. Hashed/snapshotted only
|
|
54
|
+
* when ≠ 1.
|
|
55
|
+
*/
|
|
56
|
+
timeScale: number;
|
|
37
57
|
root: Node;
|
|
38
58
|
activeCamera: Camera2D | null;
|
|
39
59
|
private seed;
|
|
40
60
|
private freeQueue;
|
|
41
61
|
private started;
|
|
42
62
|
private tuningValues;
|
|
63
|
+
private guardDeterminism;
|
|
64
|
+
private warnedClamp;
|
|
65
|
+
private warnedNondet;
|
|
43
66
|
constructor(config?: WorldConfig);
|
|
44
67
|
/**
|
|
45
68
|
* Read a tuning value resolved at world creation (declared default unless
|
|
@@ -47,6 +70,12 @@ export declare class World implements WorldContext {
|
|
|
47
70
|
* silently read `undefined` into the sim.
|
|
48
71
|
*/
|
|
49
72
|
tune<T extends number | string = number>(key: string): T;
|
|
73
|
+
/**
|
|
74
|
+
* Read a shared resource by key. Throws on a missing key (listing what IS
|
|
75
|
+
* available) — a typo here would otherwise silently read `undefined` into
|
|
76
|
+
* the sim, exactly like an undeclared tuning knob.
|
|
77
|
+
*/
|
|
78
|
+
resource<T>(key: string): T;
|
|
50
79
|
get time(): number;
|
|
51
80
|
get frame(): number;
|
|
52
81
|
/** Replace the scene root, entering the tree. */
|
|
@@ -57,7 +86,15 @@ export declare class World implements WorldContext {
|
|
|
57
86
|
* Advance exactly one fixed step with the given actions held down.
|
|
58
87
|
* This is THE deterministic transition — call it from Node or the browser loop.
|
|
59
88
|
*/
|
|
60
|
-
step(actionsDown?: Iterable<string
|
|
89
|
+
step(actionsDown?: Iterable<string>, axes?: AxisFrame): void;
|
|
90
|
+
/**
|
|
91
|
+
* Wrap `Math.random`/`Date.now` for the duration of a step so a stray call
|
|
92
|
+
* inside the sim warns ONCE per world (with a stack hint) — the values still
|
|
93
|
+
* flow through, so nothing breaks mid-frame. Returns the restore function;
|
|
94
|
+
* properly nested re-entry (a world stepped inside another's step) is safe
|
|
95
|
+
* because each install restores exactly what it replaced.
|
|
96
|
+
*/
|
|
97
|
+
private installGuard;
|
|
61
98
|
/**
|
|
62
99
|
* Feed REAL elapsed ms; runs 0+ fixed steps. Returns steps run. This is the
|
|
63
100
|
* realtime driver entry point — it CLAMPS realMs to the clock's maxFrameMs
|
|
@@ -65,14 +102,38 @@ export declare class World implements WorldContext {
|
|
|
65
102
|
* not 72. For headless tests/harnesses that want to fast-forward an exact
|
|
66
103
|
* number of steps, use `runSteps(n)` or `step()` — never `advance` with a big ms.
|
|
67
104
|
*/
|
|
68
|
-
advance(realMs: number, actionsDown?: Iterable<string
|
|
105
|
+
advance(realMs: number, actionsDown?: Iterable<string>, axes?: AxisFrame): number;
|
|
69
106
|
/**
|
|
70
107
|
* Run exactly `n` fixed steps — the deterministic fast-forward for tests and
|
|
71
108
|
* headless drivers (unlike `advance`, no realtime clamp). Pass `actionsFor(i)`
|
|
72
109
|
* to script the input per step; omit it to hold nothing down.
|
|
73
110
|
*/
|
|
74
|
-
runSteps(n: number, actionsFor?: (i: number) => Iterable<string
|
|
75
|
-
|
|
111
|
+
runSteps(n: number, actionsFor?: (i: number) => Iterable<string>, axesFor?: (i: number) => AxisFrame | undefined): void;
|
|
112
|
+
/**
|
|
113
|
+
* Run pending frees NOW instead of at the end of the step. `free()` is
|
|
114
|
+
* deferred (safe during iteration), which means freed nodes survive until
|
|
115
|
+
* the step ends — during a scene swap that lets old nodes contaminate the
|
|
116
|
+
* first frame of the new scene. Pattern: `oldRoot.free(); world.flushFree();
|
|
117
|
+
* buildNewScene()`. Called automatically at the end of every step.
|
|
118
|
+
*/
|
|
119
|
+
flushFree(): void;
|
|
120
|
+
/** Visit every live node depth-first (root first, child-index order). */
|
|
121
|
+
walk(fn: (node: Node) => void): void;
|
|
122
|
+
/** Total live nodes in the tree (root included). */
|
|
123
|
+
get nodeCount(): number;
|
|
124
|
+
/**
|
|
125
|
+
* One indented line per node — `name (type) [flags] @x,y` — for quick tree
|
|
126
|
+
* audits in a REPL or test failure message.
|
|
127
|
+
*/
|
|
128
|
+
debugTree(): string;
|
|
129
|
+
/** The active camera's world position + zoom, or null (see WorldContext.camera). */
|
|
130
|
+
camera(): {
|
|
131
|
+
pos: {
|
|
132
|
+
x: number;
|
|
133
|
+
y: number;
|
|
134
|
+
};
|
|
135
|
+
zoom: number;
|
|
136
|
+
} | null;
|
|
76
137
|
/** The view transform (inverse of the active camera), mapping world → screen. */
|
|
77
138
|
viewTransform(): Transform;
|
|
78
139
|
/**
|
|
@@ -108,4 +169,8 @@ export interface WorldSnapshot {
|
|
|
108
169
|
tree: ReturnType<Node['serialize']>;
|
|
109
170
|
/** Resolved tuning values (absent in pre-tuning saves). */
|
|
110
171
|
tuning?: Record<string, number | string>;
|
|
172
|
+
/** Pause flag (absent when false — keeps old snapshots byte-identical). */
|
|
173
|
+
paused?: boolean;
|
|
174
|
+
/** Sim-time multiplier (absent when 1). */
|
|
175
|
+
timeScale?: number;
|
|
111
176
|
}
|