@energy8platform/game-engine 0.39.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.
- package/README.md +37 -1
- package/dist/devtools.cjs.js +4 -0
- package/dist/devtools.cjs.js.map +1 -1
- package/dist/devtools.d.ts +23 -0
- package/dist/devtools.esm.js +4 -0
- package/dist/devtools.esm.js.map +1 -1
- package/dist/reel-panel-client.cjs.js +4 -0
- package/dist/reel-panel-client.cjs.js.map +1 -1
- package/dist/reel-panel-client.esm.js +4 -0
- package/dist/reel-panel-client.esm.js.map +1 -1
- package/dist/slot.cjs.js +51 -7
- package/dist/slot.cjs.js.map +1 -1
- package/dist/slot.d.ts +38 -1
- package/dist/slot.esm.js +51 -7
- package/dist/slot.esm.js.map +1 -1
- package/package.json +1 -1
- package/src/slot/config/ReelSystemConfig.ts +25 -0
- package/src/slot/devtools/fieldSchema.ts +2 -0
- package/src/slot/index.ts +2 -0
- package/src/slot/motion/SpinEngine.ts +63 -12
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
|
// ─────────────────────────────────────────────────────────────────────────────
|
|
@@ -467,6 +490,8 @@ export const DEFAULT_REEL_CONFIG: ReelSystemConfig = {
|
|
|
467
490
|
cellStagger: 24,
|
|
468
491
|
reelStaggerFactor: 0.4,
|
|
469
492
|
dropFallFactor: 0.6,
|
|
493
|
+
dropOrder: 'top-down',
|
|
494
|
+
dropSequence: 'parallel',
|
|
470
495
|
},
|
|
471
496
|
anticipation: {
|
|
472
497
|
enabled: false,
|
|
@@ -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
|
{
|
package/src/slot/index.ts
CHANGED
|
@@ -68,6 +68,13 @@ export interface ReelStopPlan {
|
|
|
68
68
|
slowdown: number;
|
|
69
69
|
/** True when `deferReveal` withheld this reel's landing (see `SpinRunOpts.deferReveal`). */
|
|
70
70
|
deferred: boolean;
|
|
71
|
+
/**
|
|
72
|
+
* `cascade-drop` only: ms from spin start at which each cell of this reel seats, INDEXED BY ROW.
|
|
73
|
+
* The reel's fill direction lives in these numbers (`motion.dropOrder` decides which row is the
|
|
74
|
+
* smallest), as does its place in the chain (`motion.dropSequence`). `stopTime` is the largest
|
|
75
|
+
* of them — the frame the whole reel has landed.
|
|
76
|
+
*/
|
|
77
|
+
cellStopTimes?: number[];
|
|
71
78
|
}
|
|
72
79
|
|
|
73
80
|
export class SpinEngine {
|
|
@@ -129,6 +136,7 @@ export class SpinEngine {
|
|
|
129
136
|
const order = (reel: number) => (this._cfg.stopOrder === 'rtl' ? cols - 1 - reel : reel);
|
|
130
137
|
const anticipate = new Set(opts?.anticipateReels ?? []);
|
|
131
138
|
const defer = new Set(opts?.deferReveal ?? []);
|
|
139
|
+
const holds: number[] = [];
|
|
132
140
|
const out: ReelStopPlan[] = [];
|
|
133
141
|
for (let reel = 0; reel < cols; reel++) {
|
|
134
142
|
const idx = order(reel);
|
|
@@ -143,7 +151,9 @@ export class SpinEngine {
|
|
|
143
151
|
else stopTime = (this._cfg.spinUp + this._cfg.hold + idx * this._cfg.stopStagger) * f;
|
|
144
152
|
|
|
145
153
|
const isAnticipated = anticipate.has(reel);
|
|
146
|
-
|
|
154
|
+
const hold = isAnticipated ? perReelValue(opts?.anticipateHoldMs, reel, 0) * f : 0;
|
|
155
|
+
stopTime += hold;
|
|
156
|
+
holds[reel] = hold;
|
|
147
157
|
const speed = isAnticipated ? perReelValue(opts?.anticipateSlowdown, reel, 1) : 1;
|
|
148
158
|
|
|
149
159
|
out.push({
|
|
@@ -156,9 +166,56 @@ export class SpinEngine {
|
|
|
156
166
|
deferred: defer.has(reel),
|
|
157
167
|
});
|
|
158
168
|
}
|
|
169
|
+
if (this._cfg.style === 'cascade-drop') this._planDrop(out, holds, f, order);
|
|
159
170
|
return out;
|
|
160
171
|
}
|
|
161
172
|
|
|
173
|
+
/**
|
|
174
|
+
* `cascade-drop` lays its reels out on a different clock from the tape styles: a reel is a
|
|
175
|
+
* sequence of per-cell arrivals, not one deceleration. Overwrite `stopTime` with the moment the
|
|
176
|
+
* reel has fully landed and fill in `cellStopTimes`, so `plan()` stays the single source of truth
|
|
177
|
+
* for WHEN anything happens — `_runDrop` below only executes these numbers.
|
|
178
|
+
*/
|
|
179
|
+
private _planDrop(
|
|
180
|
+
out: ReelStopPlan[],
|
|
181
|
+
holds: number[],
|
|
182
|
+
f: number,
|
|
183
|
+
order: (reel: number) => number,
|
|
184
|
+
): void {
|
|
185
|
+
const bottomUp = this._cfg.dropOrder === 'bottom-up';
|
|
186
|
+
// walk the reels in STOP order, so `stopOrder: 'rtl'` reverses the drop as well
|
|
187
|
+
const byPosition: number[] = [];
|
|
188
|
+
for (let reel = 0; reel < out.length; reel++) byPosition[order(reel)] = reel;
|
|
189
|
+
// the position from which reels stop overlapping and start queueing behind each other
|
|
190
|
+
const chainFrom =
|
|
191
|
+
this._cfg.dropSequence === 'chained'
|
|
192
|
+
? 0
|
|
193
|
+
: this._cfg.dropSequence === 'chained-when-anticipated'
|
|
194
|
+
? byPosition.findIndex((reel) => out[reel].anticipated)
|
|
195
|
+
: -1;
|
|
196
|
+
|
|
197
|
+
let previousEnd = 0;
|
|
198
|
+
for (let position = 0; position < byPosition.length; position++) {
|
|
199
|
+
const p = out[byPosition[position]];
|
|
200
|
+
const rows = this._grid.rowsOf(p.reel);
|
|
201
|
+
const scale = f * p.slowdown;
|
|
202
|
+
const gap = this._cfg.stopStagger * this._cfg.reelStaggerFactor * scale;
|
|
203
|
+
const fall = this._cfg.spinUp * this._cfg.dropFallFactor * scale;
|
|
204
|
+
// by formula while reels may overlap; queued behind the previous reel once the chain starts
|
|
205
|
+
const chained = chainFrom >= 0 && position > 0 && position >= chainFrom;
|
|
206
|
+
const start = (chained ? previousEnd + gap : position * gap) + (holds[p.reel] ?? 0);
|
|
207
|
+
|
|
208
|
+
const cells: number[] = [];
|
|
209
|
+
for (let i = 0; i < rows; i++) {
|
|
210
|
+
const row = bottomUp ? rows - 1 - i : i;
|
|
211
|
+
cells[row] = start + i * this._cfg.cellStagger * scale + fall;
|
|
212
|
+
}
|
|
213
|
+
p.cellStopTimes = cells;
|
|
214
|
+
p.stopTime = cells.length ? Math.max(...cells) : start;
|
|
215
|
+
previousEnd = p.stopTime;
|
|
216
|
+
}
|
|
217
|
+
}
|
|
218
|
+
|
|
162
219
|
/** Execute the spin for every reel concurrently. */
|
|
163
220
|
async run(data: SpinData, opts?: SpinRunOpts): Promise<void> {
|
|
164
221
|
this._killed = false;
|
|
@@ -328,6 +385,8 @@ export class SpinEngine {
|
|
|
328
385
|
}
|
|
329
386
|
const step = this._grid.cellPosition(p.reel, 1).y - this._grid.cellPosition(p.reel, 0).y;
|
|
330
387
|
const slow = this.slowOf(p); // anticipation drops the reel in more slowly
|
|
388
|
+
const fall = this._cfg.spinUp * this._cfg.dropFallFactor * f * slow;
|
|
389
|
+
const schedule = p.cellStopTimes ?? [];
|
|
331
390
|
await Promise.all(
|
|
332
391
|
Array.from({ length: rows }, (_, r) => r).map(async (r) => {
|
|
333
392
|
if (this._killed) return;
|
|
@@ -337,18 +396,10 @@ export class SpinEngine {
|
|
|
337
396
|
cell.setData(data);
|
|
338
397
|
cell.position.set(to.x, to.y - step * (rows + 1));
|
|
339
398
|
cell.alpha = 1;
|
|
340
|
-
|
|
341
|
-
|
|
342
|
-
r * this._cfg.cellStagger) *
|
|
343
|
-
f *
|
|
344
|
-
slow;
|
|
399
|
+
// the plan says WHEN this cell seats; back off the fall to get when it must let go
|
|
400
|
+
const delay = Math.max(0, (schedule[r] ?? fall) - fall);
|
|
345
401
|
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
|
-
);
|
|
402
|
+
await Tween.to(cell, { 'position.y': to.y }, fall, easingByName(this._cfg.settle.easing));
|
|
352
403
|
// the impact frame — fired before the squash so a game can sync its own hit feedback
|
|
353
404
|
opts?.onCellSeated?.(p.reel, r, data);
|
|
354
405
|
await this._squashCell(cell, f);
|