@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.
Files changed (68) hide show
  1. package/dist/core.cjs.js +2 -2
  2. package/dist/core.cjs.js.map +1 -1
  3. package/dist/core.d.ts +3 -3
  4. package/dist/core.esm.js +2 -2
  5. package/dist/core.esm.js.map +1 -1
  6. package/dist/devtools.cjs.js +577 -0
  7. package/dist/devtools.cjs.js.map +1 -0
  8. package/dist/devtools.d.ts +424 -0
  9. package/dist/devtools.esm.js +565 -0
  10. package/dist/devtools.esm.js.map +1 -0
  11. package/dist/harness.cjs.js +33 -0
  12. package/dist/harness.cjs.js.map +1 -0
  13. package/dist/harness.d.ts +25 -0
  14. package/dist/harness.esm.js +30 -0
  15. package/dist/harness.esm.js.map +1 -0
  16. package/dist/host.cjs.js +35 -10
  17. package/dist/host.cjs.js.map +1 -1
  18. package/dist/host.d.ts +22 -9
  19. package/dist/host.esm.js +24 -11
  20. package/dist/host.esm.js.map +1 -1
  21. package/dist/index.cjs.js +2 -2
  22. package/dist/index.cjs.js.map +1 -1
  23. package/dist/index.d.ts +3 -3
  24. package/dist/index.esm.js +2 -2
  25. package/dist/index.esm.js.map +1 -1
  26. package/dist/reel-panel-client.cjs.js +610 -0
  27. package/dist/reel-panel-client.cjs.js.map +1 -0
  28. package/dist/reel-panel-client.d.ts +5 -0
  29. package/dist/reel-panel-client.esm.js +608 -0
  30. package/dist/reel-panel-client.esm.js.map +1 -0
  31. package/dist/shell.cjs.js +4 -4
  32. package/dist/shell.d.ts +1 -1
  33. package/dist/shell.esm.js +1 -1
  34. package/dist/slot.cjs.js +226 -75
  35. package/dist/slot.cjs.js.map +1 -1
  36. package/dist/slot.d.ts +137 -19
  37. package/dist/slot.esm.js +224 -76
  38. package/dist/slot.esm.js.map +1 -1
  39. package/package.json +17 -2
  40. package/src/core/GameApplication.ts +3 -3
  41. package/src/harness/index.ts +35 -0
  42. package/src/host/createSlotGame.ts +6 -3
  43. package/src/host/index.ts +11 -1
  44. package/src/host/shellConfig.ts +22 -12
  45. package/src/host/types.ts +13 -2
  46. package/src/shell/index.ts +4 -3
  47. package/src/slot/cascade/TumbleController.ts +6 -3
  48. package/src/slot/config/ReelSystemConfig.ts +19 -0
  49. package/src/slot/devtools/configDiff.ts +41 -0
  50. package/src/slot/devtools/controlPanel.ts +151 -0
  51. package/src/slot/devtools/fieldSchema.ts +190 -0
  52. package/src/slot/devtools/index.ts +31 -0
  53. package/src/slot/devtools/panelClient.ts +113 -0
  54. package/src/slot/devtools/protocol.ts +29 -0
  55. package/src/slot/devtools/reelDevBridge.ts +80 -0
  56. package/src/slot/features/extra.ts +1 -2
  57. package/src/slot/features/symbols.ts +10 -10
  58. package/src/slot/features/types.ts +13 -5
  59. package/src/slot/features/wilds.ts +1 -1
  60. package/src/slot/grid/AnimatedSymbol.ts +20 -11
  61. package/src/slot/grid/ReelGrid.ts +80 -41
  62. package/src/slot/grid/SymbolCell.ts +14 -7
  63. package/src/slot/grid/SymbolView.ts +2 -2
  64. package/src/slot/grid/geometry.ts +155 -0
  65. package/src/slot/index.ts +3 -0
  66. package/src/slot/motion/SpinEngine.ts +1 -1
  67. package/src/slot/system/ReelSystem.ts +10 -0
  68. 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 and variable-height reels (Megaways):
25
- * when `rowsPerReel` is set each reel is vertically centred inside the tallest reel's envelope.
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 _cols: number;
31
- private _rowsPerReel: number[];
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._cols = config.cols;
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 w = this._cols * (this._cellSize + this._gap) - this._gap + pad * 2;
57
- const h = this._maxRows * (this._cellSize + this._gap) - this._gap + pad * 2;
58
- if (config.decoration.texture) {
59
- const deco = new Sprite(config.decoration.texture);
60
- deco.width = w;
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._cellSize,
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 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);
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._maxRows;
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
- get cellSize(): number {
107
- return this._cellSize;
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 in the max-rows envelope. */
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
- const step = this._cellSize + this._gap;
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 };
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._rowsPerReel = rowsPerReel.slice();
142
- this._maxRows = Math.max(1, ...this._rowsPerReel);
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
- resize(cellSize: number): void {
153
- this._cellSize = cellSize;
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: number;
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 _size: number;
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
- this._size = config.size;
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._size);
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._size / 2, -this._size / 2, this._size, this._size, this._style.radius)
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._size / 2 - 12, -this._size / 2 + 12);
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._size / 2 + 12, -this._size / 2 + 12);
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/platform-core/shell').ShellConfig | false;
116
+ shell?: import('@energy8platform/shell/html').ShellConfig | false;
117
117
  }
118
118
 
119
119
  // ─── Scene Types ───────────────────────────────────────────