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