@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.
Files changed (59) hide show
  1. package/README.md +318 -49
  2. package/dist/animation.cjs.js +191 -1
  3. package/dist/animation.cjs.js.map +1 -1
  4. package/dist/animation.d.ts +117 -1
  5. package/dist/animation.esm.js +192 -3
  6. package/dist/animation.esm.js.map +1 -1
  7. package/dist/audio.cjs.js +66 -16
  8. package/dist/audio.cjs.js.map +1 -1
  9. package/dist/audio.d.ts +4 -0
  10. package/dist/audio.esm.js +66 -16
  11. package/dist/audio.esm.js.map +1 -1
  12. package/dist/core.cjs.js +310 -84
  13. package/dist/core.cjs.js.map +1 -1
  14. package/dist/core.d.ts +60 -1
  15. package/dist/core.esm.js +311 -85
  16. package/dist/core.esm.js.map +1 -1
  17. package/dist/debug.cjs.js +36 -68
  18. package/dist/debug.cjs.js.map +1 -1
  19. package/dist/debug.d.ts +4 -6
  20. package/dist/debug.esm.js +36 -68
  21. package/dist/debug.esm.js.map +1 -1
  22. package/dist/index.cjs.js +1250 -251
  23. package/dist/index.cjs.js.map +1 -1
  24. package/dist/index.d.ts +386 -41
  25. package/dist/index.esm.js +1250 -254
  26. package/dist/index.esm.js.map +1 -1
  27. package/dist/ui.cjs.js +757 -1
  28. package/dist/ui.cjs.js.map +1 -1
  29. package/dist/ui.d.ts +208 -2
  30. package/dist/ui.esm.js +756 -2
  31. package/dist/ui.esm.js.map +1 -1
  32. package/dist/vite.cjs.js +65 -68
  33. package/dist/vite.cjs.js.map +1 -1
  34. package/dist/vite.d.ts +17 -23
  35. package/dist/vite.esm.js +66 -68
  36. package/dist/vite.esm.js.map +1 -1
  37. package/package.json +4 -5
  38. package/src/animation/SpriteAnimation.ts +210 -0
  39. package/src/animation/Tween.ts +27 -1
  40. package/src/animation/index.ts +2 -0
  41. package/src/audio/AudioManager.ts +64 -15
  42. package/src/core/EventEmitter.ts +7 -1
  43. package/src/core/GameApplication.ts +25 -7
  44. package/src/core/SceneManager.ts +3 -1
  45. package/src/debug/DevBridge.ts +49 -80
  46. package/src/index.ts +6 -0
  47. package/src/input/InputManager.ts +26 -0
  48. package/src/loading/CSSPreloader.ts +7 -33
  49. package/src/loading/LoadingScene.ts +17 -41
  50. package/src/loading/index.ts +1 -0
  51. package/src/loading/logo.ts +95 -0
  52. package/src/types.ts +4 -0
  53. package/src/ui/BalanceDisplay.ts +14 -0
  54. package/src/ui/Button.ts +1 -1
  55. package/src/ui/Layout.ts +364 -0
  56. package/src/ui/ScrollContainer.ts +557 -0
  57. package/src/ui/index.ts +4 -0
  58. package/src/viewport/ViewportManager.ts +2 -0
  59. package/src/vite/index.ts +83 -83
package/dist/vite.esm.js CHANGED
@@ -1,63 +1,68 @@
1
+ // ─── DevBridge Plugin ────────────────────────────────────
1
2
  /**
2
- * Vite plugin that injects the DevBridge mock host in development mode.
3
- *
4
- * In dev mode, wraps the game page to simulate the casino host environment,
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 VIRTUAL_DEV_BRIDGE_ID = 'virtual:game-engine-dev-bridge';
8
- const RESOLVED_VIRTUAL_ID = '\0' + VIRTUAL_DEV_BRIDGE_ID;
9
- function gameEngineDevPlugin(options = {}) {
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-dev',
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 === VIRTUAL_DEV_BRIDGE_ID)
15
- return RESOLVED_VIRTUAL_ID;
23
+ if (id === VIRTUAL_ID)
24
+ return id;
16
25
  },
17
26
  load(id) {
18
- if (id === RESOLVED_VIRTUAL_ID) {
19
- // This module goes through Vite's transform pipeline,
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
- async function initDevBridge() {
25
- let config = {};
26
- try {
27
- const mod = await import('/dev.config.ts');
28
- config = mod.default || mod.devBridgeConfig || {};
29
- } catch {
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
- initDevBridge();
40
+ await import('${entrySrc}');
40
41
  `;
41
42
  }
42
43
  },
43
44
  transformIndexHtml(html) {
44
- if (options.devBridge === false)
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
- // Inject a script that imports the virtual module — Vite resolves it properly
47
- const devScript = `<script type="module" src="/${VIRTUAL_DEV_BRIDGE_ID}"></script>`;
48
- return html.replace('</head>', `${devScript}\n</head>`);
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
- * Create a pre-configured Vite config for game projects.
59
+ * Define a Vite configuration tailored for Energy8 casino games.
54
60
  *
55
- * Includes:
56
- * - PixiJS optimization (exclude from dep optimization)
57
- * - DevBridge injection in dev mode
58
- * - HTML minification
59
- * - Gzip-friendly output
60
- * - Configurable base path for S3/CDN deploy
61
+ * Merges sensible defaults for iGaming projects:
62
+ * - Build target: ES2020+
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,51 @@ initDevBridge();
65
70
  * import { defineGameConfig } from '@energy8platform/game-engine/vite';
66
71
  *
67
72
  * export default defineGameConfig({
68
- * base: '/games/my-slot/',
73
+ * base: '/',
69
74
  * devBridge: true,
70
75
  * });
71
76
  * ```
72
77
  */
73
78
  function defineGameConfig(config = {}) {
74
- const assetExtensions = config.assetExtensions ?? [
75
- 'png', 'jpg', 'jpeg', 'gif', 'webp', 'avif', 'svg',
76
- 'mp3', 'ogg', 'wav', 'webm',
77
- 'json', 'atlas', 'skel',
78
- 'fnt', 'ttf', 'otf', 'woff', 'woff2',
79
- 'mp4', 'm4v',
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
- gameEngineDevPlugin({ devBridge: config.devBridge ?? true }),
85
- ...(config.vite?.plugins ?? []),
88
+ ...plugins,
89
+ ...(userVite.plugins ?? []),
86
90
  ],
87
91
  build: {
88
- outDir: config.outDir ?? 'dist',
89
- target: 'es2022',
90
- minify: 'terser',
92
+ target: 'es2020',
93
+ assetsInlineLimit: 8192,
91
94
  sourcemap: false,
92
- assetsInlineLimit: 4096,
93
95
  rollupOptions: {
94
96
  output: {
95
- assetFileNames: 'assets/[name]-[hash][extname]',
96
- chunkFileNames: 'js/[name]-[hash].js',
97
- entryFileNames: 'js/[name]-[hash].js',
97
+ manualChunks: {
98
+ pixi: ['pixi.js'],
99
+ },
98
100
  },
99
101
  },
100
- ...(config.vite?.build ?? {}),
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
- ...(config.vite?.server ?? {}),
107
+ ...userVite.server,
112
108
  },
113
109
  resolve: {
114
- ...(config.vite?.resolve ?? {}),
110
+ ...userVite.resolve,
111
+ },
112
+ optimizeDeps: {
113
+ include: ['pixi.js'],
114
+ ...userVite.optimizeDeps,
115
115
  },
116
- // Spread any additional user overrides
117
- ...Object.fromEntries(Object.entries(config.vite ?? {}).filter(([key]) => !['plugins', 'build', 'server', 'resolve'].includes(key))),
118
116
  };
119
117
  }
120
118
 
121
- export { defineGameConfig, gameEngineDevPlugin };
119
+ export { defineGameConfig };
122
120
  //# sourceMappingURL=vite.esm.js.map
@@ -1 +1 @@
1
- {"version":3,"file":"vite.esm.js","sources":["../src/vite/index.ts"],"sourcesContent":[null],"names":[],"mappings":"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"}
1
+ {"version":3,"file":"vite.esm.js","sources":["../src/vite/index.ts"],"sourcesContent":[null],"names":[],"mappings":"AAkBA;AAEA;;;;AAIG;AACH,MAAM,UAAU,GAAG,uBAAuB;AAE1C,SAAS,eAAe,CAAC,UAAkB,EAAA;IACzC,IAAI,QAAQ,GAAG,EAAE;IACjB,IAAI,kBAAkB,GAAG,UAAU;IAEnC,OAAO;AACL,QAAA,IAAI,EAAE,wBAAwB;QAC9B,KAAK,EAAE,OAAO;AACd,QAAA,OAAO,EAAE,KAAK;AAEd,QAAA,cAAc,CAAC,MAAM,EAAA;;;AAGnB,YAAA,IAAI,UAAU,CAAC,UAAU,CAAC,GAAG,CAAC,EAAE;AAC9B,gBAAA,kBAAkB,GAAG,MAAM,CAAC,IAAI,GAAG,GAAG,GAAG,UAAU,CAAC,OAAO,CAAC,OAAO,EAAE,EAAE,CAAC;YAC1E;QACF,CAAC;AAED,QAAA,SAAS,CAAC,EAAE,EAAA;YACV,IAAI,EAAE,KAAK,UAAU;AAAE,gBAAA,OAAO,EAAE;QAClC,CAAC;AAED,QAAA,IAAI,CAAC,EAAE,EAAA;AACL,YAAA,IAAI,EAAE,KAAK,UAAU,EAAE;;gBAErB,OAAO;;;;8BAIe,kBAAkB,CAAA;;;;;;;gBAOhC,QAAQ,CAAA;CACvB;YACK;QACF,CAAC;AAED,QAAA,kBAAkB,CAAC,IAAI,EAAA;;YAErB,MAAM,WAAW,GAAG,iEAAiE;YACrF,MAAM,KAAK,GAAG,IAAI,CAAC,KAAK,CAAC,WAAW,CAAC;YAErC,IAAI,CAAC,KAAK,EAAE;AACV,gBAAA,OAAO,CAAC,IAAI,CAAC,8DAA8D,CAAC;AAC5E,gBAAA,OAAO,IAAI;YACb;AAEA,YAAA,QAAQ,GAAG,KAAK,CAAC,CAAC,CAAC;AACnB,YAAA,OAAO,IAAI,CAAC,OAAO,CAAC,KAAK,CAAC,CAAC,CAAC,EAAE,CAAA,2BAAA,EAA8B,UAAU,CAAA,WAAA,CAAa,CAAC;QACtF,CAAC;KACF;AACH;AAEA;AAEA;;;;;;;;;;;;;;;;;;;AAmBG;AACG,SAAU,gBAAgB,CAAC,MAAA,GAAqB,EAAE,EAAA;IACtD,MAAM,OAAO,GAAa,EAAE;AAE5B,IAAA,IAAI,MAAM,CAAC,SAAS,EAAE;AACpB,QAAA,MAAM,UAAU,GAAG,MAAM,CAAC,eAAe,IAAI,cAAc;QAC3D,OAAO,CAAC,IAAI,CAAC,eAAe,CAAC,UAAU,CAAC,CAAC;IAC3C;AAEA,IAAA,MAAM,QAAQ,GAAG,MAAM,CAAC,IAAI,IAAI,EAAE;IAElC,OAAO;AACL,QAAA,IAAI,EAAE,MAAM,CAAC,IAAI,IAAI,GAAG;AAExB,QAAA,OAAO,EAAE;AACP,YAAA,GAAG,OAAO;AACV,YAAA,IAAK,QAAQ,CAAC,OAAoB,IAAI,EAAE,CAAC;AAC1C,SAAA;AAED,QAAA,KAAK,EAAE;AACL,YAAA,MAAM,EAAE,QAAQ;AAChB,YAAA,iBAAiB,EAAE,IAAI;AACvB,YAAA,SAAS,EAAE,KAAK;AAChB,YAAA,aAAa,EAAE;AACb,gBAAA,MAAM,EAAE;AACN,oBAAA,YAAY,EAAE;wBACZ,IAAI,EAAE,CAAC,SAAS,CAAC;AAClB,qBAAA;AACF,iBAAA;AACF,aAAA;YACD,GAAG,QAAQ,CAAC,KAAK;AAClB,SAAA;AAED,QAAA,MAAM,EAAE;AACN,YAAA,IAAI,EAAE,IAAI;AACV,YAAA,IAAI,EAAE,IAAI;YACV,GAAG,QAAQ,CAAC,MAAM;AACnB,SAAA;AAED,QAAA,OAAO,EAAE;YACP,GAAG,QAAQ,CAAC,OAAO;AACpB,SAAA;AAED,QAAA,YAAY,EAAE;YACZ,OAAO,EAAE,CAAC,SAAS,CAAC;YACpB,GAAG,QAAQ,CAAC,YAAY;AACzB,SAAA;KACF;AACH;;;;"}
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@energy8platform/game-engine",
3
- "version": "0.2.0",
3
+ "version": "0.3.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,7 @@
65
64
  },
66
65
  "peerDependencies": {
67
66
  "pixi.js": "^8.16.0",
68
- "@energy8platform/game-sdk": "^2.5.0"
67
+ "@energy8platform/game-sdk": "^2.6.1"
69
68
  },
70
69
  "peerDependenciesMeta": {
71
70
  "@esotericsoftware/spine-pixi-v8": {
@@ -76,7 +75,7 @@
76
75
  }
77
76
  },
78
77
  "devDependencies": {
79
- "@energy8platform/game-sdk": "file:../energy8-platform-game-sdk",
78
+ "@energy8platform/game-sdk": "^2.6.1",
80
79
  "@esotericsoftware/spine-pixi-v8": "~4.2.0",
81
80
  "@pixi/sound": "^6.0.0",
82
81
  "@rollup/plugin-typescript": "^12.1.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
+ }
@@ -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) => setTimeout(resolve, ms));
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 {
@@ -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.stop(this._currentMusic);
133
- } catch {
134
- // ignore
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
- this._currentMusic = alias;
139
- if (this._globalMuted || this._categories.music.muted) return;
157
+ this._currentMusic = alias;
158
+ if (this._globalMuted || this._categories.music.muted) return;
140
159
 
141
- try {
142
- sound.play(alias, {
143
- volume: this._categories.music.volume,
144
- loop: true,
145
- });
146
- } catch (e) {
147
- console.warn(`[AudioManager] Failed to play music "${alias}":`, e);
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;
@@ -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>(event: K, data: TEvents[K]): void {
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) {