@escapenavigator/hooks 2.2.10 → 2.2.12

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.
@@ -0,0 +1,1361 @@
1
+ "use strict";
2
+ Object.defineProperty(exports, "__esModule", { value: true });
3
+ exports.createFortuneWheelRenderer = createFortuneWheelRenderer;
4
+ // Artwork ported from escape-navigator-games, commit 2d99f68 (premium/).
5
+ // Only drawing and sound remain; the server is the sole source of prizes.
6
+ const assets_1 = require("./assets");
7
+ function createFortuneWheelRenderer(canvas, options) {
8
+ const ctx = canvas.getContext('2d'), TAU = Math.PI * 2;
9
+ const { theme, prizes, labels } = options;
10
+ let busy = false, moving = options.moving, sound = options.sound, rot = 0, motor = 0, start = 0, from = 0, target = 0, selected = 0, ballAngle = 0, ballRadius = 284, previous = performance.now(), elapsed = 0, glow = 0, reels = [0, 0, 0], audio = null, lastTick = -1, disposed = false, frameId = 0, done = null;
11
+ const seals = [false, false, false], guests = [false, false, false], nodes = [1, 2, 3];
12
+ const logo = new Image();
13
+ logo.src = assets_1.fortuneWheelLogo;
14
+ function grad(x, y, w, h, stops) {
15
+ const g = ctx.createLinearGradient(x, y, x + w, y + h);
16
+ stops.forEach((c, i) => {
17
+ g.addColorStop(i / (stops.length - 1), c);
18
+ });
19
+ return g;
20
+ }
21
+ function metal(x, y, r, cold = false) {
22
+ return grad(x - r, y - r, r * 1.4, r * 2, cold
23
+ ? ['#b4c6cf', '#657b88', '#293b48', '#8196a1']
24
+ : ['#e3d2ac', '#a48a60', '#514332', '#bba47c']);
25
+ }
26
+ function circle(x, y, r, fill, stroke = null, width = 1) {
27
+ ctx.beginPath();
28
+ ctx.arc(x, y, r, 0, TAU);
29
+ ctx.fillStyle = fill;
30
+ ctx.fill();
31
+ if (stroke) {
32
+ ctx.strokeStyle = stroke;
33
+ ctx.lineWidth = width;
34
+ ctx.stroke();
35
+ }
36
+ }
37
+ function rr(x, y, w, h, r, fill, stroke = null, lw = 1) {
38
+ ctx.beginPath();
39
+ ctx.roundRect(x, y, w, h, r);
40
+ ctx.fillStyle = fill;
41
+ ctx.fill();
42
+ if (stroke) {
43
+ ctx.strokeStyle = stroke;
44
+ ctx.lineWidth = lw;
45
+ ctx.stroke();
46
+ }
47
+ }
48
+ function text(s, x, y, size = 14, color = '#e7cc90', font = 'Arial', weight = 'normal') {
49
+ ctx.fillStyle = color;
50
+ ctx.font = `${weight} ${size}px ${font}`;
51
+ ctx.textAlign = 'center';
52
+ ctx.textBaseline = 'middle';
53
+ ctx.fillText(s, x, y, Math.max(72, size * 6));
54
+ }
55
+ function screw(x, y, cold = false) {
56
+ circle(x, y, 5, metal(x, y, 5, cold), '#171310', 1);
57
+ ctx.strokeStyle = '#332b21';
58
+ ctx.lineWidth = 1.5;
59
+ ctx.beginPath();
60
+ ctx.moveTo(x - 3, y - 1);
61
+ ctx.lineTo(x + 3, y + 1);
62
+ ctx.stroke();
63
+ }
64
+ function teeth(x, y, r, n, a, cold = false) {
65
+ ctx.save();
66
+ ctx.translate(x, y);
67
+ ctx.rotate(a);
68
+ ctx.beginPath();
69
+ for (let i = 0; i < n * 4; i++) {
70
+ const rad = i % 4 === 0 || i % 4 === 3 ? r - 7 : r, ang = (i * TAU) / (n * 4);
71
+ ctx.lineTo(Math.cos(ang) * rad, Math.sin(ang) * rad);
72
+ }
73
+ ctx.closePath();
74
+ ctx.fillStyle = metal(0, 0, r, cold);
75
+ ctx.shadowColor = '#000';
76
+ ctx.shadowBlur = 12;
77
+ ctx.shadowOffsetY = 5;
78
+ ctx.fill();
79
+ ctx.shadowBlur = 0;
80
+ ctx.shadowOffsetY = 0;
81
+ ctx.strokeStyle = cold ? '#b1dcf977' : '#fbe3ac77';
82
+ ctx.lineWidth = 1;
83
+ ctx.stroke();
84
+ circle(0, 0, r - 14, '#15151a', cold ? '#466582' : '#72572f', 3);
85
+ for (let j = 0; j < 6; j++) {
86
+ ctx.save();
87
+ ctx.rotate((j * TAU) / 6);
88
+ rr(10, -5, r - 28, 10, 2, metal(0, 0, r, cold));
89
+ ctx.restore();
90
+ }
91
+ circle(0, 0, 18, metal(0, 0, 20, cold), '#201d16', 3);
92
+ circle(0, 0, 7, '#15171c', '#c4a770', 2);
93
+ ctx.restore();
94
+ }
95
+ function chain(x, y, r, x2, y2, a) {
96
+ const dx = x2 - x, dy = y2 - y, dist = Math.hypot(dx, dy), ang = Math.atan2(dy, dx);
97
+ ctx.save();
98
+ ctx.translate(x, y);
99
+ ctx.rotate(ang);
100
+ ctx.strokeStyle = '#816a43';
101
+ ctx.lineWidth = 7;
102
+ ctx.beginPath();
103
+ ctx.moveTo(0, -r);
104
+ ctx.lineTo(dist, -r);
105
+ ctx.arc(dist, 0, r, -Math.PI / 2, Math.PI / 2);
106
+ ctx.lineTo(0, r);
107
+ ctx.arc(0, 0, r, Math.PI / 2, Math.PI * 1.5);
108
+ ctx.stroke();
109
+ const len = 2 * dist + TAU * r, count = Math.floor(len / 13);
110
+ for (let i = 0; i < count; i++) {
111
+ let s = ((((i * len) / count + a * r) % len) + len) % len, px, py;
112
+ if (s < dist) {
113
+ px = s;
114
+ py = -r;
115
+ }
116
+ else if (s < dist + Math.PI * r) {
117
+ const t = (s - dist) / r - Math.PI / 2;
118
+ px = dist + r * Math.cos(t);
119
+ py = r * Math.sin(t);
120
+ }
121
+ else if (s < 2 * dist + Math.PI * r) {
122
+ px = dist - (s - dist - Math.PI * r);
123
+ py = r;
124
+ }
125
+ else {
126
+ const t = (s - 2 * dist - Math.PI * r) / r + Math.PI / 2;
127
+ px = r * Math.cos(t);
128
+ py = r * Math.sin(t);
129
+ }
130
+ circle(px, py, 3.5, '#b19c76', '#302b22', 1);
131
+ circle(px, py, 1, '#f5e3b8');
132
+ }
133
+ ctx.restore();
134
+ }
135
+ function bolts(x, y, r, n) {
136
+ for (let i = 0; i < n; i++) {
137
+ const a = (i * TAU) / n;
138
+ screw(x + Math.cos(a) * r, y + Math.sin(a) * r);
139
+ }
140
+ }
141
+ function ring(x, y, r, a, cold = false) {
142
+ ctx.save();
143
+ ctx.shadowColor = '#0009';
144
+ ctx.shadowBlur = 22;
145
+ ctx.shadowOffsetY = 12;
146
+ circle(x, y, r, metal(x, y, r, cold), '#090b0d', 2);
147
+ ctx.restore();
148
+ circle(x, y, r - 6, '#1a1d21', cold ? '#aabcc355' : '#d8c6a055', 1);
149
+ circle(x, y, r - 17, metal(x, y, r, cold));
150
+ circle(x, y, r - 22, '#101317');
151
+ for (let i = 0; i < 36; i++) {
152
+ const t = (i * TAU) / 36;
153
+ ctx.save();
154
+ ctx.translate(x, y);
155
+ ctx.rotate(t);
156
+ ctx.fillStyle = cold ? '#c5dce14d' : '#e2cd9e55';
157
+ ctx.fillRect(-0.5, -r + 8, 1, i % 3 === 0 ? 6 : 3);
158
+ ctx.restore();
159
+ }
160
+ }
161
+ function sectors(x, y, r, a, cyber = false) {
162
+ ctx.save();
163
+ ctx.translate(x, y);
164
+ ctx.rotate(a);
165
+ const n = prizes.length;
166
+ const pal = (typeof gamePalettes !== 'undefined' ? gamePalettes[theme] : null) || [
167
+ '#355d60',
168
+ '#33354e',
169
+ ];
170
+ for (let i = 0; i < n; i++) {
171
+ const st = (i * TAU) / n - Math.PI / 2, en = ((i + 1) * TAU) / n - Math.PI / 2;
172
+ ctx.beginPath();
173
+ ctx.moveTo(0, 0);
174
+ ctx.arc(0, 0, r, st, en);
175
+ ctx.closePath();
176
+ const g = ctx.createRadialGradient(0, 0, 35, 0, 0, r);
177
+ g.addColorStop(0, '#14171c');
178
+ g.addColorStop(0.78, pal[i % pal.length]);
179
+ g.addColorStop(1, pal[i % pal.length]);
180
+ ctx.fillStyle = g;
181
+ ctx.fill();
182
+ ctx.strokeStyle = '#e7d3af44';
183
+ ctx.lineWidth = 1;
184
+ ctx.stroke();
185
+ ctx.save();
186
+ ctx.rotate(((i + 0.5) * TAU) / n);
187
+ ctx.beginPath();
188
+ ctx.arc(0, 0, r - 12, -Math.PI / 2 - TAU / n / 2 + 0.045, -Math.PI / 2 + TAU / n / 2 - 0.045);
189
+ ctx.strokeStyle = '#ffffff20';
190
+ ctx.stroke();
191
+ const fs = Math.min(30, r * 0.12), yv = -r * (theme === 'orbit' ? 0.82 : 0.72);
192
+ text(prizes[i].label, 0, yv, fs, '#f2eee4', 'NavigatorTitle, Arial', '600');
193
+ text(prizes[i].caption, 0, yv + fs * 0.85, Math.max(8, fs * 0.32), '#c8c6be', 'Arial');
194
+ ctx.restore();
195
+ }
196
+ ctx.restore();
197
+ }
198
+ function lamps(x, y, r, time, cold = false) {
199
+ for (let i = 0; i < 24; i++) {
200
+ const a = (i * TAU) / 24, v = busy ? 0.4 + 0.6 * Math.max(0, Math.cos(i * 0.6 - time * 8)) : 0.38;
201
+ ctx.save();
202
+ ctx.shadowColor = cold ? '#9cdee6' : '#e2c58b';
203
+ ctx.shadowBlur = busy ? 9 : 0;
204
+ circle(x + Math.cos(a) * r, y + Math.sin(a) * r, 2, cold ? `rgba(179,225,229,${v})` : `rgba(228,204,155,${v})`);
205
+ ctx.restore();
206
+ }
207
+ }
208
+ function hub(x, y, cold = false) {
209
+ ctx.save();
210
+ ctx.shadowColor = '#0007';
211
+ ctx.shadowBlur = 16;
212
+ ctx.shadowOffsetY = 6;
213
+ circle(x, y, 68, metal(x, y, 68, cold));
214
+ ctx.restore();
215
+ circle(x, y, 62, grad(x - 60, y - 60, 120, 120, ['#30353c', '#11161c']), cold ? '#a9bdc266' : '#dac49b66', 1);
216
+ circle(x, y, 53, '#191e25', '#ffffff0d', 1);
217
+ if (logo.complete)
218
+ ctx.drawImage(logo, x - 23, y - 25, 46, 46);
219
+ }
220
+ function pointer(x, y, t, cold = false) {
221
+ ctx.save();
222
+ ctx.translate(x, y);
223
+ ctx.rotate(busy ? Math.sin(rot * 30) * 0.035 : 0);
224
+ rr(-9, -24, 18, 42, 8, metal(0, 0, 30, cold), '#ffffff55', 1);
225
+ circle(0, -15, 3, cold ? '#dbf1f2' : '#f1dfb7');
226
+ ctx.restore();
227
+ }
228
+ function atmosphere(time, cold = false) {
229
+ const g = ctx.createRadialGradient(550, 360, 20, 550, 400, 470);
230
+ g.addColorStop(0, cold ? '#5f899c0c' : '#bba7800b');
231
+ g.addColorStop(1, '#00000000');
232
+ ctx.fillStyle = g;
233
+ ctx.fillRect(0, 0, 1100, 820);
234
+ ctx.save();
235
+ ctx.translate(550, 735);
236
+ ctx.scale(1, 0.12);
237
+ const sh = ctx.createRadialGradient(0, 0, 0, 0, 0, 340);
238
+ sh.addColorStop(0, '#0009');
239
+ sh.addColorStop(1, '#0000');
240
+ circle(0, 0, 340, sh);
241
+ ctx.restore();
242
+ }
243
+ function vault(t) {
244
+ const x = 550, y = 402;
245
+ rr(292, 208, 516, 468, 32, grad(292, 208, 516, 468, ['#252729', '#111416']), '#bba47c44', 2);
246
+ teeth(265, 490, 68, 26, -motor * 0.62);
247
+ teeth(838, 445, 82, 32, -motor * 0.5);
248
+ teeth(835, 307, 56, 22, motor * 0.73);
249
+ chain(326, 614, 24, 773, 614, motor);
250
+ teeth(326, 614, 34, 16, motor);
251
+ teeth(773, 614, 34, 16, motor);
252
+ circle(x, y + 8, 284, '#101317', '#51473a', 3);
253
+ teeth(x, y, 282, 96, rot);
254
+ ring(x, y, 270, rot);
255
+ lamps(x, y, 253, t);
256
+ sectors(x, y, 236, rot);
257
+ hub(x, y);
258
+ pointer(x, y - 255, t);
259
+ rr(365, 681, 370, 20, 5, metal(550, 690, 200));
260
+ rr(390, 701, 320, 12, 3, '#171b1f');
261
+ }
262
+ function orbit(t) {
263
+ ctx.save();
264
+ ctx.translate(550, 440);
265
+ ctx.scale(1, 0.72);
266
+ for (let j = 0; j < 4; j++) {
267
+ circle(0, 24 - j * 6, 322, metal(0, 0, 330, true), '#18213d', 2);
268
+ }
269
+ ring(0, 0, 322, rot, true);
270
+ lamps(0, 0, 304, t, true);
271
+ circle(0, 0, 291, '#0a1121', '#64b6f866', 2);
272
+ circle(0, 0, 272, '#171e36', '#567cba', 2);
273
+ sectors(0, 0, 257, rot, true);
274
+ circle(0, 0, 155, '#111b2b', '#6ebde766', 1);
275
+ for (let i = 0; i < 6; i++) {
276
+ ctx.save();
277
+ ctx.rotate((i * TAU) / 6 + rot);
278
+ ctx.strokeStyle = '#8dd8ff4d';
279
+ ctx.lineWidth = 2;
280
+ ctx.beginPath();
281
+ ctx.moveTo(0, -90);
282
+ ctx.lineTo(0, -174);
283
+ ctx.stroke();
284
+ ctx.restore();
285
+ }
286
+ hub(0, 0, true);
287
+ const bx = Math.cos(ballAngle) * ballRadius, by = Math.sin(ballAngle) * ballRadius;
288
+ ctx.save();
289
+ ctx.shadowColor = '#bdf6ff';
290
+ ctx.shadowBlur = 24;
291
+ const bg = ctx.createRadialGradient(bx - 3, by - 4, 1, bx, by, 10);
292
+ bg.addColorStop(0, '#fff');
293
+ bg.addColorStop(0.4, '#d9fbff');
294
+ bg.addColorStop(1, '#5685b0');
295
+ circle(bx, by, 9, bg);
296
+ ctx.restore();
297
+ ctx.restore();
298
+ ctx.save();
299
+ ctx.translate(550, 440);
300
+ ctx.scale(1, 0.72);
301
+ ctx.strokeStyle = '#3b9dce44';
302
+ ctx.lineWidth = 1;
303
+ for (let i = 0; i < 1; i++) {
304
+ ctx.beginPath();
305
+ ctx.ellipse(0, 0, 350 + i * 17, 350 + i * 17, 0, 0, TAU);
306
+ ctx.stroke();
307
+ }
308
+ ctx.restore();
309
+ pointer(550, 236, t, true);
310
+ }
311
+ function reelSymbol(i, x, y, size = 45) {
312
+ const p = prizes[((i % prizes.length) + prizes.length) % prizes.length];
313
+ ctx.save();
314
+ ctx.shadowColor = '#fff5ba';
315
+ ctx.shadowBlur = 0;
316
+ text(p.kind === 'gift' ? '✦' : '%', x, y - 9, size, '#d2a64e', 'Arial', 'bold');
317
+ ctx.shadowBlur = 0;
318
+ text(p.label, x, y + 34, 23, '#32233b', 'Arial', 'bold');
319
+ ctx.restore();
320
+ }
321
+ let slots = function (t) {
322
+ teeth(263, 405, 68, 24, motor);
323
+ teeth(830, 420, 75, 26, -motor * 0.92);
324
+ chain(260, 590, 25, 830, 590, motor);
325
+ rr(298, 151, 504, 535, 48, '#0b080d', '#7b5c34', 5);
326
+ rr(308, 138, 484, 535, 40, grad(310, 140, 480, 530, ['#d6c29b', '#887154', '#4c4036', '#c5ae87']), '#150b0d', 3);
327
+ rr(324, 153, 452, 503, 30, grad(330, 150, 450, 490, ['#533a42', '#241f25', '#382831']), '#e4b76e', 2);
328
+ rr(340, 170, 420, 114, 20, grad(0, 170, 0, 120, ['#130e15', '#44142c', '#160f19']), '#b58847', 3);
329
+ text('GRAND', 550, 213, 42, '#e6d6b9', 'NavigatorTitle, Arial', '600');
330
+ text(labels.nextGame, 550, 255, 10, '#cba86f', 'Arial');
331
+ for (let i = 0; i < 16; i++) {
332
+ const px = 351 + i * 26, lit = busy && (i + Math.floor(t * 9)) % 4 !== 0;
333
+ ctx.save();
334
+ ctx.shadowColor = '#ffc879';
335
+ ctx.shadowBlur = lit ? 7 : 0;
336
+ circle(px, 162, 3, lit ? '#e6d5b0' : '#75624e');
337
+ circle(px, 643, 3, lit ? '#e6d5b0' : '#75624e');
338
+ ctx.restore();
339
+ }
340
+ rr(336, 304, 428, 230, 14, '#0b0910', '#e8c482', 4);
341
+ for (let k = 0; k < 3; k++) {
342
+ const rx = 345 + k * 137;
343
+ ctx.save();
344
+ ctx.beginPath();
345
+ ctx.roundRect(rx, 315, 130, 207, 8);
346
+ ctx.clip();
347
+ ctx.fillStyle = grad(0, 315, 0, 207, [
348
+ '#5f5366',
349
+ '#ddd4ca',
350
+ '#fffbeb',
351
+ '#dcd0bd',
352
+ '#55405d',
353
+ ]);
354
+ ctx.fillRect(rx, 315, 130, 207);
355
+ const pos = reels[k], whole = Math.floor(pos), frac = pos - whole;
356
+ for (let j = -2; j <= 2; j++)
357
+ reelSymbol(whole + j, rx + 65, 415 + j * 108 - frac * 108, 44);
358
+ ctx.fillStyle = grad(0, 315, 0, 207, [
359
+ '#1b102bc9',
360
+ '#1b102b00',
361
+ '#ffffff00',
362
+ '#1b102bc9',
363
+ ]);
364
+ ctx.fillRect(rx, 315, 130, 207);
365
+ ctx.restore();
366
+ rr(rx, 315, 130, 207, 8, '#0000', '#fff0c744', 1);
367
+ }
368
+ ctx.save();
369
+ ctx.shadowColor = '#ffd491';
370
+ ctx.shadowBlur = 10;
371
+ ctx.strokeStyle = '#ffe2a2';
372
+ ctx.lineWidth = 1.2;
373
+ ctx.beginPath();
374
+ ctx.moveTo(331, 419);
375
+ ctx.lineTo(769, 419);
376
+ ctx.stroke();
377
+ ctx.restore();
378
+ rr(376, 557, 348, 58, 8, grad(0, 550, 0, 65, ['#b68745', '#f6dba3', '#8e602e']), '#24130f', 2);
379
+ rr(389, 568, 322, 34, 5, '#22101b');
380
+ text(labels.collection, 550, 586, 10, '#d8c8ab', 'Arial');
381
+ const leverY = busy ? 270 + Math.sin(Math.min(elapsed / 0.7, 1) * Math.PI) * 100 : 270;
382
+ ctx.strokeStyle = metal(820, 380, 60);
383
+ ctx.lineWidth = 13;
384
+ ctx.lineCap = 'round';
385
+ ctx.beginPath();
386
+ ctx.moveTo(794, 487);
387
+ ctx.lineTo(845, 487);
388
+ ctx.lineTo(845, leverY);
389
+ ctx.stroke();
390
+ circle(845, leverY, 25, grad(820, leverY - 25, 35, 50, ['#ffd7b2', '#c23e50', '#4b0d28']), '#b48c58', 2);
391
+ circle(837, leverY - 9, 7, '#ffd5bd77');
392
+ rr(340, 670, 420, 30, 4, metal(550, 680, 220), '#27180e', 2);
393
+ for (let i = 0; i < 4; i++) {
394
+ screw(336 + i * 143, 295);
395
+ screw(336 + i * 143, 632);
396
+ }
397
+ };
398
+ function star(x, y, r, c, angle = 0) {
399
+ ctx.save();
400
+ ctx.translate(x, y);
401
+ ctx.rotate(angle);
402
+ ctx.beginPath();
403
+ for (let i = 0; i < 10; i++) {
404
+ const a = (i * Math.PI) / 5 - Math.PI / 2, rad = i % 2 ? r * 0.46 : r;
405
+ ctx.lineTo(Math.cos(a) * rad, Math.sin(a) * rad);
406
+ }
407
+ ctx.closePath();
408
+ ctx.fillStyle = c;
409
+ ctx.fill();
410
+ ctx.strokeStyle = '#ffffff70';
411
+ ctx.lineWidth = 2;
412
+ ctx.stroke();
413
+ ctx.restore();
414
+ }
415
+ function line(points, c, width = 4, blur = 0) {
416
+ ctx.save();
417
+ ctx.strokeStyle = c;
418
+ ctx.lineWidth = width;
419
+ ctx.lineJoin = 'round';
420
+ ctx.lineCap = 'round';
421
+ ctx.shadowColor = c;
422
+ ctx.shadowBlur = blur;
423
+ ctx.beginPath();
424
+ points.forEach(([x, y], i) => {
425
+ if (i)
426
+ ctx.lineTo(x, y);
427
+ else
428
+ ctx.moveTo(x, y);
429
+ });
430
+ ctx.stroke();
431
+ ctx.restore();
432
+ }
433
+ function monster(x, y, r, c, t, friendly = true, awake = true) {
434
+ ctx.save();
435
+ ctx.translate(x, y + (moving ? Math.sin(t * 1.2 + x) * 2 : 0));
436
+ ctx.shadowColor = '#0005';
437
+ ctx.shadowBlur = 10;
438
+ const g = ctx.createRadialGradient(-r * 0.3, -r * 0.4, 2, 0, 0, r);
439
+ g.addColorStop(0, c);
440
+ g.addColorStop(1, friendly ? '#231740' : '#101614');
441
+ circle(0, 0, r, g, `${c}77`, 2);
442
+ ctx.shadowBlur = 0;
443
+ for (const side of [-1, 1]) {
444
+ ctx.beginPath();
445
+ ctx.moveTo(side * r * 0.7, -r * 0.65);
446
+ ctx.quadraticCurveTo(side * r, -r * 1.5, side * r * 0.25, -r * 0.85);
447
+ ctx.fillStyle = friendly ? '#fbe0b9' : '#acb4a1';
448
+ ctx.fill();
449
+ circle(side * r * 0.33, -r * 0.12, r * 0.22, awake ? '#f9f3d4' : '#332949');
450
+ if (awake) {
451
+ circle(side * r * 0.33 + Math.sin(t) * 3, -r * 0.1, r * 0.09, '#12101a');
452
+ circle(side * r * 0.31, -r * 0.16, r * 0.025, '#fff');
453
+ }
454
+ }
455
+ ctx.beginPath();
456
+ ctx.ellipse(0, r * 0.35, r * 0.4, r * 0.2, 0, 0, Math.PI);
457
+ ctx.fillStyle = '#130d20';
458
+ ctx.fill();
459
+ for (const i of [-1, 1]) {
460
+ ctx.beginPath();
461
+ ctx.moveTo(i * r * 0.23, r * 0.34);
462
+ ctx.lineTo(i * r * 0.09, r * 0.34);
463
+ ctx.lineTo(i * r * 0.16, r * 0.53);
464
+ ctx.fillStyle = '#fff1d3';
465
+ ctx.fill();
466
+ }
467
+ ctx.restore();
468
+ }
469
+ function haunt(t) {
470
+ for (let i = 0; i < 5; i++) {
471
+ const x = 310 + i * 120;
472
+ line([
473
+ [x, 690],
474
+ [x + Math.sin(i) * 20, 160],
475
+ ], '#30332b', 2);
476
+ }
477
+ guardian(t);
478
+ for (let i = 0; i < 4; i++) {
479
+ const x = 290 + i * 170;
480
+ rr(x, 570, 12, 65, 3, '#c4af80');
481
+ ctx.save();
482
+ ctx.shadowColor = '#ed9753';
483
+ ctx.shadowBlur = 14;
484
+ circle(x + 6, 559, 5 + Math.sin(t * 4 + i) * 1.5, '#ffe7a0');
485
+ ctx.restore();
486
+ }
487
+ ctx.save();
488
+ ctx.translate(550, 470);
489
+ ctx.scale(0.79, 0.79);
490
+ const locked = !seals.every(Boolean), hr = locked ? Math.sin(t * 3) * 0.035 : rot;
491
+ ring(0, 0, 240, hr);
492
+ sectors(0, 0, 214, hr);
493
+ hub(0, 0);
494
+ pointer(0, -232, t);
495
+ ctx.restore();
496
+ }
497
+ function monsters(t) {
498
+ rr(240, 145, 620, 540, 95, grad(240, 140, 620, 550, ['#665675', '#3d354c', '#292735']), '#ac98b0', 2);
499
+ for (let i = 0; i < 3; i++) {
500
+ const x = 350 + i * 200;
501
+ rr(x - 72, 173, 144, 170, 68, '#242531', '#b5a5a2', 2);
502
+ ctx.save();
503
+ ctx.translate(x, 258);
504
+ const age = (performance.now() - guestTime[i]) / 1000, turn = guests[i] && age < 1.3
505
+ ? (1 - Math.pow((1 - age / 1.3), 3)) * TAU * (i % 2 ? -1 : 1)
506
+ : 0;
507
+ ctx.rotate(turn);
508
+ monster(0, 0, 55, ['#b1df82', '#efb8dc', '#8bdcde'][i], t, true, guests[i]);
509
+ if (guests[i]) {
510
+ ctx.strokeStyle = '#f9dfeb';
511
+ ctx.lineWidth = 3;
512
+ ctx.beginPath();
513
+ ctx.arc(0, 15, 19, 0, Math.PI);
514
+ ctx.stroke();
515
+ circle(-30, 14, 7, '#f497b988');
516
+ circle(30, 14, 7, '#f497b988');
517
+ }
518
+ ctx.restore();
519
+ }
520
+ ctx.save();
521
+ ctx.translate(550, 495);
522
+ ctx.scale(0.65, 0.65);
523
+ ring(0, 0, 238, rot);
524
+ sectors(0, 0, 211, rot, true);
525
+ hub(0, 0, true);
526
+ lamps(0, 0, 222, t, true);
527
+ pointer(0, -227, t, true);
528
+ ctx.restore();
529
+ }
530
+ function cipher(t) {
531
+ rr(220, 145, 660, 530, 38, grad(220, 145, 650, 530, ['#162b39', '#0b111d', '#203149']), '#57798c', 3);
532
+ for (let i = 0; i < 4; i++) {
533
+ const y = 220 + i * 100;
534
+ line([
535
+ [235, y],
536
+ [290, y],
537
+ [320, y + 20],
538
+ ], '#3f6985', 1);
539
+ line([
540
+ [865, y],
541
+ [815, y],
542
+ [785, y + 20],
543
+ ], '#3f6985', 1);
544
+ }
545
+ ctx.save();
546
+ ctx.translate(550, 383);
547
+ for (let i = 0; i < 2; i++) {
548
+ ctx.save();
549
+ ctx.rotate((i % 2 ? -1 : 1) * (busy ? rot : t * 0.06) + i);
550
+ ctx.strokeStyle = ['#8bbdc5', '#ad9fc9', '#c6b28c'][i];
551
+ ctx.lineWidth = 4;
552
+ ctx.shadowColor = ctx.strokeStyle;
553
+ ctx.shadowBlur = busy ? 10 : 2;
554
+ ctx.beginPath();
555
+ ctx.arc(0, 0, 120 + i * 26, 0, Math.PI * 1.62);
556
+ ctx.stroke();
557
+ ctx.restore();
558
+ }
559
+ hub(0, 0, true);
560
+ ctx.restore();
561
+ for (let i = 0; i < 3; i++) {
562
+ const x = 410 + i * 140;
563
+ line([
564
+ [x - 63, 582],
565
+ [x + 63, 582],
566
+ ], '#59a7b8', 2);
567
+ rr(x - 42, 540, 84, 84, 12, '#121f30', '#466681', 2);
568
+ ctx.save();
569
+ ctx.translate(x, 582);
570
+ ctx.rotate((nodes[i] * Math.PI) / 2);
571
+ line([
572
+ [-31, 0],
573
+ [31, 0],
574
+ ], '#91f4e4', 7, 15);
575
+ circle(-31, 0, 4, '#fff');
576
+ circle(31, 0, 4, '#fff');
577
+ ctx.restore();
578
+ }
579
+ }
580
+ function guardian(t) {
581
+ ctx.save();
582
+ ctx.translate(550, 250);
583
+ const g = ctx.createRadialGradient(0, -40, 5, 0, 0, 190);
584
+ g.addColorStop(0, '#48514b');
585
+ g.addColorStop(1, '#0b100d');
586
+ ctx.beginPath();
587
+ ctx.moveTo(-170, 170);
588
+ ctx.bezierCurveTo(-180, 50, -120, -160, 0, -165);
589
+ ctx.bezierCurveTo(120, -160, 180, 50, 170, 170);
590
+ ctx.closePath();
591
+ ctx.fillStyle = g;
592
+ ctx.fill();
593
+ for (const side of [-1, 1]) {
594
+ ctx.beginPath();
595
+ ctx.moveTo(side * 88, -80);
596
+ ctx.bezierCurveTo(side * 210, -130, side * 152, -215, side * 150, -200);
597
+ ctx.bezierCurveTo(side * 130, -145, side * 115, -135, side * 55, -120);
598
+ ctx.fillStyle = '#737364';
599
+ ctx.fill();
600
+ ctx.save();
601
+ ctx.shadowColor = '#e3a55d';
602
+ ctx.shadowBlur = 24;
603
+ ctx.beginPath();
604
+ ctx.moveTo(side * 27, -22);
605
+ ctx.lineTo(side * 104, -46);
606
+ ctx.lineTo(side * 82, -7);
607
+ ctx.closePath();
608
+ ctx.fillStyle = '#ffe2a0';
609
+ ctx.fill();
610
+ ctx.restore();
611
+ for (let j = 0; j < 4; j++) {
612
+ ctx.beginPath();
613
+ ctx.moveTo(side * (150 + j * 14), 100 + j * 9);
614
+ ctx.quadraticCurveTo(side * (230 + j * 8), 220, side * (186 + j * 13), 300);
615
+ ctx.strokeStyle = '#646b56';
616
+ ctx.lineWidth = 11 - j;
617
+ ctx.stroke();
618
+ }
619
+ }
620
+ ctx.beginPath();
621
+ ctx.moveTo(-50, 34);
622
+ ctx.lineTo(-22, 55);
623
+ ctx.lineTo(0, 40);
624
+ ctx.lineTo(23, 55);
625
+ ctx.lineTo(50, 34);
626
+ ctx.strokeStyle = '#0a0e0a';
627
+ ctx.lineWidth = 9;
628
+ ctx.stroke();
629
+ ctx.restore();
630
+ }
631
+ let detailsLayer = (_t) => { };
632
+ function alchemy(t) {
633
+ rr(245, 112, 610, 580, 38, grad(245, 112, 610, 580, ['#3e3230', '#17181c', '#332526']), '#a8885d', 3);
634
+ for (const side of [-1, 1]) {
635
+ const x = 550 + side * 245;
636
+ line([
637
+ [x, 590],
638
+ [x, 225],
639
+ [550 + side * 130, 225],
640
+ ], '#967750', 18);
641
+ line([
642
+ [x - 3, 585],
643
+ [x - 3, 228],
644
+ [550 + side * 130, 228],
645
+ ], '#dec797', 3);
646
+ teeth(x, 580, 37, 16, side * motor);
647
+ }
648
+ for (let i = 0; i < 3; i++) {
649
+ const x = 400 + i * 150;
650
+ rr(x - 37, 260, 74, 200, 28, '#182329', '#b6c7be', 3);
651
+ const level = busy ? 60 + Math.sin(t * 2 + i) * 35 : 70;
652
+ rr(x - 30, 460 - level - 10, 60, level, 20, `${['#95d7ac', '#d8abf5', '#f3bc6b'][i]}99`);
653
+ for (let j = 0; j < 8; j++) {
654
+ const by = 450 - ((t * 35 + j * 17) % Math.max(25, level));
655
+ circle(x + Math.sin(j * 3 + t) * 18, by, 2 + (j % 3), '#ffffff90');
656
+ }
657
+ rr(x - 20, 244, 40, 20, 4, metal(x, 250, 25));
658
+ }
659
+ ctx.save();
660
+ ctx.translate(550, 535);
661
+ ctx.scale(0.45, 0.45);
662
+ ring(0, 0, 180, rot);
663
+ sectors(0, 0, 155, rot);
664
+ hub(0, 0);
665
+ pointer(0, -170, t);
666
+ ctx.restore();
667
+ }
668
+ function ocean(t) {
669
+ circle(550, 410, 300, metal(550, 410, 310, true), '#142839', 6);
670
+ circle(550, 410, 275, grad(300, 150, 500, 520, ['#193747', '#05202b', '#061119']), '#83b5bc', 3);
671
+ bolts(550, 410, 287, 8);
672
+ ctx.save();
673
+ ctx.beginPath();
674
+ ctx.arc(550, 410, 269, 0, TAU);
675
+ ctx.clip();
676
+ for (let i = 0; i < 10; i++) {
677
+ const x = 300 + ((i * 83) % 500), y = 675 - ((t * (12 + (i % 7)) + i * 31) % 540);
678
+ circle(x + Math.sin(t + i) * 10, y, 2 + (i % 5), '#0000', '#7bb5be55', 1);
679
+ }
680
+ for (let i = 0; i < 3; i++) {
681
+ ctx.beginPath();
682
+ ctx.ellipse(550, 410, 60 + i * 48, 60 + i * 48, 0, 0, TAU);
683
+ ctx.strokeStyle = '#75f3df22';
684
+ ctx.lineWidth = 1;
685
+ ctx.stroke();
686
+ }
687
+ const a = t * (busy ? 2 : 0.4);
688
+ line([
689
+ [550, 410],
690
+ [550 + Math.cos(a) * 265, 410 + Math.sin(a) * 265],
691
+ ], busy ? '#a1dcd2' : '#7cb3ad44', 2, busy ? 8 : 0);
692
+ ctx.translate(550, 410);
693
+ ctx.scale(0.65, 0.65);
694
+ sectors(0, 0, 245, rot, true);
695
+ ring(0, 0, 260, rot, true);
696
+ sectors(0, 0, 235, rot, true);
697
+ hub(0, 0, true);
698
+ pointer(0, -245, t, true);
699
+ ctx.restore();
700
+ }
701
+ const sealTime = [0, 0, 0], guestTime = [0, 0, 0], nodeTime = [0, 0, 0];
702
+ function releaseSeal(i) {
703
+ if (seals[i] || busy)
704
+ return;
705
+ sealTime[i] = performance.now();
706
+ seals[i] = true;
707
+ }
708
+ function wakeGuest(i) {
709
+ if (guests[i] || busy)
710
+ return;
711
+ guestTime[i] = performance.now();
712
+ guests[i] = true;
713
+ }
714
+ function turnNode(i) {
715
+ if (busy)
716
+ return;
717
+ nodes[i] = (nodes[i] + 1) % 4;
718
+ nodeTime[i] = performance.now();
719
+ }
720
+ const oldSlots = slots;
721
+ slots = function (t) {
722
+ ctx.save();
723
+ ctx.translate(550, 410);
724
+ ctx.scale(1.12, 1.12);
725
+ ctx.translate(-550, -410);
726
+ oldSlots(t);
727
+ ctx.restore();
728
+ };
729
+ const originalDetails = detailsLayer;
730
+ detailsLayer = function (t) {
731
+ originalDetails(t);
732
+ ctx.save();
733
+ if (theme === 'haunt') {
734
+ for (let k = 0; k < 3; k++) {
735
+ const age = seals[k] ? (performance.now() - sealTime[k]) / 1000 : 0;
736
+ if (age > 1.4)
737
+ continue;
738
+ const progress = moving ? Math.min(age / 1.4, 1) : seals[k] ? 1 : 0;
739
+ for (let i = 0; i < 23; i++) {
740
+ let u = i / 22, side = i < 11 ? -1 : 1, x = 340 + u * 420, y = 345 + k * 100 + Math.sin(u * Math.PI) * 25;
741
+ x += side * progress * 400;
742
+ y += progress * progress * 350;
743
+ ctx.save();
744
+ ctx.globalAlpha = 1 - progress;
745
+ ctx.translate(x, y);
746
+ ctx.rotate((k % 2 ? -0.3 : 0.3) + side * progress * 3);
747
+ ctx.beginPath();
748
+ ctx.ellipse(0, 0, 14, 7, 0, 0, TAU);
749
+ ctx.strokeStyle = i % 2 ? '#d7c7a5' : '#7c857d';
750
+ ctx.lineWidth = 5;
751
+ ctx.shadowColor = '#000';
752
+ ctx.shadowBlur = 5;
753
+ ctx.stroke();
754
+ ctx.restore();
755
+ }
756
+ if (!seals[k]) {
757
+ circle(550, 360 + k * 100, 25, metal(550, 360, 28), '#422a21', 3);
758
+ circle(550, 360 + k * 100, 18, '#5e1727', '#bc6158', 2);
759
+ text(['I', 'II', 'III'][k], 550, 360 + k * 100, 16, '#ffdab1');
760
+ }
761
+ else {
762
+ ctx.save();
763
+ ctx.globalAlpha = 1 - progress;
764
+ star(550 + (k - 1) * progress * 350, 360 + k * 100 - progress * 220, 25, '#b75759', progress * 9);
765
+ ctx.restore();
766
+ }
767
+ }
768
+ }
769
+ if (theme === 'orbit') {
770
+ for (let k = 0; k < 2; k++) {
771
+ const a = t * (busy ? 1.8 : 0.12) * (k % 2 ? -1 : 1) + k * 2.1, x = 550 + Math.cos(a) * (370 + k * 25), y = 420 + Math.sin(a) * (205 + k * 28);
772
+ ctx.save();
773
+ ctx.translate(x, y);
774
+ ctx.rotate(Math.atan2(Math.cos(a) * 0.6, -Math.sin(a)) + (k % 2 ? Math.PI : 0));
775
+ ctx.shadowColor = '#68dcff';
776
+ ctx.shadowBlur = busy ? 8 : 0;
777
+ line([
778
+ [0, 18],
779
+ [0, 45 + (busy ? 25 : 0)],
780
+ ], '#90cdd5', 3, busy ? 10 : 1);
781
+ ctx.beginPath();
782
+ ctx.moveTo(0, -25);
783
+ ctx.lineTo(17, 15);
784
+ ctx.lineTo(5, 10);
785
+ ctx.lineTo(0, 18);
786
+ ctx.lineTo(-5, 10);
787
+ ctx.lineTo(-17, 15);
788
+ ctx.closePath();
789
+ ctx.fillStyle = metal(0, 0, 25, true);
790
+ ctx.fill();
791
+ circle(0, -5, 4, '#c8f8ff');
792
+ ctx.restore();
793
+ }
794
+ }
795
+ if (theme === 'cipher') {
796
+ const connected = nodes.every((n) => n % 2 === 0);
797
+ for (let i = 0; i < 3; i++) {
798
+ const age = (performance.now() - nodeTime[i]) / 1000, x = 410 + i * 140;
799
+ const c = nodes[i] % 2 === 0 ? '#82ffe0' : '#d590ff';
800
+ line([
801
+ [x, 537],
802
+ [x, 510],
803
+ [550, 510],
804
+ [550, 480],
805
+ ], `${c}66`, 2, 8);
806
+ if (age < 1.2) {
807
+ ctx.save();
808
+ ctx.globalAlpha = 1 - age / 1.2;
809
+ circle(x, 582, 45 + age * 70, '#0000', c, 3);
810
+ ctx.restore();
811
+ }
812
+ if (nodes[i] % 2 === 0) {
813
+ const u = (t * 0.5 + i * 0.33) % 1;
814
+ circle(x + (550 - x) * u, 510, 4, '#efffff', c, 3);
815
+ }
816
+ }
817
+ for (let k = 0; k < 1; k++) {
818
+ const a = t * (connected ? 0.6 : 0.06) * (k % 2 ? -1 : 1), r = 192 + k * 17;
819
+ ctx.beginPath();
820
+ ctx.arc(550, 383, r, a, a + Math.PI * 0.8);
821
+ ctx.strokeStyle = ['#74f8d9', '#b084ff', '#5ab7ff'][k];
822
+ ctx.lineWidth = 2;
823
+ ctx.shadowColor = ctx.strokeStyle;
824
+ ctx.shadowBlur = busy ? 8 : 0;
825
+ ctx.stroke();
826
+ }
827
+ }
828
+ if (theme === 'alchemy') {
829
+ for (const side of [-1, 1]) {
830
+ const x = 550 + side * 350;
831
+ line([
832
+ [x, 200],
833
+ [x, 590],
834
+ [550 + side * 270, 620],
835
+ ], '#6f5741', 7);
836
+ for (let j = 0; j < 2; j++) {
837
+ const y = 310 + j * 176;
838
+ teeth(x, y, 26, 14, (j % 2 ? -1 : 1) * motor);
839
+ line([
840
+ [x - 17, y + 25],
841
+ [x + 15, y + 65],
842
+ ], '#b49a6e', 2);
843
+ }
844
+ const fx = 550 + side * 325;
845
+ ctx.beginPath();
846
+ ctx.moveTo(fx - 10, 160);
847
+ ctx.lineTo(fx - 10, 188);
848
+ ctx.bezierCurveTo(fx - 65, 236, fx - 45, 269, fx, 269);
849
+ ctx.bezierCurveTo(fx + 45, 269, fx + 65, 236, fx + 10, 188);
850
+ ctx.lineTo(fx + 10, 160);
851
+ ctx.closePath();
852
+ ctx.fillStyle = side < 0 ? '#72e3b244' : '#db9af544';
853
+ ctx.fill();
854
+ ctx.strokeStyle = '#b2d1c0';
855
+ ctx.lineWidth = 2;
856
+ ctx.stroke();
857
+ rr(fx - 14, 151, 28, 12, 3, metal(fx, 157, 20));
858
+ for (let j = 0; j < 7; j++)
859
+ circle(fx + Math.sin(t + j) * 22, 250 - ((t * 20 + j * 13) % 50), 2, '#dffff0');
860
+ line([
861
+ [fx, 150],
862
+ [fx, 118],
863
+ [550 + side * 100, 118],
864
+ [550 + side * 100, 220],
865
+ ], '#c19c60', 5);
866
+ }
867
+ }
868
+ if (theme === 'ocean') {
869
+ // Submersible, illuminated coral and floating capsule.
870
+ for (const side of [-1, 1]) {
871
+ const x = 550 + side * 325;
872
+ for (let j = 0; j < 3; j++) {
873
+ const y = 650 - j * 24;
874
+ line([
875
+ [x, 685],
876
+ [x + side * j * 9, y],
877
+ [x + side * (j * 9 + 16), y - 16],
878
+ ], j % 2 ? '#ab81bd' : '#58bcae', 4, 3);
879
+ }
880
+ }
881
+ const sx = 550 + Math.cos(t * 0.22) * 330, sy = 230 + Math.sin(t * 0.4) * 30;
882
+ ctx.save();
883
+ ctx.translate(sx, sy);
884
+ ctx.fillStyle = '#8ee8f511';
885
+ ctx.beginPath();
886
+ ctx.moveTo(15, 15);
887
+ ctx.lineTo(100, 200);
888
+ ctx.lineTo(-50, 200);
889
+ ctx.closePath();
890
+ ctx.fill();
891
+ rr(-33, -17, 66, 34, 17, metal(0, 0, 40, true), '#a8c2ca', 1);
892
+ circle(6, 0, 11, '#103747', '#8ffff4', 3);
893
+ rr(-10, -28, 21, 11, 4, '#779fa9');
894
+ teeth(-48, 0, 18, 8, t * 4, true);
895
+ ctx.restore();
896
+ }
897
+ ctx.restore();
898
+ };
899
+ const gamePalettes = {
900
+ vault: ['#344c48', '#4b3e35'],
901
+ slots: ['#63414a', '#4d3e49'],
902
+ orbit: ['#2f5269', '#363e5b'],
903
+ haunt: ['#3c4c43', '#493c41'],
904
+ monsters: ['#638b82', '#9b768a', '#81759d'],
905
+ alchemy: ['#356d60', '#6a5277', '#8c6b42'],
906
+ ocean: ['#24585f', '#344c64'],
907
+ cipher: ['#2e565d', '#49425f'],
908
+ };
909
+ const audioProfiles = {
910
+ vault: [196, 'triangle', 0.08, 0.55],
911
+ slots: [330, 'triangle', 0.045, 1.5],
912
+ orbit: [740, 'sine', 0.12, 0.4],
913
+ haunt: [110, 'sine', 0.22, 0.5],
914
+ monsters: [523, 'sine', 0.09, 1.45],
915
+ alchemy: [392, 'sine', 0.15, 2.1],
916
+ ocean: [262, 'sine', 0.27, 0.6],
917
+ cipher: [660, 'square', 0.05, 0.35],
918
+ };
919
+ let lastSound = 0;
920
+ function ensureAudio() {
921
+ try {
922
+ audio = audio || new AudioContext();
923
+ if (audio.state === 'suspended')
924
+ audio.resume().catch(() => {
925
+ sound = false;
926
+ });
927
+ return true;
928
+ }
929
+ catch (_a) {
930
+ sound = false;
931
+ return false;
932
+ }
933
+ }
934
+ function synth(freq, type, duration, sweep, offset = 0, volume = 0.022) {
935
+ if (!sound)
936
+ return;
937
+ if (!ensureAudio())
938
+ return;
939
+ const at = audio.currentTime + offset, o = audio.createOscillator(), g = audio.createGain();
940
+ o.type = type;
941
+ o.frequency.setValueAtTime(freq, at);
942
+ o.frequency.exponentialRampToValueAtTime(Math.max(25, freq * sweep), at + duration);
943
+ g.gain.setValueAtTime(0.0001, at);
944
+ g.gain.exponentialRampToValueAtTime(volume, at + 0.008);
945
+ g.gain.exponentialRampToValueAtTime(0.0001, at + duration);
946
+ o.connect(g);
947
+ g.connect(audio.destination);
948
+ o.start(at);
949
+ o.stop(at + duration + 0.02);
950
+ o.onended = () => {
951
+ o.disconnect();
952
+ g.disconnect();
953
+ };
954
+ }
955
+ function themeAudio(event) {
956
+ const [f, type, d, sweep] = audioProfiles[theme], now = performance.now();
957
+ if (event === 'tick') {
958
+ if (now - lastSound < 90)
959
+ return;
960
+ lastSound = now;
961
+ synth(f, type, d, sweep, 0, 0.012);
962
+ return;
963
+ }
964
+ if (event === 'win') {
965
+ const melody = {
966
+ arcana: [1, 1.5, 2, 2.5, 3],
967
+ vault: [1, 1.25, 1.5, 2],
968
+ slots: [1, 1.5, 2, 1.5, 2],
969
+ orbit: [1, 1.5, 2, 3],
970
+ haunt: [1, 1.2, 1.5, 2],
971
+ monsters: [1, 1.25, 1.5, 1.25, 2],
972
+ alchemy: [1, 1.5, 1.25, 2.5],
973
+ ocean: [1, 2, 1.5, 3],
974
+ cipher: [1, 1.5, 2, 3],
975
+ }[theme];
976
+ melody.forEach((v, i) => {
977
+ synth(f * v, type, theme === 'haunt' || theme === 'ocean' ? 0.5 : 0.23, 1, i * (theme === 'haunt' ? 0.19 : 0.11), 0.022);
978
+ });
979
+ return;
980
+ }
981
+ synth(f, type, d, sweep, 0, 0.024);
982
+ }
983
+ function enamel(x, y, w, h, c) {
984
+ rr(x, y, w, h, Math.min(12, h / 2), grad(x, y, w, h, [c, '#131a28']), `${c}aa`, 1);
985
+ line([
986
+ [x + 8, y + 3],
987
+ [x + w - 8, y + 3],
988
+ ], '#ffffff26', 1);
989
+ }
990
+ function lozenge(x, y, w, h, c) {
991
+ ctx.save();
992
+ ctx.translate(x, y);
993
+ ctx.beginPath();
994
+ ctx.moveTo(0, -h);
995
+ ctx.lineTo(w, 0);
996
+ ctx.lineTo(0, h);
997
+ ctx.lineTo(-w, 0);
998
+ ctx.closePath();
999
+ ctx.fillStyle = c;
1000
+ ctx.fill();
1001
+ ctx.strokeStyle = '#fff3';
1002
+ ctx.lineWidth = 1;
1003
+ ctx.stroke();
1004
+ ctx.restore();
1005
+ }
1006
+ function ornament(t) {
1007
+ const colors = gamePalettes[theme];
1008
+ if (theme === 'monsters') {
1009
+ // Three unmistakable toy props: shield, comet wand, foam hammer.
1010
+ for (let i = 0; i < 3; i++) {
1011
+ const x = 350 + i * 200, y = 345;
1012
+ ctx.save();
1013
+ ctx.translate(x, y);
1014
+ ctx.rotate(moving ? Math.sin(t * 2 + i) * 0.08 : 0);
1015
+ if (i === 0) {
1016
+ ctx.beginPath();
1017
+ ctx.moveTo(-31, -25);
1018
+ ctx.lineTo(31, -25);
1019
+ ctx.lineTo(25, 12);
1020
+ ctx.quadraticCurveTo(0, 40, -25, 12);
1021
+ ctx.closePath();
1022
+ ctx.fillStyle = '#efad55';
1023
+ ctx.fill();
1024
+ ctx.strokeStyle = '#ffe7b8';
1025
+ ctx.lineWidth = 3;
1026
+ ctx.stroke();
1027
+ lozenge(0, 0, 13, 17, '#369a89');
1028
+ }
1029
+ if (i === 1) {
1030
+ line([
1031
+ [-25, 23],
1032
+ [24, -26],
1033
+ ], '#ffe2ad', 8);
1034
+ star(24, -26, 22, '#f2bb58', t * 0.3);
1035
+ circle(24, -26, 7, '#b273d7');
1036
+ line([
1037
+ [27, -7],
1038
+ [38, 9],
1039
+ [31, 22],
1040
+ ], '#f399bc', 3);
1041
+ }
1042
+ if (i === 2) {
1043
+ line([
1044
+ [0, 26],
1045
+ [0, -24],
1046
+ ], '#e5b582', 10);
1047
+ enamel(-34, -40, 68, 28, '#36b7b8');
1048
+ rr(-40, -44, 13, 36, 5, '#fab389');
1049
+ rr(27, -44, 13, 36, 5, '#fab389');
1050
+ }
1051
+ ctx.restore();
1052
+ }
1053
+ }
1054
+ if (theme === 'cipher') {
1055
+ // Separate readable prize rails; no text is rotated with the surface.
1056
+ const spots = [];
1057
+ const rows = Math.max(3, Math.ceil(prizes.length / 2));
1058
+ for (const side of [-1, 1])
1059
+ for (let j = 0; j < rows; j++) {
1060
+ const x = 550 + side * 264, y = 373 + (j - (rows - 1) / 2) * Math.min(88, 280 / (rows - 1)), index = (side < 0 ? 0 : rows) + j, p = prizes[index % prizes.length];
1061
+ spots.push([x, y]);
1062
+ ctx.save();
1063
+ ctx.translate(x, y);
1064
+ ctx.transform(1, -side * 0.16, 0, 1, 0, 0);
1065
+ enamel(-49, -23, 98, 46, colors[j % colors.length]);
1066
+ ctx.restore();
1067
+ text(p.label, x, y - 4, 19, '#f3ffff', 'Arial', 'bold');
1068
+ text(p.caption, x, y + 13, 8, '#b3dce8');
1069
+ }
1070
+ if (busy) {
1071
+ const route = spots.length === 6
1072
+ ? [spots[0], spots[4], spots[1], spots[5], spots[2], spots[3]]
1073
+ : spots, phase = (elapsed * 2.5) % route.length, i = Math.floor(phase), u = phase - i, a = route[i], b = route[(i + 1) % route.length];
1074
+ line([a, b], '#61e8de33', 2);
1075
+ const head = [a[0] + (b[0] - a[0]) * u, a[1] + (b[1] - a[1]) * u], tail = [
1076
+ a[0] + (b[0] - a[0]) * Math.max(0, u - 0.18),
1077
+ a[1] + (b[1] - a[1]) * Math.max(0, u - 0.18),
1078
+ ];
1079
+ line([tail, head], '#9bfff2', 4, 18);
1080
+ circle(head[0], head[1], 4, '#fff');
1081
+ circle(a[0], a[1], 29, '#0000', '#cd9bff', 2);
1082
+ }
1083
+ }
1084
+ }
1085
+ const atelierDetails = detailsLayer;
1086
+ detailsLayer = function (t) {
1087
+ atelierDetails(t);
1088
+ ctx.save();
1089
+ ornament(t);
1090
+ ctx.restore();
1091
+ };
1092
+ gamePalettes.arcana = ['#355c59', '#4c425e', '#604450', '#354d63', '#554b68', '#3c615a'];
1093
+ audioProfiles.arcana = [784, 'sine', 0.18, 1.5];
1094
+ function arcGem(x, y, r, tint, angle = 0) {
1095
+ ctx.save();
1096
+ ctx.translate(x, y);
1097
+ ctx.rotate(angle);
1098
+ const pts = [
1099
+ [0, -r],
1100
+ [r * 0.65, -r * 0.35],
1101
+ [r * 0.65, r * 0.4],
1102
+ [0, r],
1103
+ [-r * 0.65, r * 0.4],
1104
+ [-r * 0.65, -r * 0.35],
1105
+ ];
1106
+ for (let i = 0; i < 6; i++) {
1107
+ ctx.beginPath();
1108
+ ctx.moveTo(-r * 0.13, -r * 0.17);
1109
+ ctx.lineTo(...pts[i]);
1110
+ ctx.lineTo(...pts[(i + 1) % 6]);
1111
+ ctx.closePath();
1112
+ ctx.fillStyle = [tint, '#c4d6d1', tint, '#182b30', tint, '#d5cba9'][i];
1113
+ ctx.fill();
1114
+ }
1115
+ ctx.restore();
1116
+ }
1117
+ function arcRing(r, w, colors) {
1118
+ ctx.beginPath();
1119
+ ctx.arc(0, 0, r, 0, TAU);
1120
+ ctx.strokeStyle = grad(-r, -r, r * 2, r * 2, colors);
1121
+ ctx.lineWidth = w;
1122
+ ctx.stroke();
1123
+ }
1124
+ function arcana(t) {
1125
+ ctx.save();
1126
+ const haze = ctx.createRadialGradient(550, 400, 200, 550, 400, 405);
1127
+ haze.addColorStop(0, '#5e506117');
1128
+ haze.addColorStop(1, '#5e506100');
1129
+ ctx.fillStyle = haze;
1130
+ ctx.fillRect(100, 30, 900, 750);
1131
+ // One fine orbital trace gives the case its silhouette.
1132
+ ctx.save();
1133
+ ctx.translate(550, 405);
1134
+ ctx.rotate(-0.27);
1135
+ ctx.beginPath();
1136
+ ctx.ellipse(0, 0, 356, 322, 0, 0, TAU);
1137
+ ctx.strokeStyle = '#b9aa8c2b';
1138
+ ctx.lineWidth = 1;
1139
+ ctx.stroke();
1140
+ ctx.restore();
1141
+ ctx.translate(550, 405);
1142
+ ctx.shadowColor = '#0009';
1143
+ ctx.shadowBlur = 30;
1144
+ ctx.shadowOffsetY = 18;
1145
+ circle(0, 0, 305, '#151819');
1146
+ ctx.shadowBlur = 0;
1147
+ ctx.shadowOffsetY = 0;
1148
+ arcRing(301, 14, ['#d6c6a1', '#9a8562', '#3d3528', '#c7b38c']);
1149
+ arcRing(289, 9, ['#3c4c4d', '#19282b', '#617471']);
1150
+ arcRing(280, 3, ['#b6a07c', '#534837', '#a08e71']);
1151
+ for (let i = 0; i < 60; i++) {
1152
+ ctx.save();
1153
+ ctx.rotate((i * TAU) / 60);
1154
+ line([
1155
+ [0, -296],
1156
+ [0, -(i % 5 === 0 ? 302 : 299)],
1157
+ ], '#e5d3ac55', 1);
1158
+ ctx.restore();
1159
+ }
1160
+ for (let i = 0; i < 18; i++) {
1161
+ ctx.save();
1162
+ ctx.rotate((i * TAU) / 18);
1163
+ const on = busy && (i + Math.floor(t * 15)) % 6 < 2;
1164
+ ctx.shadowBlur = on ? 10 : 0;
1165
+ ctx.shadowColor = '#c9ded6';
1166
+ circle(0, -289, 2.1, on ? '#e0eee1' : '#637774');
1167
+ ctx.restore();
1168
+ }
1169
+ ctx.save();
1170
+ ctx.rotate(rot);
1171
+ const n = prizes.length;
1172
+ for (let i = 0; i < n; i++) {
1173
+ const a = (i * TAU) / n - Math.PI / 2, b = ((i + 1) * TAU) / n - Math.PI / 2;
1174
+ ctx.beginPath();
1175
+ ctx.moveTo(0, 0);
1176
+ ctx.arc(0, 0, 274, a, b);
1177
+ ctx.closePath();
1178
+ const g = ctx.createRadialGradient(-65, -110, 20, 0, 0, 295);
1179
+ g.addColorStop(0, '#19202a');
1180
+ g.addColorStop(0.65, gamePalettes.arcana[i % 6]);
1181
+ g.addColorStop(1, '#171d24');
1182
+ ctx.fillStyle = g;
1183
+ ctx.fill();
1184
+ ctx.strokeStyle = '#baa38077';
1185
+ ctx.lineWidth = 1;
1186
+ ctx.stroke();
1187
+ ctx.save();
1188
+ ctx.rotate(((i + 0.5) * TAU) / n);
1189
+ ctx.beginPath();
1190
+ ctx.arc(0, 0, 258, -Math.PI / 2 - TAU / n / 2 + 0.045, -Math.PI / 2 + TAU / n / 2 - 0.045);
1191
+ ctx.strokeStyle = '#c2b08a44';
1192
+ ctx.lineWidth = 1;
1193
+ ctx.stroke();
1194
+ text(prizes[i].label, 0, -199, 28, '#eee4ce', 'NavigatorTitle, Arial', '700');
1195
+ text(prizes[i].caption, 0, -177, 10, '#d0c6b7', 'Arial', '400');
1196
+ arcGem(0, -129, 5, ['#89b9ad', '#a499ba', '#c29fa7'][i % 3]);
1197
+ ctx.restore();
1198
+ }
1199
+ ctx.restore();
1200
+ // A single moving satellite and a glass core. No ambient sparkle field.
1201
+ ctx.save();
1202
+ ctx.rotate(motor * 0.17);
1203
+ ctx.beginPath();
1204
+ ctx.arc(0, 0, 102, 0, TAU);
1205
+ ctx.strokeStyle = '#a0b3aa66';
1206
+ ctx.lineWidth = 1;
1207
+ ctx.stroke();
1208
+ arcGem(0, -102, 5, '#9ebdb1');
1209
+ ctx.restore();
1210
+ circle(0, 0, 87, metal(0, 0, 87), '#c5b18b', 1);
1211
+ const orb = ctx.createRadialGradient(-26, -34, 3, 0, 0, 80);
1212
+ orb.addColorStop(0, '#8ba7ab');
1213
+ orb.addColorStop(0.3, '#4a6b76');
1214
+ orb.addColorStop(0.7, '#2c394f');
1215
+ orb.addColorStop(1, '#141d29');
1216
+ ctx.shadowColor = '#8aadc5';
1217
+ ctx.shadowBlur = busy ? 20 : 0;
1218
+ circle(0, 0, 79, orb, '#9caaae66', 1);
1219
+ ctx.shadowBlur = 0;
1220
+ if (logo.complete)
1221
+ ctx.drawImage(logo, -27, -29, 54, 54);
1222
+ ctx.beginPath();
1223
+ ctx.ellipse(-23, -41, 24, 7, -0.5, 0, TAU);
1224
+ ctx.fillStyle = '#ffffff15';
1225
+ ctx.fill();
1226
+ // The fixed jewel quietly marks the winning sector.
1227
+ ctx.save();
1228
+ ctx.translate(0, -295);
1229
+ circle(0, 0, 20, metal(0, 0, 20), '#cfbc99', 1);
1230
+ arcGem(0, 0, 13, '#8f9fc0');
1231
+ ctx.restore();
1232
+ if (busy) {
1233
+ ctx.save();
1234
+ ctx.rotate(t * 1.6);
1235
+ ctx.beginPath();
1236
+ ctx.arc(0, 0, 313, 0, 0.6);
1237
+ ctx.strokeStyle = '#bad6cb99';
1238
+ ctx.lineWidth = 2;
1239
+ ctx.shadowColor = '#b0cdbd';
1240
+ ctx.shadowBlur = 12;
1241
+ ctx.stroke();
1242
+ ctx.restore();
1243
+ }
1244
+ ctx.restore();
1245
+ }
1246
+ const arcDetails = detailsLayer;
1247
+ detailsLayer = function (t) {
1248
+ if (theme !== 'arcana')
1249
+ arcDetails(t);
1250
+ };
1251
+ function finish() {
1252
+ rot = target;
1253
+ reels = [selected, selected, selected];
1254
+ busy = false;
1255
+ glow = 1;
1256
+ themeAudio('win');
1257
+ done === null || done === void 0 ? void 0 : done();
1258
+ done = null;
1259
+ }
1260
+ function frame(now) {
1261
+ if (disposed)
1262
+ return;
1263
+ const dt = Math.min((now - previous) / 1000, 0.05);
1264
+ previous = now;
1265
+ if (moving && !document.hidden)
1266
+ motor += dt * (busy ? 5 : 0.28);
1267
+ const t = moving ? now / 1000 : 0;
1268
+ if (busy) {
1269
+ elapsed = (now - start) / 1000;
1270
+ const p = Math.min(elapsed / 6.2, 1), e = 1 - Math.pow((1 - p), 4);
1271
+ rot = from + (target - from) * e;
1272
+ ballAngle = -Math.PI / 2 + (1 - e) * TAU * 7;
1273
+ ballRadius = 238 + 46 * (1 - Math.min(Math.max((p - 0.35) / 0.55, 0), 1));
1274
+ for (let k = 0; k < 3; k++) {
1275
+ const q = Math.min(elapsed / (4.2 + k * 0.8), 1);
1276
+ reels[k] = (1 - Math.pow((1 - q), 4)) * (prizes.length * (9 + k) + selected);
1277
+ }
1278
+ const tick = Math.floor((rot * prizes.length) / TAU);
1279
+ if (tick !== lastTick) {
1280
+ lastTick = tick;
1281
+ themeAudio('tick');
1282
+ }
1283
+ if (p === 1)
1284
+ finish();
1285
+ }
1286
+ else if (moving)
1287
+ glow = Math.max(0, glow - dt * 0.08);
1288
+ ctx.clearRect(0, 0, 1100, 820);
1289
+ atmosphere(t, theme === 'orbit');
1290
+ ({ vault, slots, orbit, haunt, monsters, alchemy, ocean, arcana, cipher })[theme](t);
1291
+ detailsLayer(t);
1292
+ if (glow > 0) {
1293
+ ctx.save();
1294
+ ctx.globalAlpha = glow;
1295
+ for (let i = 0; i < 16; i++) {
1296
+ const a = i * 2.399, rad = 110 + (((1 - glow) * 700 + i * 31) % 430), x = 550 + Math.cos(a) * rad, y = 420 + Math.sin(a) * rad * 0.7;
1297
+ ctx.fillStyle = theme === 'orbit' ? '#b5efff' : '#f8df9f';
1298
+ ctx.translate(x, y);
1299
+ ctx.rotate(a + glow * 3);
1300
+ ctx.fillRect(-2, -5, 4, 10);
1301
+ ctx.setTransform(1, 0, 0, 1, 0, 0);
1302
+ }
1303
+ ctx.restore();
1304
+ }
1305
+ frameId = requestAnimationFrame(frame);
1306
+ }
1307
+ frameId = requestAnimationFrame(frame);
1308
+ return {
1309
+ interact(i) {
1310
+ if (theme === 'haunt')
1311
+ releaseSeal(i);
1312
+ if (theme === 'monsters')
1313
+ wakeGuest(i);
1314
+ if (theme === 'cipher')
1315
+ turnNode(i);
1316
+ themeAudio('tap');
1317
+ return { seals: [...seals], guests: [...guests], nodes: [...nodes] };
1318
+ },
1319
+ setSound(value) {
1320
+ sound = value;
1321
+ if (!sound && audio) {
1322
+ audio.close();
1323
+ audio = null;
1324
+ }
1325
+ },
1326
+ setMotion(value) {
1327
+ moving = value;
1328
+ if (!moving && busy)
1329
+ finish();
1330
+ },
1331
+ unlockAudio() {
1332
+ if (sound)
1333
+ ensureAudio();
1334
+ },
1335
+ spin(index) {
1336
+ if (disposed)
1337
+ return Promise.resolve();
1338
+ selected = index;
1339
+ busy = true;
1340
+ start = performance.now();
1341
+ from = rot;
1342
+ target =
1343
+ (Math.floor(rot / TAU) + 8) * TAU + TAU - ((index + 0.5) * TAU) / prizes.length;
1344
+ glow = 0;
1345
+ lastTick = -1;
1346
+ return new Promise((resolve) => {
1347
+ done = resolve;
1348
+ if (!moving)
1349
+ finish();
1350
+ });
1351
+ },
1352
+ destroy() {
1353
+ disposed = true;
1354
+ cancelAnimationFrame(frameId);
1355
+ if (audio)
1356
+ audio.close();
1357
+ done === null || done === void 0 ? void 0 : done();
1358
+ done = null;
1359
+ },
1360
+ };
1361
+ }