import { GameState } from '../state/GameState.js'; const TOP_BAR_HEIGHT = 100; const BOTTOM_BAR_HEIGHT = 110; const BOTTOM_BAR_Y = 900 - BOTTOM_BAR_HEIGHT; // Box centers (x) for the three top fund displays const BOX_WIDTH = 1600 / 3; const PLAYER_BOX_X = BOX_WIDTH * 0 + BOX_WIDTH / 2; const LORD_BOX_X = BOX_WIDTH * 1 + BOX_WIDTH / 2; const SIN_BOX_X = BOX_WIDTH * 2 + BOX_WIDTH / 2; const BOX_CENTER_Y = TOP_BAR_HEIGHT / 2; // Center x for bottom-bar messages; resting y for main line and sub line const MSG_X = 700; const MSG_Y = BOTTOM_BAR_Y + 38; // 828 — main message line const SUB_Y = BOTTOM_BAR_Y + 78; // 868 — sub-message line const pick = arr => arr[Phaser.Math.Between(0, arr.length - 1)]; // ── Message pools ───────────────────────────────────────────────────────────── const LOSS_MESSAGES = [ 'Thou Hath Sinned.', 'The Darkness Consumes Thee.', 'Sin Claims Its Tithe.', 'The Devil Takes His Due.', 'Wretched Soul!', 'Thy Coin Belongs to Evil.', "Hell's Coffers Grow Richer.", 'Condemned by Fortune.', 'Behold Thy Folly!', 'The Serpent Strikes Again!', 'Damnation Draws Near.', 'Thy Faith Was Wanting.', 'The Wicked Path Claims Thee.', 'Evil Rejoices This Hour!', 'Another Soul Slips Away.', 'The Pit Calls Thy Name.', 'Darkness Prevails.', "Sin's Ledger Grows.", 'The Fallen Laugh at Thee.', 'Thy Virtue Wavers.', 'The Shadow Feeds.', "Perdition's Price Is Paid.", 'The Infernal Reels Mock Thee.', 'Evil Creeps Ever Closer.', 'The Unholy Slots Claim Thee.', 'Darkness Demands Its Toll.', 'The Abyss Grows Richer.', 'Thy Coins Serve the Beast.', 'Sin Savors Thy Defeat.', 'The Hellfire Burns Brighter.', ]; const LOSS_SUBS = [ 'Redeem Yourself!', 'Pray Harder!', 'Repent and Spin Again.', 'Thy Soul Hangs in the Balance.', 'Only Grace Can Save Thee Now.', 'Kneel Before the Righteous Reels.', 'The Lord Awaits Thy Return.', 'Seek Salvation in the Next Spin.', 'Sin Grows Bold — Shall Ye Falter?', 'Heaven Watches and Weeps.', 'Rise, Faithful Servant!', 'Cast Out the Darkness!', 'Penance Must Be Paid.', 'Confess and Spin Anew.', 'Thy Redemption Awaits.', 'Do Not Yield to Evil!', "The Lord's Mercy Is Boundless.", 'Turn from the Wicked Path!', 'Fight Back with Faith!', 'Let the Light Guide Thy Hand.', 'A Holy Victory Is Within Reach.', 'Fortify Thy Spirit!', 'The Divine Is Patient with Thee.', 'Evil Shall Not Have the Last Word.', 'Courage, Faithful One!', 'One More Spin for Glory.', 'Grace Favors the Persistent.', 'The Pious Press On.', 'Thy Virtue Must Endure.', 'Arise and Challenge the Darkness!', ]; const WIN_MESSAGES = [ '✝ Blessed Are the Righteous! ✝', 'The Lord Smiles Upon Thee!', 'Holy Fortune Shines Bright!', "Heaven's Reward Is Thine!", 'Grace Has Found Thee!', 'Divine Providence Strikes!', 'The Faithful Are Rewarded!', 'Thy Virtue Bears Fruit!', "God's Glory Fills Thy Coffers!", 'Righteousness Prevails!', 'The Angels Rejoice!', 'Sacred Fortune Blesses Thee!', 'The Holy Reels Align!', 'Light Conquers the Darkness!', 'Heaven Opens Its Gates!', "The Lord's Hand Guides Thee!", 'Faith Moves Mountains and Reels!', 'Hallelujah — A Holy Match!', 'Thy Piety Is Rewarded!', 'The Divine Plan Unfolds!', 'Blessed Be This Spin!', 'The Righteous Shall Inherit!', 'Favor of the Almighty!', 'A Miracle Upon the Reels!', "Heaven's Treasury Opens!", 'Sin Retreats in Shame!', 'The Virtuous Triumph!', 'Glory to the Highest!', 'The Light Pierces the Darkness!', "God's Grace Rewards Thee!", ]; const WIN_SUBS = [ 'Press On, Faithful One!', 'The Lord Provides — Spin Again!', 'Grace Continues to Flow.', 'Heaven Cheers Thy Victory!', 'Righteousness Is Its Own Reward.', 'The Holy Spirit Guides Thy Hand.', 'May the Divine Light Shine On.', 'Sin Cowers Before Thy Faith.', 'The Righteous Walk in Fortune.', 'Let Virtue Lead Every Spin.', 'The Angels Record Thy Victory.', 'Keep the Faith — Keep Spinning!', 'Thy Pious Heart Is Noticed.', 'The Lord Multiplies Thy Blessings.', 'Walk Boldly in the Light.', 'Evil Cannot Touch the Faithful.', 'Another Spin for His Glory!', 'The Righteous Path Pays Off.', "Heaven's Ledger Grows in Thy Favor.", 'Consecrated Luck Is Upon Thee.', 'The Devout Are Never Forgotten.', 'Thy Coin Returns Manifold.', 'Spin Again — Heaven Is Watching.', "The Lord's Grace Has No Limit.", 'Persist and Multiply in Faith!', 'Sin Is Losing Ground.', 'The Holy Reels Await Once More.', 'Blessed Is the Hand That Spins.', 'Fortune Favors the Faithful.', 'The Kingdom Grows with Each Spin.', ]; // ───────────────────────────────────────────────────────────────────────────── export default class UIScene extends Phaser.Scene { constructor() { super({ key: 'UIScene' }); } create() { this._spinDisabled = false; this._gameOver = false; this._wordObjects = []; this._buildTopBar(); this._buildBottomBar(); this._buildSpinButton(); this._bindEvents(); this._updateFundDisplays(); } _buildTopBar() { const g = this.add.graphics(); // Background g.fillStyle(0x12082a, 1); g.fillRect(0, 0, 1600, TOP_BAR_HEIGHT); g.lineStyle(2, 0xffd700, 0.8); g.strokeRect(0, 0, 1600, TOP_BAR_HEIGHT); // Dividers g.lineStyle(1, 0xffd700, 0.3); g.beginPath(); g.moveTo(BOX_WIDTH, 8); g.lineTo(BOX_WIDTH, TOP_BAR_HEIGHT - 8); g.strokePath(); g.beginPath(); g.moveTo(BOX_WIDTH * 2, 8); g.lineTo(BOX_WIDTH * 2, TOP_BAR_HEIGHT - 8); g.strokePath(); // Box labels const labelStyle = { fontSize: '13px', fontFamily: 'Georgia, serif', color: '#c8a87e', alpha: 0.8 }; this.add.text(PLAYER_BOX_X, 14, 'YOUR FUNDS', labelStyle).setOrigin(0.5, 0); this.add.text(LORD_BOX_X, 14, 'THE LORD', labelStyle).setOrigin(0.5, 0); this.add.text(SIN_BOX_X, 14, 'SIN', labelStyle).setOrigin(0.5, 0); // Fund value texts const valueStyle = { fontSize: '28px', fontFamily: 'Georgia, serif', color: '#ffd700', stroke: '#2a0a4e', strokeThickness: 3 }; this.playerText = this.add.text(PLAYER_BOX_X, 55, '$1000', valueStyle).setOrigin(0.5, 0.5); this.lordText = this.add.text(LORD_BOX_X, 55, '$0', valueStyle).setOrigin(0.5, 0.5); this.sinText = this.add.text(SIN_BOX_X, 55, '$0', { ...valueStyle, color: '#ff4444' }).setOrigin(0.5, 0.5); } _buildBottomBar() { const g = this.add.graphics(); g.fillStyle(0x12082a, 1); g.fillRect(0, BOTTOM_BAR_Y, 1600, BOTTOM_BAR_HEIGHT); g.lineStyle(2, 0xffd700, 0.8); g.strokeRect(0, BOTTOM_BAR_Y, 1600, BOTTOM_BAR_HEIGHT); // Static text used for initial state, game-over, and insufficient-funds messages this.messageText = this.add.text(MSG_X, MSG_Y, 'Press SPIN or SPACE to test your Righteousness against Temptation', { fontSize: '26px', fontFamily: 'Georgia, serif', color: '#e8d8b0', align: 'center', wordWrap: { width: 1100 } }).setOrigin(0.5, 0.5); // Sub-message static text (hidden by default; used for special messages) this.redeemText = this.add.text(MSG_X, SUB_Y, '$50 per spin. Tithe 40% of winnings to THE LORD. Losses go towards your SIN. Win if THE LORD reaches $2000. Lose if SIN reaches $2000 or you run out of money.', { fontSize: '18px', fontFamily: 'Georgia, serif', color: '#ff9944', align: 'center' }).setOrigin(0.5, 0.5); } _buildSpinButton() { const btnX = 1420; const btnY = BOTTOM_BAR_Y + BOTTOM_BAR_HEIGHT / 2; const btnW = 140; const btnH = 60; this.spinBtnGfx = this.add.graphics(); this._drawSpinBtn(false); this.spinBtnHitArea = this.add.zone(btnX, btnY, btnW, btnH) .setInteractive({ useHandCursor: true }); this.spinBtnLabel = this.add.text(btnX, btnY, 'SPIN', { fontSize: '26px', fontFamily: 'Georgia, serif', color: '#1a0a2e', fontStyle: 'bold' }).setOrigin(0.5, 0.5); this.spinBtnHitArea.on('pointerdown', () => { this.game.events.emit('spin'); }); this.spinBtnHitArea.on('pointerover', () => { if (!this._spinDisabled) this._drawSpinBtn(true); }); this.spinBtnHitArea.on('pointerout', () => { if (!this._spinDisabled) this._drawSpinBtn(false); }); this._btnX = btnX; this._btnY = btnY; this._btnW = btnW; this._btnH = btnH; } _drawSpinBtn(hover) { const btnX = 1420; const btnY = BOTTOM_BAR_Y + BOTTOM_BAR_HEIGHT / 2; const btnW = 140; const btnH = 60; this.spinBtnGfx.clear(); if (this._spinDisabled) { this.spinBtnGfx.fillStyle(0x3a3a3a, 1); this.spinBtnGfx.fillRoundedRect(btnX - btnW / 2, btnY - btnH / 2, btnW, btnH, 12); this.spinBtnGfx.lineStyle(3, 0x555555, 1); this.spinBtnGfx.strokeRoundedRect(btnX - btnW / 2, btnY - btnH / 2, btnW, btnH, 12); } else { this.spinBtnGfx.fillStyle(hover ? 0xffe066 : 0xffd700, 1); this.spinBtnGfx.fillRoundedRect(btnX - btnW / 2, btnY - btnH / 2, btnW, btnH, 12); this.spinBtnGfx.lineStyle(3, hover ? 0xffa500 : 0xc8a000, 1); this.spinBtnGfx.strokeRoundedRect(btnX - btnW / 2, btnY - btnH / 2, btnW, btnH, 12); } } _setSpinDisabled(disabled) { this._spinDisabled = disabled; this._drawSpinBtn(false); this.spinBtnLabel.setColor(disabled ? '#555555' : '#1a0a2e'); } _bindEvents() { this.game.events.on('win', () => { this._updateFundDisplays(); this._showWinMessage(pick(WIN_MESSAGES), pick(WIN_SUBS)); }, this); this.game.events.on('loss', () => { this._updateFundDisplays(); this._showLossMessage(pick(LOSS_MESSAGES), pick(LOSS_SUBS)); }, this); this.game.events.on('spinning-started', () => { this._setSpinDisabled(true); }, this); this.game.events.on('spin-complete', () => { if (!this._gameOver) this._setSpinDisabled(false); }, this); this.game.events.on('funds-updated', () => { this._updateFundDisplays(); }, this); this.game.events.on('insufficient-funds', () => { this._resetMessageTexts(); this.messageText.setText('Insufficient funds to spin! You have been consumed by Sin.'); this.messageText.setColor('#ff4444'); }, this); this.game.events.on('vial-winner', ({ winner }) => { this._gameOver = true; this._setSpinDisabled(true); this._resetMessageTexts(); const isLord = winner.toLowerCase().includes('lord'); this.messageText.setText( isLord ? '✝ The Lord Has Triumphed! ✝\nHis cup runneth over — glory be!' : '☠ Sin Has Prevailed! ☠\nYou have been consumed by darkness.' ); this.messageText.setColor(isLord ? '#ffd700' : '#ff4444'); this.redeemText.setAlpha(0); }, this); } // ── Animated message display ────────────────────────────────────────────── /** Kill and destroy every per-word text object from the last animation. */ _clearWordObjects() { this._wordObjects.forEach(t => { this.tweens.killTweensOf(t); if (t.active) t.destroy(); }); this._wordObjects = []; } /** * Spawn a row of word texts that individually animate in from a large starting size. * * @param {string[]} words - Words to render. * @param {number} centerX - Horizontal center of the row. * @param {number} finalY - Vertical center of the row at rest. * @param {number} fontSize - Final font size in px. * @param {string} color - CSS colour string. * @param {object} styleExtras - Extra Phaser text style properties. * @param {number} fromScale - Starting scale (>1 = starts huge, <1 = starts tiny). * @param {number} fromY - Y offset from finalY at start (negative = above). * @param {number} msPerWord - Delay increment (ms) between successive words. * @param {number} baseDelay - Global delay (ms) before the first word fires. * @returns {Phaser.GameObjects.Text[]} */ _spawnRowOfWords(words, centerX, finalY, fontSize, color, styleExtras, fromScale, fromY, msPerWord, baseDelay) { const GAP = Math.round(fontSize * 0.38); // Create off-screen first so .width is readable for layout. // setOrigin(0.5, 0.5) so that setPosition targets the word's centre, // matching the centre-based layout math below and ensuring the scale // animation shrinks/grows from the middle of each word. const objs = words.map(w => this.add.text(-4000, -4000, w, { fontSize: `${fontSize}px`, fontFamily: 'Georgia, serif', ...styleExtras, }).setOrigin(0.5, 0.5) ); const totalW = objs.reduce((s, t) => s + t.width, 0) + GAP * Math.max(0, words.length - 1); let x = centerX - totalW / 2; objs.forEach((t, i) => { const fx = x + t.width / 2; x += t.width + GAP; t.setColor(color) .setPosition(fx, finalY + fromY) .setScale(fromScale) .setAlpha(0); this.tweens.add({ targets: t, scale: 1, alpha: 1, y: finalY, duration: 280, delay: baseDelay + msPerWord * i, ease: 'Back.easeOut', easeParams: [2.5], }); }); return objs; } /** Restore the bottom bar to a clean neutral state (clears word objects). */ _resetMessageTexts() { this._clearWordObjects(); this.tweens.killTweensOf(this.messageText); this.tweens.killTweensOf(this.redeemText); this.messageText.setPosition(MSG_X, MSG_Y).setAlpha(1).setScale(1); this.redeemText.setPosition(MSG_X, SUB_Y).setAlpha(0).setScale(1); } /** * Infernal Slam — each word of the main line crashes down from above; * sub-line words rise up from below, then pulse. */ _showLossMessage(main, sub) { this._clearWordObjects(); this.tweens.killTweensOf(this.messageText); this.tweens.killTweensOf(this.redeemText); this.messageText.setText('').setAlpha(0); this.redeemText.setAlpha(0); // Dark red flash across the bar const flash = this.add.rectangle(800, MSG_Y, 1600, BOTTOM_BAR_HEIGHT, 0x990000).setAlpha(0.55); this.tweens.add({ targets: flash, alpha: 0, duration: 350, onComplete: () => flash.destroy() }); const mainWords = main.split(/\s+/).filter(Boolean); const subWords = sub.split(/\s+/).filter(Boolean); const msMain = 82; const msSub = 75; const subStart = mainWords.length * msMain + 180; // Main: each word crashes down from above, starting at 4.5× scale const mainObjs = this._spawnRowOfWords( mainWords, MSG_X, MSG_Y, 34, '#ff3333', {}, 4.5, -42, msMain, 0 ); // Sub: each word rises from below, starting at 3× scale const subObjs = this._spawnRowOfWords( subWords, MSG_X, SUB_Y, 22, '#ff9944', {}, 3.0, 28, msSub, subStart ); this._wordObjects = [...mainObjs, ...subObjs]; // After sub finishes, pulse all sub words indefinitely const pulseAt = subStart + subWords.length * msSub + 300; this.time.delayedCall(pulseAt, () => { subObjs.forEach(t => { if (!t.active) return; this.tweens.add({ targets: t, alpha: { from: 1, to: 0.3 }, duration: 850, yoyo: true, repeat: -1, }); }); }); } /** * Divine Descent — each word of the main line drops from above in gold; * sub-line words bloom up from a tiny point. */ _showWinMessage(main, sub) { this._clearWordObjects(); this.tweens.killTweensOf(this.messageText); this.tweens.killTweensOf(this.redeemText); this.messageText.setText('').setAlpha(0); this.redeemText.setAlpha(0); // Golden flash across the bar const flash = this.add.rectangle(800, MSG_Y, 1600, BOTTOM_BAR_HEIGHT, 0xffd700).setAlpha(0.4); this.tweens.add({ targets: flash, alpha: 0, duration: 500, onComplete: () => flash.destroy() }); // Floating sparkles scatter across the bar for (let i = 0; i < 7; i++) { const sp = this.add.text( Phaser.Math.Between(100, 1300), MSG_Y + Phaser.Math.Between(-20, 20), ['✦', '✝', '★', '✧'][Math.floor(Math.random() * 4)], { fontSize: `${Phaser.Math.Between(12, 26)}px`, color: '#ffd700' } ).setOrigin(0.5).setAlpha(0); this.tweens.add({ targets: sp, y: sp.y - Phaser.Math.Between(25, 55), alpha: { from: 1, to: 0 }, scale: 1.4, delay: i * 55, duration: 650, onComplete: () => sp.destroy(), }); } const mainWords = main.split(/\s+/).filter(Boolean); const subWords = sub.split(/\s+/).filter(Boolean); const msMain = 100; const msSub = 88; const subStart = mainWords.length * msMain + 200; // Main: each word descends from above, starting at 3.8× scale, with golden stroke const mainObjs = this._spawnRowOfWords( mainWords, MSG_X, MSG_Y, 34, '#ffd700', { stroke: '#5a3000', strokeThickness: 2 }, 3.8, -50, msMain, 0 ); // Sub: each word blooms up from a tiny point (fromScale 0.2 → 1) const subObjs = this._spawnRowOfWords( subWords, MSG_X, SUB_Y, 22, '#c8a87e', {}, 0.2, 0, msSub, subStart ); this._wordObjects = [...mainObjs, ...subObjs]; } // ───────────────────────────────────────────────────────────────────────── _updateFundDisplays() { this.playerText.setText(`$${GameState.playerFunds}`); this.lordText.setText(`$${GameState.lordFunds}`); this.sinText.setText(`$${GameState.sinTotal}`); // Flash update on change [this.playerText, this.lordText, this.sinText].forEach(t => { this.tweens.add({ targets: t, scaleX: { from: 1.15, to: 1 }, scaleY: { from: 1.15, to: 1 }, duration: 200, ease: 'Bounce.easeOut' }); }); } // Called by GameScene to position animations toward the right box getPlayerBoxCenter() { return { x: PLAYER_BOX_X, y: BOX_CENTER_Y }; } getLordBoxCenter() { return { x: LORD_BOX_X, y: BOX_CENTER_Y }; } getSinBoxCenter() { return { x: SIN_BOX_X, y: BOX_CENTER_Y }; } }