@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.
Files changed (93) hide show
  1. package/README.md +121 -0
  2. package/dist/animation.cjs.js +18 -3
  3. package/dist/animation.cjs.js.map +1 -1
  4. package/dist/animation.esm.js +18 -3
  5. package/dist/animation.esm.js.map +1 -1
  6. package/dist/core.cjs.js +18 -3
  7. package/dist/core.cjs.js.map +1 -1
  8. package/dist/core.esm.js +18 -3
  9. package/dist/core.esm.js.map +1 -1
  10. package/dist/host.cjs.js +18 -3
  11. package/dist/host.cjs.js.map +1 -1
  12. package/dist/host.d.ts +3 -5
  13. package/dist/host.esm.js +18 -3
  14. package/dist/host.esm.js.map +1 -1
  15. package/dist/index.cjs.js +18 -2535
  16. package/dist/index.cjs.js.map +1 -1
  17. package/dist/index.d.ts +3 -1020
  18. package/dist/index.esm.js +20 -2525
  19. package/dist/index.esm.js.map +1 -1
  20. package/dist/slot.cjs.js +1872 -61
  21. package/dist/slot.cjs.js.map +1 -1
  22. package/dist/slot.d.ts +555 -28
  23. package/dist/slot.esm.js +1857 -62
  24. package/dist/slot.esm.js.map +1 -1
  25. package/dist/vite.cjs.js +0 -5
  26. package/dist/vite.cjs.js.map +1 -1
  27. package/dist/vite.esm.js +0 -5
  28. package/dist/vite.esm.js.map +1 -1
  29. package/package.json +2 -35
  30. package/src/animation/Tween.ts +14 -3
  31. package/src/host/index.ts +0 -1
  32. package/src/host/types.ts +0 -3
  33. package/src/index.ts +0 -28
  34. package/src/slot/anim/easing-map.ts +16 -2
  35. package/src/slot/cascade/TumbleController.ts +230 -0
  36. package/src/slot/config/ReelSystemConfig.ts +559 -0
  37. package/src/slot/config/presets.ts +222 -0
  38. package/src/slot/features/extra.ts +189 -0
  39. package/src/slot/features/index.ts +44 -0
  40. package/src/slot/features/symbols.ts +183 -0
  41. package/src/slot/features/types.ts +136 -0
  42. package/src/slot/features/wilds.ts +151 -0
  43. package/src/slot/grid/ReelGrid.ts +93 -24
  44. package/src/slot/grid/SymbolCell.ts +45 -11
  45. package/src/slot/index.ts +61 -2
  46. package/src/slot/motion/AnticipationController.ts +96 -0
  47. package/src/slot/motion/SpinEngine.ts +384 -0
  48. package/src/slot/system/ReelSystem.ts +295 -0
  49. package/src/vite/index.ts +0 -5
  50. package/dist/react-jsx.cjs.js +0 -3
  51. package/dist/react-jsx.cjs.js.map +0 -1
  52. package/dist/react-jsx.d.ts +0 -602
  53. package/dist/react-jsx.esm.js +0 -2
  54. package/dist/react-jsx.esm.js.map +0 -1
  55. package/dist/react.cjs.js +0 -3787
  56. package/dist/react.cjs.js.map +0 -1
  57. package/dist/react.d.ts +0 -1467
  58. package/dist/react.esm.js +0 -3771
  59. package/dist/react.esm.js.map +0 -1
  60. package/dist/ui.cjs.js +0 -2999
  61. package/dist/ui.cjs.js.map +0 -1
  62. package/dist/ui.d.ts +0 -1116
  63. package/dist/ui.esm.js +0 -2983
  64. package/dist/ui.esm.js.map +0 -1
  65. package/src/react/EngineContext.ts +0 -26
  66. package/src/react/ReactScene.ts +0 -88
  67. package/src/react/applyProps.ts +0 -271
  68. package/src/react/catalogue.ts +0 -17
  69. package/src/react/createPixiRoot.ts +0 -31
  70. package/src/react/extendAll.ts +0 -74
  71. package/src/react/hooks.ts +0 -46
  72. package/src/react/index.ts +0 -29
  73. package/src/react/jsx-runtime.ts +0 -346
  74. package/src/react/reconciler.ts +0 -338
  75. package/src/slot/freeSpins/FreeSpinsSession.ts +0 -40
  76. package/src/state/StateMachine.ts +0 -231
  77. package/src/state/index.ts +0 -1
  78. package/src/ui/BalanceDisplay.ts +0 -159
  79. package/src/ui/Button.ts +0 -304
  80. package/src/ui/FlexContainer.ts +0 -775
  81. package/src/ui/Label.ts +0 -124
  82. package/src/ui/LabelValue.ts +0 -122
  83. package/src/ui/Layout.ts +0 -291
  84. package/src/ui/Modal.ts +0 -170
  85. package/src/ui/Panel.ts +0 -204
  86. package/src/ui/ProgressBar.ts +0 -170
  87. package/src/ui/ScrollContainer.ts +0 -478
  88. package/src/ui/Slider.ts +0 -241
  89. package/src/ui/Toast.ts +0 -150
  90. package/src/ui/Toggle.ts +0 -201
  91. package/src/ui/WinDisplay.ts +0 -145
  92. package/src/ui/index.ts +0 -33
  93. package/src/ui/view.ts +0 -28
@@ -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
- }
@@ -1 +0,0 @@
1
- export { StateMachine } from './StateMachine';