Virtue-Slots/objects/Reel.js

123 lines
4.2 KiB
JavaScript

import { SYMBOLS } from './Symbol.js';
const SYMBOL_HEIGHT = 110;
const SYMBOL_WIDTH = 200;
const VISIBLE_COUNT = 3;
const REEL_HEIGHT = SYMBOL_HEIGHT * VISIBLE_COUNT;
const BUFFER = 2; // extra cells above/below visible area for smooth scroll
const POOL_SIZE = VISIBLE_COUNT + BUFFER * 2;
export class Reel {
constructor(scene, x, y) {
this.scene = scene;
this.x = x;
this.y = y;
this.w = SYMBOL_WIDTH;
this.h = REEL_HEIGHT;
// Virtual infinite strip: cycle through SYMBOLS array using modulo
this.strip = SYMBOLS; // reference, never mutated
this.scrollY = 0;
// Container with mask for clipping
this.container = scene.add.container(x, y);
const maskShape = scene.make.graphics({ add: false });
maskShape.fillRect(x, y, this.w, this.h);
this.container.setMask(maskShape.createGeometryMask());
// Small pool of Graphics + Sprite + Text triples (only as many as visible + buffer)
this.cells = [];
for (let i = 0; i < POOL_SIZE; i++) {
const gfx = scene.add.graphics();
const spr = scene.add.sprite(0, 0, 'symbols', 0).setOrigin(0.5, 0.5);
this.container.add(gfx);
this.container.add(spr);
this.cells.push({ gfx, spr });
}
this._draw();
}
// Returns the symbol showing in the center (result) slot
getCenter() {
const s = -this.scrollY;
const centerScrollY = s + SYMBOL_HEIGHT;
const idx = Math.floor(centerScrollY / SYMBOL_HEIGHT);
return this.strip[((idx % this.strip.length) + this.strip.length) % this.strip.length];
}
_draw() {
// Negate scrollY so increasing scrollY moves symbols downward (top-to-bottom direction)
const s = -this.scrollY;
const topCellIdx = Math.floor(s / SYMBOL_HEIGHT);
for (let i = 0; i < POOL_SIZE; i++) {
const virtualIdx = topCellIdx - BUFFER + i;
const symbolIdx = ((virtualIdx % this.strip.length) + this.strip.length) % this.strip.length;
const sym = this.strip[symbolIdx];
const cellY = virtualIdx * SYMBOL_HEIGHT - s;
const { gfx, spr } = this.cells[i];
gfx.clear();
gfx.fillStyle(sym.color, 1);
gfx.fillRoundedRect(2, cellY + 2, this.w - 4, SYMBOL_HEIGHT - 4, 8);
gfx.lineStyle(2, 0xffffff, 0.35);
gfx.strokeRoundedRect(2, cellY + 2, this.w - 4, SYMBOL_HEIGHT - 4, 8);
spr.setFrame(symbolIdx);
spr.setPosition(this.w / 2, cellY + SYMBOL_HEIGHT / 2);
}
}
// Spin to targetSymbol. Duration controls how long the fast phase lasts.
// onComplete fires when the reel finishes.
spin(targetSymbol, duration, onComplete) {
const scene = this.scene;
// Pre-calculate a target scrollY that:
// - lands targetSymbol in the center slot (scrollY = targetIdx * SYMBOL_HEIGHT - SYMBOL_HEIGHT)
// - is strictly greater than current scrollY by at least MIN_ADVANCE
const MIN_ADVANCE = SYMBOL_HEIGHT * 10; // enough for a satisfying visible spin
const stripHeight = this.strip.length * SYMBOL_HEIGHT;
const targetSymbolIdx = this.strip.findIndex(s => s.id === targetSymbol.id);
// With negated draw: center when -scrollY = targetSymbolIdx * SYMBOL_HEIGHT - SYMBOL_HEIGHT
// => scrollY = SYMBOL_HEIGHT - targetSymbolIdx * SYMBOL_HEIGHT
let targetScrollY = SYMBOL_HEIGHT - targetSymbolIdx * SYMBOL_HEIGHT;
// scrollY must DECREASE by at least MIN_ADVANCE (spin goes in negative direction)
while (targetScrollY >= this.scrollY - MIN_ADVANCE) {
targetScrollY -= stripHeight;
}
// Phase 1: fast linear scroll to ~2 symbols before the final landing
const fastTarget = targetScrollY + SYMBOL_HEIGHT * 2;
scene.tweens.add({
targets: this,
scrollY: fastTarget,
duration: duration,
ease: 'Linear',
onUpdate: () => this._draw(),
onComplete: () => {
// Phase 2: decelerate smoothly into the final position
scene.tweens.add({
targets: this,
scrollY: targetScrollY,
duration: 650,
ease: 'Cubic.easeOut',
onUpdate: () => this._draw(),
onComplete: () => {
this.scrollY = targetScrollY;
this._draw();
if (onComplete) onComplete();
}
});
}
});
}
getWidth() { return this.w; }
getHeight() { return this.h; }
}