@energy8platform/game-engine 0.43.3 → 0.43.5
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/host.cjs.js +62 -22
- package/dist/host.cjs.js.map +1 -1
- package/dist/host.esm.js +62 -22
- package/dist/host.esm.js.map +1 -1
- package/package.json +2 -2
- package/src/host/autoplay.ts +58 -17
- package/src/host/connectionRecovery.ts +21 -7
- package/src/host/createSlotGame.ts +19 -3
- package/src/host/shellConfig.ts +1 -1
package/src/host/autoplay.ts
CHANGED
|
@@ -10,6 +10,12 @@
|
|
|
10
10
|
* it (a lost connection cut the run short; the spins are still owed, and the shell shows them so
|
|
11
11
|
* the player can resume).
|
|
12
12
|
*
|
|
13
|
+
* `hold()`/`release()` are a THIRD thing, and not an ending at all: the tab went to the background,
|
|
14
|
+
* so the host freezes the ticker and no new round may start — but the player did nothing, and the
|
|
15
|
+
* run must come back by itself when they return. A hold is forgotten the moment anything else ends
|
|
16
|
+
* the run (`stop`, `halt`, a new `start`), so a run killed by a lost connection or a failed round
|
|
17
|
+
* while the tab was hidden does not spring back to life on the way in.
|
|
18
|
+
*
|
|
13
19
|
* Sequential by construction: it awaits each round (incl. its bonus drain) before the next, so
|
|
14
20
|
* there is never more than one round in flight. Pure over injected deps — unit-testable.
|
|
15
21
|
*/
|
|
@@ -38,6 +44,18 @@ export interface Autoplay {
|
|
|
38
44
|
* "autoplay stops, and after reconnection the counter is displayed correctly".
|
|
39
45
|
*/
|
|
40
46
|
halt(): void;
|
|
47
|
+
/**
|
|
48
|
+
* Hold the run for a pause the player did not ask for — the tab went to the background. Halts it
|
|
49
|
+
* (counter kept) and remembers that it is owed a resume. No-op when no run is active.
|
|
50
|
+
*
|
|
51
|
+
* A certification lab reads "the autoplay sequence stops when the Stop button is pressed" as
|
|
52
|
+
* *only* the Stop button: switching tabs must not end a run. The host still freezes everything
|
|
53
|
+
* while hidden — this is what makes that freeze temporary rather than terminal.
|
|
54
|
+
*/
|
|
55
|
+
hold(): void;
|
|
56
|
+
/** Resume a run `hold()` put on ice, with the spins it still had. No-op unless a hold is
|
|
57
|
+
* outstanding — anything that ended the run in the meantime cancels it. */
|
|
58
|
+
release(): void;
|
|
41
59
|
readonly active: boolean;
|
|
42
60
|
readonly remaining: number;
|
|
43
61
|
}
|
|
@@ -46,8 +64,10 @@ export function createAutoplayLoop(deps: AutoplayDeps): Autoplay {
|
|
|
46
64
|
let active = false;
|
|
47
65
|
let remaining = 0;
|
|
48
66
|
let running = false; // guards against a second concurrent loop
|
|
67
|
+
let held = false; // a hold() is outstanding — the tab is hidden and the run is owed a resume
|
|
49
68
|
|
|
50
69
|
const stop = (): void => {
|
|
70
|
+
held = false;
|
|
51
71
|
if (!active && remaining === 0) return;
|
|
52
72
|
active = false;
|
|
53
73
|
remaining = 0;
|
|
@@ -55,6 +75,7 @@ export function createAutoplayLoop(deps: AutoplayDeps): Autoplay {
|
|
|
55
75
|
};
|
|
56
76
|
|
|
57
77
|
const halt = (): void => {
|
|
78
|
+
held = false; // whatever is halting the run outranks a pending hold — see hold()'s doc comment
|
|
58
79
|
if (!active) return; // nothing running — nothing to preserve
|
|
59
80
|
active = false;
|
|
60
81
|
deps.onState({ active: false, remaining });
|
|
@@ -66,7 +87,10 @@ export function createAutoplayLoop(deps: AutoplayDeps): Autoplay {
|
|
|
66
87
|
try {
|
|
67
88
|
while (active && remaining > 0) {
|
|
68
89
|
const action = deps.resolveAction();
|
|
69
|
-
if (!deps.canAfford(action)) {
|
|
90
|
+
if (!deps.canAfford(action)) {
|
|
91
|
+
stop();
|
|
92
|
+
return;
|
|
93
|
+
}
|
|
70
94
|
// Decrement at spin START (so the spin in flight is `total − remaining`), then play it out.
|
|
71
95
|
remaining -= 1;
|
|
72
96
|
deps.onState({ active: true, remaining });
|
|
@@ -85,24 +109,41 @@ export function createAutoplayLoop(deps: AutoplayDeps): Autoplay {
|
|
|
85
109
|
}
|
|
86
110
|
}
|
|
87
111
|
|
|
112
|
+
const start = (count: number): void => {
|
|
113
|
+
if (active || count <= 0) return;
|
|
114
|
+
held = false; // a run that is starting is no longer one that owes a resume
|
|
115
|
+
active = true;
|
|
116
|
+
remaining = count;
|
|
117
|
+
deps.onState({ active: true, remaining });
|
|
118
|
+
// `running` bars a SECOND loop, not a new run. After stop(), the round already in flight keeps
|
|
119
|
+
// animating and the loop stays parked on `await playRound`, so `running` is still true for as
|
|
120
|
+
// long as that takes — several seconds through a win. Bailing here made the restart a silent
|
|
121
|
+
// no-op, and silent is what hurt: ShellController.startAutoplay has already lit the disc with
|
|
122
|
+
// the chosen count, so the player saw autoplay running with a counter that never moved.
|
|
123
|
+
// The parked loop re-reads `active`/`remaining` on its next turn and simply carries on with
|
|
124
|
+
// the new budget — no second loop, nothing dropped.
|
|
125
|
+
if (!running) void loop();
|
|
126
|
+
};
|
|
127
|
+
|
|
88
128
|
return {
|
|
89
|
-
start
|
|
90
|
-
if (active || count <= 0) return;
|
|
91
|
-
active = true;
|
|
92
|
-
remaining = count;
|
|
93
|
-
deps.onState({ active: true, remaining });
|
|
94
|
-
// `running` bars a SECOND loop, not a new run. After stop(), the round already in flight keeps
|
|
95
|
-
// animating and the loop stays parked on `await playRound`, so `running` is still true for as
|
|
96
|
-
// long as that takes — several seconds through a win. Bailing here made the restart a silent
|
|
97
|
-
// no-op, and silent is what hurt: ShellController.startAutoplay has already lit the disc with
|
|
98
|
-
// the chosen count, so the player saw autoplay running with a counter that never moved.
|
|
99
|
-
// The parked loop re-reads `active`/`remaining` on its next turn and simply carries on with
|
|
100
|
-
// the new budget — no second loop, nothing dropped.
|
|
101
|
-
if (!running) void loop();
|
|
102
|
-
},
|
|
129
|
+
start,
|
|
103
130
|
stop,
|
|
104
131
|
halt,
|
|
105
|
-
|
|
106
|
-
|
|
132
|
+
hold(): void {
|
|
133
|
+
if (!active) return; // nothing running — nothing to hold, and nothing to bring back
|
|
134
|
+
halt(); // clears `held`; we set it again below, so only a hold() ever leaves it set
|
|
135
|
+
held = true;
|
|
136
|
+
},
|
|
137
|
+
release(): void {
|
|
138
|
+
if (!held) return;
|
|
139
|
+
held = false;
|
|
140
|
+
start(remaining); // no-op when the remainder is gone (something retired it while hidden)
|
|
141
|
+
},
|
|
142
|
+
get active() {
|
|
143
|
+
return active;
|
|
144
|
+
},
|
|
145
|
+
get remaining() {
|
|
146
|
+
return remaining;
|
|
147
|
+
},
|
|
107
148
|
};
|
|
108
149
|
}
|
|
@@ -11,14 +11,21 @@
|
|
|
11
11
|
* cleared to zero on the way. Reconnecting healed the link but nothing took the screen back down.
|
|
12
12
|
*
|
|
13
13
|
* So, per transition:
|
|
14
|
-
* - **lost** — halt autoplay (the counter survives, see `autoplay.halt()`) and put up
|
|
15
|
-
* reconnect overlay. Repeated `lost` (one per failed reconnect attempt) doesn't restack it.
|
|
14
|
+
* - **lost** — halt autoplay (the counter survives the drop, see `autoplay.halt()`) and put up
|
|
15
|
+
* the reconnect overlay. Repeated `lost` (one per failed reconnect attempt) doesn't restack it.
|
|
16
16
|
* - **lost + `ConnectionGone`** — the bridge gave up retrying. That is terminal and honest about
|
|
17
17
|
* it: a Reload modal, not a "Reconnecting…" that will never resolve.
|
|
18
|
-
* - **restored** — take the overlay down and finish what the drop interrupted:
|
|
19
|
-
* still holds an open round, play it out silently to settlement. No modal asks
|
|
20
|
-
* confirm this — it is their own round, they already paid for it, and an extra
|
|
21
|
-
* the very thing the remark objected to.
|
|
18
|
+
* - **restored** — reset autoplay, take the overlay down, and finish what the drop interrupted:
|
|
19
|
+
* if the platform still holds an open round, play it out silently to settlement. No modal asks
|
|
20
|
+
* the player to confirm this — it is their own round, they already paid for it, and an extra
|
|
21
|
+
* screen here is the very thing the remark objected to.
|
|
22
|
+
*
|
|
23
|
+
* Why halt on the way down but reset on the way up: while the link is down the run is stopped yet
|
|
24
|
+
* its remaining spins are still the player's, so the counter must keep showing them rather than
|
|
25
|
+
* snap to zero (the lab's original finding was exactly "the autoplay counter is reset"). Once the
|
|
26
|
+
* link is back, Artube wants the run **over**, not parked one tap from resuming: a run the player
|
|
27
|
+
* ordered before a network drop must not carry on across it, and the player re-orders autoplay
|
|
28
|
+
* deliberately. So `restored` clears the counter and the disc goes back to a plain SPIN.
|
|
22
29
|
*
|
|
23
30
|
* A `restored` that never followed a `lost` does nothing at all: it must not close a modal that
|
|
24
31
|
* belongs to someone else.
|
|
@@ -35,8 +42,12 @@ export interface ConnectionState {
|
|
|
35
42
|
}
|
|
36
43
|
|
|
37
44
|
export interface ConnectionRecoveryDeps {
|
|
38
|
-
/** Stop an autoplay run WITHOUT clearing its counter (`Autoplay.halt`)
|
|
45
|
+
/** Stop an autoplay run WITHOUT clearing its counter (`Autoplay.halt`) — used while the link
|
|
46
|
+
* is down, so the spins still owed stay on screen. */
|
|
39
47
|
haltAutoplay(): void;
|
|
48
|
+
/** End the run and clear its counter (`Autoplay.stop`) — used once the link is back: autoplay
|
|
49
|
+
* does not survive a reconnect. */
|
|
50
|
+
resetAutoplay(): void;
|
|
40
51
|
/** Put up the blocking "Reconnecting…" overlay. */
|
|
41
52
|
showReconnecting(): void;
|
|
42
53
|
/** Put up the terminal "the connection is gone — reload" modal. */
|
|
@@ -106,6 +117,9 @@ export function createConnectionRecovery(deps: ConnectionRecoveryDeps): Connecti
|
|
|
106
117
|
|
|
107
118
|
if (!linkLost) return; // nothing of ours is on screen; nothing of ours to recover
|
|
108
119
|
linkLost = false;
|
|
120
|
+
// The run does not resume across a reconnect. Like the halt above, this runs even when a
|
|
121
|
+
// modal owns the screen — the counter must not outlive the drop that ended the run.
|
|
122
|
+
deps.resetAutoplay();
|
|
109
123
|
if (!blocked()) deps.dismiss();
|
|
110
124
|
await recover();
|
|
111
125
|
},
|
|
@@ -506,6 +506,8 @@ export async function createSlotGame<T extends SlotSpinResultBase = SlotSpinResu
|
|
|
506
506
|
let playErrorOpen = false;
|
|
507
507
|
let stopAutoplay: () => void = () => {}; // wired to the autoplay loop once it's created (below)
|
|
508
508
|
let haltAutoplay: () => void = () => {}; // ditto — stops the run but KEEPS its counter
|
|
509
|
+
let holdAutoplay: () => void = () => {}; // ditto — pauses the run, owing it a resume
|
|
510
|
+
let releaseAutoplay: () => void = () => {}; // ditto — gives back a run that hold() paused
|
|
509
511
|
const reload = () => {
|
|
510
512
|
try {
|
|
511
513
|
window.location.reload();
|
|
@@ -549,6 +551,9 @@ export async function createSlotGame<T extends SlotSpinResultBase = SlotSpinResu
|
|
|
549
551
|
const { createConnectionRecovery } = await import('./connectionRecovery');
|
|
550
552
|
const connection = createConnectionRecovery({
|
|
551
553
|
haltAutoplay: () => haltAutoplay(),
|
|
554
|
+
// A reconnect ends the run for good (Artube): the counter the halt preserved while the link
|
|
555
|
+
// was down is cleared here, and the disc goes back to a plain SPIN.
|
|
556
|
+
resetAutoplay: () => stopAutoplay(),
|
|
552
557
|
showReconnecting: () =>
|
|
553
558
|
shell!.openModal({
|
|
554
559
|
availableClose: false,
|
|
@@ -595,8 +600,10 @@ export async function createSlotGame<T extends SlotSpinResultBase = SlotSpinResu
|
|
|
595
600
|
game.scenes.root.on('pointertap', () => skip.tap(performance.now()));
|
|
596
601
|
|
|
597
602
|
// Full auto-pause: on tab blur, freeze the ticker (stops tweens/onUpdate/in-flight onSpin),
|
|
598
|
-
// duck music to silence, hold autoplay, and notify the scene. On focus, reverse it all
|
|
599
|
-
//
|
|
603
|
+
// duck music to silence, hold autoplay, and notify the scene. On focus, reverse it all —
|
|
604
|
+
// autoplay included: a tab switch is not the player pressing STOP.
|
|
605
|
+
// `holdAutoplay`/`releaseAutoplay` are reassigned in the base-mode block below — the closures
|
|
606
|
+
// read them live.
|
|
600
607
|
const { createPauseController } = await import('./pauseController');
|
|
601
608
|
createPauseController({
|
|
602
609
|
isHidden: () => typeof document !== 'undefined' && document.hidden,
|
|
@@ -608,12 +615,19 @@ export async function createSlotGame<T extends SlotSpinResultBase = SlotSpinResu
|
|
|
608
615
|
onHidden: () => {
|
|
609
616
|
game.app.ticker.stop(); // freezes tweens, onUpdate, in-flight onSpin animation
|
|
610
617
|
game.audio.duckMusic(0); // silence music (ducked to 0; restored on resume)
|
|
611
|
-
|
|
618
|
+
// Hold, don't stop. No auto-round may start while the tab is hidden (the ticker is frozen,
|
|
619
|
+
// so an animation would never finish, and a play fired here spends the player's money out
|
|
620
|
+
// of sight) — but the run is owed back: the lab reads "the autoplay sequence stops when the
|
|
621
|
+
// Stop button is pressed" as ONLY the Stop button, and stopAutoplay() here ended the run and
|
|
622
|
+
// zeroed its counter on every alt-tab. `hold()` forgets itself if anything else ends the run
|
|
623
|
+
// meanwhile (a lost connection, a failed round), so those don't come back on the way in.
|
|
624
|
+
holdAutoplay();
|
|
612
625
|
gameScene()?.onPause?.();
|
|
613
626
|
},
|
|
614
627
|
onVisible: () => {
|
|
615
628
|
game.app.ticker.start();
|
|
616
629
|
game.audio.unduckMusic();
|
|
630
|
+
releaseAutoplay(); // the run the blur paused picks up where it left off
|
|
617
631
|
gameScene()?.onResume?.();
|
|
618
632
|
},
|
|
619
633
|
});
|
|
@@ -849,6 +863,8 @@ export async function createSlotGame<T extends SlotSpinResultBase = SlotSpinResu
|
|
|
849
863
|
});
|
|
850
864
|
stopAutoplay = () => autoplay.stop();
|
|
851
865
|
haltAutoplay = () => autoplay.halt();
|
|
866
|
+
holdAutoplay = () => autoplay.hold();
|
|
867
|
+
releaseAutoplay = () => autoplay.release();
|
|
852
868
|
shell.on('autoplayStart', (o: { remaining?: number }) => autoplay.start(o?.remaining ?? 0));
|
|
853
869
|
shell.on('autoplayStop', () => autoplay.stop());
|
|
854
870
|
|
package/src/host/shellConfig.ts
CHANGED
|
@@ -263,7 +263,7 @@ const DISCLAIMER_TITLE = 'DISCLAIMER';
|
|
|
263
263
|
/** The copyright/brand line ("TM and © {year} Engine.") is shown verbatim; the legal body
|
|
264
264
|
* lines localize. Detecting the brand keeps " Engine" out of the translation lookup entirely. */
|
|
265
265
|
function isBrandLine(line: string): boolean {
|
|
266
|
-
return /
|
|
266
|
+
return /engine/i.test(line);
|
|
267
267
|
}
|
|
268
268
|
|
|
269
269
|
/** A disclaimer section from initData's disclaimer lines; null when none supplied. The legal body is
|