@energy8platform/game-engine 0.8.0 → 0.9.1

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.
@@ -6,6 +6,7 @@ import {
6
6
  type PlayResultData,
7
7
  type SessionData,
8
8
  type PlayResultAckPayload,
9
+ type PlayParams,
9
10
  } from '@energy8platform/game-sdk';
10
11
 
11
12
  export interface DevBridgeConfig {
@@ -20,7 +21,7 @@ export interface DevBridgeConfig {
20
21
  /** Active session to resume (null = no active session) */
21
22
  session?: SessionData | null;
22
23
  /** Custom play result handler — return mock result data */
23
- onPlay?: (params: { action: string; bet: number; roundId?: string }) => Partial<PlayResultData>;
24
+ onPlay?: (params: PlayParams) => Partial<PlayResultData>;
24
25
  /** Simulated network delay in ms */
25
26
  networkDelay?: number;
26
27
  /** Enable debug logging */
@@ -101,7 +102,7 @@ export class DevBridge {
101
102
  this.handleGameReady(id);
102
103
  });
103
104
 
104
- this._bridge.on('PLAY_REQUEST', (payload: { action: string; bet: number; roundId?: string }, id?: string) => {
105
+ this._bridge.on('PLAY_REQUEST', (payload: PlayParams, id?: string) => {
105
106
  this.handlePlayRequest(payload, id);
106
107
  });
107
108
 
@@ -164,17 +165,17 @@ export class DevBridge {
164
165
  }
165
166
 
166
167
  private handlePlayRequest(
167
- payload: { action: string; bet: number; roundId?: string },
168
+ payload: PlayParams,
168
169
  id?: string,
169
170
  ): void {
170
- const { action, bet, roundId } = payload;
171
+ const { action, bet, roundId, params } = payload;
171
172
 
172
173
  // Deduct bet
173
174
  this._balance -= bet;
174
175
  this._roundCounter++;
175
176
 
176
177
  // Generate result
177
- const customResult = this._config.onPlay({ action, bet, roundId });
178
+ const customResult = this._config.onPlay({ action, bet, roundId, params });
178
179
  const totalWin = customResult.totalWin ?? (Math.random() > 0.6 ? bet * (1 + Math.random() * 10) : 0);
179
180
 
180
181
  // Credit win
@@ -0,0 +1,26 @@
1
+ import { createContext, useContext } from 'react';
2
+ import type { GameApplication } from '../core/GameApplication';
3
+ import type { AudioManager } from '../audio/AudioManager';
4
+ import type { InputManager } from '../input/InputManager';
5
+ import type { ViewportManager } from '../viewport/ViewportManager';
6
+ import type { CasinoGameSDK } from '@energy8platform/game-sdk';
7
+ import type { GameConfigData } from '@energy8platform/game-sdk';
8
+
9
+ export interface EngineContextValue {
10
+ app: GameApplication;
11
+ sdk: CasinoGameSDK | null;
12
+ audio: AudioManager;
13
+ input: InputManager;
14
+ viewport: ViewportManager;
15
+ gameConfig: GameConfigData | null;
16
+ screen: { width: number; height: number; scale: number };
17
+ isPortrait: boolean;
18
+ }
19
+
20
+ export const EngineContext = createContext<EngineContextValue | null>(null);
21
+
22
+ export function useEngine(): EngineContextValue {
23
+ const ctx = useContext(EngineContext);
24
+ if (!ctx) throw new Error('useEngine() must be used inside a ReactScene');
25
+ return ctx;
26
+ }
@@ -0,0 +1,88 @@
1
+ import { createElement } from 'react';
2
+ import type { ReactElement } from 'react';
3
+ import { Scene } from '../core/Scene';
4
+ import { createPixiRoot } from './createPixiRoot';
5
+ import type { PixiRoot } from './createPixiRoot';
6
+ import { EngineContext } from './EngineContext';
7
+ import type { EngineContextValue } from './EngineContext';
8
+ import type { GameApplication } from '../core/GameApplication';
9
+ import { Orientation } from '../types';
10
+
11
+ export abstract class ReactScene extends Scene {
12
+ private _pixiRoot: PixiRoot | null = null;
13
+ private _contextValue: EngineContextValue | null = null;
14
+
15
+ /** Subclasses implement this to return their React element tree. */
16
+ abstract render(): ReactElement;
17
+
18
+ /** Access the GameApplication instance. */
19
+ protected getApp(): GameApplication {
20
+ const app = (this as any).__engineApp;
21
+ if (!app) {
22
+ throw new Error(
23
+ '[ReactScene] No GameApplication reference. ' +
24
+ 'Ensure this scene is managed by SceneManager (not instantiated manually).',
25
+ );
26
+ }
27
+ return app;
28
+ }
29
+
30
+ override async onEnter(data?: unknown): Promise<void> {
31
+ const app = this.getApp();
32
+
33
+ this._contextValue = {
34
+ app,
35
+ sdk: app.sdk,
36
+ audio: app.audio,
37
+ input: app.input,
38
+ viewport: app.viewport,
39
+ gameConfig: app.gameConfig,
40
+ screen: {
41
+ width: app.viewport.width,
42
+ height: app.viewport.height,
43
+ scale: app.viewport.scale,
44
+ },
45
+ isPortrait: app.viewport.orientation === Orientation.PORTRAIT,
46
+ };
47
+
48
+ this._pixiRoot = createPixiRoot(this.container);
49
+ this._mountReactTree();
50
+ }
51
+
52
+ override async onExit(): Promise<void> {
53
+ this._pixiRoot?.unmount();
54
+ this._pixiRoot = null;
55
+ this._contextValue = null;
56
+ }
57
+
58
+ override onResize(width: number, height: number): void {
59
+ if (!this._contextValue) return;
60
+ const app = this.getApp();
61
+
62
+ this._contextValue = {
63
+ ...this._contextValue,
64
+ screen: { width, height, scale: app.viewport.scale },
65
+ isPortrait: height > width,
66
+ };
67
+
68
+ this._mountReactTree();
69
+ }
70
+
71
+ override onDestroy(): void {
72
+ this._pixiRoot?.unmount();
73
+ this._pixiRoot = null;
74
+ this._contextValue = null;
75
+ }
76
+
77
+ private _mountReactTree(): void {
78
+ if (!this._pixiRoot || !this._contextValue) return;
79
+
80
+ this._pixiRoot.render(
81
+ createElement(
82
+ EngineContext.Provider,
83
+ { value: this._contextValue },
84
+ this.render(),
85
+ ),
86
+ );
87
+ }
88
+ }
@@ -0,0 +1,107 @@
1
+ const RESERVED = new Set(['children', 'key', 'ref']);
2
+
3
+ const REACT_TO_PIXI_EVENTS: Record<string, string> = {
4
+ onClick: 'onclick',
5
+ onPointerDown: 'onpointerdown',
6
+ onPointerUp: 'onpointerup',
7
+ onPointerMove: 'onpointermove',
8
+ onPointerOver: 'onpointerover',
9
+ onPointerOut: 'onpointerout',
10
+ onPointerEnter: 'onpointerenter',
11
+ onPointerLeave: 'onpointerleave',
12
+ onPointerCancel: 'onpointercancel',
13
+ onPointerTap: 'onpointertap',
14
+ onPointerUpOutside: 'onpointerupoutside',
15
+ onMouseDown: 'onmousedown',
16
+ onMouseUp: 'onmouseup',
17
+ onMouseMove: 'onmousemove',
18
+ onMouseOver: 'onmouseover',
19
+ onMouseOut: 'onmouseout',
20
+ onMouseEnter: 'onmouseenter',
21
+ onMouseLeave: 'onmouseleave',
22
+ onMouseUpOutside: 'onmouseupoutside',
23
+ onTouchStart: 'ontouchstart',
24
+ onTouchEnd: 'ontouchend',
25
+ onTouchMove: 'ontouchmove',
26
+ onTouchCancel: 'ontouchcancel',
27
+ onTouchEndOutside: 'ontouchendoutside',
28
+ onWheel: 'onwheel',
29
+ onRightClick: 'onrightclick',
30
+ onRightDown: 'onrightdown',
31
+ onRightUp: 'onrightup',
32
+ onRightUpOutside: 'onrightupoutside',
33
+ onTap: 'ontap',
34
+ onGlobalpointermove: 'onglobalpointermove',
35
+ onGlobalmousemove: 'onglobalmousemove',
36
+ onGlobaltouchmove: 'onglobaltouchmove',
37
+ };
38
+
39
+ export function isEventProp(key: string): boolean {
40
+ return key in REACT_TO_PIXI_EVENTS;
41
+ }
42
+
43
+ export function hasEventProps(props: Record<string, any>): boolean {
44
+ for (const key in props) {
45
+ if (isEventProp(key)) return true;
46
+ }
47
+ return false;
48
+ }
49
+
50
+ function setNestedValue(target: any, path: string[], value: any): void {
51
+ let obj = target;
52
+ for (let i = 0; i < path.length - 1; i++) {
53
+ obj = obj[path[i]];
54
+ if (obj == null) return;
55
+ }
56
+ obj[path[path.length - 1]] = value;
57
+ }
58
+
59
+ export function applyProps(
60
+ instance: any,
61
+ newProps: Record<string, any>,
62
+ oldProps: Record<string, any> = {},
63
+ ): void {
64
+ // Remove old props not in newProps
65
+ for (const key in oldProps) {
66
+ if (RESERVED.has(key) || key in newProps) continue;
67
+
68
+ const pixiEvent = REACT_TO_PIXI_EVENTS[key];
69
+ if (pixiEvent) {
70
+ instance[pixiEvent] = null;
71
+ } else if (key === 'draw') {
72
+ // no-op: can't un-draw
73
+ } else if (key.includes('-')) {
74
+ // Nested property reset not trivially possible, skip
75
+ } else {
76
+ try {
77
+ instance[key] = undefined;
78
+ } catch {
79
+ // read-only or non-configurable
80
+ }
81
+ }
82
+ }
83
+
84
+ // Apply new props
85
+ for (const key in newProps) {
86
+ if (RESERVED.has(key)) continue;
87
+
88
+ const value = newProps[key];
89
+ const pixiEvent = REACT_TO_PIXI_EVENTS[key];
90
+
91
+ if (pixiEvent) {
92
+ instance[pixiEvent] = value;
93
+ } else if (key === 'draw' && typeof value === 'function') {
94
+ instance.clear?.();
95
+ value(instance);
96
+ } else if (key.includes('-')) {
97
+ const parts = key.split('-');
98
+ setNestedValue(instance, parts, value);
99
+ } else {
100
+ try {
101
+ instance[key] = value;
102
+ } catch {
103
+ // read-only property
104
+ }
105
+ }
106
+ }
107
+ }
@@ -0,0 +1,17 @@
1
+ /** Mutable catalogue: PascalCase name -> PixiJS constructor */
2
+ export const catalogue: Record<string, any> = {};
3
+
4
+ /**
5
+ * Register PixiJS classes for use as JSX elements.
6
+ * Keys must be PascalCase; JSX uses the camelCase equivalent.
7
+ *
8
+ * @example
9
+ * ```ts
10
+ * import { Container, Sprite, Text } from 'pixi.js';
11
+ * extend({ Container, Sprite, Text });
12
+ * // Now <container>, <sprite>, <text> work in JSX
13
+ * ```
14
+ */
15
+ export function extend(components: Record<string, any>): void {
16
+ Object.assign(catalogue, components);
17
+ }
@@ -0,0 +1,31 @@
1
+ import { ConcurrentRoot } from 'react-reconciler/constants';
2
+ import type { Container } from 'pixi.js';
3
+ import type { ReactElement } from 'react';
4
+ import { reconciler } from './reconciler';
5
+
6
+ export interface PixiRoot {
7
+ render(element: ReactElement): void;
8
+ unmount(): void;
9
+ }
10
+
11
+ export function createPixiRoot(container: Container): PixiRoot {
12
+ const fiberRoot = reconciler.createContainer(
13
+ container, // containerInfo
14
+ ConcurrentRoot, // tag
15
+ null, // hydrationCallbacks
16
+ false, // isStrictMode
17
+ null, // concurrentUpdatesByDefaultOverride
18
+ '', // identifierPrefix
19
+ (err: Error) => console.error('[PixiRoot]', err),
20
+ null, // transitionCallbacks
21
+ );
22
+
23
+ return {
24
+ render(element: ReactElement) {
25
+ reconciler.updateContainer(element, fiberRoot, null, () => {});
26
+ },
27
+ unmount() {
28
+ reconciler.updateContainer(null, fiberRoot, null, () => {});
29
+ },
30
+ };
31
+ }
@@ -0,0 +1,51 @@
1
+ import {
2
+ Container,
3
+ Sprite,
4
+ Graphics,
5
+ Text,
6
+ AnimatedSprite,
7
+ NineSliceSprite,
8
+ TilingSprite,
9
+ Mesh,
10
+ MeshPlane,
11
+ MeshRope,
12
+ MeshSimple,
13
+ BitmapText,
14
+ HTMLText,
15
+ } from 'pixi.js';
16
+ import { extend } from './catalogue';
17
+
18
+ /**
19
+ * Register all standard PixiJS display objects for JSX use.
20
+ * Call once at app startup before rendering any React scenes.
21
+ */
22
+ export function extendPixiElements(): void {
23
+ extend({
24
+ Container,
25
+ Sprite,
26
+ Graphics,
27
+ Text,
28
+ AnimatedSprite,
29
+ NineSliceSprite,
30
+ TilingSprite,
31
+ Mesh,
32
+ MeshPlane,
33
+ MeshRope,
34
+ MeshSimple,
35
+ BitmapText,
36
+ HTMLText,
37
+ });
38
+ }
39
+
40
+ /**
41
+ * Register @pixi/layout components for JSX use.
42
+ * Pass the dynamically imported module:
43
+ *
44
+ * ```ts
45
+ * const layout = await import('@pixi/layout/components');
46
+ * extendLayoutElements(layout);
47
+ * ```
48
+ */
49
+ export function extendLayoutElements(layoutModule: Record<string, any>): void {
50
+ extend(layoutModule);
51
+ }
@@ -0,0 +1,46 @@
1
+ import { useState, useEffect } from 'react';
2
+ import { useEngine } from './EngineContext';
3
+ import type { CasinoGameSDK, SessionData, GameConfigData } from '@energy8platform/game-sdk';
4
+ import type { AudioManager } from '../audio/AudioManager';
5
+ import type { InputManager } from '../input/InputManager';
6
+
7
+ export function useSDK(): CasinoGameSDK | null {
8
+ return useEngine().sdk;
9
+ }
10
+
11
+ export function useAudio(): AudioManager {
12
+ return useEngine().audio;
13
+ }
14
+
15
+ export function useInput(): InputManager {
16
+ return useEngine().input;
17
+ }
18
+
19
+ export function useViewport(): { width: number; height: number; scale: number; isPortrait: boolean } {
20
+ const { screen, isPortrait } = useEngine();
21
+ return { ...screen, isPortrait };
22
+ }
23
+
24
+ export function useBalance(): number {
25
+ const { sdk } = useEngine();
26
+ const [balance, setBalance] = useState(sdk?.balance ?? 0);
27
+
28
+ useEffect(() => {
29
+ if (!sdk) return;
30
+ const handler = (data: { balance: number }) => setBalance(data.balance);
31
+ sdk.on('balanceUpdate', handler);
32
+ return () => {
33
+ sdk.off('balanceUpdate', handler);
34
+ };
35
+ }, [sdk]);
36
+
37
+ return balance;
38
+ }
39
+
40
+ export function useSession(): SessionData | null {
41
+ return useEngine().app.session;
42
+ }
43
+
44
+ export function useGameConfig<T = GameConfigData>(): T | null {
45
+ return useEngine().gameConfig as T | null;
46
+ }
@@ -0,0 +1,23 @@
1
+ // Core
2
+ export { createPixiRoot } from './createPixiRoot';
3
+ export type { PixiRoot } from './createPixiRoot';
4
+
5
+ // Catalogue
6
+ export { extend } from './catalogue';
7
+ export { extendPixiElements, extendLayoutElements } from './extendAll';
8
+
9
+ // Scene
10
+ export { ReactScene } from './ReactScene';
11
+
12
+ // Context & Hooks
13
+ export { EngineContext, useEngine } from './EngineContext';
14
+ export type { EngineContextValue } from './EngineContext';
15
+ export {
16
+ useSDK,
17
+ useAudio,
18
+ useInput,
19
+ useViewport,
20
+ useBalance,
21
+ useSession,
22
+ useGameConfig,
23
+ } from './hooks';
@@ -0,0 +1,169 @@
1
+ import Reconciler from 'react-reconciler';
2
+ import { DefaultEventPriority } from 'react-reconciler/constants';
3
+ import { Container } from 'pixi.js';
4
+ import { catalogue } from './catalogue';
5
+ import { applyProps, hasEventProps } from './applyProps';
6
+
7
+ function toPascalCase(str: string): string {
8
+ return str.charAt(0).toUpperCase() + str.slice(1);
9
+ }
10
+
11
+ const hostConfig: Reconciler.HostConfig<
12
+ string, // Type
13
+ Record<string, any>, // Props
14
+ Container, // Container
15
+ any, // Instance
16
+ any, // TextInstance
17
+ any, // SuspenseInstance
18
+ any, // HydratableInstance
19
+ any, // PublicInstance
20
+ any, // HostContext
21
+ any, // UpdatePayload
22
+ any, // ChildSet
23
+ any, // TimeoutHandle
24
+ any // NoTimeout
25
+ > = {
26
+ isPrimaryRenderer: false,
27
+ supportsMutation: true,
28
+ supportsPersistence: false,
29
+ supportsHydration: false,
30
+
31
+ createInstance(type, props) {
32
+ const name = toPascalCase(type);
33
+ const Ctor = catalogue[name];
34
+ if (!Ctor) {
35
+ throw new Error(
36
+ `[PixiReconciler] Unknown element "<${type}>". ` +
37
+ `Call extend({ ${name} }) before rendering.`,
38
+ );
39
+ }
40
+ const instance = new Ctor();
41
+ applyProps(instance, props);
42
+
43
+ // Enable interactivity if any event prop is present
44
+ if (hasEventProps(props) && instance.eventMode === 'auto') {
45
+ instance.eventMode = 'static';
46
+ }
47
+
48
+ return instance;
49
+ },
50
+
51
+ createTextInstance() {
52
+ throw new Error(
53
+ '[PixiReconciler] Text strings are not supported. Use a <text> element.',
54
+ );
55
+ },
56
+
57
+ appendInitialChild(parent, child) {
58
+ if (child instanceof Container) parent.addChild(child);
59
+ },
60
+
61
+ appendChild(parent, child) {
62
+ if (child instanceof Container) parent.addChild(child);
63
+ },
64
+
65
+ appendChildToContainer(container, child) {
66
+ if (child instanceof Container) container.addChild(child);
67
+ },
68
+
69
+ removeChild(parent, child) {
70
+ if (child instanceof Container) {
71
+ parent.removeChild(child);
72
+ child.destroy({ children: true });
73
+ }
74
+ },
75
+
76
+ removeChildFromContainer(container, child) {
77
+ if (child instanceof Container) {
78
+ container.removeChild(child);
79
+ child.destroy({ children: true });
80
+ }
81
+ },
82
+
83
+ insertBefore(parent, child, beforeChild) {
84
+ if (child instanceof Container && beforeChild instanceof Container) {
85
+ if (child.parent) child.parent.removeChild(child);
86
+ const index = parent.getChildIndex(beforeChild);
87
+ parent.addChildAt(child, index);
88
+ }
89
+ },
90
+
91
+ insertInContainerBefore(container, child, beforeChild) {
92
+ if (child instanceof Container && beforeChild instanceof Container) {
93
+ if (child.parent) child.parent.removeChild(child);
94
+ const index = container.getChildIndex(beforeChild);
95
+ container.addChildAt(child, index);
96
+ }
97
+ },
98
+
99
+ commitUpdate(instance, _updatePayload, _type, oldProps, newProps) {
100
+ applyProps(instance, newProps, oldProps);
101
+
102
+ if (hasEventProps(newProps) && instance.eventMode === 'auto') {
103
+ instance.eventMode = 'static';
104
+ }
105
+ },
106
+
107
+ finalizeInitialChildren() {
108
+ return false;
109
+ },
110
+
111
+ prepareUpdate() {
112
+ return true;
113
+ },
114
+
115
+ shouldSetTextContent() {
116
+ return false;
117
+ },
118
+
119
+ getRootHostContext() {
120
+ return null;
121
+ },
122
+
123
+ getChildHostContext(parentHostContext: any) {
124
+ return parentHostContext;
125
+ },
126
+
127
+ getPublicInstance(instance: any) {
128
+ return instance;
129
+ },
130
+
131
+ prepareForCommit() {
132
+ return null;
133
+ },
134
+
135
+ resetAfterCommit() {},
136
+
137
+ preparePortalMount() {},
138
+
139
+ scheduleTimeout: setTimeout,
140
+ cancelTimeout: clearTimeout,
141
+ noTimeout: -1,
142
+
143
+ getCurrentEventPriority() {
144
+ return DefaultEventPriority;
145
+ },
146
+
147
+ hideInstance(instance) {
148
+ instance.visible = false;
149
+ },
150
+
151
+ unhideInstance(instance) {
152
+ instance.visible = true;
153
+ },
154
+
155
+ hideTextInstance() {},
156
+ unhideTextInstance() {},
157
+
158
+ clearContainer() {},
159
+
160
+ detachDeletedInstance() {},
161
+
162
+ prepareScopeUpdate() {},
163
+ getInstanceFromNode() { return null; },
164
+ getInstanceFromScope() { return null; },
165
+ beforeActiveInstanceBlur() {},
166
+ afterActiveInstanceBlur() {},
167
+ };
168
+
169
+ export const reconciler = Reconciler(hostConfig);
package/src/types.ts CHANGED
@@ -150,6 +150,9 @@ export interface IScene {
150
150
  /** Root display container for this scene */
151
151
  readonly container: Container;
152
152
 
153
+ /** @internal GameApplication reference — set by SceneManager */
154
+ __engineApp?: any;
155
+
153
156
  /** Called when the scene is entered */
154
157
  onEnter?(data?: unknown): Promise<void> | void;
155
158
 
package/src/vite/index.ts CHANGED
@@ -149,6 +149,9 @@ export function defineGameConfig(config: GameConfig = {}): UserConfig {
149
149
  '@pixi/ui',
150
150
  'yoga-layout',
151
151
  'yoga-layout/load',
152
+ 'react',
153
+ 'react-dom',
154
+ 'react-reconciler',
152
155
  ],
153
156
  ...userVite.resolve,
154
157
  },
@@ -160,6 +163,8 @@ export function defineGameConfig(config: GameConfig = {}): UserConfig {
160
163
  '@pixi/layout/components',
161
164
  '@pixi/ui',
162
165
  'yoga-layout/load',
166
+ 'react',
167
+ 'react-dom',
163
168
  ],
164
169
  exclude: [
165
170
  'yoga-layout',