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.
Files changed (52) hide show
  1. package/README.md +10 -0
  2. package/bin/create-hayao.mjs +1 -1
  3. package/dist/anim/blend.d.ts +68 -0
  4. package/dist/anim/clip.d.ts +87 -0
  5. package/dist/anim/ik.d.ts +40 -0
  6. package/dist/anim/skeleton.d.ts +64 -0
  7. package/dist/app/browser.d.ts +33 -3
  8. package/dist/app/game.d.ts +15 -11
  9. package/dist/audio/audio.d.ts +18 -1
  10. package/dist/audio/synth.d.ts +7 -0
  11. package/dist/audio/zzfx.d.ts +14 -0
  12. package/dist/core/clock.d.ts +1 -1
  13. package/dist/core/projection.d.ts +36 -0
  14. package/dist/core/rng.d.ts +9 -0
  15. package/dist/hayao.global.js +190 -5
  16. package/dist/index.d.ts +20 -1
  17. package/dist/index.js +4159 -979
  18. package/dist/index.js.map +4 -4
  19. package/dist/index.min.js +190 -5
  20. package/dist/input/actions.d.ts +60 -6
  21. package/dist/input/gamepad.d.ts +101 -0
  22. package/dist/input/source.d.ts +85 -4
  23. package/dist/logic/coroutine.d.ts +68 -0
  24. package/dist/render/canvas.d.ts +3 -7
  25. package/dist/render/canvas2d-core.d.ts +13 -0
  26. package/dist/render/commands.d.ts +55 -2
  27. package/dist/render/lightRun.d.ts +35 -0
  28. package/dist/render/paint.d.ts +8 -0
  29. package/dist/render/renderer.d.ts +25 -0
  30. package/dist/render/svg.d.ts +2 -1
  31. package/dist/render/svgString.d.ts +8 -3
  32. package/dist/render/webgl.d.ts +176 -0
  33. package/dist/scene/clipPlayer.d.ts +58 -0
  34. package/dist/scene/ikTarget.d.ts +25 -0
  35. package/dist/scene/iso.d.ts +73 -0
  36. package/dist/scene/light.d.ts +80 -0
  37. package/dist/scene/node.d.ts +81 -2
  38. package/dist/scene/nodes.d.ts +34 -0
  39. package/dist/scene/particles.d.ts +19 -0
  40. package/dist/scene/shadow2d.d.ts +25 -0
  41. package/dist/scene/skeletonDebug.d.ts +28 -0
  42. package/dist/scene/verletChain.d.ts +76 -0
  43. package/dist/ui/touch.d.ts +51 -0
  44. package/dist/verify/dom.d.ts +26 -0
  45. package/dist/verify/gates.d.ts +25 -0
  46. package/dist/world.d.ts +72 -7
  47. package/docs/API.md +138 -27
  48. package/docs/CONVENTIONS.md +346 -2
  49. package/docs/EMBED.md +5 -0
  50. package/docs/QUICKSTART.md +13 -1
  51. package/docs/VERIFICATION.md +34 -4
  52. 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;
@@ -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: Record<string, unknown>;
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>): void;
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>): number;
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>): void;
75
- private flushFree;
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
  }