// Game State let wasmReady = false; let gameStarted = false; let gameStarting = false; // Verhindert doppeltes Starten 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', PRESENTATION: 'presentation' }; let currentUIState = UIState.LOADING; let assetsManifest = null; let presiAssets = []; let presiPlayers = new Map(); let presiQuoteInterval = null; let presiAssetInterval = null; let presiAssetBag = []; // Shuffled bag for controlled randomness // 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'; canvas.focus(); // Canvas fokussieren für Tastatureingaben } // 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; case UIState.PRESENTATION: if (canvas) { canvas.classList.remove('game-active'); canvas.style.visibility = 'hidden'; } document.getElementById('presentationScreen').classList.remove('hidden'); startPresentationLogic(); 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; // Canvas fokussierbar machen const canvas = document.querySelector('canvas'); if (canvas) { canvas.setAttribute('tabindex', '1'); console.log('✅ Canvas tabindex gesetzt'); } // 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 = 1767643088942; // 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 startBtn = document.getElementById('startBtn'); if (startBtn) { startBtn.disabled = false; startBtn.style.opacity = '1'; startBtn.style.cursor = 'pointer'; startBtn.innerHTML = 'SOLO STARTEN'; } const coopBtn = document.getElementById('coopBtn'); if (coopBtn) { coopBtn.disabled = false; coopBtn.style.opacity = '1'; coopBtn.style.cursor = 'pointer'; } const createRoomBtn = document.getElementById('createRoomBtn'); if (createRoomBtn) { createRoomBtn.disabled = false; createRoomBtn.style.opacity = '1'; createRoomBtn.style.cursor = 'pointer'; createRoomBtn.innerHTML = 'RAUM ERSTELLEN'; } const joinRoomBtn = document.getElementById('joinRoomBtn'); if (joinRoomBtn) { joinRoomBtn.disabled = false; joinRoomBtn.style.opacity = '1'; joinRoomBtn.style.cursor = 'pointer'; joinRoomBtn.innerHTML = 'RAUM BEITRETEN'; } 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; } // Verhindere doppeltes Starten if (gameStarting) { console.log('⚠️ Game is already starting...'); return; } gameStarting = true; const playerName = document.getElementById('playerName').value || 'ANON'; const startBtn = document.getElementById('startBtn'); // Button deaktivieren und Loading anzeigen if (startBtn) { startBtn.disabled = true; startBtn.innerHTML = ' Starte...'; startBtn.style.opacity = '0.6'; } // 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; } // Verhindere doppeltes Starten if (gameStarting) { console.log('⚠️ Game is already starting...'); return; } gameStarting = true; const createBtn = document.getElementById('createRoomBtn'); // Button deaktivieren und Loading anzeigen if (createBtn) { createBtn.disabled = true; createBtn.innerHTML = ' Erstelle Raum...'; createBtn.style.opacity = '0.6'; } 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 nach kurzer Verzögerung (damit User Feedback sieht) setTimeout(() => { setUIState(UIState.LOBBY); document.getElementById('lobbyRoomCode').textContent = roomID; document.getElementById('lobbyHostControls').classList.remove('hidden'); document.getElementById('lobbyStatus').textContent = 'Du bist Host - starte wenn bereit!'; // Reset gameStarting für Lobby gameStarting = false; }, 300); // 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; } // Verhindere doppeltes Starten if (gameStarting) { console.log('⚠️ Game is already starting...'); return; } gameStarting = true; const joinBtn = document.getElementById('joinRoomBtn'); // Button deaktivieren und Loading anzeigen if (joinBtn) { joinBtn.disabled = true; joinBtn.innerHTML = ' Trete bei...'; joinBtn.style.opacity = '0.6'; } 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!'); // Reset bei Fehler gameStarting = false; if (joinBtn) { joinBtn.disabled = false; joinBtn.innerHTML = 'RAUM BEITRETEN'; joinBtn.style.opacity = '1'; } 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 nach kurzer Verzögerung setTimeout(() => { setUIState(UIState.LOBBY); document.getElementById('lobbyRoomCode').textContent = roomID; document.getElementById('lobbyHostControls').classList.add('hidden'); document.getElementById('lobbyStatus').textContent = 'Warte auf Host...'; // Reset gameStarting für Lobby gameStarting = false; }, 300); // 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') || 80) / 100; const sfxVol = parseInt(localStorage.getItem('escape_sfx_volume') || 40) / 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') || 80; 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') || 40; sfxSlider.value = savedSFX; sfxValue.textContent = savedSFX + '%'; } // Load saved player name const savedName = localStorage.getItem('escape_player_name'); if (savedName) { document.getElementById('playerName').value = savedName; } // Auto-Join if URL parameter ?room=XYZ is present const urlParams = new URLSearchParams(window.location.search); const roomParam = urlParams.get('room'); if (roomParam) { document.getElementById('joinRoomCode').value = roomParam; // Wait for WASM to be ready, then auto-join const checkWASM = setInterval(() => { if (wasmReady) { clearInterval(checkWASM); joinRoom(); } }, 100); } // 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; } // F1 to toggle presentation mode if (e.key === 'F1') { e.preventDefault(); if (window.togglePresentationMode_WASM) { window.togglePresentationMode_WASM(); } } }); // 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 } // Restart game without reload function restartGame() { console.log('🔄 Restarting game...'); // Reset game state gameStarted = false; gameStarting = false; // Return to main menu setUIState(UIState.MENU); // Re-enable start buttons enableStartButtons(); console.log('✅ Game restarted - ready to play again'); } // ===== FULLSCREEN ===== function toggleFullscreen() { if (!document.fullscreenElement) { document.documentElement.requestFullscreen().catch(() => {}); } else { document.exitFullscreen().catch(() => {}); } } function updateFullscreenBtn() { const btn = document.getElementById('fullscreen-btn'); if (!btn) return; btn.textContent = document.fullscreenElement ? '✕' : '⛶'; btn.title = document.fullscreenElement ? 'Vollbild beenden' : 'Vollbild'; } document.addEventListener('fullscreenchange', updateFullscreenBtn); // Auto-Fullscreen beim ersten Nutzer-Klick let autoFullscreenDone = false; document.addEventListener('click', function requestAutoFullscreen() { if (!autoFullscreenDone && !document.fullscreenElement) { autoFullscreenDone = true; document.documentElement.requestFullscreen().catch(() => {}); } }, { once: false, capture: true }); document.addEventListener('touchstart', function requestAutoFullscreenTouch() { if (!autoFullscreenDone && !document.fullscreenElement) { autoFullscreenDone = true; document.documentElement.requestFullscreen().catch(() => {}); } }, { once: false, capture: true }); // Export functions for WASM to call window.showMenu = showMenu; window.hideMenu = hideMenu; window.updateLeaderboard = updateLeaderboard; window.showGameOver = showGameOver; window.onGameStarted = onGameStarted; window.saveHighscoreCode = saveHighscoreCode; window.restartGame = restartGame; // Initialize on load initWASM(); console.log('🎮 Game.js loaded - Retro Edition'); // ===== PRESENTATION MODE LOGIC ===== function startPresentationLogic() { if (presiQuoteInterval) clearInterval(presiQuoteInterval); if (presiAssetInterval) clearInterval(presiAssetInterval); // Initial Quote showNextPresiQuote(); presiQuoteInterval = setInterval(showNextPresiQuote, 8000); // Asset Spawning presiAssetInterval = setInterval(spawnPresiAsset, 1500); } function showNextPresiQuote() { if (!SPRUECHE || SPRUECHE.length === 0) return; const q = SPRUECHE[Math.floor(Math.random() * SPRUECHE.length)]; document.getElementById('presiQuoteText').textContent = `"${q.text}"`; document.getElementById('presiQuoteAuthor').textContent = `- ${q.author}`; // Simple pulse effect const box = document.getElementById('presiQuoteBox'); box.style.animation = 'none'; box.offsetHeight; // trigger reflow box.style.animation = 'emotePop 0.8s ease-out'; } async function spawnPresiAsset() { if (!assetsManifest) { try { const resp = await fetchWithCache('assets/assets.json'); const data = await resp.json(); assetsManifest = data.assets; } catch(e) { return; } } const track = document.querySelector('.presi-assets-track'); if (!track) return; // Refill the bag if empty if (presiAssetBag.length === 0) { const assetKeys = Object.keys(assetsManifest).filter(k => ['player', 'coin', 'eraser', 'pc-trash', 'godmode', 'jumpboost', 'magnet', 'baskeball', 'desk'].includes(k) ); // Add each asset twice for a longer cycle presiAssetBag = [...assetKeys, ...assetKeys]; // Shuffle for (let i = presiAssetBag.length - 1; i > 0; i--) { const j = Math.floor(Math.random() * (i + 1)); [presiAssetBag[i], presiAssetBag[j]] = [presiAssetBag[j], presiAssetBag[i]]; } } const key = presiAssetBag.pop(); const def = assetsManifest[key]; const el = document.createElement('div'); el.className = 'presi-asset'; // Container for the image to handle scaling better const imgContainer = document.createElement('div'); imgContainer.style.width = '100px'; imgContainer.style.height = '100px'; imgContainer.style.display = 'flex'; imgContainer.style.alignItems = 'center'; imgContainer.style.justifyContent = 'center'; const img = document.createElement('img'); img.src = `assets/${def.Filename || 'playernew.png'}`; // Scale based on JSON // We use the Scale from JSON to determine the relative size const baseScale = def.Scale || 1.0; img.style.maxWidth = '100%'; img.style.maxHeight = '100%'; img.style.transform = `scale(${baseScale * 5.0})`; // Individual scale adjustment imgContainer.appendChild(img); el.appendChild(imgContainer); // Label const label = document.createElement('div'); label.textContent = def.ID.toUpperCase(); label.style.fontSize = '8px'; label.style.color = '#5dade2'; label.style.marginTop = '10px'; el.appendChild(label); track.appendChild(el); const duration = 15 + Math.random() * 10; el.style.animation = `assetSlide ${duration}s linear forwards`; // Random Y position across the whole screen el.style.top = `${10 + Math.random() * 80}%`; setTimeout(() => el.remove(), duration * 1000); } // WASM Callbacks for Presentation window.onPresentationStarted = function(roomID, qrBase64) { console.log('📺 Presentation started:', roomID); document.getElementById('presiRoomCode').textContent = roomID; const qrEl = document.getElementById('presiQRCode'); if (qrEl) qrEl.innerHTML = qrBase64 ? `` : ''; setUIState(UIState.PRESENTATION); }; window.onPresentationUpdate = function(players) { if (currentUIState !== UIState.PRESENTATION) return; const layer = document.querySelector('.presi-players-layer'); if (!layer) return; const currentIds = new Set(players.map(p => p.id)); // Remove left players for (let [id, el] of presiPlayers) { if (!currentIds.has(id)) { el.remove(); presiPlayers.delete(id); } } // Update or add players players.forEach(p => { let el = presiPlayers.get(p.id); if (!el) { el = document.createElement('div'); el.className = 'presi-player'; el.innerHTML = ``; layer.appendChild(el); presiPlayers.set(p.id, el); } // Map world coords to screen const screenX = ((p.x + 1280000) % 1280) / 1280 * window.innerWidth; const screenY = (p.y / 720) * window.innerHeight; el.style.left = `${screenX}px`; el.style.top = `${screenY}px`; // Handle Emotes if (p.state && p.state.startsWith('EMOTE_')) { if (el.lastEmoteState !== p.state) { el.lastEmoteState = p.state; const emoteNum = p.state.split('_')[1]; const emotes = ["❤️", "😂", "😡", "👍"]; const emoji = emotes[parseInt(emoteNum)-1] || "❓"; const oldEmote = el.querySelector('.presi-player-emote'); if (oldEmote) oldEmote.remove(); const emoteEl = document.createElement('div'); emoteEl.className = 'presi-player-emote'; emoteEl.textContent = emoji; el.appendChild(emoteEl); setTimeout(() => { if (emoteEl.parentNode === el) emoteEl.remove(); el.lastEmoteState = ""; }, 2000); } } }); };