@energy8platform/game-engine 0.20.0 → 0.22.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 +121 -0
- package/dist/animation.cjs.js +18 -3
- package/dist/animation.cjs.js.map +1 -1
- package/dist/animation.esm.js +18 -3
- package/dist/animation.esm.js.map +1 -1
- package/dist/core.cjs.js +18 -3
- package/dist/core.cjs.js.map +1 -1
- package/dist/core.esm.js +18 -3
- package/dist/core.esm.js.map +1 -1
- package/dist/host.cjs.js +18 -3
- package/dist/host.cjs.js.map +1 -1
- package/dist/host.d.ts +3 -5
- package/dist/host.esm.js +18 -3
- package/dist/host.esm.js.map +1 -1
- package/dist/index.cjs.js +18 -2535
- package/dist/index.cjs.js.map +1 -1
- package/dist/index.d.ts +3 -1020
- package/dist/index.esm.js +20 -2525
- package/dist/index.esm.js.map +1 -1
- package/dist/slot.cjs.js +1872 -61
- package/dist/slot.cjs.js.map +1 -1
- package/dist/slot.d.ts +555 -28
- package/dist/slot.esm.js +1857 -62
- package/dist/slot.esm.js.map +1 -1
- package/dist/vite.cjs.js +0 -5
- package/dist/vite.cjs.js.map +1 -1
- package/dist/vite.esm.js +0 -5
- package/dist/vite.esm.js.map +1 -1
- package/package.json +2 -35
- package/src/animation/Tween.ts +14 -3
- package/src/host/index.ts +0 -1
- package/src/host/types.ts +0 -3
- package/src/index.ts +0 -28
- package/src/slot/anim/easing-map.ts +16 -2
- package/src/slot/cascade/TumbleController.ts +230 -0
- package/src/slot/config/ReelSystemConfig.ts +559 -0
- package/src/slot/config/presets.ts +222 -0
- package/src/slot/features/extra.ts +189 -0
- package/src/slot/features/index.ts +44 -0
- package/src/slot/features/symbols.ts +183 -0
- package/src/slot/features/types.ts +136 -0
- package/src/slot/features/wilds.ts +151 -0
- package/src/slot/grid/ReelGrid.ts +93 -24
- package/src/slot/grid/SymbolCell.ts +45 -11
- package/src/slot/index.ts +61 -2
- package/src/slot/motion/AnticipationController.ts +96 -0
- package/src/slot/motion/SpinEngine.ts +384 -0
- package/src/slot/system/ReelSystem.ts +295 -0
- package/src/vite/index.ts +0 -5
- package/dist/react-jsx.cjs.js +0 -3
- package/dist/react-jsx.cjs.js.map +0 -1
- package/dist/react-jsx.d.ts +0 -602
- package/dist/react-jsx.esm.js +0 -2
- package/dist/react-jsx.esm.js.map +0 -1
- package/dist/react.cjs.js +0 -3787
- package/dist/react.cjs.js.map +0 -1
- package/dist/react.d.ts +0 -1467
- package/dist/react.esm.js +0 -3771
- package/dist/react.esm.js.map +0 -1
- package/dist/ui.cjs.js +0 -2999
- package/dist/ui.cjs.js.map +0 -1
- package/dist/ui.d.ts +0 -1116
- package/dist/ui.esm.js +0 -2983
- package/dist/ui.esm.js.map +0 -1
- package/src/react/EngineContext.ts +0 -26
- package/src/react/ReactScene.ts +0 -88
- package/src/react/applyProps.ts +0 -271
- package/src/react/catalogue.ts +0 -17
- package/src/react/createPixiRoot.ts +0 -31
- package/src/react/extendAll.ts +0 -74
- package/src/react/hooks.ts +0 -46
- package/src/react/index.ts +0 -29
- package/src/react/jsx-runtime.ts +0 -346
- package/src/react/reconciler.ts +0 -338
- package/src/slot/freeSpins/FreeSpinsSession.ts +0 -40
- package/src/state/StateMachine.ts +0 -231
- package/src/state/index.ts +0 -1
- package/src/ui/BalanceDisplay.ts +0 -159
- package/src/ui/Button.ts +0 -304
- package/src/ui/FlexContainer.ts +0 -775
- package/src/ui/Label.ts +0 -124
- package/src/ui/LabelValue.ts +0 -122
- package/src/ui/Layout.ts +0 -291
- package/src/ui/Modal.ts +0 -170
- package/src/ui/Panel.ts +0 -204
- package/src/ui/ProgressBar.ts +0 -170
- package/src/ui/ScrollContainer.ts +0 -478
- package/src/ui/Slider.ts +0 -241
- package/src/ui/Toast.ts +0 -150
- package/src/ui/Toggle.ts +0 -201
- package/src/ui/WinDisplay.ts +0 -145
- package/src/ui/index.ts +0 -33
- package/src/ui/view.ts +0 -28
|
@@ -0,0 +1,136 @@
|
|
|
1
|
+
// packages/game-engine/src/slot/features/types.ts
|
|
2
|
+
//
|
|
3
|
+
// Uniform interface for special reel feature mechanics. Each feature is a presentation module:
|
|
4
|
+
// given the current board + its config (and optional per-spin data), it plays an animation that
|
|
5
|
+
// visualises the mechanic. `demo()` is the self-contained showcase the reel-lab playground triggers.
|
|
6
|
+
|
|
7
|
+
import { Container, Graphics, Text } from 'pixi.js';
|
|
8
|
+
import { Tween } from '../../animation';
|
|
9
|
+
import { easingByName } from '../anim/easing-map';
|
|
10
|
+
import type { ReelGrid } from '../grid/ReelGrid';
|
|
11
|
+
import type { SymbolCell, CellData } from '../grid/SymbolCell';
|
|
12
|
+
import type { SymbolResolver } from '../grid/SymbolView';
|
|
13
|
+
import type { FeatureKey, ReelSystemConfig } from '../config/ReelSystemConfig';
|
|
14
|
+
|
|
15
|
+
export interface FeatureContext {
|
|
16
|
+
grid: ReelGrid;
|
|
17
|
+
resolve: SymbolResolver;
|
|
18
|
+
cfg: ReelSystemConfig;
|
|
19
|
+
/** Overlay layer above the grid for rings/labels/sprites. */
|
|
20
|
+
fx: Container;
|
|
21
|
+
/** Mutable view of the current board (featureN may rewrite it). */
|
|
22
|
+
board: CellData[][];
|
|
23
|
+
/** Whether the current spin is a free-spin (gates onlyInFreeSpins features). */
|
|
24
|
+
freeSpins?: boolean;
|
|
25
|
+
/** Emit a human-readable note for the playground log. */
|
|
26
|
+
log?: (msg: string) => void;
|
|
27
|
+
}
|
|
28
|
+
|
|
29
|
+
export interface ReelFeature {
|
|
30
|
+
/** Unique id. Built-ins use a `FeatureKey`; custom features may use any string. */
|
|
31
|
+
readonly key: FeatureKey | string;
|
|
32
|
+
readonly label: string;
|
|
33
|
+
/** Whether this feature is active for the given config. Custom features can just return `true`. */
|
|
34
|
+
enabled(cfg: ReelSystemConfig): boolean;
|
|
35
|
+
/** Run the feature presentation on the current board. */
|
|
36
|
+
demo(ctx: FeatureContext): Promise<void>;
|
|
37
|
+
}
|
|
38
|
+
|
|
39
|
+
// ── shared animation helpers ────────────────────────────────────────────────
|
|
40
|
+
|
|
41
|
+
/** Center position of a cell, in fx-layer coordinates (fx shares the grid's transform). */
|
|
42
|
+
export function cellCenter(grid: ReelGrid, col: number, row: number): { x: number; y: number } {
|
|
43
|
+
return grid.cellPosition(col, row);
|
|
44
|
+
}
|
|
45
|
+
|
|
46
|
+
export function rowStepOf(grid: ReelGrid): number {
|
|
47
|
+
return grid.cellPosition(0, 1).y - grid.cellPosition(0, 0).y;
|
|
48
|
+
}
|
|
49
|
+
|
|
50
|
+
/** A glowing ring drawn around a cell. Returns a disposer. */
|
|
51
|
+
export function glowRing(
|
|
52
|
+
fx: Container,
|
|
53
|
+
grid: ReelGrid,
|
|
54
|
+
col: number,
|
|
55
|
+
row: number,
|
|
56
|
+
color: number,
|
|
57
|
+
): () => void {
|
|
58
|
+
if (fx.destroyed) return () => {};
|
|
59
|
+
const { x, y } = cellCenter(grid, col, row);
|
|
60
|
+
const s = grid.cellSize;
|
|
61
|
+
const g = new Graphics()
|
|
62
|
+
.roundRect(x - s / 2, y - s / 2, s, s, 10)
|
|
63
|
+
.stroke({ color, width: 3, alpha: 0.9 });
|
|
64
|
+
fx.addChild(g);
|
|
65
|
+
return () => g.destroy();
|
|
66
|
+
}
|
|
67
|
+
|
|
68
|
+
/** Pulse a cell up and back. */
|
|
69
|
+
export async function pulseCell(cell: SymbolCell, scale = 1.15, ms = 220): Promise<void> {
|
|
70
|
+
if (cell.destroyed) return;
|
|
71
|
+
await Tween.to(
|
|
72
|
+
cell,
|
|
73
|
+
{ 'scale.x': scale, 'scale.y': scale },
|
|
74
|
+
ms * 0.5,
|
|
75
|
+
easingByName('easeOutBack'),
|
|
76
|
+
);
|
|
77
|
+
if (cell.destroyed) return;
|
|
78
|
+
await Tween.to(cell, { 'scale.x': 1, 'scale.y': 1 }, ms * 0.5, easingByName('easeOutQuad'));
|
|
79
|
+
}
|
|
80
|
+
|
|
81
|
+
/** Floating label that rises and fades. */
|
|
82
|
+
export async function floatLabel(
|
|
83
|
+
fx: Container,
|
|
84
|
+
x: number,
|
|
85
|
+
y: number,
|
|
86
|
+
text: string,
|
|
87
|
+
color = 0xffd24a,
|
|
88
|
+
ms = 700,
|
|
89
|
+
): Promise<void> {
|
|
90
|
+
if (fx.destroyed) return;
|
|
91
|
+
const t = new Text({ text, style: { fontSize: 26, fill: color, fontWeight: '800' } });
|
|
92
|
+
t.anchor.set(0.5);
|
|
93
|
+
t.position.set(x, y);
|
|
94
|
+
t.scale.set(0.5);
|
|
95
|
+
fx.addChild(t);
|
|
96
|
+
await Tween.to(t, { 'scale.x': 1, 'scale.y': 1 }, ms * 0.25, easingByName('easeOutBack'));
|
|
97
|
+
await Tween.to(t, { y: y - 50, alpha: 0 }, ms * 0.75, easingByName('easeOutQuad'));
|
|
98
|
+
t.destroy();
|
|
99
|
+
}
|
|
100
|
+
|
|
101
|
+
/** Replace a cell's symbol with a quick morph (shrink → swap → pop). */
|
|
102
|
+
export async function morphSymbol(cell: SymbolCell, data: CellData, ms = 280): Promise<void> {
|
|
103
|
+
if (cell.destroyed) return;
|
|
104
|
+
await Tween.to(cell, { 'scale.x': 0.1, 'scale.y': 0.1 }, ms * 0.4, easingByName('easeInBack'));
|
|
105
|
+
if (cell.destroyed) return;
|
|
106
|
+
cell.setData(data);
|
|
107
|
+
cell.scale.set(0.1);
|
|
108
|
+
await Tween.to(cell, { 'scale.x': 1, 'scale.y': 1 }, ms * 0.6, easingByName('easeOutBack'));
|
|
109
|
+
}
|
|
110
|
+
|
|
111
|
+
/** Drop a cell in from above its home position. */
|
|
112
|
+
export async function dropCell(
|
|
113
|
+
grid: ReelGrid,
|
|
114
|
+
cell: SymbolCell,
|
|
115
|
+
col: number,
|
|
116
|
+
row: number,
|
|
117
|
+
ms = 280,
|
|
118
|
+
): Promise<void> {
|
|
119
|
+
if (cell.destroyed) return;
|
|
120
|
+
const home = grid.cellPosition(col, row);
|
|
121
|
+
cell.position.set(home.x, home.y - rowStepOf(grid) * (row + 2));
|
|
122
|
+
cell.alpha = 1;
|
|
123
|
+
cell.scale.set(1);
|
|
124
|
+
await Tween.to(cell, { 'position.y': home.y }, ms, easingByName('easeOutBounce'));
|
|
125
|
+
}
|
|
126
|
+
|
|
127
|
+
export function pickFromBoard(
|
|
128
|
+
board: CellData[][],
|
|
129
|
+
predicate: (c: CellData, col: number, row: number) => boolean,
|
|
130
|
+
): { col: number; row: number }[] {
|
|
131
|
+
const out: { col: number; row: number }[] = [];
|
|
132
|
+
for (let c = 0; c < board.length; c++)
|
|
133
|
+
for (let r = 0; r < (board[c]?.length ?? 0); r++)
|
|
134
|
+
if (board[c][r] && predicate(board[c][r], c, r)) out.push({ col: c, row: r });
|
|
135
|
+
return out;
|
|
136
|
+
}
|
|
@@ -0,0 +1,151 @@
|
|
|
1
|
+
// packages/game-engine/src/slot/features/wilds.ts
|
|
2
|
+
import { Tween } from '../../animation';
|
|
3
|
+
import { easingByName } from '../anim/easing-map';
|
|
4
|
+
import {
|
|
5
|
+
type FeatureContext,
|
|
6
|
+
type ReelFeature,
|
|
7
|
+
cellCenter,
|
|
8
|
+
dropCell,
|
|
9
|
+
floatLabel,
|
|
10
|
+
glowRing,
|
|
11
|
+
morphSymbol,
|
|
12
|
+
pickFromBoard,
|
|
13
|
+
pulseCell,
|
|
14
|
+
} from './types';
|
|
15
|
+
|
|
16
|
+
function reelsOf(cfg: number[], cols: number): number[] {
|
|
17
|
+
return cfg.length
|
|
18
|
+
? cfg.filter((r) => r >= 0 && r < cols)
|
|
19
|
+
: Array.from({ length: cols }, (_, i) => i);
|
|
20
|
+
}
|
|
21
|
+
const randInt = (a: number, b: number, seed: number) =>
|
|
22
|
+
Math.min(b, a + Math.floor(((Math.sin(seed * 99.13) + 1) / 2) * (b - a + 1)));
|
|
23
|
+
|
|
24
|
+
/** Expanding wild: a landed wild grows to fill its whole reel. */
|
|
25
|
+
export const ExpandingWild: ReelFeature = {
|
|
26
|
+
key: 'expandingWild',
|
|
27
|
+
label: 'Expanding wild',
|
|
28
|
+
enabled: (c) => c.features.expandingWild.enabled,
|
|
29
|
+
async demo(ctx: FeatureContext) {
|
|
30
|
+
const f = ctx.cfg.features.expandingWild;
|
|
31
|
+
if (f.onlyInFreeSpins && !ctx.freeSpins) {
|
|
32
|
+
ctx.log?.('Expanding wild: free-spins only');
|
|
33
|
+
return;
|
|
34
|
+
}
|
|
35
|
+
const cols = ctx.grid.cols;
|
|
36
|
+
const eligible = reelsOf(f.reels, cols);
|
|
37
|
+
const reel = eligible[randInt(0, eligible.length - 1, cols)];
|
|
38
|
+
const rows = ctx.grid.rowsOf(reel);
|
|
39
|
+
ctx.log?.(`Expanding wild on reel ${reel}`);
|
|
40
|
+
const disposers: (() => void)[] = [];
|
|
41
|
+
for (let r = 0; r < (f.toFullReel ? rows : Math.min(2, rows)); r++) {
|
|
42
|
+
const cell = ctx.grid.getCell(reel, r);
|
|
43
|
+
await morphSymbol(cell, { symbol: f.symbol }, f.ms / Math.max(1, rows));
|
|
44
|
+
disposers.push(glowRing(ctx.fx, ctx.grid, reel, r, 0xffd700));
|
|
45
|
+
if (ctx.board[reel]) ctx.board[reel][r] = { symbol: f.symbol };
|
|
46
|
+
}
|
|
47
|
+
await Tween.delay(500);
|
|
48
|
+
disposers.forEach((d) => d());
|
|
49
|
+
},
|
|
50
|
+
};
|
|
51
|
+
|
|
52
|
+
/** Sticky symbols: chosen symbols lock in place for N spins. */
|
|
53
|
+
export const StickySymbols: ReelFeature = {
|
|
54
|
+
key: 'sticky',
|
|
55
|
+
label: 'Sticky symbols',
|
|
56
|
+
enabled: (c) => c.features.sticky.enabled,
|
|
57
|
+
async demo(ctx: FeatureContext) {
|
|
58
|
+
const f = ctx.cfg.features.sticky;
|
|
59
|
+
const targets = pickFromBoard(ctx.board, (c) => !!c.symbol).slice(0, 3);
|
|
60
|
+
const picks = targets.filter((_, i) => i % 2 === 0);
|
|
61
|
+
ctx.log?.(`Sticky: locking ${picks.length} cell(s) for ${f.durationSpins || 'feature'} spins`);
|
|
62
|
+
await Promise.all(
|
|
63
|
+
picks.map(async (p, i) => {
|
|
64
|
+
const cell = ctx.grid.getCell(p.col, p.row);
|
|
65
|
+
cell.setData({ symbol: f.symbols[0] ?? 'wild', sticky: { remaining: f.durationSpins } });
|
|
66
|
+
glowRing(ctx.fx, ctx.grid, p.col, p.row, f.ringColor);
|
|
67
|
+
await pulseCell(cell, 1.2, 260);
|
|
68
|
+
const { x, y } = cellCenter(ctx.grid, p.col, p.row);
|
|
69
|
+
await floatLabel(
|
|
70
|
+
ctx.fx,
|
|
71
|
+
x,
|
|
72
|
+
y - ctx.grid.cellSize / 2,
|
|
73
|
+
f.durationSpins ? `STICKY ${f.durationSpins}` : 'STICKY',
|
|
74
|
+
f.ringColor,
|
|
75
|
+
600 + i * 50,
|
|
76
|
+
);
|
|
77
|
+
}),
|
|
78
|
+
);
|
|
79
|
+
},
|
|
80
|
+
};
|
|
81
|
+
|
|
82
|
+
/** Walking wild: a wild shifts one reel each spin until it leaves the grid. */
|
|
83
|
+
export const WalkingWild: ReelFeature = {
|
|
84
|
+
key: 'walkingWild',
|
|
85
|
+
label: 'Walking wild',
|
|
86
|
+
enabled: (c) => c.features.walkingWild.enabled,
|
|
87
|
+
async demo(ctx: FeatureContext) {
|
|
88
|
+
const f = ctx.cfg.features.walkingWild;
|
|
89
|
+
const cols = ctx.grid.cols;
|
|
90
|
+
const dir = f.direction === 'left' ? -1 : 1;
|
|
91
|
+
let col = dir === 1 ? 0 : cols - 1;
|
|
92
|
+
// clamp the row into each reel's own height (Megaways reels differ in length)
|
|
93
|
+
const rowIn = (c: number) =>
|
|
94
|
+
Math.min(Math.floor(ctx.grid.rowsOf(col) / 2), ctx.grid.rowsOf(c) - 1);
|
|
95
|
+
let row = rowIn(col);
|
|
96
|
+
await morphSymbol(ctx.grid.getCell(col, row), { symbol: f.symbol }, 240);
|
|
97
|
+
ctx.log?.(`Walking wild marching ${f.direction}`);
|
|
98
|
+
// walk across the grid
|
|
99
|
+
for (let step = 0; step < cols; step++) {
|
|
100
|
+
const next = col + dir * f.stepPerSpin;
|
|
101
|
+
if (next < 0 || next >= cols) break;
|
|
102
|
+
const nextRow = rowIn(next);
|
|
103
|
+
const from = cellCenter(ctx.grid, col, row);
|
|
104
|
+
const to = cellCenter(ctx.grid, next, nextRow);
|
|
105
|
+
ctx.grid.getCell(col, row).setData({ symbol: null });
|
|
106
|
+
const ghost = ctx.grid.getCell(next, nextRow);
|
|
107
|
+
ghost.setData({ symbol: f.symbol });
|
|
108
|
+
ghost.position.set(from.x, from.y);
|
|
109
|
+
const ring = glowRing(ctx.fx, ctx.grid, next, nextRow, 0xff66cc);
|
|
110
|
+
await Tween.to(
|
|
111
|
+
ghost,
|
|
112
|
+
{ 'position.x': to.x, 'position.y': to.y },
|
|
113
|
+
260,
|
|
114
|
+
easingByName('easeInOutQuad'),
|
|
115
|
+
);
|
|
116
|
+
ring();
|
|
117
|
+
col = next;
|
|
118
|
+
row = nextRow;
|
|
119
|
+
}
|
|
120
|
+
},
|
|
121
|
+
};
|
|
122
|
+
|
|
123
|
+
/** Random wild injection: a few wilds drop onto random positions. */
|
|
124
|
+
export const RandomWild: ReelFeature = {
|
|
125
|
+
key: 'randomWild',
|
|
126
|
+
label: 'Random wild injection',
|
|
127
|
+
enabled: (c) => c.features.randomWild.enabled,
|
|
128
|
+
async demo(ctx: FeatureContext) {
|
|
129
|
+
const f = ctx.cfg.features.randomWild;
|
|
130
|
+
const n = Array.isArray(f.count) ? randInt(f.count[0], f.count[1], ctx.grid.cols + 7) : f.count;
|
|
131
|
+
const spots = pickFromBoard(ctx.board, () => true);
|
|
132
|
+
const chosen: { col: number; row: number }[] = [];
|
|
133
|
+
for (let i = 0; i < n && spots.length; i++)
|
|
134
|
+
chosen.push(spots.splice(randInt(0, spots.length - 1, i + 3), 1)[0]);
|
|
135
|
+
ctx.log?.(
|
|
136
|
+
`Injecting ${chosen.length} wild(s)${f.sticky ? ' (sticky)' : ''}${f.multiplier > 1 ? ` ×${f.multiplier}` : ''}`,
|
|
137
|
+
);
|
|
138
|
+
await Promise.all(
|
|
139
|
+
chosen.map(async (p) => {
|
|
140
|
+
const cell = ctx.grid.getCell(p.col, p.row);
|
|
141
|
+
cell.setData({
|
|
142
|
+
symbol: 'wild',
|
|
143
|
+
multiplier: f.multiplier > 1 ? f.multiplier : undefined,
|
|
144
|
+
sticky: f.sticky ? { remaining: 0 } : undefined,
|
|
145
|
+
});
|
|
146
|
+
await dropCell(ctx.grid, cell, p.col, p.row, 300);
|
|
147
|
+
if (f.sticky) glowRing(ctx.fx, ctx.grid, p.col, p.row, 0xec4899);
|
|
148
|
+
}),
|
|
149
|
+
);
|
|
150
|
+
},
|
|
151
|
+
};
|
|
@@ -2,88 +2,157 @@ import { Container, Graphics, Sprite, type Texture } from 'pixi.js';
|
|
|
2
2
|
import { SymbolCell, type CellData, type CellFrameStyle } from './SymbolCell';
|
|
3
3
|
import type { SymbolResolver } from './SymbolView';
|
|
4
4
|
|
|
5
|
-
export interface DecorationConfig {
|
|
5
|
+
export interface DecorationConfig {
|
|
6
|
+
texture?: Texture;
|
|
7
|
+
padding?: number;
|
|
8
|
+
}
|
|
6
9
|
export interface ReelGridConfig {
|
|
7
|
-
cols: number;
|
|
8
|
-
|
|
10
|
+
cols: number;
|
|
11
|
+
/** Uniform row count. Ignored per-reel when `rowsPerReel` is provided. */
|
|
12
|
+
rows: number;
|
|
13
|
+
/** Per-reel row counts for Megaways / variable-height reels. Length must equal `cols`. */
|
|
14
|
+
rowsPerReel?: number[];
|
|
15
|
+
cellSize: number;
|
|
16
|
+
gap?: number;
|
|
17
|
+
resolve: SymbolResolver;
|
|
18
|
+
frameStyle?: CellFrameStyle;
|
|
9
19
|
decoration?: DecorationConfig;
|
|
10
20
|
mask?: boolean;
|
|
11
21
|
}
|
|
12
22
|
|
|
23
|
+
/**
|
|
24
|
+
* A grid of `SymbolCell`s. Supports uniform grids and variable-height reels (Megaways):
|
|
25
|
+
* when `rowsPerReel` is set each reel is vertically centred inside the tallest reel's envelope.
|
|
26
|
+
*/
|
|
13
27
|
export class ReelGrid extends Container {
|
|
14
28
|
readonly __uiComponent = true as const;
|
|
15
29
|
|
|
16
30
|
private _cols: number;
|
|
17
|
-
private
|
|
31
|
+
private _rowsPerReel: number[];
|
|
32
|
+
private _maxRows: number;
|
|
18
33
|
private _cellSize: number;
|
|
19
34
|
private _gap: number;
|
|
20
35
|
private _cells: SymbolCell[][] = [];
|
|
21
36
|
private _cellLayer = new Container();
|
|
37
|
+
private _resolve: SymbolResolver;
|
|
38
|
+
private _frameStyle?: CellFrameStyle;
|
|
39
|
+
private _mask: Graphics | null = null;
|
|
22
40
|
|
|
23
41
|
constructor(config: ReelGridConfig) {
|
|
24
42
|
super();
|
|
25
43
|
this._cols = config.cols;
|
|
26
|
-
this.
|
|
44
|
+
this._rowsPerReel =
|
|
45
|
+
config.rowsPerReel && config.rowsPerReel.length === config.cols
|
|
46
|
+
? config.rowsPerReel.slice()
|
|
47
|
+
: Array.from({ length: config.cols }, () => config.rows);
|
|
48
|
+
this._maxRows = Math.max(1, ...this._rowsPerReel);
|
|
27
49
|
this._cellSize = config.cellSize;
|
|
28
50
|
this._gap = config.gap ?? 0;
|
|
51
|
+
this._resolve = config.resolve;
|
|
52
|
+
this._frameStyle = config.frameStyle;
|
|
29
53
|
|
|
30
54
|
if (config.decoration) {
|
|
31
55
|
const pad = config.decoration.padding ?? 0;
|
|
32
56
|
const w = this._cols * (this._cellSize + this._gap) - this._gap + pad * 2;
|
|
33
|
-
const h = this.
|
|
57
|
+
const h = this._maxRows * (this._cellSize + this._gap) - this._gap + pad * 2;
|
|
34
58
|
if (config.decoration.texture) {
|
|
35
59
|
const deco = new Sprite(config.decoration.texture);
|
|
36
|
-
deco.width = w;
|
|
60
|
+
deco.width = w;
|
|
61
|
+
deco.height = h;
|
|
62
|
+
deco.position.set(-pad - this._cellSize / 2, -pad - this._cellSize / 2);
|
|
37
63
|
this.addChild(deco);
|
|
38
64
|
}
|
|
39
65
|
}
|
|
40
66
|
|
|
41
67
|
this.addChild(this._cellLayer);
|
|
68
|
+
this._buildCells();
|
|
69
|
+
|
|
70
|
+
if (config.mask) this._applyMask();
|
|
71
|
+
}
|
|
42
72
|
|
|
73
|
+
private _buildCells(): void {
|
|
43
74
|
for (let c = 0; c < this._cols; c++) {
|
|
44
75
|
this._cells[c] = [];
|
|
45
|
-
for (let r = 0; r < this.
|
|
46
|
-
const cell = new SymbolCell({
|
|
76
|
+
for (let r = 0; r < this._rowsPerReel[c]; r++) {
|
|
77
|
+
const cell = new SymbolCell({
|
|
78
|
+
size: this._cellSize,
|
|
79
|
+
resolve: this._resolve,
|
|
80
|
+
frameStyle: this._frameStyle,
|
|
81
|
+
});
|
|
47
82
|
const { x, y } = this.cellPosition(c, r);
|
|
48
83
|
cell.position.set(x, y);
|
|
49
84
|
this._cellLayer.addChild(cell);
|
|
50
85
|
this._cells[c][r] = cell;
|
|
51
86
|
}
|
|
52
87
|
}
|
|
88
|
+
}
|
|
53
89
|
|
|
54
|
-
|
|
55
|
-
|
|
56
|
-
|
|
57
|
-
|
|
58
|
-
|
|
59
|
-
|
|
60
|
-
|
|
61
|
-
this.addChild(m);
|
|
62
|
-
}
|
|
90
|
+
private _applyMask(): void {
|
|
91
|
+
const w = this._cols * (this._cellSize + this._gap) - this._gap;
|
|
92
|
+
const h = this._maxRows * (this._cellSize + this._gap) - this._gap;
|
|
93
|
+
const m = new Graphics().rect(-this._cellSize / 2, -this._cellSize / 2, w, h).fill(0xffffff);
|
|
94
|
+
this._cellLayer.mask = m;
|
|
95
|
+
this.addChild(m);
|
|
96
|
+
this._mask = m;
|
|
63
97
|
}
|
|
64
98
|
|
|
65
|
-
get cols(): number {
|
|
66
|
-
|
|
99
|
+
get cols(): number {
|
|
100
|
+
return this._cols;
|
|
101
|
+
}
|
|
102
|
+
/** Tallest reel's row count (the grid's visual height in rows). */
|
|
103
|
+
get rows(): number {
|
|
104
|
+
return this._maxRows;
|
|
105
|
+
}
|
|
106
|
+
get cellSize(): number {
|
|
107
|
+
return this._cellSize;
|
|
108
|
+
}
|
|
109
|
+
rowsOf(col: number): number {
|
|
110
|
+
return this._rowsPerReel[col] ?? 0;
|
|
111
|
+
}
|
|
112
|
+
get rowsPerReel(): number[] {
|
|
113
|
+
return this._rowsPerReel.slice();
|
|
114
|
+
}
|
|
67
115
|
|
|
116
|
+
/** Cell centre position. Variable-height reels are centred in the max-rows envelope. */
|
|
68
117
|
cellPosition(col: number, row: number): { x: number; y: number } {
|
|
69
118
|
const step = this._cellSize + this._gap;
|
|
70
|
-
|
|
119
|
+
const reelRows = this._rowsPerReel[col] ?? this._maxRows;
|
|
120
|
+
const yOffset = ((this._maxRows - reelRows) / 2) * step;
|
|
121
|
+
return { x: col * step, y: yOffset + row * step };
|
|
71
122
|
}
|
|
72
123
|
|
|
73
|
-
getCell(col: number, row: number): SymbolCell {
|
|
124
|
+
getCell(col: number, row: number): SymbolCell {
|
|
125
|
+
return this._cells[col][row];
|
|
126
|
+
}
|
|
74
127
|
|
|
75
128
|
setGrid(cells: CellData[][]): void {
|
|
76
129
|
for (let c = 0; c < this._cols; c++) {
|
|
77
|
-
for (let r = 0; r < this.
|
|
130
|
+
for (let r = 0; r < this._rowsPerReel[c]; r++) {
|
|
78
131
|
this._cells[c]?.[r]?.setData(cells[c]?.[r] ?? { symbol: null });
|
|
79
132
|
}
|
|
80
133
|
}
|
|
81
134
|
}
|
|
82
135
|
|
|
136
|
+
/** Rebuild the grid with new per-reel row counts (Megaways re-roll / dynamic rows). */
|
|
137
|
+
reshape(rowsPerReel: number[]): void {
|
|
138
|
+
if (rowsPerReel.length !== this._cols) return;
|
|
139
|
+
this._cellLayer.removeChildren().forEach((c) => c.destroy());
|
|
140
|
+
this._cells = [];
|
|
141
|
+
this._rowsPerReel = rowsPerReel.slice();
|
|
142
|
+
this._maxRows = Math.max(1, ...this._rowsPerReel);
|
|
143
|
+
this._buildCells();
|
|
144
|
+
if (this._mask) {
|
|
145
|
+
this._mask.destroy();
|
|
146
|
+
this.removeChild(this._mask);
|
|
147
|
+
this._mask = null;
|
|
148
|
+
this._applyMask();
|
|
149
|
+
}
|
|
150
|
+
}
|
|
151
|
+
|
|
83
152
|
resize(cellSize: number): void {
|
|
84
153
|
this._cellSize = cellSize;
|
|
85
154
|
for (let c = 0; c < this._cols; c++) {
|
|
86
|
-
for (let r = 0; r < this.
|
|
155
|
+
for (let r = 0; r < this._rowsPerReel[c]; r++) {
|
|
87
156
|
const { x, y } = this.cellPosition(c, r);
|
|
88
157
|
this._cells[c][r].position.set(x, y);
|
|
89
158
|
}
|
|
@@ -15,8 +15,16 @@ export interface CellData {
|
|
|
15
15
|
bonus?: number;
|
|
16
16
|
sticky?: { remaining: number };
|
|
17
17
|
}
|
|
18
|
-
export interface CellState {
|
|
19
|
-
|
|
18
|
+
export interface CellState {
|
|
19
|
+
winning?: boolean;
|
|
20
|
+
removed?: boolean;
|
|
21
|
+
fresh?: boolean;
|
|
22
|
+
}
|
|
23
|
+
export interface SymbolCellConfig {
|
|
24
|
+
size: number;
|
|
25
|
+
resolve: SymbolResolver;
|
|
26
|
+
frameStyle?: CellFrameStyle;
|
|
27
|
+
}
|
|
20
28
|
|
|
21
29
|
const DEFAULT_STYLE: Required<CellFrameStyle> = {
|
|
22
30
|
radius: 8,
|
|
@@ -51,14 +59,23 @@ export class SymbolCell extends Container {
|
|
|
51
59
|
this._drawFrame('idle');
|
|
52
60
|
}
|
|
53
61
|
|
|
54
|
-
get view(): SymbolView | null {
|
|
62
|
+
get view(): SymbolView | null {
|
|
63
|
+
return this._view;
|
|
64
|
+
}
|
|
55
65
|
|
|
56
66
|
setData(data: CellData): void {
|
|
67
|
+
if (this.destroyed) return; // a killed-tween chain may resume after the cell is gone
|
|
57
68
|
// symbol view
|
|
58
69
|
if (data.symbol == null) {
|
|
59
|
-
if (this._view) {
|
|
70
|
+
if (this._view) {
|
|
71
|
+
this._view.destroy();
|
|
72
|
+
this._view = null;
|
|
73
|
+
}
|
|
60
74
|
} else {
|
|
61
|
-
if (this._view) {
|
|
75
|
+
if (this._view) {
|
|
76
|
+
this._view.destroy();
|
|
77
|
+
this._view = null;
|
|
78
|
+
}
|
|
62
79
|
const v = this._resolve(data.symbol);
|
|
63
80
|
if (v) {
|
|
64
81
|
v.resize?.(this._size);
|
|
@@ -72,7 +89,14 @@ export class SymbolCell extends Container {
|
|
|
72
89
|
}
|
|
73
90
|
|
|
74
91
|
setState(state: CellState): void {
|
|
75
|
-
|
|
92
|
+
if (this.destroyed) return;
|
|
93
|
+
const key = state.winning
|
|
94
|
+
? 'winning'
|
|
95
|
+
: state.removed
|
|
96
|
+
? 'removed'
|
|
97
|
+
: state.fresh
|
|
98
|
+
? 'fresh'
|
|
99
|
+
: 'idle';
|
|
76
100
|
this.frameStyleKey = key;
|
|
77
101
|
this._drawFrame(key);
|
|
78
102
|
}
|
|
@@ -81,17 +105,21 @@ export class SymbolCell extends Container {
|
|
|
81
105
|
if (this._view?.playWin) return this._view.playWin();
|
|
82
106
|
// default: scale pop
|
|
83
107
|
const target = this._view ?? this;
|
|
84
|
-
return Tween.to(target, { 'scale.x': 1.15, 'scale.y': 1.15 }, 160, Easing.easeOutBack)
|
|
85
|
-
|
|
108
|
+
return Tween.to(target, { 'scale.x': 1.15, 'scale.y': 1.15 }, 160, Easing.easeOutBack).then(
|
|
109
|
+
() => Tween.to(target, { 'scale.x': 1, 'scale.y': 1 }, 140, Easing.easeOutQuad),
|
|
110
|
+
);
|
|
86
111
|
}
|
|
87
112
|
|
|
88
|
-
playIdle(): void {
|
|
113
|
+
playIdle(): void {
|
|
114
|
+
this._view?.playIdle?.();
|
|
115
|
+
}
|
|
89
116
|
|
|
90
117
|
hasBadge(kind: 'multiplier' | 'bonus'): boolean {
|
|
91
118
|
return kind === 'multiplier' ? this._multBadge != null : this._bonusBadge != null;
|
|
92
119
|
}
|
|
93
120
|
|
|
94
121
|
private _drawFrame(key: 'idle' | 'winning' | 'removed' | 'fresh'): void {
|
|
122
|
+
if (this.destroyed || this._frame.destroyed) return;
|
|
95
123
|
const s = this._style[key];
|
|
96
124
|
this._frame.clear();
|
|
97
125
|
this._frame
|
|
@@ -102,7 +130,10 @@ export class SymbolCell extends Container {
|
|
|
102
130
|
}
|
|
103
131
|
|
|
104
132
|
private _setMultiplier(value?: number): void {
|
|
105
|
-
if (this._multBadge) {
|
|
133
|
+
if (this._multBadge) {
|
|
134
|
+
this._multBadge.destroy();
|
|
135
|
+
this._multBadge = null;
|
|
136
|
+
}
|
|
106
137
|
if (!value || value <= 1) return;
|
|
107
138
|
this._multBadge = this._badge(`×${value}`, 0xffd24a);
|
|
108
139
|
this._multBadge.position.set(this._size / 2 - 12, -this._size / 2 + 12);
|
|
@@ -110,7 +141,10 @@ export class SymbolCell extends Container {
|
|
|
110
141
|
}
|
|
111
142
|
|
|
112
143
|
private _setBonus(value?: number): void {
|
|
113
|
-
if (this._bonusBadge) {
|
|
144
|
+
if (this._bonusBadge) {
|
|
145
|
+
this._bonusBadge.destroy();
|
|
146
|
+
this._bonusBadge = null;
|
|
147
|
+
}
|
|
114
148
|
if (!value || value <= 0) return;
|
|
115
149
|
this._bonusBadge = this._badge(`+${value}`, 0x7ad7ff);
|
|
116
150
|
this._bonusBadge.position.set(-this._size / 2 + 12, -this._size / 2 + 12);
|
package/src/slot/index.ts
CHANGED
|
@@ -9,13 +9,72 @@ export { CascadeController } from './anim/CascadeController';
|
|
|
9
9
|
export type { CascadeStepData, CascadeTimings, CascadeAnim } from './anim/CascadeController';
|
|
10
10
|
export { ReelSpinController } from './anim/ReelSpinController';
|
|
11
11
|
export type { ReelSpinData, ReelSpinTimings, ReelStopPlan } from './anim/ReelSpinController';
|
|
12
|
+
export { EASING_BY_NAME, easingByName } from './anim/easing-map';
|
|
13
|
+
|
|
14
|
+
// ── configurable reel system ────────────────────────────────────────────────
|
|
15
|
+
export {
|
|
16
|
+
DEFAULT_REEL_CONFIG,
|
|
17
|
+
INTENSITY_SCALE,
|
|
18
|
+
FEATURE_KEYS,
|
|
19
|
+
resolveReelConfig,
|
|
20
|
+
mergeReelConfig,
|
|
21
|
+
effectiveRowsPerReel,
|
|
22
|
+
waysCount,
|
|
23
|
+
} from './config/ReelSystemConfig';
|
|
24
|
+
export type {
|
|
25
|
+
ReelSystemConfig,
|
|
26
|
+
DeepPartial,
|
|
27
|
+
EasingName,
|
|
28
|
+
EvaluationMode,
|
|
29
|
+
MotionStyle,
|
|
30
|
+
StopMode,
|
|
31
|
+
StopOrder,
|
|
32
|
+
Intensity,
|
|
33
|
+
GridConfig,
|
|
34
|
+
MotionConfig,
|
|
35
|
+
SettleConfig,
|
|
36
|
+
SquashConfig,
|
|
37
|
+
BlurConfig,
|
|
38
|
+
AnticipationConfig,
|
|
39
|
+
CascadeConfig,
|
|
40
|
+
WinConfig,
|
|
41
|
+
FeaturesConfig,
|
|
42
|
+
FeatureKey,
|
|
43
|
+
ExpandingWildConfig,
|
|
44
|
+
StickyConfig,
|
|
45
|
+
WalkingWildConfig,
|
|
46
|
+
MultiplierConfig,
|
|
47
|
+
MysteryConfig,
|
|
48
|
+
TransformConfig,
|
|
49
|
+
GiantConfig,
|
|
50
|
+
SplitConfig,
|
|
51
|
+
StackedConfig,
|
|
52
|
+
NudgeConfig,
|
|
53
|
+
ReelModifierConfig,
|
|
54
|
+
HoldAndSpinConfig,
|
|
55
|
+
RandomWildConfig,
|
|
56
|
+
} from './config/ReelSystemConfig';
|
|
57
|
+
export { PRESETS, PRESET_LIST } from './config/presets';
|
|
58
|
+
export type { PresetId, ReelPreset } from './config/presets';
|
|
59
|
+
|
|
60
|
+
export { SpinEngine } from './motion/SpinEngine';
|
|
61
|
+
export type { SpinData, SpinRunOpts, ReelStopPlan as SpinStopPlan } from './motion/SpinEngine';
|
|
62
|
+
export { AnticipationController } from './motion/AnticipationController';
|
|
63
|
+
export type { AnticipationDecision } from './motion/AnticipationController';
|
|
64
|
+
export { TumbleController } from './cascade/TumbleController';
|
|
65
|
+
export type { TumbleStep } from './cascade/TumbleController';
|
|
66
|
+
|
|
67
|
+
export { FEATURES, FEATURE_LIST } from './features';
|
|
68
|
+
export type { ReelFeature, FeatureContext } from './features';
|
|
69
|
+
|
|
70
|
+
export { createReelSystem } from './system/ReelSystem';
|
|
71
|
+
export type { ReelSystem, CreateReelSystemOptions } from './system/ReelSystem';
|
|
72
|
+
|
|
12
73
|
export { pickTier, tierIndexAtValue } from './overlay/tiers';
|
|
13
74
|
export type { WinTier } from './overlay/tiers';
|
|
14
75
|
export { valueAt, CountUpDisplay } from './overlay/CountUpDisplay';
|
|
15
76
|
export type { CountUpConfig } from './overlay/CountUpDisplay';
|
|
16
77
|
export { BigWinOverlay } from './overlay/BigWinOverlay';
|
|
17
78
|
export type { BigWinOverlayConfig } from './overlay/BigWinOverlay';
|
|
18
|
-
export { FreeSpinsSession } from './freeSpins/FreeSpinsSession';
|
|
19
|
-
export type { FreeSpinsSessionConfig } from './freeSpins/FreeSpinsSession';
|
|
20
79
|
export { MultiplierAccumulator } from './multiplier/MultiplierAccumulator';
|
|
21
80
|
export type { CarryPolicy } from './multiplier/MultiplierAccumulator';
|