/* global Phaser, NetworkManager, StorageManager, getRoomCodeFromURL */ class MainMenuScene extends Phaser.Scene { constructor() { super({ key: 'MainMenuScene' }); } preload() { if (!this.textures.exists('main_menu_bg')) { this.load.image('main_menu_bg', 'assets/images/ui/main_menu.png'); } if (!this.textures.exists('main_menu_logo')) { this.load.image('main_menu_logo', 'assets/images/ui/logo.png'); } if (!this.textures.exists('main_menu_sublogo')) { this.load.image('main_menu_sublogo', 'assets/images/ui/sub-logo.png'); } if (!this.cache.audio.exists('main_menu_music')) { this.load.audio('main_menu_music', 'assets/audio/music/main_menu.mp3'); } } create() { NetworkManager.connect(); // Background — displayed immediately this.add.image(960, 540, 'main_menu_bg').setDisplaySize(1920, 1080); // Start music (persists across scenes via global sound manager) this._menuMusic = this.sound.add('main_menu_music', { volume: 0.3, loop: true }); this._menuMusic.setMute(localStorage.getItem('ipuzzle_musicMuted') === 'true'); this._menuMusic.play(); // Build DOM buttons (hidden initially) this._buildDomUI(); // Run animated intro sequence this._animateIntro(); // Auto-open join dialog if URL has ?room= parameter const urlRoom = getRoomCodeFromURL(); if (urlRoom) { this.time.delayedCall(1600, () => this._showJoinDialog(urlRoom)); } } // ─── Animated Intro ───────────────────────────────────────────────── _animateIntro() { // Logo — starts off-screen above const logo = this.add.image(960, -250, 'main_menu_logo').setScale(0.6).setAlpha(0); // Sub-logo — starts off-screen below const sublogo = this.add.image(960, 1300, 'main_menu_sublogo').setScale(0.45).setAlpha(0); // Logo drops in at 300ms this.tweens.add({ targets: logo, y: 240, alpha: 1, duration: 1200, delay: 300, ease: 'Cubic.easeOut', onComplete: () => { this.tweens.add({ targets: logo, y: 250, duration: 300, ease: 'Sine.easeInOut', }); }, }); // Sub-logo rises in at 900ms (halfway through logo anim) this.tweens.add({ targets: sublogo, y: 460, alpha: 1, duration: 1000, delay: 900, ease: 'Cubic.easeOut', onComplete: () => { this.tweens.add({ targets: sublogo, y: 450, duration: 250, ease: 'Sine.easeInOut', }); }, }); // Buttons fade in at 1400ms (halfway through sub-logo anim) this.time.delayedCall(1400, () => { if (this._continueBtn) this._continueBtn.style.opacity = this._hasSavedPuzzle ? '1' : '0.4'; if (this._newBtn) this._newBtn.style.opacity = '1'; if (this._joinBtn) this._joinBtn.style.opacity = '1'; if (this._fullscreenBtn) this._fullscreenBtn.style.opacity = '1'; }); // Idle breathing pulse — starts after intro finishes, repeats every 10s this.time.delayedCall(3000, () => { this._breathePulse(logo, 0.6, sublogo, 0.45); }); } _breathePulse(logo, logoBase, sublogo, sublogoBase) { this.tweens.add({ targets: logo, scaleX: logoBase * 1.06, scaleY: logoBase * 1.06, duration: 600, ease: 'Sine.easeInOut', yoyo: true, onComplete: () => { this.tweens.add({ targets: sublogo, scaleX: sublogoBase * 1.06, scaleY: sublogoBase * 1.06, duration: 600, ease: 'Sine.easeInOut', yoyo: true, onComplete: () => { this.time.delayedCall(10000, () => { this._breathePulse(logo, logoBase, sublogo, sublogoBase); }); }, }); }, }); } // ─── DOM UI ────────────────────────────────────────────────────────── _buildDomUI() { this._uiLayer = document.createElement('div'); Object.assign(this._uiLayer.style, { position: 'fixed', top: '50%', left: '50%', transform: 'translate(-50%, -50%)', width: '100vw', height: '56.25vw', maxHeight: '100vh', maxWidth: '177.78vh', pointerEvents: 'none', zIndex: '10', }); document.body.appendChild(this._uiLayer); // Check for saved puzzle const saved = StorageManager.loadCurrent(); this._hasSavedPuzzle = saved && !saved.completed; // Continue Puzzle button this._continueBtn = this._makeDomBtn('Continue Puzzle', '#2e7d32', '#43a047', '#66bb6a', () => { const current = StorageManager.loadCurrent(); if (current && !current.completed) { this.scene.start('PuzzleScene', { roomCode: current.roomCode, _restore: true, }); } }); Object.assign(this._continueBtn.style, { top: '630px', left: '50%', transform: 'translateX(-50%)', width: '340px', opacity: '0', transition: 'opacity 0.6s ease', }); if (!this._hasSavedPuzzle) { this._continueBtn.disabled = true; this._continueBtn.style.cursor = 'not-allowed'; this._continueBtn.style.pointerEvents = 'auto'; } this._uiLayer.appendChild(this._continueBtn); // New Puzzle button this._newBtn = this._makeDomBtn('New Puzzle', '#1565c0', '#1e88e5', '#64b5f6', () => { this.scene.start('NewPuzzleScene'); }); Object.assign(this._newBtn.style, { top: '550px', left: '50%', transform: 'translateX(-50%)', width: '340px', opacity: '0', transition: 'opacity 0.6s ease', }); this._uiLayer.appendChild(this._newBtn); // Join Puzzle button this._joinBtn = this._makeDomBtn('Join Puzzle', '#e65100', '#f57c00', '#ffb74d', () => { this._showJoinDialog(); }); Object.assign(this._joinBtn.style, { top: '710px', left: '50%', transform: 'translateX(-50%)', width: '340px', opacity: '0', transition: 'opacity 0.6s ease', }); this._uiLayer.appendChild(this._joinBtn); // Enter Fullscreen button const isFullscreen = !!document.fullscreenElement; this._fullscreenBtn = this._makeDomBtn( isFullscreen ? 'Fullscreen Active' : 'Enter Fullscreen', 'rgba(60,60,60,0.8)', 'rgba(80,80,80,0.9)', '#999999', () => { if (!document.fullscreenElement) { document.documentElement.requestFullscreen().catch(() => {}); } } ); Object.assign(this._fullscreenBtn.style, { top: '790px', left: '50%', transform: 'translateX(-50%)', width: '340px', opacity: '0', transition: 'opacity 0.6s ease', }); if (isFullscreen) { this._fullscreenBtn.disabled = true; this._fullscreenBtn.style.cursor = 'default'; this._fullscreenBtn.style.opacity = '0.4'; } this._uiLayer.appendChild(this._fullscreenBtn); // Listen for fullscreen changes this._onFullscreenChange = () => { const fs = !!document.fullscreenElement; this._fullscreenBtn.textContent = fs ? 'Fullscreen Active' : 'Enter Fullscreen'; this._fullscreenBtn.disabled = fs; this._fullscreenBtn.style.cursor = fs ? 'default' : 'pointer'; this._fullscreenBtn.style.opacity = fs ? '0.4' : '1'; }; document.addEventListener('fullscreenchange', this._onFullscreenChange); this.events.once('shutdown', () => { document.removeEventListener('fullscreenchange', this._onFullscreenChange); this._destroyDomUI(); }); } _showJoinDialog(prefillCode) { if (this._joinDialogEl) return; const overlay = document.createElement('div'); Object.assign(overlay.style, { position: 'absolute', inset: '0', background: 'rgba(0,0,0,0.6)', pointerEvents: 'auto', display: 'flex', alignItems: 'center', justifyContent: 'center', }); const panel = document.createElement('div'); Object.assign(panel.style, { background: '#1a1a3e', border: '2px solid #4444aa', borderRadius: '6px', padding: '32px 44px', textAlign: 'center', display: 'flex', flexDirection: 'column', gap: '16px', }); const title = document.createElement('div'); Object.assign(title.style, { color: '#aaccff', fontSize: '26px', fontFamily: 'Arial, sans-serif' }); title.textContent = 'Join Puzzle'; // Player name input const nameLabel = document.createElement('div'); Object.assign(nameLabel.style, { color: '#8899cc', fontSize: '16px', fontFamily: 'Arial, sans-serif' }); nameLabel.textContent = 'Player Name'; const nameInput = document.createElement('input'); Object.assign(nameInput.style, { background: '#111133', color: '#eeeeff', border: '2px solid #4466aa', borderRadius: '4px', padding: '8px 16px', fontSize: '20px', fontFamily: 'Arial, sans-serif', textAlign: 'center', outline: 'none', width: '200px', }); nameInput.maxLength = 16; nameInput.placeholder = 'Your name'; nameInput.autocomplete = 'off'; nameInput.value = localStorage.getItem('ipuzzle_playerName') || ''; nameInput.addEventListener('input', () => { localStorage.setItem('ipuzzle_playerName', nameInput.value.trim()); updateJoinBtn(); }); // Room code input const codeLabel = document.createElement('div'); Object.assign(codeLabel.style, { color: '#8899cc', fontSize: '16px', fontFamily: 'Arial, sans-serif' }); codeLabel.textContent = 'Room Code'; const input = document.createElement('input'); Object.assign(input.style, { background: '#111133', color: '#eeeeff', border: '2px solid #4466aa', borderRadius: '4px', padding: '10px 16px', fontSize: '30px', fontFamily: 'monospace', textAlign: 'center', letterSpacing:'0.3em', outline: 'none', width: '160px', }); input.maxLength = 4; input.placeholder = 'ABCD'; input.autocomplete = 'off'; if (prefillCode) { input.value = prefillCode.toUpperCase().replace(/[^A-Z0-9]/g, '').substring(0, 4); } input.addEventListener('input', () => { input.value = input.value.toUpperCase().replace(/[^A-Z0-9]/g, ''); updateJoinBtn(); }); function updateJoinBtn() { const valid = input.value.length === 4 && nameInput.value.trim().length > 0; joinBtn.disabled = !valid; joinBtn.style.opacity = valid ? '1' : '0.4'; } const errorMsg = document.createElement('div'); Object.assign(errorMsg.style, { color: '#ff6666', fontSize: '16px', fontFamily: 'Arial, sans-serif', display: 'none', }); const btnRow = document.createElement('div'); Object.assign(btnRow.style, { display: 'flex', gap: '16px', justifyContent: 'center' }); const joinBtn = document.createElement('button'); joinBtn.textContent = 'Join'; Object.assign(joinBtn.style, { padding: '8px 32px', background: '#1a3322', color: '#ddeeff', border: '2px solid #44aa66', borderRadius: '4px', fontSize: '20px', fontFamily: 'Arial, sans-serif', cursor: 'pointer', opacity: '0.4', }); joinBtn.disabled = true; const cancelBtn = document.createElement('button'); cancelBtn.textContent = 'Cancel'; Object.assign(cancelBtn.style, { padding: '8px 32px', background: '#332222', color: '#ddeeff', border: '2px solid #aa4444', borderRadius: '4px', fontSize: '20px', fontFamily: 'Arial, sans-serif', cursor: 'pointer', }); btnRow.appendChild(joinBtn); btnRow.appendChild(cancelBtn); panel.appendChild(title); panel.appendChild(nameLabel); panel.appendChild(nameInput); panel.appendChild(codeLabel); panel.appendChild(input); panel.appendChild(errorMsg); panel.appendChild(btnRow); overlay.appendChild(panel); this._uiLayer.appendChild(overlay); this._joinDialogEl = overlay; // Focus the name input (or code input if name already filled and no prefill) setTimeout(() => { if (prefillCode) { nameInput.focus(); } else if (nameInput.value.trim().length > 0) { input.focus(); } else { nameInput.focus(); } }, 50); // Initial button state updateJoinBtn(); // Enter key submits from either input const onKeydown = (e) => { if (e.key === 'Enter' && !joinBtn.disabled) joinBtn.click(); if (e.key === 'Escape') cancelBtn.click(); }; nameInput.addEventListener('keydown', onKeydown); input.addEventListener('keydown', onKeydown); cancelBtn.addEventListener('click', () => { overlay.remove(); this._joinDialogEl = null; }); // Clicking overlay background dismisses overlay.addEventListener('click', (e) => { if (e.target === overlay) { overlay.remove(); this._joinDialogEl = null; } }); // Join logic const onJoinedOk = (msg) => { NetworkManager.off('room_joined', onJoinedOk); NetworkManager.off('error', onJoinError); this.scene.start('PuzzleScene', { imageKey: msg.state.imageKey, imagePath: msg.state.imagePath, pieceCount: msg.state.pieceCount, roomCode: msg.state.roomCode, playerName: nameInput.value.trim(), _networkJoin: true, _networkState: msg.state, _networkPlayers: msg.players, }); }; const onJoinError = (msg) => { NetworkManager.off('room_joined', onJoinedOk); NetworkManager.off('error', onJoinError); errorMsg.textContent = msg.message || 'Room not found'; errorMsg.style.display = 'block'; joinBtn.disabled = false; joinBtn.style.opacity = '1'; }; joinBtn.addEventListener('click', () => { const code = input.value.trim().toUpperCase(); const name = nameInput.value.trim(); if (code.length !== 4 || !name) return; errorMsg.style.display = 'none'; joinBtn.disabled = true; joinBtn.style.opacity = '0.4'; NetworkManager.on('room_joined', onJoinedOk); NetworkManager.on('error', onJoinError); NetworkManager.joinRoom(code, name); }); } _makeDomBtn(label, bgNormal, bgHover, borderColor, onClick) { const btn = document.createElement('button'); btn.textContent = label; Object.assign(btn.style, { position: 'absolute', padding: '14px 24px', background: bgNormal, color: '#ddeeff', border: `2px solid ${borderColor}`, borderRadius: '4px', fontSize: '22px', fontFamily: 'Arial, sans-serif', cursor: 'pointer', pointerEvents: 'auto', whiteSpace: 'nowrap', textAlign: 'center', }); btn.addEventListener('mouseenter', () => { if (!btn.disabled) btn.style.background = bgHover; }); btn.addEventListener('mouseleave', () => { if (!btn.disabled) btn.style.background = bgNormal; }); btn.addEventListener('mousedown', () => { if (!btn.disabled) btn.style.transform = (btn.style.transform || '') + ' scale(0.97)'; }); btn.addEventListener('mouseup', () => { btn.style.transform = btn.style.transform.replace(' scale(0.97)', ''); }); btn.addEventListener('click', onClick); return btn; } _destroyDomUI() { if (this._uiLayer && this._uiLayer.parentNode) { this._uiLayer.parentNode.removeChild(this._uiLayer); } this._uiLayer = null; } }