@energy8platform/game-engine 0.38.0 → 0.40.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.
@@ -9,8 +9,16 @@ import { Tween } from '../../animation';
9
9
  import { ReelGrid } from '../grid/ReelGrid';
10
10
  import type { CellData } from '../grid/SymbolCell';
11
11
  import type { SymbolResolver } from '../grid/SymbolView';
12
- import { SpinEngine, type SpinData, type SpinRunOpts } from '../motion/SpinEngine';
13
- import { AnticipationController } from '../motion/AnticipationController';
12
+ import {
13
+ SpinEngine,
14
+ type ReelStopPlan,
15
+ type SpinData,
16
+ type SpinRunOpts,
17
+ } from '../motion/SpinEngine';
18
+ import {
19
+ AnticipationController,
20
+ type AnticipationDecision,
21
+ } from '../motion/AnticipationController';
14
22
  import { TumbleController, type TumbleStep } from '../cascade/TumbleController';
15
23
  import { ReelStepController, type ReelStepData } from '../cascade/ReelStepController';
16
24
  import { FEATURES, FEATURE_LIST, type FeatureContext, type ReelFeature } from '../features';
@@ -65,6 +73,14 @@ export interface ReelSystem {
65
73
  /** Replace the whole config. */
66
74
  setConfig(config: ReelSystemConfig): void;
67
75
  spin(target: CellData[][], opts?: SpinRunOpts): Promise<void>;
76
+ /**
77
+ * The schedule `spin(target, opts)` WOULD run, without running it — same anticipation decision,
78
+ * same numbers. Schedule landing sounds / camera moves against this instead of re-deriving the
79
+ * engine's formula. (`spin`'s `onPlan` hands you the same array once the spin is under way.)
80
+ */
81
+ planSpin(target: CellData[][], opts?: SpinRunOpts): ReelStopPlan[];
82
+ /** The anticipation decision `spin(target, opts)` would use (run options override the config). */
83
+ anticipationFor(target: CellData[][], opts?: SpinRunOpts): AnticipationDecision;
68
84
  /** Run a cascade chain. With `freeSpins` + `cascade.multiplier.persistInFreeSpins`, the multiplier
69
85
  * carries over instead of resetting. Generic in the step type, so `onStep` hands back the game's
70
86
  * own step (with its per-step win) rather than the bare TumbleStep. */
@@ -191,6 +207,36 @@ export function createReelSystem(opts: CreateReelSystemOptions): ReelSystem {
191
207
  return { grid, resolve, cfg: config, fx, board, freeSpins, log };
192
208
  }
193
209
 
210
+ /**
211
+ * Which reels get the anticipation treatment for this spin. An explicit `anticipateReels` on the
212
+ * run options WINS over the configured decision — passing it is how a game drives anticipation
213
+ * from its own logic. Omit it and the configured `AnticipationController` decides.
214
+ */
215
+ function resolveAnticipation(target: CellData[][], runOpts?: SpinRunOpts): AnticipationDecision {
216
+ const explicit = runOpts?.anticipateReels;
217
+ if (!explicit) return anticipation.decide(target);
218
+ if (!explicit.length) return { active: false, reels: [], slowdown: 1, holdMs: 0 };
219
+ return {
220
+ active: true,
221
+ reels: explicit.slice(),
222
+ slowdown: runOpts?.anticipateSlowdown ?? config.anticipation.slowdownFactor,
223
+ holdMs: runOpts?.anticipateHoldMs ?? config.anticipation.holdMs,
224
+ };
225
+ }
226
+
227
+ /** Fold a resolved decision back onto the caller's run options (everything else passes through). */
228
+ function mergeAnticipation(
229
+ runOpts: SpinRunOpts | undefined,
230
+ decision: AnticipationDecision,
231
+ ): SpinRunOpts {
232
+ return {
233
+ ...runOpts,
234
+ anticipateReels: decision.active ? decision.reels : undefined,
235
+ anticipateSlowdown: decision.slowdown,
236
+ anticipateHoldMs: decision.holdMs,
237
+ };
238
+ }
239
+
194
240
  buildGrid();
195
241
 
196
242
  const api: ReelSystem = {
@@ -237,20 +283,24 @@ export function createReelSystem(opts: CreateReelSystemOptions): ReelSystem {
237
283
  api.setConfig(mergeReelConfig(config, partial));
238
284
  },
239
285
 
286
+ anticipationFor(target, runOpts) {
287
+ return resolveAnticipation(target, runOpts);
288
+ },
289
+
290
+ planSpin(target, runOpts) {
291
+ const decision = resolveAnticipation(target, runOpts);
292
+ return spin.plan({ targetGrid: target }, mergeAnticipation(runOpts, decision));
293
+ },
294
+
240
295
  async spin(target, runOpts) {
241
296
  const data: SpinData = { targetGrid: target };
242
- const decision = anticipation.decide(target);
297
+ const decision = resolveAnticipation(target, runOpts);
243
298
  let resetZoom: (() => Promise<void>) | null = null;
244
299
  if (decision.active) {
245
300
  log?.(`Anticipation on reels [${decision.reels.join(', ')}]`);
246
301
  resetZoom = await anticipation.zoomIn(grid);
247
302
  }
248
- await spin.run(data, {
249
- ...runOpts,
250
- anticipateReels: decision.active ? decision.reels : undefined,
251
- anticipateSlowdown: decision.slowdown,
252
- anticipateHoldMs: decision.holdMs,
253
- });
303
+ await spin.run(data, mergeAnticipation(runOpts, decision));
254
304
  if (resetZoom) await resetZoom();
255
305
  board = target;
256
306
  },