import { SaveManager } from '../managers/SaveManager.js'; import { TutorialManager } from '../managers/TutorialManager.js'; export class MainMenuScene extends Phaser.Scene { constructor() { super('MainMenuScene'); } preload() { this.load.video('menu_bg', 'assets/video/tyrants-edge-menu.mp4'); } create() { const save = this.registry.get('save'); const { width, height } = this.scale; // Start music on first visit; resume it on return from other scenes let menuMusic = this.registry.get('music_main_menu'); if (!menuMusic) { menuMusic = this.sound.add('music_main_menu', { loop: true, volume: 0.5 }); menuMusic.play(); this.registry.set('music_main_menu', menuMusic); } else if (!menuMusic.isPlaying) { menuMusic.play(); } // ── Video background ────────────────────────────────────────────────────── const video = this.add.video(width / 2, height / 2, 'menu_bg'); video.setMute(true); // setDisplaySize reads frame.realWidth which is null until the first frame // is decoded — defer sizing to the 'created' event. video.on('created', () => { if (video.scene) video.setDisplaySize(width, height); }); video.play(true); // true = loop // ── VHS / retro overlay effects ─────────────────────────────────────────── // 1. Dark tint so UI stays readable over whatever the video shows this.add.rectangle(width / 2, height / 2, width, height, 0x000000, 0.42); // 2. Scanlines — one dark horizontal pixel every 3 rows, baked into graphics const scanlines = this.add.graphics(); for (let y = 0; y < height; y += 3) { scanlines.fillStyle(0x000000, 0.22); scanlines.fillRect(0, y, width, 1); } // 3. Vignette — stepped gradient darkening from each edge inward const vig = this.add.graphics(); const steps = 12; const vigSize = 180; for (let i = 0; i < steps; i++) { const a = (1 - i / steps) * 0.65; const t = Math.round((vigSize / steps) * i); const s = Math.ceil(vigSize / steps); vig.fillStyle(0x000000, a); vig.fillRect(t, 0, s, height); // left vig.fillRect(width - t - s, 0, s, height); // right vig.fillRect(0, t, width, s); // top vig.fillRect(0, height - t - s, width, s); // bottom } // 4. Rolling VHS tracking bar — thin translucent band drifts down the screen const scanBar = this.add.rectangle(width / 2, -30, width, 60, 0xffffff, 0.045); this.tweens.add({ targets: scanBar, y: height + 30, duration: 7500, repeat: -1, ease: 'Linear' }); // 5. Occasional screen flicker — random brief brightness pulse const scheduleFlicker = () => { this.time.delayedCall(Phaser.Math.Between(2500, 7000), () => { if (!this.scene.isActive('MainMenuScene')) return; const flash = this.add.rectangle(width / 2, height / 2, width, height, 0xffffff, 0.06); this.time.delayedCall(60, () => { if (flash.scene) flash.destroy(); scheduleFlicker(); }); }); }; scheduleFlicker(); // Title this.add.text(width / 2, 120, "TYRANT'S EDGE", { fontSize: '72px', color: '#d4af37', stroke: '#000000', strokeThickness: 6, fontFamily: 'RaiderCrusader' }).setOrigin(0.5); this.add.text(width / 2, 190, 'A Collectible Card Game by Brian Fertig', { fontSize: '24px', color: '#888888', fontFamily: 'Audiowide' }).setOrigin(0.5); // Level & Gold display this.add.text(width - 30, 30, `Level: ${save.level || 1}`, { fontSize: '24px', color: '#88aaff', fontFamily: 'Audiowide' }).setOrigin(1, 0); this.goldText = this.add.text(width - 30, 60, `Gold: ${save.gold}`, { fontSize: '24px', color: '#ffd700', fontFamily: 'Audiowide' }).setOrigin(1, 0); // Version this.add.text(20, height - 20, 'v1.0', { fontSize: '14px', color: '#444444', fontFamily: 'Audiowide' }).setOrigin(0, 1); const buttons = [ { label: 'Campaign', scene: 'CampaignSelectScene' }, { label: 'Skirmish', scene: 'SkirmishSetupScene' }, { label: 'Deck Builder', scene: 'DeckBuilderScene' }, { label: 'Collection', scene: 'CollectionScene' }, { label: 'Store', scene: 'StoreScene' }, { label: 'Fusion Lab', scene: 'FusionScene' } ]; const startY = 310; const spacing = 90; buttons.forEach((btn, i) => { this._makeButton(width / 2, startY + i * spacing, btn.label, () => { this.scene.start(btn.scene, btn.data || {}); }); }); // Fullscreen toggle — utility button, visually distinct const fsY = startY + buttons.length * spacing + 20; this._makeFullscreenButton(width / 2, fsY); // Save / Restore — subtle utility buttons, upper-left this._makeSaveRestoreButtons(); // Tutorial overlay (if applicable) this._showTutorial(); } _showTutorial() { const save = this.registry.get('save'); if (!TutorialManager.isStageComplete(save, TutorialManager.STAGES.CAMPAIGN_INTRO)) { this._showCampaignIntroTutorial(save); } else if ( save.gold >= 400 && !TutorialManager.isStageComplete(save, TutorialManager.STAGES.STORE_INTRO) ) { this._showStoreIntroTutorial(save); } else if ( TutorialManager.isStageComplete(save, TutorialManager.STAGES.STORE_INTRO) && !TutorialManager.isStageComplete(save, TutorialManager.STAGES.FUSION_INTRO) && TutorialManager.hasFusibleCards(save) ) { this._showFusionIntroTutorial(save); } else if ( TutorialManager.isStageComplete(save, TutorialManager.STAGES.FUSION_INTRO) && !TutorialManager.isStageComplete(save, TutorialManager.STAGES.DECK_BUILDER_INTRO) ) { this._showDeckBuilderIntroTutorial(save); } else if ( TutorialManager.isStageComplete(save, TutorialManager.STAGES.DECK_BUILDER_INTRO) && !TutorialManager.isStageComplete(save, TutorialManager.STAGES.COLLECTION_INTRO) ) { this._showCollectionIntroTutorial(save); } } _showCampaignIntroTutorial(save) { const { width, height } = this.scale; const DEPTH = 1000; // Dark overlay — blocks all clicks beneath const overlay = this.add.rectangle(width / 2, height / 2, width, height, 0x000000, 0.75) .setDepth(DEPTH) .setInteractive(); // Duplicate Campaign button on top of overlay const btnX = width / 2; const btnY = 310; const btnBg = this.add.rectangle(btnX, btnY, 400, 65, 0x1a3a5c) .setDepth(DEPTH + 1) .setInteractive({ useHandCursor: true }) .setStrokeStyle(2, 0x4488ff); const btnText = this.add.text(btnX, btnY, 'Campaign', { fontSize: '28px', color: '#ffffff', fontFamily: 'Audiowide' }).setOrigin(0.5).setDepth(DEPTH + 1); btnBg.on('pointerover', () => { this.sound.play('sfx_menu_hover', { volume: 0.5 }); btnBg.setFillStyle(0x2a5a8c); }); btnBg.on('pointerout', () => btnBg.setFillStyle(0x1a3a5c)); // Bobbing arrow above button const arrow = this.add.text(btnX, btnY - 60, '\u25BC', { fontSize: '48px', color: '#ffd700' }).setOrigin(0.5).setDepth(DEPTH + 1); this.tweens.add({ targets: arrow, y: btnY - 45, duration: 600, yoyo: true, repeat: -1, ease: 'Sine.easeInOut' }); // Instructional text const infoText = this.add.text(btnX, btnY + 55, [ 'You are starting the game as Warlord Voss.', 'Your first mission: battle The Raiders!', 'Click Campaign to begin.' ].join('\n'), { fontSize: '22px', color: '#ffffff', fontFamily: 'Audiowide', align: 'center', lineSpacing: 8 }).setOrigin(0.5, 0).setDepth(DEPTH + 1); // Click — complete stage, clean up, navigate const tutorialElements = [overlay, btnBg, btnText, arrow, infoText]; btnBg.on('pointerdown', () => { this.sound.play('sfx_menu_select', { volume: 0.7 }); TutorialManager.completeStage(save, TutorialManager.STAGES.CAMPAIGN_INTRO); tutorialElements.forEach(el => el.destroy()); this.scene.start('CampaignSelectScene'); }); } _showStoreIntroTutorial(save) { const { width, height } = this.scale; const DEPTH = 1000; // Store button is index 4 in the button list (startY=310, spacing=90) const btnX = width / 2; const btnY = 310 + 4 * 90; // 670 // Dark overlay — blocks all clicks beneath const overlay = this.add.rectangle(width / 2, height / 2, width, height, 0x000000, 0.78) .setDepth(DEPTH) .setInteractive(); // Duplicate Store button on top of overlay const btnBg = this.add.rectangle(btnX, btnY, 400, 65, 0x1a3a5c) .setDepth(DEPTH + 1) .setInteractive({ useHandCursor: true }) .setStrokeStyle(2, 0x4488ff); const btnText = this.add.text(btnX, btnY, 'Store', { fontSize: '28px', color: '#ffffff', fontFamily: 'Audiowide' }).setOrigin(0.5).setDepth(DEPTH + 1); btnBg.on('pointerover', () => { this.sound.play('sfx_menu_hover', { volume: 0.5 }); btnBg.setFillStyle(0x2a5a8c); }); btnBg.on('pointerout', () => btnBg.setFillStyle(0x1a3a5c)); // Pulsing arrow above button const arrow = this.add.text(btnX, btnY - 60, '\u25BC', { fontSize: '48px', color: '#ffd700' }).setOrigin(0.5).setDepth(DEPTH + 1); this.tweens.add({ targets: arrow, y: btnY - 45, duration: 600, yoyo: true, repeat: -1, ease: 'Sine.easeInOut' }); // Instructional text const infoText = this.add.text(btnX, btnY + 55, [ 'Every battle — win or lose — earns you Gold.', 'Gold is spent in the Store to buy card packs', 'and strengthen your deck.', 'Click Store to spend your hard-earned Gold!' ].join('\n'), { fontSize: '22px', color: '#ffffff', fontFamily: 'Audiowide', align: 'center', lineSpacing: 8 }).setOrigin(0.5, 0).setDepth(DEPTH + 1); const tutorialElements = [overlay, btnBg, btnText, arrow, infoText]; btnBg.on('pointerdown', () => { this.sound.play('sfx_menu_select', { volume: 0.7 }); tutorialElements.forEach(el => el.destroy()); this.scene.start('StoreScene'); }); } _makeButton(x, y, label, callback) { const bg = this.add.rectangle(x, y, 400, 65, 0x1a3a5c) .setInteractive({ useHandCursor: true }) .setStrokeStyle(2, 0x4488ff); const text = this.add.text(x, y, label, { fontSize: '28px', color: '#ffffff', fontFamily: 'Audiowide' }).setOrigin(0.5); bg.on('pointerover', () => { this.sound.play('sfx_menu_hover', { volume: 0.5 }); bg.setFillStyle(0x2a5a8c); }); bg.on('pointerout', () => bg.setFillStyle(0x1a3a5c)); bg.on('pointerdown', () => { this.sound.play('sfx_menu_select', { volume: 0.7 }); callback(); }); return { bg, text }; } // ── Save file obfuscation ───────────────────────────────────────────────── // XOR-cipher + Base64. Not cryptographically secure, but the exported file // contains no readable JSON, making casual hand-editing impractical. // The key lives in client code so a determined reverse-engineer could decode // it — the goal is to deter accidental or casual cheating, not prevent it. _encodeData(obj) { const K = 'TE::v1::TyrantsEdge::SaveKey'; const s = JSON.stringify(obj); let r = ''; for (let i = 0; i < s.length; i++) { r += String.fromCharCode(s.charCodeAt(i) ^ K.charCodeAt(i % K.length)); } return btoa(r); } _decodeData(str) { // Try obfuscated format first, fall back to plain JSON for legacy files. try { const K = 'TE::v1::TyrantsEdge::SaveKey'; const s = atob(str.trim()); let r = ''; for (let i = 0; i < s.length; i++) { r += String.fromCharCode(s.charCodeAt(i) ^ K.charCodeAt(i % K.length)); } return JSON.parse(r); } catch { return JSON.parse(str); // plain JSON (old save files) } } _makeSaveRestoreButtons() { const BW = 250, BH = 34, x = 20 + BW / 2; // ── Save to Drive ── const saveBg = this.add.rectangle(x, 25, BW, BH, 0x000000, 0.55) .setInteractive({ useHandCursor: true }) .setStrokeStyle(1, 0x336633); this.add.text(x, 25, '💾 Save Game to Your Drive', { fontSize: '14px', color: '#669966', fontFamily: 'Audiowide' }).setOrigin(0.5); saveBg.on('pointerover', () => saveBg.setStrokeStyle(1, 0x55aa55)); saveBg.on('pointerout', () => saveBg.setStrokeStyle(1, 0x336633)); saveBg.on('pointerdown', () => { try { const data = this.registry.get('save'); const blob = new Blob([this._encodeData(data)], { type: 'application/octet-stream' }); const url = URL.createObjectURL(blob); const a = document.createElement('a'); a.href = url; a.download = 'tyrants-edge-save.te'; document.body.appendChild(a); a.click(); document.body.removeChild(a); URL.revokeObjectURL(url); this._showStatusMsg('Save file downloaded!', '#66bb66'); } catch (e) { this._showStatusMsg('Save failed.', '#bb4444'); } }); // ── Restore from File ── const restoreBg = this.add.rectangle(x, 65, BW, BH, 0x000000, 0.55) .setInteractive({ useHandCursor: true }) .setStrokeStyle(1, 0x334466); this.add.text(x, 65, '📂 Restore Game from File', { fontSize: '14px', color: '#6688aa', fontFamily: 'Audiowide' }).setOrigin(0.5); restoreBg.on('pointerover', () => restoreBg.setStrokeStyle(1, 0x5577bb)); restoreBg.on('pointerout', () => restoreBg.setStrokeStyle(1, 0x334466)); restoreBg.on('pointerdown', () => { const input = document.createElement('input'); input.type = 'file'; input.accept = '.te,.json,application/octet-stream,application/json'; input.style.display = 'none'; document.body.appendChild(input); input.onchange = (e) => { const file = e.target.files[0]; document.body.removeChild(input); if (!file) return; const reader = new FileReader(); reader.onload = (evt) => { try { const restored = this._decodeData(evt.target.result); // Basic sanity check if (typeof restored !== 'object' || !restored.collection) throw new Error(); SaveManager.save(restored); this.registry.set('save', restored); this._showStatusMsg('Restored! Reloading...', '#66bb66'); this.time.delayedCall(1200, () => this.scene.restart()); } catch { this._showStatusMsg('Invalid save file.', '#bb4444'); } }; reader.readAsText(file); }; input.click(); }); } _showStatusMsg(msg, color) { if (this._statusMsg) this._statusMsg.destroy(); this._statusMsg = this.add.text(145, 95, msg, { fontSize: '13px', color, fontFamily: 'Audiowide' }).setOrigin(0.5, 0); this.time.delayedCall(3000, () => { if (this._statusMsg) { this._statusMsg.destroy(); this._statusMsg = null; } }); } _showFusionIntroTutorial(save) { const { width, height } = this.scale; const DEPTH = 1000; // Fusion Lab is index 5 in the button list (startY=310, spacing=90) const btnX = width / 2; const btnY = 310 + 5 * 90; // 760 // Dark overlay — blocks all clicks beneath const overlay = this.add.rectangle(width / 2, height / 2, width, height, 0x000000, 0.78) .setDepth(DEPTH) .setInteractive(); // Duplicate Fusion Lab button on top of overlay const btnBg = this.add.rectangle(btnX, btnY, 400, 65, 0x1a3a5c) .setDepth(DEPTH + 1) .setInteractive({ useHandCursor: true }) .setStrokeStyle(2, 0x4488ff); const btnText = this.add.text(btnX, btnY, 'Fusion Lab', { fontSize: '28px', color: '#ffffff', fontFamily: 'Audiowide' }).setOrigin(0.5).setDepth(DEPTH + 1); btnBg.on('pointerover', () => { this.sound.play('sfx_menu_hover', { volume: 0.5 }); btnBg.setFillStyle(0x2a5a8c); }); btnBg.on('pointerout', () => btnBg.setFillStyle(0x1a3a5c)); // Pulsing arrow above button const arrow = this.add.text(btnX, btnY - 60, '\u25BC', { fontSize: '48px', color: '#ffd700' }).setOrigin(0.5).setDepth(DEPTH + 1); this.tweens.add({ targets: arrow, y: btnY - 45, duration: 600, yoyo: true, repeat: -1, ease: 'Sine.easeInOut' }); // Instructional text const infoText = this.add.text(btnX, btnY + 55, [ 'You have enough cards to fuse!', 'In the Fusion Lab, combine 3 copies of any card', 'to forge a random card of the next rarity in the same faction.', 'Click Fusion Lab to try it out!' ].join('\n'), { fontSize: '22px', color: '#ffffff', fontFamily: 'Audiowide', align: 'center', lineSpacing: 8 }).setOrigin(0.5, 0).setDepth(DEPTH + 1); const tutorialElements = [overlay, btnBg, btnText, arrow, infoText]; btnBg.on('pointerdown', () => { this.sound.play('sfx_menu_select', { volume: 0.7 }); tutorialElements.forEach(el => el.destroy()); this.scene.start('FusionScene'); }); } _showCollectionIntroTutorial(save) { this._showCollectionPhase1(); } _showCollectionPhase1() { const { width, height } = this.scale; const DEPTH = 1000; // Collection is index 3 (startY=310, spacing=90 → y=580) const btnX = width / 2; const btnY = 310 + 3 * 90; // 580 const btnHalfW = 200; // Full-screen overlay — blocks all clicks including the Collection button const overlay = this.add.rectangle(width / 2, height / 2, width, height, 0x000000, 0.78) .setDepth(DEPTH).setInteractive(); // Duplicate Collection button — visual only, NOT interactive const btnBg = this.add.rectangle(btnX, btnY, btnHalfW * 2, 65, 0x1a3a5c) .setDepth(DEPTH + 1).setStrokeStyle(2, 0x4488ff); const btnText = this.add.text(btnX, btnY, 'Collection', { fontSize: '28px', color: '#ffffff', fontFamily: 'Audiowide' }).setOrigin(0.5).setDepth(DEPTH + 1); // Arrow to the right of the button, pointing left at it (◄) const arrowX = btnX + btnHalfW + 70; // ~1230 const arrow = this.add.text(arrowX, btnY, '\u25C4', { fontSize: '42px', color: '#ffd700' }).setOrigin(0.5).setDepth(DEPTH + 1); this.tweens.add({ targets: arrow, x: arrowX - 14, duration: 600, yoyo: true, repeat: -1, ease: 'Sine.easeInOut' }); // Descriptive text to the right of the arrow const textX = arrowX + 180; // ~1410 const text = this.add.text(textX, btnY - 70, [ 'The Collection shows every card', 'you have ever acquired.', 'Browse by faction, rarity, or type', 'to review what you\'ve unlocked.' ].join('\n'), { fontSize: '22px', color: '#ffffff', fontFamily: 'Audiowide', align: 'center', lineSpacing: 8, wordWrap: { width: 460 } }).setOrigin(0.5, 0).setDepth(DEPTH + 1); // Continue button below text const contBg = this.add.rectangle(textX, btnY + 130, 200, 46, 0x1a3a5c) .setInteractive({ useHandCursor: true }) .setStrokeStyle(2, 0x4488ff) .setDepth(DEPTH + 1); const contTxt = this.add.text(textX, btnY + 130, 'Continue', { fontSize: '20px', color: '#ffffff', fontFamily: 'Audiowide' }).setOrigin(0.5).setDepth(DEPTH + 1); this.tweens.add({ targets: contBg, alpha: { from: 0.7, to: 1 }, duration: 800, yoyo: true, repeat: -1, ease: 'Sine.easeInOut' }); const els = [overlay, btnBg, btnText, arrow, text, contBg, contTxt]; contBg.on('pointerdown', () => { this.sound.play('sfx_menu_select', { volume: 0.7 }); els.forEach(el => { if (el.scene) el.destroy(); }); this._showCollectionPhase2(); }); } _showCollectionPhase2() { const { width, height } = this.scale; const DEPTH = 1000; // Save/Restore buttons: BW=250, x=20+125=145, y=25 and y=65 // Area bounds: left=20, right=270, top=8, bottom=82 const areaLeft = 20; const areaRight = 270; const areaBottom = 90; // slightly generous const areaCX = (areaLeft + areaRight) / 2; // 145 // Targeted dimming — leave top-left corner visible const dimTopRight = this.add.rectangle( (areaRight + width) / 2, areaBottom / 2, width - areaRight, areaBottom, 0x000000, 0.78 ).setDepth(DEPTH); const dimBelow = this.add.rectangle( width / 2, (areaBottom + height) / 2, width, height - areaBottom, 0x000000, 0.78 ).setDepth(DEPTH); // Arrow just below the save/restore area, pointing up const arrowY = areaBottom + 30; // 120 const arrow = this.add.text(areaCX, arrowY, '\u25B2', { fontSize: '36px', color: '#ffd700' }).setOrigin(0.5).setDepth(DEPTH + 1); this.tweens.add({ targets: arrow, y: arrowY - 12, duration: 600, yoyo: true, repeat: -1, ease: 'Sine.easeInOut' }); // Explanatory text below arrow const text = this.add.text(areaCX, arrowY + 50, [ 'Your game saves automatically in', 'your browser\'s local storage.', 'Use these buttons to save a backup', 'to your hard drive, or restore', 'a previously saved file.' ].join('\n'), { fontSize: '18px', color: '#ffffff', fontFamily: 'Audiowide', align: 'center', lineSpacing: 6, wordWrap: { width: 300 } }).setOrigin(0.5, 0).setDepth(DEPTH + 1); // Continue button — completes Stage 8 const contY = arrowY + 260; const contBg = this.add.rectangle(areaCX, contY, 200, 46, 0x1a3a5c) .setInteractive({ useHandCursor: true }) .setStrokeStyle(2, 0x4488ff) .setDepth(DEPTH + 1); const contTxt = this.add.text(areaCX, contY, 'Continue', { fontSize: '20px', color: '#ffffff', fontFamily: 'Audiowide' }).setOrigin(0.5).setDepth(DEPTH + 1); this.tweens.add({ targets: contBg, alpha: { from: 0.7, to: 1 }, duration: 800, yoyo: true, repeat: -1, ease: 'Sine.easeInOut' }); const els = [dimTopRight, dimBelow, arrow, text, contBg, contTxt]; contBg.on('pointerdown', () => { this.sound.play('sfx_menu_select', { volume: 0.7 }); const save = this.registry.get('save'); TutorialManager.completeStage(save, TutorialManager.STAGES.COLLECTION_INTRO); this.registry.set('save', save); els.forEach(el => { if (el.scene) el.destroy(); }); }); } _showDeckBuilderIntroTutorial(save) { const { width, height } = this.scale; const DEPTH = 1000; // Deck Builder is index 2 in the button list (startY=310, spacing=90) const btnX = width / 2; const btnY = 310 + 2 * 90; // 490 // Dark overlay — blocks all clicks beneath const overlay = this.add.rectangle(width / 2, height / 2, width, height, 0x000000, 0.78) .setDepth(DEPTH) .setInteractive(); // Duplicate Deck Builder button on top of overlay const btnBg = this.add.rectangle(btnX, btnY, 400, 65, 0x1a3a5c) .setDepth(DEPTH + 1) .setInteractive({ useHandCursor: true }) .setStrokeStyle(2, 0x4488ff); const btnText = this.add.text(btnX, btnY, 'Deck Builder', { fontSize: '28px', color: '#ffffff', fontFamily: 'Audiowide' }).setOrigin(0.5).setDepth(DEPTH + 1); btnBg.on('pointerover', () => { this.sound.play('sfx_menu_hover', { volume: 0.5 }); btnBg.setFillStyle(0x2a5a8c); }); btnBg.on('pointerout', () => btnBg.setFillStyle(0x1a3a5c)); // Pulsing arrow above button const arrow = this.add.text(btnX, btnY - 60, '\u25BC', { fontSize: '48px', color: '#ffd700' }).setOrigin(0.5).setDepth(DEPTH + 1); this.tweens.add({ targets: arrow, y: btnY - 45, duration: 600, yoyo: true, repeat: -1, ease: 'Sine.easeInOut' }); // Instructional text const infoText = this.add.text(btnX, btnY + 55, [ 'The Deck Builder lets you customize your deck.', 'Add or remove cards from your collection,', 'choose your commander, and save your changes.', 'Click Deck Builder to explore it!' ].join('\n'), { fontSize: '22px', color: '#ffffff', fontFamily: 'Audiowide', align: 'center', lineSpacing: 8 }).setOrigin(0.5, 0).setDepth(DEPTH + 1); const tutorialElements = [overlay, btnBg, btnText, arrow, infoText]; btnBg.on('pointerdown', () => { this.sound.play('sfx_menu_select', { volume: 0.7 }); tutorialElements.forEach(el => el.destroy()); this.scene.start('DeckBuilderScene'); }); } _makeFullscreenButton(x, y) { const getLabel = () => this.scale.isFullscreen ? '⛶ Exit Fullscreen' : '⛶ Toggle Fullscreen'; const bg = this.add.rectangle(x, y, 320, 50, 0x1a2a1a) .setInteractive({ useHandCursor: true }) .setStrokeStyle(2, 0x448844); const text = this.add.text(x, y, getLabel(), { fontSize: '20px', color: '#88cc88', fontFamily: 'Audiowide' }).setOrigin(0.5); bg.on('pointerover', () => { this.sound.play('sfx_menu_hover', { volume: 0.5 }); bg.setFillStyle(0x2a3f2a); }); bg.on('pointerout', () => bg.setFillStyle(0x1a2a1a)); bg.on('pointerdown', () => { this.sound.play('sfx_menu_select', { volume: 0.7 }); if (this.scale.isFullscreen) { this.scale.stopFullscreen(); } else { this.scale.startFullscreen(); } // Update label after a short delay to let the state change this.time.delayedCall(100, () => text.setText(getLabel())); }); } }