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