1087 lines
36 KiB
JavaScript
1087 lines
36 KiB
JavaScript
// 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;
|
|
|
|
// 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 = '<div class="spinner"></div><p>Fehler beim Laden: ' + err.message + '</p>';
|
|
}
|
|
}
|
|
|
|
// 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 = '<span class="spinner-small"></span> 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 = '<span class="spinner-small"></span> 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 = '<span class="spinner-small"></span> 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 = '<div style="color: #888;">Warte auf Spieler...</div>';
|
|
return;
|
|
}
|
|
|
|
list.innerHTML = players.map((player, index) => {
|
|
const hostBadge = player.is_host ? ' <span style="color:#ffcc00;">[HOST]</span>' : '';
|
|
return `<div style="padding:5px 0; color:#fff;">• ${player.name}${hostBadge}</div>`;
|
|
}).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 = '<div style="text-align:center; padding:20px;">Lädt Leaderboard...</div>';
|
|
|
|
// Request leaderboard via direct WebSocket
|
|
requestLeaderboardDirect();
|
|
|
|
// Fallback timeout
|
|
setTimeout(() => {
|
|
if (list && list.innerHTML.includes('Lädt')) {
|
|
list.innerHTML = '<div style="text-align:center; padding:20px; color:#888;">Keine Daten verfügbar</div>';
|
|
}
|
|
}, 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 = '<div style="text-align:center; padding:20px; color:#888;">Noch keine Einträge</div>';
|
|
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 `
|
|
<div class="leaderboard-item">
|
|
<div class="leaderboard-rank">${medal}</div>
|
|
<div class="leaderboard-name">${entry.player_name}</div>
|
|
<div class="leaderboard-score">${entry.score}</div>
|
|
</div>
|
|
`;
|
|
}).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 = '<div style="color: #888; text-align: center; padding: 20px;">Noch keine Highscores erreicht!</div>';
|
|
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 = '<div style="color: #888; text-align: center; padding: 20px;">Lade Positionen...</div>';
|
|
|
|
// 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 += `
|
|
<div style="background: rgba(0,0,0,0.4); border: 2px solid #fc0; padding: 12px; margin: 8px 0; border-radius: 4px;">
|
|
<div style="display: flex; justify-content: space-between; align-items: center; margin-bottom: 8px;">
|
|
<div>
|
|
<span style="color: #fc0; font-size: 20px; font-weight: bold;">${code.score} Punkte</span>
|
|
<span style="color: ${positionColor}; font-size: 14px; margin-left: 10px;">${positionText}</span>
|
|
</div>
|
|
<button onclick="deleteHighscoreCode(${index})" style="background: #ff4444; border: none; color: white; padding: 5px 10px; font-size: 10px; cursor: pointer; border-radius: 3px;">LÖSCHEN</button>
|
|
</div>
|
|
<div style="font-family: sans-serif; font-size: 12px; color: #ccc;">
|
|
<div><strong>Name:</strong> ${code.player_name}</div>
|
|
<div><strong>Code:</strong> <span style="color: #fc0; font-family: monospace;">${code.proof}</span></div>
|
|
<div><strong>Datum:</strong> ${code.date}</div>
|
|
</div>
|
|
</div>
|
|
`;
|
|
});
|
|
|
|
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, 2500);
|
|
}
|
|
|
|
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;
|
|
|
|
const assetKeys = Object.keys(assetsManifest).filter(k =>
|
|
['player', 'coin', 'eraser', 'pc-trash', 'godmode', 'jumpboost', 'magnet', 'baskeball', 'desk'].includes(k)
|
|
);
|
|
const key = assetKeys[Math.floor(Math.random() * assetKeys.length)];
|
|
const def = assetsManifest[key];
|
|
|
|
const el = document.createElement('div');
|
|
el.className = 'presi-asset';
|
|
|
|
const img = document.createElement('img');
|
|
img.src = `assets/${def.Filename || 'playernew.png'}`;
|
|
|
|
// Base scale from JSON
|
|
const baseScale = def.Scale || 1.0;
|
|
|
|
// We want the asset to have a certain base size in the track, scaled by its individual Scale factor
|
|
const trackHeight = 150;
|
|
const targetSize = trackHeight * 0.6; // target 60% of track height
|
|
|
|
img.style.height = `${targetSize}px`;
|
|
img.style.width = 'auto';
|
|
img.style.transform = `scale(${baseScale * 4.0})`; // Individual scale adjustment
|
|
img.style.transformOrigin = 'bottom center';
|
|
|
|
el.appendChild(img);
|
|
track.appendChild(el);
|
|
|
|
const duration = 12 + Math.random() * 8;
|
|
el.style.animation = `assetSlide ${duration}s linear forwards`;
|
|
|
|
// Add random vertical bobbing
|
|
el.style.bottom = `${Math.random() * 40}px`;
|
|
|
|
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 ? `<img src="${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 = `<img src="assets/playernew.png" style="height: 80px;">`;
|
|
layer.appendChild(el);
|
|
presiPlayers.set(p.id, el);
|
|
}
|
|
|
|
// Map world coords to screen
|
|
// World width is roughly 1280, height 720
|
|
const screenX = (p.x % 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_')) {
|
|
const emoteNum = p.state.split('_')[1];
|
|
const emotes = ["❤️", "😂", "😡", "👍"];
|
|
const emoji = emotes[parseInt(emoteNum)-1] || "❓";
|
|
|
|
let emoteEl = el.querySelector('.presi-player-emote');
|
|
if (!emoteEl) {
|
|
emoteEl = document.createElement('div');
|
|
emoteEl.className = 'presi-player-emote';
|
|
el.appendChild(emoteEl);
|
|
}
|
|
emoteEl.textContent = emoji;
|
|
|
|
// Auto-remove emote text after 2s
|
|
clearTimeout(el.emoteTimeout);
|
|
el.emoteTimeout = setTimeout(() => {
|
|
if (emoteEl) emoteEl.remove();
|
|
}, 2000);
|
|
}
|
|
});
|
|
};
|