@energy8platform/game-engine 0.2.0 → 0.3.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/README.md +318 -49
- package/dist/animation.cjs.js +191 -1
- package/dist/animation.cjs.js.map +1 -1
- package/dist/animation.d.ts +117 -1
- package/dist/animation.esm.js +192 -3
- package/dist/animation.esm.js.map +1 -1
- package/dist/audio.cjs.js +66 -16
- package/dist/audio.cjs.js.map +1 -1
- package/dist/audio.d.ts +4 -0
- package/dist/audio.esm.js +66 -16
- package/dist/audio.esm.js.map +1 -1
- package/dist/core.cjs.js +310 -84
- package/dist/core.cjs.js.map +1 -1
- package/dist/core.d.ts +60 -1
- package/dist/core.esm.js +311 -85
- package/dist/core.esm.js.map +1 -1
- package/dist/debug.cjs.js +36 -68
- package/dist/debug.cjs.js.map +1 -1
- package/dist/debug.d.ts +4 -6
- package/dist/debug.esm.js +36 -68
- package/dist/debug.esm.js.map +1 -1
- package/dist/index.cjs.js +1250 -251
- package/dist/index.cjs.js.map +1 -1
- package/dist/index.d.ts +386 -41
- package/dist/index.esm.js +1250 -254
- package/dist/index.esm.js.map +1 -1
- package/dist/ui.cjs.js +757 -1
- package/dist/ui.cjs.js.map +1 -1
- package/dist/ui.d.ts +208 -2
- package/dist/ui.esm.js +756 -2
- package/dist/ui.esm.js.map +1 -1
- package/dist/vite.cjs.js +65 -68
- package/dist/vite.cjs.js.map +1 -1
- package/dist/vite.d.ts +17 -23
- package/dist/vite.esm.js +66 -68
- package/dist/vite.esm.js.map +1 -1
- package/package.json +4 -5
- package/src/animation/SpriteAnimation.ts +210 -0
- package/src/animation/Tween.ts +27 -1
- package/src/animation/index.ts +2 -0
- package/src/audio/AudioManager.ts +64 -15
- package/src/core/EventEmitter.ts +7 -1
- package/src/core/GameApplication.ts +25 -7
- package/src/core/SceneManager.ts +3 -1
- package/src/debug/DevBridge.ts +49 -80
- package/src/index.ts +6 -0
- package/src/input/InputManager.ts +26 -0
- package/src/loading/CSSPreloader.ts +7 -33
- package/src/loading/LoadingScene.ts +17 -41
- package/src/loading/index.ts +1 -0
- package/src/loading/logo.ts +95 -0
- package/src/types.ts +4 -0
- package/src/ui/BalanceDisplay.ts +14 -0
- package/src/ui/Button.ts +1 -1
- package/src/ui/Layout.ts +364 -0
- package/src/ui/ScrollContainer.ts +557 -0
- package/src/ui/index.ts +4 -0
- package/src/viewport/ViewportManager.ts +2 -0
- package/src/vite/index.ts +83 -83
package/dist/index.esm.js
CHANGED
|
@@ -1,5 +1,5 @@
|
|
|
1
|
-
import { Ticker, Assets, Container, Application,
|
|
2
|
-
import { CasinoGameSDK } from '@energy8platform/game-sdk';
|
|
1
|
+
import { Ticker, Assets, Container, Text, Application, AnimatedSprite, Texture, Graphics, Sprite, NineSliceSprite } from 'pixi.js';
|
|
2
|
+
import { CasinoGameSDK, Bridge } from '@energy8platform/game-sdk';
|
|
3
3
|
|
|
4
4
|
// ─── Scale Modes ───────────────────────────────────────────
|
|
5
5
|
var ScaleMode;
|
|
@@ -30,6 +30,9 @@ var TransitionType;
|
|
|
30
30
|
/**
|
|
31
31
|
* Minimal typed event emitter.
|
|
32
32
|
* Used internally by GameApplication, SceneManager, AudioManager, etc.
|
|
33
|
+
*
|
|
34
|
+
* Supports `void` event types — events that carry no data can be emitted
|
|
35
|
+
* without arguments: `emitter.emit('eventName')`.
|
|
33
36
|
*/
|
|
34
37
|
// eslint-disable-next-line @typescript-eslint/no-empty-object-type
|
|
35
38
|
class EventEmitter {
|
|
@@ -52,7 +55,8 @@ class EventEmitter {
|
|
|
52
55
|
this.listeners.get(event)?.delete(handler);
|
|
53
56
|
return this;
|
|
54
57
|
}
|
|
55
|
-
emit(
|
|
58
|
+
emit(...args) {
|
|
59
|
+
const [event, data] = args;
|
|
56
60
|
const handlers = this.listeners.get(event);
|
|
57
61
|
if (handlers) {
|
|
58
62
|
for (const handler of handlers) {
|
|
@@ -225,9 +229,20 @@ class Tween {
|
|
|
225
229
|
}
|
|
226
230
|
/**
|
|
227
231
|
* Wait for a given duration (useful in timelines).
|
|
232
|
+
* Uses PixiJS Ticker for consistent timing with other tweens.
|
|
228
233
|
*/
|
|
229
234
|
static delay(ms) {
|
|
230
|
-
return new Promise((resolve) =>
|
|
235
|
+
return new Promise((resolve) => {
|
|
236
|
+
let elapsed = 0;
|
|
237
|
+
const onTick = (ticker) => {
|
|
238
|
+
elapsed += ticker.deltaMS;
|
|
239
|
+
if (elapsed >= ms) {
|
|
240
|
+
Ticker.shared.remove(onTick);
|
|
241
|
+
resolve();
|
|
242
|
+
}
|
|
243
|
+
};
|
|
244
|
+
Ticker.shared.add(onTick);
|
|
245
|
+
});
|
|
231
246
|
}
|
|
232
247
|
/**
|
|
233
248
|
* Kill all tweens on a target.
|
|
@@ -254,6 +269,20 @@ class Tween {
|
|
|
254
269
|
static get activeTweens() {
|
|
255
270
|
return Tween._tweens.length;
|
|
256
271
|
}
|
|
272
|
+
/**
|
|
273
|
+
* Reset the tween system — kill all tweens and remove the ticker.
|
|
274
|
+
* Useful for cleanup between game instances, tests, or hot-reload.
|
|
275
|
+
*/
|
|
276
|
+
static reset() {
|
|
277
|
+
for (const tw of Tween._tweens) {
|
|
278
|
+
tw.resolve();
|
|
279
|
+
}
|
|
280
|
+
Tween._tweens.length = 0;
|
|
281
|
+
if (Tween._tickerAdded) {
|
|
282
|
+
Ticker.shared.remove(Tween.tick);
|
|
283
|
+
Tween._tickerAdded = false;
|
|
284
|
+
}
|
|
285
|
+
}
|
|
257
286
|
// ─── Internal ──────────────────────────────────────────
|
|
258
287
|
static ensureTicker() {
|
|
259
288
|
if (Tween._tickerAdded)
|
|
@@ -456,8 +485,9 @@ class SceneManager extends EventEmitter {
|
|
|
456
485
|
}
|
|
457
486
|
// Transition in
|
|
458
487
|
await this.transitionIn(scene.container, transition);
|
|
459
|
-
|
|
488
|
+
// Push to stack BEFORE onEnter so currentKey is correct during initialization
|
|
460
489
|
this.stack.push({ scene, key });
|
|
490
|
+
await scene.onEnter?.(data);
|
|
461
491
|
this._transitioning = false;
|
|
462
492
|
}
|
|
463
493
|
async popInternal(showTransition, transition) {
|
|
@@ -756,26 +786,51 @@ class AudioManager {
|
|
|
756
786
|
if (!this._initialized || !this._soundModule)
|
|
757
787
|
return;
|
|
758
788
|
const { sound } = this._soundModule;
|
|
759
|
-
// Stop current music
|
|
760
|
-
if (this._currentMusic) {
|
|
789
|
+
// Stop current music with fade-out, start new music with fade-in
|
|
790
|
+
if (this._currentMusic && fadeDuration > 0) {
|
|
791
|
+
const prevAlias = this._currentMusic;
|
|
792
|
+
this._currentMusic = alias;
|
|
793
|
+
if (this._globalMuted || this._categories.music.muted)
|
|
794
|
+
return;
|
|
795
|
+
// Fade out the previous track
|
|
796
|
+
this.fadeVolume(prevAlias, this._categories.music.volume, 0, fadeDuration, () => {
|
|
797
|
+
try {
|
|
798
|
+
sound.stop(prevAlias);
|
|
799
|
+
}
|
|
800
|
+
catch { /* ignore */ }
|
|
801
|
+
});
|
|
802
|
+
// Start new track at zero volume, fade in
|
|
761
803
|
try {
|
|
762
|
-
sound.
|
|
804
|
+
sound.play(alias, {
|
|
805
|
+
volume: 0,
|
|
806
|
+
loop: true,
|
|
807
|
+
});
|
|
808
|
+
this.fadeVolume(alias, 0, this._categories.music.volume, fadeDuration);
|
|
763
809
|
}
|
|
764
|
-
catch {
|
|
765
|
-
|
|
810
|
+
catch (e) {
|
|
811
|
+
console.warn(`[AudioManager] Failed to play music "${alias}":`, e);
|
|
766
812
|
}
|
|
767
813
|
}
|
|
768
|
-
|
|
769
|
-
|
|
770
|
-
|
|
771
|
-
|
|
772
|
-
|
|
773
|
-
|
|
774
|
-
|
|
775
|
-
}
|
|
776
|
-
|
|
777
|
-
|
|
778
|
-
|
|
814
|
+
else {
|
|
815
|
+
// No crossfade — instant switch
|
|
816
|
+
if (this._currentMusic) {
|
|
817
|
+
try {
|
|
818
|
+
sound.stop(this._currentMusic);
|
|
819
|
+
}
|
|
820
|
+
catch { /* ignore */ }
|
|
821
|
+
}
|
|
822
|
+
this._currentMusic = alias;
|
|
823
|
+
if (this._globalMuted || this._categories.music.muted)
|
|
824
|
+
return;
|
|
825
|
+
try {
|
|
826
|
+
sound.play(alias, {
|
|
827
|
+
volume: this._categories.music.volume,
|
|
828
|
+
loop: true,
|
|
829
|
+
});
|
|
830
|
+
}
|
|
831
|
+
catch (e) {
|
|
832
|
+
console.warn(`[AudioManager] Failed to play music "${alias}":`, e);
|
|
833
|
+
}
|
|
779
834
|
}
|
|
780
835
|
}
|
|
781
836
|
/**
|
|
@@ -917,6 +972,31 @@ class AudioManager {
|
|
|
917
972
|
this._initialized = false;
|
|
918
973
|
}
|
|
919
974
|
// ─── Private ───────────────────────────────────────────
|
|
975
|
+
/**
|
|
976
|
+
* Smoothly fade a sound's volume from `fromVol` to `toVol` over `durationMs`.
|
|
977
|
+
*/
|
|
978
|
+
fadeVolume(alias, fromVol, toVol, durationMs, onComplete) {
|
|
979
|
+
if (!this._soundModule)
|
|
980
|
+
return;
|
|
981
|
+
const { sound } = this._soundModule;
|
|
982
|
+
const startTime = Date.now();
|
|
983
|
+
const tick = () => {
|
|
984
|
+
const elapsed = Date.now() - startTime;
|
|
985
|
+
const t = Math.min(elapsed / durationMs, 1);
|
|
986
|
+
const vol = fromVol + (toVol - fromVol) * t;
|
|
987
|
+
try {
|
|
988
|
+
sound.volume(alias, vol);
|
|
989
|
+
}
|
|
990
|
+
catch { /* ignore */ }
|
|
991
|
+
if (t < 1) {
|
|
992
|
+
requestAnimationFrame(tick);
|
|
993
|
+
}
|
|
994
|
+
else {
|
|
995
|
+
onComplete?.();
|
|
996
|
+
}
|
|
997
|
+
};
|
|
998
|
+
requestAnimationFrame(tick);
|
|
999
|
+
}
|
|
920
1000
|
applyVolumes() {
|
|
921
1001
|
if (!this._soundModule)
|
|
922
1002
|
return;
|
|
@@ -1021,6 +1101,10 @@ class InputManager extends EventEmitter {
|
|
|
1021
1101
|
_locked = false;
|
|
1022
1102
|
_keysDown = new Set();
|
|
1023
1103
|
_destroyed = false;
|
|
1104
|
+
// Viewport transform (set by ViewportManager via setViewportTransform)
|
|
1105
|
+
_viewportScale = 1;
|
|
1106
|
+
_viewportOffsetX = 0;
|
|
1107
|
+
_viewportOffsetY = 0;
|
|
1024
1108
|
// Gesture tracking
|
|
1025
1109
|
_pointerStart = null;
|
|
1026
1110
|
_swipeThreshold = 50; // minimum distance in px
|
|
@@ -1047,6 +1131,25 @@ class InputManager extends EventEmitter {
|
|
|
1047
1131
|
isKeyDown(key) {
|
|
1048
1132
|
return this._keysDown.has(key.toLowerCase());
|
|
1049
1133
|
}
|
|
1134
|
+
/**
|
|
1135
|
+
* Update the viewport transform used for DOM→world coordinate mapping.
|
|
1136
|
+
* Called automatically by GameApplication when ViewportManager emits resize.
|
|
1137
|
+
*/
|
|
1138
|
+
setViewportTransform(scale, offsetX, offsetY) {
|
|
1139
|
+
this._viewportScale = scale;
|
|
1140
|
+
this._viewportOffsetX = offsetX;
|
|
1141
|
+
this._viewportOffsetY = offsetY;
|
|
1142
|
+
}
|
|
1143
|
+
/**
|
|
1144
|
+
* Convert a DOM canvas position to game-world coordinates,
|
|
1145
|
+
* accounting for viewport scaling and offset.
|
|
1146
|
+
*/
|
|
1147
|
+
getWorldPosition(canvasX, canvasY) {
|
|
1148
|
+
return {
|
|
1149
|
+
x: (canvasX - this._viewportOffsetX) / this._viewportScale,
|
|
1150
|
+
y: (canvasY - this._viewportOffsetY) / this._viewportScale,
|
|
1151
|
+
};
|
|
1152
|
+
}
|
|
1050
1153
|
/** Destroy the input manager */
|
|
1051
1154
|
destroy() {
|
|
1052
1155
|
this._destroyed = true;
|
|
@@ -1302,6 +1405,8 @@ class ViewportManager extends EventEmitter {
|
|
|
1302
1405
|
this._destroyed = true;
|
|
1303
1406
|
this._resizeObserver?.disconnect();
|
|
1304
1407
|
this._resizeObserver = null;
|
|
1408
|
+
// Remove fallback window resize listener if it was used
|
|
1409
|
+
window.removeEventListener('resize', this.onWindowResize);
|
|
1305
1410
|
if (this._resizeTimeout !== null) {
|
|
1306
1411
|
clearTimeout(this._resizeTimeout);
|
|
1307
1412
|
}
|
|
@@ -1365,45 +1470,87 @@ class Scene {
|
|
|
1365
1470
|
}
|
|
1366
1471
|
|
|
1367
1472
|
/**
|
|
1368
|
-
*
|
|
1369
|
-
*
|
|
1473
|
+
* Shared Energy8 SVG logo with an embedded loader bar.
|
|
1474
|
+
*
|
|
1475
|
+
* The loader bar fill is controlled via a `<clipPath>` whose `<rect>` width
|
|
1476
|
+
* is animatable. Different consumers customise gradient IDs and the clip
|
|
1477
|
+
* element's ID/class to avoid collisions when both CSSPreloader and
|
|
1478
|
+
* LoadingScene appear in the same DOM.
|
|
1370
1479
|
*/
|
|
1371
|
-
|
|
1372
|
-
|
|
1373
|
-
<path d="m241 81.75h-19.28c-1.77 0-6.73 4.98-7.43 6.99l-4.36 12.22c-0.49 1.37 0.05 2.92 1.06 4.32-2.07 1.19-3.69 3.08-4.36 5.43l-3.25 10.41c-0.86 2.89 2.39 6.63 4.31 6.63h19.28c1.96 0 7.4-5.56 7.96-7.51l2.96-10.22c0.63-2.25 0.1-3.98-1.22-4.99 2.55-1.56 3.86-4.14 4.55-6.31l2.77-9.31c0.74-2.57-1.37-7.66-2.99-7.66zm-13.36 28.31-2.27 7.03h-8.28l2.58-8.28h8.28l-0.31 1.25zm4.06-16.97-2.11 6.7h-7.04l2.25-7.34h7.26l-0.36 0.64z" fill="url(#
|
|
1374
|
-
<path d="m202.5 81.75-9.31 14.97-2.32-14.97h-11.82l4.32 25.15-0.57 4.91-8.64 26.44 15.31-12.76 5.63-16.48 19.96-27.26h-12.56z" fill="url(#
|
|
1375
|
-
<path d="m174.2 81.75h-19.78l-5.75 5.16-10.79 33.2c-0.77 2.53 2.48 6.93 4.87 6.93h17.38c2.63 0 7.85-5.34 8.32-6.83l5.37-18.14h-15.17l-2.2 7.64h3.78l-2.25 7.2h-8.01l7.1-25.52h7.58l-1.48 8.4 12.78-5.98c1.28-0.63 1.97-3.99 1.61-6.61-0.36-2.34-1.64-5.45-3.36-5.45z" fill="url(#
|
|
1376
|
-
<path d="m140.6 81.75h-70.6l-5.36 19.37-4.26-19.37h-46.76l2.95 5.88-10.58 39.28h26.84l2.95-9.52-15.63-0.13 2.55-8.34h8.74l8.47-9.81h-14.61l2.11-7.3h15.47l2.54-8.71 2.58 4.74-11.4 39.07h11.05l6.46-21.49 8.84 36.33 19.18-55.67-1.83-3.36 3.68 4.09-12.07 40.1h28.18l3.39-10.31h-17.01l2.67-8.03h9.98l7.58-9.52h-14.28l1.93-6.6h14.61l3.25-9.73 2.81 5.12-11.3 38.89h11.05l5.23-17.81h1.62l1.48 17.6h10.69l-1.48-16.81c4.75-1.28 7.52-5.9 8.64-9.81l2.95-11.3c0.86-2.73-1.43-6.85-3.3-6.85zm-9.8 17.3h-8.69l2.54-7.84h8.35l-2.2 7.84z" fill="url(#
|
|
1377
|
-
<path d="m205.9 148.9h-122.6l-2.61-3.12h-32.4l-2.51 3.12h-1.59c-5.34 0-7.94 4.88-7.94 7.65v0.03c0 4.2 3.55 7.6 7.74 7.6h103.6l2.11 3.12h36.09l1.82-3.12h18.3c5.25 0 6.64-5.3 6.64-7.35v-0.25c0-4.23-2.9-7.68-6.64-7.68zm-0.7 12.83h-160.6c-3.69 0-6.11-2.58-6.11-5.47v-0.03c0-2.89 2.1-5.47 5.61-5.47h161.1c3.45 0 4.89 3.12 4.89 5.65v0.17c0 2.57-2.11 5.15-4.89 5.15z" fill="url(#
|
|
1378
|
-
|
|
1379
|
-
|
|
1380
|
-
|
|
1381
|
-
<path d="m204.5 152.6h-159.8c-2.78 0-4.45 1.69-4.45 3.99v0.11c0 2.04 1.42 3.43 3.64 3.43h160.6c2.88 0 3.67-2.07 3.67-3.43v-0.25c0-2.04-1.48-3.85-3.67-3.85z" fill="url(#ls5)" clip-path="url(#ge-canvas-loader-clip)"/>
|
|
1382
|
-
<text id="ge-loader-pct" x="125" y="196" text-anchor="middle" fill="rgba(255,255,255,0.7)" font-family="-apple-system,BlinkMacSystemFont,'Segoe UI',Roboto,sans-serif" font-size="8" font-weight="600" letter-spacing="1.5">0%</text>
|
|
1383
|
-
<defs>
|
|
1384
|
-
<linearGradient id="ls0" x1="223.7" x2="223.7" y1="81.75" y2="127.8" gradientUnits="userSpaceOnUse">
|
|
1480
|
+
/** SVG path data for the Energy8 wordmark — reused across loaders */
|
|
1481
|
+
const WORDMARK_PATHS = `
|
|
1482
|
+
<path d="m241 81.75h-19.28c-1.77 0-6.73 4.98-7.43 6.99l-4.36 12.22c-0.49 1.37 0.05 2.92 1.06 4.32-2.07 1.19-3.69 3.08-4.36 5.43l-3.25 10.41c-0.86 2.89 2.39 6.63 4.31 6.63h19.28c1.96 0 7.4-5.56 7.96-7.51l2.96-10.22c0.63-2.25 0.1-3.98-1.22-4.99 2.55-1.56 3.86-4.14 4.55-6.31l2.77-9.31c0.74-2.57-1.37-7.66-2.99-7.66zm-13.36 28.31-2.27 7.03h-8.28l2.58-8.28h8.28l-0.31 1.25zm4.06-16.97-2.11 6.7h-7.04l2.25-7.34h7.26l-0.36 0.64z" fill="url(#GID0)"/>
|
|
1483
|
+
<path d="m202.5 81.75-9.31 14.97-2.32-14.97h-11.82l4.32 25.15-0.57 4.91-8.64 26.44 15.31-12.76 5.63-16.48 19.96-27.26h-12.56z" fill="url(#GID1)"/>
|
|
1484
|
+
<path d="m174.2 81.75h-19.78l-5.75 5.16-10.79 33.2c-0.77 2.53 2.48 6.93 4.87 6.93h17.38c2.63 0 7.85-5.34 8.32-6.83l5.37-18.14h-15.17l-2.2 7.64h3.78l-2.25 7.2h-8.01l7.1-25.52h7.58l-1.48 8.4 12.78-5.98c1.28-0.63 1.97-3.99 1.61-6.61-0.36-2.34-1.64-5.45-3.36-5.45z" fill="url(#GID2)"/>
|
|
1485
|
+
<path d="m140.6 81.75h-70.6l-5.36 19.37-4.26-19.37h-46.76l2.95 5.88-10.58 39.28h26.84l2.95-9.52-15.63-0.13 2.55-8.34h8.74l8.47-9.81h-14.61l2.11-7.3h15.47l2.54-8.71 2.58 4.74-11.4 39.07h11.05l6.46-21.49 8.84 36.33 19.18-55.67-1.83-3.36 3.68 4.09-12.07 40.1h28.18l3.39-10.31h-17.01l2.67-8.03h9.98l7.58-9.52h-14.28l1.93-6.6h14.61l3.25-9.73 2.81 5.12-11.3 38.89h11.05l5.23-17.81h1.62l1.48 17.6h10.69l-1.48-16.81c4.75-1.28 7.52-5.9 8.64-9.81l2.95-11.3c0.86-2.73-1.43-6.85-3.3-6.85zm-9.8 17.3h-8.69l2.54-7.84h8.35l-2.2 7.84z" fill="url(#GID3)"/>
|
|
1486
|
+
<path d="m205.9 148.9h-122.6l-2.61-3.12h-32.4l-2.51 3.12h-1.59c-5.34 0-7.94 4.88-7.94 7.65v0.03c0 4.2 3.55 7.6 7.74 7.6h103.6l2.11 3.12h36.09l1.82-3.12h18.3c5.25 0 6.64-5.3 6.64-7.35v-0.25c0-4.23-2.9-7.68-6.64-7.68zm-0.7 12.83h-160.6c-3.69 0-6.11-2.58-6.11-5.47v-0.03c0-2.89 2.1-5.47 5.61-5.47h161.1c3.45 0 4.89 3.12 4.89 5.65v0.17c0 2.57-2.11 5.15-4.89 5.15z" fill="url(#GID4)"/>`;
|
|
1487
|
+
/** Gradient definitions template (gradient IDs are replaced per-consumer) */
|
|
1488
|
+
const GRADIENT_DEFS = `
|
|
1489
|
+
<linearGradient id="GID0" x1="223.7" x2="223.7" y1="81.75" y2="127.8" gradientUnits="userSpaceOnUse">
|
|
1385
1490
|
<stop stop-color="#663BA6"/><stop stop-color="#7939C2" offset=".349"/><stop stop-color="#8A2FC0" offset=".6615"/><stop stop-color="#791BA3" offset="1"/>
|
|
1386
1491
|
</linearGradient>
|
|
1387
|
-
<linearGradient id="
|
|
1492
|
+
<linearGradient id="GID1" x1="194.6" x2="194.6" y1="81.75" y2="138.3" gradientUnits="userSpaceOnUse">
|
|
1388
1493
|
<stop stop-color="#663BA6"/><stop stop-color="#7939C2" offset=".349"/><stop stop-color="#8A2FC0" offset=".6615"/><stop stop-color="#791BA3" offset="1"/>
|
|
1389
1494
|
</linearGradient>
|
|
1390
|
-
<linearGradient id="
|
|
1495
|
+
<linearGradient id="GID2" x1="157.8" x2="157.8" y1="81.75" y2="127" gradientUnits="userSpaceOnUse">
|
|
1391
1496
|
<stop stop-color="#663BA6"/><stop stop-color="#7939C2" offset=".349"/><stop stop-color="#8A2FC0" offset=".6615"/><stop stop-color="#791BA3" offset="1"/>
|
|
1392
1497
|
</linearGradient>
|
|
1393
|
-
<linearGradient id="
|
|
1498
|
+
<linearGradient id="GID3" x1="79.96" x2="79.96" y1="81.75" y2="141.8" gradientUnits="userSpaceOnUse">
|
|
1394
1499
|
<stop stop-color="#663BA6"/><stop stop-color="#7939C2" offset=".349"/><stop stop-color="#8A2FC0" offset=".6615"/><stop stop-color="#791BA3" offset="1"/>
|
|
1395
1500
|
</linearGradient>
|
|
1396
|
-
<linearGradient id="
|
|
1501
|
+
<linearGradient id="GID4" x1="36.18" x2="212.5" y1="156.6" y2="156.6" gradientUnits="userSpaceOnUse">
|
|
1397
1502
|
<stop stop-color="#316FB0"/><stop stop-color="#1FCDE6" offset=".5"/><stop stop-color="#29FEE7" offset="1"/>
|
|
1398
1503
|
</linearGradient>
|
|
1399
|
-
<linearGradient id="
|
|
1504
|
+
<linearGradient id="GID5" x1="40.27" x2="208.2" y1="156.4" y2="156.4" gradientUnits="userSpaceOnUse">
|
|
1400
1505
|
<stop stop-color="#316FB0"/><stop stop-color="#1FCDE6" offset=".5"/><stop stop-color="#29FEE7" offset="1"/>
|
|
1401
|
-
</linearGradient
|
|
1506
|
+
</linearGradient>`;
|
|
1507
|
+
/** Max width of the loader bar in SVG units */
|
|
1508
|
+
const LOADER_BAR_MAX_WIDTH = 174;
|
|
1509
|
+
/**
|
|
1510
|
+
* Build the Energy8 SVG logo with a loader bar, using unique IDs.
|
|
1511
|
+
*
|
|
1512
|
+
* @param opts - Configuration to avoid element ID collisions
|
|
1513
|
+
* @returns SVG markup string
|
|
1514
|
+
*/
|
|
1515
|
+
function buildLogoSVG(opts) {
|
|
1516
|
+
const { idPrefix, svgClass, svgStyle, clipRectClass, clipRectId, textId, textContent, textClass } = opts;
|
|
1517
|
+
// Replace gradient ID placeholders with prefixed versions
|
|
1518
|
+
const paths = WORDMARK_PATHS.replace(/GID(\d)/g, `${idPrefix}$1`);
|
|
1519
|
+
const defs = GRADIENT_DEFS.replace(/GID(\d)/g, `${idPrefix}$1`);
|
|
1520
|
+
const clipId = `${idPrefix}-loader-clip`;
|
|
1521
|
+
const fillGradientId = `${idPrefix}5`;
|
|
1522
|
+
const classAttr = svgClass ? ` class="${svgClass}"` : '';
|
|
1523
|
+
const styleAttr = svgStyle ? ` style="${svgStyle}"` : '';
|
|
1524
|
+
const rectClassAttr = clipRectClass ? ` class="${clipRectClass}"` : '';
|
|
1525
|
+
const rectIdAttr = clipRectId ? ` id="${clipRectId}"` : '';
|
|
1526
|
+
const txtIdAttr = textId ? ` id="${textId}"` : '';
|
|
1527
|
+
const txtClassAttr = textClass ? ` class="${textClass}"` : '';
|
|
1528
|
+
return `<svg xmlns="http://www.w3.org/2000/svg" viewBox="0 0 250 200" fill="none"${classAttr}${styleAttr}>
|
|
1529
|
+
${paths}
|
|
1530
|
+
<clipPath id="${clipId}">
|
|
1531
|
+
<rect${rectIdAttr} x="37" y="148" width="0" height="20"${rectClassAttr}/>
|
|
1532
|
+
</clipPath>
|
|
1533
|
+
<path d="m204.5 152.6h-159.8c-2.78 0-4.45 1.69-4.45 3.99v0.11c0 2.04 1.42 3.43 3.64 3.43h160.6c2.88 0 3.67-2.07 3.67-3.43v-0.25c0-2.04-1.48-3.85-3.67-3.85z" fill="url(#${fillGradientId})" clip-path="url(#${clipId})"/>
|
|
1534
|
+
<text${txtIdAttr} x="125" y="196" text-anchor="middle" fill="rgba(255,255,255,0.6)" font-family="-apple-system,BlinkMacSystemFont,'Segoe UI',Roboto,sans-serif" font-size="8" font-weight="600" letter-spacing="1.5"${txtClassAttr}>${textContent ?? 'Loading...'}</text>
|
|
1535
|
+
<defs>
|
|
1536
|
+
${defs}
|
|
1402
1537
|
</defs>
|
|
1403
1538
|
</svg>`;
|
|
1404
1539
|
}
|
|
1405
|
-
|
|
1406
|
-
|
|
1540
|
+
|
|
1541
|
+
/**
|
|
1542
|
+
* Build the loading scene variant of the logo SVG.
|
|
1543
|
+
* Uses unique IDs (prefixed with 'ls') to avoid collisions with CSSPreloader.
|
|
1544
|
+
*/
|
|
1545
|
+
function buildLoadingLogoSVG() {
|
|
1546
|
+
return buildLogoSVG({
|
|
1547
|
+
idPrefix: 'ls',
|
|
1548
|
+
svgStyle: 'width:100%;height:auto;',
|
|
1549
|
+
clipRectId: 'ge-loader-rect',
|
|
1550
|
+
textId: 'ge-loader-pct',
|
|
1551
|
+
textContent: '0%',
|
|
1552
|
+
});
|
|
1553
|
+
}
|
|
1407
1554
|
/**
|
|
1408
1555
|
* Built-in loading screen using the Energy8 SVG logo with animated loader bar.
|
|
1409
1556
|
*
|
|
@@ -1513,7 +1660,7 @@ class LoadingScene extends Scene {
|
|
|
1513
1660
|
this._overlay.id = '__ge-loading-overlay__';
|
|
1514
1661
|
this._overlay.innerHTML = `
|
|
1515
1662
|
<div class="ge-loading-content">
|
|
1516
|
-
${
|
|
1663
|
+
${buildLoadingLogoSVG()}
|
|
1517
1664
|
</div>
|
|
1518
1665
|
`;
|
|
1519
1666
|
const style = document.createElement('style');
|
|
@@ -1651,8 +1798,11 @@ class LoadingScene extends Scene {
|
|
|
1651
1798
|
}
|
|
1652
1799
|
// Remove overlay
|
|
1653
1800
|
this.removeOverlay();
|
|
1654
|
-
// Navigate to the target scene
|
|
1655
|
-
await this._engine.scenes.goto(this._targetScene,
|
|
1801
|
+
// Navigate to the target scene, always passing the engine reference
|
|
1802
|
+
await this._engine.scenes.goto(this._targetScene, {
|
|
1803
|
+
engine: this._engine,
|
|
1804
|
+
...(this._targetData && typeof this._targetData === 'object' ? this._targetData : { data: this._targetData }),
|
|
1805
|
+
});
|
|
1656
1806
|
}
|
|
1657
1807
|
}
|
|
1658
1808
|
|
|
@@ -1661,39 +1811,12 @@ const PRELOADER_ID = '__ge-css-preloader__';
|
|
|
1661
1811
|
* Inline SVG logo with animated loader bar.
|
|
1662
1812
|
* The `#loader` path acts as the progress fill — animated via clipPath.
|
|
1663
1813
|
*/
|
|
1664
|
-
const LOGO_SVG =
|
|
1665
|
-
|
|
1666
|
-
|
|
1667
|
-
|
|
1668
|
-
|
|
1669
|
-
|
|
1670
|
-
<!-- Loader fill with clip for progress animation -->
|
|
1671
|
-
<clipPath id="ge-loader-clip">
|
|
1672
|
-
<rect x="37" y="148" width="0" height="20" class="ge-clip-rect"/>
|
|
1673
|
-
</clipPath>
|
|
1674
|
-
<path d="m204.5 152.6h-159.8c-2.78 0-4.45 1.69-4.45 3.99v0.11c0 2.04 1.42 3.43 3.64 3.43h160.6c2.88 0 3.67-2.07 3.67-3.43v-0.25c0-2.04-1.48-3.85-3.67-3.85z" fill="url(#pl5)" clip-path="url(#ge-loader-clip)"/>
|
|
1675
|
-
<text x="125" y="196" text-anchor="middle" fill="rgba(255,255,255,0.6)" font-family="-apple-system,BlinkMacSystemFont,'Segoe UI',Roboto,sans-serif" font-size="8" font-weight="600" letter-spacing="1.5" class="ge-preloader-svg-text">Loading...</text>
|
|
1676
|
-
<defs>
|
|
1677
|
-
<linearGradient id="pl0" x1="223.7" x2="223.7" y1="81.75" y2="127.8" gradientUnits="userSpaceOnUse">
|
|
1678
|
-
<stop stop-color="#663BA6"/><stop stop-color="#7939C2" offset=".349"/><stop stop-color="#8A2FC0" offset=".6615"/><stop stop-color="#791BA3" offset="1"/>
|
|
1679
|
-
</linearGradient>
|
|
1680
|
-
<linearGradient id="pl1" x1="194.6" x2="194.6" y1="81.75" y2="138.3" gradientUnits="userSpaceOnUse">
|
|
1681
|
-
<stop stop-color="#663BA6"/><stop stop-color="#7939C2" offset=".349"/><stop stop-color="#8A2FC0" offset=".6615"/><stop stop-color="#791BA3" offset="1"/>
|
|
1682
|
-
</linearGradient>
|
|
1683
|
-
<linearGradient id="pl2" x1="157.8" x2="157.8" y1="81.75" y2="127" gradientUnits="userSpaceOnUse">
|
|
1684
|
-
<stop stop-color="#663BA6"/><stop stop-color="#7939C2" offset=".349"/><stop stop-color="#8A2FC0" offset=".6615"/><stop stop-color="#791BA3" offset="1"/>
|
|
1685
|
-
</linearGradient>
|
|
1686
|
-
<linearGradient id="pl3" x1="79.96" x2="79.96" y1="81.75" y2="141.8" gradientUnits="userSpaceOnUse">
|
|
1687
|
-
<stop stop-color="#663BA6"/><stop stop-color="#7939C2" offset=".349"/><stop stop-color="#8A2FC0" offset=".6615"/><stop stop-color="#791BA3" offset="1"/>
|
|
1688
|
-
</linearGradient>
|
|
1689
|
-
<linearGradient id="pl4" x1="36.18" x2="212.5" y1="156.6" y2="156.6" gradientUnits="userSpaceOnUse">
|
|
1690
|
-
<stop stop-color="#316FB0"/><stop stop-color="#1FCDE6" offset=".5"/><stop stop-color="#29FEE7" offset="1"/>
|
|
1691
|
-
</linearGradient>
|
|
1692
|
-
<linearGradient id="pl5" x1="40.27" x2="208.2" y1="156.4" y2="156.4" gradientUnits="userSpaceOnUse">
|
|
1693
|
-
<stop stop-color="#316FB0"/><stop stop-color="#1FCDE6" offset=".5"/><stop stop-color="#29FEE7" offset="1"/>
|
|
1694
|
-
</linearGradient>
|
|
1695
|
-
</defs>
|
|
1696
|
-
</svg>`;
|
|
1814
|
+
const LOGO_SVG = buildLogoSVG({
|
|
1815
|
+
idPrefix: 'pl',
|
|
1816
|
+
svgClass: 'ge-logo-svg',
|
|
1817
|
+
clipRectClass: 'ge-clip-rect',
|
|
1818
|
+
textClass: 'ge-preloader-svg-text',
|
|
1819
|
+
});
|
|
1697
1820
|
/**
|
|
1698
1821
|
* Creates a lightweight CSS-only preloader that appears instantly,
|
|
1699
1822
|
* BEFORE PixiJS/WebGL is initialized.
|
|
@@ -1797,6 +1920,96 @@ function removeCSSPreloader(container) {
|
|
|
1797
1920
|
});
|
|
1798
1921
|
}
|
|
1799
1922
|
|
|
1923
|
+
/**
|
|
1924
|
+
* FPS overlay for debugging performance.
|
|
1925
|
+
*
|
|
1926
|
+
* Shows FPS, frame time, and draw call count in the corner of the screen.
|
|
1927
|
+
*
|
|
1928
|
+
* @example
|
|
1929
|
+
* ```ts
|
|
1930
|
+
* const fps = new FPSOverlay(app);
|
|
1931
|
+
* fps.show();
|
|
1932
|
+
* ```
|
|
1933
|
+
*/
|
|
1934
|
+
class FPSOverlay {
|
|
1935
|
+
_app;
|
|
1936
|
+
_container;
|
|
1937
|
+
_fpsText;
|
|
1938
|
+
_visible = false;
|
|
1939
|
+
_samples = [];
|
|
1940
|
+
_maxSamples = 60;
|
|
1941
|
+
_lastUpdate = 0;
|
|
1942
|
+
_tickFn = null;
|
|
1943
|
+
constructor(app) {
|
|
1944
|
+
this._app = app;
|
|
1945
|
+
this._container = new Container();
|
|
1946
|
+
this._container.label = 'FPSOverlay';
|
|
1947
|
+
this._container.zIndex = 99999;
|
|
1948
|
+
this._fpsText = new Text({
|
|
1949
|
+
text: 'FPS: --',
|
|
1950
|
+
style: {
|
|
1951
|
+
fontFamily: 'monospace',
|
|
1952
|
+
fontSize: 14,
|
|
1953
|
+
fill: 0x00ff00,
|
|
1954
|
+
stroke: { color: 0x000000, width: 2 },
|
|
1955
|
+
},
|
|
1956
|
+
});
|
|
1957
|
+
this._fpsText.x = 8;
|
|
1958
|
+
this._fpsText.y = 8;
|
|
1959
|
+
this._container.addChild(this._fpsText);
|
|
1960
|
+
}
|
|
1961
|
+
/** Show the FPS overlay */
|
|
1962
|
+
show() {
|
|
1963
|
+
if (this._visible)
|
|
1964
|
+
return;
|
|
1965
|
+
this._visible = true;
|
|
1966
|
+
this._app.stage.addChild(this._container);
|
|
1967
|
+
this._tickFn = (ticker) => {
|
|
1968
|
+
this._samples.push(ticker.FPS);
|
|
1969
|
+
if (this._samples.length > this._maxSamples) {
|
|
1970
|
+
this._samples.shift();
|
|
1971
|
+
}
|
|
1972
|
+
// Update display every ~500ms
|
|
1973
|
+
const now = Date.now();
|
|
1974
|
+
if (now - this._lastUpdate > 500) {
|
|
1975
|
+
const avg = this._samples.reduce((a, b) => a + b, 0) / this._samples.length;
|
|
1976
|
+
const min = Math.min(...this._samples);
|
|
1977
|
+
this._fpsText.text = [
|
|
1978
|
+
`FPS: ${Math.round(avg)} (min: ${Math.round(min)})`,
|
|
1979
|
+
`Frame: ${ticker.deltaMS.toFixed(1)}ms`,
|
|
1980
|
+
].join('\n');
|
|
1981
|
+
this._lastUpdate = now;
|
|
1982
|
+
}
|
|
1983
|
+
};
|
|
1984
|
+
this._app.ticker.add(this._tickFn);
|
|
1985
|
+
}
|
|
1986
|
+
/** Hide the FPS overlay */
|
|
1987
|
+
hide() {
|
|
1988
|
+
if (!this._visible)
|
|
1989
|
+
return;
|
|
1990
|
+
this._visible = false;
|
|
1991
|
+
this._container.removeFromParent();
|
|
1992
|
+
if (this._tickFn) {
|
|
1993
|
+
this._app.ticker.remove(this._tickFn);
|
|
1994
|
+
this._tickFn = null;
|
|
1995
|
+
}
|
|
1996
|
+
}
|
|
1997
|
+
/** Toggle visibility */
|
|
1998
|
+
toggle() {
|
|
1999
|
+
if (this._visible) {
|
|
2000
|
+
this.hide();
|
|
2001
|
+
}
|
|
2002
|
+
else {
|
|
2003
|
+
this.show();
|
|
2004
|
+
}
|
|
2005
|
+
}
|
|
2006
|
+
/** Destroy the overlay */
|
|
2007
|
+
destroy() {
|
|
2008
|
+
this.hide();
|
|
2009
|
+
this._container.destroy({ children: true });
|
|
2010
|
+
}
|
|
2011
|
+
}
|
|
2012
|
+
|
|
1800
2013
|
/**
|
|
1801
2014
|
* The main entry point for a game built on @energy8platform/game-engine.
|
|
1802
2015
|
*
|
|
@@ -1844,6 +2057,8 @@ class GameApplication extends EventEmitter {
|
|
|
1844
2057
|
viewport;
|
|
1845
2058
|
/** SDK instance (null in offline mode) */
|
|
1846
2059
|
sdk = null;
|
|
2060
|
+
/** FPS overlay instance (only when debug: true) */
|
|
2061
|
+
fpsOverlay = null;
|
|
1847
2062
|
/** Data received from SDK initialization */
|
|
1848
2063
|
initData = null;
|
|
1849
2064
|
/** Configuration */
|
|
@@ -1911,14 +2126,15 @@ class GameApplication extends EventEmitter {
|
|
|
1911
2126
|
this.applySDKConfig();
|
|
1912
2127
|
// 6. Initialize sub-systems
|
|
1913
2128
|
this.initSubSystems();
|
|
1914
|
-
this.emit('initialized'
|
|
2129
|
+
this.emit('initialized');
|
|
1915
2130
|
// 7. Remove CSS preloader, show Canvas loading screen
|
|
1916
2131
|
removeCSSPreloader(this._container);
|
|
1917
2132
|
// 8. Load assets with loading screen
|
|
1918
2133
|
await this.loadAssets(firstScene, sceneData);
|
|
2134
|
+
this.emit('loaded');
|
|
1919
2135
|
// 9. Start the game loop
|
|
1920
2136
|
this._running = true;
|
|
1921
|
-
this.emit('started'
|
|
2137
|
+
this.emit('started');
|
|
1922
2138
|
}
|
|
1923
2139
|
catch (err) {
|
|
1924
2140
|
console.error('[GameEngine] Failed to start:', err);
|
|
@@ -1940,8 +2156,8 @@ class GameApplication extends EventEmitter {
|
|
|
1940
2156
|
this.viewport?.destroy();
|
|
1941
2157
|
this.sdk?.destroy();
|
|
1942
2158
|
this.app?.destroy(true, { children: true, texture: true });
|
|
2159
|
+
this.emit('destroyed');
|
|
1943
2160
|
this.removeAllListeners();
|
|
1944
|
-
this.emit('destroyed', undefined);
|
|
1945
2161
|
}
|
|
1946
2162
|
// ─── Private initialization steps ──────────────────────
|
|
1947
2163
|
resolveContainer() {
|
|
@@ -2014,14 +2230,19 @@ class GameApplication extends EventEmitter {
|
|
|
2014
2230
|
});
|
|
2015
2231
|
// Wire SceneManager to the PixiJS stage
|
|
2016
2232
|
this.scenes.setRoot(this.app.stage);
|
|
2017
|
-
// Wire viewport resize → scene manager
|
|
2018
|
-
this.viewport.on('resize', ({ width, height }) => {
|
|
2233
|
+
// Wire viewport resize → scene manager + input manager
|
|
2234
|
+
this.viewport.on('resize', ({ width, height, scale }) => {
|
|
2019
2235
|
this.scenes.resize(width, height);
|
|
2236
|
+
this.input.setViewportTransform(scale, this.app.stage.x, this.app.stage.y);
|
|
2020
2237
|
this.emit('resize', { width, height });
|
|
2021
2238
|
});
|
|
2022
2239
|
this.viewport.on('orientationChange', (orientation) => {
|
|
2023
2240
|
this.emit('orientationChange', orientation);
|
|
2024
2241
|
});
|
|
2242
|
+
// Wire scene changes → engine event
|
|
2243
|
+
this.scenes.on('change', ({ from, to }) => {
|
|
2244
|
+
this.emit('sceneChange', { from, to });
|
|
2245
|
+
});
|
|
2025
2246
|
// Connect ticker → scene updates
|
|
2026
2247
|
this.app.ticker.add((ticker) => {
|
|
2027
2248
|
// Always update scenes (loading screen needs onUpdate before _running=true)
|
|
@@ -2029,6 +2250,11 @@ class GameApplication extends EventEmitter {
|
|
|
2029
2250
|
});
|
|
2030
2251
|
// Trigger initial resize
|
|
2031
2252
|
this.viewport.refresh();
|
|
2253
|
+
// Enable FPS overlay in debug mode
|
|
2254
|
+
if (this.config.debug) {
|
|
2255
|
+
this.fpsOverlay = new FPSOverlay(this.app);
|
|
2256
|
+
this.fpsOverlay.show();
|
|
2257
|
+
}
|
|
2032
2258
|
}
|
|
2033
2259
|
async loadAssets(firstScene, sceneData) {
|
|
2034
2260
|
// Register built-in loading scene
|
|
@@ -2488,6 +2714,170 @@ class SpineHelper {
|
|
|
2488
2714
|
}
|
|
2489
2715
|
}
|
|
2490
2716
|
|
|
2717
|
+
/**
|
|
2718
|
+
* Helper for creating frame-based animations from spritesheets.
|
|
2719
|
+
*
|
|
2720
|
+
* Wraps PixiJS `AnimatedSprite` with a convenient API for
|
|
2721
|
+
* common iGaming effects: coin showers, symbol animations,
|
|
2722
|
+
* sparkle trails, win celebrations.
|
|
2723
|
+
*
|
|
2724
|
+
* Cheaper than Spine for simple frame sequences.
|
|
2725
|
+
*
|
|
2726
|
+
* @example
|
|
2727
|
+
* ```ts
|
|
2728
|
+
* // From an array of textures
|
|
2729
|
+
* const coinAnim = SpriteAnimation.create(coinTextures, {
|
|
2730
|
+
* fps: 30,
|
|
2731
|
+
* loop: true,
|
|
2732
|
+
* });
|
|
2733
|
+
* scene.addChild(coinAnim);
|
|
2734
|
+
*
|
|
2735
|
+
* // From a spritesheet with a naming pattern
|
|
2736
|
+
* const sheet = Assets.get('effects');
|
|
2737
|
+
* const sparkle = SpriteAnimation.fromSpritesheet(sheet, 'sparkle_');
|
|
2738
|
+
* sparkle.play();
|
|
2739
|
+
*
|
|
2740
|
+
* // From a numbered range
|
|
2741
|
+
* const explosion = SpriteAnimation.fromRange(sheet, 'explosion_{i}', 0, 24, {
|
|
2742
|
+
* fps: 60,
|
|
2743
|
+
* loop: false,
|
|
2744
|
+
* onComplete: () => explosion.destroy(),
|
|
2745
|
+
* });
|
|
2746
|
+
* ```
|
|
2747
|
+
*/
|
|
2748
|
+
class SpriteAnimation {
|
|
2749
|
+
/**
|
|
2750
|
+
* Create an animated sprite from an array of textures.
|
|
2751
|
+
*
|
|
2752
|
+
* @param textures - Array of PixiJS Textures
|
|
2753
|
+
* @param config - Animation options
|
|
2754
|
+
* @returns Configured AnimatedSprite
|
|
2755
|
+
*/
|
|
2756
|
+
static create(textures, config = {}) {
|
|
2757
|
+
const sprite = new AnimatedSprite(textures);
|
|
2758
|
+
// Configure
|
|
2759
|
+
sprite.animationSpeed = (config.fps ?? 24) / 60; // PixiJS uses speed relative to 60fps ticker
|
|
2760
|
+
sprite.loop = config.loop ?? true;
|
|
2761
|
+
// Anchor
|
|
2762
|
+
if (config.anchor !== undefined) {
|
|
2763
|
+
if (typeof config.anchor === 'number') {
|
|
2764
|
+
sprite.anchor.set(config.anchor);
|
|
2765
|
+
}
|
|
2766
|
+
else {
|
|
2767
|
+
sprite.anchor.set(config.anchor.x, config.anchor.y);
|
|
2768
|
+
}
|
|
2769
|
+
}
|
|
2770
|
+
else {
|
|
2771
|
+
sprite.anchor.set(0.5);
|
|
2772
|
+
}
|
|
2773
|
+
// Complete callback
|
|
2774
|
+
if (config.onComplete) {
|
|
2775
|
+
sprite.onComplete = config.onComplete;
|
|
2776
|
+
}
|
|
2777
|
+
// Auto-play
|
|
2778
|
+
if (config.autoPlay !== false) {
|
|
2779
|
+
sprite.play();
|
|
2780
|
+
}
|
|
2781
|
+
return sprite;
|
|
2782
|
+
}
|
|
2783
|
+
/**
|
|
2784
|
+
* Create an animated sprite from a spritesheet using a name prefix.
|
|
2785
|
+
*
|
|
2786
|
+
* Collects all textures whose keys start with `prefix`, sorted alphabetically.
|
|
2787
|
+
*
|
|
2788
|
+
* @param sheet - PixiJS Spritesheet instance
|
|
2789
|
+
* @param prefix - Texture name prefix (e.g., 'coin_')
|
|
2790
|
+
* @param config - Animation options
|
|
2791
|
+
* @returns Configured AnimatedSprite
|
|
2792
|
+
*/
|
|
2793
|
+
static fromSpritesheet(sheet, prefix, config = {}) {
|
|
2794
|
+
const textures = SpriteAnimation.getTexturesByPrefix(sheet, prefix);
|
|
2795
|
+
if (textures.length === 0) {
|
|
2796
|
+
console.warn(`[SpriteAnimation] No textures found with prefix "${prefix}"`);
|
|
2797
|
+
}
|
|
2798
|
+
return SpriteAnimation.create(textures, config);
|
|
2799
|
+
}
|
|
2800
|
+
/**
|
|
2801
|
+
* Create an animated sprite from a numbered range of frames.
|
|
2802
|
+
*
|
|
2803
|
+
* The `pattern` string should contain `{i}` as a placeholder for the frame number.
|
|
2804
|
+
* Numbers are zero-padded to match the length of `start`.
|
|
2805
|
+
*
|
|
2806
|
+
* @param sheet - PixiJS Spritesheet instance
|
|
2807
|
+
* @param pattern - Frame name pattern, e.g. 'explosion_{i}'
|
|
2808
|
+
* @param start - Start frame index (inclusive)
|
|
2809
|
+
* @param end - End frame index (inclusive)
|
|
2810
|
+
* @param config - Animation options
|
|
2811
|
+
* @returns Configured AnimatedSprite
|
|
2812
|
+
*/
|
|
2813
|
+
static fromRange(sheet, pattern, start, end, config = {}) {
|
|
2814
|
+
const textures = [];
|
|
2815
|
+
const padLength = String(end).length;
|
|
2816
|
+
for (let i = start; i <= end; i++) {
|
|
2817
|
+
const name = pattern.replace('{i}', String(i).padStart(padLength, '0'));
|
|
2818
|
+
const texture = sheet.textures[name];
|
|
2819
|
+
if (texture) {
|
|
2820
|
+
textures.push(texture);
|
|
2821
|
+
}
|
|
2822
|
+
else {
|
|
2823
|
+
console.warn(`[SpriteAnimation] Missing frame: "${name}"`);
|
|
2824
|
+
}
|
|
2825
|
+
}
|
|
2826
|
+
if (textures.length === 0) {
|
|
2827
|
+
console.warn(`[SpriteAnimation] No textures found for pattern "${pattern}" [${start}..${end}]`);
|
|
2828
|
+
}
|
|
2829
|
+
return SpriteAnimation.create(textures, config);
|
|
2830
|
+
}
|
|
2831
|
+
/**
|
|
2832
|
+
* Create an AnimatedSprite from texture aliases (loaded via AssetManager).
|
|
2833
|
+
*
|
|
2834
|
+
* @param aliases - Array of texture aliases
|
|
2835
|
+
* @param config - Animation options
|
|
2836
|
+
* @returns Configured AnimatedSprite
|
|
2837
|
+
*/
|
|
2838
|
+
static fromAliases(aliases, config = {}) {
|
|
2839
|
+
const textures = aliases.map((alias) => {
|
|
2840
|
+
const tex = Texture.from(alias);
|
|
2841
|
+
return tex;
|
|
2842
|
+
});
|
|
2843
|
+
return SpriteAnimation.create(textures, config);
|
|
2844
|
+
}
|
|
2845
|
+
/**
|
|
2846
|
+
* Play a one-shot animation and auto-destroy when complete.
|
|
2847
|
+
* Useful for fire-and-forget effects like coin bursts.
|
|
2848
|
+
*
|
|
2849
|
+
* @param textures - Array of textures
|
|
2850
|
+
* @param config - Animation options (loop will be forced to false)
|
|
2851
|
+
* @returns Promise that resolves when animation completes
|
|
2852
|
+
*/
|
|
2853
|
+
static playOnce(textures, config = {}) {
|
|
2854
|
+
const finished = new Promise((resolve) => {
|
|
2855
|
+
config = {
|
|
2856
|
+
...config,
|
|
2857
|
+
loop: false,
|
|
2858
|
+
onComplete: () => {
|
|
2859
|
+
config.onComplete?.();
|
|
2860
|
+
sprite.destroy();
|
|
2861
|
+
resolve();
|
|
2862
|
+
},
|
|
2863
|
+
};
|
|
2864
|
+
});
|
|
2865
|
+
const sprite = SpriteAnimation.create(textures, config);
|
|
2866
|
+
return { sprite, finished };
|
|
2867
|
+
}
|
|
2868
|
+
// ─── Utility ───────────────────────────────────────────
|
|
2869
|
+
/**
|
|
2870
|
+
* Get all textures from a spritesheet that start with a given prefix.
|
|
2871
|
+
* Results are sorted alphabetically by key.
|
|
2872
|
+
*/
|
|
2873
|
+
static getTexturesByPrefix(sheet, prefix) {
|
|
2874
|
+
const keys = Object.keys(sheet.textures)
|
|
2875
|
+
.filter((k) => k.startsWith(prefix))
|
|
2876
|
+
.sort();
|
|
2877
|
+
return keys.map((k) => sheet.textures[k]);
|
|
2878
|
+
}
|
|
2879
|
+
}
|
|
2880
|
+
|
|
2491
2881
|
const DEFAULT_COLORS = {
|
|
2492
2882
|
normal: 0xffd700,
|
|
2493
2883
|
hover: 0xffe44d,
|
|
@@ -2934,6 +3324,7 @@ class BalanceDisplay extends Container {
|
|
|
2934
3324
|
_currentValue = 0;
|
|
2935
3325
|
_displayedValue = 0;
|
|
2936
3326
|
_animating = false;
|
|
3327
|
+
_animationCancelled = false;
|
|
2937
3328
|
constructor(config = {}) {
|
|
2938
3329
|
super();
|
|
2939
3330
|
this._config = {
|
|
@@ -2995,11 +3386,22 @@ class BalanceDisplay extends Container {
|
|
|
2995
3386
|
this.updateDisplay();
|
|
2996
3387
|
}
|
|
2997
3388
|
async animateValue(from, to) {
|
|
3389
|
+
// Cancel any ongoing animation
|
|
3390
|
+
if (this._animating) {
|
|
3391
|
+
this._animationCancelled = true;
|
|
3392
|
+
}
|
|
2998
3393
|
this._animating = true;
|
|
3394
|
+
this._animationCancelled = false;
|
|
2999
3395
|
const duration = this._config.animationDuration;
|
|
3000
3396
|
const startTime = Date.now();
|
|
3001
3397
|
return new Promise((resolve) => {
|
|
3002
3398
|
const tick = () => {
|
|
3399
|
+
// If cancelled by a newer animation, stop immediately
|
|
3400
|
+
if (this._animationCancelled) {
|
|
3401
|
+
this._animating = false;
|
|
3402
|
+
resolve();
|
|
3403
|
+
return;
|
|
3404
|
+
}
|
|
3003
3405
|
const elapsed = Date.now() - startTime;
|
|
3004
3406
|
const t = Math.min(elapsed / duration, 1);
|
|
3005
3407
|
const eased = Easing.easeOutCubic(t);
|
|
@@ -3308,15 +3710,732 @@ class Toast extends Container {
|
|
|
3308
3710
|
}
|
|
3309
3711
|
}
|
|
3310
3712
|
|
|
3311
|
-
|
|
3312
|
-
|
|
3313
|
-
|
|
3314
|
-
|
|
3315
|
-
|
|
3316
|
-
|
|
3317
|
-
|
|
3318
|
-
|
|
3319
|
-
|
|
3713
|
+
/**
|
|
3714
|
+
* Responsive layout container that automatically arranges its children.
|
|
3715
|
+
*
|
|
3716
|
+
* Supports horizontal, vertical, grid, and wrap layout modes with
|
|
3717
|
+
* alignment, padding, gap, and viewport-anchor positioning.
|
|
3718
|
+
* Breakpoints allow different layouts for different screen sizes.
|
|
3719
|
+
*
|
|
3720
|
+
* @example
|
|
3721
|
+
* ```ts
|
|
3722
|
+
* const toolbar = new Layout({
|
|
3723
|
+
* direction: 'horizontal',
|
|
3724
|
+
* gap: 20,
|
|
3725
|
+
* alignment: 'center',
|
|
3726
|
+
* anchor: 'bottom-center',
|
|
3727
|
+
* padding: 16,
|
|
3728
|
+
* breakpoints: {
|
|
3729
|
+
* 768: { direction: 'vertical', gap: 10 },
|
|
3730
|
+
* },
|
|
3731
|
+
* });
|
|
3732
|
+
*
|
|
3733
|
+
* toolbar.addItem(spinButton);
|
|
3734
|
+
* toolbar.addItem(betLabel);
|
|
3735
|
+
* scene.container.addChild(toolbar);
|
|
3736
|
+
*
|
|
3737
|
+
* // On resize, update layout position relative to viewport
|
|
3738
|
+
* toolbar.updateViewport(width, height);
|
|
3739
|
+
* ```
|
|
3740
|
+
*/
|
|
3741
|
+
class Layout extends Container {
|
|
3742
|
+
_config;
|
|
3743
|
+
_padding;
|
|
3744
|
+
_anchor;
|
|
3745
|
+
_maxWidth;
|
|
3746
|
+
_breakpoints;
|
|
3747
|
+
_content;
|
|
3748
|
+
_items = [];
|
|
3749
|
+
_viewportWidth = 0;
|
|
3750
|
+
_viewportHeight = 0;
|
|
3751
|
+
constructor(config = {}) {
|
|
3752
|
+
super();
|
|
3753
|
+
this._config = {
|
|
3754
|
+
direction: config.direction ?? 'vertical',
|
|
3755
|
+
gap: config.gap ?? 0,
|
|
3756
|
+
alignment: config.alignment ?? 'start',
|
|
3757
|
+
autoLayout: config.autoLayout ?? true,
|
|
3758
|
+
columns: config.columns ?? 2,
|
|
3759
|
+
};
|
|
3760
|
+
this._padding = Layout.normalizePadding(config.padding ?? 0);
|
|
3761
|
+
this._anchor = config.anchor ?? 'top-left';
|
|
3762
|
+
this._maxWidth = config.maxWidth ?? Infinity;
|
|
3763
|
+
// Sort breakpoints by width ascending for correct resolution
|
|
3764
|
+
this._breakpoints = config.breakpoints
|
|
3765
|
+
? Object.entries(config.breakpoints)
|
|
3766
|
+
.map(([w, cfg]) => [Number(w), cfg])
|
|
3767
|
+
.sort((a, b) => a[0] - b[0])
|
|
3768
|
+
: [];
|
|
3769
|
+
this._content = new Container();
|
|
3770
|
+
this.addChild(this._content);
|
|
3771
|
+
}
|
|
3772
|
+
/** Add an item to the layout */
|
|
3773
|
+
addItem(child) {
|
|
3774
|
+
this._items.push(child);
|
|
3775
|
+
this._content.addChild(child);
|
|
3776
|
+
if (this._config.autoLayout)
|
|
3777
|
+
this.layout();
|
|
3778
|
+
return this;
|
|
3779
|
+
}
|
|
3780
|
+
/** Remove an item from the layout */
|
|
3781
|
+
removeItem(child) {
|
|
3782
|
+
const idx = this._items.indexOf(child);
|
|
3783
|
+
if (idx !== -1) {
|
|
3784
|
+
this._items.splice(idx, 1);
|
|
3785
|
+
this._content.removeChild(child);
|
|
3786
|
+
if (this._config.autoLayout)
|
|
3787
|
+
this.layout();
|
|
3788
|
+
}
|
|
3789
|
+
return this;
|
|
3790
|
+
}
|
|
3791
|
+
/** Remove all items */
|
|
3792
|
+
clearItems() {
|
|
3793
|
+
for (const item of this._items) {
|
|
3794
|
+
this._content.removeChild(item);
|
|
3795
|
+
}
|
|
3796
|
+
this._items.length = 0;
|
|
3797
|
+
if (this._config.autoLayout)
|
|
3798
|
+
this.layout();
|
|
3799
|
+
return this;
|
|
3800
|
+
}
|
|
3801
|
+
/** Get all layout items */
|
|
3802
|
+
get items() {
|
|
3803
|
+
return this._items;
|
|
3804
|
+
}
|
|
3805
|
+
/**
|
|
3806
|
+
* Update the viewport size and recalculate layout.
|
|
3807
|
+
* Should be called from `Scene.onResize()`.
|
|
3808
|
+
*/
|
|
3809
|
+
updateViewport(width, height) {
|
|
3810
|
+
this._viewportWidth = width;
|
|
3811
|
+
this._viewportHeight = height;
|
|
3812
|
+
this.layout();
|
|
3813
|
+
}
|
|
3814
|
+
/**
|
|
3815
|
+
* Recalculate layout positions of all children.
|
|
3816
|
+
*/
|
|
3817
|
+
layout() {
|
|
3818
|
+
if (this._items.length === 0)
|
|
3819
|
+
return;
|
|
3820
|
+
// Resolve effective config (apply breakpoint overrides)
|
|
3821
|
+
const effective = this.resolveConfig();
|
|
3822
|
+
const gap = effective.gap ?? this._config.gap;
|
|
3823
|
+
const direction = effective.direction ?? this._config.direction;
|
|
3824
|
+
const alignment = effective.alignment ?? this._config.alignment;
|
|
3825
|
+
const columns = effective.columns ?? this._config.columns;
|
|
3826
|
+
const padding = effective.padding !== undefined
|
|
3827
|
+
? Layout.normalizePadding(effective.padding)
|
|
3828
|
+
: this._padding;
|
|
3829
|
+
const maxWidth = effective.maxWidth ?? this._maxWidth;
|
|
3830
|
+
const [pt, pr, pb, pl] = padding;
|
|
3831
|
+
switch (direction) {
|
|
3832
|
+
case 'horizontal':
|
|
3833
|
+
this.layoutLinear('x', 'y', gap, alignment, pl, pt);
|
|
3834
|
+
break;
|
|
3835
|
+
case 'vertical':
|
|
3836
|
+
this.layoutLinear('y', 'x', gap, alignment, pt, pl);
|
|
3837
|
+
break;
|
|
3838
|
+
case 'grid':
|
|
3839
|
+
this.layoutGrid(columns, gap, alignment, pl, pt);
|
|
3840
|
+
break;
|
|
3841
|
+
case 'wrap':
|
|
3842
|
+
this.layoutWrap(maxWidth - pl - pr, gap, alignment, pl, pt);
|
|
3843
|
+
break;
|
|
3844
|
+
}
|
|
3845
|
+
// Apply anchor positioning relative to viewport
|
|
3846
|
+
this.applyAnchor(effective.anchor ?? this._anchor);
|
|
3847
|
+
}
|
|
3848
|
+
// ─── Private layout helpers ────────────────────────────
|
|
3849
|
+
layoutLinear(mainAxis, crossAxis, gap, alignment, mainOffset, crossOffset) {
|
|
3850
|
+
let pos = mainOffset;
|
|
3851
|
+
const sizes = this._items.map(item => this.getItemSize(item));
|
|
3852
|
+
const maxCross = Math.max(...sizes.map(s => (crossAxis === 'x' ? s.width : s.height)));
|
|
3853
|
+
for (let i = 0; i < this._items.length; i++) {
|
|
3854
|
+
const item = this._items[i];
|
|
3855
|
+
const size = sizes[i];
|
|
3856
|
+
item[mainAxis] = pos;
|
|
3857
|
+
// Cross-axis alignment
|
|
3858
|
+
const itemCross = crossAxis === 'x' ? size.width : size.height;
|
|
3859
|
+
switch (alignment) {
|
|
3860
|
+
case 'start':
|
|
3861
|
+
item[crossAxis] = crossOffset;
|
|
3862
|
+
break;
|
|
3863
|
+
case 'center':
|
|
3864
|
+
item[crossAxis] = crossOffset + (maxCross - itemCross) / 2;
|
|
3865
|
+
break;
|
|
3866
|
+
case 'end':
|
|
3867
|
+
item[crossAxis] = crossOffset + maxCross - itemCross;
|
|
3868
|
+
break;
|
|
3869
|
+
case 'stretch':
|
|
3870
|
+
item[crossAxis] = crossOffset;
|
|
3871
|
+
// Note: stretch doesn't resize children — that's up to the item
|
|
3872
|
+
break;
|
|
3873
|
+
}
|
|
3874
|
+
const mainSize = mainAxis === 'x' ? size.width : size.height;
|
|
3875
|
+
pos += mainSize + gap;
|
|
3876
|
+
}
|
|
3877
|
+
}
|
|
3878
|
+
layoutGrid(columns, gap, alignment, offsetX, offsetY) {
|
|
3879
|
+
const sizes = this._items.map(item => this.getItemSize(item));
|
|
3880
|
+
const maxItemWidth = Math.max(...sizes.map(s => s.width));
|
|
3881
|
+
const maxItemHeight = Math.max(...sizes.map(s => s.height));
|
|
3882
|
+
const cellW = maxItemWidth + gap;
|
|
3883
|
+
const cellH = maxItemHeight + gap;
|
|
3884
|
+
for (let i = 0; i < this._items.length; i++) {
|
|
3885
|
+
const item = this._items[i];
|
|
3886
|
+
const col = i % columns;
|
|
3887
|
+
const row = Math.floor(i / columns);
|
|
3888
|
+
const size = sizes[i];
|
|
3889
|
+
// X alignment within cell
|
|
3890
|
+
switch (alignment) {
|
|
3891
|
+
case 'center':
|
|
3892
|
+
item.x = offsetX + col * cellW + (maxItemWidth - size.width) / 2;
|
|
3893
|
+
break;
|
|
3894
|
+
case 'end':
|
|
3895
|
+
item.x = offsetX + col * cellW + maxItemWidth - size.width;
|
|
3896
|
+
break;
|
|
3897
|
+
default:
|
|
3898
|
+
item.x = offsetX + col * cellW;
|
|
3899
|
+
}
|
|
3900
|
+
item.y = offsetY + row * cellH;
|
|
3901
|
+
}
|
|
3902
|
+
}
|
|
3903
|
+
layoutWrap(maxWidth, gap, alignment, offsetX, offsetY) {
|
|
3904
|
+
let x = offsetX;
|
|
3905
|
+
let y = offsetY;
|
|
3906
|
+
let rowHeight = 0;
|
|
3907
|
+
const sizes = this._items.map(item => this.getItemSize(item));
|
|
3908
|
+
for (let i = 0; i < this._items.length; i++) {
|
|
3909
|
+
const item = this._items[i];
|
|
3910
|
+
const size = sizes[i];
|
|
3911
|
+
// Check if item fits in current row
|
|
3912
|
+
if (x + size.width > maxWidth + offsetX && x > offsetX) {
|
|
3913
|
+
// Wrap to next row
|
|
3914
|
+
x = offsetX;
|
|
3915
|
+
y += rowHeight + gap;
|
|
3916
|
+
rowHeight = 0;
|
|
3917
|
+
}
|
|
3918
|
+
item.x = x;
|
|
3919
|
+
item.y = y;
|
|
3920
|
+
x += size.width + gap;
|
|
3921
|
+
rowHeight = Math.max(rowHeight, size.height);
|
|
3922
|
+
}
|
|
3923
|
+
}
|
|
3924
|
+
applyAnchor(anchor) {
|
|
3925
|
+
if (this._viewportWidth === 0 || this._viewportHeight === 0)
|
|
3926
|
+
return;
|
|
3927
|
+
const bounds = this._content.getBounds();
|
|
3928
|
+
const contentW = bounds.width;
|
|
3929
|
+
const contentH = bounds.height;
|
|
3930
|
+
const vw = this._viewportWidth;
|
|
3931
|
+
const vh = this._viewportHeight;
|
|
3932
|
+
let anchorX = 0;
|
|
3933
|
+
let anchorY = 0;
|
|
3934
|
+
// Horizontal
|
|
3935
|
+
if (anchor.includes('left')) {
|
|
3936
|
+
anchorX = 0;
|
|
3937
|
+
}
|
|
3938
|
+
else if (anchor.includes('right')) {
|
|
3939
|
+
anchorX = vw - contentW;
|
|
3940
|
+
}
|
|
3941
|
+
else {
|
|
3942
|
+
// center
|
|
3943
|
+
anchorX = (vw - contentW) / 2;
|
|
3944
|
+
}
|
|
3945
|
+
// Vertical
|
|
3946
|
+
if (anchor.startsWith('top')) {
|
|
3947
|
+
anchorY = 0;
|
|
3948
|
+
}
|
|
3949
|
+
else if (anchor.startsWith('bottom')) {
|
|
3950
|
+
anchorY = vh - contentH;
|
|
3951
|
+
}
|
|
3952
|
+
else {
|
|
3953
|
+
// center
|
|
3954
|
+
anchorY = (vh - contentH) / 2;
|
|
3955
|
+
}
|
|
3956
|
+
// Compensate for content's local bounds offset
|
|
3957
|
+
this.x = anchorX - bounds.x;
|
|
3958
|
+
this.y = anchorY - bounds.y;
|
|
3959
|
+
}
|
|
3960
|
+
resolveConfig() {
|
|
3961
|
+
if (this._breakpoints.length === 0 || this._viewportWidth === 0) {
|
|
3962
|
+
return {};
|
|
3963
|
+
}
|
|
3964
|
+
// Find the largest breakpoint that's ≤ current viewport width
|
|
3965
|
+
let resolved = {};
|
|
3966
|
+
for (const [maxWidth, overrides] of this._breakpoints) {
|
|
3967
|
+
if (this._viewportWidth <= maxWidth) {
|
|
3968
|
+
resolved = overrides;
|
|
3969
|
+
break;
|
|
3970
|
+
}
|
|
3971
|
+
}
|
|
3972
|
+
return resolved;
|
|
3973
|
+
}
|
|
3974
|
+
getItemSize(item) {
|
|
3975
|
+
const bounds = item.getBounds();
|
|
3976
|
+
return { width: bounds.width, height: bounds.height };
|
|
3977
|
+
}
|
|
3978
|
+
static normalizePadding(padding) {
|
|
3979
|
+
if (typeof padding === 'number') {
|
|
3980
|
+
return [padding, padding, padding, padding];
|
|
3981
|
+
}
|
|
3982
|
+
return padding;
|
|
3983
|
+
}
|
|
3984
|
+
}
|
|
3985
|
+
|
|
3986
|
+
/**
|
|
3987
|
+
* Scrollable container with touch/drag, mouse wheel, inertia, and optional scrollbar.
|
|
3988
|
+
*
|
|
3989
|
+
* Perfect for paytables, settings panels, bet history, and any scrollable content
|
|
3990
|
+
* that doesn't fit on screen.
|
|
3991
|
+
*
|
|
3992
|
+
* @example
|
|
3993
|
+
* ```ts
|
|
3994
|
+
* const scroll = new ScrollContainer({
|
|
3995
|
+
* width: 600,
|
|
3996
|
+
* height: 400,
|
|
3997
|
+
* direction: 'vertical',
|
|
3998
|
+
* showScrollbar: true,
|
|
3999
|
+
* elasticity: 0.3,
|
|
4000
|
+
* });
|
|
4001
|
+
*
|
|
4002
|
+
* // Add content taller than 400px
|
|
4003
|
+
* const list = new Container();
|
|
4004
|
+
* for (let i = 0; i < 50; i++) {
|
|
4005
|
+
* const row = createRow(i);
|
|
4006
|
+
* row.y = i * 40;
|
|
4007
|
+
* list.addChild(row);
|
|
4008
|
+
* }
|
|
4009
|
+
* scroll.setContent(list);
|
|
4010
|
+
*
|
|
4011
|
+
* scene.container.addChild(scroll);
|
|
4012
|
+
* ```
|
|
4013
|
+
*/
|
|
4014
|
+
class ScrollContainer extends Container {
|
|
4015
|
+
_config;
|
|
4016
|
+
_viewport;
|
|
4017
|
+
_content = null;
|
|
4018
|
+
_mask;
|
|
4019
|
+
_bg;
|
|
4020
|
+
_scrollbarV = null;
|
|
4021
|
+
_scrollbarH = null;
|
|
4022
|
+
_scrollbarFadeTimeout = null;
|
|
4023
|
+
// Scroll state
|
|
4024
|
+
_scrollX = 0;
|
|
4025
|
+
_scrollY = 0;
|
|
4026
|
+
_velocityX = 0;
|
|
4027
|
+
_velocityY = 0;
|
|
4028
|
+
_isDragging = false;
|
|
4029
|
+
_dragStart = { x: 0, y: 0 };
|
|
4030
|
+
_scrollStart = { x: 0, y: 0 };
|
|
4031
|
+
_lastDragPos = { x: 0, y: 0 };
|
|
4032
|
+
_lastDragTime = 0;
|
|
4033
|
+
_isAnimating = false;
|
|
4034
|
+
_animationFrame = null;
|
|
4035
|
+
constructor(config) {
|
|
4036
|
+
super();
|
|
4037
|
+
this._config = {
|
|
4038
|
+
width: config.width,
|
|
4039
|
+
height: config.height,
|
|
4040
|
+
direction: config.direction ?? 'vertical',
|
|
4041
|
+
showScrollbar: config.showScrollbar ?? true,
|
|
4042
|
+
scrollbarWidth: config.scrollbarWidth ?? 6,
|
|
4043
|
+
scrollbarColor: config.scrollbarColor ?? 0xffffff,
|
|
4044
|
+
scrollbarAlpha: config.scrollbarAlpha ?? 0.4,
|
|
4045
|
+
elasticity: config.elasticity ?? 0.3,
|
|
4046
|
+
inertia: config.inertia ?? 0.92,
|
|
4047
|
+
snapSize: config.snapSize ?? 0,
|
|
4048
|
+
borderRadius: config.borderRadius ?? 0,
|
|
4049
|
+
};
|
|
4050
|
+
// Background
|
|
4051
|
+
this._bg = new Graphics();
|
|
4052
|
+
if (config.backgroundColor !== undefined) {
|
|
4053
|
+
this._bg.roundRect(0, 0, config.width, config.height, this._config.borderRadius)
|
|
4054
|
+
.fill({ color: config.backgroundColor, alpha: config.backgroundAlpha ?? 1 });
|
|
4055
|
+
}
|
|
4056
|
+
this.addChild(this._bg);
|
|
4057
|
+
// Viewport (masked area)
|
|
4058
|
+
this._viewport = new Container();
|
|
4059
|
+
this.addChild(this._viewport);
|
|
4060
|
+
// Mask
|
|
4061
|
+
this._mask = new Graphics();
|
|
4062
|
+
this._mask.roundRect(0, 0, config.width, config.height, this._config.borderRadius)
|
|
4063
|
+
.fill(0xffffff);
|
|
4064
|
+
this.addChild(this._mask);
|
|
4065
|
+
this._viewport.mask = this._mask;
|
|
4066
|
+
// Scrollbars
|
|
4067
|
+
if (this._config.showScrollbar) {
|
|
4068
|
+
if (this._config.direction !== 'horizontal') {
|
|
4069
|
+
this._scrollbarV = new Graphics();
|
|
4070
|
+
this._scrollbarV.alpha = 0;
|
|
4071
|
+
this.addChild(this._scrollbarV);
|
|
4072
|
+
}
|
|
4073
|
+
if (this._config.direction !== 'vertical') {
|
|
4074
|
+
this._scrollbarH = new Graphics();
|
|
4075
|
+
this._scrollbarH.alpha = 0;
|
|
4076
|
+
this.addChild(this._scrollbarH);
|
|
4077
|
+
}
|
|
4078
|
+
}
|
|
4079
|
+
// Interaction
|
|
4080
|
+
this.eventMode = 'static';
|
|
4081
|
+
this.cursor = 'grab';
|
|
4082
|
+
this.hitArea = { contains: (x, y) => x >= 0 && x <= config.width && y >= 0 && y <= config.height };
|
|
4083
|
+
this.on('pointerdown', this.onPointerDown);
|
|
4084
|
+
this.on('pointermove', this.onPointerMove);
|
|
4085
|
+
this.on('pointerup', this.onPointerUp);
|
|
4086
|
+
this.on('pointerupoutside', this.onPointerUp);
|
|
4087
|
+
this.on('wheel', this.onWheel);
|
|
4088
|
+
}
|
|
4089
|
+
/** Set scrollable content. Replaces any existing content. */
|
|
4090
|
+
setContent(content) {
|
|
4091
|
+
if (this._content) {
|
|
4092
|
+
this._viewport.removeChild(this._content);
|
|
4093
|
+
}
|
|
4094
|
+
this._content = content;
|
|
4095
|
+
this._viewport.addChild(content);
|
|
4096
|
+
this._scrollX = 0;
|
|
4097
|
+
this._scrollY = 0;
|
|
4098
|
+
this.applyScroll();
|
|
4099
|
+
}
|
|
4100
|
+
/** Get the content container */
|
|
4101
|
+
get content() {
|
|
4102
|
+
return this._content;
|
|
4103
|
+
}
|
|
4104
|
+
/** Scroll to a specific position (in content coordinates) */
|
|
4105
|
+
scrollTo(x, y, animate = true) {
|
|
4106
|
+
if (!animate) {
|
|
4107
|
+
this._scrollX = x;
|
|
4108
|
+
this._scrollY = y;
|
|
4109
|
+
this.clampScroll();
|
|
4110
|
+
this.applyScroll();
|
|
4111
|
+
return;
|
|
4112
|
+
}
|
|
4113
|
+
this.animateScrollTo(x, y);
|
|
4114
|
+
}
|
|
4115
|
+
/** Scroll to make a specific item/child visible */
|
|
4116
|
+
scrollToItem(index) {
|
|
4117
|
+
if (this._config.snapSize > 0) {
|
|
4118
|
+
const pos = index * this._config.snapSize;
|
|
4119
|
+
if (this._config.direction === 'horizontal') {
|
|
4120
|
+
this.scrollTo(pos, this._scrollY);
|
|
4121
|
+
}
|
|
4122
|
+
else {
|
|
4123
|
+
this.scrollTo(this._scrollX, pos);
|
|
4124
|
+
}
|
|
4125
|
+
}
|
|
4126
|
+
}
|
|
4127
|
+
/** Current scroll position */
|
|
4128
|
+
get scrollPosition() {
|
|
4129
|
+
return { x: this._scrollX, y: this._scrollY };
|
|
4130
|
+
}
|
|
4131
|
+
/** Resize the scroll viewport */
|
|
4132
|
+
resize(width, height) {
|
|
4133
|
+
this._config.width = width;
|
|
4134
|
+
this._config.height = height;
|
|
4135
|
+
// Redraw mask and background
|
|
4136
|
+
this._mask.clear();
|
|
4137
|
+
this._mask.roundRect(0, 0, width, height, this._config.borderRadius).fill(0xffffff);
|
|
4138
|
+
this._bg.clear();
|
|
4139
|
+
this.hitArea = { contains: (x, y) => x >= 0 && x <= width && y >= 0 && y <= height };
|
|
4140
|
+
this.clampScroll();
|
|
4141
|
+
this.applyScroll();
|
|
4142
|
+
}
|
|
4143
|
+
/** Destroy and clean up */
|
|
4144
|
+
destroy(options) {
|
|
4145
|
+
this.stopAnimation();
|
|
4146
|
+
if (this._scrollbarFadeTimeout !== null) {
|
|
4147
|
+
clearTimeout(this._scrollbarFadeTimeout);
|
|
4148
|
+
}
|
|
4149
|
+
this.off('pointerdown', this.onPointerDown);
|
|
4150
|
+
this.off('pointermove', this.onPointerMove);
|
|
4151
|
+
this.off('pointerup', this.onPointerUp);
|
|
4152
|
+
this.off('pointerupoutside', this.onPointerUp);
|
|
4153
|
+
this.off('wheel', this.onWheel);
|
|
4154
|
+
super.destroy(options);
|
|
4155
|
+
}
|
|
4156
|
+
// ─── Scroll mechanics ─────────────────────────────────
|
|
4157
|
+
get contentWidth() {
|
|
4158
|
+
if (!this._content)
|
|
4159
|
+
return 0;
|
|
4160
|
+
const bounds = this._content.getBounds();
|
|
4161
|
+
return bounds.width;
|
|
4162
|
+
}
|
|
4163
|
+
get contentHeight() {
|
|
4164
|
+
if (!this._content)
|
|
4165
|
+
return 0;
|
|
4166
|
+
const bounds = this._content.getBounds();
|
|
4167
|
+
return bounds.height;
|
|
4168
|
+
}
|
|
4169
|
+
get maxScrollX() {
|
|
4170
|
+
return Math.max(0, this.contentWidth - this._config.width);
|
|
4171
|
+
}
|
|
4172
|
+
get maxScrollY() {
|
|
4173
|
+
return Math.max(0, this.contentHeight - this._config.height);
|
|
4174
|
+
}
|
|
4175
|
+
canScrollX() {
|
|
4176
|
+
return this._config.direction === 'horizontal' || this._config.direction === 'both';
|
|
4177
|
+
}
|
|
4178
|
+
canScrollY() {
|
|
4179
|
+
return this._config.direction === 'vertical' || this._config.direction === 'both';
|
|
4180
|
+
}
|
|
4181
|
+
clampScroll() {
|
|
4182
|
+
if (this.canScrollX()) {
|
|
4183
|
+
this._scrollX = Math.max(0, Math.min(this._scrollX, this.maxScrollX));
|
|
4184
|
+
}
|
|
4185
|
+
else {
|
|
4186
|
+
this._scrollX = 0;
|
|
4187
|
+
}
|
|
4188
|
+
if (this.canScrollY()) {
|
|
4189
|
+
this._scrollY = Math.max(0, Math.min(this._scrollY, this.maxScrollY));
|
|
4190
|
+
}
|
|
4191
|
+
else {
|
|
4192
|
+
this._scrollY = 0;
|
|
4193
|
+
}
|
|
4194
|
+
}
|
|
4195
|
+
applyScroll() {
|
|
4196
|
+
if (!this._content)
|
|
4197
|
+
return;
|
|
4198
|
+
this._content.x = -this._scrollX;
|
|
4199
|
+
this._content.y = -this._scrollY;
|
|
4200
|
+
this.updateScrollbars();
|
|
4201
|
+
}
|
|
4202
|
+
// ─── Input handlers ────────────────────────────────────
|
|
4203
|
+
onPointerDown = (e) => {
|
|
4204
|
+
this._isDragging = true;
|
|
4205
|
+
this._isAnimating = false;
|
|
4206
|
+
this.stopAnimation();
|
|
4207
|
+
this.cursor = 'grabbing';
|
|
4208
|
+
const local = e.getLocalPosition(this);
|
|
4209
|
+
this._dragStart = { x: local.x, y: local.y };
|
|
4210
|
+
this._scrollStart = { x: this._scrollX, y: this._scrollY };
|
|
4211
|
+
this._lastDragPos = { x: local.x, y: local.y };
|
|
4212
|
+
this._lastDragTime = Date.now();
|
|
4213
|
+
this._velocityX = 0;
|
|
4214
|
+
this._velocityY = 0;
|
|
4215
|
+
this.showScrollbars();
|
|
4216
|
+
};
|
|
4217
|
+
onPointerMove = (e) => {
|
|
4218
|
+
if (!this._isDragging)
|
|
4219
|
+
return;
|
|
4220
|
+
const local = e.getLocalPosition(this);
|
|
4221
|
+
const dx = local.x - this._dragStart.x;
|
|
4222
|
+
const dy = local.y - this._dragStart.y;
|
|
4223
|
+
const now = Date.now();
|
|
4224
|
+
const dt = Math.max(1, now - this._lastDragTime);
|
|
4225
|
+
// Calculate velocity for inertia
|
|
4226
|
+
this._velocityX = (local.x - this._lastDragPos.x) / dt * 16; // normalize to ~60fps
|
|
4227
|
+
this._velocityY = (local.y - this._lastDragPos.y) / dt * 16;
|
|
4228
|
+
this._lastDragPos = { x: local.x, y: local.y };
|
|
4229
|
+
this._lastDragTime = now;
|
|
4230
|
+
// Apply scroll with elasticity for overscroll
|
|
4231
|
+
let newX = this._scrollStart.x - dx;
|
|
4232
|
+
let newY = this._scrollStart.y - dy;
|
|
4233
|
+
const elasticity = this._config.elasticity;
|
|
4234
|
+
if (this.canScrollX()) {
|
|
4235
|
+
if (newX < 0)
|
|
4236
|
+
newX *= elasticity;
|
|
4237
|
+
else if (newX > this.maxScrollX)
|
|
4238
|
+
newX = this.maxScrollX + (newX - this.maxScrollX) * elasticity;
|
|
4239
|
+
this._scrollX = newX;
|
|
4240
|
+
}
|
|
4241
|
+
if (this.canScrollY()) {
|
|
4242
|
+
if (newY < 0)
|
|
4243
|
+
newY *= elasticity;
|
|
4244
|
+
else if (newY > this.maxScrollY)
|
|
4245
|
+
newY = this.maxScrollY + (newY - this.maxScrollY) * elasticity;
|
|
4246
|
+
this._scrollY = newY;
|
|
4247
|
+
}
|
|
4248
|
+
this.applyScroll();
|
|
4249
|
+
};
|
|
4250
|
+
onPointerUp = () => {
|
|
4251
|
+
if (!this._isDragging)
|
|
4252
|
+
return;
|
|
4253
|
+
this._isDragging = false;
|
|
4254
|
+
this.cursor = 'grab';
|
|
4255
|
+
// Start inertia
|
|
4256
|
+
if (Math.abs(this._velocityX) > 0.5 || Math.abs(this._velocityY) > 0.5) {
|
|
4257
|
+
this.startInertia();
|
|
4258
|
+
}
|
|
4259
|
+
else {
|
|
4260
|
+
this.snapAndBounce();
|
|
4261
|
+
}
|
|
4262
|
+
};
|
|
4263
|
+
onWheel = (e) => {
|
|
4264
|
+
e.preventDefault?.();
|
|
4265
|
+
const delta = e.deltaY ?? 0;
|
|
4266
|
+
const deltaX = e.deltaX ?? 0;
|
|
4267
|
+
if (this.canScrollY()) {
|
|
4268
|
+
this._scrollY += delta * 0.5;
|
|
4269
|
+
}
|
|
4270
|
+
if (this.canScrollX()) {
|
|
4271
|
+
this._scrollX += deltaX * 0.5;
|
|
4272
|
+
}
|
|
4273
|
+
this.clampScroll();
|
|
4274
|
+
this.applyScroll();
|
|
4275
|
+
this.showScrollbars();
|
|
4276
|
+
this.scheduleScrollbarFade();
|
|
4277
|
+
};
|
|
4278
|
+
// ─── Inertia & snap ───────────────────────────────────
|
|
4279
|
+
startInertia() {
|
|
4280
|
+
this._isAnimating = true;
|
|
4281
|
+
const tick = () => {
|
|
4282
|
+
if (!this._isAnimating)
|
|
4283
|
+
return;
|
|
4284
|
+
this._velocityX *= this._config.inertia;
|
|
4285
|
+
this._velocityY *= this._config.inertia;
|
|
4286
|
+
if (this.canScrollX())
|
|
4287
|
+
this._scrollX -= this._velocityX;
|
|
4288
|
+
if (this.canScrollY())
|
|
4289
|
+
this._scrollY -= this._velocityY;
|
|
4290
|
+
// Bounce back if overscrolled
|
|
4291
|
+
let bounced = false;
|
|
4292
|
+
if (this.canScrollX()) {
|
|
4293
|
+
if (this._scrollX < 0) {
|
|
4294
|
+
this._scrollX *= 0.8;
|
|
4295
|
+
bounced = true;
|
|
4296
|
+
}
|
|
4297
|
+
else if (this._scrollX > this.maxScrollX) {
|
|
4298
|
+
this._scrollX = this.maxScrollX + (this._scrollX - this.maxScrollX) * 0.8;
|
|
4299
|
+
bounced = true;
|
|
4300
|
+
}
|
|
4301
|
+
}
|
|
4302
|
+
if (this.canScrollY()) {
|
|
4303
|
+
if (this._scrollY < 0) {
|
|
4304
|
+
this._scrollY *= 0.8;
|
|
4305
|
+
bounced = true;
|
|
4306
|
+
}
|
|
4307
|
+
else if (this._scrollY > this.maxScrollY) {
|
|
4308
|
+
this._scrollY = this.maxScrollY + (this._scrollY - this.maxScrollY) * 0.8;
|
|
4309
|
+
bounced = true;
|
|
4310
|
+
}
|
|
4311
|
+
}
|
|
4312
|
+
this.applyScroll();
|
|
4313
|
+
const speed = Math.abs(this._velocityX) + Math.abs(this._velocityY);
|
|
4314
|
+
if (speed < 0.1 && !bounced) {
|
|
4315
|
+
this._isAnimating = false;
|
|
4316
|
+
this.snapAndBounce();
|
|
4317
|
+
return;
|
|
4318
|
+
}
|
|
4319
|
+
this._animationFrame = requestAnimationFrame(tick);
|
|
4320
|
+
};
|
|
4321
|
+
this._animationFrame = requestAnimationFrame(tick);
|
|
4322
|
+
}
|
|
4323
|
+
snapAndBounce() {
|
|
4324
|
+
// Clamp first
|
|
4325
|
+
let targetX = Math.max(0, Math.min(this._scrollX, this.maxScrollX));
|
|
4326
|
+
let targetY = Math.max(0, Math.min(this._scrollY, this.maxScrollY));
|
|
4327
|
+
// Snap
|
|
4328
|
+
if (this._config.snapSize > 0) {
|
|
4329
|
+
if (this.canScrollY()) {
|
|
4330
|
+
targetY = Math.round(targetY / this._config.snapSize) * this._config.snapSize;
|
|
4331
|
+
targetY = Math.max(0, Math.min(targetY, this.maxScrollY));
|
|
4332
|
+
}
|
|
4333
|
+
if (this.canScrollX()) {
|
|
4334
|
+
targetX = Math.round(targetX / this._config.snapSize) * this._config.snapSize;
|
|
4335
|
+
targetX = Math.max(0, Math.min(targetX, this.maxScrollX));
|
|
4336
|
+
}
|
|
4337
|
+
}
|
|
4338
|
+
if (Math.abs(targetX - this._scrollX) < 0.5 && Math.abs(targetY - this._scrollY) < 0.5) {
|
|
4339
|
+
this._scrollX = targetX;
|
|
4340
|
+
this._scrollY = targetY;
|
|
4341
|
+
this.applyScroll();
|
|
4342
|
+
this.scheduleScrollbarFade();
|
|
4343
|
+
return;
|
|
4344
|
+
}
|
|
4345
|
+
this.animateScrollTo(targetX, targetY);
|
|
4346
|
+
}
|
|
4347
|
+
animateScrollTo(targetX, targetY) {
|
|
4348
|
+
this._isAnimating = true;
|
|
4349
|
+
const startX = this._scrollX;
|
|
4350
|
+
const startY = this._scrollY;
|
|
4351
|
+
const startTime = Date.now();
|
|
4352
|
+
const duration = 300;
|
|
4353
|
+
const tick = () => {
|
|
4354
|
+
if (!this._isAnimating)
|
|
4355
|
+
return;
|
|
4356
|
+
const elapsed = Date.now() - startTime;
|
|
4357
|
+
const t = Math.min(elapsed / duration, 1);
|
|
4358
|
+
// easeOutCubic
|
|
4359
|
+
const eased = 1 - Math.pow(1 - t, 3);
|
|
4360
|
+
this._scrollX = startX + (targetX - startX) * eased;
|
|
4361
|
+
this._scrollY = startY + (targetY - startY) * eased;
|
|
4362
|
+
this.applyScroll();
|
|
4363
|
+
if (t < 1) {
|
|
4364
|
+
this._animationFrame = requestAnimationFrame(tick);
|
|
4365
|
+
}
|
|
4366
|
+
else {
|
|
4367
|
+
this._isAnimating = false;
|
|
4368
|
+
this.scheduleScrollbarFade();
|
|
4369
|
+
}
|
|
4370
|
+
};
|
|
4371
|
+
this._animationFrame = requestAnimationFrame(tick);
|
|
4372
|
+
}
|
|
4373
|
+
stopAnimation() {
|
|
4374
|
+
this._isAnimating = false;
|
|
4375
|
+
if (this._animationFrame !== null) {
|
|
4376
|
+
cancelAnimationFrame(this._animationFrame);
|
|
4377
|
+
this._animationFrame = null;
|
|
4378
|
+
}
|
|
4379
|
+
}
|
|
4380
|
+
// ─── Scrollbars ────────────────────────────────────────
|
|
4381
|
+
updateScrollbars() {
|
|
4382
|
+
const { width, height, scrollbarWidth, scrollbarColor, scrollbarAlpha } = this._config;
|
|
4383
|
+
if (this._scrollbarV && this.canScrollY() && this.contentHeight > height) {
|
|
4384
|
+
const ratio = height / this.contentHeight;
|
|
4385
|
+
const barH = Math.max(20, height * ratio);
|
|
4386
|
+
const barY = (this._scrollY / this.maxScrollY) * (height - barH);
|
|
4387
|
+
this._scrollbarV.clear();
|
|
4388
|
+
this._scrollbarV.roundRect(width - scrollbarWidth - 2, Math.max(0, barY), scrollbarWidth, barH, scrollbarWidth / 2).fill({ color: scrollbarColor, alpha: scrollbarAlpha });
|
|
4389
|
+
}
|
|
4390
|
+
if (this._scrollbarH && this.canScrollX() && this.contentWidth > width) {
|
|
4391
|
+
const ratio = width / this.contentWidth;
|
|
4392
|
+
const barW = Math.max(20, width * ratio);
|
|
4393
|
+
const barX = (this._scrollX / this.maxScrollX) * (width - barW);
|
|
4394
|
+
this._scrollbarH.clear();
|
|
4395
|
+
this._scrollbarH.roundRect(Math.max(0, barX), height - scrollbarWidth - 2, barW, scrollbarWidth, scrollbarWidth / 2).fill({ color: scrollbarColor, alpha: scrollbarAlpha });
|
|
4396
|
+
}
|
|
4397
|
+
}
|
|
4398
|
+
showScrollbars() {
|
|
4399
|
+
if (this._scrollbarV)
|
|
4400
|
+
this._scrollbarV.alpha = 1;
|
|
4401
|
+
if (this._scrollbarH)
|
|
4402
|
+
this._scrollbarH.alpha = 1;
|
|
4403
|
+
}
|
|
4404
|
+
scheduleScrollbarFade() {
|
|
4405
|
+
if (this._scrollbarFadeTimeout !== null) {
|
|
4406
|
+
clearTimeout(this._scrollbarFadeTimeout);
|
|
4407
|
+
}
|
|
4408
|
+
this._scrollbarFadeTimeout = window.setTimeout(() => {
|
|
4409
|
+
this.fadeScrollbars();
|
|
4410
|
+
}, 1000);
|
|
4411
|
+
}
|
|
4412
|
+
fadeScrollbars() {
|
|
4413
|
+
const duration = 300;
|
|
4414
|
+
const startTime = Date.now();
|
|
4415
|
+
const startAlphaV = this._scrollbarV?.alpha ?? 0;
|
|
4416
|
+
const startAlphaH = this._scrollbarH?.alpha ?? 0;
|
|
4417
|
+
const tick = () => {
|
|
4418
|
+
const t = Math.min((Date.now() - startTime) / duration, 1);
|
|
4419
|
+
if (this._scrollbarV)
|
|
4420
|
+
this._scrollbarV.alpha = startAlphaV * (1 - t);
|
|
4421
|
+
if (this._scrollbarH)
|
|
4422
|
+
this._scrollbarH.alpha = startAlphaH * (1 - t);
|
|
4423
|
+
if (t < 1)
|
|
4424
|
+
requestAnimationFrame(tick);
|
|
4425
|
+
};
|
|
4426
|
+
requestAnimationFrame(tick);
|
|
4427
|
+
}
|
|
4428
|
+
}
|
|
4429
|
+
|
|
4430
|
+
const DEFAULT_CONFIG = {
|
|
4431
|
+
balance: 10000,
|
|
4432
|
+
currency: 'USD',
|
|
4433
|
+
gameConfig: {
|
|
4434
|
+
id: 'dev-game',
|
|
4435
|
+
type: 'slot',
|
|
4436
|
+
version: '1.0.0',
|
|
4437
|
+
viewport: { width: 1920, height: 1080 },
|
|
4438
|
+
betLevels: [0.1, 0.2, 0.5, 1, 2, 5, 10, 20, 50],
|
|
3320
4439
|
},
|
|
3321
4440
|
assetsUrl: '/assets/',
|
|
3322
4441
|
session: null,
|
|
@@ -3327,8 +4446,9 @@ const DEFAULT_CONFIG = {
|
|
|
3327
4446
|
/**
|
|
3328
4447
|
* Mock host bridge for local development.
|
|
3329
4448
|
*
|
|
3330
|
-
*
|
|
3331
|
-
*
|
|
4449
|
+
* Uses the SDK's `Bridge` class in `devMode` to communicate with
|
|
4450
|
+
* `CasinoGameSDK` via a shared in-memory `MemoryChannel`, removing
|
|
4451
|
+
* the need for postMessage and iframes.
|
|
3332
4452
|
*
|
|
3333
4453
|
* This allows games to be developed and tested without a real backend.
|
|
3334
4454
|
*
|
|
@@ -3356,8 +4476,7 @@ class DevBridge {
|
|
|
3356
4476
|
_config;
|
|
3357
4477
|
_balance;
|
|
3358
4478
|
_roundCounter = 0;
|
|
3359
|
-
|
|
3360
|
-
_handler = null;
|
|
4479
|
+
_bridge = null;
|
|
3361
4480
|
constructor(config = {}) {
|
|
3362
4481
|
this._config = { ...DEFAULT_CONFIG, ...config };
|
|
3363
4482
|
this._balance = this._config.balance;
|
|
@@ -3368,24 +4487,38 @@ class DevBridge {
|
|
|
3368
4487
|
}
|
|
3369
4488
|
/** Start listening for SDK messages */
|
|
3370
4489
|
start() {
|
|
3371
|
-
if (this.
|
|
4490
|
+
if (this._bridge)
|
|
3372
4491
|
return;
|
|
3373
|
-
|
|
3374
|
-
|
|
3375
|
-
|
|
3376
|
-
|
|
3377
|
-
|
|
4492
|
+
console.debug('[DevBridge] Starting with config:', this._config);
|
|
4493
|
+
this._bridge = new Bridge({ devMode: true, debug: this._config.debug });
|
|
4494
|
+
this._bridge.on('GAME_READY', (_payload, id) => {
|
|
4495
|
+
this.handleGameReady(id);
|
|
4496
|
+
});
|
|
4497
|
+
this._bridge.on('PLAY_REQUEST', (payload, id) => {
|
|
4498
|
+
this.handlePlayRequest(payload, id);
|
|
4499
|
+
});
|
|
4500
|
+
this._bridge.on('PLAY_RESULT_ACK', (payload) => {
|
|
4501
|
+
this.handlePlayAck(payload);
|
|
4502
|
+
});
|
|
4503
|
+
this._bridge.on('GET_BALANCE', (_payload, id) => {
|
|
4504
|
+
this.handleGetBalance(id);
|
|
4505
|
+
});
|
|
4506
|
+
this._bridge.on('GET_STATE', (_payload, id) => {
|
|
4507
|
+
this.handleGetState(id);
|
|
4508
|
+
});
|
|
4509
|
+
this._bridge.on('OPEN_DEPOSIT', () => {
|
|
4510
|
+
this.handleOpenDeposit();
|
|
4511
|
+
});
|
|
3378
4512
|
if (this._config.debug) {
|
|
3379
|
-
console.log('[DevBridge] Started — listening
|
|
4513
|
+
console.log('[DevBridge] Started — listening via Bridge (devMode)');
|
|
3380
4514
|
}
|
|
3381
4515
|
}
|
|
3382
4516
|
/** Stop listening */
|
|
3383
4517
|
stop() {
|
|
3384
|
-
if (this.
|
|
3385
|
-
|
|
3386
|
-
this.
|
|
4518
|
+
if (this._bridge) {
|
|
4519
|
+
this._bridge.destroy();
|
|
4520
|
+
this._bridge = null;
|
|
3387
4521
|
}
|
|
3388
|
-
this._listening = false;
|
|
3389
4522
|
if (this._config.debug) {
|
|
3390
4523
|
console.log('[DevBridge] Stopped');
|
|
3391
4524
|
}
|
|
@@ -3393,47 +4526,13 @@ class DevBridge {
|
|
|
3393
4526
|
/** Set mock balance */
|
|
3394
4527
|
setBalance(balance) {
|
|
3395
4528
|
this._balance = balance;
|
|
3396
|
-
|
|
3397
|
-
this.sendMessage('BALANCE_UPDATE', { balance: this._balance });
|
|
4529
|
+
this._bridge?.send('BALANCE_UPDATE', { balance: this._balance });
|
|
3398
4530
|
}
|
|
3399
4531
|
/** Destroy the dev bridge */
|
|
3400
4532
|
destroy() {
|
|
3401
4533
|
this.stop();
|
|
3402
4534
|
}
|
|
3403
4535
|
// ─── Message Handling ──────────────────────────────────
|
|
3404
|
-
handleMessage(e) {
|
|
3405
|
-
const data = e.data;
|
|
3406
|
-
// Only process bridge messages
|
|
3407
|
-
if (!data || data.__casino_bridge !== true)
|
|
3408
|
-
return;
|
|
3409
|
-
if (this._config.debug) {
|
|
3410
|
-
console.log('[DevBridge] ←', data.type, data.payload);
|
|
3411
|
-
}
|
|
3412
|
-
switch (data.type) {
|
|
3413
|
-
case 'GAME_READY':
|
|
3414
|
-
this.handleGameReady(data.id);
|
|
3415
|
-
break;
|
|
3416
|
-
case 'PLAY_REQUEST':
|
|
3417
|
-
this.handlePlayRequest(data.payload, data.id);
|
|
3418
|
-
break;
|
|
3419
|
-
case 'PLAY_RESULT_ACK':
|
|
3420
|
-
this.handlePlayAck(data.payload, data.id);
|
|
3421
|
-
break;
|
|
3422
|
-
case 'GET_BALANCE':
|
|
3423
|
-
this.handleGetBalance(data.id);
|
|
3424
|
-
break;
|
|
3425
|
-
case 'GET_STATE':
|
|
3426
|
-
this.handleGetState(data.id);
|
|
3427
|
-
break;
|
|
3428
|
-
case 'OPEN_DEPOSIT':
|
|
3429
|
-
this.handleOpenDeposit();
|
|
3430
|
-
break;
|
|
3431
|
-
default:
|
|
3432
|
-
if (this._config.debug) {
|
|
3433
|
-
console.log('[DevBridge] Unknown message type:', data.type);
|
|
3434
|
-
}
|
|
3435
|
-
}
|
|
3436
|
-
}
|
|
3437
4536
|
handleGameReady(id) {
|
|
3438
4537
|
const initData = {
|
|
3439
4538
|
balance: this._balance,
|
|
@@ -3466,13 +4565,13 @@ class DevBridge {
|
|
|
3466
4565
|
};
|
|
3467
4566
|
this.delayedSend('PLAY_RESULT', result, id);
|
|
3468
4567
|
}
|
|
3469
|
-
handlePlayAck(_payload
|
|
4568
|
+
handlePlayAck(_payload) {
|
|
3470
4569
|
if (this._config.debug) {
|
|
3471
4570
|
console.log('[DevBridge] Play acknowledged');
|
|
3472
4571
|
}
|
|
3473
4572
|
}
|
|
3474
4573
|
handleGetBalance(id) {
|
|
3475
|
-
this.delayedSend('
|
|
4574
|
+
this.delayedSend('BALANCE_UPDATE', { balance: this._balance }, id);
|
|
3476
4575
|
}
|
|
3477
4576
|
handleGetState(id) {
|
|
3478
4577
|
this.delayedSend('STATE_RESPONSE', this._config.session, id);
|
|
@@ -3482,122 +4581,19 @@ class DevBridge {
|
|
|
3482
4581
|
console.log('[DevBridge] 💰 Open deposit requested (mock: adding 1000)');
|
|
3483
4582
|
}
|
|
3484
4583
|
this._balance += 1000;
|
|
3485
|
-
this.
|
|
4584
|
+
this._bridge?.send('BALANCE_UPDATE', { balance: this._balance });
|
|
3486
4585
|
}
|
|
3487
4586
|
// ─── Communication ─────────────────────────────────────
|
|
3488
4587
|
delayedSend(type, payload, id) {
|
|
3489
4588
|
const delay = this._config.networkDelay;
|
|
3490
4589
|
if (delay > 0) {
|
|
3491
|
-
setTimeout(() => this.
|
|
4590
|
+
setTimeout(() => this._bridge?.send(type, payload, id), delay);
|
|
3492
4591
|
}
|
|
3493
4592
|
else {
|
|
3494
|
-
this.
|
|
3495
|
-
}
|
|
3496
|
-
}
|
|
3497
|
-
sendMessage(type, payload, id) {
|
|
3498
|
-
const message = {
|
|
3499
|
-
__casino_bridge: true,
|
|
3500
|
-
type,
|
|
3501
|
-
payload,
|
|
3502
|
-
id,
|
|
3503
|
-
};
|
|
3504
|
-
if (this._config.debug) {
|
|
3505
|
-
console.log('[DevBridge] →', type, payload);
|
|
4593
|
+
this._bridge?.send(type, payload, id);
|
|
3506
4594
|
}
|
|
3507
|
-
// Post to the same window (SDK listens on window)
|
|
3508
|
-
window.postMessage(message, '*');
|
|
3509
|
-
}
|
|
3510
|
-
}
|
|
3511
|
-
|
|
3512
|
-
/**
|
|
3513
|
-
* FPS overlay for debugging performance.
|
|
3514
|
-
*
|
|
3515
|
-
* Shows FPS, frame time, and draw call count in the corner of the screen.
|
|
3516
|
-
*
|
|
3517
|
-
* @example
|
|
3518
|
-
* ```ts
|
|
3519
|
-
* const fps = new FPSOverlay(app);
|
|
3520
|
-
* fps.show();
|
|
3521
|
-
* ```
|
|
3522
|
-
*/
|
|
3523
|
-
class FPSOverlay {
|
|
3524
|
-
_app;
|
|
3525
|
-
_container;
|
|
3526
|
-
_fpsText;
|
|
3527
|
-
_visible = false;
|
|
3528
|
-
_samples = [];
|
|
3529
|
-
_maxSamples = 60;
|
|
3530
|
-
_lastUpdate = 0;
|
|
3531
|
-
_tickFn = null;
|
|
3532
|
-
constructor(app) {
|
|
3533
|
-
this._app = app;
|
|
3534
|
-
this._container = new Container();
|
|
3535
|
-
this._container.label = 'FPSOverlay';
|
|
3536
|
-
this._container.zIndex = 99999;
|
|
3537
|
-
this._fpsText = new Text({
|
|
3538
|
-
text: 'FPS: --',
|
|
3539
|
-
style: {
|
|
3540
|
-
fontFamily: 'monospace',
|
|
3541
|
-
fontSize: 14,
|
|
3542
|
-
fill: 0x00ff00,
|
|
3543
|
-
stroke: { color: 0x000000, width: 2 },
|
|
3544
|
-
},
|
|
3545
|
-
});
|
|
3546
|
-
this._fpsText.x = 8;
|
|
3547
|
-
this._fpsText.y = 8;
|
|
3548
|
-
this._container.addChild(this._fpsText);
|
|
3549
|
-
}
|
|
3550
|
-
/** Show the FPS overlay */
|
|
3551
|
-
show() {
|
|
3552
|
-
if (this._visible)
|
|
3553
|
-
return;
|
|
3554
|
-
this._visible = true;
|
|
3555
|
-
this._app.stage.addChild(this._container);
|
|
3556
|
-
this._tickFn = (ticker) => {
|
|
3557
|
-
this._samples.push(ticker.FPS);
|
|
3558
|
-
if (this._samples.length > this._maxSamples) {
|
|
3559
|
-
this._samples.shift();
|
|
3560
|
-
}
|
|
3561
|
-
// Update display every ~500ms
|
|
3562
|
-
const now = Date.now();
|
|
3563
|
-
if (now - this._lastUpdate > 500) {
|
|
3564
|
-
const avg = this._samples.reduce((a, b) => a + b, 0) / this._samples.length;
|
|
3565
|
-
const min = Math.min(...this._samples);
|
|
3566
|
-
this._fpsText.text = [
|
|
3567
|
-
`FPS: ${Math.round(avg)} (min: ${Math.round(min)})`,
|
|
3568
|
-
`Frame: ${ticker.deltaMS.toFixed(1)}ms`,
|
|
3569
|
-
].join('\n');
|
|
3570
|
-
this._lastUpdate = now;
|
|
3571
|
-
}
|
|
3572
|
-
};
|
|
3573
|
-
this._app.ticker.add(this._tickFn);
|
|
3574
|
-
}
|
|
3575
|
-
/** Hide the FPS overlay */
|
|
3576
|
-
hide() {
|
|
3577
|
-
if (!this._visible)
|
|
3578
|
-
return;
|
|
3579
|
-
this._visible = false;
|
|
3580
|
-
this._container.removeFromParent();
|
|
3581
|
-
if (this._tickFn) {
|
|
3582
|
-
this._app.ticker.remove(this._tickFn);
|
|
3583
|
-
this._tickFn = null;
|
|
3584
|
-
}
|
|
3585
|
-
}
|
|
3586
|
-
/** Toggle visibility */
|
|
3587
|
-
toggle() {
|
|
3588
|
-
if (this._visible) {
|
|
3589
|
-
this.hide();
|
|
3590
|
-
}
|
|
3591
|
-
else {
|
|
3592
|
-
this.show();
|
|
3593
|
-
}
|
|
3594
|
-
}
|
|
3595
|
-
/** Destroy the overlay */
|
|
3596
|
-
destroy() {
|
|
3597
|
-
this.hide();
|
|
3598
|
-
this._container.destroy({ children: true });
|
|
3599
4595
|
}
|
|
3600
4596
|
}
|
|
3601
4597
|
|
|
3602
|
-
export { AssetManager, AudioManager, BalanceDisplay, Button, DevBridge, Easing, EventEmitter, FPSOverlay, GameApplication, InputManager, Label, LoadingScene, Modal, Orientation, Panel, ProgressBar, ScaleMode, Scene, SceneManager, SpineHelper, StateMachine, Timeline, Toast, TransitionType, Tween, ViewportManager, WinDisplay };
|
|
4598
|
+
export { AssetManager, AudioManager, BalanceDisplay, Button, DevBridge, Easing, EventEmitter, FPSOverlay, GameApplication, InputManager, Label, Layout, LoadingScene, Modal, Orientation, Panel, ProgressBar, ScaleMode, Scene, SceneManager, ScrollContainer, SpineHelper, SpriteAnimation, StateMachine, Timeline, Toast, TransitionType, Tween, ViewportManager, WinDisplay };
|
|
3603
4599
|
//# sourceMappingURL=index.esm.js.map
|