hayao 0.1.0 → 0.2.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/README.md +29 -12
  2. package/dist/art/autotile.d.ts +77 -0
  3. package/dist/art/bitmapFont.d.ts +113 -0
  4. package/dist/art/font5.d.ts +11 -0
  5. package/dist/art/palette.d.ts +38 -0
  6. package/dist/art/texture.d.ts +78 -0
  7. package/dist/audio/audio.d.ts +7 -0
  8. package/dist/content/dsl.d.ts +61 -0
  9. package/dist/core/dmath.d.ts +20 -0
  10. package/dist/core/math.d.ts +2 -0
  11. package/dist/index.d.ts +37 -1
  12. package/dist/index.js +4549 -69
  13. package/dist/index.js.map +4 -4
  14. package/dist/logic/fsm.d.ts +85 -0
  15. package/dist/logic/graph.d.ts +88 -0
  16. package/dist/logic/history.d.ts +54 -0
  17. package/dist/logic/random.d.ts +32 -0
  18. package/dist/net/browser.d.ts +37 -0
  19. package/dist/net/inputBuffer.d.ts +27 -0
  20. package/dist/net/lockstep.d.ts +79 -0
  21. package/dist/net/players.d.ts +27 -0
  22. package/dist/net/protocol.d.ts +100 -0
  23. package/dist/net/rollback.d.ts +89 -0
  24. package/dist/net/room.d.ts +78 -0
  25. package/dist/net/transport.d.ts +78 -0
  26. package/dist/persist/codec.d.ts +4 -0
  27. package/dist/persist/save.d.ts +32 -0
  28. package/dist/persist/storage.d.ts +46 -0
  29. package/dist/physics/rigidBody.d.ts +104 -0
  30. package/dist/physics/rigidCollide.d.ts +16 -0
  31. package/dist/physics/rigidJoints.d.ts +65 -0
  32. package/dist/physics/rigidQueries.d.ts +15 -0
  33. package/dist/physics/rigidStep.d.ts +14 -0
  34. package/dist/procgen/cave.d.ts +21 -0
  35. package/dist/procgen/grid.d.ts +21 -0
  36. package/dist/procgen/rooms.d.ts +34 -0
  37. package/dist/procgen/scatter.d.ts +32 -0
  38. package/dist/procgen/terrain.d.ts +24 -0
  39. package/dist/render/nineSlice.d.ts +32 -0
  40. package/dist/scene/floatingText.d.ts +51 -0
  41. package/dist/scene/particles.d.ts +64 -0
  42. package/dist/scene/pool.d.ts +16 -0
  43. package/dist/scene/tween.d.ts +26 -0
  44. package/dist/ui/transition.d.ts +107 -0
  45. package/dist/verify/layout.d.ts +39 -0
  46. package/docs/API.md +247 -8
  47. package/package.json +31 -9
package/dist/index.js CHANGED
@@ -1,11 +1,179 @@
1
+ // src/core/dmath.ts
2
+ var INV_PIO2 = 0.6366197723675814;
3
+ var PIO2_HI = 1.5707963267341256;
4
+ var PIO2_LO = 6077100506506192e-26;
5
+ var S1 = -0.16666666666666632;
6
+ var S2 = 0.00833333333332249;
7
+ var S3 = -1984126982985795e-19;
8
+ var S4 = 27557313707070068e-22;
9
+ var S5 = -25050760253406863e-24;
10
+ var S6 = 158969099521155e-24;
11
+ var C1 = 0.0416666666666666;
12
+ var C2 = -0.001388888888887411;
13
+ var C3 = 2480158728947673e-20;
14
+ var C4 = -27557314351390663e-23;
15
+ var C5 = 2087572321298175e-24;
16
+ var C6 = -11359647557788195e-27;
17
+ function kSin(r) {
18
+ const z = r * r;
19
+ return r + r * z * (S1 + z * (S2 + z * (S3 + z * (S4 + z * (S5 + z * S6)))));
20
+ }
21
+ function kCos(r) {
22
+ const z = r * r;
23
+ return 1 - 0.5 * z + z * z * (C1 + z * (C2 + z * (C3 + z * (C4 + z * (C5 + z * C6)))));
24
+ }
25
+ function reduce(x) {
26
+ const n2 = Math.round(x * INV_PIO2);
27
+ const r = x - n2 * PIO2_HI - n2 * PIO2_LO;
28
+ return { n: (n2 % 4 + 4) % 4, r };
29
+ }
30
+ function dsin(x) {
31
+ if (!Number.isFinite(x)) return NaN;
32
+ const { n: n2, r } = reduce(x);
33
+ switch (n2) {
34
+ case 0:
35
+ return kSin(r);
36
+ case 1:
37
+ return kCos(r);
38
+ case 2:
39
+ return -kSin(r);
40
+ default:
41
+ return -kCos(r);
42
+ }
43
+ }
44
+ function dcos(x) {
45
+ if (!Number.isFinite(x)) return NaN;
46
+ const { n: n2, r } = reduce(x);
47
+ switch (n2) {
48
+ case 0:
49
+ return kCos(r);
50
+ case 1:
51
+ return -kSin(r);
52
+ case 2:
53
+ return -kCos(r);
54
+ default:
55
+ return kSin(r);
56
+ }
57
+ }
58
+ var ATAN_HI = [0.4636476090008061, 0.7853981633974483, 0.982793723247329, 1.5707963267948966];
59
+ var ATAN_LO = [22698777452961687e-33, 3061616997868383e-32, 13903311031230998e-33, 6123233995736766e-32];
60
+ var AT = [
61
+ 0.3333333333333293,
62
+ -0.19999999999876483,
63
+ 0.14285714272503466,
64
+ -0.11111110405462356,
65
+ 0.09090887133436507,
66
+ -0.0769187620504483,
67
+ 0.06661073137387531,
68
+ -0.058335701337905735,
69
+ 0.049768779946159324,
70
+ -0.036531572744216916,
71
+ 0.016285820115365782
72
+ ];
73
+ function datan(x) {
74
+ if (Number.isNaN(x)) return NaN;
75
+ if (!Number.isFinite(x)) return x > 0 ? ATAN_HI[3] : -ATAN_HI[3];
76
+ const sign = x < 0 || Object.is(x, -0) ? -1 : 1;
77
+ let ax = Math.abs(x);
78
+ if (ax >= 1e19) return sign * (ATAN_HI[3] + ATAN_LO[3]);
79
+ let id = -1;
80
+ if (ax < 0.4375) {
81
+ if (ax < 1e-9) return x;
82
+ } else if (ax < 0.6875) {
83
+ id = 0;
84
+ ax = (2 * ax - 1) / (2 + ax);
85
+ } else if (ax < 1.1875) {
86
+ id = 1;
87
+ ax = (ax - 1) / (ax + 1);
88
+ } else if (ax < 2.4375) {
89
+ id = 2;
90
+ ax = (ax - 1.5) / (1 + 1.5 * ax);
91
+ } else {
92
+ id = 3;
93
+ ax = -1 / ax;
94
+ }
95
+ const z = ax * ax;
96
+ const w = z * z;
97
+ const s1 = z * (AT[0] + w * (AT[2] + w * (AT[4] + w * (AT[6] + w * (AT[8] + w * AT[10])))));
98
+ const s2 = w * (AT[1] + w * (AT[3] + w * (AT[5] + w * (AT[7] + w * AT[9]))));
99
+ if (id < 0) return sign * (ax - ax * (s1 + s2));
100
+ const r = ATAN_HI[id] - (ax * (s1 + s2) - ATAN_LO[id] - ax);
101
+ return sign * r;
102
+ }
103
+ var PI = 3.141592653589793;
104
+ function datan2(y, x) {
105
+ if (Number.isNaN(x) || Number.isNaN(y)) return NaN;
106
+ if (y === 0 && x === 0) return Object.is(x, -0) ? Object.is(y, -0) ? -PI : PI : Object.is(y, -0) ? -0 : 0;
107
+ if (x === 0 || !Number.isFinite(y) && Number.isFinite(x)) return y > 0 ? PI / 2 : -PI / 2;
108
+ if (!Number.isFinite(x)) {
109
+ if (!Number.isFinite(y)) {
110
+ const q = x > 0 ? PI / 4 : 3 * PI / 4;
111
+ return y > 0 ? q : -q;
112
+ }
113
+ return x > 0 ? y < 0 || Object.is(y, -0) ? -0 : 0 : y < 0 || Object.is(y, -0) ? -PI : PI;
114
+ }
115
+ const a = datan(y / x);
116
+ if (x > 0) return a;
117
+ return y < 0 || Object.is(y, -0) ? a - PI : a + PI;
118
+ }
119
+ var LN2 = 0.6931471805599453;
120
+ function dexp2(x) {
121
+ if (Number.isNaN(x)) return NaN;
122
+ if (x >= 1024) return Infinity;
123
+ if (x <= -1075) return 0;
124
+ const k = Math.round(x);
125
+ const y = (x - k) * LN2;
126
+ let term = 1;
127
+ let sum = 1;
128
+ for (let i = 1; i <= 13; i++) {
129
+ term = term * y / i;
130
+ sum += term;
131
+ }
132
+ return sum * 2 ** k;
133
+ }
134
+ function dexp(x) {
135
+ return dexp2(x * INV_LN2);
136
+ }
137
+ function dhypot(x, y) {
138
+ return Math.sqrt(x * x + y * y);
139
+ }
140
+ var logView = new DataView(new ArrayBuffer(8));
141
+ var LN2_HI = 0.6931471803691238;
142
+ var LN2_LO = 19082149292705877e-26;
143
+ function dlog(x) {
144
+ if (Number.isNaN(x) || x < 0) return NaN;
145
+ if (x === 0) return -Infinity;
146
+ if (!Number.isFinite(x)) return Infinity;
147
+ logView.setFloat64(0, x);
148
+ let e = (logView.getUint32(0) >>> 20 & 2047) - 1023;
149
+ if (e === -1023) {
150
+ logView.setFloat64(0, x * 18014398509481984);
151
+ e = (logView.getUint32(0) >>> 20 & 2047) - 1023 - 54;
152
+ }
153
+ logView.setUint32(0, logView.getUint32(0) & 1048575 | 1023 << 20);
154
+ let m = logView.getFloat64(0);
155
+ if (m > 1.4142135623730951) {
156
+ m *= 0.5;
157
+ e += 1;
158
+ }
159
+ const s = (m - 1) / (m + 1);
160
+ const z = s * s;
161
+ const p = 1 + z * (0.3333333333333333 + z * (0.2 + z * (0.14285714285714285 + z * (0.1111111111111111 + z * (0.09090909090909091 + z * (0.07692307692307693 + z * (0.06666666666666667 + z * (0.058823529411764705 + z * 0.05263157894736842))))))));
162
+ return e * LN2_HI + (2 * s * p + e * LN2_LO);
163
+ }
164
+ var INV_LN10 = 0.4342944819032518;
165
+ var INV_LN2 = 1.4426950408889634;
166
+ var dlog10 = (x) => dlog(x) * INV_LN10;
167
+ var dlog2 = (x) => dlog(x) * INV_LN2;
168
+
1
169
  // src/core/math.ts
2
170
  var vec2 = (x = 0, y = 0) => ({ x, y });
3
171
  var vadd = (a, b) => ({ x: a.x + b.x, y: a.y + b.y });
4
172
  var vsub = (a, b) => ({ x: a.x - b.x, y: a.y - b.y });
5
173
  var vscale = (a, s) => ({ x: a.x * s, y: a.y * s });
6
174
  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);
175
+ var vlen = (a) => dhypot(a.x, a.y);
176
+ var vdist = (a, b) => dhypot(a.x - b.x, a.y - b.y);
9
177
  function vnorm(a) {
10
178
  const l = vlen(a);
11
179
  return l === 0 ? { x: 0, y: 0 } : { x: a.x / l, y: a.y / l };
@@ -14,6 +182,10 @@ var clamp = (v, lo, hi) => v < lo ? lo : v > hi ? hi : v;
14
182
  var lerp = (a, b, t) => a + (b - a) * t;
15
183
  var invLerp = (a, b, v) => a === b ? 0 : (v - a) / (b - a);
16
184
  var remap = (v, a0, a1, b0, b1) => lerp(b0, b1, invLerp(a0, a1, v));
185
+ var smoothstep = (edge0, edge1, v) => {
186
+ const t = clamp(edge0 === edge1 ? v < edge0 ? 0 : 1 : invLerp(edge0, edge1, v), 0, 1);
187
+ return t * t * (3 - 2 * t);
188
+ };
17
189
  var TAU = Math.PI * 2;
18
190
  var deg2rad = (d) => d * Math.PI / 180;
19
191
  var rad2deg = (r) => r * 180 / Math.PI;
@@ -35,8 +207,8 @@ function composeTransform(m, n2) {
35
207
  };
36
208
  }
37
209
  function makeTransform(pos, rotation, scale) {
38
- const cos = Math.cos(rotation);
39
- const sin = Math.sin(rotation);
210
+ const cos = dcos(rotation);
211
+ const sin = dsin(rotation);
40
212
  return {
41
213
  a: cos * scale.x,
42
214
  b: sin * scale.x,
@@ -309,7 +481,7 @@ function hashValue(value) {
309
481
  return;
310
482
  }
311
483
  const obj = v;
312
- const keys = Object.keys(obj).sort();
484
+ const keys = Object.keys(obj).filter((k) => obj[k] !== void 0).sort();
313
485
  push("{");
314
486
  for (const k of keys) {
315
487
  push(k + ":");
@@ -648,27 +820,74 @@ var Timer = class extends Node {
648
820
  }
649
821
  };
650
822
 
823
+ // src/scene/pool.ts
824
+ var NodePool = class {
825
+ constructor(parent, make) {
826
+ this.parent = parent;
827
+ this.make = make;
828
+ }
829
+ parent;
830
+ make;
831
+ items = [];
832
+ used = 0;
833
+ /** Start a frame: every pooled node is up for reuse. */
834
+ begin() {
835
+ this.used = 0;
836
+ }
837
+ /** Claim the next node (created and parented on first use), made visible. */
838
+ get() {
839
+ if (this.used === this.items.length) this.items.push(this.parent.addChild(this.make()));
840
+ const n2 = this.items[this.used++];
841
+ n2.visible = true;
842
+ return n2;
843
+ }
844
+ /** End a frame: hide every node not claimed since begin(). */
845
+ end() {
846
+ for (let i = this.used; i < this.items.length; i++) this.items[i].visible = false;
847
+ }
848
+ /** Nodes claimed this frame (call after the get() loop). */
849
+ get liveCount() {
850
+ return this.used;
851
+ }
852
+ };
853
+
651
854
  // src/scene/tween.ts
652
- var pow = Math.pow;
855
+ var lerpDamp = (current, target, lambda, dt) => lerp(target, current, dexp(-lambda * dt));
856
+ var spring = (value = 0) => ({ value, vel: 0 });
857
+ function springStep(s, target, omega, dt) {
858
+ const decay = dexp(-omega * dt);
859
+ const d = s.value - target;
860
+ const c = s.vel + omega * d;
861
+ s.value = target + (d + c * dt) * decay;
862
+ s.vel = (s.vel - c * omega * dt) * decay;
863
+ return s;
864
+ }
865
+ function makeReach(start = 0, settle = 0.25) {
866
+ const omega = 4 / Math.max(1e-4, settle);
867
+ const s = spring(start);
868
+ return (target, dt) => springStep(s, target, omega, dt).value;
869
+ }
870
+ var sq = (x) => x * x;
871
+ var cube = (x) => x * x * x;
653
872
  var EASINGS = {
654
873
  linear: (t) => t,
655
874
  quadIn: (t) => t * t,
656
875
  quadOut: (t) => 1 - (1 - t) * (1 - t),
657
- quadInOut: (t) => t < 0.5 ? 2 * t * t : 1 - pow(-2 * t + 2, 2) / 2,
876
+ quadInOut: (t) => t < 0.5 ? 2 * t * t : 1 - sq(-2 * t + 2) / 2,
658
877
  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,
878
+ cubicOut: (t) => 1 - cube(1 - t),
879
+ cubicInOut: (t) => t < 0.5 ? 4 * t * t * t : 1 - cube(-2 * t + 2) / 2,
880
+ sineIn: (t) => 1 - dcos(t * Math.PI / 2),
881
+ sineOut: (t) => dsin(t * Math.PI / 2),
882
+ sineInOut: (t) => -(dcos(Math.PI * t) - 1) / 2,
664
883
  backOut: (t) => {
665
884
  const c1 = 1.70158;
666
885
  const c3 = c1 + 1;
667
- return 1 + c3 * pow(t - 1, 3) + c1 * pow(t - 1, 2);
886
+ return 1 + c3 * cube(t - 1) + c1 * sq(t - 1);
668
887
  },
669
888
  elasticOut: (t) => {
670
889
  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;
890
+ return t === 0 ? 0 : t === 1 ? 1 : dexp2(-10 * t) * dsin((t * 10 - 0.75) * c4) + 1;
672
891
  },
673
892
  bounceOut: (t) => {
674
893
  const n1 = 7.5625;
@@ -740,16 +959,16 @@ var Particles = class extends Node {
740
959
  this.maxParticles = config.maxParticles ?? 512;
741
960
  }
742
961
  /** Emit a burst at a position (in this node's local space). */
743
- burst(count, at, style) {
962
+ burst(count, at2, style) {
744
963
  const r = this.rng;
745
964
  for (let i = 0; i < count; i++) {
746
965
  const angle = style.angle !== void 0 ? style.angle + (r.float() - 0.5) * (style.spread ?? 0.6) : r.float() * TAU;
747
966
  const speed = style.speedMin + r.float() * (style.speedMax - style.speedMin);
748
967
  const p = {
749
- x: at.x,
750
- y: at.y,
751
- vx: Math.cos(angle) * speed,
752
- vy: Math.sin(angle) * speed,
968
+ x: at2.x,
969
+ y: at2.y,
970
+ vx: dcos(angle) * speed,
971
+ vy: dsin(angle) * speed,
753
972
  life: 0,
754
973
  maxLife: style.lifeMin + r.float() * (style.lifeMax - style.lifeMin),
755
974
  size: style.sizeMin + r.float() * (style.sizeMax - style.sizeMin),
@@ -805,6 +1024,114 @@ var PARTICLE_PRESETS = {
805
1024
  hit: (colors = ["#ff5e5e", "#ffd0d0"]) => ({ colors, sizeMin: 2, sizeMax: 4, speedMin: 160, speedMax: 380, lifeMin: 0.15, lifeMax: 0.35, drag: 6, shrink: true }),
806
1025
  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
1026
  };
1027
+ function weatherEnvelope(t, keys) {
1028
+ if (keys.length === 0) return 1;
1029
+ if (t <= keys[0].time) return keys[0].intensity;
1030
+ for (let i = 1; i < keys.length; i++) {
1031
+ if (t <= keys[i].time) {
1032
+ const a = keys[i - 1];
1033
+ const b = keys[i];
1034
+ return a.intensity + (b.intensity - a.intensity) * smoothstep(a.time, b.time, t);
1035
+ }
1036
+ }
1037
+ return keys[keys.length - 1].intensity;
1038
+ }
1039
+ var AmbientField = class extends Node {
1040
+ type = "AmbientField";
1041
+ rng;
1042
+ field = [];
1043
+ time = 0;
1044
+ style;
1045
+ /** Field region (px). Particles wrap within [0,width] × [0,height]. */
1046
+ width;
1047
+ height;
1048
+ /** Optional weather intensity schedule; scales count/opacity. */
1049
+ envelope;
1050
+ constructor(config) {
1051
+ super(config);
1052
+ this.cosmetic = true;
1053
+ this.rng = new Rng(config.seed ?? 11);
1054
+ this.width = config.width;
1055
+ this.height = config.height;
1056
+ this.style = config.style;
1057
+ this.envelope = config.envelope;
1058
+ const count = config.count ?? 120;
1059
+ const s = this.style;
1060
+ for (let i = 0; i < count; i++) {
1061
+ this.field.push({
1062
+ x: this.rng.float() * this.width,
1063
+ y: this.rng.float() * this.height,
1064
+ size: s.sizeMin + this.rng.float() * (s.sizeMax - s.sizeMin),
1065
+ color: s.colors[this.rng.int(s.colors.length)],
1066
+ phase: this.rng.float() * TAU,
1067
+ depth: 0.5 + this.rng.float() * 0.5
1068
+ });
1069
+ }
1070
+ }
1071
+ intensity() {
1072
+ return this.envelope ? weatherEnvelope(this.time, this.envelope) : 1;
1073
+ }
1074
+ onProcess(dt) {
1075
+ this.time += dt;
1076
+ const s = this.style;
1077
+ const wind = s.windX ?? 0;
1078
+ const w = this.width;
1079
+ const h = this.height;
1080
+ for (const p of this.field) {
1081
+ p.y += s.fallY * p.depth * dt;
1082
+ p.x += wind * dt;
1083
+ if (p.y > h) p.y -= h;
1084
+ else if (p.y < 0) p.y += h;
1085
+ if (p.x > w) p.x -= w;
1086
+ else if (p.x < 0) p.x += w;
1087
+ }
1088
+ }
1089
+ draw(out, world) {
1090
+ const s = this.style;
1091
+ const intensity = this.intensity();
1092
+ if (intensity <= 0) return;
1093
+ const shown = Math.round(this.field.length * intensity);
1094
+ const swayAmp = s.swayAmp ?? 0;
1095
+ const swayFreq = s.swayFreq ?? 0;
1096
+ for (let i = 0; i < shown; i++) {
1097
+ const p = this.field[i];
1098
+ const sway = swayAmp !== 0 ? dsin(this.time * swayFreq * TAU + p.phase) * swayAmp : 0;
1099
+ const x = p.x + sway;
1100
+ if (s.streak) {
1101
+ const len = s.streakLen ?? s.fallY * 0.03;
1102
+ out.push({
1103
+ kind: "poly",
1104
+ points: [x, p.y, x, p.y + len],
1105
+ closed: false,
1106
+ stroke: p.color,
1107
+ strokeWidth: p.size,
1108
+ opacity: intensity,
1109
+ transform: world,
1110
+ z: this.z
1111
+ });
1112
+ } else {
1113
+ out.push({
1114
+ kind: "circle",
1115
+ cx: x,
1116
+ cy: p.y,
1117
+ radius: p.size,
1118
+ fill: p.color,
1119
+ opacity: intensity,
1120
+ transform: world,
1121
+ z: this.z
1122
+ });
1123
+ }
1124
+ }
1125
+ }
1126
+ get liveCount() {
1127
+ return this.field.length;
1128
+ }
1129
+ };
1130
+ var AMBIENT_PRESETS = {
1131
+ snow: (colors = ["#ffffff", "#e8f0f8", "#cdd9e6"]) => ({ colors, sizeMin: 1.5, sizeMax: 3.5, windX: 8, fallY: 42, swayAmp: 14, swayFreq: 0.25 }),
1132
+ rain: (colors = ["#9fb4c8", "#c3d2e0"]) => ({ colors, sizeMin: 1, sizeMax: 1.8, windX: -40, fallY: 620, streak: true, streakLen: 16 }),
1133
+ ash: (colors = ["#6b6b6b", "#8a8580", "#3d3a36"]) => ({ colors, sizeMin: 1, sizeMax: 2.5, windX: 14, fallY: 26, swayAmp: 20, swayFreq: 0.18 })
1134
+ };
808
1135
  var Shaker = class extends Node {
809
1136
  type = "Shaker";
810
1137
  rng;
@@ -832,6 +1159,86 @@ var Shaker = class extends Node {
832
1159
  }
833
1160
  };
834
1161
 
1162
+ // src/scene/floatingText.ts
1163
+ var FloatingText = class extends Node {
1164
+ type = "FloatingText";
1165
+ pool = [];
1166
+ rng;
1167
+ /** Cap on live popups (oldest recycled first). */
1168
+ maxPopups;
1169
+ constructor(config = {}) {
1170
+ super(config);
1171
+ this.cosmetic = true;
1172
+ this.rng = new Rng(config.seed ?? 31);
1173
+ this.maxPopups = config.maxPopups ?? 128;
1174
+ }
1175
+ /** Spawn one popup at `at` (in this node's local space). */
1176
+ pop(text, at2, style) {
1177
+ const jitter = style.jitter ?? 0;
1178
+ const p = {
1179
+ x: at2.x + (this.rng.float() - 0.5) * jitter * 0.1,
1180
+ y: at2.y,
1181
+ vx: (this.rng.float() - 0.5) * jitter,
1182
+ vy: -(style.rise ?? 60),
1183
+ life: 0,
1184
+ maxLife: Math.max(0.05, style.life ?? 0.9),
1185
+ text,
1186
+ color: style.color,
1187
+ size: style.size ?? 20,
1188
+ font: style.font,
1189
+ weight: style.weight,
1190
+ align: style.align ?? "center",
1191
+ fade: clamp(style.fade ?? 0.4, 0, 1)
1192
+ };
1193
+ this.gravity = style.gravity ?? 0;
1194
+ if (this.pool.length >= this.maxPopups) this.pool.shift();
1195
+ this.pool.push(p);
1196
+ }
1197
+ gravity = 0;
1198
+ onProcess(dt) {
1199
+ let write = 0;
1200
+ for (const p of this.pool) {
1201
+ p.life += dt;
1202
+ if (p.life >= p.maxLife) continue;
1203
+ p.vy += this.gravity * dt;
1204
+ p.x += p.vx * dt;
1205
+ p.y += p.vy * dt;
1206
+ this.pool[write++] = p;
1207
+ }
1208
+ this.pool.length = write;
1209
+ }
1210
+ draw(out, world) {
1211
+ for (const p of this.pool) {
1212
+ const t = p.life / p.maxLife;
1213
+ const fadeStart = 1 - p.fade;
1214
+ const opacity = t < fadeStart ? 1 : clamp(1 - (t - fadeStart) / Math.max(1e-4, p.fade), 0, 1);
1215
+ out.push({
1216
+ kind: "text",
1217
+ text: p.text,
1218
+ x: p.x,
1219
+ y: p.y,
1220
+ size: p.size,
1221
+ font: p.font,
1222
+ weight: p.weight,
1223
+ align: p.align,
1224
+ fill: p.color,
1225
+ opacity,
1226
+ transform: world,
1227
+ z: this.z
1228
+ });
1229
+ }
1230
+ }
1231
+ get liveCount() {
1232
+ return this.pool.length;
1233
+ }
1234
+ };
1235
+ var FLOAT_PRESETS = {
1236
+ damage: (color = "#e14b4b") => ({ color, size: 22, weight: 700, rise: 70, gravity: 120, life: 0.8, jitter: 40, fade: 0.4 }),
1237
+ crit: (color = "#ffb020") => ({ color, size: 32, weight: 800, rise: 100, gravity: 160, life: 1, jitter: 60, fade: 0.35 }),
1238
+ heal: (color = "#4bb06a") => ({ color, size: 20, weight: 700, rise: 55, gravity: 40, life: 1, jitter: 20, fade: 0.5 }),
1239
+ label: (color = "#3d3323") => ({ color, size: 18, weight: 600, rise: 40, gravity: 0, life: 1.2, jitter: 0, fade: 0.5 })
1240
+ };
1241
+
835
1242
  // src/scene/registry.ts
836
1243
  var registry = /* @__PURE__ */ new Map();
837
1244
  function registerNode(type, factory) {
@@ -1205,7 +1612,7 @@ function stepPlatformer(s, input, dt, map, cfg = DEFAULT_PLATFORMER, platforms =
1205
1612
  let dx2 = input.moveX;
1206
1613
  let dy2 = input.moveY;
1207
1614
  if (dx2 === 0 && dy2 === 0) dx2 = s.facing;
1208
- const inv = 1 / Math.hypot(dx2, dy2);
1615
+ const inv = 1 / dhypot(dx2, dy2);
1209
1616
  s.dashing = cfg.dashTime;
1210
1617
  s.dashCd = cfg.dashCooldown;
1211
1618
  s.dashesLeft--;
@@ -1422,7 +1829,7 @@ function raycastTiles(map, x0, y0, x1, y1) {
1422
1829
  const ts = map.tileSize;
1423
1830
  const dx = x1 - x0;
1424
1831
  const dy = y1 - y0;
1425
- const maxDist = Math.hypot(dx, dy);
1832
+ const maxDist = dhypot(dx, dy);
1426
1833
  if (maxDist === 0) return { blocked: false, x: x1, y: y1, dist: 0 };
1427
1834
  const dirX = dx / maxDist;
1428
1835
  const dirY = dy / maxDist;
@@ -1458,13 +1865,855 @@ function lineOfSight(map, ax, ay, bx, by) {
1458
1865
  function inVisionCone(map, ex, ey, faceX, faceY, fov, range, tx, ty) {
1459
1866
  const dx = tx - ex;
1460
1867
  const dy = ty - ey;
1461
- const d = Math.hypot(dx, dy);
1868
+ const d = dhypot(dx, dy);
1462
1869
  if (d > range || d === 0) return false;
1463
1870
  const dot = (dx * faceX + dy * faceY) / d;
1464
- if (dot < Math.cos(fov / 2)) return false;
1871
+ if (dot < dcos(fov / 2)) return false;
1465
1872
  return lineOfSight(map, ex, ey, tx, ty);
1466
1873
  }
1467
1874
 
1875
+ // src/physics/rigidBody.ts
1876
+ function createRigidWorld(opts = {}) {
1877
+ return {
1878
+ gravityX: opts.gravityX ?? 0,
1879
+ gravityY: opts.gravityY ?? 900,
1880
+ iterations: opts.iterations ?? 16,
1881
+ cellSize: opts.cellSize ?? 96,
1882
+ nextId: 1,
1883
+ bodies: [],
1884
+ joints: [],
1885
+ warm: {}
1886
+ };
1887
+ }
1888
+ function massProps(shape) {
1889
+ if (shape.kind === "circle") {
1890
+ const area2 = Math.PI * shape.r * shape.r;
1891
+ return { area: area2, unitI: shape.r * shape.r / 2 };
1892
+ }
1893
+ const p = shape.points;
1894
+ const n2 = p.length / 2;
1895
+ let area = 0;
1896
+ let inertia = 0;
1897
+ for (let k = 0; k < n2; k++) {
1898
+ const x0 = p[k * 2], y0 = p[k * 2 + 1];
1899
+ const j = (k + 1) % n2;
1900
+ const x1 = p[j * 2], y1 = p[j * 2 + 1];
1901
+ const cross = x0 * y1 - x1 * y0;
1902
+ area += cross / 2;
1903
+ inertia += cross * (x0 * x0 + x0 * x1 + x1 * x1 + y0 * y0 + y0 * y1 + y1 * y1) / 12;
1904
+ }
1905
+ return { area: Math.abs(area), unitI: area !== 0 ? Math.abs(inertia) / Math.abs(area) : 0 };
1906
+ }
1907
+ var DENSITY_SCALE = 1e-4;
1908
+ function addBody(rw, def) {
1909
+ const kind = def.kind ?? "dynamic";
1910
+ const density = def.density ?? 1;
1911
+ const { area, unitI } = massProps(def.shape);
1912
+ const m = kind === "dynamic" ? Math.max(area * density * DENSITY_SCALE, 1e-6) : 0;
1913
+ const i = kind === "dynamic" && !def.fixedRotation ? m * unitI : 0;
1914
+ const body = {
1915
+ id: rw.nextId++,
1916
+ kind,
1917
+ shape: def.shape,
1918
+ x: def.x ?? 0,
1919
+ y: def.y ?? 0,
1920
+ a: def.a ?? 0,
1921
+ vx: def.vx ?? 0,
1922
+ vy: def.vy ?? 0,
1923
+ w: def.w ?? 0,
1924
+ m,
1925
+ invM: m > 0 ? 1 / m : 0,
1926
+ i,
1927
+ invI: i > 0 ? 1 / i : 0,
1928
+ restitution: def.restitution ?? 0.1,
1929
+ friction: def.friction ?? 0.4,
1930
+ linDamp: def.linDamp ?? 0,
1931
+ angDamp: def.angDamp ?? 0.6,
1932
+ gravityScale: def.gravityScale ?? 1,
1933
+ bullet: def.bullet ?? false,
1934
+ sensor: def.sensor ?? false,
1935
+ layer: def.layer ?? 1,
1936
+ mask: def.mask ?? 65535,
1937
+ canSleep: def.canSleep ?? true,
1938
+ sleeping: false,
1939
+ sleepTime: 0,
1940
+ sleepR: def.shape.kind === "circle" ? def.shape.r : Math.sqrt(def.shape.points.reduce((m2, _, i2, p) => i2 % 2 ? m2 : Math.max(m2, p[i2] * p[i2] + p[i2 + 1] * p[i2 + 1]), 0)),
1941
+ fx: 0,
1942
+ fy: 0,
1943
+ torque: 0
1944
+ };
1945
+ rw.bodies.push(body);
1946
+ return body.id;
1947
+ }
1948
+ function getBody(rw, id) {
1949
+ const bs = rw.bodies;
1950
+ for (let k = 0; k < bs.length; k++) if (bs[k].id === id) return bs[k];
1951
+ return void 0;
1952
+ }
1953
+ function removeBody(rw, id) {
1954
+ const idx = rw.bodies.findIndex((b) => b.id === id);
1955
+ if (idx >= 0) rw.bodies.splice(idx, 1);
1956
+ rw.joints = rw.joints.filter((j) => j.a !== id && j.b !== id);
1957
+ }
1958
+ function wakeBody(b) {
1959
+ b.sleeping = false;
1960
+ b.sleepTime = 0;
1961
+ }
1962
+ function applyImpulse(rw, id, ix, iy, px, py) {
1963
+ const b = getBody(rw, id);
1964
+ if (!b || b.invM === 0) return;
1965
+ wakeBody(b);
1966
+ b.vx += ix * b.invM;
1967
+ b.vy += iy * b.invM;
1968
+ if (px !== void 0 && py !== void 0) {
1969
+ b.w += ((px - b.x) * iy - (py - b.y) * ix) * b.invI;
1970
+ }
1971
+ }
1972
+ function worldPoints(b) {
1973
+ if (b.shape.kind !== "poly") return [];
1974
+ const c = dcos(b.a), s = dsin(b.a);
1975
+ const p = b.shape.points;
1976
+ const out = new Array(p.length);
1977
+ for (let k = 0; k < p.length; k += 2) {
1978
+ out[k] = b.x + p[k] * c - p[k + 1] * s;
1979
+ out[k + 1] = b.y + p[k] * s + p[k + 1] * c;
1980
+ }
1981
+ return out;
1982
+ }
1983
+ function bodyAABB(b) {
1984
+ if (b.shape.kind === "circle") {
1985
+ const r = b.shape.r;
1986
+ return { x: b.x - r, y: b.y - r, w: r * 2, h: r * 2 };
1987
+ }
1988
+ const wp = worldPoints(b);
1989
+ let x0 = Infinity, y0 = Infinity, x1 = -Infinity, y1 = -Infinity;
1990
+ for (let k = 0; k < wp.length; k += 2) {
1991
+ if (wp[k] < x0) x0 = wp[k];
1992
+ if (wp[k] > x1) x1 = wp[k];
1993
+ if (wp[k + 1] < y0) y0 = wp[k + 1];
1994
+ if (wp[k + 1] > y1) y1 = wp[k + 1];
1995
+ }
1996
+ return { x: x0, y: y0, w: x1 - x0, h: y1 - y0 };
1997
+ }
1998
+ function polygonBox(w, h) {
1999
+ const hw = w / 2, hh = h / 2;
2000
+ return { kind: "poly", points: [-hw, -hh, hw, -hh, hw, hh, -hw, hh] };
2001
+ }
2002
+
2003
+ // src/physics/rigidCollide.ts
2004
+ function collideCircles(a, b) {
2005
+ if (a.shape.kind !== "circle" || b.shape.kind !== "circle") return null;
2006
+ const dx = b.x - a.x, dy = b.y - a.y;
2007
+ const rsum = a.shape.r + b.shape.r;
2008
+ const d2 = dx * dx + dy * dy;
2009
+ if (d2 >= rsum * rsum) return null;
2010
+ const d = Math.sqrt(d2);
2011
+ const nx = d > 1e-9 ? dx / d : 1, ny = d > 1e-9 ? dy / d : 0;
2012
+ return {
2013
+ a,
2014
+ b,
2015
+ nx,
2016
+ ny,
2017
+ points: [{ px: a.x + nx * a.shape.r, py: a.y + ny * a.shape.r, pen: rsum - d, feature: 0 }]
2018
+ };
2019
+ }
2020
+ function collidePolyCircle(a, b) {
2021
+ if (a.shape.kind !== "poly" || b.shape.kind !== "circle") return null;
2022
+ const wp = worldPoints(a);
2023
+ const n2 = wp.length / 2;
2024
+ const r = b.shape.r;
2025
+ let bestSep = -Infinity, bestI = 0;
2026
+ for (let k2 = 0; k2 < n2; k2++) {
2027
+ const j2 = (k2 + 1) % n2;
2028
+ const ex2 = wp[j2 * 2] - wp[k2 * 2], ey2 = wp[j2 * 2 + 1] - wp[k2 * 2 + 1];
2029
+ const len = dhypot(ex2, ey2) || 1;
2030
+ const nx2 = ey2 / len, ny2 = -ex2 / len;
2031
+ const sep = (b.x - wp[k2 * 2]) * nx2 + (b.y - wp[k2 * 2 + 1]) * ny2;
2032
+ if (sep > bestSep) {
2033
+ bestSep = sep;
2034
+ bestI = k2;
2035
+ }
2036
+ }
2037
+ if (bestSep > r) return null;
2038
+ const k = bestI, j = (bestI + 1) % n2;
2039
+ const ax = wp[k * 2], ay = wp[k * 2 + 1], bx2 = wp[j * 2], by2 = wp[j * 2 + 1];
2040
+ if (bestSep < 1e-9) {
2041
+ const ex2 = bx2 - ax, ey2 = by2 - ay;
2042
+ const len = dhypot(ex2, ey2) || 1;
2043
+ const nx2 = ey2 / len, ny2 = -ex2 / len;
2044
+ return { a, b, nx: nx2, ny: ny2, points: [{ px: b.x - nx2 * r, py: b.y - ny2 * r, pen: r - bestSep, feature: bestI }] };
2045
+ }
2046
+ const ex = bx2 - ax, ey = by2 - ay;
2047
+ const e2 = ex * ex + ey * ey || 1;
2048
+ let t = ((b.x - ax) * ex + (b.y - ay) * ey) / e2;
2049
+ t = t < 0 ? 0 : t > 1 ? 1 : t;
2050
+ const cx = ax + ex * t, cy = ay + ey * t;
2051
+ const dx = b.x - cx, dy = b.y - cy;
2052
+ const d2 = dx * dx + dy * dy;
2053
+ if (d2 >= r * r) return null;
2054
+ const d = Math.sqrt(d2) || 1e-9;
2055
+ const nx = dx / d, ny = dy / d;
2056
+ return { a, b, nx, ny, points: [{ px: cx, py: cy, pen: r - d, feature: bestI }] };
2057
+ }
2058
+ function maxSeparation(wpA, wpB) {
2059
+ const nA = wpA.length / 2, nB = wpB.length / 2;
2060
+ let best = -Infinity, bestI = 0;
2061
+ for (let k = 0; k < nA; k++) {
2062
+ const j = (k + 1) % nA;
2063
+ const ex = wpA[j * 2] - wpA[k * 2], ey = wpA[j * 2 + 1] - wpA[k * 2 + 1];
2064
+ const len = dhypot(ex, ey) || 1;
2065
+ const nx = ey / len, ny = -ex / len;
2066
+ let minDot = Infinity;
2067
+ for (let v = 0; v < nB; v++) {
2068
+ const d = (wpB[v * 2] - wpA[k * 2]) * nx + (wpB[v * 2 + 1] - wpA[k * 2 + 1]) * ny;
2069
+ if (d < minDot) minDot = d;
2070
+ }
2071
+ if (minDot > best) {
2072
+ best = minDot;
2073
+ bestI = k;
2074
+ }
2075
+ }
2076
+ return [best, bestI];
2077
+ }
2078
+ function collidePolys(a, b) {
2079
+ const wpA = worldPoints(a), wpB = worldPoints(b);
2080
+ const [sepA, faceA] = maxSeparation(wpA, wpB);
2081
+ if (sepA > 0) return null;
2082
+ const [sepB, faceB] = maxSeparation(wpB, wpA);
2083
+ if (sepB > 0) return null;
2084
+ let ref, inc, refFace, flip;
2085
+ if (sepB > sepA + 1e-4) {
2086
+ ref = wpB;
2087
+ inc = wpA;
2088
+ refFace = faceB;
2089
+ flip = true;
2090
+ } else {
2091
+ ref = wpA;
2092
+ inc = wpB;
2093
+ refFace = faceA;
2094
+ flip = false;
2095
+ }
2096
+ const nRef = ref.length / 2, nInc = inc.length / 2;
2097
+ const rj = (refFace + 1) % nRef;
2098
+ const rex = ref[rj * 2] - ref[refFace * 2], rey = ref[rj * 2 + 1] - ref[refFace * 2 + 1];
2099
+ const rlen = dhypot(rex, rey) || 1;
2100
+ const rnx = rey / rlen, rny = -rex / rlen;
2101
+ const rtx = rex / rlen, rty = rey / rlen;
2102
+ let incFace = 0, minDot = Infinity;
2103
+ for (let k = 0; k < nInc; k++) {
2104
+ const j = (k + 1) % nInc;
2105
+ const ex = inc[j * 2] - inc[k * 2], ey = inc[j * 2 + 1] - inc[k * 2 + 1];
2106
+ const len = dhypot(ex, ey) || 1;
2107
+ const d = ey / len * rnx + -ex / len * rny;
2108
+ if (d < minDot) {
2109
+ minDot = d;
2110
+ incFace = k;
2111
+ }
2112
+ }
2113
+ const ij = (incFace + 1) % nInc;
2114
+ let v1x = inc[incFace * 2], v1y = inc[incFace * 2 + 1];
2115
+ let v2x = inc[ij * 2], v2y = inc[ij * 2 + 1];
2116
+ const refX = ref[refFace * 2], refY = ref[refFace * 2 + 1];
2117
+ const refX2 = ref[rj * 2], refY2 = ref[rj * 2 + 1];
2118
+ for (let side = 0; side < 2; side++) {
2119
+ const ox = side === 0 ? refX : refX2, oy = side === 0 ? refY : refY2;
2120
+ const sx = side === 0 ? rtx : -rtx, sy = side === 0 ? rty : -rty;
2121
+ const d1 = (v1x - ox) * sx + (v1y - oy) * sy;
2122
+ const d2 = (v2x - ox) * sx + (v2y - oy) * sy;
2123
+ if (d1 < 0 && d2 < 0) return null;
2124
+ if (d1 < 0) {
2125
+ const t = d1 / (d1 - d2);
2126
+ v1x = v1x + (v2x - v1x) * t;
2127
+ v1y = v1y + (v2y - v1y) * t;
2128
+ } else if (d2 < 0) {
2129
+ const t = d2 / (d2 - d1);
2130
+ v2x = v2x + (v1x - v2x) * t;
2131
+ v2y = v2y + (v1y - v2y) * t;
2132
+ }
2133
+ }
2134
+ const nx = flip ? -rnx : rnx, ny = flip ? -rny : rny;
2135
+ const points = [];
2136
+ const pen1 = -((v1x - refX) * rnx + (v1y - refY) * rny);
2137
+ const pen2 = -((v2x - refX) * rnx + (v2y - refY) * rny);
2138
+ const fid = ((flip ? 1 : 0) * 64 + refFace) * 64 + incFace;
2139
+ if (pen1 > 0) points.push({ px: v1x, py: v1y, pen: pen1, feature: fid * 2 });
2140
+ if (pen2 > 0) points.push({ px: v2x, py: v2y, pen: pen2, feature: fid * 2 + 1 });
2141
+ if (points.length === 0) return null;
2142
+ return { a, b, nx, ny, points };
2143
+ }
2144
+ function collide(a, b) {
2145
+ const ka = a.shape.kind, kb = b.shape.kind;
2146
+ if (ka === "circle" && kb === "circle") return collideCircles(a, b);
2147
+ if (ka === "poly" && kb === "poly") return collidePolys(a, b);
2148
+ if (ka === "poly" && kb === "circle") return collidePolyCircle(a, b);
2149
+ const m = collidePolyCircle(b, a);
2150
+ if (!m) return null;
2151
+ return { a, b, nx: -m.nx, ny: -m.ny, points: m.points };
2152
+ }
2153
+
2154
+ // src/physics/rigidJoints.ts
2155
+ function toLocal(b, wx, wy) {
2156
+ const c = dcos(b.a), s = dsin(b.a);
2157
+ const dx = wx - b.x, dy = wy - b.y;
2158
+ return [dx * c + dy * s, -dx * s + dy * c];
2159
+ }
2160
+ function anchorWorld(b, lx, ly) {
2161
+ const c = dcos(b.a), s = dsin(b.a);
2162
+ return [b.x + lx * c - ly * s, b.y + lx * s + ly * c];
2163
+ }
2164
+ function addDistanceJoint(rw, def) {
2165
+ const A = getBody(rw, def.a), B = getBody(rw, def.b);
2166
+ if (!A || !B) throw new Error(`distance joint: missing body ${def.a}/${def.b}`);
2167
+ const ax = def.ax ?? 0, ay = def.ay ?? 0, bx = def.bx ?? 0, by = def.by ?? 0;
2168
+ const [wax, way] = anchorWorld(A, ax, ay);
2169
+ const [wbx, wby] = anchorWorld(B, bx, by);
2170
+ const j = {
2171
+ kind: "distance",
2172
+ id: rw.nextId++,
2173
+ a: def.a,
2174
+ b: def.b,
2175
+ ax,
2176
+ ay,
2177
+ bx,
2178
+ by,
2179
+ length: def.length ?? dhypot(wbx - wax, wby - way),
2180
+ rope: def.rope ?? false
2181
+ };
2182
+ rw.joints.push(j);
2183
+ wakeBody(A);
2184
+ wakeBody(B);
2185
+ return j.id;
2186
+ }
2187
+ function addRevoluteJoint(rw, def) {
2188
+ const A = getBody(rw, def.a), B = getBody(rw, def.b);
2189
+ if (!A || !B) throw new Error(`revolute joint: missing body ${def.a}/${def.b}`);
2190
+ const [ax, ay] = toLocal(A, def.px, def.py);
2191
+ const [bx, by] = toLocal(B, def.px, def.py);
2192
+ const j = {
2193
+ kind: "revolute",
2194
+ id: rw.nextId++,
2195
+ a: def.a,
2196
+ b: def.b,
2197
+ ax,
2198
+ ay,
2199
+ bx,
2200
+ by,
2201
+ motorSpeed: def.motorSpeed ?? 0,
2202
+ maxMotorTorque: def.maxMotorTorque ?? 0,
2203
+ limitLower: def.limitLower ?? 0,
2204
+ limitUpper: def.limitUpper ?? 0,
2205
+ limitEnabled: def.limitLower !== void 0 || def.limitUpper !== void 0,
2206
+ refAngle: B.a - A.a
2207
+ };
2208
+ rw.joints.push(j);
2209
+ wakeBody(A);
2210
+ wakeBody(B);
2211
+ return j.id;
2212
+ }
2213
+ function getJoint(rw, id) {
2214
+ for (const j of rw.joints) if (j.id === id) return j;
2215
+ return void 0;
2216
+ }
2217
+ function removeJoint(rw, id) {
2218
+ rw.joints = rw.joints.filter((j) => j.id !== id);
2219
+ }
2220
+ function solveJoint(j, A, B, dt, scratch) {
2221
+ const [wax, way] = anchorWorld(A, j.ax, j.ay);
2222
+ const [wbx, wby] = anchorWorld(B, j.bx, j.by);
2223
+ const rax = wax - A.x, ray = way - A.y;
2224
+ const rbx = wbx - B.x, rby = wby - B.y;
2225
+ if (j.kind === "distance") {
2226
+ let dx = wbx - wax, dy = wby - way;
2227
+ const dist = dhypot(dx, dy);
2228
+ if (dist < 1e-9) return;
2229
+ dx /= dist;
2230
+ dy /= dist;
2231
+ const stretch = dist - j.length;
2232
+ if (j.rope && stretch <= 0) return;
2233
+ const vax2 = A.vx - A.w * ray, vay2 = A.vy + A.w * rax;
2234
+ const vbx2 = B.vx - B.w * rby, vby2 = B.vy + B.w * rbx;
2235
+ const vrel = (vbx2 - vax2) * dx + (vby2 - vay2) * dy;
2236
+ const crossA = rax * dy - ray * dx;
2237
+ const crossB = rbx * dy - rby * dx;
2238
+ const k = A.invM + B.invM + A.invI * crossA * crossA + B.invI * crossB * crossB;
2239
+ if (k < 1e-12) return;
2240
+ const bias = 0.2 / dt * stretch;
2241
+ let lambda = -(vrel + bias) / k;
2242
+ if (j.rope && lambda > 0) lambda = 0;
2243
+ const ix2 = dx * lambda, iy2 = dy * lambda;
2244
+ A.vx -= ix2 * A.invM;
2245
+ A.vy -= iy2 * A.invM;
2246
+ A.w -= (rax * iy2 - ray * ix2) * A.invI;
2247
+ B.vx += ix2 * B.invM;
2248
+ B.vy += iy2 * B.invM;
2249
+ B.w += (rbx * iy2 - rby * ix2) * B.invI;
2250
+ return;
2251
+ }
2252
+ if (j.maxMotorTorque > 0) {
2253
+ const kw = A.invI + B.invI;
2254
+ if (kw > 1e-12) {
2255
+ const cdot = B.w - A.w - j.motorSpeed;
2256
+ const imp = -cdot / kw;
2257
+ const maxImp = j.maxMotorTorque * dt;
2258
+ const acc0 = scratch.motorImpulse;
2259
+ scratch.motorImpulse = Math.min(Math.max(acc0 + imp, -maxImp), maxImp);
2260
+ const applied = scratch.motorImpulse - acc0;
2261
+ A.w -= applied * A.invI;
2262
+ B.w += applied * B.invI;
2263
+ }
2264
+ }
2265
+ if (j.limitEnabled) {
2266
+ const kw = A.invI + B.invI;
2267
+ if (kw > 1e-12) {
2268
+ const angle = B.a - A.a - j.refAngle;
2269
+ let c = 0;
2270
+ if (angle < j.limitLower) c = angle - j.limitLower;
2271
+ else if (angle > j.limitUpper) c = angle - j.limitUpper;
2272
+ if (c !== 0) {
2273
+ const cdot = B.w - A.w;
2274
+ const imp = -(cdot + 0.2 / dt * c) / kw;
2275
+ if (c < 0 && imp > 0 || c > 0 && imp < 0) {
2276
+ A.w -= imp * A.invI;
2277
+ B.w += imp * B.invI;
2278
+ }
2279
+ }
2280
+ }
2281
+ }
2282
+ const vax = A.vx - A.w * ray, vay = A.vy + A.w * rax;
2283
+ const vbx = B.vx - B.w * rby, vby = B.vy + B.w * rbx;
2284
+ const cx = wbx - wax, cy = wby - way;
2285
+ const vrx = vbx - vax + 0.2 / dt * cx;
2286
+ const vry = vby - vay + 0.2 / dt * cy;
2287
+ const k11 = A.invM + B.invM + A.invI * ray * ray + B.invI * rby * rby;
2288
+ const k12 = -A.invI * rax * ray - B.invI * rbx * rby;
2289
+ const k22 = A.invM + B.invM + A.invI * rax * rax + B.invI * rbx * rbx;
2290
+ const det = k11 * k22 - k12 * k12;
2291
+ if (Math.abs(det) < 1e-12) return;
2292
+ const ix = -(k22 * vrx - k12 * vry) / det;
2293
+ const iy = -(k11 * vry - k12 * vrx) / det;
2294
+ A.vx -= ix * A.invM;
2295
+ A.vy -= iy * A.invM;
2296
+ A.w -= (rax * iy - ray * ix) * A.invI;
2297
+ B.vx += ix * B.invM;
2298
+ B.vy += iy * B.invM;
2299
+ B.w += (rbx * iy - rby * ix) * B.invI;
2300
+ }
2301
+
2302
+ // src/physics/rigidStep.ts
2303
+ var SLOP = 0.5;
2304
+ var BIAS = 0.2;
2305
+ var REST_THRESHOLD = 40;
2306
+ var SLEEP_LIN2 = 64;
2307
+ var SLEEP_RIM = 8;
2308
+ var TIME_TO_SLEEP = 0.5;
2309
+ var WAKE_LIN2 = 100;
2310
+ function canCollide(a, b) {
2311
+ if (a.invM === 0 && b.invM === 0 && a.kind !== "kinematic" && b.kind !== "kinematic" && !a.sensor && !b.sensor) return false;
2312
+ return (a.mask & b.layer) !== 0 && (b.mask & a.layer) !== 0;
2313
+ }
2314
+ function rigidStep(rw, dt) {
2315
+ const bodies = rw.bodies;
2316
+ const events = [];
2317
+ if (dt <= 0) return events;
2318
+ for (let k = 0; k < bodies.length; k++) {
2319
+ const b = bodies[k];
2320
+ if (b.kind !== "dynamic" || b.sleeping) {
2321
+ b.fx = 0;
2322
+ b.fy = 0;
2323
+ b.torque = 0;
2324
+ continue;
2325
+ }
2326
+ b.vx += (rw.gravityX * b.gravityScale + b.fx * b.invM) * dt;
2327
+ b.vy += (rw.gravityY * b.gravityScale + b.fy * b.invM) * dt;
2328
+ b.w += b.torque * b.invI * dt;
2329
+ const ld = 1 / (1 + b.linDamp * dt);
2330
+ b.vx *= ld;
2331
+ b.vy *= ld;
2332
+ b.w *= 1 / (1 + b.angDamp * dt);
2333
+ b.fx = 0;
2334
+ b.fy = 0;
2335
+ b.torque = 0;
2336
+ }
2337
+ const hash = new SpatialHash(rw.cellSize);
2338
+ const indexOf = /* @__PURE__ */ new Map();
2339
+ for (let k = 0; k < bodies.length; k++) {
2340
+ hash.insert(bodies[k], bodyAABB(bodies[k]));
2341
+ indexOf.set(bodies[k], k);
2342
+ }
2343
+ const manifolds = [];
2344
+ const seen = /* @__PURE__ */ new Set();
2345
+ const jointed = /* @__PURE__ */ new Set();
2346
+ for (const j of rw.joints) {
2347
+ const lo = Math.min(j.a, j.b), hi = Math.max(j.a, j.b);
2348
+ jointed.add(lo * 1048576 + hi);
2349
+ }
2350
+ for (let k = 0; k < bodies.length; k++) {
2351
+ const a = bodies[k];
2352
+ const near = hash.query(bodyAABB(a));
2353
+ for (const b of near) {
2354
+ if (b.id <= a.id) continue;
2355
+ const packed = a.id * 1048576 + b.id;
2356
+ if (seen.has(packed)) continue;
2357
+ seen.add(packed);
2358
+ if (jointed.has(packed)) continue;
2359
+ if (!canCollide(a, b)) continue;
2360
+ const activeA = !a.sleeping && a.kind !== "static";
2361
+ const activeB = !b.sleeping && b.kind !== "static";
2362
+ if (!activeA && !activeB) continue;
2363
+ const m = collide(a, b);
2364
+ if (!m) continue;
2365
+ const wakeIf = (s, o) => {
2366
+ if (s.sleeping && (o.vx * o.vx + o.vy * o.vy > WAKE_LIN2 || o.kind === "kinematic")) wakeBody(s);
2367
+ };
2368
+ wakeIf(a, b);
2369
+ wakeIf(b, a);
2370
+ if (a.sensor || b.sensor) {
2371
+ events.push({ a: a.id, b: b.id, px: m.points[0].px, py: m.points[0].py, nx: m.nx, ny: m.ny, impulse: 0, sensor: true });
2372
+ continue;
2373
+ }
2374
+ const sp = [];
2375
+ const tx = -m.ny, ty = m.nx;
2376
+ for (const p of m.points) {
2377
+ const rax = p.px - a.x, ray = p.py - a.y;
2378
+ const rbx = p.px - b.x, rby = p.py - b.y;
2379
+ const crossAN = rax * m.ny - ray * m.nx;
2380
+ const crossBN = rbx * m.ny - rby * m.nx;
2381
+ const kn = a.invM + b.invM + a.invI * crossAN * crossAN + b.invI * crossBN * crossBN;
2382
+ const crossAT = rax * ty - ray * tx;
2383
+ const crossBT = rbx * ty - rby * tx;
2384
+ const kt = a.invM + b.invM + a.invI * crossAT * crossAT + b.invI * crossBT * crossBT;
2385
+ const vrx = b.vx - b.w * rby - a.vx + a.w * ray;
2386
+ const vry = b.vy + b.w * rbx - a.vy - a.w * rax;
2387
+ const vn = vrx * m.nx + vry * m.ny;
2388
+ const e = Math.max(a.restitution, b.restitution);
2389
+ const key = `${a.id}:${b.id}:${p.feature}`;
2390
+ const warm2 = rw.warm[key];
2391
+ const sp1 = {
2392
+ rax,
2393
+ ray,
2394
+ rbx,
2395
+ rby,
2396
+ pen: p.pen,
2397
+ massN: kn > 1e-12 ? 1 / kn : 0,
2398
+ massT: kt > 1e-12 ? 1 / kt : 0,
2399
+ restBias: vn < -REST_THRESHOLD ? -e * vn : 0,
2400
+ posBias: BIAS / dt * Math.max(0, p.pen - SLOP),
2401
+ pn: warm2 ? warm2[0] : 0,
2402
+ pt: warm2 ? warm2[1] : 0,
2403
+ pb: 0,
2404
+ key
2405
+ };
2406
+ sp.push(sp1);
2407
+ }
2408
+ manifolds.push({ m, ia: k, ib: indexOf.get(b), points: sp, friction: Math.sqrt(a.friction * b.friction) });
2409
+ }
2410
+ }
2411
+ for (const sm of manifolds) {
2412
+ const { a, b, nx, ny } = sm.m;
2413
+ const tx = -ny, ty = nx;
2414
+ for (const p of sm.points) {
2415
+ if (p.pn === 0 && p.pt === 0) continue;
2416
+ const ix = nx * p.pn + tx * p.pt;
2417
+ const iy = ny * p.pn + ty * p.pt;
2418
+ a.vx -= ix * a.invM;
2419
+ a.vy -= iy * a.invM;
2420
+ a.w -= (p.rax * iy - p.ray * ix) * a.invI;
2421
+ b.vx += ix * b.invM;
2422
+ b.vy += iy * b.invM;
2423
+ b.w += (p.rbx * iy - p.rby * ix) * b.invI;
2424
+ }
2425
+ }
2426
+ manifolds.sort((p, q) => {
2427
+ const ps = p.m.a.invM === 0 || p.m.b.invM === 0 ? 0 : 1;
2428
+ const qs = q.m.a.invM === 0 || q.m.b.invM === 0 ? 0 : 1;
2429
+ if (ps !== qs) return ps - qs;
2430
+ const py = Math.max(p.m.a.y, p.m.b.y), qy = Math.max(q.m.a.y, q.m.b.y);
2431
+ if (py !== qy) return qy - py;
2432
+ if (p.m.a.id !== q.m.a.id) return p.m.a.id - q.m.a.id;
2433
+ return p.m.b.id - q.m.b.id;
2434
+ });
2435
+ const bvx = new Float64Array(bodies.length);
2436
+ const bvy = new Float64Array(bodies.length);
2437
+ const bw = new Float64Array(bodies.length);
2438
+ const jointScratch = rw.joints.map(() => ({ motorImpulse: 0 }));
2439
+ for (let iter = 0; iter < rw.iterations; iter++) {
2440
+ for (let jk = 0; jk < rw.joints.length; jk++) {
2441
+ const j = rw.joints[jk];
2442
+ const A = getBody(rw, j.a), B = getBody(rw, j.b);
2443
+ if (!A || !B) continue;
2444
+ if (A.sleeping && B.sleeping) continue;
2445
+ if (A.sleeping) wakeBody(A);
2446
+ if (B.sleeping) wakeBody(B);
2447
+ solveJoint(j, A, B, dt, jointScratch[jk]);
2448
+ }
2449
+ for (const sm of manifolds) {
2450
+ const { a, b, nx, ny } = sm.m;
2451
+ const { ia, ib } = sm;
2452
+ const tx = -ny, ty = nx;
2453
+ for (const p of sm.points) {
2454
+ let vrx = b.vx - b.w * p.rby - a.vx + a.w * p.ray;
2455
+ let vry = b.vy + b.w * p.rbx - a.vy - a.w * p.rax;
2456
+ const vn = vrx * nx + vry * ny;
2457
+ let dPn = p.massN * (p.restBias - vn);
2458
+ const pn0 = p.pn;
2459
+ p.pn = Math.max(pn0 + dPn, 0);
2460
+ dPn = p.pn - pn0;
2461
+ let ix = nx * dPn, iy = ny * dPn;
2462
+ a.vx -= ix * a.invM;
2463
+ a.vy -= iy * a.invM;
2464
+ a.w -= (p.rax * iy - p.ray * ix) * a.invI;
2465
+ b.vx += ix * b.invM;
2466
+ b.vy += iy * b.invM;
2467
+ b.w += (p.rbx * iy - p.rby * ix) * b.invI;
2468
+ vrx = b.vx - b.w * p.rby - a.vx + a.w * p.ray;
2469
+ vry = b.vy + b.w * p.rbx - a.vy - a.w * p.rax;
2470
+ const vt = vrx * tx + vry * ty;
2471
+ let dPt = p.massT * -vt;
2472
+ const maxPt = sm.friction * p.pn;
2473
+ const pt0 = p.pt;
2474
+ p.pt = Math.min(Math.max(pt0 + dPt, -maxPt), maxPt);
2475
+ dPt = p.pt - pt0;
2476
+ ix = tx * dPt;
2477
+ iy = ty * dPt;
2478
+ a.vx -= ix * a.invM;
2479
+ a.vy -= iy * a.invM;
2480
+ a.w -= (p.rax * iy - p.ray * ix) * a.invI;
2481
+ b.vx += ix * b.invM;
2482
+ b.vy += iy * b.invM;
2483
+ b.w += (p.rbx * iy - p.rby * ix) * b.invI;
2484
+ if (p.posBias > 0) {
2485
+ const vbn = (bvx[ib] - bw[ib] * p.rby - bvx[ia] + bw[ia] * p.ray) * nx + (bvy[ib] + bw[ib] * p.rbx - bvy[ia] - bw[ia] * p.rax) * ny;
2486
+ let dPb = p.massN * (p.posBias - vbn);
2487
+ const pb0 = p.pb;
2488
+ p.pb = Math.max(pb0 + dPb, 0);
2489
+ dPb = p.pb - pb0;
2490
+ const bix = nx * dPb, biy = ny * dPb;
2491
+ bvx[ia] -= bix * a.invM;
2492
+ bvy[ia] -= biy * a.invM;
2493
+ bvx[ib] += bix * b.invM;
2494
+ bvy[ib] += biy * b.invM;
2495
+ }
2496
+ }
2497
+ }
2498
+ }
2499
+ const preX = [], preY = [];
2500
+ for (let k = 0; k < bodies.length; k++) {
2501
+ const b = bodies[k];
2502
+ preX.push(b.x);
2503
+ preY.push(b.y);
2504
+ if (b.invM === 0 && b.kind !== "kinematic") continue;
2505
+ if (b.sleeping) continue;
2506
+ b.x += (b.vx + bvx[k]) * dt;
2507
+ b.y += (b.vy + bvy[k]) * dt;
2508
+ b.a += (b.w + bw[k]) * dt;
2509
+ }
2510
+ for (let k = 0; k < bodies.length; k++) {
2511
+ const b = bodies[k];
2512
+ if (!b.bullet || b.shape.kind !== "circle" || b.sleeping) continue;
2513
+ const dx = b.x - preX[k], dy = b.y - preY[k];
2514
+ if (dx * dx + dy * dy < 1) continue;
2515
+ let bestT = 1;
2516
+ for (let q = 0; q < bodies.length; q++) {
2517
+ const o = bodies[q];
2518
+ if (o === b || o.bullet || o.sensor || !canCollide(b, o)) continue;
2519
+ const t = sweepCircle(preX[k], preY[k], dx, dy, b.shape.r, o);
2520
+ if (t >= 0 && t < bestT) bestT = t;
2521
+ }
2522
+ if (bestT < 1) {
2523
+ const len = dhypot(dx, dy) || 1;
2524
+ b.x = preX[k] + dx * bestT + dx / len * 0.4;
2525
+ b.y = preY[k] + dy * bestT + dy / len * 0.4;
2526
+ }
2527
+ }
2528
+ const touching = /* @__PURE__ */ new Set();
2529
+ const parent = [];
2530
+ for (let k = 0; k < bodies.length; k++) parent.push(k);
2531
+ const find = (x) => {
2532
+ while (parent[x] !== x) {
2533
+ parent[x] = parent[parent[x]];
2534
+ x = parent[x];
2535
+ }
2536
+ return x;
2537
+ };
2538
+ const union = (x, y) => {
2539
+ parent[find(x)] = find(y);
2540
+ };
2541
+ for (const sm of manifolds) {
2542
+ touching.add(sm.m.a.id);
2543
+ touching.add(sm.m.b.id);
2544
+ if (sm.m.a.kind === "dynamic" && sm.m.b.kind === "dynamic") union(sm.ia, sm.ib);
2545
+ }
2546
+ for (const j of rw.joints) {
2547
+ touching.add(j.a);
2548
+ touching.add(j.b);
2549
+ const A = getBody(rw, j.a), B = getBody(rw, j.b);
2550
+ if (A?.kind === "dynamic" && B?.kind === "dynamic") union(indexOf.get(A), indexOf.get(B));
2551
+ }
2552
+ for (let k = 0; k < bodies.length; k++) {
2553
+ const b = bodies[k];
2554
+ if (b.kind !== "dynamic") continue;
2555
+ const lin2 = b.vx * b.vx + b.vy * b.vy;
2556
+ if (b.sleeping) {
2557
+ if (lin2 > WAKE_LIN2) wakeBody(b);
2558
+ continue;
2559
+ }
2560
+ if (!b.canSleep || !touching.has(b.id)) {
2561
+ b.sleepTime = 0;
2562
+ continue;
2563
+ }
2564
+ b.sleepTime = lin2 < SLEEP_LIN2 && Math.abs(b.w) * b.sleepR < SLEEP_RIM ? b.sleepTime + dt : 0;
2565
+ }
2566
+ const islandMin = /* @__PURE__ */ new Map();
2567
+ for (let k = 0; k < bodies.length; k++) {
2568
+ const b = bodies[k];
2569
+ if (b.kind !== "dynamic" || b.sleeping) continue;
2570
+ const root = find(k);
2571
+ const t = !b.canSleep || !touching.has(b.id) ? -1 : b.sleepTime;
2572
+ const cur = islandMin.get(root);
2573
+ islandMin.set(root, cur === void 0 ? t : Math.min(cur, t));
2574
+ }
2575
+ for (let k = 0; k < bodies.length; k++) {
2576
+ const b = bodies[k];
2577
+ if (b.kind !== "dynamic" || b.sleeping) continue;
2578
+ if ((islandMin.get(find(k)) ?? -1) >= TIME_TO_SLEEP) {
2579
+ b.sleeping = true;
2580
+ b.vx = 0;
2581
+ b.vy = 0;
2582
+ b.w = 0;
2583
+ }
2584
+ }
2585
+ const warm = {};
2586
+ for (const sm of manifolds) {
2587
+ let total = 0;
2588
+ for (const p of sm.points) {
2589
+ total += p.pn;
2590
+ if (p.pn !== 0 || p.pt !== 0) warm[p.key] = [p.pn, p.pt];
2591
+ }
2592
+ const p0 = sm.m.points[0];
2593
+ events.push({
2594
+ a: sm.m.a.id,
2595
+ b: sm.m.b.id,
2596
+ px: p0.px,
2597
+ py: p0.py,
2598
+ nx: sm.m.nx,
2599
+ ny: sm.m.ny,
2600
+ impulse: total,
2601
+ sensor: false
2602
+ });
2603
+ }
2604
+ rw.warm = warm;
2605
+ return events;
2606
+ }
2607
+ function sweepCircle(x0, y0, dx, dy, r, o) {
2608
+ if (o.shape.kind === "circle") {
2609
+ return sweepVsCircle(x0, y0, dx, dy, o.x, o.y, r + o.shape.r);
2610
+ }
2611
+ const wp = worldPoints(o);
2612
+ const n2 = wp.length / 2;
2613
+ let best = -1;
2614
+ for (let k = 0; k < n2; k++) {
2615
+ const j = (k + 1) % n2;
2616
+ const ax = wp[k * 2], ay = wp[k * 2 + 1];
2617
+ const bx = wp[j * 2], by = wp[j * 2 + 1];
2618
+ const ex = bx - ax, ey = by - ay;
2619
+ const len = dhypot(ex, ey) || 1;
2620
+ const nx = ey / len, ny = -ex / len;
2621
+ const startSep = (x0 - ax) * nx + (y0 - ay) * ny - r;
2622
+ if (dx * nx + dy * ny < 0 && startSep > 0.5) {
2623
+ const t = sweepVsSegment(x0, y0, dx, dy, ax + nx * r, ay + ny * r, bx + nx * r, by + ny * r);
2624
+ if (t >= 0 && (best < 0 || t < best)) best = t;
2625
+ }
2626
+ const tv = sweepVsCircle(x0, y0, dx, dy, ax, ay, r);
2627
+ if (tv >= 0 && (best < 0 || tv < best)) best = tv;
2628
+ }
2629
+ return best;
2630
+ }
2631
+ function sweepVsCircle(x0, y0, dx, dy, cx, cy, R) {
2632
+ const fx = x0 - cx, fy = y0 - cy;
2633
+ const a = dx * dx + dy * dy;
2634
+ if (a < 1e-12) return -1;
2635
+ const b = 2 * (fx * dx + fy * dy);
2636
+ if (fx * fx + fy * fy < (R + 0.5) * (R + 0.5)) return -1;
2637
+ const c = fx * fx + fy * fy - R * R;
2638
+ const disc = b * b - 4 * a * c;
2639
+ if (disc < 0) return -1;
2640
+ const t = (-b - Math.sqrt(disc)) / (2 * a);
2641
+ return t >= 0 && t <= 1 ? t : -1;
2642
+ }
2643
+ function sweepVsSegment(x0, y0, dx, dy, ax, ay, bx, by) {
2644
+ const ex = bx - ax, ey = by - ay;
2645
+ const denom = dx * ey - dy * ex;
2646
+ if (Math.abs(denom) < 1e-12) return -1;
2647
+ const t = ((ax - x0) * ey - (ay - y0) * ex) / denom;
2648
+ const s = ((ax - x0) * dy - (ay - y0) * dx) / denom;
2649
+ return t >= 0 && t <= 1 && s >= 0 && s <= 1 ? t : -1;
2650
+ }
2651
+
2652
+ // src/physics/rigidQueries.ts
2653
+ function bodyContains(b, x, y) {
2654
+ if (b.shape.kind === "circle") {
2655
+ const dx = x - b.x, dy = y - b.y;
2656
+ return dx * dx + dy * dy <= b.shape.r * b.shape.r;
2657
+ }
2658
+ const wp = worldPoints(b);
2659
+ const n2 = wp.length / 2;
2660
+ for (let k = 0; k < n2; k++) {
2661
+ const j = (k + 1) % n2;
2662
+ const ex = wp[j * 2] - wp[k * 2], ey = wp[j * 2 + 1] - wp[k * 2 + 1];
2663
+ if ((x - wp[k * 2]) * ey - (y - wp[k * 2 + 1]) * ex > 0) return false;
2664
+ }
2665
+ return true;
2666
+ }
2667
+ function pointQuery(rw, x, y, mask = 65535) {
2668
+ let hit;
2669
+ for (const b of rw.bodies) if ((b.layer & mask) !== 0 && bodyContains(b, x, y)) hit = b;
2670
+ return hit;
2671
+ }
2672
+ function rayCircle(x0, y0, dx, dy, cx, cy, r) {
2673
+ const fx = x0 - cx, fy = y0 - cy;
2674
+ const a = dx * dx + dy * dy;
2675
+ if (a < 1e-12) return -1;
2676
+ const b = 2 * (fx * dx + fy * dy);
2677
+ const c = fx * fx + fy * fy - r * r;
2678
+ const disc = b * b - 4 * a * c;
2679
+ if (disc < 0) return -1;
2680
+ const t = (-b - Math.sqrt(disc)) / (2 * a);
2681
+ return t >= 0 && t <= 1 ? t : -1;
2682
+ }
2683
+ function rayCastRigid(rw, x0, y0, x1, y1, mask = 65535) {
2684
+ const dx = x1 - x0, dy = y1 - y0;
2685
+ let best = null;
2686
+ for (const b of rw.bodies) {
2687
+ if ((b.layer & mask) === 0) continue;
2688
+ if (b.shape.kind === "circle") {
2689
+ const t = rayCircle(x0, y0, dx, dy, b.x, b.y, b.shape.r);
2690
+ if (t >= 0 && (!best || t < best.t)) {
2691
+ const hx = x0 + dx * t, hy = y0 + dy * t;
2692
+ const len = dhypot(hx - b.x, hy - b.y) || 1;
2693
+ best = { id: b.id, t, x: hx, y: hy, nx: (hx - b.x) / len, ny: (hy - b.y) / len };
2694
+ }
2695
+ continue;
2696
+ }
2697
+ const wp = worldPoints(b);
2698
+ const n2 = wp.length / 2;
2699
+ for (let k = 0; k < n2; k++) {
2700
+ const j = (k + 1) % n2;
2701
+ const ax = wp[k * 2], ay = wp[k * 2 + 1];
2702
+ const ex = wp[j * 2] - ax, ey = wp[j * 2 + 1] - ay;
2703
+ const denom = dx * ey - dy * ex;
2704
+ if (Math.abs(denom) < 1e-12) continue;
2705
+ const t = ((ax - x0) * ey - (ay - y0) * ex) / denom;
2706
+ const s = ((ax - x0) * dy - (ay - y0) * dx) / denom;
2707
+ if (t < 0 || t > 1 || s < 0 || s > 1) continue;
2708
+ if (!best || t < best.t) {
2709
+ const len = dhypot(ex, ey) || 1;
2710
+ best = { id: b.id, t, x: x0 + dx * t, y: y0 + dy * t, nx: ey / len, ny: -ex / len };
2711
+ }
2712
+ }
2713
+ }
2714
+ return best;
2715
+ }
2716
+
1468
2717
  // src/render/commands.ts
1469
2718
  function sortCommands(cmds) {
1470
2719
  return cmds.map((c, i) => [c, i]).sort((a, b) => a[0].z - b[0].z || a[1] - b[1]).map(([c]) => c);
@@ -1585,6 +2834,7 @@ var Canvas2DRenderer = class {
1585
2834
  this.canvas = document.createElement("canvas");
1586
2835
  this.canvas.style.width = "100%";
1587
2836
  this.canvas.style.height = "100%";
2837
+ this.canvas.style.objectFit = "contain";
1588
2838
  this.canvas.style.display = "block";
1589
2839
  const ctx = this.canvas.getContext("2d");
1590
2840
  if (!ctx) throw new Error("hayao: 2D canvas context unavailable");
@@ -1727,6 +2977,57 @@ var HeadlessRenderer = class {
1727
2977
  }
1728
2978
  };
1729
2979
 
2980
+ // src/render/nineSlice.ts
2981
+ function nineSlice(rect, style, z = 0, transform = IDENTITY) {
2982
+ const b = Math.max(0, Math.min(style.border, rect.w / 2, rect.h / 2));
2983
+ const fill = style.fill ?? "#fbf6ea";
2984
+ const edge = style.edge ?? fill;
2985
+ const corner = style.corner ?? edge;
2986
+ const out = [];
2987
+ const x0 = rect.x;
2988
+ const x1 = rect.x + b;
2989
+ const x2 = rect.x + rect.w - b;
2990
+ const y0 = rect.y;
2991
+ const y1 = rect.y + b;
2992
+ const y2 = rect.y + rect.h - b;
2993
+ const iw = rect.w - 2 * b;
2994
+ const ih = rect.h - 2 * b;
2995
+ const push = (x, y, w, h, f, r2) => {
2996
+ if (w <= 0 || h <= 0) return;
2997
+ out.push({ kind: "rect", x, y, w, h, r: r2, fill: f, transform, z });
2998
+ };
2999
+ push(x1, y1, iw, ih, fill);
3000
+ push(x1, y0, iw, b, style.highlight ?? edge);
3001
+ push(x1, y2, iw, b, style.shadow ?? edge);
3002
+ push(x0, y1, b, ih, style.highlight ?? edge);
3003
+ push(x2, y1, b, ih, style.shadow ?? edge);
3004
+ const r = style.radius;
3005
+ push(x0, y0, b, b, corner, r);
3006
+ push(x2, y0, b, b, corner, r);
3007
+ push(x0, y2, b, b, corner, r);
3008
+ push(x2, y2, b, b, corner, r);
3009
+ if (style.stroke) {
3010
+ out.push({
3011
+ kind: "rect",
3012
+ x: rect.x,
3013
+ y: rect.y,
3014
+ w: rect.w,
3015
+ h: rect.h,
3016
+ r: style.radius,
3017
+ fill: "none",
3018
+ stroke: style.stroke,
3019
+ strokeWidth: style.strokeWidth ?? 2,
3020
+ transform,
3021
+ z: z + 1e-3
3022
+ });
3023
+ }
3024
+ return out;
3025
+ }
3026
+ var PANEL_PRESETS = {
3027
+ parchment: () => ({ border: 10, fill: "#fbf6ea", edge: "#efe4c8", corner: "#e2d3a8", highlight: "#fdfaf0", shadow: "#d9c79c", stroke: "#b8a06a", strokeWidth: 2, radius: 4 }),
3028
+ slate: () => ({ border: 8, fill: "#2c3040", edge: "#363c4f", corner: "#454c63", highlight: "#4a5268", shadow: "#20242f", stroke: "#151821", strokeWidth: 2, radius: 3 })
3029
+ };
3030
+
1730
3031
  // src/art/palette.ts
1731
3032
  var MEADOW = {
1732
3033
  name: "meadow",
@@ -1784,7 +3085,103 @@ function hexToRgb(hex) {
1784
3085
  return [n2 >> 16 & 255, n2 >> 8 & 255, n2 & 255];
1785
3086
  }
1786
3087
  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("");
3088
+ return "#" + [r, g, b].map((v) => Math.max(0, Math.min(255, Math.round(v))).toString(16).padStart(2, "0")).join("");
3089
+ }
3090
+ var mod360 = (h) => (h % 360 + 360) % 360;
3091
+ var clamp01 = (v) => v < 0 ? 0 : v > 1 ? 1 : v;
3092
+ function hsl(h, s, l) {
3093
+ h = mod360(h) / 360;
3094
+ s = clamp01(s);
3095
+ l = clamp01(l);
3096
+ if (s === 0) {
3097
+ const v = l * 255;
3098
+ return rgbToHex(v, v, v);
3099
+ }
3100
+ const q = l < 0.5 ? l * (1 + s) : l + s - l * s;
3101
+ const p = 2 * l - q;
3102
+ const hue = (t) => {
3103
+ t = t < 0 ? t + 1 : t > 1 ? t - 1 : t;
3104
+ if (t < 1 / 6) return p + (q - p) * 6 * t;
3105
+ if (t < 1 / 2) return q;
3106
+ if (t < 2 / 3) return p + (q - p) * (2 / 3 - t) * 6;
3107
+ return p;
3108
+ };
3109
+ return rgbToHex(hue(h + 1 / 3) * 255, hue(h) * 255, hue(h - 1 / 3) * 255);
3110
+ }
3111
+ function hsv(h, s, v) {
3112
+ h = mod360(h) / 60;
3113
+ s = clamp01(s);
3114
+ v = clamp01(v);
3115
+ const c = v * s;
3116
+ const x = c * (1 - Math.abs(h % 2 - 1));
3117
+ const m = v - c;
3118
+ let r = 0;
3119
+ let g = 0;
3120
+ let b = 0;
3121
+ if (h < 1) [r, g, b] = [c, x, 0];
3122
+ else if (h < 2) [r, g, b] = [x, c, 0];
3123
+ else if (h < 3) [r, g, b] = [0, c, x];
3124
+ else if (h < 4) [r, g, b] = [0, x, c];
3125
+ else if (h < 5) [r, g, b] = [x, 0, c];
3126
+ else [r, g, b] = [c, 0, x];
3127
+ return rgbToHex((r + m) * 255, (g + m) * 255, (b + m) * 255);
3128
+ }
3129
+ function hexToHsl(hex) {
3130
+ const [r255, g255, b255] = hexToRgb(hex);
3131
+ const r = r255 / 255;
3132
+ const g = g255 / 255;
3133
+ const b = b255 / 255;
3134
+ const max = Math.max(r, g, b);
3135
+ const min = Math.min(r, g, b);
3136
+ const l = (max + min) / 2;
3137
+ const d = max - min;
3138
+ if (d === 0) return { h: 0, s: 0, l };
3139
+ const s = l > 0.5 ? d / (2 - max - min) : d / (max + min);
3140
+ let h;
3141
+ if (max === r) h = (g - b) / d + (g < b ? 6 : 0);
3142
+ else if (max === g) h = (b - r) / d + 2;
3143
+ else h = (r - g) / d + 4;
3144
+ return { h: h * 60, s, l };
3145
+ }
3146
+ function mutateColor(rng, hex, amounts = {}) {
3147
+ const c = hexToHsl(hex);
3148
+ const dh = amounts.hue ?? 0;
3149
+ const ds = amounts.sat ?? 0;
3150
+ const dl = amounts.light ?? 0;
3151
+ return hsl(
3152
+ c.h + rng.range(-dh, dh),
3153
+ c.s + rng.range(-ds, ds),
3154
+ c.l + rng.range(-dl, dl)
3155
+ );
3156
+ }
3157
+ function dpow(base, exp) {
3158
+ if (base <= 0) return 0;
3159
+ return dexp2(exp * dlog2(base));
3160
+ }
3161
+ var srgbToLinear = (c) => c <= 0.04045 ? c / 12.92 : dpow((c + 0.055) / 1.055, 2.4);
3162
+ var linearToSrgb = (c) => c <= 31308e-7 ? c * 12.92 : 1.055 * dpow(c, 1 / 2.4) - 0.055;
3163
+ function mixLinear(a, b, t) {
3164
+ const pa = hexToRgb(a);
3165
+ const pb = hexToRgb(b);
3166
+ const chan = (i) => {
3167
+ const la = srgbToLinear(pa[i] / 255);
3168
+ const lb = srgbToLinear(pb[i] / 255);
3169
+ return linearToSrgb(la + (lb - la) * t) * 255;
3170
+ };
3171
+ return rgbToHex(chan(0), chan(1), chan(2));
3172
+ }
3173
+ function sampleGradient(stops, t) {
3174
+ if (stops.length === 0) return "#000000";
3175
+ if (stops.length === 1) return stops[0];
3176
+ const clamped = clamp01(t);
3177
+ const scaled = clamped * (stops.length - 1);
3178
+ const i = Math.min(stops.length - 2, Math.floor(scaled));
3179
+ return mixLinear(stops[i], stops[i + 1], scaled - i);
3180
+ }
3181
+ function gradient(stops, n2) {
3182
+ if (n2 <= 0) return [];
3183
+ if (n2 === 1) return [sampleGradient(stops, 0)];
3184
+ return Array.from({ length: n2 }, (_, i) => sampleGradient(stops, i / (n2 - 1)));
1788
3185
  }
1789
3186
 
1790
3187
  // src/art/shapes.ts
@@ -1792,7 +3189,7 @@ function regularPolygon(sides, radius, rotation = 0) {
1792
3189
  const pts = [];
1793
3190
  for (let i = 0; i < sides; i++) {
1794
3191
  const a = rotation + i / sides * TAU;
1795
- pts.push(Math.cos(a) * radius, Math.sin(a) * radius);
3192
+ pts.push(dcos(a) * radius, dsin(a) * radius);
1796
3193
  }
1797
3194
  return pts;
1798
3195
  }
@@ -1801,7 +3198,7 @@ function star(points, outer, inner, rotation = -Math.PI / 2) {
1801
3198
  for (let i = 0; i < points * 2; i++) {
1802
3199
  const r = i % 2 === 0 ? outer : inner;
1803
3200
  const a = rotation + i / (points * 2) * TAU;
1804
- pts.push(Math.cos(a) * r, Math.sin(a) * r);
3201
+ pts.push(dcos(a) * r, dsin(a) * r);
1805
3202
  }
1806
3203
  return pts;
1807
3204
  }
@@ -1810,7 +3207,7 @@ function blobPath(rng, radius, wobble = 0.25, lobes = 7) {
1810
3207
  for (let i = 0; i < lobes; i++) {
1811
3208
  const a = i / lobes * TAU;
1812
3209
  const r = radius * (1 - wobble + rng.float() * wobble * 2);
1813
- pts.push({ x: Math.cos(a) * r, y: Math.sin(a) * r });
3210
+ pts.push({ x: dcos(a) * r, y: dsin(a) * r });
1814
3211
  }
1815
3212
  return smoothClosedPath(pts);
1816
3213
  }
@@ -1852,40 +3249,828 @@ function smoothOpenPath(points, tension = 1) {
1852
3249
  return d;
1853
3250
  }
1854
3251
 
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;
3252
+ // src/art/texture.ts
3253
+ var PixelBuffer = class _PixelBuffer {
3254
+ width;
3255
+ height;
3256
+ data;
3257
+ constructor(width, height, data) {
3258
+ this.width = width;
3259
+ this.height = height;
3260
+ this.data = data ?? new Uint8Array(width * height);
3261
+ }
3262
+ get(x, y) {
3263
+ if (x < 0 || y < 0 || x >= this.width || y >= this.height) return 0;
3264
+ return this.data[y * this.width + x];
3265
+ }
3266
+ set(x, y, index) {
3267
+ if (x < 0 || y < 0 || x >= this.width || y >= this.height) return;
3268
+ this.data[y * this.width + x] = index;
3269
+ }
3270
+ /** Build from a rows-of-strings grid, mapping each character to an index. */
3271
+ static fromRows(rows, charToIndex) {
3272
+ const height = rows.length;
3273
+ const width = rows.reduce((w, r) => Math.max(w, r.length), 0);
3274
+ const buf = new _PixelBuffer(width, height);
3275
+ for (let y = 0; y < height; y++) {
3276
+ const row = rows[y];
3277
+ for (let x = 0; x < row.length; x++) buf.data[y * width + x] = charToIndex[row[x]] ?? 0;
3278
+ }
3279
+ return buf;
1866
3280
  }
1867
- get started() {
1868
- return !!this.ctx;
3281
+ /** A copy with every index passed through `lut` (2-bit → palette remap). */
3282
+ remap(lut) {
3283
+ const out = new Uint8Array(this.data.length);
3284
+ for (let i = 0; i < this.data.length; i++) out[i] = lut[this.data[i]] ?? 0;
3285
+ return new _PixelBuffer(this.width, this.height, out);
1869
3286
  }
1870
- /** Must be called from a user gesture (browser autoplay policy). */
1871
- start() {
1872
- if (this.ctx) {
1873
- void this.ctx.resume();
1874
- return;
3287
+ };
3288
+ function toBig(source) {
3289
+ if (typeof source === "bigint") return source;
3290
+ const s = source.trim();
3291
+ if (s.startsWith("0x") || s.startsWith("0b") || s.startsWith("0o")) return BigInt(s);
3292
+ return BigInt("0x" + s);
3293
+ }
3294
+ function decodeBits(source, width, height) {
3295
+ const bits = toBig(source);
3296
+ const total = width * height;
3297
+ const buf = new PixelBuffer(width, height);
3298
+ for (let i = 0; i < total; i++) {
3299
+ buf.data[i] = Number(bits >> BigInt(total - 1 - i) & 1n);
3300
+ }
3301
+ return buf;
3302
+ }
3303
+ function decode2bit(source, width, height) {
3304
+ const bits = toBig(source);
3305
+ const total = width * height;
3306
+ const buf = new PixelBuffer(width, height);
3307
+ for (let i = 0; i < total; i++) {
3308
+ const shift = BigInt((total - 1 - i) * 2);
3309
+ buf.data[i] = Number(bits >> shift & 3n);
3310
+ }
3311
+ return buf;
3312
+ }
3313
+ function decodeRLE(runs, width, height) {
3314
+ const buf = new PixelBuffer(width, height);
3315
+ let p = 0;
3316
+ for (let i = 0; i + 1 < runs.length; i += 2) {
3317
+ const count = runs[i];
3318
+ const index = runs[i + 1];
3319
+ for (let k = 0; k < count && p < buf.data.length; k++) buf.data[p++] = index;
3320
+ }
3321
+ return buf;
3322
+ }
3323
+ function encodeRLE(buf) {
3324
+ const runs = [];
3325
+ const d = buf.data;
3326
+ for (let i = 0; i < d.length; ) {
3327
+ const v = d[i];
3328
+ let count = 1;
3329
+ while (i + count < d.length && d[i + count] === v) count++;
3330
+ runs.push(count, v);
3331
+ i += count;
3332
+ }
3333
+ return runs;
3334
+ }
3335
+ function pixelsToCommands(buf, palette, options = {}) {
3336
+ const cell = options.cell ?? 1;
3337
+ const ox = options.x ?? 0;
3338
+ const oy = options.y ?? 0;
3339
+ const z = options.z ?? 0;
3340
+ const transform = options.transform ?? IDENTITY;
3341
+ const out = [];
3342
+ for (let y = 0; y < buf.height; y++) {
3343
+ let x = 0;
3344
+ while (x < buf.width) {
3345
+ const idx = buf.get(x, y);
3346
+ const fill = palette[idx];
3347
+ if (fill == null) {
3348
+ x++;
3349
+ continue;
3350
+ }
3351
+ let run = 1;
3352
+ while (x + run < buf.width && buf.get(x + run, y) === idx) run++;
3353
+ out.push({
3354
+ kind: "rect",
3355
+ x: ox + x * cell,
3356
+ y: oy + y * cell,
3357
+ w: run * cell,
3358
+ h: cell,
3359
+ transform,
3360
+ z,
3361
+ fill
3362
+ });
3363
+ x += run;
1875
3364
  }
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
3365
  }
1887
- setVolumes(v) {
1888
- this.vol = { ...this.vol, ...v };
3366
+ return out;
3367
+ }
3368
+ var TextureSprite = class extends Node {
3369
+ type = "TextureSprite";
3370
+ buffer;
3371
+ palette;
3372
+ cell;
3373
+ center;
3374
+ constructor(config) {
3375
+ super(config);
3376
+ this.buffer = config.buffer;
3377
+ this.palette = config.palette;
3378
+ this.cell = config.cell ?? 1;
3379
+ this.center = config.center ?? true;
3380
+ this.cosmetic = true;
3381
+ }
3382
+ draw(out, world) {
3383
+ const cell = this.cell;
3384
+ const x = this.center ? -(this.buffer.width * cell) / 2 : 0;
3385
+ const y = this.center ? -(this.buffer.height * cell) / 2 : 0;
3386
+ for (const cmd of pixelsToCommands(this.buffer, this.palette, { cell, x, y, z: this.z, transform: world })) {
3387
+ out.push(cmd);
3388
+ }
3389
+ }
3390
+ };
3391
+
3392
+ // src/art/font5.ts
3393
+ var G = {
3394
+ A: [".#.", "#.#", "###", "#.#", "#.#"],
3395
+ B: ["##.", "#.#", "##.", "#.#", "##."],
3396
+ C: [".##", "#..", "#..", "#..", ".##"],
3397
+ D: ["##.", "#.#", "#.#", "#.#", "##."],
3398
+ E: ["###", "#..", "##.", "#..", "###"],
3399
+ F: ["###", "#..", "##.", "#..", "#.."],
3400
+ G: [".##", "#..", "#.#", "#.#", ".##"],
3401
+ H: ["#.#", "#.#", "###", "#.#", "#.#"],
3402
+ I: ["#", "#", "#", "#", "#"],
3403
+ J: ["..#", "..#", "..#", "#.#", ".#."],
3404
+ K: ["#.#", "##.", "#..", "##.", "#.#"],
3405
+ L: ["#..", "#..", "#..", "#..", "###"],
3406
+ M: ["#...#", "##.##", "#.#.#", "#...#", "#...#"],
3407
+ N: ["#...#", "##..#", "#.#.#", "#..##", "#...#"],
3408
+ O: [".#.", "#.#", "#.#", "#.#", ".#."],
3409
+ P: ["##.", "#.#", "##.", "#..", "#.."],
3410
+ Q: [".#.", "#.#", "#.#", "#.#", ".##"],
3411
+ R: ["##.", "#.#", "##.", "#.#", "#.#"],
3412
+ S: [".##", "#..", ".#.", "..#", "##."],
3413
+ T: ["###", ".#.", ".#.", ".#.", ".#."],
3414
+ U: ["#.#", "#.#", "#.#", "#.#", "###"],
3415
+ V: ["#.#", "#.#", "#.#", "#.#", ".#."],
3416
+ W: ["#...#", "#...#", "#.#.#", "##.##", "#...#"],
3417
+ X: ["#.#", "#.#", ".#.", "#.#", "#.#"],
3418
+ Y: ["#.#", "#.#", ".#.", ".#.", ".#."],
3419
+ Z: ["###", "..#", ".#.", "#..", "###"],
3420
+ "0": [".#.", "#.#", "#.#", "#.#", ".#."],
3421
+ "1": [".#.", "##.", ".#.", ".#.", "###"],
3422
+ "2": ["##.", "..#", ".#.", "#..", "###"],
3423
+ "3": ["##.", "..#", ".#.", "..#", "##."],
3424
+ "4": ["#.#", "#.#", "###", "..#", "..#"],
3425
+ "5": ["###", "#..", "##.", "..#", "##."],
3426
+ "6": [".##", "#..", "##.", "#.#", ".#."],
3427
+ "7": ["###", "..#", ".#.", ".#.", ".#."],
3428
+ "8": [".#.", "#.#", ".#.", "#.#", ".#."],
3429
+ "9": [".#.", "#.#", ".##", "..#", "##."],
3430
+ ".": [".", ".", ".", ".", "#"],
3431
+ ",": ["..", "..", "..", ".#", "#."],
3432
+ "!": ["#", "#", "#", ".", "#"],
3433
+ "?": ["##.", "..#", ".#.", "...", ".#."],
3434
+ ":": [".", "#", ".", "#", "."],
3435
+ ";": ["..", ".#", "..", ".#", "#."],
3436
+ "-": ["...", "...", "###", "...", "..."],
3437
+ "+": ["...", ".#.", "###", ".#.", "..."],
3438
+ "=": ["...", "###", "...", "###", "..."],
3439
+ "/": ["..#", "..#", ".#.", "#..", "#.."],
3440
+ "'": ["#", "#", ".", ".", "."],
3441
+ '"': ["#.#", "#.#", "...", "...", "..."],
3442
+ "(": [".#", "#.", "#.", "#.", ".#"],
3443
+ ")": ["#.", ".#", ".#", ".#", "#."],
3444
+ "<": ["..#", ".#.", "#..", ".#.", "..#"],
3445
+ ">": ["#..", ".#.", "..#", ".#.", "#.."],
3446
+ "%": ["#.#", "..#", ".#.", "#..", "#.#"],
3447
+ "*": ["...", "#.#", ".#.", "#.#", "..."],
3448
+ "#": ["#.#", "###", "#.#", "###", "#.#"]
3449
+ };
3450
+ var FONT_5 = {
3451
+ height: 5,
3452
+ tracking: 1,
3453
+ spaceWidth: 3,
3454
+ glyphs: G
3455
+ };
3456
+
3457
+ // src/art/bitmapFont.ts
3458
+ function parseRich(markup, colorMap = {}) {
3459
+ const out = [];
3460
+ const stack = [];
3461
+ let i = 0;
3462
+ let srcIndex = 0;
3463
+ while (i < markup.length) {
3464
+ const c = markup[i];
3465
+ if (c === "{" && markup[i + 1] === "{") {
3466
+ out.push({ ch: "{", color: stack[stack.length - 1], i: srcIndex++ });
3467
+ i += 2;
3468
+ continue;
3469
+ }
3470
+ if (c === "{") {
3471
+ const end = markup.indexOf("}", i);
3472
+ if (end === -1) {
3473
+ out.push({ ch: c, color: stack[stack.length - 1], i: srcIndex++ });
3474
+ i++;
3475
+ continue;
3476
+ }
3477
+ const tag = markup.slice(i + 1, end);
3478
+ if (tag === "/") stack.pop();
3479
+ else stack.push(tag[0] === "#" ? tag : colorMap[tag] ?? tag);
3480
+ i = end + 1;
3481
+ continue;
3482
+ }
3483
+ out.push({ ch: c, color: stack[stack.length - 1], i: srcIndex++ });
3484
+ i++;
3485
+ }
3486
+ return out;
3487
+ }
3488
+ function resolveChar(font, ch) {
3489
+ if (ch === " " || ch === "\n") return ch;
3490
+ const up = ch.toUpperCase();
3491
+ if (font.glyphs[up]) return up;
3492
+ if (font.glyphs[ch]) return ch;
3493
+ return font.glyphs["?"] ? "?" : " ";
3494
+ }
3495
+ function glyphWidth(font, ch) {
3496
+ if (ch === " ") return font.spaceWidth;
3497
+ const rows = font.glyphs[ch];
3498
+ return rows ? rows[0].length : font.spaceWidth;
3499
+ }
3500
+ function measureLine(font, chars) {
3501
+ let w = 0;
3502
+ for (let k = 0; k < chars.length; k++) {
3503
+ w += glyphWidth(font, chars[k].ch);
3504
+ if (k < chars.length - 1) w += font.tracking;
3505
+ }
3506
+ return w;
3507
+ }
3508
+ function measureText(font, text) {
3509
+ return measureLine(font, [...text].map((ch, i) => ({ ch: resolveChar(font, ch), i })));
3510
+ }
3511
+ function layoutText(font, input, options = {}) {
3512
+ const lineSpacing = options.lineSpacing ?? 1;
3513
+ const source = typeof input === "string" ? [...input].map((ch, i) => ({ ch, i })) : input.map((r) => ({ ...r }));
3514
+ const hardLines = [[]];
3515
+ for (const rc of source) {
3516
+ if (rc.ch === "\n") hardLines.push([]);
3517
+ else hardLines[hardLines.length - 1].push({ ...rc, ch: resolveChar(font, rc.ch) });
3518
+ }
3519
+ const rows = [];
3520
+ for (const line of hardLines) {
3521
+ if (options.maxWidth === void 0) {
3522
+ rows.push(line);
3523
+ continue;
3524
+ }
3525
+ let current = [];
3526
+ let word = [];
3527
+ const flushWord = () => {
3528
+ if (word.length === 0) return;
3529
+ const sep = current.length ? font.spaceWidth + font.tracking : 0;
3530
+ if (current.length && measureLine(font, current) + sep + measureLine(font, word) > options.maxWidth) {
3531
+ rows.push(current);
3532
+ current = [];
3533
+ }
3534
+ if (current.length) current.push({ ch: " ", i: -1 });
3535
+ current.push(...word);
3536
+ word = [];
3537
+ };
3538
+ for (const rc of line) {
3539
+ if (rc.ch === " ") flushWord();
3540
+ else word.push(rc);
3541
+ }
3542
+ flushWord();
3543
+ rows.push(current);
3544
+ }
3545
+ const glyphs = [];
3546
+ let maxW = 0;
3547
+ for (const row of rows) maxW = Math.max(maxW, measureLine(font, row));
3548
+ const boundW = options.maxWidth ?? maxW;
3549
+ let y = 0;
3550
+ for (const row of rows) {
3551
+ const rowW = measureLine(font, row);
3552
+ let x = options.align === "center" ? Math.floor((boundW - rowW) / 2) : options.align === "right" ? boundW - rowW : 0;
3553
+ for (const rc of row) {
3554
+ const w = glyphWidth(font, rc.ch);
3555
+ if (rc.ch !== " ") glyphs.push({ ch: rc.ch, color: rc.color, i: rc.i, x, y, w });
3556
+ x += w + font.tracking;
3557
+ }
3558
+ y += font.height + lineSpacing;
3559
+ }
3560
+ return { glyphs, width: boundW, height: rows.length * (font.height + lineSpacing) - lineSpacing, lines: rows.length };
3561
+ }
3562
+ function typewriterCount(total, elapsedSec, charsPerSec) {
3563
+ if (charsPerSec <= 0) return total;
3564
+ const n2 = Math.floor(elapsedSec * charsPerSec);
3565
+ return n2 < 0 ? 0 : n2 > total ? total : n2;
3566
+ }
3567
+ function textToCommands(font, layout, options = {}) {
3568
+ const cell = options.cell ?? 2;
3569
+ const ox = options.x ?? 0;
3570
+ const oy = options.y ?? 0;
3571
+ const z = options.z ?? 0;
3572
+ const transform = options.transform ?? IDENTITY;
3573
+ const defColor = options.color ?? "#000";
3574
+ const reveal = options.reveal;
3575
+ const out = [];
3576
+ for (const g of layout.glyphs) {
3577
+ if (reveal !== void 0 && g.i >= reveal) continue;
3578
+ const rows = font.glyphs[g.ch];
3579
+ if (!rows) continue;
3580
+ const fill = g.color ?? defColor;
3581
+ for (let ry = 0; ry < rows.length; ry++) {
3582
+ const row = rows[ry];
3583
+ let cx = 0;
3584
+ while (cx < row.length) {
3585
+ if (row[cx] !== "#") {
3586
+ cx++;
3587
+ continue;
3588
+ }
3589
+ let run = 1;
3590
+ while (cx + run < row.length && row[cx + run] === "#") run++;
3591
+ out.push({
3592
+ kind: "rect",
3593
+ x: ox + (g.x + cx) * cell,
3594
+ y: oy + (g.y + ry) * cell,
3595
+ w: run * cell,
3596
+ h: cell,
3597
+ transform,
3598
+ z,
3599
+ fill
3600
+ });
3601
+ cx += run;
3602
+ }
3603
+ }
3604
+ }
3605
+ return out;
3606
+ }
3607
+ var BitmapText = class extends Node {
3608
+ type = "BitmapText";
3609
+ text;
3610
+ font;
3611
+ cell;
3612
+ color;
3613
+ colorMap;
3614
+ maxWidth;
3615
+ lineSpacing;
3616
+ align;
3617
+ charsPerSec;
3618
+ center;
3619
+ startTime = 0;
3620
+ constructor(config) {
3621
+ super(config);
3622
+ this.text = config.text;
3623
+ this.font = config.font ?? FONT_5;
3624
+ this.cell = config.cell ?? 2;
3625
+ this.color = config.color ?? "#000";
3626
+ this.colorMap = config.colorMap ?? {};
3627
+ this.maxWidth = config.maxWidth;
3628
+ this.lineSpacing = config.lineSpacing ?? 1;
3629
+ this.align = config.align ?? "left";
3630
+ this.charsPerSec = config.charsPerSec;
3631
+ this.center = config.center ?? false;
3632
+ this.cosmetic = true;
3633
+ }
3634
+ onReady() {
3635
+ this.startTime = this.world?.time ?? 0;
3636
+ }
3637
+ /** Restart the typewriter from the current sim time. */
3638
+ restartReveal() {
3639
+ this.startTime = this.world?.time ?? 0;
3640
+ }
3641
+ buildLayout() {
3642
+ const chars = parseRich(this.text, this.colorMap);
3643
+ return layoutText(this.font, chars, { maxWidth: this.maxWidth, lineSpacing: this.lineSpacing, align: this.align });
3644
+ }
3645
+ draw(out, world) {
3646
+ const layout = this.buildLayout();
3647
+ let reveal;
3648
+ if (this.charsPerSec !== void 0) {
3649
+ const elapsed = (this.world?.time ?? 0) - this.startTime;
3650
+ reveal = typewriterCount(this.text.length, elapsed, this.charsPerSec);
3651
+ }
3652
+ const x = this.center ? -(layout.width * this.cell) / 2 : 0;
3653
+ const y = this.center ? -(layout.height * this.cell) / 2 : 0;
3654
+ for (const cmd of textToCommands(this.font, layout, {
3655
+ cell: this.cell,
3656
+ x,
3657
+ y,
3658
+ z: this.z,
3659
+ color: this.color,
3660
+ transform: world,
3661
+ reveal
3662
+ })) {
3663
+ out.push(cmd);
3664
+ }
3665
+ }
3666
+ };
3667
+
3668
+ // src/art/autotile.ts
3669
+ function gridFromRows(rows, solid = "#") {
3670
+ return rows.map((r) => [...r].map((c) => solid.includes(c)));
3671
+ }
3672
+ function at(grid, x, y) {
3673
+ return y >= 0 && y < grid.length && x >= 0 && x < grid[y].length && grid[y][x];
3674
+ }
3675
+ var Edge = { N: 1, E: 2, S: 4, W: 8 };
3676
+ function mask4(grid, x, y) {
3677
+ return (at(grid, x, y - 1) ? Edge.N : 0) | (at(grid, x + 1, y) ? Edge.E : 0) | (at(grid, x, y + 1) ? Edge.S : 0) | (at(grid, x - 1, y) ? Edge.W : 0);
3678
+ }
3679
+ function mask8(grid, x, y) {
3680
+ let m = 0;
3681
+ const dirs = [
3682
+ [0, -1],
3683
+ [1, -1],
3684
+ [1, 0],
3685
+ [1, 1],
3686
+ [0, 1],
3687
+ [-1, 1],
3688
+ [-1, 0],
3689
+ [-1, -1]
3690
+ ];
3691
+ for (let i = 0; i < 8; i++) if (at(grid, x + dirs[i][0], y + dirs[i][1])) m |= 1 << i;
3692
+ return m;
3693
+ }
3694
+ var WangFrame = { Isolated: 0, Cap: 1, Straight: 2, Bend: 3, Tee: 4, Cross: 5 };
3695
+ function rotateCW(m) {
3696
+ let r = 0;
3697
+ if (m & Edge.N) r |= Edge.E;
3698
+ if (m & Edge.E) r |= Edge.S;
3699
+ if (m & Edge.S) r |= Edge.W;
3700
+ if (m & Edge.W) r |= Edge.N;
3701
+ return r;
3702
+ }
3703
+ var WANG4 = (() => {
3704
+ const bases = [
3705
+ [WangFrame.Isolated, 0],
3706
+ [WangFrame.Cap, Edge.N],
3707
+ [WangFrame.Straight, Edge.N | Edge.S],
3708
+ [WangFrame.Bend, Edge.N | Edge.E],
3709
+ [WangFrame.Tee, Edge.N | Edge.E | Edge.S],
3710
+ [WangFrame.Cross, 15]
3711
+ ];
3712
+ const table = new Array(16);
3713
+ for (const [frame, mask0] of bases) {
3714
+ let m = mask0;
3715
+ for (let rotation = 0; rotation < 4; rotation++) {
3716
+ if (table[m] === void 0) table[m] = { mask: m, frame, rotation };
3717
+ m = rotateCW(m);
3718
+ }
3719
+ }
3720
+ return table;
3721
+ })();
3722
+ function wangTile(mask) {
3723
+ return WANG4[mask & 15];
3724
+ }
3725
+ function autotile4(grid) {
3726
+ return grid.map((row, y) => row.map((solid, x) => solid ? wangTile(mask4(grid, x, y)) : null));
3727
+ }
3728
+ function marchingSquaresCases(grid) {
3729
+ const h = grid.length;
3730
+ const w = h ? grid[0].length : 0;
3731
+ const out = [];
3732
+ for (let y = 0; y < h - 1; y++) {
3733
+ const row = [];
3734
+ for (let x = 0; x < w - 1; x++) {
3735
+ row.push(
3736
+ (at(grid, x, y) ? 1 : 0) | (at(grid, x + 1, y) ? 2 : 0) | (at(grid, x + 1, y + 1) ? 4 : 0) | (at(grid, x, y + 1) ? 8 : 0)
3737
+ );
3738
+ }
3739
+ out.push(row);
3740
+ }
3741
+ return out;
3742
+ }
3743
+ var MS_SEGMENTS = [
3744
+ [],
3745
+ // 0
3746
+ [["L", "T"]],
3747
+ // 1 TL
3748
+ [["T", "R"]],
3749
+ // 2 TR
3750
+ [["L", "R"]],
3751
+ // 3 TL,TR
3752
+ [["R", "B"]],
3753
+ // 4 BR
3754
+ [["L", "T"], ["R", "B"]],
3755
+ // 5 saddle
3756
+ [["T", "B"]],
3757
+ // 6 TR,BR
3758
+ [["L", "B"]],
3759
+ // 7 TL,TR,BR
3760
+ [["B", "L"]],
3761
+ // 8 BL
3762
+ [["T", "B"]],
3763
+ // 9 TL,BL
3764
+ [["T", "R"], ["B", "L"]],
3765
+ // 10 saddle
3766
+ [["R", "B"]],
3767
+ // 11 TL,TR,BL
3768
+ [["L", "R"]],
3769
+ // 12 BR,BL
3770
+ [["T", "R"]],
3771
+ // 13 TL,BR,BL
3772
+ [["L", "T"]],
3773
+ // 14 TR,BR,BL
3774
+ []
3775
+ // 15
3776
+ ];
3777
+ function marchingSquaresContours(grid, options = {}) {
3778
+ const cell = options.cell ?? 1;
3779
+ const ox = options.x ?? 0;
3780
+ const oy = options.y ?? 0;
3781
+ const cases = marchingSquaresCases(grid);
3782
+ const segs = [];
3783
+ const mid = (edge, cx, cy) => {
3784
+ switch (edge) {
3785
+ case "T":
3786
+ return { x: cx + cell / 2, y: cy };
3787
+ case "R":
3788
+ return { x: cx + cell, y: cy + cell / 2 };
3789
+ case "B":
3790
+ return { x: cx + cell / 2, y: cy + cell };
3791
+ case "L":
3792
+ return { x: cx, y: cy + cell / 2 };
3793
+ }
3794
+ };
3795
+ for (let y = 0; y < cases.length; y++) {
3796
+ for (let x = 0; x < cases[y].length; x++) {
3797
+ const cx = ox + x * cell;
3798
+ const cy = oy + y * cell;
3799
+ for (const [e1, e2] of MS_SEGMENTS[cases[y][x]]) segs.push({ a: mid(e1, cx, cy), b: mid(e2, cx, cy) });
3800
+ }
3801
+ }
3802
+ return segs;
3803
+ }
3804
+ function autotileToCommands(grid, options = {}) {
3805
+ const tile = options.tile ?? 8;
3806
+ const ox = options.x ?? 0;
3807
+ const oy = options.y ?? 0;
3808
+ const z = options.z ?? 0;
3809
+ const transform = options.transform ?? IDENTITY;
3810
+ const fill = options.fill ?? "#888";
3811
+ const edge = options.edge;
3812
+ const edgeWidth = options.edgeWidth ?? Math.max(1, tile / 8);
3813
+ const out = [];
3814
+ for (let y = 0; y < grid.length; y++) {
3815
+ for (let x = 0; x < grid[y].length; x++) {
3816
+ if (!grid[y][x]) continue;
3817
+ const px = ox + x * tile;
3818
+ const py = oy + y * tile;
3819
+ out.push({ kind: "rect", x: px, y: py, w: tile, h: tile, transform, z, fill });
3820
+ if (!edge) continue;
3821
+ const m = mask4(grid, x, y);
3822
+ const line = (points) => out.push({ kind: "poly", points, closed: false, transform, z: z + 1, stroke: edge, strokeWidth: edgeWidth });
3823
+ if (!(m & Edge.N)) line([px, py, px + tile, py]);
3824
+ if (!(m & Edge.E)) line([px + tile, py, px + tile, py + tile]);
3825
+ if (!(m & Edge.S)) line([px, py + tile, px + tile, py + tile]);
3826
+ if (!(m & Edge.W)) line([px, py, px, py + tile]);
3827
+ }
3828
+ }
3829
+ return out;
3830
+ }
3831
+ function contourToCommands(grid, options = {}) {
3832
+ const tile = options.tile ?? 8;
3833
+ const transform = options.transform ?? IDENTITY;
3834
+ const z = options.z ?? 0;
3835
+ const stroke = options.edge ?? options.fill ?? "#333";
3836
+ const strokeWidth = options.edgeWidth ?? Math.max(1, tile / 8);
3837
+ const segs = marchingSquaresContours(grid, { cell: tile, x: options.x ?? 0, y: options.y ?? 0 });
3838
+ return segs.map((s) => ({
3839
+ kind: "poly",
3840
+ points: [s.a.x, s.a.y, s.b.x, s.b.y],
3841
+ closed: false,
3842
+ transform,
3843
+ z,
3844
+ stroke,
3845
+ strokeWidth,
3846
+ round: true
3847
+ }));
3848
+ }
3849
+
3850
+ // src/procgen/grid.ts
3851
+ function makeGrid(cols, rows, fill = 0) {
3852
+ return { cols, rows, cells: new Array(cols * rows).fill(fill) };
3853
+ }
3854
+ function gridAt(g, x, y) {
3855
+ if (x < 0 || y < 0 || x >= g.cols || y >= g.rows) return 1;
3856
+ return g.cells[y * g.cols + x];
3857
+ }
3858
+ function gridSet(g, x, y, v) {
3859
+ if (x < 0 || y < 0 || x >= g.cols || y >= g.rows) return;
3860
+ g.cells[y * g.cols + x] = v;
3861
+ }
3862
+ function solidNeighbours(g, x, y) {
3863
+ let n2 = 0;
3864
+ for (let dy = -1; dy <= 1; dy++) {
3865
+ for (let dx = -1; dx <= 1; dx++) {
3866
+ if (dx === 0 && dy === 0) continue;
3867
+ if (gridAt(g, x + dx, y + dy) === 1) n2++;
3868
+ }
3869
+ }
3870
+ return n2;
3871
+ }
3872
+ function gridToTilemap(g, tileSize = 32) {
3873
+ const tiles = g.cells.map((c) => c === 1 ? TILE.SOLID : TILE.EMPTY);
3874
+ return { cols: g.cols, rows: g.rows, tileSize, tiles };
3875
+ }
3876
+
3877
+ // src/procgen/scatter.ts
3878
+ function cellHash(x, y, seed = 0) {
3879
+ let h = (seed ^ 2654435769) >>> 0;
3880
+ h = Math.imul(h ^ (x | 0), 2246822507) >>> 0;
3881
+ h = Math.imul(h ^ (y | 0), 3266489909) >>> 0;
3882
+ h ^= h >>> 13;
3883
+ h = Math.imul(h, 668265263) >>> 0;
3884
+ h ^= h >>> 15;
3885
+ return h >>> 0;
3886
+ }
3887
+ function cellFloat(x, y, seed = 0) {
3888
+ return cellHash(x, y, seed) / 4294967296;
3889
+ }
3890
+ function cellInt(x, y, n2, seed = 0) {
3891
+ return Math.floor(cellFloat(x, y, seed) * n2);
3892
+ }
3893
+ function scatter(x, y, probability, seed = 0) {
3894
+ return cellFloat(x, y, seed) < probability;
3895
+ }
3896
+ function scatterCells(x0, y0, cols, rows, probability, seed = 0) {
3897
+ const out = [];
3898
+ for (let y = y0; y < y0 + rows; y++) {
3899
+ for (let x = x0; x < x0 + cols; x++) {
3900
+ if (scatter(x, y, probability, seed)) out.push({ x, y });
3901
+ }
3902
+ }
3903
+ return out;
3904
+ }
3905
+ var smooth = (t) => t * t * (3 - 2 * t);
3906
+ function valueNoise(x, y, seed = 0) {
3907
+ const x0 = Math.floor(x);
3908
+ const y0 = Math.floor(y);
3909
+ const fx = smooth(x - x0);
3910
+ const fy = smooth(y - y0);
3911
+ const c00 = cellFloat(x0, y0, seed);
3912
+ const c10 = cellFloat(x0 + 1, y0, seed);
3913
+ const c01 = cellFloat(x0, y0 + 1, seed);
3914
+ const c11 = cellFloat(x0 + 1, y0 + 1, seed);
3915
+ const top = c00 + (c10 - c00) * fx;
3916
+ const bot = c01 + (c11 - c01) * fx;
3917
+ return top + (bot - top) * fy;
3918
+ }
3919
+ function fractalNoise(x, y, seed = 0, opts = {}) {
3920
+ const octaves = opts.octaves ?? 4;
3921
+ const lacunarity = opts.lacunarity ?? 2;
3922
+ const gain = opts.gain ?? 0.5;
3923
+ let amp = 1;
3924
+ let freq = 1;
3925
+ let sum = 0;
3926
+ let norm = 0;
3927
+ for (let o = 0; o < octaves; o++) {
3928
+ sum += amp * valueNoise(x * freq, y * freq, seed + o);
3929
+ norm += amp;
3930
+ amp *= gain;
3931
+ freq *= lacunarity;
3932
+ }
3933
+ return norm === 0 ? 0 : sum / norm;
3934
+ }
3935
+
3936
+ // src/procgen/cave.ts
3937
+ function generateCave(rng, opts) {
3938
+ const { cols, rows } = opts;
3939
+ const fill = opts.fill ?? 0.45;
3940
+ const steps = opts.steps ?? 4;
3941
+ const birth = opts.birth ?? 5;
3942
+ const survive = opts.survive ?? 4;
3943
+ const border = opts.border ?? true;
3944
+ let g = makeGrid(cols, rows, 0);
3945
+ for (let y = 0; y < rows; y++) {
3946
+ for (let x = 0; x < cols; x++) {
3947
+ const edge = border && (x === 0 || y === 0 || x === cols - 1 || y === rows - 1);
3948
+ gridSet(g, x, y, edge || rng.chance(fill) ? 1 : 0);
3949
+ }
3950
+ }
3951
+ for (let s = 0; s < steps; s++) {
3952
+ const next = makeGrid(cols, rows, 0);
3953
+ for (let y = 0; y < rows; y++) {
3954
+ for (let x = 0; x < cols; x++) {
3955
+ if (border && (x === 0 || y === 0 || x === cols - 1 || y === rows - 1)) {
3956
+ gridSet(next, x, y, 1);
3957
+ continue;
3958
+ }
3959
+ const n2 = solidNeighbours(g, x, y);
3960
+ const wasSolid = g.cells[y * cols + x] === 1;
3961
+ gridSet(next, x, y, (wasSolid ? n2 >= survive : n2 >= birth) ? 1 : 0);
3962
+ }
3963
+ }
3964
+ g = next;
3965
+ }
3966
+ return g;
3967
+ }
3968
+
3969
+ // src/procgen/terrain.ts
3970
+ function terrainHeight(col, opts) {
3971
+ const scale = opts.scale ?? 0.08;
3972
+ const seed = opts.seed ?? 0;
3973
+ const n2 = fractalNoise(col * scale, 0, seed, opts.fractal) * 2 - 1;
3974
+ let row = Math.round(opts.base + n2 * opts.amplitude);
3975
+ if (opts.minRow !== void 0 || opts.maxRow !== void 0) {
3976
+ row = clamp(row, opts.minRow ?? -Infinity, opts.maxRow ?? Infinity);
3977
+ }
3978
+ return row;
3979
+ }
3980
+ function terrainSlice(startCol, count, opts) {
3981
+ const out = new Array(count);
3982
+ for (let i = 0; i < count; i++) out[i] = terrainHeight(startCol + i, opts);
3983
+ return out;
3984
+ }
3985
+ function isGround(col, row, opts) {
3986
+ return row >= terrainHeight(col, opts);
3987
+ }
3988
+
3989
+ // src/procgen/rooms.ts
3990
+ function roomCenter(r) {
3991
+ return { x: Math.floor(r.x + r.w / 2), y: Math.floor(r.y + r.h / 2) };
3992
+ }
3993
+ function overlaps(a, b) {
3994
+ 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;
3995
+ }
3996
+ function carveRoom(g, r) {
3997
+ for (let y = r.y; y < r.y + r.h; y++) {
3998
+ for (let x = r.x; x < r.x + r.w; x++) gridSet(g, x, y, 0);
3999
+ }
4000
+ }
4001
+ function carveHCorridor(g, x0, x1, y) {
4002
+ for (let x = Math.min(x0, x1); x <= Math.max(x0, x1); x++) gridSet(g, x, y, 0);
4003
+ }
4004
+ function carveVCorridor(g, y0, y1, x) {
4005
+ for (let y = Math.min(y0, y1); y <= Math.max(y0, y1); y++) gridSet(g, x, y, 0);
4006
+ }
4007
+ function generateDungeon(rng, opts) {
4008
+ const { cols, rows } = opts;
4009
+ const attempts = opts.attempts ?? 12;
4010
+ const minSize = opts.minSize ?? 4;
4011
+ const maxSize = opts.maxSize ?? 8;
4012
+ const margin = opts.border ?? true ? 1 : 0;
4013
+ const g = makeGrid(cols, rows, 1);
4014
+ const rooms = [];
4015
+ for (let i = 0; i < attempts; i++) {
4016
+ const w = rng.intRange(minSize, maxSize);
4017
+ const h = rng.intRange(minSize, maxSize);
4018
+ const x = rng.intRange(margin, cols - w - margin - 1);
4019
+ const y = rng.intRange(margin, rows - h - margin - 1);
4020
+ if (x < margin || y < margin) continue;
4021
+ const room = { x, y, w, h };
4022
+ if (rooms.some((r) => overlaps(room, r))) continue;
4023
+ carveRoom(g, room);
4024
+ if (rooms.length > 0) {
4025
+ const prev = roomCenter(rooms[rooms.length - 1]);
4026
+ const cur = roomCenter(room);
4027
+ if (rng.chance(0.5)) {
4028
+ carveHCorridor(g, prev.x, cur.x, prev.y);
4029
+ carveVCorridor(g, prev.y, cur.y, cur.x);
4030
+ } else {
4031
+ carveVCorridor(g, prev.y, cur.y, prev.x);
4032
+ carveHCorridor(g, prev.x, cur.x, cur.y);
4033
+ }
4034
+ }
4035
+ rooms.push(room);
4036
+ }
4037
+ return { rooms, grid: g };
4038
+ }
4039
+
4040
+ // src/audio/audio.ts
4041
+ var DEFAULT_VOLUMES = { master: 0.7, music: 0.6, sfx: 0.8, muted: false };
4042
+ var AudioBus = class {
4043
+ ctx = null;
4044
+ master = null;
4045
+ musicGain = null;
4046
+ sfxGain = null;
4047
+ vol = { ...DEFAULT_VOLUMES };
4048
+ padOn = false;
4049
+ get available() {
4050
+ return typeof globalThis.AudioContext !== "undefined" || "webkitAudioContext" in globalThis;
4051
+ }
4052
+ get started() {
4053
+ return !!this.ctx;
4054
+ }
4055
+ /** Must be called from a user gesture (browser autoplay policy). */
4056
+ start() {
4057
+ if (this.ctx) {
4058
+ void this.ctx.resume();
4059
+ return;
4060
+ }
4061
+ if (!this.available) return;
4062
+ const Ctx = globalThis.AudioContext || globalThis.webkitAudioContext;
4063
+ this.ctx = new Ctx();
4064
+ this.master = this.ctx.createGain();
4065
+ this.musicGain = this.ctx.createGain();
4066
+ this.sfxGain = this.ctx.createGain();
4067
+ this.musicGain.connect(this.master);
4068
+ this.sfxGain.connect(this.master);
4069
+ this.master.connect(this.ctx.destination);
4070
+ this.applyVolumes();
4071
+ }
4072
+ setVolumes(v) {
4073
+ this.vol = { ...this.vol, ...v };
1889
4074
  this.applyVolumes();
1890
4075
  }
1891
4076
  getVolumes() {
@@ -1901,7 +4086,7 @@ var AudioBus = class {
1901
4086
  /** Play a single tone (no-op if audio unstarted). */
1902
4087
  tone(spec) {
1903
4088
  if (!this.ctx || !this.sfxGain) return;
1904
- const { freq, duration, type = "sine", gain = 0.2, delay = 0 } = spec;
4089
+ const { freq, duration, type = "sine", gain = 0.2, delay = 0, pan } = spec;
1905
4090
  const t = this.ctx.currentTime + delay;
1906
4091
  const osc = this.ctx.createOscillator();
1907
4092
  osc.type = type;
@@ -1911,10 +4096,26 @@ var AudioBus = class {
1911
4096
  g.gain.linearRampToValueAtTime(gain, t + 8e-3);
1912
4097
  g.gain.exponentialRampToValueAtTime(1e-4, t + duration);
1913
4098
  osc.connect(g);
1914
- g.connect(this.sfxGain);
4099
+ if (pan !== void 0 && typeof this.ctx.createStereoPanner === "function") {
4100
+ const p = this.ctx.createStereoPanner();
4101
+ p.pan.value = Math.max(-1, Math.min(1, pan));
4102
+ g.connect(p);
4103
+ p.connect(this.sfxGain);
4104
+ } else {
4105
+ g.connect(this.sfxGain);
4106
+ }
1915
4107
  osc.start(t);
1916
4108
  osc.stop(t + duration + 0.05);
1917
4109
  }
4110
+ /**
4111
+ * A positional cue: gain falls with distance, pan follows the horizontal
4112
+ * offset. dx/dist in design-space px; hearing range sets the falloff.
4113
+ */
4114
+ spatial(freq, dx, dist, hearing = 600, duration = 0.18, type = "sawtooth") {
4115
+ if (dist > hearing) return;
4116
+ const near = 1 - dist / hearing;
4117
+ this.tone({ freq, duration, type, gain: 0.04 + near * near * 0.3, pan: Math.max(-1, Math.min(1, dx / (hearing * 0.6))) });
4118
+ }
1918
4119
  /** Play a sequence of tones as an arpeggio/chord. */
1919
4120
  play(tones) {
1920
4121
  for (const t of tones) this.tone(t);
@@ -2181,6 +4382,187 @@ var Shell = class {
2181
4382
  }
2182
4383
  };
2183
4384
 
4385
+ // src/ui/transition.ts
4386
+ var BAYER4 = [0, 8, 2, 10, 12, 4, 14, 6, 3, 11, 1, 9, 15, 7, 13, 5];
4387
+ var smoothstep2 = (t) => t * t * (3 - 2 * t);
4388
+ var ScreenTransition = class extends Node {
4389
+ type = "ScreenTransition";
4390
+ kind;
4391
+ color;
4392
+ screenW;
4393
+ screenH;
4394
+ cell;
4395
+ /** 0 = fully clear, 1 = fully covering. */
4396
+ coverage = 0;
4397
+ queue = [];
4398
+ constructor(config = {}) {
4399
+ super(config);
4400
+ this.cosmetic = true;
4401
+ this.kind = config.kind ?? "fade";
4402
+ this.color = config.color ?? "#1a1410";
4403
+ this.screenW = config.width ?? 1280;
4404
+ this.screenH = config.height ?? 720;
4405
+ this.cell = config.cell ?? 28;
4406
+ }
4407
+ /** True while a wipe is animating. */
4408
+ get busy() {
4409
+ return this.queue.length > 0;
4410
+ }
4411
+ enqueue(to, dur, onEnd) {
4412
+ this.queue.push({ from: void 0, to, dur: Math.max(1e-4, dur), elapsed: 0, onEnd });
4413
+ }
4414
+ /** Animate the overlay to fully covering over `dur` seconds. */
4415
+ cover(dur = 0.4, onEnd) {
4416
+ this.enqueue(1, dur, onEnd);
4417
+ return this;
4418
+ }
4419
+ /** Animate the overlay back to fully clear over `dur` seconds. */
4420
+ reveal(dur = 0.4, onEnd) {
4421
+ this.enqueue(0, dur, onEnd);
4422
+ return this;
4423
+ }
4424
+ /** Hold the current coverage for `dur` seconds (chains after cover/reveal). */
4425
+ hold(dur, onEnd) {
4426
+ const target = this.queue.length ? this.queue[this.queue.length - 1].to : this.coverage;
4427
+ this.enqueue(target, dur, onEnd);
4428
+ return this;
4429
+ }
4430
+ /** The classic cover → onMidpoint → reveal sequence. Returns this for chaining. */
4431
+ wipe(opts = {}) {
4432
+ const cover = opts.cover ?? 0.4;
4433
+ this.cover(cover, () => {
4434
+ this.emit("covered", void 0);
4435
+ opts.onMidpoint?.();
4436
+ });
4437
+ if (opts.hold) this.hold(opts.hold);
4438
+ this.reveal(opts.reveal ?? cover, () => {
4439
+ this.emit("done", void 0);
4440
+ opts.onDone?.();
4441
+ });
4442
+ return this;
4443
+ }
4444
+ /** Signal emitted at full cover (the midpoint). */
4445
+ get covered() {
4446
+ return this.signal("covered");
4447
+ }
4448
+ /** Signal emitted when a wipe fully finishes revealing. */
4449
+ get done() {
4450
+ return this.signal("done");
4451
+ }
4452
+ onProcess(dt) {
4453
+ const step = this.queue[0];
4454
+ if (!step) return;
4455
+ if (step.from === void 0) step.from = this.coverage;
4456
+ step.elapsed += dt;
4457
+ const t = clamp(step.elapsed / step.dur, 0, 1);
4458
+ this.coverage = lerp(step.from, step.to, smoothstep2(t));
4459
+ if (t >= 1) {
4460
+ this.coverage = step.to;
4461
+ const end = step.onEnd;
4462
+ this.queue.shift();
4463
+ end?.();
4464
+ }
4465
+ }
4466
+ draw(out, _world) {
4467
+ const c = this.coverage;
4468
+ if (c <= 0) return;
4469
+ const z = this.z;
4470
+ if (this.kind === "fade") {
4471
+ out.push({ kind: "rect", x: 0, y: 0, w: this.screenW, h: this.screenH, fill: this.color, opacity: c, transform: IDENTITY, z });
4472
+ return;
4473
+ }
4474
+ if (this.kind === "circle") {
4475
+ const cx = this.screenW / 2;
4476
+ const cy = this.screenH / 2;
4477
+ const diag = Math.sqrt(cx * cx + cy * cy);
4478
+ out.push({ kind: "circle", cx, cy, radius: c * diag, fill: this.color, transform: IDENTITY, z });
4479
+ return;
4480
+ }
4481
+ const cols = Math.ceil(this.screenW / this.cell);
4482
+ const rows = Math.ceil(this.screenH / this.cell);
4483
+ for (let cy = 0; cy < rows; cy++) {
4484
+ for (let cx = 0; cx < cols; cx++) {
4485
+ const threshold = (BAYER4[(cx & 3) + ((cy & 3) << 2)] + 0.5) / 16;
4486
+ if (threshold < c) {
4487
+ out.push({ kind: "rect", x: cx * this.cell, y: cy * this.cell, w: this.cell, h: this.cell, fill: this.color, transform: IDENTITY, z });
4488
+ }
4489
+ }
4490
+ }
4491
+ }
4492
+ };
4493
+ var CinematicPlayer = class extends Node {
4494
+ type = "CinematicPlayer";
4495
+ steps = [];
4496
+ index = -1;
4497
+ elapsed = 0;
4498
+ running = false;
4499
+ onFinish;
4500
+ constructor(config = {}) {
4501
+ super(config);
4502
+ this.cosmetic = true;
4503
+ }
4504
+ /** Load and start a sequence. `onDone` fires after the last step advances. */
4505
+ play(steps, onDone) {
4506
+ this.steps = steps;
4507
+ this.onFinish = onDone;
4508
+ this.index = -1;
4509
+ this.elapsed = 0;
4510
+ this.running = steps.length > 0;
4511
+ this.advance();
4512
+ return this;
4513
+ }
4514
+ /** True while a sequence is playing. */
4515
+ get active() {
4516
+ return this.running;
4517
+ }
4518
+ /** Index of the step currently on screen (−1 before play / after finish). */
4519
+ get step() {
4520
+ return this.index;
4521
+ }
4522
+ /** Signal emitted when the whole sequence finishes. */
4523
+ get finished() {
4524
+ return this.signal("finished");
4525
+ }
4526
+ advance() {
4527
+ this.index++;
4528
+ this.elapsed = 0;
4529
+ if (this.index >= this.steps.length) {
4530
+ this.running = false;
4531
+ this.index = -1;
4532
+ this.emit("finished", void 0);
4533
+ this.onFinish?.();
4534
+ return;
4535
+ }
4536
+ this.steps[this.index].enter?.();
4537
+ }
4538
+ /** Abort the sequence immediately (no further enters, no finish callback). */
4539
+ stop() {
4540
+ this.running = false;
4541
+ this.index = -1;
4542
+ this.steps = [];
4543
+ }
4544
+ onProcess(dt) {
4545
+ if (!this.running) return;
4546
+ this.elapsed += dt;
4547
+ const s = this.steps[this.index];
4548
+ const durationDone = this.elapsed >= (s.duration ?? 0);
4549
+ const gateOpen = s.until ? s.until() : true;
4550
+ if (durationDone && gateOpen) this.advance();
4551
+ }
4552
+ };
4553
+ function addTransition(parent, config = {}) {
4554
+ const t = new ScreenTransition({ z: 1e4, ...config });
4555
+ return parent.addChild(t);
4556
+ }
4557
+ function wipeStep(transition, opts = {}) {
4558
+ return {
4559
+ name: opts.name ?? "wipe",
4560
+ enter: () => transition.wipe(opts),
4561
+ until: () => !transition.busy
4562
+ };
4563
+ }
4564
+ var screenRect = (w, h) => ({ x: 0, y: 0, w, h });
4565
+
2184
4566
  // src/verify/solver.ts
2185
4567
  function solve(puzzle, options = {}) {
2186
4568
  const maxDepth = options.maxDepth ?? 60;
@@ -2394,6 +4776,101 @@ function steer2D(px, py, tx, ty, out, dead = 8) {
2394
4776
  else if (py > ty + dead) out.push("up");
2395
4777
  }
2396
4778
 
4779
+ // src/verify/layout.ts
4780
+ function textBox(cmd) {
4781
+ const w = cmd.text.length * cmd.size * 0.56;
4782
+ const h = cmd.size * 1.25;
4783
+ const cx = cmd.transform.e + cmd.x;
4784
+ const by = cmd.transform.f + cmd.y;
4785
+ const left = cmd.align === "center" ? cx - w / 2 : cmd.align === "right" ? cx - w : cx;
4786
+ return { cmd, x: left, y: by - cmd.size, w, h };
4787
+ }
4788
+ function shapeBox(cmd) {
4789
+ const ex = cmd.transform.e;
4790
+ const ey = cmd.transform.f;
4791
+ switch (cmd.kind) {
4792
+ case "rect":
4793
+ return { x: ex + cmd.x, y: ey + cmd.y, w: cmd.w, h: cmd.h };
4794
+ case "circle":
4795
+ return { x: ex + cmd.cx - cmd.radius, y: ey + cmd.cy - cmd.radius, w: cmd.radius * 2, h: cmd.radius * 2 };
4796
+ case "poly": {
4797
+ let minX = Infinity;
4798
+ let minY = Infinity;
4799
+ let maxX = -Infinity;
4800
+ let maxY = -Infinity;
4801
+ for (let i = 0; i < cmd.points.length; i += 2) {
4802
+ minX = Math.min(minX, cmd.points[i]);
4803
+ maxX = Math.max(maxX, cmd.points[i]);
4804
+ minY = Math.min(minY, cmd.points[i + 1]);
4805
+ maxY = Math.max(maxY, cmd.points[i + 1]);
4806
+ }
4807
+ return { x: ex + minX, y: ey + minY, w: maxX - minX, h: maxY - minY };
4808
+ }
4809
+ default:
4810
+ return null;
4811
+ }
4812
+ }
4813
+ var overlaps2 = (a, b) => a.x < b.x + b.w && a.x + a.w > b.x && a.y < b.y + b.h && a.y + a.h > b.y;
4814
+ var contains = (outer, inner, pad = 2) => outer.x - pad <= inner.x && outer.y - pad <= inner.y && outer.x + outer.w + pad >= inner.x + inner.w && outer.y + outer.h + pad >= inner.y + inner.h;
4815
+ function layoutIssues(commands, opts = {}) {
4816
+ const backgroundZ = opts.backgroundZ ?? 1;
4817
+ const margin = opts.margin ?? 0;
4818
+ const issues = [];
4819
+ const texts = [];
4820
+ for (const c of commands) if (c.kind === "text" && c.text.trim().length) texts.push(textBox(c));
4821
+ for (const t of texts) {
4822
+ const r = { x: t.x - margin, y: t.y - margin, w: t.w + margin * 2, h: t.h + margin * 2 };
4823
+ let panelZ = -Infinity;
4824
+ for (const c of commands) {
4825
+ if (c.kind === "text") continue;
4826
+ const b = shapeBox(c);
4827
+ if (b && contains(b, t) && (c.opacity === void 0 || c.opacity >= 0.6)) panelZ = Math.max(panelZ, c.z ?? 0);
4828
+ }
4829
+ for (const c of commands) {
4830
+ if (c.kind === "text") continue;
4831
+ if ((c.z ?? 0) <= backgroundZ) continue;
4832
+ if ((c.z ?? 0) <= panelZ) continue;
4833
+ if (c.opacity !== void 0 && c.opacity < 0.25) continue;
4834
+ const b = shapeBox(c);
4835
+ if (!b) continue;
4836
+ if (overlaps2(r, b) && !contains(b, t)) {
4837
+ issues.push(`text "${clip(t.cmd.text)}" collides with a ${c.kind} (z${c.z}) at ~(${Math.round(b.x)},${Math.round(b.y)}) \u2014 back it with a panel, or stay clear`);
4838
+ }
4839
+ }
4840
+ }
4841
+ for (let i = 0; i < texts.length; i++)
4842
+ for (let j = i + 1; j < texts.length; j++)
4843
+ if (overlaps2(texts[i], texts[j])) issues.push(`texts "${clip(texts[i].cmd.text)}" and "${clip(texts[j].cmd.text)}" overlap`);
4844
+ return dedupe(issues);
4845
+ }
4846
+ var clip = (s) => s.length > 28 ? s.slice(0, 25) + "\u2026" : s;
4847
+ var dedupe = (a) => [...new Set(a)];
4848
+ function keyMentions(code) {
4849
+ if (code.startsWith("Digit")) return [code.slice(5)];
4850
+ if (code.startsWith("Key")) return [code.slice(3).toLowerCase()];
4851
+ if (code.startsWith("Arrow")) return ["arrow", "\u2190", "\u2192", "\u2191", "\u2193", code.slice(5).toLowerCase()];
4852
+ if (code.startsWith("Shift")) return ["shift"];
4853
+ if (code === "Space") return ["space"];
4854
+ if (code === "Enter") return ["enter"];
4855
+ if (code === "Period") return ["."];
4856
+ return [code.toLowerCase()];
4857
+ }
4858
+ function missingControlHints(world, inputMap) {
4859
+ const textBlob = world.render().filter((c) => c.kind === "text").map((c) => c.text.toLowerCase()).join(" \xB7 ");
4860
+ const missing = [];
4861
+ const mentions = (n2) => {
4862
+ if (!n2.length) return false;
4863
+ if (n2.length <= 2 && /^[a-z0-9.]+$/.test(n2)) return new RegExp(`(^|[^a-z0-9])${n2.replace(".", "\\.")}($|[^a-z0-9])`).test(textBlob);
4864
+ return textBlob.includes(n2);
4865
+ };
4866
+ for (const [action, keys] of Object.entries(inputMap)) {
4867
+ if (action === "restart") continue;
4868
+ const names = [action.toLowerCase().replace(/-\d+$/, ""), ...keys.flatMap(keyMentions)];
4869
+ if (!names.some(mentions)) missing.push(action);
4870
+ }
4871
+ return dedupe(missing);
4872
+ }
4873
+
2397
4874
  // src/verify/feel.ts
2398
4875
  function recordTimeline(world, frames) {
2399
4876
  const out = [world.probe()];
@@ -2462,14 +4939,1781 @@ function renderFilmstrip(world, frames, opts) {
2462
4939
  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
4940
  }
2464
4941
 
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
- /**
4942
+ // src/logic/fsm.ts
4943
+ var Fsm = class {
4944
+ constructor(states, transitions, initial, ctx) {
4945
+ this.states = states;
4946
+ this.transitions = transitions;
4947
+ this.ctx = ctx;
4948
+ this.current = initial;
4949
+ this.states[initial]?.onEnter?.(ctx);
4950
+ }
4951
+ states;
4952
+ transitions;
4953
+ ctx;
4954
+ /** The active state key — this is the whole serializable state. */
4955
+ current;
4956
+ /**
4957
+ * Advance one fixed step: fire the first satisfied transition (if any), then
4958
+ * run the current state's `onUpdate`. A transition runs its target's
4959
+ * `onUpdate` this same tick, so entering and acting aren't a frame apart.
4960
+ */
4961
+ update(dt) {
4962
+ for (const t of this.transitions) {
4963
+ if ((t.from === "*" || t.from === this.current) && t.when(this.ctx)) {
4964
+ this.go(t.to);
4965
+ break;
4966
+ }
4967
+ }
4968
+ this.states[this.current]?.onUpdate?.(this.ctx, dt);
4969
+ }
4970
+ /** Force a transition (fires onLeave/onEnter). No-op if already there. */
4971
+ go(to) {
4972
+ if (to === this.current) return;
4973
+ this.states[this.current]?.onLeave?.(this.ctx);
4974
+ this.current = to;
4975
+ this.states[to]?.onEnter?.(this.ctx);
4976
+ }
4977
+ /** True when in one of the given states. */
4978
+ is(...states) {
4979
+ return states.includes(this.current);
4980
+ }
4981
+ /** Serialize (games persist this in `world.state`; the handlers are code). */
4982
+ getState() {
4983
+ return this.current;
4984
+ }
4985
+ /** Restore without firing enter/leave (it's a resume, not a transition). */
4986
+ setState(state) {
4987
+ this.current = state;
4988
+ }
4989
+ };
4990
+ var PhaseClock = class {
4991
+ constructor(defs, initial) {
4992
+ this.defs = defs;
4993
+ if (!defs[initial]) throw new Error(`hayao: PhaseClock has no phase '${initial}'`);
4994
+ this.phase = initial;
4995
+ }
4996
+ defs;
4997
+ /** Current phase key — serializable with `elapsed`. */
4998
+ phase;
4999
+ /** Seconds spent in the current phase so far. */
5000
+ elapsed = 0;
5001
+ get duration() {
5002
+ return this.defs[this.phase]?.duration ?? 0;
5003
+ }
5004
+ /** Eased fraction 0→1 through the current phase (clamped). For cosmetic interp. */
5005
+ progress(ease = (t) => t) {
5006
+ const d = this.duration;
5007
+ const raw = d > 0 ? this.elapsed / d : 1;
5008
+ return ease(raw < 0 ? 0 : raw > 1 ? 1 : raw);
5009
+ }
5010
+ /** True once the current phase's duration has fully elapsed. */
5011
+ get done() {
5012
+ return this.elapsed >= this.duration;
5013
+ }
5014
+ /**
5015
+ * Advance by `dt`. When a phase completes and has a `next`, roll into it
5016
+ * (carrying any overshoot so long phases don't drift). Returns true on the
5017
+ * tick a new phase begins — the game's cue to commit the next logical step.
5018
+ */
5019
+ update(dt) {
5020
+ this.elapsed += dt;
5021
+ let advanced = false;
5022
+ let guard = 0;
5023
+ while (this.elapsed >= this.duration && this.defs[this.phase]?.next !== void 0 && guard++ < 1024) {
5024
+ const overshoot = this.elapsed - this.duration;
5025
+ this.phase = this.defs[this.phase].next;
5026
+ this.elapsed = overshoot;
5027
+ advanced = true;
5028
+ }
5029
+ return advanced;
5030
+ }
5031
+ /** Jump to a phase and reset its timer (fresh `onEnter`-style restart). */
5032
+ to(phase) {
5033
+ if (!this.defs[phase]) throw new Error(`hayao: PhaseClock has no phase '${phase}'`);
5034
+ this.phase = phase;
5035
+ this.elapsed = 0;
5036
+ }
5037
+ getState() {
5038
+ return { phase: this.phase, elapsed: this.elapsed };
5039
+ }
5040
+ setState(state) {
5041
+ this.phase = state.phase;
5042
+ this.elapsed = state.elapsed;
5043
+ }
5044
+ };
5045
+
5046
+ // src/logic/random.ts
5047
+ function totalWeight(weights) {
5048
+ let sum = 0;
5049
+ for (const w of weights) sum += w > 0 ? w : 0;
5050
+ return sum;
5051
+ }
5052
+ function weightedIndex(rng, weights) {
5053
+ const total = totalWeight(weights);
5054
+ if (total <= 0) throw new Error("hayao: weightedIndex needs at least one positive weight");
5055
+ let r = rng.float() * total;
5056
+ for (let i = 0; i < weights.length; i++) {
5057
+ const w = weights[i] > 0 ? weights[i] : 0;
5058
+ if (r < w) return i;
5059
+ r -= w;
5060
+ }
5061
+ for (let i = weights.length - 1; i >= 0; i--) if (weights[i] > 0) return i;
5062
+ return weights.length - 1;
5063
+ }
5064
+ function weightedPick(rng, items, weights) {
5065
+ if (items.length !== weights.length) throw new Error("hayao: weightedPick items/weights length mismatch");
5066
+ return items[weightedIndex(rng, weights)];
5067
+ }
5068
+ function pickEntry(rng, entries) {
5069
+ return entries[weightedIndex(rng, entries.map((e) => e.weight))].value;
5070
+ }
5071
+ var LootTable = class {
5072
+ values;
5073
+ /** Cumulative (prefix-sum) weights; last element is the total. */
5074
+ cumulative;
5075
+ total;
5076
+ constructor(entries) {
5077
+ if (entries.length === 0) throw new Error("hayao: LootTable needs at least one entry");
5078
+ const values = [];
5079
+ const cumulative = [];
5080
+ let sum = 0;
5081
+ for (const e of entries) {
5082
+ sum += e.weight > 0 ? e.weight : 0;
5083
+ values.push(e.value);
5084
+ cumulative.push(sum);
5085
+ }
5086
+ if (sum <= 0) throw new Error("hayao: LootTable needs at least one positive weight");
5087
+ this.values = values;
5088
+ this.cumulative = cumulative;
5089
+ this.total = sum;
5090
+ }
5091
+ /** One draw with replacement. */
5092
+ roll(rng) {
5093
+ const r = rng.float() * this.total;
5094
+ for (let i = 0; i < this.cumulative.length; i++) if (r < this.cumulative[i]) return this.values[i];
5095
+ return this.values[this.values.length - 1];
5096
+ }
5097
+ /** `n` independent draws with replacement, in order. */
5098
+ rollMany(rng, n2) {
5099
+ const out = [];
5100
+ for (let i = 0; i < n2; i++) out.push(this.roll(rng));
5101
+ return out;
5102
+ }
5103
+ };
5104
+
5105
+ // src/logic/graph.ts
5106
+ function bfs(start, neighbours, key, opts = {}) {
5107
+ const maxNodes = opts.maxNodes ?? 1e6;
5108
+ const order = [];
5109
+ const cameFrom = /* @__PURE__ */ new Map();
5110
+ const dist = /* @__PURE__ */ new Map();
5111
+ const startKey = key(start);
5112
+ dist.set(startKey, 0);
5113
+ let frontier = [start];
5114
+ order.push(start);
5115
+ if (opts.goal?.(start)) return { order, cameFrom, dist };
5116
+ let expanded = 0;
5117
+ while (frontier.length > 0 && expanded < maxNodes) {
5118
+ const next = [];
5119
+ for (const node of frontier) {
5120
+ const d = dist.get(key(node)) ?? 0;
5121
+ for (const nb of neighbours(node)) {
5122
+ const k = key(nb);
5123
+ if (dist.has(k)) continue;
5124
+ dist.set(k, d + 1);
5125
+ cameFrom.set(k, node);
5126
+ order.push(nb);
5127
+ expanded++;
5128
+ if (opts.goal?.(nb)) return { order, cameFrom, dist };
5129
+ next.push(nb);
5130
+ }
5131
+ }
5132
+ frontier = next;
5133
+ }
5134
+ return { order, cameFrom, dist };
5135
+ }
5136
+ function reconstructPath(cameFrom, key, start, goal) {
5137
+ const path = [goal];
5138
+ let cur = goal;
5139
+ const startKey = key(start);
5140
+ while (key(cur) !== startKey) {
5141
+ const prev = cameFrom.get(key(cur));
5142
+ if (prev === void 0) return [];
5143
+ cur = prev;
5144
+ path.push(cur);
5145
+ }
5146
+ path.reverse();
5147
+ return path;
5148
+ }
5149
+ function astar(start, isGoal, neighbours, key, opts = {}) {
5150
+ const h = opts.heuristic ?? (() => 0);
5151
+ const maxNodes = opts.maxNodes ?? 1e6;
5152
+ const cameFrom = /* @__PURE__ */ new Map();
5153
+ const gScore = /* @__PURE__ */ new Map();
5154
+ const heap = new MinHeap();
5155
+ const startKey = key(start);
5156
+ gScore.set(startKey, 0);
5157
+ heap.push(start, h(start));
5158
+ let expanded = 0;
5159
+ while (heap.size > 0 && expanded < maxNodes) {
5160
+ const { item: cur, priority: f } = heap.pop();
5161
+ const ck = key(cur);
5162
+ const g = gScore.get(ck) ?? Infinity;
5163
+ if (f > g + h(cur) + 1e-9) continue;
5164
+ if (isGoal(cur)) return { path: rebuild(cameFrom, key, cur), cost: g };
5165
+ expanded++;
5166
+ for (const edge of neighbours(cur)) {
5167
+ const nk = key(edge.node);
5168
+ const tentative = g + edge.cost;
5169
+ if (tentative < (gScore.get(nk) ?? Infinity)) {
5170
+ gScore.set(nk, tentative);
5171
+ cameFrom.set(nk, cur);
5172
+ heap.push(edge.node, tentative + h(edge.node));
5173
+ }
5174
+ }
5175
+ }
5176
+ return null;
5177
+ }
5178
+ function rebuild(cameFrom, key, goal) {
5179
+ const path = [goal];
5180
+ let cur = goal;
5181
+ let prev = cameFrom.get(key(cur));
5182
+ while (prev !== void 0) {
5183
+ cur = prev;
5184
+ path.push(cur);
5185
+ prev = cameFrom.get(key(cur));
5186
+ }
5187
+ path.reverse();
5188
+ return path;
5189
+ }
5190
+ var MinHeap = class {
5191
+ items = [];
5192
+ prio = [];
5193
+ seq = [];
5194
+ counter = 0;
5195
+ get size() {
5196
+ return this.items.length;
5197
+ }
5198
+ push(item, priority) {
5199
+ this.items.push(item);
5200
+ this.prio.push(priority);
5201
+ this.seq.push(this.counter++);
5202
+ this.bubbleUp(this.items.length - 1);
5203
+ }
5204
+ pop() {
5205
+ const item = this.items[0];
5206
+ const priority = this.prio[0];
5207
+ const last = this.items.length - 1;
5208
+ this.swap(0, last);
5209
+ this.items.pop();
5210
+ this.prio.pop();
5211
+ this.seq.pop();
5212
+ if (this.items.length > 0) this.bubbleDown(0);
5213
+ return { item, priority };
5214
+ }
5215
+ /** true if a is strictly higher priority (lower value, then earlier seq). */
5216
+ less(a, b) {
5217
+ return this.prio[a] < this.prio[b] || this.prio[a] === this.prio[b] && this.seq[a] < this.seq[b];
5218
+ }
5219
+ bubbleUp(i) {
5220
+ while (i > 0) {
5221
+ const parent = i - 1 >> 1;
5222
+ if (!this.less(i, parent)) break;
5223
+ this.swap(i, parent);
5224
+ i = parent;
5225
+ }
5226
+ }
5227
+ bubbleDown(i) {
5228
+ const n2 = this.items.length;
5229
+ for (; ; ) {
5230
+ const l = 2 * i + 1;
5231
+ const r = 2 * i + 2;
5232
+ let smallest = i;
5233
+ if (l < n2 && this.less(l, smallest)) smallest = l;
5234
+ if (r < n2 && this.less(r, smallest)) smallest = r;
5235
+ if (smallest === i) break;
5236
+ this.swap(i, smallest);
5237
+ i = smallest;
5238
+ }
5239
+ }
5240
+ swap(a, b) {
5241
+ [this.items[a], this.items[b]] = [this.items[b], this.items[a]];
5242
+ [this.prio[a], this.prio[b]] = [this.prio[b], this.prio[a]];
5243
+ [this.seq[a], this.seq[b]] = [this.seq[b], this.seq[a]];
5244
+ }
5245
+ };
5246
+ var NEIGHBORS_4 = [
5247
+ { x: 0, y: -1 },
5248
+ { x: 1, y: 0 },
5249
+ { x: 0, y: 1 },
5250
+ { x: -1, y: 0 }
5251
+ ];
5252
+ var NEIGHBORS_8 = [
5253
+ ...NEIGHBORS_4,
5254
+ { x: 1, y: -1 },
5255
+ { x: 1, y: 1 },
5256
+ { x: -1, y: 1 },
5257
+ { x: -1, y: -1 }
5258
+ ];
5259
+ var cellKey = (c) => `${c.x},${c.y}`;
5260
+ function gridNeighbours(passable, cols, rows, diagonal) {
5261
+ const offsets = diagonal ? NEIGHBORS_8 : NEIGHBORS_4;
5262
+ return (c) => {
5263
+ const out = [];
5264
+ for (const o of offsets) {
5265
+ const x = c.x + o.x;
5266
+ const y = c.y + o.y;
5267
+ if (x < 0 || y < 0 || x >= cols || y >= rows) continue;
5268
+ if (!passable(x, y)) continue;
5269
+ out.push({ x, y });
5270
+ }
5271
+ return out;
5272
+ };
5273
+ }
5274
+ function floodFill(start, passable, opts) {
5275
+ if (!passable(start.x, start.y)) return [];
5276
+ const nb = gridNeighbours(passable, opts.cols, opts.rows, opts.diagonal ?? false);
5277
+ return bfs(start, nb, cellKey).order;
5278
+ }
5279
+ function connectedComponents(cols, rows, passable, diagonal = false) {
5280
+ const labels = new Array(cols * rows).fill(-1);
5281
+ const cells = [];
5282
+ const offsets = diagonal ? NEIGHBORS_8 : NEIGHBORS_4;
5283
+ for (let y = 0; y < rows; y++) {
5284
+ for (let x = 0; x < cols; x++) {
5285
+ if (labels[y * cols + x] !== -1 || !passable(x, y)) continue;
5286
+ const id = cells.length;
5287
+ const region = [];
5288
+ let frontier = [{ x, y }];
5289
+ labels[y * cols + x] = id;
5290
+ while (frontier.length > 0) {
5291
+ const next = [];
5292
+ for (const c of frontier) {
5293
+ region.push(c);
5294
+ for (const o of offsets) {
5295
+ const nx = c.x + o.x;
5296
+ const ny = c.y + o.y;
5297
+ if (nx < 0 || ny < 0 || nx >= cols || ny >= rows) continue;
5298
+ const idx = ny * cols + nx;
5299
+ if (labels[idx] !== -1 || !passable(nx, ny)) continue;
5300
+ labels[idx] = id;
5301
+ next.push({ x: nx, y: ny });
5302
+ }
5303
+ }
5304
+ frontier = next;
5305
+ }
5306
+ cells.push(region);
5307
+ }
5308
+ }
5309
+ return { labels, cells };
5310
+ }
5311
+ function astarGrid(start, goal, passable, opts) {
5312
+ const diagonal = opts.diagonal ?? false;
5313
+ const offsets = diagonal ? NEIGHBORS_8 : NEIGHBORS_4;
5314
+ const stepCost = opts.cost ?? (() => 1);
5315
+ const SQRT2 = 1.4142135623730951;
5316
+ const neighbours = (c) => {
5317
+ const out = [];
5318
+ for (const o of offsets) {
5319
+ const x = c.x + o.x;
5320
+ const y = c.y + o.y;
5321
+ if (x < 0 || y < 0 || x >= opts.cols || y >= opts.rows) continue;
5322
+ if (!passable(x, y)) continue;
5323
+ const diag = o.x !== 0 && o.y !== 0;
5324
+ out.push({ node: { x, y }, cost: stepCost(x, y) * (diag ? SQRT2 : 1) });
5325
+ }
5326
+ return out;
5327
+ };
5328
+ const heuristic = (c) => {
5329
+ const dx = Math.abs(c.x - goal.x);
5330
+ const dy = Math.abs(c.y - goal.y);
5331
+ if (!diagonal) return dx + dy;
5332
+ const lo = Math.min(dx, dy);
5333
+ return dx + dy - (2 - SQRT2) * lo;
5334
+ };
5335
+ if (!passable(goal.x, goal.y)) return null;
5336
+ const result = astar(start, (c) => c.x === goal.x && c.y === goal.y, neighbours, cellKey, {
5337
+ heuristic,
5338
+ maxNodes: opts.maxNodes
5339
+ });
5340
+ return result ? result.path : null;
5341
+ }
5342
+ function passableFromTilemap(map) {
5343
+ return (x, y) => tileAt(map, x, y) !== TILE.SOLID;
5344
+ }
5345
+
5346
+ // src/logic/history.ts
5347
+ function deepClone(s) {
5348
+ return structuredClone(s);
5349
+ }
5350
+ var UndoStack = class {
5351
+ history = [];
5352
+ cursor = -1;
5353
+ limit;
5354
+ clone;
5355
+ constructor(opts = {}) {
5356
+ this.limit = Math.max(1, opts.limit ?? 64);
5357
+ this.clone = opts.clone ?? deepClone;
5358
+ }
5359
+ /** Push a new state as the present, discarding any redo branch ahead of it. */
5360
+ record(state) {
5361
+ this.history.length = this.cursor + 1;
5362
+ this.history.push(this.clone(state));
5363
+ this.cursor = this.history.length - 1;
5364
+ if (this.history.length > this.limit) {
5365
+ const overflow = this.history.length - this.limit;
5366
+ this.history.splice(0, overflow);
5367
+ this.cursor -= overflow;
5368
+ }
5369
+ }
5370
+ /** Step back one state and return a clone of it (undefined if nothing to undo). */
5371
+ undo() {
5372
+ if (this.cursor <= 0) return void 0;
5373
+ this.cursor--;
5374
+ return this.clone(this.history[this.cursor]);
5375
+ }
5376
+ /** Step forward one state and return a clone of it (undefined if nothing to redo). */
5377
+ redo() {
5378
+ if (this.cursor >= this.history.length - 1) return void 0;
5379
+ this.cursor++;
5380
+ return this.clone(this.history[this.cursor]);
5381
+ }
5382
+ /** A clone of the current state, or undefined if empty. */
5383
+ current() {
5384
+ return this.cursor >= 0 ? this.clone(this.history[this.cursor]) : void 0;
5385
+ }
5386
+ get canUndo() {
5387
+ return this.cursor > 0;
5388
+ }
5389
+ get canRedo() {
5390
+ return this.cursor < this.history.length - 1;
5391
+ }
5392
+ /** Number of recorded states retained. */
5393
+ get size() {
5394
+ return this.history.length;
5395
+ }
5396
+ clear() {
5397
+ this.history.length = 0;
5398
+ this.cursor = -1;
5399
+ }
5400
+ };
5401
+ var RingBuffer = class {
5402
+ constructor(capacity) {
5403
+ this.capacity = capacity;
5404
+ if (!Number.isInteger(capacity) || capacity < 1) throw new Error("hayao: RingBuffer capacity must be a positive integer");
5405
+ this.buf = new Array(capacity);
5406
+ }
5407
+ capacity;
5408
+ buf;
5409
+ start = 0;
5410
+ count = 0;
5411
+ push(item) {
5412
+ const end = (this.start + this.count) % this.capacity;
5413
+ this.buf[end] = item;
5414
+ if (this.count < this.capacity) {
5415
+ this.count++;
5416
+ } else {
5417
+ this.start = (this.start + 1) % this.capacity;
5418
+ }
5419
+ }
5420
+ /** Number of items currently held (≤ capacity). */
5421
+ get length() {
5422
+ return this.count;
5423
+ }
5424
+ get isFull() {
5425
+ return this.count === this.capacity;
5426
+ }
5427
+ /** Item by age index: 0 = oldest, length-1 = newest. Undefined if out of range. */
5428
+ at(i) {
5429
+ if (i < 0 || i >= this.count) return void 0;
5430
+ return this.buf[(this.start + i) % this.capacity];
5431
+ }
5432
+ /** The most recently pushed item, or undefined if empty. */
5433
+ latest() {
5434
+ return this.at(this.count - 1);
5435
+ }
5436
+ /** Contents oldest→newest. */
5437
+ toArray() {
5438
+ const out = [];
5439
+ for (let i = 0; i < this.count; i++) out.push(this.buf[(this.start + i) % this.capacity]);
5440
+ return out;
5441
+ }
5442
+ clear() {
5443
+ this.start = 0;
5444
+ this.count = 0;
5445
+ this.buf.fill(void 0);
5446
+ }
5447
+ };
5448
+
5449
+ // src/persist/storage.ts
5450
+ var MemoryStorage = class {
5451
+ map = /* @__PURE__ */ new Map();
5452
+ get(key) {
5453
+ return this.map.has(key) ? this.map.get(key) : null;
5454
+ }
5455
+ set(key, value) {
5456
+ this.map.set(key, value);
5457
+ }
5458
+ remove(key) {
5459
+ this.map.delete(key);
5460
+ }
5461
+ keys() {
5462
+ return [...this.map.keys()];
5463
+ }
5464
+ };
5465
+ var NullStorage = class {
5466
+ get(_key) {
5467
+ return null;
5468
+ }
5469
+ set(_key, _value) {
5470
+ }
5471
+ remove(_key) {
5472
+ }
5473
+ keys() {
5474
+ return [];
5475
+ }
5476
+ };
5477
+ var LocalStorageAdapter = class {
5478
+ constructor(prefix = "hayao.save.") {
5479
+ this.prefix = prefix;
5480
+ }
5481
+ prefix;
5482
+ ls() {
5483
+ try {
5484
+ return typeof localStorage !== "undefined" ? localStorage : null;
5485
+ } catch {
5486
+ return null;
5487
+ }
5488
+ }
5489
+ get(key) {
5490
+ try {
5491
+ return this.ls()?.getItem(this.prefix + key) ?? null;
5492
+ } catch {
5493
+ return null;
5494
+ }
5495
+ }
5496
+ set(key, value) {
5497
+ try {
5498
+ this.ls()?.setItem(this.prefix + key, value);
5499
+ } catch {
5500
+ }
5501
+ }
5502
+ remove(key) {
5503
+ try {
5504
+ this.ls()?.removeItem(this.prefix + key);
5505
+ } catch {
5506
+ }
5507
+ }
5508
+ keys() {
5509
+ const ls = this.ls();
5510
+ if (!ls) return [];
5511
+ const out = [];
5512
+ try {
5513
+ for (let i = 0; i < ls.length; i++) {
5514
+ const k = ls.key(i);
5515
+ if (k && k.startsWith(this.prefix)) out.push(k.slice(this.prefix.length));
5516
+ }
5517
+ } catch {
5518
+ }
5519
+ return out;
5520
+ }
5521
+ };
5522
+ function defaultStorage(prefix) {
5523
+ try {
5524
+ if (typeof localStorage !== "undefined") {
5525
+ const probe = "__hayao_probe__";
5526
+ localStorage.setItem(probe, "1");
5527
+ localStorage.removeItem(probe);
5528
+ return new LocalStorageAdapter(prefix);
5529
+ }
5530
+ } catch {
5531
+ }
5532
+ return new MemoryStorage();
5533
+ }
5534
+
5535
+ // src/persist/codec.ts
5536
+ var ESC = "~";
5537
+ var RLE_MIN_RUN = 4;
5538
+ function rleEncode(input) {
5539
+ let out = "";
5540
+ let i = 0;
5541
+ while (i < input.length) {
5542
+ const ch = input[i];
5543
+ let run = 1;
5544
+ while (i + run < input.length && input[i + run] === ch) run++;
5545
+ if (run >= RLE_MIN_RUN || ch === ESC) {
5546
+ out += ESC + ch + run.toString(36) + ESC;
5547
+ } else {
5548
+ out += ch.repeat(run);
5549
+ }
5550
+ i += run;
5551
+ }
5552
+ return out;
5553
+ }
5554
+ function rleDecode(input) {
5555
+ let out = "";
5556
+ let i = 0;
5557
+ while (i < input.length) {
5558
+ if (input[i] === ESC) {
5559
+ const ch = input[i + 1];
5560
+ const end = input.indexOf(ESC, i + 2);
5561
+ if (ch === void 0 || end < 0) throw new Error("hayao: malformed RLE stream");
5562
+ const count = parseInt(input.slice(i + 2, end), 36);
5563
+ if (!Number.isFinite(count) || count < 1) throw new Error("hayao: malformed RLE count");
5564
+ out += ch.repeat(count);
5565
+ i = end + 1;
5566
+ } else {
5567
+ out += input[i];
5568
+ i++;
5569
+ }
5570
+ }
5571
+ return out;
5572
+ }
5573
+ var ALPHABET = "ABCDEFGHIJKLMNOPQRSTUVWXYZabcdefghijklmnopqrstuvwxyz0123456789-_";
5574
+ var INDEX = {};
5575
+ for (let i = 0; i < ALPHABET.length; i++) INDEX[ALPHABET[i]] = i;
5576
+ function packVarints(nums) {
5577
+ let out = "";
5578
+ for (const n2 of nums) {
5579
+ if (!Number.isInteger(n2) || n2 < 0) throw new Error(`hayao: packVarints expects non-negative integers, got ${n2}`);
5580
+ let v = n2;
5581
+ do {
5582
+ let digit = v & 31;
5583
+ v = Math.floor(v / 32);
5584
+ if (v > 0) digit |= 32;
5585
+ out += ALPHABET[digit];
5586
+ } while (v > 0);
5587
+ }
5588
+ return out;
5589
+ }
5590
+ function unpackVarints(input) {
5591
+ const out = [];
5592
+ let v = 0;
5593
+ let shift = 1;
5594
+ for (const ch of input) {
5595
+ const digit = INDEX[ch];
5596
+ if (digit === void 0) throw new Error(`hayao: unpackVarints saw invalid char "${ch}"`);
5597
+ v += (digit & 31) * shift;
5598
+ if (digit & 32) {
5599
+ shift *= 32;
5600
+ } else {
5601
+ out.push(v);
5602
+ v = 0;
5603
+ shift = 1;
5604
+ }
5605
+ }
5606
+ if (shift !== 1) throw new Error("hayao: unpackVarints hit a truncated stream");
5607
+ return out;
5608
+ }
5609
+
5610
+ // src/persist/save.ts
5611
+ var SAVE_FORMAT_VERSION = 1;
5612
+ function serializeSnapshot(snap) {
5613
+ const envelope = { v: SAVE_FORMAT_VERSION, snap };
5614
+ return JSON.stringify(envelope);
5615
+ }
5616
+ function parseSnapshot(text) {
5617
+ if (!text) return null;
5618
+ let parsed;
5619
+ try {
5620
+ parsed = JSON.parse(text);
5621
+ } catch {
5622
+ return null;
5623
+ }
5624
+ if (!parsed || typeof parsed !== "object") return null;
5625
+ const env = parsed;
5626
+ if (env.v !== SAVE_FORMAT_VERSION || !env.snap || typeof env.snap !== "object") return null;
5627
+ const snap = env.snap;
5628
+ if (typeof snap.seed !== "number" || !snap.rng || !snap.clock || !snap.input || !snap.state || !snap.tree) {
5629
+ return null;
5630
+ }
5631
+ return snap;
5632
+ }
5633
+ var SaveManager = class {
5634
+ constructor(world, storage = defaultStorage()) {
5635
+ this.world = world;
5636
+ this.storage = storage;
5637
+ }
5638
+ world;
5639
+ storage;
5640
+ /** Snapshot the world and persist it under `slot`. */
5641
+ save(slot = "auto") {
5642
+ this.storage.set(slot, serializeSnapshot(this.world.snapshot()));
5643
+ }
5644
+ /** Restore `slot` into the world. Returns false if absent/corrupt (world untouched). */
5645
+ load(slot = "auto") {
5646
+ const snap = parseSnapshot(this.storage.get(slot));
5647
+ if (!snap) return false;
5648
+ this.world.restore(snap);
5649
+ return true;
5650
+ }
5651
+ /** Is there a well-formed save in `slot`? */
5652
+ has(slot = "auto") {
5653
+ return parseSnapshot(this.storage.get(slot)) !== null;
5654
+ }
5655
+ /** Delete a save slot. */
5656
+ delete(slot = "auto") {
5657
+ this.storage.remove(slot);
5658
+ }
5659
+ /** All slot names currently persisted. */
5660
+ slots() {
5661
+ return this.storage.keys();
5662
+ }
5663
+ };
5664
+
5665
+ // src/content/dsl.ts
5666
+ function initDirector(waves) {
5667
+ return { next: waves.map((w) => w.time) };
5668
+ }
5669
+ function pollDirector(waves, state, time, rng) {
5670
+ const out = [];
5671
+ for (let i = 0; i < waves.length; i++) {
5672
+ const w = waves[i];
5673
+ let next = state.next[i] ?? w.time;
5674
+ while (next <= time) {
5675
+ let spawn;
5676
+ if (typeof w.spawn === "string") {
5677
+ spawn = w.spawn;
5678
+ } else {
5679
+ if (!rng) throw new Error("hayao: pollDirector needs an Rng to roll a weighted wave");
5680
+ spawn = pickEntry(rng, w.spawn);
5681
+ }
5682
+ out.push({ spawn, count: w.count ?? 1, wave: i });
5683
+ if (w.every && w.every > 0) {
5684
+ next += w.every;
5685
+ if (w.end !== void 0 && next >= w.end) {
5686
+ next = Infinity;
5687
+ break;
5688
+ }
5689
+ } else {
5690
+ next = Infinity;
5691
+ break;
5692
+ }
5693
+ }
5694
+ state.next[i] = next;
5695
+ }
5696
+ return out;
5697
+ }
5698
+ function availableUpgrades(defs, owned) {
5699
+ const counts = /* @__PURE__ */ new Map();
5700
+ for (const id of owned) counts.set(id, (counts.get(id) ?? 0) + 1);
5701
+ const has = (id) => (counts.get(id) ?? 0) > 0;
5702
+ return defs.filter((d) => {
5703
+ const stacks = counts.get(d.id) ?? 0;
5704
+ if (stacks >= (d.maxStacks ?? 1)) return false;
5705
+ return (d.requires ?? []).every(has);
5706
+ });
5707
+ }
5708
+
5709
+ // src/net/players.ts
5710
+ function playerIds(count) {
5711
+ const out = [];
5712
+ for (let i = 1; i <= count; i++) out.push(`p${i}`);
5713
+ return out;
5714
+ }
5715
+ var playerAction = (player, action) => `${player}:${action}`;
5716
+ function mergePlayerFrames(players, inputs) {
5717
+ const out = [];
5718
+ for (const p of players) {
5719
+ const actions = inputs.get(p) ?? [];
5720
+ for (const a of actions) out.push(playerAction(p, a));
5721
+ }
5722
+ return out.sort();
5723
+ }
5724
+ var PlayerInput = class {
5725
+ constructor(world, player) {
5726
+ this.world = world;
5727
+ this.player = player;
5728
+ }
5729
+ world;
5730
+ player;
5731
+ isDown(action) {
5732
+ return this.world.input.isDown(playerAction(this.player, action));
5733
+ }
5734
+ justPressed(action) {
5735
+ return this.world.input.justPressed(playerAction(this.player, action));
5736
+ }
5737
+ justReleased(action) {
5738
+ return this.world.input.justReleased(playerAction(this.player, action));
5739
+ }
5740
+ };
5741
+ var playerInput = (world, player) => new PlayerInput(world, player);
5742
+
5743
+ // src/net/protocol.ts
5744
+ var NET_PROTOCOL_VERSION = 1;
5745
+ var DEFAULT_SESSION_CONFIG = {
5746
+ mode: "lockstep",
5747
+ inputDelay: 2,
5748
+ hashInterval: 20,
5749
+ maxRollback: 12,
5750
+ redundancy: 3
5751
+ };
5752
+ function encodeMessage(msg) {
5753
+ return JSON.stringify(msg);
5754
+ }
5755
+ function decodeMessage(data) {
5756
+ try {
5757
+ const msg = JSON.parse(data);
5758
+ if (!msg || typeof msg !== "object" || msg.v !== NET_PROTOCOL_VERSION || typeof msg.t !== "string") return null;
5759
+ return msg;
5760
+ } catch {
5761
+ return null;
5762
+ }
5763
+ }
5764
+ function netMessage(msg) {
5765
+ return { v: NET_PROTOCOL_VERSION, ...msg };
5766
+ }
5767
+
5768
+ // src/net/transport.ts
5769
+ var LoopbackHub = class {
5770
+ peers = [];
5771
+ queue = [];
5772
+ tickCount = 0;
5773
+ seq = 0;
5774
+ sent = 0;
5775
+ delay;
5776
+ dropEvery;
5777
+ constructor(opts = {}) {
5778
+ this.delay = opts.delay ?? 0;
5779
+ this.dropEvery = opts.dropEvery ?? 0;
5780
+ }
5781
+ /** Create a new endpoint on this bus. */
5782
+ connect() {
5783
+ const t = new LoopbackTransport(this, this.peers.length);
5784
+ this.peers.push(t);
5785
+ return t;
5786
+ }
5787
+ /** @internal */
5788
+ enqueue(data, from) {
5789
+ this.sent++;
5790
+ if (this.dropEvery > 0 && this.sent % this.dropEvery === 0) return;
5791
+ this.queue.push({ data, from, dueTick: this.tickCount + this.delay, seq: this.seq++ });
5792
+ }
5793
+ /** Advance one network tick, delivering everything that is due. */
5794
+ tick() {
5795
+ const due = this.queue.filter((m) => m.dueTick <= this.tickCount).sort((a, b) => a.seq - b.seq);
5796
+ this.queue = this.queue.filter((m) => m.dueTick > this.tickCount);
5797
+ this.tickCount++;
5798
+ for (const m of due) {
5799
+ for (const peer of this.peers) {
5800
+ if (peer.peerIndex !== m.from && peer.isOpen) peer.deliver(m.data);
5801
+ }
5802
+ }
5803
+ }
5804
+ /** Deliver everything in flight (repeated ticks until the queue drains). */
5805
+ flush() {
5806
+ while (this.queue.length > 0) this.tick();
5807
+ }
5808
+ get pending() {
5809
+ return this.queue.length;
5810
+ }
5811
+ };
5812
+ var LoopbackTransport = class {
5813
+ constructor(hub, peerIndex) {
5814
+ this.hub = hub;
5815
+ this.peerIndex = peerIndex;
5816
+ }
5817
+ hub;
5818
+ peerIndex;
5819
+ listeners = [];
5820
+ closeListeners = [];
5821
+ open = true;
5822
+ send(data) {
5823
+ if (this.open) this.hub.enqueue(data, this.peerIndex);
5824
+ }
5825
+ onMessage(cb) {
5826
+ this.listeners.push(cb);
5827
+ return () => {
5828
+ this.listeners = this.listeners.filter((l) => l !== cb);
5829
+ };
5830
+ }
5831
+ onClose(cb) {
5832
+ this.closeListeners.push(cb);
5833
+ return () => {
5834
+ this.closeListeners = this.closeListeners.filter((l) => l !== cb);
5835
+ };
5836
+ }
5837
+ close() {
5838
+ if (!this.open) return;
5839
+ this.open = false;
5840
+ for (const cb of this.closeListeners) cb();
5841
+ }
5842
+ get isOpen() {
5843
+ return this.open;
5844
+ }
5845
+ /** @internal */
5846
+ deliver(data) {
5847
+ for (const cb of this.listeners) cb(data);
5848
+ }
5849
+ };
5850
+ var BroadcastChannelTransport = class {
5851
+ channel;
5852
+ listeners = [];
5853
+ closeListeners = [];
5854
+ open = true;
5855
+ constructor(room) {
5856
+ this.channel = new BroadcastChannel(`hayao-net:${room}`);
5857
+ this.channel.onmessage = (e) => {
5858
+ if (typeof e.data === "string") for (const cb of this.listeners) cb(e.data);
5859
+ };
5860
+ }
5861
+ send(data) {
5862
+ if (this.open) this.channel.postMessage(data);
5863
+ }
5864
+ onMessage(cb) {
5865
+ this.listeners.push(cb);
5866
+ return () => {
5867
+ this.listeners = this.listeners.filter((l) => l !== cb);
5868
+ };
5869
+ }
5870
+ onClose(cb) {
5871
+ this.closeListeners.push(cb);
5872
+ return () => {
5873
+ this.closeListeners = this.closeListeners.filter((l) => l !== cb);
5874
+ };
5875
+ }
5876
+ close() {
5877
+ if (!this.open) return;
5878
+ this.open = false;
5879
+ this.channel.close();
5880
+ for (const cb of this.closeListeners) cb();
5881
+ }
5882
+ get isOpen() {
5883
+ return this.open;
5884
+ }
5885
+ };
5886
+ function connectWebSocket(url) {
5887
+ return new Promise((resolve, reject) => {
5888
+ const ws = new WebSocket(url);
5889
+ const listeners = [];
5890
+ const closeListeners = [];
5891
+ let open = false;
5892
+ const transport = {
5893
+ send(data) {
5894
+ if (open) ws.send(data);
5895
+ },
5896
+ onMessage(cb) {
5897
+ listeners.push(cb);
5898
+ return () => {
5899
+ const i = listeners.indexOf(cb);
5900
+ if (i >= 0) listeners.splice(i, 1);
5901
+ };
5902
+ },
5903
+ onClose(cb) {
5904
+ closeListeners.push(cb);
5905
+ return () => {
5906
+ const i = closeListeners.indexOf(cb);
5907
+ if (i >= 0) closeListeners.splice(i, 1);
5908
+ };
5909
+ },
5910
+ close() {
5911
+ open = false;
5912
+ ws.close();
5913
+ },
5914
+ get isOpen() {
5915
+ return open;
5916
+ }
5917
+ };
5918
+ ws.onopen = () => {
5919
+ open = true;
5920
+ resolve(transport);
5921
+ };
5922
+ ws.onmessage = (e) => {
5923
+ if (typeof e.data === "string") for (const cb of listeners) cb(e.data);
5924
+ };
5925
+ ws.onclose = () => {
5926
+ const wasOpen = open;
5927
+ open = false;
5928
+ if (wasOpen) for (const cb of closeListeners) cb();
5929
+ };
5930
+ ws.onerror = () => {
5931
+ if (!open) reject(new Error(`hayao-net: could not connect to ${url}`));
5932
+ };
5933
+ });
5934
+ }
5935
+
5936
+ // src/net/inputBuffer.ts
5937
+ var InputBuffer = class {
5938
+ /** player → frame → actions (sorted). */
5939
+ frames = /* @__PURE__ */ new Map();
5940
+ /** player → highest contiguous frame received (for prediction + confirm). */
5941
+ contiguous = /* @__PURE__ */ new Map();
5942
+ addPlayer(player) {
5943
+ if (!this.frames.has(player)) {
5944
+ this.frames.set(player, /* @__PURE__ */ new Map());
5945
+ this.contiguous.set(player, -1);
5946
+ }
5947
+ }
5948
+ removePlayer(player) {
5949
+ this.frames.delete(player);
5950
+ this.contiguous.delete(player);
5951
+ }
5952
+ players() {
5953
+ return [...this.frames.keys()];
5954
+ }
5955
+ /**
5956
+ * Record a player's actions for a frame. Returns true if this was new
5957
+ * information (first write for that slot).
5958
+ */
5959
+ set(player, frame, actions) {
5960
+ const perPlayer = this.frames.get(player);
5961
+ if (!perPlayer || perPlayer.has(frame)) return false;
5962
+ perPlayer.set(frame, [...actions].sort());
5963
+ let c = this.contiguous.get(player) ?? -1;
5964
+ while (perPlayer.has(c + 1)) c++;
5965
+ this.contiguous.set(player, c);
5966
+ return true;
5967
+ }
5968
+ has(player, frame) {
5969
+ return this.frames.get(player)?.has(frame) ?? false;
5970
+ }
5971
+ get(player, frame) {
5972
+ return this.frames.get(player)?.get(frame);
5973
+ }
5974
+ /** The last actions at or before `frame` — the rollback predictor's guess. */
5975
+ latestAt(player, frame) {
5976
+ const perPlayer = this.frames.get(player);
5977
+ if (!perPlayer) return [];
5978
+ for (let f = frame; f >= 0; f--) {
5979
+ const a = perPlayer.get(f);
5980
+ if (a) return a;
5981
+ }
5982
+ return [];
5983
+ }
5984
+ /** Highest frame such that this player's inputs 0..frame are all known. */
5985
+ contiguousThrough(player) {
5986
+ return this.contiguous.get(player) ?? -1;
5987
+ }
5988
+ /** Highest frame with ALL players' inputs known contiguously. */
5989
+ confirmedFrame() {
5990
+ let min = Infinity;
5991
+ for (const c of this.contiguous.values()) min = Math.min(min, c);
5992
+ return min === Infinity ? -1 : min;
5993
+ }
5994
+ /** Forget a player's frames at/after `frame` (leave cutoff). */
5995
+ clearFrom(player, frame) {
5996
+ const perPlayer = this.frames.get(player);
5997
+ if (!perPlayer) return;
5998
+ for (const f of perPlayer.keys()) if (f >= frame) perPlayer.delete(f);
5999
+ let c = this.contiguous.get(player) ?? -1;
6000
+ if (c >= frame) c = frame - 1;
6001
+ this.contiguous.set(player, c);
6002
+ }
6003
+ /** Drop history strictly before `frame` (memory hygiene on long sessions). */
6004
+ prune(frame) {
6005
+ for (const perPlayer of this.frames.values()) {
6006
+ for (const f of perPlayer.keys()) if (f < frame) perPlayer.delete(f);
6007
+ }
6008
+ }
6009
+ };
6010
+
6011
+ // src/net/lockstep.ts
6012
+ var LockstepSession = class {
6013
+ world;
6014
+ localPlayer;
6015
+ config;
6016
+ startFrame;
6017
+ transport;
6018
+ buffer = new InputBuffer();
6019
+ roster = [];
6020
+ mergedLog = [];
6021
+ localHashes = /* @__PURE__ */ new Map();
6022
+ pendingRemoteHashes = [];
6023
+ sentThrough;
6024
+ stallCount = 0;
6025
+ desynced = false;
6026
+ afterStepListeners = [];
6027
+ unsubscribe;
6028
+ onDesync;
6029
+ onStall;
6030
+ constructor(opts) {
6031
+ this.world = opts.world;
6032
+ this.transport = opts.transport;
6033
+ this.localPlayer = opts.localPlayer;
6034
+ this.config = { ...DEFAULT_SESSION_CONFIG, ...opts.config, mode: "lockstep" };
6035
+ this.startFrame = opts.startFrame ?? 0;
6036
+ this.onDesync = opts.onDesync;
6037
+ this.onStall = opts.onStall;
6038
+ for (const p of opts.players) {
6039
+ this.roster.push({ player: p, from: this.startFrame, until: Infinity });
6040
+ this.buffer.addPlayer(p);
6041
+ if (this.startFrame === 0 || p === this.localPlayer) {
6042
+ for (let f = this.startFrame; f < this.startFrame + this.config.inputDelay; f++) this.buffer.set(p, f, []);
6043
+ }
6044
+ }
6045
+ this.sentThrough = this.startFrame + this.config.inputDelay - 1;
6046
+ this.unsubscribe = this.transport.onMessage((data) => this.receive(data));
6047
+ }
6048
+ // ── the roster (players can come and go at agreed frames) ────
6049
+ activePlayers(frame) {
6050
+ return this.roster.filter((r) => r.from <= frame && frame < r.until).map((r) => r.player);
6051
+ }
6052
+ /** All peers must call this with the SAME atFrame (the room layer's job). */
6053
+ addPlayer(player, atFrame) {
6054
+ if (this.roster.some((r) => r.player === player && r.until === Infinity)) return;
6055
+ this.roster.push({ player, from: atFrame, until: Infinity });
6056
+ this.buffer.addPlayer(player);
6057
+ for (let f = atFrame; f < atFrame + this.config.inputDelay; f++) this.buffer.set(player, f, []);
6058
+ }
6059
+ /** All peers must call this with the SAME atFrame. */
6060
+ removePlayer(player, atFrame) {
6061
+ const entry = this.roster.find((r) => r.player === player && r.until === Infinity);
6062
+ if (entry) entry.until = atFrame;
6063
+ this.buffer.clearFrom(player, atFrame);
6064
+ }
6065
+ // ── driving the sim ──────────────────────────────────────────
6066
+ get frame() {
6067
+ return this.world.frame;
6068
+ }
6069
+ /** Highest frame every currently-active player's input is known for. */
6070
+ get confirmedFrame() {
6071
+ let min = Infinity;
6072
+ for (const p of this.activePlayers(this.world.frame)) min = Math.min(min, this.buffer.contiguousThrough(p));
6073
+ return min === Infinity ? -1 : min;
6074
+ }
6075
+ get stalls() {
6076
+ return this.stallCount;
6077
+ }
6078
+ /**
6079
+ * Try to advance exactly one frame with the local player's current actions.
6080
+ * Returns 1 if the world stepped, 0 if blocked waiting for a remote input.
6081
+ */
6082
+ tick(localActions = []) {
6083
+ if (this.desynced) return 0;
6084
+ const f = this.world.frame;
6085
+ this.scheduleLocal(localActions);
6086
+ const active2 = this.activePlayers(f);
6087
+ for (const p of active2) {
6088
+ if (!this.buffer.has(p, f)) {
6089
+ this.stallCount++;
6090
+ this.onStall?.(p, f);
6091
+ return 0;
6092
+ }
6093
+ }
6094
+ const inputs = /* @__PURE__ */ new Map();
6095
+ for (const p of active2) inputs.set(p, this.buffer.get(p, f));
6096
+ const merged = mergePlayerFrames(active2, inputs);
6097
+ this.world.step(merged);
6098
+ this.mergedLog.push(merged);
6099
+ this.afterStep(this.world.frame);
6100
+ return 1;
6101
+ }
6102
+ /**
6103
+ * Feed real elapsed ms (the browser loop's dt). Runs the fixed steps the
6104
+ * clock grants — stalling drops time (the sim slows rather than desyncs) —
6105
+ * then catches up if we're measurably behind the other peers.
6106
+ */
6107
+ advance(realMs, localActions = []) {
6108
+ let stepped = 0;
6109
+ const steps = this.world.clock.advance(realMs);
6110
+ for (let i = 0; i < steps; i++) {
6111
+ if (this.tick(localActions) === 0) break;
6112
+ stepped++;
6113
+ }
6114
+ let guard = 0;
6115
+ while (guard++ < 8 && this.world.frame < this.remoteFrameEstimate() && this.tick(localActions) === 1) stepped++;
6116
+ return stepped;
6117
+ }
6118
+ /** The merged input log from startFrame — replayable when startFrame is 0. */
6119
+ inputLog() {
6120
+ return { frames: this.mergedLog.map((f) => f.slice()) };
6121
+ }
6122
+ dispose() {
6123
+ this.unsubscribe();
6124
+ }
6125
+ /** Feed a raw transport message that arrived before this session existed. */
6126
+ deliver(data) {
6127
+ this.receive(data);
6128
+ }
6129
+ /** Subscribe to "the world just reached frame N" (room layer uses this). */
6130
+ onAfterStep(cb) {
6131
+ this.afterStepListeners.push(cb);
6132
+ return () => {
6133
+ this.afterStepListeners = this.afterStepListeners.filter((l) => l !== cb);
6134
+ };
6135
+ }
6136
+ // ── internals ────────────────────────────────────────────────
6137
+ scheduleLocal(localActions) {
6138
+ const target = this.world.frame + this.config.inputDelay;
6139
+ if (this.sentThrough >= target) return;
6140
+ while (this.sentThrough < target) {
6141
+ this.sentThrough++;
6142
+ this.buffer.set(this.localPlayer, this.sentThrough, localActions);
6143
+ }
6144
+ const from = Math.max(this.startFrame, target - this.config.redundancy + 1);
6145
+ const frames = [];
6146
+ for (let f = from; f <= target; f++) frames.push(this.buffer.get(this.localPlayer, f) ?? []);
6147
+ this.transport.send(encodeMessage(netMessage({ t: "input", player: this.localPlayer, from, frames })));
6148
+ }
6149
+ afterStep(frame) {
6150
+ if (frame % this.config.hashInterval === 0) {
6151
+ const hash = this.world.hash();
6152
+ this.localHashes.set(frame, hash);
6153
+ this.transport.send(encodeMessage(netMessage({ t: "hash", player: this.localPlayer, frame, hash })));
6154
+ this.checkPendingHashes();
6155
+ }
6156
+ if (frame % 64 === 0) {
6157
+ this.buffer.prune(frame - 128);
6158
+ for (const f of this.localHashes.keys()) if (f < frame - 512) this.localHashes.delete(f);
6159
+ }
6160
+ for (const cb of this.afterStepListeners) cb(frame);
6161
+ }
6162
+ receive(data) {
6163
+ const msg = decodeMessage(data);
6164
+ if (!msg) return;
6165
+ if (msg.t === "input" && msg.player !== this.localPlayer) {
6166
+ for (let i = 0; i < msg.frames.length; i++) this.buffer.set(msg.player, msg.from + i, msg.frames[i]);
6167
+ } else if (msg.t === "hash" && msg.player !== this.localPlayer) {
6168
+ this.pendingRemoteHashes.push({ player: msg.player, frame: msg.frame, hash: msg.hash });
6169
+ this.checkPendingHashes();
6170
+ }
6171
+ }
6172
+ checkPendingHashes() {
6173
+ const still = [];
6174
+ for (const remote of this.pendingRemoteHashes) {
6175
+ const local = this.localHashes.get(remote.frame);
6176
+ if (local === void 0) {
6177
+ if (remote.frame > this.world.frame - 512) still.push(remote);
6178
+ continue;
6179
+ }
6180
+ if (local !== remote.hash && !this.desynced) {
6181
+ this.desynced = true;
6182
+ this.onDesync?.({
6183
+ frame: remote.frame,
6184
+ player: remote.player,
6185
+ localHash: local,
6186
+ remoteHash: remote.hash,
6187
+ log: this.inputLog(),
6188
+ startFrame: this.startFrame
6189
+ });
6190
+ }
6191
+ }
6192
+ this.pendingRemoteHashes = still;
6193
+ }
6194
+ remoteFrameEstimate() {
6195
+ let max = -1;
6196
+ for (const entry of this.roster) {
6197
+ if (entry.player === this.localPlayer || entry.until !== Infinity) continue;
6198
+ max = Math.max(max, this.buffer.contiguousThrough(entry.player) - this.config.inputDelay);
6199
+ }
6200
+ return max;
6201
+ }
6202
+ };
6203
+
6204
+ // src/net/rollback.ts
6205
+ var RollbackSession = class {
6206
+ world;
6207
+ localPlayer;
6208
+ config;
6209
+ transport;
6210
+ players;
6211
+ buffer = new InputBuffer();
6212
+ attach;
6213
+ onDesync;
6214
+ onRollbackOverflow;
6215
+ /** Snapshot AT frame f (state before stepping f), keyed f % ring length. */
6216
+ ring;
6217
+ /** What we actually fed `step()` per frame — prediction bookkeeping. */
6218
+ usedInputs = /* @__PURE__ */ new Map();
6219
+ earliestBad = Infinity;
6220
+ rollbackCount = 0;
6221
+ resimulatedFrames = 0;
6222
+ localHashes = /* @__PURE__ */ new Map();
6223
+ /** player → cutoff frame: their inputs are [] from there on (departed). */
6224
+ dropped = /* @__PURE__ */ new Map();
6225
+ pendingRemoteHashes = [];
6226
+ hashedThrough = -1;
6227
+ desynced = false;
6228
+ overflowed = false;
6229
+ unsubscribe;
6230
+ constructor(opts) {
6231
+ this.world = opts.world;
6232
+ this.transport = opts.transport;
6233
+ this.localPlayer = opts.localPlayer;
6234
+ this.players = [...opts.players];
6235
+ this.config = { ...DEFAULT_SESSION_CONFIG, mode: "rollback", inputDelay: 0, ...opts.config };
6236
+ this.attach = opts.attach;
6237
+ this.onDesync = opts.onDesync;
6238
+ this.onRollbackOverflow = opts.onRollbackOverflow;
6239
+ this.ring = new Array(this.config.maxRollback + 1).fill(null);
6240
+ for (const p of this.players) this.buffer.addPlayer(p);
6241
+ this.unsubscribe = this.transport.onMessage((data) => this.receive(data));
6242
+ }
6243
+ get frame() {
6244
+ return this.world.frame;
6245
+ }
6246
+ /** Highest frame all players' real inputs are known for (inclusive). */
6247
+ get confirmedFrame() {
6248
+ let min = Infinity;
6249
+ for (const p of this.players) {
6250
+ const cut = this.dropped.get(p);
6251
+ let c = this.buffer.contiguousThrough(p);
6252
+ if (cut !== void 0 && c >= cut - 1) c = Infinity;
6253
+ min = Math.min(min, c);
6254
+ }
6255
+ return min === Infinity ? -1 : min;
6256
+ }
6257
+ /** Highest contiguous frame received from `player` — the drop-cutoff basis. */
6258
+ lastKnownFrame(player) {
6259
+ return this.buffer.contiguousThrough(player);
6260
+ }
6261
+ /**
6262
+ * A player left (or vanished): their inputs are [] from `atFrame` on, so the
6263
+ * session stops waiting on them. All peers must apply the SAME atFrame (the
6264
+ * room layer broadcasts it). Frames already simulated with a non-empty
6265
+ * prediction for the departed player roll back and re-simulate as usual.
6266
+ */
6267
+ removePlayer(player, atFrame) {
6268
+ if (player === this.localPlayer || this.dropped.has(player)) return;
6269
+ this.dropped.set(player, atFrame);
6270
+ this.buffer.clearFrom(player, atFrame);
6271
+ for (let f = atFrame; f < this.world.frame; f++) {
6272
+ const used = this.usedInputs.get(f)?.get(player);
6273
+ if (used !== void 0 && used !== "[]") this.earliestBad = Math.min(this.earliestBad, f);
6274
+ }
6275
+ this.settleHashes();
6276
+ }
6277
+ get stats() {
6278
+ return { rollbacks: this.rollbackCount, resimulatedFrames: this.resimulatedFrames };
6279
+ }
6280
+ /**
6281
+ * Advance one frame: publish local input, apply any pending correction,
6282
+ * then step with confirmed-or-predicted inputs. Returns 1 unless the sim
6283
+ * has outrun its rollback window (backpressure) or is desynced.
6284
+ */
6285
+ tick(localActions = []) {
6286
+ if (this.desynced || this.overflowed) return 0;
6287
+ const f = this.world.frame;
6288
+ if (f - (this.confirmedFrame + 1) >= this.config.maxRollback) return 0;
6289
+ this.publishLocal(f, localActions);
6290
+ if (!this.resolveCorrections()) return 0;
6291
+ this.storeSnapshot(f);
6292
+ this.stepFrame(f);
6293
+ this.settleHashes();
6294
+ if (f % 64 === 0) this.pruneOld(f);
6295
+ return 1;
6296
+ }
6297
+ /** Feed real elapsed ms; runs the fixed steps the clock grants. */
6298
+ advance(realMs, localActions = []) {
6299
+ let stepped = 0;
6300
+ const steps = this.world.clock.advance(realMs);
6301
+ for (let i = 0; i < steps; i++) {
6302
+ if (this.tick(localActions) === 0) break;
6303
+ stepped++;
6304
+ }
6305
+ return stepped;
6306
+ }
6307
+ /** Merged CONFIRMED input log from frame 0 — replayable ground truth. */
6308
+ inputLog() {
6309
+ const frames = [];
6310
+ const through = this.confirmedFrame;
6311
+ for (let f = 0; f <= through; f++) {
6312
+ const inputs = /* @__PURE__ */ new Map();
6313
+ for (const p of this.players) inputs.set(p, this.buffer.get(p, f) ?? []);
6314
+ frames.push(mergePlayerFrames(this.players, inputs));
6315
+ }
6316
+ return { frames };
6317
+ }
6318
+ dispose() {
6319
+ this.unsubscribe();
6320
+ }
6321
+ /** Feed a raw transport message that arrived before this session existed. */
6322
+ deliver(data) {
6323
+ this.receive(data);
6324
+ }
6325
+ // ── internals ────────────────────────────────────────────────
6326
+ publishLocal(frame, actions) {
6327
+ if (!this.buffer.set(this.localPlayer, frame, actions)) return;
6328
+ const from = Math.max(0, frame - this.config.redundancy + 1);
6329
+ const frames = [];
6330
+ for (let f = from; f <= frame; f++) frames.push(this.buffer.get(this.localPlayer, f) ?? []);
6331
+ this.transport.send(encodeMessage(netMessage({ t: "input", player: this.localPlayer, from, frames })));
6332
+ }
6333
+ inputsFor(frame) {
6334
+ const inputs = /* @__PURE__ */ new Map();
6335
+ for (const p of this.players) {
6336
+ const cut = this.dropped.get(p);
6337
+ if (cut !== void 0 && frame >= cut) inputs.set(p, []);
6338
+ else inputs.set(p, this.buffer.get(p, frame) ?? this.buffer.latestAt(p, frame));
6339
+ }
6340
+ return inputs;
6341
+ }
6342
+ stepFrame(frame) {
6343
+ const inputs = this.inputsFor(frame);
6344
+ const used = /* @__PURE__ */ new Map();
6345
+ for (const p of this.players) used.set(p, JSON.stringify(inputs.get(p)));
6346
+ this.usedInputs.set(frame, used);
6347
+ this.world.step(mergePlayerFrames(this.players, inputs));
6348
+ }
6349
+ /** Apply pending corrections. False = overflow (fatal). */
6350
+ resolveCorrections() {
6351
+ if (this.earliestBad === Infinity) return true;
6352
+ const target = this.world.frame;
6353
+ const bad = this.earliestBad;
6354
+ this.earliestBad = Infinity;
6355
+ const entry = this.ring[bad % this.ring.length];
6356
+ if (!entry || entry.frame !== bad) {
6357
+ this.overflowed = true;
6358
+ this.onRollbackOverflow?.(bad);
6359
+ return false;
6360
+ }
6361
+ this.rollbackCount++;
6362
+ this.world.restore(entry.snap);
6363
+ this.attach?.(this.world);
6364
+ for (let f = bad; f < target; f++) {
6365
+ this.storeSnapshot(f);
6366
+ this.stepFrame(f);
6367
+ this.resimulatedFrames++;
6368
+ }
6369
+ return true;
6370
+ }
6371
+ storeSnapshot(frame) {
6372
+ this.ring[frame % this.ring.length] = { frame, snap: this.world.snapshot() };
6373
+ }
6374
+ receive(data) {
6375
+ const msg = decodeMessage(data);
6376
+ if (!msg) return;
6377
+ if (msg.t === "input" && msg.player !== this.localPlayer) {
6378
+ const cut = this.dropped.get(msg.player);
6379
+ for (let i = 0; i < msg.frames.length; i++) {
6380
+ const frame = msg.from + i;
6381
+ if (cut !== void 0 && frame >= cut) continue;
6382
+ if (!this.buffer.set(msg.player, frame, msg.frames[i])) continue;
6383
+ if (frame < this.world.frame) {
6384
+ const used = this.usedInputs.get(frame)?.get(msg.player);
6385
+ const actual = JSON.stringify([...msg.frames[i]].sort());
6386
+ if (used !== void 0 && used !== actual) this.earliestBad = Math.min(this.earliestBad, frame);
6387
+ }
6388
+ }
6389
+ this.settleHashes();
6390
+ } else if (msg.t === "hash" && msg.player !== this.localPlayer) {
6391
+ this.pendingRemoteHashes.push({ player: msg.player, frame: msg.frame, hash: msg.hash });
6392
+ this.compareHashes();
6393
+ }
6394
+ }
6395
+ /**
6396
+ * Hash exchange runs on CONFIRMED frames only (predicted state is
6397
+ * provisional by design). A snapshot has exactly the shape `world.hash()`
6398
+ * hashes, so ring entries give us hashes of past frames for free.
6399
+ */
6400
+ settleHashes() {
6401
+ const settled = Math.min(this.confirmedFrame + 1, this.world.frame, this.earliestBad === Infinity ? Infinity : this.earliestBad);
6402
+ const interval = this.config.hashInterval;
6403
+ let h = (Math.floor(this.hashedThrough / interval) + 1) * interval;
6404
+ for (; h <= settled; h += interval) {
6405
+ const entry = this.ring[h % this.ring.length];
6406
+ if (!entry || entry.frame !== h) continue;
6407
+ const hash = hashValue(entry.snap);
6408
+ this.localHashes.set(h, hash);
6409
+ this.hashedThrough = h;
6410
+ this.transport.send(encodeMessage(netMessage({ t: "hash", player: this.localPlayer, frame: h, hash })));
6411
+ }
6412
+ this.compareHashes();
6413
+ }
6414
+ compareHashes() {
6415
+ const still = [];
6416
+ for (const remote of this.pendingRemoteHashes) {
6417
+ const local = this.localHashes.get(remote.frame);
6418
+ if (local === void 0) {
6419
+ if (remote.frame > this.hashedThrough - 512) still.push(remote);
6420
+ continue;
6421
+ }
6422
+ if (local !== remote.hash && !this.desynced) {
6423
+ this.desynced = true;
6424
+ this.onDesync?.({
6425
+ frame: remote.frame,
6426
+ player: remote.player,
6427
+ localHash: local,
6428
+ remoteHash: remote.hash,
6429
+ log: this.inputLog(),
6430
+ startFrame: 0
6431
+ });
6432
+ }
6433
+ }
6434
+ this.pendingRemoteHashes = still;
6435
+ }
6436
+ pruneOld(frame) {
6437
+ this.buffer.prune(frame - 128);
6438
+ for (const f of this.usedInputs.keys()) if (f < frame - 128) this.usedInputs.delete(f);
6439
+ for (const f of this.localHashes.keys()) if (f < frame - 512) this.localHashes.delete(f);
6440
+ }
6441
+ };
6442
+
6443
+ // src/net/room.ts
6444
+ function makeUid() {
6445
+ return globalThis.crypto.randomUUID();
6446
+ }
6447
+ function buildSession(mode, world, transport, localPlayer, players, startFrame, config, attach, callbacks) {
6448
+ if (mode === "rollback") {
6449
+ return new RollbackSession({ world, transport, localPlayer, players, config, attach, onDesync: callbacks.onDesync });
6450
+ }
6451
+ return new LockstepSession({ world, transport, localPlayer, players, startFrame, config, onDesync: callbacks.onDesync });
6452
+ }
6453
+ function makeGame(world, session, localPlayer, players, extraDispose) {
6454
+ return {
6455
+ world,
6456
+ session,
6457
+ localPlayer,
6458
+ players,
6459
+ advance: (realMs, localActions = []) => session.advance(realMs, localActions),
6460
+ dispose: () => {
6461
+ session.dispose();
6462
+ extraDispose?.();
6463
+ }
6464
+ };
6465
+ }
6466
+ var RoomHost = class {
6467
+ localPlayer = "p1";
6468
+ seed;
6469
+ config;
6470
+ transport;
6471
+ makeWorld;
6472
+ maxPlayers;
6473
+ joinMargin;
6474
+ attach;
6475
+ callbacks;
6476
+ players = ["p1"];
6477
+ uidToPlayer = /* @__PURE__ */ new Map();
6478
+ nextPlayerIndex = 2;
6479
+ started = false;
6480
+ game = null;
6481
+ pendingSnapshots = [];
6482
+ unsubscribe;
6483
+ constructor(opts) {
6484
+ this.transport = opts.transport;
6485
+ this.makeWorld = opts.makeWorld;
6486
+ this.seed = opts.seed ?? 1;
6487
+ this.config = { ...DEFAULT_SESSION_CONFIG, ...opts.config };
6488
+ this.maxPlayers = opts.maxPlayers ?? 4;
6489
+ this.joinMargin = opts.joinMargin ?? 30;
6490
+ this.attach = opts.attach;
6491
+ this.callbacks = opts;
6492
+ this.unsubscribe = this.transport.onMessage((data) => this.receive(data));
6493
+ }
6494
+ /** Players currently in the room (host first). */
6495
+ get roster() {
6496
+ return [...this.players];
6497
+ }
6498
+ get isStarted() {
6499
+ return this.started;
6500
+ }
6501
+ /** Freeze the roster's frame 0 and begin simulating. */
6502
+ start() {
6503
+ if (this.game) return this.game;
6504
+ this.started = true;
6505
+ const world = this.makeWorld(this.seed);
6506
+ const session = buildSession(
6507
+ this.config.mode,
6508
+ world,
6509
+ this.transport,
6510
+ this.localPlayer,
6511
+ this.players,
6512
+ 0,
6513
+ this.config,
6514
+ this.attach,
6515
+ this.callbacks
6516
+ );
6517
+ if (session instanceof LockstepSession) {
6518
+ session.onAfterStep((frame) => this.serviceSnapshots(frame, world));
6519
+ }
6520
+ this.transport.send(encodeMessage(netMessage({ t: "start", players: [...this.players] })));
6521
+ this.game = makeGame(world, session, this.localPlayer, this.players, () => this.unsubscribe());
6522
+ return this.game;
6523
+ }
6524
+ /** Announce a player's departure (disconnect handling is the app's call). */
6525
+ dropPlayer(player) {
6526
+ const session = this.game?.session;
6527
+ if (!session) return;
6528
+ const atFrame = session instanceof LockstepSession ? Math.max(session.frame, session.confirmedFrame + 1) : session.lastKnownFrame(player) + 1;
6529
+ this.transport.send(encodeMessage(netMessage({ t: "leave", player, atFrame })));
6530
+ session.removePlayer(player, atFrame);
6531
+ this.players = this.players.filter((p) => p !== player);
6532
+ this.callbacks.onPlayerLeave?.(player, atFrame);
6533
+ }
6534
+ dispose() {
6535
+ this.unsubscribe();
6536
+ this.game?.dispose();
6537
+ }
6538
+ // ── internals ────────────────────────────────────────────────
6539
+ receive(data) {
6540
+ const msg = decodeMessage(data);
6541
+ if (!msg) return;
6542
+ if (msg.t === "hello") this.handleHello(msg);
6543
+ else if (msg.t === "bye") {
6544
+ const player = this.uidToPlayer.get(msg.uid);
6545
+ if (player && this.started) this.dropPlayer(player);
6546
+ else if (player) {
6547
+ this.players = this.players.filter((p) => p !== player);
6548
+ this.uidToPlayer.delete(msg.uid);
6549
+ }
6550
+ }
6551
+ }
6552
+ handleHello(msg) {
6553
+ if (this.uidToPlayer.has(msg.uid)) return;
6554
+ if (this.players.length >= this.maxPlayers) {
6555
+ this.transport.send(encodeMessage(netMessage({ t: "deny", to: msg.uid, reason: "room full" })));
6556
+ return;
6557
+ }
6558
+ if (this.started && this.config.mode === "rollback") {
6559
+ this.transport.send(encodeMessage(netMessage({ t: "deny", to: msg.uid, reason: "rollback rooms cannot be joined mid-game" })));
6560
+ return;
6561
+ }
6562
+ const player = `p${this.nextPlayerIndex++}`;
6563
+ this.uidToPlayer.set(msg.uid, player);
6564
+ this.players.push(player);
6565
+ if (!this.started) {
6566
+ this.transport.send(
6567
+ encodeMessage(
6568
+ netMessage({
6569
+ t: "welcome",
6570
+ to: msg.uid,
6571
+ player,
6572
+ players: [...this.players],
6573
+ seed: this.seed,
6574
+ config: this.config,
6575
+ startFrame: 0
6576
+ })
6577
+ )
6578
+ );
6579
+ this.callbacks.onPlayerJoin?.(player, 0);
6580
+ return;
6581
+ }
6582
+ const session = this.game.session;
6583
+ const atFrame = session.frame + this.joinMargin;
6584
+ this.transport.send(encodeMessage(netMessage({ t: "join", player, atFrame })));
6585
+ session.addPlayer(player, atFrame);
6586
+ this.pendingSnapshots.push({ uid: msg.uid, player, atFrame });
6587
+ this.callbacks.onPlayerJoin?.(player, atFrame);
6588
+ }
6589
+ serviceSnapshots(frame, world) {
6590
+ if (this.pendingSnapshots.length === 0) return;
6591
+ const due = this.pendingSnapshots.filter((p) => p.atFrame === frame);
6592
+ if (due.length === 0) return;
6593
+ this.pendingSnapshots = this.pendingSnapshots.filter((p) => p.atFrame !== frame);
6594
+ const snapshot = world.snapshot();
6595
+ for (const join of due) {
6596
+ const stillPending = this.pendingSnapshots.filter((p) => p.player !== join.player);
6597
+ const pendingIds = new Set(stillPending.map((p) => p.player));
6598
+ this.transport.send(
6599
+ encodeMessage(
6600
+ netMessage({
6601
+ t: "welcome",
6602
+ to: join.uid,
6603
+ player: join.player,
6604
+ players: this.players.filter((p) => !pendingIds.has(p)),
6605
+ seed: this.seed,
6606
+ config: this.config,
6607
+ startFrame: frame,
6608
+ snapshot,
6609
+ joins: stillPending.map((p) => ({ player: p.player, atFrame: p.atFrame }))
6610
+ })
6611
+ )
6612
+ );
6613
+ }
6614
+ }
6615
+ };
6616
+ function joinRoom(opts) {
6617
+ const uid = makeUid();
6618
+ const transport = opts.transport;
6619
+ return new Promise((resolve, reject) => {
6620
+ let welcome = null;
6621
+ let started = false;
6622
+ let startRoster = null;
6623
+ const buffered = [];
6624
+ const pendingRoster = [];
6625
+ const finish = () => {
6626
+ if (!welcome || !started) return;
6627
+ unsub();
6628
+ const roster = startRoster ?? welcome.players;
6629
+ const world = opts.makeWorld(welcome.seed);
6630
+ if (welcome.snapshot) {
6631
+ world.restore(welcome.snapshot);
6632
+ opts.attach?.(world);
6633
+ }
6634
+ const session = buildSession(
6635
+ welcome.config.mode,
6636
+ world,
6637
+ transport,
6638
+ welcome.player,
6639
+ roster,
6640
+ welcome.startFrame,
6641
+ welcome.config,
6642
+ opts.attach,
6643
+ opts
6644
+ );
6645
+ if (welcome.joins && session instanceof LockstepSession) {
6646
+ for (const j of welcome.joins) session.addPlayer(j.player, j.atFrame);
6647
+ }
6648
+ for (const m of pendingRoster) {
6649
+ if (m.t === "join" && session instanceof LockstepSession) session.addPlayer(m.player, m.atFrame);
6650
+ if (m.t === "leave") session.removePlayer(m.player, m.atFrame);
6651
+ }
6652
+ const roomUnsub = transport.onMessage((data) => {
6653
+ const msg = decodeMessage(data);
6654
+ if (!msg) return;
6655
+ if (msg.t === "join" && session instanceof LockstepSession) {
6656
+ session.addPlayer(msg.player, msg.atFrame);
6657
+ opts.onPlayerJoin?.(msg.player, msg.atFrame);
6658
+ } else if (msg.t === "leave") {
6659
+ session.removePlayer(msg.player, msg.atFrame);
6660
+ opts.onPlayerLeave?.(msg.player, msg.atFrame);
6661
+ }
6662
+ });
6663
+ const game = makeGame(world, session, welcome.player, roster, () => {
6664
+ roomUnsub();
6665
+ transport.send(encodeMessage(netMessage({ t: "bye", uid })));
6666
+ });
6667
+ for (const data of buffered) session.deliver(data);
6668
+ resolve(game);
6669
+ };
6670
+ const unsub = transport.onMessage((data) => {
6671
+ const msg = decodeMessage(data);
6672
+ if (!msg) return;
6673
+ switch (msg.t) {
6674
+ case "welcome":
6675
+ if (msg.to === uid) {
6676
+ welcome = msg;
6677
+ if (msg.snapshot || msg.startFrame > 0) started = true;
6678
+ finish();
6679
+ }
6680
+ break;
6681
+ case "deny":
6682
+ if (msg.to === uid) {
6683
+ unsub();
6684
+ reject(new Error(`hayao-net: join denied \u2014 ${msg.reason}`));
6685
+ }
6686
+ break;
6687
+ case "start":
6688
+ started = true;
6689
+ startRoster = msg.players;
6690
+ finish();
6691
+ break;
6692
+ case "join":
6693
+ case "leave":
6694
+ pendingRoster.push(msg);
6695
+ break;
6696
+ case "input":
6697
+ case "hash":
6698
+ buffered.push(data);
6699
+ break;
6700
+ }
6701
+ });
6702
+ transport.send(encodeMessage(netMessage({ t: "hello", uid, name: opts.name })));
6703
+ });
6704
+ }
6705
+ function hostRoom(opts) {
6706
+ return new RoomHost(opts);
6707
+ }
6708
+
6709
+ // src/world.ts
6710
+ var World = class {
6711
+ rng;
6712
+ clock;
6713
+ input = new InputState();
6714
+ events = new EventBus();
6715
+ resources = /* @__PURE__ */ new Map();
6716
+ /**
2473
6717
  * Canonical game state that lives OUTSIDE the scene tree (pure-sim structs,
2474
6718
  * controllers). Must be plain JSON-serializable data: it is included in
2475
6719
  * `hash()` and `snapshot()`, so hidden state here cannot escape determinism
@@ -2634,6 +6878,86 @@ function runHeadless(def, inputLog) {
2634
6878
  return { world, hash: world.hash(), steps: frames.length };
2635
6879
  }
2636
6880
 
6881
+ // src/net/browser.ts
6882
+ function runBrowserNet(def, mount, opts) {
6883
+ const width = def.width ?? 1280;
6884
+ const height = def.height ?? 720;
6885
+ const background = def.background ?? "#f3ecdb";
6886
+ const renderer = opts.renderer === "canvas" ? new Canvas2DRenderer({ width, height, background }) : new SvgRenderer({ width, height, background });
6887
+ mount.style.position = mount.style.position || "relative";
6888
+ renderer.mount?.(mount);
6889
+ setOverlayHost(mount);
6890
+ const input = new KeyboardSource(def.inputMap ?? {}, document);
6891
+ const makeWorld = (seed) => createWorld(def, seed);
6892
+ let game = null;
6893
+ let host2 = null;
6894
+ let raf = 0;
6895
+ let stopped = false;
6896
+ const loop = (last) => (now) => {
6897
+ if (!stopped && game) {
6898
+ const stepped = game.advance(now - last, input.currentActions());
6899
+ if (stepped > 0) input.clearPressed();
6900
+ renderer.draw(game.world.render());
6901
+ }
6902
+ raf = requestAnimationFrame(loop(now));
6903
+ };
6904
+ const begin = (g) => {
6905
+ game = g;
6906
+ opts.onStatus?.(`playing as ${g.localPlayer} (${g.players.length} players)`);
6907
+ raf = requestAnimationFrame(loop(performance.now()));
6908
+ };
6909
+ if (opts.role === "host") {
6910
+ host2 = hostRoom({
6911
+ transport: opts.transport,
6912
+ makeWorld,
6913
+ seed: opts.seed ?? def.seed ?? 1,
6914
+ config: opts.config,
6915
+ maxPlayers: opts.maxPlayers,
6916
+ attach: opts.attach,
6917
+ onDesync: opts.onDesync,
6918
+ onPlayerJoin: (p, f) => {
6919
+ opts.onStatus?.(`${p} joined`);
6920
+ opts.onPlayerJoin?.(p, f);
6921
+ },
6922
+ onPlayerLeave: opts.onPlayerLeave
6923
+ });
6924
+ opts.onStatus?.("hosting \u2014 waiting for players");
6925
+ } else {
6926
+ opts.onStatus?.("joining\u2026");
6927
+ joinRoom({
6928
+ transport: opts.transport,
6929
+ makeWorld,
6930
+ attach: opts.attach,
6931
+ onDesync: opts.onDesync,
6932
+ onPlayerJoin: opts.onPlayerJoin,
6933
+ onPlayerLeave: opts.onPlayerLeave
6934
+ }).then(begin).catch((err) => opts.onStatus?.(err.message));
6935
+ }
6936
+ return {
6937
+ start() {
6938
+ if (host2 && !game) begin(host2.start());
6939
+ },
6940
+ get game() {
6941
+ return game;
6942
+ },
6943
+ get localPlayer() {
6944
+ return game?.localPlayer ?? (opts.role === "host" ? "p1" : null);
6945
+ },
6946
+ get roster() {
6947
+ return host2?.roster ?? game?.players ?? [];
6948
+ },
6949
+ input,
6950
+ stop() {
6951
+ stopped = true;
6952
+ cancelAnimationFrame(raf);
6953
+ game?.dispose();
6954
+ host2?.dispose();
6955
+ input.dispose();
6956
+ renderer.dispose?.();
6957
+ }
6958
+ };
6959
+ }
6960
+
2637
6961
  // src/app/browser.ts
2638
6962
  function runBrowser(def, mount, opts = {}) {
2639
6963
  const width = def.width ?? 1280;
@@ -2711,33 +7035,66 @@ function runBrowser(def, mount, opts = {}) {
2711
7035
  }
2712
7036
 
2713
7037
  // src/index.ts
2714
- var VERSION = "0.1.0";
7038
+ var VERSION = "0.2.0";
2715
7039
  export {
7040
+ AMBIENT_PRESETS,
7041
+ AmbientField,
2716
7042
  AnimationPlayer,
2717
7043
  AudioBus,
7044
+ BitmapText,
7045
+ BroadcastChannelTransport,
2718
7046
  Camera2D,
2719
7047
  Canvas2DRenderer,
7048
+ CinematicPlayer,
2720
7049
  Clock,
2721
7050
  DEFAULT_INPUT_MAP,
2722
7051
  DEFAULT_PLATFORMER,
7052
+ DEFAULT_SESSION_CONFIG,
2723
7053
  DEFAULT_TILE_CHARS,
2724
7054
  DUSK,
2725
7055
  EASINGS,
7056
+ Edge,
2726
7057
  EventBus,
7058
+ FLOAT_PRESETS,
7059
+ FONT_5,
7060
+ FloatingText,
7061
+ Fsm,
2727
7062
  HeadlessRenderer,
2728
7063
  IDENTITY,
7064
+ InputBuffer,
2729
7065
  InputRecorder,
2730
7066
  InputState,
2731
7067
  KeyboardSource,
7068
+ LocalStorageAdapter,
7069
+ LockstepSession,
7070
+ LoopbackHub,
7071
+ LoopbackTransport,
7072
+ LootTable,
2732
7073
  MEADOW,
7074
+ MemoryStorage,
7075
+ NEIGHBORS_4,
7076
+ NEIGHBORS_8,
7077
+ NET_PROTOCOL_VERSION,
2733
7078
  Node,
2734
7079
  Node as Node2D,
7080
+ NodePool,
7081
+ NullStorage,
2735
7082
  PAD_NEUTRAL,
2736
7083
  PALETTES,
7084
+ PANEL_PRESETS,
2737
7085
  PAPER,
2738
7086
  PARTICLE_PRESETS,
2739
7087
  Particles,
7088
+ PhaseClock,
7089
+ PixelBuffer,
7090
+ PlayerInput,
7091
+ RingBuffer,
2740
7092
  Rng,
7093
+ RollbackSession,
7094
+ RoomHost,
7095
+ SAVE_FORMAT_VERSION,
7096
+ SaveManager,
7097
+ ScreenTransition,
2741
7098
  Shaker,
2742
7099
  Shell,
2743
7100
  Signal,
@@ -2747,59 +7104,150 @@ export {
2747
7104
  TAU,
2748
7105
  TILE,
2749
7106
  Text,
7107
+ TextureSprite,
2750
7108
  Timer,
7109
+ UndoStack,
2751
7110
  VERSION,
7111
+ WangFrame,
2752
7112
  World,
7113
+ addBody,
7114
+ addDistanceJoint,
7115
+ addRevoluteJoint,
7116
+ addTransition,
7117
+ applyImpulse,
2753
7118
  applyTransform,
2754
7119
  asciiEntities,
2755
7120
  assertDeterministic,
2756
7121
  assertSnapshotStable,
2757
7122
  assertSolvable,
7123
+ astar,
7124
+ astarGrid,
2758
7125
  audio,
7126
+ autotile4,
7127
+ autotileToCommands,
7128
+ availableUpgrades,
7129
+ bfs,
2759
7130
  blobPath,
7131
+ bodyAABB,
7132
+ bodyContains,
7133
+ cellFloat,
7134
+ cellHash,
7135
+ cellInt,
2760
7136
  changeFrames,
2761
7137
  checkDeterministic,
2762
7138
  clamp,
7139
+ collide,
2763
7140
  commandsToSVGInner,
2764
7141
  composeTransform,
7142
+ connectWebSocket,
7143
+ connectedComponents,
7144
+ contourToCommands,
2765
7145
  createPlanBot,
2766
7146
  createPlatformerState,
7147
+ createRigidWorld,
2767
7148
  createWorld,
2768
7149
  dashJumpDistance,
7150
+ datan,
7151
+ datan2,
7152
+ dcos,
7153
+ decode2bit,
7154
+ decodeBits,
7155
+ decodeMessage,
7156
+ decodeRLE,
7157
+ defaultStorage,
2769
7158
  defineGame,
2770
7159
  deg2rad,
2771
7160
  deserializeNode,
7161
+ dexp,
7162
+ dexp2,
7163
+ dhypot,
7164
+ dlog,
7165
+ dlog10,
7166
+ dlog2,
2772
7167
  drive,
7168
+ dsin,
7169
+ encodeMessage,
7170
+ encodeRLE,
2773
7171
  firstFrame,
7172
+ floodFill,
7173
+ fractalNoise,
2774
7174
  frameActions,
2775
7175
  gameInputMap,
7176
+ generateCave,
7177
+ generateDungeon,
7178
+ getBody,
7179
+ getJoint,
7180
+ gradient,
7181
+ gridAt,
7182
+ gridFromRows,
7183
+ gridSet,
7184
+ gridToTilemap,
2776
7185
  groundAt,
2777
7186
  hashString,
2778
7187
  hashValue,
7188
+ hexToHsl,
2779
7189
  hideScreen,
2780
7190
  hold,
7191
+ hostRoom,
7192
+ hsl,
7193
+ hsv,
2781
7194
  inVisionCone,
7195
+ initDirector,
2782
7196
  inputDensity,
2783
7197
  installCapture,
2784
7198
  invLerp,
2785
7199
  invertTransform,
2786
7200
  isCaptureMode,
7201
+ isGround,
2787
7202
  isMonotonic,
7203
+ joinRoom,
2788
7204
  jumpAirtime,
2789
7205
  jumpDistance,
2790
7206
  jumpHeight,
7207
+ keyMentions,
2791
7208
  keysToActions,
7209
+ layoutIssues,
7210
+ layoutText,
2792
7211
  lerp,
7212
+ lerpDamp,
2793
7213
  lineOfSight,
2794
7214
  longestLull,
7215
+ makeGrid,
7216
+ makeReach,
2795
7217
  makeTransform,
2796
7218
  mapHeight,
2797
7219
  mapWidth,
7220
+ marchingSquaresCases,
7221
+ marchingSquaresContours,
7222
+ mask4,
7223
+ mask8,
7224
+ measureLine,
7225
+ measureText,
7226
+ mergePlayerFrames,
7227
+ missingControlHints,
2798
7228
  mix,
7229
+ mixLinear,
2799
7230
  moveRect,
7231
+ mutateColor,
7232
+ netMessage,
7233
+ nineSlice,
7234
+ packVarints,
7235
+ parseRich,
7236
+ parseSnapshot,
7237
+ passableFromTilemap,
7238
+ pickEntry,
7239
+ pixelsToCommands,
7240
+ playerAction,
7241
+ playerIds,
7242
+ playerInput,
7243
+ pointQuery,
7244
+ pollDirector,
7245
+ polygonBox,
2800
7246
  pump,
2801
7247
  rad2deg,
7248
+ rayCastRigid,
2802
7249
  raycastTiles,
7250
+ reconstructPath,
2803
7251
  recordTimeline,
2804
7252
  rectBlocked,
2805
7253
  rectContains,
@@ -2807,30 +7255,55 @@ export {
2807
7255
  registerNode,
2808
7256
  regularPolygon,
2809
7257
  remap,
7258
+ removeBody,
7259
+ removeJoint,
2810
7260
  renderFilmstrip,
2811
7261
  renderToSVGString,
2812
7262
  replay,
2813
7263
  resetNodeIds,
7264
+ rigidStep,
7265
+ rleDecode,
7266
+ rleEncode,
7267
+ roomCenter,
2814
7268
  runBrowser,
7269
+ runBrowserNet,
2815
7270
  runHeadless,
7271
+ sampleGradient,
7272
+ scatter,
7273
+ scatterCells,
7274
+ screenRect,
2816
7275
  scriptToFrames,
2817
7276
  scriptedPlaythrough,
7277
+ serializeSnapshot,
2818
7278
  series,
2819
7279
  setOverlayHost,
2820
7280
  settings,
7281
+ shapeBox,
2821
7282
  showScreen,
2822
7283
  smoothClosedPath,
2823
7284
  smoothOpenPath,
7285
+ smoothstep,
7286
+ solidNeighbours,
2824
7287
  solve,
7288
+ solveJoint,
2825
7289
  sortCommands,
7290
+ spring,
7291
+ springStep,
2826
7292
  star,
2827
7293
  steer2D,
2828
7294
  stepPlatformer,
7295
+ terrainHeight,
7296
+ terrainSlice,
7297
+ textBox,
7298
+ textToCommands,
2829
7299
  tileAt,
2830
7300
  tileAtPoint,
2831
7301
  tilemapFromAscii,
2832
7302
  toggleFullscreen,
7303
+ typewriterCount,
7304
+ unpackVarints,
2833
7305
  vadd,
7306
+ valueNoise,
2834
7307
  vdist,
2835
7308
  vdot,
2836
7309
  vec2,
@@ -2839,6 +7312,13 @@ export {
2839
7312
  vscale,
2840
7313
  vsub,
2841
7314
  wait,
2842
- withAlpha
7315
+ wakeBody,
7316
+ wangTile,
7317
+ weatherEnvelope,
7318
+ weightedIndex,
7319
+ weightedPick,
7320
+ wipeStep,
7321
+ withAlpha,
7322
+ worldPoints
2843
7323
  };
2844
7324
  //# sourceMappingURL=index.js.map