@energy8platform/game-engine 0.2.0 → 0.3.0
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/README.md +318 -49
- package/dist/animation.cjs.js +191 -1
- package/dist/animation.cjs.js.map +1 -1
- package/dist/animation.d.ts +117 -1
- package/dist/animation.esm.js +192 -3
- package/dist/animation.esm.js.map +1 -1
- package/dist/audio.cjs.js +66 -16
- package/dist/audio.cjs.js.map +1 -1
- package/dist/audio.d.ts +4 -0
- package/dist/audio.esm.js +66 -16
- package/dist/audio.esm.js.map +1 -1
- package/dist/core.cjs.js +310 -84
- package/dist/core.cjs.js.map +1 -1
- package/dist/core.d.ts +60 -1
- package/dist/core.esm.js +311 -85
- package/dist/core.esm.js.map +1 -1
- package/dist/debug.cjs.js +36 -68
- package/dist/debug.cjs.js.map +1 -1
- package/dist/debug.d.ts +4 -6
- package/dist/debug.esm.js +36 -68
- package/dist/debug.esm.js.map +1 -1
- package/dist/index.cjs.js +1250 -251
- package/dist/index.cjs.js.map +1 -1
- package/dist/index.d.ts +386 -41
- package/dist/index.esm.js +1250 -254
- package/dist/index.esm.js.map +1 -1
- package/dist/ui.cjs.js +757 -1
- package/dist/ui.cjs.js.map +1 -1
- package/dist/ui.d.ts +208 -2
- package/dist/ui.esm.js +756 -2
- package/dist/ui.esm.js.map +1 -1
- package/dist/vite.cjs.js +65 -68
- package/dist/vite.cjs.js.map +1 -1
- package/dist/vite.d.ts +17 -23
- package/dist/vite.esm.js +66 -68
- package/dist/vite.esm.js.map +1 -1
- package/package.json +4 -5
- package/src/animation/SpriteAnimation.ts +210 -0
- package/src/animation/Tween.ts +27 -1
- package/src/animation/index.ts +2 -0
- package/src/audio/AudioManager.ts +64 -15
- package/src/core/EventEmitter.ts +7 -1
- package/src/core/GameApplication.ts +25 -7
- package/src/core/SceneManager.ts +3 -1
- package/src/debug/DevBridge.ts +49 -80
- package/src/index.ts +6 -0
- package/src/input/InputManager.ts +26 -0
- package/src/loading/CSSPreloader.ts +7 -33
- package/src/loading/LoadingScene.ts +17 -41
- package/src/loading/index.ts +1 -0
- package/src/loading/logo.ts +95 -0
- package/src/types.ts +4 -0
- package/src/ui/BalanceDisplay.ts +14 -0
- package/src/ui/Button.ts +1 -1
- package/src/ui/Layout.ts +364 -0
- package/src/ui/ScrollContainer.ts +557 -0
- package/src/ui/index.ts +4 -0
- package/src/viewport/ViewportManager.ts +2 -0
- package/src/vite/index.ts +83 -83
package/dist/core.esm.js
CHANGED
|
@@ -1,4 +1,4 @@
|
|
|
1
|
-
import { Ticker, Assets, Container, Application } from 'pixi.js';
|
|
1
|
+
import { Ticker, Assets, Container, Text, Application } from 'pixi.js';
|
|
2
2
|
import { CasinoGameSDK } from '@energy8platform/game-sdk';
|
|
3
3
|
|
|
4
4
|
// ─── Scale Modes ───────────────────────────────────────────
|
|
@@ -30,6 +30,9 @@ var TransitionType;
|
|
|
30
30
|
/**
|
|
31
31
|
* Minimal typed event emitter.
|
|
32
32
|
* Used internally by GameApplication, SceneManager, AudioManager, etc.
|
|
33
|
+
*
|
|
34
|
+
* Supports `void` event types — events that carry no data can be emitted
|
|
35
|
+
* without arguments: `emitter.emit('eventName')`.
|
|
33
36
|
*/
|
|
34
37
|
// eslint-disable-next-line @typescript-eslint/no-empty-object-type
|
|
35
38
|
class EventEmitter {
|
|
@@ -52,7 +55,8 @@ class EventEmitter {
|
|
|
52
55
|
this.listeners.get(event)?.delete(handler);
|
|
53
56
|
return this;
|
|
54
57
|
}
|
|
55
|
-
emit(
|
|
58
|
+
emit(...args) {
|
|
59
|
+
const [event, data] = args;
|
|
56
60
|
const handlers = this.listeners.get(event);
|
|
57
61
|
if (handlers) {
|
|
58
62
|
for (const handler of handlers) {
|
|
@@ -155,9 +159,20 @@ class Tween {
|
|
|
155
159
|
}
|
|
156
160
|
/**
|
|
157
161
|
* Wait for a given duration (useful in timelines).
|
|
162
|
+
* Uses PixiJS Ticker for consistent timing with other tweens.
|
|
158
163
|
*/
|
|
159
164
|
static delay(ms) {
|
|
160
|
-
return new Promise((resolve) =>
|
|
165
|
+
return new Promise((resolve) => {
|
|
166
|
+
let elapsed = 0;
|
|
167
|
+
const onTick = (ticker) => {
|
|
168
|
+
elapsed += ticker.deltaMS;
|
|
169
|
+
if (elapsed >= ms) {
|
|
170
|
+
Ticker.shared.remove(onTick);
|
|
171
|
+
resolve();
|
|
172
|
+
}
|
|
173
|
+
};
|
|
174
|
+
Ticker.shared.add(onTick);
|
|
175
|
+
});
|
|
161
176
|
}
|
|
162
177
|
/**
|
|
163
178
|
* Kill all tweens on a target.
|
|
@@ -184,6 +199,20 @@ class Tween {
|
|
|
184
199
|
static get activeTweens() {
|
|
185
200
|
return Tween._tweens.length;
|
|
186
201
|
}
|
|
202
|
+
/**
|
|
203
|
+
* Reset the tween system — kill all tweens and remove the ticker.
|
|
204
|
+
* Useful for cleanup between game instances, tests, or hot-reload.
|
|
205
|
+
*/
|
|
206
|
+
static reset() {
|
|
207
|
+
for (const tw of Tween._tweens) {
|
|
208
|
+
tw.resolve();
|
|
209
|
+
}
|
|
210
|
+
Tween._tweens.length = 0;
|
|
211
|
+
if (Tween._tickerAdded) {
|
|
212
|
+
Ticker.shared.remove(Tween.tick);
|
|
213
|
+
Tween._tickerAdded = false;
|
|
214
|
+
}
|
|
215
|
+
}
|
|
187
216
|
// ─── Internal ──────────────────────────────────────────
|
|
188
217
|
static ensureTicker() {
|
|
189
218
|
if (Tween._tickerAdded)
|
|
@@ -386,8 +415,9 @@ class SceneManager extends EventEmitter {
|
|
|
386
415
|
}
|
|
387
416
|
// Transition in
|
|
388
417
|
await this.transitionIn(scene.container, transition);
|
|
389
|
-
|
|
418
|
+
// Push to stack BEFORE onEnter so currentKey is correct during initialization
|
|
390
419
|
this.stack.push({ scene, key });
|
|
420
|
+
await scene.onEnter?.(data);
|
|
391
421
|
this._transitioning = false;
|
|
392
422
|
}
|
|
393
423
|
async popInternal(showTransition, transition) {
|
|
@@ -686,26 +716,51 @@ class AudioManager {
|
|
|
686
716
|
if (!this._initialized || !this._soundModule)
|
|
687
717
|
return;
|
|
688
718
|
const { sound } = this._soundModule;
|
|
689
|
-
// Stop current music
|
|
690
|
-
if (this._currentMusic) {
|
|
719
|
+
// Stop current music with fade-out, start new music with fade-in
|
|
720
|
+
if (this._currentMusic && fadeDuration > 0) {
|
|
721
|
+
const prevAlias = this._currentMusic;
|
|
722
|
+
this._currentMusic = alias;
|
|
723
|
+
if (this._globalMuted || this._categories.music.muted)
|
|
724
|
+
return;
|
|
725
|
+
// Fade out the previous track
|
|
726
|
+
this.fadeVolume(prevAlias, this._categories.music.volume, 0, fadeDuration, () => {
|
|
727
|
+
try {
|
|
728
|
+
sound.stop(prevAlias);
|
|
729
|
+
}
|
|
730
|
+
catch { /* ignore */ }
|
|
731
|
+
});
|
|
732
|
+
// Start new track at zero volume, fade in
|
|
691
733
|
try {
|
|
692
|
-
sound.
|
|
734
|
+
sound.play(alias, {
|
|
735
|
+
volume: 0,
|
|
736
|
+
loop: true,
|
|
737
|
+
});
|
|
738
|
+
this.fadeVolume(alias, 0, this._categories.music.volume, fadeDuration);
|
|
693
739
|
}
|
|
694
|
-
catch {
|
|
695
|
-
|
|
740
|
+
catch (e) {
|
|
741
|
+
console.warn(`[AudioManager] Failed to play music "${alias}":`, e);
|
|
696
742
|
}
|
|
697
743
|
}
|
|
698
|
-
|
|
699
|
-
|
|
700
|
-
|
|
701
|
-
|
|
702
|
-
|
|
703
|
-
|
|
704
|
-
|
|
705
|
-
}
|
|
706
|
-
|
|
707
|
-
|
|
708
|
-
|
|
744
|
+
else {
|
|
745
|
+
// No crossfade — instant switch
|
|
746
|
+
if (this._currentMusic) {
|
|
747
|
+
try {
|
|
748
|
+
sound.stop(this._currentMusic);
|
|
749
|
+
}
|
|
750
|
+
catch { /* ignore */ }
|
|
751
|
+
}
|
|
752
|
+
this._currentMusic = alias;
|
|
753
|
+
if (this._globalMuted || this._categories.music.muted)
|
|
754
|
+
return;
|
|
755
|
+
try {
|
|
756
|
+
sound.play(alias, {
|
|
757
|
+
volume: this._categories.music.volume,
|
|
758
|
+
loop: true,
|
|
759
|
+
});
|
|
760
|
+
}
|
|
761
|
+
catch (e) {
|
|
762
|
+
console.warn(`[AudioManager] Failed to play music "${alias}":`, e);
|
|
763
|
+
}
|
|
709
764
|
}
|
|
710
765
|
}
|
|
711
766
|
/**
|
|
@@ -847,6 +902,31 @@ class AudioManager {
|
|
|
847
902
|
this._initialized = false;
|
|
848
903
|
}
|
|
849
904
|
// ─── Private ───────────────────────────────────────────
|
|
905
|
+
/**
|
|
906
|
+
* Smoothly fade a sound's volume from `fromVol` to `toVol` over `durationMs`.
|
|
907
|
+
*/
|
|
908
|
+
fadeVolume(alias, fromVol, toVol, durationMs, onComplete) {
|
|
909
|
+
if (!this._soundModule)
|
|
910
|
+
return;
|
|
911
|
+
const { sound } = this._soundModule;
|
|
912
|
+
const startTime = Date.now();
|
|
913
|
+
const tick = () => {
|
|
914
|
+
const elapsed = Date.now() - startTime;
|
|
915
|
+
const t = Math.min(elapsed / durationMs, 1);
|
|
916
|
+
const vol = fromVol + (toVol - fromVol) * t;
|
|
917
|
+
try {
|
|
918
|
+
sound.volume(alias, vol);
|
|
919
|
+
}
|
|
920
|
+
catch { /* ignore */ }
|
|
921
|
+
if (t < 1) {
|
|
922
|
+
requestAnimationFrame(tick);
|
|
923
|
+
}
|
|
924
|
+
else {
|
|
925
|
+
onComplete?.();
|
|
926
|
+
}
|
|
927
|
+
};
|
|
928
|
+
requestAnimationFrame(tick);
|
|
929
|
+
}
|
|
850
930
|
applyVolumes() {
|
|
851
931
|
if (!this._soundModule)
|
|
852
932
|
return;
|
|
@@ -951,6 +1031,10 @@ class InputManager extends EventEmitter {
|
|
|
951
1031
|
_locked = false;
|
|
952
1032
|
_keysDown = new Set();
|
|
953
1033
|
_destroyed = false;
|
|
1034
|
+
// Viewport transform (set by ViewportManager via setViewportTransform)
|
|
1035
|
+
_viewportScale = 1;
|
|
1036
|
+
_viewportOffsetX = 0;
|
|
1037
|
+
_viewportOffsetY = 0;
|
|
954
1038
|
// Gesture tracking
|
|
955
1039
|
_pointerStart = null;
|
|
956
1040
|
_swipeThreshold = 50; // minimum distance in px
|
|
@@ -977,6 +1061,25 @@ class InputManager extends EventEmitter {
|
|
|
977
1061
|
isKeyDown(key) {
|
|
978
1062
|
return this._keysDown.has(key.toLowerCase());
|
|
979
1063
|
}
|
|
1064
|
+
/**
|
|
1065
|
+
* Update the viewport transform used for DOM→world coordinate mapping.
|
|
1066
|
+
* Called automatically by GameApplication when ViewportManager emits resize.
|
|
1067
|
+
*/
|
|
1068
|
+
setViewportTransform(scale, offsetX, offsetY) {
|
|
1069
|
+
this._viewportScale = scale;
|
|
1070
|
+
this._viewportOffsetX = offsetX;
|
|
1071
|
+
this._viewportOffsetY = offsetY;
|
|
1072
|
+
}
|
|
1073
|
+
/**
|
|
1074
|
+
* Convert a DOM canvas position to game-world coordinates,
|
|
1075
|
+
* accounting for viewport scaling and offset.
|
|
1076
|
+
*/
|
|
1077
|
+
getWorldPosition(canvasX, canvasY) {
|
|
1078
|
+
return {
|
|
1079
|
+
x: (canvasX - this._viewportOffsetX) / this._viewportScale,
|
|
1080
|
+
y: (canvasY - this._viewportOffsetY) / this._viewportScale,
|
|
1081
|
+
};
|
|
1082
|
+
}
|
|
980
1083
|
/** Destroy the input manager */
|
|
981
1084
|
destroy() {
|
|
982
1085
|
this._destroyed = true;
|
|
@@ -1232,6 +1335,8 @@ class ViewportManager extends EventEmitter {
|
|
|
1232
1335
|
this._destroyed = true;
|
|
1233
1336
|
this._resizeObserver?.disconnect();
|
|
1234
1337
|
this._resizeObserver = null;
|
|
1338
|
+
// Remove fallback window resize listener if it was used
|
|
1339
|
+
window.removeEventListener('resize', this.onWindowResize);
|
|
1235
1340
|
if (this._resizeTimeout !== null) {
|
|
1236
1341
|
clearTimeout(this._resizeTimeout);
|
|
1237
1342
|
}
|
|
@@ -1295,45 +1400,87 @@ class Scene {
|
|
|
1295
1400
|
}
|
|
1296
1401
|
|
|
1297
1402
|
/**
|
|
1298
|
-
*
|
|
1299
|
-
*
|
|
1403
|
+
* Shared Energy8 SVG logo with an embedded loader bar.
|
|
1404
|
+
*
|
|
1405
|
+
* The loader bar fill is controlled via a `<clipPath>` whose `<rect>` width
|
|
1406
|
+
* is animatable. Different consumers customise gradient IDs and the clip
|
|
1407
|
+
* element's ID/class to avoid collisions when both CSSPreloader and
|
|
1408
|
+
* LoadingScene appear in the same DOM.
|
|
1300
1409
|
*/
|
|
1301
|
-
|
|
1302
|
-
|
|
1303
|
-
<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(#
|
|
1304
|
-
<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(#
|
|
1305
|
-
<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(#
|
|
1306
|
-
<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(#
|
|
1307
|
-
<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(#
|
|
1308
|
-
|
|
1309
|
-
|
|
1310
|
-
|
|
1311
|
-
<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)"/>
|
|
1312
|
-
<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>
|
|
1313
|
-
<defs>
|
|
1314
|
-
<linearGradient id="ls0" x1="223.7" x2="223.7" y1="81.75" y2="127.8" gradientUnits="userSpaceOnUse">
|
|
1410
|
+
/** SVG path data for the Energy8 wordmark — reused across loaders */
|
|
1411
|
+
const WORDMARK_PATHS = `
|
|
1412
|
+
<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)"/>
|
|
1413
|
+
<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)"/>
|
|
1414
|
+
<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)"/>
|
|
1415
|
+
<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)"/>
|
|
1416
|
+
<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)"/>`;
|
|
1417
|
+
/** Gradient definitions template (gradient IDs are replaced per-consumer) */
|
|
1418
|
+
const GRADIENT_DEFS = `
|
|
1419
|
+
<linearGradient id="GID0" x1="223.7" x2="223.7" y1="81.75" y2="127.8" gradientUnits="userSpaceOnUse">
|
|
1315
1420
|
<stop stop-color="#663BA6"/><stop stop-color="#7939C2" offset=".349"/><stop stop-color="#8A2FC0" offset=".6615"/><stop stop-color="#791BA3" offset="1"/>
|
|
1316
1421
|
</linearGradient>
|
|
1317
|
-
<linearGradient id="
|
|
1422
|
+
<linearGradient id="GID1" x1="194.6" x2="194.6" y1="81.75" y2="138.3" gradientUnits="userSpaceOnUse">
|
|
1318
1423
|
<stop stop-color="#663BA6"/><stop stop-color="#7939C2" offset=".349"/><stop stop-color="#8A2FC0" offset=".6615"/><stop stop-color="#791BA3" offset="1"/>
|
|
1319
1424
|
</linearGradient>
|
|
1320
|
-
<linearGradient id="
|
|
1425
|
+
<linearGradient id="GID2" x1="157.8" x2="157.8" y1="81.75" y2="127" gradientUnits="userSpaceOnUse">
|
|
1321
1426
|
<stop stop-color="#663BA6"/><stop stop-color="#7939C2" offset=".349"/><stop stop-color="#8A2FC0" offset=".6615"/><stop stop-color="#791BA3" offset="1"/>
|
|
1322
1427
|
</linearGradient>
|
|
1323
|
-
<linearGradient id="
|
|
1428
|
+
<linearGradient id="GID3" x1="79.96" x2="79.96" y1="81.75" y2="141.8" gradientUnits="userSpaceOnUse">
|
|
1324
1429
|
<stop stop-color="#663BA6"/><stop stop-color="#7939C2" offset=".349"/><stop stop-color="#8A2FC0" offset=".6615"/><stop stop-color="#791BA3" offset="1"/>
|
|
1325
1430
|
</linearGradient>
|
|
1326
|
-
<linearGradient id="
|
|
1431
|
+
<linearGradient id="GID4" x1="36.18" x2="212.5" y1="156.6" y2="156.6" gradientUnits="userSpaceOnUse">
|
|
1327
1432
|
<stop stop-color="#316FB0"/><stop stop-color="#1FCDE6" offset=".5"/><stop stop-color="#29FEE7" offset="1"/>
|
|
1328
1433
|
</linearGradient>
|
|
1329
|
-
<linearGradient id="
|
|
1434
|
+
<linearGradient id="GID5" x1="40.27" x2="208.2" y1="156.4" y2="156.4" gradientUnits="userSpaceOnUse">
|
|
1330
1435
|
<stop stop-color="#316FB0"/><stop stop-color="#1FCDE6" offset=".5"/><stop stop-color="#29FEE7" offset="1"/>
|
|
1331
|
-
</linearGradient
|
|
1436
|
+
</linearGradient>`;
|
|
1437
|
+
/** Max width of the loader bar in SVG units */
|
|
1438
|
+
const LOADER_BAR_MAX_WIDTH = 174;
|
|
1439
|
+
/**
|
|
1440
|
+
* Build the Energy8 SVG logo with a loader bar, using unique IDs.
|
|
1441
|
+
*
|
|
1442
|
+
* @param opts - Configuration to avoid element ID collisions
|
|
1443
|
+
* @returns SVG markup string
|
|
1444
|
+
*/
|
|
1445
|
+
function buildLogoSVG(opts) {
|
|
1446
|
+
const { idPrefix, svgClass, svgStyle, clipRectClass, clipRectId, textId, textContent, textClass } = opts;
|
|
1447
|
+
// Replace gradient ID placeholders with prefixed versions
|
|
1448
|
+
const paths = WORDMARK_PATHS.replace(/GID(\d)/g, `${idPrefix}$1`);
|
|
1449
|
+
const defs = GRADIENT_DEFS.replace(/GID(\d)/g, `${idPrefix}$1`);
|
|
1450
|
+
const clipId = `${idPrefix}-loader-clip`;
|
|
1451
|
+
const fillGradientId = `${idPrefix}5`;
|
|
1452
|
+
const classAttr = svgClass ? ` class="${svgClass}"` : '';
|
|
1453
|
+
const styleAttr = svgStyle ? ` style="${svgStyle}"` : '';
|
|
1454
|
+
const rectClassAttr = clipRectClass ? ` class="${clipRectClass}"` : '';
|
|
1455
|
+
const rectIdAttr = clipRectId ? ` id="${clipRectId}"` : '';
|
|
1456
|
+
const txtIdAttr = textId ? ` id="${textId}"` : '';
|
|
1457
|
+
const txtClassAttr = textClass ? ` class="${textClass}"` : '';
|
|
1458
|
+
return `<svg xmlns="http://www.w3.org/2000/svg" viewBox="0 0 250 200" fill="none"${classAttr}${styleAttr}>
|
|
1459
|
+
${paths}
|
|
1460
|
+
<clipPath id="${clipId}">
|
|
1461
|
+
<rect${rectIdAttr} x="37" y="148" width="0" height="20"${rectClassAttr}/>
|
|
1462
|
+
</clipPath>
|
|
1463
|
+
<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})"/>
|
|
1464
|
+
<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>
|
|
1465
|
+
<defs>
|
|
1466
|
+
${defs}
|
|
1332
1467
|
</defs>
|
|
1333
1468
|
</svg>`;
|
|
1334
1469
|
}
|
|
1335
|
-
|
|
1336
|
-
|
|
1470
|
+
|
|
1471
|
+
/**
|
|
1472
|
+
* Build the loading scene variant of the logo SVG.
|
|
1473
|
+
* Uses unique IDs (prefixed with 'ls') to avoid collisions with CSSPreloader.
|
|
1474
|
+
*/
|
|
1475
|
+
function buildLoadingLogoSVG() {
|
|
1476
|
+
return buildLogoSVG({
|
|
1477
|
+
idPrefix: 'ls',
|
|
1478
|
+
svgStyle: 'width:100%;height:auto;',
|
|
1479
|
+
clipRectId: 'ge-loader-rect',
|
|
1480
|
+
textId: 'ge-loader-pct',
|
|
1481
|
+
textContent: '0%',
|
|
1482
|
+
});
|
|
1483
|
+
}
|
|
1337
1484
|
/**
|
|
1338
1485
|
* Built-in loading screen using the Energy8 SVG logo with animated loader bar.
|
|
1339
1486
|
*
|
|
@@ -1443,7 +1590,7 @@ class LoadingScene extends Scene {
|
|
|
1443
1590
|
this._overlay.id = '__ge-loading-overlay__';
|
|
1444
1591
|
this._overlay.innerHTML = `
|
|
1445
1592
|
<div class="ge-loading-content">
|
|
1446
|
-
${
|
|
1593
|
+
${buildLoadingLogoSVG()}
|
|
1447
1594
|
</div>
|
|
1448
1595
|
`;
|
|
1449
1596
|
const style = document.createElement('style');
|
|
@@ -1581,8 +1728,11 @@ class LoadingScene extends Scene {
|
|
|
1581
1728
|
}
|
|
1582
1729
|
// Remove overlay
|
|
1583
1730
|
this.removeOverlay();
|
|
1584
|
-
// Navigate to the target scene
|
|
1585
|
-
await this._engine.scenes.goto(this._targetScene,
|
|
1731
|
+
// Navigate to the target scene, always passing the engine reference
|
|
1732
|
+
await this._engine.scenes.goto(this._targetScene, {
|
|
1733
|
+
engine: this._engine,
|
|
1734
|
+
...(this._targetData && typeof this._targetData === 'object' ? this._targetData : { data: this._targetData }),
|
|
1735
|
+
});
|
|
1586
1736
|
}
|
|
1587
1737
|
}
|
|
1588
1738
|
|
|
@@ -1591,39 +1741,12 @@ const PRELOADER_ID = '__ge-css-preloader__';
|
|
|
1591
1741
|
* Inline SVG logo with animated loader bar.
|
|
1592
1742
|
* The `#loader` path acts as the progress fill — animated via clipPath.
|
|
1593
1743
|
*/
|
|
1594
|
-
const LOGO_SVG =
|
|
1595
|
-
|
|
1596
|
-
|
|
1597
|
-
|
|
1598
|
-
|
|
1599
|
-
|
|
1600
|
-
<!-- Loader fill with clip for progress animation -->
|
|
1601
|
-
<clipPath id="ge-loader-clip">
|
|
1602
|
-
<rect x="37" y="148" width="0" height="20" class="ge-clip-rect"/>
|
|
1603
|
-
</clipPath>
|
|
1604
|
-
<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)"/>
|
|
1605
|
-
<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>
|
|
1606
|
-
<defs>
|
|
1607
|
-
<linearGradient id="pl0" x1="223.7" x2="223.7" y1="81.75" y2="127.8" gradientUnits="userSpaceOnUse">
|
|
1608
|
-
<stop stop-color="#663BA6"/><stop stop-color="#7939C2" offset=".349"/><stop stop-color="#8A2FC0" offset=".6615"/><stop stop-color="#791BA3" offset="1"/>
|
|
1609
|
-
</linearGradient>
|
|
1610
|
-
<linearGradient id="pl1" x1="194.6" x2="194.6" y1="81.75" y2="138.3" gradientUnits="userSpaceOnUse">
|
|
1611
|
-
<stop stop-color="#663BA6"/><stop stop-color="#7939C2" offset=".349"/><stop stop-color="#8A2FC0" offset=".6615"/><stop stop-color="#791BA3" offset="1"/>
|
|
1612
|
-
</linearGradient>
|
|
1613
|
-
<linearGradient id="pl2" x1="157.8" x2="157.8" y1="81.75" y2="127" gradientUnits="userSpaceOnUse">
|
|
1614
|
-
<stop stop-color="#663BA6"/><stop stop-color="#7939C2" offset=".349"/><stop stop-color="#8A2FC0" offset=".6615"/><stop stop-color="#791BA3" offset="1"/>
|
|
1615
|
-
</linearGradient>
|
|
1616
|
-
<linearGradient id="pl3" x1="79.96" x2="79.96" y1="81.75" y2="141.8" gradientUnits="userSpaceOnUse">
|
|
1617
|
-
<stop stop-color="#663BA6"/><stop stop-color="#7939C2" offset=".349"/><stop stop-color="#8A2FC0" offset=".6615"/><stop stop-color="#791BA3" offset="1"/>
|
|
1618
|
-
</linearGradient>
|
|
1619
|
-
<linearGradient id="pl4" x1="36.18" x2="212.5" y1="156.6" y2="156.6" gradientUnits="userSpaceOnUse">
|
|
1620
|
-
<stop stop-color="#316FB0"/><stop stop-color="#1FCDE6" offset=".5"/><stop stop-color="#29FEE7" offset="1"/>
|
|
1621
|
-
</linearGradient>
|
|
1622
|
-
<linearGradient id="pl5" x1="40.27" x2="208.2" y1="156.4" y2="156.4" gradientUnits="userSpaceOnUse">
|
|
1623
|
-
<stop stop-color="#316FB0"/><stop stop-color="#1FCDE6" offset=".5"/><stop stop-color="#29FEE7" offset="1"/>
|
|
1624
|
-
</linearGradient>
|
|
1625
|
-
</defs>
|
|
1626
|
-
</svg>`;
|
|
1744
|
+
const LOGO_SVG = buildLogoSVG({
|
|
1745
|
+
idPrefix: 'pl',
|
|
1746
|
+
svgClass: 'ge-logo-svg',
|
|
1747
|
+
clipRectClass: 'ge-clip-rect',
|
|
1748
|
+
textClass: 'ge-preloader-svg-text',
|
|
1749
|
+
});
|
|
1627
1750
|
/**
|
|
1628
1751
|
* Creates a lightweight CSS-only preloader that appears instantly,
|
|
1629
1752
|
* BEFORE PixiJS/WebGL is initialized.
|
|
@@ -1727,6 +1850,96 @@ function removeCSSPreloader(container) {
|
|
|
1727
1850
|
});
|
|
1728
1851
|
}
|
|
1729
1852
|
|
|
1853
|
+
/**
|
|
1854
|
+
* FPS overlay for debugging performance.
|
|
1855
|
+
*
|
|
1856
|
+
* Shows FPS, frame time, and draw call count in the corner of the screen.
|
|
1857
|
+
*
|
|
1858
|
+
* @example
|
|
1859
|
+
* ```ts
|
|
1860
|
+
* const fps = new FPSOverlay(app);
|
|
1861
|
+
* fps.show();
|
|
1862
|
+
* ```
|
|
1863
|
+
*/
|
|
1864
|
+
class FPSOverlay {
|
|
1865
|
+
_app;
|
|
1866
|
+
_container;
|
|
1867
|
+
_fpsText;
|
|
1868
|
+
_visible = false;
|
|
1869
|
+
_samples = [];
|
|
1870
|
+
_maxSamples = 60;
|
|
1871
|
+
_lastUpdate = 0;
|
|
1872
|
+
_tickFn = null;
|
|
1873
|
+
constructor(app) {
|
|
1874
|
+
this._app = app;
|
|
1875
|
+
this._container = new Container();
|
|
1876
|
+
this._container.label = 'FPSOverlay';
|
|
1877
|
+
this._container.zIndex = 99999;
|
|
1878
|
+
this._fpsText = new Text({
|
|
1879
|
+
text: 'FPS: --',
|
|
1880
|
+
style: {
|
|
1881
|
+
fontFamily: 'monospace',
|
|
1882
|
+
fontSize: 14,
|
|
1883
|
+
fill: 0x00ff00,
|
|
1884
|
+
stroke: { color: 0x000000, width: 2 },
|
|
1885
|
+
},
|
|
1886
|
+
});
|
|
1887
|
+
this._fpsText.x = 8;
|
|
1888
|
+
this._fpsText.y = 8;
|
|
1889
|
+
this._container.addChild(this._fpsText);
|
|
1890
|
+
}
|
|
1891
|
+
/** Show the FPS overlay */
|
|
1892
|
+
show() {
|
|
1893
|
+
if (this._visible)
|
|
1894
|
+
return;
|
|
1895
|
+
this._visible = true;
|
|
1896
|
+
this._app.stage.addChild(this._container);
|
|
1897
|
+
this._tickFn = (ticker) => {
|
|
1898
|
+
this._samples.push(ticker.FPS);
|
|
1899
|
+
if (this._samples.length > this._maxSamples) {
|
|
1900
|
+
this._samples.shift();
|
|
1901
|
+
}
|
|
1902
|
+
// Update display every ~500ms
|
|
1903
|
+
const now = Date.now();
|
|
1904
|
+
if (now - this._lastUpdate > 500) {
|
|
1905
|
+
const avg = this._samples.reduce((a, b) => a + b, 0) / this._samples.length;
|
|
1906
|
+
const min = Math.min(...this._samples);
|
|
1907
|
+
this._fpsText.text = [
|
|
1908
|
+
`FPS: ${Math.round(avg)} (min: ${Math.round(min)})`,
|
|
1909
|
+
`Frame: ${ticker.deltaMS.toFixed(1)}ms`,
|
|
1910
|
+
].join('\n');
|
|
1911
|
+
this._lastUpdate = now;
|
|
1912
|
+
}
|
|
1913
|
+
};
|
|
1914
|
+
this._app.ticker.add(this._tickFn);
|
|
1915
|
+
}
|
|
1916
|
+
/** Hide the FPS overlay */
|
|
1917
|
+
hide() {
|
|
1918
|
+
if (!this._visible)
|
|
1919
|
+
return;
|
|
1920
|
+
this._visible = false;
|
|
1921
|
+
this._container.removeFromParent();
|
|
1922
|
+
if (this._tickFn) {
|
|
1923
|
+
this._app.ticker.remove(this._tickFn);
|
|
1924
|
+
this._tickFn = null;
|
|
1925
|
+
}
|
|
1926
|
+
}
|
|
1927
|
+
/** Toggle visibility */
|
|
1928
|
+
toggle() {
|
|
1929
|
+
if (this._visible) {
|
|
1930
|
+
this.hide();
|
|
1931
|
+
}
|
|
1932
|
+
else {
|
|
1933
|
+
this.show();
|
|
1934
|
+
}
|
|
1935
|
+
}
|
|
1936
|
+
/** Destroy the overlay */
|
|
1937
|
+
destroy() {
|
|
1938
|
+
this.hide();
|
|
1939
|
+
this._container.destroy({ children: true });
|
|
1940
|
+
}
|
|
1941
|
+
}
|
|
1942
|
+
|
|
1730
1943
|
/**
|
|
1731
1944
|
* The main entry point for a game built on @energy8platform/game-engine.
|
|
1732
1945
|
*
|
|
@@ -1774,6 +1987,8 @@ class GameApplication extends EventEmitter {
|
|
|
1774
1987
|
viewport;
|
|
1775
1988
|
/** SDK instance (null in offline mode) */
|
|
1776
1989
|
sdk = null;
|
|
1990
|
+
/** FPS overlay instance (only when debug: true) */
|
|
1991
|
+
fpsOverlay = null;
|
|
1777
1992
|
/** Data received from SDK initialization */
|
|
1778
1993
|
initData = null;
|
|
1779
1994
|
/** Configuration */
|
|
@@ -1841,14 +2056,15 @@ class GameApplication extends EventEmitter {
|
|
|
1841
2056
|
this.applySDKConfig();
|
|
1842
2057
|
// 6. Initialize sub-systems
|
|
1843
2058
|
this.initSubSystems();
|
|
1844
|
-
this.emit('initialized'
|
|
2059
|
+
this.emit('initialized');
|
|
1845
2060
|
// 7. Remove CSS preloader, show Canvas loading screen
|
|
1846
2061
|
removeCSSPreloader(this._container);
|
|
1847
2062
|
// 8. Load assets with loading screen
|
|
1848
2063
|
await this.loadAssets(firstScene, sceneData);
|
|
2064
|
+
this.emit('loaded');
|
|
1849
2065
|
// 9. Start the game loop
|
|
1850
2066
|
this._running = true;
|
|
1851
|
-
this.emit('started'
|
|
2067
|
+
this.emit('started');
|
|
1852
2068
|
}
|
|
1853
2069
|
catch (err) {
|
|
1854
2070
|
console.error('[GameEngine] Failed to start:', err);
|
|
@@ -1870,8 +2086,8 @@ class GameApplication extends EventEmitter {
|
|
|
1870
2086
|
this.viewport?.destroy();
|
|
1871
2087
|
this.sdk?.destroy();
|
|
1872
2088
|
this.app?.destroy(true, { children: true, texture: true });
|
|
2089
|
+
this.emit('destroyed');
|
|
1873
2090
|
this.removeAllListeners();
|
|
1874
|
-
this.emit('destroyed', undefined);
|
|
1875
2091
|
}
|
|
1876
2092
|
// ─── Private initialization steps ──────────────────────
|
|
1877
2093
|
resolveContainer() {
|
|
@@ -1944,14 +2160,19 @@ class GameApplication extends EventEmitter {
|
|
|
1944
2160
|
});
|
|
1945
2161
|
// Wire SceneManager to the PixiJS stage
|
|
1946
2162
|
this.scenes.setRoot(this.app.stage);
|
|
1947
|
-
// Wire viewport resize → scene manager
|
|
1948
|
-
this.viewport.on('resize', ({ width, height }) => {
|
|
2163
|
+
// Wire viewport resize → scene manager + input manager
|
|
2164
|
+
this.viewport.on('resize', ({ width, height, scale }) => {
|
|
1949
2165
|
this.scenes.resize(width, height);
|
|
2166
|
+
this.input.setViewportTransform(scale, this.app.stage.x, this.app.stage.y);
|
|
1950
2167
|
this.emit('resize', { width, height });
|
|
1951
2168
|
});
|
|
1952
2169
|
this.viewport.on('orientationChange', (orientation) => {
|
|
1953
2170
|
this.emit('orientationChange', orientation);
|
|
1954
2171
|
});
|
|
2172
|
+
// Wire scene changes → engine event
|
|
2173
|
+
this.scenes.on('change', ({ from, to }) => {
|
|
2174
|
+
this.emit('sceneChange', { from, to });
|
|
2175
|
+
});
|
|
1955
2176
|
// Connect ticker → scene updates
|
|
1956
2177
|
this.app.ticker.add((ticker) => {
|
|
1957
2178
|
// Always update scenes (loading screen needs onUpdate before _running=true)
|
|
@@ -1959,6 +2180,11 @@ class GameApplication extends EventEmitter {
|
|
|
1959
2180
|
});
|
|
1960
2181
|
// Trigger initial resize
|
|
1961
2182
|
this.viewport.refresh();
|
|
2183
|
+
// Enable FPS overlay in debug mode
|
|
2184
|
+
if (this.config.debug) {
|
|
2185
|
+
this.fpsOverlay = new FPSOverlay(this.app);
|
|
2186
|
+
this.fpsOverlay.show();
|
|
2187
|
+
}
|
|
1962
2188
|
}
|
|
1963
2189
|
async loadAssets(firstScene, sceneData) {
|
|
1964
2190
|
// Register built-in loading scene
|