@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.
- package/README.md +1134 -0
- package/dist/animation.cjs.js +505 -0
- package/dist/animation.cjs.js.map +1 -0
- package/dist/animation.d.ts +235 -0
- package/dist/animation.esm.js +500 -0
- package/dist/animation.esm.js.map +1 -0
- package/dist/assets.cjs.js +148 -0
- package/dist/assets.cjs.js.map +1 -0
- package/dist/assets.d.ts +97 -0
- package/dist/assets.esm.js +146 -0
- package/dist/assets.esm.js.map +1 -0
- package/dist/audio.cjs.js +345 -0
- package/dist/audio.cjs.js.map +1 -0
- package/dist/audio.d.ts +135 -0
- package/dist/audio.esm.js +343 -0
- package/dist/audio.esm.js.map +1 -0
- package/dist/core.cjs.js +1832 -0
- package/dist/core.cjs.js.map +1 -0
- package/dist/core.d.ts +633 -0
- package/dist/core.esm.js +1827 -0
- package/dist/core.esm.js.map +1 -0
- package/dist/debug.cjs.js +298 -0
- package/dist/debug.cjs.js.map +1 -0
- package/dist/debug.d.ts +114 -0
- package/dist/debug.esm.js +295 -0
- package/dist/debug.esm.js.map +1 -0
- package/dist/index.cjs.js +3623 -0
- package/dist/index.cjs.js.map +1 -0
- package/dist/index.d.ts +1607 -0
- package/dist/index.esm.js +3598 -0
- package/dist/index.esm.js.map +1 -0
- package/dist/ui.cjs.js +1081 -0
- package/dist/ui.cjs.js.map +1 -0
- package/dist/ui.d.ts +387 -0
- package/dist/ui.esm.js +1072 -0
- package/dist/ui.esm.js.map +1 -0
- package/dist/vite.cjs.js +125 -0
- package/dist/vite.cjs.js.map +1 -0
- package/dist/vite.d.ts +42 -0
- package/dist/vite.esm.js +122 -0
- package/dist/vite.esm.js.map +1 -0
- package/package.json +107 -0
- package/src/animation/Easing.ts +116 -0
- package/src/animation/SpineHelper.ts +162 -0
- package/src/animation/Timeline.ts +138 -0
- package/src/animation/Tween.ts +225 -0
- package/src/animation/index.ts +4 -0
- package/src/assets/AssetManager.ts +174 -0
- package/src/assets/index.ts +2 -0
- package/src/audio/AudioManager.ts +366 -0
- package/src/audio/index.ts +1 -0
- package/src/core/EventEmitter.ts +47 -0
- package/src/core/GameApplication.ts +306 -0
- package/src/core/Scene.ts +48 -0
- package/src/core/SceneManager.ts +258 -0
- package/src/core/index.ts +4 -0
- package/src/debug/DevBridge.ts +259 -0
- package/src/debug/FPSOverlay.ts +102 -0
- package/src/debug/index.ts +3 -0
- package/src/index.ts +71 -0
- package/src/input/InputManager.ts +171 -0
- package/src/input/index.ts +1 -0
- package/src/loading/CSSPreloader.ts +155 -0
- package/src/loading/LoadingScene.ts +356 -0
- package/src/loading/index.ts +2 -0
- package/src/state/StateMachine.ts +228 -0
- package/src/state/index.ts +1 -0
- package/src/types.ts +218 -0
- package/src/ui/BalanceDisplay.ts +155 -0
- package/src/ui/Button.ts +199 -0
- package/src/ui/Label.ts +111 -0
- package/src/ui/Modal.ts +134 -0
- package/src/ui/Panel.ts +125 -0
- package/src/ui/ProgressBar.ts +121 -0
- package/src/ui/Toast.ts +124 -0
- package/src/ui/WinDisplay.ts +133 -0
- package/src/ui/index.ts +16 -0
- package/src/viewport/ViewportManager.ts +241 -0
- package/src/viewport/index.ts +1 -0
- package/src/vite/index.ts +153 -0
package/src/ui/Toast.ts
ADDED
|
@@ -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
|
+
}
|
package/src/ui/index.ts
ADDED
|
@@ -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 };
|