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.
- package/README.md +29 -12
- package/dist/art/autotile.d.ts +77 -0
- package/dist/art/bitmapFont.d.ts +113 -0
- package/dist/art/font5.d.ts +11 -0
- package/dist/art/palette.d.ts +38 -0
- package/dist/art/texture.d.ts +78 -0
- package/dist/audio/audio.d.ts +7 -0
- package/dist/content/dsl.d.ts +61 -0
- package/dist/core/dmath.d.ts +20 -0
- package/dist/core/math.d.ts +2 -0
- package/dist/index.d.ts +37 -1
- package/dist/index.js +4549 -69
- package/dist/index.js.map +4 -4
- package/dist/logic/fsm.d.ts +85 -0
- package/dist/logic/graph.d.ts +88 -0
- package/dist/logic/history.d.ts +54 -0
- package/dist/logic/random.d.ts +32 -0
- package/dist/net/browser.d.ts +37 -0
- package/dist/net/inputBuffer.d.ts +27 -0
- package/dist/net/lockstep.d.ts +79 -0
- package/dist/net/players.d.ts +27 -0
- package/dist/net/protocol.d.ts +100 -0
- package/dist/net/rollback.d.ts +89 -0
- package/dist/net/room.d.ts +78 -0
- package/dist/net/transport.d.ts +78 -0
- package/dist/persist/codec.d.ts +4 -0
- package/dist/persist/save.d.ts +32 -0
- package/dist/persist/storage.d.ts +46 -0
- package/dist/physics/rigidBody.d.ts +104 -0
- package/dist/physics/rigidCollide.d.ts +16 -0
- package/dist/physics/rigidJoints.d.ts +65 -0
- package/dist/physics/rigidQueries.d.ts +15 -0
- package/dist/physics/rigidStep.d.ts +14 -0
- package/dist/procgen/cave.d.ts +21 -0
- package/dist/procgen/grid.d.ts +21 -0
- package/dist/procgen/rooms.d.ts +34 -0
- package/dist/procgen/scatter.d.ts +32 -0
- package/dist/procgen/terrain.d.ts +24 -0
- package/dist/render/nineSlice.d.ts +32 -0
- package/dist/scene/floatingText.d.ts +51 -0
- package/dist/scene/particles.d.ts +64 -0
- package/dist/scene/pool.d.ts +16 -0
- package/dist/scene/tween.d.ts +26 -0
- package/dist/ui/transition.d.ts +107 -0
- package/dist/verify/layout.d.ts +39 -0
- package/docs/API.md +247 -8
- 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) =>
|
|
8
|
-
var vdist = (a, b) =>
|
|
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 =
|
|
39
|
-
const sin =
|
|
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
|
|
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 -
|
|
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 -
|
|
660
|
-
cubicInOut: (t) => t < 0.5 ? 4 * t * t * t : 1 -
|
|
661
|
-
sineIn: (t) => 1 -
|
|
662
|
-
sineOut: (t) =>
|
|
663
|
-
sineInOut: (t) => -(
|
|
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 *
|
|
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 :
|
|
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,
|
|
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:
|
|
750
|
-
y:
|
|
751
|
-
vx:
|
|
752
|
-
vy:
|
|
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 /
|
|
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 =
|
|
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 =
|
|
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 <
|
|
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(
|
|
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(
|
|
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:
|
|
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/
|
|
1856
|
-
var
|
|
1857
|
-
|
|
1858
|
-
|
|
1859
|
-
|
|
1860
|
-
|
|
1861
|
-
|
|
1862
|
-
|
|
1863
|
-
|
|
1864
|
-
|
|
1865
|
-
|
|
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
|
-
|
|
1868
|
-
|
|
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
|
-
|
|
1871
|
-
|
|
1872
|
-
|
|
1873
|
-
|
|
1874
|
-
|
|
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
|
-
|
|
1888
|
-
|
|
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
|
-
|
|
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/
|
|
2466
|
-
var
|
|
2467
|
-
|
|
2468
|
-
|
|
2469
|
-
|
|
2470
|
-
|
|
2471
|
-
|
|
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.
|
|
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
|
-
|
|
7315
|
+
wakeBody,
|
|
7316
|
+
wangTile,
|
|
7317
|
+
weatherEnvelope,
|
|
7318
|
+
weightedIndex,
|
|
7319
|
+
weightedPick,
|
|
7320
|
+
wipeStep,
|
|
7321
|
+
withAlpha,
|
|
7322
|
+
worldPoints
|
|
2843
7323
|
};
|
|
2844
7324
|
//# sourceMappingURL=index.js.map
|