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