@energy8platform/game-engine 0.1.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 (80) hide show
  1. package/README.md +1134 -0
  2. package/dist/animation.cjs.js +505 -0
  3. package/dist/animation.cjs.js.map +1 -0
  4. package/dist/animation.d.ts +235 -0
  5. package/dist/animation.esm.js +500 -0
  6. package/dist/animation.esm.js.map +1 -0
  7. package/dist/assets.cjs.js +148 -0
  8. package/dist/assets.cjs.js.map +1 -0
  9. package/dist/assets.d.ts +97 -0
  10. package/dist/assets.esm.js +146 -0
  11. package/dist/assets.esm.js.map +1 -0
  12. package/dist/audio.cjs.js +345 -0
  13. package/dist/audio.cjs.js.map +1 -0
  14. package/dist/audio.d.ts +135 -0
  15. package/dist/audio.esm.js +343 -0
  16. package/dist/audio.esm.js.map +1 -0
  17. package/dist/core.cjs.js +1832 -0
  18. package/dist/core.cjs.js.map +1 -0
  19. package/dist/core.d.ts +633 -0
  20. package/dist/core.esm.js +1827 -0
  21. package/dist/core.esm.js.map +1 -0
  22. package/dist/debug.cjs.js +298 -0
  23. package/dist/debug.cjs.js.map +1 -0
  24. package/dist/debug.d.ts +114 -0
  25. package/dist/debug.esm.js +295 -0
  26. package/dist/debug.esm.js.map +1 -0
  27. package/dist/index.cjs.js +3623 -0
  28. package/dist/index.cjs.js.map +1 -0
  29. package/dist/index.d.ts +1607 -0
  30. package/dist/index.esm.js +3598 -0
  31. package/dist/index.esm.js.map +1 -0
  32. package/dist/ui.cjs.js +1081 -0
  33. package/dist/ui.cjs.js.map +1 -0
  34. package/dist/ui.d.ts +387 -0
  35. package/dist/ui.esm.js +1072 -0
  36. package/dist/ui.esm.js.map +1 -0
  37. package/dist/vite.cjs.js +125 -0
  38. package/dist/vite.cjs.js.map +1 -0
  39. package/dist/vite.d.ts +42 -0
  40. package/dist/vite.esm.js +122 -0
  41. package/dist/vite.esm.js.map +1 -0
  42. package/package.json +107 -0
  43. package/src/animation/Easing.ts +116 -0
  44. package/src/animation/SpineHelper.ts +162 -0
  45. package/src/animation/Timeline.ts +138 -0
  46. package/src/animation/Tween.ts +225 -0
  47. package/src/animation/index.ts +4 -0
  48. package/src/assets/AssetManager.ts +174 -0
  49. package/src/assets/index.ts +2 -0
  50. package/src/audio/AudioManager.ts +366 -0
  51. package/src/audio/index.ts +1 -0
  52. package/src/core/EventEmitter.ts +47 -0
  53. package/src/core/GameApplication.ts +306 -0
  54. package/src/core/Scene.ts +48 -0
  55. package/src/core/SceneManager.ts +258 -0
  56. package/src/core/index.ts +4 -0
  57. package/src/debug/DevBridge.ts +259 -0
  58. package/src/debug/FPSOverlay.ts +102 -0
  59. package/src/debug/index.ts +3 -0
  60. package/src/index.ts +71 -0
  61. package/src/input/InputManager.ts +171 -0
  62. package/src/input/index.ts +1 -0
  63. package/src/loading/CSSPreloader.ts +155 -0
  64. package/src/loading/LoadingScene.ts +356 -0
  65. package/src/loading/index.ts +2 -0
  66. package/src/state/StateMachine.ts +228 -0
  67. package/src/state/index.ts +1 -0
  68. package/src/types.ts +218 -0
  69. package/src/ui/BalanceDisplay.ts +155 -0
  70. package/src/ui/Button.ts +199 -0
  71. package/src/ui/Label.ts +111 -0
  72. package/src/ui/Modal.ts +134 -0
  73. package/src/ui/Panel.ts +125 -0
  74. package/src/ui/ProgressBar.ts +121 -0
  75. package/src/ui/Toast.ts +124 -0
  76. package/src/ui/WinDisplay.ts +133 -0
  77. package/src/ui/index.ts +16 -0
  78. package/src/viewport/ViewportManager.ts +241 -0
  79. package/src/viewport/index.ts +1 -0
  80. package/src/vite/index.ts +153 -0
@@ -0,0 +1,124 @@
1
+ import { Container, Graphics, Text } from 'pixi.js';
2
+ import { Tween } from '../animation/Tween';
3
+ import { Easing } from '../animation/Easing';
4
+
5
+ export type ToastType = 'info' | 'success' | 'warning' | 'error';
6
+
7
+ export interface ToastConfig {
8
+ /** Auto-dismiss after this many ms (0 = manual dismiss only) */
9
+ duration?: number;
10
+ /** Toast position from bottom */
11
+ bottomOffset?: number;
12
+ }
13
+
14
+ const TOAST_COLORS: Record<ToastType, number> = {
15
+ info: 0x3498db,
16
+ success: 0x27ae60,
17
+ warning: 0xf39c12,
18
+ error: 0xe74c3c,
19
+ };
20
+
21
+ /**
22
+ * Toast notification component for displaying transient messages.
23
+ *
24
+ * @example
25
+ * ```ts
26
+ * const toast = new Toast();
27
+ * scene.container.addChild(toast);
28
+ * await toast.show('Connection lost', 'error', 1920, 1080);
29
+ * ```
30
+ */
31
+ export class Toast extends Container {
32
+ private _bg: Graphics;
33
+ private _text: Text;
34
+ private _config: Required<ToastConfig>;
35
+ private _dismissTimeout: ReturnType<typeof setTimeout> | null = null;
36
+
37
+ constructor(config: ToastConfig = {}) {
38
+ super();
39
+
40
+ this._config = {
41
+ duration: 3000,
42
+ bottomOffset: 60,
43
+ ...config,
44
+ };
45
+
46
+ this._bg = new Graphics();
47
+ this.addChild(this._bg);
48
+
49
+ this._text = new Text({
50
+ text: '',
51
+ style: {
52
+ fontFamily: '-apple-system, BlinkMacSystemFont, "Segoe UI", Roboto, sans-serif',
53
+ fontSize: 16,
54
+ fill: 0xffffff,
55
+ },
56
+ });
57
+ this._text.anchor.set(0.5);
58
+ this.addChild(this._text);
59
+
60
+ this.visible = false;
61
+ }
62
+
63
+ /**
64
+ * Show a toast message.
65
+ */
66
+ async show(
67
+ message: string,
68
+ type: ToastType = 'info',
69
+ viewWidth?: number,
70
+ viewHeight?: number,
71
+ ): Promise<void> {
72
+ // Clear previous dismiss
73
+ if (this._dismissTimeout) {
74
+ clearTimeout(this._dismissTimeout);
75
+ }
76
+
77
+ this._text.text = message;
78
+
79
+ const padding = 20;
80
+ const width = Math.max(200, this._text.width + padding * 2);
81
+ const height = 44;
82
+ const radius = 8;
83
+
84
+ this._bg.clear();
85
+ this._bg.roundRect(-width / 2, -height / 2, width, height, radius).fill(TOAST_COLORS[type]);
86
+ this._bg.roundRect(-width / 2, -height / 2, width, height, radius)
87
+ .fill({ color: 0x000000, alpha: 0.2 });
88
+
89
+ // Position
90
+ if (viewWidth && viewHeight) {
91
+ this.x = viewWidth / 2;
92
+ this.y = viewHeight - this._config.bottomOffset;
93
+ }
94
+
95
+ this.visible = true;
96
+ this.alpha = 0;
97
+ this.y += 20;
98
+
99
+ // Animate in
100
+ await Tween.to(this, { alpha: 1, y: this.y - 20 }, 300, Easing.easeOutCubic);
101
+
102
+ // Auto-dismiss
103
+ if (this._config.duration > 0) {
104
+ this._dismissTimeout = setTimeout(() => {
105
+ this.dismiss();
106
+ }, this._config.duration);
107
+ }
108
+ }
109
+
110
+ /**
111
+ * Dismiss the toast.
112
+ */
113
+ async dismiss(): Promise<void> {
114
+ if (!this.visible) return;
115
+
116
+ if (this._dismissTimeout) {
117
+ clearTimeout(this._dismissTimeout);
118
+ this._dismissTimeout = null;
119
+ }
120
+
121
+ await Tween.to(this, { alpha: 0, y: this.y + 20 }, 200, Easing.easeInCubic);
122
+ this.visible = false;
123
+ }
124
+ }
@@ -0,0 +1,133 @@
1
+ import { Container } from 'pixi.js';
2
+ import { Label } from './Label';
3
+ import { Easing } from '../animation/Easing';
4
+
5
+ export interface WinDisplayConfig {
6
+ /** Text style overrides */
7
+ style?: Record<string, unknown>;
8
+ /** Currency code */
9
+ currency?: string;
10
+ /** Locale for number formatting */
11
+ locale?: string;
12
+ /** Countup duration in ms */
13
+ countupDuration?: number;
14
+ /** Scale pop animation on win */
15
+ popScale?: number;
16
+ }
17
+
18
+ /**
19
+ * Win amount display with countup animation.
20
+ *
21
+ * Shows a dramatic countup from 0 to the win amount, with optional
22
+ * scale pop effect — typical of slot games.
23
+ *
24
+ * @example
25
+ * ```ts
26
+ * const winDisplay = new WinDisplay({ currency: 'USD' });
27
+ * scene.container.addChild(winDisplay);
28
+ * await winDisplay.showWin(150.50); // countup animation
29
+ * winDisplay.hide();
30
+ * ```
31
+ */
32
+ export class WinDisplay extends Container {
33
+ private _label: Label;
34
+ private _config: Required<Pick<WinDisplayConfig, 'currency' | 'locale' | 'countupDuration' | 'popScale'>>;
35
+ private _cancelCountup = false;
36
+
37
+ constructor(config: WinDisplayConfig = {}) {
38
+ super();
39
+
40
+ this._config = {
41
+ currency: config.currency ?? 'USD',
42
+ locale: config.locale ?? 'en-US',
43
+ countupDuration: config.countupDuration ?? 1500,
44
+ popScale: config.popScale ?? 1.2,
45
+ };
46
+
47
+ this._label = new Label({
48
+ text: '',
49
+ style: {
50
+ fontSize: 48,
51
+ fontWeight: 'bold',
52
+ fill: 0xffd700,
53
+ stroke: { color: 0x000000, width: 3 },
54
+ ...(config.style as any),
55
+ },
56
+ });
57
+ this.addChild(this._label);
58
+
59
+ this.visible = false;
60
+ }
61
+
62
+ /**
63
+ * Show a win with countup animation.
64
+ *
65
+ * @param amount - Win amount
66
+ * @returns Promise that resolves when the animation completes
67
+ */
68
+ async showWin(amount: number): Promise<void> {
69
+ this.visible = true;
70
+ this._cancelCountup = false;
71
+ this.alpha = 1;
72
+
73
+ const duration = this._config.countupDuration;
74
+ const startTime = Date.now();
75
+
76
+ // Scale pop
77
+ this.scale.set(0.5);
78
+
79
+ return new Promise<void>((resolve) => {
80
+ const tick = () => {
81
+ if (this._cancelCountup) {
82
+ this.displayAmount(amount);
83
+ resolve();
84
+ return;
85
+ }
86
+
87
+ const elapsed = Date.now() - startTime;
88
+ const t = Math.min(elapsed / duration, 1);
89
+ const eased = Easing.easeOutCubic(t);
90
+
91
+ // Countup
92
+ const current = amount * eased;
93
+ this.displayAmount(current);
94
+
95
+ // Scale animation
96
+ const scaleT = Math.min(elapsed / 300, 1);
97
+ const scaleEased = Easing.easeOutBack(scaleT);
98
+ const targetScale = 1;
99
+ this.scale.set(0.5 + (targetScale - 0.5) * scaleEased);
100
+
101
+ if (t < 1) {
102
+ requestAnimationFrame(tick);
103
+ } else {
104
+ this.displayAmount(amount);
105
+ this.scale.set(1);
106
+ resolve();
107
+ }
108
+ };
109
+ requestAnimationFrame(tick);
110
+ });
111
+ }
112
+
113
+ /**
114
+ * Skip the countup animation and show the final amount immediately.
115
+ */
116
+ skipCountup(amount: number): void {
117
+ this._cancelCountup = true;
118
+ this.displayAmount(amount);
119
+ this.scale.set(1);
120
+ }
121
+
122
+ /**
123
+ * Hide the win display.
124
+ */
125
+ hide(): void {
126
+ this.visible = false;
127
+ this._label.text = '';
128
+ }
129
+
130
+ private displayAmount(amount: number): void {
131
+ this._label.setCurrency(amount, this._config.currency, this._config.locale);
132
+ }
133
+ }
@@ -0,0 +1,16 @@
1
+ export { Button } from './Button';
2
+ export type { ButtonConfig, ButtonState } from './Button';
3
+ export { ProgressBar } from './ProgressBar';
4
+ export type { ProgressBarConfig } from './ProgressBar';
5
+ export { Label } from './Label';
6
+ export type { LabelConfig } from './Label';
7
+ export { Panel } from './Panel';
8
+ export type { PanelConfig } from './Panel';
9
+ export { BalanceDisplay } from './BalanceDisplay';
10
+ export type { BalanceDisplayConfig } from './BalanceDisplay';
11
+ export { WinDisplay } from './WinDisplay';
12
+ export type { WinDisplayConfig } from './WinDisplay';
13
+ export { Modal } from './Modal';
14
+ export type { ModalConfig } from './Modal';
15
+ export { Toast } from './Toast';
16
+ export type { ToastConfig, ToastType } from './Toast';
@@ -0,0 +1,241 @@
1
+ import type { Application } from 'pixi.js';
2
+ import { EventEmitter } from '../core/EventEmitter';
3
+ import { ScaleMode, Orientation } from '../types';
4
+
5
+ interface ViewportConfig {
6
+ designWidth: number;
7
+ designHeight: number;
8
+ scaleMode: ScaleMode;
9
+ orientation: Orientation;
10
+ }
11
+
12
+ interface ViewportEvents {
13
+ resize: { width: number; height: number; scale: number };
14
+ orientationChange: Orientation;
15
+ }
16
+
17
+ /**
18
+ * Manages responsive scaling of the game canvas to fit its container.
19
+ *
20
+ * Supports three scale modes:
21
+ * - **FIT** — letterbox/pillarbox to maintain aspect ratio (industry standard)
22
+ * - **FILL** — fill container, crop edges
23
+ * - **STRETCH** — stretch to fill (distorts)
24
+ *
25
+ * Also handles:
26
+ * - Orientation detection (landscape/portrait)
27
+ * - Safe areas (mobile notch)
28
+ * - ResizeObserver for smooth container resizing
29
+ *
30
+ * @example
31
+ * ```ts
32
+ * const viewport = new ViewportManager(app, container, {
33
+ * designWidth: 1920,
34
+ * designHeight: 1080,
35
+ * scaleMode: ScaleMode.FIT,
36
+ * orientation: Orientation.LANDSCAPE,
37
+ * });
38
+ *
39
+ * viewport.on('resize', ({ width, height, scale }) => {
40
+ * console.log(`New size: ${width}x${height} @ ${scale}x`);
41
+ * });
42
+ * ```
43
+ */
44
+ export class ViewportManager extends EventEmitter<ViewportEvents> {
45
+ private _app: Application;
46
+ private _container: HTMLElement;
47
+ private _config: ViewportConfig;
48
+ private _resizeObserver: ResizeObserver | null = null;
49
+ private _currentOrientation: Orientation = Orientation.LANDSCAPE;
50
+ private _currentWidth = 0;
51
+ private _currentHeight = 0;
52
+ private _currentScale = 1;
53
+ private _destroyed = false;
54
+ private _resizeTimeout: number | null = null;
55
+
56
+ constructor(app: Application, container: HTMLElement, config: ViewportConfig) {
57
+ super();
58
+ this._app = app;
59
+ this._container = container;
60
+ this._config = config;
61
+
62
+ this.setupObserver();
63
+ }
64
+
65
+ /** Current canvas width in game units */
66
+ get width(): number {
67
+ return this._currentWidth;
68
+ }
69
+
70
+ /** Current canvas height in game units */
71
+ get height(): number {
72
+ return this._currentHeight;
73
+ }
74
+
75
+ /** Current scale factor */
76
+ get scale(): number {
77
+ return this._currentScale;
78
+ }
79
+
80
+ /** Current orientation */
81
+ get orientation(): Orientation {
82
+ return this._currentOrientation;
83
+ }
84
+
85
+ /** Design reference width */
86
+ get designWidth(): number {
87
+ return this._config.designWidth;
88
+ }
89
+
90
+ /** Design reference height */
91
+ get designHeight(): number {
92
+ return this._config.designHeight;
93
+ }
94
+
95
+ /**
96
+ * Force a resize calculation. Called automatically on container size change.
97
+ */
98
+ refresh(): void {
99
+ if (this._destroyed) return;
100
+
101
+ const containerWidth = this._container.clientWidth || window.innerWidth;
102
+ const containerHeight = this._container.clientHeight || window.innerHeight;
103
+
104
+ if (containerWidth === 0 || containerHeight === 0) return;
105
+
106
+ const { designWidth, designHeight, scaleMode } = this._config;
107
+ const designRatio = designWidth / designHeight;
108
+ const containerRatio = containerWidth / containerHeight;
109
+
110
+ let gameWidth: number;
111
+ let gameHeight: number;
112
+ let scale: number;
113
+
114
+ switch (scaleMode) {
115
+ case ScaleMode.FIT: {
116
+ if (containerRatio > designRatio) {
117
+ // Container is wider → pillarbox
118
+ scale = containerHeight / designHeight;
119
+ gameWidth = designWidth;
120
+ gameHeight = designHeight;
121
+ } else {
122
+ // Container is taller → letterbox
123
+ scale = containerWidth / designWidth;
124
+ gameWidth = designWidth;
125
+ gameHeight = designHeight;
126
+ }
127
+ break;
128
+ }
129
+
130
+ case ScaleMode.FILL: {
131
+ if (containerRatio > designRatio) {
132
+ // Container is wider → crop top/bottom
133
+ scale = containerWidth / designWidth;
134
+ } else {
135
+ // Container is taller → crop left/right
136
+ scale = containerHeight / designHeight;
137
+ }
138
+ gameWidth = containerWidth / scale;
139
+ gameHeight = containerHeight / scale;
140
+ break;
141
+ }
142
+
143
+ case ScaleMode.STRETCH: {
144
+ gameWidth = designWidth;
145
+ gameHeight = designHeight;
146
+ scale = 1; // stretch is handled by CSS
147
+ break;
148
+ }
149
+
150
+ default:
151
+ gameWidth = designWidth;
152
+ gameHeight = designHeight;
153
+ scale = 1;
154
+ }
155
+
156
+ // Resize the renderer
157
+ this._app.renderer.resize(
158
+ Math.round(containerWidth),
159
+ Math.round(containerHeight),
160
+ );
161
+
162
+ // Scale the stage
163
+ const stageScale = scaleMode === ScaleMode.STRETCH
164
+ ? Math.min(containerWidth / designWidth, containerHeight / designHeight)
165
+ : scale;
166
+
167
+ this._app.stage.scale.set(stageScale);
168
+
169
+ // Center the stage for FIT mode
170
+ if (scaleMode === ScaleMode.FIT) {
171
+ this._app.stage.x = Math.round((containerWidth - designWidth * stageScale) / 2);
172
+ this._app.stage.y = Math.round((containerHeight - designHeight * stageScale) / 2);
173
+ } else if (scaleMode === ScaleMode.FILL) {
174
+ this._app.stage.x = Math.round((containerWidth - gameWidth * stageScale) / 2);
175
+ this._app.stage.y = Math.round((containerHeight - gameHeight * stageScale) / 2);
176
+ } else {
177
+ this._app.stage.x = 0;
178
+ this._app.stage.y = 0;
179
+ }
180
+
181
+ this._currentWidth = gameWidth;
182
+ this._currentHeight = gameHeight;
183
+ this._currentScale = stageScale;
184
+
185
+ // Check orientation
186
+ const newOrientation =
187
+ containerWidth >= containerHeight ? Orientation.LANDSCAPE : Orientation.PORTRAIT;
188
+
189
+ if (newOrientation !== this._currentOrientation) {
190
+ this._currentOrientation = newOrientation;
191
+ this.emit('orientationChange', newOrientation);
192
+ }
193
+
194
+ this.emit('resize', {
195
+ width: gameWidth,
196
+ height: gameHeight,
197
+ scale: stageScale,
198
+ });
199
+ }
200
+
201
+ /**
202
+ * Destroy the viewport manager.
203
+ */
204
+ destroy(): void {
205
+ this._destroyed = true;
206
+ this._resizeObserver?.disconnect();
207
+ this._resizeObserver = null;
208
+ if (this._resizeTimeout !== null) {
209
+ clearTimeout(this._resizeTimeout);
210
+ }
211
+ this.removeAllListeners();
212
+ }
213
+
214
+ // ─── Private ───────────────────────────────────────────
215
+
216
+ private setupObserver(): void {
217
+ if (typeof ResizeObserver !== 'undefined') {
218
+ this._resizeObserver = new ResizeObserver(() => {
219
+ this.debouncedRefresh();
220
+ });
221
+ this._resizeObserver.observe(this._container);
222
+ } else {
223
+ // Fallback for older browsers
224
+ window.addEventListener('resize', this.onWindowResize);
225
+ }
226
+ }
227
+
228
+ private onWindowResize = (): void => {
229
+ this.debouncedRefresh();
230
+ };
231
+
232
+ private debouncedRefresh(): void {
233
+ if (this._resizeTimeout !== null) {
234
+ clearTimeout(this._resizeTimeout);
235
+ }
236
+ this._resizeTimeout = window.setTimeout(() => {
237
+ this.refresh();
238
+ this._resizeTimeout = null;
239
+ }, 16); // ~1 frame
240
+ }
241
+ }
@@ -0,0 +1 @@
1
+ export { ViewportManager } from './ViewportManager';
@@ -0,0 +1,153 @@
1
+ import type { UserConfig, Plugin } from 'vite';
2
+
3
+ export interface GameViteConfig {
4
+ /** Base path for production builds (e.g., '/games/my-slot/') */
5
+ base?: string;
6
+ /** Output directory (default: 'dist') */
7
+ outDir?: string;
8
+ /** Enable DevBridge auto-injection in dev mode */
9
+ devBridge?: boolean;
10
+ /** Asset file extensions to include */
11
+ assetExtensions?: string[];
12
+ /** Additional Vite config overrides */
13
+ vite?: UserConfig;
14
+ }
15
+
16
+ /**
17
+ * Vite plugin that injects the DevBridge mock host in development mode.
18
+ *
19
+ * In dev mode, wraps the game page to simulate the casino host environment,
20
+ * allowing SDK communication to work without a real backend.
21
+ */
22
+ const VIRTUAL_DEV_BRIDGE_ID = 'virtual:game-engine-dev-bridge';
23
+ const RESOLVED_VIRTUAL_ID = '\0' + VIRTUAL_DEV_BRIDGE_ID;
24
+
25
+ function gameEngineDevPlugin(options: { devBridge?: boolean } = {}): Plugin {
26
+ return {
27
+ name: 'game-engine-dev',
28
+ apply: 'serve',
29
+
30
+ resolveId(id) {
31
+ if (id === VIRTUAL_DEV_BRIDGE_ID) return RESOLVED_VIRTUAL_ID;
32
+ },
33
+
34
+ load(id) {
35
+ if (id === RESOLVED_VIRTUAL_ID) {
36
+ // This module goes through Vite's transform pipeline,
37
+ // so bare specifiers like '@energy8platform/game-engine/debug' resolve correctly.
38
+ return `
39
+ import { DevBridge } from '@energy8platform/game-engine/debug';
40
+
41
+ async function initDevBridge() {
42
+ let config = {};
43
+ try {
44
+ const mod = await import('/dev.config.ts');
45
+ config = mod.default || mod.devBridgeConfig || {};
46
+ } catch {
47
+ // No dev config — use defaults
48
+ }
49
+
50
+ const bridge = new DevBridge(config);
51
+ bridge.start();
52
+ window.__devBridge = bridge;
53
+ console.log('[GameEngine] DevBridge started in development mode');
54
+ }
55
+
56
+ initDevBridge();
57
+ `;
58
+ }
59
+ },
60
+
61
+ transformIndexHtml(html) {
62
+ if (options.devBridge === false) return html;
63
+
64
+ // Inject a script that imports the virtual module — Vite resolves it properly
65
+ const devScript = `<script type="module" src="/${VIRTUAL_DEV_BRIDGE_ID}"></script>`;
66
+
67
+ return html.replace('</head>', `${devScript}\n</head>`);
68
+ },
69
+ };
70
+ }
71
+
72
+ /**
73
+ * Create a pre-configured Vite config for game projects.
74
+ *
75
+ * Includes:
76
+ * - PixiJS optimization (exclude from dep optimization)
77
+ * - DevBridge injection in dev mode
78
+ * - HTML minification
79
+ * - Gzip-friendly output
80
+ * - Configurable base path for S3/CDN deploy
81
+ *
82
+ * @example
83
+ * ```ts
84
+ * // vite.config.ts
85
+ * import { defineGameConfig } from '@energy8platform/game-engine/vite';
86
+ *
87
+ * export default defineGameConfig({
88
+ * base: '/games/my-slot/',
89
+ * devBridge: true,
90
+ * });
91
+ * ```
92
+ */
93
+ export function defineGameConfig(config: GameViteConfig = {}): UserConfig {
94
+ const assetExtensions = config.assetExtensions ?? [
95
+ 'png', 'jpg', 'jpeg', 'gif', 'webp', 'avif', 'svg',
96
+ 'mp3', 'ogg', 'wav', 'webm',
97
+ 'json', 'atlas', 'skel',
98
+ 'fnt', 'ttf', 'otf', 'woff', 'woff2',
99
+ 'mp4', 'm4v',
100
+ ];
101
+
102
+ return {
103
+ base: config.base ?? '/',
104
+
105
+ plugins: [
106
+ gameEngineDevPlugin({ devBridge: config.devBridge ?? true }),
107
+ ...(config.vite?.plugins ?? []) as Plugin[],
108
+ ],
109
+
110
+ build: {
111
+ outDir: config.outDir ?? 'dist',
112
+ target: 'es2022',
113
+ minify: 'terser',
114
+ sourcemap: false,
115
+ assetsInlineLimit: 4096,
116
+ rollupOptions: {
117
+ output: {
118
+ assetFileNames: 'assets/[name]-[hash][extname]',
119
+ chunkFileNames: 'js/[name]-[hash].js',
120
+ entryFileNames: 'js/[name]-[hash].js',
121
+ },
122
+ },
123
+ ...(config.vite?.build ?? {}),
124
+ },
125
+
126
+ optimizeDeps: {
127
+ // Pre-bundle libs for fast dev startup
128
+ include: ['pixi.js'],
129
+ exclude: ['@esotericsoftware/spine-pixi-v8'],
130
+ },
131
+
132
+ assetsInclude: assetExtensions.map((ext) => `**/*.${ext}`),
133
+
134
+ server: {
135
+ port: 3000,
136
+ open: true,
137
+ ...(config.vite?.server ?? {}),
138
+ },
139
+
140
+ resolve: {
141
+ ...(config.vite?.resolve ?? {}),
142
+ },
143
+
144
+ // Spread any additional user overrides
145
+ ...Object.fromEntries(
146
+ Object.entries(config.vite ?? {}).filter(
147
+ ([key]) => !['plugins', 'build', 'server', 'resolve'].includes(key),
148
+ ),
149
+ ),
150
+ };
151
+ }
152
+
153
+ export { gameEngineDevPlugin };