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.
Files changed (47) hide show
  1. package/LICENSE +21 -0
  2. package/README.md +98 -0
  3. package/dist/app/browser.d.ts +19 -0
  4. package/dist/app/game.d.ts +34 -0
  5. package/dist/art/palette.d.ts +20 -0
  6. package/dist/art/shapes.d.ts +12 -0
  7. package/dist/audio/audio.d.ts +42 -0
  8. package/dist/core/clock.d.ts +37 -0
  9. package/dist/core/events.d.ts +23 -0
  10. package/dist/core/hash.d.ts +6 -0
  11. package/dist/core/math.d.ts +44 -0
  12. package/dist/core/rng.d.ts +38 -0
  13. package/dist/index.d.ts +43 -0
  14. package/dist/index.js +2844 -0
  15. package/dist/index.js.map +7 -0
  16. package/dist/input/actions.d.ts +40 -0
  17. package/dist/input/source.d.ts +25 -0
  18. package/dist/physics/aabb.d.ts +38 -0
  19. package/dist/physics/platformer.d.ts +112 -0
  20. package/dist/physics/raycast.d.ts +22 -0
  21. package/dist/physics/spatialHash.d.ts +15 -0
  22. package/dist/physics/tilemap.d.ts +38 -0
  23. package/dist/render/canvas.d.ts +19 -0
  24. package/dist/render/commands.d.ts +65 -0
  25. package/dist/render/headless.d.ts +17 -0
  26. package/dist/render/renderer.d.ts +16 -0
  27. package/dist/render/svg.d.ts +16 -0
  28. package/dist/render/svgString.d.ts +5 -0
  29. package/dist/scene/node.d.ts +100 -0
  30. package/dist/scene/nodes.d.ts +90 -0
  31. package/dist/scene/particles.d.ts +78 -0
  32. package/dist/scene/registry.d.ts +5 -0
  33. package/dist/scene/tween.d.ts +20 -0
  34. package/dist/ui/overlay.d.ts +25 -0
  35. package/dist/ui/settings.d.ts +23 -0
  36. package/dist/ui/shell.d.ts +19 -0
  37. package/dist/verify/bot.d.ts +21 -0
  38. package/dist/verify/capture.d.ts +21 -0
  39. package/dist/verify/determinism.d.ts +29 -0
  40. package/dist/verify/driver.d.ts +23 -0
  41. package/dist/verify/feel.d.ts +25 -0
  42. package/dist/verify/filmstrip.d.ts +19 -0
  43. package/dist/verify/playthrough.d.ts +21 -0
  44. package/dist/verify/solver.d.ts +37 -0
  45. package/dist/world.d.ts +75 -0
  46. package/docs/API.md +226 -0
  47. package/package.json +44 -0
package/dist/index.js ADDED
@@ -0,0 +1,2844 @@
1
+ // src/core/math.ts
2
+ var vec2 = (x = 0, y = 0) => ({ x, y });
3
+ var vadd = (a, b) => ({ x: a.x + b.x, y: a.y + b.y });
4
+ var vsub = (a, b) => ({ x: a.x - b.x, y: a.y - b.y });
5
+ var vscale = (a, s) => ({ x: a.x * s, y: a.y * s });
6
+ var vdot = (a, b) => a.x * b.x + a.y * b.y;
7
+ var vlen = (a) => Math.hypot(a.x, a.y);
8
+ var vdist = (a, b) => Math.hypot(a.x - b.x, a.y - b.y);
9
+ function vnorm(a) {
10
+ const l = vlen(a);
11
+ return l === 0 ? { x: 0, y: 0 } : { x: a.x / l, y: a.y / l };
12
+ }
13
+ var clamp = (v, lo, hi) => v < lo ? lo : v > hi ? hi : v;
14
+ var lerp = (a, b, t) => a + (b - a) * t;
15
+ var invLerp = (a, b, v) => a === b ? 0 : (v - a) / (b - a);
16
+ var remap = (v, a0, a1, b0, b1) => lerp(b0, b1, invLerp(a0, a1, v));
17
+ var TAU = Math.PI * 2;
18
+ var deg2rad = (d) => d * Math.PI / 180;
19
+ var rad2deg = (r) => r * 180 / Math.PI;
20
+ function rectContains(r, p) {
21
+ return p.x >= r.x && p.x <= r.x + r.w && p.y >= r.y && p.y <= r.y + r.h;
22
+ }
23
+ function rectsOverlap(a, b) {
24
+ return a.x < b.x + b.w && a.x + a.w > b.x && a.y < b.y + b.h && a.y + a.h > b.y;
25
+ }
26
+ var IDENTITY = { a: 1, b: 0, c: 0, d: 1, e: 0, f: 0 };
27
+ function composeTransform(m, n2) {
28
+ return {
29
+ a: m.a * n2.a + m.c * n2.b,
30
+ b: m.b * n2.a + m.d * n2.b,
31
+ c: m.a * n2.c + m.c * n2.d,
32
+ d: m.b * n2.c + m.d * n2.d,
33
+ e: m.a * n2.e + m.c * n2.f + m.e,
34
+ f: m.b * n2.e + m.d * n2.f + m.f
35
+ };
36
+ }
37
+ function makeTransform(pos, rotation, scale) {
38
+ const cos = Math.cos(rotation);
39
+ const sin = Math.sin(rotation);
40
+ return {
41
+ a: cos * scale.x,
42
+ b: sin * scale.x,
43
+ c: -sin * scale.y,
44
+ d: cos * scale.y,
45
+ e: pos.x,
46
+ f: pos.y
47
+ };
48
+ }
49
+ function applyTransform(m, p) {
50
+ return { x: m.a * p.x + m.c * p.y + m.e, y: m.b * p.x + m.d * p.y + m.f };
51
+ }
52
+ function invertTransform(m) {
53
+ const det = m.a * m.d - m.b * m.c;
54
+ if (det === 0) return { ...IDENTITY };
55
+ const id = 1 / det;
56
+ return {
57
+ a: m.d * id,
58
+ b: -m.b * id,
59
+ c: -m.c * id,
60
+ d: m.a * id,
61
+ e: (m.c * m.f - m.d * m.e) * id,
62
+ f: (m.b * m.e - m.a * m.f) * id
63
+ };
64
+ }
65
+
66
+ // src/core/rng.ts
67
+ function splitmix32(seed) {
68
+ let a = seed >>> 0;
69
+ return () => {
70
+ a = a + 2654435769 >>> 0;
71
+ let t = a;
72
+ t = Math.imul(t ^ t >>> 16, 569420461);
73
+ t = Math.imul(t ^ t >>> 15, 1935289751);
74
+ return (t ^ t >>> 15) >>> 0;
75
+ };
76
+ }
77
+ var Rng = class _Rng {
78
+ s0;
79
+ s1;
80
+ s2;
81
+ s3;
82
+ constructor(seed = 0) {
83
+ if (typeof seed === "number") {
84
+ const sm = splitmix32(seed === 0 ? 305441741 : seed);
85
+ this.s0 = sm();
86
+ this.s1 = sm();
87
+ this.s2 = sm();
88
+ this.s3 = sm();
89
+ } else {
90
+ [this.s0, this.s1, this.s2, this.s3] = seed.s;
91
+ }
92
+ }
93
+ /** Raw 32-bit unsigned integer. */
94
+ next() {
95
+ const result = Math.imul(this.rotl(Math.imul(this.s1, 5), 7), 9) >>> 0;
96
+ const t = this.s1 << 9 >>> 0;
97
+ this.s2 ^= this.s0;
98
+ this.s3 ^= this.s1;
99
+ this.s1 ^= this.s2;
100
+ this.s0 ^= this.s3;
101
+ this.s2 = (this.s2 ^ t) >>> 0;
102
+ this.s3 = this.rotl(this.s3, 11);
103
+ return result >>> 0;
104
+ }
105
+ rotl(x, k) {
106
+ return (x << k | x >>> 32 - k) >>> 0;
107
+ }
108
+ /** Float in [0, 1). */
109
+ float() {
110
+ return this.next() / 4294967296;
111
+ }
112
+ /** Float in [lo, hi). */
113
+ range(lo, hi) {
114
+ return lo + this.float() * (hi - lo);
115
+ }
116
+ /** Integer in [0, n). */
117
+ int(n2) {
118
+ return Math.floor(this.float() * n2);
119
+ }
120
+ /** Integer in [lo, hi] inclusive. */
121
+ intRange(lo, hi) {
122
+ return lo + this.int(hi - lo + 1);
123
+ }
124
+ /** True with probability p. */
125
+ chance(p) {
126
+ return this.float() < p;
127
+ }
128
+ /** Uniform element of an array. */
129
+ pick(arr) {
130
+ return arr[this.int(arr.length)];
131
+ }
132
+ /** In-place Fisher–Yates shuffle (deterministic). */
133
+ shuffle(arr) {
134
+ for (let i = arr.length - 1; i > 0; i--) {
135
+ const j = this.int(i + 1);
136
+ [arr[i], arr[j]] = [arr[j], arr[i]];
137
+ }
138
+ return arr;
139
+ }
140
+ /**
141
+ * Derive an independent child stream. Same parent state + same key always
142
+ * yields the same child — so subsystems (enemy AI, loot, terrain) get stable,
143
+ * decorrelated randomness without threading one generator through everything.
144
+ */
145
+ split(key = 0) {
146
+ const mix2 = (this.s0 ^ Math.imul(key + 1, 2654435769)) >>> 0;
147
+ const child = new _Rng(mix2 ^ this.s3);
148
+ this.next();
149
+ return child;
150
+ }
151
+ /** Serialize / restore for saves and replay. */
152
+ getState() {
153
+ return { s: [this.s0, this.s1, this.s2, this.s3] };
154
+ }
155
+ setState(state) {
156
+ [this.s0, this.s1, this.s2, this.s3] = state.s;
157
+ }
158
+ };
159
+ function hashString(str) {
160
+ let h = 2166136261 >>> 0;
161
+ for (let i = 0; i < str.length; i++) {
162
+ h ^= str.charCodeAt(i);
163
+ h = Math.imul(h, 16777619);
164
+ }
165
+ return h >>> 0;
166
+ }
167
+
168
+ // src/core/clock.ts
169
+ var Clock = class {
170
+ stepMs;
171
+ dt;
172
+ // fixed delta in seconds, for game logic
173
+ maxFrameMs;
174
+ accumulator = 0;
175
+ _frame = 0;
176
+ _simTimeMs = 0;
177
+ constructor(config = {}) {
178
+ const hz = config.hz ?? 60;
179
+ this.stepMs = 1e3 / hz;
180
+ this.dt = 1 / hz;
181
+ this.maxFrameMs = config.maxFrameMs ?? 250;
182
+ }
183
+ /**
184
+ * Feed real elapsed ms; returns how many fixed steps to run now.
185
+ * Caller runs the sim exactly that many times.
186
+ */
187
+ advance(realMs) {
188
+ this.accumulator += Math.min(realMs, this.maxFrameMs);
189
+ let steps = 0;
190
+ while (this.accumulator >= this.stepMs) {
191
+ this.accumulator -= this.stepMs;
192
+ steps++;
193
+ }
194
+ return steps;
195
+ }
196
+ /** Called by the World once per executed step to keep counters honest. */
197
+ tick() {
198
+ this._frame++;
199
+ this._simTimeMs += this.stepMs;
200
+ }
201
+ /** Interpolation alpha in [0,1) for smooth rendering between fixed steps. */
202
+ get alpha() {
203
+ return this.accumulator / this.stepMs;
204
+ }
205
+ get frame() {
206
+ return this._frame;
207
+ }
208
+ get simTimeMs() {
209
+ return this._simTimeMs;
210
+ }
211
+ get simTimeSec() {
212
+ return this._simTimeMs / 1e3;
213
+ }
214
+ getState() {
215
+ return { accumulator: this.accumulator, frame: this._frame, simTimeMs: this._simTimeMs };
216
+ }
217
+ setState(s) {
218
+ this.accumulator = s.accumulator;
219
+ this._frame = s.frame;
220
+ this._simTimeMs = s.simTimeMs;
221
+ }
222
+ };
223
+
224
+ // src/core/events.ts
225
+ var Signal = class {
226
+ listeners = [];
227
+ connect(fn) {
228
+ this.listeners.push(fn);
229
+ return () => this.disconnect(fn);
230
+ }
231
+ once(fn) {
232
+ const off = this.connect((p) => {
233
+ off();
234
+ fn(p);
235
+ });
236
+ return off;
237
+ }
238
+ disconnect(fn) {
239
+ const i = this.listeners.indexOf(fn);
240
+ if (i >= 0) this.listeners.splice(i, 1);
241
+ }
242
+ emit(payload) {
243
+ for (const fn of this.listeners.slice()) fn(payload);
244
+ }
245
+ get count() {
246
+ return this.listeners.length;
247
+ }
248
+ clear() {
249
+ this.listeners.length = 0;
250
+ }
251
+ };
252
+ var EventBus = class {
253
+ signals = /* @__PURE__ */ new Map();
254
+ signalFor(key) {
255
+ let s = this.signals.get(key);
256
+ if (!s) {
257
+ s = new Signal();
258
+ this.signals.set(key, s);
259
+ }
260
+ return s;
261
+ }
262
+ on(key, fn) {
263
+ return this.signalFor(key).connect(fn);
264
+ }
265
+ once(key, fn) {
266
+ return this.signalFor(key).once(fn);
267
+ }
268
+ emit(key, payload) {
269
+ this.signalFor(key).emit(payload);
270
+ }
271
+ clear() {
272
+ this.signals.clear();
273
+ }
274
+ };
275
+
276
+ // src/core/hash.ts
277
+ function canonicalNumber(n2) {
278
+ if (Number.isNaN(n2)) return "NaN";
279
+ if (n2 === Infinity) return "Inf";
280
+ if (n2 === -Infinity) return "-Inf";
281
+ if (n2 === 0) return "0";
282
+ return n2.toString();
283
+ }
284
+ function hashValue(value) {
285
+ let h1 = 2166136261 >>> 0;
286
+ let h2 = 2166136261 >>> 0;
287
+ const push = (s) => {
288
+ for (let i = 0; i < s.length; i++) {
289
+ const c = s.charCodeAt(i);
290
+ h1 = Math.imul(h1 ^ c, 16777619) >>> 0;
291
+ h2 = Math.imul(h2 ^ (c << 3 | c >>> 5), 2246822519) >>> 0;
292
+ }
293
+ };
294
+ const walk = (v) => {
295
+ if (v === null) return push("n");
296
+ if (v === void 0) return push("u");
297
+ switch (typeof v) {
298
+ case "number":
299
+ return push("#" + canonicalNumber(v));
300
+ case "boolean":
301
+ return push(v ? "t" : "f");
302
+ case "string":
303
+ return push('"' + v);
304
+ case "object": {
305
+ if (Array.isArray(v)) {
306
+ push("[");
307
+ for (const el of v) walk(el);
308
+ push("]");
309
+ return;
310
+ }
311
+ const obj = v;
312
+ const keys = Object.keys(obj).sort();
313
+ push("{");
314
+ for (const k of keys) {
315
+ push(k + ":");
316
+ walk(obj[k]);
317
+ }
318
+ push("}");
319
+ return;
320
+ }
321
+ default:
322
+ push("?");
323
+ }
324
+ };
325
+ walk(value);
326
+ const hex = (n2) => n2.toString(16).padStart(8, "0");
327
+ return hex(h1) + hex(h2);
328
+ }
329
+
330
+ // src/scene/node.ts
331
+ var __idCounter = 0;
332
+ function resetNodeIds(start = 0) {
333
+ __idCounter = start;
334
+ }
335
+ var Node = class {
336
+ id;
337
+ name;
338
+ /** Type tag for serialization; subclasses override. */
339
+ type = "Node";
340
+ pos;
341
+ rotation;
342
+ scale;
343
+ z;
344
+ visible;
345
+ /**
346
+ * Cosmetic nodes are pure *view* (derived from game state, rebuildable) and are
347
+ * excluded from serialize()/snapshot()/hash(). Mark a container cosmetic when it
348
+ * only renders state that lives elsewhere — so transient display (move counters,
349
+ * particles, tweened positions) never pollutes the canonical, verifiable state.
350
+ */
351
+ cosmetic = false;
352
+ parent = null;
353
+ children = [];
354
+ world = null;
355
+ /** Optional inline update without subclassing: node.onUpdate = (n, dt) => {…}. */
356
+ onUpdate;
357
+ behaviors = [];
358
+ signals = /* @__PURE__ */ new Map();
359
+ _ready = false;
360
+ _freed = false;
361
+ constructor(config = {}) {
362
+ this.id = `n${__idCounter++}`;
363
+ this.name = config.name ?? this.constructor.name;
364
+ this.pos = config.pos ? { ...config.pos } : { x: 0, y: 0 };
365
+ this.rotation = config.rotation ?? 0;
366
+ this.scale = config.scale ? { ...config.scale } : { x: 1, y: 1 };
367
+ this.z = config.z ?? 0;
368
+ this.visible = config.visible ?? true;
369
+ }
370
+ // ── Tree structure ─────────────────────────────────────────────
371
+ addChild(child) {
372
+ child.parent = this;
373
+ this.children.push(child);
374
+ if (this.world && this._ready) child.enterTree(this.world);
375
+ return child;
376
+ }
377
+ removeChild(child) {
378
+ const i = this.children.indexOf(child);
379
+ if (i >= 0) {
380
+ this.children.splice(i, 1);
381
+ child.parent = null;
382
+ }
383
+ }
384
+ /** Deferred free: runs exit hooks and detaches at the end of the step. */
385
+ free() {
386
+ this.world?.requestFree(this);
387
+ }
388
+ /** Depth-first find by name. */
389
+ find(name) {
390
+ if (this.name === name) return this;
391
+ for (const c of this.children) {
392
+ const r = c.find(name);
393
+ if (r) return r;
394
+ }
395
+ return null;
396
+ }
397
+ /** All descendants (and self) of a given type tag. */
398
+ query(type, out = []) {
399
+ if (this.type === type) out.push(this);
400
+ for (const c of this.children) c.query(type, out);
401
+ return out;
402
+ }
403
+ // ── Behaviors ─────────────────────────────────────────────────
404
+ addBehavior(b) {
405
+ this.behaviors.push(b);
406
+ if (this._ready) b.ready?.(this);
407
+ return this;
408
+ }
409
+ // ── Signals ───────────────────────────────────────────────────
410
+ signal(name) {
411
+ let s = this.signals.get(name);
412
+ if (!s) {
413
+ s = new Signal();
414
+ this.signals.set(name, s);
415
+ }
416
+ return s;
417
+ }
418
+ emit(name, payload) {
419
+ this.signals.get(name)?.emit(payload);
420
+ }
421
+ // ── Lifecycle (engine-called) ─────────────────────────────────
422
+ enterTree(world) {
423
+ this.world = world;
424
+ if (!this._ready) {
425
+ this._ready = true;
426
+ this.onReady();
427
+ for (const b of this.behaviors) b.ready?.(this);
428
+ }
429
+ for (const c of this.children) c.enterTree(world);
430
+ }
431
+ updateTree(dt) {
432
+ if (this._freed) return;
433
+ for (const b of this.behaviors) b.update?.(this, dt);
434
+ this.onUpdate?.(this, dt);
435
+ this.onProcess(dt);
436
+ for (const c of this.children.slice()) c.updateTree(dt);
437
+ }
438
+ exitTree() {
439
+ for (const b of this.behaviors) b.exit?.(this);
440
+ this.onExit();
441
+ for (const c of this.children.slice()) c.exitTree();
442
+ this._freed = true;
443
+ }
444
+ get isFreed() {
445
+ return this._freed;
446
+ }
447
+ // ── Transforms ────────────────────────────────────────────────
448
+ localTransform() {
449
+ return makeTransform(this.pos, this.rotation, this.scale);
450
+ }
451
+ worldTransform() {
452
+ const local = this.localTransform();
453
+ return this.parent ? composeTransform(this.parent.worldTransform(), local) : local;
454
+ }
455
+ // ── Rendering (projection) ────────────────────────────────────
456
+ /** Walk the subtree, appending draw commands with computed world transforms. */
457
+ collectDraw(out, parentWorld = IDENTITY) {
458
+ if (!this.visible) return;
459
+ const world = composeTransform(parentWorld, this.localTransform());
460
+ this.draw(out, world);
461
+ for (const c of this.children) c.collectDraw(out, world);
462
+ }
463
+ /** Subclasses emit their own commands here. Base draws nothing. */
464
+ draw(_out, _world) {
465
+ }
466
+ // ── Overridable hooks ─────────────────────────────────────────
467
+ onReady() {
468
+ }
469
+ onProcess(_dt) {
470
+ }
471
+ onExit() {
472
+ }
473
+ // ── Serialization (data only; behaviors/closures are re-attached by scene code) ──
474
+ serialize() {
475
+ return {
476
+ type: this.type,
477
+ name: this.name,
478
+ pos: { ...this.pos },
479
+ rotation: this.rotation,
480
+ scale: { ...this.scale },
481
+ z: this.z,
482
+ visible: this.visible,
483
+ props: this.serializeProps(),
484
+ children: this.children.filter((c) => !c.cosmetic).map((c) => c.serialize())
485
+ };
486
+ }
487
+ /** Subclasses persist their own fields here. */
488
+ serializeProps() {
489
+ return {};
490
+ }
491
+ /** Subclasses restore their own fields here. */
492
+ applyProps(_props) {
493
+ }
494
+ };
495
+
496
+ // src/scene/nodes.ts
497
+ var Sprite = class extends Node {
498
+ type = "Sprite";
499
+ shape;
500
+ paint;
501
+ constructor(config) {
502
+ super(config);
503
+ this.shape = config.shape;
504
+ this.paint = {
505
+ fill: config.fill,
506
+ stroke: config.stroke,
507
+ strokeWidth: config.strokeWidth,
508
+ opacity: config.opacity,
509
+ round: config.round
510
+ };
511
+ }
512
+ draw(out, world) {
513
+ const p = this.paint;
514
+ const base = { transform: world, z: this.z, ...p };
515
+ switch (this.shape.kind) {
516
+ case "rect":
517
+ out.push({ kind: "rect", x: -this.shape.w / 2, y: -this.shape.h / 2, w: this.shape.w, h: this.shape.h, r: this.shape.r, ...base });
518
+ break;
519
+ case "circle":
520
+ out.push({ kind: "circle", cx: 0, cy: 0, radius: this.shape.radius, ...base });
521
+ break;
522
+ case "poly":
523
+ out.push({ kind: "poly", points: this.shape.points, closed: this.shape.closed ?? true, ...base });
524
+ break;
525
+ case "path":
526
+ out.push({ kind: "path", d: this.shape.d, ...base });
527
+ break;
528
+ case "glyph":
529
+ out.push({ kind: "text", text: this.shape.char, x: 0, y: 0, size: this.shape.size, align: "center", ...base });
530
+ break;
531
+ }
532
+ }
533
+ serializeProps() {
534
+ return { shape: this.shape, paint: this.paint };
535
+ }
536
+ applyProps(props) {
537
+ if (props.shape) this.shape = props.shape;
538
+ if (props.paint) this.paint = props.paint;
539
+ }
540
+ };
541
+ var Text = class extends Node {
542
+ type = "Text";
543
+ text;
544
+ size;
545
+ font;
546
+ align;
547
+ weight;
548
+ paint;
549
+ constructor(config) {
550
+ super(config);
551
+ this.text = config.text;
552
+ this.size = config.size ?? 24;
553
+ this.font = config.font;
554
+ this.align = config.align ?? "left";
555
+ this.weight = config.weight;
556
+ this.paint = { fill: config.fill ?? "#000", stroke: config.stroke, strokeWidth: config.strokeWidth, opacity: config.opacity };
557
+ }
558
+ draw(out, world) {
559
+ out.push({
560
+ kind: "text",
561
+ text: this.text,
562
+ x: 0,
563
+ y: 0,
564
+ size: this.size,
565
+ font: this.font,
566
+ align: this.align,
567
+ weight: this.weight,
568
+ transform: world,
569
+ z: this.z,
570
+ ...this.paint
571
+ });
572
+ }
573
+ serializeProps() {
574
+ return { text: this.text, size: this.size, align: this.align, paint: this.paint };
575
+ }
576
+ applyProps(props) {
577
+ if (typeof props.text === "string") this.text = props.text;
578
+ if (typeof props.size === "number") this.size = props.size;
579
+ if (props.align) this.align = props.align;
580
+ if (props.paint) this.paint = props.paint;
581
+ }
582
+ };
583
+ var Camera2D = class extends Node {
584
+ type = "Camera2D";
585
+ zoom;
586
+ current;
587
+ constructor(config = {}) {
588
+ super(config);
589
+ this.zoom = config.zoom ?? 1;
590
+ this.current = config.current ?? true;
591
+ }
592
+ onReady() {
593
+ if (this.current) this.world.activeCamera = this;
594
+ }
595
+ serializeProps() {
596
+ return { zoom: this.zoom, current: this.current };
597
+ }
598
+ applyProps(props) {
599
+ if (typeof props.zoom === "number") this.zoom = props.zoom;
600
+ if (typeof props.current === "boolean") this.current = props.current;
601
+ }
602
+ };
603
+ var Timer = class extends Node {
604
+ type = "Timer";
605
+ duration;
606
+ oneShot;
607
+ remaining;
608
+ running;
609
+ constructor(config) {
610
+ super(config);
611
+ this.duration = config.duration;
612
+ this.oneShot = config.oneShot ?? true;
613
+ this.remaining = config.duration;
614
+ this.running = config.autostart ?? true;
615
+ }
616
+ start(duration) {
617
+ if (duration !== void 0) this.duration = duration;
618
+ this.remaining = this.duration;
619
+ this.running = true;
620
+ }
621
+ stop() {
622
+ this.running = false;
623
+ }
624
+ get timeLeft() {
625
+ return this.remaining;
626
+ }
627
+ onProcess(dt) {
628
+ if (!this.running) return;
629
+ this.remaining -= dt;
630
+ if (this.remaining <= 0) {
631
+ this.emit("timeout", void 0);
632
+ if (this.oneShot) this.running = false;
633
+ else this.remaining += this.duration;
634
+ }
635
+ }
636
+ /** Signal emitted when the countdown reaches zero. */
637
+ get timeout() {
638
+ return this.signal("timeout");
639
+ }
640
+ serializeProps() {
641
+ return { duration: this.duration, oneShot: this.oneShot, remaining: this.remaining, running: this.running };
642
+ }
643
+ applyProps(props) {
644
+ if (typeof props.duration === "number") this.duration = props.duration;
645
+ if (typeof props.oneShot === "boolean") this.oneShot = props.oneShot;
646
+ if (typeof props.remaining === "number") this.remaining = props.remaining;
647
+ if (typeof props.running === "boolean") this.running = props.running;
648
+ }
649
+ };
650
+
651
+ // src/scene/tween.ts
652
+ var pow = Math.pow;
653
+ var EASINGS = {
654
+ linear: (t) => t,
655
+ quadIn: (t) => t * t,
656
+ quadOut: (t) => 1 - (1 - t) * (1 - t),
657
+ quadInOut: (t) => t < 0.5 ? 2 * t * t : 1 - pow(-2 * t + 2, 2) / 2,
658
+ cubicIn: (t) => t * t * t,
659
+ cubicOut: (t) => 1 - pow(1 - t, 3),
660
+ cubicInOut: (t) => t < 0.5 ? 4 * t * t * t : 1 - pow(-2 * t + 2, 3) / 2,
661
+ sineIn: (t) => 1 - Math.cos(t * Math.PI / 2),
662
+ sineOut: (t) => Math.sin(t * Math.PI / 2),
663
+ sineInOut: (t) => -(Math.cos(Math.PI * t) - 1) / 2,
664
+ backOut: (t) => {
665
+ const c1 = 1.70158;
666
+ const c3 = c1 + 1;
667
+ return 1 + c3 * pow(t - 1, 3) + c1 * pow(t - 1, 2);
668
+ },
669
+ elasticOut: (t) => {
670
+ const c4 = 2 * Math.PI / 3;
671
+ return t === 0 ? 0 : t === 1 ? 1 : pow(2, -10 * t) * Math.sin((t * 10 - 0.75) * c4) + 1;
672
+ },
673
+ bounceOut: (t) => {
674
+ const n1 = 7.5625;
675
+ const d1 = 2.75;
676
+ if (t < 1 / d1) return n1 * t * t;
677
+ if (t < 2 / d1) return n1 * (t -= 1.5 / d1) * t + 0.75;
678
+ if (t < 2.5 / d1) return n1 * (t -= 2.25 / d1) * t + 0.9375;
679
+ return n1 * (t -= 2.625 / d1) * t + 0.984375;
680
+ }
681
+ };
682
+ var AnimationPlayer = class extends Node {
683
+ type = "AnimationPlayer";
684
+ tracks = [];
685
+ /**
686
+ * Animate a value from → to over `duration` seconds via `apply(value)`.
687
+ * Returns this for chaining.
688
+ */
689
+ to(apply, from, to, duration, ease = "cubicOut", opts = {}) {
690
+ this.tracks.push({
691
+ apply,
692
+ from,
693
+ to,
694
+ duration: Math.max(1e-6, duration),
695
+ elapsed: 0,
696
+ ease: typeof ease === "function" ? ease : EASINGS[ease] ?? EASINGS.linear,
697
+ delay: opts.delay ?? 0,
698
+ onDone: opts.onDone
699
+ });
700
+ return this;
701
+ }
702
+ get active() {
703
+ return this.tracks.length > 0;
704
+ }
705
+ /** Signal emitted when all tracks complete. */
706
+ get finished() {
707
+ return this.signal("finished");
708
+ }
709
+ onProcess(dt) {
710
+ if (this.tracks.length === 0) return;
711
+ for (let i = this.tracks.length - 1; i >= 0; i--) {
712
+ const tk = this.tracks[i];
713
+ if (tk.delay > 0) {
714
+ tk.delay -= dt;
715
+ continue;
716
+ }
717
+ tk.elapsed += dt;
718
+ const t = clamp(tk.elapsed / tk.duration, 0, 1);
719
+ tk.apply(tk.from + (tk.to - tk.from) * tk.ease(t));
720
+ if (t >= 1) {
721
+ tk.onDone?.();
722
+ this.tracks.splice(i, 1);
723
+ }
724
+ }
725
+ if (this.tracks.length === 0) this.emit("finished", void 0);
726
+ }
727
+ };
728
+
729
+ // src/scene/particles.ts
730
+ var Particles = class extends Node {
731
+ type = "Particles";
732
+ pool = [];
733
+ rng;
734
+ /** Cap on live particles (oldest are recycled first). */
735
+ maxParticles;
736
+ constructor(config = {}) {
737
+ super(config);
738
+ this.cosmetic = true;
739
+ this.rng = new Rng(config.seed ?? 7);
740
+ this.maxParticles = config.maxParticles ?? 512;
741
+ }
742
+ /** Emit a burst at a position (in this node's local space). */
743
+ burst(count, at, style) {
744
+ const r = this.rng;
745
+ for (let i = 0; i < count; i++) {
746
+ const angle = style.angle !== void 0 ? style.angle + (r.float() - 0.5) * (style.spread ?? 0.6) : r.float() * TAU;
747
+ const speed = style.speedMin + r.float() * (style.speedMax - style.speedMin);
748
+ const p = {
749
+ x: at.x,
750
+ y: at.y,
751
+ vx: Math.cos(angle) * speed,
752
+ vy: Math.sin(angle) * speed,
753
+ life: 0,
754
+ maxLife: style.lifeMin + r.float() * (style.lifeMax - style.lifeMin),
755
+ size: style.sizeMin + r.float() * (style.sizeMax - style.sizeMin),
756
+ color: style.colors[r.int(style.colors.length)]
757
+ };
758
+ if (this.pool.length >= this.maxParticles) this.pool.shift();
759
+ this.pool.push(p);
760
+ }
761
+ this.gravity = style.gravity ?? 0;
762
+ this.drag = style.drag ?? 0;
763
+ this.shrink = style.shrink ?? true;
764
+ }
765
+ gravity = 0;
766
+ drag = 0;
767
+ shrink = true;
768
+ onProcess(dt) {
769
+ const drag = Math.max(0, 1 - this.drag * dt);
770
+ let write = 0;
771
+ for (const p of this.pool) {
772
+ p.life += dt;
773
+ if (p.life >= p.maxLife) continue;
774
+ p.vy += this.gravity * dt;
775
+ p.vx *= drag;
776
+ p.vy *= drag;
777
+ p.x += p.vx * dt;
778
+ p.y += p.vy * dt;
779
+ this.pool[write++] = p;
780
+ }
781
+ this.pool.length = write;
782
+ }
783
+ draw(out, world) {
784
+ for (const p of this.pool) {
785
+ const t = 1 - p.life / p.maxLife;
786
+ out.push({
787
+ kind: "circle",
788
+ cx: p.x,
789
+ cy: p.y,
790
+ radius: this.shrink ? p.size * t : p.size,
791
+ fill: p.color,
792
+ opacity: Math.min(1, t * 2),
793
+ transform: world,
794
+ z: this.z
795
+ });
796
+ }
797
+ }
798
+ get liveCount() {
799
+ return this.pool.length;
800
+ }
801
+ };
802
+ var PARTICLE_PRESETS = {
803
+ dust: (colors = ["#c8c2b6", "#a09a8c"]) => ({ colors, sizeMin: 2, sizeMax: 4, speedMin: 30, speedMax: 90, lifeMin: 0.2, lifeMax: 0.45, gravity: 300, drag: 3, shrink: true }),
804
+ burst: (colors = ["#ffd75e", "#ff9d47", "#fff2c9"]) => ({ colors, sizeMin: 2, sizeMax: 5, speedMin: 120, speedMax: 320, lifeMin: 0.25, lifeMax: 0.6, drag: 4, shrink: true }),
805
+ hit: (colors = ["#ff5e5e", "#ffd0d0"]) => ({ colors, sizeMin: 2, sizeMax: 4, speedMin: 160, speedMax: 380, lifeMin: 0.15, lifeMax: 0.35, drag: 6, shrink: true }),
806
+ sparkle: (colors = ["#9ef7ff", "#e8fdff", "#4ed8e8"]) => ({ colors, sizeMin: 1.5, sizeMax: 3.5, speedMin: 20, speedMax: 70, lifeMin: 0.4, lifeMax: 0.9, gravity: -40, drag: 2, shrink: true })
807
+ };
808
+ var Shaker = class extends Node {
809
+ type = "Shaker";
810
+ rng;
811
+ trauma = 0;
812
+ /** Max offset in px at full trauma. */
813
+ amplitude;
814
+ /** Trauma decay per second. */
815
+ decay;
816
+ constructor(config = {}) {
817
+ super(config);
818
+ this.cosmetic = true;
819
+ this.rng = new Rng(config.seed ?? 99);
820
+ this.amplitude = config.amplitude ?? 14;
821
+ this.decay = config.decay ?? 2.6;
822
+ }
823
+ /** Add shake (0..1). Stacks, clamped to 1. Quadratic falloff feels right. */
824
+ addTrauma(amount) {
825
+ this.trauma = Math.min(1, this.trauma + amount);
826
+ }
827
+ onProcess(dt) {
828
+ this.trauma = Math.max(0, this.trauma - this.decay * dt);
829
+ const s = this.trauma * this.trauma * this.amplitude;
830
+ this.pos.x = (this.rng.float() * 2 - 1) * s;
831
+ this.pos.y = (this.rng.float() * 2 - 1) * s;
832
+ }
833
+ };
834
+
835
+ // src/scene/registry.ts
836
+ var registry = /* @__PURE__ */ new Map();
837
+ function registerNode(type, factory) {
838
+ registry.set(type, factory);
839
+ }
840
+ registerNode("Node", () => new Node());
841
+ registerNode("Sprite", () => new Sprite({ shape: { kind: "rect", w: 1, h: 1 } }));
842
+ registerNode("Text", () => new Text({ text: "" }));
843
+ registerNode("Camera2D", () => new Camera2D());
844
+ registerNode("Timer", () => new Timer({ duration: 1 }));
845
+ registerNode("AnimationPlayer", () => new AnimationPlayer());
846
+ function deserializeNode(data) {
847
+ const factory = registry.get(data.type);
848
+ if (!factory) throw new Error(`hayao: unknown node type "${data.type}" (register it first)`);
849
+ const node = factory();
850
+ node.name = data.name;
851
+ node.pos = { ...data.pos };
852
+ node.rotation = data.rotation;
853
+ node.scale = { ...data.scale };
854
+ node.z = data.z;
855
+ node.visible = data.visible;
856
+ node.applyProps(data.props);
857
+ for (const child of data.children) node.addChild(deserializeNode(child));
858
+ return node;
859
+ }
860
+
861
+ // src/input/actions.ts
862
+ var InputState = class {
863
+ down = /* @__PURE__ */ new Set();
864
+ prev = /* @__PURE__ */ new Set();
865
+ /** Analog axes (e.g. from gamepad or pointer), sampled per step. */
866
+ axes = /* @__PURE__ */ new Map();
867
+ /** Engine: set which actions are down this step (from live input or replay). */
868
+ beginFrame(actionsDown) {
869
+ this.prev = new Set(this.down);
870
+ this.down = new Set(actionsDown);
871
+ }
872
+ isDown(action) {
873
+ return this.down.has(action);
874
+ }
875
+ justPressed(action) {
876
+ return this.down.has(action) && !this.prev.has(action);
877
+ }
878
+ justReleased(action) {
879
+ return !this.down.has(action) && this.prev.has(action);
880
+ }
881
+ axis(name) {
882
+ return this.axes.get(name) ?? 0;
883
+ }
884
+ /** The current down-set as a stable sorted array (for recording). */
885
+ snapshot() {
886
+ return [...this.down].sort();
887
+ }
888
+ getState() {
889
+ return { down: [...this.down].sort(), prev: [...this.prev].sort() };
890
+ }
891
+ setState(s) {
892
+ this.down = new Set(s.down);
893
+ this.prev = new Set(s.prev);
894
+ }
895
+ };
896
+ function keysToActions(map, keysDown) {
897
+ const actions = [];
898
+ for (const action in map) {
899
+ if (map[action].some((k) => keysDown.has(k))) actions.push(action);
900
+ }
901
+ return actions.sort();
902
+ }
903
+ var InputRecorder = class {
904
+ frames = [];
905
+ record(actionsDown) {
906
+ this.frames.push(actionsDown.slice().sort());
907
+ }
908
+ get length() {
909
+ return this.frames.length;
910
+ }
911
+ toLog() {
912
+ return { frames: this.frames.map((f) => f.slice()) };
913
+ }
914
+ };
915
+ function frameActions(log, i) {
916
+ return log.frames[i] ?? [];
917
+ }
918
+ var DEFAULT_INPUT_MAP = {
919
+ up: ["ArrowUp", "KeyW"],
920
+ down: ["ArrowDown", "KeyS"],
921
+ left: ["ArrowLeft", "KeyA"],
922
+ right: ["ArrowRight", "KeyD"],
923
+ confirm: ["Enter", "Space"],
924
+ cancel: ["Escape", "Backspace"],
925
+ action: ["KeyZ", "KeyJ"],
926
+ action2: ["KeyX", "KeyK"],
927
+ undo: ["KeyU"],
928
+ restart: ["KeyR"]
929
+ };
930
+
931
+ // src/input/source.ts
932
+ var KeyboardSource = class {
933
+ keysDown = /* @__PURE__ */ new Set();
934
+ /** Virtual action taps (from DOM buttons etc.) pending consumption by a step. */
935
+ pressed = /* @__PURE__ */ new Set();
936
+ map;
937
+ target;
938
+ onDown;
939
+ onUp;
940
+ onBlur;
941
+ constructor(map, target = document) {
942
+ this.map = map;
943
+ this.target = target;
944
+ this.onDown = (e) => {
945
+ this.keysDown.add(e.code);
946
+ if (e.code.startsWith("Arrow") || e.code === "Space") e.preventDefault();
947
+ };
948
+ this.onUp = (e) => this.keysDown.delete(e.code);
949
+ this.onBlur = () => this.keysDown.clear();
950
+ target.addEventListener("keydown", this.onDown);
951
+ target.addEventListener("keyup", this.onUp);
952
+ globalThis.addEventListener?.("blur", this.onBlur);
953
+ }
954
+ /** The actions currently held down, as a stable sorted array. */
955
+ currentActions() {
956
+ const acts = keysToActions(this.map, this.keysDown);
957
+ if (this.pressed.size === 0) return acts;
958
+ const merged = new Set(acts);
959
+ for (const a of this.pressed) merged.add(a);
960
+ return [...merged].sort();
961
+ }
962
+ /**
963
+ * Virtually tap an action (DOM button, touch control). The tap is held until
964
+ * at least one fixed step has sampled it — the driver calls clearPressed()
965
+ * after a successful advance — so UI clicks enter the SAME deterministic
966
+ * input log as keys, and record/replay covers them.
967
+ */
968
+ press(action) {
969
+ this.pressed.add(action);
970
+ }
971
+ /** Consume pending virtual taps (driver-called after ≥1 step ran). */
972
+ clearPressed() {
973
+ this.pressed.clear();
974
+ }
975
+ setMap(map) {
976
+ this.map = map;
977
+ }
978
+ dispose() {
979
+ this.target.removeEventListener("keydown", this.onDown);
980
+ this.target.removeEventListener("keyup", this.onUp);
981
+ globalThis.removeEventListener?.("blur", this.onBlur);
982
+ }
983
+ };
984
+
985
+ // src/physics/tilemap.ts
986
+ var TILE = {
987
+ EMPTY: 0,
988
+ SOLID: 1,
989
+ /** Passable from below/sides; solid only when landing on its top edge. */
990
+ ONEWAY: 2,
991
+ HAZARD: 3
992
+ };
993
+ var DEFAULT_TILE_CHARS = {
994
+ "#": TILE.SOLID,
995
+ "-": TILE.ONEWAY,
996
+ "^": TILE.HAZARD
997
+ };
998
+ function tilemapFromAscii(rowsAscii, tileSize = 32, chars = DEFAULT_TILE_CHARS) {
999
+ const rows = rowsAscii.length;
1000
+ const cols = Math.max(...rowsAscii.map((r) => r.length));
1001
+ const tiles = new Array(cols * rows).fill(TILE.EMPTY);
1002
+ rowsAscii.forEach((row, ty) => {
1003
+ for (let tx = 0; tx < row.length; tx++) tiles[ty * cols + tx] = chars[row[tx]] ?? TILE.EMPTY;
1004
+ });
1005
+ return { cols, rows, tileSize, tiles };
1006
+ }
1007
+ function asciiEntities(rowsAscii, tileSize = 32, chars = DEFAULT_TILE_CHARS) {
1008
+ const out = [];
1009
+ rowsAscii.forEach((row, ty) => {
1010
+ for (let tx = 0; tx < row.length; tx++) {
1011
+ const c = row[tx];
1012
+ if (c === " " || c === "." || chars[c] !== void 0) continue;
1013
+ out.push({ char: c, tx, ty, x: (tx + 0.5) * tileSize, y: (ty + 0.5) * tileSize });
1014
+ }
1015
+ });
1016
+ return out;
1017
+ }
1018
+ function tileAt(map, tx, ty) {
1019
+ if (tx < 0 || ty < 0 || tx >= map.cols || ty >= map.rows) return TILE.SOLID;
1020
+ return map.tiles[ty * map.cols + tx];
1021
+ }
1022
+ function tileAtPoint(map, x, y) {
1023
+ return tileAt(map, Math.floor(x / map.tileSize), Math.floor(y / map.tileSize));
1024
+ }
1025
+ var mapWidth = (map) => map.cols * map.tileSize;
1026
+ var mapHeight = (map) => map.rows * map.tileSize;
1027
+
1028
+ // src/physics/aabb.ts
1029
+ var EPS = 1e-6;
1030
+ var spanTiles = (lo, hi, ts) => [Math.floor(lo / ts), Math.floor((hi - EPS) / ts)];
1031
+ function rectBlocked(map, x, y, w, h, solids = []) {
1032
+ const ts = map.tileSize;
1033
+ const [tx0, tx1] = spanTiles(x, x + w, ts);
1034
+ const [ty0, ty1] = spanTiles(y, y + h, ts);
1035
+ for (let ty = ty0; ty <= ty1; ty++)
1036
+ for (let tx = tx0; tx <= tx1; tx++) if (tileAt(map, tx, ty) === TILE.SOLID) return true;
1037
+ for (const s of solids) {
1038
+ if (s.oneway) continue;
1039
+ if (x < s.x + s.w - EPS && x + w > s.x + EPS && y < s.y + s.h - EPS && y + h > s.y + EPS) return true;
1040
+ }
1041
+ return false;
1042
+ }
1043
+ function moveRect(map, rect, dx, dy, opts = {}) {
1044
+ const ts = map.tileSize;
1045
+ const solids = opts.solids ?? [];
1046
+ let { x, y } = rect;
1047
+ const { w, h } = rect;
1048
+ let hitX = false;
1049
+ let hitY = false;
1050
+ let onWallLeft = false;
1051
+ let onWallRight = false;
1052
+ if (dx !== 0) {
1053
+ const [ty0, ty1] = spanTiles(y, y + h, ts);
1054
+ if (dx > 0) {
1055
+ let limit = x + dx;
1056
+ const [c0, c1] = [Math.floor((x + w) / ts), Math.floor((x + w + dx - EPS) / ts)];
1057
+ for (let tx = c0; tx <= c1; tx++)
1058
+ for (let ty = ty0; ty <= ty1; ty++)
1059
+ if (tileAt(map, tx, ty) === TILE.SOLID) limit = Math.min(limit, tx * ts - w);
1060
+ for (const s of solids)
1061
+ if (!s.oneway && s.y < y + h - EPS && s.y + s.h > y + EPS && s.x >= x + w - EPS) limit = Math.min(limit, s.x - w);
1062
+ if (limit < x + dx - EPS) {
1063
+ hitX = true;
1064
+ onWallRight = true;
1065
+ }
1066
+ x = Math.min(x + dx, limit);
1067
+ } else {
1068
+ let limit = x + dx;
1069
+ const [c0, c1] = [Math.floor((x - EPS) / ts), Math.floor((x + dx) / ts)];
1070
+ for (let tx = c0; tx >= c1; tx--)
1071
+ for (let ty = ty0; ty <= ty1; ty++)
1072
+ if (tileAt(map, tx, ty) === TILE.SOLID) limit = Math.max(limit, (tx + 1) * ts);
1073
+ for (const s of solids)
1074
+ if (!s.oneway && s.y < y + h - EPS && s.y + s.h > y + EPS && s.x + s.w <= x + EPS) limit = Math.max(limit, s.x + s.w);
1075
+ if (limit > x + dx + EPS) {
1076
+ hitX = true;
1077
+ onWallLeft = true;
1078
+ }
1079
+ x = Math.max(x + dx, limit);
1080
+ }
1081
+ }
1082
+ const prevBottom = y + h;
1083
+ let onFloor = false;
1084
+ let onCeiling = false;
1085
+ let floorSolid = -1;
1086
+ if (dy !== 0) {
1087
+ const [tx0, tx1] = spanTiles(x, x + w, ts);
1088
+ if (dy > 0) {
1089
+ let limit = y + dy;
1090
+ const [r0, r1] = [Math.floor(prevBottom / ts), Math.floor((prevBottom + dy - EPS) / ts)];
1091
+ for (let ty = r0; ty <= r1; ty++)
1092
+ for (let tx = tx0; tx <= tx1; tx++) {
1093
+ const t = tileAt(map, tx, ty);
1094
+ const blocks = t === TILE.SOLID || t === TILE.ONEWAY && !opts.dropThrough && prevBottom <= ty * ts + EPS;
1095
+ if (blocks) limit = Math.min(limit, ty * ts - h);
1096
+ }
1097
+ for (let i = 0; i < solids.length; i++) {
1098
+ const s = solids[i];
1099
+ const inX = s.x < x + w - EPS && s.x + s.w > x + EPS;
1100
+ if (!inX || s.y < prevBottom - EPS) continue;
1101
+ if (s.oneway && (opts.dropThrough || prevBottom > s.y + EPS)) continue;
1102
+ if (s.y - h < limit) {
1103
+ limit = s.y - h;
1104
+ floorSolid = i;
1105
+ }
1106
+ }
1107
+ if (limit < y + dy - EPS) {
1108
+ hitY = true;
1109
+ onFloor = true;
1110
+ } else floorSolid = -1;
1111
+ y = Math.min(y + dy, limit);
1112
+ } else {
1113
+ let limit = y + dy;
1114
+ const [r0, r1] = [Math.floor((y - EPS) / ts), Math.floor((y + dy) / ts)];
1115
+ for (let ty = r0; ty >= r1; ty--)
1116
+ for (let tx = tx0; tx <= tx1; tx++)
1117
+ if (tileAt(map, tx, ty) === TILE.SOLID) limit = Math.max(limit, (ty + 1) * ts);
1118
+ for (const s of solids)
1119
+ if (!s.oneway && s.x < x + w - EPS && s.x + s.w > x + EPS && s.y + s.h <= y + EPS) limit = Math.max(limit, s.y + s.h);
1120
+ if (limit > y + dy + EPS) {
1121
+ hitY = true;
1122
+ onCeiling = true;
1123
+ }
1124
+ y = Math.max(y + dy, limit);
1125
+ }
1126
+ }
1127
+ if (!onFloor) {
1128
+ const probe = groundAt(map, x, y, w, h, solids, opts.dropThrough ?? false);
1129
+ onFloor = probe.grounded;
1130
+ floorSolid = probe.solid;
1131
+ }
1132
+ const inset = 3;
1133
+ let hazard = false;
1134
+ {
1135
+ const [hx0, hx1] = spanTiles(x + inset, x + w - inset, ts);
1136
+ const [hy0, hy1] = spanTiles(y + inset, y + h - inset, ts);
1137
+ for (let ty = hy0; ty <= hy1 && !hazard; ty++)
1138
+ for (let tx = hx0; tx <= hx1 && !hazard; tx++) if (tileAt(map, tx, ty) === TILE.HAZARD) hazard = true;
1139
+ }
1140
+ return { x, y, hitX, hitY, onFloor, onCeiling, onWallLeft, onWallRight, hazard, floorSolid };
1141
+ }
1142
+ function groundAt(map, x, y, w, h, solids = [], dropThrough = false) {
1143
+ const ts = map.tileSize;
1144
+ const bottom = y + h;
1145
+ const [tx0, tx1] = spanTiles(x, x + w, ts);
1146
+ const ty = Math.floor((bottom + 1) / ts);
1147
+ for (let tx = tx0; tx <= tx1; tx++) {
1148
+ const t = tileAt(map, tx, ty);
1149
+ if (t === TILE.SOLID) return { grounded: true, solid: -1 };
1150
+ if (t === TILE.ONEWAY && !dropThrough && Math.abs(bottom - ty * ts) <= 1 + EPS) return { grounded: true, solid: -1 };
1151
+ }
1152
+ for (let i = 0; i < solids.length; i++) {
1153
+ const s = solids[i];
1154
+ const inX = s.x < x + w - EPS && s.x + s.w > x + EPS;
1155
+ if (inX && bottom <= s.y + 1 + EPS && bottom >= s.y - 1 - EPS) return { grounded: true, solid: i };
1156
+ }
1157
+ return { grounded: false, solid: -1 };
1158
+ }
1159
+
1160
+ // src/physics/platformer.ts
1161
+ var DEFAULT_PLATFORMER = {
1162
+ width: 22,
1163
+ height: 28,
1164
+ runSpeed: 340,
1165
+ groundAccel: 3800,
1166
+ groundFriction: 4200,
1167
+ airAccel: 2600,
1168
+ airFriction: 260,
1169
+ gravity: 2300,
1170
+ maxFall: 660,
1171
+ apexThreshold: 60,
1172
+ apexGravityMult: 0.5,
1173
+ jumpVelocity: 650,
1174
+ jumpCutMult: 0.4,
1175
+ coyoteTime: 0.1,
1176
+ jumpBuffer: 0.12,
1177
+ jumpCornerNudge: 10,
1178
+ dashCornerNudge: 12,
1179
+ airJumps: 0,
1180
+ airJumpMult: 0.92,
1181
+ dashSpeed: 640,
1182
+ dashTime: 0.14,
1183
+ dashCooldown: 0.25,
1184
+ dashCharges: 1,
1185
+ wallSlideMaxFall: 150,
1186
+ wallJumpVelX: 360,
1187
+ wallJumpVelY: 600,
1188
+ wallJumpLock: 0.13
1189
+ };
1190
+ var PAD_NEUTRAL = { moveX: 0, moveY: 0, jumpHeld: false, jumpPressed: false, dashPressed: false };
1191
+ function createPlatformerState(x, y) {
1192
+ return { x, y, vx: 0, vy: 0, facing: 1, onGround: false, onWall: 0, coyote: 0, buffer: 0, jumping: false, wallLock: 0, airJumpsLeft: 0, dashing: 0, dashCd: 0, dashesLeft: 1, dashVx: 0, dashVy: 0, carryVx: 0, carryVy: 0, dead: false };
1193
+ }
1194
+ var approach = (v, target, delta) => v < target ? Math.min(v + delta, target) : Math.max(v - delta, target);
1195
+ function stepPlatformer(s, input, dt, map, cfg = DEFAULT_PLATFORMER, platforms = []) {
1196
+ const ev = { jumped: false, wallJumped: false, airJumped: false, dashed: false, landed: false, died: false };
1197
+ if (s.dead) return ev;
1198
+ const wasGrounded = s.onGround;
1199
+ s.coyote = Math.max(0, s.coyote - dt);
1200
+ s.buffer = Math.max(0, s.buffer - dt);
1201
+ s.wallLock = Math.max(0, s.wallLock - dt);
1202
+ s.dashCd = Math.max(0, s.dashCd - dt);
1203
+ if (input.jumpPressed) s.buffer = cfg.jumpBuffer;
1204
+ if (input.dashPressed && s.dashing <= 0 && s.dashCd <= 0 && s.dashesLeft > 0) {
1205
+ let dx2 = input.moveX;
1206
+ let dy2 = input.moveY;
1207
+ if (dx2 === 0 && dy2 === 0) dx2 = s.facing;
1208
+ const inv = 1 / Math.hypot(dx2, dy2);
1209
+ s.dashing = cfg.dashTime;
1210
+ s.dashCd = cfg.dashCooldown;
1211
+ s.dashesLeft--;
1212
+ s.dashVx = dx2 * inv * cfg.dashSpeed;
1213
+ s.dashVy = dy2 * inv * cfg.dashSpeed;
1214
+ s.jumping = false;
1215
+ ev.dashed = true;
1216
+ }
1217
+ if (s.dashing > 0) {
1218
+ s.vx = s.dashVx;
1219
+ s.vy = s.dashVy;
1220
+ s.dashing -= dt;
1221
+ } else if (s.wallLock <= 0) {
1222
+ const target = input.moveX * cfg.runSpeed;
1223
+ const accel = s.onGround ? input.moveX !== 0 ? cfg.groundAccel : cfg.groundFriction : input.moveX !== 0 ? cfg.airAccel : cfg.airFriction;
1224
+ s.vx = approach(s.vx, target, accel * dt);
1225
+ if (input.moveX !== 0) s.facing = input.moveX > 0 ? 1 : -1;
1226
+ }
1227
+ if (s.dashing <= 0 && !s.onGround) {
1228
+ let g = cfg.gravity;
1229
+ if (Math.abs(s.vy) < cfg.apexThreshold && input.jumpHeld && s.jumping) g *= cfg.apexGravityMult;
1230
+ s.vy = Math.min(s.vy + g * dt, cfg.maxFall);
1231
+ if (s.onWall !== 0 && s.vy > cfg.wallSlideMaxFall && input.moveX === s.onWall) s.vy = cfg.wallSlideMaxFall;
1232
+ }
1233
+ if (s.jumping && !input.jumpHeld && s.vy < 0) {
1234
+ s.vy *= cfg.jumpCutMult;
1235
+ s.jumping = false;
1236
+ }
1237
+ if (s.buffer > 0 && s.dashing <= 0) {
1238
+ if (s.onGround || s.coyote > 0) {
1239
+ s.vy = -cfg.jumpVelocity;
1240
+ s.vx += s.carryVx;
1241
+ if (s.carryVy < 0) s.vy += s.carryVy;
1242
+ s.jumping = true;
1243
+ s.buffer = 0;
1244
+ s.coyote = 0;
1245
+ s.onGround = false;
1246
+ ev.jumped = true;
1247
+ } else if (s.onWall !== 0 && cfg.wallJumpVelY > 0) {
1248
+ s.vx = -s.onWall * cfg.wallJumpVelX;
1249
+ s.vy = -cfg.wallJumpVelY;
1250
+ s.facing = -s.onWall;
1251
+ s.jumping = true;
1252
+ s.buffer = 0;
1253
+ s.wallLock = cfg.wallJumpLock;
1254
+ ev.wallJumped = true;
1255
+ ev.jumped = true;
1256
+ } else if (s.airJumpsLeft > 0) {
1257
+ s.vy = -cfg.jumpVelocity * cfg.airJumpMult;
1258
+ s.airJumpsLeft--;
1259
+ s.jumping = true;
1260
+ s.buffer = 0;
1261
+ ev.jumped = true;
1262
+ ev.airJumped = true;
1263
+ }
1264
+ }
1265
+ const dropThrough = input.moveY > 0 && s.buffer <= 0;
1266
+ let dx = s.vx * dt + s.carryVx * dt * (s.onGround ? 1 : 0);
1267
+ let dy = s.vy * dt + Math.max(0, s.carryVy) * dt * (s.onGround ? 1 : 0);
1268
+ let res = moveRect(map, { x: s.x, y: s.y, w: cfg.width, h: cfg.height }, dx, dy, { solids: platforms, dropThrough });
1269
+ if (res.onCeiling && s.vy < 0 && s.dashing <= 0) {
1270
+ const slip = cornerSlipX(map, platforms, res.x, res.y, cfg.width, cfg.height, dy, cfg.jumpCornerNudge);
1271
+ if (slip !== null) {
1272
+ res = moveRect(map, { x: slip, y: res.y, w: cfg.width, h: cfg.height }, 0, dy, { solids: platforms, dropThrough });
1273
+ res.x = slip;
1274
+ }
1275
+ }
1276
+ if (res.hitX && s.dashing > 0 && Math.abs(s.dashVy) < 1) {
1277
+ const slip = cornerSlipY(map, platforms, res.x, res.y, cfg.width, cfg.height, dx, cfg.dashCornerNudge);
1278
+ if (slip !== null) {
1279
+ res = moveRect(map, { x: res.x, y: slip, w: cfg.width, h: cfg.height }, dx, 0, { solids: platforms, dropThrough });
1280
+ res.y = slip;
1281
+ }
1282
+ }
1283
+ s.x = res.x;
1284
+ s.y = res.y;
1285
+ if (res.hitY && s.vy > 0) s.vy = 0;
1286
+ if (res.onCeiling) {
1287
+ s.vy = Math.max(s.vy, 0);
1288
+ s.jumping = false;
1289
+ }
1290
+ if (res.hitX && s.dashing <= 0) s.vx = 0;
1291
+ s.onWall = res.onWallLeft ? -1 : res.onWallRight ? 1 : wallTouch(map, platforms, s.x, s.y, cfg.width, cfg.height);
1292
+ s.onGround = res.onFloor;
1293
+ if (s.onGround) {
1294
+ s.coyote = cfg.coyoteTime;
1295
+ s.dashesLeft = cfg.dashCharges;
1296
+ s.airJumpsLeft = cfg.airJumps;
1297
+ if (!wasGrounded) ev.landed = true;
1298
+ if (res.floorSolid >= 0) {
1299
+ const p = platforms[res.floorSolid];
1300
+ s.carryVx = p.vx;
1301
+ s.carryVy = p.vy;
1302
+ s.y = p.y - cfg.height;
1303
+ } else {
1304
+ s.carryVx = 0;
1305
+ s.carryVy = 0;
1306
+ }
1307
+ if (s.vy > 0) s.vy = 0;
1308
+ s.jumping = false;
1309
+ } else if (wasGrounded && s.carryVy !== 0) {
1310
+ }
1311
+ if (res.hazard) {
1312
+ s.dead = true;
1313
+ ev.died = true;
1314
+ }
1315
+ return ev;
1316
+ }
1317
+ function jumpHeight(cfg = DEFAULT_PLATFORMER) {
1318
+ return cfg.jumpVelocity * cfg.jumpVelocity / (2 * cfg.gravity);
1319
+ }
1320
+ function jumpAirtime(cfg = DEFAULT_PLATFORMER) {
1321
+ return 2 * cfg.jumpVelocity / cfg.gravity;
1322
+ }
1323
+ function jumpDistance(cfg = DEFAULT_PLATFORMER) {
1324
+ return cfg.runSpeed * jumpAirtime(cfg);
1325
+ }
1326
+ function dashJumpDistance(cfg = DEFAULT_PLATFORMER) {
1327
+ return jumpDistance(cfg) + (cfg.dashSpeed - cfg.runSpeed) * cfg.dashTime;
1328
+ }
1329
+ function cornerSlipX(map, solids, x, y, w, h, dy, nudge) {
1330
+ for (let n2 = 1; n2 <= nudge; n2++) {
1331
+ for (const dir of [1, -1]) {
1332
+ const nx = x + dir * n2;
1333
+ if (!rectBlocked(map, nx, y + dy, w, h, solids) && !rectBlocked(map, nx, y, w, h, solids)) return nx;
1334
+ }
1335
+ }
1336
+ return null;
1337
+ }
1338
+ function cornerSlipY(map, solids, x, y, w, h, dx, nudge) {
1339
+ for (let n2 = 1; n2 <= nudge; n2++) {
1340
+ for (const dir of [-1, 1]) {
1341
+ const ny = y + dir * n2;
1342
+ if (!rectBlocked(map, x + dx, ny, w, h, solids) && !rectBlocked(map, x, ny, w, h, solids)) return ny;
1343
+ }
1344
+ }
1345
+ return null;
1346
+ }
1347
+ function wallTouch(map, solids, x, y, w, h) {
1348
+ if (rectBlocked(map, x + 1, y, w, h, solids)) return 1;
1349
+ if (rectBlocked(map, x - 1, y, w, h, solids)) return -1;
1350
+ return 0;
1351
+ }
1352
+
1353
+ // src/physics/spatialHash.ts
1354
+ var SpatialHash = class {
1355
+ cells = /* @__PURE__ */ new Map();
1356
+ bounds = /* @__PURE__ */ new Map();
1357
+ cellSize;
1358
+ constructor(cellSize = 64) {
1359
+ this.cellSize = cellSize;
1360
+ }
1361
+ clear() {
1362
+ this.cells.clear();
1363
+ this.bounds.clear();
1364
+ }
1365
+ get size() {
1366
+ return this.bounds.size;
1367
+ }
1368
+ key(cx, cy) {
1369
+ return (cx + 32768) * 65536 + (cy + 32768);
1370
+ }
1371
+ insert(item, rect) {
1372
+ this.bounds.set(item, rect);
1373
+ const cs = this.cellSize;
1374
+ const x0 = Math.floor(rect.x / cs);
1375
+ const x1 = Math.floor((rect.x + rect.w) / cs);
1376
+ const y0 = Math.floor(rect.y / cs);
1377
+ const y1 = Math.floor((rect.y + rect.h) / cs);
1378
+ for (let cy = y0; cy <= y1; cy++)
1379
+ for (let cx = x0; cx <= x1; cx++) {
1380
+ const k = this.key(cx, cy);
1381
+ const cell = this.cells.get(k);
1382
+ if (cell) cell.push(item);
1383
+ else this.cells.set(k, [item]);
1384
+ }
1385
+ }
1386
+ /** All items whose rects overlap the query rect (deduplicated, deterministic order). */
1387
+ query(rect) {
1388
+ const cs = this.cellSize;
1389
+ const x0 = Math.floor(rect.x / cs);
1390
+ const x1 = Math.floor((rect.x + rect.w) / cs);
1391
+ const y0 = Math.floor(rect.y / cs);
1392
+ const y1 = Math.floor((rect.y + rect.h) / cs);
1393
+ const seen = /* @__PURE__ */ new Set();
1394
+ const out = [];
1395
+ for (let cy = y0; cy <= y1; cy++)
1396
+ for (let cx = x0; cx <= x1; cx++) {
1397
+ const cell = this.cells.get(this.key(cx, cy));
1398
+ if (!cell) continue;
1399
+ for (const item of cell) {
1400
+ if (seen.has(item)) continue;
1401
+ seen.add(item);
1402
+ const b = this.bounds.get(item);
1403
+ if (b.x < rect.x + rect.w && b.x + b.w > rect.x && b.y < rect.y + rect.h && b.y + b.h > rect.y) out.push(item);
1404
+ }
1405
+ }
1406
+ return out;
1407
+ }
1408
+ /** All items within `radius` of a point (circle vs rect-center test on bounds). */
1409
+ queryCircle(x, y, radius) {
1410
+ const near = this.query({ x: x - radius, y: y - radius, w: radius * 2, h: radius * 2 });
1411
+ return near.filter((item) => {
1412
+ const b = this.bounds.get(item);
1413
+ const cx = Math.max(b.x, Math.min(x, b.x + b.w));
1414
+ const cy = Math.max(b.y, Math.min(y, b.y + b.h));
1415
+ return (x - cx) * (x - cx) + (y - cy) * (y - cy) <= radius * radius;
1416
+ });
1417
+ }
1418
+ };
1419
+
1420
+ // src/physics/raycast.ts
1421
+ function raycastTiles(map, x0, y0, x1, y1) {
1422
+ const ts = map.tileSize;
1423
+ const dx = x1 - x0;
1424
+ const dy = y1 - y0;
1425
+ const maxDist = Math.hypot(dx, dy);
1426
+ if (maxDist === 0) return { blocked: false, x: x1, y: y1, dist: 0 };
1427
+ const dirX = dx / maxDist;
1428
+ const dirY = dy / maxDist;
1429
+ let tx = Math.floor(x0 / ts);
1430
+ let ty = Math.floor(y0 / ts);
1431
+ const stepX = dirX > 0 ? 1 : -1;
1432
+ const stepY = dirY > 0 ? 1 : -1;
1433
+ const tDeltaX = dirX !== 0 ? Math.abs(ts / dirX) : Infinity;
1434
+ const tDeltaY = dirY !== 0 ? Math.abs(ts / dirY) : Infinity;
1435
+ let tMaxX = dirX !== 0 ? (dirX > 0 ? (tx + 1) * ts - x0 : x0 - tx * ts) / Math.abs(dirX) : Infinity;
1436
+ let tMaxY = dirY !== 0 ? (dirY > 0 ? (ty + 1) * ts - y0 : y0 - ty * ts) / Math.abs(dirY) : Infinity;
1437
+ let t = 0;
1438
+ for (let guard = 0; guard < 512; guard++) {
1439
+ if (tMaxX < tMaxY) {
1440
+ t = tMaxX;
1441
+ tMaxX += tDeltaX;
1442
+ tx += stepX;
1443
+ } else {
1444
+ t = tMaxY;
1445
+ tMaxY += tDeltaY;
1446
+ ty += stepY;
1447
+ }
1448
+ if (t >= maxDist) return { blocked: false, x: x1, y: y1, dist: maxDist };
1449
+ if (tileAt(map, tx, ty) === TILE.SOLID) {
1450
+ return { blocked: true, x: x0 + dirX * t, y: y0 + dirY * t, dist: t };
1451
+ }
1452
+ }
1453
+ return { blocked: true, x: x0 + dirX * t, y: y0 + dirY * t, dist: t };
1454
+ }
1455
+ function lineOfSight(map, ax, ay, bx, by) {
1456
+ return !raycastTiles(map, ax, ay, bx, by).blocked;
1457
+ }
1458
+ function inVisionCone(map, ex, ey, faceX, faceY, fov, range, tx, ty) {
1459
+ const dx = tx - ex;
1460
+ const dy = ty - ey;
1461
+ const d = Math.hypot(dx, dy);
1462
+ if (d > range || d === 0) return false;
1463
+ const dot = (dx * faceX + dy * faceY) / d;
1464
+ if (dot < Math.cos(fov / 2)) return false;
1465
+ return lineOfSight(map, ex, ey, tx, ty);
1466
+ }
1467
+
1468
+ // src/render/commands.ts
1469
+ function sortCommands(cmds) {
1470
+ return cmds.map((c, i) => [c, i]).sort((a, b) => a[0].z - b[0].z || a[1] - b[1]).map(([c]) => c);
1471
+ }
1472
+
1473
+ // src/render/svgString.ts
1474
+ var n = (v) => Number.isInteger(v) ? String(v) : (Math.round(v * 1e3) / 1e3).toString();
1475
+ function matrix(t) {
1476
+ return `matrix(${n(t.a)} ${n(t.b)} ${n(t.c)} ${n(t.d)} ${n(t.e)} ${n(t.f)})`;
1477
+ }
1478
+ function paintAttrs(p) {
1479
+ const parts = [];
1480
+ parts.push(`fill="${p.fill ?? "none"}"`);
1481
+ if (p.stroke) {
1482
+ parts.push(`stroke="${p.stroke}"`);
1483
+ parts.push(`stroke-width="${n(p.strokeWidth ?? 1)}"`);
1484
+ if (p.round) parts.push('stroke-linejoin="round" stroke-linecap="round"');
1485
+ }
1486
+ if (p.opacity !== void 0 && p.opacity !== 1) parts.push(`opacity="${n(p.opacity)}"`);
1487
+ return parts.join(" ");
1488
+ }
1489
+ function escapeText(s) {
1490
+ return s.replace(/&/g, "&amp;").replace(/</g, "&lt;").replace(/>/g, "&gt;");
1491
+ }
1492
+ function commandToSVG(c) {
1493
+ const tf = `transform="${matrix(c.transform)}"`;
1494
+ const paint = paintAttrs(c);
1495
+ switch (c.kind) {
1496
+ case "rect":
1497
+ return `<rect x="${n(c.x)}" y="${n(c.y)}" width="${n(c.w)}" height="${n(c.h)}"${c.r ? ` rx="${n(c.r)}"` : ""} ${tf} ${paint}/>`;
1498
+ case "circle":
1499
+ return `<circle cx="${n(c.cx)}" cy="${n(c.cy)}" r="${n(c.radius)}" ${tf} ${paint}/>`;
1500
+ case "poly": {
1501
+ const pts = [];
1502
+ for (let i = 0; i < c.points.length; i += 2) pts.push(`${n(c.points[i])},${n(c.points[i + 1])}`);
1503
+ const tag = c.closed ? "polygon" : "polyline";
1504
+ return `<${tag} points="${pts.join(" ")}" ${tf} ${paint}/>`;
1505
+ }
1506
+ case "path":
1507
+ return `<path d="${c.d}" ${tf} ${paint}/>`;
1508
+ case "text": {
1509
+ const anchor = c.align === "center" ? "middle" : c.align === "right" ? "end" : "start";
1510
+ const font = c.font ? ` font-family="${c.font}"` : "";
1511
+ const weight = c.weight ? ` font-weight="${c.weight}"` : "";
1512
+ const fill = c.fill ?? "#000";
1513
+ return `<text x="${n(c.x)}" y="${n(c.y)}" font-size="${n(c.size)}" text-anchor="${anchor}" dominant-baseline="middle"${font}${weight} fill="${fill}"${c.opacity !== void 0 && c.opacity !== 1 ? ` opacity="${n(c.opacity)}"` : ""} ${tf}>${escapeText(c.text)}</text>`;
1514
+ }
1515
+ case "image":
1516
+ return `<image href="${c.href}" x="${n(c.x)}" y="${n(c.y)}" width="${n(c.w)}" height="${n(c.h)}" ${tf}${c.opacity !== void 0 && c.opacity !== 1 ? ` opacity="${n(c.opacity)}"` : ""}/>`;
1517
+ }
1518
+ }
1519
+ function commandsToSVGInner(commands) {
1520
+ return sortCommands(commands).map(commandToSVG).join("");
1521
+ }
1522
+ function renderToSVGString(commands, width, height, background = "#ffffff") {
1523
+ return `<svg xmlns="http://www.w3.org/2000/svg" viewBox="0 0 ${width} ${height}" width="${width}" height="${height}"><rect x="0" y="0" width="${width}" height="${height}" fill="${background}"/>` + commandsToSVGInner(commands) + `</svg>`;
1524
+ }
1525
+
1526
+ // src/render/svg.ts
1527
+ var SVGNS = "http://www.w3.org/2000/svg";
1528
+ var SvgRenderer = class {
1529
+ width;
1530
+ height;
1531
+ background;
1532
+ svg;
1533
+ bg;
1534
+ layer;
1535
+ constructor(config) {
1536
+ this.width = config.width;
1537
+ this.height = config.height;
1538
+ this.background = config.background ?? "#ffffff";
1539
+ this.svg = document.createElementNS(SVGNS, "svg");
1540
+ this.svg.setAttribute("viewBox", `0 0 ${this.width} ${this.height}`);
1541
+ this.svg.setAttribute("preserveAspectRatio", "xMidYMid meet");
1542
+ this.svg.style.width = "100%";
1543
+ this.svg.style.height = "100%";
1544
+ this.svg.style.display = "block";
1545
+ this.bg = document.createElementNS(SVGNS, "rect");
1546
+ this.bg.setAttribute("x", "0");
1547
+ this.bg.setAttribute("y", "0");
1548
+ this.bg.setAttribute("width", String(this.width));
1549
+ this.bg.setAttribute("height", String(this.height));
1550
+ this.bg.setAttribute("fill", this.background);
1551
+ this.svg.appendChild(this.bg);
1552
+ this.layer = document.createElementNS(SVGNS, "g");
1553
+ this.svg.appendChild(this.layer);
1554
+ }
1555
+ mount(parent) {
1556
+ parent.appendChild(this.svg);
1557
+ }
1558
+ draw(commands) {
1559
+ this.layer.innerHTML = commandsToSVGInner(commands);
1560
+ }
1561
+ setBackground(color) {
1562
+ this.background = color;
1563
+ this.bg.setAttribute("fill", color);
1564
+ }
1565
+ get element() {
1566
+ return this.svg;
1567
+ }
1568
+ dispose() {
1569
+ this.svg.remove();
1570
+ }
1571
+ };
1572
+
1573
+ // src/render/canvas.ts
1574
+ var Canvas2DRenderer = class {
1575
+ width;
1576
+ height;
1577
+ background;
1578
+ canvas;
1579
+ ctx;
1580
+ dpr = 1;
1581
+ constructor(config) {
1582
+ this.width = config.width;
1583
+ this.height = config.height;
1584
+ this.background = config.background ?? "#ffffff";
1585
+ this.canvas = document.createElement("canvas");
1586
+ this.canvas.style.width = "100%";
1587
+ this.canvas.style.height = "100%";
1588
+ this.canvas.style.display = "block";
1589
+ const ctx = this.canvas.getContext("2d");
1590
+ if (!ctx) throw new Error("hayao: 2D canvas context unavailable");
1591
+ this.ctx = ctx;
1592
+ this.resize();
1593
+ }
1594
+ mount(parent) {
1595
+ parent.appendChild(this.canvas);
1596
+ this.resize();
1597
+ }
1598
+ resize() {
1599
+ this.dpr = Math.min(3, globalThis.devicePixelRatio || 1);
1600
+ this.canvas.width = Math.round(this.width * this.dpr);
1601
+ this.canvas.height = Math.round(this.height * this.dpr);
1602
+ }
1603
+ draw(commands) {
1604
+ const ctx = this.ctx;
1605
+ ctx.setTransform(this.dpr, 0, 0, this.dpr, 0, 0);
1606
+ ctx.fillStyle = this.background;
1607
+ ctx.fillRect(0, 0, this.width, this.height);
1608
+ for (const c of sortCommands(commands)) {
1609
+ ctx.save();
1610
+ const t = c.transform;
1611
+ ctx.transform(t.a, t.b, t.c, t.d, t.e, t.f);
1612
+ ctx.globalAlpha = c.opacity ?? 1;
1613
+ this.paint(ctx, c);
1614
+ ctx.restore();
1615
+ }
1616
+ }
1617
+ stroke(ctx, c) {
1618
+ if (c.fill && c.fill !== "none") {
1619
+ ctx.fillStyle = c.fill;
1620
+ ctx.fill();
1621
+ }
1622
+ if (c.stroke) {
1623
+ ctx.strokeStyle = c.stroke;
1624
+ ctx.lineWidth = c.strokeWidth ?? 1;
1625
+ if (c.round) {
1626
+ ctx.lineJoin = "round";
1627
+ ctx.lineCap = "round";
1628
+ }
1629
+ ctx.stroke();
1630
+ }
1631
+ }
1632
+ paint(ctx, c) {
1633
+ switch (c.kind) {
1634
+ case "rect":
1635
+ ctx.beginPath();
1636
+ if (c.r) this.roundRect(ctx, c.x, c.y, c.w, c.h, c.r);
1637
+ else ctx.rect(c.x, c.y, c.w, c.h);
1638
+ this.stroke(ctx, c);
1639
+ break;
1640
+ case "circle":
1641
+ ctx.beginPath();
1642
+ ctx.arc(c.cx, c.cy, c.radius, 0, Math.PI * 2);
1643
+ this.stroke(ctx, c);
1644
+ break;
1645
+ case "poly":
1646
+ ctx.beginPath();
1647
+ for (let i = 0; i < c.points.length; i += 2) {
1648
+ if (i === 0) ctx.moveTo(c.points[i], c.points[i + 1]);
1649
+ else ctx.lineTo(c.points[i], c.points[i + 1]);
1650
+ }
1651
+ if (c.closed) ctx.closePath();
1652
+ this.stroke(ctx, c);
1653
+ break;
1654
+ case "path":
1655
+ {
1656
+ const p = new Path2D(c.d);
1657
+ if (c.fill && c.fill !== "none") {
1658
+ ctx.fillStyle = c.fill;
1659
+ ctx.fill(p);
1660
+ }
1661
+ if (c.stroke) {
1662
+ ctx.strokeStyle = c.stroke;
1663
+ ctx.lineWidth = c.strokeWidth ?? 1;
1664
+ if (c.round) {
1665
+ ctx.lineJoin = "round";
1666
+ ctx.lineCap = "round";
1667
+ }
1668
+ ctx.stroke(p);
1669
+ }
1670
+ }
1671
+ break;
1672
+ case "text":
1673
+ ctx.fillStyle = c.fill ?? "#000";
1674
+ ctx.font = `${c.weight ?? 400} ${c.size}px ${c.font ?? "sans-serif"}`;
1675
+ ctx.textAlign = c.align ?? "left";
1676
+ ctx.textBaseline = "middle";
1677
+ ctx.fillText(c.text, c.x, c.y);
1678
+ break;
1679
+ case "image":
1680
+ break;
1681
+ }
1682
+ }
1683
+ roundRect(ctx, x, y, w, h, r) {
1684
+ const rr = Math.min(r, w / 2, h / 2);
1685
+ ctx.moveTo(x + rr, y);
1686
+ ctx.arcTo(x + w, y, x + w, y + h, rr);
1687
+ ctx.arcTo(x + w, y + h, x, y + h, rr);
1688
+ ctx.arcTo(x, y + h, x, y, rr);
1689
+ ctx.arcTo(x, y, x + w, y, rr);
1690
+ ctx.closePath();
1691
+ }
1692
+ get element() {
1693
+ return this.canvas;
1694
+ }
1695
+ dispose() {
1696
+ this.canvas.remove();
1697
+ }
1698
+ };
1699
+
1700
+ // src/render/headless.ts
1701
+ var HeadlessRenderer = class {
1702
+ width;
1703
+ height;
1704
+ background;
1705
+ last = [];
1706
+ frameCount = 0;
1707
+ constructor(config) {
1708
+ this.width = config.width;
1709
+ this.height = config.height;
1710
+ this.background = config.background ?? "#ffffff";
1711
+ }
1712
+ draw(commands) {
1713
+ this.last = commands;
1714
+ this.frameCount++;
1715
+ }
1716
+ /** The most recently drawn display list. */
1717
+ get commands() {
1718
+ return this.last;
1719
+ }
1720
+ /** Count commands of a given kind (handy for assertions). */
1721
+ count(kind) {
1722
+ return this.last.filter((c) => c.kind === kind).length;
1723
+ }
1724
+ /** A vector screenshot of the last frame. */
1725
+ toSVGString() {
1726
+ return renderToSVGString(this.last, this.width, this.height, this.background);
1727
+ }
1728
+ };
1729
+
1730
+ // src/art/palette.ts
1731
+ var MEADOW = {
1732
+ name: "meadow",
1733
+ bg: "#f3ecdb",
1734
+ ink: "#3d3323",
1735
+ inkSoft: "#6f6047",
1736
+ line: "#d9ccae",
1737
+ accent: "#a11d3a",
1738
+ accent2: "#5a7d4e",
1739
+ good: "#4d6b3c",
1740
+ warn: "#c8791f",
1741
+ ramp: ["#a11d3a", "#c8791f", "#c9a22f", "#5a7d4e", "#3f7d8c", "#6a4c93"]
1742
+ };
1743
+ var DUSK = {
1744
+ name: "dusk",
1745
+ bg: "#1c1a24",
1746
+ ink: "#efe9f2",
1747
+ inkSoft: "#a89fb5",
1748
+ line: "#3a3546",
1749
+ accent: "#e26d8a",
1750
+ accent2: "#6cc4a1",
1751
+ good: "#6cc4a1",
1752
+ warn: "#e8b64a",
1753
+ ramp: ["#e26d8a", "#e8b64a", "#f2e07a", "#6cc4a1", "#5aa9d6", "#b493e6"]
1754
+ };
1755
+ var PAPER = {
1756
+ name: "paper",
1757
+ bg: "#faf7f0",
1758
+ ink: "#2b2b2b",
1759
+ inkSoft: "#666",
1760
+ line: "#e2ddd2",
1761
+ accent: "#d1495b",
1762
+ accent2: "#2e86ab",
1763
+ good: "#3c896d",
1764
+ warn: "#e0902f",
1765
+ ramp: ["#d1495b", "#e0902f", "#edc531", "#3c896d", "#2e86ab", "#8a5a9e"]
1766
+ };
1767
+ var PALETTES = { meadow: MEADOW, dusk: DUSK, paper: PAPER };
1768
+ function mix(a, b, t) {
1769
+ const pa = hexToRgb(a);
1770
+ const pb = hexToRgb(b);
1771
+ const r = Math.round(pa[0] + (pb[0] - pa[0]) * t);
1772
+ const g = Math.round(pa[1] + (pb[1] - pa[1]) * t);
1773
+ const bl = Math.round(pa[2] + (pb[2] - pa[2]) * t);
1774
+ return rgbToHex(r, g, bl);
1775
+ }
1776
+ function withAlpha(hex, alpha) {
1777
+ const [r, g, b] = hexToRgb(hex);
1778
+ return `rgba(${r}, ${g}, ${b}, ${alpha})`;
1779
+ }
1780
+ function hexToRgb(hex) {
1781
+ let h = hex.replace("#", "");
1782
+ if (h.length === 3) h = h.split("").map((c) => c + c).join("");
1783
+ const n2 = parseInt(h, 16);
1784
+ return [n2 >> 16 & 255, n2 >> 8 & 255, n2 & 255];
1785
+ }
1786
+ function rgbToHex(r, g, b) {
1787
+ return "#" + [r, g, b].map((v) => Math.max(0, Math.min(255, v)).toString(16).padStart(2, "0")).join("");
1788
+ }
1789
+
1790
+ // src/art/shapes.ts
1791
+ function regularPolygon(sides, radius, rotation = 0) {
1792
+ const pts = [];
1793
+ for (let i = 0; i < sides; i++) {
1794
+ const a = rotation + i / sides * TAU;
1795
+ pts.push(Math.cos(a) * radius, Math.sin(a) * radius);
1796
+ }
1797
+ return pts;
1798
+ }
1799
+ function star(points, outer, inner, rotation = -Math.PI / 2) {
1800
+ const pts = [];
1801
+ for (let i = 0; i < points * 2; i++) {
1802
+ const r = i % 2 === 0 ? outer : inner;
1803
+ const a = rotation + i / (points * 2) * TAU;
1804
+ pts.push(Math.cos(a) * r, Math.sin(a) * r);
1805
+ }
1806
+ return pts;
1807
+ }
1808
+ function blobPath(rng, radius, wobble = 0.25, lobes = 7) {
1809
+ const pts = [];
1810
+ for (let i = 0; i < lobes; i++) {
1811
+ const a = i / lobes * TAU;
1812
+ const r = radius * (1 - wobble + rng.float() * wobble * 2);
1813
+ pts.push({ x: Math.cos(a) * r, y: Math.sin(a) * r });
1814
+ }
1815
+ return smoothClosedPath(pts);
1816
+ }
1817
+ function smoothClosedPath(points, tension = 1) {
1818
+ const nPts = points.length;
1819
+ if (nPts < 3) return "";
1820
+ const p = (i) => points[(i % nPts + nPts) % nPts];
1821
+ const r = (v) => Math.round(v * 100) / 100;
1822
+ let d = `M ${r(p(0).x)} ${r(p(0).y)}`;
1823
+ for (let i = 0; i < nPts; i++) {
1824
+ const p0 = p(i - 1);
1825
+ const p1 = p(i);
1826
+ const p2 = p(i + 1);
1827
+ const p3 = p(i + 2);
1828
+ const c1x = p1.x + (p2.x - p0.x) / 6 * tension;
1829
+ const c1y = p1.y + (p2.y - p0.y) / 6 * tension;
1830
+ const c2x = p2.x - (p3.x - p1.x) / 6 * tension;
1831
+ const c2y = p2.y - (p3.y - p1.y) / 6 * tension;
1832
+ d += ` C ${r(c1x)} ${r(c1y)}, ${r(c2x)} ${r(c2y)}, ${r(p2.x)} ${r(p2.y)}`;
1833
+ }
1834
+ return d + " Z";
1835
+ }
1836
+ function smoothOpenPath(points, tension = 1) {
1837
+ const nPts = points.length;
1838
+ if (nPts < 2) return "";
1839
+ const r = (v) => Math.round(v * 100) / 100;
1840
+ let d = `M ${r(points[0].x)} ${r(points[0].y)}`;
1841
+ for (let i = 0; i < nPts - 1; i++) {
1842
+ const p0 = points[Math.max(0, i - 1)];
1843
+ const p1 = points[i];
1844
+ const p2 = points[i + 1];
1845
+ const p3 = points[Math.min(nPts - 1, i + 2)];
1846
+ const c1x = p1.x + (p2.x - p0.x) / 6 * tension;
1847
+ const c1y = p1.y + (p2.y - p0.y) / 6 * tension;
1848
+ const c2x = p2.x - (p3.x - p1.x) / 6 * tension;
1849
+ const c2y = p2.y - (p3.y - p1.y) / 6 * tension;
1850
+ d += ` C ${r(c1x)} ${r(c1y)}, ${r(c2x)} ${r(c2y)}, ${r(p2.x)} ${r(p2.y)}`;
1851
+ }
1852
+ return d;
1853
+ }
1854
+
1855
+ // src/audio/audio.ts
1856
+ var DEFAULT_VOLUMES = { master: 0.7, music: 0.6, sfx: 0.8, muted: false };
1857
+ var AudioBus = class {
1858
+ ctx = null;
1859
+ master = null;
1860
+ musicGain = null;
1861
+ sfxGain = null;
1862
+ vol = { ...DEFAULT_VOLUMES };
1863
+ padOn = false;
1864
+ get available() {
1865
+ return typeof globalThis.AudioContext !== "undefined" || "webkitAudioContext" in globalThis;
1866
+ }
1867
+ get started() {
1868
+ return !!this.ctx;
1869
+ }
1870
+ /** Must be called from a user gesture (browser autoplay policy). */
1871
+ start() {
1872
+ if (this.ctx) {
1873
+ void this.ctx.resume();
1874
+ return;
1875
+ }
1876
+ if (!this.available) return;
1877
+ const Ctx = globalThis.AudioContext || globalThis.webkitAudioContext;
1878
+ this.ctx = new Ctx();
1879
+ this.master = this.ctx.createGain();
1880
+ this.musicGain = this.ctx.createGain();
1881
+ this.sfxGain = this.ctx.createGain();
1882
+ this.musicGain.connect(this.master);
1883
+ this.sfxGain.connect(this.master);
1884
+ this.master.connect(this.ctx.destination);
1885
+ this.applyVolumes();
1886
+ }
1887
+ setVolumes(v) {
1888
+ this.vol = { ...this.vol, ...v };
1889
+ this.applyVolumes();
1890
+ }
1891
+ getVolumes() {
1892
+ return { ...this.vol };
1893
+ }
1894
+ applyVolumes() {
1895
+ if (!this.ctx || !this.master || !this.musicGain || !this.sfxGain) return;
1896
+ const t = this.ctx.currentTime;
1897
+ this.master.gain.setTargetAtTime(this.vol.muted ? 0 : this.vol.master, t, 0.04);
1898
+ this.musicGain.gain.setTargetAtTime(this.vol.music * 0.5, t, 0.04);
1899
+ this.sfxGain.gain.setTargetAtTime(this.vol.sfx, t, 0.04);
1900
+ }
1901
+ /** Play a single tone (no-op if audio unstarted). */
1902
+ tone(spec) {
1903
+ if (!this.ctx || !this.sfxGain) return;
1904
+ const { freq, duration, type = "sine", gain = 0.2, delay = 0 } = spec;
1905
+ const t = this.ctx.currentTime + delay;
1906
+ const osc = this.ctx.createOscillator();
1907
+ osc.type = type;
1908
+ osc.frequency.value = freq;
1909
+ const g = this.ctx.createGain();
1910
+ g.gain.setValueAtTime(0, t);
1911
+ g.gain.linearRampToValueAtTime(gain, t + 8e-3);
1912
+ g.gain.exponentialRampToValueAtTime(1e-4, t + duration);
1913
+ osc.connect(g);
1914
+ g.connect(this.sfxGain);
1915
+ osc.start(t);
1916
+ osc.stop(t + duration + 0.05);
1917
+ }
1918
+ /** Play a sequence of tones as an arpeggio/chord. */
1919
+ play(tones) {
1920
+ for (const t of tones) this.tone(t);
1921
+ }
1922
+ /** Convenience SFX. */
1923
+ blip(freq = 520) {
1924
+ this.tone({ freq, duration: 0.06, type: "sine", gain: 0.18 });
1925
+ }
1926
+ chime() {
1927
+ [523.25, 659.25, 783.99].forEach((f, i) => this.tone({ freq: f, duration: 0.9 - i * 0.15, type: "sine", gain: 0.15, delay: i * 0.04 }));
1928
+ }
1929
+ success() {
1930
+ [392, 493.88, 587.33, 783.99].forEach((f, i) => this.tone({ freq: f, duration: 0.4, type: "triangle", gain: 0.15, delay: i * 0.08 }));
1931
+ }
1932
+ thud() {
1933
+ this.tone({ freq: 130, duration: 0.14, type: "sine", gain: 0.22 });
1934
+ }
1935
+ /** Start a soft evolving ambient pad on the music bus. */
1936
+ startAmbient(root = 110, voices = [1, 1.5, 2, 2.5]) {
1937
+ if (!this.ctx || !this.musicGain || this.padOn) return;
1938
+ this.padOn = true;
1939
+ const filter = this.ctx.createBiquadFilter();
1940
+ filter.type = "lowpass";
1941
+ filter.frequency.value = 900;
1942
+ filter.connect(this.musicGain);
1943
+ const bus = this.ctx.createGain();
1944
+ bus.gain.value = 0;
1945
+ bus.connect(filter);
1946
+ for (const mult of voices) {
1947
+ const osc = this.ctx.createOscillator();
1948
+ osc.type = "triangle";
1949
+ osc.frequency.value = root * mult;
1950
+ const g = this.ctx.createGain();
1951
+ g.gain.value = 0.1 / voices.length;
1952
+ osc.connect(g);
1953
+ g.connect(bus);
1954
+ osc.start();
1955
+ }
1956
+ bus.gain.setTargetAtTime(0.9, this.ctx.currentTime, 3);
1957
+ }
1958
+ };
1959
+ var audio = new AudioBus();
1960
+
1961
+ // src/ui/overlay.ts
1962
+ var active = null;
1963
+ var STYLE_ID = "hayao-overlay-style";
1964
+ var CSS = `
1965
+ .hy-scrim{position:absolute;inset:0;display:grid;place-items:center;z-index:50;font-family:var(--hy-serif,Georgia,serif)}
1966
+ .hy-scrim.dim{background:rgba(30,24,14,.5);backdrop-filter:blur(2px)}
1967
+ .hy-card{background:var(--hy-paper,#fbf6ea);color:var(--hy-ink,#3d3323);border:1px solid var(--hy-line,#d9ccae);border-radius:14px;box-shadow:0 14px 44px rgba(40,30,15,.3);padding:26px 30px;max-width:440px;text-align:center}
1968
+ .hy-card h1{margin:0 0 6px;font-size:30px}
1969
+ .hy-card .hy-body{color:var(--hy-ink-soft,#6f6047);font-size:15px;line-height:1.5;margin-bottom:18px}
1970
+ .hy-menu{display:flex;flex-direction:column;gap:8px;align-items:stretch}
1971
+ .hy-item{font:inherit;font-size:16px;padding:9px 16px;border-radius:9px;border:1px solid var(--hy-line,#d9ccae);background:transparent;color:inherit;cursor:pointer;transition:transform .1s,background .1s}
1972
+ .hy-item:hover,.hy-item.sel{background:var(--hy-accent,#a11d3a);color:#fdf3ee;border-color:transparent;transform:translateY(-1px)}
1973
+ .hy-item.primary{background:var(--hy-accent,#a11d3a);color:#fdf3ee;border-color:transparent}
1974
+ `;
1975
+ function ensureStyle() {
1976
+ if (typeof document === "undefined" || document.getElementById(STYLE_ID)) return;
1977
+ const s = document.createElement("style");
1978
+ s.id = STYLE_ID;
1979
+ s.textContent = CSS;
1980
+ document.head.appendChild(s);
1981
+ }
1982
+ var host = null;
1983
+ function setOverlayHost(el) {
1984
+ host = el;
1985
+ }
1986
+ function showScreen(spec) {
1987
+ if (typeof document === "undefined") {
1988
+ return { close() {
1989
+ }, element: null };
1990
+ }
1991
+ ensureStyle();
1992
+ hideScreen();
1993
+ const scrim = document.createElement("div");
1994
+ scrim.className = `hy-scrim${spec.dim === false ? "" : " dim"}${spec.className ? " " + spec.className : ""}`;
1995
+ const card = document.createElement("div");
1996
+ card.className = "hy-card";
1997
+ if (spec.title) {
1998
+ const h = document.createElement("h1");
1999
+ h.textContent = spec.title;
2000
+ card.appendChild(h);
2001
+ }
2002
+ if (spec.body) {
2003
+ const b = document.createElement("div");
2004
+ b.className = "hy-body";
2005
+ b.innerHTML = spec.body;
2006
+ card.appendChild(b);
2007
+ }
2008
+ const actions = spec.actions ?? [];
2009
+ let sel = Math.max(0, actions.findIndex((a) => a.primary));
2010
+ if (sel < 0) sel = 0;
2011
+ const items = [];
2012
+ if (actions.length) {
2013
+ const menu = document.createElement("div");
2014
+ menu.className = "hy-menu";
2015
+ actions.forEach((a, i) => {
2016
+ const btn = document.createElement("button");
2017
+ btn.className = `hy-item${a.primary ? " primary" : ""}`;
2018
+ btn.textContent = a.label;
2019
+ btn.addEventListener("click", () => a.onSelect());
2020
+ btn.addEventListener("mouseenter", () => setSel(i));
2021
+ menu.appendChild(btn);
2022
+ items.push(btn);
2023
+ });
2024
+ card.appendChild(menu);
2025
+ }
2026
+ function setSel(i) {
2027
+ sel = (i + items.length) % items.length;
2028
+ items.forEach((el, k) => el.classList.toggle("sel", k === sel));
2029
+ }
2030
+ if (items.length) setSel(sel);
2031
+ const onKey = (e) => {
2032
+ if (!items.length) return;
2033
+ if (e.code === "ArrowDown") {
2034
+ setSel(sel + 1);
2035
+ e.preventDefault();
2036
+ } else if (e.code === "ArrowUp") {
2037
+ setSel(sel - 1);
2038
+ e.preventDefault();
2039
+ } else if (e.code === "Enter" || e.code === "Space") {
2040
+ actions[sel]?.onSelect();
2041
+ e.preventDefault();
2042
+ }
2043
+ };
2044
+ document.addEventListener("keydown", onKey);
2045
+ scrim.appendChild(card);
2046
+ (host ?? document.body).appendChild(scrim);
2047
+ const handle = {
2048
+ element: scrim,
2049
+ close() {
2050
+ document.removeEventListener("keydown", onKey);
2051
+ scrim.remove();
2052
+ if (active === handle) active = null;
2053
+ }
2054
+ };
2055
+ active = handle;
2056
+ return handle;
2057
+ }
2058
+ function hideScreen() {
2059
+ active?.close();
2060
+ }
2061
+
2062
+ // src/ui/settings.ts
2063
+ var KEY = "hayao.settings.v1";
2064
+ var DEFAULTS = { master: 0.7, music: 0.6, sfx: 0.8, muted: false, colorblind: false, reducedMotion: false };
2065
+ var SettingsStore = class {
2066
+ state;
2067
+ subs = /* @__PURE__ */ new Set();
2068
+ constructor() {
2069
+ this.state = this.load();
2070
+ this.pushToAudio();
2071
+ }
2072
+ get() {
2073
+ return { ...this.state };
2074
+ }
2075
+ set(patch) {
2076
+ this.state = { ...this.state, ...patch };
2077
+ this.save();
2078
+ this.pushToAudio();
2079
+ for (const fn of this.subs) fn(this.get());
2080
+ }
2081
+ subscribe(fn) {
2082
+ this.subs.add(fn);
2083
+ return () => this.subs.delete(fn);
2084
+ }
2085
+ pushToAudio() {
2086
+ audio.setVolumes({ master: this.state.master, music: this.state.music, sfx: this.state.sfx, muted: this.state.muted });
2087
+ }
2088
+ load() {
2089
+ try {
2090
+ const raw = typeof localStorage !== "undefined" && localStorage.getItem(KEY);
2091
+ if (raw) return { ...DEFAULTS, ...JSON.parse(raw) };
2092
+ } catch {
2093
+ }
2094
+ return { ...DEFAULTS };
2095
+ }
2096
+ save() {
2097
+ try {
2098
+ localStorage.setItem(KEY, JSON.stringify(this.state));
2099
+ } catch {
2100
+ }
2101
+ }
2102
+ };
2103
+ var settings = new SettingsStore();
2104
+ function toggleFullscreen() {
2105
+ if (typeof document === "undefined") return;
2106
+ const doc = document;
2107
+ const el = document.documentElement;
2108
+ if (document.fullscreenElement || doc.webkitFullscreenElement) {
2109
+ (document.exitFullscreen ?? doc.webkitExitFullscreen)?.call(document);
2110
+ } else {
2111
+ (el.requestFullscreen ?? el.webkitRequestFullscreen)?.call(el);
2112
+ }
2113
+ }
2114
+
2115
+ // src/ui/shell.ts
2116
+ var Shell = class {
2117
+ paused = false;
2118
+ opts;
2119
+ keyHandler;
2120
+ constructor(opts = {}) {
2121
+ this.opts = opts;
2122
+ this.keyHandler = (e) => {
2123
+ if (e.code === "Escape") {
2124
+ e.preventDefault();
2125
+ this.toggle();
2126
+ }
2127
+ };
2128
+ if (typeof document !== "undefined") document.addEventListener("keydown", this.keyHandler);
2129
+ }
2130
+ get isPaused() {
2131
+ return this.paused;
2132
+ }
2133
+ toggle() {
2134
+ this.paused ? this.resume() : this.pause();
2135
+ }
2136
+ pause() {
2137
+ if (this.paused) return;
2138
+ this.paused = true;
2139
+ audio.blip(440);
2140
+ this.opts.onPause?.(true);
2141
+ this.render();
2142
+ }
2143
+ resume() {
2144
+ if (!this.paused) return;
2145
+ this.paused = false;
2146
+ hideScreen();
2147
+ this.opts.onPause?.(false);
2148
+ }
2149
+ render() {
2150
+ const s = settings.get();
2151
+ const vol = (label, v) => `${label}: ${Math.round(v * 100)}`;
2152
+ showScreen({
2153
+ title: this.opts.title ?? "Paused",
2154
+ body: `<div style="font-size:13px;text-align:left;line-height:1.9">${vol("Master", s.master)} \xB7 ${vol("Music", s.music)} \xB7 ${vol("Sfx", s.sfx)}<br/><span style="opacity:.7">Use the menu, or keys: M mute \xB7 F fullscreen</span></div>`,
2155
+ actions: [
2156
+ { label: "Resume", primary: true, onSelect: () => this.resume() },
2157
+ { label: s.muted ? "Unmute (M)" : "Mute (M)", onSelect: () => {
2158
+ settings.set({ muted: !settings.get().muted });
2159
+ this.render();
2160
+ } },
2161
+ { label: "Music \u2212/+", onSelect: () => {
2162
+ const m = Math.min(1, settings.get().music + 0.1) % 1.05;
2163
+ settings.set({ music: m > 1 ? 0 : m });
2164
+ this.render();
2165
+ } },
2166
+ { label: "Fullscreen (F)", onSelect: () => toggleFullscreen() },
2167
+ ...this.opts.onRestart ? [{ label: "Restart", onSelect: () => {
2168
+ this.resume();
2169
+ this.opts.onRestart();
2170
+ } }] : [],
2171
+ ...this.opts.onQuit ? [{ label: "Quit", onSelect: () => {
2172
+ this.resume();
2173
+ this.opts.onQuit();
2174
+ } }] : []
2175
+ ]
2176
+ });
2177
+ }
2178
+ dispose() {
2179
+ if (typeof document !== "undefined") document.removeEventListener("keydown", this.keyHandler);
2180
+ hideScreen();
2181
+ }
2182
+ };
2183
+
2184
+ // src/verify/solver.ts
2185
+ function solve(puzzle, options = {}) {
2186
+ const maxDepth = options.maxDepth ?? 60;
2187
+ const nodeCap = options.nodeCap ?? 1e6;
2188
+ const start = puzzle.initial(options.level);
2189
+ if (puzzle.isWin(start)) return { solvable: true, path: [], depth: 0, nodes: 0, exhausted: false };
2190
+ let frontier = [{ state: start, path: [] }];
2191
+ const seen = /* @__PURE__ */ new Set([puzzle.key(start)]);
2192
+ let nodes = 0;
2193
+ for (let depth = 0; depth < maxDepth && frontier.length > 0; depth++) {
2194
+ const next = [];
2195
+ for (const { state, path } of frontier) {
2196
+ for (const move of puzzle.moves(state)) {
2197
+ if (nodes >= nodeCap) return { solvable: false, nodes, exhausted: true };
2198
+ nodes++;
2199
+ const after = puzzle.apply(state, move);
2200
+ if (puzzle.isDead?.(after)) continue;
2201
+ if (puzzle.isWin(after)) {
2202
+ return { solvable: true, path: [...path, move], depth: depth + 1, nodes, exhausted: false };
2203
+ }
2204
+ const k = puzzle.key(after);
2205
+ if (!seen.has(k)) {
2206
+ seen.add(k);
2207
+ next.push({ state: after, path: [...path, move] });
2208
+ }
2209
+ }
2210
+ }
2211
+ frontier = next;
2212
+ }
2213
+ return { solvable: false, nodes, exhausted: frontier.length > 0 };
2214
+ }
2215
+ function assertSolvable(puzzle, options = {}) {
2216
+ const result = solve(puzzle, options);
2217
+ if (!result.solvable) {
2218
+ throw new Error(
2219
+ `hayao: level ${options.level ?? 0} is NOT winnable (expanded ${result.nodes} nodes${result.exhausted ? ", search capped \u2014 raise nodeCap/maxDepth" : ""}).`
2220
+ );
2221
+ }
2222
+ return result;
2223
+ }
2224
+
2225
+ // src/verify/determinism.ts
2226
+ function replay(makeWorld, log) {
2227
+ const world = makeWorld();
2228
+ const hashes = [];
2229
+ for (let i = 0; i < log.frames.length; i++) {
2230
+ world.step(frameActions(log, i));
2231
+ hashes.push(world.hash());
2232
+ }
2233
+ return { finalHash: world.hash(), hashes };
2234
+ }
2235
+ function checkDeterministic(makeWorld, log) {
2236
+ const a = replay(makeWorld, log);
2237
+ const b = replay(makeWorld, log);
2238
+ let divergedAt = -1;
2239
+ for (let i = 0; i < a.hashes.length; i++) {
2240
+ if (a.hashes[i] !== b.hashes[i]) {
2241
+ divergedAt = i;
2242
+ break;
2243
+ }
2244
+ }
2245
+ return {
2246
+ ok: divergedAt === -1 && a.finalHash === b.finalHash,
2247
+ frames: log.frames.length,
2248
+ divergedAt,
2249
+ finalHash: a.finalHash
2250
+ };
2251
+ }
2252
+ function assertDeterministic(makeWorld, log) {
2253
+ const report = checkDeterministic(makeWorld, log);
2254
+ if (!report.ok) {
2255
+ throw new Error(
2256
+ `hayao: sim is NON-deterministic \u2014 runs diverged at frame ${report.divergedAt} of ${report.frames}. Check for Math.random/Date.now/Set-iteration/unordered-map in the sim.`
2257
+ );
2258
+ }
2259
+ return report;
2260
+ }
2261
+ function assertSnapshotStable(makeWorld, log, warmup) {
2262
+ const original = makeWorld();
2263
+ for (let i = 0; i < warmup; i++) original.step(frameActions(log, i));
2264
+ const snap = original.snapshot();
2265
+ const restored = makeWorld();
2266
+ restored.restore(snap);
2267
+ for (let i = warmup; i < log.frames.length; i++) {
2268
+ original.step(frameActions(log, i));
2269
+ restored.step(frameActions(log, i));
2270
+ }
2271
+ const hashA = original.hash();
2272
+ const hashB = restored.hash();
2273
+ return { ok: hashA === hashB, hashA, hashB };
2274
+ }
2275
+
2276
+ // src/verify/playthrough.ts
2277
+ function hold(actions, frames) {
2278
+ return { actions, frames };
2279
+ }
2280
+ function wait(frames) {
2281
+ return { actions: [], frames };
2282
+ }
2283
+ function scriptedPlaythrough(world, script) {
2284
+ const probes = [];
2285
+ let total = 0;
2286
+ for (const seg of script) {
2287
+ for (let i = 0; i < seg.frames; i++) world.step(seg.actions ?? []);
2288
+ total += seg.frames;
2289
+ probes.push(world.probe());
2290
+ }
2291
+ return { totalFrames: total, finalHash: world.hash(), probes };
2292
+ }
2293
+ function pump(world, frames, actions = []) {
2294
+ for (let i = 0; i < frames; i++) world.step(actions);
2295
+ }
2296
+
2297
+ // src/verify/capture.ts
2298
+ function isCaptureMode() {
2299
+ return typeof location !== "undefined" && new URLSearchParams(location.search).has("capture");
2300
+ }
2301
+ function installCapture(target) {
2302
+ const api = {
2303
+ pump(frames, actions = []) {
2304
+ target.setPaused(true);
2305
+ for (let i = 0; i < frames; i++) target.stepOnce(actions);
2306
+ return target.world.probe();
2307
+ },
2308
+ probe: () => target.world.probe(),
2309
+ hash: () => target.world.hash(),
2310
+ shot: () => target.renderSVG(),
2311
+ async save(path) {
2312
+ const svg = target.renderSVG();
2313
+ try {
2314
+ const res = await fetch("/__shot", {
2315
+ method: "POST",
2316
+ headers: { "content-type": "application/json" },
2317
+ body: JSON.stringify({ path, svg })
2318
+ });
2319
+ return res.ok;
2320
+ } catch {
2321
+ return false;
2322
+ }
2323
+ },
2324
+ key(type, code) {
2325
+ document.dispatchEvent(new KeyboardEvent(type, { code, bubbles: true }));
2326
+ },
2327
+ get world() {
2328
+ return target.world;
2329
+ }
2330
+ };
2331
+ globalThis.__hayao = api;
2332
+ return api;
2333
+ }
2334
+
2335
+ // src/verify/driver.ts
2336
+ function scriptToFrames(script) {
2337
+ const out = [];
2338
+ for (const seg of script) {
2339
+ const n2 = seg.frames ?? 1;
2340
+ const hold2 = seg.hold ?? [];
2341
+ for (let i = 0; i < n2; i++) out.push(i === 0 && seg.press ? [...hold2, ...seg.press] : [...hold2]);
2342
+ }
2343
+ return out;
2344
+ }
2345
+ function drive(world, script, until) {
2346
+ let frames = 0;
2347
+ for (const f of scriptToFrames(script)) {
2348
+ if (until && until(world.probe())) return { frames, matched: true };
2349
+ world.step(f);
2350
+ frames++;
2351
+ }
2352
+ return { frames, matched: until ? until(world.probe()) : false };
2353
+ }
2354
+
2355
+ // src/verify/bot.ts
2356
+ function createPlanBot(plan, execs) {
2357
+ let i = 0;
2358
+ let frames = 0;
2359
+ let mem = {};
2360
+ const fn = (probe) => {
2361
+ const out = [];
2362
+ const step = plan[i];
2363
+ if (!step) return out;
2364
+ frames++;
2365
+ const ctx = {
2366
+ get frames() {
2367
+ return frames;
2368
+ },
2369
+ mem,
2370
+ next() {
2371
+ i++;
2372
+ frames = 0;
2373
+ mem = {};
2374
+ },
2375
+ retry() {
2376
+ frames = 0;
2377
+ mem = {};
2378
+ }
2379
+ };
2380
+ const exec = execs[step.kind];
2381
+ if (!exec) throw new Error(`plan bot: no executor for step kind "${step.kind}"`);
2382
+ exec(step, probe, out, ctx);
2383
+ return out;
2384
+ };
2385
+ const bot = fn;
2386
+ bot.stepIndex = () => i;
2387
+ bot.done = () => i >= plan.length;
2388
+ return bot;
2389
+ }
2390
+ function steer2D(px, py, tx, ty, out, dead = 8) {
2391
+ if (px < tx - dead) out.push("right");
2392
+ else if (px > tx + dead) out.push("left");
2393
+ if (py < ty - dead) out.push("down");
2394
+ else if (py > ty + dead) out.push("up");
2395
+ }
2396
+
2397
+ // src/verify/feel.ts
2398
+ function recordTimeline(world, frames) {
2399
+ const out = [world.probe()];
2400
+ for (const f of frames) {
2401
+ world.step(f);
2402
+ out.push(world.probe());
2403
+ }
2404
+ return out;
2405
+ }
2406
+ function firstFrame(timeline, pred) {
2407
+ for (let i = 0; i < timeline.length; i++) if (pred(timeline[i])) return i;
2408
+ return -1;
2409
+ }
2410
+ function series(timeline, key) {
2411
+ return timeline.map((p) => p[key]);
2412
+ }
2413
+ function changeFrames(timeline, key) {
2414
+ const out = [];
2415
+ for (let i = 1; i < timeline.length; i++) if (timeline[i][key] !== timeline[i - 1][key]) out.push(i);
2416
+ return out;
2417
+ }
2418
+ function isMonotonic(values, dir, slack = 0) {
2419
+ for (let i = 1; i < values.length; i++) {
2420
+ if (dir === "up" ? values[i] < values[i - 1] - slack : values[i] > values[i - 1] + slack) return false;
2421
+ }
2422
+ return true;
2423
+ }
2424
+ function inputDensity(frames) {
2425
+ if (frames.length === 0) return 0;
2426
+ return frames.filter((f) => f.length > 0).length / frames.length;
2427
+ }
2428
+ function longestLull(eventFrames, totalFrames) {
2429
+ const pts = [0, ...eventFrames, totalFrames];
2430
+ let worst = 0;
2431
+ for (let i = 1; i < pts.length; i++) worst = Math.max(worst, pts[i] - pts[i - 1]);
2432
+ return worst;
2433
+ }
2434
+
2435
+ // src/verify/filmstrip.ts
2436
+ function renderFilmstrip(world, frames, opts) {
2437
+ const panels = Math.max(2, opts.panels ?? 12);
2438
+ const every = Math.max(1, Math.ceil(frames.length / (panels - 1)));
2439
+ const shots = [{ frame: 0, inner: commandsToSVGInner(world.render()) }];
2440
+ for (let i = 0; i < frames.length; i++) {
2441
+ world.step(frames[i]);
2442
+ if ((i + 1) % every === 0 || i === frames.length - 1) {
2443
+ shots.push({ frame: i + 1, inner: commandsToSVGInner(world.render()) });
2444
+ }
2445
+ }
2446
+ const cols = Math.max(1, opts.cols ?? 4);
2447
+ const pw = opts.panelWidth ?? 320;
2448
+ const ph = Math.round(pw * opts.height / opts.width);
2449
+ const labelH = 18;
2450
+ const gap = 8;
2451
+ const cellW = pw + gap;
2452
+ const cellH = ph + labelH + gap;
2453
+ const rows = Math.ceil(shots.length / cols);
2454
+ const totalW = cols * cellW + gap;
2455
+ const totalH = rows * cellH + gap;
2456
+ const bg = opts.background ?? "#ffffff";
2457
+ const cells = shots.map((s, i) => {
2458
+ const x = gap + i % cols * cellW;
2459
+ const y = gap + Math.floor(i / cols) * cellH;
2460
+ return `<g transform="translate(${x} ${y})"><svg width="${pw}" height="${ph}" viewBox="0 0 ${opts.width} ${opts.height}"><rect x="0" y="0" width="${opts.width}" height="${opts.height}" fill="${bg}"/>` + s.inner + `</svg><rect x="0" y="0" width="${pw}" height="${ph}" fill="none" stroke="#999" stroke-width="1"/><text x="${pw / 2}" y="${ph + 13}" font-size="11" font-family="monospace" text-anchor="middle" fill="#555">f ${s.frame} \xB7 ${(s.frame / 60).toFixed(1)}s</text></g>`;
2461
+ }).join("");
2462
+ return `<svg xmlns="http://www.w3.org/2000/svg" viewBox="0 0 ${totalW} ${totalH}" width="${totalW}" height="${totalH}"><rect x="0" y="0" width="${totalW}" height="${totalH}" fill="#f4f4f2"/>` + cells + `</svg>`;
2463
+ }
2464
+
2465
+ // src/world.ts
2466
+ var World = class {
2467
+ rng;
2468
+ clock;
2469
+ input = new InputState();
2470
+ events = new EventBus();
2471
+ resources = /* @__PURE__ */ new Map();
2472
+ /**
2473
+ * Canonical game state that lives OUTSIDE the scene tree (pure-sim structs,
2474
+ * controllers). Must be plain JSON-serializable data: it is included in
2475
+ * `hash()` and `snapshot()`, so hidden state here cannot escape determinism
2476
+ * checks the way ad-hoc module variables can.
2477
+ */
2478
+ state = {};
2479
+ width;
2480
+ height;
2481
+ root;
2482
+ activeCamera = null;
2483
+ seed;
2484
+ freeQueue = [];
2485
+ started = false;
2486
+ constructor(config = {}) {
2487
+ this.seed = config.seed ?? 1;
2488
+ this.rng = new Rng(this.seed);
2489
+ this.clock = new Clock(config.clock);
2490
+ this.width = config.width ?? 1280;
2491
+ this.height = config.height ?? 720;
2492
+ this.root = new Node({ name: "root" });
2493
+ }
2494
+ get time() {
2495
+ return this.clock.simTimeSec;
2496
+ }
2497
+ get frame() {
2498
+ return this.clock.frame;
2499
+ }
2500
+ /** Replace the scene root, entering the tree. */
2501
+ setRoot(node) {
2502
+ if (this.root) this.root.exitTree();
2503
+ this.root = node;
2504
+ this.started = false;
2505
+ }
2506
+ requestFree(node) {
2507
+ this.freeQueue.push(node);
2508
+ }
2509
+ ensureStarted() {
2510
+ if (!this.started) {
2511
+ this.root.enterTree(this);
2512
+ this.started = true;
2513
+ }
2514
+ }
2515
+ /**
2516
+ * Advance exactly one fixed step with the given actions held down.
2517
+ * This is THE deterministic transition — call it from Node or the browser loop.
2518
+ */
2519
+ step(actionsDown = []) {
2520
+ this.ensureStarted();
2521
+ this.input.beginFrame(actionsDown);
2522
+ this.root.updateTree(this.clock.dt);
2523
+ this.flushFree();
2524
+ this.clock.tick();
2525
+ }
2526
+ /** Feed real elapsed ms; runs 0+ fixed steps. Returns steps run. */
2527
+ advance(realMs, actionsDown = []) {
2528
+ const steps = this.clock.advance(realMs);
2529
+ for (let i = 0; i < steps; i++) this.step(actionsDown);
2530
+ return steps;
2531
+ }
2532
+ flushFree() {
2533
+ if (this.freeQueue.length === 0) return;
2534
+ const q = this.freeQueue;
2535
+ this.freeQueue = [];
2536
+ for (const node of q) {
2537
+ node.exitTree();
2538
+ node.parent?.removeChild(node);
2539
+ if (this.activeCamera === node) this.activeCamera = null;
2540
+ }
2541
+ }
2542
+ // ── Rendering ────────────────────────────────────────────────
2543
+ /** The view transform (inverse of the active camera), mapping world → screen. */
2544
+ viewTransform() {
2545
+ if (!this.activeCamera) return IDENTITY;
2546
+ const cam = this.activeCamera;
2547
+ const camWorld = cam.worldTransform();
2548
+ const centered = composeTransform(makeTransform({ x: this.width / 2, y: this.height / 2 }, 0, { x: cam.zoom, y: cam.zoom }), invertTransform(camWorld));
2549
+ return centered;
2550
+ }
2551
+ /** Project the whole scene to a display list (already camera-applied). */
2552
+ render() {
2553
+ this.ensureStarted();
2554
+ const out = [];
2555
+ this.root.collectDraw(out, this.viewTransform());
2556
+ return out;
2557
+ }
2558
+ // ── Determinism & saves ──────────────────────────────────────
2559
+ /** Deterministic structural hash of the whole sim state. */
2560
+ hash() {
2561
+ return hashValue({
2562
+ seed: this.seed,
2563
+ rng: this.rng.getState(),
2564
+ clock: this.clock.getState(),
2565
+ input: this.input.getState(),
2566
+ state: this.state,
2567
+ tree: this.root.serialize()
2568
+ });
2569
+ }
2570
+ /** Compact snapshot for undo/time-travel and saves. */
2571
+ snapshot() {
2572
+ return {
2573
+ seed: this.seed,
2574
+ rng: this.rng.getState(),
2575
+ clock: this.clock.getState(),
2576
+ input: this.input.getState(),
2577
+ state: structuredClone(this.state),
2578
+ tree: this.root.serialize()
2579
+ };
2580
+ }
2581
+ /** Restore a snapshot. Rebuilds the tree from data (behaviors are re-attached by scene code). */
2582
+ restore(snap) {
2583
+ this.seed = snap.seed;
2584
+ this.rng.setState(snap.rng);
2585
+ this.clock.setState(snap.clock);
2586
+ this.input.setState(snap.input);
2587
+ this.state = structuredClone(snap.state);
2588
+ resetNodeIds(1e6);
2589
+ this.setRoot(deserializeNode(snap.tree));
2590
+ this.ensureStarted();
2591
+ }
2592
+ /** A compact probe snapshot for the verification harness (override-friendly). */
2593
+ probe() {
2594
+ return {
2595
+ frame: this.frame,
2596
+ time: this.time,
2597
+ hash: this.hash(),
2598
+ nodes: this.root.query("Sprite").length + this.root.query("Text").length
2599
+ };
2600
+ }
2601
+ };
2602
+
2603
+ // src/app/game.ts
2604
+ function defineGame(def) {
2605
+ return {
2606
+ width: 1280,
2607
+ height: 720,
2608
+ seed: 1,
2609
+ background: "#f3ecdb",
2610
+ inputMap: DEFAULT_INPUT_MAP,
2611
+ ...def
2612
+ };
2613
+ }
2614
+ function createWorld(def, seedOverride) {
2615
+ const world = new World({
2616
+ seed: seedOverride ?? def.seed ?? 1,
2617
+ width: def.width ?? 1280,
2618
+ height: def.height ?? 720,
2619
+ clock: def.clock
2620
+ });
2621
+ world.setRoot(def.build(world));
2622
+ if (def.probe) {
2623
+ world.probe = () => def.probe(world);
2624
+ }
2625
+ return world;
2626
+ }
2627
+ function gameInputMap(def) {
2628
+ return def.inputMap ?? DEFAULT_INPUT_MAP;
2629
+ }
2630
+ function runHeadless(def, inputLog) {
2631
+ const world = createWorld(def);
2632
+ const frames = inputLog?.frames ?? [];
2633
+ for (const f of frames) world.step(f);
2634
+ return { world, hash: world.hash(), steps: frames.length };
2635
+ }
2636
+
2637
+ // src/app/browser.ts
2638
+ function runBrowser(def, mount, opts = {}) {
2639
+ const width = def.width ?? 1280;
2640
+ const height = def.height ?? 720;
2641
+ const background = def.background ?? "#f3ecdb";
2642
+ let world = createWorld(def);
2643
+ const renderer = opts.renderer === "canvas" ? new Canvas2DRenderer({ width, height, background }) : new SvgRenderer({ width, height, background });
2644
+ mount.style.position = mount.style.position || "relative";
2645
+ renderer.mount?.(mount);
2646
+ setOverlayHost(mount);
2647
+ const input = new KeyboardSource(def.inputMap ?? {}, document);
2648
+ const capture = isCaptureMode();
2649
+ const startAudio = () => {
2650
+ audio.start();
2651
+ const s = settings.get();
2652
+ audio.setVolumes(s);
2653
+ window.removeEventListener("pointerdown", startAudio);
2654
+ window.removeEventListener("keydown", startAudio);
2655
+ };
2656
+ window.addEventListener("pointerdown", startAudio);
2657
+ window.addEventListener("keydown", startAudio);
2658
+ const renderFrame = () => renderer.draw(world.render());
2659
+ const restart = () => {
2660
+ world = createWorld(def);
2661
+ renderFrame();
2662
+ };
2663
+ const shell = opts.shell === false ? null : new Shell({
2664
+ title: def.title,
2665
+ onRestart: opts.onRestart ?? restart,
2666
+ onPause: () => {
2667
+ }
2668
+ });
2669
+ let raf = 0;
2670
+ let last = performance.now();
2671
+ const loop = (now) => {
2672
+ const dt = now - last;
2673
+ last = now;
2674
+ if (!capture && !shell?.isPaused) {
2675
+ const steps = world.advance(dt, input.currentActions());
2676
+ if (steps > 0) input.clearPressed();
2677
+ }
2678
+ renderFrame();
2679
+ raf = requestAnimationFrame(loop);
2680
+ };
2681
+ raf = requestAnimationFrame(loop);
2682
+ const handle = {
2683
+ get world() {
2684
+ return world;
2685
+ },
2686
+ renderer,
2687
+ input,
2688
+ stop() {
2689
+ cancelAnimationFrame(raf);
2690
+ input.dispose();
2691
+ shell?.dispose();
2692
+ renderer.dispose?.();
2693
+ },
2694
+ restart
2695
+ };
2696
+ if (capture) {
2697
+ installCapture({
2698
+ get world() {
2699
+ return world;
2700
+ },
2701
+ stepOnce: (actions = []) => {
2702
+ world.step(actions);
2703
+ renderFrame();
2704
+ },
2705
+ renderSVG: () => renderToSVGString(world.render(), width, height, background),
2706
+ setPaused: () => {
2707
+ }
2708
+ });
2709
+ }
2710
+ return handle;
2711
+ }
2712
+
2713
+ // src/index.ts
2714
+ var VERSION = "0.1.0";
2715
+ export {
2716
+ AnimationPlayer,
2717
+ AudioBus,
2718
+ Camera2D,
2719
+ Canvas2DRenderer,
2720
+ Clock,
2721
+ DEFAULT_INPUT_MAP,
2722
+ DEFAULT_PLATFORMER,
2723
+ DEFAULT_TILE_CHARS,
2724
+ DUSK,
2725
+ EASINGS,
2726
+ EventBus,
2727
+ HeadlessRenderer,
2728
+ IDENTITY,
2729
+ InputRecorder,
2730
+ InputState,
2731
+ KeyboardSource,
2732
+ MEADOW,
2733
+ Node,
2734
+ Node as Node2D,
2735
+ PAD_NEUTRAL,
2736
+ PALETTES,
2737
+ PAPER,
2738
+ PARTICLE_PRESETS,
2739
+ Particles,
2740
+ Rng,
2741
+ Shaker,
2742
+ Shell,
2743
+ Signal,
2744
+ SpatialHash,
2745
+ Sprite,
2746
+ SvgRenderer,
2747
+ TAU,
2748
+ TILE,
2749
+ Text,
2750
+ Timer,
2751
+ VERSION,
2752
+ World,
2753
+ applyTransform,
2754
+ asciiEntities,
2755
+ assertDeterministic,
2756
+ assertSnapshotStable,
2757
+ assertSolvable,
2758
+ audio,
2759
+ blobPath,
2760
+ changeFrames,
2761
+ checkDeterministic,
2762
+ clamp,
2763
+ commandsToSVGInner,
2764
+ composeTransform,
2765
+ createPlanBot,
2766
+ createPlatformerState,
2767
+ createWorld,
2768
+ dashJumpDistance,
2769
+ defineGame,
2770
+ deg2rad,
2771
+ deserializeNode,
2772
+ drive,
2773
+ firstFrame,
2774
+ frameActions,
2775
+ gameInputMap,
2776
+ groundAt,
2777
+ hashString,
2778
+ hashValue,
2779
+ hideScreen,
2780
+ hold,
2781
+ inVisionCone,
2782
+ inputDensity,
2783
+ installCapture,
2784
+ invLerp,
2785
+ invertTransform,
2786
+ isCaptureMode,
2787
+ isMonotonic,
2788
+ jumpAirtime,
2789
+ jumpDistance,
2790
+ jumpHeight,
2791
+ keysToActions,
2792
+ lerp,
2793
+ lineOfSight,
2794
+ longestLull,
2795
+ makeTransform,
2796
+ mapHeight,
2797
+ mapWidth,
2798
+ mix,
2799
+ moveRect,
2800
+ pump,
2801
+ rad2deg,
2802
+ raycastTiles,
2803
+ recordTimeline,
2804
+ rectBlocked,
2805
+ rectContains,
2806
+ rectsOverlap,
2807
+ registerNode,
2808
+ regularPolygon,
2809
+ remap,
2810
+ renderFilmstrip,
2811
+ renderToSVGString,
2812
+ replay,
2813
+ resetNodeIds,
2814
+ runBrowser,
2815
+ runHeadless,
2816
+ scriptToFrames,
2817
+ scriptedPlaythrough,
2818
+ series,
2819
+ setOverlayHost,
2820
+ settings,
2821
+ showScreen,
2822
+ smoothClosedPath,
2823
+ smoothOpenPath,
2824
+ solve,
2825
+ sortCommands,
2826
+ star,
2827
+ steer2D,
2828
+ stepPlatformer,
2829
+ tileAt,
2830
+ tileAtPoint,
2831
+ tilemapFromAscii,
2832
+ toggleFullscreen,
2833
+ vadd,
2834
+ vdist,
2835
+ vdot,
2836
+ vec2,
2837
+ vlen,
2838
+ vnorm,
2839
+ vscale,
2840
+ vsub,
2841
+ wait,
2842
+ withAlpha
2843
+ };
2844
+ //# sourceMappingURL=index.js.map