@energy8platform/game-engine 0.20.0 → 0.22.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 +121 -0
- package/dist/animation.cjs.js +18 -3
- package/dist/animation.cjs.js.map +1 -1
- package/dist/animation.esm.js +18 -3
- package/dist/animation.esm.js.map +1 -1
- package/dist/core.cjs.js +18 -3
- package/dist/core.cjs.js.map +1 -1
- package/dist/core.esm.js +18 -3
- package/dist/core.esm.js.map +1 -1
- package/dist/host.cjs.js +18 -3
- package/dist/host.cjs.js.map +1 -1
- package/dist/host.d.ts +3 -5
- package/dist/host.esm.js +18 -3
- package/dist/host.esm.js.map +1 -1
- package/dist/index.cjs.js +18 -2535
- package/dist/index.cjs.js.map +1 -1
- package/dist/index.d.ts +3 -1020
- package/dist/index.esm.js +20 -2525
- package/dist/index.esm.js.map +1 -1
- package/dist/slot.cjs.js +1872 -61
- package/dist/slot.cjs.js.map +1 -1
- package/dist/slot.d.ts +555 -28
- package/dist/slot.esm.js +1857 -62
- package/dist/slot.esm.js.map +1 -1
- package/dist/vite.cjs.js +0 -5
- package/dist/vite.cjs.js.map +1 -1
- package/dist/vite.esm.js +0 -5
- package/dist/vite.esm.js.map +1 -1
- package/package.json +2 -35
- package/src/animation/Tween.ts +14 -3
- package/src/host/index.ts +0 -1
- package/src/host/types.ts +0 -3
- package/src/index.ts +0 -28
- package/src/slot/anim/easing-map.ts +16 -2
- package/src/slot/cascade/TumbleController.ts +230 -0
- package/src/slot/config/ReelSystemConfig.ts +559 -0
- package/src/slot/config/presets.ts +222 -0
- package/src/slot/features/extra.ts +189 -0
- package/src/slot/features/index.ts +44 -0
- package/src/slot/features/symbols.ts +183 -0
- package/src/slot/features/types.ts +136 -0
- package/src/slot/features/wilds.ts +151 -0
- package/src/slot/grid/ReelGrid.ts +93 -24
- package/src/slot/grid/SymbolCell.ts +45 -11
- package/src/slot/index.ts +61 -2
- package/src/slot/motion/AnticipationController.ts +96 -0
- package/src/slot/motion/SpinEngine.ts +384 -0
- package/src/slot/system/ReelSystem.ts +295 -0
- package/src/vite/index.ts +0 -5
- package/dist/react-jsx.cjs.js +0 -3
- package/dist/react-jsx.cjs.js.map +0 -1
- package/dist/react-jsx.d.ts +0 -602
- package/dist/react-jsx.esm.js +0 -2
- package/dist/react-jsx.esm.js.map +0 -1
- package/dist/react.cjs.js +0 -3787
- package/dist/react.cjs.js.map +0 -1
- package/dist/react.d.ts +0 -1467
- package/dist/react.esm.js +0 -3771
- package/dist/react.esm.js.map +0 -1
- package/dist/ui.cjs.js +0 -2999
- package/dist/ui.cjs.js.map +0 -1
- package/dist/ui.d.ts +0 -1116
- package/dist/ui.esm.js +0 -2983
- package/dist/ui.esm.js.map +0 -1
- package/src/react/EngineContext.ts +0 -26
- package/src/react/ReactScene.ts +0 -88
- package/src/react/applyProps.ts +0 -271
- package/src/react/catalogue.ts +0 -17
- package/src/react/createPixiRoot.ts +0 -31
- package/src/react/extendAll.ts +0 -74
- package/src/react/hooks.ts +0 -46
- package/src/react/index.ts +0 -29
- package/src/react/jsx-runtime.ts +0 -346
- package/src/react/reconciler.ts +0 -338
- package/src/slot/freeSpins/FreeSpinsSession.ts +0 -40
- package/src/state/StateMachine.ts +0 -231
- package/src/state/index.ts +0 -1
- package/src/ui/BalanceDisplay.ts +0 -159
- package/src/ui/Button.ts +0 -304
- package/src/ui/FlexContainer.ts +0 -775
- package/src/ui/Label.ts +0 -124
- package/src/ui/LabelValue.ts +0 -122
- package/src/ui/Layout.ts +0 -291
- package/src/ui/Modal.ts +0 -170
- package/src/ui/Panel.ts +0 -204
- package/src/ui/ProgressBar.ts +0 -170
- package/src/ui/ScrollContainer.ts +0 -478
- package/src/ui/Slider.ts +0 -241
- package/src/ui/Toast.ts +0 -150
- package/src/ui/Toggle.ts +0 -201
- package/src/ui/WinDisplay.ts +0 -145
- package/src/ui/index.ts +0 -33
- package/src/ui/view.ts +0 -28
package/src/react/reconciler.ts
DELETED
|
@@ -1,338 +0,0 @@
|
|
|
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, applyContainerProps, hasEventProps, extractConfig, diffConfig, applyEventProps } from './applyProps';
|
|
6
|
-
import { FlexContainer } from '../ui/FlexContainer';
|
|
7
|
-
import type { FlexItemConfig } from '../ui/FlexContainer';
|
|
8
|
-
|
|
9
|
-
/** Flex item prop names that should be forwarded to _flexConfig on the child */
|
|
10
|
-
const FLEX_ITEM_PROPS = ['flexGrow', 'flexShrink', 'layoutWidth', 'layoutHeight', 'alignSelf', 'flexExclude', 'top', 'right', 'bottom', 'left', 'centerX', 'centerY'] as const;
|
|
11
|
-
|
|
12
|
-
/** Any component with suspendLayout/resumeLayout (FlexContainer, Panel, etc.) */
|
|
13
|
-
interface Suspendable {
|
|
14
|
-
suspendLayout(): void;
|
|
15
|
-
resumeLayout(): void;
|
|
16
|
-
}
|
|
17
|
-
|
|
18
|
-
function isSuspendable(obj: any): obj is Suspendable {
|
|
19
|
-
return typeof obj?.suspendLayout === 'function' && typeof obj?.resumeLayout === 'function';
|
|
20
|
-
}
|
|
21
|
-
|
|
22
|
-
/** Layout containers that need flush after commit phase */
|
|
23
|
-
const pendingLayoutFlush = new Set<Suspendable>();
|
|
24
|
-
|
|
25
|
-
/** Suspend a layout container and all its suspendable ancestors, add them to pending flush */
|
|
26
|
-
function suspendWithAncestors(instance: any): void {
|
|
27
|
-
let current = instance;
|
|
28
|
-
while (current) {
|
|
29
|
-
if (isSuspendable(current)) {
|
|
30
|
-
current.suspendLayout();
|
|
31
|
-
pendingLayoutFlush.add(current);
|
|
32
|
-
}
|
|
33
|
-
current = current.parent;
|
|
34
|
-
}
|
|
35
|
-
}
|
|
36
|
-
|
|
37
|
-
/** Extract FlexItemConfig from props if any flex item props are present */
|
|
38
|
-
function extractFlexItemConfig(props: Record<string, any>): FlexItemConfig | undefined {
|
|
39
|
-
let config: FlexItemConfig | undefined;
|
|
40
|
-
for (const key of FLEX_ITEM_PROPS) {
|
|
41
|
-
if (key in props) {
|
|
42
|
-
if (!config) config = {};
|
|
43
|
-
(config as any)[key] = props[key];
|
|
44
|
-
}
|
|
45
|
-
}
|
|
46
|
-
return config;
|
|
47
|
-
}
|
|
48
|
-
|
|
49
|
-
/** Apply flex item config to a child being added to a FlexContainer */
|
|
50
|
-
function addChildToFlex(parent: FlexContainer, child: Container & { _flexConfig?: FlexItemConfig }): void {
|
|
51
|
-
const flexConfig = child._flexConfig;
|
|
52
|
-
if (flexConfig && Object.keys(flexConfig).length > 0) {
|
|
53
|
-
parent.addFlexChild(child, flexConfig);
|
|
54
|
-
} else {
|
|
55
|
-
parent.addChild(child);
|
|
56
|
-
}
|
|
57
|
-
}
|
|
58
|
-
|
|
59
|
-
function toPascalCase(str: string): string {
|
|
60
|
-
return str.charAt(0).toUpperCase() + str.slice(1);
|
|
61
|
-
}
|
|
62
|
-
|
|
63
|
-
const hostConfig: Reconciler.HostConfig<
|
|
64
|
-
string, // Type
|
|
65
|
-
Record<string, any>, // Props
|
|
66
|
-
Container, // Container
|
|
67
|
-
any, // Instance
|
|
68
|
-
any, // TextInstance
|
|
69
|
-
any, // SuspenseInstance
|
|
70
|
-
any, // HydratableInstance
|
|
71
|
-
any, // PublicInstance
|
|
72
|
-
any, // HostContext
|
|
73
|
-
any, // UpdatePayload
|
|
74
|
-
any, // ChildSet
|
|
75
|
-
any, // TimeoutHandle
|
|
76
|
-
any // NoTimeout
|
|
77
|
-
> = {
|
|
78
|
-
isPrimaryRenderer: false,
|
|
79
|
-
supportsMutation: true,
|
|
80
|
-
supportsPersistence: false,
|
|
81
|
-
supportsHydration: false,
|
|
82
|
-
|
|
83
|
-
createInstance(type, props) {
|
|
84
|
-
const name = toPascalCase(type);
|
|
85
|
-
const Ctor = catalogue[name];
|
|
86
|
-
if (!Ctor) {
|
|
87
|
-
throw new Error(
|
|
88
|
-
`[PixiReconciler] Unknown element "<${type}>". ` +
|
|
89
|
-
`Call extend({ ${name} }) before rendering.`,
|
|
90
|
-
);
|
|
91
|
-
}
|
|
92
|
-
|
|
93
|
-
let instance;
|
|
94
|
-
if (typeof Ctor.prototype.updateConfig === 'function') {
|
|
95
|
-
// Config-based UI component: pass props as constructor config
|
|
96
|
-
const config = extractConfig(props);
|
|
97
|
-
instance = new Ctor(config);
|
|
98
|
-
applyContainerProps(instance, props);
|
|
99
|
-
applyEventProps(instance, props);
|
|
100
|
-
} else {
|
|
101
|
-
// Standard PixiJS element
|
|
102
|
-
instance = new Ctor();
|
|
103
|
-
applyProps(instance, props);
|
|
104
|
-
}
|
|
105
|
-
|
|
106
|
-
if (hasEventProps(props) && instance.eventMode === 'auto') {
|
|
107
|
-
instance.eventMode = 'static';
|
|
108
|
-
}
|
|
109
|
-
|
|
110
|
-
// Store flex item config for when this child is added to a FlexContainer parent
|
|
111
|
-
const flexItemConfig = extractFlexItemConfig(props);
|
|
112
|
-
if (flexItemConfig) {
|
|
113
|
-
instance._flexConfig = { ...instance._flexConfig, ...flexItemConfig };
|
|
114
|
-
}
|
|
115
|
-
|
|
116
|
-
// Auto-set layoutWidth/layoutHeight from explicit width/height props for UI components.
|
|
117
|
-
// This ensures parent FlexContainers measure children correctly without relying on
|
|
118
|
-
// getLocalBounds() which may return wrong values before the first render pass
|
|
119
|
-
// (e.g. Text with unloaded fonts, Graphics with pending geometry).
|
|
120
|
-
if (typeof Ctor.prototype.updateConfig === 'function' && !(instance instanceof FlexContainer)) {
|
|
121
|
-
const w = typeof props.width === 'number' ? props.width : undefined;
|
|
122
|
-
const h = typeof props.height === 'number' ? props.height : undefined;
|
|
123
|
-
if (w !== undefined || h !== undefined) {
|
|
124
|
-
if (!instance._flexConfig) instance._flexConfig = {};
|
|
125
|
-
if (w !== undefined && instance._flexConfig.layoutWidth === undefined) {
|
|
126
|
-
instance._flexConfig.layoutWidth = w;
|
|
127
|
-
}
|
|
128
|
-
if (h !== undefined && instance._flexConfig.layoutHeight === undefined) {
|
|
129
|
-
instance._flexConfig.layoutHeight = h;
|
|
130
|
-
}
|
|
131
|
-
}
|
|
132
|
-
}
|
|
133
|
-
|
|
134
|
-
return instance;
|
|
135
|
-
},
|
|
136
|
-
|
|
137
|
-
createTextInstance() {
|
|
138
|
-
throw new Error(
|
|
139
|
-
'[PixiReconciler] Text strings are not supported. Use a <text> element.',
|
|
140
|
-
);
|
|
141
|
-
},
|
|
142
|
-
|
|
143
|
-
appendInitialChild(parent, child) {
|
|
144
|
-
if (child instanceof Container) {
|
|
145
|
-
if (isSuspendable(parent)) parent.suspendLayout();
|
|
146
|
-
if (parent instanceof FlexContainer) {
|
|
147
|
-
addChildToFlex(parent, child);
|
|
148
|
-
} else {
|
|
149
|
-
parent.addChild(child);
|
|
150
|
-
}
|
|
151
|
-
}
|
|
152
|
-
},
|
|
153
|
-
|
|
154
|
-
appendChild(parent, child) {
|
|
155
|
-
if (child instanceof Container) {
|
|
156
|
-
if (isSuspendable(parent)) {
|
|
157
|
-
parent.suspendLayout();
|
|
158
|
-
pendingLayoutFlush.add(parent);
|
|
159
|
-
}
|
|
160
|
-
if (parent instanceof FlexContainer) {
|
|
161
|
-
addChildToFlex(parent, child);
|
|
162
|
-
} else {
|
|
163
|
-
parent.addChild(child);
|
|
164
|
-
}
|
|
165
|
-
}
|
|
166
|
-
},
|
|
167
|
-
|
|
168
|
-
appendChildToContainer(container, child) {
|
|
169
|
-
if (child instanceof Container) container.addChild(child);
|
|
170
|
-
},
|
|
171
|
-
|
|
172
|
-
removeChild(parent, child) {
|
|
173
|
-
if (child instanceof Container) {
|
|
174
|
-
if (isSuspendable(parent)) {
|
|
175
|
-
parent.suspendLayout();
|
|
176
|
-
pendingLayoutFlush.add(parent);
|
|
177
|
-
}
|
|
178
|
-
parent.removeChild(child);
|
|
179
|
-
child.destroy({ children: true });
|
|
180
|
-
}
|
|
181
|
-
},
|
|
182
|
-
|
|
183
|
-
removeChildFromContainer(container, child) {
|
|
184
|
-
if (child instanceof Container) {
|
|
185
|
-
container.removeChild(child);
|
|
186
|
-
child.destroy({ children: true });
|
|
187
|
-
}
|
|
188
|
-
},
|
|
189
|
-
|
|
190
|
-
insertBefore(parent, child, beforeChild) {
|
|
191
|
-
if (child instanceof Container && beforeChild instanceof Container) {
|
|
192
|
-
if (child.parent) child.parent.removeChild(child);
|
|
193
|
-
if (isSuspendable(parent)) {
|
|
194
|
-
parent.suspendLayout();
|
|
195
|
-
pendingLayoutFlush.add(parent);
|
|
196
|
-
}
|
|
197
|
-
const index = parent.getChildIndex(beforeChild);
|
|
198
|
-
parent.addChildAt(child, index);
|
|
199
|
-
}
|
|
200
|
-
},
|
|
201
|
-
|
|
202
|
-
insertInContainerBefore(container, child, beforeChild) {
|
|
203
|
-
if (child instanceof Container && beforeChild instanceof Container) {
|
|
204
|
-
if (child.parent) child.parent.removeChild(child);
|
|
205
|
-
const index = container.getChildIndex(beforeChild);
|
|
206
|
-
container.addChildAt(child, index);
|
|
207
|
-
}
|
|
208
|
-
},
|
|
209
|
-
|
|
210
|
-
commitUpdate(instance, _updatePayload, _type, oldProps, newProps) {
|
|
211
|
-
// Suspend this instance and ancestors before applying changes —
|
|
212
|
-
// prevents intermediate relayouts with partially-updated tree
|
|
213
|
-
const dimensionsChanged = newProps.width !== oldProps.width || newProps.height !== oldProps.height;
|
|
214
|
-
if (isSuspendable(instance) || dimensionsChanged) {
|
|
215
|
-
suspendWithAncestors(instance);
|
|
216
|
-
}
|
|
217
|
-
|
|
218
|
-
if (typeof instance.updateConfig === 'function') {
|
|
219
|
-
const changed = diffConfig(newProps, oldProps);
|
|
220
|
-
if (Object.keys(changed).length > 0) {
|
|
221
|
-
instance.updateConfig(changed);
|
|
222
|
-
}
|
|
223
|
-
applyContainerProps(instance, newProps, oldProps);
|
|
224
|
-
applyEventProps(instance, newProps, oldProps);
|
|
225
|
-
} else {
|
|
226
|
-
applyProps(instance, newProps, oldProps);
|
|
227
|
-
}
|
|
228
|
-
|
|
229
|
-
// Update flex item config if parent is FlexContainer
|
|
230
|
-
const newFlexConfig = extractFlexItemConfig(newProps);
|
|
231
|
-
const oldFlexConfig = extractFlexItemConfig(oldProps);
|
|
232
|
-
if (newFlexConfig || oldFlexConfig) {
|
|
233
|
-
instance._flexConfig = { ...instance._flexConfig, ...newFlexConfig };
|
|
234
|
-
}
|
|
235
|
-
|
|
236
|
-
// Keep auto layoutWidth/layoutHeight in sync with width/height for UI components
|
|
237
|
-
if (typeof instance.updateConfig === 'function' && !(instance instanceof FlexContainer)) {
|
|
238
|
-
if (instance._flexConfig) {
|
|
239
|
-
if (typeof newProps.width === 'number' && instance._flexConfig.layoutWidth !== undefined) {
|
|
240
|
-
instance._flexConfig.layoutWidth = newProps.width;
|
|
241
|
-
}
|
|
242
|
-
if (typeof newProps.height === 'number' && instance._flexConfig.layoutHeight !== undefined) {
|
|
243
|
-
instance._flexConfig.layoutHeight = newProps.height;
|
|
244
|
-
}
|
|
245
|
-
}
|
|
246
|
-
}
|
|
247
|
-
|
|
248
|
-
// Mark ancestors dirty if flex config or dimensions changed
|
|
249
|
-
if (newFlexConfig || oldFlexConfig || dimensionsChanged) {
|
|
250
|
-
suspendWithAncestors(instance);
|
|
251
|
-
}
|
|
252
|
-
|
|
253
|
-
if (hasEventProps(newProps) && instance.eventMode === 'auto') {
|
|
254
|
-
instance.eventMode = 'static';
|
|
255
|
-
}
|
|
256
|
-
},
|
|
257
|
-
|
|
258
|
-
finalizeInitialChildren(instance) {
|
|
259
|
-
// Resume layout after all initial children have been appended
|
|
260
|
-
if (isSuspendable(instance)) {
|
|
261
|
-
instance.resumeLayout();
|
|
262
|
-
}
|
|
263
|
-
return false;
|
|
264
|
-
},
|
|
265
|
-
|
|
266
|
-
prepareUpdate() {
|
|
267
|
-
return true;
|
|
268
|
-
},
|
|
269
|
-
|
|
270
|
-
shouldSetTextContent() {
|
|
271
|
-
return false;
|
|
272
|
-
},
|
|
273
|
-
|
|
274
|
-
getRootHostContext() {
|
|
275
|
-
return null;
|
|
276
|
-
},
|
|
277
|
-
|
|
278
|
-
getChildHostContext(parentHostContext: any) {
|
|
279
|
-
return parentHostContext;
|
|
280
|
-
},
|
|
281
|
-
|
|
282
|
-
getPublicInstance(instance: any) {
|
|
283
|
-
return instance;
|
|
284
|
-
},
|
|
285
|
-
|
|
286
|
-
prepareForCommit() {
|
|
287
|
-
return null;
|
|
288
|
-
},
|
|
289
|
-
|
|
290
|
-
resetAfterCommit() {
|
|
291
|
-
if (pendingLayoutFlush.size === 0) return;
|
|
292
|
-
|
|
293
|
-
// Sort by depth (deepest first) so children compute sizes before parents lay out
|
|
294
|
-
const sorted = [...pendingLayoutFlush].sort((a, b) => {
|
|
295
|
-
let da = 0; let n: any = a; while (n.parent) { da++; n = n.parent; }
|
|
296
|
-
let db = 0; n = b; while (n.parent) { db++; n = n.parent; }
|
|
297
|
-
return db - da; // deepest first
|
|
298
|
-
});
|
|
299
|
-
|
|
300
|
-
for (const fc of sorted) {
|
|
301
|
-
fc.resumeLayout();
|
|
302
|
-
}
|
|
303
|
-
pendingLayoutFlush.clear();
|
|
304
|
-
},
|
|
305
|
-
|
|
306
|
-
preparePortalMount() {},
|
|
307
|
-
|
|
308
|
-
scheduleTimeout: setTimeout,
|
|
309
|
-
cancelTimeout: clearTimeout,
|
|
310
|
-
noTimeout: -1,
|
|
311
|
-
|
|
312
|
-
getCurrentEventPriority() {
|
|
313
|
-
return DefaultEventPriority;
|
|
314
|
-
},
|
|
315
|
-
|
|
316
|
-
hideInstance(instance) {
|
|
317
|
-
instance.visible = false;
|
|
318
|
-
},
|
|
319
|
-
|
|
320
|
-
unhideInstance(instance) {
|
|
321
|
-
instance.visible = true;
|
|
322
|
-
},
|
|
323
|
-
|
|
324
|
-
hideTextInstance() {},
|
|
325
|
-
unhideTextInstance() {},
|
|
326
|
-
|
|
327
|
-
clearContainer() {},
|
|
328
|
-
|
|
329
|
-
detachDeletedInstance() {},
|
|
330
|
-
|
|
331
|
-
prepareScopeUpdate() {},
|
|
332
|
-
getInstanceFromNode() { return null; },
|
|
333
|
-
getInstanceFromScope() { return null; },
|
|
334
|
-
beforeActiveInstanceBlur() {},
|
|
335
|
-
afterActiveInstanceBlur() {},
|
|
336
|
-
};
|
|
337
|
-
|
|
338
|
-
export const reconciler = Reconciler(hostConfig);
|
|
@@ -1,40 +0,0 @@
|
|
|
1
|
-
import type { SlotSpinResultBase } from '@energy8platform/platform-core/slot-result';
|
|
2
|
-
|
|
3
|
-
export interface FreeSpinsSessionConfig {
|
|
4
|
-
initialSpins: number;
|
|
5
|
-
/** Optional: extra spins to award from a result (retrigger). Default: none. */
|
|
6
|
-
retrigger?: (result: SlotSpinResultBase) => number;
|
|
7
|
-
/** Optional hard exit (e.g. max-win reached). */
|
|
8
|
-
isMaxWin?: () => boolean;
|
|
9
|
-
}
|
|
10
|
-
|
|
11
|
-
/** Headless free-spins state machine. The scene drives it; rendering/HUD reflect it. */
|
|
12
|
-
export class FreeSpinsSession {
|
|
13
|
-
remaining: number;
|
|
14
|
-
total: number;
|
|
15
|
-
totalWin = 0;
|
|
16
|
-
private readonly cfg: FreeSpinsSessionConfig;
|
|
17
|
-
|
|
18
|
-
constructor(cfg: FreeSpinsSessionConfig) {
|
|
19
|
-
this.cfg = cfg;
|
|
20
|
-
this.remaining = cfg.initialSpins;
|
|
21
|
-
this.total = cfg.initialSpins;
|
|
22
|
-
}
|
|
23
|
-
|
|
24
|
-
award(extra: number): void {
|
|
25
|
-
if (extra > 0) { this.remaining += extra; this.total += extra; }
|
|
26
|
-
}
|
|
27
|
-
|
|
28
|
-
/** Convenience: award using the configured retrigger rule. */
|
|
29
|
-
applyRetrigger(result: SlotSpinResultBase): void {
|
|
30
|
-
this.award(this.cfg.retrigger?.(result) ?? 0);
|
|
31
|
-
}
|
|
32
|
-
|
|
33
|
-
addWin(amount: number): void { this.totalWin += amount; }
|
|
34
|
-
|
|
35
|
-
consume(): void { if (this.remaining > 0) this.remaining -= 1; }
|
|
36
|
-
|
|
37
|
-
get isComplete(): boolean {
|
|
38
|
-
return this.remaining <= 0 || (this.cfg.isMaxWin?.() ?? false);
|
|
39
|
-
}
|
|
40
|
-
}
|
|
@@ -1,231 +0,0 @@
|
|
|
1
|
-
import { EventEmitter } from '../core/EventEmitter';
|
|
2
|
-
|
|
3
|
-
interface StateConfig<TContext> {
|
|
4
|
-
enter?: (ctx: TContext, data?: unknown) => void | Promise<void>;
|
|
5
|
-
exit?: (ctx: TContext) => void | Promise<void>;
|
|
6
|
-
update?: (ctx: TContext, dt: number) => void;
|
|
7
|
-
}
|
|
8
|
-
|
|
9
|
-
interface StateMachineEvents {
|
|
10
|
-
transition: { from: string | null; to: string };
|
|
11
|
-
error: Error;
|
|
12
|
-
}
|
|
13
|
-
|
|
14
|
-
/**
|
|
15
|
-
* Generic finite state machine for game flow management.
|
|
16
|
-
*
|
|
17
|
-
* Supports:
|
|
18
|
-
* - Typed context object shared across all states
|
|
19
|
-
* - Async enter/exit hooks
|
|
20
|
-
* - Per-frame update per state
|
|
21
|
-
* - Transition guards
|
|
22
|
-
* - Event emission on state change
|
|
23
|
-
*
|
|
24
|
-
* @example
|
|
25
|
-
* ```ts
|
|
26
|
-
* interface GameContext {
|
|
27
|
-
* balance: number;
|
|
28
|
-
* bet: number;
|
|
29
|
-
* lastWin: number;
|
|
30
|
-
* }
|
|
31
|
-
*
|
|
32
|
-
* const fsm = new StateMachine<GameContext>({ balance: 1000, bet: 10, lastWin: 0 });
|
|
33
|
-
*
|
|
34
|
-
* fsm.addState('idle', {
|
|
35
|
-
* enter: (ctx) => console.log('Waiting for spin...'),
|
|
36
|
-
* update: (ctx, dt) => { // optional per-frame },
|
|
37
|
-
* });
|
|
38
|
-
*
|
|
39
|
-
* fsm.addState('spinning', {
|
|
40
|
-
* enter: async (ctx) => {
|
|
41
|
-
* const result = await sdk.play({ action: 'spin', bet: ctx.bet });
|
|
42
|
-
* ctx.lastWin = result.totalWin;
|
|
43
|
-
* await fsm.transition('presenting');
|
|
44
|
-
* },
|
|
45
|
-
* });
|
|
46
|
-
*
|
|
47
|
-
* fsm.addState('presenting', {
|
|
48
|
-
* enter: async (ctx) => {
|
|
49
|
-
* await showWinPresentation(ctx.lastWin);
|
|
50
|
-
* await fsm.transition('idle');
|
|
51
|
-
* },
|
|
52
|
-
* });
|
|
53
|
-
*
|
|
54
|
-
* // Optional guard
|
|
55
|
-
* fsm.addGuard('idle', 'spinning', (ctx) => ctx.balance >= ctx.bet);
|
|
56
|
-
*
|
|
57
|
-
* await fsm.start('idle');
|
|
58
|
-
* ```
|
|
59
|
-
*/
|
|
60
|
-
export class StateMachine<TContext = Record<string, unknown>> extends EventEmitter<StateMachineEvents> {
|
|
61
|
-
private static MAX_TRANSITION_DEPTH = 10;
|
|
62
|
-
|
|
63
|
-
private _states = new Map<string, StateConfig<TContext>>();
|
|
64
|
-
private _guards = new Map<string, (ctx: TContext) => boolean>();
|
|
65
|
-
private _current: string | null = null;
|
|
66
|
-
private _transitionDepth = 0;
|
|
67
|
-
private _context: TContext;
|
|
68
|
-
|
|
69
|
-
constructor(context: TContext) {
|
|
70
|
-
super();
|
|
71
|
-
this._context = context;
|
|
72
|
-
}
|
|
73
|
-
|
|
74
|
-
/** Current state name */
|
|
75
|
-
get current(): string | null {
|
|
76
|
-
return this._current;
|
|
77
|
-
}
|
|
78
|
-
|
|
79
|
-
/** Whether a transition is in progress */
|
|
80
|
-
get isTransitioning(): boolean {
|
|
81
|
-
return this._transitionDepth > 0;
|
|
82
|
-
}
|
|
83
|
-
|
|
84
|
-
/** State machine context (shared data) */
|
|
85
|
-
get context(): TContext {
|
|
86
|
-
return this._context;
|
|
87
|
-
}
|
|
88
|
-
|
|
89
|
-
/**
|
|
90
|
-
* Register a state with optional enter/exit/update hooks.
|
|
91
|
-
*/
|
|
92
|
-
addState(name: string, config: StateConfig<TContext>): this {
|
|
93
|
-
this._states.set(name, config);
|
|
94
|
-
return this;
|
|
95
|
-
}
|
|
96
|
-
|
|
97
|
-
/**
|
|
98
|
-
* Add a transition guard.
|
|
99
|
-
* The guard function must return true to allow the transition.
|
|
100
|
-
*
|
|
101
|
-
* @param from - Source state
|
|
102
|
-
* @param to - Target state
|
|
103
|
-
* @param guard - Guard function
|
|
104
|
-
*/
|
|
105
|
-
addGuard(from: string, to: string, guard: (ctx: TContext) => boolean): this {
|
|
106
|
-
this._guards.set(`${from}->${to}`, guard);
|
|
107
|
-
return this;
|
|
108
|
-
}
|
|
109
|
-
|
|
110
|
-
/**
|
|
111
|
-
* Start the state machine in the given initial state.
|
|
112
|
-
*/
|
|
113
|
-
async start(initialState: string, data?: unknown): Promise<void> {
|
|
114
|
-
if (this._current !== null) {
|
|
115
|
-
throw new Error('[StateMachine] Already started. Use transition() to change states.');
|
|
116
|
-
}
|
|
117
|
-
|
|
118
|
-
const state = this._states.get(initialState);
|
|
119
|
-
if (!state) {
|
|
120
|
-
throw new Error(`[StateMachine] State "${initialState}" not registered.`);
|
|
121
|
-
}
|
|
122
|
-
|
|
123
|
-
this._current = initialState;
|
|
124
|
-
await state.enter?.(this._context, data);
|
|
125
|
-
this.emit('transition', { from: null, to: initialState });
|
|
126
|
-
}
|
|
127
|
-
|
|
128
|
-
/**
|
|
129
|
-
* Transition to a new state.
|
|
130
|
-
*
|
|
131
|
-
* @param to - Target state name
|
|
132
|
-
* @param data - Optional data passed to the new state's enter hook
|
|
133
|
-
* @returns true if the transition succeeded, false if blocked by a guard
|
|
134
|
-
*/
|
|
135
|
-
async transition(to: string, data?: unknown): Promise<boolean> {
|
|
136
|
-
if (this._transitionDepth >= StateMachine.MAX_TRANSITION_DEPTH) {
|
|
137
|
-
throw new Error(
|
|
138
|
-
'[StateMachine] Max transition depth exceeded — possible infinite loop',
|
|
139
|
-
);
|
|
140
|
-
}
|
|
141
|
-
|
|
142
|
-
const from = this._current;
|
|
143
|
-
|
|
144
|
-
// Check guard
|
|
145
|
-
if (from !== null) {
|
|
146
|
-
const guardKey = `${from}->${to}`;
|
|
147
|
-
const guard = this._guards.get(guardKey);
|
|
148
|
-
if (guard && !guard(this._context)) {
|
|
149
|
-
return false;
|
|
150
|
-
}
|
|
151
|
-
}
|
|
152
|
-
|
|
153
|
-
const toState = this._states.get(to);
|
|
154
|
-
if (!toState) {
|
|
155
|
-
throw new Error(`[StateMachine] State "${to}" not registered.`);
|
|
156
|
-
}
|
|
157
|
-
|
|
158
|
-
this._transitionDepth++;
|
|
159
|
-
|
|
160
|
-
try {
|
|
161
|
-
// Exit current state
|
|
162
|
-
if (from !== null) {
|
|
163
|
-
const fromState = this._states.get(from);
|
|
164
|
-
await fromState?.exit?.(this._context);
|
|
165
|
-
}
|
|
166
|
-
|
|
167
|
-
// Enter new state
|
|
168
|
-
this._current = to;
|
|
169
|
-
await toState.enter?.(this._context, data);
|
|
170
|
-
|
|
171
|
-
this.emit('transition', { from, to });
|
|
172
|
-
} catch (err) {
|
|
173
|
-
this.emit('error', err instanceof Error ? err : new Error(String(err)));
|
|
174
|
-
throw err;
|
|
175
|
-
} finally {
|
|
176
|
-
this._transitionDepth--;
|
|
177
|
-
}
|
|
178
|
-
|
|
179
|
-
return true;
|
|
180
|
-
}
|
|
181
|
-
|
|
182
|
-
/**
|
|
183
|
-
* Call the current state's update function.
|
|
184
|
-
* Should be called from the game loop.
|
|
185
|
-
*/
|
|
186
|
-
update(dt: number): void {
|
|
187
|
-
if (this._current === null) return;
|
|
188
|
-
const state = this._states.get(this._current);
|
|
189
|
-
state?.update?.(this._context, dt);
|
|
190
|
-
}
|
|
191
|
-
|
|
192
|
-
/**
|
|
193
|
-
* Check if a state is registered.
|
|
194
|
-
*/
|
|
195
|
-
hasState(name: string): boolean {
|
|
196
|
-
return this._states.has(name);
|
|
197
|
-
}
|
|
198
|
-
|
|
199
|
-
/**
|
|
200
|
-
* Check if a transition is allowed (guard passes).
|
|
201
|
-
*/
|
|
202
|
-
canTransition(to: string): boolean {
|
|
203
|
-
if (this._current === null) return false;
|
|
204
|
-
const guardKey = `${this._current}->${to}`;
|
|
205
|
-
const guard = this._guards.get(guardKey);
|
|
206
|
-
if (!guard) return true;
|
|
207
|
-
return guard(this._context);
|
|
208
|
-
}
|
|
209
|
-
|
|
210
|
-
/**
|
|
211
|
-
* Reset the state machine (exit current state, clear current).
|
|
212
|
-
*/
|
|
213
|
-
async reset(): Promise<void> {
|
|
214
|
-
if (this._current !== null) {
|
|
215
|
-
const state = this._states.get(this._current);
|
|
216
|
-
await state?.exit?.(this._context);
|
|
217
|
-
}
|
|
218
|
-
this._current = null;
|
|
219
|
-
this._transitionDepth = 0;
|
|
220
|
-
}
|
|
221
|
-
|
|
222
|
-
/**
|
|
223
|
-
* Destroy the state machine.
|
|
224
|
-
*/
|
|
225
|
-
async destroy(): Promise<void> {
|
|
226
|
-
await this.reset();
|
|
227
|
-
this._states.clear();
|
|
228
|
-
this._guards.clear();
|
|
229
|
-
this.removeAllListeners();
|
|
230
|
-
}
|
|
231
|
-
}
|
package/src/state/index.ts
DELETED
|
@@ -1 +0,0 @@
|
|
|
1
|
-
export { StateMachine } from './StateMachine';
|