@energy8platform/game-engine 0.20.0 → 0.21.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 (47) 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.esm.js +18 -3
  13. package/dist/host.esm.js.map +1 -1
  14. package/dist/index.cjs.js +18 -3
  15. package/dist/index.cjs.js.map +1 -1
  16. package/dist/index.esm.js +18 -3
  17. package/dist/index.esm.js.map +1 -1
  18. package/dist/react.cjs.js +18 -3
  19. package/dist/react.cjs.js.map +1 -1
  20. package/dist/react.esm.js +18 -3
  21. package/dist/react.esm.js.map +1 -1
  22. package/dist/slot.cjs.js +1872 -31
  23. package/dist/slot.cjs.js.map +1 -1
  24. package/dist/slot.d.ts +555 -5
  25. package/dist/slot.esm.js +1857 -33
  26. package/dist/slot.esm.js.map +1 -1
  27. package/dist/ui.cjs.js +18 -3
  28. package/dist/ui.cjs.js.map +1 -1
  29. package/dist/ui.esm.js +18 -3
  30. package/dist/ui.esm.js.map +1 -1
  31. package/package.json +1 -1
  32. package/src/animation/Tween.ts +14 -3
  33. package/src/slot/anim/easing-map.ts +16 -2
  34. package/src/slot/cascade/TumbleController.ts +230 -0
  35. package/src/slot/config/ReelSystemConfig.ts +559 -0
  36. package/src/slot/config/presets.ts +222 -0
  37. package/src/slot/features/extra.ts +189 -0
  38. package/src/slot/features/index.ts +44 -0
  39. package/src/slot/features/symbols.ts +183 -0
  40. package/src/slot/features/types.ts +136 -0
  41. package/src/slot/features/wilds.ts +151 -0
  42. package/src/slot/grid/ReelGrid.ts +93 -24
  43. package/src/slot/grid/SymbolCell.ts +45 -11
  44. package/src/slot/index.ts +61 -0
  45. package/src/slot/motion/AnticipationController.ts +96 -0
  46. package/src/slot/motion/SpinEngine.ts +384 -0
  47. package/src/slot/system/ReelSystem.ts +295 -0
@@ -0,0 +1,295 @@
1
+ // packages/game-engine/src/slot/system/ReelSystem.ts
2
+ //
3
+ // The configurable reel system facade. `createReelSystem({ resolve, config })` builds a grid and
4
+ // wires the spin engine, anticipation controller, tumble/cascade controller and the special-feature
5
+ // registry — all driven by one `ReelSystemConfig`. Presentation only: it draws boards you feed it.
6
+
7
+ import { Container } from 'pixi.js';
8
+ import { Tween } from '../../animation';
9
+ import { ReelGrid } from '../grid/ReelGrid';
10
+ import type { CellData } from '../grid/SymbolCell';
11
+ import type { SymbolResolver } from '../grid/SymbolView';
12
+ import { SpinEngine, type SpinData, type SpinRunOpts } from '../motion/SpinEngine';
13
+ import { AnticipationController } from '../motion/AnticipationController';
14
+ import { TumbleController, type TumbleStep } from '../cascade/TumbleController';
15
+ import { FEATURES, FEATURE_LIST, type FeatureContext, type ReelFeature } from '../features';
16
+ import {
17
+ DEFAULT_REEL_CONFIG,
18
+ effectiveRowsPerReel,
19
+ mergeReelConfig,
20
+ resolveReelConfig,
21
+ waysCount,
22
+ type DeepPartial,
23
+ type FeatureKey,
24
+ type ReelSystemConfig,
25
+ } from '../config/ReelSystemConfig';
26
+
27
+ export interface CreateReelSystemOptions {
28
+ resolve: SymbolResolver;
29
+ config?: DeepPartial<ReelSystemConfig>;
30
+ /** Initial board (board[col][row]); defaults to empty cells. */
31
+ board?: CellData[][];
32
+ log?: (msg: string) => void;
33
+ /** Custom feature modules to register alongside the 13 built-ins. */
34
+ features?: ReelFeature[];
35
+ }
36
+
37
+ export interface ReelSystem {
38
+ /** Root container — add this to your scene. */
39
+ readonly view: Container;
40
+ readonly grid: ReelGrid;
41
+ /** Overlay layer above the grid (masked off) — draw bespoke rings/labels/sprites here. */
42
+ readonly fx: Container;
43
+ readonly config: ReelSystemConfig;
44
+ /** Current board (board[col][row]). */
45
+ readonly board: CellData[][];
46
+ /** Ways-to-win for the current grid (ways/megaways evaluation). */
47
+ readonly ways: number;
48
+ setBoard(board: CellData[][]): void;
49
+ /** Merge a partial config; rebuilds the grid when geometry changes. */
50
+ update(partial: DeepPartial<ReelSystemConfig>): void;
51
+ /** Replace the whole config. */
52
+ setConfig(config: ReelSystemConfig): void;
53
+ spin(target: CellData[][], opts?: SpinRunOpts): Promise<void>;
54
+ /** Run a cascade chain. With `freeSpins` + `cascade.multiplier.persistInFreeSpins`, the multiplier carries over instead of resetting. */
55
+ cascade(steps: TumbleStep[], opts?: { turbo?: boolean; freeSpins?: boolean }): Promise<void>;
56
+ /** Current running cascade multiplier. */
57
+ readonly multiplier: number;
58
+ /** Register a custom feature (or override a built-in by reusing its key). */
59
+ registerFeature(feature: ReelFeature): void;
60
+ /** All registered features (built-ins + custom) in canonical order. */
61
+ features(): ReelFeature[];
62
+ /** Enabled feature modules (built-ins + custom). */
63
+ enabledFeatures(): ReelFeature[];
64
+ /** Run a feature by key — built-in `FeatureKey` or a custom feature's key. */
65
+ runFeature(key: FeatureKey | string, opts?: { freeSpins?: boolean }): Promise<void>;
66
+ /** Build a FeatureContext for driving a feature/bespoke animation yourself. */
67
+ featureContext(opts?: { freeSpins?: boolean }): FeatureContext;
68
+ resize(cellSize: number): void;
69
+ skip(): void;
70
+ destroy(): void;
71
+ }
72
+
73
+ /** Drop a `rowsPerReel` whose length no longer matches `cols` so geometry/ways stay consistent. */
74
+ function normalizeGrid(cfg: ReelSystemConfig): ReelSystemConfig {
75
+ if (cfg.grid.rowsPerReel && cfg.grid.rowsPerReel.length !== cfg.grid.cols) {
76
+ const next = { ...cfg, grid: { ...cfg.grid } };
77
+ delete next.grid.rowsPerReel;
78
+ return next;
79
+ }
80
+ return cfg;
81
+ }
82
+
83
+ export function createReelSystem(opts: CreateReelSystemOptions): ReelSystem {
84
+ let config = normalizeGrid(resolveReelConfig(opts.config));
85
+ const resolve = opts.resolve;
86
+ const log = opts.log;
87
+
88
+ const view = new Container();
89
+ let grid!: ReelGrid;
90
+ let fx!: Container;
91
+ let spin!: SpinEngine;
92
+ let anticipation!: AnticipationController;
93
+ let tumble!: TumbleController;
94
+ let board: CellData[][] = opts.board ?? emptyBoard(config);
95
+ // custom features keyed by id; built-ins live in FEATURES/FEATURE_LIST
96
+ const custom = new Map<string, ReelFeature>();
97
+ for (const f of opts.features ?? []) custom.set(f.key, f);
98
+ const allFeatures = (): ReelFeature[] => {
99
+ const seen = new Set<string>();
100
+ const out: ReelFeature[] = [];
101
+ for (const f of [...FEATURE_LIST, ...custom.values()]) {
102
+ const eff = custom.get(f.key) ?? f; // a custom feature overrides a built-in with the same key
103
+ if (seen.has(eff.key)) continue;
104
+ seen.add(eff.key);
105
+ out.push(eff);
106
+ }
107
+ return out;
108
+ };
109
+ const findFeature = (key: string): ReelFeature | undefined =>
110
+ custom.get(key) ?? (FEATURES as Record<string, ReelFeature>)[key];
111
+
112
+ function buildGrid(): void {
113
+ if (grid) {
114
+ spin?.skip();
115
+ tumble?.skip();
116
+ for (const child of fx?.children.slice() ?? []) Tween.killTweensOf(child);
117
+ grid.destroy({ children: true });
118
+ }
119
+ grid = new ReelGrid({
120
+ cols: config.grid.cols,
121
+ rows: config.grid.rows,
122
+ rowsPerReel: config.grid.rowsPerReel ?? effectiveRowsPerReel(config.grid),
123
+ cellSize: config.grid.cellSize,
124
+ gap: config.grid.gap,
125
+ resolve,
126
+ frameStyle: config.grid.frameStyle,
127
+ mask: config.grid.mask,
128
+ decoration: config.grid.decoration?.padding
129
+ ? { padding: config.grid.decoration.padding }
130
+ : undefined,
131
+ });
132
+ fx = new Container();
133
+ grid.addChild(fx);
134
+ view.addChild(grid);
135
+ spin = new SpinEngine(grid, resolve, config.motion, config.win);
136
+ anticipation = new AnticipationController(config.anticipation);
137
+ tumble = new TumbleController(grid, config.cascade, config.win);
138
+ grid.setGrid(board);
139
+ }
140
+
141
+ function geometryChanged(next: ReelSystemConfig): boolean {
142
+ const a = config.grid,
143
+ b = next.grid;
144
+ return (
145
+ a.cols !== b.cols ||
146
+ a.rows !== b.rows ||
147
+ a.cellSize !== b.cellSize ||
148
+ a.gap !== b.gap ||
149
+ a.mask !== b.mask ||
150
+ JSON.stringify(a.rowsPerReel) !== JSON.stringify(b.rowsPerReel) ||
151
+ (a.decoration?.padding ?? 0) !== (b.decoration?.padding ?? 0)
152
+ );
153
+ }
154
+
155
+ function ctx(freeSpins?: boolean): FeatureContext {
156
+ return { grid, resolve, cfg: config, fx, board, freeSpins, log };
157
+ }
158
+
159
+ buildGrid();
160
+
161
+ const api: ReelSystem = {
162
+ view,
163
+ get grid() {
164
+ return grid;
165
+ },
166
+ get fx() {
167
+ return fx;
168
+ },
169
+ get config() {
170
+ return config;
171
+ },
172
+ get board() {
173
+ return board;
174
+ },
175
+ get ways() {
176
+ return waysCount(config.grid);
177
+ },
178
+
179
+ setBoard(next) {
180
+ board = next;
181
+ // grow the board to the grid shape so feature code can index safely
182
+ grid.setGrid(next);
183
+ },
184
+
185
+ setConfig(next) {
186
+ const norm = normalizeGrid(next);
187
+ const rebuild = geometryChanged(norm);
188
+ config = norm;
189
+ if (rebuild) buildGrid();
190
+ else {
191
+ spin.setConfig(config.motion);
192
+ spin.setWin(config.win);
193
+ anticipation.setConfig(config.anticipation);
194
+ tumble.setConfig(config.cascade);
195
+ tumble.setWin(config.win);
196
+ }
197
+ },
198
+
199
+ update(partial) {
200
+ api.setConfig(mergeReelConfig(config, partial));
201
+ },
202
+
203
+ async spin(target, runOpts) {
204
+ const data: SpinData = { targetGrid: target };
205
+ const decision = anticipation.decide(target);
206
+ let resetZoom: (() => Promise<void>) | null = null;
207
+ if (decision.active) {
208
+ log?.(`Anticipation on reels [${decision.reels.join(', ')}]`);
209
+ resetZoom = await anticipation.zoomIn(grid);
210
+ }
211
+ await spin.run(data, {
212
+ ...runOpts,
213
+ anticipateReels: decision.active ? decision.reels : undefined,
214
+ anticipateSlowdown: decision.slowdown,
215
+ anticipateHoldMs: decision.holdMs,
216
+ });
217
+ if (resetZoom) await resetZoom();
218
+ board = target;
219
+ },
220
+
221
+ get multiplier() {
222
+ return tumble.multiplier;
223
+ },
224
+
225
+ async cascade(steps, cOpts) {
226
+ // keep the multiplier climbing across free-spins when configured; otherwise reset per spin
227
+ const persist = config.cascade.multiplier.persistInFreeSpins && !!cOpts?.freeSpins;
228
+ if (!persist) tumble.resetMultiplier();
229
+ for (let i = 0; i < steps.length; i++) {
230
+ await tumble.step(steps[i], i, cOpts);
231
+ board = steps[i].settledGrid;
232
+ }
233
+ if (config.cascade.multiplier.enabled) log?.(`Cascade multiplier ×${tumble.multiplier}`);
234
+ },
235
+
236
+ registerFeature(feature) {
237
+ custom.set(feature.key, feature);
238
+ },
239
+
240
+ features() {
241
+ return allFeatures();
242
+ },
243
+
244
+ enabledFeatures() {
245
+ return allFeatures().filter((f) => f.enabled(config));
246
+ },
247
+
248
+ featureContext(fOpts) {
249
+ return ctx(fOpts?.freeSpins);
250
+ },
251
+
252
+ async runFeature(key, fOpts) {
253
+ const feature = findFeature(key);
254
+ if (!feature) {
255
+ log?.(`Unknown feature "${key}"`);
256
+ return;
257
+ }
258
+ if (!feature.enabled(config)) {
259
+ log?.(`${feature.label} is disabled`);
260
+ return;
261
+ }
262
+ await feature.demo(ctx(fOpts?.freeSpins));
263
+ },
264
+
265
+ resize(cellSize) {
266
+ config = mergeReelConfig(config, { grid: { cellSize } } as DeepPartial<ReelSystemConfig>);
267
+ grid.resize(cellSize);
268
+ },
269
+
270
+ skip() {
271
+ spin.skip();
272
+ tumble.skip();
273
+ // kill in-flight overlay tweens (labels/rings) so a rebuild never animates destroyed nodes
274
+ for (const child of fx.children.slice()) Tween.killTweensOf(child);
275
+ fx.removeChildren().forEach((c) => c.destroy());
276
+ },
277
+
278
+ destroy() {
279
+ api.skip();
280
+ grid.destroy({ children: true });
281
+ view.destroy({ children: true });
282
+ },
283
+ };
284
+
285
+ return api;
286
+ }
287
+
288
+ function emptyBoard(cfg: ReelSystemConfig): CellData[][] {
289
+ const rows = effectiveRowsPerReel(cfg.grid);
290
+ return Array.from({ length: cfg.grid.cols }, (_, c) =>
291
+ Array.from({ length: rows[c] }, () => ({ symbol: null as string | null })),
292
+ );
293
+ }
294
+
295
+ export { DEFAULT_REEL_CONFIG };