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
package/README.md
CHANGED
|
@@ -199,6 +199,16 @@ pathfinding, procgen, synth, juice) for learning one primitive in isolation.
|
|
|
199
199
|
of the engine and what generalized lives in
|
|
200
200
|
[docs/BUILDLOG.md](https://github.com/hellojanpacan/hayao-js/blob/main/docs/BUILDLOG.md).
|
|
201
201
|
|
|
202
|
+
Since v0.3 (on main, toward 0.4.0): a screen-space HUD layer
|
|
203
|
+
(`node.screenSpace`), pause + time scale (`world.paused`/`timeScale`,
|
|
204
|
+
`node.pauseMode`), deterministic generator coroutines, typed `world.state`
|
|
205
|
+
(`defineGame<TState>`), ellipse/arc/lineDash rendering, a ZzFX porting bridge
|
|
206
|
+
(`specFromZzfx`), pointer-button actions, and a built-in hidden-tab ticker —
|
|
207
|
+
driven by the js13k recreation triage in
|
|
208
|
+
[docs/TRIAGE-2026-07-recreations.md](https://github.com/hellojanpacan/hayao-js/blob/main/docs/TRIAGE-2026-07-recreations.md);
|
|
209
|
+
authoring contracts documented in
|
|
210
|
+
[docs/ENGINE.md](https://github.com/hellojanpacan/hayao-js/blob/main/docs/ENGINE.md).
|
|
211
|
+
|
|
202
212
|
## License
|
|
203
213
|
|
|
204
214
|
[MIT](LICENSE) © Jan Pacan
|
package/bin/create-hayao.mjs
CHANGED
|
@@ -0,0 +1,68 @@
|
|
|
1
|
+
import { type ClipDef } from './clip';
|
|
2
|
+
/** A sampled pose: "target/channel" → value. Plain object; order-insensitive. */
|
|
3
|
+
export type Pose = Record<string, number>;
|
|
4
|
+
/** Compose a pose key from a track's target + channel. */
|
|
5
|
+
export declare const poseKey: (target: string, channel: string) => string;
|
|
6
|
+
/**
|
|
7
|
+
* Sample a whole clip into a Pose at NORMALIZED phase `phase` in [0,1) — the
|
|
8
|
+
* blend-space entry point. `phase * duration` is the clip-local time (so two
|
|
9
|
+
* clips of different length stay in step). Pure; allocates one fresh Pose.
|
|
10
|
+
*/
|
|
11
|
+
export declare function samplePose(def: ClipDef, phase: number): Pose;
|
|
12
|
+
/**
|
|
13
|
+
* Linear blend of two poses: `out[k] = lerp(a[k], b[k], t)`. Keys present in only
|
|
14
|
+
* one pose pass through unblended (the other side has no opinion). When `out` is
|
|
15
|
+
* supplied it is reused (cleared then filled) — the allocation-free path
|
|
16
|
+
* ClipPlayer takes during a crossfade. Returns `out`.
|
|
17
|
+
*/
|
|
18
|
+
export declare function mixPose(a: Pose, b: Pose, t: number, out?: Pose): Pose;
|
|
19
|
+
/** One entry of a blend space: a clip pinned at a parameter position. */
|
|
20
|
+
export interface BlendSample {
|
|
21
|
+
clip: ClipDef;
|
|
22
|
+
/** Parameter position of this sample (e.g. speed). Blend1D reads `.x` only. */
|
|
23
|
+
x: number;
|
|
24
|
+
/** Second parameter (Blend2D only). */
|
|
25
|
+
y?: number;
|
|
26
|
+
}
|
|
27
|
+
/**
|
|
28
|
+
* A 1-D blend space (e.g. idle→walk→run along speed). Samples are sorted by `x`
|
|
29
|
+
* on construction; `sample(param, phase)` finds the bracketing pair and mixes
|
|
30
|
+
* them by the normalized distance, all clips sampled at the SAME phase (foot-lock).
|
|
31
|
+
* Below the first / above the last sample it clamps to the end clip.
|
|
32
|
+
*/
|
|
33
|
+
export declare class Blend1D {
|
|
34
|
+
readonly samples: BlendSample[];
|
|
35
|
+
constructor(samples: BlendSample[]);
|
|
36
|
+
/** The clip weights at parameter `param` (sums to 1; empty space → {}). */
|
|
37
|
+
weights(param: number): {
|
|
38
|
+
clip: ClipDef;
|
|
39
|
+
weight: number;
|
|
40
|
+
}[];
|
|
41
|
+
/** Blend the neighbor clips at `param`, all sampled at normalized `phase`. */
|
|
42
|
+
sample(param: number, phase: number): Pose;
|
|
43
|
+
}
|
|
44
|
+
/**
|
|
45
|
+
* A 2-D blend space (e.g. a locomotion pad: forward/back × left/right). No
|
|
46
|
+
* triangulation library — we pick the NEAREST THREE samples to the query point
|
|
47
|
+
* and blend them barycentrically; if the point is outside their triangle the
|
|
48
|
+
* barycentric weights are clamped to >= 0 and renormalized (a documented
|
|
49
|
+
* approximation: no Delaunay, so a sparse/concave set can pick a non-ideal trio).
|
|
50
|
+
*/
|
|
51
|
+
export declare class Blend2D {
|
|
52
|
+
readonly samples: Required<BlendSample>[];
|
|
53
|
+
constructor(samples: BlendSample[]);
|
|
54
|
+
/** Clip weights at (px,py): nearest-3 barycentric, clamped + renormalized. */
|
|
55
|
+
weights(px: number, py: number): {
|
|
56
|
+
clip: ClipDef;
|
|
57
|
+
weight: number;
|
|
58
|
+
}[];
|
|
59
|
+
private barycentric;
|
|
60
|
+
/** Blend the nearest-3 clips at (px,py), all sampled at normalized `phase`. */
|
|
61
|
+
sample(px: number, py: number, phase: number): Pose;
|
|
62
|
+
}
|
|
63
|
+
/**
|
|
64
|
+
* Validate a blend space's samples (levelIssues idiom). Catches empty spaces,
|
|
65
|
+
* duplicate parameter positions (a degenerate bracket / triangle), and non-finite
|
|
66
|
+
* coordinates. `dims` selects which axes must be finite.
|
|
67
|
+
*/
|
|
68
|
+
export declare function blendIssues(samples: BlendSample[], dims?: 1 | 2): string[];
|
|
@@ -0,0 +1,87 @@
|
|
|
1
|
+
import { EASINGS } from '../scene/tween';
|
|
2
|
+
/** Which transform channel a track drives. */
|
|
3
|
+
export type ClipChannel = 'x' | 'y' | 'rotation' | 'scaleX' | 'scaleY' | 'opacity';
|
|
4
|
+
export declare const CLIP_CHANNELS: readonly ClipChannel[];
|
|
5
|
+
/** How the playhead behaves past `duration`. */
|
|
6
|
+
export type ClipLoop = 'loop' | 'once' | 'pingpong';
|
|
7
|
+
/** One keyframe: value `v` at time `t` (seconds), with the ease used to reach it FROM the previous key. */
|
|
8
|
+
export interface Keyframe {
|
|
9
|
+
t: number;
|
|
10
|
+
v: number;
|
|
11
|
+
/** EASINGS name applied over the segment ENDING at this key. Default 'linear'. */
|
|
12
|
+
ease?: keyof typeof EASINGS;
|
|
13
|
+
}
|
|
14
|
+
/** One track: a channel of one target node, driven by an ordered keyframe list. */
|
|
15
|
+
export interface TrackDef {
|
|
16
|
+
/** '/'-separated path from the rig root to the target node (matched by Node.name). */
|
|
17
|
+
target: string;
|
|
18
|
+
channel: ClipChannel;
|
|
19
|
+
/** Keyframes, ascending in `t`. At least one. */
|
|
20
|
+
keys: Keyframe[];
|
|
21
|
+
}
|
|
22
|
+
/** A discrete event fired as the playhead crosses `t` (half-open (t0,t1] window). */
|
|
23
|
+
export interface ClipEvent {
|
|
24
|
+
t: number;
|
|
25
|
+
name: string;
|
|
26
|
+
}
|
|
27
|
+
/** A full clip: duration, loop mode, tracks, optional events. Plain data. */
|
|
28
|
+
export interface ClipDef {
|
|
29
|
+
/** Clip length in seconds (> 0). */
|
|
30
|
+
duration: number;
|
|
31
|
+
loop: ClipLoop;
|
|
32
|
+
tracks: TrackDef[];
|
|
33
|
+
events?: ClipEvent[];
|
|
34
|
+
/** RESERVED — root motion is not implemented (see file header). */
|
|
35
|
+
rootMotion?: never;
|
|
36
|
+
}
|
|
37
|
+
/**
|
|
38
|
+
* Map a raw elapsed time to a clip-local time in [0, duration], honoring the
|
|
39
|
+
* loop mode. `once` clamps at both ends; `loop` wraps; `pingpong` folds so the
|
|
40
|
+
* playhead ping-pongs 0→d→0. Pure and total (guards duration <= 0 → 0).
|
|
41
|
+
*/
|
|
42
|
+
export declare function clipTime(def: ClipDef, elapsed: number): number;
|
|
43
|
+
/** True once a `once` clip has run past its end (used by ClipPlayer to fire `finished`). */
|
|
44
|
+
export declare function clipFinished(def: ClipDef, elapsed: number): boolean;
|
|
45
|
+
/**
|
|
46
|
+
* Binary-search the keyframe segment containing local time `t` and return the
|
|
47
|
+
* eased-interpolated value. Before the first key → first value; after the last →
|
|
48
|
+
* last value (clips are sampled at a clamped clipTime, so this only bites at the
|
|
49
|
+
* exact ends). Allocation-free: no array building, no closures.
|
|
50
|
+
*/
|
|
51
|
+
export declare function sampleTrack(keys: Keyframe[], t: number): number;
|
|
52
|
+
/**
|
|
53
|
+
* A sampled pose channel: which target/channel and the value. `sampleClip`
|
|
54
|
+
* returns one per track. Kept flat (not a nested object) so ClipPlayer can bind
|
|
55
|
+
* each to a resolved node once and apply in O(tracks) with no lookups.
|
|
56
|
+
*/
|
|
57
|
+
export interface SampledChannel {
|
|
58
|
+
target: string;
|
|
59
|
+
channel: ClipChannel;
|
|
60
|
+
value: number;
|
|
61
|
+
}
|
|
62
|
+
/**
|
|
63
|
+
* PURE sample of the whole clip at raw `elapsed` seconds → one SampledChannel per
|
|
64
|
+
* track. Never mutates `def`. Two calls with the same args are deep-equal. This
|
|
65
|
+
* is the reference sampler; ClipPlayer uses a prebound, allocation-free variant
|
|
66
|
+
* of the same math for the hot path.
|
|
67
|
+
*/
|
|
68
|
+
export declare function sampleClip(def: ClipDef, elapsed: number): SampledChannel[];
|
|
69
|
+
/**
|
|
70
|
+
* Events crossed as the playhead advances from raw `prev` to raw `next`, in the
|
|
71
|
+
* order they fire. FIXED SEMANTICS:
|
|
72
|
+
* - half-open window `(t0, t1]` in clip-local time (an event exactly at the new
|
|
73
|
+
* time fires; one exactly at the old time does NOT re-fire);
|
|
74
|
+
* - `loop` wrap emits the TAIL segment (prevLocal, duration] then the HEAD
|
|
75
|
+
* segment (0, nextLocal];
|
|
76
|
+
* - `once` never re-fires an event once the playhead is at/after duration;
|
|
77
|
+
* - order within a segment is ascending by `t`, tail before head on a wrap.
|
|
78
|
+
* Pure; returns event names in fire order.
|
|
79
|
+
*/
|
|
80
|
+
export declare function clipEvents(def: ClipDef, prev: number, next: number): string[];
|
|
81
|
+
/**
|
|
82
|
+
* Validate a clip's DATA, in the levelIssues/tuningIssues idiom: return ALL
|
|
83
|
+
* problems as human-readable strings (empty = clean), never throw. Optionally
|
|
84
|
+
* pass the set of valid target paths (`knownTargets`) to catch tracks that point
|
|
85
|
+
* at a node the rig doesn't contain.
|
|
86
|
+
*/
|
|
87
|
+
export declare function clipIssues(def: ClipDef, knownTargets?: readonly string[]): string[];
|
|
@@ -0,0 +1,40 @@
|
|
|
1
|
+
import type { Vec2 } from '../core/math';
|
|
2
|
+
/** Result of a 2-bone solve: absolute WORLD-space angles (radians) for each joint. */
|
|
3
|
+
export interface TwoBoneResult {
|
|
4
|
+
/** Absolute angle of bone 0 (root→mid). */
|
|
5
|
+
angle0: number;
|
|
6
|
+
/** Absolute angle of bone 1 (mid→end). */
|
|
7
|
+
angle1: number;
|
|
8
|
+
/** True if the target was in range; false → chain clamped to full extension toward it. */
|
|
9
|
+
reachable: boolean;
|
|
10
|
+
}
|
|
11
|
+
/**
|
|
12
|
+
* Analytic two-bone IK. `root` is the base joint (world), `l0`/`l1` the bone
|
|
13
|
+
* lengths, `target` the desired end-effector position (world). `bendDir` is +1
|
|
14
|
+
* or −1 to choose which side the middle joint bends to (elbow up/down). Returns
|
|
15
|
+
* the two ABSOLUTE bone angles (add to nothing — they are world angles; convert
|
|
16
|
+
* to local by subtracting the parent's world angle at the call site).
|
|
17
|
+
*/
|
|
18
|
+
export declare function solveTwoBoneIK(root: Vec2, l0: number, l1: number, target: Vec2, bendDir?: 1 | -1): TwoBoneResult;
|
|
19
|
+
export interface FabrikResult {
|
|
20
|
+
/** Joint positions (world), joints[0] === root, joints[n] === end effector. */
|
|
21
|
+
joints: Vec2[];
|
|
22
|
+
/** Absolute bone angles (radians), one per bone (length joints.length - 1). */
|
|
23
|
+
angles: number[];
|
|
24
|
+
reachable: boolean;
|
|
25
|
+
/** Iterations actually run before convergence (or the cap). */
|
|
26
|
+
iterations: number;
|
|
27
|
+
}
|
|
28
|
+
/**
|
|
29
|
+
* FABRIK N-bone solver. `root` anchors joint 0; `lengths` are the bone lengths
|
|
30
|
+
* (n bones → n+1 joints); `target` is the desired end position. Alternates a
|
|
31
|
+
* backward reach (pull the end to the target, propagate inward) and a forward
|
|
32
|
+
* reach (pin the root, propagate outward) until the end is within `tolerance` of
|
|
33
|
+
* the target or `maxIter` is hit. When the target is unreachable the chain is
|
|
34
|
+
* laid out straight toward it (reachable=false). Pure; deterministic (dmath).
|
|
35
|
+
*/
|
|
36
|
+
export declare function solveFabrik(root: Vec2, lengths: number[], target: Vec2, opts?: {
|
|
37
|
+
maxIter?: number;
|
|
38
|
+
tolerance?: number;
|
|
39
|
+
initial?: Vec2[];
|
|
40
|
+
}): FabrikResult;
|
|
@@ -0,0 +1,64 @@
|
|
|
1
|
+
import { Node, type NodeConfig } from '../scene/node';
|
|
2
|
+
import type { ClipChannel, ClipDef, Keyframe } from './clip';
|
|
3
|
+
export interface Bone2DConfig extends NodeConfig {
|
|
4
|
+
/** Bone reach in local px (root→tip along +x at rotation 0). Default 0. */
|
|
5
|
+
length?: number;
|
|
6
|
+
}
|
|
7
|
+
/**
|
|
8
|
+
* A rig joint: a Node with a `length`. Rotation is the joint angle; children
|
|
9
|
+
* inherit the transform, so a chain of Bone2Ds nested tip-to-root forms a limb.
|
|
10
|
+
* Registered (`Bone2D`) so a serialized rig round-trips — `length` persists.
|
|
11
|
+
*/
|
|
12
|
+
export declare class Bone2D extends Node {
|
|
13
|
+
readonly type = "Bone2D";
|
|
14
|
+
/** Reach in local px along +x. Used by IK, skinning, and the debug overlay. */
|
|
15
|
+
length: number;
|
|
16
|
+
constructor(config?: Bone2DConfig);
|
|
17
|
+
/** Tip position in LOCAL space (root is the node origin; tip is `length` along +x). */
|
|
18
|
+
get tip(): {
|
|
19
|
+
x: number;
|
|
20
|
+
y: number;
|
|
21
|
+
};
|
|
22
|
+
protected serializeProps(): Record<string, unknown>;
|
|
23
|
+
applyProps(props: Record<string, unknown>): void;
|
|
24
|
+
}
|
|
25
|
+
/**
|
|
26
|
+
* An index of a rig subtree: '/'-separated name path → node. Paths are relative
|
|
27
|
+
* to the rig root passed to `buildSkeleton` (the root itself is '' and its direct
|
|
28
|
+
* children are just their names). Ordered iteration only (a plain Map, built by a
|
|
29
|
+
* deterministic DFS).
|
|
30
|
+
*/
|
|
31
|
+
export declare class Skeleton {
|
|
32
|
+
readonly root: Node;
|
|
33
|
+
readonly byPath: Map<string, Node>;
|
|
34
|
+
constructor(root: Node);
|
|
35
|
+
private index;
|
|
36
|
+
/** Resolve a track/IK target path to a node (null if the rig lacks it). */
|
|
37
|
+
resolve(path: string): Node | null;
|
|
38
|
+
/** All target paths in the rig — feed to `clipIssues(def, skeleton.targets())`. */
|
|
39
|
+
targets(): string[];
|
|
40
|
+
/** Only the Bone2D joints, by path (for IK / skinning). */
|
|
41
|
+
bones(): Bone2D[];
|
|
42
|
+
}
|
|
43
|
+
/** Build a skeleton index from a rig root. */
|
|
44
|
+
export declare function buildSkeleton(root: Node): Skeleton;
|
|
45
|
+
/**
|
|
46
|
+
* A track prebound to its target node: the resolved Node, the channel, and the
|
|
47
|
+
* clip's keyframes captured by reference. ClipPlayer holds an array of these and
|
|
48
|
+
* applies each with a single write in the hot path — no path lookup, no find().
|
|
49
|
+
*/
|
|
50
|
+
export interface BoundTrack {
|
|
51
|
+
node: Node;
|
|
52
|
+
channel: ClipChannel;
|
|
53
|
+
keys: Keyframe[];
|
|
54
|
+
/** The track's path, kept for diagnostics / rebind. */
|
|
55
|
+
target: string;
|
|
56
|
+
}
|
|
57
|
+
/**
|
|
58
|
+
* Resolve every track of a clip against a skeleton ONCE. Tracks whose target the
|
|
59
|
+
* rig doesn't contain are dropped (surfaced separately by clipIssues) so the hot
|
|
60
|
+
* path never guards for a null node. Returns bound tracks in clip order.
|
|
61
|
+
*/
|
|
62
|
+
export declare function resolveTracks(def: ClipDef, skeleton: Skeleton): BoundTrack[];
|
|
63
|
+
/** Apply one channel's value to a node's transform (the single hot-path write). */
|
|
64
|
+
export declare function applyChannel(node: Node, channel: ClipChannel, value: number): void;
|
package/dist/app/browser.d.ts
CHANGED
|
@@ -1,10 +1,11 @@
|
|
|
1
1
|
import type { CreateWorldOptions, GameDefinition } from './game';
|
|
2
|
-
import { KeyboardSource, PointerSource } from '../input/source';
|
|
2
|
+
import { KeyboardSource, PointerSource, type InputSource } from '../input/source';
|
|
3
3
|
import type { Vec2 } from '../core/math';
|
|
4
|
-
import type { Renderer } from '../render/renderer';
|
|
4
|
+
import type { Renderer, Viewport } from '../render/renderer';
|
|
5
5
|
import type { World } from '../world';
|
|
6
6
|
export interface RunOptions {
|
|
7
|
-
|
|
7
|
+
/** Rendering backend. Default 'svg'. Use 'webgl' for post-processing effects. */
|
|
8
|
+
renderer?: 'svg' | 'canvas' | 'webgl';
|
|
8
9
|
/** Start the pause/settings shell (Esc). Default true. */
|
|
9
10
|
shell?: boolean;
|
|
10
11
|
onRestart?: () => void;
|
|
@@ -24,6 +25,13 @@ export interface RunOptions {
|
|
|
24
25
|
* steps (no pause overlay — Studio's scrubber holds the sim with this).
|
|
25
26
|
*/
|
|
26
27
|
isHeld?: () => boolean;
|
|
28
|
+
/**
|
|
29
|
+
* Extra input sources (e.g. GamepadSource) sampled each step alongside the
|
|
30
|
+
* built-in KeyboardSource and PointerSource. Each source's sample() is called
|
|
31
|
+
* before world.advance(), and dispose() on stop(). You can also add sources
|
|
32
|
+
* after the handle is returned via GameHandle.addSource().
|
|
33
|
+
*/
|
|
34
|
+
sources?: InputSource[];
|
|
27
35
|
}
|
|
28
36
|
export interface GameHandle {
|
|
29
37
|
world: World;
|
|
@@ -40,10 +48,32 @@ export interface GameHandle {
|
|
|
40
48
|
canvas: HTMLElement | SVGElement | undefined;
|
|
41
49
|
/** Map a pointer event's clientX/Y to design coordinates (undoes the letterbox). */
|
|
42
50
|
toDesign(clientX: number, clientY: number): Vec2;
|
|
51
|
+
/** The drawn (letterboxed) area within the mount — anchor host-drawn UI here. */
|
|
52
|
+
viewport(): Viewport | undefined;
|
|
43
53
|
/** Resolves after preload completes and the first real frame has rendered. */
|
|
44
54
|
ready: Promise<void>;
|
|
45
55
|
/** Run a callback once the game is ready (fires immediately if already ready). */
|
|
46
56
|
onReady(cb: () => void): void;
|
|
57
|
+
/**
|
|
58
|
+
* Register an extra input source (e.g. GamepadSource) to be sampled each step
|
|
59
|
+
* alongside PointerSource. Returns a cleanup function that removes the source
|
|
60
|
+
* (calling its dispose()) when invoked.
|
|
61
|
+
*
|
|
62
|
+
* Typical pattern:
|
|
63
|
+
* const handle = runBrowser(def, mount);
|
|
64
|
+
* const gamepad = new GamepadSource({ keyboard: handle.input });
|
|
65
|
+
* const removeGamepad = handle.addSource(gamepad);
|
|
66
|
+
*/
|
|
67
|
+
addSource(source: InputSource): () => void;
|
|
68
|
+
/**
|
|
69
|
+
* Drive one frame by hand: sample the pointer (and extra sources), advance
|
|
70
|
+
* the world by dtMs with the currently-held actions, and render — exactly
|
|
71
|
+
* what the real loop does per frame. Default dtMs is one fixed step's ms.
|
|
72
|
+
* For headless/tool-driven frame-stepping with visuals updating; the wall-
|
|
73
|
+
* clock loop keeps running independently (pause the shell or use isHeld to
|
|
74
|
+
* make tick() the only driver).
|
|
75
|
+
*/
|
|
76
|
+
tick(dtMs?: number): void;
|
|
47
77
|
stop(): void;
|
|
48
78
|
restart(): void;
|
|
49
79
|
}
|
package/dist/app/game.d.ts
CHANGED
|
@@ -19,7 +19,7 @@ export interface SplashConfig {
|
|
|
19
19
|
/** Keep the splash up at least this long, so a fast preload doesn't flash by. */
|
|
20
20
|
minDurationMs?: number;
|
|
21
21
|
}
|
|
22
|
-
export interface GameDefinition {
|
|
22
|
+
export interface GameDefinition<TState extends Record<string, unknown> = Record<string, unknown>> {
|
|
23
23
|
title: string;
|
|
24
24
|
width?: number;
|
|
25
25
|
height?: number;
|
|
@@ -28,9 +28,9 @@ export interface GameDefinition {
|
|
|
28
28
|
clock?: ClockConfig;
|
|
29
29
|
inputMap?: InputMap;
|
|
30
30
|
/** Build the initial scene tree for a fresh world. */
|
|
31
|
-
build(world: World): Node;
|
|
31
|
+
build(world: World<TState>): Node;
|
|
32
32
|
/** Optional compact probe snapshot for verification (defaults to World.probe). */
|
|
33
|
-
probe?(world: World): Record<string, unknown>;
|
|
33
|
+
probe?(world: World<TState>): Record<string, unknown>;
|
|
34
34
|
/**
|
|
35
35
|
* Live-tunable parameters, declared once. Defaults ARE the config; Studio and
|
|
36
36
|
* tests override them via `createWorld(def, { tuning })` and the sim reads
|
|
@@ -42,18 +42,22 @@ export interface GameDefinition {
|
|
|
42
42
|
* from data (closures do not survive a restore). One contract serves every
|
|
43
43
|
* carryover path: Studio knob changes, variant toggles, HMR, and net rollback.
|
|
44
44
|
*/
|
|
45
|
-
attach?(world: World): void;
|
|
45
|
+
attach?(world: World<TState>): void;
|
|
46
46
|
/**
|
|
47
47
|
* Awaited before the world starts stepping — load fonts, sprite atlases, a
|
|
48
48
|
* SoundFont, anything async. The engine holds a splash on screen until it
|
|
49
49
|
* resolves, so there is no asset pop-in and no ungoverned pre-first-frame window.
|
|
50
50
|
*/
|
|
51
|
-
preload?(world: World): Promise<void>;
|
|
51
|
+
preload?(world: World<TState>): Promise<void>;
|
|
52
52
|
/** Boot splash config, or `false` to start on the first frame with no cover. */
|
|
53
53
|
splash?: SplashConfig | false;
|
|
54
54
|
}
|
|
55
|
-
/**
|
|
56
|
-
|
|
55
|
+
/**
|
|
56
|
+
* Identity + defaults. Kept as a function so games read `export default defineGame({…})`.
|
|
57
|
+
* Pass a state shape for a typed world: `defineGame<{ score: number }>({ … })`
|
|
58
|
+
* types `world.state` in build/probe/attach/preload (default keeps untyped calls as-is).
|
|
59
|
+
*/
|
|
60
|
+
export declare function defineGame<TState extends Record<string, unknown> = Record<string, unknown>>(def: GameDefinition<TState>): Required<Pick<GameDefinition<TState>, 'width' | 'height' | 'seed' | 'inputMap' | 'background'>> & GameDefinition<TState>;
|
|
57
61
|
export interface CreateWorldOptions {
|
|
58
62
|
seed?: number;
|
|
59
63
|
/** Overrides for declared tuning knobs (undeclared keys are dropped). */
|
|
@@ -63,11 +67,11 @@ export interface CreateWorldOptions {
|
|
|
63
67
|
* Build a live, deterministic World from a game definition. No browser needed.
|
|
64
68
|
* `opts` as a bare number is the legacy seed override.
|
|
65
69
|
*/
|
|
66
|
-
export declare function createWorld(def: GameDefinition
|
|
70
|
+
export declare function createWorld<TState extends Record<string, unknown> = Record<string, unknown>>(def: GameDefinition<TState>, opts?: number | CreateWorldOptions): World<TState>;
|
|
67
71
|
/** The input map a game uses (with defaults applied). */
|
|
68
72
|
export declare function gameInputMap(def: GameDefinition): InputMap;
|
|
69
|
-
export interface HeadlessResult {
|
|
70
|
-
world: World
|
|
73
|
+
export interface HeadlessResult<TState extends Record<string, unknown> = Record<string, unknown>> {
|
|
74
|
+
world: World<TState>;
|
|
71
75
|
hash: string;
|
|
72
76
|
steps: number;
|
|
73
77
|
}
|
|
@@ -76,4 +80,4 @@ export interface HeadlessResult {
|
|
|
76
80
|
* steps) and return the final world + state hash. This is what tests, the CI
|
|
77
81
|
* verifier, and replays call. The browser is never involved.
|
|
78
82
|
*/
|
|
79
|
-
export declare function runHeadless(def: GameDefinition
|
|
83
|
+
export declare function runHeadless<TState extends Record<string, unknown> = Record<string, unknown>>(def: GameDefinition<TState>, inputLog?: InputLog): HeadlessResult<TState>;
|
package/dist/audio/audio.d.ts
CHANGED
|
@@ -17,12 +17,29 @@ export interface Tone {
|
|
|
17
17
|
pan?: number;
|
|
18
18
|
}
|
|
19
19
|
export declare class AudioBus {
|
|
20
|
-
private
|
|
20
|
+
private _ctx;
|
|
21
21
|
private master;
|
|
22
22
|
private musicGain;
|
|
23
23
|
private sfxGain;
|
|
24
24
|
private vol;
|
|
25
25
|
private padOn;
|
|
26
|
+
/**
|
|
27
|
+
* The live AudioContext (null until `start()` unlocks audio). External
|
|
28
|
+
* synths being ported in (bundled ZzFX/ZzFXM, hand-rolled Web Audio) should
|
|
29
|
+
* create their sources ON this context — never their own — so they inherit
|
|
30
|
+
* the autoplay unlock the user gesture already paid for.
|
|
31
|
+
*/
|
|
32
|
+
get ctx(): AudioContext | null;
|
|
33
|
+
/**
|
|
34
|
+
* The SFX mix bus (null until `start()`). Connect external one-shot sources
|
|
35
|
+
* here instead of `ctx.destination` so master/sfx volume and mute apply.
|
|
36
|
+
*/
|
|
37
|
+
get sfxBus(): AudioNode | null;
|
|
38
|
+
/**
|
|
39
|
+
* The music mix bus (null until `start()`). Connect external music sources
|
|
40
|
+
* (e.g. a ZzFXM-rendered buffer) here so master/music volume and mute apply.
|
|
41
|
+
*/
|
|
42
|
+
get musicBus(): AudioNode | null;
|
|
26
43
|
get available(): boolean;
|
|
27
44
|
get started(): boolean;
|
|
28
45
|
/** Must be called from a user gesture (browser autoplay policy). */
|
package/dist/audio/synth.d.ts
CHANGED
|
@@ -28,6 +28,13 @@ export interface SoundSpec {
|
|
|
28
28
|
pitchJumpTime?: number;
|
|
29
29
|
vibrato?: number;
|
|
30
30
|
vibratoFreq?: number;
|
|
31
|
+
/**
|
|
32
|
+
* Pitch-envelope repeat period, seconds (ZzFX `repeatTime`). Every `repeat`
|
|
33
|
+
* seconds the slide/slideAccel/pitchJump progression resets to its start
|
|
34
|
+
* while the amplitude envelope keeps going — the classic ZzFX stutter/trill.
|
|
35
|
+
* 0 or absent = off (bit-identical to the pre-`repeat` renderer).
|
|
36
|
+
*/
|
|
37
|
+
repeat?: number;
|
|
31
38
|
/** Second detuned oscillator, cents (0 = off). Adds unison warmth/chorus. */
|
|
32
39
|
detune?: number;
|
|
33
40
|
/** Sub-oscillator: a sine one octave down, mixed at this level 0..1. Adds weight. */
|
|
@@ -0,0 +1,14 @@
|
|
|
1
|
+
import type { SoundSpec } from './synth';
|
|
2
|
+
/** Test hook: clear the warn-once memory. */
|
|
3
|
+
export declare function resetZzfxWarnings(): void;
|
|
4
|
+
/**
|
|
5
|
+
* Convert a positional ZzFX parameter array to a hayao SoundSpec.
|
|
6
|
+
* Argument order (defaults in parens) is ZzFX's `zzfx(...)` signature:
|
|
7
|
+
* volume(1), randomness(.05), frequency(220), attack(0), sustain(0),
|
|
8
|
+
* release(.1), shape(0), shapeCurve(1), slide(0), deltaSlide(0), pitchJump(0),
|
|
9
|
+
* pitchJumpTime(0), repeatTime(0), noise(0), modulation(0), bitCrush(0),
|
|
10
|
+
* delay(0), sustainVolume(1), decay(0), tremolo(0), filter(0).
|
|
11
|
+
* Holes/undefined take the ZzFX defaults, so `[,,1675,,.06,.24,1,1.82,,,837,.06]`
|
|
12
|
+
* pastes straight in.
|
|
13
|
+
*/
|
|
14
|
+
export declare function specFromZzfx(p: readonly (number | undefined)[]): SoundSpec;
|
package/dist/core/clock.d.ts
CHANGED
|
@@ -0,0 +1,36 @@
|
|
|
1
|
+
import type { Vec2 } from './math';
|
|
2
|
+
export interface IsoConfig {
|
|
3
|
+
/** Full width of a tile diamond in screen px (the horizontal diagonal). */
|
|
4
|
+
tileW: number;
|
|
5
|
+
/** Full height of a tile diamond in screen px (the vertical diagonal). 2:1 iso ⇒ tileW/2. */
|
|
6
|
+
tileH: number;
|
|
7
|
+
/** Screen px where grid cell (0,0)'s CENTER lands. Default (0,0). */
|
|
8
|
+
origin?: Vec2;
|
|
9
|
+
/** Screen px a tile rises per unit of elevation. Default tileH (one tile-height per level). */
|
|
10
|
+
elevStep?: number;
|
|
11
|
+
}
|
|
12
|
+
/**
|
|
13
|
+
* A parameterised grid↔screen projection. `toScreen` places a (possibly raised)
|
|
14
|
+
* cell centre; `toGrid` inverts the ground plane (elev 0) to FRACTIONAL grid
|
|
15
|
+
* coordinates — floor them for the containing tile. Round-trips exactly for
|
|
16
|
+
* every integer cell. Covers 2:1 iso (tileH = tileW/2), true dimetric, and
|
|
17
|
+
* top-down/oblique (tileH = tileW) by choice of tile size.
|
|
18
|
+
*/
|
|
19
|
+
export interface IsoProjection {
|
|
20
|
+
readonly tileW: number;
|
|
21
|
+
readonly tileH: number;
|
|
22
|
+
readonly origin: Vec2;
|
|
23
|
+
readonly elevStep: number;
|
|
24
|
+
/** Grid cell → screen-space centre. `elev` shifts the point up by `elevStep` per unit. */
|
|
25
|
+
toScreen(gx: number, gy: number, elev?: number): Vec2;
|
|
26
|
+
/** Screen point → fractional grid coords on the ground plane (floor for the tile). */
|
|
27
|
+
toGrid(sx: number, sy: number): Vec2;
|
|
28
|
+
}
|
|
29
|
+
/**
|
|
30
|
+
* Build an isometric/dimetric/oblique projection. Diamond lattice:
|
|
31
|
+
* screenX = origin.x + (gx - gy) · tileW/2
|
|
32
|
+
* screenY = origin.y + (gx + gy) · tileH/2 − elev · elevStep
|
|
33
|
+
* The inverse solves the 2×2 system directly, so `toGrid(toScreen(g)) === g` for
|
|
34
|
+
* all integer cells (no accumulated float drift).
|
|
35
|
+
*/
|
|
36
|
+
export declare function iso(cfg: IsoConfig): IsoProjection;
|
package/dist/core/rng.d.ts
CHANGED
|
@@ -34,5 +34,14 @@ export declare class Rng {
|
|
|
34
34
|
getState(): RngState;
|
|
35
35
|
setState(state: RngState): void;
|
|
36
36
|
}
|
|
37
|
+
/**
|
|
38
|
+
* Stateless deterministic noise: hash any mix of numbers to a float in [0,1).
|
|
39
|
+
* Same inputs ALWAYS give the same output and no stream is consumed, so it is
|
|
40
|
+
* safe to call from `draw()` for per-entity cosmetic variation (jitter, phase
|
|
41
|
+
* offsets, hue nudges) — `hashNoise(entity.id, ctx.frame)` never touches
|
|
42
|
+
* `world.rng` and cannot desync a replay. Mixes the full float bits of each
|
|
43
|
+
* value through a strong integer hash (murmur3-style avalanche).
|
|
44
|
+
*/
|
|
45
|
+
export declare function hashNoise(...values: number[]): number;
|
|
37
46
|
/** Deterministic 32-bit FNV-1a hash of a string — handy for stable ids/seeds. */
|
|
38
47
|
export declare function hashString(str: string): number;
|