import { SaveManager } from '../managers/SaveManager.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; // Resume main menu music if it was stopped by a battle const menuMusic = this.registry.get('music_main_menu'); if (menuMusic && !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 }).setOrigin(0.5); this.add.text(width / 2, 190, 'A Collectible Card Game', { fontSize: '24px', color: '#888888' }).setOrigin(0.5); // Level & Gold display this.add.text(width - 30, 30, `Level: ${save.level || 1}`, { fontSize: '24px', color: '#88aaff' }).setOrigin(1, 0); this.goldText = this.add.text(width - 30, 60, `Gold: ${save.gold}`, { fontSize: '24px', color: '#ffd700' }).setOrigin(1, 0); // Version this.add.text(20, height - 20, 'v1.0', { fontSize: '14px', color: '#444444' }).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(); } _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' }).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' }).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' }).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 }).setOrigin(0.5, 0); this.time.delayedCall(3000, () => { if (this._statusMsg) { this._statusMsg.destroy(); this._statusMsg = null; } }); } _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' }).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())); }); } }