hayao 0.3.0 → 0.4.0
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/README.md +10 -0
- package/bin/create-hayao.mjs +1 -1
- 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 +10 -1
- package/dist/index.js +2341 -328
- 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 +9 -0
- package/dist/render/commands.d.ts +36 -2
- 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/webgl.d.ts +176 -0
- package/dist/scene/iso.d.ts +73 -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/verletChain.d.ts +76 -0
- package/dist/ui/touch.d.ts +51 -0
- package/dist/verify/dom.d.ts +26 -0
- package/dist/world.d.ts +72 -7
- package/docs/API.md +79 -26
- package/docs/CONVENTIONS.md +290 -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
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;
|