@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.cjs.js
CHANGED
|
@@ -2,6 +2,9 @@
|
|
|
2
2
|
|
|
3
3
|
var pixi_js = require('pixi.js');
|
|
4
4
|
var gameSdk = require('@energy8platform/game-sdk');
|
|
5
|
+
require('@pixi/layout');
|
|
6
|
+
var ui = require('@pixi/ui');
|
|
7
|
+
var components = require('@pixi/layout/components');
|
|
5
8
|
|
|
6
9
|
// ─── Scale Modes ───────────────────────────────────────────
|
|
7
10
|
exports.ScaleMode = void 0;
|
|
@@ -32,6 +35,9 @@ exports.TransitionType = void 0;
|
|
|
32
35
|
/**
|
|
33
36
|
* Minimal typed event emitter.
|
|
34
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')`.
|
|
35
41
|
*/
|
|
36
42
|
// eslint-disable-next-line @typescript-eslint/no-empty-object-type
|
|
37
43
|
class EventEmitter {
|
|
@@ -54,7 +60,8 @@ class EventEmitter {
|
|
|
54
60
|
this.listeners.get(event)?.delete(handler);
|
|
55
61
|
return this;
|
|
56
62
|
}
|
|
57
|
-
emit(
|
|
63
|
+
emit(...args) {
|
|
64
|
+
const [event, data] = args;
|
|
58
65
|
const handlers = this.listeners.get(event);
|
|
59
66
|
if (handlers) {
|
|
60
67
|
for (const handler of handlers) {
|
|
@@ -227,9 +234,20 @@ class Tween {
|
|
|
227
234
|
}
|
|
228
235
|
/**
|
|
229
236
|
* Wait for a given duration (useful in timelines).
|
|
237
|
+
* Uses PixiJS Ticker for consistent timing with other tweens.
|
|
230
238
|
*/
|
|
231
239
|
static delay(ms) {
|
|
232
|
-
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
|
+
pixi_js.Ticker.shared.remove(onTick);
|
|
246
|
+
resolve();
|
|
247
|
+
}
|
|
248
|
+
};
|
|
249
|
+
pixi_js.Ticker.shared.add(onTick);
|
|
250
|
+
});
|
|
233
251
|
}
|
|
234
252
|
/**
|
|
235
253
|
* Kill all tweens on a target.
|
|
@@ -256,6 +274,20 @@ class Tween {
|
|
|
256
274
|
static get activeTweens() {
|
|
257
275
|
return Tween._tweens.length;
|
|
258
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
|
+
pixi_js.Ticker.shared.remove(Tween.tick);
|
|
288
|
+
Tween._tickerAdded = false;
|
|
289
|
+
}
|
|
290
|
+
}
|
|
259
291
|
// ─── Internal ──────────────────────────────────────────
|
|
260
292
|
static ensureTicker() {
|
|
261
293
|
if (Tween._tickerAdded)
|
|
@@ -458,8 +490,9 @@ class SceneManager extends EventEmitter {
|
|
|
458
490
|
}
|
|
459
491
|
// Transition in
|
|
460
492
|
await this.transitionIn(scene.container, transition);
|
|
461
|
-
|
|
493
|
+
// Push to stack BEFORE onEnter so currentKey is correct during initialization
|
|
462
494
|
this.stack.push({ scene, key });
|
|
495
|
+
await scene.onEnter?.(data);
|
|
463
496
|
this._transitioning = false;
|
|
464
497
|
}
|
|
465
498
|
async popInternal(showTransition, transition) {
|
|
@@ -758,26 +791,51 @@ class AudioManager {
|
|
|
758
791
|
if (!this._initialized || !this._soundModule)
|
|
759
792
|
return;
|
|
760
793
|
const { sound } = this._soundModule;
|
|
761
|
-
// Stop current music
|
|
762
|
-
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
|
|
763
808
|
try {
|
|
764
|
-
sound.
|
|
809
|
+
sound.play(alias, {
|
|
810
|
+
volume: 0,
|
|
811
|
+
loop: true,
|
|
812
|
+
});
|
|
813
|
+
this.fadeVolume(alias, 0, this._categories.music.volume, fadeDuration);
|
|
765
814
|
}
|
|
766
|
-
catch {
|
|
767
|
-
|
|
815
|
+
catch (e) {
|
|
816
|
+
console.warn(`[AudioManager] Failed to play music "${alias}":`, e);
|
|
768
817
|
}
|
|
769
818
|
}
|
|
770
|
-
|
|
771
|
-
|
|
772
|
-
|
|
773
|
-
|
|
774
|
-
|
|
775
|
-
|
|
776
|
-
|
|
777
|
-
}
|
|
778
|
-
|
|
779
|
-
|
|
780
|
-
|
|
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
|
+
}
|
|
781
839
|
}
|
|
782
840
|
}
|
|
783
841
|
/**
|
|
@@ -919,6 +977,31 @@ class AudioManager {
|
|
|
919
977
|
this._initialized = false;
|
|
920
978
|
}
|
|
921
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
|
+
}
|
|
922
1005
|
applyVolumes() {
|
|
923
1006
|
if (!this._soundModule)
|
|
924
1007
|
return;
|
|
@@ -1023,6 +1106,10 @@ class InputManager extends EventEmitter {
|
|
|
1023
1106
|
_locked = false;
|
|
1024
1107
|
_keysDown = new Set();
|
|
1025
1108
|
_destroyed = false;
|
|
1109
|
+
// Viewport transform (set by ViewportManager via setViewportTransform)
|
|
1110
|
+
_viewportScale = 1;
|
|
1111
|
+
_viewportOffsetX = 0;
|
|
1112
|
+
_viewportOffsetY = 0;
|
|
1026
1113
|
// Gesture tracking
|
|
1027
1114
|
_pointerStart = null;
|
|
1028
1115
|
_swipeThreshold = 50; // minimum distance in px
|
|
@@ -1049,6 +1136,25 @@ class InputManager extends EventEmitter {
|
|
|
1049
1136
|
isKeyDown(key) {
|
|
1050
1137
|
return this._keysDown.has(key.toLowerCase());
|
|
1051
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
|
+
}
|
|
1052
1158
|
/** Destroy the input manager */
|
|
1053
1159
|
destroy() {
|
|
1054
1160
|
this._destroyed = true;
|
|
@@ -1304,6 +1410,8 @@ class ViewportManager extends EventEmitter {
|
|
|
1304
1410
|
this._destroyed = true;
|
|
1305
1411
|
this._resizeObserver?.disconnect();
|
|
1306
1412
|
this._resizeObserver = null;
|
|
1413
|
+
// Remove fallback window resize listener if it was used
|
|
1414
|
+
window.removeEventListener('resize', this.onWindowResize);
|
|
1307
1415
|
if (this._resizeTimeout !== null) {
|
|
1308
1416
|
clearTimeout(this._resizeTimeout);
|
|
1309
1417
|
}
|
|
@@ -1367,45 +1475,87 @@ class Scene {
|
|
|
1367
1475
|
}
|
|
1368
1476
|
|
|
1369
1477
|
/**
|
|
1370
|
-
*
|
|
1371
|
-
*
|
|
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.
|
|
1372
1484
|
*/
|
|
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">
|
|
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">
|
|
1387
1495
|
<stop stop-color="#663BA6"/><stop stop-color="#7939C2" offset=".349"/><stop stop-color="#8A2FC0" offset=".6615"/><stop stop-color="#791BA3" offset="1"/>
|
|
1388
1496
|
</linearGradient>
|
|
1389
|
-
<linearGradient id="
|
|
1497
|
+
<linearGradient id="GID1" x1="194.6" x2="194.6" y1="81.75" y2="138.3" gradientUnits="userSpaceOnUse">
|
|
1390
1498
|
<stop stop-color="#663BA6"/><stop stop-color="#7939C2" offset=".349"/><stop stop-color="#8A2FC0" offset=".6615"/><stop stop-color="#791BA3" offset="1"/>
|
|
1391
1499
|
</linearGradient>
|
|
1392
|
-
<linearGradient id="
|
|
1500
|
+
<linearGradient id="GID2" x1="157.8" x2="157.8" y1="81.75" y2="127" gradientUnits="userSpaceOnUse">
|
|
1393
1501
|
<stop stop-color="#663BA6"/><stop stop-color="#7939C2" offset=".349"/><stop stop-color="#8A2FC0" offset=".6615"/><stop stop-color="#791BA3" offset="1"/>
|
|
1394
1502
|
</linearGradient>
|
|
1395
|
-
<linearGradient id="
|
|
1503
|
+
<linearGradient id="GID3" x1="79.96" x2="79.96" y1="81.75" y2="141.8" gradientUnits="userSpaceOnUse">
|
|
1396
1504
|
<stop stop-color="#663BA6"/><stop stop-color="#7939C2" offset=".349"/><stop stop-color="#8A2FC0" offset=".6615"/><stop stop-color="#791BA3" offset="1"/>
|
|
1397
1505
|
</linearGradient>
|
|
1398
|
-
<linearGradient id="
|
|
1506
|
+
<linearGradient id="GID4" x1="36.18" x2="212.5" y1="156.6" y2="156.6" gradientUnits="userSpaceOnUse">
|
|
1399
1507
|
<stop stop-color="#316FB0"/><stop stop-color="#1FCDE6" offset=".5"/><stop stop-color="#29FEE7" offset="1"/>
|
|
1400
1508
|
</linearGradient>
|
|
1401
|
-
<linearGradient id="
|
|
1509
|
+
<linearGradient id="GID5" x1="40.27" x2="208.2" y1="156.4" y2="156.4" gradientUnits="userSpaceOnUse">
|
|
1402
1510
|
<stop stop-color="#316FB0"/><stop stop-color="#1FCDE6" offset=".5"/><stop stop-color="#29FEE7" offset="1"/>
|
|
1403
|
-
</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}
|
|
1404
1542
|
</defs>
|
|
1405
1543
|
</svg>`;
|
|
1406
1544
|
}
|
|
1407
|
-
|
|
1408
|
-
|
|
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
|
+
}
|
|
1409
1559
|
/**
|
|
1410
1560
|
* Built-in loading screen using the Energy8 SVG logo with animated loader bar.
|
|
1411
1561
|
*
|
|
@@ -1515,7 +1665,7 @@ class LoadingScene extends Scene {
|
|
|
1515
1665
|
this._overlay.id = '__ge-loading-overlay__';
|
|
1516
1666
|
this._overlay.innerHTML = `
|
|
1517
1667
|
<div class="ge-loading-content">
|
|
1518
|
-
${
|
|
1668
|
+
${buildLoadingLogoSVG()}
|
|
1519
1669
|
</div>
|
|
1520
1670
|
`;
|
|
1521
1671
|
const style = document.createElement('style');
|
|
@@ -1653,8 +1803,11 @@ class LoadingScene extends Scene {
|
|
|
1653
1803
|
}
|
|
1654
1804
|
// Remove overlay
|
|
1655
1805
|
this.removeOverlay();
|
|
1656
|
-
// Navigate to the target scene
|
|
1657
|
-
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
|
+
});
|
|
1658
1811
|
}
|
|
1659
1812
|
}
|
|
1660
1813
|
|
|
@@ -1663,39 +1816,12 @@ const PRELOADER_ID = '__ge-css-preloader__';
|
|
|
1663
1816
|
* Inline SVG logo with animated loader bar.
|
|
1664
1817
|
* The `#loader` path acts as the progress fill — animated via clipPath.
|
|
1665
1818
|
*/
|
|
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>`;
|
|
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
|
+
});
|
|
1699
1825
|
/**
|
|
1700
1826
|
* Creates a lightweight CSS-only preloader that appears instantly,
|
|
1701
1827
|
* BEFORE PixiJS/WebGL is initialized.
|
|
@@ -1799,6 +1925,96 @@ function removeCSSPreloader(container) {
|
|
|
1799
1925
|
});
|
|
1800
1926
|
}
|
|
1801
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 pixi_js.Container();
|
|
1951
|
+
this._container.label = 'FPSOverlay';
|
|
1952
|
+
this._container.zIndex = 99999;
|
|
1953
|
+
this._fpsText = new pixi_js.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
|
+
|
|
1802
2018
|
/**
|
|
1803
2019
|
* The main entry point for a game built on @energy8platform/game-engine.
|
|
1804
2020
|
*
|
|
@@ -1846,6 +2062,8 @@ class GameApplication extends EventEmitter {
|
|
|
1846
2062
|
viewport;
|
|
1847
2063
|
/** SDK instance (null in offline mode) */
|
|
1848
2064
|
sdk = null;
|
|
2065
|
+
/** FPS overlay instance (only when debug: true) */
|
|
2066
|
+
fpsOverlay = null;
|
|
1849
2067
|
/** Data received from SDK initialization */
|
|
1850
2068
|
initData = null;
|
|
1851
2069
|
/** Configuration */
|
|
@@ -1913,15 +2131,15 @@ class GameApplication extends EventEmitter {
|
|
|
1913
2131
|
this.applySDKConfig();
|
|
1914
2132
|
// 6. Initialize sub-systems
|
|
1915
2133
|
this.initSubSystems();
|
|
1916
|
-
this.emit('initialized'
|
|
2134
|
+
this.emit('initialized');
|
|
1917
2135
|
// 7. Remove CSS preloader, show Canvas loading screen
|
|
1918
2136
|
removeCSSPreloader(this._container);
|
|
1919
2137
|
// 8. Load assets with loading screen
|
|
1920
2138
|
await this.loadAssets(firstScene, sceneData);
|
|
1921
|
-
this.emit('loaded'
|
|
2139
|
+
this.emit('loaded');
|
|
1922
2140
|
// 9. Start the game loop
|
|
1923
2141
|
this._running = true;
|
|
1924
|
-
this.emit('started'
|
|
2142
|
+
this.emit('started');
|
|
1925
2143
|
}
|
|
1926
2144
|
catch (err) {
|
|
1927
2145
|
console.error('[GameEngine] Failed to start:', err);
|
|
@@ -1943,7 +2161,7 @@ class GameApplication extends EventEmitter {
|
|
|
1943
2161
|
this.viewport?.destroy();
|
|
1944
2162
|
this.sdk?.destroy();
|
|
1945
2163
|
this.app?.destroy(true, { children: true, texture: true });
|
|
1946
|
-
this.emit('destroyed'
|
|
2164
|
+
this.emit('destroyed');
|
|
1947
2165
|
this.removeAllListeners();
|
|
1948
2166
|
}
|
|
1949
2167
|
// ─── Private initialization steps ──────────────────────
|
|
@@ -1959,6 +2177,7 @@ class GameApplication extends EventEmitter {
|
|
|
1959
2177
|
async initPixi() {
|
|
1960
2178
|
this.app = new pixi_js.Application();
|
|
1961
2179
|
const pixiOpts = {
|
|
2180
|
+
preference: 'webgl',
|
|
1962
2181
|
background: typeof this.config.loading?.backgroundColor === 'number'
|
|
1963
2182
|
? this.config.loading.backgroundColor
|
|
1964
2183
|
: 0x000000,
|
|
@@ -2017,9 +2236,10 @@ class GameApplication extends EventEmitter {
|
|
|
2017
2236
|
});
|
|
2018
2237
|
// Wire SceneManager to the PixiJS stage
|
|
2019
2238
|
this.scenes.setRoot(this.app.stage);
|
|
2020
|
-
// Wire viewport resize → scene manager
|
|
2021
|
-
this.viewport.on('resize', ({ width, height }) => {
|
|
2239
|
+
// Wire viewport resize → scene manager + input manager
|
|
2240
|
+
this.viewport.on('resize', ({ width, height, scale }) => {
|
|
2022
2241
|
this.scenes.resize(width, height);
|
|
2242
|
+
this.input.setViewportTransform(scale, this.app.stage.x, this.app.stage.y);
|
|
2023
2243
|
this.emit('resize', { width, height });
|
|
2024
2244
|
});
|
|
2025
2245
|
this.viewport.on('orientationChange', (orientation) => {
|
|
@@ -2036,6 +2256,11 @@ class GameApplication extends EventEmitter {
|
|
|
2036
2256
|
});
|
|
2037
2257
|
// Trigger initial resize
|
|
2038
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
|
+
}
|
|
2039
2264
|
}
|
|
2040
2265
|
async loadAssets(firstScene, sceneData) {
|
|
2041
2266
|
// Register built-in loading scene
|
|
@@ -2495,162 +2720,277 @@ class SpineHelper {
|
|
|
2495
2720
|
}
|
|
2496
2721
|
}
|
|
2497
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 pixi_js.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 = pixi_js.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
|
+
|
|
2498
2887
|
const DEFAULT_COLORS = {
|
|
2499
|
-
|
|
2888
|
+
default: 0xffd700,
|
|
2500
2889
|
hover: 0xffe44d,
|
|
2501
2890
|
pressed: 0xccac00,
|
|
2502
2891
|
disabled: 0x666666,
|
|
2503
2892
|
};
|
|
2893
|
+
function makeGraphicsView(w, h, radius, color) {
|
|
2894
|
+
const g = new pixi_js.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
|
+
}
|
|
2504
2900
|
/**
|
|
2505
|
-
* Interactive button component
|
|
2901
|
+
* Interactive button component powered by `@pixi/ui` FancyButton.
|
|
2506
2902
|
*
|
|
2507
|
-
* 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.
|
|
2508
2905
|
*
|
|
2509
2906
|
* @example
|
|
2510
2907
|
* ```ts
|
|
2511
2908
|
* const btn = new Button({
|
|
2512
2909
|
* width: 200, height: 60, borderRadius: 12,
|
|
2513
|
-
* colors: {
|
|
2910
|
+
* colors: { default: 0x22aa22, hover: 0x33cc33 },
|
|
2911
|
+
* text: 'SPIN',
|
|
2514
2912
|
* });
|
|
2515
2913
|
*
|
|
2516
|
-
* btn.
|
|
2914
|
+
* btn.onPress.connect(() => console.log('Clicked!'));
|
|
2517
2915
|
* scene.container.addChild(btn);
|
|
2518
2916
|
* ```
|
|
2519
2917
|
*/
|
|
2520
|
-
class Button extends
|
|
2521
|
-
|
|
2522
|
-
_bg;
|
|
2523
|
-
_sprites = {};
|
|
2524
|
-
_config;
|
|
2525
|
-
/** Called when the button is tapped/clicked */
|
|
2526
|
-
onTap;
|
|
2527
|
-
/** Called when the button state changes */
|
|
2528
|
-
onStateChange;
|
|
2918
|
+
class Button extends ui.FancyButton {
|
|
2919
|
+
_buttonConfig;
|
|
2529
2920
|
constructor(config = {}) {
|
|
2530
|
-
|
|
2531
|
-
|
|
2532
|
-
|
|
2533
|
-
|
|
2534
|
-
|
|
2535
|
-
|
|
2536
|
-
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,
|
|
2537
2927
|
...config,
|
|
2538
2928
|
};
|
|
2539
|
-
|
|
2540
|
-
|
|
2541
|
-
|
|
2542
|
-
|
|
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
|
|
2543
2946
|
if (config.textures) {
|
|
2544
|
-
|
|
2545
|
-
|
|
2546
|
-
|
|
2547
|
-
|
|
2548
|
-
|
|
2549
|
-
|
|
2550
|
-
|
|
2551
|
-
|
|
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;
|
|
2552
2955
|
}
|
|
2553
|
-
|
|
2554
|
-
|
|
2555
|
-
|
|
2556
|
-
|
|
2557
|
-
|
|
2558
|
-
|
|
2559
|
-
|
|
2560
|
-
|
|
2561
|
-
|
|
2562
|
-
|
|
2563
|
-
|
|
2564
|
-
|
|
2565
|
-
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;
|
|
2566
2969
|
if (config.disabled) {
|
|
2567
|
-
this.
|
|
2970
|
+
this.enabled = false;
|
|
2568
2971
|
}
|
|
2569
2972
|
}
|
|
2570
|
-
/** Current button state */
|
|
2571
|
-
get state() {
|
|
2572
|
-
return this._state;
|
|
2573
|
-
}
|
|
2574
2973
|
/** Enable the button */
|
|
2575
2974
|
enable() {
|
|
2576
|
-
|
|
2577
|
-
this.setState('normal');
|
|
2578
|
-
this.eventMode = 'static';
|
|
2579
|
-
this.cursor = 'pointer';
|
|
2580
|
-
}
|
|
2975
|
+
this.enabled = true;
|
|
2581
2976
|
}
|
|
2582
2977
|
/** Disable the button */
|
|
2583
2978
|
disable() {
|
|
2584
|
-
this.
|
|
2585
|
-
this.eventMode = 'none';
|
|
2586
|
-
this.cursor = 'default';
|
|
2979
|
+
this.enabled = false;
|
|
2587
2980
|
}
|
|
2588
2981
|
/** Whether the button is disabled */
|
|
2589
2982
|
get disabled() {
|
|
2590
|
-
return this.
|
|
2983
|
+
return !this.enabled;
|
|
2591
2984
|
}
|
|
2592
|
-
setState(state) {
|
|
2593
|
-
if (this._state === state)
|
|
2594
|
-
return;
|
|
2595
|
-
this._state = state;
|
|
2596
|
-
this.render();
|
|
2597
|
-
this.onStateChange?.(state);
|
|
2598
|
-
}
|
|
2599
|
-
render() {
|
|
2600
|
-
const { width, height, borderRadius, colors } = this._config;
|
|
2601
|
-
const colorMap = { ...DEFAULT_COLORS, ...colors };
|
|
2602
|
-
// Update Graphics
|
|
2603
|
-
this._bg.clear();
|
|
2604
|
-
this._bg.roundRect(0, 0, width, height, borderRadius).fill(colorMap[this._state]);
|
|
2605
|
-
// Add highlight for normal/hover
|
|
2606
|
-
if (this._state === 'normal' || this._state === 'hover') {
|
|
2607
|
-
this._bg
|
|
2608
|
-
.roundRect(2, 2, width - 4, height * 0.45, borderRadius)
|
|
2609
|
-
.fill({ color: 0xffffff, alpha: 0.1 });
|
|
2610
|
-
}
|
|
2611
|
-
// Update sprite visibility
|
|
2612
|
-
for (const [state, sprite] of Object.entries(this._sprites)) {
|
|
2613
|
-
if (sprite)
|
|
2614
|
-
sprite.visible = state === this._state;
|
|
2615
|
-
}
|
|
2616
|
-
// Fall back to normal sprite if state sprite doesn't exist
|
|
2617
|
-
if (!this._sprites[this._state] && this._sprites.normal) {
|
|
2618
|
-
this._sprites.normal.visible = true;
|
|
2619
|
-
}
|
|
2620
|
-
}
|
|
2621
|
-
onPointerOver = () => {
|
|
2622
|
-
if (this._state === 'disabled')
|
|
2623
|
-
return;
|
|
2624
|
-
this.setState('hover');
|
|
2625
|
-
};
|
|
2626
|
-
onPointerOut = () => {
|
|
2627
|
-
if (this._state === 'disabled')
|
|
2628
|
-
return;
|
|
2629
|
-
this.setState('normal');
|
|
2630
|
-
Tween.to(this.scale, { x: 1, y: 1 }, this._config.animationDuration);
|
|
2631
|
-
};
|
|
2632
|
-
onPointerDown = () => {
|
|
2633
|
-
if (this._state === 'disabled')
|
|
2634
|
-
return;
|
|
2635
|
-
this.setState('pressed');
|
|
2636
|
-
const s = this._config.pressScale;
|
|
2637
|
-
Tween.to(this.scale, { x: s, y: s }, this._config.animationDuration, Easing.easeOutQuad);
|
|
2638
|
-
};
|
|
2639
|
-
onPointerUp = () => {
|
|
2640
|
-
if (this._state === 'disabled')
|
|
2641
|
-
return;
|
|
2642
|
-
this.setState('hover');
|
|
2643
|
-
Tween.to(this.scale, { x: 1, y: 1 }, this._config.animationDuration, Easing.easeOutBack);
|
|
2644
|
-
};
|
|
2645
|
-
onPointerTap = () => {
|
|
2646
|
-
if (this._state === 'disabled')
|
|
2647
|
-
return;
|
|
2648
|
-
this.onTap?.();
|
|
2649
|
-
};
|
|
2650
2985
|
}
|
|
2651
2986
|
|
|
2987
|
+
function makeBarGraphics(w, h, radius, color) {
|
|
2988
|
+
return new pixi_js.Graphics().roundRect(0, 0, w, h, radius).fill(color);
|
|
2989
|
+
}
|
|
2652
2990
|
/**
|
|
2653
|
-
* Horizontal progress bar
|
|
2991
|
+
* Horizontal progress bar powered by `@pixi/ui` ProgressBar.
|
|
2992
|
+
*
|
|
2993
|
+
* Provides optional smooth animated fill via per-frame `update()`.
|
|
2654
2994
|
*
|
|
2655
2995
|
* @example
|
|
2656
2996
|
* ```ts
|
|
@@ -2660,33 +3000,48 @@ class Button extends pixi_js.Container {
|
|
|
2660
3000
|
* ```
|
|
2661
3001
|
*/
|
|
2662
3002
|
class ProgressBar extends pixi_js.Container {
|
|
2663
|
-
|
|
2664
|
-
|
|
2665
|
-
_border;
|
|
3003
|
+
_bar;
|
|
3004
|
+
_borderGfx;
|
|
2666
3005
|
_config;
|
|
2667
3006
|
_progress = 0;
|
|
2668
3007
|
_displayedProgress = 0;
|
|
2669
3008
|
constructor(config = {}) {
|
|
2670
3009
|
super();
|
|
2671
3010
|
this._config = {
|
|
2672
|
-
width: 300,
|
|
2673
|
-
height: 16,
|
|
2674
|
-
borderRadius: 8,
|
|
2675
|
-
fillColor: 0xffd700,
|
|
2676
|
-
trackColor: 0x333333,
|
|
2677
|
-
borderColor: 0x555555,
|
|
2678
|
-
borderWidth: 1,
|
|
2679
|
-
animated: true,
|
|
2680
|
-
animationSpeed: 0.1,
|
|
2681
|
-
|
|
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,
|
|
2682
3034
|
};
|
|
2683
|
-
this.
|
|
2684
|
-
this.
|
|
2685
|
-
|
|
2686
|
-
this.
|
|
2687
|
-
|
|
2688
|
-
|
|
2689
|
-
|
|
3035
|
+
this._bar = new ui.ProgressBar(options);
|
|
3036
|
+
this.addChild(this._bar);
|
|
3037
|
+
// Border overlay
|
|
3038
|
+
this._borderGfx = new pixi_js.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);
|
|
2690
3045
|
}
|
|
2691
3046
|
/** Get/set progress (0..1) */
|
|
2692
3047
|
get progress() {
|
|
@@ -2696,13 +3051,13 @@ class ProgressBar extends pixi_js.Container {
|
|
|
2696
3051
|
this._progress = Math.max(0, Math.min(1, value));
|
|
2697
3052
|
if (!this._config.animated) {
|
|
2698
3053
|
this._displayedProgress = this._progress;
|
|
2699
|
-
this.
|
|
3054
|
+
this._bar.progress = this._displayedProgress * 100;
|
|
2700
3055
|
}
|
|
2701
3056
|
}
|
|
2702
3057
|
/**
|
|
2703
3058
|
* Call each frame if animated is true.
|
|
2704
3059
|
*/
|
|
2705
|
-
update(
|
|
3060
|
+
update(_dt) {
|
|
2706
3061
|
if (!this._config.animated)
|
|
2707
3062
|
return;
|
|
2708
3063
|
if (Math.abs(this._displayedProgress - this._progress) < 0.001) {
|
|
@@ -2711,35 +3066,7 @@ class ProgressBar extends pixi_js.Container {
|
|
|
2711
3066
|
}
|
|
2712
3067
|
this._displayedProgress +=
|
|
2713
3068
|
(this._progress - this._displayedProgress) * this._config.animationSpeed;
|
|
2714
|
-
this.
|
|
2715
|
-
}
|
|
2716
|
-
drawTrack() {
|
|
2717
|
-
const { width, height, borderRadius, trackColor } = this._config;
|
|
2718
|
-
this._track.clear();
|
|
2719
|
-
this._track.roundRect(0, 0, width, height, borderRadius).fill(trackColor);
|
|
2720
|
-
}
|
|
2721
|
-
drawBorder() {
|
|
2722
|
-
const { width, height, borderRadius, borderColor, borderWidth } = this._config;
|
|
2723
|
-
this._border.clear();
|
|
2724
|
-
this._border
|
|
2725
|
-
.roundRect(0, 0, width, height, borderRadius)
|
|
2726
|
-
.stroke({ color: borderColor, width: borderWidth });
|
|
2727
|
-
}
|
|
2728
|
-
drawFill(progress) {
|
|
2729
|
-
const { width, height, borderRadius, fillColor, borderWidth } = this._config;
|
|
2730
|
-
const innerWidth = width - borderWidth * 2;
|
|
2731
|
-
const innerHeight = height - borderWidth * 2;
|
|
2732
|
-
const fillWidth = Math.max(0, innerWidth * progress);
|
|
2733
|
-
this._fill.clear();
|
|
2734
|
-
if (fillWidth > 0) {
|
|
2735
|
-
this._fill.x = borderWidth;
|
|
2736
|
-
this._fill.y = borderWidth;
|
|
2737
|
-
this._fill.roundRect(0, 0, fillWidth, innerHeight, borderRadius - 1).fill(fillColor);
|
|
2738
|
-
// Highlight
|
|
2739
|
-
this._fill
|
|
2740
|
-
.roundRect(0, 0, fillWidth, innerHeight * 0.4, borderRadius - 1)
|
|
2741
|
-
.fill({ color: 0xffffff, alpha: 0.15 });
|
|
2742
|
-
}
|
|
3069
|
+
this._bar.progress = this._displayedProgress * 100;
|
|
2743
3070
|
}
|
|
2744
3071
|
}
|
|
2745
3072
|
|
|
@@ -2835,7 +3162,10 @@ class Label extends pixi_js.Container {
|
|
|
2835
3162
|
}
|
|
2836
3163
|
|
|
2837
3164
|
/**
|
|
2838
|
-
* 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.
|
|
2839
3169
|
*
|
|
2840
3170
|
* @example
|
|
2841
3171
|
* ```ts
|
|
@@ -2850,72 +3180,67 @@ class Label extends pixi_js.Container {
|
|
|
2850
3180
|
* });
|
|
2851
3181
|
* ```
|
|
2852
3182
|
*/
|
|
2853
|
-
class Panel extends
|
|
2854
|
-
|
|
2855
|
-
_content;
|
|
2856
|
-
_config;
|
|
3183
|
+
class Panel extends components.LayoutContainer {
|
|
3184
|
+
_panelConfig;
|
|
2857
3185
|
constructor(config = {}) {
|
|
2858
|
-
|
|
2859
|
-
|
|
2860
|
-
|
|
2861
|
-
|
|
2862
|
-
|
|
2863
|
-
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,
|
|
2864
3191
|
...config,
|
|
2865
3192
|
};
|
|
2866
|
-
//
|
|
3193
|
+
// If using a 9-slice texture, pass it as a custom background
|
|
3194
|
+
let customBackground;
|
|
2867
3195
|
if (config.nineSliceTexture) {
|
|
2868
3196
|
const texture = typeof config.nineSliceTexture === 'string'
|
|
2869
3197
|
? pixi_js.Texture.from(config.nineSliceTexture)
|
|
2870
3198
|
: config.nineSliceTexture;
|
|
2871
3199
|
const [left, top, right, bottom] = config.nineSliceBorders ?? [10, 10, 10, 10];
|
|
2872
|
-
|
|
3200
|
+
const nineSlice = new pixi_js.NineSliceSprite({
|
|
2873
3201
|
texture,
|
|
2874
3202
|
leftWidth: left,
|
|
2875
3203
|
topHeight: top,
|
|
2876
3204
|
rightWidth: right,
|
|
2877
3205
|
bottomHeight: bottom,
|
|
2878
3206
|
});
|
|
2879
|
-
|
|
2880
|
-
|
|
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
|
+
}
|
|
2881
3229
|
}
|
|
2882
|
-
|
|
2883
|
-
|
|
2884
|
-
this.
|
|
3230
|
+
this.layout = layoutStyles;
|
|
3231
|
+
if (!config.nineSliceTexture) {
|
|
3232
|
+
this.background.alpha = resolvedConfig.backgroundAlpha;
|
|
2885
3233
|
}
|
|
2886
|
-
this._bg.alpha = this._config.backgroundAlpha;
|
|
2887
|
-
this.addChild(this._bg);
|
|
2888
|
-
// Content container with padding
|
|
2889
|
-
this._content = new pixi_js.Container();
|
|
2890
|
-
this._content.x = this._config.padding;
|
|
2891
|
-
this._content.y = this._config.padding;
|
|
2892
|
-
this.addChild(this._content);
|
|
2893
3234
|
}
|
|
2894
|
-
/**
|
|
3235
|
+
/** Access the content container (children added here participate in layout) */
|
|
2895
3236
|
get content() {
|
|
2896
|
-
return this.
|
|
3237
|
+
return this.overflowContainer;
|
|
2897
3238
|
}
|
|
2898
3239
|
/** Resize the panel */
|
|
2899
3240
|
setSize(width, height) {
|
|
2900
|
-
this.
|
|
2901
|
-
this.
|
|
2902
|
-
|
|
2903
|
-
this.drawGraphicsBg();
|
|
2904
|
-
}
|
|
2905
|
-
else {
|
|
2906
|
-
this._bg.width = width;
|
|
2907
|
-
this._bg.height = height;
|
|
2908
|
-
}
|
|
2909
|
-
}
|
|
2910
|
-
drawGraphicsBg() {
|
|
2911
|
-
const bg = this._bg;
|
|
2912
|
-
const { width, height, backgroundColor, borderRadius, borderColor, borderWidth, } = this._config;
|
|
2913
|
-
bg.clear();
|
|
2914
|
-
bg.roundRect(0, 0, width, height, borderRadius ?? 0).fill(backgroundColor ?? 0x1a1a2e);
|
|
2915
|
-
if (borderColor !== undefined && borderWidth) {
|
|
2916
|
-
bg.roundRect(0, 0, width, height, borderRadius ?? 0)
|
|
2917
|
-
.stroke({ color: borderColor, width: borderWidth });
|
|
2918
|
-
}
|
|
3241
|
+
this._panelConfig.width = width;
|
|
3242
|
+
this._panelConfig.height = height;
|
|
3243
|
+
this._layout?.setStyle({ width, height });
|
|
2919
3244
|
}
|
|
2920
3245
|
}
|
|
2921
3246
|
|
|
@@ -2941,6 +3266,7 @@ class BalanceDisplay extends pixi_js.Container {
|
|
|
2941
3266
|
_currentValue = 0;
|
|
2942
3267
|
_displayedValue = 0;
|
|
2943
3268
|
_animating = false;
|
|
3269
|
+
_animationCancelled = false;
|
|
2944
3270
|
constructor(config = {}) {
|
|
2945
3271
|
super();
|
|
2946
3272
|
this._config = {
|
|
@@ -3002,11 +3328,20 @@ class BalanceDisplay extends pixi_js.Container {
|
|
|
3002
3328
|
this.updateDisplay();
|
|
3003
3329
|
}
|
|
3004
3330
|
async animateValue(from, to) {
|
|
3331
|
+
if (this._animating) {
|
|
3332
|
+
this._animationCancelled = true;
|
|
3333
|
+
}
|
|
3005
3334
|
this._animating = true;
|
|
3335
|
+
this._animationCancelled = false;
|
|
3006
3336
|
const duration = this._config.animationDuration;
|
|
3007
3337
|
const startTime = Date.now();
|
|
3008
3338
|
return new Promise((resolve) => {
|
|
3009
3339
|
const tick = () => {
|
|
3340
|
+
if (this._animationCancelled) {
|
|
3341
|
+
this._animating = false;
|
|
3342
|
+
resolve();
|
|
3343
|
+
return;
|
|
3344
|
+
}
|
|
3010
3345
|
const elapsed = Date.now() - startTime;
|
|
3011
3346
|
const t = Math.min(elapsed / duration, 1);
|
|
3012
3347
|
const eased = Easing.easeOutCubic(t);
|
|
@@ -3143,6 +3478,8 @@ class WinDisplay extends pixi_js.Container {
|
|
|
3143
3478
|
* Modal overlay component.
|
|
3144
3479
|
* Shows content on top of a dark overlay with enter/exit animations.
|
|
3145
3480
|
*
|
|
3481
|
+
* The content container uses `@pixi/layout` for automatic centering.
|
|
3482
|
+
*
|
|
3146
3483
|
* @example
|
|
3147
3484
|
* ```ts
|
|
3148
3485
|
* const modal = new Modal({ closeOnOverlay: true });
|
|
@@ -3161,11 +3498,10 @@ class Modal extends pixi_js.Container {
|
|
|
3161
3498
|
constructor(config = {}) {
|
|
3162
3499
|
super();
|
|
3163
3500
|
this._config = {
|
|
3164
|
-
overlayColor: 0x000000,
|
|
3165
|
-
overlayAlpha: 0.7,
|
|
3166
|
-
closeOnOverlay: true,
|
|
3167
|
-
animationDuration: 300,
|
|
3168
|
-
...config,
|
|
3501
|
+
overlayColor: config.overlayColor ?? 0x000000,
|
|
3502
|
+
overlayAlpha: config.overlayAlpha ?? 0.7,
|
|
3503
|
+
closeOnOverlay: config.closeOnOverlay ?? true,
|
|
3504
|
+
animationDuration: config.animationDuration ?? 300,
|
|
3169
3505
|
};
|
|
3170
3506
|
// Overlay
|
|
3171
3507
|
this._overlay = new pixi_js.Graphics();
|
|
@@ -3233,6 +3569,8 @@ const TOAST_COLORS = {
|
|
|
3233
3569
|
/**
|
|
3234
3570
|
* Toast notification component for displaying transient messages.
|
|
3235
3571
|
*
|
|
3572
|
+
* Uses `@pixi/layout` LayoutContainer for auto-sized background.
|
|
3573
|
+
*
|
|
3236
3574
|
* @example
|
|
3237
3575
|
* ```ts
|
|
3238
3576
|
* const toast = new Toast();
|
|
@@ -3248,11 +3586,10 @@ class Toast extends pixi_js.Container {
|
|
|
3248
3586
|
constructor(config = {}) {
|
|
3249
3587
|
super();
|
|
3250
3588
|
this._config = {
|
|
3251
|
-
duration: 3000,
|
|
3252
|
-
bottomOffset: 60,
|
|
3253
|
-
...config,
|
|
3589
|
+
duration: config.duration ?? 3000,
|
|
3590
|
+
bottomOffset: config.bottomOffset ?? 60,
|
|
3254
3591
|
};
|
|
3255
|
-
this._bg = new
|
|
3592
|
+
this._bg = new components.LayoutContainer();
|
|
3256
3593
|
this.addChild(this._bg);
|
|
3257
3594
|
this._text = new pixi_js.Text({
|
|
3258
3595
|
text: '',
|
|
@@ -3270,7 +3607,6 @@ class Toast extends pixi_js.Container {
|
|
|
3270
3607
|
* Show a toast message.
|
|
3271
3608
|
*/
|
|
3272
3609
|
async show(message, type = 'info', viewWidth, viewHeight) {
|
|
3273
|
-
// Clear previous dismiss
|
|
3274
3610
|
if (this._dismissTimeout) {
|
|
3275
3611
|
clearTimeout(this._dismissTimeout);
|
|
3276
3612
|
}
|
|
@@ -3279,10 +3615,16 @@ class Toast extends pixi_js.Container {
|
|
|
3279
3615
|
const width = Math.max(200, this._text.width + padding * 2);
|
|
3280
3616
|
const height = 44;
|
|
3281
3617
|
const radius = 8;
|
|
3282
|
-
|
|
3283
|
-
this._bg.
|
|
3284
|
-
|
|
3285
|
-
|
|
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;
|
|
3286
3628
|
// Position
|
|
3287
3629
|
if (viewWidth && viewHeight) {
|
|
3288
3630
|
this.x = viewWidth / 2;
|
|
@@ -3291,9 +3633,7 @@ class Toast extends pixi_js.Container {
|
|
|
3291
3633
|
this.visible = true;
|
|
3292
3634
|
this.alpha = 0;
|
|
3293
3635
|
this.y += 20;
|
|
3294
|
-
// Animate in
|
|
3295
3636
|
await Tween.to(this, { alpha: 1, y: this.y - 20 }, 300, Easing.easeOutCubic);
|
|
3296
|
-
// Auto-dismiss
|
|
3297
3637
|
if (this._config.duration > 0) {
|
|
3298
3638
|
this._dismissTimeout = setTimeout(() => {
|
|
3299
3639
|
this.dismiss();
|
|
@@ -3315,6 +3655,292 @@ class Toast extends pixi_js.Container {
|
|
|
3315
3655
|
}
|
|
3316
3656
|
}
|
|
3317
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 pixi_js.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 ui.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
|
+
|
|
3318
3944
|
const DEFAULT_CONFIG = {
|
|
3319
3945
|
balance: 10000,
|
|
3320
3946
|
currency: 'USD',
|
|
@@ -3334,8 +3960,9 @@ const DEFAULT_CONFIG = {
|
|
|
3334
3960
|
/**
|
|
3335
3961
|
* Mock host bridge for local development.
|
|
3336
3962
|
*
|
|
3337
|
-
*
|
|
3338
|
-
*
|
|
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.
|
|
3339
3966
|
*
|
|
3340
3967
|
* This allows games to be developed and tested without a real backend.
|
|
3341
3968
|
*
|
|
@@ -3363,8 +3990,7 @@ class DevBridge {
|
|
|
3363
3990
|
_config;
|
|
3364
3991
|
_balance;
|
|
3365
3992
|
_roundCounter = 0;
|
|
3366
|
-
|
|
3367
|
-
_handler = null;
|
|
3993
|
+
_bridge = null;
|
|
3368
3994
|
constructor(config = {}) {
|
|
3369
3995
|
this._config = { ...DEFAULT_CONFIG, ...config };
|
|
3370
3996
|
this._balance = this._config.balance;
|
|
@@ -3375,24 +4001,38 @@ class DevBridge {
|
|
|
3375
4001
|
}
|
|
3376
4002
|
/** Start listening for SDK messages */
|
|
3377
4003
|
start() {
|
|
3378
|
-
if (this.
|
|
4004
|
+
if (this._bridge)
|
|
3379
4005
|
return;
|
|
3380
|
-
|
|
3381
|
-
|
|
3382
|
-
|
|
3383
|
-
|
|
3384
|
-
|
|
4006
|
+
console.debug('[DevBridge] Starting with config:', this._config);
|
|
4007
|
+
this._bridge = new gameSdk.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
|
+
});
|
|
3385
4026
|
if (this._config.debug) {
|
|
3386
|
-
console.log('[DevBridge] Started — listening
|
|
4027
|
+
console.log('[DevBridge] Started — listening via Bridge (devMode)');
|
|
3387
4028
|
}
|
|
3388
4029
|
}
|
|
3389
4030
|
/** Stop listening */
|
|
3390
4031
|
stop() {
|
|
3391
|
-
if (this.
|
|
3392
|
-
|
|
3393
|
-
this.
|
|
4032
|
+
if (this._bridge) {
|
|
4033
|
+
this._bridge.destroy();
|
|
4034
|
+
this._bridge = null;
|
|
3394
4035
|
}
|
|
3395
|
-
this._listening = false;
|
|
3396
4036
|
if (this._config.debug) {
|
|
3397
4037
|
console.log('[DevBridge] Stopped');
|
|
3398
4038
|
}
|
|
@@ -3400,47 +4040,13 @@ class DevBridge {
|
|
|
3400
4040
|
/** Set mock balance */
|
|
3401
4041
|
setBalance(balance) {
|
|
3402
4042
|
this._balance = balance;
|
|
3403
|
-
|
|
3404
|
-
this.sendMessage('BALANCE_UPDATE', { balance: this._balance });
|
|
4043
|
+
this._bridge?.send('BALANCE_UPDATE', { balance: this._balance });
|
|
3405
4044
|
}
|
|
3406
4045
|
/** Destroy the dev bridge */
|
|
3407
4046
|
destroy() {
|
|
3408
4047
|
this.stop();
|
|
3409
4048
|
}
|
|
3410
4049
|
// ─── 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
4050
|
handleGameReady(id) {
|
|
3445
4051
|
const initData = {
|
|
3446
4052
|
balance: this._balance,
|
|
@@ -3473,13 +4079,13 @@ class DevBridge {
|
|
|
3473
4079
|
};
|
|
3474
4080
|
this.delayedSend('PLAY_RESULT', result, id);
|
|
3475
4081
|
}
|
|
3476
|
-
handlePlayAck(_payload
|
|
4082
|
+
handlePlayAck(_payload) {
|
|
3477
4083
|
if (this._config.debug) {
|
|
3478
4084
|
console.log('[DevBridge] Play acknowledged');
|
|
3479
4085
|
}
|
|
3480
4086
|
}
|
|
3481
4087
|
handleGetBalance(id) {
|
|
3482
|
-
this.delayedSend('
|
|
4088
|
+
this.delayedSend('BALANCE_UPDATE', { balance: this._balance }, id);
|
|
3483
4089
|
}
|
|
3484
4090
|
handleGetState(id) {
|
|
3485
4091
|
this.delayedSend('STATE_RESPONSE', this._config.session, id);
|
|
@@ -3489,123 +4095,36 @@ class DevBridge {
|
|
|
3489
4095
|
console.log('[DevBridge] 💰 Open deposit requested (mock: adding 1000)');
|
|
3490
4096
|
}
|
|
3491
4097
|
this._balance += 1000;
|
|
3492
|
-
this.
|
|
4098
|
+
this._bridge?.send('BALANCE_UPDATE', { balance: this._balance });
|
|
3493
4099
|
}
|
|
3494
4100
|
// ─── Communication ─────────────────────────────────────
|
|
3495
4101
|
delayedSend(type, payload, id) {
|
|
3496
4102
|
const delay = this._config.networkDelay;
|
|
3497
4103
|
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();
|
|
4104
|
+
setTimeout(() => this._bridge?.send(type, payload, id), delay);
|
|
3597
4105
|
}
|
|
3598
4106
|
else {
|
|
3599
|
-
this.
|
|
4107
|
+
this._bridge?.send(type, payload, id);
|
|
3600
4108
|
}
|
|
3601
4109
|
}
|
|
3602
|
-
/** Destroy the overlay */
|
|
3603
|
-
destroy() {
|
|
3604
|
-
this.hide();
|
|
3605
|
-
this._container.destroy({ children: true });
|
|
3606
|
-
}
|
|
3607
4110
|
}
|
|
3608
4111
|
|
|
4112
|
+
Object.defineProperty(exports, "ButtonContainer", {
|
|
4113
|
+
enumerable: true,
|
|
4114
|
+
get: function () { return ui.ButtonContainer; }
|
|
4115
|
+
});
|
|
4116
|
+
Object.defineProperty(exports, "FancyButton", {
|
|
4117
|
+
enumerable: true,
|
|
4118
|
+
get: function () { return ui.FancyButton; }
|
|
4119
|
+
});
|
|
4120
|
+
Object.defineProperty(exports, "ScrollBox", {
|
|
4121
|
+
enumerable: true,
|
|
4122
|
+
get: function () { return ui.ScrollBox; }
|
|
4123
|
+
});
|
|
4124
|
+
Object.defineProperty(exports, "LayoutContainer", {
|
|
4125
|
+
enumerable: true,
|
|
4126
|
+
get: function () { return components.LayoutContainer; }
|
|
4127
|
+
});
|
|
3609
4128
|
exports.AssetManager = AssetManager;
|
|
3610
4129
|
exports.AudioManager = AudioManager;
|
|
3611
4130
|
exports.BalanceDisplay = BalanceDisplay;
|
|
@@ -3617,13 +4136,16 @@ exports.FPSOverlay = FPSOverlay;
|
|
|
3617
4136
|
exports.GameApplication = GameApplication;
|
|
3618
4137
|
exports.InputManager = InputManager;
|
|
3619
4138
|
exports.Label = Label;
|
|
4139
|
+
exports.Layout = Layout;
|
|
3620
4140
|
exports.LoadingScene = LoadingScene;
|
|
3621
4141
|
exports.Modal = Modal;
|
|
3622
4142
|
exports.Panel = Panel;
|
|
3623
4143
|
exports.ProgressBar = ProgressBar;
|
|
3624
4144
|
exports.Scene = Scene;
|
|
3625
4145
|
exports.SceneManager = SceneManager;
|
|
4146
|
+
exports.ScrollContainer = ScrollContainer;
|
|
3626
4147
|
exports.SpineHelper = SpineHelper;
|
|
4148
|
+
exports.SpriteAnimation = SpriteAnimation;
|
|
3627
4149
|
exports.StateMachine = StateMachine;
|
|
3628
4150
|
exports.Timeline = Timeline;
|
|
3629
4151
|
exports.Toast = Toast;
|