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; } }