@energy8platform/game-engine 0.2.1 → 0.4.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 +400 -35
- 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 +307 -85
- package/dist/core.cjs.js.map +1 -1
- package/dist/core.d.ts +60 -1
- package/dist/core.esm.js +308 -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 +997 -475
- package/dist/index.cjs.js.map +1 -1
- package/dist/index.d.ts +356 -79
- package/dist/index.esm.js +983 -478
- package/dist/index.esm.js.map +1 -1
- package/dist/ui.cjs.js +816 -529
- package/dist/ui.cjs.js.map +1 -1
- package/dist/ui.d.ts +179 -41
- package/dist/ui.esm.js +798 -531
- package/dist/ui.esm.js.map +1 -1
- package/dist/vite.cjs.js +85 -68
- package/dist/vite.cjs.js.map +1 -1
- package/dist/vite.d.ts +17 -23
- package/dist/vite.esm.js +86 -68
- package/dist/vite.esm.js.map +1 -1
- package/package.json +19 -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 +19 -7
- package/src/core/SceneManager.ts +3 -1
- package/src/debug/DevBridge.ts +49 -80
- package/src/index.ts +22 -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 +12 -1
- package/src/ui/Button.ts +71 -130
- package/src/ui/Layout.ts +286 -0
- package/src/ui/Modal.ts +6 -5
- package/src/ui/Panel.ts +52 -55
- package/src/ui/ProgressBar.ts +52 -57
- package/src/ui/ScrollContainer.ts +126 -0
- package/src/ui/Toast.ts +19 -13
- package/src/ui/index.ts +17 -0
- package/src/viewport/ViewportManager.ts +2 -0
- package/src/vite/index.ts +103 -83
package/dist/index.esm.js
CHANGED
|
@@ -1,5 +1,10 @@
|
|
|
1
|
-
import { Ticker, Assets, Container, Application,
|
|
2
|
-
import { CasinoGameSDK } from '@energy8platform/game-sdk';
|
|
1
|
+
import { Ticker, Assets, Container, Text, Application, AnimatedSprite, Texture, Graphics, NineSliceSprite } from 'pixi.js';
|
|
2
|
+
import { CasinoGameSDK, Bridge } from '@energy8platform/game-sdk';
|
|
3
|
+
import '@pixi/layout';
|
|
4
|
+
import { FancyButton, ProgressBar as ProgressBar$1, ScrollBox } from '@pixi/ui';
|
|
5
|
+
export { ButtonContainer, FancyButton, ScrollBox } from '@pixi/ui';
|
|
6
|
+
import { LayoutContainer } from '@pixi/layout/components';
|
|
7
|
+
export { LayoutContainer } from '@pixi/layout/components';
|
|
3
8
|
|
|
4
9
|
// ─── Scale Modes ───────────────────────────────────────────
|
|
5
10
|
var ScaleMode;
|
|
@@ -30,6 +35,9 @@ var TransitionType;
|
|
|
30
35
|
/**
|
|
31
36
|
* Minimal typed event emitter.
|
|
32
37
|
* Used internally by GameApplication, SceneManager, AudioManager, etc.
|
|
38
|
+
*
|
|
39
|
+
* Supports `void` event types — events that carry no data can be emitted
|
|
40
|
+
* without arguments: `emitter.emit('eventName')`.
|
|
33
41
|
*/
|
|
34
42
|
// eslint-disable-next-line @typescript-eslint/no-empty-object-type
|
|
35
43
|
class EventEmitter {
|
|
@@ -52,7 +60,8 @@ class EventEmitter {
|
|
|
52
60
|
this.listeners.get(event)?.delete(handler);
|
|
53
61
|
return this;
|
|
54
62
|
}
|
|
55
|
-
emit(
|
|
63
|
+
emit(...args) {
|
|
64
|
+
const [event, data] = args;
|
|
56
65
|
const handlers = this.listeners.get(event);
|
|
57
66
|
if (handlers) {
|
|
58
67
|
for (const handler of handlers) {
|
|
@@ -225,9 +234,20 @@ class Tween {
|
|
|
225
234
|
}
|
|
226
235
|
/**
|
|
227
236
|
* Wait for a given duration (useful in timelines).
|
|
237
|
+
* Uses PixiJS Ticker for consistent timing with other tweens.
|
|
228
238
|
*/
|
|
229
239
|
static delay(ms) {
|
|
230
|
-
return new Promise((resolve) =>
|
|
240
|
+
return new Promise((resolve) => {
|
|
241
|
+
let elapsed = 0;
|
|
242
|
+
const onTick = (ticker) => {
|
|
243
|
+
elapsed += ticker.deltaMS;
|
|
244
|
+
if (elapsed >= ms) {
|
|
245
|
+
Ticker.shared.remove(onTick);
|
|
246
|
+
resolve();
|
|
247
|
+
}
|
|
248
|
+
};
|
|
249
|
+
Ticker.shared.add(onTick);
|
|
250
|
+
});
|
|
231
251
|
}
|
|
232
252
|
/**
|
|
233
253
|
* Kill all tweens on a target.
|
|
@@ -254,6 +274,20 @@ class Tween {
|
|
|
254
274
|
static get activeTweens() {
|
|
255
275
|
return Tween._tweens.length;
|
|
256
276
|
}
|
|
277
|
+
/**
|
|
278
|
+
* Reset the tween system — kill all tweens and remove the ticker.
|
|
279
|
+
* Useful for cleanup between game instances, tests, or hot-reload.
|
|
280
|
+
*/
|
|
281
|
+
static reset() {
|
|
282
|
+
for (const tw of Tween._tweens) {
|
|
283
|
+
tw.resolve();
|
|
284
|
+
}
|
|
285
|
+
Tween._tweens.length = 0;
|
|
286
|
+
if (Tween._tickerAdded) {
|
|
287
|
+
Ticker.shared.remove(Tween.tick);
|
|
288
|
+
Tween._tickerAdded = false;
|
|
289
|
+
}
|
|
290
|
+
}
|
|
257
291
|
// ─── Internal ──────────────────────────────────────────
|
|
258
292
|
static ensureTicker() {
|
|
259
293
|
if (Tween._tickerAdded)
|
|
@@ -456,8 +490,9 @@ class SceneManager extends EventEmitter {
|
|
|
456
490
|
}
|
|
457
491
|
// Transition in
|
|
458
492
|
await this.transitionIn(scene.container, transition);
|
|
459
|
-
|
|
493
|
+
// Push to stack BEFORE onEnter so currentKey is correct during initialization
|
|
460
494
|
this.stack.push({ scene, key });
|
|
495
|
+
await scene.onEnter?.(data);
|
|
461
496
|
this._transitioning = false;
|
|
462
497
|
}
|
|
463
498
|
async popInternal(showTransition, transition) {
|
|
@@ -756,26 +791,51 @@ class AudioManager {
|
|
|
756
791
|
if (!this._initialized || !this._soundModule)
|
|
757
792
|
return;
|
|
758
793
|
const { sound } = this._soundModule;
|
|
759
|
-
// Stop current music
|
|
760
|
-
if (this._currentMusic) {
|
|
794
|
+
// Stop current music with fade-out, start new music with fade-in
|
|
795
|
+
if (this._currentMusic && fadeDuration > 0) {
|
|
796
|
+
const prevAlias = this._currentMusic;
|
|
797
|
+
this._currentMusic = alias;
|
|
798
|
+
if (this._globalMuted || this._categories.music.muted)
|
|
799
|
+
return;
|
|
800
|
+
// Fade out the previous track
|
|
801
|
+
this.fadeVolume(prevAlias, this._categories.music.volume, 0, fadeDuration, () => {
|
|
802
|
+
try {
|
|
803
|
+
sound.stop(prevAlias);
|
|
804
|
+
}
|
|
805
|
+
catch { /* ignore */ }
|
|
806
|
+
});
|
|
807
|
+
// Start new track at zero volume, fade in
|
|
761
808
|
try {
|
|
762
|
-
sound.
|
|
809
|
+
sound.play(alias, {
|
|
810
|
+
volume: 0,
|
|
811
|
+
loop: true,
|
|
812
|
+
});
|
|
813
|
+
this.fadeVolume(alias, 0, this._categories.music.volume, fadeDuration);
|
|
763
814
|
}
|
|
764
|
-
catch {
|
|
765
|
-
|
|
815
|
+
catch (e) {
|
|
816
|
+
console.warn(`[AudioManager] Failed to play music "${alias}":`, e);
|
|
766
817
|
}
|
|
767
818
|
}
|
|
768
|
-
|
|
769
|
-
|
|
770
|
-
|
|
771
|
-
|
|
772
|
-
|
|
773
|
-
|
|
774
|
-
|
|
775
|
-
}
|
|
776
|
-
|
|
777
|
-
|
|
778
|
-
|
|
819
|
+
else {
|
|
820
|
+
// No crossfade — instant switch
|
|
821
|
+
if (this._currentMusic) {
|
|
822
|
+
try {
|
|
823
|
+
sound.stop(this._currentMusic);
|
|
824
|
+
}
|
|
825
|
+
catch { /* ignore */ }
|
|
826
|
+
}
|
|
827
|
+
this._currentMusic = alias;
|
|
828
|
+
if (this._globalMuted || this._categories.music.muted)
|
|
829
|
+
return;
|
|
830
|
+
try {
|
|
831
|
+
sound.play(alias, {
|
|
832
|
+
volume: this._categories.music.volume,
|
|
833
|
+
loop: true,
|
|
834
|
+
});
|
|
835
|
+
}
|
|
836
|
+
catch (e) {
|
|
837
|
+
console.warn(`[AudioManager] Failed to play music "${alias}":`, e);
|
|
838
|
+
}
|
|
779
839
|
}
|
|
780
840
|
}
|
|
781
841
|
/**
|
|
@@ -917,6 +977,31 @@ class AudioManager {
|
|
|
917
977
|
this._initialized = false;
|
|
918
978
|
}
|
|
919
979
|
// ─── Private ───────────────────────────────────────────
|
|
980
|
+
/**
|
|
981
|
+
* Smoothly fade a sound's volume from `fromVol` to `toVol` over `durationMs`.
|
|
982
|
+
*/
|
|
983
|
+
fadeVolume(alias, fromVol, toVol, durationMs, onComplete) {
|
|
984
|
+
if (!this._soundModule)
|
|
985
|
+
return;
|
|
986
|
+
const { sound } = this._soundModule;
|
|
987
|
+
const startTime = Date.now();
|
|
988
|
+
const tick = () => {
|
|
989
|
+
const elapsed = Date.now() - startTime;
|
|
990
|
+
const t = Math.min(elapsed / durationMs, 1);
|
|
991
|
+
const vol = fromVol + (toVol - fromVol) * t;
|
|
992
|
+
try {
|
|
993
|
+
sound.volume(alias, vol);
|
|
994
|
+
}
|
|
995
|
+
catch { /* ignore */ }
|
|
996
|
+
if (t < 1) {
|
|
997
|
+
requestAnimationFrame(tick);
|
|
998
|
+
}
|
|
999
|
+
else {
|
|
1000
|
+
onComplete?.();
|
|
1001
|
+
}
|
|
1002
|
+
};
|
|
1003
|
+
requestAnimationFrame(tick);
|
|
1004
|
+
}
|
|
920
1005
|
applyVolumes() {
|
|
921
1006
|
if (!this._soundModule)
|
|
922
1007
|
return;
|
|
@@ -1021,6 +1106,10 @@ class InputManager extends EventEmitter {
|
|
|
1021
1106
|
_locked = false;
|
|
1022
1107
|
_keysDown = new Set();
|
|
1023
1108
|
_destroyed = false;
|
|
1109
|
+
// Viewport transform (set by ViewportManager via setViewportTransform)
|
|
1110
|
+
_viewportScale = 1;
|
|
1111
|
+
_viewportOffsetX = 0;
|
|
1112
|
+
_viewportOffsetY = 0;
|
|
1024
1113
|
// Gesture tracking
|
|
1025
1114
|
_pointerStart = null;
|
|
1026
1115
|
_swipeThreshold = 50; // minimum distance in px
|
|
@@ -1047,6 +1136,25 @@ class InputManager extends EventEmitter {
|
|
|
1047
1136
|
isKeyDown(key) {
|
|
1048
1137
|
return this._keysDown.has(key.toLowerCase());
|
|
1049
1138
|
}
|
|
1139
|
+
/**
|
|
1140
|
+
* Update the viewport transform used for DOM→world coordinate mapping.
|
|
1141
|
+
* Called automatically by GameApplication when ViewportManager emits resize.
|
|
1142
|
+
*/
|
|
1143
|
+
setViewportTransform(scale, offsetX, offsetY) {
|
|
1144
|
+
this._viewportScale = scale;
|
|
1145
|
+
this._viewportOffsetX = offsetX;
|
|
1146
|
+
this._viewportOffsetY = offsetY;
|
|
1147
|
+
}
|
|
1148
|
+
/**
|
|
1149
|
+
* Convert a DOM canvas position to game-world coordinates,
|
|
1150
|
+
* accounting for viewport scaling and offset.
|
|
1151
|
+
*/
|
|
1152
|
+
getWorldPosition(canvasX, canvasY) {
|
|
1153
|
+
return {
|
|
1154
|
+
x: (canvasX - this._viewportOffsetX) / this._viewportScale,
|
|
1155
|
+
y: (canvasY - this._viewportOffsetY) / this._viewportScale,
|
|
1156
|
+
};
|
|
1157
|
+
}
|
|
1050
1158
|
/** Destroy the input manager */
|
|
1051
1159
|
destroy() {
|
|
1052
1160
|
this._destroyed = true;
|
|
@@ -1302,6 +1410,8 @@ class ViewportManager extends EventEmitter {
|
|
|
1302
1410
|
this._destroyed = true;
|
|
1303
1411
|
this._resizeObserver?.disconnect();
|
|
1304
1412
|
this._resizeObserver = null;
|
|
1413
|
+
// Remove fallback window resize listener if it was used
|
|
1414
|
+
window.removeEventListener('resize', this.onWindowResize);
|
|
1305
1415
|
if (this._resizeTimeout !== null) {
|
|
1306
1416
|
clearTimeout(this._resizeTimeout);
|
|
1307
1417
|
}
|
|
@@ -1365,45 +1475,87 @@ class Scene {
|
|
|
1365
1475
|
}
|
|
1366
1476
|
|
|
1367
1477
|
/**
|
|
1368
|
-
*
|
|
1369
|
-
*
|
|
1478
|
+
* Shared Energy8 SVG logo with an embedded loader bar.
|
|
1479
|
+
*
|
|
1480
|
+
* The loader bar fill is controlled via a `<clipPath>` whose `<rect>` width
|
|
1481
|
+
* is animatable. Different consumers customise gradient IDs and the clip
|
|
1482
|
+
* element's ID/class to avoid collisions when both CSSPreloader and
|
|
1483
|
+
* LoadingScene appear in the same DOM.
|
|
1370
1484
|
*/
|
|
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">
|
|
1485
|
+
/** SVG path data for the Energy8 wordmark — reused across loaders */
|
|
1486
|
+
const WORDMARK_PATHS = `
|
|
1487
|
+
<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)"/>
|
|
1488
|
+
<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)"/>
|
|
1489
|
+
<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)"/>
|
|
1490
|
+
<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)"/>
|
|
1491
|
+
<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)"/>`;
|
|
1492
|
+
/** Gradient definitions template (gradient IDs are replaced per-consumer) */
|
|
1493
|
+
const GRADIENT_DEFS = `
|
|
1494
|
+
<linearGradient id="GID0" x1="223.7" x2="223.7" y1="81.75" y2="127.8" gradientUnits="userSpaceOnUse">
|
|
1385
1495
|
<stop stop-color="#663BA6"/><stop stop-color="#7939C2" offset=".349"/><stop stop-color="#8A2FC0" offset=".6615"/><stop stop-color="#791BA3" offset="1"/>
|
|
1386
1496
|
</linearGradient>
|
|
1387
|
-
<linearGradient id="
|
|
1497
|
+
<linearGradient id="GID1" x1="194.6" x2="194.6" y1="81.75" y2="138.3" gradientUnits="userSpaceOnUse">
|
|
1388
1498
|
<stop stop-color="#663BA6"/><stop stop-color="#7939C2" offset=".349"/><stop stop-color="#8A2FC0" offset=".6615"/><stop stop-color="#791BA3" offset="1"/>
|
|
1389
1499
|
</linearGradient>
|
|
1390
|
-
<linearGradient id="
|
|
1500
|
+
<linearGradient id="GID2" x1="157.8" x2="157.8" y1="81.75" y2="127" gradientUnits="userSpaceOnUse">
|
|
1391
1501
|
<stop stop-color="#663BA6"/><stop stop-color="#7939C2" offset=".349"/><stop stop-color="#8A2FC0" offset=".6615"/><stop stop-color="#791BA3" offset="1"/>
|
|
1392
1502
|
</linearGradient>
|
|
1393
|
-
<linearGradient id="
|
|
1503
|
+
<linearGradient id="GID3" x1="79.96" x2="79.96" y1="81.75" y2="141.8" gradientUnits="userSpaceOnUse">
|
|
1394
1504
|
<stop stop-color="#663BA6"/><stop stop-color="#7939C2" offset=".349"/><stop stop-color="#8A2FC0" offset=".6615"/><stop stop-color="#791BA3" offset="1"/>
|
|
1395
1505
|
</linearGradient>
|
|
1396
|
-
<linearGradient id="
|
|
1506
|
+
<linearGradient id="GID4" x1="36.18" x2="212.5" y1="156.6" y2="156.6" gradientUnits="userSpaceOnUse">
|
|
1397
1507
|
<stop stop-color="#316FB0"/><stop stop-color="#1FCDE6" offset=".5"/><stop stop-color="#29FEE7" offset="1"/>
|
|
1398
1508
|
</linearGradient>
|
|
1399
|
-
<linearGradient id="
|
|
1509
|
+
<linearGradient id="GID5" x1="40.27" x2="208.2" y1="156.4" y2="156.4" gradientUnits="userSpaceOnUse">
|
|
1400
1510
|
<stop stop-color="#316FB0"/><stop stop-color="#1FCDE6" offset=".5"/><stop stop-color="#29FEE7" offset="1"/>
|
|
1401
|
-
</linearGradient
|
|
1511
|
+
</linearGradient>`;
|
|
1512
|
+
/** Max width of the loader bar in SVG units */
|
|
1513
|
+
const LOADER_BAR_MAX_WIDTH = 174;
|
|
1514
|
+
/**
|
|
1515
|
+
* Build the Energy8 SVG logo with a loader bar, using unique IDs.
|
|
1516
|
+
*
|
|
1517
|
+
* @param opts - Configuration to avoid element ID collisions
|
|
1518
|
+
* @returns SVG markup string
|
|
1519
|
+
*/
|
|
1520
|
+
function buildLogoSVG(opts) {
|
|
1521
|
+
const { idPrefix, svgClass, svgStyle, clipRectClass, clipRectId, textId, textContent, textClass } = opts;
|
|
1522
|
+
// Replace gradient ID placeholders with prefixed versions
|
|
1523
|
+
const paths = WORDMARK_PATHS.replace(/GID(\d)/g, `${idPrefix}$1`);
|
|
1524
|
+
const defs = GRADIENT_DEFS.replace(/GID(\d)/g, `${idPrefix}$1`);
|
|
1525
|
+
const clipId = `${idPrefix}-loader-clip`;
|
|
1526
|
+
const fillGradientId = `${idPrefix}5`;
|
|
1527
|
+
const classAttr = svgClass ? ` class="${svgClass}"` : '';
|
|
1528
|
+
const styleAttr = svgStyle ? ` style="${svgStyle}"` : '';
|
|
1529
|
+
const rectClassAttr = clipRectClass ? ` class="${clipRectClass}"` : '';
|
|
1530
|
+
const rectIdAttr = clipRectId ? ` id="${clipRectId}"` : '';
|
|
1531
|
+
const txtIdAttr = textId ? ` id="${textId}"` : '';
|
|
1532
|
+
const txtClassAttr = textClass ? ` class="${textClass}"` : '';
|
|
1533
|
+
return `<svg xmlns="http://www.w3.org/2000/svg" viewBox="0 0 250 200" fill="none"${classAttr}${styleAttr}>
|
|
1534
|
+
${paths}
|
|
1535
|
+
<clipPath id="${clipId}">
|
|
1536
|
+
<rect${rectIdAttr} x="37" y="148" width="0" height="20"${rectClassAttr}/>
|
|
1537
|
+
</clipPath>
|
|
1538
|
+
<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})"/>
|
|
1539
|
+
<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>
|
|
1540
|
+
<defs>
|
|
1541
|
+
${defs}
|
|
1402
1542
|
</defs>
|
|
1403
1543
|
</svg>`;
|
|
1404
1544
|
}
|
|
1405
|
-
|
|
1406
|
-
|
|
1545
|
+
|
|
1546
|
+
/**
|
|
1547
|
+
* Build the loading scene variant of the logo SVG.
|
|
1548
|
+
* Uses unique IDs (prefixed with 'ls') to avoid collisions with CSSPreloader.
|
|
1549
|
+
*/
|
|
1550
|
+
function buildLoadingLogoSVG() {
|
|
1551
|
+
return buildLogoSVG({
|
|
1552
|
+
idPrefix: 'ls',
|
|
1553
|
+
svgStyle: 'width:100%;height:auto;',
|
|
1554
|
+
clipRectId: 'ge-loader-rect',
|
|
1555
|
+
textId: 'ge-loader-pct',
|
|
1556
|
+
textContent: '0%',
|
|
1557
|
+
});
|
|
1558
|
+
}
|
|
1407
1559
|
/**
|
|
1408
1560
|
* Built-in loading screen using the Energy8 SVG logo with animated loader bar.
|
|
1409
1561
|
*
|
|
@@ -1513,7 +1665,7 @@ class LoadingScene extends Scene {
|
|
|
1513
1665
|
this._overlay.id = '__ge-loading-overlay__';
|
|
1514
1666
|
this._overlay.innerHTML = `
|
|
1515
1667
|
<div class="ge-loading-content">
|
|
1516
|
-
${
|
|
1668
|
+
${buildLoadingLogoSVG()}
|
|
1517
1669
|
</div>
|
|
1518
1670
|
`;
|
|
1519
1671
|
const style = document.createElement('style');
|
|
@@ -1651,8 +1803,11 @@ class LoadingScene extends Scene {
|
|
|
1651
1803
|
}
|
|
1652
1804
|
// Remove overlay
|
|
1653
1805
|
this.removeOverlay();
|
|
1654
|
-
// Navigate to the target scene
|
|
1655
|
-
await this._engine.scenes.goto(this._targetScene,
|
|
1806
|
+
// Navigate to the target scene, always passing the engine reference
|
|
1807
|
+
await this._engine.scenes.goto(this._targetScene, {
|
|
1808
|
+
engine: this._engine,
|
|
1809
|
+
...(this._targetData && typeof this._targetData === 'object' ? this._targetData : { data: this._targetData }),
|
|
1810
|
+
});
|
|
1656
1811
|
}
|
|
1657
1812
|
}
|
|
1658
1813
|
|
|
@@ -1661,39 +1816,12 @@ const PRELOADER_ID = '__ge-css-preloader__';
|
|
|
1661
1816
|
* Inline SVG logo with animated loader bar.
|
|
1662
1817
|
* The `#loader` path acts as the progress fill — animated via clipPath.
|
|
1663
1818
|
*/
|
|
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>`;
|
|
1819
|
+
const LOGO_SVG = buildLogoSVG({
|
|
1820
|
+
idPrefix: 'pl',
|
|
1821
|
+
svgClass: 'ge-logo-svg',
|
|
1822
|
+
clipRectClass: 'ge-clip-rect',
|
|
1823
|
+
textClass: 'ge-preloader-svg-text',
|
|
1824
|
+
});
|
|
1697
1825
|
/**
|
|
1698
1826
|
* Creates a lightweight CSS-only preloader that appears instantly,
|
|
1699
1827
|
* BEFORE PixiJS/WebGL is initialized.
|
|
@@ -1797,6 +1925,96 @@ function removeCSSPreloader(container) {
|
|
|
1797
1925
|
});
|
|
1798
1926
|
}
|
|
1799
1927
|
|
|
1928
|
+
/**
|
|
1929
|
+
* FPS overlay for debugging performance.
|
|
1930
|
+
*
|
|
1931
|
+
* Shows FPS, frame time, and draw call count in the corner of the screen.
|
|
1932
|
+
*
|
|
1933
|
+
* @example
|
|
1934
|
+
* ```ts
|
|
1935
|
+
* const fps = new FPSOverlay(app);
|
|
1936
|
+
* fps.show();
|
|
1937
|
+
* ```
|
|
1938
|
+
*/
|
|
1939
|
+
class FPSOverlay {
|
|
1940
|
+
_app;
|
|
1941
|
+
_container;
|
|
1942
|
+
_fpsText;
|
|
1943
|
+
_visible = false;
|
|
1944
|
+
_samples = [];
|
|
1945
|
+
_maxSamples = 60;
|
|
1946
|
+
_lastUpdate = 0;
|
|
1947
|
+
_tickFn = null;
|
|
1948
|
+
constructor(app) {
|
|
1949
|
+
this._app = app;
|
|
1950
|
+
this._container = new Container();
|
|
1951
|
+
this._container.label = 'FPSOverlay';
|
|
1952
|
+
this._container.zIndex = 99999;
|
|
1953
|
+
this._fpsText = new Text({
|
|
1954
|
+
text: 'FPS: --',
|
|
1955
|
+
style: {
|
|
1956
|
+
fontFamily: 'monospace',
|
|
1957
|
+
fontSize: 14,
|
|
1958
|
+
fill: 0x00ff00,
|
|
1959
|
+
stroke: { color: 0x000000, width: 2 },
|
|
1960
|
+
},
|
|
1961
|
+
});
|
|
1962
|
+
this._fpsText.x = 8;
|
|
1963
|
+
this._fpsText.y = 8;
|
|
1964
|
+
this._container.addChild(this._fpsText);
|
|
1965
|
+
}
|
|
1966
|
+
/** Show the FPS overlay */
|
|
1967
|
+
show() {
|
|
1968
|
+
if (this._visible)
|
|
1969
|
+
return;
|
|
1970
|
+
this._visible = true;
|
|
1971
|
+
this._app.stage.addChild(this._container);
|
|
1972
|
+
this._tickFn = (ticker) => {
|
|
1973
|
+
this._samples.push(ticker.FPS);
|
|
1974
|
+
if (this._samples.length > this._maxSamples) {
|
|
1975
|
+
this._samples.shift();
|
|
1976
|
+
}
|
|
1977
|
+
// Update display every ~500ms
|
|
1978
|
+
const now = Date.now();
|
|
1979
|
+
if (now - this._lastUpdate > 500) {
|
|
1980
|
+
const avg = this._samples.reduce((a, b) => a + b, 0) / this._samples.length;
|
|
1981
|
+
const min = Math.min(...this._samples);
|
|
1982
|
+
this._fpsText.text = [
|
|
1983
|
+
`FPS: ${Math.round(avg)} (min: ${Math.round(min)})`,
|
|
1984
|
+
`Frame: ${ticker.deltaMS.toFixed(1)}ms`,
|
|
1985
|
+
].join('\n');
|
|
1986
|
+
this._lastUpdate = now;
|
|
1987
|
+
}
|
|
1988
|
+
};
|
|
1989
|
+
this._app.ticker.add(this._tickFn);
|
|
1990
|
+
}
|
|
1991
|
+
/** Hide the FPS overlay */
|
|
1992
|
+
hide() {
|
|
1993
|
+
if (!this._visible)
|
|
1994
|
+
return;
|
|
1995
|
+
this._visible = false;
|
|
1996
|
+
this._container.removeFromParent();
|
|
1997
|
+
if (this._tickFn) {
|
|
1998
|
+
this._app.ticker.remove(this._tickFn);
|
|
1999
|
+
this._tickFn = null;
|
|
2000
|
+
}
|
|
2001
|
+
}
|
|
2002
|
+
/** Toggle visibility */
|
|
2003
|
+
toggle() {
|
|
2004
|
+
if (this._visible) {
|
|
2005
|
+
this.hide();
|
|
2006
|
+
}
|
|
2007
|
+
else {
|
|
2008
|
+
this.show();
|
|
2009
|
+
}
|
|
2010
|
+
}
|
|
2011
|
+
/** Destroy the overlay */
|
|
2012
|
+
destroy() {
|
|
2013
|
+
this.hide();
|
|
2014
|
+
this._container.destroy({ children: true });
|
|
2015
|
+
}
|
|
2016
|
+
}
|
|
2017
|
+
|
|
1800
2018
|
/**
|
|
1801
2019
|
* The main entry point for a game built on @energy8platform/game-engine.
|
|
1802
2020
|
*
|
|
@@ -1844,6 +2062,8 @@ class GameApplication extends EventEmitter {
|
|
|
1844
2062
|
viewport;
|
|
1845
2063
|
/** SDK instance (null in offline mode) */
|
|
1846
2064
|
sdk = null;
|
|
2065
|
+
/** FPS overlay instance (only when debug: true) */
|
|
2066
|
+
fpsOverlay = null;
|
|
1847
2067
|
/** Data received from SDK initialization */
|
|
1848
2068
|
initData = null;
|
|
1849
2069
|
/** Configuration */
|
|
@@ -1911,15 +2131,15 @@ class GameApplication extends EventEmitter {
|
|
|
1911
2131
|
this.applySDKConfig();
|
|
1912
2132
|
// 6. Initialize sub-systems
|
|
1913
2133
|
this.initSubSystems();
|
|
1914
|
-
this.emit('initialized'
|
|
2134
|
+
this.emit('initialized');
|
|
1915
2135
|
// 7. Remove CSS preloader, show Canvas loading screen
|
|
1916
2136
|
removeCSSPreloader(this._container);
|
|
1917
2137
|
// 8. Load assets with loading screen
|
|
1918
2138
|
await this.loadAssets(firstScene, sceneData);
|
|
1919
|
-
this.emit('loaded'
|
|
2139
|
+
this.emit('loaded');
|
|
1920
2140
|
// 9. Start the game loop
|
|
1921
2141
|
this._running = true;
|
|
1922
|
-
this.emit('started'
|
|
2142
|
+
this.emit('started');
|
|
1923
2143
|
}
|
|
1924
2144
|
catch (err) {
|
|
1925
2145
|
console.error('[GameEngine] Failed to start:', err);
|
|
@@ -1941,7 +2161,7 @@ class GameApplication extends EventEmitter {
|
|
|
1941
2161
|
this.viewport?.destroy();
|
|
1942
2162
|
this.sdk?.destroy();
|
|
1943
2163
|
this.app?.destroy(true, { children: true, texture: true });
|
|
1944
|
-
this.emit('destroyed'
|
|
2164
|
+
this.emit('destroyed');
|
|
1945
2165
|
this.removeAllListeners();
|
|
1946
2166
|
}
|
|
1947
2167
|
// ─── Private initialization steps ──────────────────────
|
|
@@ -1957,6 +2177,7 @@ class GameApplication extends EventEmitter {
|
|
|
1957
2177
|
async initPixi() {
|
|
1958
2178
|
this.app = new Application();
|
|
1959
2179
|
const pixiOpts = {
|
|
2180
|
+
preference: 'webgl',
|
|
1960
2181
|
background: typeof this.config.loading?.backgroundColor === 'number'
|
|
1961
2182
|
? this.config.loading.backgroundColor
|
|
1962
2183
|
: 0x000000,
|
|
@@ -2015,9 +2236,10 @@ class GameApplication extends EventEmitter {
|
|
|
2015
2236
|
});
|
|
2016
2237
|
// Wire SceneManager to the PixiJS stage
|
|
2017
2238
|
this.scenes.setRoot(this.app.stage);
|
|
2018
|
-
// Wire viewport resize → scene manager
|
|
2019
|
-
this.viewport.on('resize', ({ width, height }) => {
|
|
2239
|
+
// Wire viewport resize → scene manager + input manager
|
|
2240
|
+
this.viewport.on('resize', ({ width, height, scale }) => {
|
|
2020
2241
|
this.scenes.resize(width, height);
|
|
2242
|
+
this.input.setViewportTransform(scale, this.app.stage.x, this.app.stage.y);
|
|
2021
2243
|
this.emit('resize', { width, height });
|
|
2022
2244
|
});
|
|
2023
2245
|
this.viewport.on('orientationChange', (orientation) => {
|
|
@@ -2034,6 +2256,11 @@ class GameApplication extends EventEmitter {
|
|
|
2034
2256
|
});
|
|
2035
2257
|
// Trigger initial resize
|
|
2036
2258
|
this.viewport.refresh();
|
|
2259
|
+
// Enable FPS overlay in debug mode
|
|
2260
|
+
if (this.config.debug) {
|
|
2261
|
+
this.fpsOverlay = new FPSOverlay(this.app);
|
|
2262
|
+
this.fpsOverlay.show();
|
|
2263
|
+
}
|
|
2037
2264
|
}
|
|
2038
2265
|
async loadAssets(firstScene, sceneData) {
|
|
2039
2266
|
// Register built-in loading scene
|
|
@@ -2493,162 +2720,277 @@ class SpineHelper {
|
|
|
2493
2720
|
}
|
|
2494
2721
|
}
|
|
2495
2722
|
|
|
2723
|
+
/**
|
|
2724
|
+
* Helper for creating frame-based animations from spritesheets.
|
|
2725
|
+
*
|
|
2726
|
+
* Wraps PixiJS `AnimatedSprite` with a convenient API for
|
|
2727
|
+
* common iGaming effects: coin showers, symbol animations,
|
|
2728
|
+
* sparkle trails, win celebrations.
|
|
2729
|
+
*
|
|
2730
|
+
* Cheaper than Spine for simple frame sequences.
|
|
2731
|
+
*
|
|
2732
|
+
* @example
|
|
2733
|
+
* ```ts
|
|
2734
|
+
* // From an array of textures
|
|
2735
|
+
* const coinAnim = SpriteAnimation.create(coinTextures, {
|
|
2736
|
+
* fps: 30,
|
|
2737
|
+
* loop: true,
|
|
2738
|
+
* });
|
|
2739
|
+
* scene.addChild(coinAnim);
|
|
2740
|
+
*
|
|
2741
|
+
* // From a spritesheet with a naming pattern
|
|
2742
|
+
* const sheet = Assets.get('effects');
|
|
2743
|
+
* const sparkle = SpriteAnimation.fromSpritesheet(sheet, 'sparkle_');
|
|
2744
|
+
* sparkle.play();
|
|
2745
|
+
*
|
|
2746
|
+
* // From a numbered range
|
|
2747
|
+
* const explosion = SpriteAnimation.fromRange(sheet, 'explosion_{i}', 0, 24, {
|
|
2748
|
+
* fps: 60,
|
|
2749
|
+
* loop: false,
|
|
2750
|
+
* onComplete: () => explosion.destroy(),
|
|
2751
|
+
* });
|
|
2752
|
+
* ```
|
|
2753
|
+
*/
|
|
2754
|
+
class SpriteAnimation {
|
|
2755
|
+
/**
|
|
2756
|
+
* Create an animated sprite from an array of textures.
|
|
2757
|
+
*
|
|
2758
|
+
* @param textures - Array of PixiJS Textures
|
|
2759
|
+
* @param config - Animation options
|
|
2760
|
+
* @returns Configured AnimatedSprite
|
|
2761
|
+
*/
|
|
2762
|
+
static create(textures, config = {}) {
|
|
2763
|
+
const sprite = new AnimatedSprite(textures);
|
|
2764
|
+
// Configure
|
|
2765
|
+
sprite.animationSpeed = (config.fps ?? 24) / 60; // PixiJS uses speed relative to 60fps ticker
|
|
2766
|
+
sprite.loop = config.loop ?? true;
|
|
2767
|
+
// Anchor
|
|
2768
|
+
if (config.anchor !== undefined) {
|
|
2769
|
+
if (typeof config.anchor === 'number') {
|
|
2770
|
+
sprite.anchor.set(config.anchor);
|
|
2771
|
+
}
|
|
2772
|
+
else {
|
|
2773
|
+
sprite.anchor.set(config.anchor.x, config.anchor.y);
|
|
2774
|
+
}
|
|
2775
|
+
}
|
|
2776
|
+
else {
|
|
2777
|
+
sprite.anchor.set(0.5);
|
|
2778
|
+
}
|
|
2779
|
+
// Complete callback
|
|
2780
|
+
if (config.onComplete) {
|
|
2781
|
+
sprite.onComplete = config.onComplete;
|
|
2782
|
+
}
|
|
2783
|
+
// Auto-play
|
|
2784
|
+
if (config.autoPlay !== false) {
|
|
2785
|
+
sprite.play();
|
|
2786
|
+
}
|
|
2787
|
+
return sprite;
|
|
2788
|
+
}
|
|
2789
|
+
/**
|
|
2790
|
+
* Create an animated sprite from a spritesheet using a name prefix.
|
|
2791
|
+
*
|
|
2792
|
+
* Collects all textures whose keys start with `prefix`, sorted alphabetically.
|
|
2793
|
+
*
|
|
2794
|
+
* @param sheet - PixiJS Spritesheet instance
|
|
2795
|
+
* @param prefix - Texture name prefix (e.g., 'coin_')
|
|
2796
|
+
* @param config - Animation options
|
|
2797
|
+
* @returns Configured AnimatedSprite
|
|
2798
|
+
*/
|
|
2799
|
+
static fromSpritesheet(sheet, prefix, config = {}) {
|
|
2800
|
+
const textures = SpriteAnimation.getTexturesByPrefix(sheet, prefix);
|
|
2801
|
+
if (textures.length === 0) {
|
|
2802
|
+
console.warn(`[SpriteAnimation] No textures found with prefix "${prefix}"`);
|
|
2803
|
+
}
|
|
2804
|
+
return SpriteAnimation.create(textures, config);
|
|
2805
|
+
}
|
|
2806
|
+
/**
|
|
2807
|
+
* Create an animated sprite from a numbered range of frames.
|
|
2808
|
+
*
|
|
2809
|
+
* The `pattern` string should contain `{i}` as a placeholder for the frame number.
|
|
2810
|
+
* Numbers are zero-padded to match the length of `start`.
|
|
2811
|
+
*
|
|
2812
|
+
* @param sheet - PixiJS Spritesheet instance
|
|
2813
|
+
* @param pattern - Frame name pattern, e.g. 'explosion_{i}'
|
|
2814
|
+
* @param start - Start frame index (inclusive)
|
|
2815
|
+
* @param end - End frame index (inclusive)
|
|
2816
|
+
* @param config - Animation options
|
|
2817
|
+
* @returns Configured AnimatedSprite
|
|
2818
|
+
*/
|
|
2819
|
+
static fromRange(sheet, pattern, start, end, config = {}) {
|
|
2820
|
+
const textures = [];
|
|
2821
|
+
const padLength = String(end).length;
|
|
2822
|
+
for (let i = start; i <= end; i++) {
|
|
2823
|
+
const name = pattern.replace('{i}', String(i).padStart(padLength, '0'));
|
|
2824
|
+
const texture = sheet.textures[name];
|
|
2825
|
+
if (texture) {
|
|
2826
|
+
textures.push(texture);
|
|
2827
|
+
}
|
|
2828
|
+
else {
|
|
2829
|
+
console.warn(`[SpriteAnimation] Missing frame: "${name}"`);
|
|
2830
|
+
}
|
|
2831
|
+
}
|
|
2832
|
+
if (textures.length === 0) {
|
|
2833
|
+
console.warn(`[SpriteAnimation] No textures found for pattern "${pattern}" [${start}..${end}]`);
|
|
2834
|
+
}
|
|
2835
|
+
return SpriteAnimation.create(textures, config);
|
|
2836
|
+
}
|
|
2837
|
+
/**
|
|
2838
|
+
* Create an AnimatedSprite from texture aliases (loaded via AssetManager).
|
|
2839
|
+
*
|
|
2840
|
+
* @param aliases - Array of texture aliases
|
|
2841
|
+
* @param config - Animation options
|
|
2842
|
+
* @returns Configured AnimatedSprite
|
|
2843
|
+
*/
|
|
2844
|
+
static fromAliases(aliases, config = {}) {
|
|
2845
|
+
const textures = aliases.map((alias) => {
|
|
2846
|
+
const tex = Texture.from(alias);
|
|
2847
|
+
return tex;
|
|
2848
|
+
});
|
|
2849
|
+
return SpriteAnimation.create(textures, config);
|
|
2850
|
+
}
|
|
2851
|
+
/**
|
|
2852
|
+
* Play a one-shot animation and auto-destroy when complete.
|
|
2853
|
+
* Useful for fire-and-forget effects like coin bursts.
|
|
2854
|
+
*
|
|
2855
|
+
* @param textures - Array of textures
|
|
2856
|
+
* @param config - Animation options (loop will be forced to false)
|
|
2857
|
+
* @returns Promise that resolves when animation completes
|
|
2858
|
+
*/
|
|
2859
|
+
static playOnce(textures, config = {}) {
|
|
2860
|
+
const finished = new Promise((resolve) => {
|
|
2861
|
+
config = {
|
|
2862
|
+
...config,
|
|
2863
|
+
loop: false,
|
|
2864
|
+
onComplete: () => {
|
|
2865
|
+
config.onComplete?.();
|
|
2866
|
+
sprite.destroy();
|
|
2867
|
+
resolve();
|
|
2868
|
+
},
|
|
2869
|
+
};
|
|
2870
|
+
});
|
|
2871
|
+
const sprite = SpriteAnimation.create(textures, config);
|
|
2872
|
+
return { sprite, finished };
|
|
2873
|
+
}
|
|
2874
|
+
// ─── Utility ───────────────────────────────────────────
|
|
2875
|
+
/**
|
|
2876
|
+
* Get all textures from a spritesheet that start with a given prefix.
|
|
2877
|
+
* Results are sorted alphabetically by key.
|
|
2878
|
+
*/
|
|
2879
|
+
static getTexturesByPrefix(sheet, prefix) {
|
|
2880
|
+
const keys = Object.keys(sheet.textures)
|
|
2881
|
+
.filter((k) => k.startsWith(prefix))
|
|
2882
|
+
.sort();
|
|
2883
|
+
return keys.map((k) => sheet.textures[k]);
|
|
2884
|
+
}
|
|
2885
|
+
}
|
|
2886
|
+
|
|
2496
2887
|
const DEFAULT_COLORS = {
|
|
2497
|
-
|
|
2888
|
+
default: 0xffd700,
|
|
2498
2889
|
hover: 0xffe44d,
|
|
2499
2890
|
pressed: 0xccac00,
|
|
2500
2891
|
disabled: 0x666666,
|
|
2501
2892
|
};
|
|
2893
|
+
function makeGraphicsView(w, h, radius, color) {
|
|
2894
|
+
const g = new Graphics();
|
|
2895
|
+
g.roundRect(0, 0, w, h, radius).fill(color);
|
|
2896
|
+
// Highlight overlay
|
|
2897
|
+
g.roundRect(2, 2, w - 4, h * 0.45, radius).fill({ color: 0xffffff, alpha: 0.1 });
|
|
2898
|
+
return g;
|
|
2899
|
+
}
|
|
2502
2900
|
/**
|
|
2503
|
-
* Interactive button component
|
|
2901
|
+
* Interactive button component powered by `@pixi/ui` FancyButton.
|
|
2504
2902
|
*
|
|
2505
|
-
* Supports both texture-based and Graphics-based rendering
|
|
2903
|
+
* Supports both texture-based and Graphics-based rendering with
|
|
2904
|
+
* per-state views, press animation, and text.
|
|
2506
2905
|
*
|
|
2507
2906
|
* @example
|
|
2508
2907
|
* ```ts
|
|
2509
2908
|
* const btn = new Button({
|
|
2510
2909
|
* width: 200, height: 60, borderRadius: 12,
|
|
2511
|
-
* colors: {
|
|
2910
|
+
* colors: { default: 0x22aa22, hover: 0x33cc33 },
|
|
2911
|
+
* text: 'SPIN',
|
|
2512
2912
|
* });
|
|
2513
2913
|
*
|
|
2514
|
-
* btn.
|
|
2914
|
+
* btn.onPress.connect(() => console.log('Clicked!'));
|
|
2515
2915
|
* scene.container.addChild(btn);
|
|
2516
2916
|
* ```
|
|
2517
2917
|
*/
|
|
2518
|
-
class Button extends
|
|
2519
|
-
|
|
2520
|
-
_bg;
|
|
2521
|
-
_sprites = {};
|
|
2522
|
-
_config;
|
|
2523
|
-
/** Called when the button is tapped/clicked */
|
|
2524
|
-
onTap;
|
|
2525
|
-
/** Called when the button state changes */
|
|
2526
|
-
onStateChange;
|
|
2918
|
+
class Button extends FancyButton {
|
|
2919
|
+
_buttonConfig;
|
|
2527
2920
|
constructor(config = {}) {
|
|
2528
|
-
|
|
2529
|
-
|
|
2530
|
-
|
|
2531
|
-
|
|
2532
|
-
|
|
2533
|
-
|
|
2534
|
-
animationDuration: 100,
|
|
2921
|
+
const resolvedConfig = {
|
|
2922
|
+
width: config.width ?? 200,
|
|
2923
|
+
height: config.height ?? 60,
|
|
2924
|
+
borderRadius: config.borderRadius ?? 8,
|
|
2925
|
+
pressScale: config.pressScale ?? 0.95,
|
|
2926
|
+
animationDuration: config.animationDuration ?? 100,
|
|
2535
2927
|
...config,
|
|
2536
2928
|
};
|
|
2537
|
-
|
|
2538
|
-
|
|
2539
|
-
|
|
2540
|
-
|
|
2929
|
+
const colorMap = { ...DEFAULT_COLORS, ...config.colors };
|
|
2930
|
+
const { width, height, borderRadius } = resolvedConfig;
|
|
2931
|
+
// Build FancyButton options
|
|
2932
|
+
const options = {
|
|
2933
|
+
anchor: 0.5,
|
|
2934
|
+
animations: {
|
|
2935
|
+
hover: {
|
|
2936
|
+
props: { scale: { x: 1.03, y: 1.03 } },
|
|
2937
|
+
duration: resolvedConfig.animationDuration,
|
|
2938
|
+
},
|
|
2939
|
+
pressed: {
|
|
2940
|
+
props: { scale: { x: resolvedConfig.pressScale, y: resolvedConfig.pressScale } },
|
|
2941
|
+
duration: resolvedConfig.animationDuration,
|
|
2942
|
+
},
|
|
2943
|
+
},
|
|
2944
|
+
};
|
|
2945
|
+
// Texture-based views
|
|
2541
2946
|
if (config.textures) {
|
|
2542
|
-
|
|
2543
|
-
|
|
2544
|
-
|
|
2545
|
-
|
|
2546
|
-
|
|
2547
|
-
|
|
2548
|
-
|
|
2549
|
-
|
|
2947
|
+
if (config.textures.default)
|
|
2948
|
+
options.defaultView = config.textures.default;
|
|
2949
|
+
if (config.textures.hover)
|
|
2950
|
+
options.hoverView = config.textures.hover;
|
|
2951
|
+
if (config.textures.pressed)
|
|
2952
|
+
options.pressedView = config.textures.pressed;
|
|
2953
|
+
if (config.textures.disabled)
|
|
2954
|
+
options.disabledView = config.textures.disabled;
|
|
2550
2955
|
}
|
|
2551
|
-
|
|
2552
|
-
|
|
2553
|
-
|
|
2554
|
-
|
|
2555
|
-
|
|
2556
|
-
|
|
2557
|
-
|
|
2558
|
-
|
|
2559
|
-
|
|
2560
|
-
|
|
2561
|
-
|
|
2562
|
-
|
|
2563
|
-
this.
|
|
2956
|
+
else {
|
|
2957
|
+
// Graphics-based views
|
|
2958
|
+
options.defaultView = makeGraphicsView(width, height, borderRadius, colorMap.default);
|
|
2959
|
+
options.hoverView = makeGraphicsView(width, height, borderRadius, colorMap.hover);
|
|
2960
|
+
options.pressedView = makeGraphicsView(width, height, borderRadius, colorMap.pressed);
|
|
2961
|
+
options.disabledView = makeGraphicsView(width, height, borderRadius, colorMap.disabled);
|
|
2962
|
+
}
|
|
2963
|
+
// Text
|
|
2964
|
+
if (config.text) {
|
|
2965
|
+
options.text = config.text;
|
|
2966
|
+
}
|
|
2967
|
+
super(options);
|
|
2968
|
+
this._buttonConfig = resolvedConfig;
|
|
2564
2969
|
if (config.disabled) {
|
|
2565
|
-
this.
|
|
2970
|
+
this.enabled = false;
|
|
2566
2971
|
}
|
|
2567
2972
|
}
|
|
2568
|
-
/** Current button state */
|
|
2569
|
-
get state() {
|
|
2570
|
-
return this._state;
|
|
2571
|
-
}
|
|
2572
2973
|
/** Enable the button */
|
|
2573
2974
|
enable() {
|
|
2574
|
-
|
|
2575
|
-
this.setState('normal');
|
|
2576
|
-
this.eventMode = 'static';
|
|
2577
|
-
this.cursor = 'pointer';
|
|
2578
|
-
}
|
|
2975
|
+
this.enabled = true;
|
|
2579
2976
|
}
|
|
2580
2977
|
/** Disable the button */
|
|
2581
2978
|
disable() {
|
|
2582
|
-
this.
|
|
2583
|
-
this.eventMode = 'none';
|
|
2584
|
-
this.cursor = 'default';
|
|
2979
|
+
this.enabled = false;
|
|
2585
2980
|
}
|
|
2586
2981
|
/** Whether the button is disabled */
|
|
2587
2982
|
get disabled() {
|
|
2588
|
-
return this.
|
|
2983
|
+
return !this.enabled;
|
|
2589
2984
|
}
|
|
2590
|
-
setState(state) {
|
|
2591
|
-
if (this._state === state)
|
|
2592
|
-
return;
|
|
2593
|
-
this._state = state;
|
|
2594
|
-
this.render();
|
|
2595
|
-
this.onStateChange?.(state);
|
|
2596
|
-
}
|
|
2597
|
-
render() {
|
|
2598
|
-
const { width, height, borderRadius, colors } = this._config;
|
|
2599
|
-
const colorMap = { ...DEFAULT_COLORS, ...colors };
|
|
2600
|
-
// Update Graphics
|
|
2601
|
-
this._bg.clear();
|
|
2602
|
-
this._bg.roundRect(0, 0, width, height, borderRadius).fill(colorMap[this._state]);
|
|
2603
|
-
// Add highlight for normal/hover
|
|
2604
|
-
if (this._state === 'normal' || this._state === 'hover') {
|
|
2605
|
-
this._bg
|
|
2606
|
-
.roundRect(2, 2, width - 4, height * 0.45, borderRadius)
|
|
2607
|
-
.fill({ color: 0xffffff, alpha: 0.1 });
|
|
2608
|
-
}
|
|
2609
|
-
// Update sprite visibility
|
|
2610
|
-
for (const [state, sprite] of Object.entries(this._sprites)) {
|
|
2611
|
-
if (sprite)
|
|
2612
|
-
sprite.visible = state === this._state;
|
|
2613
|
-
}
|
|
2614
|
-
// Fall back to normal sprite if state sprite doesn't exist
|
|
2615
|
-
if (!this._sprites[this._state] && this._sprites.normal) {
|
|
2616
|
-
this._sprites.normal.visible = true;
|
|
2617
|
-
}
|
|
2618
|
-
}
|
|
2619
|
-
onPointerOver = () => {
|
|
2620
|
-
if (this._state === 'disabled')
|
|
2621
|
-
return;
|
|
2622
|
-
this.setState('hover');
|
|
2623
|
-
};
|
|
2624
|
-
onPointerOut = () => {
|
|
2625
|
-
if (this._state === 'disabled')
|
|
2626
|
-
return;
|
|
2627
|
-
this.setState('normal');
|
|
2628
|
-
Tween.to(this.scale, { x: 1, y: 1 }, this._config.animationDuration);
|
|
2629
|
-
};
|
|
2630
|
-
onPointerDown = () => {
|
|
2631
|
-
if (this._state === 'disabled')
|
|
2632
|
-
return;
|
|
2633
|
-
this.setState('pressed');
|
|
2634
|
-
const s = this._config.pressScale;
|
|
2635
|
-
Tween.to(this.scale, { x: s, y: s }, this._config.animationDuration, Easing.easeOutQuad);
|
|
2636
|
-
};
|
|
2637
|
-
onPointerUp = () => {
|
|
2638
|
-
if (this._state === 'disabled')
|
|
2639
|
-
return;
|
|
2640
|
-
this.setState('hover');
|
|
2641
|
-
Tween.to(this.scale, { x: 1, y: 1 }, this._config.animationDuration, Easing.easeOutBack);
|
|
2642
|
-
};
|
|
2643
|
-
onPointerTap = () => {
|
|
2644
|
-
if (this._state === 'disabled')
|
|
2645
|
-
return;
|
|
2646
|
-
this.onTap?.();
|
|
2647
|
-
};
|
|
2648
2985
|
}
|
|
2649
2986
|
|
|
2987
|
+
function makeBarGraphics(w, h, radius, color) {
|
|
2988
|
+
return new Graphics().roundRect(0, 0, w, h, radius).fill(color);
|
|
2989
|
+
}
|
|
2650
2990
|
/**
|
|
2651
|
-
* Horizontal progress bar
|
|
2991
|
+
* Horizontal progress bar powered by `@pixi/ui` ProgressBar.
|
|
2992
|
+
*
|
|
2993
|
+
* Provides optional smooth animated fill via per-frame `update()`.
|
|
2652
2994
|
*
|
|
2653
2995
|
* @example
|
|
2654
2996
|
* ```ts
|
|
@@ -2658,33 +3000,48 @@ class Button extends Container {
|
|
|
2658
3000
|
* ```
|
|
2659
3001
|
*/
|
|
2660
3002
|
class ProgressBar extends Container {
|
|
2661
|
-
|
|
2662
|
-
|
|
2663
|
-
_border;
|
|
3003
|
+
_bar;
|
|
3004
|
+
_borderGfx;
|
|
2664
3005
|
_config;
|
|
2665
3006
|
_progress = 0;
|
|
2666
3007
|
_displayedProgress = 0;
|
|
2667
3008
|
constructor(config = {}) {
|
|
2668
3009
|
super();
|
|
2669
3010
|
this._config = {
|
|
2670
|
-
width: 300,
|
|
2671
|
-
height: 16,
|
|
2672
|
-
borderRadius: 8,
|
|
2673
|
-
fillColor: 0xffd700,
|
|
2674
|
-
trackColor: 0x333333,
|
|
2675
|
-
borderColor: 0x555555,
|
|
2676
|
-
borderWidth: 1,
|
|
2677
|
-
animated: true,
|
|
2678
|
-
animationSpeed: 0.1,
|
|
2679
|
-
|
|
3011
|
+
width: config.width ?? 300,
|
|
3012
|
+
height: config.height ?? 16,
|
|
3013
|
+
borderRadius: config.borderRadius ?? 8,
|
|
3014
|
+
fillColor: config.fillColor ?? 0xffd700,
|
|
3015
|
+
trackColor: config.trackColor ?? 0x333333,
|
|
3016
|
+
borderColor: config.borderColor ?? 0x555555,
|
|
3017
|
+
borderWidth: config.borderWidth ?? 1,
|
|
3018
|
+
animated: config.animated ?? true,
|
|
3019
|
+
animationSpeed: config.animationSpeed ?? 0.1,
|
|
3020
|
+
};
|
|
3021
|
+
const { width, height, borderRadius, fillColor, trackColor, borderColor, borderWidth } = this._config;
|
|
3022
|
+
const bgGraphics = makeBarGraphics(width, height, borderRadius, trackColor);
|
|
3023
|
+
const fillGraphics = makeBarGraphics(width - borderWidth * 2, height - borderWidth * 2, Math.max(0, borderRadius - 1), fillColor);
|
|
3024
|
+
const options = {
|
|
3025
|
+
bg: bgGraphics,
|
|
3026
|
+
fill: fillGraphics,
|
|
3027
|
+
fillPaddings: {
|
|
3028
|
+
top: borderWidth,
|
|
3029
|
+
right: borderWidth,
|
|
3030
|
+
bottom: borderWidth,
|
|
3031
|
+
left: borderWidth,
|
|
3032
|
+
},
|
|
3033
|
+
progress: 0,
|
|
2680
3034
|
};
|
|
2681
|
-
this.
|
|
2682
|
-
this.
|
|
2683
|
-
|
|
2684
|
-
this.
|
|
2685
|
-
|
|
2686
|
-
|
|
2687
|
-
|
|
3035
|
+
this._bar = new ProgressBar$1(options);
|
|
3036
|
+
this.addChild(this._bar);
|
|
3037
|
+
// Border overlay
|
|
3038
|
+
this._borderGfx = new Graphics();
|
|
3039
|
+
if (borderColor !== undefined && borderWidth > 0) {
|
|
3040
|
+
this._borderGfx
|
|
3041
|
+
.roundRect(0, 0, width, height, borderRadius)
|
|
3042
|
+
.stroke({ color: borderColor, width: borderWidth });
|
|
3043
|
+
}
|
|
3044
|
+
this.addChild(this._borderGfx);
|
|
2688
3045
|
}
|
|
2689
3046
|
/** Get/set progress (0..1) */
|
|
2690
3047
|
get progress() {
|
|
@@ -2694,13 +3051,13 @@ class ProgressBar extends Container {
|
|
|
2694
3051
|
this._progress = Math.max(0, Math.min(1, value));
|
|
2695
3052
|
if (!this._config.animated) {
|
|
2696
3053
|
this._displayedProgress = this._progress;
|
|
2697
|
-
this.
|
|
3054
|
+
this._bar.progress = this._displayedProgress * 100;
|
|
2698
3055
|
}
|
|
2699
3056
|
}
|
|
2700
3057
|
/**
|
|
2701
3058
|
* Call each frame if animated is true.
|
|
2702
3059
|
*/
|
|
2703
|
-
update(
|
|
3060
|
+
update(_dt) {
|
|
2704
3061
|
if (!this._config.animated)
|
|
2705
3062
|
return;
|
|
2706
3063
|
if (Math.abs(this._displayedProgress - this._progress) < 0.001) {
|
|
@@ -2709,35 +3066,7 @@ class ProgressBar extends Container {
|
|
|
2709
3066
|
}
|
|
2710
3067
|
this._displayedProgress +=
|
|
2711
3068
|
(this._progress - this._displayedProgress) * this._config.animationSpeed;
|
|
2712
|
-
this.
|
|
2713
|
-
}
|
|
2714
|
-
drawTrack() {
|
|
2715
|
-
const { width, height, borderRadius, trackColor } = this._config;
|
|
2716
|
-
this._track.clear();
|
|
2717
|
-
this._track.roundRect(0, 0, width, height, borderRadius).fill(trackColor);
|
|
2718
|
-
}
|
|
2719
|
-
drawBorder() {
|
|
2720
|
-
const { width, height, borderRadius, borderColor, borderWidth } = this._config;
|
|
2721
|
-
this._border.clear();
|
|
2722
|
-
this._border
|
|
2723
|
-
.roundRect(0, 0, width, height, borderRadius)
|
|
2724
|
-
.stroke({ color: borderColor, width: borderWidth });
|
|
2725
|
-
}
|
|
2726
|
-
drawFill(progress) {
|
|
2727
|
-
const { width, height, borderRadius, fillColor, borderWidth } = this._config;
|
|
2728
|
-
const innerWidth = width - borderWidth * 2;
|
|
2729
|
-
const innerHeight = height - borderWidth * 2;
|
|
2730
|
-
const fillWidth = Math.max(0, innerWidth * progress);
|
|
2731
|
-
this._fill.clear();
|
|
2732
|
-
if (fillWidth > 0) {
|
|
2733
|
-
this._fill.x = borderWidth;
|
|
2734
|
-
this._fill.y = borderWidth;
|
|
2735
|
-
this._fill.roundRect(0, 0, fillWidth, innerHeight, borderRadius - 1).fill(fillColor);
|
|
2736
|
-
// Highlight
|
|
2737
|
-
this._fill
|
|
2738
|
-
.roundRect(0, 0, fillWidth, innerHeight * 0.4, borderRadius - 1)
|
|
2739
|
-
.fill({ color: 0xffffff, alpha: 0.15 });
|
|
2740
|
-
}
|
|
3069
|
+
this._bar.progress = this._displayedProgress * 100;
|
|
2741
3070
|
}
|
|
2742
3071
|
}
|
|
2743
3072
|
|
|
@@ -2833,7 +3162,10 @@ class Label extends Container {
|
|
|
2833
3162
|
}
|
|
2834
3163
|
|
|
2835
3164
|
/**
|
|
2836
|
-
* Background panel
|
|
3165
|
+
* Background panel powered by `@pixi/layout` LayoutContainer.
|
|
3166
|
+
*
|
|
3167
|
+
* Supports both Graphics-based (color + border) and 9-slice sprite backgrounds.
|
|
3168
|
+
* Children added to `content` participate in flexbox layout automatically.
|
|
2837
3169
|
*
|
|
2838
3170
|
* @example
|
|
2839
3171
|
* ```ts
|
|
@@ -2848,72 +3180,67 @@ class Label extends Container {
|
|
|
2848
3180
|
* });
|
|
2849
3181
|
* ```
|
|
2850
3182
|
*/
|
|
2851
|
-
class Panel extends
|
|
2852
|
-
|
|
2853
|
-
_content;
|
|
2854
|
-
_config;
|
|
3183
|
+
class Panel extends LayoutContainer {
|
|
3184
|
+
_panelConfig;
|
|
2855
3185
|
constructor(config = {}) {
|
|
2856
|
-
|
|
2857
|
-
|
|
2858
|
-
|
|
2859
|
-
|
|
2860
|
-
|
|
2861
|
-
backgroundAlpha: 1,
|
|
3186
|
+
const resolvedConfig = {
|
|
3187
|
+
width: config.width ?? 400,
|
|
3188
|
+
height: config.height ?? 300,
|
|
3189
|
+
padding: config.padding ?? 16,
|
|
3190
|
+
backgroundAlpha: config.backgroundAlpha ?? 1,
|
|
2862
3191
|
...config,
|
|
2863
3192
|
};
|
|
2864
|
-
//
|
|
3193
|
+
// If using a 9-slice texture, pass it as a custom background
|
|
3194
|
+
let customBackground;
|
|
2865
3195
|
if (config.nineSliceTexture) {
|
|
2866
3196
|
const texture = typeof config.nineSliceTexture === 'string'
|
|
2867
3197
|
? Texture.from(config.nineSliceTexture)
|
|
2868
3198
|
: config.nineSliceTexture;
|
|
2869
3199
|
const [left, top, right, bottom] = config.nineSliceBorders ?? [10, 10, 10, 10];
|
|
2870
|
-
|
|
3200
|
+
const nineSlice = new NineSliceSprite({
|
|
2871
3201
|
texture,
|
|
2872
3202
|
leftWidth: left,
|
|
2873
3203
|
topHeight: top,
|
|
2874
3204
|
rightWidth: right,
|
|
2875
3205
|
bottomHeight: bottom,
|
|
2876
3206
|
});
|
|
2877
|
-
|
|
2878
|
-
|
|
3207
|
+
nineSlice.width = resolvedConfig.width;
|
|
3208
|
+
nineSlice.height = resolvedConfig.height;
|
|
3209
|
+
nineSlice.alpha = resolvedConfig.backgroundAlpha;
|
|
3210
|
+
customBackground = nineSlice;
|
|
3211
|
+
}
|
|
3212
|
+
super(customBackground ? { background: customBackground } : undefined);
|
|
3213
|
+
this._panelConfig = resolvedConfig;
|
|
3214
|
+
// Apply layout styles
|
|
3215
|
+
const layoutStyles = {
|
|
3216
|
+
width: resolvedConfig.width,
|
|
3217
|
+
height: resolvedConfig.height,
|
|
3218
|
+
padding: resolvedConfig.padding,
|
|
3219
|
+
flexDirection: 'column',
|
|
3220
|
+
};
|
|
3221
|
+
// Graphics-based background via layout styles
|
|
3222
|
+
if (!config.nineSliceTexture) {
|
|
3223
|
+
layoutStyles.backgroundColor = config.backgroundColor ?? 0x1a1a2e;
|
|
3224
|
+
layoutStyles.borderRadius = config.borderRadius ?? 0;
|
|
3225
|
+
if (config.borderColor !== undefined && config.borderWidth) {
|
|
3226
|
+
layoutStyles.borderColor = config.borderColor;
|
|
3227
|
+
layoutStyles.borderWidth = config.borderWidth;
|
|
3228
|
+
}
|
|
2879
3229
|
}
|
|
2880
|
-
|
|
2881
|
-
|
|
2882
|
-
this.
|
|
3230
|
+
this.layout = layoutStyles;
|
|
3231
|
+
if (!config.nineSliceTexture) {
|
|
3232
|
+
this.background.alpha = resolvedConfig.backgroundAlpha;
|
|
2883
3233
|
}
|
|
2884
|
-
this._bg.alpha = this._config.backgroundAlpha;
|
|
2885
|
-
this.addChild(this._bg);
|
|
2886
|
-
// Content container with padding
|
|
2887
|
-
this._content = new Container();
|
|
2888
|
-
this._content.x = this._config.padding;
|
|
2889
|
-
this._content.y = this._config.padding;
|
|
2890
|
-
this.addChild(this._content);
|
|
2891
3234
|
}
|
|
2892
|
-
/**
|
|
3235
|
+
/** Access the content container (children added here participate in layout) */
|
|
2893
3236
|
get content() {
|
|
2894
|
-
return this.
|
|
3237
|
+
return this.overflowContainer;
|
|
2895
3238
|
}
|
|
2896
3239
|
/** Resize the panel */
|
|
2897
3240
|
setSize(width, height) {
|
|
2898
|
-
this.
|
|
2899
|
-
this.
|
|
2900
|
-
|
|
2901
|
-
this.drawGraphicsBg();
|
|
2902
|
-
}
|
|
2903
|
-
else {
|
|
2904
|
-
this._bg.width = width;
|
|
2905
|
-
this._bg.height = height;
|
|
2906
|
-
}
|
|
2907
|
-
}
|
|
2908
|
-
drawGraphicsBg() {
|
|
2909
|
-
const bg = this._bg;
|
|
2910
|
-
const { width, height, backgroundColor, borderRadius, borderColor, borderWidth, } = this._config;
|
|
2911
|
-
bg.clear();
|
|
2912
|
-
bg.roundRect(0, 0, width, height, borderRadius ?? 0).fill(backgroundColor ?? 0x1a1a2e);
|
|
2913
|
-
if (borderColor !== undefined && borderWidth) {
|
|
2914
|
-
bg.roundRect(0, 0, width, height, borderRadius ?? 0)
|
|
2915
|
-
.stroke({ color: borderColor, width: borderWidth });
|
|
2916
|
-
}
|
|
3241
|
+
this._panelConfig.width = width;
|
|
3242
|
+
this._panelConfig.height = height;
|
|
3243
|
+
this._layout?.setStyle({ width, height });
|
|
2917
3244
|
}
|
|
2918
3245
|
}
|
|
2919
3246
|
|
|
@@ -2939,6 +3266,7 @@ class BalanceDisplay extends Container {
|
|
|
2939
3266
|
_currentValue = 0;
|
|
2940
3267
|
_displayedValue = 0;
|
|
2941
3268
|
_animating = false;
|
|
3269
|
+
_animationCancelled = false;
|
|
2942
3270
|
constructor(config = {}) {
|
|
2943
3271
|
super();
|
|
2944
3272
|
this._config = {
|
|
@@ -3000,11 +3328,20 @@ class BalanceDisplay extends Container {
|
|
|
3000
3328
|
this.updateDisplay();
|
|
3001
3329
|
}
|
|
3002
3330
|
async animateValue(from, to) {
|
|
3331
|
+
if (this._animating) {
|
|
3332
|
+
this._animationCancelled = true;
|
|
3333
|
+
}
|
|
3003
3334
|
this._animating = true;
|
|
3335
|
+
this._animationCancelled = false;
|
|
3004
3336
|
const duration = this._config.animationDuration;
|
|
3005
3337
|
const startTime = Date.now();
|
|
3006
3338
|
return new Promise((resolve) => {
|
|
3007
3339
|
const tick = () => {
|
|
3340
|
+
if (this._animationCancelled) {
|
|
3341
|
+
this._animating = false;
|
|
3342
|
+
resolve();
|
|
3343
|
+
return;
|
|
3344
|
+
}
|
|
3008
3345
|
const elapsed = Date.now() - startTime;
|
|
3009
3346
|
const t = Math.min(elapsed / duration, 1);
|
|
3010
3347
|
const eased = Easing.easeOutCubic(t);
|
|
@@ -3141,6 +3478,8 @@ class WinDisplay extends Container {
|
|
|
3141
3478
|
* Modal overlay component.
|
|
3142
3479
|
* Shows content on top of a dark overlay with enter/exit animations.
|
|
3143
3480
|
*
|
|
3481
|
+
* The content container uses `@pixi/layout` for automatic centering.
|
|
3482
|
+
*
|
|
3144
3483
|
* @example
|
|
3145
3484
|
* ```ts
|
|
3146
3485
|
* const modal = new Modal({ closeOnOverlay: true });
|
|
@@ -3159,11 +3498,10 @@ class Modal extends Container {
|
|
|
3159
3498
|
constructor(config = {}) {
|
|
3160
3499
|
super();
|
|
3161
3500
|
this._config = {
|
|
3162
|
-
overlayColor: 0x000000,
|
|
3163
|
-
overlayAlpha: 0.7,
|
|
3164
|
-
closeOnOverlay: true,
|
|
3165
|
-
animationDuration: 300,
|
|
3166
|
-
...config,
|
|
3501
|
+
overlayColor: config.overlayColor ?? 0x000000,
|
|
3502
|
+
overlayAlpha: config.overlayAlpha ?? 0.7,
|
|
3503
|
+
closeOnOverlay: config.closeOnOverlay ?? true,
|
|
3504
|
+
animationDuration: config.animationDuration ?? 300,
|
|
3167
3505
|
};
|
|
3168
3506
|
// Overlay
|
|
3169
3507
|
this._overlay = new Graphics();
|
|
@@ -3231,6 +3569,8 @@ const TOAST_COLORS = {
|
|
|
3231
3569
|
/**
|
|
3232
3570
|
* Toast notification component for displaying transient messages.
|
|
3233
3571
|
*
|
|
3572
|
+
* Uses `@pixi/layout` LayoutContainer for auto-sized background.
|
|
3573
|
+
*
|
|
3234
3574
|
* @example
|
|
3235
3575
|
* ```ts
|
|
3236
3576
|
* const toast = new Toast();
|
|
@@ -3246,11 +3586,10 @@ class Toast extends Container {
|
|
|
3246
3586
|
constructor(config = {}) {
|
|
3247
3587
|
super();
|
|
3248
3588
|
this._config = {
|
|
3249
|
-
duration: 3000,
|
|
3250
|
-
bottomOffset: 60,
|
|
3251
|
-
...config,
|
|
3589
|
+
duration: config.duration ?? 3000,
|
|
3590
|
+
bottomOffset: config.bottomOffset ?? 60,
|
|
3252
3591
|
};
|
|
3253
|
-
this._bg = new
|
|
3592
|
+
this._bg = new LayoutContainer();
|
|
3254
3593
|
this.addChild(this._bg);
|
|
3255
3594
|
this._text = new Text({
|
|
3256
3595
|
text: '',
|
|
@@ -3268,7 +3607,6 @@ class Toast extends Container {
|
|
|
3268
3607
|
* Show a toast message.
|
|
3269
3608
|
*/
|
|
3270
3609
|
async show(message, type = 'info', viewWidth, viewHeight) {
|
|
3271
|
-
// Clear previous dismiss
|
|
3272
3610
|
if (this._dismissTimeout) {
|
|
3273
3611
|
clearTimeout(this._dismissTimeout);
|
|
3274
3612
|
}
|
|
@@ -3277,10 +3615,16 @@ class Toast extends Container {
|
|
|
3277
3615
|
const width = Math.max(200, this._text.width + padding * 2);
|
|
3278
3616
|
const height = 44;
|
|
3279
3617
|
const radius = 8;
|
|
3280
|
-
|
|
3281
|
-
this._bg.
|
|
3282
|
-
|
|
3283
|
-
|
|
3618
|
+
// Style the background
|
|
3619
|
+
this._bg.layout = {
|
|
3620
|
+
width,
|
|
3621
|
+
height,
|
|
3622
|
+
borderRadius: radius,
|
|
3623
|
+
backgroundColor: TOAST_COLORS[type],
|
|
3624
|
+
};
|
|
3625
|
+
// Center the bg around origin
|
|
3626
|
+
this._bg.x = -width / 2;
|
|
3627
|
+
this._bg.y = -height / 2;
|
|
3284
3628
|
// Position
|
|
3285
3629
|
if (viewWidth && viewHeight) {
|
|
3286
3630
|
this.x = viewWidth / 2;
|
|
@@ -3289,9 +3633,7 @@ class Toast extends Container {
|
|
|
3289
3633
|
this.visible = true;
|
|
3290
3634
|
this.alpha = 0;
|
|
3291
3635
|
this.y += 20;
|
|
3292
|
-
// Animate in
|
|
3293
3636
|
await Tween.to(this, { alpha: 1, y: this.y - 20 }, 300, Easing.easeOutCubic);
|
|
3294
|
-
// Auto-dismiss
|
|
3295
3637
|
if (this._config.duration > 0) {
|
|
3296
3638
|
this._dismissTimeout = setTimeout(() => {
|
|
3297
3639
|
this.dismiss();
|
|
@@ -3313,6 +3655,292 @@ class Toast extends Container {
|
|
|
3313
3655
|
}
|
|
3314
3656
|
}
|
|
3315
3657
|
|
|
3658
|
+
// ─── Helpers ─────────────────────────────────────────────
|
|
3659
|
+
const ALIGNMENT_MAP = {
|
|
3660
|
+
start: 'flex-start',
|
|
3661
|
+
center: 'center',
|
|
3662
|
+
end: 'flex-end',
|
|
3663
|
+
stretch: 'stretch',
|
|
3664
|
+
};
|
|
3665
|
+
function normalizePadding(padding) {
|
|
3666
|
+
if (typeof padding === 'number')
|
|
3667
|
+
return [padding, padding, padding, padding];
|
|
3668
|
+
return padding;
|
|
3669
|
+
}
|
|
3670
|
+
function directionToFlexStyles(direction, maxWidth) {
|
|
3671
|
+
switch (direction) {
|
|
3672
|
+
case 'horizontal':
|
|
3673
|
+
return { flexDirection: 'row', flexWrap: 'nowrap' };
|
|
3674
|
+
case 'vertical':
|
|
3675
|
+
return { flexDirection: 'column', flexWrap: 'nowrap' };
|
|
3676
|
+
case 'grid':
|
|
3677
|
+
return { flexDirection: 'row', flexWrap: 'wrap' };
|
|
3678
|
+
case 'wrap':
|
|
3679
|
+
return {
|
|
3680
|
+
flexDirection: 'row',
|
|
3681
|
+
flexWrap: 'wrap',
|
|
3682
|
+
...(maxWidth < Infinity ? { maxWidth } : {}),
|
|
3683
|
+
};
|
|
3684
|
+
}
|
|
3685
|
+
}
|
|
3686
|
+
function buildLayoutStyles(config) {
|
|
3687
|
+
const [pt, pr, pb, pl] = config.padding;
|
|
3688
|
+
return {
|
|
3689
|
+
...directionToFlexStyles(config.direction, config.maxWidth),
|
|
3690
|
+
gap: config.gap,
|
|
3691
|
+
alignItems: ALIGNMENT_MAP[config.alignment],
|
|
3692
|
+
paddingTop: pt,
|
|
3693
|
+
paddingRight: pr,
|
|
3694
|
+
paddingBottom: pb,
|
|
3695
|
+
paddingLeft: pl,
|
|
3696
|
+
};
|
|
3697
|
+
}
|
|
3698
|
+
/**
|
|
3699
|
+
* Responsive layout container powered by `@pixi/layout` (Yoga flexbox engine).
|
|
3700
|
+
*
|
|
3701
|
+
* Supports horizontal, vertical, grid, and wrap layout modes with
|
|
3702
|
+
* alignment, padding, gap, and viewport-anchor positioning.
|
|
3703
|
+
* Breakpoints allow different layouts for different screen sizes.
|
|
3704
|
+
*
|
|
3705
|
+
* @example
|
|
3706
|
+
* ```ts
|
|
3707
|
+
* const toolbar = new Layout({
|
|
3708
|
+
* direction: 'horizontal',
|
|
3709
|
+
* gap: 20,
|
|
3710
|
+
* alignment: 'center',
|
|
3711
|
+
* anchor: 'bottom-center',
|
|
3712
|
+
* padding: 16,
|
|
3713
|
+
* breakpoints: {
|
|
3714
|
+
* 768: { direction: 'vertical', gap: 10 },
|
|
3715
|
+
* },
|
|
3716
|
+
* });
|
|
3717
|
+
*
|
|
3718
|
+
* toolbar.addItem(spinButton);
|
|
3719
|
+
* toolbar.addItem(betLabel);
|
|
3720
|
+
* scene.container.addChild(toolbar);
|
|
3721
|
+
*
|
|
3722
|
+
* toolbar.updateViewport(width, height);
|
|
3723
|
+
* ```
|
|
3724
|
+
*/
|
|
3725
|
+
class Layout extends Container {
|
|
3726
|
+
_layoutConfig;
|
|
3727
|
+
_padding;
|
|
3728
|
+
_anchor;
|
|
3729
|
+
_maxWidth;
|
|
3730
|
+
_breakpoints;
|
|
3731
|
+
_items = [];
|
|
3732
|
+
_viewportWidth = 0;
|
|
3733
|
+
_viewportHeight = 0;
|
|
3734
|
+
constructor(config = {}) {
|
|
3735
|
+
super();
|
|
3736
|
+
this._layoutConfig = {
|
|
3737
|
+
direction: config.direction ?? 'vertical',
|
|
3738
|
+
gap: config.gap ?? 0,
|
|
3739
|
+
alignment: config.alignment ?? 'start',
|
|
3740
|
+
autoLayout: config.autoLayout ?? true,
|
|
3741
|
+
columns: config.columns ?? 2,
|
|
3742
|
+
};
|
|
3743
|
+
this._padding = normalizePadding(config.padding ?? 0);
|
|
3744
|
+
this._anchor = config.anchor ?? 'top-left';
|
|
3745
|
+
this._maxWidth = config.maxWidth ?? Infinity;
|
|
3746
|
+
this._breakpoints = config.breakpoints
|
|
3747
|
+
? Object.entries(config.breakpoints)
|
|
3748
|
+
.map(([w, cfg]) => [Number(w), cfg])
|
|
3749
|
+
.sort((a, b) => a[0] - b[0])
|
|
3750
|
+
: [];
|
|
3751
|
+
this.applyLayoutStyles();
|
|
3752
|
+
}
|
|
3753
|
+
/** Add an item to the layout */
|
|
3754
|
+
addItem(child) {
|
|
3755
|
+
this._items.push(child);
|
|
3756
|
+
this.addChild(child);
|
|
3757
|
+
if (this._layoutConfig.direction === 'grid') {
|
|
3758
|
+
this.applyGridChildWidth(child);
|
|
3759
|
+
}
|
|
3760
|
+
return this;
|
|
3761
|
+
}
|
|
3762
|
+
/** Remove an item from the layout */
|
|
3763
|
+
removeItem(child) {
|
|
3764
|
+
const idx = this._items.indexOf(child);
|
|
3765
|
+
if (idx !== -1) {
|
|
3766
|
+
this._items.splice(idx, 1);
|
|
3767
|
+
this.removeChild(child);
|
|
3768
|
+
}
|
|
3769
|
+
return this;
|
|
3770
|
+
}
|
|
3771
|
+
/** Remove all items */
|
|
3772
|
+
clearItems() {
|
|
3773
|
+
for (const item of this._items) {
|
|
3774
|
+
this.removeChild(item);
|
|
3775
|
+
}
|
|
3776
|
+
this._items.length = 0;
|
|
3777
|
+
return this;
|
|
3778
|
+
}
|
|
3779
|
+
/** Get all layout items */
|
|
3780
|
+
get items() {
|
|
3781
|
+
return this._items;
|
|
3782
|
+
}
|
|
3783
|
+
/**
|
|
3784
|
+
* Update the viewport size and recalculate layout.
|
|
3785
|
+
* Should be called from `Scene.onResize()`.
|
|
3786
|
+
*/
|
|
3787
|
+
updateViewport(width, height) {
|
|
3788
|
+
this._viewportWidth = width;
|
|
3789
|
+
this._viewportHeight = height;
|
|
3790
|
+
this.applyLayoutStyles();
|
|
3791
|
+
this.applyAnchor();
|
|
3792
|
+
}
|
|
3793
|
+
applyLayoutStyles() {
|
|
3794
|
+
const effective = this.resolveConfig();
|
|
3795
|
+
const direction = effective.direction ?? this._layoutConfig.direction;
|
|
3796
|
+
const gap = effective.gap ?? this._layoutConfig.gap;
|
|
3797
|
+
const alignment = effective.alignment ?? this._layoutConfig.alignment;
|
|
3798
|
+
effective.columns ?? this._layoutConfig.columns;
|
|
3799
|
+
const padding = effective.padding !== undefined
|
|
3800
|
+
? normalizePadding(effective.padding)
|
|
3801
|
+
: this._padding;
|
|
3802
|
+
const maxWidth = effective.maxWidth ?? this._maxWidth;
|
|
3803
|
+
const styles = buildLayoutStyles({ direction, gap, alignment, padding, maxWidth });
|
|
3804
|
+
this.layout = styles;
|
|
3805
|
+
if (direction === 'grid') {
|
|
3806
|
+
for (const item of this._items) {
|
|
3807
|
+
this.applyGridChildWidth(item);
|
|
3808
|
+
}
|
|
3809
|
+
}
|
|
3810
|
+
}
|
|
3811
|
+
applyGridChildWidth(child) {
|
|
3812
|
+
const effective = this.resolveConfig();
|
|
3813
|
+
const columns = effective.columns ?? this._layoutConfig.columns;
|
|
3814
|
+
const pct = `${(100 / columns).toFixed(2)}%`;
|
|
3815
|
+
if (child._layout) {
|
|
3816
|
+
child._layout.setStyle({ width: pct });
|
|
3817
|
+
}
|
|
3818
|
+
else {
|
|
3819
|
+
child.layout = { width: pct };
|
|
3820
|
+
}
|
|
3821
|
+
}
|
|
3822
|
+
applyAnchor() {
|
|
3823
|
+
const anchor = this.resolveConfig().anchor ?? this._anchor;
|
|
3824
|
+
if (this._viewportWidth === 0 || this._viewportHeight === 0)
|
|
3825
|
+
return;
|
|
3826
|
+
const bounds = this.getLocalBounds();
|
|
3827
|
+
const contentW = bounds.width * this.scale.x;
|
|
3828
|
+
const contentH = bounds.height * this.scale.y;
|
|
3829
|
+
const vw = this._viewportWidth;
|
|
3830
|
+
const vh = this._viewportHeight;
|
|
3831
|
+
let anchorX = 0;
|
|
3832
|
+
let anchorY = 0;
|
|
3833
|
+
if (anchor.includes('left')) {
|
|
3834
|
+
anchorX = 0;
|
|
3835
|
+
}
|
|
3836
|
+
else if (anchor.includes('right')) {
|
|
3837
|
+
anchorX = vw - contentW;
|
|
3838
|
+
}
|
|
3839
|
+
else {
|
|
3840
|
+
anchorX = (vw - contentW) / 2;
|
|
3841
|
+
}
|
|
3842
|
+
if (anchor.startsWith('top')) {
|
|
3843
|
+
anchorY = 0;
|
|
3844
|
+
}
|
|
3845
|
+
else if (anchor.startsWith('bottom')) {
|
|
3846
|
+
anchorY = vh - contentH;
|
|
3847
|
+
}
|
|
3848
|
+
else {
|
|
3849
|
+
anchorY = (vh - contentH) / 2;
|
|
3850
|
+
}
|
|
3851
|
+
this.x = anchorX - bounds.x * this.scale.x;
|
|
3852
|
+
this.y = anchorY - bounds.y * this.scale.y;
|
|
3853
|
+
}
|
|
3854
|
+
resolveConfig() {
|
|
3855
|
+
if (this._breakpoints.length === 0 || this._viewportWidth === 0) {
|
|
3856
|
+
return {};
|
|
3857
|
+
}
|
|
3858
|
+
for (const [maxWidth, overrides] of this._breakpoints) {
|
|
3859
|
+
if (this._viewportWidth <= maxWidth) {
|
|
3860
|
+
return overrides;
|
|
3861
|
+
}
|
|
3862
|
+
}
|
|
3863
|
+
return {};
|
|
3864
|
+
}
|
|
3865
|
+
}
|
|
3866
|
+
|
|
3867
|
+
const DIRECTION_MAP = {
|
|
3868
|
+
vertical: 'vertical',
|
|
3869
|
+
horizontal: 'horizontal',
|
|
3870
|
+
both: 'bidirectional',
|
|
3871
|
+
};
|
|
3872
|
+
/**
|
|
3873
|
+
* Scrollable container powered by `@pixi/ui` ScrollBox.
|
|
3874
|
+
*
|
|
3875
|
+
* Provides touch/drag scrolling, mouse wheel support, inertia, and
|
|
3876
|
+
* dynamic rendering optimization for off-screen items.
|
|
3877
|
+
*
|
|
3878
|
+
* @example
|
|
3879
|
+
* ```ts
|
|
3880
|
+
* const scroll = new ScrollContainer({
|
|
3881
|
+
* width: 600,
|
|
3882
|
+
* height: 400,
|
|
3883
|
+
* direction: 'vertical',
|
|
3884
|
+
* elementsMargin: 8,
|
|
3885
|
+
* });
|
|
3886
|
+
*
|
|
3887
|
+
* for (let i = 0; i < 50; i++) {
|
|
3888
|
+
* scroll.addItem(createRow(i));
|
|
3889
|
+
* }
|
|
3890
|
+
*
|
|
3891
|
+
* scene.container.addChild(scroll);
|
|
3892
|
+
* ```
|
|
3893
|
+
*/
|
|
3894
|
+
class ScrollContainer extends ScrollBox {
|
|
3895
|
+
_scrollConfig;
|
|
3896
|
+
constructor(config) {
|
|
3897
|
+
const options = {
|
|
3898
|
+
width: config.width,
|
|
3899
|
+
height: config.height,
|
|
3900
|
+
type: DIRECTION_MAP[config.direction ?? 'vertical'],
|
|
3901
|
+
radius: config.borderRadius ?? 0,
|
|
3902
|
+
elementsMargin: config.elementsMargin ?? 0,
|
|
3903
|
+
padding: config.padding ?? 0,
|
|
3904
|
+
disableDynamicRendering: config.disableDynamicRendering ?? false,
|
|
3905
|
+
disableEasing: config.disableEasing ?? false,
|
|
3906
|
+
globalScroll: config.globalScroll ?? true,
|
|
3907
|
+
};
|
|
3908
|
+
if (config.backgroundColor !== undefined) {
|
|
3909
|
+
options.background = config.backgroundColor;
|
|
3910
|
+
}
|
|
3911
|
+
super(options);
|
|
3912
|
+
this._scrollConfig = config;
|
|
3913
|
+
}
|
|
3914
|
+
/** Set scrollable content. Replaces any existing content. */
|
|
3915
|
+
setContent(content) {
|
|
3916
|
+
// Remove existing items
|
|
3917
|
+
const existing = this.items;
|
|
3918
|
+
if (existing.length > 0) {
|
|
3919
|
+
for (let i = existing.length - 1; i >= 0; i--) {
|
|
3920
|
+
this.removeItem(i);
|
|
3921
|
+
}
|
|
3922
|
+
}
|
|
3923
|
+
// Add all children from the content container
|
|
3924
|
+
const children = [...content.children];
|
|
3925
|
+
if (children.length > 0) {
|
|
3926
|
+
this.addItems(children);
|
|
3927
|
+
}
|
|
3928
|
+
}
|
|
3929
|
+
/** Add a single item */
|
|
3930
|
+
addItem(...items) {
|
|
3931
|
+
this.addItems(items);
|
|
3932
|
+
return items[0];
|
|
3933
|
+
}
|
|
3934
|
+
/** Scroll to make a specific item/child visible */
|
|
3935
|
+
scrollToItem(index) {
|
|
3936
|
+
this.scrollTo(index);
|
|
3937
|
+
}
|
|
3938
|
+
/** Current scroll position */
|
|
3939
|
+
get scrollPosition() {
|
|
3940
|
+
return { x: this.scrollX, y: this.scrollY };
|
|
3941
|
+
}
|
|
3942
|
+
}
|
|
3943
|
+
|
|
3316
3944
|
const DEFAULT_CONFIG = {
|
|
3317
3945
|
balance: 10000,
|
|
3318
3946
|
currency: 'USD',
|
|
@@ -3332,8 +3960,9 @@ const DEFAULT_CONFIG = {
|
|
|
3332
3960
|
/**
|
|
3333
3961
|
* Mock host bridge for local development.
|
|
3334
3962
|
*
|
|
3335
|
-
*
|
|
3336
|
-
*
|
|
3963
|
+
* Uses the SDK's `Bridge` class in `devMode` to communicate with
|
|
3964
|
+
* `CasinoGameSDK` via a shared in-memory `MemoryChannel`, removing
|
|
3965
|
+
* the need for postMessage and iframes.
|
|
3337
3966
|
*
|
|
3338
3967
|
* This allows games to be developed and tested without a real backend.
|
|
3339
3968
|
*
|
|
@@ -3361,8 +3990,7 @@ class DevBridge {
|
|
|
3361
3990
|
_config;
|
|
3362
3991
|
_balance;
|
|
3363
3992
|
_roundCounter = 0;
|
|
3364
|
-
|
|
3365
|
-
_handler = null;
|
|
3993
|
+
_bridge = null;
|
|
3366
3994
|
constructor(config = {}) {
|
|
3367
3995
|
this._config = { ...DEFAULT_CONFIG, ...config };
|
|
3368
3996
|
this._balance = this._config.balance;
|
|
@@ -3373,24 +4001,38 @@ class DevBridge {
|
|
|
3373
4001
|
}
|
|
3374
4002
|
/** Start listening for SDK messages */
|
|
3375
4003
|
start() {
|
|
3376
|
-
if (this.
|
|
4004
|
+
if (this._bridge)
|
|
3377
4005
|
return;
|
|
3378
|
-
|
|
3379
|
-
|
|
3380
|
-
|
|
3381
|
-
|
|
3382
|
-
|
|
4006
|
+
console.debug('[DevBridge] Starting with config:', this._config);
|
|
4007
|
+
this._bridge = new Bridge({ devMode: true, debug: this._config.debug });
|
|
4008
|
+
this._bridge.on('GAME_READY', (_payload, id) => {
|
|
4009
|
+
this.handleGameReady(id);
|
|
4010
|
+
});
|
|
4011
|
+
this._bridge.on('PLAY_REQUEST', (payload, id) => {
|
|
4012
|
+
this.handlePlayRequest(payload, id);
|
|
4013
|
+
});
|
|
4014
|
+
this._bridge.on('PLAY_RESULT_ACK', (payload) => {
|
|
4015
|
+
this.handlePlayAck(payload);
|
|
4016
|
+
});
|
|
4017
|
+
this._bridge.on('GET_BALANCE', (_payload, id) => {
|
|
4018
|
+
this.handleGetBalance(id);
|
|
4019
|
+
});
|
|
4020
|
+
this._bridge.on('GET_STATE', (_payload, id) => {
|
|
4021
|
+
this.handleGetState(id);
|
|
4022
|
+
});
|
|
4023
|
+
this._bridge.on('OPEN_DEPOSIT', () => {
|
|
4024
|
+
this.handleOpenDeposit();
|
|
4025
|
+
});
|
|
3383
4026
|
if (this._config.debug) {
|
|
3384
|
-
console.log('[DevBridge] Started — listening
|
|
4027
|
+
console.log('[DevBridge] Started — listening via Bridge (devMode)');
|
|
3385
4028
|
}
|
|
3386
4029
|
}
|
|
3387
4030
|
/** Stop listening */
|
|
3388
4031
|
stop() {
|
|
3389
|
-
if (this.
|
|
3390
|
-
|
|
3391
|
-
this.
|
|
4032
|
+
if (this._bridge) {
|
|
4033
|
+
this._bridge.destroy();
|
|
4034
|
+
this._bridge = null;
|
|
3392
4035
|
}
|
|
3393
|
-
this._listening = false;
|
|
3394
4036
|
if (this._config.debug) {
|
|
3395
4037
|
console.log('[DevBridge] Stopped');
|
|
3396
4038
|
}
|
|
@@ -3398,47 +4040,13 @@ class DevBridge {
|
|
|
3398
4040
|
/** Set mock balance */
|
|
3399
4041
|
setBalance(balance) {
|
|
3400
4042
|
this._balance = balance;
|
|
3401
|
-
|
|
3402
|
-
this.sendMessage('BALANCE_UPDATE', { balance: this._balance });
|
|
4043
|
+
this._bridge?.send('BALANCE_UPDATE', { balance: this._balance });
|
|
3403
4044
|
}
|
|
3404
4045
|
/** Destroy the dev bridge */
|
|
3405
4046
|
destroy() {
|
|
3406
4047
|
this.stop();
|
|
3407
4048
|
}
|
|
3408
4049
|
// ─── Message Handling ──────────────────────────────────
|
|
3409
|
-
handleMessage(e) {
|
|
3410
|
-
const data = e.data;
|
|
3411
|
-
// Only process bridge messages
|
|
3412
|
-
if (!data || data.__casino_bridge !== true)
|
|
3413
|
-
return;
|
|
3414
|
-
if (this._config.debug) {
|
|
3415
|
-
console.log('[DevBridge] ←', data.type, data.payload);
|
|
3416
|
-
}
|
|
3417
|
-
switch (data.type) {
|
|
3418
|
-
case 'GAME_READY':
|
|
3419
|
-
this.handleGameReady(data.id);
|
|
3420
|
-
break;
|
|
3421
|
-
case 'PLAY_REQUEST':
|
|
3422
|
-
this.handlePlayRequest(data.payload, data.id);
|
|
3423
|
-
break;
|
|
3424
|
-
case 'PLAY_RESULT_ACK':
|
|
3425
|
-
this.handlePlayAck(data.payload, data.id);
|
|
3426
|
-
break;
|
|
3427
|
-
case 'GET_BALANCE':
|
|
3428
|
-
this.handleGetBalance(data.id);
|
|
3429
|
-
break;
|
|
3430
|
-
case 'GET_STATE':
|
|
3431
|
-
this.handleGetState(data.id);
|
|
3432
|
-
break;
|
|
3433
|
-
case 'OPEN_DEPOSIT':
|
|
3434
|
-
this.handleOpenDeposit();
|
|
3435
|
-
break;
|
|
3436
|
-
default:
|
|
3437
|
-
if (this._config.debug) {
|
|
3438
|
-
console.log('[DevBridge] Unknown message type:', data.type);
|
|
3439
|
-
}
|
|
3440
|
-
}
|
|
3441
|
-
}
|
|
3442
4050
|
handleGameReady(id) {
|
|
3443
4051
|
const initData = {
|
|
3444
4052
|
balance: this._balance,
|
|
@@ -3471,13 +4079,13 @@ class DevBridge {
|
|
|
3471
4079
|
};
|
|
3472
4080
|
this.delayedSend('PLAY_RESULT', result, id);
|
|
3473
4081
|
}
|
|
3474
|
-
handlePlayAck(_payload
|
|
4082
|
+
handlePlayAck(_payload) {
|
|
3475
4083
|
if (this._config.debug) {
|
|
3476
4084
|
console.log('[DevBridge] Play acknowledged');
|
|
3477
4085
|
}
|
|
3478
4086
|
}
|
|
3479
4087
|
handleGetBalance(id) {
|
|
3480
|
-
this.delayedSend('
|
|
4088
|
+
this.delayedSend('BALANCE_UPDATE', { balance: this._balance }, id);
|
|
3481
4089
|
}
|
|
3482
4090
|
handleGetState(id) {
|
|
3483
4091
|
this.delayedSend('STATE_RESPONSE', this._config.session, id);
|
|
@@ -3487,122 +4095,19 @@ class DevBridge {
|
|
|
3487
4095
|
console.log('[DevBridge] 💰 Open deposit requested (mock: adding 1000)');
|
|
3488
4096
|
}
|
|
3489
4097
|
this._balance += 1000;
|
|
3490
|
-
this.
|
|
4098
|
+
this._bridge?.send('BALANCE_UPDATE', { balance: this._balance });
|
|
3491
4099
|
}
|
|
3492
4100
|
// ─── Communication ─────────────────────────────────────
|
|
3493
4101
|
delayedSend(type, payload, id) {
|
|
3494
4102
|
const delay = this._config.networkDelay;
|
|
3495
4103
|
if (delay > 0) {
|
|
3496
|
-
setTimeout(() => this.
|
|
3497
|
-
}
|
|
3498
|
-
else {
|
|
3499
|
-
this.sendMessage(type, payload, id);
|
|
3500
|
-
}
|
|
3501
|
-
}
|
|
3502
|
-
sendMessage(type, payload, id) {
|
|
3503
|
-
const message = {
|
|
3504
|
-
__casino_bridge: true,
|
|
3505
|
-
type,
|
|
3506
|
-
payload,
|
|
3507
|
-
id,
|
|
3508
|
-
};
|
|
3509
|
-
if (this._config.debug) {
|
|
3510
|
-
console.log('[DevBridge] →', type, payload);
|
|
3511
|
-
}
|
|
3512
|
-
// Post to the same window (SDK listens on window)
|
|
3513
|
-
window.postMessage(message, '*');
|
|
3514
|
-
}
|
|
3515
|
-
}
|
|
3516
|
-
|
|
3517
|
-
/**
|
|
3518
|
-
* FPS overlay for debugging performance.
|
|
3519
|
-
*
|
|
3520
|
-
* Shows FPS, frame time, and draw call count in the corner of the screen.
|
|
3521
|
-
*
|
|
3522
|
-
* @example
|
|
3523
|
-
* ```ts
|
|
3524
|
-
* const fps = new FPSOverlay(app);
|
|
3525
|
-
* fps.show();
|
|
3526
|
-
* ```
|
|
3527
|
-
*/
|
|
3528
|
-
class FPSOverlay {
|
|
3529
|
-
_app;
|
|
3530
|
-
_container;
|
|
3531
|
-
_fpsText;
|
|
3532
|
-
_visible = false;
|
|
3533
|
-
_samples = [];
|
|
3534
|
-
_maxSamples = 60;
|
|
3535
|
-
_lastUpdate = 0;
|
|
3536
|
-
_tickFn = null;
|
|
3537
|
-
constructor(app) {
|
|
3538
|
-
this._app = app;
|
|
3539
|
-
this._container = new Container();
|
|
3540
|
-
this._container.label = 'FPSOverlay';
|
|
3541
|
-
this._container.zIndex = 99999;
|
|
3542
|
-
this._fpsText = new Text({
|
|
3543
|
-
text: 'FPS: --',
|
|
3544
|
-
style: {
|
|
3545
|
-
fontFamily: 'monospace',
|
|
3546
|
-
fontSize: 14,
|
|
3547
|
-
fill: 0x00ff00,
|
|
3548
|
-
stroke: { color: 0x000000, width: 2 },
|
|
3549
|
-
},
|
|
3550
|
-
});
|
|
3551
|
-
this._fpsText.x = 8;
|
|
3552
|
-
this._fpsText.y = 8;
|
|
3553
|
-
this._container.addChild(this._fpsText);
|
|
3554
|
-
}
|
|
3555
|
-
/** Show the FPS overlay */
|
|
3556
|
-
show() {
|
|
3557
|
-
if (this._visible)
|
|
3558
|
-
return;
|
|
3559
|
-
this._visible = true;
|
|
3560
|
-
this._app.stage.addChild(this._container);
|
|
3561
|
-
this._tickFn = (ticker) => {
|
|
3562
|
-
this._samples.push(ticker.FPS);
|
|
3563
|
-
if (this._samples.length > this._maxSamples) {
|
|
3564
|
-
this._samples.shift();
|
|
3565
|
-
}
|
|
3566
|
-
// Update display every ~500ms
|
|
3567
|
-
const now = Date.now();
|
|
3568
|
-
if (now - this._lastUpdate > 500) {
|
|
3569
|
-
const avg = this._samples.reduce((a, b) => a + b, 0) / this._samples.length;
|
|
3570
|
-
const min = Math.min(...this._samples);
|
|
3571
|
-
this._fpsText.text = [
|
|
3572
|
-
`FPS: ${Math.round(avg)} (min: ${Math.round(min)})`,
|
|
3573
|
-
`Frame: ${ticker.deltaMS.toFixed(1)}ms`,
|
|
3574
|
-
].join('\n');
|
|
3575
|
-
this._lastUpdate = now;
|
|
3576
|
-
}
|
|
3577
|
-
};
|
|
3578
|
-
this._app.ticker.add(this._tickFn);
|
|
3579
|
-
}
|
|
3580
|
-
/** Hide the FPS overlay */
|
|
3581
|
-
hide() {
|
|
3582
|
-
if (!this._visible)
|
|
3583
|
-
return;
|
|
3584
|
-
this._visible = false;
|
|
3585
|
-
this._container.removeFromParent();
|
|
3586
|
-
if (this._tickFn) {
|
|
3587
|
-
this._app.ticker.remove(this._tickFn);
|
|
3588
|
-
this._tickFn = null;
|
|
3589
|
-
}
|
|
3590
|
-
}
|
|
3591
|
-
/** Toggle visibility */
|
|
3592
|
-
toggle() {
|
|
3593
|
-
if (this._visible) {
|
|
3594
|
-
this.hide();
|
|
4104
|
+
setTimeout(() => this._bridge?.send(type, payload, id), delay);
|
|
3595
4105
|
}
|
|
3596
4106
|
else {
|
|
3597
|
-
this.
|
|
4107
|
+
this._bridge?.send(type, payload, id);
|
|
3598
4108
|
}
|
|
3599
4109
|
}
|
|
3600
|
-
/** Destroy the overlay */
|
|
3601
|
-
destroy() {
|
|
3602
|
-
this.hide();
|
|
3603
|
-
this._container.destroy({ children: true });
|
|
3604
|
-
}
|
|
3605
4110
|
}
|
|
3606
4111
|
|
|
3607
|
-
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 };
|
|
4112
|
+
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 };
|
|
3608
4113
|
//# sourceMappingURL=index.esm.js.map
|