import { GameState } from '../state/GameState.js'; import { SlotMachine } from '../objects/SlotMachine.js'; import { WinAnimation } from '../objects/WinAnimation.js'; import { LossAnimation } from '../objects/LossAnimation.js'; import { VialDisplay } from '../objects/VialDisplay.js'; export default class GameScene extends Phaser.Scene { constructor() { super({ key: 'GameScene' }); } create() { // Background image — stretched to fill the canvas this.add.image(800, 450, 'bg-gates').setDisplaySize(1600, 900); // ── Left section: Stage video panel ────────────────────────────────────── // Centered at x=160 (within the 360px left of the slot machine), y=490 // (below the title gradient which ends at y=230). Size: 300x480. this._vidX = 160; this._vidY = 490; this._vidW = 300; this._vidH = 480; this._currentStage = 2; this._activeVideo = null; this._gameOver = false; const vidX = this._vidX; const vidY = this._vidY; const vidW = this._vidW; const vidH = this._vidH; const vidFrame = this.add.graphics(); vidFrame.fillStyle(0x0c0620, 0.65); vidFrame.fillRoundedRect(vidX - vidW / 2 - 8, vidY - vidH / 2 - 8, vidW + 16, vidH + 16, 14); vidFrame.lineStyle(1, 0xffd700, 0.3); vidFrame.strokeRoundedRect(vidX - vidW / 2 - 8, vidY - vidH / 2 - 8, vidW + 16, vidH + 16, 14); // Start with the stage-02 loop (game begins at stage 2) this._setVideo('stage-02', true); // Gradient backdrop behind title — opaque on left, fades to transparent const titleBg = this.add.graphics(); titleBg.fillGradientStyle(0x000000, 0x000000, 0x000000, 0x000000, 0.62, 0, 0.62, 0); titleBg.fillRect(0, 110, 700, 120); // Title above the machine — left aligned with padding this.add.text(40, 150, 'VIRTUE SLOTS', { fontSize: '42px', fontFamily: 'Georgia, serif', color: '#ffd700', stroke: '#5a3000', strokeThickness: 4, shadow: { offsetX: 2, offsetY: 2, color: '#000', blur: 6, fill: true } }).setOrigin(0, 0.5); this.add.text(40, 195, '✝ May Fortune Favor the Faithful ✝', { fontSize: '18px', fontFamily: 'Georgia, serif', color: '#c8a87e', alpha: 0.8 }).setOrigin(0, 0.5); // Slot machine — vertically centered to match The Reckoning panel (y=202–690) this.slotMachine = new SlotMachine(this, 710, 446); this.winAnim = new WinAnimation(); this.lossAnim = new LossAnimation(); // ── Right section: The Reckoning vials ────────────────────────────────── // Panel vertically centered in play area (y=100–790), with ~87px right margin const sectionBg = this.add.graphics(); sectionBg.fillStyle(0x0c0620, 0.65); sectionBg.fillRoundedRect(1085, 202, 428, 488, 14); sectionBg.lineStyle(1, 0xffd700, 0.3); sectionBg.strokeRoundedRect(1085, 202, 428, 488, 14); this.add.text(1299, 220, 'THE RECKONING', { fontSize: '13px', fontFamily: 'Georgia, serif', color: '#c8a87e', letterSpacing: 5, }).setOrigin(0.5, 0.5); this.add.text(1299, 238, 'First to $2,000 wins', { fontSize: '10px', fontFamily: 'Georgia, serif', color: '#4a5a6a', }).setOrigin(0.5, 0.5); this.add.text(1295, 443, 'VS', { fontSize: '20px', fontFamily: 'Georgia, serif', fontStyle: 'bold', color: '#2a1a4a', stroke: '#000000', strokeThickness: 3, }).setOrigin(0.5, 0.5); this.lordVial = new VialDisplay(this, 1193, 258, 'The Lord', 0xffd700, 0xc8a87e); this.sinVial = new VialDisplay(this, 1398, 258, 'Sin', 0xff4444, 0xff6666); // Keyboard: Space to spin this.input.keyboard.on('keydown-SPACE', () => this._triggerSpin()); // Listen for spin button events from UIScene via global event bus this.game.events.on('spin', () => this._triggerSpin(), this); // Victory: play the victory video and shatter the losing vial this.game.events.on('vial-winner', ({ winner }) => { this._gameOver = true; const lordWins = winner.toLowerCase().includes('lord'); const key = lordWins ? 'lord-victory' : 'sin-victory'; this._setVideo(key, false); // Shatter the losing vial after a brief dramatic pause this.time.delayedCall(550, () => { if (lordWins) { this.sinVial.shatterEvil(); } else { this.lordVial.shatterHoly(); } }); }, this); } _triggerSpin() { if (GameState.spinning) return; if (GameState.playerFunds < GameState.spinCost) { this.game.events.emit('insufficient-funds'); return; } GameState.playerFunds -= GameState.spinCost; GameState.spinning = true; this.game.events.emit('funds-updated'); this.slotMachine.spin((result) => this._handleResult(result)); } _handleResult({ win, symbols, payout }) { if (win) { const playerGain = Math.round(payout * 0.6); const lordGain = payout - playerGain; this.game.events.emit('win', { playerGain, lordGain, symbol: symbols[0] }); // Resolve UI box positions from UIScene const uiScene = this.scene.get('UIScene'); const playerBox = uiScene ? uiScene.getPlayerBoxCenter() : { x: 267, y: 60 }; const lordBox = uiScene ? uiScene.getLordBoxCenter() : { x: 800, y: 60 }; this.winAnim.play( this, this.slotMachine.getCenterX(), this.slotMachine.getCenterY(), playerBox, lordBox, symbols[0], () => { GameState.playerFunds += playerGain; GameState.lordFunds += lordGain; this.game.events.emit('funds-updated'); this.lordVial.animateUpdate(GameState.lordFunds, lordBox.x, 115, () => { this._handleStageChange(this._computeStage()); GameState.spinning = false; this.game.events.emit('spin-complete'); }); } ); } else { this.game.events.emit('loss', { sinAdded: GameState.spinCost }); const uiScene = this.scene.get('UIScene'); const sinBox = uiScene ? uiScene.getSinBoxCenter() : { x: 1333, y: 60 }; this.lossAnim.play( uiScene || this, this.slotMachine.getCenterX(), this.slotMachine.getCenterY(), sinBox, () => { GameState.sinTotal += GameState.spinCost; this.game.events.emit('funds-updated'); this.sinVial.animateUpdate(GameState.sinTotal, sinBox.x, 115, () => { this._handleStageChange(this._computeStage()); GameState.spinning = false; this.game.events.emit('spin-complete'); }); } ); } } // ── Stage video helpers ─────────────────────────────────────────────────── /** Compute which stage the Reckoning is currently in based on lord vs sin. */ _computeStage() { const diff = GameState.lordFunds - GameState.sinTotal; if (diff >= 200) return 1; if (diff > -200) return 2; if (diff > -350) return 3; if (diff > -500) return 4; if (diff > -650) return 5; return 6; } /** * Switch the panel video. * loop=true → loops indefinitely (no onComplete needed). * loop=false → plays once; calls onComplete when finished (if provided). */ _setVideo(key, loop, onComplete) { if (this._activeVideo) { this._activeVideo.stop(); this._activeVideo.destroy(); this._activeVideo = null; } const vid = this.add.video(this._vidX, this._vidY, key); //vid.setDisplaySize(this._vidW, this._vidH); this._activeVideo = vid; vid.play(loop); if (!loop && onComplete) { vid.once('complete', () => { if (this._activeVideo === vid) onComplete(); }); } } /** * Play a sequence of one-shot videos in order, then call onComplete. * keys = array of video keys to play in sequence. */ _playSequence(keys, onComplete) { if (keys.length === 0) { if (onComplete) onComplete(); return; } const [first, ...rest] = keys; this._setVideo(first, false, () => this._playSequence(rest, onComplete)); } /** * Called after each spin resolves. Calculates needed transitions and * plays them in sequence before resuming the looping stage video. */ _handleStageChange(newStage) { if (this._gameOver) return; if (newStage === this._currentStage) return; const oldStage = this._currentStage; this._currentStage = newStage; const pad = n => String(n).padStart(2, '0'); const direction = newStage > oldStage ? 1 : -1; const transitions = []; for (let s = oldStage; s !== newStage; s += direction) { transitions.push(`stage-${pad(s)}-${pad(s + direction)}`); } this._playSequence(transitions, () => { if (!this._gameOver) this._setVideo(`stage-${pad(newStage)}`, true); }); } }