// Game State let wasmReady = false; let gameStarted = false; let audioMuted = false; let currentLeaderboard = []; // Store full leaderboard data with proof codes // UI State Management - Single Source of Truth const UIState = { LOADING: 'loading', MENU: 'menu', LOBBY: 'lobby', PLAYING: 'playing', GAME_OVER: 'gameover', LEADERBOARD: 'leaderboard', SETTINGS: 'settings', COOP_MENU: 'coop_menu', MY_CODES: 'mycodes', IMPRESSUM: 'impressum', DATENSCHUTZ: 'datenschutz' }; let currentUIState = UIState.LOADING; // Central UI State Manager function setUIState(newState) { console.log('🎨 UI State:', currentUIState, '->', newState); currentUIState = newState; const canvas = document.querySelector('canvas'); const loadingScreen = document.getElementById('loading'); // Hide all overlays first document.querySelectorAll('.overlay-screen').forEach(screen => { screen.classList.add('hidden'); }); if (loadingScreen) loadingScreen.style.display = 'none'; // Scroll alle overlay-screens nach oben setTimeout(() => { document.querySelectorAll('.overlay-screen:not(.hidden)').forEach(screen => { screen.scrollTop = 0; }); }, 10); // Manage Canvas and Overlays based on state switch(newState) { case UIState.LOADING: if (canvas) { canvas.classList.remove('game-active'); canvas.style.visibility = 'hidden'; } if (loadingScreen) loadingScreen.style.display = 'flex'; break; case UIState.MENU: if (canvas) { canvas.classList.remove('game-active'); canvas.style.visibility = 'hidden'; } document.getElementById('menu').classList.remove('hidden'); break; case UIState.LOBBY: if (canvas) { canvas.classList.remove('game-active'); canvas.style.visibility = 'hidden'; } document.getElementById('lobbyScreen').classList.remove('hidden'); break; case UIState.PLAYING: if (canvas) { canvas.classList.add('game-active'); canvas.style.visibility = 'visible'; } // No overlays shown during gameplay break; case UIState.GAME_OVER: if (canvas) { canvas.classList.remove('game-active'); canvas.style.visibility = 'hidden'; } document.getElementById('gameOverScreen').classList.remove('hidden'); break; case UIState.LEADERBOARD: if (canvas) { canvas.classList.remove('game-active'); canvas.style.visibility = 'hidden'; } document.getElementById('leaderboardMenu').classList.remove('hidden'); break; case UIState.SETTINGS: if (canvas) { canvas.classList.remove('game-active'); canvas.style.visibility = 'hidden'; } document.getElementById('settingsMenu').classList.remove('hidden'); break; case UIState.COOP_MENU: if (canvas) { canvas.classList.remove('game-active'); canvas.style.visibility = 'hidden'; } document.getElementById('coopMenu').classList.remove('hidden'); break; case UIState.MY_CODES: if (canvas) { canvas.classList.remove('game-active'); canvas.style.visibility = 'hidden'; } document.getElementById('myCodesMenu').classList.remove('hidden'); break; case UIState.IMPRESSUM: if (canvas) { canvas.classList.remove('game-active'); canvas.style.visibility = 'hidden'; } document.getElementById('impressumMenu').classList.remove('hidden'); break; case UIState.DATENSCHUTZ: if (canvas) { canvas.classList.remove('game-active'); canvas.style.visibility = 'hidden'; } document.getElementById('datenschutzMenu').classList.remove('hidden'); break; } } // WebSocket for Leaderboard (direct JS connection) let leaderboardWS = null; let leaderboardSessionID = 'lb_' + Date.now() + '_' + Math.random().toString(36).substr(2, 9); function connectLeaderboardWebSocket() { if (leaderboardWS && leaderboardWS.readyState === WebSocket.OPEN) { return; // Already connected } // Automatisch die richtige WebSocket-URL basierend auf der aktuellen Domain const protocol = window.location.protocol === 'https:' ? 'wss:' : 'ws:'; const host = window.location.host; const wsURL = `${protocol}//${host}/ws`; console.log('🔌 Verbinde zu WebSocket:', wsURL); leaderboardWS = new WebSocket(wsURL); leaderboardWS.onopen = () => { console.log('📡 Leaderboard WebSocket connected with session:', leaderboardSessionID); // Send JOIN message to register session const joinMsg = JSON.stringify({ type: 'join', payload: { name: leaderboardSessionID, room_id: 'leaderboard_viewer', game_mode: 'solo', is_host: false, team_name: '' } }); leaderboardWS.send(joinMsg); console.log('📝 Registered leaderboard session:', leaderboardSessionID); }; leaderboardWS.onmessage = (event) => { try { const msg = JSON.parse(event.data); if (msg.type === 'leaderboard_response') { console.log('📊 Received leaderboard:', msg.payload?.entries?.length || 0, 'entries'); updateLeaderboard(msg.payload?.entries || []); } } catch (e) { console.error('Failed to parse WebSocket message:', e); } }; leaderboardWS.onerror = (error) => { console.error('❌ Leaderboard WebSocket error:', error); }; leaderboardWS.onclose = () => { console.log('📡 Leaderboard WebSocket closed'); leaderboardWS = null; }; } function requestLeaderboardDirect() { connectLeaderboardWebSocket(); // Wait for connection then request const checkAndRequest = setInterval(() => { if (leaderboardWS && leaderboardWS.readyState === WebSocket.OPEN) { clearInterval(checkAndRequest); const msg = JSON.stringify({ type: 'leaderboard_request', payload: { mode: 'solo', limit: 10 } }); leaderboardWS.send(msg); console.log('📤 Requesting leaderboard via WebSocket (session:', leaderboardSessionID + ')'); } }, 100); // Timeout after 3 seconds setTimeout(() => clearInterval(checkAndRequest), 3000); } // Callback von WASM wenn vollständig geladen window.onWasmReady = function() { console.log('✅ WASM fully initialized'); wasmReady = true; // Switch to menu state setUIState(UIState.MENU); // Enable all start buttons enableStartButtons(); // Load initial leaderboard via direct WebSocket setTimeout(() => { requestLeaderboardDirect(); }, 500); }; // Cache Management - Version wird bei jedem Build aktualisiert const CACHE_VERSION = 1767555402485; // Wird durch Build-Prozess ersetzt // Fetch mit Cache-Busting async function fetchWithCache(url) { const cacheBustedUrl = `${url}?v=${CACHE_VERSION}`; console.log(`📦 Loading: ${cacheBustedUrl}`); try { const response = await fetch(cacheBustedUrl, { cache: 'no-cache', // Immer vom Server holen wenn Version neu ist headers: { 'Cache-Control': 'no-cache, no-store, must-revalidate', 'Pragma': 'no-cache' } }); if (!response.ok) { throw new Error(`HTTP ${response.status}: ${response.statusText}`); } return response; } catch (err) { console.error(`❌ Failed to fetch ${url}:`, err); throw err; } } // Initialize WASM async function initWASM() { const go = new Go(); try { console.log(`🚀 Loading WASM (Cache Version: ${CACHE_VERSION})...`); // WASM mit Cache-Busting laden const response = await fetchWithCache("main.wasm"); const result = await WebAssembly.instantiateStreaming(response, go.importObject); go.run(result.instance); // WICHTIG: wasmReady wird erst in onWasmReady() gesetzt, nicht hier! console.log('✅ WASM runtime started, waiting for full initialization...'); } catch (err) { console.error('❌ Failed to load WASM:', err); document.getElementById('loading').innerHTML = '

Fehler beim Laden: ' + err.message + '

'; } } // Enable start buttons after WASM is ready function enableStartButtons() { const buttons = ['startBtn', 'coopBtn', 'createRoomBtn', 'joinRoomBtn']; buttons.forEach(btnId => { const btn = document.getElementById(btnId); if (btn) { btn.disabled = false; btn.style.opacity = '1'; btn.style.cursor = 'pointer'; } }); console.log('✅ Start-Buttons aktiviert (Solo + Coop)'); } // Menu Navigation // Legacy function wrappers - use setUIState instead function showMainMenu() { setUIState(UIState.MENU); } function showCoopMenu() { setUIState(UIState.COOP_MENU); } function showSettings() { setUIState(UIState.SETTINGS); } function showLeaderboard() { setUIState(UIState.LEADERBOARD); loadLeaderboard(); } function showMyCodes() { setUIState(UIState.MY_CODES); loadMyCodes(); } function showImpressum() { setUIState(UIState.IMPRESSUM); } function showDatenschutz() { setUIState(UIState.DATENSCHUTZ); } function hideAllScreens() { /* Handled by setUIState */ } function hideMenu() { /* Handled by setUIState */ } function showMenu() { setUIState(UIState.MENU); } // Game Functions function startSoloGame() { if (!wasmReady) { alert('Spiel wird noch geladen...'); return; } const playerName = document.getElementById('playerName').value || 'ANON'; // Store in localStorage for WASM to read localStorage.setItem('escape_player_name', playerName); localStorage.setItem('escape_game_mode', 'solo'); localStorage.setItem('escape_room_id', ''); gameStarted = true; // Don't switch UI state yet - wait for WASM callback onGameStarted() // The server will auto-start solo games after 2 seconds // Trigger WASM game start if (window.startGame) { window.startGame('solo', playerName, ''); } console.log('🎮 Solo game starting - waiting for server auto-start...'); } function createRoom() { if (!wasmReady) { alert('Spiel wird noch geladen...'); return; } const playerName = document.getElementById('playerName').value || 'ANON'; const roomID = 'R' + Math.random().toString(36).substr(2, 5).toUpperCase(); const teamName = 'TEAM'; // Store in localStorage localStorage.setItem('escape_player_name', playerName); localStorage.setItem('escape_game_mode', 'coop'); localStorage.setItem('escape_room_id', roomID); localStorage.setItem('escape_team_name', teamName); localStorage.setItem('escape_is_host', 'true'); // Show Lobby setUIState(UIState.LOBBY); document.getElementById('lobbyRoomCode').textContent = roomID; document.getElementById('lobbyHostControls').classList.remove('hidden'); document.getElementById('lobbyStatus').textContent = 'Du bist Host - starte wenn bereit!'; // Trigger WASM game start (im Hintergrund) if (window.startGame) { console.log('🎮 Calling window.startGame with:', 'coop', playerName, roomID, teamName, true); window.startGame('coop', playerName, roomID, teamName, true); } else { console.error('❌ window.startGame is not defined!'); } console.log('🎮 Room created:', roomID); } function joinRoom() { if (!wasmReady) { alert('Spiel wird noch geladen...'); return; } const playerName = document.getElementById('playerName').value || 'ANON'; const roomID = document.getElementById('joinRoomCode').value.toUpperCase(); const teamName = document.getElementById('teamNameJoin').value || 'TEAM'; if (!roomID || roomID.length < 4) { alert('Bitte gib einen gültigen Raum-Code ein!'); return; } // Store in localStorage localStorage.setItem('escape_player_name', playerName); localStorage.setItem('escape_game_mode', 'coop'); localStorage.setItem('escape_room_id', roomID); localStorage.setItem('escape_team_name', teamName); localStorage.setItem('escape_is_host', 'false'); // Show Lobby setUIState(UIState.LOBBY); document.getElementById('lobbyRoomCode').textContent = roomID; document.getElementById('lobbyHostControls').classList.add('hidden'); document.getElementById('lobbyStatus').textContent = 'Warte auf Host...'; // Trigger WASM game start (im Hintergrund) if (window.startGame) { console.log('🎮 Calling window.startGame with:', 'coop', playerName, roomID, teamName, false); window.startGame('coop', playerName, roomID, teamName, false); } else { console.error('❌ window.startGame is not defined!'); } console.log('🎮 Joining room:', roomID); } // Lobby Functions function startGameFromLobby() { // Host startet das Spiel - Lobby bleibt sichtbar bis Server COUNTDOWN/RUNNING sendet // Signal an WASM senden dass Spiel starten soll if (window.startGameFromLobby_WASM) { window.startGameFromLobby_WASM(); } console.log('🎮 Host requested game start - waiting for server...'); } function leaveLobby() { location.reload(); } // Update Lobby Player List (called by WASM) function updateLobbyPlayers(players) { const list = document.getElementById('lobbyPlayerList'); if (!players || players.length === 0) { list.innerHTML = '
Warte auf Spieler...
'; return; } list.innerHTML = players.map((player, index) => { const hostBadge = player.is_host ? ' [HOST]' : ''; return `
• ${player.name}${hostBadge}
`; }).join(''); console.log('👥 Lobby players updated:', players.length); } // Update Lobby Team Name (called by WASM) function updateLobbyTeamName(teamName, isHost) { const teamNameBox = document.getElementById('lobbyTeamNameBox'); const teamNameDisplay = document.getElementById('teamNameDisplay'); // Zeige Team-Name Box nur für Host if (isHost) { teamNameBox.classList.remove('hidden'); // Setup Event Listener für Input-Feld (nur einmal) const input = document.getElementById('lobbyTeamName'); if (!input.dataset.listenerAdded) { input.addEventListener('input', function() { const newTeamName = this.value.toUpperCase().trim(); if (newTeamName && window.setTeamName_WASM) { window.setTeamName_WASM(newTeamName); } }); input.dataset.listenerAdded = 'true'; } } else { teamNameBox.classList.add('hidden'); } // Aktualisiere Team-Name Anzeige if (teamName && teamName !== '') { teamNameDisplay.textContent = teamName; teamNameDisplay.style.color = '#00ff00'; } else { teamNameDisplay.textContent = 'Nicht gesetzt'; teamNameDisplay.style.color = '#888'; } console.log('🏷️ Team name updated:', teamName, 'isHost:', isHost); } function loadLeaderboard() { const list = document.getElementById('leaderboardList'); list.innerHTML = '
Lädt Leaderboard...
'; // Request leaderboard via direct WebSocket requestLeaderboardDirect(); // Fallback timeout setTimeout(() => { if (list && list.innerHTML.includes('Lädt')) { list.innerHTML = '
Keine Daten verfügbar
'; } }, 3000); } // Called by WASM to update leaderboard function updateLeaderboard(entries) { // Store full leaderboard data globally currentLeaderboard = entries || []; // Update ALL leaderboard displays const list = document.getElementById('leaderboardList'); const startList = document.getElementById('startLeaderboardList'); const gameOverList = document.getElementById('gameOverLeaderboardList'); if (!entries || entries.length === 0) { const emptyMsg = '
Noch keine Einträge
'; if (list) list.innerHTML = emptyMsg; if (startList) startList.innerHTML = emptyMsg; if (gameOverList) gameOverList.innerHTML = emptyMsg; return; } const html = entries.slice(0, 10).map((entry, index) => { const medal = index === 0 ? '🥇' : index === 1 ? '🥈' : index === 2 ? '🥉' : `${index + 1}.`; return `
${medal}
${entry.player_name}
${entry.score}
`; }).join(''); if (list) list.innerHTML = html; if (startList) startList.innerHTML = html; if (gameOverList) gameOverList.innerHTML = html; console.log('📊 Leaderboard updated with', entries.length, 'entries'); } // Audio Toggle function toggleAudio() { audioMuted = !audioMuted; const btn = document.getElementById('mute-btn'); if (audioMuted) { btn.textContent = '🔇'; if (window.setMusicVolume) window.setMusicVolume(0); if (window.setSFXVolume) window.setSFXVolume(0); } else { btn.textContent = '🔊'; const musicVol = parseInt(localStorage.getItem('escape_music_volume') || 70) / 100; const sfxVol = parseInt(localStorage.getItem('escape_sfx_volume') || 70) / 100; if (window.setMusicVolume) window.setMusicVolume(musicVol); if (window.setSFXVolume) window.setSFXVolume(sfxVol); } } // Settings Volume Sliders document.addEventListener('DOMContentLoaded', () => { const musicSlider = document.getElementById('musicVolume'); const sfxSlider = document.getElementById('sfxVolume'); const musicValue = document.getElementById('musicValue'); const sfxValue = document.getElementById('sfxValue'); if (musicSlider) { musicSlider.addEventListener('input', (e) => { const value = e.target.value; musicValue.textContent = value + '%'; localStorage.setItem('escape_music_volume', value); if (window.setMusicVolume && !audioMuted) { window.setMusicVolume(value / 100); } }); // Load saved value const savedMusic = localStorage.getItem('escape_music_volume') || 70; musicSlider.value = savedMusic; musicValue.textContent = savedMusic + '%'; } if (sfxSlider) { sfxSlider.addEventListener('input', (e) => { const value = e.target.value; sfxValue.textContent = value + '%'; localStorage.setItem('escape_sfx_volume', value); if (window.setSFXVolume && !audioMuted) { window.setSFXVolume(value / 100); } }); // Load saved value const savedSFX = localStorage.getItem('escape_sfx_volume') || 70; sfxSlider.value = savedSFX; sfxValue.textContent = savedSFX + '%'; } // Load saved player name const savedName = localStorage.getItem('escape_player_name'); if (savedName) { document.getElementById('playerName').value = savedName; } // Load local highscore const highscore = localStorage.getItem('escape_local_highscore') || 0; const hsElement = document.getElementById('localHighscore'); if (hsElement) { hsElement.textContent = highscore; } }); // Keyboard shortcuts document.addEventListener('keydown', (e) => { // ESC to show menu (only if game is started) if (e.key === 'Escape' && gameStarted) { showMenu(); gameStarted = false; } }); // Show Game Over Screen (called by WASM) function showGameOver(score) { setUIState(UIState.GAME_OVER); document.getElementById('finalScore').textContent = score; // Update local highscore const currentHS = parseInt(localStorage.getItem('escape_local_highscore') || 0); if (score > currentHS) { localStorage.setItem('escape_local_highscore', score); } // Request leaderboard via direct WebSocket requestLeaderboardDirect(); // Note: Proof-Code wird jetzt direkt vom Server über score_response WebSocket Nachricht gesendet // und von WASM (connection_wasm.go) automatisch an saveHighscoreCode() weitergeleitet console.log('💀 Game Over! Score:', score, '- Warte auf Proof-Code vom Server...'); } // Called by WASM when game actually starts function onGameStarted() { console.log('🎮 Game Started - Making canvas visible'); gameStarted = true; setUIState(UIState.PLAYING); } // ===== MY CODES MANAGEMENT ===== // Save highscore code to localStorage function saveHighscoreCode(score, proofCode, playerName) { const codes = getMySavedCodes(); const newCode = { score: score, proof: proofCode, player_name: playerName, timestamp: Date.now(), date: new Date().toLocaleString('de-DE') }; codes.push(newCode); localStorage.setItem('escape_highscore_codes', JSON.stringify(codes)); console.log('💾 Highscore-Code gespeichert:', proofCode, 'für Score:', score); } // Get all saved codes from localStorage function getMySavedCodes() { const stored = localStorage.getItem('escape_highscore_codes'); if (!stored) return []; try { return JSON.parse(stored); } catch (e) { return []; } } // Load and display my codes function loadMyCodes() { const codes = getMySavedCodes(); const list = document.getElementById('myCodesList'); if (codes.length === 0) { list.innerHTML = '
Noch keine Highscores erreicht!
'; return; } // Sort by score descending codes.sort((a, b) => b.score - a.score); // Request current leaderboard to check positions requestLeaderboardForCodes(codes); } // Request leaderboard and then display codes with positions function requestLeaderboardForCodes(codes) { const list = document.getElementById('myCodesList'); list.innerHTML = '
Lade Positionen...
'; // Use the direct leaderboard WebSocket requestLeaderboardDirect(); // Wait a bit for leaderboard to arrive, then display setTimeout(() => { displayMyCodesWithPositions(codes); }, 1000); } // Display codes with their leaderboard positions function displayMyCodesWithPositions(codes) { const list = document.getElementById('myCodesList'); let html = ''; codes.forEach((code, index) => { // Try to find position in current leaderboard const position = findPositionInLeaderboard(code.proof); const positionText = position > 0 ? `#${position}` : 'Nicht in Top 10'; const positionColor = position > 0 ? '#fc0' : '#888'; html += `
${code.score} Punkte ${positionText}
Name: ${code.player_name}
Code: ${code.proof}
Datum: ${code.date}
`; }); list.innerHTML = html; } // Find position of a proof code in the current leaderboard function findPositionInLeaderboard(proofCode) { if (!currentLeaderboard || currentLeaderboard.length === 0) return -1; // Find the entry with matching proof code const index = currentLeaderboard.findIndex(entry => entry.proof_code === proofCode); return index >= 0 ? index + 1 : -1; // Return 1-based position } // Delete a highscore code function deleteHighscoreCode(index) { if (!confirm('Diesen Highscore-Code wirklich löschen?')) return; const codes = getMySavedCodes(); codes.splice(index, 1); localStorage.setItem('escape_highscore_codes', JSON.stringify(codes)); console.log('🗑️ Highscore-Code gelöscht'); loadMyCodes(); // Reload display } // Export functions for WASM to call window.showMenu = showMenu; window.hideMenu = hideMenu; window.updateLeaderboard = updateLeaderboard; window.showGameOver = showGameOver; window.onGameStarted = onGameStarted; window.saveHighscoreCode = saveHighscoreCode; // Initialize on load initWASM(); console.log('🎮 Game.js loaded - Retro Edition');