iPuzzle/js/scenes/MainMenuScene.js

418 lines
13 KiB
JavaScript

/* global Phaser, NetworkManager */
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();
}
// ─── 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: 270,
alpha: 1,
duration: 1200,
delay: 300,
ease: 'Cubic.easeOut',
onComplete: () => {
// Subtle bounce settle
this.tweens.add({
targets: logo,
y: 280,
duration: 300,
ease: 'Sine.easeInOut',
});
},
});
// Sub-logo rises in at 900ms (halfway through logo anim)
this.tweens.add({
targets: sublogo,
y: 500,
alpha: 1,
duration: 1000,
delay: 900,
ease: 'Cubic.easeOut',
onComplete: () => {
// Subtle bounce settle
this.tweens.add({
targets: sublogo,
y: 490,
duration: 250,
ease: 'Sine.easeInOut',
});
},
});
// Buttons fade in at 1400ms (halfway through sub-logo anim)
this.time.delayedCall(1400, () => {
if (this._newBtn) this._newBtn.style.opacity = '1';
if (this._joinBtn) this._joinBtn.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) {
// Logo scales up then back down
this.tweens.add({
targets: logo,
scaleX: logoBase * 1.06,
scaleY: logoBase * 1.06,
duration: 600,
ease: 'Sine.easeInOut',
yoyo: true,
onComplete: () => {
// Sub-logo follows after logo settles
this.tweens.add({
targets: sublogo,
scaleX: sublogoBase * 1.06,
scaleY: sublogoBase * 1.06,
duration: 600,
ease: 'Sine.easeInOut',
yoyo: true,
onComplete: () => {
// Schedule next pulse ~10s from now
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);
this._newBtn = this._makeDomBtn('New Puzzle', '#1565c0', '#1e88e5', '#64b5f6', () => {
this.scene.start('NewPuzzleScene');
});
Object.assign(this._newBtn.style, {
top: '630px',
left: '50%',
transform: 'translateX(-50%)',
width: '340px',
opacity: '0',
transition: 'opacity 0.6s ease',
});
this._uiLayer.appendChild(this._newBtn);
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);
this.events.once('shutdown', () => this._destroyDomUI());
}
_showJoinDialog() {
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';
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)
setTimeout(() => {
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', () => { btn.style.background = bgHover; });
btn.addEventListener('mouseleave', () => { btn.style.background = bgNormal; });
btn.addEventListener('mousedown', () => { 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;
}
}