@energy8platform/shell 0.8.0 → 0.9.0
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/dist/html.cjs.js +77 -11
- package/dist/html.cjs.js.map +1 -1
- package/dist/html.d.ts +9 -4
- package/dist/html.esm.js +77 -11
- package/dist/html.esm.js.map +1 -1
- package/dist/index.cjs.js +42 -3
- package/dist/index.cjs.js.map +1 -1
- package/dist/index.d.ts +9 -4
- package/dist/index.esm.js +42 -3
- package/dist/index.esm.js.map +1 -1
- package/dist/pixi.cjs.js +122 -30
- package/dist/pixi.cjs.js.map +1 -1
- package/dist/pixi.d.ts +9 -4
- package/dist/pixi.esm.js +122 -30
- package/dist/pixi.esm.js.map +1 -1
- package/package.json +1 -1
- package/src/core/ShellController.ts +10 -2
- package/src/core/device.ts +37 -0
- package/src/core/types.ts +7 -2
- package/src/core/version.ts +1 -1
- package/src/ui/html/components/BottomBar.ts +20 -5
- package/src/ui/html/components/GameInfo.ts +8 -3
- package/src/ui/html/shell.css.ts +5 -0
- package/src/ui/pixi/components/BottomBar.ts +22 -3
- package/src/ui/pixi/components/GameInfo.ts +8 -3
- package/src/ui/pixi/primitives/widgets.ts +65 -19
package/package.json
CHANGED
|
@@ -5,6 +5,7 @@ import { formatCurrency } from './format';
|
|
|
5
5
|
import { createI18n, type I18n } from './i18n';
|
|
6
6
|
import { KeyboardController, type KeyboardHost } from './keyboard';
|
|
7
7
|
import { DEFAULT_MENU, rangeBounds, seedMenuValues, type MenuItem, type MenuRangeItem } from './menu';
|
|
8
|
+
import { keyboardCapable } from './device';
|
|
8
9
|
import { PACKAGE_VERSION } from './version';
|
|
9
10
|
import type {
|
|
10
11
|
ShellConfig,
|
|
@@ -46,7 +47,10 @@ export function resolveConfig(config: ShellConfig): ResolvedShellConfig {
|
|
|
46
47
|
win: config.win,
|
|
47
48
|
mode: config.mode,
|
|
48
49
|
gameInfo: config.gameInfo,
|
|
49
|
-
|
|
50
|
+
// `hotkeys` unset means "decide for me": a touchscreen has no keys to press, so the shell
|
|
51
|
+
// neither binds them nor advertises them there. A host that knows better — the platform's own
|
|
52
|
+
// `device` field, a jurisdiction rule — says so outright and that wins. See core/device.ts.
|
|
53
|
+
features: { ...config.features, hotkeys: config.features.hotkeys ?? keyboardCapable() },
|
|
50
54
|
theme: config.theme,
|
|
51
55
|
onBonusBuy: config.onBonusBuy,
|
|
52
56
|
volumes: config.volumes,
|
|
@@ -145,7 +149,11 @@ export class ShellController extends EventEmitter<ShellEvents> implements ShellH
|
|
|
145
149
|
this.renderer.renderBar();
|
|
146
150
|
},
|
|
147
151
|
toggleAutoplay: () => {
|
|
148
|
-
|
|
152
|
+
// A halted run (stopped, but with spins still owed after a lost connection) counts as
|
|
153
|
+
// "autoplay is on screen": the toggle retires its leftover count, exactly as it stops a
|
|
154
|
+
// running one. Resuming those spins is the disc's job, not this one's.
|
|
155
|
+
const { active, remaining } = this.state.autoplay;
|
|
156
|
+
if (active || remaining > 0) a.stopAutoplay();
|
|
149
157
|
else this.openAutoplayPicker();
|
|
150
158
|
},
|
|
151
159
|
startAutoplay: (remaining) => {
|
|
@@ -0,0 +1,37 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Can the player in front of this client actually press a key?
|
|
3
|
+
*
|
|
4
|
+
* The shell documents its shortcuts in a Hotkeys section and binds Spacebar to spin. On a phone
|
|
5
|
+
* neither is reachable, and showing a keycap chart to someone holding a touchscreen is a promise
|
|
6
|
+
* the game can't keep — a certification lab reads it as a feature offered where it doesn't work.
|
|
7
|
+
*
|
|
8
|
+
* The question is deliberately NOT "is the layout narrow" (a portrait desktop window still has a
|
|
9
|
+
* keyboard) and NOT "is there a touchscreen" (a touch laptop has both). It is: what does the
|
|
10
|
+
* PRIMARY pointer look like, and can it hover? Coarse-and-hoverless is a touchscreen, and a
|
|
11
|
+
* touchscreen is the one case where the keys genuinely aren't there.
|
|
12
|
+
*
|
|
13
|
+
* A tablet with a keyboard case answers "coarse, no hover" too and loses the chart. That is the
|
|
14
|
+
* right side to be wrong on: the chart is a convenience, and the keys keep working for anyone who
|
|
15
|
+
* has them — the media query only decides what the shell ADVERTISES (hosts can still say outright,
|
|
16
|
+
* via `features.hotkeys`, and the platform's own `device` field does exactly that).
|
|
17
|
+
*/
|
|
18
|
+
|
|
19
|
+
interface MediaQueryHost {
|
|
20
|
+
matchMedia?(query: string): { matches: boolean };
|
|
21
|
+
}
|
|
22
|
+
|
|
23
|
+
/** A touchscreen: the primary pointer is a finger, and nothing can hover. */
|
|
24
|
+
const TOUCH_ONLY = '(pointer: coarse) and (hover: none)';
|
|
25
|
+
|
|
26
|
+
export function keyboardCapable(
|
|
27
|
+
win: MediaQueryHost | undefined = typeof window === 'undefined' ? undefined : window,
|
|
28
|
+
): boolean {
|
|
29
|
+
// No window (SSR, node tests) or a browser too old for matchMedia: assume a keyboard rather than
|
|
30
|
+
// silently stripping shortcuts from a desktop we simply failed to measure.
|
|
31
|
+
if (typeof win?.matchMedia !== 'function') return true;
|
|
32
|
+
try {
|
|
33
|
+
return !win.matchMedia(TOUCH_ONLY).matches;
|
|
34
|
+
} catch {
|
|
35
|
+
return true;
|
|
36
|
+
}
|
|
37
|
+
}
|
package/src/core/types.ts
CHANGED
|
@@ -156,8 +156,13 @@ export interface AutoplayConfig {
|
|
|
156
156
|
|
|
157
157
|
export interface ShellFeatures {
|
|
158
158
|
turbo: 0 | 1 | 2 | 3;
|
|
159
|
-
/** Master keyboard-shortcut switch
|
|
160
|
-
*
|
|
159
|
+
/** Master keyboard-shortcut switch: `false` disables ALL hotkeys (overrides `spacebar` and any
|
|
160
|
+
* future hotkey) AND hides the Hotkeys section of Game Info, including one the game supplied
|
|
161
|
+
* itself — a keycap chart for keys that do nothing is worse than no chart.
|
|
162
|
+
*
|
|
163
|
+
* Left unset, the shell measures the client (`core/device.ts`): a touchscreen has no keys to
|
|
164
|
+
* press, so it gets neither the shortcuts nor the chart. Set it explicitly when you know better
|
|
165
|
+
* than the media query — which is what the host does with the platform's `device` field. */
|
|
161
166
|
hotkeys?: boolean;
|
|
162
167
|
/** Spacebar starts a spin in base mode. Defaults to `true`; set `false` to disable the
|
|
163
168
|
* keyboard shortcut (e.g. jurisdictions that forbid quick-spin keys). */
|
package/src/core/version.ts
CHANGED
|
@@ -225,20 +225,32 @@ function betLocked(host: ShellHost): boolean {
|
|
|
225
225
|
return host.state.busy || host.state.autoplay.active;
|
|
226
226
|
}
|
|
227
227
|
|
|
228
|
-
/**
|
|
228
|
+
/**
|
|
229
|
+
* SPIN disc — rotates while busy; becomes a STOP + countdown while autoplay runs; becomes an
|
|
230
|
+
* autoplay glyph + the SAME countdown when a run was halted with spins still owed (a lost
|
|
231
|
+
* connection), where a tap resumes it. That third state is what a certification lab means by "after
|
|
232
|
+
* reconnection the counter is displayed correctly": the run stopped, but the spins the player asked
|
|
233
|
+
* for are still on screen and one tap away, instead of silently reset to zero.
|
|
234
|
+
*/
|
|
229
235
|
function spinButton(host: ShellHost): HTMLButtonElement {
|
|
230
236
|
const { state } = host;
|
|
231
237
|
const sp = document.createElement('button');
|
|
232
238
|
sp.className = 'ge-shell-spin';
|
|
233
239
|
sp.dataset.ge = 'spin';
|
|
240
|
+
const rem = state.autoplay.remaining;
|
|
241
|
+
const count = Number.isFinite(rem) ? String(rem) : '∞';
|
|
234
242
|
if (state.autoplay.active) {
|
|
235
243
|
sp.classList.add('ge-stop');
|
|
236
|
-
|
|
237
|
-
const label = Number.isFinite(rem) ? String(rem) : '∞';
|
|
238
|
-
sp.innerHTML = `<span class="ge-spin-stop">${icon('stop')}</span><span class="ge-spin-count">${label}</span>`;
|
|
244
|
+
sp.innerHTML = `<span class="ge-spin-stop">${icon('stop')}</span><span class="ge-spin-count">${count}</span>`;
|
|
239
245
|
sp.addEventListener('click', () => {
|
|
240
246
|
if (!sp.disabled) host.actions.stopAutoplay();
|
|
241
247
|
});
|
|
248
|
+
} else if (rem > 0) {
|
|
249
|
+
sp.classList.add('ge-auto-paused');
|
|
250
|
+
sp.innerHTML = `<span class="ge-spin-auto">${icon('autoplay')}</span><span class="ge-spin-count">${count}</span>`;
|
|
251
|
+
sp.addEventListener('click', () => {
|
|
252
|
+
if (!sp.disabled) host.actions.startAutoplay(rem);
|
|
253
|
+
});
|
|
242
254
|
} else {
|
|
243
255
|
sp.innerHTML = icon('spin');
|
|
244
256
|
if (state.busy) sp.classList.add('ge-spinning');
|
|
@@ -257,8 +269,11 @@ function autoButton(host: ShellHost): HTMLButtonElement {
|
|
|
257
269
|
return b;
|
|
258
270
|
}
|
|
259
271
|
|
|
272
|
+
/** Same button, three jobs: stop a running run, retire a halted run's leftover count (which frees
|
|
273
|
+
* the disc for a manual spin again), or open the picker. */
|
|
260
274
|
function onAutoplay(host: ShellHost): void {
|
|
261
|
-
|
|
275
|
+
const { active, remaining } = host.state.autoplay;
|
|
276
|
+
if (active || remaining > 0) host.actions.stopAutoplay();
|
|
262
277
|
else host.actions.openAutoplayPicker();
|
|
263
278
|
}
|
|
264
279
|
|
|
@@ -24,10 +24,15 @@ export function openGameInfoModal(host: ShellHost): GameInfoModal {
|
|
|
24
24
|
});
|
|
25
25
|
root.dataset.ge = 'info-modal';
|
|
26
26
|
|
|
27
|
-
const
|
|
28
|
-
// Auto-inject a hotkeys section unless the game already provides one
|
|
27
|
+
const allSections = host.config.gameInfo.sections ?? [];
|
|
28
|
+
// Auto-inject a hotkeys section unless the game already provides one. With hotkeys off — a
|
|
29
|
+
// jurisdiction that forbids them, or a touchscreen that has no keys at all (see core/device.ts) —
|
|
30
|
+
// there is no keyboard surface to document, and a game-supplied section is dropped along with the
|
|
31
|
+
// auto-injected one: a keycap chart for keys the player cannot press is a promise the game breaks.
|
|
32
|
+
const keys = host.config.features.hotkeys !== false;
|
|
33
|
+
const rawSections = keys ? allSections : allSections.filter((s) => s.type !== 'hotkeys');
|
|
29
34
|
const sectionsWithHotkeys: GameInfoSection[] = [...rawSections];
|
|
30
|
-
if (
|
|
35
|
+
if (keys && !rawSections.some((s) => s.type === 'hotkeys')) {
|
|
31
36
|
sectionsWithHotkeys.push({ type: 'hotkeys', order: HOTKEYS_DEFAULT_ORDER });
|
|
32
37
|
}
|
|
33
38
|
const sections = sectionsWithHotkeys;
|
package/src/ui/html/shell.css.ts
CHANGED
|
@@ -57,6 +57,11 @@ export const SHELL_CSS = SHELL_FONT_CSS + SHELL_DIGIT_FONT_CSS + `
|
|
|
57
57
|
/* the STOP glyph is a solid dark square, so the autoplay count is always pure white to read on it. */
|
|
58
58
|
#${SHELL_ROOT_ID} .ge-spin-count { position:absolute; inset:0; display:flex; align-items:center; justify-content:center;
|
|
59
59
|
font-size:22px; font-weight:800; line-height:1; font-variant-numeric:tabular-nums; color:#fff; }
|
|
60
|
+
/* autoplay halted with spins still owed (a lost connection): the run is stopped, so no STOP square —
|
|
61
|
+
the auto glyph sits above the leftover count, both in the disc's own ink, and a tap resumes. */
|
|
62
|
+
#${SHELL_ROOT_ID} .ge-shell-spin.ge-auto-paused { flex-direction:column; gap:1px; position:relative; }
|
|
63
|
+
#${SHELL_ROOT_ID} .ge-auto-paused .ge-spin-auto { display:flex; font-size:.46em; line-height:0; }
|
|
64
|
+
#${SHELL_ROOT_ID} .ge-auto-paused .ge-spin-count { position:static; inset:auto; color:inherit; font-size:20px; }
|
|
60
65
|
|
|
61
66
|
/* BUY BONUS — round accent badge, 2-line label, text pulses + accent glow on hover */
|
|
62
67
|
#${SHELL_ROOT_ID} .ge-shell-buybonus { pointer-events:auto; cursor:pointer; box-sizing:border-box;
|
|
@@ -13,9 +13,19 @@ import {
|
|
|
13
13
|
FsHero,
|
|
14
14
|
divider,
|
|
15
15
|
} from '../primitives/widgets';
|
|
16
|
+
import type { SpinAutoplayMode } from '../primitives/widgets';
|
|
16
17
|
import type { IconName } from '../icons';
|
|
17
18
|
|
|
18
19
|
// ── design constants (mirror the DOM `.ge-bar-panel` / mobile rules) ──────────
|
|
20
|
+
/** Which of the disc's three faces the autoplay state calls for — see `SpinAutoplayMode`. A run
|
|
21
|
+
* that halted with spins still owed (`!active && remaining > 0`) keeps its counter on the disc. */
|
|
22
|
+
function autoplayDiscMode(state: {
|
|
23
|
+
autoplay: { active: boolean; remaining: number };
|
|
24
|
+
}): SpinAutoplayMode {
|
|
25
|
+
if (state.autoplay.active) return 'running';
|
|
26
|
+
return state.autoplay.remaining > 0 ? 'paused' : 'off';
|
|
27
|
+
}
|
|
28
|
+
|
|
19
29
|
const BAR_H = 68; // continuous dark panel height
|
|
20
30
|
const SPIN = 84; // hero disc — pops above/below the bar
|
|
21
31
|
const SPIN_POP = (SPIN - BAR_H) / 2; // 8 — how far the disc sticks out top/bottom
|
|
@@ -296,8 +306,9 @@ export class BottomBar extends Container {
|
|
|
296
306
|
ticker: this.host.ticker,
|
|
297
307
|
onSpin: () => this.host.actions.spin(),
|
|
298
308
|
onStop: () => this.stopAutoplay(),
|
|
309
|
+
onResume: () => this.resumeAutoplay(),
|
|
299
310
|
});
|
|
300
|
-
|
|
311
|
+
this.spin.setAutoplay(autoplayDiscMode(state), state.autoplay.remaining);
|
|
301
312
|
if (state.busy) this.spin.setBusy(true);
|
|
302
313
|
spinWrap.add(this.spin);
|
|
303
314
|
} else if (showFsBlocks) {
|
|
@@ -373,8 +384,9 @@ export class BottomBar extends Container {
|
|
|
373
384
|
ticker: this.host.ticker,
|
|
374
385
|
onSpin: () => this.host.actions.spin(),
|
|
375
386
|
onStop: () => this.stopAutoplay(),
|
|
387
|
+
onResume: () => this.resumeAutoplay(),
|
|
376
388
|
});
|
|
377
|
-
|
|
389
|
+
this.spin.setAutoplay(autoplayDiscMode(state), state.autoplay.remaining);
|
|
378
390
|
if (state.busy) this.spin.setBusy(true);
|
|
379
391
|
hero = this.spin;
|
|
380
392
|
} else if (showFsBlocks) {
|
|
@@ -569,13 +581,20 @@ export class BottomBar extends Container {
|
|
|
569
581
|
private onTurbo(): void {
|
|
570
582
|
this.host.actions.cycleTurbo();
|
|
571
583
|
}
|
|
584
|
+
/** Same button, three jobs: stop a running run, retire a halted run's leftover count (freeing the
|
|
585
|
+
* disc for a manual spin again), or open the picker. Mirrors the DOM bar's `onAutoplay`. */
|
|
572
586
|
private onAutoplay(): void {
|
|
573
|
-
|
|
587
|
+
const { active, remaining } = this.host.state.autoplay;
|
|
588
|
+
if (active || remaining > 0) this.stopAutoplay();
|
|
574
589
|
else this.host.actions.openAutoplayPicker();
|
|
575
590
|
}
|
|
576
591
|
private stopAutoplay(): void {
|
|
577
592
|
this.host.actions.stopAutoplay();
|
|
578
593
|
}
|
|
594
|
+
/** Halted run, tapped: play out the spins it still owes. */
|
|
595
|
+
private resumeAutoplay(): void {
|
|
596
|
+
this.host.actions.startAutoplay(this.host.state.autoplay.remaining);
|
|
597
|
+
}
|
|
579
598
|
private betLocked(): boolean {
|
|
580
599
|
return this.host.state.busy || this.host.state.autoplay.active;
|
|
581
600
|
}
|
|
@@ -37,10 +37,15 @@ export function openGameInfo(host: PixiComponentContext): ShellLayer {
|
|
|
37
37
|
|
|
38
38
|
function buildBody(host: PixiComponentContext, width: number): Container {
|
|
39
39
|
const col = new FlexBox({ direction: 'column', align: 'stretch', gap: 12 });
|
|
40
|
-
const
|
|
41
|
-
// Auto-inject a hotkeys section unless the game already provides one
|
|
40
|
+
const allSections = host.config.gameInfo.sections ?? [];
|
|
41
|
+
// Auto-inject a hotkeys section unless the game already provides one. With hotkeys off — a
|
|
42
|
+
// jurisdiction that forbids them, or a touchscreen that has no keys at all (see core/device.ts) —
|
|
43
|
+
// there is no keyboard surface to document, and a game-supplied section is dropped along with the
|
|
44
|
+
// auto-injected one: a keycap chart for keys the player cannot press is a promise the game breaks.
|
|
45
|
+
const keys = host.config.features.hotkeys !== false;
|
|
46
|
+
const rawSections = keys ? allSections : allSections.filter((s) => s.type !== 'hotkeys');
|
|
42
47
|
const sectionsWithHotkeys: GameInfoSection[] = [...rawSections];
|
|
43
|
-
if (
|
|
48
|
+
if (keys && !rawSections.some((s) => s.type === 'hotkeys')) {
|
|
44
49
|
sectionsWithHotkeys.push({ type: 'hotkeys', order: HOTKEYS_DEFAULT_ORDER });
|
|
45
50
|
}
|
|
46
51
|
const sections = sectionsWithHotkeys;
|
|
@@ -362,8 +362,20 @@ export interface SpinDiscOpts {
|
|
|
362
362
|
ticker: Ticker;
|
|
363
363
|
onSpin: () => void;
|
|
364
364
|
onStop: () => void;
|
|
365
|
+
/** Tapped in the halted-autoplay state — resume the run with the count still on the disc. */
|
|
366
|
+
onResume?: () => void;
|
|
365
367
|
}
|
|
366
368
|
|
|
369
|
+
/**
|
|
370
|
+
* What the disc is showing:
|
|
371
|
+
* - `off` — an ordinary SPIN disc;
|
|
372
|
+
* - `running` — STOP glyph over the live countdown;
|
|
373
|
+
* - `paused` — a run halted with spins still owed (a lost connection). The count stays put and a
|
|
374
|
+
* tap resumes; clearing it is the autoplay button's job. Certification asks for exactly this:
|
|
375
|
+
* the run stops, and after reconnection the counter is still there and still correct.
|
|
376
|
+
*/
|
|
377
|
+
export type SpinAutoplayMode = 'off' | 'running' | 'paused';
|
|
378
|
+
|
|
367
379
|
export class SpinDisc extends Container implements Sizable {
|
|
368
380
|
private size: number;
|
|
369
381
|
private glyphSize: number;
|
|
@@ -375,13 +387,14 @@ export class SpinDisc extends Container implements Sizable {
|
|
|
375
387
|
private glyph: IconView;
|
|
376
388
|
private dim = new Graphics();
|
|
377
389
|
private countText?: Text;
|
|
378
|
-
private mode: 'spin' | 'stop' = 'spin';
|
|
390
|
+
private mode: 'spin' | 'stop' | 'resume' = 'spin';
|
|
379
391
|
private _busy = false;
|
|
380
392
|
private _disabled = false;
|
|
381
393
|
private hovering = false;
|
|
382
394
|
private rotTick?: (t: Ticker) => void;
|
|
383
395
|
private onSpin: () => void;
|
|
384
396
|
private onStop: () => void;
|
|
397
|
+
private onResume: () => void;
|
|
385
398
|
|
|
386
399
|
constructor(opts: SpinDiscOpts) {
|
|
387
400
|
super();
|
|
@@ -391,6 +404,7 @@ export class SpinDisc extends Container implements Sizable {
|
|
|
391
404
|
this.ticker = opts.ticker;
|
|
392
405
|
this.onSpin = opts.onSpin;
|
|
393
406
|
this.onStop = opts.onStop;
|
|
407
|
+
this.onResume = opts.onResume ?? opts.onSpin;
|
|
394
408
|
this.disc = new Graphics();
|
|
395
409
|
this.glyph = makeIcon('spin', this.glyphSize, this.tokens.btnInk);
|
|
396
410
|
this.glyph.position.set((this.size - this.glyphSize) / 2, (this.size - this.glyphSize) / 2);
|
|
@@ -409,6 +423,7 @@ export class SpinDisc extends Container implements Sizable {
|
|
|
409
423
|
attachPress(this, 0.94, () => {
|
|
410
424
|
if (this._disabled) return;
|
|
411
425
|
if (this.mode === 'stop') this.onStop();
|
|
426
|
+
else if (this.mode === 'resume') this.onResume();
|
|
412
427
|
else this.onSpin();
|
|
413
428
|
});
|
|
414
429
|
}
|
|
@@ -420,8 +435,10 @@ export class SpinDisc extends Container implements Sizable {
|
|
|
420
435
|
drawDisc(this.disc, this.size, this.tokens.btn, 4);
|
|
421
436
|
const glyphColor = hot ? this.tokens.accent : this.tokens.btnInk;
|
|
422
437
|
this.glyph.setColor(glyphColor);
|
|
423
|
-
// the count sits ON the solid (btnInk) STOP square, so it must be light to read
|
|
424
|
-
|
|
438
|
+
// the count sits ON the solid (btnInk) STOP square, so it must be light to read; halted, it
|
|
439
|
+
// sits on the bare disc instead and takes the disc's own ink.
|
|
440
|
+
if (this.countText)
|
|
441
|
+
this.countText.style.fill = this.mode === 'resume' ? this.tokens.btnInk : '#ffffff';
|
|
425
442
|
// Disabled (mid-spin / can't spin): darken OPAQUELY (≈ filter:grayscale(.4) brightness(.62))
|
|
426
443
|
// with a dark veil over the disc — not alpha, which would let the bright board show through and
|
|
427
444
|
// read as a translucent/missing button (what looked "transparent" while spinning).
|
|
@@ -431,31 +448,44 @@ export class SpinDisc extends Container implements Sizable {
|
|
|
431
448
|
}
|
|
432
449
|
}
|
|
433
450
|
|
|
434
|
-
/** STOP glyph + remaining-count
|
|
435
|
-
setAutoplay(
|
|
436
|
-
if (
|
|
437
|
-
this.mode = 'stop';
|
|
438
|
-
this.stopRotation();
|
|
439
|
-
// STOP glyph at the disc's full size (like SPIN); the count is centred on top of it.
|
|
440
|
-
this.glyph.visible = false;
|
|
441
|
-
this.stopGlyph();
|
|
442
|
-
if (!this.countText) {
|
|
443
|
-
this.countText = makeText('', { size: 22, weight: '800', color: '#ffffff', align: 'center', family: NUM_FONT_FAMILY });
|
|
444
|
-
this.addChild(this.countText); // added after the STOP glyph → renders on top of it
|
|
445
|
-
}
|
|
446
|
-
const label = Number.isFinite(remaining) ? String(remaining) : '∞';
|
|
447
|
-
setText(this.countText, label);
|
|
448
|
-
this.countText.position.set((this.size - this.countText.width) / 2, (this.size - this.countText.height) / 2);
|
|
449
|
-
} else {
|
|
451
|
+
/** STOP glyph + remaining-count while autoplay runs; auto glyph + the same count once it halts. */
|
|
452
|
+
setAutoplay(mode: SpinAutoplayMode, remaining: number): void {
|
|
453
|
+
if (mode === 'off') {
|
|
450
454
|
this.mode = 'spin';
|
|
451
455
|
this.glyph.visible = true;
|
|
452
456
|
this.removeStopGlyph();
|
|
457
|
+
this.removeAutoGlyph();
|
|
453
458
|
if (this.countText) {
|
|
454
459
|
this.removeChild(this.countText);
|
|
455
460
|
this.countText.destroy();
|
|
456
461
|
this.countText = undefined;
|
|
457
462
|
}
|
|
463
|
+
this.paint();
|
|
464
|
+
return;
|
|
465
|
+
}
|
|
466
|
+
const running = mode === 'running';
|
|
467
|
+
this.mode = running ? 'stop' : 'resume';
|
|
468
|
+
this.stopRotation();
|
|
469
|
+
this.glyph.visible = false;
|
|
470
|
+
// Running: a solid STOP square at the disc's full size (like SPIN), count centred on top of it.
|
|
471
|
+
// Halted: the auto glyph, smaller and lifted, with the count under it — no dark square, because
|
|
472
|
+
// nothing is running to stop, and a white count would have nothing to read against.
|
|
473
|
+
if (running) {
|
|
474
|
+
this.removeAutoGlyph();
|
|
475
|
+
this.stopGlyph();
|
|
476
|
+
} else {
|
|
477
|
+
this.removeStopGlyph();
|
|
478
|
+
this.autoGlyph();
|
|
458
479
|
}
|
|
480
|
+
if (!this.countText) {
|
|
481
|
+
this.countText = makeText('', { size: 22, weight: '800', color: '#ffffff', align: 'center', family: NUM_FONT_FAMILY });
|
|
482
|
+
this.addChild(this.countText); // added after the glyph → renders on top of it
|
|
483
|
+
}
|
|
484
|
+
setText(this.countText, Number.isFinite(remaining) ? String(remaining) : '∞');
|
|
485
|
+
const cy = running
|
|
486
|
+
? (this.size - this.countText.height) / 2
|
|
487
|
+
: this.size * 0.56; // sits under the lifted auto glyph
|
|
488
|
+
this.countText.position.set((this.size - this.countText.width) / 2, cy);
|
|
459
489
|
this.paint();
|
|
460
490
|
}
|
|
461
491
|
|
|
@@ -475,6 +505,22 @@ export class SpinDisc extends Container implements Sizable {
|
|
|
475
505
|
}
|
|
476
506
|
}
|
|
477
507
|
|
|
508
|
+
private autoGlyphView?: IconView;
|
|
509
|
+
private autoGlyph(): void {
|
|
510
|
+
if (this.autoGlyphView) return;
|
|
511
|
+
const size = this.glyphSize * 0.42;
|
|
512
|
+
this.autoGlyphView = makeIcon('autoplay', size, this.tokens.btnInk);
|
|
513
|
+
this.autoGlyphView.position.set((this.size - size) / 2, this.size * 0.2);
|
|
514
|
+
this.addChild(this.autoGlyphView);
|
|
515
|
+
}
|
|
516
|
+
private removeAutoGlyph(): void {
|
|
517
|
+
if (this.autoGlyphView) {
|
|
518
|
+
this.removeChild(this.autoGlyphView);
|
|
519
|
+
this.autoGlyphView.destroy();
|
|
520
|
+
this.autoGlyphView = undefined;
|
|
521
|
+
}
|
|
522
|
+
}
|
|
523
|
+
|
|
478
524
|
setBusy(busy: boolean): void {
|
|
479
525
|
this._busy = busy;
|
|
480
526
|
if (busy && this.mode === 'spin') this.startRotation();
|