@open-slot-ui/pixi 0.4.2 → 0.5.1

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,3733 @@
1
+ import { Container, Graphics, Text, Circle, Sprite, Rectangle, Ticker, BlurFilter, Texture } from 'pixi.js';
2
+ import { resolvePlacement, displayDigits, valueFitMaxWidth, formatAmount, EventLog, composeMenu, buildBlocks, buttonBlocks } from '@open-slot-ui/core';
3
+
4
+ // src/views/ControlView.ts
5
+ var ControlView = class extends Container {
6
+ constructor(control, ui) {
7
+ super();
8
+ this.control = control;
9
+ this.ui = ui;
10
+ this.control.viewInspect = () => ({ bounds: this.computeRect(), animating: this.animating });
11
+ }
12
+ control;
13
+ ui;
14
+ animating = false;
15
+ disposers = [];
16
+ /** Position + fit-scale from the control's layout spec against the screen. */
17
+ applyLayout(screen) {
18
+ const p = resolvePlacement(this.control.layout, screen);
19
+ this.position.set(p.x, p.y);
20
+ this.scale.set(p.scale);
21
+ this.rotation = p.rotation;
22
+ }
23
+ computeRect() {
24
+ if (this.destroyed) return null;
25
+ const b = this.getBounds();
26
+ return { x: b.minX, y: b.minY, width: b.maxX - b.minX, height: b.maxY - b.minY };
27
+ }
28
+ dispose() {
29
+ for (const d of this.disposers) d();
30
+ this.disposers.length = 0;
31
+ this.control.viewInspect = void 0;
32
+ if (!this.destroyed) this.destroy({ children: true });
33
+ }
34
+ };
35
+ var Tweener = class {
36
+ constructor(ticker) {
37
+ this.ticker = ticker;
38
+ }
39
+ ticker;
40
+ frames = /* @__PURE__ */ new Set();
41
+ loopResolve = null;
42
+ stop() {
43
+ for (const fn of this.frames) this.ticker.remove(fn);
44
+ this.frames.clear();
45
+ if (this.loopResolve) {
46
+ const resolve = this.loopResolve;
47
+ this.loopResolve = null;
48
+ resolve();
49
+ }
50
+ }
51
+ run(target, transition, _theme) {
52
+ this.stop();
53
+ target.scale.set(1);
54
+ target.rotation = 0;
55
+ target.alpha = 1;
56
+ if (!transition) return Promise.resolve();
57
+ return this.exec(target, transition);
58
+ }
59
+ add(fn) {
60
+ this.frames.add(fn);
61
+ this.ticker.add(fn);
62
+ }
63
+ done(fn) {
64
+ this.ticker.remove(fn);
65
+ this.frames.delete(fn);
66
+ }
67
+ exec(target, t) {
68
+ switch (t.kind) {
69
+ case "squish":
70
+ case "pulse":
71
+ return this.scaleYoyo(target, t.scale, t.ms);
72
+ case "fade":
73
+ return this.fadeTo(target, t.alpha, t.ms);
74
+ case "turn":
75
+ return this.turn(target, t.rps);
76
+ case "sequence":
77
+ return this.runSequence(target, t.steps);
78
+ case "parallel":
79
+ return Promise.all(t.steps.map((s) => this.exec(target, s))).then(() => void 0);
80
+ default:
81
+ return Promise.resolve();
82
+ }
83
+ }
84
+ scaleYoyo(target, peak, ms) {
85
+ return new Promise((resolve) => {
86
+ let elapsed = 0;
87
+ const half = Math.max(ms / 2, 1);
88
+ const fn = (ticker) => {
89
+ elapsed += ticker.deltaMS;
90
+ const k = elapsed < half ? elapsed / half : 1 - (elapsed - half) / half;
91
+ const s = 1 + (peak - 1) * Math.min(Math.max(k, 0), 1);
92
+ target.scale.set(s);
93
+ if (elapsed >= ms) {
94
+ target.scale.set(1);
95
+ this.done(fn);
96
+ resolve();
97
+ }
98
+ };
99
+ this.add(fn);
100
+ });
101
+ }
102
+ fadeTo(target, alpha, ms) {
103
+ return new Promise((resolve) => {
104
+ let elapsed = 0;
105
+ const from = target.alpha;
106
+ const dur = Math.max(ms, 1);
107
+ const fn = (ticker) => {
108
+ elapsed += ticker.deltaMS;
109
+ const k = Math.min(elapsed / dur, 1);
110
+ target.alpha = from + (alpha - from) * k;
111
+ if (k >= 1) {
112
+ this.done(fn);
113
+ resolve();
114
+ }
115
+ };
116
+ this.add(fn);
117
+ });
118
+ }
119
+ /** Continuous rotation; the promise resolves when `stop()` is next called. */
120
+ turn(target, rps) {
121
+ return new Promise((resolve) => {
122
+ this.loopResolve = resolve;
123
+ const fn = (ticker) => {
124
+ target.rotation += rps * Math.PI * 2 * (ticker.deltaMS / 1e3);
125
+ };
126
+ this.add(fn);
127
+ });
128
+ }
129
+ async runSequence(target, steps) {
130
+ for (const step of steps) await this.exec(target, step);
131
+ }
132
+ };
133
+ function drawSpin(g, theme, state) {
134
+ g.clear();
135
+ const r = 64;
136
+ const ring = state === "disabled" ? theme.color.disabled : theme.color.accent;
137
+ const glyph = state === "disabled" ? theme.color.textDim : theme.color.text;
138
+ g.circle(0, 0, r).fill({ color: theme.color.surface });
139
+ g.circle(0, 0, r).stroke({ width: 6, color: ring, alpha: 1 });
140
+ const s = 20;
141
+ g.moveTo(-s * 0.5, -s).lineTo(s, 0).lineTo(-s * 0.5, s).closePath().fill({ color: glyph });
142
+ }
143
+ var defaultSpinSkin = (theme) => {
144
+ const g = new Graphics();
145
+ const view = new Container();
146
+ view.addChild(g);
147
+ return {
148
+ view,
149
+ update: (state) => drawSpin(g, theme, state),
150
+ destroy: () => view.destroy({ children: true })
151
+ };
152
+ };
153
+
154
+ // src/util.ts
155
+ function isDesktop() {
156
+ if (typeof window === "undefined") return true;
157
+ const noTouch = !("ontouchstart" in window);
158
+ const noPoints = (navigator.maxTouchPoints ?? 0) === 0;
159
+ return noTouch && noPoints;
160
+ }
161
+ var SpinView = class extends ControlView {
162
+ constructor(spin, ui, ticker, skinOrOpts = defaultSpinSkin) {
163
+ super(spin, ui);
164
+ this.spin = spin;
165
+ this.tween = new Tweener(ticker);
166
+ const opts = typeof skinOrOpts === "function" ? { skin: skinOrOpts } : skinOrOpts;
167
+ const skinFactory = opts.skin ?? defaultSpinSkin;
168
+ this.holdDelayMs = opts.holdDelayMs ?? 260;
169
+ this.skin = skinFactory(ui.theme);
170
+ this.art.addChild(this.skin.view);
171
+ const fsRing = new Graphics().circle(0, 0, 98).fill({ color: 16777215 }).circle(0, 0, 94).stroke({ width: 8, color: 657930 });
172
+ this.fsCount = new Text({ text: "0", style: { fontFamily: ui.theme.type.family, fontSize: 60, fill: 657930, fontWeight: "900" } });
173
+ this.fsCount.anchor.set(0.5);
174
+ this.fsCount.y = -14;
175
+ this.fsLabel = new Text({ text: ui.t("openui.freeSpins"), style: { fontFamily: ui.theme.type.family, fontSize: 24, fill: 657930, fontWeight: "900", letterSpacing: 3 } });
176
+ this.fsLabel.anchor.set(0.5);
177
+ this.fsLabel.y = 34;
178
+ this.fsFace.addChild(fsRing, this.fsCount, this.fsLabel);
179
+ this.fsFace.visible = false;
180
+ this.art.addChild(this.fsFace);
181
+ const stopRing = new Graphics().circle(0, 0, 96).fill({ color: 16777215 }).stroke({ width: 7, color: 657930 });
182
+ this.stopLabel = new Text({ text: ui.t("openui.stop") === "openui.stop" ? "STOP" : ui.t("openui.stop"), style: { fontFamily: ui.theme.type.family, fontSize: 26, fill: 657930, fontWeight: "900", letterSpacing: 3 } });
183
+ this.stopLabel.anchor.set(0.5);
184
+ this.stopLabel.y = -38;
185
+ this.stopCount = new Text({ text: "0", style: { fontFamily: ui.theme.type.family, fontSize: 64, fill: 657930, fontWeight: "900" } });
186
+ this.stopCount.anchor.set(0.5);
187
+ this.stopCount.y = 16;
188
+ this.stopFace.addChild(stopRing, this.stopLabel, this.stopCount);
189
+ this.stopFace.visible = false;
190
+ this.art.addChild(this.stopFace);
191
+ this.addChild(this.art);
192
+ this.hitArea = new Circle(0, 0, 118);
193
+ this.eventMode = "static";
194
+ this.cursor = "pointer";
195
+ this.on("pointerover", this.onOver);
196
+ this.on("pointerout", this.onOut);
197
+ this.on("pointerdown", this.onDown);
198
+ this.on("pointerup", this.onUp);
199
+ this.on("pointerupoutside", this.onUpOutside);
200
+ this.disposers.push(
201
+ this.spin.state.subscribe(() => {
202
+ this.skin.update(this.spin.current);
203
+ this.updateInteractive();
204
+ }),
205
+ // re-dim if slam-stop is toggled at runtime (e.g. applyJurisdiction)
206
+ this.spin.allowSlamStop.subscribe(() => this.updateInteractive()),
207
+ this.spin.onTransition((t) => this.play(t)),
208
+ // free-spins face: switch between the normal skin and the "N FS" counter
209
+ this.spin.freeSpins.subscribe(() => this.updateFaces()),
210
+ // autoplay: while active the spin button becomes the STOP-count button
211
+ this.ui.autoplay.state.subscribe(() => {
212
+ this.updateFaces();
213
+ this.updateInteractive();
214
+ }),
215
+ this.ui.autoplay.count.subscribe(() => this.updateFaces()),
216
+ this.ui.locale.subscribe(() => {
217
+ if (!this.destroyed) this.fsLabel.text = this.ui.t("openui.freeSpins");
218
+ })
219
+ );
220
+ this.skin.update(this.spin.current);
221
+ this.updateFaces();
222
+ this.updateInteractive();
223
+ }
224
+ spin;
225
+ art = new Container();
226
+ skin;
227
+ tween;
228
+ holdDelayMs;
229
+ holdTimer;
230
+ holding = false;
231
+ /** Free-spins face — a ring + remaining count + "FS"; shown when `spin.freeSpins > 0`. */
232
+ fsFace = new Container();
233
+ fsCount;
234
+ fsLabel;
235
+ /** Autoplay STOP face — a "STOP" label over the remaining count (or ∞); shown while
236
+ * autoplay is active. Tapping the spin button then STOPS autoplay. */
237
+ stopFace = new Container();
238
+ stopCount;
239
+ stopLabel;
240
+ /** Pick the spin face: autoplay STOP-count (top priority) → free-spins counter →
241
+ * the normal skin. */
242
+ updateFaces() {
243
+ const auto = this.ui.autoplay.isActive;
244
+ const n = this.spin.freeSpins.get();
245
+ const fs = n > 0 && !auto;
246
+ if (auto) {
247
+ const c = this.ui.autoplay.count.get();
248
+ this.stopCount.text = c === Infinity ? "\u221E" : String(c);
249
+ }
250
+ if (fs) this.fsCount.text = String(n);
251
+ this.stopFace.visible = auto;
252
+ this.fsFace.visible = fs;
253
+ this.skin.view.visible = !auto && !fs;
254
+ }
255
+ onOver = () => {
256
+ if (isDesktop() && this.spin.current === "idle") this.spin.setState("hover");
257
+ };
258
+ onOut = () => {
259
+ if (this.spin.current === "hover") this.spin.setState("idle");
260
+ };
261
+ onDown = () => {
262
+ if (!this.spin.interactable) return;
263
+ this.spin.setState("pressed");
264
+ if (this.spin.holdToSpin) {
265
+ this.holdTimer = setTimeout(() => {
266
+ this.holdTimer = void 0;
267
+ if (this.spin.current !== "pressed") return;
268
+ if (this.spin.holdBegin()) {
269
+ this.holding = true;
270
+ if (typeof window !== "undefined") {
271
+ window.addEventListener("pointerup", this.endHoldGlobal);
272
+ window.addEventListener("pointercancel", this.endHoldGlobal);
273
+ }
274
+ }
275
+ }, this.holdDelayMs);
276
+ }
277
+ };
278
+ onUp = () => {
279
+ if (this.holding) return this.endHold();
280
+ this.clearHoldTimer();
281
+ if (this.ui.autoplay.isActive) {
282
+ this.ui.autoplay.stop();
283
+ return;
284
+ }
285
+ if (this.spin.current === "pressed") {
286
+ this.spin.setState("idle");
287
+ this.spin.activate();
288
+ }
289
+ };
290
+ onUpOutside = () => {
291
+ if (this.holding) return this.endHold();
292
+ this.clearHoldTimer();
293
+ if (this.spin.current === "pressed") this.spin.setState("idle");
294
+ };
295
+ endHoldGlobal = () => this.endHold();
296
+ clearHoldTimer() {
297
+ if (this.holdTimer != null) {
298
+ clearTimeout(this.holdTimer);
299
+ this.holdTimer = void 0;
300
+ }
301
+ }
302
+ endHold() {
303
+ if (!this.holding) return;
304
+ this.holding = false;
305
+ if (typeof window !== "undefined") {
306
+ window.removeEventListener("pointerup", this.endHoldGlobal);
307
+ window.removeEventListener("pointercancel", this.endHoldGlobal);
308
+ }
309
+ this.spin.holdEnd();
310
+ if (this.spin.current === "pressed") this.spin.setState("idle");
311
+ }
312
+ updateInteractive() {
313
+ const ok = this.spin.interactable || this.ui.autoplay.isActive;
314
+ this.eventMode = ok ? "static" : "none";
315
+ this.cursor = ok ? "pointer" : "default";
316
+ const inSpin = this.spin.current === "spinning" || this.spin.current === "auto" || this.spin.current === "stop";
317
+ this.art.alpha = !this.spin.allowSlamStop.get() && inSpin ? 0.45 : 1;
318
+ }
319
+ play(t) {
320
+ this.animating = true;
321
+ void this.tween.run(this.art, t, this.ui.theme).then(() => {
322
+ this.animating = false;
323
+ });
324
+ }
325
+ dispose() {
326
+ this.clearHoldTimer();
327
+ this.endHold();
328
+ this.tween.stop();
329
+ this.skin.destroy();
330
+ super.dispose();
331
+ }
332
+ };
333
+ var __defProp = Object.defineProperty;
334
+ var __export = (target, all) => {
335
+ for (var name in all)
336
+ __defProp(target, name, { get: all[name], enumerable: true });
337
+ };
338
+ var CELLS_PER_STRIP = 20;
339
+ var Strip = class {
340
+ container;
341
+ cells;
342
+ renderer;
343
+ digitHeight;
344
+ dirtyLow = 1;
345
+ dirtyHigh = 0;
346
+ constructor(renderer, digitHeight) {
347
+ this.container = new Container();
348
+ this.renderer = renderer;
349
+ this.digitHeight = digitHeight;
350
+ this.cells = new Array(CELLS_PER_STRIP);
351
+ for (let i = 0; i < CELLS_PER_STRIP; i++) {
352
+ const cell = renderer.createCell(i % 10);
353
+ cell.y = i * digitHeight;
354
+ this.cells[i] = cell;
355
+ this.container.addChild(cell);
356
+ }
357
+ }
358
+ paintFillers(fromIdx, toIdx) {
359
+ const cells = this.cells;
360
+ if (this.dirtyHigh >= this.dirtyLow) {
361
+ for (let i = this.dirtyLow; i <= this.dirtyHigh; i++) {
362
+ this.renderer.setDigit(cells[i], i % 10);
363
+ }
364
+ }
365
+ const lo = fromIdx < toIdx ? fromIdx : toIdx;
366
+ const hi = fromIdx < toIdx ? toIdx : fromIdx;
367
+ const fillStart = lo + 1;
368
+ const fillEnd = hi - 1;
369
+ if (fillEnd < fillStart) {
370
+ this.dirtyLow = 1;
371
+ this.dirtyHigh = 0;
372
+ return;
373
+ }
374
+ for (let i = fillStart; i <= fillEnd; i++) {
375
+ this.renderer.setFiller(cells[i], i % 10);
376
+ }
377
+ this.dirtyLow = fillStart;
378
+ this.dirtyHigh = fillEnd;
379
+ }
380
+ clearFillers() {
381
+ if (this.dirtyHigh < this.dirtyLow) return;
382
+ const cells = this.cells;
383
+ for (let i = this.dirtyLow; i <= this.dirtyHigh; i++) {
384
+ this.renderer.setDigit(cells[i], i % 10);
385
+ }
386
+ this.dirtyLow = 1;
387
+ this.dirtyHigh = 0;
388
+ }
389
+ setY(y) {
390
+ this.container.y = y;
391
+ }
392
+ yForIndex(index) {
393
+ return -index * this.digitHeight;
394
+ }
395
+ destroy() {
396
+ const cells = this.cells;
397
+ if (this.renderer.destroyCell) {
398
+ for (let i = 0; i < CELLS_PER_STRIP; i++) {
399
+ this.renderer.destroyCell(cells[i]);
400
+ }
401
+ }
402
+ this.container.destroy({ children: true });
403
+ }
404
+ };
405
+ var DigitColumn = class {
406
+ container;
407
+ strip;
408
+ mask;
409
+ digitWidth;
410
+ digitHeight;
411
+ currentIndex = 0;
412
+ positionTweenIndex = -1;
413
+ alphaTweenIndex = -1;
414
+ constructor(renderer, digitWidth, digitHeight) {
415
+ this.digitWidth = digitWidth;
416
+ this.digitHeight = digitHeight;
417
+ this.container = new Container();
418
+ this.strip = new Strip(renderer, digitHeight);
419
+ this.container.addChild(this.strip.container);
420
+ this.mask = new Graphics();
421
+ this.mask.rect(0, 0, digitWidth, digitHeight).fill({ color: 16777215 });
422
+ this.container.addChild(this.mask);
423
+ this.strip.container.mask = this.mask;
424
+ this.setDigit(0);
425
+ }
426
+ setDigit(digit) {
427
+ const idx = (digit % 10 + 10) % 10;
428
+ this.currentIndex = idx;
429
+ this.strip.setY(this.strip.yForIndex(idx));
430
+ this.strip.clearFillers();
431
+ }
432
+ get currentDigit() {
433
+ return this.currentIndex % 10;
434
+ }
435
+ paintFillers(fromIdx, toIdx) {
436
+ this.strip.paintFillers(fromIdx, toIdx);
437
+ }
438
+ getStripY() {
439
+ return this.strip.container.y;
440
+ }
441
+ setStripY(y) {
442
+ this.strip.setY(y);
443
+ }
444
+ commitIndex(index) {
445
+ this.currentIndex = (index % 10 + 10) % 10;
446
+ this.strip.setY(this.strip.yForIndex(this.currentIndex));
447
+ this.strip.clearFillers();
448
+ }
449
+ destroy() {
450
+ this.mask.destroy();
451
+ this.strip.destroy();
452
+ this.container.destroy({ children: true });
453
+ }
454
+ };
455
+ function getSeparatorAfter(digits, every, decimals = 0) {
456
+ if (!every || every < 1 || digits < 2) return [];
457
+ const out = [];
458
+ for (let i = 0; i < digits - 1; i++) {
459
+ const placeFromRight = digits - 1 - i;
460
+ const integerPlace = placeFromRight - decimals;
461
+ if (integerPlace > 0 && integerPlace % every === 0) out.push(i);
462
+ }
463
+ return out;
464
+ }
465
+ function getDecimalSeparatorAfter(digits, decimals) {
466
+ if (decimals <= 0 || decimals >= digits) return -1;
467
+ return digits - decimals - 1;
468
+ }
469
+ function placeColumns(digits, digitWidth, separatorAfterColumns, separatorWidth) {
470
+ const columnX = new Array(digits);
471
+ const separatorX = [];
472
+ let x = 0;
473
+ let sepIdx = 0;
474
+ for (let i = 0; i < digits; i++) {
475
+ columnX[i] = x;
476
+ x += digitWidth;
477
+ if (sepIdx < separatorAfterColumns.length && separatorAfterColumns[sepIdx] === i) {
478
+ separatorX.push(x);
479
+ x += separatorWidth;
480
+ sepIdx++;
481
+ }
482
+ }
483
+ return { columnX, separatorX, totalWidth: x };
484
+ }
485
+ function digitAtPlace(value, place) {
486
+ return Math.floor(Math.abs(value) / Math.pow(10, place)) % 10;
487
+ }
488
+ function leadingZeroCount(value, digits, decimals = 0) {
489
+ const integerDigits = digits - decimals;
490
+ if (integerDigits <= 0) return 0;
491
+ const integerValue = Math.floor(Math.abs(value) / Math.pow(10, decimals));
492
+ if (integerValue <= 0) return integerDigits - 1;
493
+ let count = 0;
494
+ for (let i = integerDigits - 1; i > 0; i--) {
495
+ if (Math.floor(integerValue / Math.pow(10, i)) % 10 === 0) count++;
496
+ else break;
497
+ }
498
+ return count;
499
+ }
500
+ function computeSteps(oldDigit, newDigit, direction) {
501
+ return direction === "down" ? (oldDigit - newDigit + 10) % 10 || 10 : (newDigit - oldDigit + 10) % 10 || 10;
502
+ }
503
+ function computeDuration(distance, placeFromRight, motion) {
504
+ const raw = motion.msPerStep * distance + motion.placeDurationBump * placeFromRight;
505
+ if (raw < motion.minMs) return motion.minMs;
506
+ if (raw > motion.maxMs) return motion.maxMs;
507
+ return raw;
508
+ }
509
+ function resolveTargetIndex(fromIdx, newDigit, direction) {
510
+ if (direction === "up") {
511
+ let norm2 = fromIdx;
512
+ while (norm2 >= 10) norm2 -= 10;
513
+ while (norm2 < 0) norm2 += 10;
514
+ const toIdx2 = newDigit > norm2 ? newDigit : newDigit + 10;
515
+ return { normalizedFromIdx: norm2, toIdx: toIdx2 };
516
+ }
517
+ let norm = fromIdx;
518
+ while (norm < 10) norm += 10;
519
+ while (norm >= 20) norm -= 10;
520
+ const toIdx = newDigit + 10 < norm ? newDigit + 10 : newDigit;
521
+ return { normalizedFromIdx: norm, toIdx };
522
+ }
523
+ function resolveDirection(hint, oldValue, newValue) {
524
+ if (hint === "up") return "up";
525
+ if (hint === "down") return "down";
526
+ return newValue >= oldValue ? "up" : "down";
527
+ }
528
+ function createInactiveTween() {
529
+ return {
530
+ active: false,
531
+ startTime: 0,
532
+ delay: 0,
533
+ duration: 0,
534
+ startValue: 0,
535
+ targetValue: 0,
536
+ currentValue: 0,
537
+ ease: (t) => t,
538
+ rollId: 0
539
+ };
540
+ }
541
+ var NOOP_COMPLETE = () => void 0;
542
+ var Scheduler = class {
543
+ ticker;
544
+ tweens;
545
+ onApply;
546
+ onTweenComplete;
547
+ activeCount = 0;
548
+ attached = false;
549
+ constructor(ticker, tweenCount, onApply, onTweenComplete = NOOP_COMPLETE) {
550
+ this.ticker = ticker;
551
+ this.onApply = onApply;
552
+ this.onTweenComplete = onTweenComplete;
553
+ this.tweens = new Array(tweenCount);
554
+ for (let i = 0; i < tweenCount; i++) this.tweens[i] = createInactiveTween();
555
+ }
556
+ get(index) {
557
+ return this.tweens[index];
558
+ }
559
+ get tweenCount() {
560
+ return this.tweens.length;
561
+ }
562
+ get activeTweenCount() {
563
+ return this.activeCount;
564
+ }
565
+ get hasActive() {
566
+ return this.activeCount > 0;
567
+ }
568
+ start(index, now = performance.now()) {
569
+ const t = this.tweens[index];
570
+ if (!t.active) {
571
+ t.active = true;
572
+ this.activeCount++;
573
+ }
574
+ t.startTime = now;
575
+ if (!this.attached) {
576
+ this.ticker.add(this.tick);
577
+ this.attached = true;
578
+ }
579
+ }
580
+ stop(index) {
581
+ const t = this.tweens[index];
582
+ if (!t.active) return;
583
+ t.active = false;
584
+ this.activeCount--;
585
+ if (this.activeCount === 0) this.detach();
586
+ }
587
+ finish(index) {
588
+ const t = this.tweens[index];
589
+ if (!t.active) return;
590
+ const final = t.startValue + (t.targetValue - t.startValue) * t.ease(1);
591
+ t.currentValue = final;
592
+ t.active = false;
593
+ this.activeCount--;
594
+ this.onApply(index, final);
595
+ this.onTweenComplete(index);
596
+ if (this.activeCount === 0) this.detach();
597
+ }
598
+ finishAll() {
599
+ const tweens = this.tweens;
600
+ for (let i = 0, n = tweens.length; i < n; i++) {
601
+ if (tweens[i].active) this.finish(i);
602
+ }
603
+ }
604
+ destroy() {
605
+ this.detach();
606
+ this.activeCount = 0;
607
+ }
608
+ step(now = performance.now()) {
609
+ this.tickImpl(now);
610
+ }
611
+ detach() {
612
+ if (this.attached) {
613
+ this.ticker.remove(this.tick);
614
+ this.attached = false;
615
+ }
616
+ }
617
+ tick = () => {
618
+ if (this.activeCount === 0) {
619
+ this.detach();
620
+ return;
621
+ }
622
+ this.tickImpl(performance.now());
623
+ };
624
+ tickImpl(now) {
625
+ const tweens = this.tweens;
626
+ for (let i = 0, n = tweens.length; i < n; i++) {
627
+ const t = tweens[i];
628
+ if (!t.active) continue;
629
+ const elapsed = now - t.startTime - t.delay;
630
+ if (elapsed < 0) {
631
+ t.currentValue = t.startValue;
632
+ this.onApply(i, t.currentValue);
633
+ continue;
634
+ }
635
+ if (elapsed >= t.duration) {
636
+ const final = t.startValue + (t.targetValue - t.startValue) * t.ease(1);
637
+ t.currentValue = final;
638
+ t.active = false;
639
+ this.activeCount--;
640
+ this.onApply(i, final);
641
+ this.onTweenComplete(i);
642
+ } else {
643
+ const p = t.ease(elapsed / t.duration);
644
+ t.currentValue = t.startValue + (t.targetValue - t.startValue) * p;
645
+ this.onApply(i, t.currentValue);
646
+ }
647
+ }
648
+ }
649
+ };
650
+ var ease_exports = {};
651
+ __export(ease_exports, {
652
+ cubicOut: () => cubicOut,
653
+ defaultEase: () => defaultEase,
654
+ expoOut: () => expoOut,
655
+ linear: () => linear,
656
+ quadOut: () => quadOut,
657
+ quartOut: () => quartOut,
658
+ quintOut: () => quintOut,
659
+ sineInOut: () => sineInOut,
660
+ slotRoll: () => slotRoll
661
+ });
662
+ var linear = (t) => t;
663
+ var quadOut = (t) => 1 - (1 - t) * (1 - t);
664
+ var cubicOut = (t) => 1 - Math.pow(1 - t, 3);
665
+ var quartOut = (t) => 1 - Math.pow(1 - t, 4);
666
+ var quintOut = (t) => 1 - Math.pow(1 - t, 5);
667
+ var expoOut = (t) => t >= 1 ? 1 : 1 - Math.pow(2, -10 * t);
668
+ var sineInOut = (t) => -(Math.cos(Math.PI * t) - 1) / 2;
669
+ var slotRoll = (t) => 1 - Math.pow(1 - t, 4.5);
670
+ var defaultEase = slotRoll;
671
+ var DEFAULTS = {
672
+ motion: {
673
+ msPerStep: 35,
674
+ staggerMs: 28,
675
+ placeDurationBump: 25,
676
+ minMs: 280,
677
+ maxMs: 620
678
+ },
679
+ leadingZeros: {
680
+ mode: "show",
681
+ alpha: 0.35,
682
+ tweenMs: 150
683
+ },
684
+ blur: {
685
+ enabled: false,
686
+ peak: 8
687
+ },
688
+ separatorEvery: 3
689
+ };
690
+ var pulseSin = (t) => Math.sin(Math.PI * t);
691
+ var Counter = class extends Container {
692
+ digits;
693
+ digitWidth;
694
+ digitHeight;
695
+ cellRenderer;
696
+ motion;
697
+ leadingZeros;
698
+ blurOpt;
699
+ tickerRef;
700
+ columns;
701
+ separators;
702
+ sepAfterCol;
703
+ sepAlwaysVisible;
704
+ digitRow;
705
+ blurFilter;
706
+ maxValue;
707
+ decimals;
708
+ columnXLocal;
709
+ fullWidth;
710
+ fitOpt;
711
+ ALPHA_BASE;
712
+ BLUR_INDEX;
713
+ value;
714
+ scheduler;
715
+ prefixContainer = null;
716
+ suffixContainer = null;
717
+ currentRollId = 0;
718
+ activeRollCount = 0;
719
+ pendingResolve = null;
720
+ pendingOnComplete = null;
721
+ _rollStartPayload = { from: 0, to: 0, direction: "up" };
722
+ _rollEndPayload = { value: 0 };
723
+ _digitSettlePayload = { column: 0, digit: 0 };
724
+ constructor(options) {
725
+ super();
726
+ if (options.digits < 1) throw new Error("Counter: digits must be >= 1");
727
+ this.digits = options.digits;
728
+ this.digitWidth = options.digitWidth;
729
+ this.digitHeight = options.digitHeight;
730
+ this.cellRenderer = options.cellRenderer;
731
+ this.tickerRef = options.ticker ?? Ticker.shared;
732
+ this.decimals = options.decimals ?? 0;
733
+ if (this.decimals < 0) throw new Error("Counter: decimals must be >= 0");
734
+ if (this.decimals >= this.digits) {
735
+ throw new Error("Counter: decimals must be less than digits");
736
+ }
737
+ this.maxValue = Math.pow(10, this.digits) - 1;
738
+ this.value = this.clampValue(options.initialValue ?? 0);
739
+ this.motion = {
740
+ msPerStep: options.motion?.msPerStep ?? DEFAULTS.motion.msPerStep,
741
+ staggerMs: options.motion?.staggerMs ?? DEFAULTS.motion.staggerMs,
742
+ placeDurationBump: options.motion?.placeDurationBump ?? DEFAULTS.motion.placeDurationBump,
743
+ minMs: options.motion?.minMs ?? DEFAULTS.motion.minMs,
744
+ maxMs: options.motion?.maxMs ?? DEFAULTS.motion.maxMs,
745
+ ease: options.motion?.ease ?? defaultEase
746
+ };
747
+ this.leadingZeros = {
748
+ mode: options.leadingZeros?.mode ?? DEFAULTS.leadingZeros.mode,
749
+ alpha: options.leadingZeros?.alpha ?? DEFAULTS.leadingZeros.alpha,
750
+ tweenMs: options.leadingZeros?.tweenMs ?? DEFAULTS.leadingZeros.tweenMs
751
+ };
752
+ this.blurOpt = {
753
+ enabled: options.blur?.enabled ?? DEFAULTS.blur.enabled,
754
+ peak: options.blur?.peak ?? DEFAULTS.blur.peak
755
+ };
756
+ this.ALPHA_BASE = this.digits;
757
+ this.BLUR_INDEX = 2 * this.digits;
758
+ if (options.prefix) {
759
+ this.prefixContainer = this.coerceContainer(options.prefix);
760
+ this.addChild(this.prefixContainer);
761
+ }
762
+ this.digitRow = new Container();
763
+ this.addChild(this.digitRow);
764
+ this.columns = new Array(this.digits);
765
+ for (let i = 0; i < this.digits; i++) {
766
+ const col = new DigitColumn(this.cellRenderer, this.digitWidth, this.digitHeight);
767
+ col.positionTweenIndex = i;
768
+ col.alphaTweenIndex = this.ALPHA_BASE + i;
769
+ this.columns[i] = col;
770
+ this.digitRow.addChild(col.container);
771
+ }
772
+ const sepChar = options.separator?.char;
773
+ const sepEvery = options.separator?.every ?? DEFAULTS.separatorEvery;
774
+ const decimalChar = options.decimalChar ?? ".";
775
+ const thousandsCols = sepChar ? getSeparatorAfter(this.digits, sepEvery, this.decimals) : [];
776
+ const decimalCol = getDecimalSeparatorAfter(this.digits, this.decimals);
777
+ const sepCols = [];
778
+ const sepChars = [];
779
+ const sepAlwaysVisible = [];
780
+ let ti = 0;
781
+ for (let col = 0; col < this.digits - 1; col++) {
782
+ if (ti < thousandsCols.length && thousandsCols[ti] === col) {
783
+ sepCols.push(col);
784
+ sepChars.push(sepChar);
785
+ sepAlwaysVisible.push(false);
786
+ ti++;
787
+ }
788
+ if (col === decimalCol) {
789
+ sepCols.push(col);
790
+ sepChars.push(decimalChar);
791
+ sepAlwaysVisible.push(true);
792
+ }
793
+ }
794
+ this.separators = new Array(sepCols.length);
795
+ this.sepAfterCol = new Array(this.digits).fill(-1);
796
+ this.sepAlwaysVisible = sepAlwaysVisible;
797
+ let sepWidth = 0;
798
+ if (sepCols.length > 0 && this.cellRenderer.createSeparator) {
799
+ for (let i = 0; i < sepCols.length; i++) {
800
+ const sep = this.cellRenderer.createSeparator(sepChars[i]);
801
+ this.separators[i] = sep;
802
+ this.digitRow.addChild(sep);
803
+ this.sepAfterCol[sepCols[i]] = i;
804
+ if (sep.width > sepWidth) sepWidth = sep.width;
805
+ }
806
+ }
807
+ const layout = placeColumns(this.digits, this.digitWidth, sepCols, sepWidth);
808
+ this.columnXLocal = layout.columnX.slice();
809
+ for (let i = 0; i < this.digits; i++) {
810
+ this.columns[i].container.x = layout.columnX[i];
811
+ }
812
+ for (let i = 0; i < this.separators.length; i++) {
813
+ this.separators[i].x = layout.separatorX[i];
814
+ }
815
+ if (this.prefixContainer) {
816
+ const prefixWidth = this.prefixContainer.width;
817
+ this.digitRow.x = prefixWidth;
818
+ }
819
+ let suffixWidth = 0;
820
+ if (options.suffix) {
821
+ this.suffixContainer = this.coerceContainer(options.suffix);
822
+ this.suffixContainer.x = this.digitRow.x + layout.totalWidth;
823
+ this.addChild(this.suffixContainer);
824
+ suffixWidth = this.suffixContainer.width;
825
+ }
826
+ this.fullWidth = this.digitRow.x + layout.totalWidth + suffixWidth;
827
+ if (options.fit) {
828
+ this.fitOpt = {
829
+ maxWidth: options.fit.maxWidth,
830
+ minScale: options.fit.minScale ?? 0.4,
831
+ duration: options.fit.duration ?? 0.3,
832
+ ease: options.fit.ease ?? "power2.out",
833
+ anchor: options.fit.anchor ?? "left",
834
+ gsap: options.fit.gsap
835
+ };
836
+ this.pivot.x = this.fitOpt.anchor === "right" ? this.fullWidth : this.fitOpt.anchor === "center" ? this.fullWidth / 2 : 0;
837
+ const initialScale = this.computeFitScale(this.value);
838
+ this.scale.set(initialScale);
839
+ } else {
840
+ this.fitOpt = null;
841
+ }
842
+ if (this.blurOpt.enabled) {
843
+ this.blurFilter = new BlurFilter({ strength: 0, quality: 2 });
844
+ this.blurFilter.strengthX = 0;
845
+ this.digitRow.filters = [this.blurFilter];
846
+ } else {
847
+ this.blurFilter = null;
848
+ }
849
+ this.scheduler = new Scheduler(
850
+ this.tickerRef,
851
+ 2 * this.digits + 1,
852
+ this.onApply,
853
+ this.onTweenComplete
854
+ );
855
+ this.applyValueInstant(this.value);
856
+ }
857
+ getValue() {
858
+ return this.value;
859
+ }
860
+ isAnimating() {
861
+ return this.scheduler.hasActive;
862
+ }
863
+ setValue(value, opts = {}) {
864
+ const target = this.clampValue(value);
865
+ const oldValue = this.value;
866
+ if (opts.instant) {
867
+ this.resolvePending();
868
+ this.scheduler.finishAll();
869
+ this.value = target;
870
+ this.applyValueInstant(target);
871
+ this.applyFit(target, false);
872
+ opts.onComplete?.();
873
+ return Promise.resolve();
874
+ }
875
+ if (target === oldValue && !this.scheduler.hasActive) {
876
+ opts.onComplete?.();
877
+ return Promise.resolve();
878
+ }
879
+ this.applyFit(target, true);
880
+ this.resolvePending();
881
+ this.value = target;
882
+ const direction = resolveDirection(opts.direction, oldValue, target);
883
+ const now = performance.now();
884
+ const rollId = ++this.currentRollId;
885
+ this.activeRollCount = 0;
886
+ this.finishStaleTweens(target, rollId);
887
+ const changingCount = this.startColumnTweens(oldValue, target, direction, opts.duration, now, rollId);
888
+ if (changingCount === 0) {
889
+ this.startBlurTween(0, now, rollId);
890
+ this.startLeadingZeroTweens(target, now, rollId);
891
+ opts.onComplete?.();
892
+ return Promise.resolve();
893
+ }
894
+ this.activeRollCount = changingCount;
895
+ let longestEnd = 0;
896
+ for (let col = 0; col < this.digits; col++) {
897
+ const t = this.scheduler.get(col);
898
+ if (t.active && t.rollId === rollId) {
899
+ const end = t.delay + t.duration;
900
+ if (end > longestEnd) longestEnd = end;
901
+ }
902
+ }
903
+ this.startBlurTween(longestEnd, now, rollId);
904
+ this.startLeadingZeroTweens(target, now, rollId);
905
+ this._rollStartPayload.from = oldValue;
906
+ this._rollStartPayload.to = target;
907
+ this._rollStartPayload.direction = direction;
908
+ this.emit("rollstart", this._rollStartPayload);
909
+ return new Promise((resolve) => {
910
+ this.pendingResolve = resolve;
911
+ this.pendingOnComplete = opts.onComplete ?? null;
912
+ });
913
+ }
914
+ /**
915
+ * Skip the in-flight animation straight to its current target value.
916
+ *
917
+ * - Every active position tween jumps to its target Y (the column shows the
918
+ * target digit, sharp).
919
+ * - Leading-zero alpha tweens jump to their target alpha (dim / hidden / full).
920
+ * - The blur tween snaps to 0 (no residual blur).
921
+ * - `digitsettle` fires for each settling column, then `rollend` fires once.
922
+ * - The pending `setValue` Promise resolves.
923
+ *
924
+ * Use it to fast-forward when the player has impatiently queued another input:
925
+ *
926
+ * ```ts
927
+ * function onBetClick() {
928
+ * counter.cancel(); // snap any in-flight roll to its end
929
+ * counter.setValue(balance); // start a fresh roll from there
930
+ * }
931
+ * ```
932
+ *
933
+ * If no roll is in flight, this is a cheap no-op. Safe to call on every input.
934
+ */
935
+ cancel() {
936
+ this.scheduler.finishAll();
937
+ this.resolvePending();
938
+ }
939
+ destroy(options) {
940
+ this.scheduler.destroy();
941
+ this.resolvePending();
942
+ if (this.fitOpt) this.fitOpt.gsap.killTweensOf(this.scale);
943
+ for (let i = 0; i < this.columns.length; i++) this.columns[i].destroy();
944
+ if (this.blurFilter) this.blurFilter.destroy();
945
+ super.destroy(options);
946
+ }
947
+ computeFitScale(value) {
948
+ if (!this.fitOpt) return 1;
949
+ const visibleWidth = this.computeVisibleWidth(value);
950
+ if (visibleWidth <= 0) return 1;
951
+ const raw = this.fitOpt.maxWidth / visibleWidth;
952
+ if (raw >= 1) return 1;
953
+ return Math.max(this.fitOpt.minScale, raw);
954
+ }
955
+ computeVisibleWidth(value) {
956
+ if (this.leadingZeros.mode !== "hide") return this.fullWidth;
957
+ const dimCount = leadingZeroCount(value, this.digits, this.decimals);
958
+ if (dimCount >= this.digits) return 0;
959
+ const leftEdge = this.columnXLocal[dimCount] + this.digitRow.x;
960
+ return this.fullWidth - leftEdge;
961
+ }
962
+ applyFit(value, animated) {
963
+ if (!this.fitOpt) return;
964
+ const target = this.computeFitScale(value);
965
+ const gsap = this.fitOpt.gsap;
966
+ gsap.killTweensOf(this.scale);
967
+ if (!animated || this.scale.x === target && this.scale.y === target) {
968
+ this.scale.set(target);
969
+ return;
970
+ }
971
+ gsap.to(this.scale, {
972
+ x: target,
973
+ y: target,
974
+ duration: this.fitOpt.duration,
975
+ ease: this.fitOpt.ease
976
+ });
977
+ }
978
+ clampValue(value) {
979
+ const i = Math.floor(value);
980
+ if (i < 0) return 0;
981
+ if (i > this.maxValue) return this.maxValue;
982
+ return i;
983
+ }
984
+ coerceContainer(value) {
985
+ if (typeof value !== "string") return value;
986
+ if (this.cellRenderer.createSeparator) return this.cellRenderer.createSeparator(value);
987
+ return new Container();
988
+ }
989
+ applyValueInstant(value) {
990
+ for (let col = 0; col < this.digits; col++) {
991
+ const placeFromRight = this.digits - 1 - col;
992
+ const digit = digitAtPlace(value, placeFromRight);
993
+ this.columns[col].setDigit(digit);
994
+ }
995
+ this.applyLeadingZerosInstant(value);
996
+ }
997
+ applyLeadingZerosInstant(value) {
998
+ const dimCount = this.leadingZeros.mode === "show" ? 0 : leadingZeroCount(value, this.digits, this.decimals);
999
+ const dimAlpha = this.leadingZeros.mode === "hide" ? 0 : this.leadingZeros.alpha;
1000
+ for (let col = 0; col < this.digits; col++) {
1001
+ const wantAlpha = col < dimCount ? dimAlpha : 1;
1002
+ this.columns[col].container.alpha = wantAlpha;
1003
+ const sepIdx = this.sepAfterCol[col];
1004
+ if (sepIdx >= 0 && !this.sepAlwaysVisible[sepIdx]) {
1005
+ this.separators[sepIdx].alpha = wantAlpha;
1006
+ }
1007
+ }
1008
+ }
1009
+ finishStaleTweens(target, _rollId) {
1010
+ for (let col = 0; col < this.digits; col++) {
1011
+ const t = this.scheduler.get(col);
1012
+ if (!t.active) continue;
1013
+ const placeFromRight = this.digits - 1 - col;
1014
+ const newDigit = digitAtPlace(target, placeFromRight);
1015
+ const targetIdx = Math.round(-t.targetValue / this.digitHeight);
1016
+ const targetDigit = (targetIdx % 10 + 10) % 10;
1017
+ if (newDigit === targetDigit) {
1018
+ this.scheduler.finish(col);
1019
+ }
1020
+ }
1021
+ }
1022
+ startColumnTweens(oldValue, target, direction, durationOverride, now, rollId) {
1023
+ let changingCount = 0;
1024
+ for (let col = 0; col < this.digits; col++) {
1025
+ const placeFromRight = this.digits - 1 - col;
1026
+ const oldDigit = digitAtPlace(oldValue, placeFromRight);
1027
+ const newDigit = digitAtPlace(target, placeFromRight);
1028
+ if (oldDigit === newDigit) continue;
1029
+ const column = this.columns[col];
1030
+ const rawFromIdx = -column.getStripY() / this.digitHeight;
1031
+ const { normalizedFromIdx, toIdx } = resolveTargetIndex(rawFromIdx, newDigit, direction);
1032
+ const distance = Math.abs(toIdx - normalizedFromIdx);
1033
+ const integerSteps = computeSteps(oldDigit, newDigit, direction);
1034
+ const distanceForDuration = distance > 0 ? distance : integerSteps;
1035
+ const duration = durationOverride ?? computeDuration(distanceForDuration, placeFromRight, this.motion);
1036
+ const delay = changingCount * this.motion.staggerMs;
1037
+ const fromY = -normalizedFromIdx * this.digitHeight;
1038
+ const targetY = -toIdx * this.digitHeight;
1039
+ column.setStripY(fromY);
1040
+ column.paintFillers(Math.floor(normalizedFromIdx), toIdx);
1041
+ const t = this.scheduler.get(col);
1042
+ t.startValue = fromY;
1043
+ t.targetValue = targetY;
1044
+ t.duration = duration;
1045
+ t.delay = delay;
1046
+ t.ease = this.motion.ease;
1047
+ t.rollId = rollId;
1048
+ this.scheduler.start(col, now);
1049
+ changingCount++;
1050
+ }
1051
+ return changingCount;
1052
+ }
1053
+ startBlurTween(longestEnd, now, rollId) {
1054
+ if (!this.blurFilter) return;
1055
+ const t = this.scheduler.get(this.BLUR_INDEX);
1056
+ if (longestEnd <= 0) {
1057
+ this.blurFilter.strengthY = 0;
1058
+ if (t.active) this.scheduler.finish(this.BLUR_INDEX);
1059
+ return;
1060
+ }
1061
+ t.startValue = 0;
1062
+ t.targetValue = 1;
1063
+ t.duration = longestEnd;
1064
+ t.delay = 0;
1065
+ t.ease = pulseSin;
1066
+ t.rollId = rollId;
1067
+ this.scheduler.start(this.BLUR_INDEX, now);
1068
+ }
1069
+ startLeadingZeroTweens(target, now, rollId) {
1070
+ if (this.leadingZeros.mode === "show") return;
1071
+ const dimCount = leadingZeroCount(target, this.digits, this.decimals);
1072
+ const dimAlpha = this.leadingZeros.mode === "hide" ? 0 : this.leadingZeros.alpha;
1073
+ for (let col = 0; col < this.digits; col++) {
1074
+ const wantAlpha = col < dimCount ? dimAlpha : 1;
1075
+ const column = this.columns[col];
1076
+ const currentAlpha = column.container.alpha;
1077
+ if (Math.abs(currentAlpha - wantAlpha) < 1e-3) continue;
1078
+ const t = this.scheduler.get(this.ALPHA_BASE + col);
1079
+ t.startValue = currentAlpha;
1080
+ t.targetValue = wantAlpha;
1081
+ t.duration = this.leadingZeros.tweenMs;
1082
+ t.delay = 0;
1083
+ t.ease = linear;
1084
+ t.rollId = rollId;
1085
+ this.scheduler.start(this.ALPHA_BASE + col, now);
1086
+ }
1087
+ }
1088
+ onApply = (idx, value) => {
1089
+ if (idx < this.digits) {
1090
+ this.columns[idx].setStripY(value);
1091
+ return;
1092
+ }
1093
+ if (idx < this.BLUR_INDEX) {
1094
+ const colIdx = idx - this.digits;
1095
+ this.columns[colIdx].container.alpha = value;
1096
+ const sepIdx = this.sepAfterCol[colIdx];
1097
+ if (sepIdx >= 0 && !this.sepAlwaysVisible[sepIdx]) {
1098
+ this.separators[sepIdx].alpha = value;
1099
+ }
1100
+ return;
1101
+ }
1102
+ if (this.blurFilter) this.blurFilter.strengthY = value * this.blurOpt.peak;
1103
+ };
1104
+ onTweenComplete = (idx) => {
1105
+ if (idx >= this.digits) return;
1106
+ const t = this.scheduler.get(idx);
1107
+ const finalIdx = Math.round(-t.targetValue / this.digitHeight);
1108
+ const column = this.columns[idx];
1109
+ column.commitIndex(finalIdx);
1110
+ this._digitSettlePayload.column = idx;
1111
+ this._digitSettlePayload.digit = column.currentDigit;
1112
+ this.emit("digitsettle", this._digitSettlePayload);
1113
+ if (t.rollId !== this.currentRollId) return;
1114
+ if (this.activeRollCount === 0) return;
1115
+ this.activeRollCount--;
1116
+ if (this.activeRollCount === 0) {
1117
+ this._rollEndPayload.value = this.value;
1118
+ this.emit("rollend", this._rollEndPayload);
1119
+ this.resolvePending();
1120
+ }
1121
+ };
1122
+ resolvePending() {
1123
+ const resolve = this.pendingResolve;
1124
+ const onComplete = this.pendingOnComplete;
1125
+ this.pendingResolve = null;
1126
+ this.pendingOnComplete = null;
1127
+ if (resolve) resolve();
1128
+ if (onComplete) onComplete();
1129
+ }
1130
+ };
1131
+
1132
+ // src/renderers/TextCellRenderer.ts
1133
+ var TextCellRenderer = class {
1134
+ constructor(o) {
1135
+ this.o = o;
1136
+ }
1137
+ o;
1138
+ createCell(digit) {
1139
+ const cell = new Container();
1140
+ const t = new Text({ text: String(digit), style: this.o.style });
1141
+ t.anchor.set(0.5, 0.5);
1142
+ t.x = this.o.digitWidth / 2;
1143
+ t.y = this.o.digitHeight / 2;
1144
+ cell.addChild(t);
1145
+ return cell;
1146
+ }
1147
+ setDigit(cell, digit) {
1148
+ const t = cell.children[0];
1149
+ const next = String(digit);
1150
+ if (t.text !== next) t.text = next;
1151
+ }
1152
+ setFiller(cell, digit) {
1153
+ const t = cell.children[0];
1154
+ const next = this.o.fillerChar ?? String(digit);
1155
+ if (t.text !== next) t.text = next;
1156
+ }
1157
+ createSeparator(char) {
1158
+ const cell = new Container();
1159
+ const t = new Text({ text: char, style: this.o.style });
1160
+ t.anchor.set(0.5, 0.5);
1161
+ cell.addChild(t);
1162
+ t.y = this.o.digitHeight / 2;
1163
+ t.x = t.width / 2;
1164
+ return cell;
1165
+ }
1166
+ destroyCell(cell) {
1167
+ cell.destroy({ children: true });
1168
+ }
1169
+ };
1170
+ var DIGIT_W = 39;
1171
+ var DIGIT_H = 68;
1172
+ var FONT_SIZE = 48;
1173
+ var FIT_BUDGET_COLS = 9;
1174
+ var ValueDisplayView = class extends ControlView {
1175
+ constructor(vd, ui, ticker, fitGsap) {
1176
+ super(vd, ui);
1177
+ this.vd = vd;
1178
+ this.ticker = ticker;
1179
+ this.fitGsap = fitGsap;
1180
+ this.build();
1181
+ this.disposers.push(
1182
+ this.vd.value.subscribe(() => {
1183
+ if (this.destroyed) return;
1184
+ const cur = this.vd.currency.get();
1185
+ const needed = displayDigits(this.vd.get(), this.vd.digits, cur.decimals);
1186
+ if (this.counter && needed !== this.counter.digits) {
1187
+ const from = needed > this.counter.digits ? this.counter.getValue() : this.vd.minorUnits;
1188
+ this.buildCounter(needed, from);
1189
+ }
1190
+ void this.counter?.setValue(this.vd.minorUnits);
1191
+ }),
1192
+ this.vd.currency.subscribe(() => {
1193
+ if (!this.destroyed) this.rebuild();
1194
+ }),
1195
+ // re-translate the caption on language switch (guard a post-dispose emit)
1196
+ this.ui.locale.subscribe(() => {
1197
+ if (!this.destroyed) this.rebuild();
1198
+ }),
1199
+ // accent emphasis (e.g. a bet display showing the boosted effective stake):
1200
+ // rebuild the odometer with the accent fill at the same width
1201
+ this.vd.emphasized.subscribe(() => {
1202
+ if (!this.destroyed && this.counter) this.buildCounter(this.counter.digits, this.vd.minorUnits);
1203
+ })
1204
+ );
1205
+ }
1206
+ vd;
1207
+ ticker;
1208
+ fitGsap;
1209
+ counter;
1210
+ caption;
1211
+ captionPill;
1212
+ side() {
1213
+ const a = this.vd.layout.anchor;
1214
+ return a.includes("left") ? "left" : a.includes("right") ? "right" : "center";
1215
+ }
1216
+ build() {
1217
+ const theme = this.ui.theme;
1218
+ if (this.vd.label) {
1219
+ this.caption = new Text({
1220
+ // Figma: the label is NOT uppercased — "Баланс" / "Ставка" as authored.
1221
+ text: this.ui.t(this.vd.label),
1222
+ style: { fontFamily: theme.type.family, fontSize: 28, fill: 16777215, fontWeight: "900", letterSpacing: 0 }
1223
+ });
1224
+ this.caption.anchor.set(0.5, 0.5);
1225
+ this.captionPill = new Graphics();
1226
+ this.addChild(this.captionPill, this.caption);
1227
+ this.sizeCaptionPill();
1228
+ if (typeof document !== "undefined" && document.fonts?.ready) {
1229
+ void document.fonts.ready.then(() => {
1230
+ if (!this.destroyed && this.caption) this.sizeCaptionPill();
1231
+ });
1232
+ }
1233
+ }
1234
+ const cur = this.vd.currency.get();
1235
+ this.buildCounter(displayDigits(this.vd.get(), this.vd.digits, cur.decimals), this.vd.minorUnits);
1236
+ }
1237
+ /** Size the caption pill to the caption's current measured width + padding and centre
1238
+ * the text in it, anchored to the value's side. Re-runnable (fonts-ready / relayout). */
1239
+ sizeCaptionPill() {
1240
+ if (!this.caption || !this.captionPill) return;
1241
+ const side = this.side();
1242
+ const padX = 24;
1243
+ const padY = 8;
1244
+ const pillW = Math.ceil(this.caption.width) + padX * 2;
1245
+ const pillH = Math.ceil(this.caption.height) + padY * 2;
1246
+ const pillX = side === "left" ? 0 : side === "right" ? -pillW : -pillW / 2;
1247
+ const pillY = -DIGIT_H / 2 - pillH - 14;
1248
+ this.captionPill.clear().roundRect(pillX, pillY, pillW, pillH, 12).fill({ color: 0 });
1249
+ this.caption.position.set(pillX + pillW / 2, pillY + pillH / 2);
1250
+ }
1251
+ /** (Re)create the rolling counter sized to `total` columns, starting at `fromMinor`. */
1252
+ buildCounter(total, fromMinor) {
1253
+ this.counter?.destroy();
1254
+ const theme = this.ui.theme;
1255
+ const cur = this.vd.currency.get();
1256
+ const side = this.side();
1257
+ const renderer = new TextCellRenderer({
1258
+ // Figma "default" readout: Montserrat Black, white, with a bold BLACK OUTLINE
1259
+ // plus a soft drop shadow (0px 4px, 25% black) under it.
1260
+ style: {
1261
+ fontFamily: theme.type.family,
1262
+ fontSize: FONT_SIZE,
1263
+ // Emphasis (ValueDisplay.setEmphasis) renders the value in the theme
1264
+ // accent — "this number is modified" (boosted effective stake, etc.).
1265
+ fill: this.vd.emphasized.get() ? theme.color.accent : 16777215,
1266
+ fontWeight: "900",
1267
+ stroke: { color: 0, width: 8, join: "round" },
1268
+ dropShadow: { color: 0, alpha: 0.25, blur: 2, distance: 4, angle: Math.PI / 2 }
1269
+ },
1270
+ digitWidth: DIGIT_W,
1271
+ digitHeight: DIGIT_H
1272
+ // no fillerChar → rolling cells show real digits (crisp, no blur, no blank flicker)
1273
+ });
1274
+ const opts = {
1275
+ digits: total,
1276
+ decimals: cur.decimals,
1277
+ decimalChar: cur.decimalChar ?? ".",
1278
+ separator: { char: cur.separator ?? ",", every: 3 },
1279
+ cellRenderer: renderer,
1280
+ digitWidth: DIGIT_W,
1281
+ digitHeight: DIGIT_H,
1282
+ initialValue: fromMinor,
1283
+ // Tight sizing means the value fills its columns — leading-zero hide only ever
1284
+ // trims a digit or two after a grow, never a permanent reserve gap.
1285
+ leadingZeros: { mode: "hide" },
1286
+ blur: { enabled: false },
1287
+ ticker: this.ticker
1288
+ };
1289
+ const symbolMode = cur.display === "symbol" && !!cur.symbol;
1290
+ const affix = symbolMode ? cur.symbol : cur.code;
1291
+ if ((cur.position ?? "suffix") === "prefix") opts.prefix = symbolMode ? affix : `${affix} `;
1292
+ else opts.suffix = ` ${affix}`;
1293
+ if (this.fitGsap) {
1294
+ opts.fit = {
1295
+ gsap: this.fitGsap,
1296
+ maxWidth: valueFitMaxWidth(Math.max(this.vd.digits, FIT_BUDGET_COLS), DIGIT_W),
1297
+ minScale: 0.5,
1298
+ anchor: side
1299
+ // left→0 · right→fullWidth · center→fullWidth/2
1300
+ };
1301
+ }
1302
+ this.counter = new Counter(opts);
1303
+ this.counter.y = -DIGIT_H / 2;
1304
+ if (this.fitGsap) {
1305
+ this.counter.x = 0;
1306
+ } else {
1307
+ const budget = valueFitMaxWidth(Math.max(this.vd.digits, FIT_BUDGET_COLS), DIGIT_W);
1308
+ const natural = this.counter.width;
1309
+ const k = natural > budget ? Math.max(0.5, budget / natural) : 1;
1310
+ this.counter.scale.set(k);
1311
+ this.counter.y = -DIGIT_H * k / 2;
1312
+ const w = natural * k;
1313
+ this.counter.x = side === "left" ? 0 : side === "right" ? -w : -w / 2;
1314
+ }
1315
+ this.addChild(this.counter);
1316
+ }
1317
+ rebuild() {
1318
+ this.counter?.destroy();
1319
+ this.caption?.destroy();
1320
+ this.captionPill?.destroy();
1321
+ this.counter = void 0;
1322
+ this.caption = void 0;
1323
+ this.captionPill = void 0;
1324
+ this.removeChildren();
1325
+ this.build();
1326
+ }
1327
+ dispose() {
1328
+ this.counter?.destroy();
1329
+ super.dispose();
1330
+ }
1331
+ };
1332
+ var GLYPH_SVG = {
1333
+ speaker: '<svg viewBox="0 0 22.5 22.5" xmlns="http://www.w3.org/2000/svg"><path d="M13.6745 5.01252C13.9316 4.84399 14.2647 4.8292 14.5397 4.97456C14.8146 5.1199 14.9868 5.40142 14.9872 5.706L15.0007 16.8107C15.0011 17.1153 14.8296 17.3934 14.555 17.5333C14.2804 17.6731 13.9472 17.6517 13.6897 17.4781L10.4454 15.2905L8.61605 15.2722C6.76074 15.2537 5.25495 13.7655 5.25266 11.9484L5.25073 10.3739C5.2486 8.55679 6.75081 7.09863 8.6061 7.11714L10.4355 7.13539L13.6745 5.01252Z" fill="none" stroke="black" stroke-width="1.5" stroke-linejoin="round"/><path d="M16.1933 9.375C16.6252 10.4652 16.6015 11.654 16.1257 12.75" fill="none" stroke="black" stroke-width="3" stroke-linecap="round"/></svg>',
1334
+ "speaker-mute": '<svg viewBox="0 0 22.5 22.5" xmlns="http://www.w3.org/2000/svg"><path d="M13.6745 5.01252C13.9316 4.84399 14.2647 4.8292 14.5397 4.97456C14.8146 5.1199 14.9868 5.40142 14.9872 5.706L15.0007 16.8107C15.0011 17.1153 14.8296 17.3934 14.555 17.5333C14.2804 17.6731 13.9472 17.6517 13.6897 17.4781L10.4454 15.2905L8.61605 15.2722C6.76074 15.2537 5.25495 13.7655 5.25266 11.9484L5.25073 10.3739C5.2486 8.55679 6.75081 7.09863 8.6061 7.11714L10.4355 7.13539L13.6745 5.01252Z" fill="none" stroke="black" stroke-width="1.5" stroke-linejoin="round"/><path d="M15.5 8.5L19.5 13.5M19.5 8.5L15.5 13.5" fill="none" stroke="black" stroke-width="1.8" stroke-linecap="round"/></svg>',
1335
+ fullscreen: '<svg viewBox="0 0 22.5 22.5" xmlns="http://www.w3.org/2000/svg"><path d="M7.875 18H7.5C5.84315 18 4.5 16.6569 4.5 15V14.25M7.875 4.5H7.5C5.84315 4.5 4.5 5.84315 4.5 7.5V8.25M14.25 4.5H15C16.6569 4.5 18 5.84315 18 7.5V8.25M14.25 18H15C16.6569 18 18 16.6569 18 15V14.25" fill="none" stroke="black" stroke-width="1.5" stroke-linecap="round" stroke-linejoin="round"/></svg>',
1336
+ "fullscreen-exit": '<svg viewBox="0 0 22.5 22.5" xmlns="http://www.w3.org/2000/svg"><path d="M4.5 8.25H5.25C6.9 8.25 8.25 6.9 8.25 5.25V4.5M14.25 4.5V5.25C14.25 6.9 15.6 8.25 17.25 8.25H18M18 14.25H17.25C15.6 14.25 14.25 15.6 14.25 17.25V18M8.25 18V17.25C8.25 15.6 6.9 14.25 5.25 14.25H4.5" fill="none" stroke="black" stroke-width="1.5" stroke-linecap="round" stroke-linejoin="round"/></svg>'
1337
+ };
1338
+ var ButtonView = class extends ControlView {
1339
+ constructor(btn, ui, ticker, opts = {}) {
1340
+ super(btn, ui);
1341
+ this.btn = btn;
1342
+ this.tween = new Tweener(ticker);
1343
+ this.shape = opts.shape ?? "circle";
1344
+ this.radius = opts.radius ?? 40;
1345
+ this.pillHeight = opts.height ?? 56;
1346
+ this.glyph = opts.glyph ?? "none";
1347
+ this.iconTarget = opts.iconTarget ?? 84;
1348
+ this.mono = opts.mono ?? false;
1349
+ this.dark = opts.dark ?? false;
1350
+ this.addChild(this.art);
1351
+ if (opts.iconTexture) {
1352
+ this.sprite = new Sprite(opts.iconTexture);
1353
+ this.sprite.anchor.set(0.5);
1354
+ this.fitSprite();
1355
+ this.art.addChild(this.sprite);
1356
+ } else {
1357
+ this.art.addChild(this.bg, this.glyphG);
1358
+ if (btn.label) {
1359
+ this.labelText = new Text({
1360
+ text: ui.t(btn.label),
1361
+ style: { fontFamily: ui.theme.type.family, fontSize: 22, fill: ui.theme.color.text, fontWeight: "700" }
1362
+ });
1363
+ this.labelText.anchor.set(0.5);
1364
+ this.art.addChild(this.labelText);
1365
+ }
1366
+ }
1367
+ this.eventMode = "static";
1368
+ this.cursor = "pointer";
1369
+ this.on("pointerdown", this.onDown);
1370
+ this.on("pointerup", this.onUp);
1371
+ this.on("pointerupoutside", this.onUpOutside);
1372
+ this.disposers.push(
1373
+ this.btn.state.subscribe(() => {
1374
+ this.redraw();
1375
+ this.updateInteractive();
1376
+ }),
1377
+ this.btn.onTransition((t) => this.play(t)),
1378
+ // The host input lock gates `interactable` without a state change, so react to
1379
+ // it directly: a locked/disabled button dims to a semi-transparent state.
1380
+ this.ui.locked.subscribe(() => {
1381
+ this.redraw();
1382
+ this.updateInteractive();
1383
+ }),
1384
+ this.ui.locale.subscribe(() => {
1385
+ if (this.destroyed) return;
1386
+ if (this.labelText && this.btn.label) {
1387
+ this.labelText.text = this.ui.t(this.btn.label);
1388
+ this.updateHit();
1389
+ }
1390
+ })
1391
+ );
1392
+ this.redraw();
1393
+ this.updateHit();
1394
+ this.updateInteractive();
1395
+ }
1396
+ btn;
1397
+ art = new Container();
1398
+ bg = new Graphics();
1399
+ glyphG = new Graphics();
1400
+ sprite;
1401
+ labelText;
1402
+ tween;
1403
+ shape;
1404
+ radius;
1405
+ pillHeight;
1406
+ glyph;
1407
+ iconTarget;
1408
+ mono;
1409
+ dark;
1410
+ /** Swap the displayed art (e.g. ☰ → ✕). */
1411
+ setIconTexture(tex) {
1412
+ if (!this.sprite) return;
1413
+ this.sprite.texture = tex;
1414
+ this.fitSprite();
1415
+ this.updateHit();
1416
+ }
1417
+ /** Swap the placeholder glyph (e.g. speaker ↔ speaker-mute, fullscreen ↔ exit). */
1418
+ setGlyph(glyph) {
1419
+ if (this.glyph === glyph) return;
1420
+ this.glyph = glyph;
1421
+ this.redraw();
1422
+ this.updateHit();
1423
+ }
1424
+ fitSprite() {
1425
+ if (!this.sprite) return;
1426
+ this.sprite.scale.set(1);
1427
+ this.sprite.scale.set(this.iconTarget / this.sprite.height);
1428
+ }
1429
+ onDown = () => {
1430
+ if (this.btn.interactable) this.btn.setState("pressed");
1431
+ };
1432
+ onUp = () => {
1433
+ if (this.btn.current === "pressed") {
1434
+ this.btn.setState("idle");
1435
+ this.btn.activate();
1436
+ }
1437
+ };
1438
+ onUpOutside = () => {
1439
+ if (this.btn.current === "pressed") this.btn.setState("idle");
1440
+ };
1441
+ updateHit() {
1442
+ if (this.sprite) {
1443
+ const w = this.sprite.width;
1444
+ const h = this.sprite.height;
1445
+ this.hitArea = new Rectangle(-w / 2, -h / 2, w, h);
1446
+ } else if (this.shape === "circle") {
1447
+ this.hitArea = new Circle(0, 0, this.radius + 6);
1448
+ } else {
1449
+ const w = this.labelText ? this.labelText.width + 48 : 120;
1450
+ this.hitArea = new Rectangle(-w / 2, -this.pillHeight / 2, w, this.pillHeight);
1451
+ }
1452
+ }
1453
+ updateInteractive() {
1454
+ const ok = this.btn.interactable;
1455
+ this.eventMode = ok ? "static" : "none";
1456
+ this.cursor = ok ? "pointer" : "default";
1457
+ this.alpha = ok ? 1 : 0.4;
1458
+ }
1459
+ redraw() {
1460
+ if (this.sprite) {
1461
+ this.sprite.alpha = this.btn.current === "disabled" ? 0.5 : 1;
1462
+ return;
1463
+ }
1464
+ const t = this.ui.theme;
1465
+ const g = this.bg;
1466
+ g.clear();
1467
+ const disabled = this.btn.current === "disabled";
1468
+ const fillC = this.dark ? "#0a0a0a" : this.mono ? "#ffffff" : t.color.surface;
1469
+ const lineC = this.mono ? "#0a0a0a" : t.color.accent;
1470
+ const ink = this.dark ? "#ffffff" : this.mono ? disabled ? "#9aa0a6" : "#0a0a0a" : disabled ? t.color.textDim : t.color.text;
1471
+ this.glyphG.clear();
1472
+ this.glyphG.visible = false;
1473
+ if (this.shape === "circle") {
1474
+ const r = this.radius;
1475
+ if (this.mono) {
1476
+ g.circle(0, r * 0.16, r).fill({ color: 0, alpha: disabled ? 0.08 : 0.18 });
1477
+ g.circle(0, 0, r * 0.98).fill({ color: "#ffffff", alpha: disabled ? 0.6 : 1 });
1478
+ g.circle(0, 0, r * 0.925).stroke({ width: r * 0.15, color: "#0a0a0a", alpha: disabled ? 0.5 : 1 });
1479
+ } else if (this.dark) {
1480
+ g.circle(0, r * 0.16, r).fill({ color: 0, alpha: 0.18 });
1481
+ g.circle(0, 0, r).fill({ color: "#0a0a0a" });
1482
+ } else {
1483
+ g.circle(0, 0, r).fill({ color: fillC }).circle(0, 0, r).stroke({ width: 4, color: lineC });
1484
+ }
1485
+ const svg = GLYPH_SVG[this.glyph];
1486
+ if (svg) {
1487
+ const s = r / 15;
1488
+ this.glyphG.svg(svg);
1489
+ this.glyphG.scale.set(s);
1490
+ this.glyphG.position.set(-11.25 * s, -11.25 * s);
1491
+ this.glyphG.alpha = disabled ? 0.5 : 1;
1492
+ this.glyphG.visible = true;
1493
+ } else if (this.glyph === "menu") {
1494
+ const barW = this.dark ? 6 : 4;
1495
+ const spread = this.dark ? 9 : 8;
1496
+ for (const dy of [-spread, 0, spread]) g.moveTo(-14, dy).lineTo(14, dy).stroke({ width: barW, color: ink, cap: "round" });
1497
+ } else if (this.glyph === "close") {
1498
+ g.moveTo(-10, -10).lineTo(10, 10).moveTo(10, -10).lineTo(-10, 10).stroke({ width: 4, color: ink });
1499
+ }
1500
+ } else {
1501
+ const w = this.labelText ? this.labelText.width + 48 : 120;
1502
+ const h = this.pillHeight;
1503
+ g.roundRect(-w / 2, -h / 2, w, h, h / 2).fill({ color: t.color.surfaceAlt }).stroke({ width: 3, color: t.color.accent });
1504
+ }
1505
+ if (this.labelText) this.labelText.style.fill = ink;
1506
+ }
1507
+ play(t) {
1508
+ this.animating = true;
1509
+ void this.tween.run(this.art, t, this.ui.theme).then(() => {
1510
+ this.animating = false;
1511
+ });
1512
+ }
1513
+ dispose() {
1514
+ this.tween.stop();
1515
+ super.dispose();
1516
+ }
1517
+ };
1518
+ var TurboView = class extends ControlView {
1519
+ constructor(turbo, ui, ticker, opts = {}) {
1520
+ super(turbo, ui);
1521
+ this.turbo = turbo;
1522
+ this.tween = new Tweener(ticker);
1523
+ this.modeTex = opts.modeTextures?.length ? opts.modeTextures : void 0;
1524
+ this.offTex = opts.offTexture;
1525
+ this.onTex = opts.onTexture;
1526
+ this.target = opts.target ?? 84;
1527
+ this.radius = opts.radius ?? 40;
1528
+ const hasArt = this.modeTex || this.offTex || this.onTex;
1529
+ if (hasArt) {
1530
+ this.sprite = new Sprite(this.texFor());
1531
+ this.sprite.anchor.set(0.5);
1532
+ this.art.addChild(this.sprite);
1533
+ } else {
1534
+ this.art.addChild(this.bg);
1535
+ }
1536
+ this.addChild(this.art);
1537
+ this.eventMode = "static";
1538
+ this.cursor = "pointer";
1539
+ this.hitArea = new Circle(0, 0, (this.sprite ? this.target / 2 : this.radius) + 8);
1540
+ this.on("pointerup", this.onUp);
1541
+ this.disposers.push(
1542
+ this.turbo.state.subscribe(() => this.redraw()),
1543
+ this.turbo.index.subscribe(() => this.redraw()),
1544
+ this.turbo.onTransition((t) => this.play(t))
1545
+ );
1546
+ this.redraw();
1547
+ }
1548
+ turbo;
1549
+ art = new Container();
1550
+ bg = new Graphics();
1551
+ sprite;
1552
+ tween;
1553
+ modeTex;
1554
+ offTex;
1555
+ onTex;
1556
+ target;
1557
+ radius;
1558
+ onUp = () => {
1559
+ this.turbo.cycle();
1560
+ };
1561
+ texFor() {
1562
+ const i = this.turbo.index.get();
1563
+ if (this.modeTex) return this.modeTex[Math.min(i, this.modeTex.length - 1)] ?? this.modeTex[0];
1564
+ return i > 0 ? this.onTex ?? this.offTex : this.offTex ?? this.onTex;
1565
+ }
1566
+ fit() {
1567
+ if (!this.sprite) return;
1568
+ this.sprite.scale.set(1);
1569
+ this.sprite.scale.set(this.target / this.sprite.height);
1570
+ }
1571
+ redraw() {
1572
+ const th = this.ui.theme;
1573
+ const i = this.turbo.index.get();
1574
+ const disabled = this.turbo.current === "disabled";
1575
+ if (this.sprite) {
1576
+ const tex = this.texFor();
1577
+ if (tex) this.sprite.texture = tex;
1578
+ this.fit();
1579
+ this.sprite.alpha = disabled ? 0.5 : 1;
1580
+ } else {
1581
+ const lit = i > 0;
1582
+ const r = this.radius;
1583
+ this.bg.clear();
1584
+ this.bg.circle(0, 0, r).fill({ color: lit ? th.color.accent : th.color.surface, alpha: lit ? Math.min(1, 0.55 + i * 0.25) : 1 }).stroke({ width: 4, color: th.color.accent });
1585
+ this.bg.poly([-6, -r * 0.42, 8, -4, 0, -4, 7, r * 0.42, -9, 2, 0, 2]).fill({ color: lit ? th.color.accentText : th.color.accent, alpha: disabled ? 0.5 : 1 });
1586
+ }
1587
+ }
1588
+ play(t) {
1589
+ this.animating = true;
1590
+ void this.tween.run(this.art, t, this.ui.theme).then(() => {
1591
+ this.animating = false;
1592
+ });
1593
+ }
1594
+ dispose() {
1595
+ this.tween.stop();
1596
+ super.dispose();
1597
+ }
1598
+ };
1599
+ var DEG = Math.PI / 180;
1600
+ var AutoplayView = class extends ControlView {
1601
+ constructor(auto, ui, ticker, opts = {}) {
1602
+ super(auto, ui);
1603
+ this.auto = auto;
1604
+ this.ticker = ticker;
1605
+ this.idleTex = opts.idleTexture;
1606
+ this.activeTex = opts.activeTexture;
1607
+ this.target = opts.target ?? 88;
1608
+ this.radius = opts.radius ?? 44;
1609
+ this.useRadial = opts.picker === "radial";
1610
+ this.art.addChild(this.bg);
1611
+ if (this.idleTex || this.activeTex) {
1612
+ this.sprite = new Sprite(this.idleTex ?? this.activeTex);
1613
+ this.sprite.anchor.set(0.5);
1614
+ this.fit();
1615
+ this.art.addChild(this.sprite);
1616
+ }
1617
+ this.countText = new Text({ text: "", style: { fontFamily: ui.theme.type.family, fontSize: 26, fill: ui.theme.color.accentText, fontWeight: "800" } });
1618
+ this.countText.anchor.set(0.5);
1619
+ this.addChild(this.chipLayer, this.art, this.countText);
1620
+ this.eventMode = "static";
1621
+ this.cursor = "pointer";
1622
+ this.hitArea = new Circle(0, 0, (this.sprite ? this.target / 2 : this.radius) + 8);
1623
+ this.on("pointerup", () => this.auto.press());
1624
+ if (this.useRadial) this.buildChips(opts);
1625
+ this.tick = (t) => this.update(t.deltaMS);
1626
+ this.disposers.push(
1627
+ this.auto.state.subscribe(() => this.onState()),
1628
+ this.auto.count.subscribe(() => this.drawButton())
1629
+ );
1630
+ this.drawButton();
1631
+ }
1632
+ auto;
1633
+ ticker;
1634
+ art = new Container();
1635
+ // the rotating button
1636
+ bg = new Graphics();
1637
+ sprite;
1638
+ countText;
1639
+ chipLayer = new Container();
1640
+ // chips stay upright (don't rotate)
1641
+ chips = [];
1642
+ idleTex;
1643
+ activeTex;
1644
+ target;
1645
+ radius;
1646
+ rot = 0;
1647
+ rotTarget = 0;
1648
+ phase = 0;
1649
+ // ms since last state change (drives the stagger)
1650
+ running = false;
1651
+ useRadial;
1652
+ tick;
1653
+ fit() {
1654
+ if (!this.sprite) return;
1655
+ this.sprite.scale.set(1);
1656
+ this.sprite.scale.set(this.target / this.sprite.height);
1657
+ }
1658
+ fmt(n) {
1659
+ return n === Infinity ? "\u221E" : String(n);
1660
+ }
1661
+ buildChips(opts) {
1662
+ const cx = opts.arcCenter?.x ?? 0;
1663
+ const cy = opts.arcCenter?.y ?? 0;
1664
+ const R = opts.arcRadius ?? (Math.hypot(cx, cy) || 220);
1665
+ const start = Math.atan2(-cy, -cx);
1666
+ const step = (opts.arcStepDeg ?? 24) * DEG * (opts.arcDir ?? 1);
1667
+ const th = this.ui.theme;
1668
+ const CR = 34;
1669
+ this.auto.options.forEach((opt, i) => {
1670
+ const ang = start + (i + 1) * step;
1671
+ const node = new Container();
1672
+ node.addChild(new Graphics().circle(0, 0, CR).fill({ color: 16777215 }).stroke({ width: 4, color: 0 }));
1673
+ const t = new Text({ text: this.fmt(opt), style: { fontFamily: th.type.family, fontSize: 25, fill: 1382172, fontWeight: "800" } });
1674
+ t.anchor.set(0.5);
1675
+ node.addChild(t);
1676
+ node.position.set(cx + R * Math.cos(ang), cy + R * Math.sin(ang));
1677
+ node.scale.set(0);
1678
+ node.visible = false;
1679
+ node.eventMode = "static";
1680
+ node.cursor = "pointer";
1681
+ node.hitArea = new Circle(0, 0, CR + 4);
1682
+ node.on("pointerup", (e) => {
1683
+ e.stopPropagation();
1684
+ this.auto.pick(opt);
1685
+ });
1686
+ this.chipLayer.addChild(node);
1687
+ this.chips.push({ node, scale: 0 });
1688
+ });
1689
+ }
1690
+ onState() {
1691
+ this.rotTarget = this.useRadial && this.auto.current === "picking" ? -60 * DEG : 0;
1692
+ this.phase = 0;
1693
+ this.drawButton();
1694
+ if (!this.running) {
1695
+ this.running = true;
1696
+ this.ticker.add(this.tick);
1697
+ }
1698
+ }
1699
+ update(dt) {
1700
+ this.phase += Math.min(dt, 40);
1701
+ this.rot += (this.rotTarget - this.rot) * Math.min(1, dt / 110);
1702
+ this.art.rotation = this.rot;
1703
+ const opening = this.auto.current === "picking";
1704
+ const n = this.chips.length;
1705
+ let settled = Math.abs(this.rot - this.rotTarget) < 3e-3;
1706
+ for (let i = 0; i < n; i++) {
1707
+ const chip = this.chips[i];
1708
+ let target;
1709
+ let ease;
1710
+ if (opening) {
1711
+ target = this.phase > i * 70 ? 1 : 0;
1712
+ ease = Math.min(1, dt / 95);
1713
+ } else {
1714
+ target = this.phase > (n - 1 - i) * 36 ? 0 : 1;
1715
+ ease = Math.min(1, dt / 42);
1716
+ }
1717
+ chip.scale += (target - chip.scale) * ease;
1718
+ chip.node.scale.set(chip.scale);
1719
+ chip.node.visible = chip.scale > 0.01;
1720
+ if (Math.abs(chip.scale - (opening ? 1 : 0)) > 0.01) settled = false;
1721
+ }
1722
+ if (settled) {
1723
+ this.running = false;
1724
+ this.ticker.remove(this.tick);
1725
+ }
1726
+ }
1727
+ drawButton() {
1728
+ const st = this.auto.current;
1729
+ this.countText.visible = false;
1730
+ const th = this.ui.theme;
1731
+ const active = st === "active";
1732
+ this.eventMode = active ? "none" : "static";
1733
+ this.cursor = active ? "default" : "pointer";
1734
+ if (active) {
1735
+ this.bg.clear();
1736
+ if (this.sprite) {
1737
+ this.sprite.visible = true;
1738
+ this.sprite.tint = th.color.accent;
1739
+ this.sprite.alpha = 0.9;
1740
+ } else {
1741
+ this.bg.circle(0, 0, this.radius).fill({ color: th.color.accent }).stroke({ width: 4, color: th.color.accentText });
1742
+ }
1743
+ return;
1744
+ }
1745
+ if (this.sprite) {
1746
+ this.sprite.visible = true;
1747
+ this.sprite.tint = 16777215;
1748
+ this.bg.clear();
1749
+ const tex = this.idleTex ?? this.activeTex;
1750
+ if (tex) this.sprite.texture = tex;
1751
+ this.fit();
1752
+ this.sprite.alpha = st === "disabled" ? 0.5 : 1;
1753
+ } else {
1754
+ this.bg.clear();
1755
+ this.bg.circle(0, 0, this.radius).fill({ color: th.color.surface }).stroke({ width: 4, color: th.color.accent });
1756
+ }
1757
+ }
1758
+ dispose() {
1759
+ if (this.running) {
1760
+ this.ticker.remove(this.tick);
1761
+ this.running = false;
1762
+ }
1763
+ super.dispose();
1764
+ }
1765
+ };
1766
+ var WHITE = 16777215;
1767
+ var BLACK = 657930;
1768
+ var DARK = 789776;
1769
+ var CHIP = 2303532;
1770
+ var DIM = 10133670;
1771
+ var AutoplayDrawerView = class extends Container {
1772
+ constructor(auto, ui, ticker) {
1773
+ super();
1774
+ this.auto = auto;
1775
+ this.ui = ui;
1776
+ this.ticker = ticker;
1777
+ this.zIndex = 200;
1778
+ this.visible = false;
1779
+ this.eventMode = "none";
1780
+ const fam = ui.theme.type.family;
1781
+ this.backdrop.eventMode = "static";
1782
+ this.backdrop.cursor = "pointer";
1783
+ this.backdrop.on("pointertap", () => this.auto.cancelPicker());
1784
+ this.title = new Text({ text: ui.t("Autoplay"), style: { fontFamily: fam, fontSize: 30, fill: WHITE, fontWeight: "800", letterSpacing: 1 } });
1785
+ this.title.anchor.set(0.5, 0);
1786
+ this.startLabel = new Text({ text: ui.t("Start"), style: { fontFamily: fam, fontSize: 26, fill: BLACK, fontWeight: "800", letterSpacing: 1 } });
1787
+ this.startLabel.anchor.set(0.5);
1788
+ this.startBtn.eventMode = "static";
1789
+ this.startBtn.cursor = "pointer";
1790
+ this.startBtn.on("pointertap", () => this.start());
1791
+ this.startBtn.addChild(this.startBg, this.startLabel);
1792
+ this.sheet.addChild(this.sheetBg, this.handle, this.title);
1793
+ this.addGroup("count", this.auto.options);
1794
+ if (this.auto.lossLimitOptions.length) this.addGroup("loss", this.auto.lossLimitOptions, "Stop on loss");
1795
+ if (this.auto.winLimitOptions.length) this.addGroup("win", this.auto.winLimitOptions, "Stop on single win");
1796
+ this.sheet.addChild(this.startBtn);
1797
+ this.addChild(this.backdrop, this.sheet);
1798
+ this.tick = (t) => this.update(t.deltaMS);
1799
+ this.disposers.push(this.auto.state.subscribe(() => this.onState()));
1800
+ this.disposers.push(
1801
+ this.ui.locale.subscribe(() => {
1802
+ if (this.destroyed) return;
1803
+ this.title.text = this.ui.t("Autoplay");
1804
+ this.startLabel.text = this.ui.t("Start");
1805
+ for (const g of this.groups) if (g.heading) g.heading.text = this.ui.t(headingKey(g.kind));
1806
+ })
1807
+ );
1808
+ }
1809
+ auto;
1810
+ ui;
1811
+ ticker;
1812
+ backdrop = new Graphics();
1813
+ sheet = new Container();
1814
+ sheetBg = new Graphics();
1815
+ handle = new Graphics();
1816
+ title;
1817
+ startBtn = new Container();
1818
+ startBg = new Graphics();
1819
+ startLabel;
1820
+ groups = [];
1821
+ screen;
1822
+ prog = 0;
1823
+ // 0 hidden → 1 fully shown
1824
+ running = false;
1825
+ sheetH = 300;
1826
+ kk = 1;
1827
+ // active layout scale (base scale shrunk to fit the viewport height)
1828
+ disposers = [];
1829
+ tick;
1830
+ addGroup(kind, values, headingText) {
1831
+ const fam = this.ui.theme.type.family;
1832
+ const group = { kind, chips: [], selected: 0 };
1833
+ if (headingText) {
1834
+ group.heading = new Text({ text: this.ui.t(headingText), style: { fontFamily: fam, fontSize: 16, fill: DIM, fontWeight: "700", letterSpacing: 0.5 } });
1835
+ group.heading.anchor.set(0.5, 0.5);
1836
+ this.sheet.addChild(group.heading);
1837
+ }
1838
+ values.forEach((value) => {
1839
+ const node = new Container();
1840
+ const bg = new Graphics();
1841
+ const label = new Text({ text: chipText(kind, value), style: { fontFamily: fam, fontSize: 24, fill: WHITE, fontWeight: "800" } });
1842
+ label.anchor.set(0.5);
1843
+ node.eventMode = "static";
1844
+ node.cursor = "pointer";
1845
+ const idx = group.chips.length;
1846
+ node.on("pointertap", () => this.select(group, idx));
1847
+ node.addChild(bg, label);
1848
+ this.sheet.addChild(node);
1849
+ group.chips.push({ node, bg, label, value });
1850
+ });
1851
+ group.selected = defaultSelected(kind, values);
1852
+ this.groups.push(group);
1853
+ }
1854
+ applyLayout(screen) {
1855
+ this.screen = screen;
1856
+ this.position.set(0, 0);
1857
+ this.scale.set(1);
1858
+ this.relayout();
1859
+ }
1860
+ /** Base scale from the screen's shorter edge (before fit-to-height). */
1861
+ baseK() {
1862
+ const s = this.screen;
1863
+ if (!s) return 1;
1864
+ return Math.max(0.78, Math.min(1.3, Math.min(s.width, s.height) / 1e3));
1865
+ }
1866
+ relayout() {
1867
+ const s = this.screen;
1868
+ if (!s) return;
1869
+ let k = this.baseK();
1870
+ let h = this.layoutAt(k);
1871
+ for (let pass = 0; pass < 4 && h > s.height; pass++) {
1872
+ k = Math.max(0.35, k * (s.height / h) * 0.97);
1873
+ h = this.layoutAt(k);
1874
+ }
1875
+ this.render2();
1876
+ }
1877
+ /** Lay the sheet out at scale `k`; returns the full slide height (sheetH + 40). */
1878
+ layoutAt(k) {
1879
+ const s = this.screen;
1880
+ if (!s) return 0;
1881
+ this.kk = k;
1882
+ const W = s.width;
1883
+ const H = s.height;
1884
+ this.backdrop.clear().rect(0, 0, W, H).fill({ color: 0, alpha: 1 });
1885
+ this.backdrop.hitArea = new Rectangle(0, 0, W, H);
1886
+ this.title.style.fontSize = 30 * k;
1887
+ this.title.position.set(W / 2, 22 * k);
1888
+ const cw = 86 * k;
1889
+ const ch = 64 * k;
1890
+ const gap = 14 * k;
1891
+ const maxRowW = W - 48 * k;
1892
+ let y = 22 * k + 30 * k + 20 * k;
1893
+ for (const g of this.groups) {
1894
+ if (g.heading) {
1895
+ g.heading.style.fontSize = 16 * k;
1896
+ g.heading.position.set(W / 2, y + 10 * k);
1897
+ y += 30 * k;
1898
+ }
1899
+ const n = g.chips.length;
1900
+ const perRow = Math.max(1, Math.min(n, Math.floor((maxRowW + gap) / (cw + gap))));
1901
+ const rows = Math.ceil(n / perRow);
1902
+ g.chips.forEach((chip, i) => {
1903
+ const row = Math.floor(i / perRow);
1904
+ const inRow = i % perRow;
1905
+ const countThisRow = Math.min(perRow, n - row * perRow);
1906
+ const rowW = countThisRow * cw + (countThisRow - 1) * gap;
1907
+ const x = (W - rowW) / 2 + inRow * (cw + gap) + cw / 2;
1908
+ const cy = y + row * (ch + gap) + ch / 2;
1909
+ chip.node.position.set(x, cy);
1910
+ chip.label.style.fontSize = 24 * k;
1911
+ chip.node.hitArea = new Rectangle(-cw / 2, -ch / 2, cw, ch);
1912
+ });
1913
+ this.drawGroup(g);
1914
+ y += rows * (ch + gap) + 12 * k;
1915
+ }
1916
+ const startW = Math.min(W - 48 * k, 320 * k);
1917
+ const startH = 58 * k;
1918
+ const startY = y + 4 * k + startH / 2;
1919
+ this.startBg.clear().rect(-startW / 2, -startH / 2, startW, startH).fill({ color: WHITE });
1920
+ this.startLabel.style.fontSize = 24 * k;
1921
+ this.startBtn.position.set(W / 2, startY);
1922
+ this.startBtn.hitArea = new Rectangle(-startW / 2, -startH / 2, startW, startH);
1923
+ this.sheetH = startY + startH / 2 + 20 * k;
1924
+ this.sheetBg.clear().rect(-1, 0, W + 2, this.sheetH + 40 + 600).fill({ color: DARK });
1925
+ this.handle.clear().roundRect(W / 2 - 30 * k, 13 * k, 60 * k, 5 * k, 2.5 * k).fill({ color: DIM });
1926
+ return this.sheetH + 40;
1927
+ }
1928
+ drawGroup(g) {
1929
+ const k = this.kk;
1930
+ const cw = 86 * k;
1931
+ const ch = 64 * k;
1932
+ g.chips.forEach((chip, idx) => {
1933
+ const on = idx === g.selected;
1934
+ chip.bg.clear().rect(-cw / 2, -ch / 2, cw, ch).fill({ color: on ? WHITE : CHIP });
1935
+ chip.label.style.fill = on ? BLACK : WHITE;
1936
+ });
1937
+ }
1938
+ select(group, i) {
1939
+ group.selected = i;
1940
+ this.drawGroup(group);
1941
+ }
1942
+ valueOf(kind) {
1943
+ const g = this.groups.find((x) => x.kind === kind);
1944
+ return g?.chips[g.selected]?.value;
1945
+ }
1946
+ start() {
1947
+ const count = this.valueOf("count") ?? this.auto.options[0] ?? 1;
1948
+ this.auto.pick(count, {
1949
+ lossLimit: this.valueOf("loss") ?? Infinity,
1950
+ singleWinLimit: this.valueOf("win") ?? Infinity
1951
+ });
1952
+ }
1953
+ onState() {
1954
+ if (this.auto.current === "picking") this.visible = true;
1955
+ if (!this.running) {
1956
+ this.running = true;
1957
+ this.ticker.add(this.tick);
1958
+ }
1959
+ }
1960
+ update(dt) {
1961
+ const target = this.auto.current === "picking" ? 1 : 0;
1962
+ this.prog += (target - this.prog) * Math.min(1, dt / 140);
1963
+ if (Math.abs(this.prog - target) < 4e-3) {
1964
+ this.prog = target;
1965
+ this.running = false;
1966
+ this.ticker.remove(this.tick);
1967
+ }
1968
+ this.render2();
1969
+ }
1970
+ render2() {
1971
+ const s = this.screen;
1972
+ if (!s) return;
1973
+ const SH = this.sheetH + 40;
1974
+ this.sheet.position.set(0, s.height - SH * this.prog);
1975
+ this.backdrop.alpha = 0.62 * this.prog;
1976
+ const shown = this.prog > 0.01;
1977
+ this.visible = shown;
1978
+ this.eventMode = shown ? "auto" : "none";
1979
+ this.backdrop.eventMode = shown ? "static" : "none";
1980
+ }
1981
+ dispose() {
1982
+ if (this.running) {
1983
+ this.ticker.remove(this.tick);
1984
+ this.running = false;
1985
+ }
1986
+ for (const d of this.disposers) d();
1987
+ this.disposers.length = 0;
1988
+ if (!this.destroyed) this.destroy({ children: true });
1989
+ }
1990
+ };
1991
+ function chipText(kind, value) {
1992
+ if (value === Infinity) return "\u221E";
1993
+ return kind === "count" ? String(value) : `${value}\xD7`;
1994
+ }
1995
+ function defaultSelected(kind, values) {
1996
+ if (kind === "count") {
1997
+ const i = values.findIndex((v) => v !== Infinity);
1998
+ return i >= 0 ? i : 0;
1999
+ }
2000
+ const inf = values.indexOf(Infinity);
2001
+ return inf >= 0 ? inf : 0;
2002
+ }
2003
+ function headingKey(kind) {
2004
+ return kind === "loss" ? "Stop on loss" : kind === "win" ? "Stop on single win" : "Autoplay";
2005
+ }
2006
+ var VB_W = 290;
2007
+ var VB_H = 60;
2008
+ var GROOVE_X0 = 63;
2009
+ var GROOVE_X1 = 263;
2010
+ var HANDLE_Y = 30;
2011
+ var HANDLE_R = 12;
2012
+ var DISPLAY_W = 260;
2013
+ var S = DISPLAY_W / VB_W;
2014
+ var SliderView = class extends ControlView {
2015
+ constructor(slider, ui, trackTexture) {
2016
+ super(slider, ui);
2017
+ this.slider = slider;
2018
+ this.hasTexture = !!trackTexture;
2019
+ const cy = HANDLE_Y * S;
2020
+ const x0 = GROOVE_X0 * S;
2021
+ const x1 = GROOVE_X1 * S;
2022
+ const trackH = 8 * S;
2023
+ if (trackTexture) {
2024
+ const track = new Sprite(trackTexture);
2025
+ track.width = VB_W * S;
2026
+ track.height = VB_H * S;
2027
+ this.addChild(track);
2028
+ } else {
2029
+ const t = ui.theme;
2030
+ const track = new Graphics().roundRect(x0 - trackH / 2, cy - trackH / 2, x1 - x0 + trackH, trackH, trackH / 2).fill({ color: t.color.surfaceAlt }).roundRect(x0 - trackH / 2, cy - trackH / 2, x1 - x0 + trackH, trackH, trackH / 2).stroke({ width: 1, color: t.color.textDim, alpha: 0.35 });
2031
+ this.addChild(track);
2032
+ }
2033
+ this.addChild(this.handle);
2034
+ this.eventMode = "static";
2035
+ this.cursor = "pointer";
2036
+ this.hitArea = new Rectangle(0, 0, VB_W * S, VB_H * S);
2037
+ this.on("pointerdown", this.onDown);
2038
+ this.on("globalpointermove", this.onMove);
2039
+ this.on("pointerup", this.onUp);
2040
+ this.on("pointerupoutside", this.onUp);
2041
+ this.disposers.push(this.slider.value.subscribe(() => this.redraw()));
2042
+ this.redraw();
2043
+ }
2044
+ slider;
2045
+ handle = new Graphics();
2046
+ hasTexture;
2047
+ dragging = false;
2048
+ redraw() {
2049
+ const cy = HANDLE_Y * S;
2050
+ const x0 = GROOVE_X0 * S;
2051
+ const x1 = GROOVE_X1 * S;
2052
+ const x = x0 + this.slider.value.get() * (x1 - x0);
2053
+ if (this.hasTexture) {
2054
+ this.handle.clear().circle(x, cy, HANDLE_R * S).fill({ color: 16777215 }).stroke({ width: 4 * S, color: 0 });
2055
+ return;
2056
+ }
2057
+ const t = this.ui.theme;
2058
+ const trackH = 8 * S;
2059
+ const knobR = 11 * S;
2060
+ this.handle.clear().roundRect(x0 - trackH / 2, cy - trackH / 2, x - x0 + trackH, trackH, trackH / 2).fill({ color: t.color.accent }).circle(x, cy, knobR).fill({ color: 16777215 }).circle(x, cy, knobR).stroke({ width: 3, color: t.color.accent });
2061
+ }
2062
+ valueFromEvent(e) {
2063
+ const svgX = this.toLocal(e.global).x / S;
2064
+ return Math.min(1, Math.max(0, (svgX - GROOVE_X0) / (GROOVE_X1 - GROOVE_X0)));
2065
+ }
2066
+ onDown = (e) => {
2067
+ this.dragging = true;
2068
+ this.slider.beginDrag();
2069
+ this.slider.setNormalized(this.valueFromEvent(e));
2070
+ };
2071
+ onMove = (e) => {
2072
+ if (this.dragging) this.slider.setNormalized(this.valueFromEvent(e));
2073
+ };
2074
+ onUp = () => {
2075
+ if (this.dragging) {
2076
+ this.dragging = false;
2077
+ this.slider.endDrag();
2078
+ }
2079
+ };
2080
+ };
2081
+ var ToggleView = class extends ControlView {
2082
+ constructor(toggle, ui, ticker, opts = {}) {
2083
+ super(toggle, ui);
2084
+ this.toggle = toggle;
2085
+ this.tween = new Tweener(ticker);
2086
+ this.offTex = opts.offTexture;
2087
+ this.onTex = opts.onTexture;
2088
+ this.target = opts.target ?? 84;
2089
+ this.radius = opts.radius ?? 40;
2090
+ if (this.offTex || this.onTex) {
2091
+ this.sprite = new Sprite(this.texFor());
2092
+ this.sprite.anchor.set(0.5);
2093
+ this.fit();
2094
+ this.art.addChild(this.sprite);
2095
+ } else {
2096
+ this.art.addChild(this.bg);
2097
+ }
2098
+ this.addChild(this.art);
2099
+ this.eventMode = "static";
2100
+ this.cursor = "pointer";
2101
+ this.hitArea = this.sprite ? new Circle(0, 0, this.target / 2 + 8) : new Rectangle(-35, -23, 70, 46);
2102
+ this.on("pointerup", this.onUp);
2103
+ this.disposers.push(
2104
+ this.toggle.state.subscribe(() => this.redraw()),
2105
+ this.toggle.onTransition((t) => this.play(t))
2106
+ );
2107
+ this.redraw();
2108
+ }
2109
+ toggle;
2110
+ art = new Container();
2111
+ bg = new Graphics();
2112
+ sprite;
2113
+ tween;
2114
+ offTex;
2115
+ onTex;
2116
+ target;
2117
+ radius;
2118
+ onUp = () => {
2119
+ this.toggle.toggle();
2120
+ };
2121
+ texFor() {
2122
+ return this.toggle.isOn ? this.onTex ?? this.offTex : this.offTex ?? this.onTex;
2123
+ }
2124
+ fit() {
2125
+ if (!this.sprite) return;
2126
+ this.sprite.scale.set(1);
2127
+ this.sprite.scale.set(this.target / this.sprite.height);
2128
+ }
2129
+ redraw() {
2130
+ if (this.sprite) {
2131
+ const tex = this.texFor();
2132
+ if (tex) this.sprite.texture = tex;
2133
+ this.fit();
2134
+ this.sprite.alpha = this.toggle.current === "disabled" ? 0.5 : 1;
2135
+ return;
2136
+ }
2137
+ const th = this.ui.theme;
2138
+ const on = this.toggle.isOn;
2139
+ const w = 54;
2140
+ const h = 30;
2141
+ const r = h / 2;
2142
+ const knobX = on ? w / 2 - r : -12;
2143
+ this.bg.clear().roundRect(-w / 2, -h / 2, w, h, r).fill({ color: on ? th.color.accent : th.color.surfaceAlt }).roundRect(-w / 2, -h / 2, w, h, r).stroke({ width: 2, color: on ? th.color.accent : th.color.textDim, alpha: on ? 1 : 0.6 }).circle(knobX, 0, r - 4).fill({ color: 16777215 });
2144
+ }
2145
+ play(t) {
2146
+ this.animating = true;
2147
+ void this.tween.run(this.art, t, this.ui.theme).then(() => {
2148
+ this.animating = false;
2149
+ });
2150
+ }
2151
+ dispose() {
2152
+ this.tween.stop();
2153
+ super.dispose();
2154
+ }
2155
+ };
2156
+ var SelectView = class extends ControlView {
2157
+ constructor(select, ui, ticker, opts = {}) {
2158
+ super(select, ui);
2159
+ this.select = select;
2160
+ this.tween = new Tweener(ticker);
2161
+ this.w = opts.width ?? 240;
2162
+ this.h = opts.height ?? 56;
2163
+ this.dropdownLayer = opts.dropdownLayer;
2164
+ const t = ui.theme;
2165
+ this.captionText = new Text({ text: select.caption ?? "", style: { fontFamily: t.type.family, fontSize: 16, fill: t.color.textDim, fontWeight: "600" } });
2166
+ this.captionText.anchor.set(0, 0.5);
2167
+ this.valueText = new Text({ text: select.optionLabel, style: { fontFamily: t.type.family, fontSize: 20, fill: t.color.text, fontWeight: "700" } });
2168
+ this.valueText.anchor.set(1, 0.5);
2169
+ this.art.addChild(this.bg, this.captionText, this.valueText, this.chevron);
2170
+ this.addChild(this.art);
2171
+ this.eventMode = "static";
2172
+ this.cursor = "pointer";
2173
+ this.hitArea = new Rectangle(-this.w / 2, -this.h / 2, this.w, this.h);
2174
+ this.on("pointerup", this.onUp);
2175
+ this.disposers.push(
2176
+ this.select.index.subscribe(() => this.redraw()),
2177
+ this.select.state.subscribe(() => {
2178
+ this.redraw();
2179
+ this.syncDropdown();
2180
+ }),
2181
+ this.select.onTransition((tr) => this.play(tr)),
2182
+ this.ui.locale.subscribe(() => {
2183
+ if (!this.destroyed) this.redraw();
2184
+ })
2185
+ );
2186
+ this.redraw();
2187
+ }
2188
+ select;
2189
+ art = new Container();
2190
+ bg = new Graphics();
2191
+ chevron = new Graphics();
2192
+ captionText;
2193
+ valueText;
2194
+ tween;
2195
+ w;
2196
+ h;
2197
+ dropdownLayer;
2198
+ dropdown;
2199
+ onUp = () => {
2200
+ if (!this.select.interactable) return;
2201
+ if (this.dropdownLayer) this.select.isOpen ? this.select.closeList() : this.select.openList();
2202
+ else this.select.cycle();
2203
+ };
2204
+ syncDropdown() {
2205
+ if (!this.dropdownLayer) return;
2206
+ if (this.select.isOpen) this.showDropdown();
2207
+ else this.hideDropdown();
2208
+ }
2209
+ showDropdown() {
2210
+ this.hideDropdown();
2211
+ const layer = this.dropdownLayer;
2212
+ if (!layer) return;
2213
+ const t = this.ui.theme;
2214
+ const w = Math.max(this.w, 220);
2215
+ const rowH = 46;
2216
+ const opts = this.select.options;
2217
+ const listH = opts.length * rowH + 8;
2218
+ const panel = new Container();
2219
+ panel.zIndex = 1;
2220
+ const backdrop = new Graphics().rect(-6e3, -6e3, 12e3, 12e3).fill({ color: 0, alpha: 1e-3 });
2221
+ backdrop.eventMode = "static";
2222
+ backdrop.on("pointertap", () => this.select.closeList());
2223
+ panel.addChild(backdrop);
2224
+ const list = new Container();
2225
+ list.addChild(new Graphics().roundRect(-w / 2, 0, w, listH, 14).fill({ color: t.color.surface }).stroke({ width: 2, color: t.color.accent }));
2226
+ opts.forEach((o, i) => {
2227
+ const selected = i === this.select.index.get();
2228
+ const cy = 4 + i * rowH + rowH / 2;
2229
+ const row = new Container();
2230
+ if (selected) row.addChild(new Graphics().roundRect(-w / 2 + 5, 4 + i * rowH, w - 10, rowH, 9).fill({ color: t.color.accent }));
2231
+ const label = new Text({ text: this.ui.t(o.label), style: { fontFamily: t.type.family, fontSize: 18, fill: selected ? t.color.accentText : t.color.text, fontWeight: "700" } });
2232
+ label.anchor.set(0, 0.5);
2233
+ label.position.set(-w / 2 + 20, cy);
2234
+ row.addChild(label);
2235
+ row.eventMode = "static";
2236
+ row.cursor = "pointer";
2237
+ row.hitArea = new Rectangle(-w / 2, 4 + i * rowH, w, rowH);
2238
+ row.on("pointertap", (e) => {
2239
+ e.stopPropagation();
2240
+ this.select.choose(o.value);
2241
+ });
2242
+ list.addChild(row);
2243
+ });
2244
+ panel.addChild(list);
2245
+ const g = this.toGlobal({ x: 0, y: this.h / 2 + 6 });
2246
+ const screenH = typeof window !== "undefined" ? window.innerHeight : 1080;
2247
+ const py = listH > screenH - 16 ? 8 : Math.min(g.y, screenH - 8 - listH);
2248
+ list.position.set(g.x, Math.max(8, py));
2249
+ layer.addChild(panel);
2250
+ this.dropdown = panel;
2251
+ }
2252
+ hideDropdown() {
2253
+ if (this.dropdown && !this.dropdown.destroyed) this.dropdown.destroy({ children: true });
2254
+ this.dropdown = void 0;
2255
+ }
2256
+ redraw() {
2257
+ const t = this.ui.theme;
2258
+ const disabled = this.select.current === "disabled";
2259
+ const open = this.select.isOpen;
2260
+ const w = this.w;
2261
+ const h = this.h;
2262
+ const edge = disabled ? t.color.disabled : t.color.accent;
2263
+ this.bg.clear().roundRect(-w / 2, -h / 2, w, h, h / 2).fill({ color: t.color.surfaceAlt }).stroke({ width: 3, color: edge });
2264
+ this.captionText.text = this.ui.t(this.select.caption ?? "");
2265
+ this.captionText.position.set(-w / 2 + 18, 0);
2266
+ this.captionText.style.fill = t.color.textDim;
2267
+ this.valueText.text = this.ui.t(this.select.optionLabel);
2268
+ this.valueText.position.set(w / 2 - 32, 0);
2269
+ this.valueText.style.fill = disabled ? t.color.textDim : t.color.text;
2270
+ const cx = w / 2 - 16;
2271
+ const dir = open ? -1 : 1;
2272
+ this.chevron.clear().moveTo(cx - 5, -4 * dir).lineTo(cx, 4 * dir).lineTo(cx + 5, -4 * dir).stroke({ width: 3, color: edge, cap: "round", join: "round" });
2273
+ this.art.alpha = disabled ? 0.6 : 1;
2274
+ }
2275
+ play(tr) {
2276
+ this.animating = true;
2277
+ void this.tween.run(this.art, tr, this.ui.theme).then(() => {
2278
+ this.animating = false;
2279
+ });
2280
+ }
2281
+ dispose() {
2282
+ this.hideDropdown();
2283
+ this.tween.stop();
2284
+ super.dispose();
2285
+ }
2286
+ };
2287
+ var StepperView = class extends ControlView {
2288
+ constructor(stepper, ui, _ticker) {
2289
+ super(stepper, ui);
2290
+ this.stepper = stepper;
2291
+ const t = ui.theme;
2292
+ if (stepper.label) {
2293
+ this.captionText = new Text({
2294
+ text: ui.t(stepper.label),
2295
+ style: { fontFamily: t.type.family, fontSize: 14, fill: t.color.textDim, fontWeight: "600" }
2296
+ });
2297
+ this.captionText.anchor.set(0.5);
2298
+ this.captionText.position.set(0, -22);
2299
+ this.addChild(this.captionText);
2300
+ }
2301
+ this.valueText = new Text({
2302
+ text: "",
2303
+ style: { fontFamily: t.type.family, fontSize: 20, fill: t.color.text, fontWeight: "700" }
2304
+ });
2305
+ this.valueText.anchor.set(0.5);
2306
+ this.valueText.position.set(0, this.captionText ? 6 : 0);
2307
+ this.minus.position.set(-64, this.valueText.y);
2308
+ this.plus.position.set(64, this.valueText.y);
2309
+ this.minus.hitArea = new Circle(0, 0, 20);
2310
+ this.plus.hitArea = new Circle(0, 0, 20);
2311
+ this.minus.on("pointerup", () => this.stepper.dec());
2312
+ this.plus.on("pointerup", () => this.stepper.inc());
2313
+ this.addChild(this.minus, this.valueText, this.plus);
2314
+ this.disposers.push(
2315
+ this.stepper.index.subscribe(() => this.redraw()),
2316
+ this.ui.locale.subscribe(() => {
2317
+ if (!this.destroyed) this.redraw();
2318
+ })
2319
+ );
2320
+ this.redraw();
2321
+ }
2322
+ stepper;
2323
+ minus = new Graphics();
2324
+ plus = new Graphics();
2325
+ valueText;
2326
+ captionText;
2327
+ fmt(v) {
2328
+ return Number.isInteger(v) ? String(v) : v.toFixed(2);
2329
+ }
2330
+ redraw() {
2331
+ const t = this.ui.theme;
2332
+ if (this.captionText) this.captionText.text = this.ui.t(this.stepper.label ?? "");
2333
+ this.valueText.text = this.fmt(this.stepper.value);
2334
+ drawStepButton(this.minus, "minus", this.stepper.canDec, t);
2335
+ drawStepButton(this.plus, "plus", this.stepper.canInc, t);
2336
+ }
2337
+ };
2338
+ function drawStepButton(g, kind, enabled, t) {
2339
+ const col = enabled ? t.color.accent : t.color.disabled;
2340
+ g.clear();
2341
+ g.circle(0, 0, 18).fill({ color: t.color.surfaceAlt }).stroke({ width: 3, color: col });
2342
+ g.moveTo(-7, 0).lineTo(7, 0).stroke({ width: 3, color: col, cap: "round" });
2343
+ if (kind === "plus") g.moveTo(0, -7).lineTo(0, 7).stroke({ width: 3, color: col, cap: "round" });
2344
+ g.alpha = enabled ? 1 : 0.5;
2345
+ g.eventMode = enabled ? "static" : "none";
2346
+ g.cursor = enabled ? "pointer" : "default";
2347
+ }
2348
+ var ROW_H = 72;
2349
+ var PAD = 28;
2350
+ var GAP = 16;
2351
+ function buildBlockColumn(blocks, controls, ui, ticker, bodyW, opts = {}) {
2352
+ const t = ui.theme;
2353
+ const tr = (s) => ui.t(s);
2354
+ const innerW = bodyW - PAD * 2;
2355
+ const byId = new Map(controls.map((c) => [c.id, c]));
2356
+ const content = new Container();
2357
+ const views = [];
2358
+ let y = PAD;
2359
+ const placeFixed = (node, h) => {
2360
+ node.position.set(0, y + h / 2);
2361
+ content.addChild(node);
2362
+ y += h;
2363
+ };
2364
+ const placeAuto = (node, gap = GAP) => {
2365
+ const h = node.height || 1;
2366
+ node.position.set(0, y + h / 2);
2367
+ content.addChild(node);
2368
+ y += h + gap;
2369
+ };
2370
+ const heading = (s, size = 18) => {
2371
+ const wrap = new Container();
2372
+ const txt = new Text({
2373
+ text: tr(s),
2374
+ style: { fontFamily: t.type.family, fontSize: size, fill: t.color.text, fontWeight: "800", letterSpacing: 1 }
2375
+ });
2376
+ txt.anchor.set(0.5);
2377
+ txt.position.set(0, 0);
2378
+ const half = Math.min(txt.width / 2, innerW / 2 - 24);
2379
+ const gap = 16;
2380
+ wrap.addChild(
2381
+ new Graphics().moveTo(-innerW / 2, 0).lineTo(-half - gap, 0).stroke({ width: 2, color: t.color.text, alpha: 0.85 }),
2382
+ new Graphics().moveTo(half + gap, 0).lineTo(innerW / 2, 0).stroke({ width: 2, color: t.color.text, alpha: 0.85 }),
2383
+ txt
2384
+ );
2385
+ return wrap;
2386
+ };
2387
+ const body = (s) => richParagraphNode(s, ui, innerW, { fontFamily: t.type.family, fontSize: 15, fill: t.color.textDim, lineHeight: 22 });
2388
+ const subheading = (s) => {
2389
+ const wrap = new Container();
2390
+ const txt = new Text({ text: tr(s), style: { fontFamily: t.type.family, fontSize: 15, fill: t.color.text, fontWeight: "800", letterSpacing: 0.5 } });
2391
+ txt.anchor.set(0, 0.5);
2392
+ txt.position.set(-innerW / 2, 0);
2393
+ wrap.addChild(txt);
2394
+ return wrap;
2395
+ };
2396
+ const legal = (s) => richParagraphNode(s, ui, innerW, { fontFamily: t.type.family, fontSize: 12, fill: t.color.textDim, lineHeight: 17 });
2397
+ const caption = (s) => {
2398
+ const wrap = new Container();
2399
+ const txt = new Text({ text: s, style: { fontFamily: t.type.family, fontSize: 15, fill: t.color.text, fontWeight: "700" } });
2400
+ txt.anchor.set(0.5);
2401
+ wrap.addChild(txt);
2402
+ return wrap;
2403
+ };
2404
+ const hintNode = (s) => {
2405
+ const wrap = new Container();
2406
+ const txt = new Text({
2407
+ text: s,
2408
+ style: { fontFamily: t.type.family, fontSize: 12.5, fill: t.color.textDim, align: "center", wordWrap: true, wordWrapWidth: innerW * 0.86, lineHeight: 17 }
2409
+ });
2410
+ txt.anchor.set(0.5);
2411
+ wrap.addChild(txt);
2412
+ return wrap;
2413
+ };
2414
+ const makeView = (b, control) => {
2415
+ const skin = opts.controlSkins?.[b.id];
2416
+ if (skin) return skin(control, ui, ticker);
2417
+ switch (b.kind) {
2418
+ case "slider":
2419
+ return new SliderView(control, ui);
2420
+ case "toggle":
2421
+ return new ToggleView(control, ui, ticker, { radius: 22 });
2422
+ case "button":
2423
+ return new ButtonView(control, ui, ticker, { shape: "pill", height: 48 });
2424
+ case "select":
2425
+ return new SelectView(control, ui, ticker, { width: 320, dropdownLayer: opts.dropdownLayer });
2426
+ case "stepper":
2427
+ return new StepperView(control, ui, ticker);
2428
+ case "value":
2429
+ return new ValueDisplayView(control, ui, ticker);
2430
+ default:
2431
+ console.warn(`[open-ui] block column: no view for kind "${b.kind}" (id="${b.id}") \u2014 skipped`);
2432
+ return null;
2433
+ }
2434
+ };
2435
+ const walk = (bs) => {
2436
+ for (const b of bs) {
2437
+ switch (b.kind) {
2438
+ case "group":
2439
+ if (b.title) placeAuto(heading(b.title, 14), 10);
2440
+ walk(b.children);
2441
+ break;
2442
+ case "heading":
2443
+ placeAuto(heading(b.text), 12);
2444
+ break;
2445
+ case "subheading":
2446
+ placeAuto(subheading(b.text), 8);
2447
+ break;
2448
+ case "text":
2449
+ placeAuto(body(tr(b.text)));
2450
+ break;
2451
+ case "legal":
2452
+ placeAuto(legal(tr(b.text)));
2453
+ break;
2454
+ case "divider":
2455
+ placeAuto(dividerNode(innerW, ui), 10);
2456
+ break;
2457
+ case "callout":
2458
+ placeAuto(calloutNode(b, ui, innerW));
2459
+ break;
2460
+ case "stat-grid":
2461
+ placeAuto(statGridNode(b, ui, innerW));
2462
+ break;
2463
+ case "steps":
2464
+ placeAuto(body(b.items.map((s, i) => `${b.ordered ? `${i + 1}.` : "\u2022"} ${tr(s)}`).join("\n")));
2465
+ break;
2466
+ case "table":
2467
+ placeAuto(tableNode(b, ui, innerW));
2468
+ break;
2469
+ case "cards":
2470
+ placeAuto(cardsNode(b, ui, innerW));
2471
+ break;
2472
+ case "paytable":
2473
+ placeAuto(paytableNode(b, ui, innerW));
2474
+ break;
2475
+ case "media":
2476
+ placeAuto(mediaNode(b, ui, innerW));
2477
+ break;
2478
+ case "image": {
2479
+ let iw = b.width || 64;
2480
+ let ih = b.height || 64;
2481
+ if (iw > innerW) {
2482
+ ih = Math.round(ih * innerW / iw);
2483
+ iw = innerW;
2484
+ }
2485
+ placeAuto(imageBoxNode(b.src, iw, ih, ui));
2486
+ break;
2487
+ }
2488
+ default: {
2489
+ const control = byId.get(b.id);
2490
+ if (!control) break;
2491
+ if ((b.kind === "slider" || b.kind === "toggle") && b.label) placeAuto(caption(tr(b.label)), 6);
2492
+ const view = makeView(b, control);
2493
+ if (!view) break;
2494
+ views.push(view);
2495
+ const wrap = new Container();
2496
+ if (b.kind === "slider") view.position.set(-130, -27);
2497
+ wrap.addChild(view);
2498
+ placeFixed(wrap, ROW_H);
2499
+ const hint = b.hint;
2500
+ if (hint) placeAuto(hintNode(tr(hint)), 12);
2501
+ }
2502
+ }
2503
+ }
2504
+ };
2505
+ walk(blocks);
2506
+ return { content, views, height: y + PAD, innerW };
2507
+ }
2508
+ function richParagraphNode(s, ui, width, style) {
2509
+ const t = ui.theme;
2510
+ const c = new Container();
2511
+ const lineH = typeof style.lineHeight === "number" ? style.lineHeight : 22;
2512
+ const dim = style.fill;
2513
+ const runs = [];
2514
+ const re = /\*\*(.+?)\*\*/g;
2515
+ let last = 0;
2516
+ let m;
2517
+ while ((m = re.exec(s)) !== null) {
2518
+ if (m.index > last) runs.push({ text: s.slice(last, m.index), bold: false });
2519
+ runs.push({ text: m[1], bold: true });
2520
+ last = re.lastIndex;
2521
+ }
2522
+ if (last < s.length) runs.push({ text: s.slice(last), bold: false });
2523
+ let x = 0;
2524
+ let line = 0;
2525
+ const x0 = -width / 2;
2526
+ for (const run of runs) {
2527
+ for (const w of run.text.split(/(\n|\s+)/).filter((p) => p.length)) {
2528
+ if (w === "\n") {
2529
+ x = 0;
2530
+ line += 1;
2531
+ continue;
2532
+ }
2533
+ const isSpace = /^\s+$/.test(w);
2534
+ if (isSpace && x === 0) continue;
2535
+ const txt = new Text({ text: w, style: { ...style, fontWeight: run.bold ? "800" : style.fontWeight ?? "400", fill: run.bold ? t.color.text : dim } });
2536
+ txt.anchor.set(0, 0);
2537
+ if (!isSpace && x > 0 && x + txt.width > width) {
2538
+ x = 0;
2539
+ line += 1;
2540
+ }
2541
+ txt.position.set(x0 + x, line * lineH);
2542
+ c.addChild(txt);
2543
+ x += txt.width;
2544
+ }
2545
+ }
2546
+ const totalH = (line + 1) * lineH;
2547
+ for (const ch of c.children) ch.y -= totalH / 2;
2548
+ return c;
2549
+ }
2550
+ function calloutNode(b, ui, innerW) {
2551
+ const t = ui.theme;
2552
+ const tone = b.tone === "warning" ? "#ffb020" : t.color.accent;
2553
+ const c = new Container();
2554
+ const padX = 16;
2555
+ const padY = 12;
2556
+ const textW = innerW - padX * 2 - 4;
2557
+ const title = b.title ? new Text({ text: ui.t(b.title), style: { fontFamily: t.type.family, fontSize: 14, fill: tone, fontWeight: "800", letterSpacing: 0.3 } }) : null;
2558
+ title?.anchor.set(0, 0);
2559
+ const bodyNode = richParagraphNode(ui.t(b.text), ui, textW, { fontFamily: t.type.family, fontSize: 14, fill: t.color.text, lineHeight: 20 });
2560
+ const bodyH = bodyNode.height;
2561
+ const titleH = title ? title.height + 6 : 0;
2562
+ const h = padY * 2 + titleH + bodyH;
2563
+ const x = -innerW / 2;
2564
+ const bg = new Graphics().roundRect(x, -h / 2, innerW, h, 10).fill({ color: tone, alpha: 0.08 }).roundRect(x, -h / 2, innerW, h, 10).stroke({ width: 1.5, color: tone, alpha: 0.5 }).roundRect(x, -h / 2, 4, h, 2).fill({ color: tone });
2565
+ c.addChild(bg);
2566
+ if (title) {
2567
+ title.position.set(x + padX, -h / 2 + padY);
2568
+ c.addChild(title);
2569
+ }
2570
+ bodyNode.position.set(x + padX + textW / 2, -h / 2 + padY + titleH + bodyH / 2);
2571
+ c.addChild(bodyNode);
2572
+ return c;
2573
+ }
2574
+ function statGridNode(b, ui, innerW) {
2575
+ const t = ui.theme;
2576
+ const c = new Container();
2577
+ const rowH = 30;
2578
+ const totalH = Math.max(b.items.length * rowH, rowH);
2579
+ b.items.forEach((it, i) => {
2580
+ const cy = -totalH / 2 + rowH / 2 + i * rowH;
2581
+ if (i > 0) {
2582
+ c.addChild(new Graphics().moveTo(-innerW / 2, cy - rowH / 2).lineTo(innerW / 2, cy - rowH / 2).stroke({ width: 1, color: t.color.textDim, alpha: 0.18 }));
2583
+ }
2584
+ const label = new Text({ text: ui.t(it.label), style: { fontFamily: t.type.family, fontSize: 14, fill: t.color.textDim } });
2585
+ label.anchor.set(0, 0.5);
2586
+ label.position.set(-innerW / 2, cy);
2587
+ const value = new Text({ text: ui.t(it.value), style: { fontFamily: t.type.family, fontSize: 14, fill: t.color.text, fontWeight: "700" } });
2588
+ value.anchor.set(1, 0.5);
2589
+ value.position.set(innerW / 2, cy);
2590
+ c.addChild(label, value);
2591
+ });
2592
+ return c;
2593
+ }
2594
+ function dividerNode(innerW, ui) {
2595
+ const c = new Container();
2596
+ c.addChild(new Graphics().rect(-innerW / 2, -0.5, innerW, 1).fill({ color: ui.theme.color.textDim, alpha: 0.3 }));
2597
+ return c;
2598
+ }
2599
+ function tableNode(b, ui, innerW) {
2600
+ const t = ui.theme;
2601
+ const c = new Container();
2602
+ const head = b.columns && b.columns.length ? [b.columns] : [];
2603
+ const allRows = [...head, ...b.rows];
2604
+ const cols = Math.max(1, b.columns?.length ?? b.rows[0]?.length ?? 1);
2605
+ const colW = innerW / cols;
2606
+ const rowH = 30;
2607
+ const totalH = Math.max(allRows.length * rowH, rowH);
2608
+ const left = -innerW / 2;
2609
+ allRows.forEach((row, ri) => {
2610
+ const cy = -totalH / 2 + rowH / 2 + ri * rowH;
2611
+ const isHeader = head.length > 0 && ri === 0;
2612
+ if (ri > 0) {
2613
+ c.addChild(new Graphics().moveTo(left, cy - rowH / 2).lineTo(innerW / 2, cy - rowH / 2).stroke({ width: 1, color: t.color.textDim, alpha: 0.18 }));
2614
+ }
2615
+ for (let ci = 0; ci < cols; ci++) {
2616
+ const raw = row[ci] ?? "";
2617
+ const cx = left + ci * colW + 8;
2618
+ const fill = isHeader ? t.color.text : ci === 0 ? t.color.text : t.color.accent;
2619
+ const weight = isHeader ? "800" : ci === 0 ? "700" : "700";
2620
+ const txt = new Text({ text: ui.t(raw), style: { fontFamily: t.type.family, fontSize: 13, fill, fontWeight: weight } });
2621
+ txt.anchor.set(0, 0.5);
2622
+ txt.position.set(cx, cy);
2623
+ c.addChild(txt);
2624
+ }
2625
+ });
2626
+ return c;
2627
+ }
2628
+ function cardsNode(b, ui, innerW) {
2629
+ const t = ui.theme;
2630
+ const c = new Container();
2631
+ const n = b.items.length || 1;
2632
+ const gap = 12;
2633
+ const cols = Math.max(1, Math.min(n, Math.floor((innerW + gap) / (150 + gap))));
2634
+ const cardW = (innerW - gap * (cols - 1)) / cols;
2635
+ const rows = Math.ceil(n / cols);
2636
+ const cardH = 132;
2637
+ const totalH = rows * cardH + (rows - 1) * gap;
2638
+ b.items.forEach((it, i) => {
2639
+ const col = i % cols;
2640
+ const row = Math.floor(i / cols);
2641
+ const cardX = -innerW / 2 + col * (cardW + gap);
2642
+ const cardY = -totalH / 2 + row * (cardH + gap);
2643
+ const card = new Container();
2644
+ card.addChild(
2645
+ new Graphics().roundRect(0, 0, cardW, cardH, 12).fill({ color: t.color.surfaceAlt, alpha: 0.6 }).roundRect(0, 0, cardW, cardH, 12).stroke({ width: 1, color: t.color.textDim, alpha: 0.15 })
2646
+ );
2647
+ if (it.icon) {
2648
+ const icon = imageBoxNode(it.icon, 44, 44, ui);
2649
+ icon.position.set(cardW / 2, 34);
2650
+ card.addChild(icon);
2651
+ }
2652
+ const title = new Text({ text: ui.t(it.title), style: { fontFamily: t.type.family, fontSize: 14, fill: t.color.text, fontWeight: "800" } });
2653
+ title.anchor.set(0.5, 0);
2654
+ title.position.set(cardW / 2, it.icon ? 64 : 14);
2655
+ card.addChild(title);
2656
+ if (it.text) {
2657
+ const para = richParagraphNode(ui.t(it.text), ui, cardW - 20, { fontFamily: t.type.family, fontSize: 12, fill: t.color.textDim, lineHeight: 16 });
2658
+ para.position.set(cardW / 2, (it.icon ? 96 : 46) + para.height / 2);
2659
+ card.addChild(para);
2660
+ }
2661
+ card.position.set(cardX, cardY);
2662
+ c.addChild(card);
2663
+ });
2664
+ return c;
2665
+ }
2666
+ function mediaNode(b, ui, innerW) {
2667
+ const t = ui.theme;
2668
+ const c = new Container();
2669
+ const imgW = Math.min(b.width ?? 200, Math.round(innerW * 0.4));
2670
+ const imgH = b.width && b.height ? Math.round(imgW * b.height / b.width) : Math.round(imgW * 0.62);
2671
+ const gap = 18;
2672
+ const textW = innerW - imgW - gap;
2673
+ const side = b.side ?? "left";
2674
+ const imgX = side === "left" ? -innerW / 2 + imgW / 2 : innerW / 2 - imgW / 2;
2675
+ const textCx = side === "left" ? -innerW / 2 + imgW + gap + textW / 2 : -innerW / 2 + textW / 2;
2676
+ const title = b.title ? new Text({ text: ui.t(b.title), style: { fontFamily: t.type.family, fontSize: 15, fill: t.color.text, fontWeight: "800" } }) : null;
2677
+ title?.anchor.set(0, 0);
2678
+ const para = richParagraphNode(ui.t(b.text), ui, textW, { fontFamily: t.type.family, fontSize: 14, fill: t.color.textDim, lineHeight: 19 });
2679
+ const titleH = title ? title.height + 8 : 0;
2680
+ const textH = titleH + para.height;
2681
+ const h = Math.max(imgH, textH);
2682
+ const img = imageBoxNode(b.src, imgW, imgH, ui);
2683
+ img.position.set(imgX, 0);
2684
+ c.addChild(img);
2685
+ if (title) {
2686
+ title.position.set(textCx - textW / 2, -textH / 2);
2687
+ c.addChild(title);
2688
+ }
2689
+ para.position.set(textCx, -textH / 2 + titleH + para.height / 2);
2690
+ c.addChild(para);
2691
+ c.addChild(new Graphics().rect(-innerW / 2, -h / 2, 1, h).fill({ color: 16777215, alpha: 0 }));
2692
+ return c;
2693
+ }
2694
+ function imageBoxNode(src, w, h, ui) {
2695
+ const t = ui.theme;
2696
+ const box = new Container();
2697
+ const placeholder = new Graphics().rect(-w / 2, -h / 2, w, h).fill({ color: t.color.surfaceAlt, alpha: 0.5 });
2698
+ box.addChild(placeholder);
2699
+ if (typeof Image !== "undefined") {
2700
+ const img = new Image();
2701
+ img.crossOrigin = "anonymous";
2702
+ img.onload = () => {
2703
+ if (box.destroyed) return;
2704
+ try {
2705
+ const sp = new Sprite(Texture.from(img));
2706
+ sp.anchor.set(0.5);
2707
+ sp.width = w;
2708
+ sp.height = h;
2709
+ box.addChild(sp);
2710
+ placeholder.visible = false;
2711
+ } catch {
2712
+ }
2713
+ };
2714
+ img.onerror = () => {
2715
+ };
2716
+ img.src = src;
2717
+ }
2718
+ return box;
2719
+ }
2720
+ function paytableNode(b, ui, innerW) {
2721
+ const want = Math.max(1, b.columns ?? 1);
2722
+ const fit = Math.max(1, Math.floor(innerW / 170));
2723
+ const cols = Math.max(1, Math.min(want, fit, b.rows.length || 1));
2724
+ return cols <= 1 ? paytableList(b, ui, innerW) : paytableGrid(b, ui, innerW, cols);
2725
+ }
2726
+ function paytableList(b, ui, innerW) {
2727
+ const t = ui.theme;
2728
+ const c = new Container();
2729
+ const rowH = 44;
2730
+ const totalH = Math.max(b.rows.length * rowH, rowH);
2731
+ const hasIcons = b.rows.some((r) => r.icon);
2732
+ const left = -innerW / 2;
2733
+ const symbolX = left + (hasIcons ? 48 : 4);
2734
+ b.rows.forEach((r, i) => {
2735
+ const cy = -totalH / 2 + rowH / 2 + i * rowH;
2736
+ if (i > 0) {
2737
+ c.addChild(new Graphics().moveTo(left, cy - rowH / 2).lineTo(innerW / 2, cy - rowH / 2).stroke({ width: 1, color: t.color.textDim, alpha: 0.15 }));
2738
+ }
2739
+ if (r.icon) {
2740
+ const icon = imageBoxNode(r.icon, 36, 36, ui);
2741
+ icon.position.set(left + 22, cy);
2742
+ c.addChild(icon);
2743
+ }
2744
+ const sym = new Text({ text: ui.t(r.symbol ?? ""), style: { fontFamily: t.type.family, fontSize: 15, fill: t.color.text, fontWeight: "700" } });
2745
+ sym.anchor.set(0, 0.5);
2746
+ sym.position.set(symbolX, cy);
2747
+ const pay = new Text({ text: r.payouts, style: { fontFamily: t.type.family, fontSize: 14, fill: t.color.accent, fontWeight: "700" } });
2748
+ pay.anchor.set(1, 0.5);
2749
+ pay.position.set(innerW / 2, cy);
2750
+ c.addChild(sym, pay);
2751
+ });
2752
+ return c;
2753
+ }
2754
+ function paytableGrid(b, ui, innerW, cols) {
2755
+ const c = new Container();
2756
+ const cellW = innerW / cols;
2757
+ const gridRows = Math.ceil(b.rows.length / cols);
2758
+ const lineH = 17;
2759
+ const maxLines = b.rows.reduce((m, r) => Math.max(m, (r.payouts || "").split("\n").length), 1);
2760
+ const cellH = Math.max(48, 14 + maxLines * lineH);
2761
+ const totalH = gridRows * cellH;
2762
+ b.rows.forEach((r, i) => {
2763
+ const col = i % cols;
2764
+ const row = Math.floor(i / cols);
2765
+ const cell = paytableCell(r, ui, cellW, lineH);
2766
+ cell.position.set(-innerW / 2 + col * cellW + cellW / 2, -totalH / 2 + row * cellH + cellH / 2);
2767
+ c.addChild(cell);
2768
+ });
2769
+ return c;
2770
+ }
2771
+ function paytableCell(r, ui, cellW, lineH) {
2772
+ const t = ui.theme;
2773
+ const cell = new Container();
2774
+ const lx = -cellW / 2 + 10;
2775
+ let leftW = 0;
2776
+ if (r.icon) {
2777
+ const icon = imageBoxNode(r.icon, 40, 40, ui);
2778
+ icon.position.set(lx + 20, 0);
2779
+ cell.addChild(icon);
2780
+ leftW = 52;
2781
+ } else if (r.symbol) {
2782
+ const sym = new Text({ text: ui.t(r.symbol), style: { fontFamily: t.type.family, fontSize: 14, fontWeight: "800", fill: t.color.text } });
2783
+ sym.anchor.set(0, 0.5);
2784
+ sym.position.set(lx, 0);
2785
+ cell.addChild(sym);
2786
+ leftW = sym.width + 14;
2787
+ }
2788
+ const lines = (r.payouts || "").split("\n");
2789
+ const blockH = lines.length * lineH;
2790
+ const px = lx + leftW;
2791
+ lines.forEach((ln, j) => {
2792
+ const ly = -blockH / 2 + lineH / 2 + j * lineH;
2793
+ const ci = ln.indexOf(":");
2794
+ if (ci >= 0) {
2795
+ const label = new Text({ text: ln.slice(0, ci + 1), style: { fontFamily: t.type.family, fontSize: 13, fontWeight: "800", fill: t.color.text } });
2796
+ label.anchor.set(0, 0.5);
2797
+ label.position.set(px, ly);
2798
+ const val = new Text({ text: ln.slice(ci + 1), style: { fontFamily: t.type.family, fontSize: 13, fontWeight: "700", fill: t.color.accent } });
2799
+ val.anchor.set(0, 0.5);
2800
+ val.position.set(px + label.width + 4, ly);
2801
+ cell.addChild(label, val);
2802
+ } else {
2803
+ const txt = new Text({ text: ln, style: { fontFamily: t.type.family, fontSize: 13, fontWeight: "700", fill: t.color.accent } });
2804
+ txt.anchor.set(0, 0.5);
2805
+ txt.position.set(px, ly);
2806
+ cell.addChild(txt);
2807
+ }
2808
+ });
2809
+ return cell;
2810
+ }
2811
+ var MARGIN = 16;
2812
+ var HEADER_H = 60;
2813
+ var INSET = 22;
2814
+ var LIGHT = {
2815
+ surface: "#ffffff",
2816
+ surfaceAlt: "#eef1f6",
2817
+ text: "#181b20",
2818
+ textDim: "#5b6472",
2819
+ border: "#000000",
2820
+ accent: "#d99000"
2821
+ };
2822
+ var MenuView = class extends ControlView {
2823
+ constructor(panel, controls, blocks, ui, ticker, opts = {}) {
2824
+ super(panel, ui);
2825
+ this.panel = panel;
2826
+ this.controls = controls;
2827
+ this.blocks = blocks;
2828
+ this.ticker = ticker;
2829
+ this.opts = opts;
2830
+ this.zIndex = 120;
2831
+ this.titleKey = opts.title ?? "Menu";
2832
+ this.maxWidth = opts.maxWidth ?? 1600;
2833
+ const lightTheme = { ...ui.theme, color: { ...ui.theme.color, surface: LIGHT.surface, surfaceAlt: LIGHT.surfaceAlt, text: LIGHT.text, textDim: LIGHT.textDim, accent: LIGHT.accent } };
2834
+ this.lightUi = new Proxy(ui, { get: (t, p) => p === "theme" ? lightTheme : Reflect.get(t, p) });
2835
+ this.backdrop.eventMode = "static";
2836
+ this.backdrop.on("pointertap", () => this.panel.closePanel());
2837
+ this.title = new Text({ text: ui.t(this.titleKey), style: { fontFamily: ui.theme.type.family, fontSize: 24, fill: LIGHT.text, fontWeight: "800", letterSpacing: 1 } });
2838
+ this.title.anchor.set(0, 0.5);
2839
+ this.buildClose();
2840
+ this.viewport.addChild(this.catcher, this.content);
2841
+ this.viewport.mask = this.maskG;
2842
+ this.catcher.eventMode = "static";
2843
+ this.catcher.on("pointerdown", this.onDown);
2844
+ this.catcher.on("globalpointermove", this.onMove);
2845
+ this.catcher.on("pointerup", this.onUp);
2846
+ this.catcher.on("pointerupoutside", this.onUp);
2847
+ this.addChild(this.backdrop, this.card, this.viewport, this.maskG, this.headerBar, this.title, this.closeBtn, this.dropdownLayer);
2848
+ this.rebuildContent();
2849
+ this.applyOpen(this.panel.isOpen);
2850
+ this.disposers.push(
2851
+ this.panel.state.subscribe(() => this.applyOpen(this.panel.isOpen)),
2852
+ this.ui.locale.subscribe(() => {
2853
+ if (this.destroyed) return;
2854
+ this.title.text = this.ui.t(this.titleKey);
2855
+ this.rebuildContent();
2856
+ })
2857
+ );
2858
+ }
2859
+ panel;
2860
+ controls;
2861
+ blocks;
2862
+ ticker;
2863
+ opts;
2864
+ backdrop = new Graphics();
2865
+ card = new Graphics();
2866
+ headerBar = new Graphics();
2867
+ title;
2868
+ closeBtn = new Container();
2869
+ viewport = new Container();
2870
+ maskG = new Graphics();
2871
+ catcher = new Graphics();
2872
+ content = new Container();
2873
+ /** Unclipped layer above the scroll mask for open select dropdowns. */
2874
+ dropdownLayer = new Container();
2875
+ childViews = [];
2876
+ titleKey;
2877
+ maxWidth;
2878
+ /** `ui` proxy with a light theme — feeds the shared block renderer dark-on-white. */
2879
+ lightUi;
2880
+ vpW = 0;
2881
+ vpH = 0;
2882
+ scrollY = 0;
2883
+ contentH = 0;
2884
+ dragging = false;
2885
+ lastY = 0;
2886
+ wheelHandler;
2887
+ buildClose() {
2888
+ const r = 22;
2889
+ const bg = new Graphics().circle(0, 0, r).fill({ color: LIGHT.border });
2890
+ const x = new Graphics().moveTo(-7, -7).lineTo(7, 7).moveTo(7, -7).lineTo(-7, 7).stroke({ width: 3, color: "#ffffff", cap: "round" });
2891
+ this.closeBtn.addChild(bg, x);
2892
+ this.closeBtn.eventMode = "static";
2893
+ this.closeBtn.cursor = "pointer";
2894
+ this.closeBtn.hitArea = new Rectangle(-r, -r, r * 2, r * 2);
2895
+ this.closeBtn.on("pointertap", () => this.panel.closePanel());
2896
+ }
2897
+ rebuildContent() {
2898
+ for (const v of this.childViews) v.dispose();
2899
+ this.childViews.length = 0;
2900
+ for (const child of this.content.removeChildren()) child.destroy();
2901
+ const bodyW = this.vpW > 0 ? this.vpW : Math.min(this.maxWidth - INSET * 2, 520);
2902
+ const col = buildBlockColumn(this.blocks, this.controls, this.lightUi, this.ticker, bodyW, { controlSkins: this.opts.controlSkins, dropdownLayer: this.dropdownLayer });
2903
+ this.childViews = col.views;
2904
+ this.content.addChild(...col.content.removeChildren());
2905
+ this.content.x = bodyW / 2;
2906
+ this.contentH = col.height;
2907
+ this.clampScroll();
2908
+ }
2909
+ applyLayout(screen) {
2910
+ const W = screen.width;
2911
+ const H = screen.height;
2912
+ this.position.set(0, 0);
2913
+ this.scale.set(1);
2914
+ this.backdrop.clear().rect(0, 0, W, H).fill({ color: 0, alpha: 0.5 });
2915
+ this.backdrop.hitArea = new Rectangle(0, 0, W, H);
2916
+ const marginX = Math.max(MARGIN, Math.round(W * 0.05));
2917
+ const cardW = Math.min(W - marginX * 2, this.maxWidth);
2918
+ const cardH = H - MARGIN * 2;
2919
+ const cx = (W - cardW) / 2;
2920
+ const cy = MARGIN;
2921
+ this.card.clear().roundRect(cx, cy, cardW, cardH, 14).fill({ color: LIGHT.surface }).stroke({ width: 2.5, color: LIGHT.border });
2922
+ this.headerBar.clear().roundRect(cx, cy, cardW, HEADER_H, 14).fill({ color: LIGHT.surfaceAlt }).moveTo(cx + INSET, cy + HEADER_H).lineTo(cx + cardW - INSET, cy + HEADER_H).stroke({ width: 1, color: LIGHT.textDim, alpha: 0.4 });
2923
+ this.title.position.set(cx + INSET, cy + HEADER_H / 2);
2924
+ this.closeBtn.position.set(cx + cardW - INSET - 8, cy + HEADER_H / 2);
2925
+ const vpX = cx + INSET;
2926
+ const vpY = cy + HEADER_H + INSET;
2927
+ const newVpW = cardW - INSET * 2;
2928
+ this.vpW = newVpW;
2929
+ this.vpH = cardH - HEADER_H - INSET * 2;
2930
+ this.viewport.position.set(vpX, vpY);
2931
+ this.maskG.clear().rect(vpX, vpY, this.vpW, this.vpH).fill({ color: 16777215 });
2932
+ this.catcher.clear().rect(0, 0, this.vpW, this.vpH).fill({ color: 16777215, alpha: 1e-4 });
2933
+ this.catcher.hitArea = new Rectangle(0, 0, this.vpW, this.vpH);
2934
+ this.rebuildContent();
2935
+ this.clampScroll();
2936
+ }
2937
+ /** Close any open select dropdown (so scrolling/closing doesn't strand it). */
2938
+ closeSelects() {
2939
+ for (const c of this.controls) {
2940
+ const sc = c;
2941
+ if (sc.isOpen && typeof sc.closeList === "function") sc.closeList();
2942
+ }
2943
+ }
2944
+ onDown = (e) => {
2945
+ this.dragging = true;
2946
+ this.lastY = e.global.y;
2947
+ this.closeSelects();
2948
+ };
2949
+ onMove = (e) => {
2950
+ if (!this.dragging) return;
2951
+ this.scrollY += e.global.y - this.lastY;
2952
+ this.lastY = e.global.y;
2953
+ this.clampScroll();
2954
+ };
2955
+ onUp = () => {
2956
+ this.dragging = false;
2957
+ };
2958
+ clampScroll() {
2959
+ const min = Math.min(0, this.vpH - this.contentH);
2960
+ this.scrollY = Math.max(min, Math.min(0, this.scrollY));
2961
+ this.content.y = this.scrollY;
2962
+ }
2963
+ applyOpen(open) {
2964
+ this.visible = open;
2965
+ this.eventMode = open ? "static" : "none";
2966
+ if (open) {
2967
+ this.scrollY = 0;
2968
+ this.clampScroll();
2969
+ if (!this.wheelHandler && typeof window !== "undefined") {
2970
+ this.wheelHandler = (e) => {
2971
+ this.closeSelects();
2972
+ this.scrollY -= e.deltaY;
2973
+ this.clampScroll();
2974
+ };
2975
+ window.addEventListener("wheel", this.wheelHandler, { passive: true });
2976
+ }
2977
+ } else {
2978
+ this.closeSelects();
2979
+ if (this.wheelHandler && typeof window !== "undefined") {
2980
+ window.removeEventListener("wheel", this.wheelHandler);
2981
+ this.wheelHandler = void 0;
2982
+ }
2983
+ }
2984
+ }
2985
+ dispose() {
2986
+ if (this.wheelHandler && typeof window !== "undefined") window.removeEventListener("wheel", this.wheelHandler);
2987
+ for (const v of this.childViews) v.dispose();
2988
+ this.childViews.length = 0;
2989
+ super.dispose();
2990
+ }
2991
+ };
2992
+ var CAP_DY = -20;
2993
+ function fmtDuration(totalSec) {
2994
+ const s = Math.max(0, Math.floor(totalSec));
2995
+ const h = Math.floor(s / 3600);
2996
+ const m = Math.floor(s % 3600 / 60);
2997
+ const sec = String(s % 60).padStart(2, "0");
2998
+ return h > 0 ? `${h}:${String(m).padStart(2, "0")}:${sec}` : `${m}:${sec}`;
2999
+ }
3000
+ var ReadoutView = class extends ControlView {
3001
+ constructor(ro, ui, ticker, opts = {}) {
3002
+ super(ro, ui);
3003
+ this.ro = ro;
3004
+ this.ticker = ticker;
3005
+ const t = ui.theme;
3006
+ const inline = opts.inline ?? false;
3007
+ this.prefix = opts.prefix ?? false;
3008
+ const fill = opts.fill ?? (opts.mono ? "#ffffff" : t.color.text);
3009
+ this.baseFill = this.prefix ? opts.fill ?? "#ffffff" : fill;
3010
+ this.accentFill = t.color.accent;
3011
+ if (this.prefix) {
3012
+ this.valueText = new Text({
3013
+ text: "",
3014
+ style: {
3015
+ fontFamily: t.type.family,
3016
+ fontSize: 18,
3017
+ fill: opts.fill ?? "#ffffff",
3018
+ fontWeight: "600",
3019
+ dropShadow: { color: 0, alpha: 0.7, blur: 4, distance: 0, angle: 0 }
3020
+ }
3021
+ });
3022
+ this.valueText.alpha = opts.fill ? 1 : 0.92;
3023
+ this.valueText.anchor.set(0, 0);
3024
+ this.addChild(this.valueText);
3025
+ this.disposers.push(
3026
+ this.ro.value.subscribe(() => this.render()),
3027
+ this.ui.locale.subscribe(() => {
3028
+ if (!this.destroyed) this.render();
3029
+ }),
3030
+ this.ro.emphasized.subscribe(() => this.applyEmphasis())
3031
+ );
3032
+ if (this.ro.currency) this.disposers.push(this.ro.currency.subscribe(() => this.render()));
3033
+ if (this.ro.kind === "duration") {
3034
+ this.tick = (tk) => this.ro.tick(tk.deltaMS / 1e3);
3035
+ this.ticker.add(this.tick);
3036
+ }
3037
+ this.applyEmphasis();
3038
+ this.render();
3039
+ return;
3040
+ }
3041
+ if (ro.label) {
3042
+ this.caption = new Text({
3043
+ text: ui.t(ro.label).toUpperCase(),
3044
+ style: { fontFamily: t.type.family, fontSize: 12, fill, fontWeight: "700", letterSpacing: inline ? 0.8 : 2 }
3045
+ });
3046
+ this.caption.alpha = inline ? 0.65 : 0.55;
3047
+ if (inline) {
3048
+ this.caption.anchor.set(1, 0.5);
3049
+ this.caption.position.set(-7, 0);
3050
+ } else {
3051
+ this.caption.anchor.set(0.5, 1);
3052
+ this.caption.y = CAP_DY;
3053
+ }
3054
+ this.addChild(this.caption);
3055
+ }
3056
+ this.valueText = new Text({ text: "", style: { fontFamily: t.type.family, fontSize: inline ? 20 : 26, fill, fontWeight: "800" } });
3057
+ if (inline) {
3058
+ this.valueText.anchor.set(0, 0.5);
3059
+ this.valueText.position.set(this.caption ? 7 : 0, 0);
3060
+ } else {
3061
+ this.valueText.anchor.set(0.5, 0.5);
3062
+ }
3063
+ this.addChild(this.valueText);
3064
+ this.disposers.push(
3065
+ this.ro.value.subscribe(() => this.render()),
3066
+ this.ui.locale.subscribe(() => {
3067
+ if (this.destroyed) return;
3068
+ if (this.caption && this.ro.label) this.caption.text = this.ui.t(this.ro.label).toUpperCase();
3069
+ })
3070
+ );
3071
+ if (this.ro.currency) this.disposers.push(this.ro.currency.subscribe(() => this.render()));
3072
+ this.disposers.push(this.ro.emphasized.subscribe(() => this.applyEmphasis()));
3073
+ if (this.ro.kind === "duration") {
3074
+ this.tick = (tk) => this.ro.tick(tk.deltaMS / 1e3);
3075
+ this.ticker.add(this.tick);
3076
+ }
3077
+ this.applyEmphasis();
3078
+ this.render();
3079
+ }
3080
+ ro;
3081
+ ticker;
3082
+ caption;
3083
+ valueText;
3084
+ tick;
3085
+ prefix;
3086
+ /** Normal + accent value colours for the `emphasized` (modified) tint. */
3087
+ baseFill;
3088
+ accentFill;
3089
+ /** Tint the value in the theme accent while the readout is emphasized (modified). */
3090
+ applyEmphasis() {
3091
+ if (this.destroyed) return;
3092
+ this.valueText.style.fill = this.ro.emphasized.get() ? this.accentFill : this.baseFill;
3093
+ }
3094
+ render() {
3095
+ const v = this.ro.value.get();
3096
+ let text;
3097
+ switch (this.ro.kind) {
3098
+ case "currency":
3099
+ text = this.ro.currency ? formatAmount(v, this.ro.currency.get(), { signed: this.ro.signed }) : String(v);
3100
+ break;
3101
+ case "percent":
3102
+ text = `${v.toFixed(this.ro.decimals)}%`;
3103
+ break;
3104
+ case "duration":
3105
+ text = fmtDuration(v);
3106
+ break;
3107
+ default:
3108
+ text = String(v);
3109
+ }
3110
+ this.valueText.text = this.prefix && this.ro.label ? `${this.ui.t(this.ro.label)}: ${text}` : text;
3111
+ }
3112
+ dispose() {
3113
+ if (this.tick) this.ticker.remove(this.tick);
3114
+ super.dispose();
3115
+ }
3116
+ };
3117
+ var INSET2 = 24;
3118
+ var BTN_H = 52;
3119
+ var BTN_GAP = 14;
3120
+ var ROW_GAP = 16;
3121
+ var LIGHT2 = {
3122
+ surface: "#ffffff",
3123
+ surfaceAlt: "#eef1f6",
3124
+ text: "#181b20",
3125
+ textDim: "#5b6472",
3126
+ border: "#000000",
3127
+ primary: "#0a0a0a",
3128
+ primaryText: "#ffffff"
3129
+ };
3130
+ var DialogView = class extends ControlView {
3131
+ constructor(panel, blocks, actions, ui, ticker, opts = {}) {
3132
+ super(panel, ui);
3133
+ this.panel = panel;
3134
+ this.blocks = blocks;
3135
+ this.actions = actions;
3136
+ this.ticker = ticker;
3137
+ this.opts = opts;
3138
+ this.zIndex = 130;
3139
+ this.maxWidth = opts.maxWidth ?? 520;
3140
+ const lightTheme = { ...ui.theme, color: { ...ui.theme.color, surface: LIGHT2.surface, surfaceAlt: LIGHT2.surfaceAlt, text: LIGHT2.text, textDim: LIGHT2.textDim } };
3141
+ this.lightUi = new Proxy(ui, { get: (t, p) => p === "theme" ? lightTheme : Reflect.get(t, p) });
3142
+ this.backdrop.eventMode = "static";
3143
+ this.backdrop.on("pointertap", () => {
3144
+ if (!this.ui.noticeBlocking.get()) this.panel.closePanel();
3145
+ });
3146
+ this.buildClose();
3147
+ this.content.mask = this.maskG;
3148
+ this.addChild(this.backdrop, this.card, this.content, this.maskG, this.buttons, this.closeBtn);
3149
+ this.applyOpen(this.panel.isOpen);
3150
+ this.disposers.push(
3151
+ this.panel.state.subscribe(() => this.applyOpen(this.panel.isOpen)),
3152
+ // hide the ✕ when the open notice is blocking (re-renders on toggle)
3153
+ this.ui.noticeBlocking.subscribe(() => {
3154
+ this.closeBtn.visible = !this.ui.noticeBlocking.get();
3155
+ }),
3156
+ this.blocks.subscribe(() => {
3157
+ if (!this.destroyed) this.relayout();
3158
+ }),
3159
+ this.actions.subscribe(() => {
3160
+ if (!this.destroyed) this.relayout();
3161
+ }),
3162
+ this.ui.locale.subscribe(() => {
3163
+ if (!this.destroyed) this.relayout();
3164
+ })
3165
+ );
3166
+ }
3167
+ panel;
3168
+ blocks;
3169
+ actions;
3170
+ ticker;
3171
+ opts;
3172
+ backdrop = new Graphics();
3173
+ card = new Graphics();
3174
+ closeBtn = new Container();
3175
+ content = new Container();
3176
+ maskG = new Graphics();
3177
+ buttons = new Container();
3178
+ childViews = [];
3179
+ screen;
3180
+ maxWidth;
3181
+ /** `ui` proxy with a light theme — so the shared block renderer draws dark-on-white
3182
+ * content inside the white modal card (Figma "default" dialog look). */
3183
+ lightUi;
3184
+ buildClose() {
3185
+ const r = 22;
3186
+ const bg = new Graphics().circle(0, 0, r).fill({ color: LIGHT2.primary });
3187
+ const x = new Graphics().moveTo(-7, -7).lineTo(7, 7).moveTo(7, -7).lineTo(-7, 7).stroke({ width: 3, color: "#ffffff", cap: "round" });
3188
+ this.closeBtn.addChild(bg, x);
3189
+ this.closeBtn.eventMode = "static";
3190
+ this.closeBtn.cursor = "pointer";
3191
+ this.closeBtn.hitArea = new Rectangle(-r, -r, r * 2, r * 2);
3192
+ this.closeBtn.on("pointertap", () => this.panel.closePanel());
3193
+ }
3194
+ applyLayout(screen) {
3195
+ this.screen = screen;
3196
+ this.position.set(0, 0);
3197
+ this.scale.set(1);
3198
+ this.relayout();
3199
+ }
3200
+ relayout() {
3201
+ const s = this.screen;
3202
+ if (!s) return;
3203
+ if (!this.panel.isOpen) return;
3204
+ const W = s.width;
3205
+ const H = s.height;
3206
+ for (const v of this.childViews) v.dispose();
3207
+ this.childViews.length = 0;
3208
+ for (const ch of this.content.removeChildren()) ch.destroy();
3209
+ const cardW = Math.min(W - 48, this.maxWidth);
3210
+ const innerW = cardW - INSET2 * 2;
3211
+ const col = buildBlockColumn(this.blocks.get(), [], this.lightUi, this.ticker, innerW, { controlSkins: this.opts.controlSkins });
3212
+ this.childViews = col.views;
3213
+ const kids = col.content.removeChildren();
3214
+ if (kids.length) this.content.addChild(...kids);
3215
+ const contentH = col.height;
3216
+ const buttonsH = this.buildButtons(innerW);
3217
+ const wantH = contentH + (buttonsH ? ROW_GAP + buttonsH + INSET2 : INSET2);
3218
+ const cardH = Math.min(H - 48, wantH);
3219
+ const cx = (W - cardW) / 2;
3220
+ const cy = (H - cardH) / 2;
3221
+ this.backdrop.clear().rect(0, 0, W, H).fill({ color: 0, alpha: 0.5 });
3222
+ this.backdrop.hitArea = new Rectangle(0, 0, W, H);
3223
+ this.card.clear().roundRect(cx, cy, cardW, cardH, 14).fill({ color: LIGHT2.surface }).stroke({ width: 2.5, color: LIGHT2.border });
3224
+ const bodyH = cardH - (buttonsH ? ROW_GAP + buttonsH + INSET2 : INSET2);
3225
+ this.content.x = cx + cardW / 2;
3226
+ this.content.y = cy;
3227
+ this.maskG.clear().rect(cx, cy, cardW, Math.max(0, bodyH)).fill({ color: 16777215 });
3228
+ this.buttons.position.set(cx + cardW / 2, cy + cardH - INSET2 / 2 - buttonsH / 2);
3229
+ this.closeBtn.position.set(cx + cardW - 8, cy + 8);
3230
+ }
3231
+ /** Build the footer buttons from `noticeActions` into a centered row (shrunk to
3232
+ * fit if needed). Returns the row height (0 when there are no actions). */
3233
+ buildButtons(innerW) {
3234
+ for (const b of this.buttons.removeChildren()) b.destroy();
3235
+ const acts = this.ui.noticeActions.get();
3236
+ if (!acts.length) return 0;
3237
+ const made = acts.map((a, i) => this.makeButton(a, a.variant ?? (i === 0 ? "primary" : "secondary"), innerW));
3238
+ let total = made.reduce((sum, m) => sum + m.width, 0) + BTN_GAP * (made.length - 1);
3239
+ if (total > innerW) {
3240
+ const scale = innerW / total;
3241
+ for (const m of made) {
3242
+ m.width *= scale;
3243
+ this.drawButton(m.bg, m.variant, m.width);
3244
+ m.node.hitArea = new Rectangle(-m.width / 2, -BTN_H / 2, m.width, BTN_H);
3245
+ }
3246
+ total = made.reduce((sum, m) => sum + m.width, 0) + BTN_GAP * (made.length - 1);
3247
+ }
3248
+ let x = -total / 2;
3249
+ for (const m of made) {
3250
+ m.node.position.set(x + m.width / 2, 0);
3251
+ x += m.width + BTN_GAP;
3252
+ this.buttons.addChild(m.node);
3253
+ }
3254
+ return BTN_H;
3255
+ }
3256
+ makeButton(action, variant, innerW) {
3257
+ const t = this.ui.theme;
3258
+ const node = new Container();
3259
+ const bg = new Graphics();
3260
+ const label = new Text({
3261
+ text: this.ui.t(action.label),
3262
+ style: { fontFamily: t.type.family, fontSize: 18, fontWeight: "800", fill: variant === "primary" ? LIGHT2.primaryText : LIGHT2.text, letterSpacing: 0.5 }
3263
+ });
3264
+ label.anchor.set(0.5);
3265
+ node.addChild(bg, label);
3266
+ const width = Math.min(innerW, Math.max(140, label.width + 56));
3267
+ this.drawButton(bg, variant, width);
3268
+ node.eventMode = "static";
3269
+ node.cursor = "pointer";
3270
+ node.hitArea = new Rectangle(-width / 2, -BTN_H / 2, width, BTN_H);
3271
+ node.on("pointertap", () => {
3272
+ action.onSelect?.();
3273
+ if (action.emit) this.ui.bus.emit("buttonActivated", { id: action.emit });
3274
+ if (!action.keepOpen) this.panel.closePanel();
3275
+ });
3276
+ return { node, bg, width, variant };
3277
+ }
3278
+ drawButton(bg, variant, width) {
3279
+ bg.clear();
3280
+ if (variant === "primary") {
3281
+ bg.roundRect(-width / 2, -BTN_H / 2, width, BTN_H, BTN_H / 2).fill({ color: LIGHT2.primary });
3282
+ } else {
3283
+ bg.roundRect(-width / 2, -BTN_H / 2, width, BTN_H, BTN_H / 2).fill({ color: LIGHT2.surface }).stroke({ width: 2.5, color: LIGHT2.border });
3284
+ }
3285
+ }
3286
+ applyOpen(open) {
3287
+ this.visible = open;
3288
+ this.eventMode = open ? "static" : "none";
3289
+ this.closeBtn.visible = !this.ui.noticeBlocking.get();
3290
+ if (open) this.relayout();
3291
+ }
3292
+ dispose() {
3293
+ for (const v of this.childViews) v.dispose();
3294
+ this.childViews.length = 0;
3295
+ super.dispose();
3296
+ }
3297
+ };
3298
+ function easeInOutCubic(p) {
3299
+ return p < 0.5 ? 4 * p * p * p : 1 - Math.pow(-2 * p + 2, 3) / 2;
3300
+ }
3301
+ var OpenUIPixi = class {
3302
+ constructor(ui, opts = {}) {
3303
+ this.ui = ui;
3304
+ this.opts = opts;
3305
+ }
3306
+ ui;
3307
+ opts;
3308
+ root = new Container();
3309
+ views = [];
3310
+ /** Full-screen overlays (e.g. the autoplay drawer) that own their own layout. */
3311
+ overlays = [];
3312
+ disposers = [];
3313
+ _eventLog;
3314
+ /** Show/hide slide: each interactive view slides toward its anchored edge (bottom
3315
+ * controls down, top up — behind the status-bar plaque). Views stay direct children
3316
+ * of `root` (introspection bounds unchanged); only their y moves, and the
3317
+ * ref-counted input lock makes them non-interactive while moving/hidden. */
3318
+ slideProg = 0;
3319
+ // 0 = shown · 1 = fully hidden (set per `intro` on mount)
3320
+ slideTarget = 1;
3321
+ slideHeld = false;
3322
+ lastScreenH = 1080;
3323
+ appTicker;
3324
+ slideTickFn;
3325
+ slideBaseY = /* @__PURE__ */ new Map();
3326
+ slideSign = /* @__PURE__ */ new Map();
3327
+ /** Buy-feature ⇄ total-win swap: only one shows at a time (see `setupBonusSwap`). */
3328
+ bonusView;
3329
+ totalWinView;
3330
+ fadeFns = /* @__PURE__ */ new Map();
3331
+ /** The bus event log backing `window.__OPENUI__.events` (available after mount). */
3332
+ get eventLog() {
3333
+ return this._eventLog;
3334
+ }
3335
+ mount(app) {
3336
+ const { stage, renderer, ticker } = app;
3337
+ this.appTicker = ticker;
3338
+ stage.sortableChildren = true;
3339
+ this.root.zIndex = 1e4;
3340
+ this.root.sortableChildren = true;
3341
+ stage.addChild(this.root);
3342
+ const ic = this.opts.icons ?? {};
3343
+ const spinView = new SpinView(this.ui.spin, this.ui, ticker, this.opts.spinSkin);
3344
+ const balanceView = new ValueDisplayView(this.ui.balance, this.ui, ticker, this.opts.gsap);
3345
+ const betView = new ValueDisplayView(this.ui.bet, this.ui, ticker, this.opts.gsap);
3346
+ const settingsView = new ButtonView(this.ui.settingsButton, this.ui, ticker, {
3347
+ shape: "circle",
3348
+ radius: 40,
3349
+ glyph: "menu",
3350
+ iconTexture: ic.settingsIdle,
3351
+ iconTarget: 88,
3352
+ dark: true
3353
+ // Figma ☰: solid black circle, white bars, no ring
3354
+ });
3355
+ settingsView.zIndex = 5;
3356
+ const menuView = this.opts.menu === false ? void 0 : this.buildMenu(ticker);
3357
+ const turboView = new TurboView(this.ui.turbo, this.ui, ticker, {
3358
+ offTexture: ic.turboOff,
3359
+ onTexture: ic.turboOn,
3360
+ modeTextures: ic.turboModes,
3361
+ target: 79
3362
+ });
3363
+ const spinOff = this.ui.spin.layout.offset ?? [0, 0];
3364
+ const autoOff = this.ui.autoplay.layout.offset ?? [0, 0];
3365
+ const picker = this.opts.autoplayPicker ?? "drawer";
3366
+ const autoplayView = new AutoplayView(this.ui.autoplay, this.ui, ticker, {
3367
+ idleTexture: ic.autoIdle,
3368
+ activeTexture: ic.autoActive,
3369
+ target: 73,
3370
+ picker,
3371
+ arcCenter: { x: spinOff[0] - autoOff[0], y: spinOff[1] - autoOff[1] },
3372
+ arcStepDeg: 22
3373
+ });
3374
+ autoplayView.zIndex = 20;
3375
+ const bonusView = new ButtonView(this.ui.bonusButton, this.ui, ticker, { shape: "circle", radius: 60, iconTexture: ic.bonus, iconTarget: 130 });
3376
+ const betPlusView = new ButtonView(this.ui.betPlus, this.ui, ticker, { shape: "circle", radius: 30, iconTexture: ic.betPlus, iconTarget: 64 });
3377
+ const betMinusView = new ButtonView(this.ui.betMinus, this.ui, ticker, { shape: "circle", radius: 30, iconTexture: ic.betMinus, iconTarget: 64 });
3378
+ const totalWinView = new ValueDisplayView(this.ui.totalWin, this.ui, ticker, this.opts.gsap);
3379
+ this.bonusView = bonusView;
3380
+ this.totalWinView = totalWinView;
3381
+ const muteView = new ButtonView(this.ui.muteButton, this.ui, ticker, { shape: "circle", radius: 30, glyph: "speaker", iconTarget: 60, mono: true });
3382
+ const fullscreenView = new ButtonView(this.ui.fullscreenButton, this.ui, ticker, { shape: "circle", radius: 30, glyph: "fullscreen", iconTarget: 60, mono: true });
3383
+ muteView.zIndex = 65;
3384
+ fullscreenView.zIndex = 65;
3385
+ const roOpts = { prefix: true, fill: this.opts.readoutColor };
3386
+ const rtpView = new ReadoutView(this.ui.rtp, this.ui, ticker, roOpts);
3387
+ const netView = new ReadoutView(this.ui.netPosition, this.ui, ticker, roOpts);
3388
+ const timerView = new ReadoutView(this.ui.sessionTimer, this.ui, ticker, roOpts);
3389
+ const entries = [
3390
+ [this.ui.spin.id, spinView],
3391
+ [this.ui.balance.id, balanceView],
3392
+ [this.ui.bet.id, betView],
3393
+ [this.ui.settingsButton.id, settingsView],
3394
+ [this.ui.turbo.id, turboView],
3395
+ [this.ui.autoplay.id, autoplayView],
3396
+ [this.ui.bonusButton.id, bonusView],
3397
+ [this.ui.totalWin.id, totalWinView],
3398
+ [this.ui.betPlus.id, betPlusView],
3399
+ [this.ui.betMinus.id, betMinusView],
3400
+ [this.ui.muteButton.id, muteView],
3401
+ [this.ui.fullscreenButton.id, fullscreenView]
3402
+ ];
3403
+ entries.push([this.ui.rtp.id, rtpView]);
3404
+ entries.push([this.ui.netPosition.id, netView]);
3405
+ entries.push([this.ui.sessionTimer.id, timerView]);
3406
+ const viewById = /* @__PURE__ */ new Map();
3407
+ const idByView = /* @__PURE__ */ new Map();
3408
+ for (const [id, view] of entries) {
3409
+ view.visible = !this.ui.hidden.has(id);
3410
+ this.root.addChild(view);
3411
+ this.views.push(view);
3412
+ viewById.set(id, view);
3413
+ idByView.set(view, id);
3414
+ }
3415
+ this.disposers.push(
3416
+ this.ui.on("visibilityChanged", ({ id, hidden }) => {
3417
+ const v = viewById.get(id);
3418
+ if (v) v.visible = !hidden;
3419
+ })
3420
+ );
3421
+ this.disposers.push(this.ui.muted.subscribe((m) => muteView.setGlyph(m ? "speaker-mute" : "speaker")));
3422
+ if (typeof document !== "undefined") {
3423
+ this.disposers.push(
3424
+ this.ui.bus.on("buttonActivated", ({ id }) => {
3425
+ if (id !== "fullscreen") return;
3426
+ const el = app.canvas.parentElement ?? document.documentElement;
3427
+ if (document.fullscreenElement) void document.exitFullscreen?.();
3428
+ else void el.requestFullscreen?.();
3429
+ })
3430
+ );
3431
+ const onFsChange = () => fullscreenView.setGlyph(document.fullscreenElement ? "fullscreen-exit" : "fullscreen");
3432
+ document.addEventListener("fullscreenchange", onFsChange);
3433
+ this.disposers.push(() => document.removeEventListener("fullscreenchange", onFsChange));
3434
+ }
3435
+ if (typeof window !== "undefined") {
3436
+ let keyHeld = false;
3437
+ const onKeyDown = (e) => {
3438
+ if (e.code !== "Space" && e.key !== "Enter") return;
3439
+ if (!this.ui.spin.allowKeyboard.get() || e.repeat || keyHeld) return;
3440
+ keyHeld = true;
3441
+ e.preventDefault();
3442
+ if (this.ui.spin.interactable) this.ui.spin.activate();
3443
+ };
3444
+ const onKeyUp = (e) => {
3445
+ if (e.code === "Space" || e.key === "Enter") keyHeld = false;
3446
+ };
3447
+ window.addEventListener("keydown", onKeyDown);
3448
+ window.addEventListener("keyup", onKeyUp);
3449
+ this.disposers.push(() => {
3450
+ window.removeEventListener("keydown", onKeyDown);
3451
+ window.removeEventListener("keyup", onKeyUp);
3452
+ });
3453
+ }
3454
+ if (menuView) {
3455
+ this.root.addChild(menuView);
3456
+ this.overlays.push(menuView);
3457
+ }
3458
+ if (picker === "drawer") {
3459
+ const drawer = new AutoplayDrawerView(this.ui.autoplay, this.ui, ticker);
3460
+ this.root.addChild(drawer);
3461
+ this.overlays.push(drawer);
3462
+ }
3463
+ const dialog = new DialogView(this.ui.noticePanel, this.ui.noticeBlocks, this.ui.noticeActions, this.ui, ticker, { controlSkins: this.opts.controlSkins });
3464
+ this.root.addChild(dialog);
3465
+ this.overlays.push(dialog);
3466
+ const replayBadge = new Container();
3467
+ const replayBg = new Graphics();
3468
+ const replayText = new Text({ text: this.ui.t("openui.replay").toUpperCase(), style: { fontFamily: this.ui.theme.type.family, fontSize: 16, fontWeight: "800", fill: 16777215, letterSpacing: 2 } });
3469
+ replayText.anchor.set(0.5);
3470
+ replayBadge.addChild(replayBg, replayText);
3471
+ replayBadge.zIndex = 310;
3472
+ replayBadge.visible = this.ui.replay.get();
3473
+ this.root.addChild(replayBadge);
3474
+ this.overlays.push({
3475
+ applyLayout: (s) => {
3476
+ const w = replayText.width + 36;
3477
+ const h = 32;
3478
+ replayBg.clear().roundRect(-w / 2, -h / 2, w, h, h / 2).fill({ color: 789776 }).stroke({ width: 2, color: 16777215 });
3479
+ replayBadge.position.set(s.width / 2, 40);
3480
+ },
3481
+ dispose: () => {
3482
+ if (!replayBadge.destroyed) replayBadge.destroy({ children: true });
3483
+ }
3484
+ });
3485
+ this.disposers.push(
3486
+ this.ui.replay.subscribe((v) => {
3487
+ replayBadge.visible = v;
3488
+ }),
3489
+ this.ui.locale.subscribe(() => {
3490
+ if (!replayBadge.destroyed) replayText.text = this.ui.t("openui.replay").toUpperCase();
3491
+ })
3492
+ );
3493
+ const applyLayout = () => {
3494
+ const screen = this.ui.screen.get();
3495
+ this.slideBaseY.clear();
3496
+ this.slideSign.clear();
3497
+ for (const v of this.views) {
3498
+ v.applyLayout(screen);
3499
+ const anchor = this.ui.control(idByView.get(v) ?? "")?.layout.anchor ?? "bottom-center";
3500
+ this.slideBaseY.set(v, v.y);
3501
+ this.slideSign.set(v, anchor.startsWith("top") ? -1 : 1);
3502
+ }
3503
+ this.lastScreenH = screen.height;
3504
+ this.applySlide();
3505
+ for (const o of this.overlays) o.applyLayout(screen);
3506
+ };
3507
+ const onResize = () => {
3508
+ this.ui.setScreen(app.screen.width, app.screen.height);
3509
+ };
3510
+ renderer.on("resize", onResize);
3511
+ const unsubScreen = this.ui.screen.subscribe(applyLayout);
3512
+ onResize();
3513
+ applyLayout();
3514
+ const intro = this.opts.intro ?? "shown";
3515
+ if (intro === "shown") {
3516
+ this.slideProg = 0;
3517
+ this.applySlide();
3518
+ } else {
3519
+ this.slideProg = 1;
3520
+ this.applySlide();
3521
+ this.ui.lock();
3522
+ this.slideHeld = true;
3523
+ if (intro === "slide-in") this.setControlsVisible(true);
3524
+ }
3525
+ this.disposers.push(() => renderer.off("resize", onResize), unsubScreen);
3526
+ this.setupBonusSwap();
3527
+ this._eventLog = new EventLog(this.ui.bus);
3528
+ if (this.opts.expose ?? true) this.expose();
3529
+ }
3530
+ /**
3531
+ * Wire the buy-feature ⇄ total-win swap. In base play the buy button shows (unless
3532
+ * hidden by config/jurisdiction); once the spin button enters free-spins mode
3533
+ * (`setFreeSpins(n > 0)` — you "can't buy" mid-bonus) the buy button fades out and
3534
+ * the localized total-win counter fades in over the same slot, then back on exit.
3535
+ */
3536
+ setupBonusSwap() {
3537
+ const bonus = this.bonusView;
3538
+ const tw = this.totalWinView;
3539
+ if (!bonus || !tw) return;
3540
+ const apply = (inBonus, animate) => {
3541
+ const bonusShouldShow = !inBonus && !this.ui.hidden.has(this.ui.bonusButton.id);
3542
+ if (animate) {
3543
+ if (inBonus) {
3544
+ tw.visible = true;
3545
+ tw.alpha = 0;
3546
+ this.fadeTo(tw, 1, 220);
3547
+ this.fadeTo(bonus, 0, 160, () => bonus.visible = false);
3548
+ } else {
3549
+ if (bonusShouldShow) {
3550
+ bonus.visible = true;
3551
+ bonus.alpha = 0;
3552
+ this.fadeTo(bonus, this.ui.bonusButton.interactable ? 1 : 0.4, 220);
3553
+ }
3554
+ this.fadeTo(tw, 0, 160, () => tw.visible = false);
3555
+ }
3556
+ } else {
3557
+ this.stopFade(bonus);
3558
+ this.stopFade(tw);
3559
+ bonus.visible = bonusShouldShow;
3560
+ bonus.alpha = this.ui.bonusButton.interactable ? 1 : 0.4;
3561
+ tw.visible = inBonus;
3562
+ tw.alpha = inBonus ? 1 : 0;
3563
+ }
3564
+ };
3565
+ apply(this.ui.spin.freeSpins.get() > 0, false);
3566
+ this.disposers.push(this.ui.spin.freeSpins.subscribe((n) => apply(n > 0, true)));
3567
+ }
3568
+ /** Fade a view's alpha to `to` over `ms` (wall-clock paced); cancels any prior fade
3569
+ * on the same view. `onDone` runs once when it settles. */
3570
+ fadeTo(view, to, ms, onDone) {
3571
+ this.stopFade(view);
3572
+ const t = this.appTicker;
3573
+ const from = view.alpha;
3574
+ if (!t || from === to) {
3575
+ view.alpha = to;
3576
+ onDone?.();
3577
+ return;
3578
+ }
3579
+ const startMs = typeof performance !== "undefined" ? performance.now() : 0;
3580
+ const fn = () => {
3581
+ const nowMs = typeof performance !== "undefined" ? performance.now() : startMs + ms;
3582
+ const k = Math.min(1, (nowMs - startMs) / Math.max(ms, 1));
3583
+ view.alpha = from + (to - from) * k;
3584
+ if (k >= 1) {
3585
+ view.alpha = to;
3586
+ this.stopFade(view);
3587
+ onDone?.();
3588
+ }
3589
+ };
3590
+ this.fadeFns.set(view, fn);
3591
+ t.add(fn);
3592
+ }
3593
+ /** Cancel an in-flight fade on `view` (if any). */
3594
+ stopFade(view) {
3595
+ const fn = this.fadeFns.get(view);
3596
+ if (fn) {
3597
+ this.appTicker?.remove(fn);
3598
+ this.fadeFns.delete(view);
3599
+ }
3600
+ }
3601
+ /**
3602
+ * Build the unified scrollable MENU bound to the ☰ panel. Composed `menu` blocks
3603
+ * are given by `mountHud`; absent → a default Settings-only menu. Built-in ids
3604
+ * ('music'/'sfx') are reused, not shadowed (P10); button blocks wire `closePanel`.
3605
+ */
3606
+ buildMenu(ticker) {
3607
+ const menu = this.opts.menu && this.opts.menu.length ? this.opts.menu : composeMenu(void 0, {});
3608
+ const controls = buildBlocks(menu, this.ui.bus, void 0, (id) => this.ui.control(id));
3609
+ for (const c of controls) if (!this.ui.control(c.id)) this.ui.register(c);
3610
+ const buttons = buttonBlocks(menu);
3611
+ this.disposers.push(
3612
+ this.ui.bus.on("buttonActivated", ({ id }) => {
3613
+ const b = buttons.find((x) => x.id === id);
3614
+ if (b?.action === "closePanel") this.ui.settingsPanel.closePanel();
3615
+ })
3616
+ );
3617
+ return new MenuView(this.ui.settingsPanel, controls, menu, this.ui, ticker, {
3618
+ controlSkins: this.opts.controlSkins,
3619
+ title: this.opts.menuTitle
3620
+ });
3621
+ }
3622
+ /**
3623
+ * Slide the whole interactive HUD in (`true`) or out (`false`): bottom-anchored
3624
+ * controls travel down, top-anchored travel up (behind the status-bar plaque).
3625
+ * Pure translation (no scaling); controls are non-interactive while moving/hidden.
3626
+ */
3627
+ setControlsVisible(visible) {
3628
+ const target = visible ? 0 : 1;
3629
+ this.slideTarget = target;
3630
+ if (!this.slideHeld) {
3631
+ this.ui.lock();
3632
+ this.slideHeld = true;
3633
+ }
3634
+ const settle = () => {
3635
+ if (this.slideProg === 0 && this.slideHeld) {
3636
+ this.ui.unlock();
3637
+ this.slideHeld = false;
3638
+ }
3639
+ };
3640
+ if (!this.appTicker) {
3641
+ this.slideProg = target;
3642
+ this.applySlide();
3643
+ settle();
3644
+ return;
3645
+ }
3646
+ const from = this.slideProg;
3647
+ const startMs = typeof performance !== "undefined" ? performance.now() : 0;
3648
+ if (this.slideTickFn) this.appTicker.remove(this.slideTickFn);
3649
+ const tick = () => {
3650
+ const nowMs = typeof performance !== "undefined" ? performance.now() : startMs + 360;
3651
+ const k = Math.min(1, (nowMs - startMs) / 360);
3652
+ this.slideProg = from + (target - from) * k;
3653
+ if (k >= 1) {
3654
+ this.slideProg = target;
3655
+ this.appTicker?.remove(tick);
3656
+ this.slideTickFn = void 0;
3657
+ settle();
3658
+ }
3659
+ this.applySlide();
3660
+ };
3661
+ this.slideTickFn = tick;
3662
+ this.appTicker.add(tick);
3663
+ }
3664
+ /** Apply the current slide progress: translate each interactive view toward its
3665
+ * anchored edge (pure translation — no scaling). */
3666
+ applySlide() {
3667
+ const off = easeInOutCubic(this.slideProg) * this.lastScreenH;
3668
+ for (const [view, baseY] of this.slideBaseY) {
3669
+ view.y = baseY + (this.slideSign.get(view) ?? 1) * off;
3670
+ }
3671
+ }
3672
+ unmount() {
3673
+ if (this.slideTickFn && this.appTicker) this.appTicker.remove(this.slideTickFn);
3674
+ this.slideTickFn = void 0;
3675
+ for (const fn of this.fadeFns.values()) this.appTicker?.remove(fn);
3676
+ this.fadeFns.clear();
3677
+ for (const v of this.views) v.dispose();
3678
+ this.views.length = 0;
3679
+ for (const o of this.overlays) o.dispose();
3680
+ this.overlays.length = 0;
3681
+ this._eventLog?.dispose();
3682
+ this._eventLog = void 0;
3683
+ for (const d of this.disposers) d();
3684
+ this.disposers.length = 0;
3685
+ if (!this.root.destroyed) this.root.destroy({ children: true });
3686
+ if (typeof window !== "undefined") {
3687
+ delete window.__OPENUI__;
3688
+ }
3689
+ }
3690
+ expose() {
3691
+ if (typeof window === "undefined") return;
3692
+ window.__OPENUI__ = {
3693
+ snapshot: () => this.ui.snapshot(),
3694
+ getState: (id) => this.ui.control(id)?.current ?? null,
3695
+ isInteractable: (id) => this.ui.control(id)?.interactable ?? false,
3696
+ isAnimating: (id) => this.ui.control(id)?.inspect().animating ?? false,
3697
+ bounds: (id) => this.ui.snapshot().find((s) => s.id === id)?.bounds ?? null,
3698
+ events: (since) => this._eventLog?.since(since) ?? [],
3699
+ controlsReady: () => this.slideProg < 1e-3
3700
+ };
3701
+ }
3702
+ };
3703
+ var ANCHOR_X = 100 / 203;
3704
+ var ANCHOR_Y = 100 / 213;
3705
+ var TARGET_DIAMETER = 220;
3706
+ function makeSprite(texture) {
3707
+ const s = new Sprite(texture);
3708
+ s.anchor.set(ANCHOR_X, ANCHOR_Y);
3709
+ s.scale.set(TARGET_DIAMETER / 200);
3710
+ return s;
3711
+ }
3712
+ function svgSpinSkin(textures) {
3713
+ const view = new Container();
3714
+ const def = makeSprite(textures.default);
3715
+ const auto = makeSprite(textures.auto);
3716
+ auto.visible = false;
3717
+ view.addChild(def, auto);
3718
+ return {
3719
+ view,
3720
+ update(state) {
3721
+ const isAuto = state === "auto";
3722
+ def.visible = !isAuto;
3723
+ auto.visible = isAuto;
3724
+ },
3725
+ destroy() {
3726
+ view.destroy({ children: true });
3727
+ }
3728
+ };
3729
+ }
3730
+
3731
+ export { AutoplayDrawerView, AutoplayView, ButtonView, ControlView, DialogView, MenuView, OpenUIPixi, ReadoutView, SelectView, SliderView, SpinView, StepperView, TextCellRenderer, ToggleView, TurboView, Tweener, ValueDisplayView, buildBlockColumn, defaultSpinSkin, drawSpin, isDesktop, svgSpinSkin };
3732
+ //# sourceMappingURL=chunk-PYXZIGXG.js.map
3733
+ //# sourceMappingURL=chunk-PYXZIGXG.js.map