@energy8platform/game-engine 0.23.0 → 0.24.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 (45) 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/host.cjs.js +35 -10
  7. package/dist/host.cjs.js.map +1 -1
  8. package/dist/host.d.ts +22 -9
  9. package/dist/host.esm.js +24 -11
  10. package/dist/host.esm.js.map +1 -1
  11. package/dist/index.cjs.js +2 -2
  12. package/dist/index.cjs.js.map +1 -1
  13. package/dist/index.d.ts +3 -3
  14. package/dist/index.esm.js +2 -2
  15. package/dist/index.esm.js.map +1 -1
  16. package/dist/shell.cjs.js +4 -4
  17. package/dist/shell.d.ts +1 -1
  18. package/dist/shell.esm.js +1 -1
  19. package/dist/slot.cjs.js +226 -75
  20. package/dist/slot.cjs.js.map +1 -1
  21. package/dist/slot.d.ts +137 -19
  22. package/dist/slot.esm.js +224 -76
  23. package/dist/slot.esm.js.map +1 -1
  24. package/package.json +2 -2
  25. package/src/core/GameApplication.ts +3 -3
  26. package/src/host/createSlotGame.ts +6 -3
  27. package/src/host/index.ts +11 -1
  28. package/src/host/shellConfig.ts +22 -12
  29. package/src/host/types.ts +13 -2
  30. package/src/shell/index.ts +4 -3
  31. package/src/slot/cascade/TumbleController.ts +6 -3
  32. package/src/slot/config/ReelSystemConfig.ts +19 -0
  33. package/src/slot/features/extra.ts +1 -2
  34. package/src/slot/features/symbols.ts +10 -10
  35. package/src/slot/features/types.ts +13 -5
  36. package/src/slot/features/wilds.ts +1 -1
  37. package/src/slot/grid/AnimatedSymbol.ts +20 -11
  38. package/src/slot/grid/ReelGrid.ts +80 -41
  39. package/src/slot/grid/SymbolCell.ts +14 -7
  40. package/src/slot/grid/SymbolView.ts +2 -2
  41. package/src/slot/grid/geometry.ts +155 -0
  42. package/src/slot/index.ts +3 -0
  43. package/src/slot/motion/SpinEngine.ts +1 -1
  44. package/src/slot/system/ReelSystem.ts +10 -0
  45. package/src/types.ts +1 -1
@@ -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 ───────────────────────────────────────────