@energy8platform/game-engine 0.40.0 → 0.42.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 (44) hide show
  1. package/README.md +18 -4
  2. package/dist/animation.cjs.js.map +1 -1
  3. package/dist/animation.esm.js.map +1 -1
  4. package/dist/assets.cjs.js.map +1 -1
  5. package/dist/assets.esm.js.map +1 -1
  6. package/dist/audio.cjs.js.map +1 -1
  7. package/dist/audio.esm.js.map +1 -1
  8. package/dist/core.cjs.js.map +1 -1
  9. package/dist/core.esm.js.map +1 -1
  10. package/dist/debug.cjs.js.map +1 -1
  11. package/dist/debug.esm.js.map +1 -1
  12. package/dist/devtools.cjs.js +2 -0
  13. package/dist/devtools.cjs.js.map +1 -1
  14. package/dist/devtools.d.ts +12 -0
  15. package/dist/devtools.esm.js +2 -0
  16. package/dist/devtools.esm.js.map +1 -1
  17. package/dist/harness.cjs.js.map +1 -1
  18. package/dist/harness.esm.js.map +1 -1
  19. package/dist/host.cjs.js +5 -4
  20. package/dist/host.cjs.js.map +1 -1
  21. package/dist/host.d.ts +5 -2
  22. package/dist/host.esm.js +6 -5
  23. package/dist/host.esm.js.map +1 -1
  24. package/dist/index.cjs.js.map +1 -1
  25. package/dist/index.esm.js.map +1 -1
  26. package/dist/reel-panel-client.cjs.js +2 -0
  27. package/dist/reel-panel-client.cjs.js.map +1 -1
  28. package/dist/reel-panel-client.esm.js +2 -0
  29. package/dist/reel-panel-client.esm.js.map +1 -1
  30. package/dist/slot.cjs.js +24 -7
  31. package/dist/slot.cjs.js.map +1 -1
  32. package/dist/slot.d.ts +20 -0
  33. package/dist/slot.esm.js +24 -7
  34. package/dist/slot.esm.js.map +1 -1
  35. package/dist/vite.cjs.js.map +1 -1
  36. package/dist/vite.esm.js.map +1 -1
  37. package/package.json +2 -2
  38. package/src/host/createSlotGame.ts +6 -4
  39. package/src/host/shellConfig.ts +8 -5
  40. package/src/slot/config/ReelSystemConfig.ts +13 -0
  41. package/src/slot/devtools/fieldSchema.ts +1 -0
  42. package/src/slot/motion/AnticipationController.ts +12 -2
  43. package/src/slot/motion/SpinEngine.ts +16 -2
  44. package/src/slot/system/ReelSystem.ts +5 -1
@@ -1 +1 @@
1
- {"version":3,"file":"vite.cjs.js","sources":["../src/vite/index.ts"],"sourcesContent":[null],"names":["devBridgePlugin","spinPlugin"],"mappings":";;;;AAoCA;AAEA;;;;;;;;;;;;;;;;;;;;;;;AAuBG;AACG,SAAU,gBAAgB,CAAC,MAAA,GAAqB,EAAE,EAAA;IACtD,MAAM,OAAO,GAAa,EAAE;AAE5B,IAAA,IAAI,MAAM,CAAC,SAAS,EAAE;AACpB,QAAA,MAAM,UAAU,GAAG,MAAM,CAAC,eAAe,IAAI,cAAc;QAC3D,OAAO,CAAC,IAAI,CAACA,oBAAe,CAAC,UAAU,CAAC,CAAC;;AAEzC,QAAA,OAAO,CAAC,IAAI,CACVC,eAAU,CAAC;AACT,YAAA,QAAQ,EAAE,MAAM,CAAC,UAAU,IAAI,wBAAwB;YACvD,MAAM,EAAE,MAAM,CAAC,MAAM;YACrB,IAAI,EAAE,MAAM,CAAC,QAAQ;AACtB,SAAA,CAAC,CACH;IACH;AAEA,IAAA,MAAM,QAAQ,GAAG,MAAM,CAAC,IAAI,IAAI,EAAE;IAElC,OAAO;AACL,QAAA,IAAI,EAAE,MAAM,CAAC,IAAI,IAAI,IAAI;AAEzB,QAAA,OAAO,EAAE;AACP,YAAA,GAAG,OAAO;AACV,YAAA,IAAK,QAAQ,CAAC,OAAoB,IAAI,EAAE,CAAC;AAC1C,SAAA;AAED,QAAA,KAAK,EAAE;AACL,YAAA,MAAM,EAAE,QAAQ;AAChB,YAAA,iBAAiB,EAAE,IAAI;AACvB,YAAA,SAAS,EAAE,KAAK;AAChB,YAAA,aAAa,EAAE;AACb,gBAAA,MAAM,EAAE;AACN,oBAAA,YAAY,EAAE;wBACZ,IAAI,EAAE,CAAC,SAAS,CAAC;AAClB,qBAAA;AACF,iBAAA;AACF,aAAA;YACD,GAAG,QAAQ,CAAC,KAAK;AAClB,SAAA;AAED,QAAA,MAAM,EAAE;AACN,YAAA,IAAI,EAAE,IAAI;AACV,YAAA,IAAI,EAAE,IAAI;YACV,GAAG,QAAQ,CAAC,MAAM;AACnB,SAAA;AAED,QAAA,OAAO,EAAE;AACP,YAAA,MAAM,EAAE;gBACN,SAAS;AACV,aAAA;YACD,GAAG,QAAQ,CAAC,OAAO;AACpB,SAAA;AAED,QAAA,YAAY,EAAE;AACZ,YAAA,OAAO,EAAE;gBACP,SAAS;AACV,aAAA;AACD,YAAA,cAAc,EAAE;AACd,gBAAA,MAAM,EAAE,QAAQ;AACjB,aAAA;YACD,GAAG,QAAQ,CAAC,YAAY;AACzB,SAAA;KACF;AACH;;;;;;;;;;;;"}
1
+ {"version":3,"file":"vite.cjs.js","sources":["../src/vite/index.ts"],"sourcesContent":[null],"names":["devBridgePlugin","spinPlugin"],"mappings":";;;;AAoCA;AAEA;;;;;;;;;;;;;;;;;;;;;;;AAuBG;AACG,SAAU,gBAAgB,CAAC,MAAA,GAAqB,EAAE,EAAA;IACtD,MAAM,OAAO,GAAa,EAAE;AAE5B,IAAA,IAAI,MAAM,CAAC,SAAS,EAAE;AACpB,QAAA,MAAM,UAAU,GAAG,MAAM,CAAC,eAAe,IAAI,cAAc;QAC3D,OAAO,CAAC,IAAI,CAACA,oBAAe,CAAC,UAAU,CAAC,CAAC;;AAEzC,QAAA,OAAO,CAAC,IAAI,CACVC,eAAU,CAAC;AACT,YAAA,QAAQ,EAAE,MAAM,CAAC,UAAU,IAAI,wBAAwB;YACvD,MAAM,EAAE,MAAM,CAAC,MAAM;YACrB,IAAI,EAAE,MAAM,CAAC,QAAQ;AACtB,SAAA,CAAC,CACH;IACH;AAEA,IAAA,MAAM,QAAQ,GAAG,MAAM,CAAC,IAAI,IAAI,EAAE;IAElC,OAAO;AACL,QAAA,IAAI,EAAE,MAAM,CAAC,IAAI,IAAI,IAAI;AAEzB,QAAA,OAAO,EAAE;AACP,YAAA,GAAG,OAAO;AACV,YAAA,IAAK,QAAQ,CAAC,OAAoB,IAAI,EAAE,CAAC;AAC1C,SAAA;AAED,QAAA,KAAK,EAAE;AACL,YAAA,MAAM,EAAE,QAAQ;AAChB,YAAA,iBAAiB,EAAE,IAAI;AACvB,YAAA,SAAS,EAAE,KAAK;AAChB,YAAA,aAAa,EAAE;AACb,gBAAA,MAAM,EAAE;AACN,oBAAA,YAAY,EAAE;wBACZ,IAAI,EAAE,CAAC,SAAS,CAAC;AAClB,qBAAA;AACF,iBAAA;AACF,aAAA;YACD,GAAG,QAAQ,CAAC,KAAK;AAClB,SAAA;AAED,QAAA,MAAM,EAAE;AACN,YAAA,IAAI,EAAE,IAAI;AACV,YAAA,IAAI,EAAE,IAAI;YACV,GAAG,QAAQ,CAAC,MAAM;AACnB,SAAA;AAED,QAAA,OAAO,EAAE;AACP,YAAA,MAAM,EAAE;gBACN,SAAS;AACV,aAAA;YACD,GAAG,QAAQ,CAAC,OAAO;AACpB,SAAA;AAED,QAAA,YAAY,EAAE;AACZ,YAAA,OAAO,EAAE;gBACP,SAAS;AACV,aAAA;AACD,YAAA,cAAc,EAAE;AACd,gBAAA,MAAM,EAAE,QAAQ;AACjB,aAAA;YACD,GAAG,QAAQ,CAAC,YAAY;AACzB,SAAA;KACF;AACH;;;;;;;;;;"}
@@ -1 +1 @@
1
- {"version":3,"file":"vite.esm.js","sources":["../src/vite/index.ts"],"sourcesContent":[null],"names":[],"mappings":";;;AAoCA;AAEA;;;;;;;;;;;;;;;;;;;;;;;AAuBG;AACG,SAAU,gBAAgB,CAAC,MAAA,GAAqB,EAAE,EAAA;IACtD,MAAM,OAAO,GAAa,EAAE;AAE5B,IAAA,IAAI,MAAM,CAAC,SAAS,EAAE;AACpB,QAAA,MAAM,UAAU,GAAG,MAAM,CAAC,eAAe,IAAI,cAAc;QAC3D,OAAO,CAAC,IAAI,CAAC,eAAe,CAAC,UAAU,CAAC,CAAC;;AAEzC,QAAA,OAAO,CAAC,IAAI,CACV,UAAU,CAAC;AACT,YAAA,QAAQ,EAAE,MAAM,CAAC,UAAU,IAAI,wBAAwB;YACvD,MAAM,EAAE,MAAM,CAAC,MAAM;YACrB,IAAI,EAAE,MAAM,CAAC,QAAQ;AACtB,SAAA,CAAC,CACH;IACH;AAEA,IAAA,MAAM,QAAQ,GAAG,MAAM,CAAC,IAAI,IAAI,EAAE;IAElC,OAAO;AACL,QAAA,IAAI,EAAE,MAAM,CAAC,IAAI,IAAI,IAAI;AAEzB,QAAA,OAAO,EAAE;AACP,YAAA,GAAG,OAAO;AACV,YAAA,IAAK,QAAQ,CAAC,OAAoB,IAAI,EAAE,CAAC;AAC1C,SAAA;AAED,QAAA,KAAK,EAAE;AACL,YAAA,MAAM,EAAE,QAAQ;AAChB,YAAA,iBAAiB,EAAE,IAAI;AACvB,YAAA,SAAS,EAAE,KAAK;AAChB,YAAA,aAAa,EAAE;AACb,gBAAA,MAAM,EAAE;AACN,oBAAA,YAAY,EAAE;wBACZ,IAAI,EAAE,CAAC,SAAS,CAAC;AAClB,qBAAA;AACF,iBAAA;AACF,aAAA;YACD,GAAG,QAAQ,CAAC,KAAK;AAClB,SAAA;AAED,QAAA,MAAM,EAAE;AACN,YAAA,IAAI,EAAE,IAAI;AACV,YAAA,IAAI,EAAE,IAAI;YACV,GAAG,QAAQ,CAAC,MAAM;AACnB,SAAA;AAED,QAAA,OAAO,EAAE;AACP,YAAA,MAAM,EAAE;gBACN,SAAS;AACV,aAAA;YACD,GAAG,QAAQ,CAAC,OAAO;AACpB,SAAA;AAED,QAAA,YAAY,EAAE;AACZ,YAAA,OAAO,EAAE;gBACP,SAAS;AACV,aAAA;AACD,YAAA,cAAc,EAAE;AACd,gBAAA,MAAM,EAAE,QAAQ;AACjB,aAAA;YACD,GAAG,QAAQ,CAAC,YAAY;AACzB,SAAA;KACF;AACH;;;;"}
1
+ {"version":3,"file":"vite.esm.js","sources":["../src/vite/index.ts"],"sourcesContent":[null],"names":[],"mappings":";;;AAoCA;AAEA;;;;;;;;;;;;;;;;;;;;;;;AAuBG;AACG,SAAU,gBAAgB,CAAC,MAAA,GAAqB,EAAE,EAAA;IACtD,MAAM,OAAO,GAAa,EAAE;AAE5B,IAAA,IAAI,MAAM,CAAC,SAAS,EAAE;AACpB,QAAA,MAAM,UAAU,GAAG,MAAM,CAAC,eAAe,IAAI,cAAc;QAC3D,OAAO,CAAC,IAAI,CAAC,eAAe,CAAC,UAAU,CAAC,CAAC;;AAEzC,QAAA,OAAO,CAAC,IAAI,CACV,UAAU,CAAC;AACT,YAAA,QAAQ,EAAE,MAAM,CAAC,UAAU,IAAI,wBAAwB;YACvD,MAAM,EAAE,MAAM,CAAC,MAAM;YACrB,IAAI,EAAE,MAAM,CAAC,QAAQ;AACtB,SAAA,CAAC,CACH;IACH;AAEA,IAAA,MAAM,QAAQ,GAAG,MAAM,CAAC,IAAI,IAAI,EAAE;IAElC,OAAO;AACL,QAAA,IAAI,EAAE,MAAM,CAAC,IAAI,IAAI,IAAI;AAEzB,QAAA,OAAO,EAAE;AACP,YAAA,GAAG,OAAO;AACV,YAAA,IAAK,QAAQ,CAAC,OAAoB,IAAI,EAAE,CAAC;AAC1C,SAAA;AAED,QAAA,KAAK,EAAE;AACL,YAAA,MAAM,EAAE,QAAQ;AAChB,YAAA,iBAAiB,EAAE,IAAI;AACvB,YAAA,SAAS,EAAE,KAAK;AAChB,YAAA,aAAa,EAAE;AACb,gBAAA,MAAM,EAAE;AACN,oBAAA,YAAY,EAAE;wBACZ,IAAI,EAAE,CAAC,SAAS,CAAC;AAClB,qBAAA;AACF,iBAAA;AACF,aAAA;YACD,GAAG,QAAQ,CAAC,KAAK;AAClB,SAAA;AAED,QAAA,MAAM,EAAE;AACN,YAAA,IAAI,EAAE,IAAI;AACV,YAAA,IAAI,EAAE,IAAI;YACV,GAAG,QAAQ,CAAC,MAAM;AACnB,SAAA;AAED,QAAA,OAAO,EAAE;AACP,YAAA,MAAM,EAAE;gBACN,SAAS;AACV,aAAA;YACD,GAAG,QAAQ,CAAC,OAAO;AACpB,SAAA;AAED,QAAA,YAAY,EAAE;AACZ,YAAA,OAAO,EAAE;gBACP,SAAS;AACV,aAAA;AACD,YAAA,cAAc,EAAE;AACd,gBAAA,MAAM,EAAE,QAAQ;AACjB,aAAA;YACD,GAAG,QAAQ,CAAC,YAAY;AACzB,SAAA;KACF;AACH;;"}
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@energy8platform/game-engine",
3
- "version": "0.40.0",
3
+ "version": "0.42.0",
4
4
  "description": "Universal casino game engine built on PixiJS v8 and @energy8platform/game-sdk",
5
5
  "type": "module",
6
6
  "main": "./dist/index.cjs.js",
@@ -94,7 +94,7 @@
94
94
  },
95
95
  "dependencies": {
96
96
  "@energy8platform/platform-core": ">=0.33.0",
97
- "@energy8platform/shell": ">=0.9.0"
97
+ "@energy8platform/shell": ">=0.10.0"
98
98
  },
99
99
  "peerDependencies": {
100
100
  "@energy8platform/game-sdk": "^2.9.0",
@@ -280,7 +280,8 @@ export async function createSlotGame<T extends SlotSpinResultBase = SlotSpinResu
280
280
  * and the Artube bridge fill it the same way, so the resumed-bet path below is shared. */
281
281
  session?: { betAmount?: number };
282
282
  /** Session currency CODE. The Artube bridge's only currency surface (the platform picks it
283
- * per session; demo sessions are 'FUN'); Stake sends full meta on `config.currency` instead. */
283
+ * per session; a demo session is labelled 'DEMO'); Stake sends full meta on
284
+ * `config.currency` instead. */
284
285
  currency?: string;
285
286
  lang?: string;
286
287
  /** The client the platform launched us on. Both bridges read it off the launch URL
@@ -307,9 +308,10 @@ export async function createSlotGame<T extends SlotSpinResultBase = SlotSpinResu
307
308
  // is absent and we only have the spec's currency CODE — resolve it through the SAME table
308
309
  // (stake-bridge's lookupCurrency) so e.g. 'EUR' renders as '€', not the literal text "EUR".
309
310
  // stake-bridge ships with every scaffold; if it's somehow absent we degrade to the code.
310
- // On Artube the session currency is the PLATFORM's (per player, and 'FUN' for demo sessions) and
311
- // arrives as a bare code on initData — there is no meta object. It outranks the spec's static
312
- // code, which would otherwise show every Artube player the spec's currency symbol.
311
+ // On Artube the session currency is the PLATFORM's (per player, and the word 'DEMO' for a demo
312
+ // session — see artube-bridge's displayCurrency) and arrives as a bare code on initData — there
313
+ // is no meta object. It outranks the spec's static code, which would otherwise show every
314
+ // Artube player the spec's currency symbol.
313
315
  const currencyCode = (isArtubeNow ? initData?.currency : undefined) || opts.model.spec.currency;
314
316
  let currencyMeta = config?.currency;
315
317
  if (!currencyMeta?.symbol && currencyCode) {
@@ -2,7 +2,7 @@
2
2
  // `socialize` / `createI18n` / `Lang` are renderer-agnostic helpers from the shell core
3
3
  // (@energy8platform/shell). Renderer-agnostic types (GameInfoSection/Content, PaytableRow,
4
4
  // GameMode) and the pixi-specific surface types come from the @energy8platform/shell/pixi entry.
5
- import { socialize, createI18n } from '@energy8platform/shell';
5
+ import { socialize, createI18n, DISCLAIMER_LINES } from '@energy8platform/shell';
6
6
  import type { Lang } from '@energy8platform/shell';
7
7
  import type { GameInfoContent, GameInfoSection, PaytableRow, GameMode } from '@energy8platform/shell/pixi';
8
8
  import type {
@@ -60,8 +60,11 @@ export interface ShellRuntime {
60
60
  mode: ShellMode;
61
61
  /** Social-casino mode from initData (`config.socialMode`); swaps shell vocabulary. */
62
62
  social?: boolean;
63
- /** Stake-required disclaimer lines from initData (`config.disclaimerLines`); when
64
- * absent (non-stake/dev) no disclaimer section is rendered. */
63
+ /** Disclaimer lines the PLATFORM supplied (`initData.config.disclaimerLines` — in practice the
64
+ * Stake bridge, which appends its own "TM and © {year} Stake Engine." to the mandated body).
65
+ * They outrank the default. When absent — Artube, dev, any other host — the shell's canonical
66
+ * `DISCLAIMER_LINES` are used: the same mandated wording, minus a mark that isn't theirs. The
67
+ * disclaimer is required legal copy, so there is no case where the section is simply missing. */
65
68
  disclaimerLines?: string[];
66
69
  /** Jurisdiction flags from initData (`config.jurisdiction`). Restrict shell features — applied
67
70
  * OVER the author's features so a jurisdiction restriction always wins. */
@@ -268,7 +271,7 @@ function isBrandLine(line: string): boolean {
268
271
  * caller passes a translation-only resolver (never socializing) — mandated legal copy must not be
269
272
  * word-swapped even in social mode. */
270
273
  function disclaimerSection(
271
- lines: string[] | undefined,
274
+ lines: readonly string[] | undefined,
272
275
  t: (s: string) => string = (s) => s,
273
276
  ): GameInfoSection | null {
274
277
  const clean = (lines ?? []).map((l) => l.trim()).filter(Boolean);
@@ -312,7 +315,7 @@ export function defaultGameInfo(
312
315
  const modes = modesSection(model, t);
313
316
  if (modes) sections.push(modes);
314
317
  sections.push({ type: 'controls' });
315
- const disclaimer = disclaimerSection(runtime.disclaimerLines, tDisclaimer);
318
+ const disclaimer = disclaimerSection(runtime.disclaimerLines ?? DISCLAIMER_LINES, tDisclaimer);
316
319
  if (disclaimer) sections.push(disclaimer);
317
320
  return { sections };
318
321
  }
@@ -184,6 +184,8 @@ export interface AnticipationOverride {
184
184
  slowdown?: PerReel<number>;
185
185
  /** Extra hold before landing. Scalar, or per-reel indexed by reel index. */
186
186
  holdMs?: PerReel<number>;
187
+ /** Growth of the gap between successive cells within a reel. Scalar, or per-reel. */
188
+ cellStaggerRamp?: PerReel<number>;
187
189
  }
188
190
 
189
191
  export interface AnticipationConfig {
@@ -213,6 +215,16 @@ export interface AnticipationConfig {
213
215
  progressiveSlowdown: number;
214
216
  /** Extra hold (ms) added per successive anticipated reel: reel #i gets `holdMs + i * this`. */
215
217
  progressiveHoldMs: number;
218
+ /**
219
+ * `cascade-drop`: how the gap between SUCCESSIVE CELLS grows inside an anticipated reel. The
220
+ * gap before cell #i is `cellStagger * <reel slowdown> * progressiveCellStagger ** i`, so 1
221
+ * (the default) keeps the reel's cells evenly spaced and > 1 makes each symbol land later than
222
+ * the last — the drip that turns a reel into a countdown.
223
+ *
224
+ * It compounds with `progressiveSlowdown`: that widens the base gap reel by reel, this widens
225
+ * it cell by cell, so a later reel drips both slower AND with a steeper ramp.
226
+ */
227
+ progressiveCellStagger: number;
216
228
  /** Optional grid zoom while anticipating (magnum-opus uses 1.3×). */
217
229
  zoom: { enabled: boolean; scale: number; ms: number };
218
230
  }
@@ -503,6 +515,7 @@ export const DEFAULT_REEL_CONFIG: ReelSystemConfig = {
503
515
  decide: null,
504
516
  progressiveSlowdown: 1,
505
517
  progressiveHoldMs: 0,
518
+ progressiveCellStagger: 1,
506
519
  zoom: { enabled: false, scale: 1.15, ms: 600 },
507
520
  },
508
521
  cascade: {
@@ -151,6 +151,7 @@ export const REEL_FIELD_SCHEMA: Section[] = [
151
151
  { kind: 'range', path: 'anticipation.holdMs', label: 'Hold (ms)', min: 0, max: 1200, step: 50 },
152
152
  { kind: 'range', path: 'anticipation.progressiveSlowdown', label: 'Slowdown ramp ×/reel', min: 0.3, max: 1, step: 0.05 },
153
153
  { kind: 'range', path: 'anticipation.progressiveHoldMs', label: 'Hold ramp (ms/reel)', min: 0, max: 600, step: 25 },
154
+ { kind: 'range', path: 'anticipation.progressiveCellStagger', label: 'Cell ramp ×/cell', min: 1, max: 2.5, step: 0.05 },
154
155
  { kind: 'toggle', path: 'anticipation.zoom.enabled', label: 'Reel zoom' },
155
156
  { kind: 'range', path: 'anticipation.zoom.scale', label: 'Zoom scale', min: 1, max: 1.6, step: 0.05 },
156
157
  { kind: 'range', path: 'anticipation.zoom.ms', label: 'Zoom (ms)', min: 200, max: 1200, step: 50 },
@@ -18,10 +18,18 @@ export interface AnticipationDecision {
18
18
  slowdown: PerReel<number>;
19
19
  /** Extra hold before landing. Scalar or per-reel array, same as `slowdown`. */
20
20
  holdMs: PerReel<number>;
21
+ /** `cascade-drop`: growth of the gap between successive cells inside the reel (1 = even). */
22
+ cellStaggerRamp: PerReel<number>;
21
23
  }
22
24
 
23
25
  /** Fresh "nothing to anticipate" decision (fresh, so callers may mutate `reels` freely). */
24
- const none = (): AnticipationDecision => ({ active: false, reels: [], slowdown: 1, holdMs: 0 });
26
+ const none = (): AnticipationDecision => ({
27
+ active: false,
28
+ reels: [],
29
+ slowdown: 1,
30
+ holdMs: 0,
31
+ cellStaggerRamp: 1,
32
+ });
25
33
 
26
34
  export class AnticipationController {
27
35
  private _cfg: AnticipationConfig;
@@ -53,7 +61,7 @@ export class AnticipationController {
53
61
  if (!custom) return none();
54
62
  const o: AnticipationOverride = Array.isArray(custom) ? { reels: custom } : custom;
55
63
  if (!o.reels?.length) return none();
56
- return this.build(o.reels.slice(), o.slowdown, o.holdMs);
64
+ return this.build(o.reels.slice(), o.slowdown, o.holdMs, o.cellStaggerRamp);
57
65
  }
58
66
 
59
67
  if (Array.isArray(this._cfg.reels)) {
@@ -86,10 +94,12 @@ export class AnticipationController {
86
94
  reels: number[],
87
95
  slowdown?: PerReel<number>,
88
96
  holdMs?: PerReel<number>,
97
+ cellStaggerRamp?: PerReel<number>,
89
98
  ): AnticipationDecision {
90
99
  return {
91
100
  active: true,
92
101
  reels,
102
+ cellStaggerRamp: cellStaggerRamp ?? this._cfg.progressiveCellStagger,
93
103
  slowdown:
94
104
  slowdown ??
95
105
  this.ramp(reels, this._cfg.slowdownFactor, (base, i) =>
@@ -38,6 +38,12 @@ export interface SpinRunOpts {
38
38
  anticipateSlowdown?: PerReel<number>;
39
39
  /** Extra hold (ms) for anticipated reels. Scalar, or per-reel indexed by reel. */
40
40
  anticipateHoldMs?: PerReel<number>;
41
+ /**
42
+ * `cascade-drop`: growth of the gap between successive cells inside an anticipated reel — the
43
+ * gap before cell #i is `cellStagger * <reel slowdown> * ramp ** i`. 1 keeps the reel evenly
44
+ * spaced. Scalar, or per-reel indexed by reel.
45
+ */
46
+ anticipateCellStaggerRamp?: PerReel<number>;
41
47
  /**
42
48
  * Reels whose tape runs normally but whose landing is NOT handed back. The engine stops and
43
49
  * disposes of the tape as usual and leaves the real cells hidden and unseated; the caller owns
@@ -137,6 +143,7 @@ export class SpinEngine {
137
143
  const anticipate = new Set(opts?.anticipateReels ?? []);
138
144
  const defer = new Set(opts?.deferReveal ?? []);
139
145
  const holds: number[] = [];
146
+ const ramps: number[] = [];
140
147
  const out: ReelStopPlan[] = [];
141
148
  for (let reel = 0; reel < cols; reel++) {
142
149
  const idx = order(reel);
@@ -154,6 +161,7 @@ export class SpinEngine {
154
161
  const hold = isAnticipated ? perReelValue(opts?.anticipateHoldMs, reel, 0) * f : 0;
155
162
  stopTime += hold;
156
163
  holds[reel] = hold;
164
+ ramps[reel] = isAnticipated ? perReelValue(opts?.anticipateCellStaggerRamp, reel, 1) : 1;
157
165
  const speed = isAnticipated ? perReelValue(opts?.anticipateSlowdown, reel, 1) : 1;
158
166
 
159
167
  out.push({
@@ -166,7 +174,7 @@ export class SpinEngine {
166
174
  deferred: defer.has(reel),
167
175
  });
168
176
  }
169
- if (this._cfg.style === 'cascade-drop') this._planDrop(out, holds, f, order);
177
+ if (this._cfg.style === 'cascade-drop') this._planDrop(out, holds, ramps, f, order);
170
178
  return out;
171
179
  }
172
180
 
@@ -179,6 +187,7 @@ export class SpinEngine {
179
187
  private _planDrop(
180
188
  out: ReelStopPlan[],
181
189
  holds: number[],
190
+ ramps: number[],
182
191
  f: number,
183
192
  order: (reel: number) => number,
184
193
  ): void {
@@ -205,10 +214,15 @@ export class SpinEngine {
205
214
  const chained = chainFrom >= 0 && position > 0 && position >= chainFrom;
206
215
  const start = (chained ? previousEnd + gap : position * gap) + (holds[p.reel] ?? 0);
207
216
 
217
+ // The gap before cell #i is `cellStagger * scale * ramp**i` — accumulate rather than
218
+ // multiply, so a ramp of 1 stays exactly the old evenly-spaced schedule.
219
+ const ramp = ramps[p.reel] ?? 1;
208
220
  const cells: number[] = [];
221
+ let offset = 0;
209
222
  for (let i = 0; i < rows; i++) {
210
223
  const row = bottomUp ? rows - 1 - i : i;
211
- cells[row] = start + i * this._cfg.cellStagger * scale + fall;
224
+ cells[row] = start + offset + fall;
225
+ offset += this._cfg.cellStagger * scale * Math.pow(ramp, i);
212
226
  }
213
227
  p.cellStopTimes = cells;
214
228
  p.stopTime = cells.length ? Math.max(...cells) : start;
@@ -215,12 +215,15 @@ export function createReelSystem(opts: CreateReelSystemOptions): ReelSystem {
215
215
  function resolveAnticipation(target: CellData[][], runOpts?: SpinRunOpts): AnticipationDecision {
216
216
  const explicit = runOpts?.anticipateReels;
217
217
  if (!explicit) return anticipation.decide(target);
218
- if (!explicit.length) return { active: false, reels: [], slowdown: 1, holdMs: 0 };
218
+ if (!explicit.length)
219
+ return { active: false, reels: [], slowdown: 1, holdMs: 0, cellStaggerRamp: 1 };
219
220
  return {
220
221
  active: true,
221
222
  reels: explicit.slice(),
222
223
  slowdown: runOpts?.anticipateSlowdown ?? config.anticipation.slowdownFactor,
223
224
  holdMs: runOpts?.anticipateHoldMs ?? config.anticipation.holdMs,
225
+ cellStaggerRamp:
226
+ runOpts?.anticipateCellStaggerRamp ?? config.anticipation.progressiveCellStagger,
224
227
  };
225
228
  }
226
229
 
@@ -234,6 +237,7 @@ export function createReelSystem(opts: CreateReelSystemOptions): ReelSystem {
234
237
  anticipateReels: decision.active ? decision.reels : undefined,
235
238
  anticipateSlowdown: decision.slowdown,
236
239
  anticipateHoldMs: decision.holdMs,
240
+ anticipateCellStaggerRamp: decision.cellStaggerRamp,
237
241
  };
238
242
  }
239
243