@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/vite.esm.js
CHANGED
|
@@ -1,63 +1,68 @@
|
|
|
1
|
+
// ─── DevBridge Plugin ────────────────────────────────────
|
|
1
2
|
/**
|
|
2
|
-
* Vite plugin that injects the DevBridge
|
|
3
|
-
*
|
|
4
|
-
*
|
|
5
|
-
* allowing SDK communication to work without a real backend.
|
|
3
|
+
* Vite plugin that auto-injects the DevBridge script into the
|
|
4
|
+
* HTML during development, so the game can communicate with a
|
|
5
|
+
* mock casino host without manual setup.
|
|
6
6
|
*/
|
|
7
|
-
const
|
|
8
|
-
|
|
9
|
-
|
|
7
|
+
const VIRTUAL_ID = '/@dev-bridge-entry.js';
|
|
8
|
+
function devBridgePlugin(configPath) {
|
|
9
|
+
let entrySrc = '';
|
|
10
|
+
let resolvedConfigPath = configPath;
|
|
10
11
|
return {
|
|
11
|
-
name: 'game-engine-
|
|
12
|
-
apply: 'serve',
|
|
12
|
+
name: 'game-engine:dev-bridge',
|
|
13
|
+
apply: 'serve', // dev only
|
|
14
|
+
enforce: 'pre',
|
|
15
|
+
configResolved(config) {
|
|
16
|
+
// Resolve relative config path against Vite root so the virtual
|
|
17
|
+
// module can import it with an absolute path.
|
|
18
|
+
if (configPath.startsWith('.')) {
|
|
19
|
+
resolvedConfigPath = config.root + '/' + configPath.replace(/^\.\//, '');
|
|
20
|
+
}
|
|
21
|
+
},
|
|
13
22
|
resolveId(id) {
|
|
14
|
-
if (id ===
|
|
15
|
-
return
|
|
23
|
+
if (id === VIRTUAL_ID)
|
|
24
|
+
return id;
|
|
16
25
|
},
|
|
17
26
|
load(id) {
|
|
18
|
-
if (id ===
|
|
19
|
-
// This
|
|
20
|
-
// so bare specifiers like '@energy8platform/game-engine/debug' resolve correctly.
|
|
27
|
+
if (id === VIRTUAL_ID) {
|
|
28
|
+
// This goes through Vite's pipeline so bare imports are resolved
|
|
21
29
|
return `
|
|
22
30
|
import { DevBridge } from '@energy8platform/game-engine/debug';
|
|
23
31
|
|
|
24
|
-
|
|
25
|
-
|
|
26
|
-
|
|
27
|
-
|
|
28
|
-
|
|
29
|
-
|
|
30
|
-
// No dev config — use defaults
|
|
31
|
-
}
|
|
32
|
-
|
|
33
|
-
const bridge = new DevBridge(config);
|
|
34
|
-
bridge.start();
|
|
35
|
-
window.__devBridge = bridge;
|
|
36
|
-
console.log('[GameEngine] DevBridge started in development mode');
|
|
32
|
+
try {
|
|
33
|
+
const mod = await import('${resolvedConfigPath}');
|
|
34
|
+
const config = mod.default ?? mod.config ?? mod;
|
|
35
|
+
new DevBridge(config).start();
|
|
36
|
+
} catch (e) {
|
|
37
|
+
console.warn('[DevBridge] Failed to load config:', e);
|
|
37
38
|
}
|
|
38
39
|
|
|
39
|
-
|
|
40
|
+
await import('${entrySrc}');
|
|
40
41
|
`;
|
|
41
42
|
}
|
|
42
43
|
},
|
|
43
44
|
transformIndexHtml(html) {
|
|
44
|
-
|
|
45
|
+
// Find the app's entry module script (skip Vite internal /@... scripts)
|
|
46
|
+
const scriptRegex = /<script\s+type="module"\s+src="((?!\/@)[^"]+)"\s*>\s*<\/script>/;
|
|
47
|
+
const match = html.match(scriptRegex);
|
|
48
|
+
if (!match) {
|
|
49
|
+
console.warn('[DevBridge] Could not find entry module script in index.html');
|
|
45
50
|
return html;
|
|
46
|
-
|
|
47
|
-
|
|
48
|
-
return html.replace(
|
|
51
|
+
}
|
|
52
|
+
entrySrc = match[1];
|
|
53
|
+
return html.replace(match[0], `<script type="module" src="${VIRTUAL_ID}"></script>`);
|
|
49
54
|
},
|
|
50
55
|
};
|
|
51
56
|
}
|
|
57
|
+
// ─── defineGameConfig ────────────────────────────────────
|
|
52
58
|
/**
|
|
53
|
-
*
|
|
59
|
+
* Define a Vite configuration tailored for Energy8 casino games.
|
|
54
60
|
*
|
|
55
|
-
*
|
|
56
|
-
* -
|
|
57
|
-
* -
|
|
58
|
-
* -
|
|
59
|
-
* -
|
|
60
|
-
* - Configurable base path for S3/CDN deploy
|
|
61
|
+
* Merges sensible defaults for iGaming projects:
|
|
62
|
+
* - Build target: ESNext (required for yoga-layout WASM top-level await)
|
|
63
|
+
* - Asset inlining threshold: 8KB
|
|
64
|
+
* - Source maps for dev, none for prod
|
|
65
|
+
* - Optional DevBridge auto-injection in dev mode
|
|
61
66
|
*
|
|
62
67
|
* @example
|
|
63
68
|
* ```ts
|
|
@@ -65,58 +70,71 @@ initDevBridge();
|
|
|
65
70
|
* import { defineGameConfig } from '@energy8platform/game-engine/vite';
|
|
66
71
|
*
|
|
67
72
|
* export default defineGameConfig({
|
|
68
|
-
* base: '/
|
|
73
|
+
* base: '/',
|
|
69
74
|
* devBridge: true,
|
|
70
75
|
* });
|
|
71
76
|
* ```
|
|
72
77
|
*/
|
|
73
78
|
function defineGameConfig(config = {}) {
|
|
74
|
-
const
|
|
75
|
-
|
|
76
|
-
|
|
77
|
-
|
|
78
|
-
|
|
79
|
-
|
|
80
|
-
];
|
|
79
|
+
const plugins = [];
|
|
80
|
+
if (config.devBridge) {
|
|
81
|
+
const configPath = config.devBridgeConfig ?? './dev.config';
|
|
82
|
+
plugins.push(devBridgePlugin(configPath));
|
|
83
|
+
}
|
|
84
|
+
const userVite = config.vite ?? {};
|
|
81
85
|
return {
|
|
82
86
|
base: config.base ?? '/',
|
|
83
87
|
plugins: [
|
|
84
|
-
|
|
85
|
-
...(
|
|
88
|
+
...plugins,
|
|
89
|
+
...(userVite.plugins ?? []),
|
|
86
90
|
],
|
|
87
91
|
build: {
|
|
88
|
-
|
|
89
|
-
|
|
90
|
-
minify: 'terser',
|
|
92
|
+
target: 'esnext',
|
|
93
|
+
assetsInlineLimit: 8192,
|
|
91
94
|
sourcemap: false,
|
|
92
|
-
assetsInlineLimit: 4096,
|
|
93
95
|
rollupOptions: {
|
|
94
96
|
output: {
|
|
95
|
-
|
|
96
|
-
|
|
97
|
-
|
|
97
|
+
manualChunks: {
|
|
98
|
+
pixi: ['pixi.js'],
|
|
99
|
+
},
|
|
98
100
|
},
|
|
99
101
|
},
|
|
100
|
-
...
|
|
102
|
+
...userVite.build,
|
|
101
103
|
},
|
|
102
|
-
optimizeDeps: {
|
|
103
|
-
// Pre-bundle libs for fast dev startup
|
|
104
|
-
include: ['pixi.js'],
|
|
105
|
-
exclude: ['@esotericsoftware/spine-pixi-v8'],
|
|
106
|
-
},
|
|
107
|
-
assetsInclude: assetExtensions.map((ext) => `**/*.${ext}`),
|
|
108
104
|
server: {
|
|
109
105
|
port: 3000,
|
|
110
106
|
open: true,
|
|
111
|
-
...
|
|
107
|
+
...userVite.server,
|
|
112
108
|
},
|
|
113
109
|
resolve: {
|
|
114
|
-
|
|
110
|
+
dedupe: [
|
|
111
|
+
'pixi.js',
|
|
112
|
+
'@pixi/layout',
|
|
113
|
+
'@pixi/layout/components',
|
|
114
|
+
'@pixi/ui',
|
|
115
|
+
'yoga-layout',
|
|
116
|
+
'yoga-layout/load',
|
|
117
|
+
],
|
|
118
|
+
...userVite.resolve,
|
|
119
|
+
},
|
|
120
|
+
optimizeDeps: {
|
|
121
|
+
include: [
|
|
122
|
+
'pixi.js',
|
|
123
|
+
'@pixi/layout',
|
|
124
|
+
'@pixi/layout/components',
|
|
125
|
+
'@pixi/ui',
|
|
126
|
+
'yoga-layout/load',
|
|
127
|
+
],
|
|
128
|
+
exclude: [
|
|
129
|
+
'yoga-layout',
|
|
130
|
+
],
|
|
131
|
+
esbuildOptions: {
|
|
132
|
+
target: 'esnext',
|
|
133
|
+
},
|
|
134
|
+
...userVite.optimizeDeps,
|
|
115
135
|
},
|
|
116
|
-
// Spread any additional user overrides
|
|
117
|
-
...Object.fromEntries(Object.entries(config.vite ?? {}).filter(([key]) => !['plugins', 'build', 'server', 'resolve'].includes(key))),
|
|
118
136
|
};
|
|
119
137
|
}
|
|
120
138
|
|
|
121
|
-
export { defineGameConfig
|
|
139
|
+
export { defineGameConfig };
|
|
122
140
|
//# sourceMappingURL=vite.esm.js.map
|
package/dist/vite.esm.js.map
CHANGED
|
@@ -1 +1 @@
|
|
|
1
|
-
{"version":3,"file":"vite.esm.js","sources":["../src/vite/index.ts"],"sourcesContent":[null],"names":[],"mappings":"
|
|
1
|
+
{"version":3,"file":"vite.esm.js","sources":["../src/vite/index.ts"],"sourcesContent":[null],"names":[],"mappings":"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"}
|
package/package.json
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "@energy8platform/game-engine",
|
|
3
|
-
"version": "0.
|
|
3
|
+
"version": "0.4.0",
|
|
4
4
|
"description": "Universal casino game engine built on PixiJS v8 and @energy8platform/game-sdk",
|
|
5
5
|
"type": "module",
|
|
6
6
|
"main": "./dist/index.cjs.js",
|
|
@@ -50,8 +50,7 @@
|
|
|
50
50
|
},
|
|
51
51
|
"files": [
|
|
52
52
|
"dist",
|
|
53
|
-
"src"
|
|
54
|
-
"loading.css"
|
|
53
|
+
"src"
|
|
55
54
|
],
|
|
56
55
|
"scripts": {
|
|
57
56
|
"build": "rollup -c rollup.config.mjs",
|
|
@@ -65,7 +64,10 @@
|
|
|
65
64
|
},
|
|
66
65
|
"peerDependencies": {
|
|
67
66
|
"pixi.js": "^8.16.0",
|
|
68
|
-
"@energy8platform/game-sdk": "^2.
|
|
67
|
+
"@energy8platform/game-sdk": "^2.6.1",
|
|
68
|
+
"@pixi/ui": "^2.3.0",
|
|
69
|
+
"@pixi/layout": "^3.2.0",
|
|
70
|
+
"yoga-layout": "^3.0.0"
|
|
69
71
|
},
|
|
70
72
|
"peerDependenciesMeta": {
|
|
71
73
|
"@esotericsoftware/spine-pixi-v8": {
|
|
@@ -73,12 +75,24 @@
|
|
|
73
75
|
},
|
|
74
76
|
"@pixi/sound": {
|
|
75
77
|
"optional": true
|
|
78
|
+
},
|
|
79
|
+
"@pixi/ui": {
|
|
80
|
+
"optional": true
|
|
81
|
+
},
|
|
82
|
+
"@pixi/layout": {
|
|
83
|
+
"optional": true
|
|
84
|
+
},
|
|
85
|
+
"yoga-layout": {
|
|
86
|
+
"optional": true
|
|
76
87
|
}
|
|
77
88
|
},
|
|
78
89
|
"devDependencies": {
|
|
79
|
-
"@energy8platform/game-sdk": "
|
|
90
|
+
"@energy8platform/game-sdk": "^2.6.1",
|
|
80
91
|
"@esotericsoftware/spine-pixi-v8": "~4.2.0",
|
|
81
92
|
"@pixi/sound": "^6.0.0",
|
|
93
|
+
"@pixi/ui": "^2.3.0",
|
|
94
|
+
"@pixi/layout": "^3.2.0",
|
|
95
|
+
"yoga-layout": "^3.0.0",
|
|
82
96
|
"@rollup/plugin-typescript": "^12.1.0",
|
|
83
97
|
"@typescript-eslint/eslint-plugin": "^8.0.0",
|
|
84
98
|
"@typescript-eslint/parser": "^8.0.0",
|
|
@@ -0,0 +1,210 @@
|
|
|
1
|
+
import { AnimatedSprite, Texture, Spritesheet } from 'pixi.js';
|
|
2
|
+
|
|
3
|
+
// ─── Types ───────────────────────────────────────────────
|
|
4
|
+
|
|
5
|
+
export interface SpriteAnimationConfig {
|
|
6
|
+
/** Frames per second (default: 24) */
|
|
7
|
+
fps?: number;
|
|
8
|
+
/** Whether to loop (default: true) */
|
|
9
|
+
loop?: boolean;
|
|
10
|
+
/** Start playing immediately (default: true) */
|
|
11
|
+
autoPlay?: boolean;
|
|
12
|
+
/** Callback when animation completes (non-looping) */
|
|
13
|
+
onComplete?: () => void;
|
|
14
|
+
/** Anchor point (default: 0.5 = center) */
|
|
15
|
+
anchor?: number | { x: number; y: number };
|
|
16
|
+
}
|
|
17
|
+
|
|
18
|
+
/**
|
|
19
|
+
* Helper for creating frame-based animations from spritesheets.
|
|
20
|
+
*
|
|
21
|
+
* Wraps PixiJS `AnimatedSprite` with a convenient API for
|
|
22
|
+
* common iGaming effects: coin showers, symbol animations,
|
|
23
|
+
* sparkle trails, win celebrations.
|
|
24
|
+
*
|
|
25
|
+
* Cheaper than Spine for simple frame sequences.
|
|
26
|
+
*
|
|
27
|
+
* @example
|
|
28
|
+
* ```ts
|
|
29
|
+
* // From an array of textures
|
|
30
|
+
* const coinAnim = SpriteAnimation.create(coinTextures, {
|
|
31
|
+
* fps: 30,
|
|
32
|
+
* loop: true,
|
|
33
|
+
* });
|
|
34
|
+
* scene.addChild(coinAnim);
|
|
35
|
+
*
|
|
36
|
+
* // From a spritesheet with a naming pattern
|
|
37
|
+
* const sheet = Assets.get('effects');
|
|
38
|
+
* const sparkle = SpriteAnimation.fromSpritesheet(sheet, 'sparkle_');
|
|
39
|
+
* sparkle.play();
|
|
40
|
+
*
|
|
41
|
+
* // From a numbered range
|
|
42
|
+
* const explosion = SpriteAnimation.fromRange(sheet, 'explosion_{i}', 0, 24, {
|
|
43
|
+
* fps: 60,
|
|
44
|
+
* loop: false,
|
|
45
|
+
* onComplete: () => explosion.destroy(),
|
|
46
|
+
* });
|
|
47
|
+
* ```
|
|
48
|
+
*/
|
|
49
|
+
export class SpriteAnimation {
|
|
50
|
+
/**
|
|
51
|
+
* Create an animated sprite from an array of textures.
|
|
52
|
+
*
|
|
53
|
+
* @param textures - Array of PixiJS Textures
|
|
54
|
+
* @param config - Animation options
|
|
55
|
+
* @returns Configured AnimatedSprite
|
|
56
|
+
*/
|
|
57
|
+
static create(textures: Texture[], config: SpriteAnimationConfig = {}): AnimatedSprite {
|
|
58
|
+
const sprite = new AnimatedSprite(textures);
|
|
59
|
+
|
|
60
|
+
// Configure
|
|
61
|
+
sprite.animationSpeed = (config.fps ?? 24) / 60; // PixiJS uses speed relative to 60fps ticker
|
|
62
|
+
sprite.loop = config.loop ?? true;
|
|
63
|
+
|
|
64
|
+
// Anchor
|
|
65
|
+
if (config.anchor !== undefined) {
|
|
66
|
+
if (typeof config.anchor === 'number') {
|
|
67
|
+
sprite.anchor.set(config.anchor);
|
|
68
|
+
} else {
|
|
69
|
+
sprite.anchor.set(config.anchor.x, config.anchor.y);
|
|
70
|
+
}
|
|
71
|
+
} else {
|
|
72
|
+
sprite.anchor.set(0.5);
|
|
73
|
+
}
|
|
74
|
+
|
|
75
|
+
// Complete callback
|
|
76
|
+
if (config.onComplete) {
|
|
77
|
+
sprite.onComplete = config.onComplete;
|
|
78
|
+
}
|
|
79
|
+
|
|
80
|
+
// Auto-play
|
|
81
|
+
if (config.autoPlay !== false) {
|
|
82
|
+
sprite.play();
|
|
83
|
+
}
|
|
84
|
+
|
|
85
|
+
return sprite;
|
|
86
|
+
}
|
|
87
|
+
|
|
88
|
+
/**
|
|
89
|
+
* Create an animated sprite from a spritesheet using a name prefix.
|
|
90
|
+
*
|
|
91
|
+
* Collects all textures whose keys start with `prefix`, sorted alphabetically.
|
|
92
|
+
*
|
|
93
|
+
* @param sheet - PixiJS Spritesheet instance
|
|
94
|
+
* @param prefix - Texture name prefix (e.g., 'coin_')
|
|
95
|
+
* @param config - Animation options
|
|
96
|
+
* @returns Configured AnimatedSprite
|
|
97
|
+
*/
|
|
98
|
+
static fromSpritesheet(
|
|
99
|
+
sheet: Spritesheet,
|
|
100
|
+
prefix: string,
|
|
101
|
+
config: SpriteAnimationConfig = {},
|
|
102
|
+
): AnimatedSprite {
|
|
103
|
+
const textures = SpriteAnimation.getTexturesByPrefix(sheet, prefix);
|
|
104
|
+
|
|
105
|
+
if (textures.length === 0) {
|
|
106
|
+
console.warn(`[SpriteAnimation] No textures found with prefix "${prefix}"`);
|
|
107
|
+
}
|
|
108
|
+
|
|
109
|
+
return SpriteAnimation.create(textures, config);
|
|
110
|
+
}
|
|
111
|
+
|
|
112
|
+
/**
|
|
113
|
+
* Create an animated sprite from a numbered range of frames.
|
|
114
|
+
*
|
|
115
|
+
* The `pattern` string should contain `{i}` as a placeholder for the frame number.
|
|
116
|
+
* Numbers are zero-padded to match the length of `start`.
|
|
117
|
+
*
|
|
118
|
+
* @param sheet - PixiJS Spritesheet instance
|
|
119
|
+
* @param pattern - Frame name pattern, e.g. 'explosion_{i}'
|
|
120
|
+
* @param start - Start frame index (inclusive)
|
|
121
|
+
* @param end - End frame index (inclusive)
|
|
122
|
+
* @param config - Animation options
|
|
123
|
+
* @returns Configured AnimatedSprite
|
|
124
|
+
*/
|
|
125
|
+
static fromRange(
|
|
126
|
+
sheet: Spritesheet,
|
|
127
|
+
pattern: string,
|
|
128
|
+
start: number,
|
|
129
|
+
end: number,
|
|
130
|
+
config: SpriteAnimationConfig = {},
|
|
131
|
+
): AnimatedSprite {
|
|
132
|
+
const textures: Texture[] = [];
|
|
133
|
+
const padLength = String(end).length;
|
|
134
|
+
|
|
135
|
+
for (let i = start; i <= end; i++) {
|
|
136
|
+
const name = pattern.replace('{i}', String(i).padStart(padLength, '0'));
|
|
137
|
+
const texture = sheet.textures[name];
|
|
138
|
+
|
|
139
|
+
if (texture) {
|
|
140
|
+
textures.push(texture);
|
|
141
|
+
} else {
|
|
142
|
+
console.warn(`[SpriteAnimation] Missing frame: "${name}"`);
|
|
143
|
+
}
|
|
144
|
+
}
|
|
145
|
+
|
|
146
|
+
if (textures.length === 0) {
|
|
147
|
+
console.warn(`[SpriteAnimation] No textures found for pattern "${pattern}" [${start}..${end}]`);
|
|
148
|
+
}
|
|
149
|
+
|
|
150
|
+
return SpriteAnimation.create(textures, config);
|
|
151
|
+
}
|
|
152
|
+
|
|
153
|
+
/**
|
|
154
|
+
* Create an AnimatedSprite from texture aliases (loaded via AssetManager).
|
|
155
|
+
*
|
|
156
|
+
* @param aliases - Array of texture aliases
|
|
157
|
+
* @param config - Animation options
|
|
158
|
+
* @returns Configured AnimatedSprite
|
|
159
|
+
*/
|
|
160
|
+
static fromAliases(aliases: string[], config: SpriteAnimationConfig = {}): AnimatedSprite {
|
|
161
|
+
const textures = aliases.map((alias) => {
|
|
162
|
+
const tex = Texture.from(alias);
|
|
163
|
+
return tex;
|
|
164
|
+
});
|
|
165
|
+
|
|
166
|
+
return SpriteAnimation.create(textures, config);
|
|
167
|
+
}
|
|
168
|
+
|
|
169
|
+
/**
|
|
170
|
+
* Play a one-shot animation and auto-destroy when complete.
|
|
171
|
+
* Useful for fire-and-forget effects like coin bursts.
|
|
172
|
+
*
|
|
173
|
+
* @param textures - Array of textures
|
|
174
|
+
* @param config - Animation options (loop will be forced to false)
|
|
175
|
+
* @returns Promise that resolves when animation completes
|
|
176
|
+
*/
|
|
177
|
+
static playOnce(
|
|
178
|
+
textures: Texture[],
|
|
179
|
+
config: SpriteAnimationConfig = {},
|
|
180
|
+
): { sprite: AnimatedSprite; finished: Promise<void> } {
|
|
181
|
+
const finished = new Promise<void>((resolve) => {
|
|
182
|
+
config = {
|
|
183
|
+
...config,
|
|
184
|
+
loop: false,
|
|
185
|
+
onComplete: () => {
|
|
186
|
+
config.onComplete?.();
|
|
187
|
+
sprite.destroy();
|
|
188
|
+
resolve();
|
|
189
|
+
},
|
|
190
|
+
};
|
|
191
|
+
});
|
|
192
|
+
|
|
193
|
+
const sprite = SpriteAnimation.create(textures, config);
|
|
194
|
+
return { sprite, finished };
|
|
195
|
+
}
|
|
196
|
+
|
|
197
|
+
// ─── Utility ───────────────────────────────────────────
|
|
198
|
+
|
|
199
|
+
/**
|
|
200
|
+
* Get all textures from a spritesheet that start with a given prefix.
|
|
201
|
+
* Results are sorted alphabetically by key.
|
|
202
|
+
*/
|
|
203
|
+
static getTexturesByPrefix(sheet: Spritesheet, prefix: string): Texture[] {
|
|
204
|
+
const keys = Object.keys(sheet.textures)
|
|
205
|
+
.filter((k) => k.startsWith(prefix))
|
|
206
|
+
.sort();
|
|
207
|
+
|
|
208
|
+
return keys.map((k) => sheet.textures[k]);
|
|
209
|
+
}
|
|
210
|
+
}
|
package/src/animation/Tween.ts
CHANGED
|
@@ -117,9 +117,20 @@ export class Tween {
|
|
|
117
117
|
|
|
118
118
|
/**
|
|
119
119
|
* Wait for a given duration (useful in timelines).
|
|
120
|
+
* Uses PixiJS Ticker for consistent timing with other tweens.
|
|
120
121
|
*/
|
|
121
122
|
static delay(ms: number): Promise<void> {
|
|
122
|
-
return new Promise((resolve) =>
|
|
123
|
+
return new Promise((resolve) => {
|
|
124
|
+
let elapsed = 0;
|
|
125
|
+
const onTick = (ticker: Ticker) => {
|
|
126
|
+
elapsed += ticker.deltaMS;
|
|
127
|
+
if (elapsed >= ms) {
|
|
128
|
+
Ticker.shared.remove(onTick);
|
|
129
|
+
resolve();
|
|
130
|
+
}
|
|
131
|
+
};
|
|
132
|
+
Ticker.shared.add(onTick);
|
|
133
|
+
});
|
|
123
134
|
}
|
|
124
135
|
|
|
125
136
|
/**
|
|
@@ -150,6 +161,21 @@ export class Tween {
|
|
|
150
161
|
return Tween._tweens.length;
|
|
151
162
|
}
|
|
152
163
|
|
|
164
|
+
/**
|
|
165
|
+
* Reset the tween system — kill all tweens and remove the ticker.
|
|
166
|
+
* Useful for cleanup between game instances, tests, or hot-reload.
|
|
167
|
+
*/
|
|
168
|
+
static reset(): void {
|
|
169
|
+
for (const tw of Tween._tweens) {
|
|
170
|
+
tw.resolve();
|
|
171
|
+
}
|
|
172
|
+
Tween._tweens.length = 0;
|
|
173
|
+
if (Tween._tickerAdded) {
|
|
174
|
+
Ticker.shared.remove(Tween.tick);
|
|
175
|
+
Tween._tickerAdded = false;
|
|
176
|
+
}
|
|
177
|
+
}
|
|
178
|
+
|
|
153
179
|
// ─── Internal ──────────────────────────────────────────
|
|
154
180
|
|
|
155
181
|
private static ensureTicker(): void {
|
package/src/animation/index.ts
CHANGED
|
@@ -2,3 +2,5 @@ export { Tween } from './Tween';
|
|
|
2
2
|
export { Timeline } from './Timeline';
|
|
3
3
|
export { Easing } from './Easing';
|
|
4
4
|
export { SpineHelper } from './SpineHelper';
|
|
5
|
+
export { SpriteAnimation } from './SpriteAnimation';
|
|
6
|
+
export type { SpriteAnimationConfig } from './SpriteAnimation';
|
|
@@ -126,25 +126,45 @@ export class AudioManager {
|
|
|
126
126
|
|
|
127
127
|
const { sound } = this._soundModule;
|
|
128
128
|
|
|
129
|
-
// Stop current music
|
|
130
|
-
if (this._currentMusic) {
|
|
129
|
+
// Stop current music with fade-out, start new music with fade-in
|
|
130
|
+
if (this._currentMusic && fadeDuration > 0) {
|
|
131
|
+
const prevAlias = this._currentMusic;
|
|
132
|
+
this._currentMusic = alias;
|
|
133
|
+
|
|
134
|
+
if (this._globalMuted || this._categories.music.muted) return;
|
|
135
|
+
|
|
136
|
+
// Fade out the previous track
|
|
137
|
+
this.fadeVolume(prevAlias, this._categories.music.volume, 0, fadeDuration, () => {
|
|
138
|
+
try { sound.stop(prevAlias); } catch { /* ignore */ }
|
|
139
|
+
});
|
|
140
|
+
|
|
141
|
+
// Start new track at zero volume, fade in
|
|
131
142
|
try {
|
|
132
|
-
sound.
|
|
133
|
-
|
|
134
|
-
|
|
143
|
+
sound.play(alias, {
|
|
144
|
+
volume: 0,
|
|
145
|
+
loop: true,
|
|
146
|
+
});
|
|
147
|
+
this.fadeVolume(alias, 0, this._categories.music.volume, fadeDuration);
|
|
148
|
+
} catch (e) {
|
|
149
|
+
console.warn(`[AudioManager] Failed to play music "${alias}":`, e);
|
|
150
|
+
}
|
|
151
|
+
} else {
|
|
152
|
+
// No crossfade — instant switch
|
|
153
|
+
if (this._currentMusic) {
|
|
154
|
+
try { sound.stop(this._currentMusic); } catch { /* ignore */ }
|
|
135
155
|
}
|
|
136
|
-
}
|
|
137
156
|
|
|
138
|
-
|
|
139
|
-
|
|
157
|
+
this._currentMusic = alias;
|
|
158
|
+
if (this._globalMuted || this._categories.music.muted) return;
|
|
140
159
|
|
|
141
|
-
|
|
142
|
-
|
|
143
|
-
|
|
144
|
-
|
|
145
|
-
|
|
146
|
-
|
|
147
|
-
|
|
160
|
+
try {
|
|
161
|
+
sound.play(alias, {
|
|
162
|
+
volume: this._categories.music.volume,
|
|
163
|
+
loop: true,
|
|
164
|
+
});
|
|
165
|
+
} catch (e) {
|
|
166
|
+
console.warn(`[AudioManager] Failed to play music "${alias}":`, e);
|
|
167
|
+
}
|
|
148
168
|
}
|
|
149
169
|
}
|
|
150
170
|
|
|
@@ -293,6 +313,35 @@ export class AudioManager {
|
|
|
293
313
|
|
|
294
314
|
// ─── Private ───────────────────────────────────────────
|
|
295
315
|
|
|
316
|
+
/**
|
|
317
|
+
* Smoothly fade a sound's volume from `fromVol` to `toVol` over `durationMs`.
|
|
318
|
+
*/
|
|
319
|
+
private fadeVolume(
|
|
320
|
+
alias: string,
|
|
321
|
+
fromVol: number,
|
|
322
|
+
toVol: number,
|
|
323
|
+
durationMs: number,
|
|
324
|
+
onComplete?: () => void,
|
|
325
|
+
): void {
|
|
326
|
+
if (!this._soundModule) return;
|
|
327
|
+
const { sound } = this._soundModule;
|
|
328
|
+
const startTime = Date.now();
|
|
329
|
+
|
|
330
|
+
const tick = () => {
|
|
331
|
+
const elapsed = Date.now() - startTime;
|
|
332
|
+
const t = Math.min(elapsed / durationMs, 1);
|
|
333
|
+
const vol = fromVol + (toVol - fromVol) * t;
|
|
334
|
+
try { sound.volume(alias, vol); } catch { /* ignore */ }
|
|
335
|
+
|
|
336
|
+
if (t < 1) {
|
|
337
|
+
requestAnimationFrame(tick);
|
|
338
|
+
} else {
|
|
339
|
+
onComplete?.();
|
|
340
|
+
}
|
|
341
|
+
};
|
|
342
|
+
requestAnimationFrame(tick);
|
|
343
|
+
}
|
|
344
|
+
|
|
296
345
|
private applyVolumes(): void {
|
|
297
346
|
if (!this._soundModule) return;
|
|
298
347
|
const { sound } = this._soundModule;
|
package/src/core/EventEmitter.ts
CHANGED
|
@@ -1,6 +1,9 @@
|
|
|
1
1
|
/**
|
|
2
2
|
* Minimal typed event emitter.
|
|
3
3
|
* Used internally by GameApplication, SceneManager, AudioManager, etc.
|
|
4
|
+
*
|
|
5
|
+
* Supports `void` event types — events that carry no data can be emitted
|
|
6
|
+
* without arguments: `emitter.emit('eventName')`.
|
|
4
7
|
*/
|
|
5
8
|
// eslint-disable-next-line @typescript-eslint/no-empty-object-type
|
|
6
9
|
export class EventEmitter<TEvents extends {}> {
|
|
@@ -27,7 +30,10 @@ export class EventEmitter<TEvents extends {}> {
|
|
|
27
30
|
return this;
|
|
28
31
|
}
|
|
29
32
|
|
|
30
|
-
emit<K extends keyof TEvents>(
|
|
33
|
+
emit<K extends keyof TEvents>(
|
|
34
|
+
...args: TEvents[K] extends void ? [event: K] : [event: K, data: TEvents[K]]
|
|
35
|
+
): void {
|
|
36
|
+
const [event, data] = args as [K, TEvents[K]];
|
|
31
37
|
const handlers = this.listeners.get(event);
|
|
32
38
|
if (handlers) {
|
|
33
39
|
for (const handler of handlers) {
|