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