hayao 0.1.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/LICENSE +21 -0
- package/README.md +98 -0
- package/dist/app/browser.d.ts +19 -0
- package/dist/app/game.d.ts +34 -0
- package/dist/art/palette.d.ts +20 -0
- package/dist/art/shapes.d.ts +12 -0
- package/dist/audio/audio.d.ts +42 -0
- package/dist/core/clock.d.ts +37 -0
- package/dist/core/events.d.ts +23 -0
- package/dist/core/hash.d.ts +6 -0
- package/dist/core/math.d.ts +44 -0
- package/dist/core/rng.d.ts +38 -0
- package/dist/index.d.ts +43 -0
- package/dist/index.js +2844 -0
- package/dist/index.js.map +7 -0
- package/dist/input/actions.d.ts +40 -0
- package/dist/input/source.d.ts +25 -0
- package/dist/physics/aabb.d.ts +38 -0
- package/dist/physics/platformer.d.ts +112 -0
- package/dist/physics/raycast.d.ts +22 -0
- package/dist/physics/spatialHash.d.ts +15 -0
- package/dist/physics/tilemap.d.ts +38 -0
- package/dist/render/canvas.d.ts +19 -0
- package/dist/render/commands.d.ts +65 -0
- package/dist/render/headless.d.ts +17 -0
- package/dist/render/renderer.d.ts +16 -0
- package/dist/render/svg.d.ts +16 -0
- package/dist/render/svgString.d.ts +5 -0
- package/dist/scene/node.d.ts +100 -0
- package/dist/scene/nodes.d.ts +90 -0
- package/dist/scene/particles.d.ts +78 -0
- package/dist/scene/registry.d.ts +5 -0
- package/dist/scene/tween.d.ts +20 -0
- package/dist/ui/overlay.d.ts +25 -0
- package/dist/ui/settings.d.ts +23 -0
- package/dist/ui/shell.d.ts +19 -0
- package/dist/verify/bot.d.ts +21 -0
- package/dist/verify/capture.d.ts +21 -0
- package/dist/verify/determinism.d.ts +29 -0
- package/dist/verify/driver.d.ts +23 -0
- package/dist/verify/feel.d.ts +25 -0
- package/dist/verify/filmstrip.d.ts +19 -0
- package/dist/verify/playthrough.d.ts +21 -0
- package/dist/verify/solver.d.ts +37 -0
- package/dist/world.d.ts +75 -0
- package/docs/API.md +226 -0
- package/package.json +44 -0
|
@@ -0,0 +1,25 @@
|
|
|
1
|
+
import { type InputMap } from './actions';
|
|
2
|
+
export declare class KeyboardSource {
|
|
3
|
+
private keysDown;
|
|
4
|
+
/** Virtual action taps (from DOM buttons etc.) pending consumption by a step. */
|
|
5
|
+
private pressed;
|
|
6
|
+
private map;
|
|
7
|
+
private target;
|
|
8
|
+
private onDown;
|
|
9
|
+
private onUp;
|
|
10
|
+
private onBlur;
|
|
11
|
+
constructor(map: InputMap, target?: Document | HTMLElement);
|
|
12
|
+
/** The actions currently held down, as a stable sorted array. */
|
|
13
|
+
currentActions(): string[];
|
|
14
|
+
/**
|
|
15
|
+
* Virtually tap an action (DOM button, touch control). The tap is held until
|
|
16
|
+
* at least one fixed step has sampled it — the driver calls clearPressed()
|
|
17
|
+
* after a successful advance — so UI clicks enter the SAME deterministic
|
|
18
|
+
* input log as keys, and record/replay covers them.
|
|
19
|
+
*/
|
|
20
|
+
press(action: string): void;
|
|
21
|
+
/** Consume pending virtual taps (driver-called after ≥1 step ran). */
|
|
22
|
+
clearPressed(): void;
|
|
23
|
+
setMap(map: InputMap): void;
|
|
24
|
+
dispose(): void;
|
|
25
|
+
}
|
|
@@ -0,0 +1,38 @@
|
|
|
1
|
+
import type { Rect } from '../core/math';
|
|
2
|
+
import { type TilemapData } from './tilemap';
|
|
3
|
+
/** An extra solid the body collides with (moving platform, door, crate…). */
|
|
4
|
+
export interface SolidRect extends Rect {
|
|
5
|
+
/** Solid only when landing on its top (jump-through platform). */
|
|
6
|
+
oneway?: boolean;
|
|
7
|
+
}
|
|
8
|
+
export interface MoveResult {
|
|
9
|
+
x: number;
|
|
10
|
+
y: number;
|
|
11
|
+
hitX: boolean;
|
|
12
|
+
hitY: boolean;
|
|
13
|
+
onFloor: boolean;
|
|
14
|
+
onCeiling: boolean;
|
|
15
|
+
onWallLeft: boolean;
|
|
16
|
+
onWallRight: boolean;
|
|
17
|
+
/** Final rect overlaps a HAZARD tile (with a small forgiveness inset). */
|
|
18
|
+
hazard: boolean;
|
|
19
|
+
/** Index into `solids` of the rect the body stands on, or -1. */
|
|
20
|
+
floorSolid: number;
|
|
21
|
+
}
|
|
22
|
+
export interface MoveOpts {
|
|
23
|
+
solids?: SolidRect[];
|
|
24
|
+
/** Pass true while the player holds "down" to drop through one-way platforms. */
|
|
25
|
+
dropThrough?: boolean;
|
|
26
|
+
}
|
|
27
|
+
/** Would a rect at (x, y) overlap any solid geometry? (Used for corner-correction probes.) */
|
|
28
|
+
export declare function rectBlocked(map: TilemapData, x: number, y: number, w: number, h: number, solids?: SolidRect[]): boolean;
|
|
29
|
+
/**
|
|
30
|
+
* Move a rect by (dx, dy) with axis-separated collision: X first, then Y.
|
|
31
|
+
* One-way surfaces block only downward motion that starts at-or-above their top.
|
|
32
|
+
*/
|
|
33
|
+
export declare function moveRect(map: TilemapData, rect: Rect, dx: number, dy: number, opts?: MoveOpts): MoveResult;
|
|
34
|
+
/** Is there support directly under the rect (within 1px)? */
|
|
35
|
+
export declare function groundAt(map: TilemapData, x: number, y: number, w: number, h: number, solids?: SolidRect[], dropThrough?: boolean): {
|
|
36
|
+
grounded: boolean;
|
|
37
|
+
solid: number;
|
|
38
|
+
};
|
|
@@ -0,0 +1,112 @@
|
|
|
1
|
+
import { type SolidRect } from './aabb';
|
|
2
|
+
import type { TilemapData } from './tilemap';
|
|
3
|
+
export interface PlatformerConfig {
|
|
4
|
+
width: number;
|
|
5
|
+
height: number;
|
|
6
|
+
/** Ground run speed (px/s) and accelerations (px/s²). */
|
|
7
|
+
runSpeed: number;
|
|
8
|
+
groundAccel: number;
|
|
9
|
+
groundFriction: number;
|
|
10
|
+
airAccel: number;
|
|
11
|
+
/**
|
|
12
|
+
* Horizontal decel with no input while airborne (px/s²). Keep LOW: inherited
|
|
13
|
+
* momentum (lift jumps, wall kicks) must survive flight to feel right.
|
|
14
|
+
*/
|
|
15
|
+
airFriction: number;
|
|
16
|
+
/** Gravity (px/s²), terminal fall speed, and the apex modifier. */
|
|
17
|
+
gravity: number;
|
|
18
|
+
maxFall: number;
|
|
19
|
+
/** |vy| below this counts as the jump apex… */
|
|
20
|
+
apexThreshold: number;
|
|
21
|
+
/** …where gravity is multiplied by this while jump is held (the floaty peak). */
|
|
22
|
+
apexGravityMult: number;
|
|
23
|
+
/** Initial jump velocity (px/s, upward) and early-release cut multiplier. */
|
|
24
|
+
jumpVelocity: number;
|
|
25
|
+
jumpCutMult: number;
|
|
26
|
+
/** Grace timers (seconds). */
|
|
27
|
+
coyoteTime: number;
|
|
28
|
+
jumpBuffer: number;
|
|
29
|
+
/** Max px the body is nudged sideways to slip past a ceiling corner when jumping. */
|
|
30
|
+
jumpCornerNudge: number;
|
|
31
|
+
/** Max px the body is nudged vertically to slip past a corner while dashing. */
|
|
32
|
+
dashCornerNudge: number;
|
|
33
|
+
/** Mid-air jumps (0 = none, 1 = double jump…), refilled on landing/wall. */
|
|
34
|
+
airJumps: number;
|
|
35
|
+
/** Air-jump velocity as a fraction of jumpVelocity. */
|
|
36
|
+
airJumpMult: number;
|
|
37
|
+
/** Dash: speed (px/s), duration (s), cooldown (s), charges refilled on landing. */
|
|
38
|
+
dashSpeed: number;
|
|
39
|
+
dashTime: number;
|
|
40
|
+
dashCooldown: number;
|
|
41
|
+
dashCharges: number;
|
|
42
|
+
/** Wall interaction. */
|
|
43
|
+
wallSlideMaxFall: number;
|
|
44
|
+
wallJumpVelX: number;
|
|
45
|
+
wallJumpVelY: number;
|
|
46
|
+
/** Seconds after a wall jump during which input can't cancel the outward velocity. */
|
|
47
|
+
wallJumpLock: number;
|
|
48
|
+
}
|
|
49
|
+
/** Tuned for 32px tiles at 60Hz in the 1280×720 design space. */
|
|
50
|
+
export declare const DEFAULT_PLATFORMER: PlatformerConfig;
|
|
51
|
+
/** Per-step intent, sampled from actions by the game (never raw keys). */
|
|
52
|
+
export interface PadInput {
|
|
53
|
+
moveX: number;
|
|
54
|
+
moveY: number;
|
|
55
|
+
jumpHeld: boolean;
|
|
56
|
+
jumpPressed: boolean;
|
|
57
|
+
dashPressed: boolean;
|
|
58
|
+
}
|
|
59
|
+
export declare const PAD_NEUTRAL: PadInput;
|
|
60
|
+
/** A moving platform: its current rect, plus its velocity this step (px/s). */
|
|
61
|
+
export interface Platform extends SolidRect {
|
|
62
|
+
vx: number;
|
|
63
|
+
vy: number;
|
|
64
|
+
}
|
|
65
|
+
/** Plain serializable controller state — put it in `world.state` so it hashes. */
|
|
66
|
+
export interface PlatformerState {
|
|
67
|
+
x: number;
|
|
68
|
+
y: number;
|
|
69
|
+
vx: number;
|
|
70
|
+
vy: number;
|
|
71
|
+
facing: number;
|
|
72
|
+
onGround: boolean;
|
|
73
|
+
onWall: number;
|
|
74
|
+
coyote: number;
|
|
75
|
+
buffer: number;
|
|
76
|
+
jumping: boolean;
|
|
77
|
+
wallLock: number;
|
|
78
|
+
airJumpsLeft: number;
|
|
79
|
+
dashing: number;
|
|
80
|
+
dashCd: number;
|
|
81
|
+
dashesLeft: number;
|
|
82
|
+
dashVx: number;
|
|
83
|
+
dashVy: number;
|
|
84
|
+
/** Velocity inherited from the platform we stand on (lift momentum storage). */
|
|
85
|
+
carryVx: number;
|
|
86
|
+
carryVy: number;
|
|
87
|
+
dead: boolean;
|
|
88
|
+
}
|
|
89
|
+
export declare function createPlatformerState(x: number, y: number): PlatformerState;
|
|
90
|
+
/** Feel events emitted by a step — hooks for SFX / particles / screen shake. */
|
|
91
|
+
export interface PlatformerEvents {
|
|
92
|
+
jumped: boolean;
|
|
93
|
+
wallJumped: boolean;
|
|
94
|
+
airJumped: boolean;
|
|
95
|
+
dashed: boolean;
|
|
96
|
+
landed: boolean;
|
|
97
|
+
died: boolean;
|
|
98
|
+
}
|
|
99
|
+
/**
|
|
100
|
+
* Advance the controller one fixed step. Mutates `s`; returns feel events.
|
|
101
|
+
* `platforms` should already be at their post-move positions for this step,
|
|
102
|
+
* with `vx/vy` set to their velocity, so the body is carried exactly.
|
|
103
|
+
*/
|
|
104
|
+
export declare function stepPlatformer(s: PlatformerState, input: PadInput, dt: number, map: TilemapData, cfg?: PlatformerConfig, platforms?: Platform[]): PlatformerEvents;
|
|
105
|
+
/** Max rise of a full jump (px). */
|
|
106
|
+
export declare function jumpHeight(cfg?: PlatformerConfig): number;
|
|
107
|
+
/** Full-jump airtime returning to takeoff height (s). */
|
|
108
|
+
export declare function jumpAirtime(cfg?: PlatformerConfig): number;
|
|
109
|
+
/** Max horizontal gap clearable by a running jump (px, flat-to-flat). */
|
|
110
|
+
export declare function jumpDistance(cfg?: PlatformerConfig): number;
|
|
111
|
+
/** Max gap clearable by jump + one apex dash (px, flat-to-flat, horizontal dash). */
|
|
112
|
+
export declare function dashJumpDistance(cfg?: PlatformerConfig): number;
|
|
@@ -0,0 +1,22 @@
|
|
|
1
|
+
import { type TilemapData } from './tilemap';
|
|
2
|
+
export interface RayHit {
|
|
3
|
+
/** True if a solid tile stopped the ray before reaching (x1, y1). */
|
|
4
|
+
blocked: boolean;
|
|
5
|
+
/** Where the ray stopped (the hit point, or the target if unblocked). */
|
|
6
|
+
x: number;
|
|
7
|
+
y: number;
|
|
8
|
+
/** Distance travelled. */
|
|
9
|
+
dist: number;
|
|
10
|
+
}
|
|
11
|
+
/**
|
|
12
|
+
* March a ray from (x0,y0) toward (x1,y1) through the tile grid (DDA — exact,
|
|
13
|
+
* no step-size tuning). SOLID tiles block; everything else passes.
|
|
14
|
+
*/
|
|
15
|
+
export declare function raycastTiles(map: TilemapData, x0: number, y0: number, x1: number, y1: number): RayHit;
|
|
16
|
+
/** Can a see b with no solid tile between? */
|
|
17
|
+
export declare function lineOfSight(map: TilemapData, ax: number, ay: number, bx: number, by: number): boolean;
|
|
18
|
+
/**
|
|
19
|
+
* Is the target inside a vision cone AND visible? (facing is a unit vector;
|
|
20
|
+
* fov is the FULL cone angle in radians.)
|
|
21
|
+
*/
|
|
22
|
+
export declare function inVisionCone(map: TilemapData, ex: number, ey: number, faceX: number, faceY: number, fov: number, range: number, tx: number, ty: number): boolean;
|
|
@@ -0,0 +1,15 @@
|
|
|
1
|
+
import type { Rect } from '../core/math';
|
|
2
|
+
export declare class SpatialHash<T> {
|
|
3
|
+
private cells;
|
|
4
|
+
private bounds;
|
|
5
|
+
private readonly cellSize;
|
|
6
|
+
constructor(cellSize?: number);
|
|
7
|
+
clear(): void;
|
|
8
|
+
get size(): number;
|
|
9
|
+
private key;
|
|
10
|
+
insert(item: T, rect: Rect): void;
|
|
11
|
+
/** All items whose rects overlap the query rect (deduplicated, deterministic order). */
|
|
12
|
+
query(rect: Rect): T[];
|
|
13
|
+
/** All items within `radius` of a point (circle vs rect-center test on bounds). */
|
|
14
|
+
queryCircle(x: number, y: number, radius: number): T[];
|
|
15
|
+
}
|
|
@@ -0,0 +1,38 @@
|
|
|
1
|
+
export declare const TILE: {
|
|
2
|
+
readonly EMPTY: 0;
|
|
3
|
+
readonly SOLID: 1;
|
|
4
|
+
/** Passable from below/sides; solid only when landing on its top edge. */
|
|
5
|
+
readonly ONEWAY: 2;
|
|
6
|
+
readonly HAZARD: 3;
|
|
7
|
+
};
|
|
8
|
+
export type TileId = (typeof TILE)[keyof typeof TILE];
|
|
9
|
+
export interface TilemapData {
|
|
10
|
+
/** Size in tiles. */
|
|
11
|
+
cols: number;
|
|
12
|
+
rows: number;
|
|
13
|
+
/** Tile edge in design-space px. */
|
|
14
|
+
tileSize: number;
|
|
15
|
+
/** Row-major tile ids, length cols*rows. */
|
|
16
|
+
tiles: number[];
|
|
17
|
+
}
|
|
18
|
+
/** Default ASCII vocabulary: `#` solid, `-` one-way, `^` hazard, else empty. */
|
|
19
|
+
export declare const DEFAULT_TILE_CHARS: Record<string, TileId>;
|
|
20
|
+
/**
|
|
21
|
+
* Build a tilemap from ASCII rows. Unknown characters are EMPTY, so level
|
|
22
|
+
* strings can carry entity markers (`@` spawn, `*` shard…) for game code to
|
|
23
|
+
* read separately via `asciiEntities`.
|
|
24
|
+
*/
|
|
25
|
+
export declare function tilemapFromAscii(rowsAscii: string[], tileSize?: number, chars?: Record<string, TileId>): TilemapData;
|
|
26
|
+
/** Extract entity markers (any char not in the tile vocabulary, not space/dot). */
|
|
27
|
+
export declare function asciiEntities(rowsAscii: string[], tileSize?: number, chars?: Record<string, TileId>): {
|
|
28
|
+
char: string;
|
|
29
|
+
tx: number;
|
|
30
|
+
ty: number;
|
|
31
|
+
x: number;
|
|
32
|
+
y: number;
|
|
33
|
+
}[];
|
|
34
|
+
/** Tile id at tile coords; out-of-bounds reads as SOLID (levels are sealed). */
|
|
35
|
+
export declare function tileAt(map: TilemapData, tx: number, ty: number): number;
|
|
36
|
+
export declare function tileAtPoint(map: TilemapData, x: number, y: number): number;
|
|
37
|
+
export declare const mapWidth: (map: TilemapData) => number;
|
|
38
|
+
export declare const mapHeight: (map: TilemapData) => number;
|
|
@@ -0,0 +1,19 @@
|
|
|
1
|
+
import { type DrawCommand } from './commands';
|
|
2
|
+
import type { Renderer, RendererConfig } from './renderer';
|
|
3
|
+
export declare class Canvas2DRenderer implements Renderer {
|
|
4
|
+
readonly width: number;
|
|
5
|
+
readonly height: number;
|
|
6
|
+
private background;
|
|
7
|
+
private canvas;
|
|
8
|
+
private ctx;
|
|
9
|
+
private dpr;
|
|
10
|
+
constructor(config: RendererConfig);
|
|
11
|
+
mount(parent: HTMLElement): void;
|
|
12
|
+
private resize;
|
|
13
|
+
draw(commands: DrawCommand[]): void;
|
|
14
|
+
private stroke;
|
|
15
|
+
private paint;
|
|
16
|
+
private roundRect;
|
|
17
|
+
get element(): HTMLCanvasElement;
|
|
18
|
+
dispose(): void;
|
|
19
|
+
}
|
|
@@ -0,0 +1,65 @@
|
|
|
1
|
+
import type { Transform } from '../core/math';
|
|
2
|
+
export interface Paint {
|
|
3
|
+
fill?: string;
|
|
4
|
+
stroke?: string;
|
|
5
|
+
strokeWidth?: number;
|
|
6
|
+
opacity?: number;
|
|
7
|
+
/** Round line joins/caps for organic shapes. */
|
|
8
|
+
round?: boolean;
|
|
9
|
+
}
|
|
10
|
+
export type TextAlign = 'left' | 'center' | 'right';
|
|
11
|
+
interface Base extends Paint {
|
|
12
|
+
/** World transform (design-space). */
|
|
13
|
+
transform: Transform;
|
|
14
|
+
/** Painter's-order key; ties broken by tree order. Default 0. */
|
|
15
|
+
z: number;
|
|
16
|
+
}
|
|
17
|
+
export interface RectCommand extends Base {
|
|
18
|
+
kind: 'rect';
|
|
19
|
+
x: number;
|
|
20
|
+
y: number;
|
|
21
|
+
w: number;
|
|
22
|
+
h: number;
|
|
23
|
+
/** Corner radius. */
|
|
24
|
+
r?: number;
|
|
25
|
+
}
|
|
26
|
+
export interface CircleCommand extends Base {
|
|
27
|
+
kind: 'circle';
|
|
28
|
+
cx: number;
|
|
29
|
+
cy: number;
|
|
30
|
+
radius: number;
|
|
31
|
+
}
|
|
32
|
+
export interface PolyCommand extends Base {
|
|
33
|
+
kind: 'poly';
|
|
34
|
+
/** Flat [x0,y0,x1,y1,…] in local space. */
|
|
35
|
+
points: number[];
|
|
36
|
+
closed: boolean;
|
|
37
|
+
}
|
|
38
|
+
export interface PathCommand extends Base {
|
|
39
|
+
kind: 'path';
|
|
40
|
+
/** SVG path data in local space. */
|
|
41
|
+
d: string;
|
|
42
|
+
}
|
|
43
|
+
export interface TextCommand extends Base {
|
|
44
|
+
kind: 'text';
|
|
45
|
+
text: string;
|
|
46
|
+
x: number;
|
|
47
|
+
y: number;
|
|
48
|
+
size: number;
|
|
49
|
+
font?: string;
|
|
50
|
+
align?: TextAlign;
|
|
51
|
+
weight?: number;
|
|
52
|
+
}
|
|
53
|
+
export interface ImageCommand extends Base {
|
|
54
|
+
kind: 'image';
|
|
55
|
+
/** data: URI or path. */
|
|
56
|
+
href: string;
|
|
57
|
+
x: number;
|
|
58
|
+
y: number;
|
|
59
|
+
w: number;
|
|
60
|
+
h: number;
|
|
61
|
+
}
|
|
62
|
+
export type DrawCommand = RectCommand | CircleCommand | PolyCommand | PathCommand | TextCommand | ImageCommand;
|
|
63
|
+
/** Stable painter's sort: by z, then original index (tree order) as tiebreak. */
|
|
64
|
+
export declare function sortCommands(cmds: DrawCommand[]): DrawCommand[];
|
|
65
|
+
export {};
|
|
@@ -0,0 +1,17 @@
|
|
|
1
|
+
import type { DrawCommand } from './commands';
|
|
2
|
+
import type { Renderer, RendererConfig } from './renderer';
|
|
3
|
+
export declare class HeadlessRenderer implements Renderer {
|
|
4
|
+
readonly width: number;
|
|
5
|
+
readonly height: number;
|
|
6
|
+
private background;
|
|
7
|
+
private last;
|
|
8
|
+
frameCount: number;
|
|
9
|
+
constructor(config: RendererConfig);
|
|
10
|
+
draw(commands: DrawCommand[]): void;
|
|
11
|
+
/** The most recently drawn display list. */
|
|
12
|
+
get commands(): DrawCommand[];
|
|
13
|
+
/** Count commands of a given kind (handy for assertions). */
|
|
14
|
+
count(kind: DrawCommand['kind']): number;
|
|
15
|
+
/** A vector screenshot of the last frame. */
|
|
16
|
+
toSVGString(): string;
|
|
17
|
+
}
|
|
@@ -0,0 +1,16 @@
|
|
|
1
|
+
import type { DrawCommand } from './commands';
|
|
2
|
+
/** A backend that paints a display list into a host (DOM, canvas, or nothing). */
|
|
3
|
+
export interface Renderer {
|
|
4
|
+
readonly width: number;
|
|
5
|
+
readonly height: number;
|
|
6
|
+
/** Paint one frame. */
|
|
7
|
+
draw(commands: DrawCommand[]): void;
|
|
8
|
+
/** Attach to a DOM parent (browser backends). */
|
|
9
|
+
mount?(parent: HTMLElement): void;
|
|
10
|
+
dispose?(): void;
|
|
11
|
+
}
|
|
12
|
+
export interface RendererConfig {
|
|
13
|
+
width: number;
|
|
14
|
+
height: number;
|
|
15
|
+
background?: string;
|
|
16
|
+
}
|
|
@@ -0,0 +1,16 @@
|
|
|
1
|
+
import type { DrawCommand } from './commands';
|
|
2
|
+
import type { Renderer, RendererConfig } from './renderer';
|
|
3
|
+
export declare class SvgRenderer implements Renderer {
|
|
4
|
+
readonly width: number;
|
|
5
|
+
readonly height: number;
|
|
6
|
+
private background;
|
|
7
|
+
private svg;
|
|
8
|
+
private bg;
|
|
9
|
+
private layer;
|
|
10
|
+
constructor(config: RendererConfig);
|
|
11
|
+
mount(parent: HTMLElement): void;
|
|
12
|
+
draw(commands: DrawCommand[]): void;
|
|
13
|
+
setBackground(color: string): void;
|
|
14
|
+
get element(): SVGSVGElement;
|
|
15
|
+
dispose(): void;
|
|
16
|
+
}
|
|
@@ -0,0 +1,5 @@
|
|
|
1
|
+
import { type DrawCommand } from './commands';
|
|
2
|
+
/** Inner SVG markup for a display list (no wrapping <svg>). */
|
|
3
|
+
export declare function commandsToSVGInner(commands: DrawCommand[]): string;
|
|
4
|
+
/** A complete, standalone SVG document string for a frame — a headless screenshot. */
|
|
5
|
+
export declare function renderToSVGString(commands: DrawCommand[], width: number, height: number, background?: string): string;
|
|
@@ -0,0 +1,100 @@
|
|
|
1
|
+
import type { Rng } from '../core/rng';
|
|
2
|
+
import type { Clock } from '../core/clock';
|
|
3
|
+
import { Signal } from '../core/events';
|
|
4
|
+
import { type Transform, type Vec2 } from '../core/math';
|
|
5
|
+
import type { DrawCommand } from '../render/commands';
|
|
6
|
+
/** What a live node can see of its host world. The World implements this. */
|
|
7
|
+
export interface WorldContext {
|
|
8
|
+
readonly rng: Rng;
|
|
9
|
+
readonly clock: Clock;
|
|
10
|
+
/** Queue a node to be freed at the end of the current step (safe during iteration). */
|
|
11
|
+
requestFree(node: Node): void;
|
|
12
|
+
/** Seconds elapsed in sim time. */
|
|
13
|
+
readonly time: number;
|
|
14
|
+
}
|
|
15
|
+
/** A composable update unit attached to a node (favour over deep inheritance). */
|
|
16
|
+
export interface Behavior {
|
|
17
|
+
ready?(node: Node): void;
|
|
18
|
+
update?(node: Node, dt: number): void;
|
|
19
|
+
exit?(node: Node): void;
|
|
20
|
+
/** Optional tag for lookup/serialization. */
|
|
21
|
+
readonly kind?: string;
|
|
22
|
+
}
|
|
23
|
+
/** Deterministic id source — reset by the World on load so ids are reproducible. */
|
|
24
|
+
export declare function resetNodeIds(start?: number): void;
|
|
25
|
+
export interface NodeConfig {
|
|
26
|
+
name?: string;
|
|
27
|
+
pos?: Vec2;
|
|
28
|
+
rotation?: number;
|
|
29
|
+
scale?: Vec2;
|
|
30
|
+
z?: number;
|
|
31
|
+
visible?: boolean;
|
|
32
|
+
}
|
|
33
|
+
export declare class Node {
|
|
34
|
+
readonly id: string;
|
|
35
|
+
name: string;
|
|
36
|
+
/** Type tag for serialization; subclasses override. */
|
|
37
|
+
readonly type: string;
|
|
38
|
+
pos: Vec2;
|
|
39
|
+
rotation: number;
|
|
40
|
+
scale: Vec2;
|
|
41
|
+
z: number;
|
|
42
|
+
visible: boolean;
|
|
43
|
+
/**
|
|
44
|
+
* Cosmetic nodes are pure *view* (derived from game state, rebuildable) and are
|
|
45
|
+
* excluded from serialize()/snapshot()/hash(). Mark a container cosmetic when it
|
|
46
|
+
* only renders state that lives elsewhere — so transient display (move counters,
|
|
47
|
+
* particles, tweened positions) never pollutes the canonical, verifiable state.
|
|
48
|
+
*/
|
|
49
|
+
cosmetic: boolean;
|
|
50
|
+
parent: Node | null;
|
|
51
|
+
readonly children: Node[];
|
|
52
|
+
world: WorldContext | null;
|
|
53
|
+
/** Optional inline update without subclassing: node.onUpdate = (n, dt) => {…}. */
|
|
54
|
+
onUpdate?: (node: Node, dt: number) => void;
|
|
55
|
+
private behaviors;
|
|
56
|
+
private signals;
|
|
57
|
+
private _ready;
|
|
58
|
+
private _freed;
|
|
59
|
+
constructor(config?: NodeConfig);
|
|
60
|
+
addChild<T extends Node>(child: T): T;
|
|
61
|
+
removeChild(child: Node): void;
|
|
62
|
+
/** Deferred free: runs exit hooks and detaches at the end of the step. */
|
|
63
|
+
free(): void;
|
|
64
|
+
/** Depth-first find by name. */
|
|
65
|
+
find(name: string): Node | null;
|
|
66
|
+
/** All descendants (and self) of a given type tag. */
|
|
67
|
+
query(type: string, out?: Node[]): Node[];
|
|
68
|
+
addBehavior(b: Behavior): this;
|
|
69
|
+
signal<T = void>(name: string): Signal<T>;
|
|
70
|
+
emit<T>(name: string, payload: T): void;
|
|
71
|
+
enterTree(world: WorldContext): void;
|
|
72
|
+
updateTree(dt: number): void;
|
|
73
|
+
exitTree(): void;
|
|
74
|
+
get isFreed(): boolean;
|
|
75
|
+
localTransform(): Transform;
|
|
76
|
+
worldTransform(): Transform;
|
|
77
|
+
/** Walk the subtree, appending draw commands with computed world transforms. */
|
|
78
|
+
collectDraw(out: DrawCommand[], parentWorld?: Transform): void;
|
|
79
|
+
/** Subclasses emit their own commands here. Base draws nothing. */
|
|
80
|
+
protected draw(_out: DrawCommand[], _world: Transform): void;
|
|
81
|
+
protected onReady(): void;
|
|
82
|
+
protected onProcess(_dt: number): void;
|
|
83
|
+
protected onExit(): void;
|
|
84
|
+
serialize(): SerializedNode;
|
|
85
|
+
/** Subclasses persist their own fields here. */
|
|
86
|
+
protected serializeProps(): Record<string, unknown>;
|
|
87
|
+
/** Subclasses restore their own fields here. */
|
|
88
|
+
applyProps(_props: Record<string, unknown>): void;
|
|
89
|
+
}
|
|
90
|
+
export interface SerializedNode {
|
|
91
|
+
type: string;
|
|
92
|
+
name: string;
|
|
93
|
+
pos: Vec2;
|
|
94
|
+
rotation: number;
|
|
95
|
+
scale: Vec2;
|
|
96
|
+
z: number;
|
|
97
|
+
visible: boolean;
|
|
98
|
+
props: Record<string, unknown>;
|
|
99
|
+
children: SerializedNode[];
|
|
100
|
+
}
|
|
@@ -0,0 +1,90 @@
|
|
|
1
|
+
import type { Transform } from '../core/math';
|
|
2
|
+
import type { DrawCommand, Paint, TextAlign } from '../render/commands';
|
|
3
|
+
import { Node, type NodeConfig } from './node';
|
|
4
|
+
export type Shape = {
|
|
5
|
+
kind: 'rect';
|
|
6
|
+
w: number;
|
|
7
|
+
h: number;
|
|
8
|
+
r?: number;
|
|
9
|
+
} | {
|
|
10
|
+
kind: 'circle';
|
|
11
|
+
radius: number;
|
|
12
|
+
} | {
|
|
13
|
+
kind: 'poly';
|
|
14
|
+
points: number[];
|
|
15
|
+
closed?: boolean;
|
|
16
|
+
} | {
|
|
17
|
+
kind: 'path';
|
|
18
|
+
d: string;
|
|
19
|
+
} | {
|
|
20
|
+
kind: 'glyph';
|
|
21
|
+
char: string;
|
|
22
|
+
size: number;
|
|
23
|
+
};
|
|
24
|
+
export interface SpriteConfig extends NodeConfig, Paint {
|
|
25
|
+
shape: Shape;
|
|
26
|
+
}
|
|
27
|
+
export declare class Sprite extends Node {
|
|
28
|
+
readonly type = "Sprite";
|
|
29
|
+
shape: Shape;
|
|
30
|
+
paint: Paint;
|
|
31
|
+
constructor(config: SpriteConfig);
|
|
32
|
+
protected draw(out: DrawCommand[], world: Transform): void;
|
|
33
|
+
protected serializeProps(): Record<string, unknown>;
|
|
34
|
+
applyProps(props: Record<string, unknown>): void;
|
|
35
|
+
}
|
|
36
|
+
export interface TextConfig extends NodeConfig, Paint {
|
|
37
|
+
text: string;
|
|
38
|
+
size?: number;
|
|
39
|
+
font?: string;
|
|
40
|
+
align?: TextAlign;
|
|
41
|
+
weight?: number;
|
|
42
|
+
}
|
|
43
|
+
export declare class Text extends Node {
|
|
44
|
+
readonly type = "Text";
|
|
45
|
+
text: string;
|
|
46
|
+
size: number;
|
|
47
|
+
font?: string;
|
|
48
|
+
align: TextAlign;
|
|
49
|
+
weight?: number;
|
|
50
|
+
paint: Paint;
|
|
51
|
+
constructor(config: TextConfig);
|
|
52
|
+
protected draw(out: DrawCommand[], world: Transform): void;
|
|
53
|
+
protected serializeProps(): Record<string, unknown>;
|
|
54
|
+
applyProps(props: Record<string, unknown>): void;
|
|
55
|
+
}
|
|
56
|
+
export interface CameraConfig extends NodeConfig {
|
|
57
|
+
zoom?: number;
|
|
58
|
+
/** Make this the active camera on ready. */
|
|
59
|
+
current?: boolean;
|
|
60
|
+
}
|
|
61
|
+
export declare class Camera2D extends Node {
|
|
62
|
+
readonly type = "Camera2D";
|
|
63
|
+
zoom: number;
|
|
64
|
+
current: boolean;
|
|
65
|
+
constructor(config?: CameraConfig);
|
|
66
|
+
protected onReady(): void;
|
|
67
|
+
protected serializeProps(): Record<string, unknown>;
|
|
68
|
+
applyProps(props: Record<string, unknown>): void;
|
|
69
|
+
}
|
|
70
|
+
export interface TimerConfig extends NodeConfig {
|
|
71
|
+
duration: number;
|
|
72
|
+
autostart?: boolean;
|
|
73
|
+
oneShot?: boolean;
|
|
74
|
+
}
|
|
75
|
+
export declare class Timer extends Node {
|
|
76
|
+
readonly type = "Timer";
|
|
77
|
+
duration: number;
|
|
78
|
+
oneShot: boolean;
|
|
79
|
+
private remaining;
|
|
80
|
+
private running;
|
|
81
|
+
constructor(config: TimerConfig);
|
|
82
|
+
start(duration?: number): void;
|
|
83
|
+
stop(): void;
|
|
84
|
+
get timeLeft(): number;
|
|
85
|
+
protected onProcess(dt: number): void;
|
|
86
|
+
/** Signal emitted when the countdown reaches zero. */
|
|
87
|
+
get timeout(): import("..").Signal<void>;
|
|
88
|
+
protected serializeProps(): Record<string, unknown>;
|
|
89
|
+
applyProps(props: Record<string, unknown>): void;
|
|
90
|
+
}
|
|
@@ -0,0 +1,78 @@
|
|
|
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 ParticleStyle {
|
|
5
|
+
/** Colors drawn from at random per particle. */
|
|
6
|
+
colors: string[];
|
|
7
|
+
/** Particle radius range (px). */
|
|
8
|
+
sizeMin: number;
|
|
9
|
+
sizeMax: number;
|
|
10
|
+
/** Initial speed range (px/s). */
|
|
11
|
+
speedMin: number;
|
|
12
|
+
speedMax: number;
|
|
13
|
+
/** Emission arc: center angle + spread (radians). Default: full circle. */
|
|
14
|
+
angle?: number;
|
|
15
|
+
spread?: number;
|
|
16
|
+
/** Lifetime range (s). */
|
|
17
|
+
lifeMin: number;
|
|
18
|
+
lifeMax: number;
|
|
19
|
+
/** Gravity applied to particles (px/s², +y is down). */
|
|
20
|
+
gravity?: number;
|
|
21
|
+
/** Velocity damping per second (0 = none, 5 = strong). */
|
|
22
|
+
drag?: number;
|
|
23
|
+
/** Shrink to zero over life (default true; also fades opacity). */
|
|
24
|
+
shrink?: boolean;
|
|
25
|
+
}
|
|
26
|
+
/**
|
|
27
|
+
* A pooled particle emitter node. Call `burst(n, at, style)` from game code in
|
|
28
|
+
* response to feel events. Always cosmetic; safe to add anywhere in the tree.
|
|
29
|
+
*/
|
|
30
|
+
export declare class Particles extends Node {
|
|
31
|
+
readonly type: string;
|
|
32
|
+
private pool;
|
|
33
|
+
private rng;
|
|
34
|
+
/** Cap on live particles (oldest are recycled first). */
|
|
35
|
+
maxParticles: number;
|
|
36
|
+
constructor(config?: NodeConfig & {
|
|
37
|
+
seed?: number;
|
|
38
|
+
maxParticles?: number;
|
|
39
|
+
});
|
|
40
|
+
/** Emit a burst at a position (in this node's local space). */
|
|
41
|
+
burst(count: number, at: Vec2, style: ParticleStyle): void;
|
|
42
|
+
private gravity;
|
|
43
|
+
private drag;
|
|
44
|
+
private shrink;
|
|
45
|
+
protected onProcess(dt: number): void;
|
|
46
|
+
protected draw(out: DrawCommand[], world: Transform): void;
|
|
47
|
+
get liveCount(): number;
|
|
48
|
+
}
|
|
49
|
+
/** Ready-made styles for the common feel moments. */
|
|
50
|
+
export declare const PARTICLE_PRESETS: {
|
|
51
|
+
readonly dust: (colors?: string[]) => ParticleStyle;
|
|
52
|
+
readonly burst: (colors?: string[]) => ParticleStyle;
|
|
53
|
+
readonly hit: (colors?: string[]) => ParticleStyle;
|
|
54
|
+
readonly sparkle: (colors?: string[]) => ParticleStyle;
|
|
55
|
+
};
|
|
56
|
+
/**
|
|
57
|
+
* Screen shake as a cosmetic camera offset with its own Rng and exponential
|
|
58
|
+
* decay. Attach to whatever node the camera follows and read `offset` into the
|
|
59
|
+
* camera's position AFTER canonical follow logic — or simpler, add the shaker
|
|
60
|
+
* as the camera's parent so the offset composes for free.
|
|
61
|
+
*/
|
|
62
|
+
export declare class Shaker extends Node {
|
|
63
|
+
readonly type: string;
|
|
64
|
+
private rng;
|
|
65
|
+
private trauma;
|
|
66
|
+
/** Max offset in px at full trauma. */
|
|
67
|
+
amplitude: number;
|
|
68
|
+
/** Trauma decay per second. */
|
|
69
|
+
decay: number;
|
|
70
|
+
constructor(config?: NodeConfig & {
|
|
71
|
+
seed?: number;
|
|
72
|
+
amplitude?: number;
|
|
73
|
+
decay?: number;
|
|
74
|
+
});
|
|
75
|
+
/** Add shake (0..1). Stacks, clamped to 1. Quadratic falloff feels right. */
|
|
76
|
+
addTrauma(amount: number): void;
|
|
77
|
+
protected onProcess(dt: number): void;
|
|
78
|
+
}
|
|
@@ -0,0 +1,5 @@
|
|
|
1
|
+
import { Node, type SerializedNode } from './node';
|
|
2
|
+
export type NodeFactory = () => Node;
|
|
3
|
+
export declare function registerNode(type: string, factory: NodeFactory): void;
|
|
4
|
+
/** Rebuild a live node tree from serialized data. */
|
|
5
|
+
export declare function deserializeNode(data: SerializedNode): Node;
|