@energy8platform/game-engine 0.23.0 → 0.25.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/dist/core.cjs.js +2 -2
- package/dist/core.cjs.js.map +1 -1
- package/dist/core.d.ts +3 -3
- package/dist/core.esm.js +2 -2
- package/dist/core.esm.js.map +1 -1
- package/dist/devtools.cjs.js +577 -0
- package/dist/devtools.cjs.js.map +1 -0
- package/dist/devtools.d.ts +424 -0
- package/dist/devtools.esm.js +565 -0
- package/dist/devtools.esm.js.map +1 -0
- package/dist/harness.cjs.js +33 -0
- package/dist/harness.cjs.js.map +1 -0
- package/dist/harness.d.ts +25 -0
- package/dist/harness.esm.js +30 -0
- package/dist/harness.esm.js.map +1 -0
- package/dist/host.cjs.js +35 -10
- package/dist/host.cjs.js.map +1 -1
- package/dist/host.d.ts +22 -9
- package/dist/host.esm.js +24 -11
- package/dist/host.esm.js.map +1 -1
- package/dist/index.cjs.js +2 -2
- package/dist/index.cjs.js.map +1 -1
- package/dist/index.d.ts +3 -3
- package/dist/index.esm.js +2 -2
- package/dist/index.esm.js.map +1 -1
- package/dist/reel-panel-client.cjs.js +610 -0
- package/dist/reel-panel-client.cjs.js.map +1 -0
- package/dist/reel-panel-client.d.ts +5 -0
- package/dist/reel-panel-client.esm.js +608 -0
- package/dist/reel-panel-client.esm.js.map +1 -0
- package/dist/shell.cjs.js +4 -4
- package/dist/shell.d.ts +1 -1
- package/dist/shell.esm.js +1 -1
- package/dist/slot.cjs.js +226 -75
- package/dist/slot.cjs.js.map +1 -1
- package/dist/slot.d.ts +137 -19
- package/dist/slot.esm.js +224 -76
- package/dist/slot.esm.js.map +1 -1
- package/package.json +17 -2
- package/src/core/GameApplication.ts +3 -3
- package/src/harness/index.ts +35 -0
- package/src/host/createSlotGame.ts +6 -3
- package/src/host/index.ts +11 -1
- package/src/host/shellConfig.ts +22 -12
- package/src/host/types.ts +13 -2
- package/src/shell/index.ts +4 -3
- package/src/slot/cascade/TumbleController.ts +6 -3
- package/src/slot/config/ReelSystemConfig.ts +19 -0
- package/src/slot/devtools/configDiff.ts +41 -0
- package/src/slot/devtools/controlPanel.ts +151 -0
- package/src/slot/devtools/fieldSchema.ts +190 -0
- package/src/slot/devtools/index.ts +31 -0
- package/src/slot/devtools/panelClient.ts +113 -0
- package/src/slot/devtools/protocol.ts +29 -0
- package/src/slot/devtools/reelDevBridge.ts +80 -0
- package/src/slot/features/extra.ts +1 -2
- package/src/slot/features/symbols.ts +10 -10
- package/src/slot/features/types.ts +13 -5
- package/src/slot/features/wilds.ts +1 -1
- package/src/slot/grid/AnimatedSymbol.ts +20 -11
- package/src/slot/grid/ReelGrid.ts +80 -41
- package/src/slot/grid/SymbolCell.ts +14 -7
- package/src/slot/grid/SymbolView.ts +2 -2
- package/src/slot/grid/geometry.ts +155 -0
- package/src/slot/index.ts +3 -0
- package/src/slot/motion/SpinEngine.ts +1 -1
- package/src/slot/system/ReelSystem.ts +10 -0
- package/src/types.ts +1 -1
|
@@ -1,6 +1,12 @@
|
|
|
1
1
|
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
|
+
import {
|
|
5
|
+
resolveGeometry,
|
|
6
|
+
cellPositionOf,
|
|
7
|
+
type CellSizeSpec,
|
|
8
|
+
type ResolvedGeometry,
|
|
9
|
+
} from './geometry';
|
|
4
10
|
|
|
5
11
|
export interface DecorationConfig {
|
|
6
12
|
texture?: Texture;
|
|
@@ -12,8 +18,19 @@ export interface ReelGridConfig {
|
|
|
12
18
|
rows: number;
|
|
13
19
|
/** Per-reel row counts for Megaways / variable-height reels. Length must equal `cols`. */
|
|
14
20
|
rowsPerReel?: number[];
|
|
21
|
+
/** Square cell size (shorthand: same width & height, all reels). */
|
|
15
22
|
cellSize: number;
|
|
23
|
+
/** Rectangular cells, uniform across reels. Override `cellSize` when set. */
|
|
24
|
+
cellWidth?: number;
|
|
25
|
+
cellHeight?: number;
|
|
26
|
+
/** Per-strip cell size (square scalar or {width,height}). */
|
|
27
|
+
cellSizePerReel?: CellSizeSpec[];
|
|
28
|
+
/** Uniform gap (shorthand for both axes). */
|
|
16
29
|
gap?: number;
|
|
30
|
+
/** Horizontal gap between adjacent reels. Scalar, or per-boundary (length cols-1). */
|
|
31
|
+
colGap?: number | number[];
|
|
32
|
+
/** Vertical gap between rows. Scalar, or per-reel (length cols). */
|
|
33
|
+
rowGap?: number | number[];
|
|
17
34
|
resolve: SymbolResolver;
|
|
18
35
|
frameStyle?: CellFrameStyle;
|
|
19
36
|
decoration?: DecorationConfig;
|
|
@@ -21,17 +38,16 @@ export interface ReelGridConfig {
|
|
|
21
38
|
}
|
|
22
39
|
|
|
23
40
|
/**
|
|
24
|
-
* A grid of `SymbolCell`s. Supports uniform grids
|
|
25
|
-
*
|
|
41
|
+
* A grid of `SymbolCell`s. Supports uniform grids, variable-height reels (Megaways), and
|
|
42
|
+
* rectangular / per-strip cell sizes with per-strip gaps. All layout flows through a single
|
|
43
|
+
* resolved geometry (see grid/geometry.ts); every consumer reads positions via `cellPosition`
|
|
44
|
+
* and dimensions via `cellSize(col)` rather than assuming a square cell.
|
|
26
45
|
*/
|
|
27
46
|
export class ReelGrid extends Container {
|
|
28
47
|
readonly __uiComponent = true as const;
|
|
29
48
|
|
|
30
|
-
private
|
|
31
|
-
private
|
|
32
|
-
private _maxRows: number;
|
|
33
|
-
private _cellSize: number;
|
|
34
|
-
private _gap: number;
|
|
49
|
+
private _cfg: ReelGridConfig;
|
|
50
|
+
private _geom: ResolvedGeometry;
|
|
35
51
|
private _cells: SymbolCell[][] = [];
|
|
36
52
|
private _cellLayer = new Container();
|
|
37
53
|
private _resolve: SymbolResolver;
|
|
@@ -40,28 +56,18 @@ export class ReelGrid extends Container {
|
|
|
40
56
|
|
|
41
57
|
constructor(config: ReelGridConfig) {
|
|
42
58
|
super();
|
|
43
|
-
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);
|
|
49
|
-
this._cellSize = config.cellSize;
|
|
50
|
-
this._gap = config.gap ?? 0;
|
|
59
|
+
this._cfg = { ...config };
|
|
51
60
|
this._resolve = config.resolve;
|
|
52
61
|
this._frameStyle = config.frameStyle;
|
|
62
|
+
this._geom = resolveGeometry(config);
|
|
53
63
|
|
|
54
|
-
if (config.decoration) {
|
|
64
|
+
if (config.decoration?.texture) {
|
|
55
65
|
const pad = config.decoration.padding ?? 0;
|
|
56
|
-
const
|
|
57
|
-
|
|
58
|
-
|
|
59
|
-
|
|
60
|
-
|
|
61
|
-
deco.height = h;
|
|
62
|
-
deco.position.set(-pad - this._cellSize / 2, -pad - this._cellSize / 2);
|
|
63
|
-
this.addChild(deco);
|
|
64
|
-
}
|
|
66
|
+
const deco = new Sprite(config.decoration.texture);
|
|
67
|
+
deco.width = this._geom.gridW + pad * 2;
|
|
68
|
+
deco.height = this._geom.gridH + pad * 2;
|
|
69
|
+
deco.position.set(this._geom.leftX - pad, this._geom.topY - pad);
|
|
70
|
+
this.addChild(deco);
|
|
65
71
|
}
|
|
66
72
|
|
|
67
73
|
this.addChild(this._cellLayer);
|
|
@@ -70,12 +76,19 @@ export class ReelGrid extends Container {
|
|
|
70
76
|
if (config.mask) this._applyMask();
|
|
71
77
|
}
|
|
72
78
|
|
|
79
|
+
private get _cols(): number {
|
|
80
|
+
return this._geom.cols;
|
|
81
|
+
}
|
|
82
|
+
private get _rowsPerReel(): number[] {
|
|
83
|
+
return this._geom.rowsPerReel;
|
|
84
|
+
}
|
|
85
|
+
|
|
73
86
|
private _buildCells(): void {
|
|
74
87
|
for (let c = 0; c < this._cols; c++) {
|
|
75
88
|
this._cells[c] = [];
|
|
76
89
|
for (let r = 0; r < this._rowsPerReel[c]; r++) {
|
|
77
90
|
const cell = new SymbolCell({
|
|
78
|
-
size: this.
|
|
91
|
+
size: this.cellSize(c),
|
|
79
92
|
resolve: this._resolve,
|
|
80
93
|
frameStyle: this._frameStyle,
|
|
81
94
|
});
|
|
@@ -87,10 +100,19 @@ export class ReelGrid extends Container {
|
|
|
87
100
|
}
|
|
88
101
|
}
|
|
89
102
|
|
|
103
|
+
/** Per-strip mask: one window per reel sized to that reel's own cell width × column height. */
|
|
90
104
|
private _applyMask(): void {
|
|
91
|
-
const
|
|
92
|
-
const
|
|
93
|
-
|
|
105
|
+
const g = this._geom;
|
|
106
|
+
const m = new Graphics();
|
|
107
|
+
for (let c = 0; c < this._cols; c++) {
|
|
108
|
+
const rows = this._rowsPerReel[c];
|
|
109
|
+
const w = g.cellW[c];
|
|
110
|
+
const h = rows * g.cellH[c] + Math.max(0, rows - 1) * g.rowGap[c];
|
|
111
|
+
const x = g.colX[c] - w / 2;
|
|
112
|
+
const y = g.yOff[c] - g.cellH[c] / 2;
|
|
113
|
+
m.rect(x, y, w, h);
|
|
114
|
+
}
|
|
115
|
+
m.fill(0xffffff);
|
|
94
116
|
this._cellLayer.mask = m;
|
|
95
117
|
this.addChild(m);
|
|
96
118
|
this._mask = m;
|
|
@@ -101,10 +123,15 @@ export class ReelGrid extends Container {
|
|
|
101
123
|
}
|
|
102
124
|
/** Tallest reel's row count (the grid's visual height in rows). */
|
|
103
125
|
get rows(): number {
|
|
104
|
-
return this.
|
|
126
|
+
return this._geom.maxRows;
|
|
127
|
+
}
|
|
128
|
+
/** Cell dimensions (px) for a reel. Rectangular / per-strip aware. */
|
|
129
|
+
cellSize(col: number): { width: number; height: number } {
|
|
130
|
+
return { width: this._geom.cellW[col] ?? 0, height: this._geom.cellH[col] ?? 0 };
|
|
105
131
|
}
|
|
106
|
-
|
|
107
|
-
|
|
132
|
+
/** The fully-resolved grid geometry (read-only view). */
|
|
133
|
+
get geometry(): ResolvedGeometry {
|
|
134
|
+
return this._geom;
|
|
108
135
|
}
|
|
109
136
|
rowsOf(col: number): number {
|
|
110
137
|
return this._rowsPerReel[col] ?? 0;
|
|
@@ -113,12 +140,14 @@ export class ReelGrid extends Container {
|
|
|
113
140
|
return this._rowsPerReel.slice();
|
|
114
141
|
}
|
|
115
142
|
|
|
116
|
-
/** Cell centre position. Variable-height reels are centred
|
|
143
|
+
/** Cell centre position. Variable-height reels are centred about a shared centre line. */
|
|
117
144
|
cellPosition(col: number, row: number): { x: number; y: number } {
|
|
118
|
-
|
|
119
|
-
|
|
120
|
-
|
|
121
|
-
|
|
145
|
+
return cellPositionOf(this._geom, col, row);
|
|
146
|
+
}
|
|
147
|
+
|
|
148
|
+
/** Grid bounding-box centre in local coords. */
|
|
149
|
+
center(): { x: number; y: number } {
|
|
150
|
+
return { x: this._geom.centerX, y: this._geom.centerY };
|
|
122
151
|
}
|
|
123
152
|
|
|
124
153
|
getCell(col: number, row: number): SymbolCell {
|
|
@@ -138,8 +167,8 @@ export class ReelGrid extends Container {
|
|
|
138
167
|
if (rowsPerReel.length !== this._cols) return;
|
|
139
168
|
this._cellLayer.removeChildren().forEach((c) => c.destroy());
|
|
140
169
|
this._cells = [];
|
|
141
|
-
this.
|
|
142
|
-
this.
|
|
170
|
+
this._cfg = { ...this._cfg, rowsPerReel: rowsPerReel.slice() };
|
|
171
|
+
this._geom = resolveGeometry(this._cfg);
|
|
143
172
|
this._buildCells();
|
|
144
173
|
if (this._mask) {
|
|
145
174
|
this._mask.destroy();
|
|
@@ -149,8 +178,18 @@ export class ReelGrid extends Container {
|
|
|
149
178
|
}
|
|
150
179
|
}
|
|
151
180
|
|
|
152
|
-
|
|
153
|
-
|
|
181
|
+
/**
|
|
182
|
+
* Re-resolve geometry after a base cell-size change and reposition cells. Accepts a square
|
|
183
|
+
* scalar (updates `cellSize`) or explicit `{width,height}`. Per-strip overrides still apply.
|
|
184
|
+
* (Cell frames are not re-drawn here — a geometry change routes through a full rebuild upstream.)
|
|
185
|
+
*/
|
|
186
|
+
resize(size: number | { width: number; height: number }): void {
|
|
187
|
+
if (typeof size === 'number') {
|
|
188
|
+
this._cfg = { ...this._cfg, cellSize: size, cellWidth: undefined, cellHeight: undefined };
|
|
189
|
+
} else {
|
|
190
|
+
this._cfg = { ...this._cfg, cellWidth: size.width, cellHeight: size.height };
|
|
191
|
+
}
|
|
192
|
+
this._geom = resolveGeometry(this._cfg);
|
|
154
193
|
for (let c = 0; c < this._cols; c++) {
|
|
155
194
|
for (let r = 0; r < this._rowsPerReel[c]; r++) {
|
|
156
195
|
const { x, y } = this.cellPosition(c, r);
|
|
@@ -21,7 +21,8 @@ export interface CellState {
|
|
|
21
21
|
fresh?: boolean;
|
|
22
22
|
}
|
|
23
23
|
export interface SymbolCellConfig {
|
|
24
|
-
size
|
|
24
|
+
/** Square scalar or rectangular cell size in px. */
|
|
25
|
+
size: number | { width: number; height: number };
|
|
25
26
|
resolve: SymbolResolver;
|
|
26
27
|
frameStyle?: CellFrameStyle;
|
|
27
28
|
}
|
|
@@ -34,10 +35,14 @@ const DEFAULT_STYLE: Required<CellFrameStyle> = {
|
|
|
34
35
|
fresh: { color: 0xffffff, alpha: 0.6 },
|
|
35
36
|
};
|
|
36
37
|
|
|
38
|
+
const cellDims = (size: number | { width: number; height: number }) =>
|
|
39
|
+
typeof size === 'number' ? { width: size, height: size } : size;
|
|
40
|
+
|
|
37
41
|
export class SymbolCell extends Container {
|
|
38
42
|
readonly __uiComponent = true as const;
|
|
39
43
|
|
|
40
|
-
private
|
|
44
|
+
private _w: number;
|
|
45
|
+
private _h: number;
|
|
41
46
|
private _resolve: SymbolResolver;
|
|
42
47
|
private _style: Required<CellFrameStyle>;
|
|
43
48
|
private _frame: Graphics;
|
|
@@ -50,7 +55,9 @@ export class SymbolCell extends Container {
|
|
|
50
55
|
|
|
51
56
|
constructor(config: SymbolCellConfig) {
|
|
52
57
|
super();
|
|
53
|
-
|
|
58
|
+
const { width, height } = cellDims(config.size);
|
|
59
|
+
this._w = width;
|
|
60
|
+
this._h = height;
|
|
54
61
|
this._resolve = config.resolve;
|
|
55
62
|
this._style = { ...DEFAULT_STYLE, ...(config.frameStyle ?? {}) } as Required<CellFrameStyle>;
|
|
56
63
|
this._frame = new Graphics();
|
|
@@ -78,7 +85,7 @@ export class SymbolCell extends Container {
|
|
|
78
85
|
}
|
|
79
86
|
const v = this._resolve(data.symbol);
|
|
80
87
|
if (v) {
|
|
81
|
-
v.resize?.(this.
|
|
88
|
+
v.resize?.({ width: this._w, height: this._h });
|
|
82
89
|
this.addChildAt(v, 1); // above frame, below badges
|
|
83
90
|
this._view = v;
|
|
84
91
|
}
|
|
@@ -123,7 +130,7 @@ export class SymbolCell extends Container {
|
|
|
123
130
|
const s = this._style[key];
|
|
124
131
|
this._frame.clear();
|
|
125
132
|
this._frame
|
|
126
|
-
.roundRect(-this.
|
|
133
|
+
.roundRect(-this._w / 2, -this._h / 2, this._w, this._h, this._style.radius)
|
|
127
134
|
.fill({ color: s.color, alpha: s.alpha });
|
|
128
135
|
// store the colour as tint for cheap inspection/testing
|
|
129
136
|
this._frame.tint = s.color;
|
|
@@ -136,7 +143,7 @@ export class SymbolCell extends Container {
|
|
|
136
143
|
}
|
|
137
144
|
if (!value || value <= 1) return;
|
|
138
145
|
this._multBadge = this._badge(`×${value}`, 0xffd24a);
|
|
139
|
-
this._multBadge.position.set(this.
|
|
146
|
+
this._multBadge.position.set(this._w / 2 - 12, -this._h / 2 + 12);
|
|
140
147
|
this._badges.addChild(this._multBadge);
|
|
141
148
|
}
|
|
142
149
|
|
|
@@ -147,7 +154,7 @@ export class SymbolCell extends Container {
|
|
|
147
154
|
}
|
|
148
155
|
if (!value || value <= 0) return;
|
|
149
156
|
this._bonusBadge = this._badge(`+${value}`, 0x7ad7ff);
|
|
150
|
-
this._bonusBadge.position.set(-this.
|
|
157
|
+
this._bonusBadge.position.set(-this._w / 2 + 12, -this._h / 2 + 12);
|
|
151
158
|
this._badges.addChild(this._bonusBadge);
|
|
152
159
|
}
|
|
153
160
|
|
|
@@ -5,8 +5,8 @@ export interface SymbolView extends Container {
|
|
|
5
5
|
playIdle?(): void;
|
|
6
6
|
playWin?(): Promise<void>;
|
|
7
7
|
showStatic?(): void;
|
|
8
|
-
/** Resize the symbol to the given cell size in pixels. */
|
|
9
|
-
resize?(size: number): void;
|
|
8
|
+
/** Resize the symbol to the given cell size in pixels (square scalar or rectangular). */
|
|
9
|
+
resize?(size: number | { width: number; height: number }): void;
|
|
10
10
|
}
|
|
11
11
|
|
|
12
12
|
/** Game-supplied factory: build the view for a symbol id (sprite / layered sprites / Spine / composite). */
|
|
@@ -0,0 +1,155 @@
|
|
|
1
|
+
// packages/game-engine/src/slot/grid/geometry.ts
|
|
2
|
+
//
|
|
3
|
+
// Pure geometry resolver for the reel grid. Turns the (backward-compatible) grid config
|
|
4
|
+
// — a square `cellSize` + single `gap`, optionally overridden by rectangular / per-strip
|
|
5
|
+
// dimensions and per-strip gaps — into a fully-resolved, per-reel layout.
|
|
6
|
+
//
|
|
7
|
+
// Coordinate convention (unchanged from the original square grid):
|
|
8
|
+
// - `cellPosition` returns CELL-CENTRE coordinates.
|
|
9
|
+
// - Cell (0,0)'s centre sits at local x = 0. Reels extend to the right.
|
|
10
|
+
// - Variable-height reels are vertically CENTRED about a shared centre line, so the
|
|
11
|
+
// tallest reel's row 0 sits at y = 0 (matching the old Megaways behaviour).
|
|
12
|
+
//
|
|
13
|
+
// See docs/reels-analysis-and-design.md §6.
|
|
14
|
+
|
|
15
|
+
/** Per-strip cell size override: a square scalar, or an explicit width/height. */
|
|
16
|
+
export type CellSizeSpec = number | { width: number; height: number };
|
|
17
|
+
|
|
18
|
+
/** Structural input any grid config (GridConfig / ReelGridConfig) satisfies. */
|
|
19
|
+
export interface GeometryInput {
|
|
20
|
+
cols: number;
|
|
21
|
+
/** Uniform row count. Ignored when `rowsPerReel` is set (and length matches `cols`). */
|
|
22
|
+
rows: number;
|
|
23
|
+
rowsPerReel?: number[];
|
|
24
|
+
/** Square cell size (shorthand: same width & height, all reels). */
|
|
25
|
+
cellSize: number;
|
|
26
|
+
/** Rectangular cells, uniform across reels. Override `cellSize`. */
|
|
27
|
+
cellWidth?: number;
|
|
28
|
+
cellHeight?: number;
|
|
29
|
+
/** Per-strip cell size (square scalar or {width,height}). Overrides the above for that reel. */
|
|
30
|
+
cellSizePerReel?: CellSizeSpec[];
|
|
31
|
+
/** Uniform gap (shorthand for both axes). */
|
|
32
|
+
gap?: number;
|
|
33
|
+
/** Horizontal gap between adjacent reels. Scalar, or per-boundary (length cols-1). */
|
|
34
|
+
colGap?: number | number[];
|
|
35
|
+
/** Vertical gap between rows. Scalar, or per-reel (length cols). */
|
|
36
|
+
rowGap?: number | number[];
|
|
37
|
+
}
|
|
38
|
+
|
|
39
|
+
export interface ResolvedGeometry {
|
|
40
|
+
cols: number;
|
|
41
|
+
rowsPerReel: number[];
|
|
42
|
+
maxRows: number;
|
|
43
|
+
/** Per-reel cell width / height (px). */
|
|
44
|
+
cellW: number[];
|
|
45
|
+
cellH: number[];
|
|
46
|
+
/** Per-reel vertical gap between rows (px). */
|
|
47
|
+
rowGap: number[];
|
|
48
|
+
/** Horizontal gap between reel i and i+1 (px), length cols-1. */
|
|
49
|
+
colGap: number[];
|
|
50
|
+
/** Cell-centre X of each reel (px). */
|
|
51
|
+
colX: number[];
|
|
52
|
+
/** Row-0 cell-centre Y of each reel (px) — centring offset baked in. */
|
|
53
|
+
yOff: number[];
|
|
54
|
+
/** Total grid bounding size (px). */
|
|
55
|
+
gridW: number;
|
|
56
|
+
gridH: number;
|
|
57
|
+
/** Grid bounding-box centre in local coords (px). */
|
|
58
|
+
centerX: number;
|
|
59
|
+
centerY: number;
|
|
60
|
+
/** Top-left corner of the grid bounding box in local coords (px). */
|
|
61
|
+
leftX: number;
|
|
62
|
+
topY: number;
|
|
63
|
+
}
|
|
64
|
+
|
|
65
|
+
const perReelSize = (spec: CellSizeSpec | undefined, w: number, h: number): [number, number] => {
|
|
66
|
+
if (typeof spec === 'number') return [spec, spec];
|
|
67
|
+
if (spec && typeof spec === 'object') return [spec.width, spec.height];
|
|
68
|
+
return [w, h];
|
|
69
|
+
};
|
|
70
|
+
|
|
71
|
+
const gapAt = (g: number | number[] | undefined, i: number, base: number): number =>
|
|
72
|
+
Array.isArray(g) ? (g[i] ?? base) : (g ?? base);
|
|
73
|
+
|
|
74
|
+
/** Resolve a grid config into a fully-populated per-reel geometry. */
|
|
75
|
+
export function resolveGeometry(g: GeometryInput): ResolvedGeometry {
|
|
76
|
+
const cols = Math.max(0, g.cols);
|
|
77
|
+
const rowsPerReel =
|
|
78
|
+
g.rowsPerReel && g.rowsPerReel.length === cols
|
|
79
|
+
? g.rowsPerReel.slice()
|
|
80
|
+
: Array.from({ length: cols }, () => g.rows);
|
|
81
|
+
const maxRows = cols ? Math.max(1, ...rowsPerReel) : 0;
|
|
82
|
+
|
|
83
|
+
const baseW = g.cellWidth ?? g.cellSize;
|
|
84
|
+
const baseH = g.cellHeight ?? g.cellSize;
|
|
85
|
+
const baseGap = g.gap ?? 0;
|
|
86
|
+
|
|
87
|
+
const cellW: number[] = [];
|
|
88
|
+
const cellH: number[] = [];
|
|
89
|
+
const rowGap: number[] = [];
|
|
90
|
+
for (let c = 0; c < cols; c++) {
|
|
91
|
+
const [w, h] = perReelSize(g.cellSizePerReel?.[c], baseW, baseH);
|
|
92
|
+
cellW[c] = w;
|
|
93
|
+
cellH[c] = h;
|
|
94
|
+
rowGap[c] = gapAt(g.rowGap, c, baseGap);
|
|
95
|
+
}
|
|
96
|
+
const colGap: number[] = [];
|
|
97
|
+
for (let i = 0; i < Math.max(0, cols - 1); i++) colGap[i] = gapAt(g.colGap, i, baseGap);
|
|
98
|
+
|
|
99
|
+
// Horizontal: reel 0 centre at x = 0, then accumulate half-widths + between-reel gaps.
|
|
100
|
+
const colX: number[] = [];
|
|
101
|
+
if (cols) colX[0] = 0;
|
|
102
|
+
for (let c = 1; c < cols; c++)
|
|
103
|
+
colX[c] = colX[c - 1] + cellW[c - 1] / 2 + colGap[c - 1] + cellW[c] / 2;
|
|
104
|
+
|
|
105
|
+
// Vertical: each reel's rows span (rows-1)*step; centre every reel about a shared line so
|
|
106
|
+
// the tallest reel's row 0 stays at y = 0 (parity with the old uniform Megaways layout).
|
|
107
|
+
const span: number[] = rowsPerReel.map((rr, c) => Math.max(0, rr - 1) * (cellH[c] + rowGap[c]));
|
|
108
|
+
const halfMaxSpan = cols ? Math.max(...span) / 2 : 0;
|
|
109
|
+
const yOff: number[] = span.map((s) => halfMaxSpan - s / 2);
|
|
110
|
+
|
|
111
|
+
// Bounding box.
|
|
112
|
+
let leftX = 0;
|
|
113
|
+
let rightX = 0;
|
|
114
|
+
let topY = 0;
|
|
115
|
+
let bottomY = 0;
|
|
116
|
+
for (let c = 0; c < cols; c++) {
|
|
117
|
+
leftX = Math.min(leftX, colX[c] - cellW[c] / 2);
|
|
118
|
+
rightX = Math.max(rightX, colX[c] + cellW[c] / 2);
|
|
119
|
+
const reelTop = yOff[c] - cellH[c] / 2;
|
|
120
|
+
topY = Math.min(topY, reelTop);
|
|
121
|
+
bottomY = Math.max(bottomY, reelTop + rowsPerReel[c] * cellH[c] + Math.max(0, rowsPerReel[c] - 1) * rowGap[c]);
|
|
122
|
+
}
|
|
123
|
+
const gridW = rightX - leftX;
|
|
124
|
+
const gridH = bottomY - topY;
|
|
125
|
+
|
|
126
|
+
return {
|
|
127
|
+
cols,
|
|
128
|
+
rowsPerReel,
|
|
129
|
+
maxRows,
|
|
130
|
+
cellW,
|
|
131
|
+
cellH,
|
|
132
|
+
rowGap,
|
|
133
|
+
colGap,
|
|
134
|
+
colX,
|
|
135
|
+
yOff,
|
|
136
|
+
gridW,
|
|
137
|
+
gridH,
|
|
138
|
+
centerX: (leftX + rightX) / 2,
|
|
139
|
+
centerY: (topY + bottomY) / 2,
|
|
140
|
+
leftX,
|
|
141
|
+
topY,
|
|
142
|
+
};
|
|
143
|
+
}
|
|
144
|
+
|
|
145
|
+
/** Cell-centre position from a resolved geometry. */
|
|
146
|
+
export function cellPositionOf(
|
|
147
|
+
geom: ResolvedGeometry,
|
|
148
|
+
col: number,
|
|
149
|
+
row: number,
|
|
150
|
+
): { x: number; y: number } {
|
|
151
|
+
return {
|
|
152
|
+
x: geom.colX[col] ?? 0,
|
|
153
|
+
y: (geom.yOff[col] ?? 0) + row * ((geom.cellH[col] ?? 0) + (geom.rowGap[col] ?? 0)),
|
|
154
|
+
};
|
|
155
|
+
}
|
package/src/slot/index.ts
CHANGED
|
@@ -5,6 +5,8 @@ export { SymbolCell } from './grid/SymbolCell';
|
|
|
5
5
|
export type { CellFrameStyle, CellData, CellState, SymbolCellConfig } from './grid/SymbolCell';
|
|
6
6
|
export { ReelGrid } from './grid/ReelGrid';
|
|
7
7
|
export type { DecorationConfig, ReelGridConfig } from './grid/ReelGrid';
|
|
8
|
+
export { resolveGeometry, cellPositionOf } from './grid/geometry';
|
|
9
|
+
export type { GeometryInput, ResolvedGeometry, CellSizeSpec } from './grid/geometry';
|
|
8
10
|
export { CascadeController } from './anim/CascadeController';
|
|
9
11
|
export type { CascadeStepData, CascadeTimings, CascadeAnim } from './anim/CascadeController';
|
|
10
12
|
export { ReelSpinController } from './anim/ReelSpinController';
|
|
@@ -19,6 +21,7 @@ export {
|
|
|
19
21
|
resolveReelConfig,
|
|
20
22
|
mergeReelConfig,
|
|
21
23
|
effectiveRowsPerReel,
|
|
24
|
+
resolveGridGeometry,
|
|
22
25
|
waysCount,
|
|
23
26
|
} from './config/ReelSystemConfig';
|
|
24
27
|
export type {
|
|
@@ -210,7 +210,7 @@ export class SpinEngine {
|
|
|
210
210
|
tape.x = base.x;
|
|
211
211
|
const landingStart = tapeLen - rows;
|
|
212
212
|
for (let i = 0; i < tapeLen; i++) {
|
|
213
|
-
const cell = new SymbolCell({ size: this._grid.cellSize, resolve: this._resolve });
|
|
213
|
+
const cell = new SymbolCell({ size: this._grid.cellSize(p.reel), resolve: this._resolve });
|
|
214
214
|
const sym =
|
|
215
215
|
i >= landingStart
|
|
216
216
|
? (p.landing[i - landingStart]?.symbol ?? null)
|
|
@@ -121,7 +121,12 @@ export function createReelSystem(opts: CreateReelSystemOptions): ReelSystem {
|
|
|
121
121
|
rows: config.grid.rows,
|
|
122
122
|
rowsPerReel: config.grid.rowsPerReel ?? effectiveRowsPerReel(config.grid),
|
|
123
123
|
cellSize: config.grid.cellSize,
|
|
124
|
+
cellWidth: config.grid.cellWidth,
|
|
125
|
+
cellHeight: config.grid.cellHeight,
|
|
126
|
+
cellSizePerReel: config.grid.cellSizePerReel,
|
|
124
127
|
gap: config.grid.gap,
|
|
128
|
+
colGap: config.grid.colGap,
|
|
129
|
+
rowGap: config.grid.rowGap,
|
|
125
130
|
resolve,
|
|
126
131
|
frameStyle: config.grid.frameStyle,
|
|
127
132
|
mask: config.grid.mask,
|
|
@@ -145,8 +150,13 @@ export function createReelSystem(opts: CreateReelSystemOptions): ReelSystem {
|
|
|
145
150
|
a.cols !== b.cols ||
|
|
146
151
|
a.rows !== b.rows ||
|
|
147
152
|
a.cellSize !== b.cellSize ||
|
|
153
|
+
a.cellWidth !== b.cellWidth ||
|
|
154
|
+
a.cellHeight !== b.cellHeight ||
|
|
148
155
|
a.gap !== b.gap ||
|
|
149
156
|
a.mask !== b.mask ||
|
|
157
|
+
JSON.stringify(a.cellSizePerReel) !== JSON.stringify(b.cellSizePerReel) ||
|
|
158
|
+
JSON.stringify(a.colGap) !== JSON.stringify(b.colGap) ||
|
|
159
|
+
JSON.stringify(a.rowGap) !== JSON.stringify(b.rowGap) ||
|
|
150
160
|
JSON.stringify(a.rowsPerReel) !== JSON.stringify(b.rowsPerReel) ||
|
|
151
161
|
(a.decoration?.padding ?? 0) !== (b.decoration?.padding ?? 0)
|
|
152
162
|
);
|
package/src/types.ts
CHANGED
|
@@ -113,7 +113,7 @@ export interface GameApplicationConfig {
|
|
|
113
113
|
debug?: boolean;
|
|
114
114
|
|
|
115
115
|
/** When set, GameApplication mounts the branded game shell after the SDK handshake. */
|
|
116
|
-
shell?: import('@energy8platform/
|
|
116
|
+
shell?: import('@energy8platform/shell/html').ShellConfig | false;
|
|
117
117
|
}
|
|
118
118
|
|
|
119
119
|
// ─── Scene Types ───────────────────────────────────────────
|