@energy8platform/game-engine 0.22.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 +4 -4
  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
package/dist/slot.esm.js CHANGED
@@ -452,22 +452,23 @@ class SpriteAnimation {
452
452
  }
453
453
  }
454
454
 
455
+ const dims = (size) => typeof size === 'number' ? { width: size, height: size } : size;
455
456
  /** Built-in SymbolView: a static base sprite with optional idle/win spritesheet frames. */
456
457
  class AnimatedSymbol extends Container {
457
458
  _base;
458
459
  _anim = null;
459
460
  _textures;
460
- _size;
461
+ _w = 0;
462
+ _h = 0;
461
463
  _fps;
462
464
  constructor(config) {
463
465
  super();
464
466
  this._textures = config.textures;
465
- this._size = config.size;
466
467
  this._fps = config.fps ?? 24;
467
468
  this._base = new Sprite(config.textures.base);
468
469
  this._base.anchor.set(0.5);
469
470
  this.addChild(this._base);
470
- this.resize(this._size);
471
+ this.resize(config.size);
471
472
  }
472
473
  setTextures(t) {
473
474
  this._textures = t;
@@ -475,12 +476,14 @@ class AnimatedSymbol extends Container {
475
476
  this.showStatic();
476
477
  }
477
478
  resize(size) {
478
- this._size = size;
479
- this._base.width = size;
480
- this._base.height = size;
479
+ const { width, height } = dims(size);
480
+ this._w = width;
481
+ this._h = height;
482
+ this._base.width = width;
483
+ this._base.height = height;
481
484
  if (this._anim) {
482
- this._anim.width = size;
483
- this._anim.height = size;
485
+ this._anim.width = width;
486
+ this._anim.height = height;
484
487
  }
485
488
  }
486
489
  showStatic() {
@@ -510,8 +513,8 @@ class AnimatedSymbol extends Container {
510
513
  this._base.visible = false;
511
514
  const a = SpriteAnimation.create(frames, { loop, autoPlay: true, onComplete });
512
515
  a.anchor.set(0.5);
513
- a.width = this._size;
514
- a.height = this._size;
516
+ a.width = this._w;
517
+ a.height = this._h;
515
518
  a.animationSpeed = this._fps / 60;
516
519
  this.addChild(a);
517
520
  this._anim = a;
@@ -525,9 +528,11 @@ const DEFAULT_STYLE = {
525
528
  removed: { color: 0x223047, alpha: 0.12 },
526
529
  fresh: { color: 0xffffff, alpha: 0.6 },
527
530
  };
531
+ const cellDims = (size) => typeof size === 'number' ? { width: size, height: size } : size;
528
532
  class SymbolCell extends Container {
529
533
  __uiComponent = true;
530
- _size;
534
+ _w;
535
+ _h;
531
536
  _resolve;
532
537
  _style;
533
538
  _frame;
@@ -539,7 +544,9 @@ class SymbolCell extends Container {
539
544
  frameStyleKey = 'idle';
540
545
  constructor(config) {
541
546
  super();
542
- this._size = config.size;
547
+ const { width, height } = cellDims(config.size);
548
+ this._w = width;
549
+ this._h = height;
543
550
  this._resolve = config.resolve;
544
551
  this._style = { ...DEFAULT_STYLE, ...(config.frameStyle ?? {}) };
545
552
  this._frame = new Graphics();
@@ -567,7 +574,7 @@ class SymbolCell extends Container {
567
574
  }
568
575
  const v = this._resolve(data.symbol);
569
576
  if (v) {
570
- v.resize?.(this._size);
577
+ v.resize?.({ width: this._w, height: this._h });
571
578
  this.addChildAt(v, 1); // above frame, below badges
572
579
  this._view = v;
573
580
  }
@@ -608,7 +615,7 @@ class SymbolCell extends Container {
608
615
  const s = this._style[key];
609
616
  this._frame.clear();
610
617
  this._frame
611
- .roundRect(-this._size / 2, -this._size / 2, this._size, this._size, this._style.radius)
618
+ .roundRect(-this._w / 2, -this._h / 2, this._w, this._h, this._style.radius)
612
619
  .fill({ color: s.color, alpha: s.alpha });
613
620
  // store the colour as tint for cheap inspection/testing
614
621
  this._frame.tint = s.color;
@@ -621,7 +628,7 @@ class SymbolCell extends Container {
621
628
  if (!value || value <= 1)
622
629
  return;
623
630
  this._multBadge = this._badge(`×${value}`, 0xffd24a);
624
- this._multBadge.position.set(this._size / 2 - 12, -this._size / 2 + 12);
631
+ this._multBadge.position.set(this._w / 2 - 12, -this._h / 2 + 12);
625
632
  this._badges.addChild(this._multBadge);
626
633
  }
627
634
  _setBonus(value) {
@@ -632,7 +639,7 @@ class SymbolCell extends Container {
632
639
  if (!value || value <= 0)
633
640
  return;
634
641
  this._bonusBadge = this._badge(`+${value}`, 0x7ad7ff);
635
- this._bonusBadge.position.set(-this._size / 2 + 12, -this._size / 2 + 12);
642
+ this._bonusBadge.position.set(-this._w / 2 + 12, -this._h / 2 + 12);
636
643
  this._badges.addChild(this._bonusBadge);
637
644
  }
638
645
  _badge(label, color) {
@@ -644,17 +651,110 @@ class SymbolCell extends Container {
644
651
  }
645
652
  }
646
653
 
654
+ // packages/game-engine/src/slot/grid/geometry.ts
655
+ //
656
+ // Pure geometry resolver for the reel grid. Turns the (backward-compatible) grid config
657
+ // — a square `cellSize` + single `gap`, optionally overridden by rectangular / per-strip
658
+ // dimensions and per-strip gaps — into a fully-resolved, per-reel layout.
659
+ //
660
+ // Coordinate convention (unchanged from the original square grid):
661
+ // - `cellPosition` returns CELL-CENTRE coordinates.
662
+ // - Cell (0,0)'s centre sits at local x = 0. Reels extend to the right.
663
+ // - Variable-height reels are vertically CENTRED about a shared centre line, so the
664
+ // tallest reel's row 0 sits at y = 0 (matching the old Megaways behaviour).
665
+ //
666
+ // See docs/reels-analysis-and-design.md §6.
667
+ const perReelSize = (spec, w, h) => {
668
+ if (typeof spec === 'number')
669
+ return [spec, spec];
670
+ if (spec && typeof spec === 'object')
671
+ return [spec.width, spec.height];
672
+ return [w, h];
673
+ };
674
+ const gapAt = (g, i, base) => Array.isArray(g) ? (g[i] ?? base) : (g ?? base);
675
+ /** Resolve a grid config into a fully-populated per-reel geometry. */
676
+ function resolveGeometry(g) {
677
+ const cols = Math.max(0, g.cols);
678
+ const rowsPerReel = g.rowsPerReel && g.rowsPerReel.length === cols
679
+ ? g.rowsPerReel.slice()
680
+ : Array.from({ length: cols }, () => g.rows);
681
+ const maxRows = cols ? Math.max(1, ...rowsPerReel) : 0;
682
+ const baseW = g.cellWidth ?? g.cellSize;
683
+ const baseH = g.cellHeight ?? g.cellSize;
684
+ const baseGap = g.gap ?? 0;
685
+ const cellW = [];
686
+ const cellH = [];
687
+ const rowGap = [];
688
+ for (let c = 0; c < cols; c++) {
689
+ const [w, h] = perReelSize(g.cellSizePerReel?.[c], baseW, baseH);
690
+ cellW[c] = w;
691
+ cellH[c] = h;
692
+ rowGap[c] = gapAt(g.rowGap, c, baseGap);
693
+ }
694
+ const colGap = [];
695
+ for (let i = 0; i < Math.max(0, cols - 1); i++)
696
+ colGap[i] = gapAt(g.colGap, i, baseGap);
697
+ // Horizontal: reel 0 centre at x = 0, then accumulate half-widths + between-reel gaps.
698
+ const colX = [];
699
+ if (cols)
700
+ colX[0] = 0;
701
+ for (let c = 1; c < cols; c++)
702
+ colX[c] = colX[c - 1] + cellW[c - 1] / 2 + colGap[c - 1] + cellW[c] / 2;
703
+ // Vertical: each reel's rows span (rows-1)*step; centre every reel about a shared line so
704
+ // the tallest reel's row 0 stays at y = 0 (parity with the old uniform Megaways layout).
705
+ const span = rowsPerReel.map((rr, c) => Math.max(0, rr - 1) * (cellH[c] + rowGap[c]));
706
+ const halfMaxSpan = cols ? Math.max(...span) / 2 : 0;
707
+ const yOff = span.map((s) => halfMaxSpan - s / 2);
708
+ // Bounding box.
709
+ let leftX = 0;
710
+ let rightX = 0;
711
+ let topY = 0;
712
+ let bottomY = 0;
713
+ for (let c = 0; c < cols; c++) {
714
+ leftX = Math.min(leftX, colX[c] - cellW[c] / 2);
715
+ rightX = Math.max(rightX, colX[c] + cellW[c] / 2);
716
+ const reelTop = yOff[c] - cellH[c] / 2;
717
+ topY = Math.min(topY, reelTop);
718
+ bottomY = Math.max(bottomY, reelTop + rowsPerReel[c] * cellH[c] + Math.max(0, rowsPerReel[c] - 1) * rowGap[c]);
719
+ }
720
+ const gridW = rightX - leftX;
721
+ const gridH = bottomY - topY;
722
+ return {
723
+ cols,
724
+ rowsPerReel,
725
+ maxRows,
726
+ cellW,
727
+ cellH,
728
+ rowGap,
729
+ colGap,
730
+ colX,
731
+ yOff,
732
+ gridW,
733
+ gridH,
734
+ centerX: (leftX + rightX) / 2,
735
+ centerY: (topY + bottomY) / 2,
736
+ leftX,
737
+ topY,
738
+ };
739
+ }
740
+ /** Cell-centre position from a resolved geometry. */
741
+ function cellPositionOf(geom, col, row) {
742
+ return {
743
+ x: geom.colX[col] ?? 0,
744
+ y: (geom.yOff[col] ?? 0) + row * ((geom.cellH[col] ?? 0) + (geom.rowGap[col] ?? 0)),
745
+ };
746
+ }
747
+
647
748
  /**
648
- * A grid of `SymbolCell`s. Supports uniform grids and variable-height reels (Megaways):
649
- * when `rowsPerReel` is set each reel is vertically centred inside the tallest reel's envelope.
749
+ * A grid of `SymbolCell`s. Supports uniform grids, variable-height reels (Megaways), and
750
+ * rectangular / per-strip cell sizes with per-strip gaps. All layout flows through a single
751
+ * resolved geometry (see grid/geometry.ts); every consumer reads positions via `cellPosition`
752
+ * and dimensions via `cellSize(col)` rather than assuming a square cell.
650
753
  */
651
754
  class ReelGrid extends Container {
652
755
  __uiComponent = true;
653
- _cols;
654
- _rowsPerReel;
655
- _maxRows;
656
- _cellSize;
657
- _gap;
756
+ _cfg;
757
+ _geom;
658
758
  _cells = [];
659
759
  _cellLayer = new Container();
660
760
  _resolve;
@@ -662,39 +762,35 @@ class ReelGrid extends Container {
662
762
  _mask = null;
663
763
  constructor(config) {
664
764
  super();
665
- this._cols = config.cols;
666
- this._rowsPerReel =
667
- config.rowsPerReel && config.rowsPerReel.length === config.cols
668
- ? config.rowsPerReel.slice()
669
- : Array.from({ length: config.cols }, () => config.rows);
670
- this._maxRows = Math.max(1, ...this._rowsPerReel);
671
- this._cellSize = config.cellSize;
672
- this._gap = config.gap ?? 0;
765
+ this._cfg = { ...config };
673
766
  this._resolve = config.resolve;
674
767
  this._frameStyle = config.frameStyle;
675
- if (config.decoration) {
768
+ this._geom = resolveGeometry(config);
769
+ if (config.decoration?.texture) {
676
770
  const pad = config.decoration.padding ?? 0;
677
- const w = this._cols * (this._cellSize + this._gap) - this._gap + pad * 2;
678
- const h = this._maxRows * (this._cellSize + this._gap) - this._gap + pad * 2;
679
- if (config.decoration.texture) {
680
- const deco = new Sprite(config.decoration.texture);
681
- deco.width = w;
682
- deco.height = h;
683
- deco.position.set(-pad - this._cellSize / 2, -pad - this._cellSize / 2);
684
- this.addChild(deco);
685
- }
771
+ const deco = new Sprite(config.decoration.texture);
772
+ deco.width = this._geom.gridW + pad * 2;
773
+ deco.height = this._geom.gridH + pad * 2;
774
+ deco.position.set(this._geom.leftX - pad, this._geom.topY - pad);
775
+ this.addChild(deco);
686
776
  }
687
777
  this.addChild(this._cellLayer);
688
778
  this._buildCells();
689
779
  if (config.mask)
690
780
  this._applyMask();
691
781
  }
782
+ get _cols() {
783
+ return this._geom.cols;
784
+ }
785
+ get _rowsPerReel() {
786
+ return this._geom.rowsPerReel;
787
+ }
692
788
  _buildCells() {
693
789
  for (let c = 0; c < this._cols; c++) {
694
790
  this._cells[c] = [];
695
791
  for (let r = 0; r < this._rowsPerReel[c]; r++) {
696
792
  const cell = new SymbolCell({
697
- size: this._cellSize,
793
+ size: this.cellSize(c),
698
794
  resolve: this._resolve,
699
795
  frameStyle: this._frameStyle,
700
796
  });
@@ -705,10 +801,19 @@ class ReelGrid extends Container {
705
801
  }
706
802
  }
707
803
  }
804
+ /** Per-strip mask: one window per reel sized to that reel's own cell width × column height. */
708
805
  _applyMask() {
709
- const w = this._cols * (this._cellSize + this._gap) - this._gap;
710
- const h = this._maxRows * (this._cellSize + this._gap) - this._gap;
711
- const m = new Graphics().rect(-this._cellSize / 2, -this._cellSize / 2, w, h).fill(0xffffff);
806
+ const g = this._geom;
807
+ const m = new Graphics();
808
+ for (let c = 0; c < this._cols; c++) {
809
+ const rows = this._rowsPerReel[c];
810
+ const w = g.cellW[c];
811
+ const h = rows * g.cellH[c] + Math.max(0, rows - 1) * g.rowGap[c];
812
+ const x = g.colX[c] - w / 2;
813
+ const y = g.yOff[c] - g.cellH[c] / 2;
814
+ m.rect(x, y, w, h);
815
+ }
816
+ m.fill(0xffffff);
712
817
  this._cellLayer.mask = m;
713
818
  this.addChild(m);
714
819
  this._mask = m;
@@ -718,10 +823,15 @@ class ReelGrid extends Container {
718
823
  }
719
824
  /** Tallest reel's row count (the grid's visual height in rows). */
720
825
  get rows() {
721
- return this._maxRows;
826
+ return this._geom.maxRows;
827
+ }
828
+ /** Cell dimensions (px) for a reel. Rectangular / per-strip aware. */
829
+ cellSize(col) {
830
+ return { width: this._geom.cellW[col] ?? 0, height: this._geom.cellH[col] ?? 0 };
722
831
  }
723
- get cellSize() {
724
- return this._cellSize;
832
+ /** The fully-resolved grid geometry (read-only view). */
833
+ get geometry() {
834
+ return this._geom;
725
835
  }
726
836
  rowsOf(col) {
727
837
  return this._rowsPerReel[col] ?? 0;
@@ -729,12 +839,13 @@ class ReelGrid extends Container {
729
839
  get rowsPerReel() {
730
840
  return this._rowsPerReel.slice();
731
841
  }
732
- /** Cell centre position. Variable-height reels are centred in the max-rows envelope. */
842
+ /** Cell centre position. Variable-height reels are centred about a shared centre line. */
733
843
  cellPosition(col, row) {
734
- const step = this._cellSize + this._gap;
735
- const reelRows = this._rowsPerReel[col] ?? this._maxRows;
736
- const yOffset = ((this._maxRows - reelRows) / 2) * step;
737
- return { x: col * step, y: yOffset + row * step };
844
+ return cellPositionOf(this._geom, col, row);
845
+ }
846
+ /** Grid bounding-box centre in local coords. */
847
+ center() {
848
+ return { x: this._geom.centerX, y: this._geom.centerY };
738
849
  }
739
850
  getCell(col, row) {
740
851
  return this._cells[col][row];
@@ -752,8 +863,8 @@ class ReelGrid extends Container {
752
863
  return;
753
864
  this._cellLayer.removeChildren().forEach((c) => c.destroy());
754
865
  this._cells = [];
755
- this._rowsPerReel = rowsPerReel.slice();
756
- this._maxRows = Math.max(1, ...this._rowsPerReel);
866
+ this._cfg = { ...this._cfg, rowsPerReel: rowsPerReel.slice() };
867
+ this._geom = resolveGeometry(this._cfg);
757
868
  this._buildCells();
758
869
  if (this._mask) {
759
870
  this._mask.destroy();
@@ -762,8 +873,19 @@ class ReelGrid extends Container {
762
873
  this._applyMask();
763
874
  }
764
875
  }
765
- resize(cellSize) {
766
- this._cellSize = cellSize;
876
+ /**
877
+ * Re-resolve geometry after a base cell-size change and reposition cells. Accepts a square
878
+ * scalar (updates `cellSize`) or explicit `{width,height}`. Per-strip overrides still apply.
879
+ * (Cell frames are not re-drawn here — a geometry change routes through a full rebuild upstream.)
880
+ */
881
+ resize(size) {
882
+ if (typeof size === 'number') {
883
+ this._cfg = { ...this._cfg, cellSize: size, cellWidth: undefined, cellHeight: undefined };
884
+ }
885
+ else {
886
+ this._cfg = { ...this._cfg, cellWidth: size.width, cellHeight: size.height };
887
+ }
888
+ this._geom = resolveGeometry(this._cfg);
767
889
  for (let c = 0; c < this._cols; c++) {
768
890
  for (let r = 0; r < this._rowsPerReel[c]; r++) {
769
891
  const { x, y } = this.cellPosition(c, r);
@@ -1136,6 +1258,10 @@ function effectiveRowsPerReel(grid) {
1136
1258
  return grid.rowsPerReel.slice();
1137
1259
  return Array.from({ length: grid.cols }, () => grid.rows);
1138
1260
  }
1261
+ /** Resolve a `GridConfig` into a fully-populated per-reel geometry (rectangular / per-strip aware). */
1262
+ function resolveGridGeometry(grid) {
1263
+ return resolveGeometry(grid);
1264
+ }
1139
1265
  /** Total ways-to-win for a ways/megaways grid (product of per-reel heights). */
1140
1266
  function waysCount(grid) {
1141
1267
  return effectiveRowsPerReel(grid).reduce((a, b) => a * b, 1);
@@ -1495,7 +1621,7 @@ class SpinEngine {
1495
1621
  tape.x = base.x;
1496
1622
  const landingStart = tapeLen - rows;
1497
1623
  for (let i = 0; i < tapeLen; i++) {
1498
- const cell = new SymbolCell({ size: this._grid.cellSize, resolve: this._resolve });
1624
+ const cell = new SymbolCell({ size: this._grid.cellSize(p.reel), resolve: this._resolve });
1499
1625
  const sym = i >= landingStart
1500
1626
  ? (p.landing[i - landingStart]?.symbol ?? null)
1501
1627
  : (pool[i % pool.length] ?? null);
@@ -1820,8 +1946,10 @@ class TumbleController {
1820
1946
  cell.alpha = 1;
1821
1947
  }
1822
1948
  this._undim();
1823
- // 3. gravity: survivors slide down, new cells drop from above
1824
- const rowStep = this._grid.cellPosition(0, 1).y - this._grid.cellPosition(0, 0).y;
1949
+ // 3. gravity: survivors slide down, new cells drop from above (per-reel row step)
1950
+ const rowStepOf = (col) => this._grid.rowsOf(col) > 1
1951
+ ? this._grid.cellPosition(col, 1).y - this._grid.cellPosition(col, 0).y
1952
+ : this._grid.cellSize(col).height;
1825
1953
  const slides = this._cfg.gravity ? (step.drops ?? this.deriveDrops(step)) : [];
1826
1954
  const anims = [];
1827
1955
  for (const d of slides) {
@@ -1841,7 +1969,7 @@ class TumbleController {
1841
1969
  cell.setState({ fresh: true });
1842
1970
  cell.alpha = 1;
1843
1971
  cell.scale.set(1);
1844
- cell.position.set(home.x, home.y - rowStep * (this._grid.rowsOf(n.col) + 1));
1972
+ cell.position.set(home.x, home.y - rowStepOf(n.col) * (this._grid.rowsOf(n.col) + 1));
1845
1973
  const idx = (perCol[n.col] = (perCol[n.col] ?? 0) + 1);
1846
1974
  anims.push((async () => {
1847
1975
  await Tween.delay(idx * 28 * f);
@@ -1905,17 +2033,26 @@ class TumbleController {
1905
2033
  function cellCenter(grid, col, row) {
1906
2034
  return grid.cellPosition(col, row);
1907
2035
  }
1908
- function rowStepOf(grid) {
1909
- return grid.cellPosition(0, 1).y - grid.cellPosition(0, 0).y;
2036
+ /** Vertical row-to-row step for a reel (falls back to the reel's cell height for 1-row reels). */
2037
+ function rowStepOf(grid, col = 0) {
2038
+ if (grid.rowsOf(col) > 1)
2039
+ return grid.cellPosition(col, 1).y - grid.cellPosition(col, 0).y;
2040
+ return grid.cellSize(col).height;
2041
+ }
2042
+ /** Horizontal reel-to-reel step at a boundary (falls back to the reel's cell width for 1-col grids). */
2043
+ function colStepOf(grid, col = 0) {
2044
+ if (col + 1 < grid.cols)
2045
+ return grid.cellPosition(col + 1, 0).x - grid.cellPosition(col, 0).x;
2046
+ return grid.cellSize(col).width;
1910
2047
  }
1911
2048
  /** A glowing ring drawn around a cell. Returns a disposer. */
1912
2049
  function glowRing(fx, grid, col, row, color) {
1913
2050
  if (fx.destroyed)
1914
2051
  return () => { };
1915
2052
  const { x, y } = cellCenter(grid, col, row);
1916
- const s = grid.cellSize;
2053
+ const { width, height } = grid.cellSize(col);
1917
2054
  const g = new Graphics()
1918
- .roundRect(x - s / 2, y - s / 2, s, s, 10)
2055
+ .roundRect(x - width / 2, y - height / 2, width, height, 10)
1919
2056
  .stroke({ color, width: 3, alpha: 0.9 });
1920
2057
  fx.addChild(g);
1921
2058
  return () => g.destroy();
@@ -1958,7 +2095,7 @@ async function dropCell(grid, cell, col, row, ms = 280) {
1958
2095
  if (cell.destroyed)
1959
2096
  return;
1960
2097
  const home = grid.cellPosition(col, row);
1961
- cell.position.set(home.x, home.y - rowStepOf(grid) * (row + 2));
2098
+ cell.position.set(home.x, home.y - rowStepOf(grid, col) * (row + 2));
1962
2099
  cell.alpha = 1;
1963
2100
  cell.scale.set(1);
1964
2101
  await Tween.to(cell, { 'position.y': home.y }, ms, easingByName('easeOutBounce'));
@@ -2023,7 +2160,7 @@ const StickySymbols = {
2023
2160
  glowRing(ctx.fx, ctx.grid, p.col, p.row, f.ringColor);
2024
2161
  await pulseCell(cell, 1.2, 260);
2025
2162
  const { x, y } = cellCenter(ctx.grid, p.col, p.row);
2026
- await floatLabel(ctx.fx, x, y - ctx.grid.cellSize / 2, f.durationSpins ? `STICKY ${f.durationSpins}` : 'STICKY', f.ringColor, 600 + i * 50);
2163
+ await floatLabel(ctx.fx, x, y - ctx.grid.cellSize(p.col).height / 2, f.durationSpins ? `STICKY ${f.durationSpins}` : 'STICKY', f.ringColor, 600 + i * 50);
2027
2164
  }));
2028
2165
  },
2029
2166
  };
@@ -2164,14 +2301,15 @@ const GiantSymbol = {
2164
2301
  const view = ctx.resolve(sym);
2165
2302
  if (!view)
2166
2303
  return;
2167
- const step = rowStepOf(ctx.grid);
2304
+ const cs = ctx.grid.cellSize(anchorCol);
2305
+ const stepX = colStepOf(ctx.grid, anchorCol);
2306
+ const step = rowStepOf(ctx.grid, anchorCol);
2168
2307
  const tl = cellCenter(ctx.grid, anchorCol, 0);
2169
2308
  const giant = new Container();
2170
2309
  giant.addChild(view);
2171
- view.resize?.(ctx.grid.cellSize * Math.max(w, h));
2172
- if (view.scale)
2173
- view.scale.set(((ctx.grid.cellSize * w) / (ctx.grid.cellSize * Math.max(w, h))) * view.scale.x, ((ctx.grid.cellSize * h) / (ctx.grid.cellSize * Math.max(w, h))) * view.scale.y);
2174
- giant.position.set(tl.x + ((w - 1) * step) / 2, tl.y + ((h - 1) * step) / 2);
2310
+ // span w×h cells (footprint ignores gaps, matching the previous single-cell unit)
2311
+ view.resize?.({ width: cs.width * w, height: cs.height * h });
2312
+ giant.position.set(tl.x + ((w - 1) * stepX) / 2, tl.y + ((h - 1) * step) / 2);
2175
2313
  // hide covered cells
2176
2314
  for (let c = anchorCol; c < anchorCol + w; c++)
2177
2315
  for (let r = 0; r < h; r++)
@@ -2203,8 +2341,9 @@ const SplitSymbol = {
2203
2341
  await Promise.all(left.map(async (p, i) => {
2204
2342
  await Tween.delay((i % 6) * 30);
2205
2343
  const { x, y } = cellCenter(ctx.grid, p.col, p.row);
2344
+ const cs = ctx.grid.cellSize(p.col);
2206
2345
  await pulseCell(ctx.grid.getCell(p.col, p.row), 1.12, 200);
2207
- await floatLabel(ctx.fx, x + ctx.grid.cellSize / 3, y - ctx.grid.cellSize / 3, `×${f.factor}`, 0x9b5cff, 600);
2346
+ await floatLabel(ctx.fx, x + cs.width / 3, y - cs.height / 3, `×${f.factor}`, 0x9b5cff, 600);
2208
2347
  }));
2209
2348
  },
2210
2349
  };
@@ -2257,8 +2396,7 @@ const MultiplierSymbols = {
2257
2396
  const { x, y } = cellCenter(ctx.grid, p.col, p.row);
2258
2397
  await floatLabel(ctx.fx, x, y, `×${values[spots.indexOf(p)]}`, 0xffd24a, 500);
2259
2398
  }));
2260
- const cx = (ctx.grid.cols * ctx.grid.cellSize) / 2 - ctx.grid.cellSize / 2;
2261
- const cy = (ctx.grid.rows * ctx.grid.cellSize) / 2 - ctx.grid.cellSize / 2;
2399
+ const { x: cx, y: cy } = ctx.grid.center();
2262
2400
  await floatLabel(ctx.fx, cx, cy, `×${capped}`, 0xffe24a, 900);
2263
2401
  },
2264
2402
  };
@@ -2464,7 +2602,12 @@ function createReelSystem(opts) {
2464
2602
  rows: config.grid.rows,
2465
2603
  rowsPerReel: config.grid.rowsPerReel ?? effectiveRowsPerReel(config.grid),
2466
2604
  cellSize: config.grid.cellSize,
2605
+ cellWidth: config.grid.cellWidth,
2606
+ cellHeight: config.grid.cellHeight,
2607
+ cellSizePerReel: config.grid.cellSizePerReel,
2467
2608
  gap: config.grid.gap,
2609
+ colGap: config.grid.colGap,
2610
+ rowGap: config.grid.rowGap,
2468
2611
  resolve,
2469
2612
  frameStyle: config.grid.frameStyle,
2470
2613
  mask: config.grid.mask,
@@ -2485,8 +2628,13 @@ function createReelSystem(opts) {
2485
2628
  return (a.cols !== b.cols ||
2486
2629
  a.rows !== b.rows ||
2487
2630
  a.cellSize !== b.cellSize ||
2631
+ a.cellWidth !== b.cellWidth ||
2632
+ a.cellHeight !== b.cellHeight ||
2488
2633
  a.gap !== b.gap ||
2489
2634
  a.mask !== b.mask ||
2635
+ JSON.stringify(a.cellSizePerReel) !== JSON.stringify(b.cellSizePerReel) ||
2636
+ JSON.stringify(a.colGap) !== JSON.stringify(b.colGap) ||
2637
+ JSON.stringify(a.rowGap) !== JSON.stringify(b.rowGap) ||
2490
2638
  JSON.stringify(a.rowsPerReel) !== JSON.stringify(b.rowsPerReel) ||
2491
2639
  (a.decoration?.padding ?? 0) !== (b.decoration?.padding ?? 0));
2492
2640
  }
@@ -2776,5 +2924,5 @@ class MultiplierAccumulator {
2776
2924
  }
2777
2925
  }
2778
2926
 
2779
- export { AnimatedSymbol, AnticipationController, BigWinOverlay, CascadeController, CountUpDisplay, DEFAULT_REEL_CONFIG, EASING_BY_NAME, FEATURES, FEATURE_KEYS, FEATURE_LIST, INTENSITY_SCALE, MultiplierAccumulator, PRESETS, PRESET_LIST, ReelGrid, ReelSpinController, SpinEngine, SymbolCell, TumbleController, createReelSystem, easingByName, effectiveRowsPerReel, mergeReelConfig, pickTier, resolveReelConfig, tierIndexAtValue, valueAt, waysCount };
2927
+ export { AnimatedSymbol, AnticipationController, BigWinOverlay, CascadeController, CountUpDisplay, DEFAULT_REEL_CONFIG, EASING_BY_NAME, FEATURES, FEATURE_KEYS, FEATURE_LIST, INTENSITY_SCALE, MultiplierAccumulator, PRESETS, PRESET_LIST, ReelGrid, ReelSpinController, SpinEngine, SymbolCell, TumbleController, cellPositionOf, createReelSystem, easingByName, effectiveRowsPerReel, mergeReelConfig, pickTier, resolveGeometry, resolveGridGeometry, resolveReelConfig, tierIndexAtValue, valueAt, waysCount };
2780
2928
  //# sourceMappingURL=slot.esm.js.map