@energy8platform/game-engine 0.39.0 → 0.41.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 +52 -2
- package/dist/devtools.cjs.js +6 -0
- package/dist/devtools.cjs.js.map +1 -1
- package/dist/devtools.d.ts +35 -0
- package/dist/devtools.esm.js +6 -0
- package/dist/devtools.esm.js.map +1 -1
- package/dist/reel-panel-client.cjs.js +6 -0
- package/dist/reel-panel-client.cjs.js.map +1 -1
- package/dist/reel-panel-client.esm.js +6 -0
- package/dist/reel-panel-client.esm.js.map +1 -1
- package/dist/slot.cjs.js +72 -11
- package/dist/slot.cjs.js.map +1 -1
- package/dist/slot.d.ts +58 -1
- package/dist/slot.esm.js +72 -11
- package/dist/slot.esm.js.map +1 -1
- package/package.json +1 -1
- package/src/slot/config/ReelSystemConfig.ts +38 -0
- package/src/slot/devtools/fieldSchema.ts +3 -0
- package/src/slot/index.ts +2 -0
- package/src/slot/motion/AnticipationController.ts +12 -2
- package/src/slot/motion/SpinEngine.ts +77 -12
- package/src/slot/system/ReelSystem.ts +5 -1
package/package.json
CHANGED
|
@@ -92,6 +92,10 @@ export type MotionStyle =
|
|
|
92
92
|
| 'cascade-drop'; // symbols drop in from above (tumble-style boards)
|
|
93
93
|
|
|
94
94
|
export type StopMode = 'sequential' | 'sync' | 'random';
|
|
95
|
+
/** `cascade-drop` fill direction within one reel. */
|
|
96
|
+
export type DropOrder = 'top-down' | 'bottom-up';
|
|
97
|
+
/** How `cascade-drop` spaces reels: by formula, or one strictly after the other. */
|
|
98
|
+
export type DropSequence = 'parallel' | 'chained' | 'chained-when-anticipated';
|
|
95
99
|
export type StopOrder = 'ltr' | 'rtl';
|
|
96
100
|
export type Intensity = 'full' | 'reduced' | 'minimal';
|
|
97
101
|
|
|
@@ -147,6 +151,25 @@ export interface MotionConfig {
|
|
|
147
151
|
reelStaggerFactor: number;
|
|
148
152
|
/** `cascade-drop`: fall duration as a fraction of `spinUp`. Default 0.6. */
|
|
149
153
|
dropFallFactor: number;
|
|
154
|
+
/**
|
|
155
|
+
* `cascade-drop`: which cell of a reel lands first. `'top-down'` (default, the engine's original
|
|
156
|
+
* behaviour) deals the reel like cards from the top; `'bottom-up'` fills it the way gravity
|
|
157
|
+
* would — the lowest cell arrives first and the rest stack on top of it.
|
|
158
|
+
*/
|
|
159
|
+
dropOrder: DropOrder;
|
|
160
|
+
/**
|
|
161
|
+
* `cascade-drop`: how reels are spaced.
|
|
162
|
+
* - `'parallel'` (default) starts every reel at its own formula offset —
|
|
163
|
+
* `reel * stopStagger * reelStaggerFactor` — so a reel can open while the previous one is
|
|
164
|
+
* still dropping. Fast, and what a normal spin wants.
|
|
165
|
+
* - `'chained'` starts a reel only once the previous one has seated its LAST cell, plus that
|
|
166
|
+
* same offset as the gap. One reel at a time, always — correct, but it makes every spin as
|
|
167
|
+
* long as the sum of its reels.
|
|
168
|
+
* - `'chained-when-anticipated'` runs the un-armed reels in parallel and switches to the chain
|
|
169
|
+
* from the first ANTICIPATED reel onwards. The base spin keeps its stop window; the hunt for
|
|
170
|
+
* the last scatter goes strictly reel by reel, each slower than the last.
|
|
171
|
+
*/
|
|
172
|
+
dropSequence: DropSequence;
|
|
150
173
|
}
|
|
151
174
|
|
|
152
175
|
// ─────────────────────────────────────────────────────────────────────────────
|
|
@@ -161,6 +184,8 @@ export interface AnticipationOverride {
|
|
|
161
184
|
slowdown?: PerReel<number>;
|
|
162
185
|
/** Extra hold before landing. Scalar, or per-reel indexed by reel index. */
|
|
163
186
|
holdMs?: PerReel<number>;
|
|
187
|
+
/** Growth of the gap between successive cells within a reel. Scalar, or per-reel. */
|
|
188
|
+
cellStaggerRamp?: PerReel<number>;
|
|
164
189
|
}
|
|
165
190
|
|
|
166
191
|
export interface AnticipationConfig {
|
|
@@ -190,6 +215,16 @@ export interface AnticipationConfig {
|
|
|
190
215
|
progressiveSlowdown: number;
|
|
191
216
|
/** Extra hold (ms) added per successive anticipated reel: reel #i gets `holdMs + i * this`. */
|
|
192
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;
|
|
193
228
|
/** Optional grid zoom while anticipating (magnum-opus uses 1.3×). */
|
|
194
229
|
zoom: { enabled: boolean; scale: number; ms: number };
|
|
195
230
|
}
|
|
@@ -467,6 +502,8 @@ export const DEFAULT_REEL_CONFIG: ReelSystemConfig = {
|
|
|
467
502
|
cellStagger: 24,
|
|
468
503
|
reelStaggerFactor: 0.4,
|
|
469
504
|
dropFallFactor: 0.6,
|
|
505
|
+
dropOrder: 'top-down',
|
|
506
|
+
dropSequence: 'parallel',
|
|
470
507
|
},
|
|
471
508
|
anticipation: {
|
|
472
509
|
enabled: false,
|
|
@@ -478,6 +515,7 @@ export const DEFAULT_REEL_CONFIG: ReelSystemConfig = {
|
|
|
478
515
|
decide: null,
|
|
479
516
|
progressiveSlowdown: 1,
|
|
480
517
|
progressiveHoldMs: 0,
|
|
518
|
+
progressiveCellStagger: 1,
|
|
481
519
|
zoom: { enabled: false, scale: 1.15, ms: 600 },
|
|
482
520
|
},
|
|
483
521
|
cascade: {
|
|
@@ -137,6 +137,8 @@ export const REEL_FIELD_SCHEMA: Section[] = [
|
|
|
137
137
|
{ kind: 'range', path: 'motion.cellStagger', label: 'Cell stagger (ms)', min: 0, max: 700, step: 5 },
|
|
138
138
|
{ kind: 'range', path: 'motion.reelStaggerFactor', label: 'Reel stagger ×', min: 0, max: 3, step: 0.05 },
|
|
139
139
|
{ kind: 'range', path: 'motion.dropFallFactor', label: 'Drop fall ×', min: 0.1, max: 3, step: 0.05 },
|
|
140
|
+
{ kind: 'select', path: 'motion.dropOrder', label: 'Drop order', options: ['top-down', 'bottom-up'] },
|
|
141
|
+
{ kind: 'select', path: 'motion.dropSequence', label: 'Drop sequence', options: ['parallel', 'chained', 'chained-when-anticipated'] },
|
|
140
142
|
],
|
|
141
143
|
},
|
|
142
144
|
{
|
|
@@ -149,6 +151,7 @@ export const REEL_FIELD_SCHEMA: Section[] = [
|
|
|
149
151
|
{ kind: 'range', path: 'anticipation.holdMs', label: 'Hold (ms)', min: 0, max: 1200, step: 50 },
|
|
150
152
|
{ kind: 'range', path: 'anticipation.progressiveSlowdown', label: 'Slowdown ramp ×/reel', min: 0.3, max: 1, step: 0.05 },
|
|
151
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 },
|
|
152
155
|
{ kind: 'toggle', path: 'anticipation.zoom.enabled', label: 'Reel zoom' },
|
|
153
156
|
{ kind: 'range', path: 'anticipation.zoom.scale', label: 'Zoom scale', min: 1, max: 1.6, step: 0.05 },
|
|
154
157
|
{ kind: 'range', path: 'anticipation.zoom.ms', label: 'Zoom (ms)', min: 200, max: 1200, step: 50 },
|
package/src/slot/index.ts
CHANGED
|
@@ -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 => ({
|
|
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
|
|
@@ -68,6 +74,13 @@ export interface ReelStopPlan {
|
|
|
68
74
|
slowdown: number;
|
|
69
75
|
/** True when `deferReveal` withheld this reel's landing (see `SpinRunOpts.deferReveal`). */
|
|
70
76
|
deferred: boolean;
|
|
77
|
+
/**
|
|
78
|
+
* `cascade-drop` only: ms from spin start at which each cell of this reel seats, INDEXED BY ROW.
|
|
79
|
+
* The reel's fill direction lives in these numbers (`motion.dropOrder` decides which row is the
|
|
80
|
+
* smallest), as does its place in the chain (`motion.dropSequence`). `stopTime` is the largest
|
|
81
|
+
* of them — the frame the whole reel has landed.
|
|
82
|
+
*/
|
|
83
|
+
cellStopTimes?: number[];
|
|
71
84
|
}
|
|
72
85
|
|
|
73
86
|
export class SpinEngine {
|
|
@@ -129,6 +142,8 @@ export class SpinEngine {
|
|
|
129
142
|
const order = (reel: number) => (this._cfg.stopOrder === 'rtl' ? cols - 1 - reel : reel);
|
|
130
143
|
const anticipate = new Set(opts?.anticipateReels ?? []);
|
|
131
144
|
const defer = new Set(opts?.deferReveal ?? []);
|
|
145
|
+
const holds: number[] = [];
|
|
146
|
+
const ramps: number[] = [];
|
|
132
147
|
const out: ReelStopPlan[] = [];
|
|
133
148
|
for (let reel = 0; reel < cols; reel++) {
|
|
134
149
|
const idx = order(reel);
|
|
@@ -143,7 +158,10 @@ export class SpinEngine {
|
|
|
143
158
|
else stopTime = (this._cfg.spinUp + this._cfg.hold + idx * this._cfg.stopStagger) * f;
|
|
144
159
|
|
|
145
160
|
const isAnticipated = anticipate.has(reel);
|
|
146
|
-
|
|
161
|
+
const hold = isAnticipated ? perReelValue(opts?.anticipateHoldMs, reel, 0) * f : 0;
|
|
162
|
+
stopTime += hold;
|
|
163
|
+
holds[reel] = hold;
|
|
164
|
+
ramps[reel] = isAnticipated ? perReelValue(opts?.anticipateCellStaggerRamp, reel, 1) : 1;
|
|
147
165
|
const speed = isAnticipated ? perReelValue(opts?.anticipateSlowdown, reel, 1) : 1;
|
|
148
166
|
|
|
149
167
|
out.push({
|
|
@@ -156,9 +174,62 @@ export class SpinEngine {
|
|
|
156
174
|
deferred: defer.has(reel),
|
|
157
175
|
});
|
|
158
176
|
}
|
|
177
|
+
if (this._cfg.style === 'cascade-drop') this._planDrop(out, holds, ramps, f, order);
|
|
159
178
|
return out;
|
|
160
179
|
}
|
|
161
180
|
|
|
181
|
+
/**
|
|
182
|
+
* `cascade-drop` lays its reels out on a different clock from the tape styles: a reel is a
|
|
183
|
+
* sequence of per-cell arrivals, not one deceleration. Overwrite `stopTime` with the moment the
|
|
184
|
+
* reel has fully landed and fill in `cellStopTimes`, so `plan()` stays the single source of truth
|
|
185
|
+
* for WHEN anything happens — `_runDrop` below only executes these numbers.
|
|
186
|
+
*/
|
|
187
|
+
private _planDrop(
|
|
188
|
+
out: ReelStopPlan[],
|
|
189
|
+
holds: number[],
|
|
190
|
+
ramps: number[],
|
|
191
|
+
f: number,
|
|
192
|
+
order: (reel: number) => number,
|
|
193
|
+
): void {
|
|
194
|
+
const bottomUp = this._cfg.dropOrder === 'bottom-up';
|
|
195
|
+
// walk the reels in STOP order, so `stopOrder: 'rtl'` reverses the drop as well
|
|
196
|
+
const byPosition: number[] = [];
|
|
197
|
+
for (let reel = 0; reel < out.length; reel++) byPosition[order(reel)] = reel;
|
|
198
|
+
// the position from which reels stop overlapping and start queueing behind each other
|
|
199
|
+
const chainFrom =
|
|
200
|
+
this._cfg.dropSequence === 'chained'
|
|
201
|
+
? 0
|
|
202
|
+
: this._cfg.dropSequence === 'chained-when-anticipated'
|
|
203
|
+
? byPosition.findIndex((reel) => out[reel].anticipated)
|
|
204
|
+
: -1;
|
|
205
|
+
|
|
206
|
+
let previousEnd = 0;
|
|
207
|
+
for (let position = 0; position < byPosition.length; position++) {
|
|
208
|
+
const p = out[byPosition[position]];
|
|
209
|
+
const rows = this._grid.rowsOf(p.reel);
|
|
210
|
+
const scale = f * p.slowdown;
|
|
211
|
+
const gap = this._cfg.stopStagger * this._cfg.reelStaggerFactor * scale;
|
|
212
|
+
const fall = this._cfg.spinUp * this._cfg.dropFallFactor * scale;
|
|
213
|
+
// by formula while reels may overlap; queued behind the previous reel once the chain starts
|
|
214
|
+
const chained = chainFrom >= 0 && position > 0 && position >= chainFrom;
|
|
215
|
+
const start = (chained ? previousEnd + gap : position * gap) + (holds[p.reel] ?? 0);
|
|
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;
|
|
220
|
+
const cells: number[] = [];
|
|
221
|
+
let offset = 0;
|
|
222
|
+
for (let i = 0; i < rows; i++) {
|
|
223
|
+
const row = bottomUp ? rows - 1 - i : i;
|
|
224
|
+
cells[row] = start + offset + fall;
|
|
225
|
+
offset += this._cfg.cellStagger * scale * Math.pow(ramp, i);
|
|
226
|
+
}
|
|
227
|
+
p.cellStopTimes = cells;
|
|
228
|
+
p.stopTime = cells.length ? Math.max(...cells) : start;
|
|
229
|
+
previousEnd = p.stopTime;
|
|
230
|
+
}
|
|
231
|
+
}
|
|
232
|
+
|
|
162
233
|
/** Execute the spin for every reel concurrently. */
|
|
163
234
|
async run(data: SpinData, opts?: SpinRunOpts): Promise<void> {
|
|
164
235
|
this._killed = false;
|
|
@@ -328,6 +399,8 @@ export class SpinEngine {
|
|
|
328
399
|
}
|
|
329
400
|
const step = this._grid.cellPosition(p.reel, 1).y - this._grid.cellPosition(p.reel, 0).y;
|
|
330
401
|
const slow = this.slowOf(p); // anticipation drops the reel in more slowly
|
|
402
|
+
const fall = this._cfg.spinUp * this._cfg.dropFallFactor * f * slow;
|
|
403
|
+
const schedule = p.cellStopTimes ?? [];
|
|
331
404
|
await Promise.all(
|
|
332
405
|
Array.from({ length: rows }, (_, r) => r).map(async (r) => {
|
|
333
406
|
if (this._killed) return;
|
|
@@ -337,18 +410,10 @@ export class SpinEngine {
|
|
|
337
410
|
cell.setData(data);
|
|
338
411
|
cell.position.set(to.x, to.y - step * (rows + 1));
|
|
339
412
|
cell.alpha = 1;
|
|
340
|
-
|
|
341
|
-
|
|
342
|
-
r * this._cfg.cellStagger) *
|
|
343
|
-
f *
|
|
344
|
-
slow;
|
|
413
|
+
// the plan says WHEN this cell seats; back off the fall to get when it must let go
|
|
414
|
+
const delay = Math.max(0, (schedule[r] ?? fall) - fall);
|
|
345
415
|
if (delay) await Tween.delay(delay);
|
|
346
|
-
await Tween.to(
|
|
347
|
-
cell,
|
|
348
|
-
{ 'position.y': to.y },
|
|
349
|
-
this._cfg.spinUp * this._cfg.dropFallFactor * f * slow,
|
|
350
|
-
easingByName(this._cfg.settle.easing),
|
|
351
|
-
);
|
|
416
|
+
await Tween.to(cell, { 'position.y': to.y }, fall, easingByName(this._cfg.settle.easing));
|
|
352
417
|
// the impact frame — fired before the squash so a game can sync its own hit feedback
|
|
353
418
|
opts?.onCellSeated?.(p.reel, r, data);
|
|
354
419
|
await this._squashCell(cell, f);
|
|
@@ -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)
|
|
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
|
|