// Globals verwacht door drengine_launcher.js DataLoader (ook gedefinieerd in head script) window.canvas = window.canvasElement = document.getElementById('canvas'); window._nativeCharPreviewReady = false; window._wasmRuntimeReady = false; window._pendingGLB = null; window._pendingCharacterId = null; let socket = null; let gameSocket = null; let inQueue = false; let queueSeconds = 0; let queueTimerInterval = null; let loadingTimerInterval = null; let loadStartTime = 0; let defaultServersFromServer = []; let wasmScriptLoaded = false; let isGameAdmin = false; let gameSocketInstance = null; // Standaard wachttijd-mode: DangerRoyale (modus 2) - kan worden overschreven door game-mode-selector let currentQueueMode = 0; const TARGET_LOBBY_SIZE = 4; // === CSP NONCE HELPER === // Returns nonce attribute string for inline event handlers, read from server-injected window.DR_NONCE function nonceAttr() { return window.DR_NONCE ? ` nonce="${window.DR_NONCE}"` : ''; } // Post-processes dynamically-set innerHTML to add nonce to all inline event handler elements function applyCspNonces(container) { if (!window.DR_NONCE) return; const el = typeof container === 'string' ? document.querySelector(container) : ((container && container.nodeType) ? container : document.body); if (!el) return; const inlineEls = el.querySelectorAll('[onclick],[onchange],[oninput],[onkeydown],[onkeyup],[onload],[onsubmit]'); inlineEls.forEach(e => { if (!e.hasAttribute('nonce')) e.setAttribute('nonce', window.DR_NONCE); }); } // === EULA SHIM === // This string mirrors the C++ getEngineEulaNative() in main.cpp // The native WASM function is preferred; this is the JS fallback const ENGINE_EULA_TEXT = "DangerRoyale SourceNetwork EULA v1.0\n" + "\n" + "1. All intellectual property, source code, and binary distributions of the\n" + " DangerRoyale engine core are exclusively owned by SourceNetwork BV.\n" + "2. Reverse-engineering, decompiling, disassembling, or attempting to extract,\n" + " modify, or tamper with the WASM heap snapshots, binary network protocol,\n" + " or encrypted game state data is strictly prohibited.\n" + "3. Interception or modification of binary network snapshots sent between\n" + " the client and server is a violation of this license and may result in\n" + " immediate, permanent account termination and legal action.\n" + "4. Unauthorized redistribution or hosting of the compiled engine assets,\n" + " including drengine_launcher.js and drengine_launcher.wasm, is forbidden.\n" + "5. All skins, textures, and digital assets within DangerRoyale are\n" + " non-transferable, revocable virtual software licenses exclusively owned\n" + " by SourceNetwork BV, granting the user a limited right-of-use strictly\n" + " within the game client.\n" + "6. SourceNetwork BV operates a completely independent, closed internal\n" + " virtual economy powered by Market Coins. SourceNetwork BV does not\n" + " facilitate, authorize, or guarantee any real-world economic value,\n" + " cash-outs, fiat conversions, or speculatory trading actions taking place\n" + " on external third-party platforms or APIs. Any external usage of public\n" + " endpoints for alternative peer-to-peer trading is entirely at the user's\n" + " and external platform's sole risk and financial liability.\n" + "7. By continuing past this notice, you acknowledge that you have read and\n" + " agree to be bound by these terms. Violation of Sections 2-3 and 5-6\n" + " constitute grounds for immediate civil and criminal prosecution.\n" + "\n" + "© 2026 SourceNetwork BV — All Rights Reserved."; const TERMS_OF_SERVICE_TEXT = "DangerRoyale TERMS OF SERVICE & LEGAL DISCLAIMER\n" + "\n" + "1. VIRTUAL ASSETS LICENSE\n" + " All skins, textures, and digital assets within DangerRoyale are\n" + " non-transferable, revocable virtual software licenses exclusively owned\n" + " by SourceNetwork BV, granting the user a limited right-of-use strictly\n" + " within the game client. These assets have no cash value and cannot be\n" + " redeemed for fiat currency.\n" + "\n" + "2. INDEPENDENT VIRTUAL ECONOMY\n" + " SourceNetwork BV operates a completely independent, closed internal\n" + " virtual economy powered by Market Coins. SourceNetwork BV does not\n" + " facilitate, authorize, or guarantee any real-world economic value,\n" + " cash-outs, fiat conversions, or speculatory trading actions taking place\n" + " on external third-party platforms or APIs. Any external usage of public\n" + " endpoints for alternative peer-to-peer trading is entirely at the user's\n" + " and external platform's sole risk and financial liability.\n" + "\n" + "3. BACKEND INFRASTRUCTURE\n" + " All virtual wallet logic (retrieveObfuscatedWallet) and marketplace\n" + " P2P database pointers handle trades entirely within SourceNetwork BV's\n" + " independent secure backend infrastructure. No external systems have\n" + " access to user wallet data or transaction records.\n" + "\n" + "© 2026 SourceNetwork BV — All Rights Reserved."; function getEngineEula() { if (typeof Module !== 'undefined' && typeof Module._getEngineEulaNative === 'function') { try { const ptr = Module._getEngineEulaNative(); if (ptr) return Module.UTF8ToString(ptr); } catch (e) { console.warn('[DREngine] Native EULA fetch failed, using JS fallback:', e.message); } } return ENGINE_EULA_TEXT; } function showEulaModal() { const alreadyAccepted = localStorage.getItem('dangerroyale_eula_accepted'); if (alreadyAccepted) return; let modal = document.getElementById('eula-modal'); if (!modal) { modal = document.createElement('div'); modal.id = 'eula-modal'; modal.innerHTML = `

ENGINE LICENSE AGREEMENT

${getEngineEula()}
`; document.body.appendChild(modal); document.getElementById('eula-accept').onclick = function() { localStorage.setItem('dangerroyale_eula_accepted', 'true'); modal.style.display = 'none'; document.body.style.overflow = ''; }; } modal.style.display = 'flex'; document.body.style.overflow = 'hidden'; } function showTermsOfService() { const alreadyShown = localStorage.getItem('dangerroyale_tos_shown'); if (alreadyShown) return; let modal = document.getElementById('tos-modal'); if (!modal) { modal = document.createElement('div'); modal.id = 'tos-modal'; modal.innerHTML = `

TERMS OF SERVICE & LEGAL DISCLAIMER

${TERMS_OF_SERVICE_TEXT}
`; document.body.appendChild(modal); document.getElementById('tos-accept').onclick = function() { localStorage.setItem('dangerroyale_tos_shown', 'true'); modal.style.display = 'none'; document.body.style.overflow = ''; }; } modal.style.display = 'flex'; document.body.style.overflow = 'hidden'; } // Force all asset pipeline, WebGL2 packages, and WebSocket/WebRTC connections through the // secure MainNet domain: https://drsrc.net (or wss://drsrc.net for binary network pipeline). // The DNS for drsrc.net points to our VPS. This avoids the Chromium security error // "unsupported command-line flag: --unsafely-treat-insecure-origin-as-secure". const MAINNET_DOMAIN = 'drsrc.net'; const socketProtocol = 'wss:'; const socketPort = ''; const socketUrl = `${socketProtocol}//${MAINNET_DOMAIN}${socketPort ? ':' + socketPort : ''}`; // === PACKET JITTER BUFFER === // Smooths frame-timestamp delivery to the WASM module HEAP to prevent the C++ // validateInputFrequency trap from misinterpreting domain routing latency as // an input frequency exploit. const PACKET_JITTER_BUFFER = { packets: [], maxBufferSize: 10, lastFlushTime: 0, flushInterval: 33, _timer: null, _initialized: false, push(packet) { this.packets.push({ data: packet, receivedAt: Date.now() }); if (this.packets.length > this.maxBufferSize) { this.packets.shift(); } this._scheduleFlush(); }, _scheduleFlush() { if (this._timer) return; this._timer = setTimeout(() => { this.flush(); }, this.flushInterval); }, flush() { this._timer = null; const now = Date.now(); if (this.packets.length === 0) return; const batch = []; while (this.packets.length > 0) { const pkt = this.packets.shift(); if (typeof pkt.data === 'object' && pkt.data !== null) { batch.push(pkt.data); } else { batch.push({ raw: pkt.data }); } } if (typeof Module !== 'undefined' && Module !== null && Module.HEAP8) { try { const serialized = JSON.stringify(batch); const buffer = new TextEncoder().encode(serialized); const heapPtr = Module._malloc(buffer.length); if (heapPtr) { try { new Uint8Array(Module.HEAP8.buffer, heapPtr, buffer.length).set(buffer); if (typeof Module._processJitterBufferedDelta === 'function') { Module._processJitterBufferedDelta(heapPtr, buffer.length); } else if (typeof Module._processDeltaSnapshotNative === 'function') { Module._processDeltaSnapshotNative(heapPtr, buffer.length); } } finally { Module._free(heapPtr); } } } catch (e) { console.warn('[DREngine] Jitter buffer flush error:', e.message); } } }, clear() { this.packets = []; if (this._timer) { clearTimeout(this._timer); this._timer = null; } } }; // ============================================ // AUDIO ENGINE BRIDGE (full impl in audio-engine.js) // ============================================ const bgMusicActive = { value: true }; const sfxEnabled = { value: true }; function playSlapSound() { if (window.AudioEngine) window.AudioEngine.playSlapSound(); } function playAdminBipSound() { if (window.AudioEngine) window.AudioEngine.playAdminBipSound(); } function playNukeAlarmSound() { if (window.AudioEngine) window.AudioEngine.playNukeAlarmSound(); } function playHoverSfx() { if (window.AudioEngine) window.AudioEngine.playHoverSfx(); } function playClickSfx() { if (window.AudioEngine) window.AudioEngine.playClickSfx(); } function playExplosionSound() { if (window.AudioEngine) window.AudioEngine.playExplosionSound(); } function startBackgroundMusic() { if (window.AudioEngine) window.AudioEngine.startBackgroundMusic(); } function stopBackgroundMusic() { if (window.AudioEngine) window.AudioEngine.stopBackgroundMusic(); } function isMusicActive() { return window.AudioEngine ? window.AudioEngine.isMusicActive() : true; } function isSfxEnabled() { return window.AudioEngine ? window.AudioEngine.isSfxEnabled() : true; } function loadAudioPreferences() { if (window.AudioEngine) window.AudioEngine.loadAudioPreferences(); } // Audio toggle handlers attached to window EARLY (hoisted above onload) so that // any dynamically-injected UI button (settings/music-sfx toggles) can call them // without throwing a ReferenceError during initial mount. window.toggleBackgroundMusic = function(checked) { if (typeof window.AudioEngine === 'undefined' || !window.AudioEngine) return; var enabled = checked !== false; if (typeof window.AudioEngine.toggleBackgroundMusic === 'function') { window.AudioEngine.toggleBackgroundMusic(enabled); } else if (enabled) { if (typeof window.AudioEngine.startBackgroundMusic === 'function') window.AudioEngine.startBackgroundMusic(); } else { if (typeof window.AudioEngine.stopBackgroundMusic === 'function') window.AudioEngine.stopBackgroundMusic(); } }; window.toggleSfx = function(checked) { if (typeof window.AudioEngine === 'undefined' || !window.AudioEngine) return; if (typeof window.AudioEngine.toggleSfx === 'function') window.AudioEngine.toggleSfx(checked !== false); }; // ============================================ // DEVELOPER PANEL: PIN UPLOAD & ELEVATION // ============================================ let selectedPinFile = null; function openDevPanel() { if (!isGameAdmin && !window.isGameAdmin) { alert('Access denied. This panel requires game admin credentials (forced Steam ID or verified .pin).'); return; } const container = document.getElementById('admin-panel-container'); if (container) { container.classList.add('visible'); container.style.display = 'block'; container.style.visibility = 'visible'; } const complexSection = document.getElementById('admin-complex-functions'); const verifySection = document.getElementById('admin-verify-section'); if (complexSection && verifySection) { if (isGameAdmin) { complexSection.style.display = 'block'; verifySection.style.display = 'none'; } else { complexSection.style.display = 'none'; verifySection.style.display = 'block'; } } } function closeDevPanel() { const container = document.getElementById('admin-panel-container'); if (container) { container.style.display = 'none'; container.style.visibility = 'hidden'; container.classList.remove('visible'); } selectedPinFile = null; const status = document.getElementById('dev-pin-status'); if (status) status.textContent = 'No file selected'; const complexSection = document.getElementById('admin-complex-functions'); const verifySection = document.getElementById('admin-verify-section'); if (complexSection && verifySection && !isGameAdmin) { complexSection.style.display = 'none'; verifySection.style.display = 'block'; } } function openUserSettings() { const overlay = document.getElementById('settings-overlay'); if (overlay) { overlay.style.display = 'flex'; const settingsBtn = document.querySelector('.settings-modal-body'); if (settingsBtn) settingsBtn.focus(); } } function handlePinFileSelect(event) { const file = event.target.files[0]; if (!file) return; selectedPinFile = file; const status = document.getElementById('dev-pin-status'); if (status) status.textContent = `Selected: ${file.name} (${file.size} bytes)`; } function handleBspFileSelect(event) { const file = event.target.files[0]; if (!file) return; if (!file.name.toLowerCase().endsWith('.bsp')) { alert('Please select a .bsp file.'); return; } const reader = new FileReader(); reader.onload = function(e) { const arrayBuffer = e.target.result; const fileSize = (arrayBuffer.byteLength / (1024 * 1024)).toFixed(1); let header = ''; try { const firstBytes = new Uint8Array(arrayBuffer.slice(0, 64)); const decoder = new TextDecoder('ascii'); header = decoder.decode(firstBytes).replace(/\x00/g, '').trim().substring(0, 32); } catch (e) { header = 'Unknown'; } window._dangerCustomBsp = { name: file.name.replace(/\.bsp$/i, ''), file: file, size: arrayBuffer.byteLength, buffer: arrayBuffer, header: header }; const preview = document.getElementById('bsp-preview'); if (preview) { preview.innerHTML = `${file.name} (${fileSize} MB)
Format: ${header || 'Binary BSP'}
Ready to load`; } const mapSelect = document.getElementById('solo-map-select'); if (mapSelect) { mapSelect.value = ''; } }; reader.readAsArrayBuffer(file); } async function uploadMasterKey() { if (!selectedPinFile) { alert('Please select a pin file first.'); return; } const arrayBuffer = await selectedPinFile.arrayBuffer(); const hexData = Array.from(new Uint8Array(arrayBuffer)).map(b => b.toString(16).padStart(2, '0')).join(''); // Probeer eerst WebSocket-verificatie (als gamesocket verbonden is) if (window.dangerGameSocket && window.dangerGameSocket.connected) { window.dangerGameSocket.emit('verify_master_key', { pinHex: hexData, userId: window.dangerUser?.steamId || '' }); setTimeout(() => { if (!isGameAdmin) { appendChatMessage({ author: 'SYSTEM', text: 'Socket verification did not elevate. Trying HTTP fallback...', color: '#ff6600', timestamp: Date.now() }); } else { unlockComplexFunctions(); } }, 1000); } else { // Terugval op HTTP-endpoint try { const response = await fetch('/api/verify-master-key', { method: 'POST', headers: { 'Content-Type': 'application/json' }, credentials: 'include', body: JSON.stringify({ pin: hexData }) }); const result = await response.json(); if (result.success && result.isGameAdmin) { isGameAdmin = true; window.isGameAdmin = true; playAdminBipSound(); unlockComplexFunctions(); appendChatMessage({ author: 'SYSTEM', text: 'PIN VERIFIED - COMPLEX FUNCTIONS UNLOCKED (RECOVERY ACCESS ACTIVE)', color: '#ff00ff', timestamp: Date.now() }); } else { appendChatMessage({ author: 'SYSTEM', text: result.error || 'PIN VERIFICATION FAILED', color: '#ff5555', timestamp: Date.now() }); } } catch (e) { appendChatMessage({ author: 'SYSTEM', text: 'CONNECTION FAILED: ' + e.message, color: '#ff5555', timestamp: Date.now() }); } } } // Unlock complex administrator functions after .pin verification function unlockComplexFunctions() { isGameAdmin = true; window.isGameAdmin = true; window._pinMasterKeyVerified = true; const complexSection = document.getElementById('admin-complex-functions'); if (complexSection) complexSection.style.display = 'block'; const verifySection = document.getElementById('admin-verify-section'); if (verifySection) verifySection.style.display = 'none'; const status = document.getElementById('dev-pin-status'); if (status) status.textContent = 'VERIFIED - recovery access active'; const devPanelBtn = document.getElementById('dev-panel-trigger'); if (devPanelBtn) { devPanelBtn.style.display = 'block'; devPanelBtn.textContent = 'ADMIN PANEL (ACTIVE)'; } // Unhide native C++ injected admin sub-sections (Skin Drop, Map Pool, Model Loader) document.querySelectorAll('.admin-sub-section').forEach(function(el) { el.style.display = 'block'; }); // Do NOT auto-open - admin panel stays hidden until user clicks the trigger button } // Open master key status info function openMasterKeyStatus() { fetch('/api/master-key-status') .then(r => r.json()) .then(data => { const status = data.masterKeyHash ? 'Active (SHA-256 hash registered)' : 'Not initialized'; alert('Master Key Status:\n' + status + '\n\nThe .pin file is your recovery key — keep it secure offline.'); }) .catch(e => alert('Status check failed: ' + e.message)); } // Clear the selected pin file function clearPinSelection() { selectedPinFile = null; const status = document.getElementById('dev-pin-status'); if (status) status.textContent = 'No file selected'; const fileInput = document.getElementById('dev-pin-upload'); if (fileInput) fileInput.value = ''; } // Lock complex functions (hide the advanced controls) function lockComplexFunctions() { isGameAdmin = false; window.isGameAdmin = false; window._pinMasterKeyVerified = false; isGameAdmin = false; window.isGameAdmin = false; window._pinMasterKeyVerified = false; const complexSection = document.getElementById('admin-complex-functions'); if (complexSection) complexSection.style.display = 'none'; document.querySelectorAll('.admin-sub-section').forEach(function(el) { el.style.display = 'none'; }); const verifySection = document.getElementById('admin-verify-section'); if (verifySection) verifySection.style.display = 'block'; const status = document.getElementById('dev-pin-status'); if (status) status.textContent = 'No file selected'; selectedPinFile = null; const fileInput = document.getElementById('dev-pin-upload'); if (fileInput) fileInput.value = ''; appendChatMessage({ author: 'SYSTEM', text: 'Complex functions locked.', color: '#888', timestamp: Date.now() }); } // ============================================ // HUD CHAT LOG WINDOW // ============================================ function showChatLog() { const log = document.getElementById('game-chat-log'); if (log) log.style.display = 'block'; } function hideChatLog() { const log = document.getElementById('game-chat-log'); if (log) log.style.display = 'none'; } function appendChatMessage({ author, text, color, timestamp, isCommand }) { const log = document.getElementById('game-chat-log'); if (!log) return; log.style.display = 'block'; const entry = document.createElement('div'); entry.className = 'chat-entry'; const authorEl = document.createElement('span'); authorEl.className = 'chat-author'; authorEl.textContent = author || 'SYSTEM'; const textEl = document.createElement('span'); if (isCommand) { textEl.className = 'chat-command'; } else if (color === '#00ffff' || color === '#ff00ff') { textEl.className = 'chat-text-admin'; } else if (color === '#ff6600') { textEl.className = 'chat-text-warning'; } else if (color === '#ff5555') { textEl.className = 'chat-text-error'; } else { textEl.className = 'chat-text-basic'; } textEl.textContent = ` ${text || ''}`; entry.appendChild(authorEl); entry.appendChild(textEl); log.appendChild(entry); log.scrollTop = log.scrollHeight; while (log.children.length > 20) { log.removeChild(log.firstChild); } } function showChatInput() { const chatInput = document.getElementById('dw-chat-input'); const chatContainer = document.getElementById('dw-chat-container'); if (!chatInput || !chatContainer) return; chatInput.style.display = 'block'; chatContainer.style.display = 'block'; chatInput.focus(); chatInput.value = ''; } function hideChatInput() { const chatInput = document.getElementById('dw-chat-input'); const chatContainer = document.getElementById('dw-chat-container'); if (chatInput) chatInput.style.display = 'none'; if (chatContainer) chatContainer.style.display = 'none'; } // ============================================ // COMMAND KEYBINDING (Enter for chat, / for commands) // ============================================ document.addEventListener('keydown', (e) => { if (e.key === 'Enter' && !e.shiftKey) { const chatInput = document.getElementById('dw-chat-input'); if (!chatInput || chatInput.style.display !== 'block') { e.preventDefault(); if (window.dangerGameActive) { showChatInput(); } else { toggleChatInput(); } return; } } if (e.key === 'Escape') { const chatInput = document.getElementById('dw-chat-input'); if (chatInput && chatInput.style.display === 'block') { hideChatInput(); e.preventDefault(); } } }); window.isGameAdmin = false; window.onGameAdminElevated = function() { isGameAdmin = true; window.isGameAdmin = true; }; function submitChatMessage() { const chatInput = document.getElementById('dw-chat-input'); if (!chatInput) return; const message = chatInput.value.trim(); if (!message) return; if (message.startsWith('/')) { handleChatCommand(message); appendChatMessage({ author: 'YOU', text: message, color: '#00ffff', timestamp: Date.now(), isCommand: true }); } else { if (gameSocketInstance && gameSocketInstance.connected) { gameSocketInstance.emit('chat_message', { message }); } else if (window.dangerGameSocket && window.dangerGameSocket.connected) { window.dangerGameSocket.emit('chat_message', { message }); } } hideChatInput(); setTimeout(() => { if (typeof Module !== 'undefined' && Module.canvas) { document.exitPointerLock?.(); } }, 50); } // Toggle the isolated admin popup modal visibility. // The modal starts hidden (display:none) and only appears when user clicks // 'ADMIN PANEL' button. Starts with empty auth screen, shows backend instances // only after successful .pin verification via WASM heap. function toggleAdminPanel() { const popup = document.getElementById("admin-popup-modal"); if (!popup) return; const isHidden = popup.style.display === 'none' || popup.classList.contains('closed'); if (isHidden) { if (!isGameAdmin && !window.isGameAdmin) { alert('Access denied. This panel requires game admin credentials (forced Steam ID or verified .pin).'); return; } popup.style.display = 'flex'; popup.classList.remove('closed'); popup.classList.add('open'); renderAdminAuthScreen(); } else { popup.style.display = 'none'; popup.classList.remove('open'); popup.classList.add('closed'); } } // Render the initial empty admin authentication screen inside the popup modal. // Asks user to upload cryptografische .pin-file for verification. function renderAdminAuthScreen() { const body = document.getElementById("admin-popup-body"); if (!body) return; body.innerHTML = `

Upload your pin file to verify admin credentials.

No file selected
`; } // Render the complex admin functions with backend instances after pin verification. function renderAdminComplexFunctions() { const body = document.getElementById("admin-popup-body"); if (!body) return; body.innerHTML = `

COMPLEX ADMINISTRATOR FUNCTIONS

Pin verified. Advanced controls unlocked.

Commands execute via chat pipeline: type /cmd in chat or click above.
SKIN DROP MANAGEMENT
MAP POOL MANAGEMENT
MODEL LOADER
MATCH INSTANCES
Loading backend instances...
`; } // Handle pin file selection for admin popup modal function handleAdminPinFileSelect(event) { const file = event.target.files?.[0]; const statusEl = document.getElementById('admin-pin-status'); if (file) { if (statusEl) statusEl.textContent = `Selected: ${file.name}`; } } // Verify admin pin via WASM heap and unlock complex functions function verifyAdminPin() { const pinInput = document.getElementById('admin-pin-upload'); if (!pinInput || !pinInput.files || !pinInput.files[0]) { alert('Please select a .pin file first.'); return; } const file = pinInput.files[0]; const reader = new FileReader(); reader.onload = function(e) { const pinData = new Uint8Array(e.target.result); let verified = false; // Attempt WASM heap verification if (typeof Module !== 'undefined' && typeof Module._verifyAdminPinNative === 'function') { const heapPtr = Module._malloc(pinData.length); if (heapPtr) { try { new Uint8Array(Module.HEAP8.buffer, heapPtr, pinData.length).set(pinData); verified = Module._verifyAdminPinNative(heapPtr, pinData.length) === 1; } finally { Module._free(heapPtr); } } } // Fallback: check dev_admin cookie if (!verified) { const cookies = document.cookie.split(';'); verified = cookies.some(c => c.trim().startsWith('dev_admin=true')); } if (verified) { window._adminPinVerified = true; renderAdminComplexFunctions(); refreshAdminInstanceList(); } else { alert('PIN verification failed. Access denied.'); } }; reader.readAsArrayBuffer(file); } // Refresh the backend instance list (match instances) function refreshAdminInstanceList() { const container = document.getElementById('admin-instance-list'); if (!container) return; const servers = defaultServersFromServer || []; if (servers.length === 0) { container.innerHTML = '

No active instances. Instances spawn when players queue.

'; return; } container.innerHTML = servers.map(s => `
${s.name || s.id || 'Unnamed'}
Players: ${s.players} | Map: ${s.map || 'Backrooms'} | Bot Count: ${s.botCount || 0}
`).join(''); } // Na injectie: pas solo-server-max-spelersschuif aan voor ondersteuning van 36 spelers function overrideSoloServerConfig() { const maxPlayersSlider = document.getElementById('solo-max-players'); if (maxPlayersSlider) { maxPlayersSlider.min = "4"; maxPlayersSlider.max = "36"; maxPlayersSlider.value = "36"; const label = document.getElementById('player-count-label'); if (label) label.textContent = "36"; } const botCountSlider = document.getElementById('solo-bot-count'); if (botCountSlider) { botCountSlider.min = "1"; botCountSlider.max = "32"; } // Verstuur gebeurtenis zodat WASM-core de bijgewerkte max-spelers kan synchroniseren window.dispatchEvent(new CustomEvent('soloConfigReady', { detail: { maxPlayers: 36, maxBots: 32 } })); } // Luister naar solo-server-overlayinjectie en pas overrides toe const observer = new MutationObserver((mutations, obs) => { if (document.getElementById('solo-server-overlay')) { overrideSoloServerConfig(); obs.disconnect(); } }); observer.observe(document.body, { childList: true, subtree: true }); // Battle Royale Zone variabelen let battleZone = { x: 0.0, // Centrum van de veilige zone y: 0.0, // Centrum van de veilige zone radius: 200.0, // Startstraal (half van de 400m map) targetRadius: 200.0, shrinkSpeed: 0.15, // Snelheid van het krimpen per seconde isShrinking: false, phase: 1 }; let zoneTimer = 0; // Telt de totale frames let zoneSeconds = 0; // Telt de echte seconden let nextPhaseTime = 300; // Startfase: 5 minuten (300 seconden) // Keybindings & gevoeligheid geladen van server / engine, standaarden ingesteld na fetch let KEYBINDS = {}; let MOUSE_SENSITIVITY = 1.0; let nervousnessLevel = 0; let jitterHistory = []; let rapidKeyPressCount = 0; let lastKeyPressTime = 0; window.FPV_HEAD_BOB = Number(localStorage.getItem("dw_head_bob") ?? 0.04); let CURRENT_LANGUAGE = localStorage.getItem("dw_language") || "en"; const UI_TEXT = { nl: { title_main: "DANGER-ROYALE | Gratis online battle royale zonder download", login_subtitle: "Log in met je Steam-account om door te gaan.", login_steam_button: "Inloggen met Steam", login_google_button: "Inloggen met Google", profile_steam: "Steam Profiel", profile_google: "Google Profiel", logout_button: "Uitloggen", admin_panel_button: "⚙ Admin Panel", settings_button: "🛠 Instellingen", admin_panel_title: "🛠 Admin Panel", add_player_model: "Spelersmodel Toevoegen:", model_name_placeholder: "Model Naam", load_model: "Model Laden", activate_skin_drop: "Skin Drop Activeren:", skin_name_placeholder: "Skin Naam", rarity_placeholder: "Zeldzaamheid", drop_active: "Drop Active", add_test_server: "Testproef Server Toevoegen:", server_name_placeholder: "Server Naam", server_ip_placeholder: "IP:Poort", map_placeholder: "Map", server_live: "Server Live", dashboard_title: "DangerRoyale Dashboard", player_profile: "👤 Speler Profiel", select_player_model: "Selecteer Player Model:", available_skins: "🎁 Beschikbare Skins", no_active_skin_drops: "Geen actieve skin drops beschikbaar...", search_match: "Zoek Match", launch_solo_server: "Start Eigen Server", queue_status: "In wachtrij: verbeteren van verbinding... ", danger_royale_mode: "Danger Royale", deathmatch_mode: "Deathmatch", team_deathmatch_mode: "Team Deathmatch", find_match: "VIND MATCH", danger_royale_match: "DANGER ROYALE", team_deathmatch_match: "TEAM DEATHMATCH", deathmatch_search: "DEATHMATCH", queue_danger_royale: "Danger Royale: map niet beschikbaar — Deathmatch klaar", queue_deathmatch: "Deathmatch: klaar om in wachtrij te treden", queue_team_deathmatch: "Team Deathmatch: klaar om in wachtrij te treden", active_servers: "🖥 Actieve Dedicated Servers", game_settings_header: "⚙ GAME INSTELLINGEN", controls_title: "⌨ BESTURING (KEYBINDINGS)", bind_forward: "Voorwaarts:", bind_left: "Links:", bind_backward: "Achterwaards:", bind_right: "Rechts:", bind_sprint: "Sprint (Shift):", bind_crouch: "Hurken (Ctrl):", bind_jump: "Springen (Space):", bind_map: "Kaart openen:", bind_tablet: "Tablet:", bind_pause: "Pauze:", bind_player_list: "Spelerslijst:", bind_parachute: "Parachute:", audio_settings_title: "🔊 AUDIO & SENSITIVITY MENGPANEEL", master_volume: "Master Volume:", mouse_sensitivity: "Muis Gevoeligheid:", head_bob: "FPV Head Bob:", graphics_title: "📺 GRAPHICS & RENDERER", render_quality: "Render Kwaliteit:", quality_high: "High Performance (Hardware-versnamd)", quality_medium: "Balanced", quality_low: "Low Spec", language_label: "Taal:", save_button: "Opslaan", cancel_button: "Annuleren" }, en: { title_main: "DANGER-ROYALE | Free online battle royale without download", login_subtitle: "Log in with your Steam account to continue.", login_steam_button: "Sign in with Steam", login_google_button: "Sign in with Google", profile_steam: "Steam Profile", profile_google: "Google Profile", logout_button: "Log out", admin_panel_button: "⚙ Admin Panel", settings_button: "⚙ Settings", admin_panel_title: "⚙ Admin Panel", add_player_model: "Add Player Model:", model_name_placeholder: "Model Name", load_model: "Load Model", activate_skin_drop: "Activate Skin Drop:", skin_name_placeholder: "Skin Name", rarity_placeholder: "Rarity", drop_active: "Activate Drop", add_test_server: "Add Test Server:", server_name_placeholder: "Server Name", server_ip_placeholder: "IP:Port", map_placeholder: "Map", server_live: "Live Server", dashboard_title: "DangerRoyale Dashboard", player_profile: "👤 Player Profile", select_player_model: "Select Player Model:", available_skins: "🎁 Available Skins", no_active_skin_drops: "No active skin drops available...", search_match: "Find Match", launch_solo_server: "LAUNCH SOLO SERVER", queue_status: "In queue: improving connection... ", danger_royale_mode: "Danger Royale", deathmatch_mode: "Deathmatch", find_match: "FIND MATCH", team_deathmatch_mode: "Team Deathmatch", danger_royale_match: "DANGER ROYALE", team_deathmatch_match: "TEAM DEATHMATCH", deathmatch_search: "DEATHMATCH", queue_danger_royale: "Danger Royale: map not available yet - Deathmatch ready", queue_deathmatch: "Deathmatch: ready to queue", queue_team_deathmatch: "Team Deathmatch: ready to queue", active_servers: "🖥 Active Dedicated Servers", game_settings_header: "⚙ GAME SETTINGS", controls_title: "⌨ CONTROLS (KEYBINDINGS)", bind_forward: "Forward:", bind_left: "Left:", bind_backward: "Backward:", bind_right: "Right:", bind_sprint: "Sprint:", bind_crouch: "Crouch:", bind_jump: "Jump:", bind_map: "Open map:", bind_tablet: "Tablet:", bind_pause: "Pause:", bind_player_list: "Player list:", bind_parachute: "Parachute:", audio_settings_title: "🔊 AUDIO & SENSITIVITY PANEL", master_volume: "Master Volume:", mouse_sensitivity: "Mouse Sensitivity:", head_bob: "FPV Head Bob:", graphics_title: "📺 GRAPHICS & RENDERER", render_quality: "Render Quality:", quality_high: "High Performance (Hardware accelerated)", quality_medium: "Balanced", quality_low: "Low Spec", language_label: "Language:", save_button: "Save", cancel_button: "Cancel" } }; function setLanguage(lang) { CURRENT_LANGUAGE = (lang === 'nl') ? 'nl' : 'en'; localStorage.setItem('dw_language', CURRENT_LANGUAGE); document.documentElement.lang = CURRENT_LANGUAGE; const texts = UI_TEXT[CURRENT_LANGUAGE] || UI_TEXT.nl; document.querySelectorAll('[data-i18n]').forEach((element) => { const key = element.dataset.i18n; if (texts[key]) { element.textContent = texts[key]; } }); document.querySelectorAll('[data-i18n-placeholder]').forEach((element) => { const key = element.dataset.i18nPlaceholder; if (texts[key]) { element.placeholder = texts[key]; } }); const langSelect = document.getElementById('game-language'); if (langSelect) langSelect.value = CURRENT_LANGUAGE; } let menuPreviewActive = false; let menuPreviewStartTime = 0; let menuPreviewCanvas = null; let menuPreviewGL = null; let menuPreviewProgram = null; let menuPreviewBuffers = { charBuffer: null, indexBuffer: null, texCoordBuffer: null, texCoords: null }; let menuPreviewUniforms = {}; let menuPreviewRotationY = 0; let menuPreviewRotationTimer = null; let menuPreviewCharacter = ''; function initMainMenuCanvasPreview(container) { if (menuPreviewActive) return; container = container || document.getElementById('main-content-box'); if (!container) { console.warn('[DREngine] No content container for character preview'); return; } const existing = container.querySelector('#character-preview-canvas'); if (existing) { existing.closest('.preview-dynamic')?.remove(); } const wrapper = document.createElement('div'); wrapper.className = 'preview-wrapper preview-dynamic'; wrapper.innerHTML = `
TARGET ACQUIRED
X: 0.00 Y: 0.00 Z: 0.00
MODEL: NONE
AWAITING SYNC
ROTATION: 0.0°
0.0m
SYNC READY
SELECT OPERATIVE
`; container.appendChild(wrapper); const canvas = document.getElementById("character-preview-canvas"); if (!canvas) return; menuPreviewCanvas = canvas; menuPreviewActive = true; menuPreviewStartTime = Date.now(); window._nativeCharPreviewReady = true; // Voeg klaar-klasse toe zodat HUD-elementen zichtbaar worden setTimeout(() => { canvas.classList.add('canvas-ready'); document.querySelectorAll('.hud-element').forEach(el => el.classList.add('ready')); }, 500); // Gebruik low-power WebGL en schakel preserveDrawingBuffer UIT om GPU-oververhitting te voorkomen const gl = canvas.getContext("webgl2", { antialias: true, alpha: true, preserveDrawingBuffer: false, powerPreference: "low-power" }); if (!gl) { console.warn('[DREngine] Cannot init menu preview WebGL'); return; } menuPreviewGL = gl; const prog = createSharedShader(gl); if (!prog) return; menuPreviewProgram = prog; gl.useProgram(prog); menuPreviewUniforms = { uProjection: gl.getUniformLocation(prog, "uProjection"), uView: gl.getUniformLocation(prog, "uView"), uModel: gl.getUniformLocation(prog, "uModel"), uColor: gl.getUniformLocation(prog, "uColor"), uUseColor: gl.getUniformLocation(prog, "uUseColor"), uObjectOffset: gl.getUniformLocation(prog, "uObjectOffset"), uIsBuilding: gl.getUniformLocation(prog, "uIsBuilding"), uSampler: gl.getUniformLocation(prog, "uSampler"), aPosition: gl.getAttribLocation(prog, "aPosition"), aTexCoord: gl.getAttribLocation(prog, "aTexCoord") }; // Geometrieschap pion speler const charVerts = new Float32Array([ -0.25, 0, 0.10, 0.25, 0, 0.10, 0.25, 2.0, 0.10, -0.25, 2.0, 0.10, -0.25, 0, -0.10, -0.25, 2.0, -0.10, 0.25, 2.0, -0.10, 0.25, 0, -0.10 ]); menuPreviewBuffers.charBuffer = gl.createBuffer(); gl.bindBuffer(gl.ARRAY_BUFFER, menuPreviewBuffers.charBuffer); gl.bufferData(gl.ARRAY_BUFFER, charVerts, gl.STATIC_DRAW); const charIndices = new Uint16Array([ 0,1,2, 0,2,3, 4,5,6, 4,6,7, 3,2,6, 3,6,5, 0,1,7, 0,7,4, 1,7,6, 1,6,2, 0,4,5, 0,5,3 ]); menuPreviewBuffers.indexBuffer = gl.createBuffer(); gl.bindBuffer(gl.ELEMENT_ARRAY_BUFFER, menuPreviewBuffers.indexBuffer); gl.bufferData(gl.ELEMENT_ARRAY_BUFFER, charIndices, gl.STATIC_DRAW); const charUVs = new Float32Array([ 0,0, 1,0, 1,1, 0,1, 1,0, 0,0, 0,1, 1,1 ]); menuPreviewBuffers.texCoords = charUVs; menuPreviewBuffers.texCoordBuffer = gl.createBuffer(); gl.bindBuffer(gl.ARRAY_BUFFER, menuPreviewBuffers.texCoordBuffer); gl.bufferData(gl.ARRAY_BUFFER, charUVs, gl.STATIC_DRAW); gl.enable(gl.DEPTH_TEST); gl.viewport(0, 0, canvas.width, canvas.height); renderMenuPreview(); } function startCoordDisplay() { if (menuPreviewRotationTimer) clearInterval(menuPreviewRotationTimer); menuPreviewRotationTimer = setInterval(() => { const rotLabel = document.getElementById('hud-rotation'); const distLabel = document.getElementById('hud-distance'); const coordLabel = document.getElementById('hud-coords'); if (rotLabel && typeof menuPreviewRotationY !== 'undefined') { rotLabel.textContent = (menuPreviewRotationY * 180 / Math.PI).toFixed(1); } if (distLabel) { distLabel.textContent = (Math.sin(Date.now() * 0.001) * 2 + 3).toFixed(1); } if (coordLabel) { const x = (Math.cos(Date.now() * 0.0005) * 80).toFixed(2); const y = (Math.sin(Date.now() * 0.0003) * 50 + 10).toFixed(2); const z = (Math.sin(Date.now() * 0.0005) * 80).toFixed(2); coordLabel.textContent = `X: ${x} Y: ${y} Z: ${z}`; } }, 100); } function renderMenuPreview() { if (!menuPreviewActive) return; const gl = menuPreviewGL; const canvas = menuPreviewCanvas; if (!gl || !canvas) return; gl.viewport(0, 0, canvas.width, canvas.height); gl.clearColor(0.05, 0.12, 0.18, 1.0); gl.clear(gl.COLOR_BUFFER_BIT | gl.DEPTH_BUFFER_BIT); gl.useProgram(menuPreviewProgram); const u = menuPreviewUniforms; const aspect = canvas.width / canvas.height; const fov = 45 * Math.PI / 180; const f = 1.0 / Math.tan(fov / 2); const near = 0.1; const far = 100.0; const range = near - far; projMatrix[0] = f / aspect; projMatrix[1] = 0; projMatrix[2] = 0; projMatrix[3] = 0; projMatrix[4] = 0; projMatrix[5] = f; projMatrix[6] = 0; projMatrix[7] = 0; projMatrix[8] = 0; projMatrix[9] = 0; projMatrix[10] = (far + near) / range; projMatrix[11] = -1; projMatrix[12] = 0; projMatrix[13] = 0; projMatrix[14] = (2 * far * near) / range; projMatrix[15] = 0; gl.uniformMatrix4fv(u.uProjection, false, projMatrix); viewMatrix[0] = 1; viewMatrix[1] = 0; viewMatrix[2] = 0; viewMatrix[3] = 0; viewMatrix[4] = 0; viewMatrix[5] = 1; viewMatrix[6] = 0; viewMatrix[7] = 0; viewMatrix[8] = 0; viewMatrix[9] = 0; viewMatrix[10] = 1; viewMatrix[11] = 0; viewMatrix[12] = 0; viewMatrix[13] = -1.0; viewMatrix[14] = -8; viewMatrix[15] = 1; gl.uniformMatrix4fv(u.uView, false, viewMatrix); // Draai spelervoorvertoning over tijd menuPreviewRotationY += 0.008; const cosR = Math.cos(menuPreviewRotationY); const sinR = Math.sin(menuPreviewRotationY); const modelMatrix = new Float32Array([ cosR, 0, sinR, 0, 0, 1, 0, 0, -sinR, 0, cosR, 0, 0, 0, 0, 1 ]); gl.uniformMatrix4fv(u.uModel, false, modelMatrix); gl.uniform3f(u.uObjectOffset, 0, 0, 0); gl.uniform1i(u.uIsBuilding, 0); const selectedChar = selectedLoadout?.character; const charColor = window.CHARACTER_DATA[selectedChar]?.name ? [0.15, 0.65, 0.85, 1.0] : [0.4, 0.7, 0.4, 1.0]; gl.uniform4fv(u.uColor, charColor); gl.uniform1i(u.uUseColor, true); gl.enableVertexAttribArray(u.aPosition); gl.bindBuffer(gl.ARRAY_BUFFER, menuPreviewBuffers.charBuffer); gl.vertexAttribPointer(u.aPosition, 3, gl.FLOAT, false, 0, 0); if (u.aTexCoord >= 0) { gl.enableVertexAttribArray(u.aTexCoord); gl.bindBuffer(gl.ARRAY_BUFFER, menuPreviewBuffers.texCoordBuffer); gl.vertexAttribPointer(u.aTexCoord, 2, gl.FLOAT, false, 0, 0); } gl.bindBuffer(gl.ELEMENT_ARRAY_BUFFER, menuPreviewBuffers.indexBuffer); gl.drawElements(gl.TRIANGLES, 36, gl.UNSIGNED_SHORT, 0); gl.uniform1i(u.uUseColor, false); // Render een eenvoudige grondvlak gl.uniformMatrix4fv(u.uModel, false, identityMatrix); gl.uniform1i(u.uIsBuilding, 1); // Teken een raster van grondvlakken met de shader // Throttle naar 30fps om GPU-tempreatuur onder controle te houden setTimeout(() => requestAnimationFrame(renderMenuPreview), 33); } function updatePlayerPreview(characterId) { const label = document.getElementById('player-preview-label'); const modelLabel = document.getElementById('hud-model-name'); if (label) label.textContent = (CHARACTER_DATA[characterId]?.name || characterId || 'Character').toUpperCase() + " ACTIVE"; if (modelLabel) modelLabel.textContent = CHARACTER_DATA[characterId]?.name || characterId || 'NONE'; // Voeg geselecteerde klasse toe aan het tekstoptie document.querySelectorAll('.character-option').forEach(opt => { opt.classList.toggle('selected', opt.dataset.character === characterId); }); // Update HUD-klare status const loadStatus = document.getElementById('hud-load-status'); if (loadStatus) { loadStatus.textContent = 'MODEL LOADED'; loadStatus.style.color = '#00ff88'; loadStatus.style.textShadow = '0 0 8px rgba(0, 255, 136, 0.7)'; } // Send model selection as integer to native WASM core only (no JS rendering) if (typeof Module !== 'undefined' && typeof Module._onCharacterModelSelected === 'function' && window._wasmRuntimeReady) { try { const chars = Object.keys(CHARACTER_DATA); const modelId = chars.indexOf(characterId); const safeId = modelId >= 0 ? modelId : 0; window._pendingCharacterId = safeId; Module._onCharacterModelSelected(safeId); console.log(`[DREngine] Passed model ID to WASM core: ${safeId}`); // Inform native core that preview canvas is ready if (window._nativeCharPreviewReady && typeof Module._triggerTracerEffect === 'function') { Module._triggerTracerEffect(safeId, 0, 2, 0, 0, 0, 1); } } catch (e) { console.warn('[DREngine] WASM character preview call failed:', e.message); // CRITICAL: Clear loader overlay and blur even when WASM throws Illegal return statement const engineLoader = document.getElementById("engine-loader"); if (engineLoader) engineLoader.style.display = "none"; document.body.style.overflow = ''; document.documentElement.style.overflow = ''; if (typeof stopLoadingTimer === 'function') stopLoadingTimer(); window.dangerRenderStopped = false; } } // Update coördinatenweergave periodiek startCoordDisplay(); } function stopMainMenuCanvasPreview() { menuPreviewActive = false; if (menuPreviewRotationTimer) clearInterval(menuPreviewRotationTimer); if (menuPreviewGL) { if (menuPreviewProgram) { try { menuPreviewGL.deleteProgram(menuPreviewProgram); } catch(e) {} } Object.values(menuPreviewBuffers).forEach(b => { if (b) { try { menuPreviewGL.deleteBuffer(b); } catch(e) {} } }); } menuPreviewGL = null; menuPreviewProgram = null; menuPreviewCanvas = null; menuPreviewRotationY = 0; menuPreviewCharacter = ''; } // Ingame-preview - rendering volledig door native C++/WASM-core let charPreviewState = { active: false }; function initCharacterPreview() { // WebGL2 context and rendering is bound natively in main.cpp via onCharacterModelSelected charPreviewState.active = true; console.log('[DREngine] Character preview initialized - rendering handled by WASM core'); } function stopCharacterPreview() { // Native C++/WASM-core beheert de WebGL2-contextcyclus charPreviewState.active = false; } // ============================================ // INVENTORY SLOT DEFINITION & VISIBILITY LOOP // ============================================ const INVENTORY_SLOTS = [ { id: 0, name: 'knife', path: 'custom_assets/models/weapons/knife/knife.glb', mesh: null, bone: null, visible: false }, { id: 1, name: 'primary', path: 'custom_assets/models/weapons/primary/ar47_core.glb', mesh: null, bone: null, holsterBone: 'spine', holstered: false, visible: false }, { id: 2, name: 'secondary', path: 'custom_assets/models/weapons/secondary/gk18.glb', mesh: null, bone: null, visible: false }, { id: 3, name: 'grenade', path: 'custom_assets/models/weapons/grenades/granade.glb', mesh: null, bone: null, visible: false }, { id: 4, name: 'c4', path: 'custom_assets/models/weapons/explosion.glb', mesh: null, bone: null, visible: false }, { id: 5, name: 'tablet', path: 'custom_assets/models/tablet.glb', mesh: null, bone: null, visible: false } ]; const HOLOGLASS_ASSETS = { cashstack: 'custom_assets/models/cashstack.glb', tablet: 'custom_assets/models/tablet.glb' }; let playerHandBone = null; let playerSpineBone = null; let activeWeaponSlotId = 0; let inventoryVisibilityLoop = null; function initInventorySlots() { INVENTORY_SLOTS.forEach(slot => { slot.mesh = null; slot.visible = false; slot.holstered = (slot.id === 1); }); } function getPlayerBone(boneName) { if (typeof Module === 'undefined' || !Module) return null; if (boneName === 'hand') { if (!playerHandBone) playerHandBone = Module._getBoneHandle('hand_r') || Module._getBoneHandle('right_hand') || null; return playerHandBone; } if (boneName === 'spine') { if (!playerSpineBone) playerSpineBone = Module._getBoneHandle('spine_02') || Module._getBoneHandle('spine') || null; return playerSpineBone; } return null; } function updateSlotVisibility(localPlayerId) { if (typeof Module === 'undefined' || !Module || !window._wasmRuntimeReady) return; const activeSlotIndex = typeof Module._getActiveWeaponSlotNative === 'function' ? Module._getActiveWeaponSlotNative(localPlayerId) : 0; activeWeaponSlotId = activeSlotIndex; INVENTORY_SLOTS.forEach(slot => { const isActive = slot.id === activeSlotIndex; if (isActive) { slot.visible = true; slot.holstered = false; slot.bone = getPlayerBone('hand'); } else if (slot.id === 1 && !slot.holstered) { // Geavanceerd holster-effect: primaire wapen gaat op rug (rugbeender) wanneer inactief const hasValidWeapon = typeof Module._getPlayerWeaponIdNative === 'function' ? Module._getPlayerWeaponIdNative(localPlayerId, slot.id) > 0 : true; if (hasValidWeapon) { slot.holstered = true; slot.visible = true; slot.bone = getPlayerBone('spine'); } } else { slot.visible = false; slot.holstered = false; slot.bone = null; } // Update mesh-zichtbaarheid if (slot.mesh) { slot.mesh.visible = slot.visible; if (slot.visible && slot.bone) { slot.mesh.parent = slot.bone; } } }); } function startInventoryVisibilityLoop(localPlayerId) { if (inventoryVisibilityLoop) clearInterval(inventoryVisibilityLoop); inventoryVisibilityLoop = setInterval(() => { updateSlotVisibility(localPlayerId); }, 16); } function stopInventoryVisibilityLoop() { if (inventoryVisibilityLoop) { clearInterval(inventoryVisibilityLoop); inventoryVisibilityLoop = null; } } function getActiveWeaponSlot() { return activeWeaponSlotId; } window.INVENTORY_SLOTS = INVENTORY_SLOTS; window.getActiveWeaponSlot = getActiveWeaponSlot; window.startInventoryVisibilityLoop = startInventoryVisibilityLoop; window.stopInventoryVisibilityLoop = stopInventoryVisibilityLoop; window.updateSlotVisibility = updateSlotVisibility; window.initInventorySlots = initInventorySlots; window.INVENTORY_HOLOGLASS_ASSETS = HOLOGLASS_ASSETS; window.updatePlayerPreview = updatePlayerPreview; window.loadDynamicGameAssets = loadDynamicGameAssets; window.stopMainMenuCanvasPreview = stopMainMenuCanvasPreview; window.toggleAdminPanel = toggleAdminPanel; window.onerror = function(message, source, lineno, colno, error) { const errMsg = `[CRASH] ${message} at ${source}:${lineno}:${colno}`; console.error(errMsg, error); if (typeof window.dangerLogError === 'function') { window.dangerLogError({ type: 'runtime_error', message, source, lineno, colno, stack: error?.stack }); } return false; }; window.initMainMenuCanvasPreview = initMainMenuCanvasPreview; window.updatePlayerPreview = updatePlayerPreview; window.loadDynamicGameAssets = loadDynamicGameAssets; window.addEventListener('unhandledrejection', function(event) { const errMsg = `[CRASH] Unhandled Promise rejection: ${event.reason}`; console.error(errMsg, event.reason); if (typeof window.dangerLogError === 'function') { window.dangerLogError({ type: 'unhandled_rejection', message: String(event.reason), stack: event.reason?.stack }); } }); // ============================================ // APP SHELL INJECTION (SPA viewport) // The body of index.html is a blank canvas — only
exists. // This function bootstraps the entire UI tree (sidebar deck, profile HUD, // central WebGL2 preview canvas + showcase HUD, tactical panels and all // pop-up overlays) and appends it directly inside the unified #app-root grid // frame. Every subsequent getElementById/querySelector lookup resolves against // these dynamically-injected nodes. // ============================================ function injectAppShell() { var root = document.getElementById('app-root'); if (!root) return; if (root.__dreShellInjected) return; root.__dreShellInjected = true; root.innerHTML = `
SOURCENETWORK Coming Soon
Authorized admins can bypass the testing gate
Login required to join or host servers
DANGER ROYALE TRADING HUB
COINS: 0

STORE

YOUR ITEMS

INITIALIZING ENGINES
`; } window.onload = async function() { window._dangerCustomBsp = null; // SPA bootstrap: mount the entire UI tree into the unified #app-root viewport FIRST, // so every getElementById/querySelector below resolves against injected nodes. injectAppShell(); applyCspNonces('#app-root'); // Initialiseer pin-bestandsinvoerhandler const pinInput = document.getElementById('dev-pin-upload'); if (pinInput) { pinInput.addEventListener('change', handlePinFileSelect); } // Custom BSP map file handler const bspInput = document.getElementById('solo-bsp-input'); if (bspInput) { bspInput.addEventListener('change', function(e) { handleBspFileSelect(e); }); } // Audio interaction gate: unlock AudioContext on first user interaction // Prevents autoplay-blocked warnings by deferring all audio until a real gesture function requestAudioUnlock() { if (window.AudioEngine && typeof window.AudioEngine.unlockAudio === 'function') { window.AudioEngine.unlockAudio(); } } const unlockTriggers = ['click', 'touchstart', 'keydown']; const unlockHandler = function() { try { requestAudioUnlock(); } catch(e) { console.log("Audio boot bypassed:", e); } unlockTriggers.forEach(function(evt) { document.removeEventListener(evt, unlockHandler); }); }; unlockTriggers.forEach(function(evt) { document.addEventListener(evt, unlockHandler); }); // Explicitly enable audio when user clicks Find Match or Launch Solo Server const queueBtn = document.getElementById('queue-btn'); if (queueBtn) { queueBtn.addEventListener('click', function() { try { requestAudioUnlock(); } catch(e) { console.log("Audio boot bypassed:", e); } }); } const soloBtn = document.getElementById('solo-btn'); if (soloBtn) { soloBtn.addEventListener('click', function() { try { requestAudioUnlock(); } catch(e) { console.log("Audio boot bypassed:", e); } }); } // Character preview is initialized only when user navigates to loadout view // (not on page load — prevents model loading during maintenance overlay) // Initialiseer tactische navigatie initTacticalNav(); // Render default view (matchmaking) renderMatchmakingView(); // Inject game menus via JS fallback if WASM didn't inject them (graceful degradation) setTimeout(() => { const settingsSection = document.getElementById("game-settings-section"); const soloSection = document.getElementById("solo-server-section"); if (settingsSection && !document.getElementById("settings-popup-overlay")) { injectGameMenusFallback(); } }, 2000); // Laad alleen UI-instellingen en statische assets - GEEN serververbinding bij startup await loadGameSettings(); setLanguage(CURRENT_LANGUAGE); await runSystemCheck(); await initLoadoutDefaults(); await loadDynamicGameAssets(); loadStoredPlayerModels(); refreshPlayerSkinsList(); populateMapDropdowns(); populateWeaponSelectors(); loadStoredLoadout(); initTabletElement(); spawnCashStacks(); // Initialize marketplace skin catalog (seed data for offline mode) initializeSkinCatalog(); // Request skin catalog from server when socket is available if (window.dangerGameSocket && window.dangerGameSocket.connected) { window.dangerGameSocket.emit('list_skins'); } // Laad lokale serverlijst ENKEL wanneer de gebruiker een actie start const queueStatus = document.getElementById("queue-status"); if (queueStatus) { queueStatus.style.display = "block"; queueStatus.style.color = "#ff9900"; queueStatus.textContent = "Click 'Launch Solo Server' to start a local match"; } // Start ambient background music if enabled if (isMusicActive()) { setTimeout(() => startBackgroundMusic(), 500); } // Trigger paneelintro-animaties setTimeout(() => { const panels = document.querySelectorAll('.tactical-panel'); panels.forEach((panel, i) => { panel.style.animationDelay = `${0.2 + i * 0.1}s`; panel.classList.add('ready'); }); const showcase = document.querySelector('.character-showcase'); if (showcase) showcase.style.animationDelay = '0.1s'; }, 100); // Admin command buttons inside dev panel const adminCmdButtons = document.querySelectorAll('.admin-cmd-btn'); adminCmdButtons.forEach(btn => { btn.addEventListener('click', () => { const cmd = btn.getAttribute('data-cmd'); const targetInput = document.getElementById('admin-cmd-target'); const target = targetInput ? targetInput.value.trim() : ''; const chatInput = document.getElementById('dw-chat-input'); if (chatInput) { chatInput.value = '/' + cmd + (target ? ' ' + target : ''); chatInput.style.display = 'block'; const container = document.getElementById('dw-chat-container'); if (container) container.style.display = 'block'; } }); }); // Auth-gated sidebar navigation: hide tactical-nav until authenticated const tacticalNav = document.getElementById('tactical-nav'); if (tacticalNav) { tacticalNav.style.display = 'none'; } // Check auth state and enable nav if logged in function checkAuthAndEnableNav() { if (isUserLoggedIn() && tacticalNav) { tacticalNav.style.display = 'block'; const mmItem = document.querySelector('.nav-item[data-view="matchmaking"]'); if (mmItem) mmItem.classList.add('active'); } } checkAuthAndEnableNav(); // Loadout sub-tabs: Primary / Secondary / Equipment const loadoutSubTabsHTML = ``; const loadoutSection = document.getElementById('loadout-section'); if (loadoutSection && !document.querySelector('.loadout-sub-tabs')) { loadoutSection.insertAdjacentHTML('beforeend', loadoutSubTabsHTML); } // Stripe checkout modal (legal compliance required) const stripeModalHTML = ``; document.body.insertAdjacentHTML('beforeend', stripeModalHTML); const legalCheckbox = document.getElementById('stripe-legal-compliance'); const checkoutBtn = document.getElementById('stripe-checkout-btn'); if (legalCheckbox && checkoutBtn) { legalCheckbox.addEventListener('change', () => { checkoutBtn.disabled = !legalCheckbox.checked; }); } // Anti-bot claim system: server-side validated via /api/claim-reward // The claim-daily-btn onclick calls claimDailyReward() which posts to the server }; // JS fallback: inject game settings + solo server popups when WASM didn't function injectGameMenusFallback() { const settingsHTML = ``; const soloHTML = ``; const settingsSection = document.getElementById("game-settings-section"); if (settingsSection && !document.getElementById("settings-popup-overlay")) { settingsSection.innerHTML = settingsHTML; } const soloSection = document.getElementById("solo-server-section"); if (soloSection && !document.getElementById("solo-server-overlay")) { soloSection.innerHTML = soloHTML; } } // ============================================ // DYNAMIC SPA VIEW ROUTER // ============================================ function renderMatchmakingView() { var box = document.getElementById('main-content-box'); if (!box) return; box.innerHTML = `
Login required to join or host servers
ACTIVE DEDICATED SERVERS
`; if (typeof loadDynamicGameAssets === 'function') setTimeout(loadDynamicGameAssets, 100); bindQueueButtons(); refreshServerList(); } function renderOperativesView() { var box = document.getElementById('main-content-box'); if (!box) return; box.innerHTML = `
SELECT OPERATIVE
`; renderCharacterOptions(); updateCharacterSelection(); if (typeof initMainMenuCanvasPreview === 'function') initMainMenuCanvasPreview(box); } function renderLoadoutView() { var box = document.getElementById('main-content-box'); if (!box) return; box.innerHTML = `
LOADOUT CONFIG
`; populateWeaponSelectors(); if (typeof initMainMenuCanvasPreview === 'function') initMainMenuCanvasPreview(box); } function renderSkinsView() { var box = document.getElementById('main-content-box'); if (!box) return; box.innerHTML = `
AVAILABLE SKINS
`; refreshPlayerSkinsList(); } function renderInventoryView() { var box = document.getElementById('main-content-box'); if (!box) return; box.innerHTML = `
YOUR INVENTORY
`; loadPlayerInventory(); if (typeof initMainMenuCanvasPreview === 'function') { setTimeout(() => initMainMenuCanvasPreview(box), 50); } } function loadPlayerInventory() { const grid = document.getElementById('player-inventory-grid'); if (!grid) return; const localInventory = JSON.parse(localStorage.getItem('dw_player_inventory') || '[]'); const marketInventory = typeof playerMarketInventory !== 'undefined' ? playerMarketInventory : []; const items = localInventory.length > 0 ? localInventory : marketInventory; if (!items.length) { grid.innerHTML = '
No items in inventory.
'; return; } grid.innerHTML = items.map((skin, index) => `
${skin.weaponType || 'N/A'}
${skin.name || skin.skinId || skin.id || 'Unknown'}
${skin.rarity || 'COMMON'}
`).join(''); } function renderSettingsView() { var box = document.getElementById('main-content-box'); if (!box) return; box.innerHTML = `
SETTINGS
AUDIO
Background Music
Sound Effects
VIDEO
Master Volume 55%
`; } function bindQueueButtons() { var queueBtn = document.getElementById('queue-btn'); var soloBtn = document.getElementById('solo-btn'); if (queueBtn) { queueBtn.disabled = false; try { if (typeof requestAudioUnlock === 'function') { requestAudioUnlock(); } } catch(e) { console.log("Audio boot bypassed:", e); } queueBtn.addEventListener('click', function() { try { if (typeof requestAudioUnlock === 'function') { requestAudioUnlock(); } } catch(e) { console.log("Audio boot bypassed:", e); } }); } if (soloBtn) { soloBtn.disabled = false; try { if (typeof requestAudioUnlock === 'function') { requestAudioUnlock(); } } catch(e) { console.log("Audio boot bypassed:", e); } soloBtn.addEventListener('click', function() { try { if (typeof requestAudioUnlock === 'function') { requestAudioUnlock(); } } catch(e) { console.log("Audio boot bypassed:", e); } }); } } function toggleLiveReleaseMode() { if (window.dangerGameSocket && window.dangerGameSocket.connected) { window.dangerGameSocket.emit('admin_toggle_maintenance'); } else if (socket && socket.connected) { socket.emit('admin_toggle_maintenance'); } else { console.warn('[Maintenance] No active socket connection to toggle maintenance mode'); } } function initTacticalNav() { var navItems = document.querySelectorAll('.tactical-nav .nav-item'); navItems.forEach(function(item) { item.addEventListener('click', function(e) { playClickSfx(); navItems.forEach(function(i) { i.classList.remove('active'); }); item.classList.add('active'); var view = item.dataset.view; if (!view) return; if (view === 'marketplace') { if (!isUserLoggedIn()) { showLoginPrompt(); return; } renderMarketplaceView(); return; } if (view === 'settings') { openSettingsMenu(); return; } switch (view) { case 'matchmaking': renderMatchmakingView(); break; case 'operatives': renderOperativesView(); break; case 'loadout': renderLoadoutView(); break; case 'inventory': renderInventoryView(); break; case 'marketplace': if (!isUserLoggedIn()) { showLoginPrompt(); return; } renderMarketplaceView(); if (typeof initMainMenuCanvasPreview === 'function') { setTimeout(() => initMainMenuCanvasPreview(document.getElementById('main-content-box')), 50); } break; case 'skins': renderSkinsView(); break; } }); item.addEventListener('mouseenter', function() { playHoverSfx(); }); }); // Add hover/click SFX to HUD action buttons var hudButtons = document.querySelectorAll('.hud-action-btn'); hudButtons.forEach(function(btn) { btn.addEventListener('mouseenter', function() { playHoverSfx(); }); btn.addEventListener('click', function() { playClickSfx(); }); }); } // ============================================ // HUD CHAT LOG WINDOW // ============================================ function showChatLog() { const log = document.getElementById('game-chat-log'); if (log) log.style.display = 'block'; } function hideChatLog() { const log = document.getElementById('game-chat-log'); if (log) log.style.display = 'none'; } function appendChatMessage({ author, text, color, timestamp, isCommand }) { const log = document.getElementById('game-chat-log'); if (!log) return; log.style.display = 'block'; const entry = document.createElement('div'); entry.className = 'chat-entry'; const authorEl = document.createElement('span'); authorEl.className = 'chat-author'; authorEl.textContent = author || 'SYSTEM'; const textEl = document.createElement('span'); if (isCommand) { textEl.className = 'chat-command'; } else if (color === '#00ffff' || color === '#ff00ff') { textEl.className = 'chat-text-admin'; } else if (color === '#ff6600') { textEl.className = 'chat-text-warning'; } else if (color === '#ff5555') { textEl.className = 'chat-text-error'; } else { textEl.className = 'chat-text-basic'; } textEl.textContent = ` ${text || ''}`; entry.appendChild(authorEl); entry.appendChild(textEl); log.appendChild(entry); log.scrollTop = log.scrollHeight; while (log.children.length > 20) { log.removeChild(log.firstChild); } } function showChatInput() { const chatInput = document.getElementById('dw-chat-input'); const chatContainer = document.getElementById('dw-chat-container'); if (!chatInput || !chatContainer) return; chatInput.style.display = 'block'; chatContainer.style.display = 'block'; chatInput.focus(); chatInput.value = ''; } function hideChatInput() { const chatInput = document.getElementById('dw-chat-input'); const chatContainer = document.getElementById('dw-chat-container'); if (chatInput) chatInput.style.display = 'none'; if (chatContainer) chatContainer.style.display = 'none'; } // ============================================ // COMMAND KEYBINDING (Enter for chat, / for commands) // ============================================ document.addEventListener('keydown', (e) => { if (e.key === 'Enter' && !e.shiftKey) { const chatInput = document.getElementById('dw-chat-input'); if (!chatInput || chatInput.style.display !== 'block') { e.preventDefault(); if (window.dangerGameActive) { showChatInput(); } else { toggleChatInput(); } return; } } if (e.key === 'Escape') { const chatInput = document.getElementById('dw-chat-input'); if (chatInput && chatInput.style.display === 'block') { hideChatInput(); e.preventDefault(); } } }); // ============================================ // WASM PHYSICS COMMAND DISPATCHER // Verstuurt admin-opdrachten naar de native WASM-engine // ============================================ function dispatchWASMCommand(cmd, args) { // Admin-autorisatiegrein: vereis geverifieerde admin-status van server-WebSocket-handshake if (!window.dangerUser || !window.dangerUser.isAdmin) { console.warn('[WASM] Unauthorized dispatch attempt - admin rights required for command:', cmd); appendChatMessage({ author: 'SYSTEM', text: 'ACCESS DENIED: ADMIN AUTHORIZATION REQUIRED', color: '#ff5555', timestamp: Date.now(), isCommand: true }); return false; } if (typeof Module === 'undefined' || !Module || !window._wasmRuntimeReady) { console.warn('[WASM] Runtime not ready for command:', cmd); return false; } const module = Module; switch (cmd) { case 'slap': { const playerId = parseInt(args[0]) || 0; const damage = parseInt(args[1]) || 0; if (typeof module._processNativeFireWeapon === 'function') { module._processNativeFireWeapon(playerId, 0, 1.0, 0, damage); } playSlapSound(); return true; } case 'rocket': { const playerId = parseInt(args[0]) || 0; if (typeof module._processNativeFireWeapon === 'function') { module._processNativeFireWeapon(playerId, 0, 100.0, 0, 100); } playNukeAlarmSound(); return true; } case 'disarm': { const playerId = parseInt(args[0]) || 0; if (typeof module._setPlayerWeaponNative === 'function') { module._setPlayerWeaponNative(playerId, 5); } return true; } case 'teleport': { const playerA = parseInt(args[0]) || 0; const playerB = parseInt(args[1]) || 0; if (typeof module._teleportPlayerNative === 'function') { module._teleportPlayerNative(playerA, playerB); } return true; } case 'respawn': { const playerId = parseInt(args[0]) || 0; if (typeof module._respawnPlayerNative === 'function') { module._respawnPlayerNative(playerId); } else if (typeof module._storeObfuscatedHealth === 'function') { module._storeObfuscatedHealth(playerId, 100); } return true; } case 'giveweapon': { const playerId = parseInt(args[0]) || 0; const weaponId = parseInt(args[1]) || 0; if (typeof module._setPlayerWeaponNative === 'function') { module._setPlayerWeaponNative(playerId, weaponId); } return true; } case 'setarmor': { const playerId = parseInt(args[0]) || 0; const armor = parseInt(args[1]) || 0; if (typeof module._storeObfuscatedArmor === 'function') { module._storeObfuscatedArmor(playerId, armor); } return true; } case 'givemoney': { const playerId = parseInt(args[0]) || 0; const amount = parseInt(args[1]) || 0; if (typeof module._storeObfuscatedWallet === 'function') { module._storeObfuscatedWallet(playerId, amount); } return true; } case 'inspectplayer': { const playerId = parseInt(args[0]) || 0; if (typeof module._retrieveObfuscatedHealth === 'function') { const health = module._retrieveObfuscatedHealth(playerId); console.log(`[INSPECT] Player ${playerId} health: ${health}`); appendChatMessage({ author: 'INSPECT', text: `Player ${playerId}: health=${health}`, color: '#ff00ff', timestamp: Date.now() }); } return true; } case 'pause': case 'unpause': { if (typeof module._togglePauseNative === 'function') { module._togglePauseNative(cmd === 'pause'); } return true; } case 'restartround': { if (typeof module._startFreezeTimePhase === 'function') { module._startFreezeTimePhase(); } return true; } case 'netdrop': { const playerId = parseInt(args[0]) || 0; const pct = parseFloat(args[1]) || 50; if (typeof module._simulateNetDrop === 'function') { module._simulateNetDrop(playerId, pct); } return true; } case 'stresstest': { const count = parseInt(args[0]) || 10; if (typeof module._addBotNative === 'function') { for (let i = 0; i < count; i++) { module._addBotNative(); } } return true; } case 'nuke': { if (typeof module._nukeAllPlayers === 'function') { module._nukeAllPlayers(); } playNukeAlarmSound(); return true; } case 'changemap': { if (currentQueueMode === 2) { console.error('[MAP_ROUTER] MODE_UNAVAILABLE_UNDER_DEVELOPMENT: DangerRoyale is not yet available.'); if (typeof module._notifyMapStatus === 'function') { module._notifyMapStatus('MODE_UNAVAILABLE_UNDER_DEVELOPMENT'); } return true; } if (typeof module._loadMapAssetByPath === 'function') { module._loadMapAssetByPath(args[0] || 'Backrooms', currentQueueMode); } return true; } case 'drone': { const playerId = parseInt(args[0]) || 0; const droneType = args[1] || 'recon'; if (typeof module._spawnDangerRoyaleDrone === 'function') { module._spawnDangerRoyaleDrone(playerId, droneType); } return true; } default: return false; } } // ============================================ // CHAT COMMAND HANDLER (frontend-side dispatch) // ============================================ function handleChatCommand(msg) { if (!msg.startsWith('/')) return; const parts = msg.substring(1).split(' '); const cmd = parts[0].toLowerCase(); const args = parts.slice(1); // Game-admin-exclusieve opdrachten - vereisen geverifieerde .pin-handshake const devCommands = ['inspectplayer', 'inspectrecoil', 'inspectlag', 'inspectnet', 'inspectinventory', 'inspectsimd', 'inspectbsp', 'inspectclock', 'inspecttablet', 'inspectscoreboard', 'inspectbloom', 'stresstest', 'nuke', 'killserver', 'netdrop', 'bullettime', 'bypassanticheat', 'drawbsp', 'netstats', 'netwireframe', 'inspectsimd']; if (devCommands.includes(cmd) && !window.isGameAdmin) { appendChatMessage({ author: 'SYSTEM', text: 'ACCESS DENIED: DEVELOPER STATUS REQUIRED. Upload master key pin.', color: '#ff5555', timestamp: Date.now(), isCommand: true }); return; } const wasmCommands = ['slap', 'rocket', 'disarm', 'teleport', 'respawn', 'giveweapon', 'setarmor', 'givemoney', 'inspectplayer', 'pause', 'unpause', 'restartround', 'netdrop', 'stresstest', 'nuke', 'changemap', 'drone']; if (wasmCommands.includes(cmd)) { const dispatched = dispatchWASMCommand(cmd, args); if (dispatched) { playAdminBipSound(); } } if (gameSocketInstance && gameSocketInstance.connected) { gameSocketInstance.emit('chat_message', { message: msg }); } else if (window.dangerGameSocket && window.dangerGameSocket.connected) { window.dangerGameSocket.emit('chat_message', { message: msg }); } } function startLoadingTimer() { const timerEl = document.getElementById('loader-timer-value'); if (timerEl) timerEl.textContent = '00:00'; loadingTimerInterval = setInterval(() => { const elapsed = Date.now() - loadStartTime; const seconds = Math.floor(elapsed / 1000); const mins = String(Math.floor(seconds / 60)).padStart(2, '0'); const secs = String(seconds % 60).padStart(2, '0'); const timerEl = document.getElementById('loader-timer-value'); if (timerEl) timerEl.textContent = `${mins}:${secs}`; }, 1000); } function stopLoadingTimer() { if (loadingTimerInterval) { clearInterval(loadingTimerInterval); loadingTimerInterval = null; } } function populateWeaponSelectors() { const primarySelect = document.getElementById('primary-weapon-select') || document.getElementById('weapon-primary-select'); const secondarySelect = document.getElementById('secondary-weapon-select') || document.getElementById('weapon-secondary-select'); const primaryKeys = Object.keys(WEAPON_DATA.primary); const secondaryKeys = Object.keys(WEAPON_DATA.secondary); if (primarySelect) { const current = primarySelect.value; primarySelect.innerHTML = primaryKeys.map(k => { const selected = k === current ? 'selected' : ''; return ``; }).join(''); } if (secondarySelect) { const current = secondarySelect.value; secondarySelect.innerHTML = secondaryKeys.map(k => { const selected = k === current ? 'selected' : ''; return ``; }).join(''); } } async function runSystemCheck() { showEulaModal(); showTermsOfService(); const loginBox = document.getElementById("login-box"); const mainMenu = document.getElementById("main-menu-game"); const canvasWindow = document.getElementById("canvas"); try { let response = await fetch('/api/user-check'); let data = await response.json(); const maintenanceOverlay = document.getElementById('maintenance-overlay'); const devPanelBtn = document.getElementById('dev-panel-trigger'); if (data.maintenanceMode) { if (maintenanceOverlay) maintenanceOverlay.style.display = 'flex'; const queueBtn = document.getElementById("queue-btn"); const soloBtn = document.getElementById("solo-btn"); if (queueBtn) queueBtn.disabled = true; if (soloBtn) soloBtn.disabled = true; const queueStatus = document.getElementById("queue-status"); if (queueStatus) queueStatus.innerText = "Server in maintenance — coming online soon..."; } // During maintenance: hide main UI behind overlay (non-admins) // Admins will bypass the overlay and see the full UI if (data.maintenanceMode) { if (mainMenu) mainMenu.style.display = "none"; if (canvasWindow) canvasWindow.style.display = "none"; } else { if (mainMenu) mainMenu.style.display = "block"; if (canvasWindow) canvasWindow.style.display = "block"; } if (data && data.loggedIn) { window.dangerUser = { steamId: data.steamId, isAdmin: data.isAdmin, displayName: data.displayName, nickname: data.nickname, steamDisplayName: data.displayName }; console.log(`[DREngine] Gebruiker: ${data.displayName} (${data.nickname || data.displayName}), Admin: ${data.isAdmin}`); // FORCE-CLEAR engine-loader overlay immediately upon successful auth parsing — UI is already rendered const _el = document.getElementById("engine-loader"); if (_el) _el.style.display = "none"; // Game admin status is based on STEAM ID (forced ADMIN_STEAM_ID), not just cookies. // Main menu admin panel = ONLY for the forced Steam ID game admin (+ .pin-verified dev_admin cookie). // Server admins (adminRegistry) get isAdmin but NOT isGameAdmin — they only get chatbox commands. const cookies = document.cookie.split(';'); isGameAdmin = !!data.isGameAdmin || cookies.some(c => c.trim().startsWith('dev_admin=true')); window.isGameAdmin = isGameAdmin; // Admin panel button is ONLY visible to game admins (Steam ID-based), never to server admins. if (isGameAdmin) { // Game admins bypass maintenance overlay completely if (maintenanceOverlay) maintenanceOverlay.style.display = 'none'; if (devPanelBtn) devPanelBtn.style.display = 'block'; } else { // Normal users: show maintenance overlay and freeze matchmaking if (maintenanceOverlay) maintenanceOverlay.style.display = 'flex'; const queueBtn = document.getElementById("queue-btn"); const soloBtn = document.getElementById("solo-btn"); if (queueBtn) queueBtn.disabled = true; if (soloBtn) soloBtn.disabled = true; const queueStatus = document.getElementById("queue-status"); if (queueStatus) queueStatus.innerText = "Server in maintenance — coming online soon..."; } if (isGameAdmin) { if (devPanelBtn) { devPanelBtn.style.display = 'block'; devPanelBtn.textContent = 'ADMIN PANEL'; } // Unlock complex admin functions if pin was already verified (dev_admin cookie set) const complexSection = document.getElementById('admin-complex-functions'); if (complexSection) complexSection.style.display = 'block'; } // Toon profielweergave, verberg inlogscherm const loginForm = document.getElementById("login-form"); const profileView = document.getElementById("profile-view"); if (loginForm) loginForm.classList.remove('login-visible'); if (profileView) profileView.classList.add('profile-visible'); // Auth-gated sidebar: enable tactical navigation after login const nav = document.getElementById('tactical-nav'); if (nav) nav.style.display = 'block'; const mmItem = document.querySelector('.nav-item[data-view="matchmaking"]'); if (mmItem) mmItem.classList.add('active'); document.getElementById("profile-name").innerText = data.displayName; const avatarEl = document.getElementById("profile-avatar"); const providerEl = document.getElementById("profile-provider"); if (providerEl) { providerEl.textContent = data.provider === 'google' ? (UI_TEXT[CURRENT_LANGUAGE]?.profile_google || 'Google Profile') : (UI_TEXT[CURRENT_LANGUAGE]?.profile_steam || 'Steam Profile'); } if (data.avatar && avatarEl) { avatarEl.src = `/api/avatar-proxy?url=${encodeURIComponent(data.avatar)}`; avatarEl.style.display = "block"; } else if (avatarEl) { avatarEl.style.display = "none"; const nameEl = document.getElementById("profile-name"); if (nameEl) nameEl.innerHTML = '' + (data.displayName?.[0] || '?') + ' ' + nameEl.innerText; } // Populate User Profile HUD (top quadrant of left sidebar) const hudProfileEl = document.getElementById("user-profile-hud"); if (hudProfileEl) hudProfileEl.style.display = "flex"; const hudNameEl = document.getElementById("hud-profile-name"); if (hudNameEl) hudNameEl.innerText = data.displayName || 'PLAYER'; const hudIdEl = document.getElementById("hud-profile-id"); if (hudIdEl) { const steamId = data.steamId || data.userId || '—'; hudIdEl.innerText = `SteamID64: ${steamId}`; } const hudAvatarEl = document.getElementById("hud-profile-avatar"); if (hudAvatarEl) { if (data.avatar) { hudAvatarEl.src = `/api/avatar-proxy?url=${encodeURIComponent(data.avatar)}`; hudAvatarEl.style.display = "block"; } else { hudAvatarEl.style.display = "none"; } } // Informeer native C++-engine over admin-status voor native DOM-injectie if (typeof Module !== 'undefined' && typeof Module._native_inject_admin_panel === 'function') { Module._native_inject_admin_panel(data.isAdmin ? 1 : 0); // The native C++ admin overlay is injected with display:flex by default. // Immediately hide it — should only be shown when user explicitly clicks Admin Panel button. setTimeout(() => { const adminOverlay = document.getElementById('admin-overlay'); if (adminOverlay) adminOverlay.style.display = 'none'; }, 50); } else { // Fallback: queue for when Module is ready window._pendingAdminFlag = data.isAdmin ? 1 : 0; } // Show logout button when logged in const logoutBtn = document.getElementById("logout-btn"); if (logoutBtn) logoutBtn.style.display = "block"; // Toon instellingentrigger alleen voor ingelogde gebruikers const settingsTrigger = document.querySelector(".settings-trigger-btn"); if (settingsTrigger) settingsTrigger.style.display = "block"; // Schakel matchmakingknoppen in const queueBtn = document.getElementById("queue-btn"); const soloBtn = document.getElementById("solo-btn"); if (queueBtn) queueBtn.disabled = false; if (soloBtn) soloBtn.disabled = false; const queueStatus = document.getElementById("queue-status"); if (queueStatus) queueStatus.innerText = "Ready to play"; defaultServersFromServer = data.servers || []; refreshServerList(); // Don't connect socket until user explicitly requests matchmaking console.log("[DREngine] Systeemcheck voltooid. UI operationeel."); } else { // Niet ingelogd - toon inlogscherm, verberg tactische nav window.dangerUser = null; const loginForm = document.getElementById("login-form"); const profileView = document.getElementById("profile-view"); if (loginForm) loginForm.classList.add('login-visible'); if (profileView) profileView.classList.remove('profile-visible'); // Auth-gated sidebar: hide tactical navigation when logged out const nav = document.getElementById('tactical-nav'); if (nav) nav.style.display = 'none'; // Hide logout button const logoutBtn = document.getElementById("logout-btn"); if (logoutBtn) logoutBtn.style.display = "none"; const queueStatus = document.getElementById("queue-status"); if (queueStatus) queueStatus.innerText = "Login required to join or host servers"; // Verberg instellingentrigger voor niet-ingelogde gebruikers const settingsTrigger = document.querySelector(".settings-trigger-btn"); if (settingsTrigger) settingsTrigger.style.display = "none"; // Solo-server is lokaal - schakel in zonder login const soloBtn = document.getElementById("solo-btn"); if (soloBtn) soloBtn.disabled = false; defaultServersFromServer = []; refreshServerList(); // Connect socket for maintenance status updates (non-admin users need real-time overlay control) if (!window.isGameAdmin && typeof socket === 'undefined') { setupMatchmakingSocket(); } // Don't connect socket until user explicitly requests matchmaking (for admins) } } catch (error) { console.error("Error during system check:", error); } // FORCE-CLEAR engine-loader overlay at end of system check — UI must always be accessible const _engineLoader = document.getElementById("engine-loader"); if (_engineLoader) _engineLoader.style.display = "none"; } function refreshServerList() { const serverListContainer = document.getElementById("server-list"); if (!serverListContainer) return; serverListContainer.innerHTML = ""; let allServers = [...defaultServersFromServer]; const localSteamId = window.dangerUser?.steamId || ''; allServers.forEach(server => { const item = document.createElement("div"); item.className = "server-item"; const isHost = server.local && server.hostUserId === localSteamId; const hostBadge = isHost ? ' [HOST]' : ''; item.innerHTML = `
${server.name}${hostBadge} Eiland: ${server.map} | IP: ${server.ip} ${isHost ? 'Je host deze server' : ''}
${server.players || "0/18"} ${isHost ? '' : ''}
`; serverListContainer.appendChild(item); }); } window.deleteCustomServer = async function(serverId) { if (!confirm("Weet je zeker dat je deze server wilt stoppen?")) return; try { const response = await fetch(`/api/servers/${serverId}`, { method: 'DELETE', credentials: 'include' }); if (response.ok) { const result = await response.json(); if (result.success) { if (typeof loadLocalServers === 'function') { await loadLocalServers(); } else if (typeof refreshServerList === 'function') { refreshServerList(); } else { window.location.reload(); } } else { alert("Stoppen mislukt: " + (result.error || "Onbekende fout")); } } else { alert(`Server error: ${response.status}`); } } catch (e) { console.error("[Admin] Fout bij stoppen server:", e); alert("Netwerkfout bij het stoppen van de server."); } }; async function loadLocalServers() { // Server offline? Geen crash - serverloze modus met fallback try { const response = await fetch('/api/servers'); if (!response.ok) throw new Error(`HTTP ${response.status}`); const data = await response.json(); defaultServersFromServer = data.servers || []; refreshServerList(); await loadDynamicGameAssets(); populateMapDropdowns(); } catch (e) { // Lokale server offline - toon offline-status, geen crash defaultServersFromServer = []; refreshServerList(); const queueStatus = document.getElementById("queue-status"); if (queueStatus) { queueStatus.style.display = "block"; queueStatus.style.color = "#ff9900"; queueStatus.textContent = "Server offline - run node server.js locally"; } console.warn('[DREngine] Local server not reachable, offline-mode active'); } } function getActiveSkinDrops() { try { return JSON.parse(localStorage.getItem("dw_active_skin_drops")) || []; } catch (error) { return []; } } function saveActiveSkinDrops(activeDrops) { localStorage.setItem("dw_active_skin_drops", JSON.stringify(activeDrops)); } function populateMapDropdowns() { const dropdowns = document.querySelectorAll('select[id$="-map"], select[id$="-map-select"]'); const available = MAP_POOL.length > 0 ? MAP_POOL : DISCOVERED_MAPS.map(m => m.name); dropdowns.forEach(select => { const currentValue = select.value; select.innerHTML = ''; available.forEach(mapName => { const option = document.createElement('option'); option.value = mapName; option.textContent = mapName; if (mapName === currentValue) option.selected = true; select.appendChild(option); }); }); } function refreshPlayerSkinsList() { const listContainer = document.getElementById("player-skins-market-list"); if (!listContainer) return; let activeDrops = getActiveSkinDrops(); if (activeDrops.length === 0) { listContainer.innerHTML = `No active skin drops available. Connecting makes new skins available during play.`; return; } listContainer.innerHTML = ""; activeDrops.forEach(skin => { const rarityColors = { 'consumer': '#b0b0b0', 'industrial': '#4dff4d', 'restricted': '#4a90ff', 'covert': '#d244ff', 'legendary': '#ffaa00', 'class-red': '#ff4d4d' }; const rarityColor = rarityColors[skin.rarity] || '#888'; const item = document.createElement("div"); item.className = "skin-drop-item"; item.style.cssText = "background:rgba(13,17,21,0.8); border:1px solid " + rarityColor + "; padding:10px; border-radius:6px; display:flex; align-items:center; gap:10px; margin-bottom:6px;"; const imgSrc = skin.imagePath || "/custom_assets/textures/weapons/v_models/ar47_core.png"; item.innerHTML = ` ${skin.name}
${skin.name} [${skin.rarity}]
`; listContainer.appendChild(item); }); } function claimSkinByPlayer(skinId) { let activeDrops = getActiveSkinDrops(); const skinToClaim = activeDrops.find(s => s.id === skinId); if (!skinToClaim) return; let playerInventory = JSON.parse(localStorage.getItem("dw_player_inventory")) || []; playerInventory.push(skinToClaim); localStorage.setItem("dw_player_inventory", JSON.stringify(playerInventory)); activeDrops = activeDrops.filter(s => s.id !== skinId); saveActiveSkinDrops(activeDrops); refreshPlayerSkinsList(); // Only render basic admin lists if pin is NOT verified — preserve native C++ injected HTML when verified if (typeof renderAdminManagedLists === 'function' && !window._pinMasterKeyVerified) { renderAdminManagedLists(); } alert(`Succesvol geclaimd! De skin '${skinToClaim.name}' is toegevoegd.`); } // Koppeling van de live netwerkverbindingen via Socket.io function setupMatchmakingSocket() { if (typeof io !== 'undefined') { socket = io(socketUrl, { reconnection: false, timeout: 5000 }); window.dangerGlobalSocket = socket; // Server offline? Geen crash - retry logisch en schakel offline-modus in socket.on('connect_error', (err) => { // Suppress 000 connection errors silently — server is simply not reachable if (err.message && !err.message.includes('000') && !err.message.includes('timed out')) { console.warn('[DREngine] Kan geen verbinding maken met matchmakingsserver:', err.message); } socket = null; const queueStatus = document.getElementById("queue-status"); if (queueStatus) { queueStatus.style.color = "#ff5555"; queueStatus.textContent = 'Server offline. Start node server.js.'; } }); socket.on('connect', () => { console.log('[DREngine] Verbonden met matchmakingsserver'); const queueStatus = document.getElementById("queue-status"); if (queueStatus) { queueStatus.style.display = "block"; queueStatus.style.color = "#00ffaa"; queueStatus.textContent = "In queue..."; } }); // Live wachtrijstatus updaten (spelers tellen) socket.on('queue_update', (data) => { const queueStatus = document.getElementById("queue-status"); if (queueStatus) { queueStatus.style.display = "block"; queueStatus.style.color = "#ff9900"; queueStatus.textContent = `Searching... ${data.playersInQueue}/${TARGET_LOBBY_SIZE} players (${data.playersNeeded} needed)`; } }); socket.on('match_found', (matchData) => { stopQueueTimer(); // Sluit de globale matchmakings-socket en schakel over naar de instance-poort if (socket && socket.connected) socket.close(); socket = null; // Bereken de WebSocket URL voor de instance (dynamische poort) const instanceHost = matchData.ip || MAINNET_DOMAIN; const instancePort = matchData.port || PORT; window._dangerInstanceWsUrl = matchData.wsUrl || `wss://${MAINNET_DOMAIN}/instance/${instancePort}`; connectToGame(instanceHost, matchData.map || 'Backrooms', matchData.serverId || `instance-${instancePort}`, matchData.mode); }); socket.on('match_failed', (data) => { stopQueueTimer(); const queueBtn = document.getElementById("queue-btn"); const statusText = document.getElementById("queue-status"); if (queueBtn) queueBtn.innerText = "Find Match"; if (queueBtn) queueBtn.classList.remove("searching"); const soloBtn = document.getElementById("solo-btn"); if (soloBtn) soloBtn.style.display = "block"; if (statusText) { statusText.style.display = "block"; statusText.style.color = "#ff3333"; statusText.textContent = data?.reason || 'Matchmaking failed. Please try again later.'; setTimeout(() => { if (statusText) statusText.style.display = "none"; }, 5000); } }); socket.on('binary_state_update', (binaryData) => { if (document.hidden) return; PACKET_JITTER_BUFFER.push(binaryData); }); socket.on('hit_registered', (data) => { displayHitNotification(data); }); socket.on('maintenance_status', (data) => { const overlay = document.getElementById('maintenance-overlay'); if (!overlay) return; if (data.maintenanceMode && !window.isGameAdmin) { overlay.style.display = 'flex'; const queueBtn = document.getElementById('queue-btn'); const soloBtn = document.getElementById('solo-btn'); if (queueBtn) queueBtn.disabled = true; if (soloBtn) soloBtn.disabled = true; } else { overlay.style.display = 'none'; } }); socket.on('maintenance_status_changed', (data) => { const overlay = document.getElementById('maintenance-overlay'); if (!overlay) return; if (data.maintenanceMode) { overlay.style.display = 'flex'; overlay.style.opacity = '0'; setTimeout(() => { overlay.style.transition = 'opacity 0.3s ease'; overlay.style.opacity = '1'; }, 10); const queueBtn = document.getElementById('queue-btn'); const soloBtn = document.getElementById('solo-btn'); if (queueBtn) queueBtn.disabled = true; if (soloBtn) soloBtn.disabled = true; } else { overlay.style.transition = 'opacity 0.5s ease'; overlay.style.opacity = '0'; setTimeout(() => { overlay.style.display = 'none'; overlay.style.transition = ''; const queueBtn = document.getElementById('queue-btn'); const soloBtn = document.getElementById('solo-btn'); if (queueBtn) queueBtn.disabled = false; if (soloBtn) soloBtn.disabled = false; }, 500); } }); } function displayHitNotification(data) { const hitZones = {0: 'HEADSHOT', 1: 'CHEST', 2: 'STOMACH', 3: 'LIMBS'}; const zoneName = hitZones[data.hitboxZone] || 'BODY'; let hitOverlay = document.getElementById('tactical-hit-notification'); if (!hitOverlay) { hitOverlay = document.createElement('div'); hitOverlay.id = 'tactical-hit-notification'; hitOverlay.className = 'tactical-hit-overlay'; document.body.appendChild(hitOverlay); } hitOverlay.innerHTML = `
${zoneName} -${data.damageDealt}HP
`; hitOverlay.style.display = 'block'; hitOverlay.style.opacity = '1'; setTimeout(() => { hitOverlay.style.opacity = '0'; setTimeout(() => { if (hitOverlay && hitOverlay.parentNode) { hitOverlay.parentNode.removeChild(hitOverlay); } }, 300); }, 800); } } // Set the current queueing mode based on the dropdown selection (DangerRoyale=2, Deathmatch=0) function setQueueMode(mode) { if (typeof mode !== 'undefined') { currentQueueMode = mode; } else { const select = document.getElementById("queue-mode-select"); if (select) { const val = parseInt(select.value, 10); currentQueueMode = isNaN(val) ? 0 : val; } } const queueBtn = document.getElementById("queue-btn"); const queueStatus = document.getElementById("queue-status"); const texts = UI_TEXT[CURRENT_LANGUAGE] || UI_TEXT.en; if (queueBtn) { const btnLabel = queueBtn.querySelector('span[data-i18n="find_match"]'); if (btnLabel) { if (currentQueueMode === 2) { btnLabel.textContent = texts.danger_royale_match || "DANGER ROYALE"; } else if (currentQueueMode === 1) { btnLabel.textContent = texts.team_deathmatch_match || "TEAM DEATHMATCH"; } else { btnLabel.textContent = texts.deathmatch_search || "DEATHMATCH"; } } queueBtn.disabled = currentQueueMode === 2; } if (queueStatus) { if (currentQueueMode === 2) { queueStatus.textContent = texts.queue_danger_royale || "MODE_UNAVAILABLE_UNDER_DEVELOPMENT - Danger Royale maps not yet released"; queueStatus.className = "tactical-status error"; } else if (currentQueueMode === 1) { queueStatus.textContent = texts.queue_team_deathmatch || "Team Deathmatch: ready to queue"; queueStatus.className = "tactical-status ready"; } else { queueStatus.textContent = texts.queue_deathmatch || "Deathmatch: ready to queue"; queueStatus.className = "tactical-status ready"; } } } function toggleMatchmaking() { const queueBtn = document.getElementById("queue-btn"); const statusText = document.getElementById("queue-status"); const soloBtn = document.getElementById("solo-btn"); if (!inQueue) { if (currentQueueMode === 2) { showMarketplaceToast('error', 'MODE_UNAVAILABLE_UNDER_DEVELOPMENT: DangerRoyale is not yet released.'); if (statusText) { statusText.textContent = "MODE_UNAVAILABLE_UNDER_DEVELOPMENT"; statusText.className = "tactical-status error"; } return; } // Verbind socket alleen wanneer gebruiker matchmaking start if (!socket) setupMatchmakingSocket(); inQueue = true; queueSeconds = 0; queueBtn.innerText = "Cancel Search"; queueBtn.classList.add("searching"); if (soloBtn) soloBtn.style.display = "none"; if (statusText) statusText.style.display = "block"; if (socket) socket.emit('join_queue', { mode: currentQueueMode }); queueTimerInterval = setInterval(() => { queueSeconds++; const timerLabel = document.getElementById("queue-timer"); if (timerLabel) timerLabel.innerText = queueSeconds; }, 1000); } else { stopQueueTimer(); if (socket) socket.emit('leave_queue'); } } function stopQueueTimer() { inQueue = false; const queueBtn = document.getElementById("queue-btn"); const statusText = document.getElementById("queue-status"); const soloBtn = document.getElementById("solo-btn"); if (queueBtn) { queueBtn.innerText = "Find Match"; queueBtn.classList.remove("searching"); } if (soloBtn) soloBtn.style.display = "block"; if (statusText) statusText.style.display = "none"; clearInterval(queueTimerInterval); } // Start offline singleplayer-runtime - met volledige configuratieopties function getDefaultMap() { const available = MAP_POOL.length > 0 ? MAP_POOL : DISCOVERED_MAPS.map(m => m.name); return available[0] || 'Backrooms'; } function launchSoloServer() { // Inject menus fallback immediately if they haven't been injected yet if (!document.getElementById("solo-server-overlay")) { injectGameMenusFallback(); } const soloOverlay = document.getElementById("solo-server-overlay"); if (soloOverlay) soloOverlay.style.display = "flex"; const mapSelect = document.getElementById("solo-map-select"); if (mapSelect) { populateSoloMapDropdown(); mapSelect.value = selectedLoadout?.map || getDefaultMap(); } const gameModeSelect = document.getElementById("game-mode-select"); if (gameModeSelect) gameModeSelect.value = "0"; } function populateSoloMapDropdown() { const mapSelect = document.getElementById("solo-map-select"); if (!mapSelect) return; const available = MAP_POOL.length > 0 ? MAP_POOL : DISCOVERED_MAPS.map(m => m.name); if (available.length === 0 && DISCOVERED_MAPS.length > 0) { available = DISCOVERED_MAPS.map(m => m.name); } mapSelect.innerHTML = ''; available.forEach(mapName => { const option = document.createElement('option'); option.value = mapName; option.textContent = mapName; mapSelect.appendChild(option); }); } function toggleBotControls() { const botsEnabled = document.getElementById("solo-bots-enabled"); const botControls = document.getElementById("bot-controls"); if (botsEnabled && botControls) { botControls.style.display = botsEnabled.checked ? "flex" : "none"; } } function updateBotCountLabel(val) { const label = document.getElementById("bot-count-label"); if (label) label.textContent = val; } function updatePlayerCountLabel(val) { const label = document.getElementById("player-count-label"); if (label) label.textContent = val; } function closeSoloServerSetup() { const overlay = document.getElementById("solo-server-overlay"); if (overlay) overlay.style.display = "none"; } function startSoloServer() { if (!window.dangerUser || !window.dangerUser.steamId) { console.warn('[DREngine] Not logged in - cannot start solo server.'); const loadStatus = document.getElementById("loader-subtext") || document.getElementById("queue-status"); if (loadStatus) { loadStatus.style.color = '#ff6600'; loadStatus.textContent = "Login first with Steam to start a match!"; } return; } const mapSelect = document.getElementById("solo-map-select"); const gameModeSelect = document.getElementById("game-mode-select"); const botsEnabled = document.getElementById("solo-bots-enabled"); const botCount = document.getElementById("solo-bot-count"); const maxPlayers = document.getElementById("solo-max-players"); if (mapSelect && gameModeSelect && botsEnabled && botCount && maxPlayers) { const gameModeInt = parseInt(gameModeSelect.value, 10) || 0; const isDangerRoyale = gameModeInt === 2; window._dangerSoloConfig = { map: mapSelect.value, gameMode: gameModeInt, bots: botsEnabled.checked, botCount: parseInt(botCount.value) || 0, maxPlayers: parseInt(maxPlayers.value) || 8, dangerRoyale: isDangerRoyale, mode: gameModeInt }; if (window._dangerCustomBsp && window._dangerCustomBsp.name) { window._dangerSoloConfig.map = window._dangerCustomBsp.name; window._dangerSoloConfig.customBsp = window._dangerCustomBsp; const bspBase64 = btoa(String.fromCharCode(...new Uint8Array(window._dangerCustomBsp.buffer))); fetch('/api/maps/upload-custom-bsp', { method: 'POST', headers: { 'Content-Type': 'application/json' }, body: JSON.stringify({ fileName: window._dangerCustomBsp.file.name, data: bspBase64, mapName: window._dangerCustomBsp.name }) }).then(r => r.json()).then(res => { if (res.bspPath) window._dangerCustomBsp.bspPath = res.bspPath; }).catch(err => console.warn('[DREngine] Custom BSP upload failed:', err)); } if (typeof Module !== 'undefined' && Module._setGameModeNative) { try { if (window._wasmRuntimeReady) { Module._setGameModeNative(gameModeInt); } else if (!window._pendingGameMode) { window._pendingGameMode = [gameModeInt]; } else { window._pendingGameMode.push(gameModeInt); } } catch (e) { console.warn('[DREngine] WASM setGameModeNative failed:', e.message); } } } closeSoloServerSetup(); // Unieke serverId met solo- prefix — backend maakt hier automatisch een room voor const soloId = `solo-${window.dangerUser?.steamId || 'guest'}`; connectToGame(soloId, window._dangerSoloConfig?.map || selectedLoadout?.map || getDefaultMap(), soloId); } // Bijgewerkt connectToGame om rechtstreeks server-ID (solo) of ip/map/serverId te verwerken // Accepteert optioneel een game-mode en maakt gebruik van de instance WebSocket URL bij matchmaking function connectToGame(ip, map, serverId = "local-main", mode = 0) { console.log(`[DREngine] Connecting to ${ip} on map ${map}...`); window.dangerRenderStopped = false; const isSolo = serverId !== "local-main" || window._dangerSoloConfig; initGameWeapons(isSolo); const dashboard = document.getElementById("menu-dashboard"); const engineLoader = document.getElementById("engine-loader"); const loaderText = document.getElementById("loader-subtext"); if (dashboard) dashboard.style.display = "none"; if (engineLoader) engineLoader.style.display = "flex"; if (loaderText) loaderText.innerText = `Connecting to ${ip} [MAP: ${(map || window._dangerSoloConfig?.map || selectedLoadout?.map || getDefaultMap()).toUpperCase()}]`; // Sla verbindingsparameters op voor DREngine-uitvoering window._dangerLastIp = ip; window._dangerLastMap = map || (window._dangerSoloConfig?.map || selectedLoadout?.map || getDefaultMap()); window._dangerLastServerId = serverId; window._dangerLoadout = selectedLoadout; window._dangerActiveModel = selectedLoadout.character; // Initialiseer gamesocket voor binaire snapshot-pijplijn indien nog niet aanwezig if (!window.dangerGameSocket && typeof io !== 'undefined') { // Gebruik instance WebSocket URL als beschikbaar (matchmaking), anders default socketUrl const gameSocketUrl = window._dangerInstanceWsUrl || socketUrl; window.dangerGameSocket = io(gameSocketUrl, { timeout: 10000 }); // Handle connection errors gracefully - geen spam, geen crash window.dangerGameSocket.on('connect_error', (err) => { console.warn('[DREngine] Game socket connect error:', err.message); }); window.dangerGameSocket.on('connect', () => { // Instance URL is verbruikt na succesvolle verbinding if (window._dangerInstanceWsUrl) window._dangerInstanceWsUrl = null; }); // Binair statusbijwerking: ontvang raw buffer en pompen direct naar WASM HEAP window.dangerGameSocket.on('binary_state_update', (binaryData) => { if (document.hidden) return; if (typeof Module === 'undefined' || Module === null || typeof window._wasmRuntimeReady === 'undefined' || window._wasmRuntimeReady !== true) { return; } try { const buffer = new Uint8Array(binaryData); const heapPtr = Module._malloc(buffer.length); if (heapPtr) { try { new Uint8Array(Module.HEAP8.buffer, heapPtr, buffer.length).set(buffer); if (typeof Module._processDeltaSnapshotNative === 'function') { Module._processDeltaSnapshotNative(heapPtr, buffer.length); } } finally { Module._free(heapPtr); } } } catch (e) { console.warn('[DREngine] Game binary pump error:', e.message); PACKET_JITTER_BUFFER.clear(); if (typeof Module._safeRenderFrameReset === 'function') { Module._safeRenderFrameReset(); } } }); // Use jitter-buffered snapshot pump to smooth domain routing latency window.dangerGameSocket.on('binary_state_update', (binaryData) => { if (document.hidden) return; try { PACKET_JITTER_BUFFER.push({ snapshot: binaryData, timestamp: Date.now() }); } catch (e) { console.warn('[DREngine] Jitter buffer push error:', e.message); PACKET_JITTER_BUFFER.clear(); } }); // Chatbericht ontvangen window.dangerGameSocket.on('chat_broadcast', (chatData) => { appendChatMessage(chatData); }); // Luister naar game-adminverhoging van server-zijde pin-verificatie window.dangerGameSocket.on('dev_status_result', (result) => { if (result.success && result.isGameAdmin) { isGameAdmin = true; window.isGameAdmin = true; playAdminBipSound(); appendChatMessage({ author: 'SYSTEM', text: 'DEVELOPER STATUS ELEVATED VIA WEBSOCKET - IMMUNITY SHIELD ACTIVE', color: '#ff00ff', timestamp: Date.now() }); const devPanelBtn = document.getElementById('dev-panel-trigger'); if (devPanelBtn) { devPanelBtn.style.display = 'block'; devPanelBtn.textContent = 'ADMIN PANEL'; } // Unlock complex admin functions section const complexSection = document.getElementById('admin-complex-functions'); if (complexSection) complexSection.style.display = 'block'; } }); // Luister ook naar verhoging via HTTP-endpoint window.dangerGameSocket.on('dev_status_elevated', () => { isGameAdmin = true; window.isGameAdmin = true; playAdminBipSound(); appendChatMessage({ author: 'SYSTEM', text: 'DEVELOPER STATUS ELEVATED - IMMUNITY SHIELD ACTIVE', color: '#ff00ff', timestamp: Date.now() }); const devPanelBtn = document.getElementById('dev-panel-trigger'); if (devPanelBtn) { devPanelBtn.style.display = 'block'; devPanelBtn.textContent = 'ADMIN PANEL'; } // Unlock complex admin functions section const complexSection = document.getElementById('admin-complex-functions'); if (complexSection) complexSection.style.display = 'block'; }); // Luister naar stemmings-updates window.dangerGameSocket.on('vote_update', (data) => { const timerEl = document.getElementById('vote-timer-value'); if (timerEl) timerEl.textContent = String(data.remaining || 0); }); window.dangerGameSocket.on('vote_start', (data) => { showChatLog(); appendChatMessage({ author: 'VOTE', text: `Map vote started! 60s remaining`, color: '#00ffff', timestamp: Date.now() }); data.maps.forEach((m, i) => { appendChatMessage({ author: 'VOTE', text: `!${i + 1} = ${m}`, color: '#00ffff', timestamp: Date.now() }); }); }); // Luister naar chatopdrachten met !vote-gestemd window.dangerGameSocket.on('chat_message', (msg) => { if (msg.text && msg.text.startsWith('!')) { const voteNum = parseInt(msg.text.substring(1)); if (voteNum >= 1 && voteNum <= 3 && window._activeVote) { window.dangerGameSocket.emit('vote_cast', { choice: voteNum }); } } }); // Initialize marketplace socket event handlers initMarketplaceSocketHandlers(); // Luister naar serverstatus-HUD-updates window.dangerGameSocket.on('server_status_hud', (data) => { const statusEl = document.getElementById('hud-server-status'); if (statusEl) { statusEl.textContent = `TICK:${data.tickRate}Hz | HEAP:${data.heapMB}MB | P:${data.playerCount} B:${data.botCount} | ${data.phase.toUpperCase()}`; } }); window.dangerGameSocket.on('weapon_dropped', (data) => { appendChatMessage({ author: 'SYSTEM', text: `Weapon dropped: ${data.weapon}`, color: '#ff6600', timestamp: Date.now() }); if (typeof Module !== 'undefined' && window._wasmRuntimeReady) { if (typeof Module._dropWeaponNative === 'function') { Module._dropWeaponNative(data.x, data.y, data.z); } } }); window.dangerGameSocket.on('slot_assign', (data) => { if (data.slot === 5 && typeof Module !== 'undefined' && window._wasmRuntimeReady) { if (typeof Module._setPlayerWeaponSlotNative === 'function') { Module._setPlayerWeaponSlotNative(localPlayerId || 0, data.slot, data.weaponId || 6); } activeWeaponSlot = data.slot; updateActiveWeapon(); showChatLog(); appendChatMessage({ author: 'SYSTEM', text: `Tablet forced to Slot 5`, color: '#00ffff', timestamp: Date.now() }); } }); window.dangerGameSocket.on('server_joined', (data) => { if (data.customBsp) { window._soloServerConfig = window._soloServerConfig || {}; window._soloServerConfig.customBsp = data.customBsp; if (!window._dangerCustomBsp && data.map === data.customBsp.name) { window._dangerCustomBsp = { name: data.customBsp.name, bspPath: data.customBsp.bspPath, size: data.customBsp.size, header: data.customBsp.header }; } } }); // Handler for purchase success broadcast from server window.dangerGameSocket.on('purchase_success', (data) => { const weaponEl = document.getElementById('hud-weapon-name'); if (weaponEl) weaponEl.textContent = data.weapon || ''; updateHUDWeapon(); // Handle marketplace skin purchase result if (data && data.item) { marketplaceCoinBalance = data.coins !== undefined ? data.coins : marketplaceCoinBalance; syncMarketplaceCoins(); showMarketplaceToast('success', 'PURCHASE CONFIRMED: ' + (data.item.name || 'ITEM')); } }); // Handler for player fired broadcast - sync muzzle flashes, recoil via C++ core window.dangerGameSocket.on('player_fired_broadcast', (data) => { if (typeof Module !== 'undefined' && Module._notifyPlayerFiredNative === 'function' && window._wasmRuntimeReady) { try { Module._notifyPlayerFiredNative(data.playerId, data.weaponId); } catch(e) { console.warn('[DREngine] notifyPlayerFiredNative failed:', e.message); } } }); // Handler for weapon state updates window.dangerGameSocket.on('weapon_state', (data) => { updateHUDWeapon(); }); // Join server with solo server config window.dangerGameSocket.emit('join_server', { serverId: serverId, displayName: window.dangerUser?.displayName || 'Guest', userId: window.dangerUser?.steamId || '', isGlobalAdmin: window.dangerUser?.isAdmin || false, isGameAdmin: isGameAdmin, map: window._dangerSoloConfig?.map || getDefaultMap(), maxPlayers: window._dangerSoloConfig?.maxPlayers || 36, bots: window._dangerSoloConfig?.bots || false, botCount: window._dangerSoloConfig?.botCount || 0, dangerRoyale: window._dangerSoloConfig?.dangerRoyale || false, mode: window._dangerSoloConfig?.mode || 0, characterModel: selectedLoadout?.character || 'hound_soldier', customBsp: window._dangerCustomBsp ? { name: window._dangerCustomBsp.name, size: window._dangerCustomBsp.size, header: window._dangerCustomBsp.header } : null }); // Request skin catalog from server window.dangerGameSocket.emit('list_skins'); gameSocketInstance = window.dangerGameSocket; gameSocketInstance.isGameAdmin = isGameAdmin; // Instance URL is verbruikt na gebruik - maak leeg voor volgende verbinding if (window._dangerInstanceWsUrl) window._dangerInstanceWsUrl = null; } // Start DREngine: gebruik WASM-engine als geladen, altijd brug naar GLB-renderer saveLoadout(); setTimeout(() => { if (window.dangerGameActive) return; const map = window._dangerLastMap || selectedLoadout?.map || getDefaultMap(); const serverId = window._dangerLastServerId || "local-main"; const ip = window._dangerLastIp || MAINNET_DOMAIN; // De WASM-core biedt laag-niveau binaire parsing; rendering wordt gebridged naar GLB executeWasmEngine(); }, 50); return; } function openSettingsMenu() { // Inject settings menu fallback immediately if WASM didn't inject it if (!document.getElementById("settings-popup-overlay")) { injectGameMenusFallback(); } window.dangerSettingsOpen = true; window.dangerGameSocket?.emit('player_input', { forward: false, backward: false, left: false, right: false, jump: false, sprint: false, crouch: false }); document.exitPointerLock?.(); let overlay = document.getElementById("dw-settings-layer") || document.getElementById("settings-popup-overlay"); if (overlay) overlay.style.display = "flex"; const langSelect = document.getElementById("game-language"); if (langSelect) { langSelect.value = CURRENT_LANGUAGE; } let bForward = document.getElementById("bind-forward"); if (bForward) bForward.value = KEYBINDS.FORWARD || 'KeyW'; let bLeft = document.getElementById("bind-left"); if (bLeft) bLeft.value = KEYBINDS.LEFT || 'KeyA'; let bBackward = document.getElementById("bind-backward"); if (bBackward) bBackward.value = KEYBINDS.BACKWARD || 'KeyS'; let bRight = document.getElementById("bind-right"); if (bRight) bRight.value = KEYBINDS.RIGHT || 'KeyD'; let bJump = document.getElementById("bind-jump"); if (bJump) bJump.value = KEYBINDS.JUMP || 'Space'; let bCrouch = document.getElementById("bind-crouch"); if (bCrouch) bCrouch.value = KEYBINDS.CROUCH || 'ControlLeft'; let bSprint = document.getElementById("bind-sprint"); if (bSprint) bSprint.value = KEYBINDS.SPRINT || 'ShiftLeft'; let bParachute = document.getElementById("bind-parachute"); if (bParachute) bParachute.value = KEYBINDS.PARACHUTE || 'KeyH'; let bMap = document.getElementById("bind-map"); if (bMap) bMap.value = KEYBINDS.MAP || 'KeyG'; let bTablet = document.getElementById("bind-tablet"); if (bTablet) bTablet.value = KEYBINDS.TABLET || 'KeyT'; let bPause = document.getElementById("bind-pause"); if (bPause) bPause.value = KEYBINDS.PAUSE || 'Escape'; let bPlayerList = document.getElementById("bind-player-list"); if (bPlayerList) bPlayerList.value = KEYBINDS.PLAYER_LIST || 'Tab'; if (document.getElementById("mouse-sensitivity")) { document.getElementById("mouse-sensitivity").value = MOUSE_SENSITIVITY; let sensLabel = document.getElementById("sensitivity-value-label"); if (sensLabel) sensLabel.innerText = MOUSE_SENSITIVITY.toFixed(1); } const headBob = document.getElementById("head-bob-strength"); if (headBob) { headBob.value = window.FPV_HEAD_BOB; updateHeadBobLabel(headBob.value); } let volMaster = document.getElementById("volume-master"); if (volMaster) { updateVolumeLabel(volMaster.value); } } function closeSettingsMenu() { window.dangerSettingsOpen = false; let overlay = document.getElementById("dw-settings-layer") || document.getElementById("settings-popup-overlay"); if (overlay) overlay.style.display = "none"; } function captureKey(inputElement) { inputElement.value = "press..."; const handler = function(e) { inputElement.value = e.code; document.removeEventListener('keydown', handler); }; document.addEventListener('keydown', handler); } function saveSettingsMenu() { const bindIds = ['forward', 'left', 'backward', 'right', 'jump', 'crouch', 'sprint', 'parachute', 'map', 'tablet', 'pause', 'playerList']; bindIds.forEach(id => { const el = document.getElementById(`bind-${id}`); if (el && el.value) { const key = id.charAt(0).toUpperCase() + id.slice(1).toUpperCase().replace('LIST', 'LIST').replace('LIST', 'LIST'); // Kaart naar KEYBINDS-eigenschapnamen const keyMap = { forward: 'FORWARD', left: 'LEFT', backward: 'BACKWARD', right: 'RIGHT', jump: 'JUMP', crouch: 'CROUCH', sprint: 'SPRINT', parachute: 'PARACHUTE', map: 'MAP', tablet: 'TABLET', pause: 'PAUSE', playerList: 'PLAYER_LIST' }; if (keyMap[id]) KEYBINDS[keyMap[id]] = el.value; } }); if (document.getElementById("mouse-sensitivity")) { MOUSE_SENSITIVITY = parseFloat(document.getElementById("mouse-sensitivity").value); } const headBob = document.getElementById("head-bob-strength"); if (headBob) { window.FPV_HEAD_BOB = Math.max(0, Math.min(0.12, parseFloat(headBob.value))); localStorage.setItem("dw_head_bob", window.FPV_HEAD_BOB); } localStorage.setItem("dw_keybinds", JSON.stringify(KEYBINDS)); localStorage.setItem("dw_sensitivity", MOUSE_SENSITIVITY); let volMaster = document.getElementById("volume-master"); if (volMaster) localStorage.setItem("dw_volume", volMaster.value); let musicVol = document.getElementById("music-volume"); if (musicVol) { localStorage.setItem("dw_music_vol", musicVol.value); } let gfxQuality = document.getElementById("graphics-quality"); if (gfxQuality) { localStorage.setItem("dw_graphics", gfxQuality.value); } const languageSelect = document.getElementById("game-language"); if (languageSelect) { localStorage.setItem("dw_language", languageSelect.value); setLanguage(languageSelect.value); } const musicToggle = document.getElementById("music-toggle"); if (musicToggle) { localStorage.setItem("dw_music", musicToggle.checked ? '1' : '0'); } const sfxToggle = document.getElementById("sfx-toggle"); if (sfxToggle) { localStorage.setItem("dw_sfx", sfxToggle.checked ? '1' : '0'); } closeSettingsMenu(); console.log("[DangerRoyale Settings] Settings saved successfully."); } function updateSensitivityLabel(val) { if (document.getElementById("sensitivity-value-label")) { document.getElementById("sensitivity-value-label").innerText = parseFloat(val).toFixed(1); } } function updateHeadBobLabel(val) { const label = document.getElementById("head-bob-value-label"); if (label) label.innerText = Number(val).toFixed(2); } function updateMusicVolumeLabel(val) { const label = document.getElementById("music-volume-value-label"); if (label) label.innerText = val + "%"; if (window.AudioEngine) { window.AudioEngine.setMusicVolume(parseInt(val, 10) / 100); } } async function loadGameSettings() { let serverDefaults = {}; try { const response = await fetch('/api/settings'); if (response.ok) { serverDefaults = await response.json(); } } catch (e) { console.warn("[DREngine] Could not load settings from server, using fallbacks"); } const defaultKeybinds = serverDefaults.keybinds || { FORWARD: 'KeyW', LEFT: 'KeyA', BACKWARD: 'KeyS', RIGHT: 'KeyD', JUMP: 'Space', CROUCH: 'ControlLeft', SPRINT: 'ShiftLeft', PARACHUTE: 'KeyH', MAP: 'KeyM', TABLET: 'KeyN', PAUSE: 'Escape', PLAYER_LIST: 'Tab' }; KEYBINDS = { ...defaultKeybinds }; let savedBinds = JSON.parse(localStorage.getItem("dw_keybinds")); if (savedBinds) { KEYBINDS = { ...KEYBINDS, ...savedBinds }; } MOUSE_SENSITIVITY = serverDefaults.mouseSensitivity || 1.0; window.FPV_HEAD_BOB = Number(localStorage.getItem("dw_head_bob") ?? (serverDefaults.headBob || 0.04)); CURRENT_LANGUAGE = localStorage.getItem("dw_language") || (serverDefaults.language || "en"); let savedSens = localStorage.getItem("dw_sensitivity"); if (savedSens) MOUSE_SENSITIVITY = parseFloat(savedSens); let savedVol = localStorage.getItem("dw_volume"); if (!savedVol) { localStorage.setItem("dw_volume", "55"); savedVol = "55"; } if (savedVol && document.getElementById("volume-master")) { document.getElementById("volume-master").value = savedVol; } let savedMusicVol = localStorage.getItem("dw_music_vol"); if (!savedMusicVol) { localStorage.setItem("dw_music_vol", "55"); savedMusicVol = "55"; } if (savedMusicVol && document.getElementById("music-volume")) { document.getElementById("music-volume").value = savedMusicVol; } if (document.getElementById("music-volume-value-label")) { const mv = savedMusicVol || 55; document.getElementById("music-volume-value-label").innerText = mv + "%"; } let savedGraph = localStorage.getItem("dw_graphics"); if (savedGraph && document.getElementById("graphics-quality")) { document.getElementById("graphics-quality").value = savedGraph; } const savedLanguage = localStorage.getItem("dw_language"); if (savedLanguage) { CURRENT_LANGUAGE = savedLanguage === 'nl' ? 'nl' : 'en'; } // Load audio preferences via AudioEngine if (window.AudioEngine) { window.AudioEngine.loadAudioPreferences(); } else { const savedMusic = localStorage.getItem("dw_music"); const musicOn = savedMusic !== '0'; const musicToggle = document.getElementById("music-toggle"); if (musicToggle) musicToggle.checked = musicOn; const musicLabel = document.getElementById("music-label"); if (musicLabel) musicLabel.textContent = musicOn ? 'ON' : 'OFF'; const savedSfx = localStorage.getItem("dw_sfx"); const sfxOn = savedSfx !== '0'; const sfxToggle = document.getElementById("sfx-toggle"); if (sfxToggle) sfxToggle.checked = sfxOn; const sfxLabel = document.getElementById("sfx-label"); if (sfxLabel) sfxLabel.textContent = sfxOn ? 'ON' : 'OFF'; } window._dangerKeybinds = KEYBINDS; window._dangerMouseSensitivity = MOUSE_SENSITIVITY; } function proceedToGame() { console.log("[DREngine] All assets loaded! Bridging WASM core to GLB renderer..."); const progressBar = document.getElementById('loader-progress-fill'); if (progressBar) progressBar.style.width = '100%'; const detailEl = document.getElementById('loader-detail'); if (detailEl) detailEl.innerText = "All assets loaded!"; setTimeout(() => { const engineLoader = document.getElementById("engine-loader"); if (engineLoader) engineLoader.style.display = "none"; stopLoadingTimer?.(); canvasEl = document.getElementById("canvas"); const map = window._dangerLastMap || selectedLoadout?.map || getDefaultMap(); if (window._wasmRuntimeReady && typeof Module !== 'undefined' && Module._main) { console.log("[DREngine] Attempting DREngine WASM core main..."); try { Module._main(); } catch (wasmErr) { console.warn("[DREngine] WASM main failed, using WebGL renderer:", wasmErr.message); if (typeof initWebGLViewport === 'function') { initWebGLViewport(map, 0); } } } else if (typeof initWebGLViewport === 'function') { initWebGLViewport(map, 0); } }, 100); } function executeWasmEngine() { document.documentElement.style.overflow = 'hidden'; document.body.style.overflow = 'hidden'; document.body.style.margin = '0'; document.body.style.padding = '0'; document.body.style.height = '100vh'; document.body.style.width = '100vw'; const mainMenu = document.getElementById("main-menu"); if (mainMenu) mainMenu.style.display = "none"; const loginBox = document.getElementById("login-box"); if (loginBox) loginBox.style.display = "none"; // Stop JS WebGL preview loops before transitioning to native engine stopMainMenuCanvasPreview(); if (typeof stopLoadingTimer === 'function') stopLoadingTimer(); // Create the game canvas if it doesn't exist - required for both WASM and WebGL fallback paths var gameCanvas = document.getElementById("canvas"); if (!gameCanvas) { gameCanvas = document.createElement("canvas"); gameCanvas.id = "canvas"; gameCanvas.width = window.innerWidth; gameCanvas.height = window.innerHeight; gameCanvas.style.cssText = "position:absolute; top:0; left:0; width:100vw; height:100vh; display:block; background:#000;"; document.body.appendChild(gameCanvas); } window.addEventListener('wheel', function(e) { e.preventDefault(); }, { passive: false }); const engineLoader = document.getElementById("engine-loader"); const loaderText = document.getElementById("loader-subtext"); gameCanvas.style.display = "none"; if (gameCanvas) { gameCanvas.style.display = "block"; if (typeof Module !== 'undefined') Module.canvas = gameCanvas; gameCanvas.requestPointerLock = gameCanvas.requestPointerLock || gameCanvas.mozRequestPointerLock; gameCanvas.onclick = function() { gameCanvas.requestPointerLock().catch(err => { console.log("[DREngine] Pointerlock denied."); }); }; document.addEventListener('pointerlockchange', pointerLockChangeHandler, false); document.addEventListener('mozpointerlockchange', pointerLockChangeHandler, false); window.addEventListener('keydown', handleGameInputDown, false); window.addEventListener('keyup', handleGameInputUp, false); window.addEventListener('mousemove', handleMouseMoveLook, false); setTimeout(() => { const activeMap = window._dangerLastMap || window._dangerSoloConfig?.map || selectedLoadout?.map; if (activeMap) preloadWorldMap(activeMap); }, 100); } // DECOUPLED: UI is always accessible regardless of WASM 3D asset loading status. // The setInterval loadCheckInterval has been REMOVED — it blocked the UI when // Module.FS writes failed due to COOP/COEP HTTP restrictions. // Instead: wait briefly for async asset preloading, then force-clear overlay. setTimeout(() => { console.log('[DREngine] Decoupling UI from 3D asset load — forcing engine-loader hidden'); if (engineLoader) engineLoader.style.display = "none"; document.body.style.overflow = ''; document.documentElement.style.overflow = ''; if (typeof stopLoadingTimer === 'function') stopLoadingTimer(); proceedToGame(); }, 1500); } // WASM memory cleanup - prevents leaks when unloading the engine core function cleanupWasmMemory() { if (typeof Module !== 'undefined' && Module) { try { if (typeof Module.flushPersistentState === 'function') { Module.flushPersistentState("engine_shutdown"); } } catch (e) { console.warn("[DREngine] WASM cleanup warning:", e.message); } window._drengineModule = null; } // Vrijg heaps WASM-geheugen toe als ze bestaan if (window._dangerMapVertexPtr && typeof Module !== 'undefined' && Module._free) { try { Module._free(window._dangerMapVertexPtr); } catch (e) {} window._dangerMapVertexPtr = 0; } // Annuleer eventuele lopende render-lus if (rafId !== null) { cancelAnimationFrame(rafId); rafId = null; } // Maak WebGL-bronnen op if (gl) { if (mapTexture) gl.deleteTexture(mapTexture); if (weaponTexture) gl.deleteTexture(weaponTexture); if (charBuffer) gl.deleteBuffer(charBuffer); if (indexBuffer) gl.deleteBuffer(indexBuffer); if (texCoordBuffer) gl.deleteBuffer(texCoordBuffer); if (weaponBuffer) gl.deleteBuffer(weaponBuffer); if (weaponIndexBuffer) gl.deleteBuffer(weaponIndexBuffer); if (weaponUVBuffer) gl.deleteBuffer(weaponUVBuffer); if (window.staticWorldMapBuffer) gl.deleteBuffer(window.staticWorldMapBuffer); if (program) gl.deleteProgram(program); } gl = null; program = null; mapTexture = null; weaponTexture = null; charBuffer = null; indexBuffer = null; texCoordBuffer = null; weaponBuffer = null; weaponIndexBuffer = null; weaponUVBuffer = null; window.staticWorldMapBuffer = null; window.dangerRenderStopped = true; } window.addEventListener('pagehide', () => { cleanupAll(); }); window.addEventListener('visibilitychange', () => { if (document.hidden) { if (rafId !== null) { cancelAnimationFrame(rafId); rafId = null; } stopWeaponPreviews(); if (typeof Module !== 'undefined' && Module._onPause && typeof Module._onPause === 'function') { try { Module._onPause(); } catch (e) {} } } else { if (!window.dangerRenderStopped && !rafId && !window.dangerGameActive && !window.dangerMapOpen) { rafId = requestAnimationFrame(renderFrame); } if (typeof weaponPreviewState !== 'undefined' && weaponPreviewState.primary.active) { initWeaponPreview('primary'); } if (typeof weaponPreviewState !== 'undefined' && weaponPreviewState.secondary.active) { initWeaponPreview('secondary'); } } }); // Uitgebreide opschoning om geheugenlekken te voorkomen function cleanupAll() { cleanupWasmMemory(); stopMainMenuCanvasPreview(); stopCharacterPreview(); stopWeaponPreviews(); stopLoadingTimer(); stopQueueTimer(); if (observer) observer.disconnect(); if (socket) { socket.removeAllListeners(); socket.close(); socket = null; } // Verwijder globale gebeurtenislisteners toegevoegd tijdens game-play window.removeEventListener('keydown', handleGameInputDown); window.removeEventListener('keyup', handleGameInputUp); window.removeEventListener('mousemove', handleMouseMoveLook); document.removeEventListener('pointerlockchange', pointerLockChangeHandler, false); document.removeEventListener('mozpointerlockchange', pointerLockChangeHandler, false); } let playerPos = { x: 110.0, y: -30.0 }; let verticalVelocity = 0.0; let isGrounded = true; let isNoclipActive = false; let noclipHeight = 0.0; let cameraLook = { x: 0.0, y: 0.0 }; let activeKeysPressed = {}; let currentHP = 100; let currentAmmo = 30; let maxAmmoPerMag = 30; let reserveMagazines = 4; let isReloading = false; let weaponFired = false; let lastShootTime = 0; let lastSlotSwitchTime = 0; const WEAPON_DEPLOY_COOLDOWN = 350; // ms before firing allowed after slot switch let projectiles = []; window.dangerProjectiles = projectiles; let cashStacks = []; let explosions = []; window.dangerExplosions = explosions; let ejectedCases = []; window.dangerEjectedCases = ejectedCases; let droppedMags = []; window.dangerDroppedMags = droppedMags; let holdingTablet = false; let holdingBuyMenu = false; // Wapenstatistieken dynamisch geladen van server - geen gecodeerde wapengegevens const WEAPON_DATA = { primary: {}, secondary: {}, knife: {}, grenade: {} }; const LOOT_PROP_NAMES = new Set(['556_case', '9mm_case', '9mm_bullet', '9mmcase', 'explosion', 'cashstack', 'ssg_case', 'c4', 'tablet']); let ALL_WEAPON_STATS = {}; let DISCOVERED_MAPS = []; let DISCOVERED_WEAPONS = []; let MAP_POOL = []; async function loadDynamicGameAssets() { try { const [weaponsResp, mapsResp] = await Promise.all([ fetch('/api/weapons'), fetch('/api/maps') ]); if (weaponsResp.ok) { const wData = await weaponsResp.json(); DISCOVERED_WEAPONS = wData.weapons || []; ALL_WEAPON_STATS = wData.stats || {}; DISCOVERED_WEAPONS.forEach(w => { const s = ALL_WEAPON_STATS[w.name] || {}; const entry = { name: s.name || w.name, damage: s.damage || 20, fireRate: s.fireRate || 400, magazine: s.magazine || 30, reserves: s.reserves || 90, type: s.type || 'primary', cost: s.cost || 0, model: w.path, texture: (s.texture) || "/custom_assets/textures/weapons/v_models/" + w.name + ".png", cartridge: s.cartridge || '9mm', magModel: w.path }; if (w.name === 'knife' || s.type === 'melee') { WEAPON_DATA.knife[w.name] = entry; } else if (w.name.toLowerCase().includes('granade') || w.name.toLowerCase().includes('grenade') || s.type === 'throwable') { WEAPON_DATA.grenade[w.name] = entry; } else if (s.type === 'primary' || ['ar47_core', 'm4a4_sandstorm', 'awp', 'sniper_rifle'].includes(w.name)) { WEAPON_DATA.primary[w.name] = entry; } else if (!LOOT_PROP_NAMES.has(w.name.toLowerCase()) && (s.type === 'secondary' || ['gk18', 'usp', 'p250'].includes(w.name))) { WEAPON_DATA.secondary[w.name] = entry; } if (typeof Module !== 'undefined' && Module && Module._loadWeaponModel) { try { const ptr = Module.allocateUTF8(w.name, 256); try { Module._loadWeaponModel(ptr); } finally { Module._free(ptr); } } catch (e) { console.warn("[DREngine] Could not register weapon model with WASM:", w.name, e.message); } } }); } if (mapsResp.ok) { const mData = await mapsResp.json(); DISCOVERED_MAPS = mData.maps || []; MAP_POOL = mData.activePool || mData.pool || []; } } catch (e) { console.warn("[DREngine] Could not load dynamic game assets:", e); } } function updateActiveWeapon() { const weaponKey = window.WEAPON_KEYS[activeWeaponSlot]; if (!weaponKey) return; if (activeWeaponSlot === 1) { currentWeapon = playerWeapons[1]; } else if (activeWeaponSlot === 2) { currentWeapon = playerWeapons[2]; } else if (activeWeaponSlot === 3) { currentWeapon = WEAPON_DATA.knife.knife; } else if (activeWeaponSlot === 4) { currentWeapon = WEAPON_DATA.grenade.grenade; } if (!currentWeapon) { currentWeapon = WEAPON_DATA.knife.knife; } currentAmmo = currentWeapon?.magazine || 0; maxAmmoPerMag = currentWeapon?.magazine || 30; reserveMagazines = Math.floor(currentWeapon?.reserves || 0 / maxAmmoPerMag); const ammoEl = document.getElementById("hud-ammo"); if (ammoEl) { const nameEl = document.getElementById("hud-weapon-name"); if (nameEl) nameEl.innerText = currentWeapon?.name || ""; if (activeWeaponSlot === 3) { ammoEl.innerText = "MELEE"; } else if (activeWeaponSlot === 4) { ammoEl.innerText = `GRENADES: ${currentWeapon?.magazine || 1}`; } else if (currentWeapon?.type === 'throwable') { ammoEl.innerText = `THROWABLE: ${currentAmmo}`; } else { ammoEl.innerText = `AMMO: ${currentAmmo}/${maxAmmoPerMag} | RESERVES: ${reserveMagazines}`; } } updateHudSlotBinding(); } // Dynamische HUD-slotbinding: update actieve slotbenadering in HUD function updateHudSlotBinding() { const slotEls = document.querySelectorAll('.hud-weapon-slot'); slotEls.forEach(el => { el.classList.remove('active-slot'); }); const activeEl = document.getElementById(`hud-slot-${activeWeaponSlot}`); if (activeEl) activeEl.classList.add('active-slot'); // Update wapennaamweergave voor slot 5 (tablet) const nameEl = document.getElementById("hud-weapon-name"); if (nameEl && activeWeaponSlot === 5) { nameEl.innerText = "TABLET"; } } window.WEAPON_KEYS = { 0: 'knife', 1: 'primary', 2: 'secondary', 3: 'knife', 4: 'grenade', 5: 'tablet' }; // Projectieltypen en hun eigenschappen const PROJECTILE_SPEED = 12.0; const PROJECTILE_LIFETIME = 2000; // ms let activeWeaponSlot = 1; let currentWeapon = null; let playerWeapons = { 0: null, 1: null, 2: null, 3: null, 4: null, 5: null }; let isSoloMode = false; let playerMoney = 0; let maxMoney = 99999; // Granaatmijn kooktimer let grenadeCookStartTime = 0; let isGrenadeCooking = false; const GRENADE_COOK_TIME = 3500; // 3.5 seconds before auto-detonation // Granaatmijn koken: controleer op autonome detonatie bij vasthouden vuur op granaatmijnslot function checkGrenadeCook() { if (activeWeaponSlot !== 4) { isGrenadeCooking = false; return; } // Koken wordt afgehandeld in de render-lus bij vasthouden muisknop } // Teken voorspellende granaatmijntrajectboog (subtiele stippen parabool) function drawGrenadeTrajectory(startPos, cameraLookAngles, velocity) { if (typeof Module === 'undefined' || !Module || !window._wasmRuntimeReady) return; const gravity = -9.8; const timeStep = 0.1; const points = []; let t = 0; const startX = startPos.x; const startY = 1.2; const startZ = startPos.y; const vx = -Math.sin(cameraLookAngles.x) * Math.cos(Math.max(-0.5, Math.min(0.5, cameraLookAngles.y))) * velocity; const vy = Math.max(0.5, Math.cos(cameraLookAngles.y)) * 5.0; const vz = -Math.cos(cameraLookAngles.x) * Math.cos(Math.max(-0.5, Math.min(0.5, cameraLookAngles.y))) * velocity; for (let i = 0; i < 30; i++) { const x = startX + vx * t; const z = startZ + vz * t; const y = startY + vy * t + 0.5 * gravity * t * t; if (y < -165) break; // Ground level points.push({ x, y, z }); t += timeStep; } if (typeof Module._drawTrajectoryNative === 'function') { Module._drawTrajectoryNative(points, points.length); } } window.checkGrenadeCook = checkGrenadeCook; function initGameWeapons(soloMode) { isSoloMode = soloMode; playerWeapons = { 1: null, 2: null, 3: null, 4: null }; playerMoney = 0; const gk18Stats = ALL_WEAPON_STATS['gk18'] || { name: "GK18", damage: 17, fireRate: 400, magazine: 20, reserves: 60, type: "secondary", cost: 200 }; playerWeapons[2] = { ...gk18Stats, key: 'gk18' }; if (soloMode) { const ar47_coreStats = ALL_WEAPON_STATS['ar47_core'] || { name: "AR-47", damage: 42, fireRate: 650, magazine: 30, reserves: 90, type: "primary", cost: 1800 }; playerWeapons[1] = { ...ar47_coreStats, key: 'ar47_core' }; } const knifeStats = ALL_WEAPON_STATS['knife'] || { name: "Combat Knife", damage: 40, fireRate: 0, magazine: 0, reserves: 0, type: "melee", cost: 0 }; playerWeapons[3] = { ...knifeStats, key: 'knife' }; activeWeaponSlot = soloMode ? 1 : 2; updateActiveWeapon(); const cashEl = document.getElementById("hud-cash"); if (cashEl) cashEl.innerText = `$${playerMoney}`; } window.ALL_WEAPON_STATS = ALL_WEAPON_STATS; let CHARACTER_DATA = {}; window.CHARACTER_DATA = CHARACTER_DATA; async function detectAvailableCharacters() { try { const response = await fetch('/api/models/characters'); if (response.ok) { const data = await response.json(); data.models.forEach(f => { CHARACTER_DATA[f] = { name: formatModelName(f), description: "", model: f }; }); window.CHARACTER_DATA = CHARACTER_DATA; return Object.keys(CHARACTER_DATA); } } catch (e) { console.warn('[DREngine] Character detection fallback:', e.message); } return Object.keys(CHARACTER_DATA); } function formatModelName(filename) { return filename .replace(/_/g, ' ') .replace(/\\/g, ' ') .replace(/\b\w/g, l => l.toUpperCase()); } async function initLoadoutDefaults() { await detectAvailableCharacters(); const chars = Object.keys(CHARACTER_DATA); if (chars.length > 0 && (!selectedLoadout.character || !CHARACTER_DATA[selectedLoadout.character])) { selectedLoadout.character = chars[0]; } const availableMaps = MAP_POOL.length > 0 ? MAP_POOL : DISCOVERED_MAPS.map(m => m.name); if (!selectedLoadout.map || !availableMaps.includes(selectedLoadout.map)) { selectedLoadout.map = availableMaps[0] || ''; } } let selectedLoadout = { character: "", primary: "", secondary: "", map: "" }; function loadStoredLoadout() { const saved = localStorage.getItem("dw_loadout"); if (saved) { try { const parsed = JSON.parse(saved); selectedLoadout = { ...selectedLoadout, ...parsed }; } catch (e) {} } renderCharacterOptions(); updateCharacterSelection(); } function renderCharacterOptions() { const grid = document.getElementById('character-select-grid'); if (!grid) return; const chars = Object.keys(CHARACTER_DATA); if (chars.length === 0) return; grid.innerHTML = ''; const colors = ['#5aa9d6', '#d4a76a', '#8a8a8a', '#5aa65a', '#de9b35', '#70d2ff']; chars.forEach((charKey, i) => { const data = CHARACTER_DATA[charKey]; const color = colors[i % colors.length]; const selected = selectedLoadout.character === charKey; grid.innerHTML += `
${data.name} ${data.description || 'Player Model'}
`; }); } async function loadGLBPreview(characterId) { const canvas = document.getElementById('character-preview-canvas'); if (!canvas) return; const modelName = CHARACTER_DATA[characterId]?.model || characterId; if (!modelName) return; const cleanName = modelName.replace(/^tm_/, ''); const url = `/custom_assets/models/player/${cleanName}.glb`; try { const res = await fetch(url); if (!res.ok) { // Suppress 404/000 noise; only warn on genuine server errors if (res.status !== 404 && res.status !== 0) { console.warn(`[DREngine] GLB preview: ${url} failed (${res.status})`); } return; } const arrayBuffer = await res.arrayBuffer(); const modelBytes = new Uint8Array(arrayBuffer); // Only attempt WASM FS write if runtime is fully initialized if (window._wasmRuntimeReady && typeof Module !== 'undefined' && Module.FS && typeof Module.FS.writeFile === 'function') { try { Module.FS.writeFile(`/drengine/models/player/${cleanName}.glb`, modelBytes); console.log(`[DREngine] GLB (${cleanName}.glb, ${modelBytes.length} bytes) written to WASM FS`); } catch (fsErr) { console.warn('[DREngine] WASM FS write failed — GLB queued for next ready state'); window._pendingGLB = { name: cleanName, bytes: modelBytes }; } } else { // Defer GLB bytes until WASM runtime is ready window._pendingGLB = { name: cleanName, bytes: modelBytes }; console.log('[DREngine] WASM runtime not ready — GLB deferred to _pendingGLB'); } } catch (e) { console.warn('[DREngine] GLB preview fetch failed:', e.message); } } function saveLoadout() { localStorage.setItem("dw_loadout", JSON.stringify(selectedLoadout)); } function selectCharacter(element, characterId) { selectedLoadout.character = characterId; document.querySelectorAll('.character-option').forEach(el => el.classList.remove('selected')); element.classList.add('selected'); const label = document.getElementById('player-preview-label'); if (label) label.textContent = (CHARACTER_DATA[characterId]?.name || characterId || 'Character').toUpperCase() + " ACTIVE"; loadGLBPreview(characterId); if (window.updatePlayerPreview) { window.updatePlayerPreview(characterId); } saveLoadout(); } function updateCharacterSelection() { const char = selectedLoadout.character; const options = document.querySelectorAll('.character-option'); options.forEach(opt => { opt.classList.toggle('selected', opt.dataset.character === char); }); const label = document.getElementById('player-preview-label'); if (label) label.textContent = (CHARACTER_DATA[char]?.name || 'Character').toUpperCase() + " ACTIVE"; // Initialiseer of schakel karaktermodelvoorvertoning (WASM-core verwerkt model-renderen) loadGLBPreview(char); if (window.updatePlayerPreview) { window.updatePlayerPreview(char); } } function confirmCharacterSelection() { const char = selectedLoadout.character; const chars = Object.keys(CHARACTER_DATA); const modelId = Math.max(0, chars.indexOf(char)); const label = document.getElementById('player-preview-label'); if (label) label.textContent = (CHARACTER_DATA[char]?.name || char || 'Character').toUpperCase() + " ACTIVE"; if (window.updatePlayerPreview) window.updatePlayerPreview(char); // Pass model ID to C++ core so connectToGame can boot 3D viewport with correct model window._confirmedModelId = modelId; saveLoadout(); } function multiplyMatrices(a, b, out) { for (let i = 0; i < 4; i++) { for (let j = 0; j < 4; j++) { out[j*4 + i] = a[0*4+i]*b[j*4+0] + a[1*4+i]*b[j*4+1] + a[2*4+i]*b[j*4+2] + a[3*4+i]*b[j*4+3]; } } } function updateWeaponPreview() { const primary = WEAPON_DATA.primary[selectedLoadout.primary]; const secondary = WEAPON_DATA.secondary[selectedLoadout.secondary]; const primaryStats = document.getElementById('primary-stats'); const secondaryStats = document.getElementById('secondary-stats'); // Update wapenvoorvertoningscanvassen voor de zojnieuw geselecteerde wapens if (primary) weaponPreviewState.primary.currentModel = selectedLoadout.primary; if (secondary) weaponPreviewState.secondary.currentModel = selectedLoadout.secondary; if (primaryStats && primary) { primaryStats.innerHTML = ` DAMAGE${primary.damage} FIRE RATE${primary.fireRate} RPM MAGAZINE${primary.magazine} `; } if (secondaryStats && secondary) { secondaryStats.innerHTML = ` DAMAGE${secondary.damage} FIRE RATE${secondary.fireRate} RPM MAGAZINE${secondary.magazine} `; } } window.selectCharacter = selectCharacter; window.updateWeaponPreview = updateWeaponPreview; // 3D-wapenvoorvertoning kijkers const weaponPreviewState = { primary: { canvas: null, gl: null, program: null, buffer: null, indexBuffer: null, active: false, currentModel: null, frameId: null, rotationMatrix: new Float32Array(16), vertexCount: 36 }, secondary: { canvas: null, gl: null, program: null, buffer: null, indexBuffer: null, active: false, currentModel: null, frameId: null, rotationMatrix: new Float32Array(16), vertexCount: 36 } }; function cleanupWeaponPreview(slot) { const state = weaponPreviewState[slot]; if (state.frameId) { cancelAnimationFrame(state.frameId); state.frameId = null; } if (state.gl) { if (state.program) state.gl.deleteProgram(state.program); if (state.buffer) state.gl.deleteBuffer(state.buffer); if (state.indexBuffer) state.gl.deleteBuffer(state.indexBuffer); state.gl = null; } state.program = null; state.buffer = null; state.indexBuffer = null; state.active = false; } function stopWeaponPreviews() { cleanupWeaponPreview('primary'); cleanupWeaponPreview('secondary'); } // Gedeelde shaders - gebruikt door zowel hoofd-WebGL-renderer als wapenvoorvertoning const SHARED_VS_SOURCE = ` attribute vec4 aPosition; attribute vec2 aTexCoord; uniform mat4 uProjection; uniform mat4 uView; uniform mat4 uModel; uniform vec3 uObjectOffset; varying vec2 vTexCoord; varying vec3 vWorldPos; void main() { vec4 worldPos = uModel * vec4(aPosition.x + uObjectOffset.x, aPosition.y + uObjectOffset.y, aPosition.z + uObjectOffset.z, 1.0); gl_Position = uProjection * uView * worldPos; vTexCoord = aTexCoord; vWorldPos = worldPos.xyz; }`; const SHARED_FS_SOURCE = ` precision mediump float; uniform sampler2D uSampler; uniform int uIsBuilding; uniform bool uUseColor; uniform vec4 uColor; varying vec2 vTexCoord; varying vec3 vWorldPos; void main() { if (uUseColor) { gl_FragColor = uColor; } else if (uIsBuilding == 1) { vec3 baseConcrete = vec3(0.32, 0.35, 0.38); float heightGlow = clamp((vWorldPos.y + 10.0) * 0.05, 0.5, 1.0); float noise = fract(sin(dot(vWorldPos.xz, vec2(12.9898, 78.233))) * 43758.5453); vec3 concreteColor = (baseConcrete + vec3(noise * 0.03)) * heightGlow; gl_FragColor = vec4(concreteColor, 1.0); } else { vec2 tiledTexCoord = fract(vTexCoord); vec4 texColor = texture2D(uSampler, tiledTexCoord); if (texColor.a == 0.0 || (texColor.r == 0.0 && texColor.g == 0.0 && texColor.b == 0.0)) { gl_FragColor = vec4(0.28, 0.35, 0.22, 1.0); } else { gl_FragColor = texColor; } } }`; function createSharedShader(gl) { const vs = gl.createShader(gl.VERTEX_SHADER); gl.shaderSource(vs, SHARED_VS_SOURCE); gl.compileShader(vs); if (!gl.getShaderParameter(vs, gl.COMPILE_STATUS)) { console.error('[DREngine] Vertex shader compile error:', gl.getShaderInfoLog(vs)); return null; } const fs = gl.createShader(gl.FRAGMENT_SHADER); gl.shaderSource(fs, SHARED_FS_SOURCE); gl.compileShader(fs); if (!gl.getShaderParameter(fs, gl.COMPILE_STATUS)) { console.error('[DREngine] Fragment shader compile error:', gl.getShaderInfoLog(fs)); return null; } const prog = gl.createProgram(); gl.attachShader(prog, vs); gl.attachShader(prog, fs); gl.linkProgram(prog); if (!gl.getProgramParameter(prog, gl.LINK_STATUS)) { console.error('[DREngine] Shader program link error:', gl.getProgramInfoLog(prog)); return null; } gl.deleteShader(vs); gl.deleteShader(fs); return prog; } function initWeaponPreview(slot) { const state = weaponPreviewState[slot]; const canvas = slot === 'primary' ? document.getElementById('primary-weapon-canvas') : document.getElementById('secondary-weapon-canvas'); if (!canvas || state.active) return; const gl = canvas.getContext('webgl2', { alpha: true, premultipliedAlpha: false, powerPreference: "low-power" }); if (!gl) { console.warn('[DREngine] No WebGL context for weapon preview'); return; } state.canvas = canvas; state.gl = gl; canvas.width = canvas.clientWidth * 2; canvas.height = canvas.clientHeight * 2; gl.viewport(0, 0, canvas.width, canvas.height); const prog = createSharedShader(gl); if (!prog) return; state.program = prog; const uModelLoc = gl.getUniformLocation(prog, "uModel"); const uProjectionLoc = gl.getUniformLocation(prog, "uProjection"); const uViewLoc = gl.getUniformLocation(prog, "uView"); const uColorLoc = gl.getUniformLocation(prog, "uColor"); const uUseColorLoc = gl.getUniformLocation(prog, "uUseColor"); const uObjectOffsetLoc = gl.getUniformLocation(prog, "uObjectOffset"); const uIsBuildingLoc = gl.getUniformLocation(prog, "uIsBuilding"); const sSamplerLoc = gl.getUniformLocation(prog, "uSampler"); const aPositionLoc = gl.getAttribLocation(prog, "aPosition"); const aTexCoordLoc = gl.getAttribLocation(prog, "aTexCoord"); state.uniforms = { uModelLoc, uProjectionLoc, uViewLoc, uColorLoc, uUseColorLoc, uObjectOffsetLoc, uIsBuildingLoc, sSamplerLoc, aPositionLoc, aTexCoordLoc }; const verts = new Float32Array([ -0.15, -0.08, 0.15, 0.15, -0.08, 0.15, 0.15, 0.08, 0.15, -0.15, 0.08, 0.15, -0.15, -0.08, -0.15, -0.15, 0.08, -0.15, 0.15, 0.08, -0.15, 0.15, -0.08, -0.15 ]); state.buffer = gl.createBuffer(); gl.bindBuffer(gl.ARRAY_BUFFER, state.buffer); gl.bufferData(gl.ARRAY_BUFFER, verts, gl.STATIC_DRAW); const idx = new Uint16Array([ 0,1,2, 0,2,3, 4,5,6, 4,6,7, 3,2,6, 3,6,5, 0,1,7, 0,7,4, 1,7,6, 1,6,2, 0,4,5, 0,5,3 ]); state.indexBuffer = gl.createBuffer(); gl.bindBuffer(gl.ELEMENT_ARRAY_BUFFER, state.indexBuffer); gl.bufferData(gl.ELEMENT_ARRAY_BUFFER, idx, gl.STATIC_DRAW); state.vertexCount = 36; // Initialiseer rotatiematrix (identiteit) const m = state.rotationMatrix; m[0] = 1; m[1] = 0; m[2] = 0; m[3] = 0; m[4] = 0; m[5] = 1; m[6] = 0; m[7] = 0; m[8] = 0; m[9] = 0; m[10] = 1; m[11] = 0; m[12] = 0; m[13] = 0; m[14] = 0; m[15] = 1; state.currentModel = slot === 'primary' ? selectedLoadout.primary : selectedLoadout.secondary; state.active = true; // Vraag WASM-core om wapenmodel te laden (skelet, botten, hitboxes allemaal verwerkt in C++) if (typeof Module !== 'undefined' && Module._loadWeaponModel) { const weaponKey = state.currentModel; if (weaponKey) { const ptr = Module.allocateUTF8(weaponKey, 256); try { Module._loadWeaponModel(ptr); } finally { Module._free(ptr); } } } renderWeaponPreview(slot); } function renderWeaponPreview(slot) { const state = weaponPreviewState[slot]; if (!state.active || !state.gl || !state.program) return; const weaponKey = slot === 'primary' ? selectedLoadout.primary : selectedLoadout.secondary; const weapon = slot === 'primary' ? WEAPON_DATA.primary[weaponKey] : WEAPON_DATA.secondary[weaponKey]; const gl = state.gl; const canvas = state.canvas; gl.viewport(0, 0, canvas.width, canvas.height); gl.clearColor(0, 0, 0, 0); gl.clear(gl.COLOR_BUFFER_BIT | gl.DEPTH_BUFFER_BIT); gl.enable(gl.DEPTH_TEST); gl.useProgram(state.program); const u = state.uniforms; // Controleer op modelwijziging en informeer WASM-core if (state.currentModel !== weaponKey) { state.currentModel = weaponKey; if (typeof Module !== 'undefined' && Module._loadWeaponModel && weaponKey) { const ptr = Module.allocateUTF8(weaponKey, 256); try { Module._loadWeaponModel(ptr); } finally { Module._free(ptr); } } } // Verkrijg wapenvertexgegevens van WASM-core (klaar-om-vertex-buffers) let vertexCount = state.vertexCount; if (typeof Module !== 'undefined' && Module._getWeaponVertexCount) { const newCount = Module._getWeaponVertexCount(); if (newCount > 0) { vertexCount = newCount; state.vertexCount = vertexCount; } } // Update vertexbuffer van WASM indien beschikbaar if (typeof Module !== 'undefined' && Module._getWeaponVertexPointer && Module.HEAPF32) { const vertexPtr = Module._getWeaponVertexPointer(); if (vertexPtr) { const vertexCount = Module._getWeaponVertexCount(); gl.bindBuffer(gl.ARRAY_BUFFER, state.buffer); const heapView = new Float32Array(Module.HEAPF32.buffer, vertexPtr, vertexCount * 8); gl.bufferSubData(gl.ARRAY_BUFFER, 0, heapView); } } gl.enableVertexAttribArray(u.aPositionLoc); gl.bindBuffer(gl.ARRAY_BUFFER, state.buffer); gl.vertexAttribPointer(u.aPositionLoc, 3, gl.FLOAT, false, 32, 0); // Zet texcoord-attribuut als buffer UVs heeft if (u.aTexCoordLoc >= 0) { gl.enableVertexAttribArray(u.aTexCoordLoc); gl.vertexAttribPointer(u.aTexCoordLoc, 2, gl.FLOAT, false, 32, 12); } const aspect = canvas.width / canvas.height; const f = 1.0 / Math.tan(30 * Math.PI / 180); // Herobruik projMatrix-buffer (module-niveau gecachte Float32Array) projMatrix[0] = f / aspect; projMatrix[1] = 0; projMatrix[2] = 0; projMatrix[3] = 0; projMatrix[4] = 0; projMatrix[5] = f; projMatrix[6] = 0; projMatrix[7] = 0; projMatrix[8] = 0; projMatrix[9] = 0; projMatrix[10] = 1; projMatrix[11] = 0; projMatrix[12] = 0; projMatrix[13] = 0; projMatrix[14] = 1.2; projMatrix[15] = 1; gl.uniformMatrix4fv(u.uProjectionLoc, false, projMatrix); // Herobruik viewMatrix (identiteit voor voorvertoning) viewMatrix[0] = 1; viewMatrix[1] = 0; viewMatrix[2] = 0; viewMatrix[3] = 0; viewMatrix[4] = 0; viewMatrix[5] = 1; viewMatrix[6] = 0; viewMatrix[7] = 0; viewMatrix[8] = 0; viewMatrix[9] = 0; viewMatrix[10] = 1; viewMatrix[11] = 0; viewMatrix[12] = 0; viewMatrix[13] = 0; viewMatrix[14] = 0; viewMatrix[15] = 1; gl.uniformMatrix4fv(u.uViewLoc, false, viewMatrix); // Pas rotatiematrix van WASM/anim-toestand toe gl.uniformMatrix4fv(u.uModelLoc, false, state.rotationMatrix); gl.uniform3f(u.uObjectOffsetLoc, 0, 0, 0); const color = weapon ? [0.55, 0.55, 0.58, 1.0] : [0.4, 0.4, 0.4, 1.0]; gl.uniform4fv(u.uColorLoc, color); gl.uniform1i(u.uUseColorLoc, true); gl.uniform1i(u.uIsBuildingLoc, 0); gl.bindBuffer(gl.ELEMENT_ARRAY_BUFFER, state.indexBuffer); gl.drawElements(gl.TRIANGLES, state.vertexCount, gl.UNSIGNED_SHORT, 0); state.frameId = requestAnimationFrame(() => renderWeaponPreview(slot)); } // Initialiseer wapenvoorvertoningen wanneer dashboard wordt getoond (WASM-core verwerkt echte model-renderen) const weaponPreviewObserver = new MutationObserver(() => { if (document.getElementById('primary-weapon-canvas') && !weaponPreviewState.primary.active) { initWeaponPreview('primary'); } if (document.getElementById('secondary-weapon-canvas') && !weaponPreviewState.secondary.active) { initWeaponPreview('secondary'); } }); weaponPreviewObserver.observe(document.body, { childList: true, subtree: true }); let islandBuildings = []; function preloadMapBuildings(mapName) { if (typeof Module !== 'undefined' && Module._getMapBuildingsPointer && Module._getMapBuildingsCount) { try { const ptr = Module._getMapBuildingsPointer(); const count = Module._getMapBuildingsCount(); if (ptr && count > 0) { const buildings = []; const structSize = 32; for (let i = 0; i < count; i++) { const offset = ptr + i * structSize; buildings.push({ id: `b${i}`, x: Module.HEAPF32[offset / 4], y: Module.HEAPF32[(offset + 4) / 4], w: Module.HEAPF32[(offset + 8) / 4], h: Module.HEAPF32[(offset + 12) / 4], label: `Building ${i + 1}` }); } islandBuildings = buildings; console.log(`[DREngine] Loaded ${buildings.length} buildings from WASM core`); } } catch (e) { console.warn('[DREngine] WASM building data unavailable, using empty', e.message); } } } function handleGameInputDown(e) { const key = e.key.toUpperCase(); if (key === KEYBINDS.FORWARD) activeKeysPressed.FORWARD = true; if (key === KEYBINDS.LEFT) activeKeysPressed.LEFT = true; if (key === KEYBINDS.BACKWARD) activeKeysPressed.BACKWARD = true; if (key === KEYBINDS.RIGHT) activeKeysPressed.RIGHT = true; if (e.shiftKey || key === "SHIFT") activeKeysPressed.SPRINT = true; if (e.ctrlKey || key === "CONTROL") activeKeysPressed.CROUCH = true; if (e.key === " " || e.code === "Space") activeKeysPressed.JUMP = true; if (e.key.toUpperCase() === "G") { var bm = document.getElementById("dw-bigmap-container"); if (bm) bm.style.display = "block"; } if (e.code === "KeyN" && window.dangerUser?.isAdmin) { isNoclipActive = !isNoclipActive; console.log(`[DREngine] Noclip ${isNoclipActive ? 'AAN' : 'UIT'}`); } if (e.key.toUpperCase() === "B") { toggleBuyMenu(); } if (e.key.toUpperCase() === "T" && (window.dangerUser?.isAdmin || true)) { if (document.getElementById("dw-chat-input")) { toggleChatInput(); } else { toggleTablet(); } } if (e.key.toUpperCase() === "ENTER" && document.getElementById("dw-chat-input") && document.getElementById("dw-chat-input").style.display !== "none") { submitChatMessage(); e.preventDefault(); return; } // ESC key always triggers exit menu if (e.key === "Escape" || e.key === "Esc" || e.keyCode === 27) { const gameCanvas = document.getElementById("canvas"); if (gameCanvas && gameCanvas.style.display === "block") { e.preventDefault(); if (document.pointerLockElement === gameCanvas) { document.exitPointerLock(); } returnToDashboard(); return; } if (document.pointerLockElement === gameCanvas) { document.exitPointerLock(); } else if (gameCanvas && gameCanvas.style.display === "block") { handleInGameEscapeMenu(); } } if (!holdingTablet && !holdingBuyMenu) { // Reload-interruptmatrix: slotschakelen tijdens herladen annuleert herladen if (isReloading && (e.key >= '1' && e.key <= '5')) { isReloading = false; } if (e.key === "1") { if (playerWeapons[1]) { activeWeaponSlot = 1; lastSlotSwitchTime = Date.now(); updateActiveWeapon(); } else { activeWeaponSlot = 0; lastSlotSwitchTime = Date.now(); updateActiveWeapon(); } } if (e.key === "2") { if (playerWeapons[2]) { activeWeaponSlot = 2; lastSlotSwitchTime = Date.now(); updateActiveWeapon(); } } if (e.key === "3") { activeWeaponSlot = 3; lastSlotSwitchTime = Date.now(); updateActiveWeapon(); } if (e.key === "4") { if (playerWeapons[4]) { activeWeaponSlot = 4; lastSlotSwitchTime = Date.now(); updateActiveWeapon(); } } if (e.key === "5") { // Slot 5: Tablet - spawn via native WASM als Speelmodus 2 if (typeof Module !== 'undefined' && Module && window._wasmRuntimeReady) { if (typeof Module._setPlayerWeaponSlotNative === 'function') { Module._setPlayerWeaponSlotNative(localPlayerId || 0, 5, 6); } } activeWeaponSlot = 5; holdingTablet = true; updateActiveWeapon(); } } } function handleGameInputUp(e) { const key = e.key.toUpperCase(); if (e.key === " ") e.preventDefault(); if (key === KEYBINDS.FORWARD) activeKeysPressed.FORWARD = false; if (key === KEYBINDS.LEFT) activeKeysPressed.LEFT = false; if (key === KEYBINDS.BACKWARD) activeKeysPressed.BACKWARD = false; if (key === KEYBINDS.RIGHT) activeKeysPressed.RIGHT = false; if (key === "SHIFT") activeKeysPressed.SPRINT = false; if (key === "CONTROL") activeKeysPressed.CROUCH = false; if (e.key === " " || e.code === "Space") activeKeysPressed.JUMP = false; if (e.key.toUpperCase() === "G") { var bm = document.getElementById("dw-bigmap-container"); if (bm) bm.style.display = "none"; } } function handleMouseMoveLook(e) { var canvas = document.getElementById("canvas"); if (document.pointerLockElement === canvas || document.mozPointerLockElement === canvas) { const mouseDelta = Math.abs(e.movementX) + Math.abs(e.movementY); jitterHistory.push({ t: Date.now(), delta: mouseDelta }); if (jitterHistory.length > 30) jitterHistory.shift(); // Bereken spanning van muisjitter (0-1 bereik) const now = Date.now(); const recentJitters = jitterHistory.filter(j => now - j.t < 1000); const avgJitter = recentJitters.reduce((sum, j) => sum + j.delta, 0) / (recentJitters.length || 1); nervousnessLevel = Math.min(1.0, avgJitter / 80); cameraLook.x -= e.movementX * 0.002 * MOUSE_SENSITIVITY; cameraLook.y -= e.movementY * 0.002 * MOUSE_SENSITIVITY; cameraLook.y = Math.max(-0.8, Math.min(0.8, cameraLook.y)); } } function pointerLockChangeHandler() { var gameCanvas = document.getElementById("canvas"); if (document.pointerLockElement === gameCanvas || document.mozPointerLockElement === gameCanvas) { console.log("[DangerRoyale Core] Cursor lock activated."); } } function exitActiveGame() { console.log("[DREngine] Match ended. Hiding UI cleanly..."); if (socket) { socket.emit('leave_server'); } if (document.pointerLockElement) { document.exitPointerLock(); } const overlays = [ "dw-hud-overlay", "dw-compass-overlay", "dw-minimap-container", "dw-bigmap-container", "dw-crosshair", "dw-game-chat", "canvas", "dw-escape-panel", "dw-player-list-overlay" ]; overlays.forEach(id => { let el = document.getElementById(id); if (el) el.style.display = "none"; }); let mainMenu = document.getElementById("main-menu"); if (mainMenu) mainMenu.style.display = "block"; let menuDashboard = document.getElementById("menu-dashboard"); if (menuDashboard) menuDashboard.style.display = "block"; // 5. Reset alle Battle Royale stats naar de beginwaarden voor een volgende pot currentHP = 100; currentAmmo = 30; reserveMagazines = 4; isReloading = false; zoneTimer = 0; zoneSeconds = 0; battleZone.radius = 200.0; battleZone.targetRadius = 200.0; battleZone.isShrinking = false; battleZone.phase = 1; nextPhaseTime = 300; // Reset naar de 5 minuten roamtijd! // Reset de HUD tekst alvast voor de volgende match let hpElement = document.getElementById("hud-hp"); if (hpElement) hpElement.innerText = `HP: 100`; // Maak WebGL-bronnen op to prevent memory leaks if (rafId !== null) { cancelAnimationFrame(rafId); rafId = null; } if (gl) { if (mapTexture) gl.deleteTexture(mapTexture); if (weaponTexture) gl.deleteTexture(weaponTexture); if (charBuffer) gl.deleteBuffer(charBuffer); if (indexBuffer) gl.deleteBuffer(indexBuffer); if (texCoordBuffer) gl.deleteBuffer(texCoordBuffer); if (weaponBuffer) gl.deleteBuffer(weaponBuffer); if (weaponIndexBuffer) gl.deleteBuffer(weaponIndexBuffer); if (weaponUVBuffer) gl.deleteBuffer(weaponUVBuffer); if (window.staticWorldMapBuffer) gl.deleteBuffer(window.staticWorldMapBuffer); if (program) gl.deleteProgram(program); } gl = null; program = null; mapTexture = null; weaponTexture = null; charBuffer = null; indexBuffer = null; texCoordBuffer = null; weaponBuffer = null; weaponIndexBuffer = null; weaponUVBuffer = null; window.staticWorldMapBuffer = null; isMapLoaded = false; isMdlLoaded = false; window.dangerRenderStopped = true; window.dangerGameActive = false; let compassOverlay = document.getElementById("dw-compass-overlay"); if (compassOverlay) compassOverlay.style.display = "none"; console.log("[DangerRoyale Core] Reset voltooid. Terug op dashboard."); } window.playerMoney = 0; function spawnCashStacks() { cashStacks = []; if (islandBuildings.length > 0) { islandBuildings.forEach(b => { cashStacks.push({ id: b.id + "-cash", x: b.x, z: b.y, y: 0.5, value: 50 + Math.floor(Math.random() * 100), collected: false }); }); } // Spawn ook enkele willekeurige geldstapels for (let i = 0; i < 5; i++) { const angle = (i / 5) * Math.PI * 2; cashStacks.push({ id: `cash-${i}`, x: Math.cos(angle) * 80, z: Math.sin(angle) * 80, y: 0.5, value: 25 + Math.floor(Math.random() * 50), collected: false }); } } function createExplosion(x, y, z) { explosions.push({ x: x, y: y, z: z, scale: 0.1, alpha: 1.0, startTime: Date.now(), duration: 800 }); if (window.AudioEngine && window.AudioEngine.playExplosionSound) { window.AudioEngine.playExplosionSound(); } // Shake camera briefly cameraLook.x += (Math.random() - 0.5) * 0.1; cameraLook.y += (Math.random() - 0.5) * 0.05; console.log(`[EXPLOSION] at (${x.toFixed(1)}, ${y.toFixed(1)}, ${z.toFixed(1)})`); if (socket) { socket.emit('explosion', { x, y, z }); } } function createBuyMenuHTML() { let html = `
`; html += `
BUY WEAPONS ($${playerMoney})
`; const ammoCost = 300; html += `
PRIMARY
`; // Use dynamic weapon data from WEAPON_DATA const primaryKeys = Object.keys(WEAPON_DATA.primary); if (primaryKeys.length === 0) { html += `
No primary weapons discovered.
`; } else { for (const key of primaryKeys) { const weapon = WEAPON_DATA.primary[key]; const canAfford = playerMoney >= weapon.cost; html += `
`; html += `${weapon.name} $${weapon.cost}`; html += ``; html += `
`; } } html += `
SECONDARY
`; const secondaryKeys = Object.keys(WEAPON_DATA.secondary); if (secondaryKeys.length === 0) { html += `
No secondary weapons discovered.
`; } else { for (const key of secondaryKeys) { const weapon = WEAPON_DATA.secondary[key]; const canAfford = playerMoney >= weapon.cost; html += `
`; html += `${weapon.name} $${weapon.cost}`; html += ``; html += `
`; } } html += `
`; html += ``; html += `
`; html += `
`; return html; } function toggleBuyMenu() { if (holdingTablet) return; let buyMenu = document.getElementById("dw-buy-menu"); if (!buyMenu) { buyMenu = document.createElement("div"); buyMenu.id = "dw-buy-menu"; buyMenu.style.cssText = "position:fixed; top:100px; right:20px; width:220px; z-index:99999; font-family:monospace;"; buyMenu.innerHTML = createBuyMenuHTML(); document.body.appendChild(buyMenu); holdingBuyMenu = true; } else { buyMenu.remove(); holdingBuyMenu = false; } } function buyWeapon(weaponKey, cost, slot) { // Client sends purchase request; server validates coins and assigns weapon authoritatively const buyMenu = document.getElementById("dw-buy-menu"); if (buyMenu) { buyMenu.remove(); holdingBuyMenu = false; } if (gameSocketInstance && gameSocketInstance.connected) { gameSocketInstance.emit('purchase_weapon', { weapon: weaponKey, cost: cost, slot: slot }); } else if (socket && socket.connected) { socket.emit('purchase_weapon', { weapon: weaponKey, cost: cost, slot: slot }); } console.log(`[DREngine] Purchase request sent: ${weaponKey} for slot ${slot}`); } function buyAmmo(cost) { if (playerMoney < cost) { const cashEl = document.getElementById("hud-cash"); if (cashEl) cashEl.style.color = "#ff3333"; setTimeout(() => { const el = document.getElementById("hud-cash"); if (el) el.style.color = ""; }, 300); return; } playerMoney -= cost; const cashEl = document.getElementById("hud-cash"); if (cashEl) cashEl.innerText = `$${playerMoney}`; if (currentWeapon && (activeWeaponSlot === 1 || activeWeaponSlot === 2)) { const weaponKey = activeWeaponSlot === 1 ? selectedLoadout.primary : selectedLoadout.secondary; const weapon = WEAPON_DATA[activeWeaponSlot === 1 ? 'primary' : 'secondary'][weaponKey]; if (weapon) { const fullMag = weapon.magazine; const maxReserves = weapon.reserves; const neededReserves = Math.min(maxReserves - (reserveMagazines * fullMag), fullMag); if (neededReserves > 0) { const newReserveMags = Math.floor(neededReserves / fullMag); reserveMagazines += newReserveMags; } currentAmmo = fullMag; maxAmmoPerMag = fullMag; updateActiveWeapon(); } } if (socket) { socket.emit('purchase_ammo', { cost: cost }); } console.log('[DREngine] Bought ammo refill for $' + cost); const buyMenu = document.getElementById("dw-buy-menu"); if (buyMenu) { buyMenu.remove(); holdingBuyMenu = false; } } function toggleTablet() { holdingTablet = !holdingTablet; const tablet = document.getElementById("dw-tablet"); if (!tablet) return; if (holdingTablet) { tablet.style.display = "block"; tablet.innerHTML = `
💊 DANGERWEB TABLET
💰 CASH: $${playerMoney}
📍 POSITION: X:${Math.round(playerPos.x)}, Z:${Math.round(playerPos.y)}
🎯 WEAPONS:
🗺 ZONE STATUS:
`; updateTabletWeapons(); updateTabletZone(); } else { tablet.innerHTML = ""; tablet.style.display = "none"; } } function updateTabletWeapons() { const container = document.getElementById("tablet-weapons"); if (!container) return; let html = `
PRIMARY: ${WEAPON_DATA.primary[selectedLoadout.primary]?.name || selectedLoadout.primary}
`; html += `
SECONDARY: ${WEAPON_DATA.secondary[selectedLoadout.secondary]?.name || selectedLoadout.secondary}
`; container.innerHTML = html; } function updateTabletZone() { const container = document.getElementById("tablet-zone"); if (!container) return; const timeLeft = nextPhaseTime - zoneSeconds; const timeStr = battleZone.isShrinking ? "KRIMPEN BEZIG" : `${Math.floor(timeLeft / 60)}:${(timeLeft % 60).toString().padStart(2, '0')}`; container.innerHTML = `Phase: ${battleZone.phase} | Zone: ${battleZone.radius.toFixed(0)}m | Timer: ${timeStr}`; } function initTabletElement() { if (!document.getElementById("dw-tablet")) { const tablet = document.createElement("div"); tablet.id = "dw-tablet"; tablet.style.cssText = "position:fixed; top:0; left:0; width:100vw; height:100vh; pointer-events:none; z-index:9998; display:none;"; document.body.appendChild(tablet); } } // Dropdown beheer functies function addModelToDropdown(name) { const dropdown = document.getElementById("player-model-dropdown"); if (dropdown) { const option = document.createElement("option"); option.value = name.toLowerCase().replace(/\s+/g, '_'); option.innerText = name; dropdown.appendChild(option); } } function loadStoredPlayerModels() { let extraModels = JSON.parse(localStorage.getItem("dw_custom_models")) || []; extraModels.forEach(model => addModelToDropdown(model)); const savedActive = localStorage.getItem("dw_active_model"); if (savedActive) { const dropdown = document.getElementById("player-model-dropdown"); if (dropdown) dropdown.value = savedActive; } if (window.updatePlayerPreview) window.updatePlayerPreview(selectedLoadout.character); } function changePlayerModel() { const dropdown = document.getElementById("player-model-dropdown"); if (dropdown) { const activeModel = dropdown.value; localStorage.setItem("dw_active_model", activeModel); window.updatePlayerPreview?.(activeModel); } } // 📱 DRENGINE BINARY LOADER BRIDGE: Streamt de .glb bytes dynamic naar het videogeheugen let isMdlLoaded = false; let isMapLoaded = false; let mapVertices = []; let gl = null; let program = null; let uProjection = null; let uView = null; let uColor = null; let uObjectOffset = null; let uUseColor = null; let uIsBuildingLoc = null; let uModel = null; let charBuffer = null; let indexBuffer = null; let texCoordBuffer = null; let mapTexture = null; let weaponTexture = null; let weaponBuffer = null; let weaponIndexBuffer = null; let weaponUVBuffer = null; let canvasEl = null; let rafId = null; let staticWorldMapSize = null; let staticWorldMapVertexRef = null; let projMatrix = new Float32Array(16); let viewMatrix = new Float32Array(16); let identityMatrix = new Float32Array([1, 0, 0, 0, 0, 1, 0, 0, 0, 0, 1, 0, 0, 0, 0, 1]); let aPositionLoc = null; let aTexCoordLoc = null; let sSamplerLoc = null; const DEFAULT_MAP_COLORS = { "Backrooms": [0.15, 0.15, 0.17], "Desert_Short": [0.85, 0.70, 0.40], "Hallway": [0.50, 0.50, 0.50], "Landscape": [0.20, 0.45, 0.25], "Maxentius": [0.40, 0.33, 0.25] }; function getMapColor(mapName) { return DEFAULT_MAP_COLORS[mapName] || [0.32, 0.35, 0.38]; } function findMapAsset(mapName) { const found = DISCOVERED_MAPS.find(m => m.name === mapName); if (found) return found; return DISCOVERED_MAPS[0] || null; } function preloadWorldMap(mapName) { console.log(`[DREngine] Loading map: ${mapName}...`); const loaderText = document.getElementById("loader-subtext"); const mapColor = getMapColor(mapName); if (loaderText) loaderText.innerText = `LOADING 3D MAP: ${mapName.toUpperCase()}...`; setTimeout(async () => { try { let mapDataBytes = null; let customBspBuffer = null; if (window._dangerCustomBsp && window._dangerCustomBsp.name === mapName) { customBspBuffer = window._dangerCustomBsp.buffer; mapDataBytes = new Uint8Array(customBspBuffer); console.log(`[DREngine] Using custom BSP: ${window._dangerCustomBsp.file.name} (${mapDataBytes.length} bytes)`); } if (window._soloServerConfig?.customBsp?.bspPath && !mapDataBytes) { try { const response = await fetch(window._soloServerConfig.customBsp.bspPath); if (response.ok) { mapDataBytes = new Uint8Array(await response.arrayBuffer()); console.log(`[DREngine] Custom BSP loaded from server: ${mapDataBytes.length} bytes`); } } catch (e) { console.warn("[DREngine] Custom BSP fetch from server failed:", e); } } if (!mapDataBytes) { const mapAsset = findMapAsset(mapName); const bspPath = mapAsset ? `${mapAsset.path}/${mapAsset.bsp}` : `/custom_assets/maps/${mapName}.bsp`; try { const response = await fetch(bspPath); if (response.ok) { mapDataBytes = new Uint8Array(await response.arrayBuffer()); console.log(`[DREngine] BSP loaded: ${mapDataBytes.length} bytes`); } } catch (e) { console.warn("[DREngine] Map BSP file not found in custom_assets"); } } if (mapDataBytes && typeof Module !== 'undefined' && Module.FS && typeof Module.FS.writeFile === 'function') { try { Module.FS.mkdir('/drengine/maps', 0o777, true); Module.FS.writeFile(`/drengine/maps/${mapName}.bsp`, mapDataBytes); console.log(`[DREngine] Map data written to WASM FS`); } catch (fsErr) { console.warn("[DREngine] FS write failed:", fsErr.message); } } if (mapDataBytes && mapDataBytes.length > 0 && typeof Module !== 'undefined' && Module._getMapVerticesPointer && Module._getMapVertexCount) { const vertexPtr = Module._getMapVerticesPointer(); const vertexCount = Module._getMapVertexCount(); if (vertexPtr && vertexCount > 0) { mapVertices = new Float32Array(Module.HEAPF32.buffer, vertexPtr, vertexCount); console.log(`[DREngine] WASM core returned ${vertexCount} vertices`); } else { mapVertices = generateFallbackMapVertices(mapName); } } else { mapVertices = generateFallbackMapVertices(mapName); } window._dangerMapColor = mapColor; preloadMapBuildings(mapName); isMapLoaded = true; if (loaderText) loaderText.innerText = "3D GRAPHICS PIPELINE INITIALISEREN..."; } catch (e) { console.error("[DREngine] Error loading map:", e); isMapLoaded = true; mapVertices = generateFallbackMapVertices(mapName); window._dangerMapColor = mapColor; } }, 100); } function generateFallbackMapVertices(mapName) { const color = getMapColor(mapName); const size = 200; return new Float32Array([ -size, -2, -size, size, -2, -size, -size, -2, size, size, -2, -size, size, -2, size, -size, -2, size ]); } function preloadGLBModel(modelName) { if (!modelName || typeof modelName !== 'string' || modelName.trim() === '') { console.warn(`[DREngine] No model name provided, cannot preload.`); return; } try { const cleanName = modelName.replace(/^tm_/, ''); const modelUrl = `/custom_assets/models/player/${cleanName}.glb`; const xhr = new XMLHttpRequest(); xhr.open("GET", modelUrl, true); xhr.responseType = "arraybuffer"; xhr.onload = function() { if (xhr.status === 200 && xhr.response) { const arrayBuffer = xhr.response; const modelBytes = new Uint8Array(arrayBuffer); if (typeof Module !== 'undefined' && Module.FS && typeof Module.FS.writeFile === 'function') { try { Module.FS.writeFile(`/drengine/models/player/${cleanName}.glb`, modelBytes); console.log(`[DREngine] Model ${cleanName}.glb (${modelBytes.length} bytes) written to WASM FS`); } catch (fsErr) { console.warn("[DREngine] FS write failed for model — graceful degradation, UI remains active:", fsErr.message); } } isMdlLoaded = true; console.log(`[DREngine] Player model (${cleanName}.glb) ready for WASM core.`); } else { console.error(`[DREngine Error] Could not find ${modelUrl} - using fallback.`); } }; xhr.onerror = function() { // Graceful degradation: 3D model failed to load, but UI stays fully interactive console.warn(`[DREngine] XHR-error loading ${cleanName}.glb — 3D disabled, UI active`); isMdlLoaded = true; const _el = document.getElementById("engine-loader"); if (_el) _el.style.display = "none"; }; xhr.send(); } catch (e) { // Catch-all: if SharedArrayBuffer or any other browser restriction blocks 3D, // log the warning but let the rest of the application run uninterrupted. console.warn("[DREngine] preloadGLBModel failed — graceful degradation, UI stays active:", e.message); isMdlLoaded = true; const _el = document.getElementById("engine-loader"); if (_el) _el.style.display = "none"; } } function initWebGLViewport(mapName, sizeMb) { console.log(`[DREngine] Initializing WebGL viewport for ${mapName}...`); const canvas = document.getElementById("canvas"); if (!canvas) { console.error("[DREngine] Canvas not found!"); return; } canvasEl = canvas; canvas.width = window.innerWidth; canvas.height = window.innerHeight; canvas.style.display = "block"; gl = canvas.getContext("webgl2", { antialias: true, alpha: true, depth: true, stencil: true, preserveDrawingBuffer: false, powerPreference: "low-power" }); if (!gl) { console.error("[DREngine] WebGL2 not supported!"); return; } gl.enable(gl.DEPTH_TEST); gl.depthFunc(gl.LEQUAL); gl.enable(gl.CULL_FACE); gl.cullFace(gl.BACK); gl.frontFace(gl.CCW); gl.clearColor(0.05, 0.12, 0.18, 1.0); gl.clearDepth(1.0); gl.viewport(0, 0, canvas.width, canvas.height); // Use shared shader sources (same as weapon preview and main game) program = createSharedShader(gl); if (!program) { console.error("[DREngine] Failed to create shared shader program!"); return; } gl.useProgram(program); uProjection = gl.getUniformLocation(program, "uProjection"); uView = gl.getUniformLocation(program, "uView"); let mapGridVertices = []; for (let i = -20; i <= 20; i += 2) { mapGridVertices.push(i, -0.5, -20, i, -0.5, 20); mapGridVertices.push(-20, -0.5, i, 20, -0.5, i); } var mapBuffer = gl.createBuffer(); gl.bindBuffer(gl.ARRAY_BUFFER, mapBuffer); gl.bufferData(gl.ARRAY_BUFFER, new Float32Array(mapGridVertices), gl.STATIC_DRAW); let charBoxVertices = [ -0.15, -0.4, 0.15, 0.15, -0.4, 0.15, 0.15, 0.4, 0.15, -0.15, 0.4, 0.15, -0.15, -0.4, -0.15, -0.15, 0.4, -0.15, 0.15, 0.4, -0.15, 0.15, -0.4, -0.15 ]; charBuffer = gl.createBuffer(); gl.bindBuffer(gl.ARRAY_BUFFER, charBuffer); gl.bufferData(gl.ARRAY_BUFFER, new Float32Array(charBoxVertices), gl.STATIC_DRAW); let charIndices = [ 0, 1, 2, 0, 2, 3, 4, 5, 6, 4, 6, 7, 3, 2, 6, 3, 6, 5, 0, 1, 7, 0, 7, 4, 1, 7, 6, 1, 6, 2, 0, 4, 5, 0, 5, 3 ]; indexBuffer = gl.createBuffer(); gl.bindBuffer(gl.ELEMENT_ARRAY_BUFFER, indexBuffer); gl.bufferData(gl.ELEMENT_ARRAY_BUFFER, new Uint16Array(charIndices), gl.STATIC_DRAW); var aPosition = gl.getAttribLocation(program, "aPosition"); aPositionLoc = aPosition; aTexCoordLoc = gl.getAttribLocation(program, "aTexCoord"); sSamplerLoc = gl.getUniformLocation(program, "uSampler"); uColor = gl.getUniformLocation(program, "uColor"); uObjectOffset = gl.getUniformLocation(program, "uObjectOffset"); uUseColor = gl.getUniformLocation(program, "uUseColor"); uIsBuildingLoc = gl.getUniformLocation(program, "uIsBuilding"); uModel = gl.getUniformLocation(program, "uModel"); uProjection = gl.getUniformLocation(program, "uProjection"); uView = gl.getUniformLocation(program, "uView"); // === MULTIPLAYER GLB INTERNAL TEXTURE PIPELINE === function loadEmbeddedGlbTexture(gl, textureIndex) { const texture = gl.createTexture(); gl.bindTexture(gl.TEXTURE_2D, texture); console.warn(`[DangerRoyale Engine] Texture #${textureIndex}: GLB data is not an image, using solid color.`); gl.texImage2D(gl.TEXTURE_2D, 0, gl.RGBA, 1, 1, 0, gl.RGBA, gl.UNSIGNED_BYTE, new Uint8Array([64, 97, 71, 255])); gl.generateMipmap(gl.TEXTURE_2D); gl.texParameteri(gl.TEXTURE_2D, gl.TEXTURE_MIN_FILTER, gl.LINEAR); gl.texParameteri(gl.TEXTURE_2D, gl.TEXTURE_MAG_FILTER, gl.LINEAR); gl.bindTexture(gl.TEXTURE_2D, null); return texture; } mapTexture = loadEmbeddedGlbTexture(gl, 0); // ECHTE UV-COÖRDINATEN UIT JE BLENDER EXPORT MERGEN texCoordBuffer = gl.createBuffer(); gl.bindBuffer(gl.ARRAY_BUFFER, texCoordBuffer); if (typeof Module !== 'undefined' && Module._getMapTexCoordsPointer && Module._getMapTexCoordsCount) { const uvPtr = Module._getMapTexCoordsPointer(); const uvCount = Module._getMapTexCoordsCount(); const mapUVs = new Float32Array(Module.HEAPF32.buffer, uvPtr, uvCount); gl.bufferData(gl.ARRAY_BUFFER, mapUVs, gl.STATIC_DRAW); } else { gl.bufferData(gl.ARRAY_BUFFER, new Float32Array(mapVertices.length * (2/3)), gl.STATIC_DRAW); } // Weapon texture (with fallback to solid color if file not found) weaponTexture = gl.createTexture(); gl.bindTexture(gl.TEXTURE_2D, weaponTexture); gl.texImage2D(gl.TEXTURE_2D, 0, gl.RGBA, 1, 1, 0, gl.RGBA, gl.UNSIGNED_BYTE, new Uint8Array([100, 110, 120, 255])); gl.texParameteri(gl.TEXTURE_2D, gl.TEXTURE_MIN_FILTER, gl.LINEAR); gl.texParameteri(gl.TEXTURE_2D, gl.TEXTURE_MAG_FILTER, gl.LINEAR); gl.bindTexture(gl.TEXTURE_2D, null); const weaponTexturePath = selectedLoadout?.primary ? WEAPON_DATA.primary[selectedLoadout.primary]?.texture || "/custom_assets/textures/weapons/v_models/ar47_core.png" : "/custom_assets/textures/weapons/v_models/ar47_core.png"; const weaponImg = new Image(); weaponImg.onload = function() { if (!weaponTexture) return; gl.bindTexture(gl.TEXTURE_2D, weaponTexture); gl.texImage2D(gl.TEXTURE_2D, 0, gl.RGBA, gl.RGBA, gl.UNSIGNED_BYTE, weaponImg); gl.texParameteri(gl.TEXTURE_2D, gl.TEXTURE_MIN_FILTER, gl.LINEAR); gl.texParameteri(gl.TEXTURE_2D, gl.TEXTURE_MAG_FILTER, gl.LINEAR); gl.bindTexture(gl.TEXTURE_2D, null); console.log(`[WebGL] Weapon texture loaded: ${weaponImg.width}x${weaponImg.height}`); }; weaponImg.onerror = function() { console.warn(`[WebGL] Weapon texture not found (${weaponTexturePath}), using solid color fallback`); }; weaponImg.src = weaponTexturePath; // AR-47 weapon model (simple box-based representation) let weaponVertices = [ -0.05, -0.12, 0.10, 0.05, -0.12, 0.10, 0.05, 0.12, 0.10, -0.05, 0.12, 0.10, -0.05, -0.12, -0.05, -0.05, 0.12, -0.05, 0.05, 0.12, -0.05, 0.05, -0.12, -0.05 ]; weaponBuffer = gl.createBuffer(); gl.bindBuffer(gl.ARRAY_BUFFER, weaponBuffer); gl.bufferData(gl.ARRAY_BUFFER, new Float32Array(weaponVertices), gl.STATIC_DRAW); let weaponIndices = [ 0, 1, 2, 0, 2, 3, 4, 5, 6, 4, 6, 7, 0, 4, 7, 0, 7, 1, 2, 6, 5, 2, 5, 3, 0, 3, 5, 0, 5, 4, 1, 7, 6, 1, 6, 2 ]; weaponIndexBuffer = gl.createBuffer(); gl.bindBuffer(gl.ELEMENT_ARRAY_BUFFER, weaponIndexBuffer); gl.bufferData(gl.ELEMENT_ARRAY_BUFFER, new Uint16Array(weaponIndices), gl.STATIC_DRAW); let weaponUVs = [ 0.0, 0.0, 1.0, 0.0, 1.0, 1.0, 0.0, 1.0, 1.0, 0.0, 0.0, 0.0, 0.0, 1.0, 1.0, 1.0 ]; weaponUVBuffer = gl.createBuffer(); gl.bindBuffer(gl.ARRAY_BUFFER, weaponUVBuffer); gl.bufferData(gl.ARRAY_BUFFER, new Float32Array(weaponUVs), gl.STATIC_DRAW); // 📺 DANGERWEB HUD INJECTOR & UPDATER if (!document.getElementById("dw-hud-overlay")) { let hud = document.createElement("div"); hud.id = "dw-hud-overlay"; hud.style.cssText = "position:absolute; bottom:20px; left:20px; color:#fff; font-family:monospace; font-size:18px; z-index:9999; text-shadow:2px 2px #000; background:rgba(0,0,0,0.5); padding:10px 20px; border-radius:5px; border-left:4px solid #ff3333;"; hud.innerHTML = `
HP: 100
AMMO: 30 | MAGS: 4
$0
[1] PRIMARY [2] SECONDARY [3] KNIFE [4] GRENADE
NERVOUSNESS: 0%
`; document.body.appendChild(hud); } // === HIER DE GRADENMETER (KOMPAS) INJECTEREN === if (!document.getElementById("dw-compass-overlay")) { let compass = document.createElement("div"); compass.id = "dw-compass-overlay"; compass.style.cssText = "position:absolute; top:20px; left:50%; transform:translateX(-50%); width:300px; height:35px; background:rgba(17, 20, 26, 0.85); border:2px solid #3a4454; border-radius:4px; box-shadow:0 4px 15px rgba(0,0,0,0.5); z-index:9999; font-family:monospace; color:#fff; overflow:hidden; pointer-events:none;"; let strip = document.createElement("div"); strip.id = "compass-strip"; strip.style.cssText = "position:absolute; width:1440px; height:100%; top:0; left:0; display:flex; align-items:center; white-space:nowrap; font-size:13px; font-weight:bold; letter-spacing:1px; transition: left 0.02s linear;"; let compassHTML = ""; for (let deg = 0; deg < 360; deg += 15) { let label = deg; if (deg === 0) label = "N"; if (deg === 90) label = "E"; if (deg === 180) label = "S"; if (deg === 270) label = "W"; compassHTML += `
${label}
|
`; } strip.innerHTML = compassHTML + compassHTML + compassHTML + compassHTML; let centerNotch = document.createElement("div"); centerNotch.style.cssText = "position:absolute; top:0; left:50%; transform:translateX(-50%); width:0; height:0; border-left:5px solid transparent; border-right:5px solid transparent; border-top:6px solid #00ffaa; z-index:10000;"; compass.appendChild(centerNotch); document.body.appendChild(compass); } else { if (document.getElementById("dw-compass-overlay")) { document.getElementById("dw-compass-overlay").style.display = "block"; } } // 🗺️ DANGERWEB RONDE MINIMAP INJECTOR WITH BUILDING SILHOUETTES if (!document.getElementById("dw-minimap-container")) { let miniContainer = document.createElement("div"); miniContainer.id = "dw-minimap-container"; miniContainer.style.cssText = "position:absolute; top:20px; right:20px; width:140px; height:140px; border-radius:50%; border:3px solid #3a4454; overflow:hidden; box-shadow:0 0 15px rgba(0,0,0,0.5); background:#11141a; z-index:9999;"; let radarMap = document.createElement("div"); radarMap.id = "mini-radar-map"; radarMap.style.cssText = "position:absolute; width:100%; height:100%; top:0; left:0; transition: transform 0.02s linear;"; islandBuildings.forEach(b => { let bSil = document.createElement("div"); bSil.id = `mini-b-${b.id}`; bSil.style.cssText = `position:absolute; width:${b.w * 0.5}px; height:${b.h * 0.5}px; background:rgba(80, 85, 95, 0.85); border:1px solid #2d323d; border-radius:2px; transform-origin: center;`; radarMap.appendChild(bSil); }); let playerDot = document.createElement("div"); playerDot.id = "mini-player-dot"; playerDot.style.cssText = "position:absolute; top:50%; left:50%; width:8px; height:8px; background:#00ffaa; border-radius:50%; transform:translate(-50%,-50%); border:1px solid #000; box-shadow: 0 0 4px #00ffaa; z-index:10000;"; miniContainer.appendChild(radarMap); if (!document.getElementById("minimap-zone-circle")) { let miniZoneCircle = document.createElement("div"); miniZoneCircle.id = "minimap-zone-circle"; miniZoneCircle.style.cssText = "position:absolute; border:1.5px dashed #ff3333; border-radius:50%; background:rgba(255,51,51,0.02); pointer-events:none; z-index:9999; transform:translate(-50%, -50%); transform-origin: center;"; radarMap.appendChild(miniZoneCircle); } miniContainer.appendChild(playerDot); document.body.appendChild(miniContainer); } // // DANGERWEB BIG MAP OVERLAY SYSTEM (G-KNOP) if (!document.getElementById("dw-bigmap-container")) { let bigMap = document.createElement("div"); bigMap.id = "dw-bigmap-container"; bigMap.style.cssText = "position:absolute; top:50%; left:50%; width:500px; height:500px; background:rgba(17, 20, 26, 0.95); border:4px solid #3a4454; box-shadow:0 0 30px rgba(0,0,0,0.8); z-index:99999; transform:translate(-50%, -50%); display:none; overflow:hidden; font-family:monospace; color:#fff; border-radius:8px;"; let mapTitle = document.createElement("div"); mapTitle.id = "bigmap-title"; mapTitle.style.cssText = "position:absolute; top:15px; left:20px; font-size:16px; font-weight:bold; color:#00ffaa; text-shadow:2px 2px #000; z-index:100001;"; mapTitle.innerText = "ISLAND TACTICAL MAP [400m x 400m]"; bigMap.appendChild(mapTitle); let mapInner = document.createElement("div"); mapInner.id = "bigmap-inner"; mapInner.style.cssText = "position:absolute; width:100%; height:100%; top:0; left:0;"; bigMap.appendChild(mapInner); let columns = ['A', 'B', 'C', 'D', 'E']; for (let i = 0; i < 5; i++) { for (let j = 0; j < 5; j++) { let gridCell = document.createElement("div"); gridCell.style.cssText = `position:absolute; width:100px; height:100px; left:${i*100}px; top:${j*100}px; border:1px solid rgba(88, 109, 133, 0.15); box-sizing:border-box; pointer-events:none;`; let label = document.createElement("span"); label.style.cssText = "position:absolute; top:4px; left:6px; font-size:10px; color:rgba(255,255,255,0.25); font-weight:bold;"; label.innerText = columns[i] + (j + 1); gridCell.appendChild(label); mapInner.appendChild(gridCell); } } let bigPlayerDot = document.createElement("div"); bigPlayerDot.id = "bigmap-player-dot"; bigPlayerDot.style.cssText = "position:absolute; width:14px; height:14px; background:#ff3333; border:2px solid #fff; border-radius:50%; box-shadow:0 0 10px #ff3333; z-index:100000; transform:translate(-50%, -50%); transform-origin: center;"; let arrow = document.createElement("div"); arrow.style.cssText = "position:absolute; top:-10px; left:4px; width:0; height:0; border-left:3px solid transparent; border-right:3px solid transparent; border-bottom:7px solid #fff;"; bigPlayerDot.appendChild(arrow); bigMap.appendChild(bigPlayerDot); let zoneCircle = document.createElement("div"); zoneCircle.id = "bigmap-zone-circle"; zoneCircle.style.cssText = "position:absolute; border:2px dashed #ff3333; border-radius:50%; background:rgba(255,51,51,0.06); pointer-events:none; z-index:99998; transform:translate(-50%, -50%); transform-origin: center;"; bigMap.appendChild(zoneCircle); document.body.appendChild(bigMap); } // Start the render loop rafId = requestAnimationFrame(renderFrame); } // Model is loaded via loadGLBPreview when user selects character in loadout, // not on game start. The engine uses the already-loaded model from WASM FS. function renderFrame() { if (!gl) { rafId = requestAnimationFrame(renderFrame); return; } if (window.dangerGameActive || window.dangerMapOpen) { rafId = requestAnimationFrame(renderFrame); return; } if (window.dangerRenderStopped) { return; } try { canvasEl = canvasEl || document.getElementById("canvas"); const canvas = canvasEl; // 1. Maak het scherm leeg (Blauwe lucht via je clearColor) gl.clear(gl.COLOR_BUFFER_BIT | gl.DEPTH_BUFFER_BIT); // 2. GEGARANDEERDE UNIFORM STATE: Activeer ALTIJD eerst het shader-programma voor we uniforms sturen! if (typeof program !== 'undefined' && program !== null) { gl.useProgram(program); } else { requestAnimationFrame(renderFrame); return; } // DEBUG: Laadstatus if (typeof window.wasmDebugFrame === 'undefined') { window.wasmDebugFrame = 0; window.wasmMapRendered = false; } window.wasmDebugFrame++; if (!window.wasmMapRendered && mapVertices.length > 0) { window.wasmMapRendered = true; console.log(`[DangerRoyale Engine] WERELD GETEKEN! Vertices: ${mapVertices.length / 3}, Map fallback: ${mapVertices.length < 10000}`); } if (window.wasmDebugFrame % 300 === 0) { console.log(`[DREngine] Frame:${window.wasmDebugFrame} Vertices:${mapVertices.length / 3} pos:(${playerPos.x.toFixed(1)}, ${playerPos.y.toFixed(1)})`); } // 3. MULTIPLAYER SPAWN EN HOOGTERADAR COÖRDINATEN if (typeof window.hasSpawnedAtBunker === 'undefined') { window.hasSpawnedAtBunker = true; playerPos.x = 10.0; playerPos.y = 0.01; updateActiveWeapon(); console.log("[DangerRoyale Core] Player successfully spawned on map!"); } // If the canvas is invisible (player is in the main menu/dashboard), we stop rendering if (canvas.style.display === "none") return; // Check if a menu is open (such as settings or escape-pause menu) let menuActive = false; let menuEl = document.getElementById("dw-mainmenu-layer"); let setEl = document.getElementById("dw-settings-layer"); if ((menuEl && menuEl.style.display === "block") || (setEl && setEl.style.display === "block")) { menuActive = true; } // --- ZONE TIMERS EN SCHADE BEREKENEN --- // --- BATTLE ROYALE NETWERK- & TIMINGSYSTEUM --- zoneTimer++; // Update projectiles const now = Date.now(); projectiles = projectiles.filter(proj => { if (now - proj.time > PROJECTILE_LIFETIME) return false; // Check grenade explosion timer if (proj.type === 'grenade' && now >= proj.explodeTime) { createExplosion(proj.x, proj.y, proj.z); return false; } proj.x += proj.vx * 0.016; proj.y += proj.vy * 0.016; proj.z += proj.vz * 0.016; if (proj.gravity) proj.vy -= 9.8 * 0.016; // gravity return proj.y > 0; }); window.dangerProjectiles = projectiles; // Update explosions explosions = explosions.filter(expl => { if (now - expl.startTime > expl.duration) return false; expl.scale += 0.015; expl.alpha = Math.max(0, 1 - (now - expl.startTime) / expl.duration); return true; }); window.dangerExplosions = explosions; // Update ejected cartridge cases ejectedCases = ejectedCases.filter(case_ => { if (now - case_.time > case_.lifetime) return false; case_.y += case_.vy * 0.016; case_.x += case_.vx * 0.016; case_.z += case_.vz * 0.016; if (case_.gravity) case_.vy -= 9.8 * 0.016; return case_.y > 0; }); window.dangerEjectedCases = ejectedCases; // Update dropped magazines droppedMags = droppedMags.filter(mag => { if (now - mag.time > mag.lifetime) return false; mag.y += mag.vy * 0.016; mag.x += mag.vx * 0.016; mag.z += mag.vz * 0.016; mag.rotation += 0.05; if (mag.gravity) mag.vy -= 9.8 * 0.016; return mag.y > 0; }); window.dangerDroppedMags = droppedMags; // Converteer frames naar echte seconden (bij 60 frames per seconde) if (zoneTimer % 60 === 0) { zoneSeconds++; let timeLeft = nextPhaseTime - zoneSeconds; // Waarschuwingen sturen in de in-game chat overlay let cLogs = document.getElementById("game-chat-logs"); if (cLogs) { if (timeLeft === 60) { let row = document.createElement("div"); row.style.color = "#ffaa00"; row.innerText = "[DREngine] WARNING: The zone will start shrinking in 1 minute!"; cLogs.appendChild(row); } else if (timeLeft === 10) { let row = document.createElement("div"); row.style.color = "#ff3333"; row.style.fontWeight = "bold"; row.innerText = "[DREngine] CRITICAL: Circle shrinking in 10 seconds! Find cover!"; cLogs.appendChild(row); } } // If the timer reaches the target time, we start shrinking if (zoneSeconds >= nextPhaseTime) { if (!battleZone.isShrinking) { // Shrink the zone by 15% relative to the current size battleZone.targetRadius = battleZone.radius * 0.85; battleZone.isShrinking = true; let cLogs = document.getElementById("game-chat-logs"); if (cLogs) { let row = document.createElement("div"); row.style.color = "#ff3333"; row.innerText = `[DREngine] THE ZONE IS SHRINKING! Next phase in 2 minutes.`; cLogs.appendChild(row); } } // Update mode selector button visual state const modeBtns = document.querySelectorAll('.mode-select-btn'); if (modeBtns.length > 0) { modeBtns.forEach(btn => { btn.classList.toggle('active', btn.getAttribute('data-mode') === String(currentQueueMode)); }); } } } // Gradually make the zone smaller when the shrink-phase is active if (battleZone.isShrinking) { if (battleZone.radius > battleZone.targetRadius) { battleZone.radius -= (battleZone.shrinkSpeed / 60); // Gelijkmatig verdeeld over de frames } else { battleZone.radius = battleZone.targetRadius; battleZone.isShrinking = false; zoneSeconds = 0; nextPhaseTime = 120; // Elke volgende krimp-fase duurt 2 minuten (120 seconden) battleZone.phase++; let cLogs = document.getElementById("game-chat-logs"); if (cLogs) { let row = document.createElement("div"); row.style.color = "#00ffaa"; row.innerText = `[DREngine] The circle has stabilized. Phase ${battleZone.phase} active.`; cLogs.appendChild(row); } } } // --- LIVE UPDATES VAN DE GRIDS EN MARKERS (G-KNOP) --- let bigMapContainer = document.getElementById("dw-bigmap-container"); if (bigMapContainer && bigMapContainer.style.display === "block") { // 1. Update de Groene Speler-marker live op de G-kaart let pDot = document.getElementById("bigmap-player-dot"); if (pDot) { // We mirror and rotate the axes so the marker exactly matches your WASD movement! let pX = ((-playerPos.y + 200) / 400) * 500; let pY = ((-playerPos.x + 200) / 400) * 500; pDot.style.left = `${pX}px`; pDot.style.top = `${pY}px`; // The rotation now perfectly matches the new direction of travel (+90 degrees) pDot.style.transform = `translate(-50%, -50%) rotate(${(cameraLook.x * (180 / Math.PI)) + 90}deg)`; // Dynamically calculate which grid cell the player is currently in let gridCols = ['A', 'B', 'C', 'D', 'E']; let gridX = Math.floor(pX / 100); let gridY = Math.floor(pY / 100); gridX = Math.max(0, Math.min(4, gridX)); gridY = Math.max(0, Math.min(4, gridY)); let mapTitleEl = document.getElementById("bigmap-title"); if (mapTitleEl) { let timeLeft = nextPhaseTime - zoneSeconds; let timeString = battleZone.isShrinking ? "KRIMPEN BEZIG" : `${Math.floor(timeLeft / 60)}:${(timeLeft % 60).toString().padStart(2, '0')}`; mapTitleEl.innerText = `ISLAND TACTICAL MAP - SECTOR: ${gridCols[gridX]}${gridY + 1} | ZONE TIMER: ${timeString}`; } } // 2. Update de Rode Gestreepte Zone live op het grid let zCircle = document.getElementById("bigmap-zone-circle"); if (zCircle) { let cssRadius = (battleZone.radius / 400) * 500; let zX = ((battleZone.x + 200) / 400) * 500; let zY = ((-battleZone.y + 200) / 400) * 500; zCircle.style.width = `${cssRadius * 2}px`; zCircle.style.height = `${cssRadius * 2}px`; zCircle.style.left = `${zX}px`; zCircle.style.top = `${zY}px`; } } // --- LIVE HUD & MINIMAP UPDATES --- let hpEl = document.getElementById("hud-hp"); if (hpEl) hpEl.innerText = `HP: ${currentHP}`; let ammoEl = document.getElementById("hud-ammo"); if (ammoEl) ammoEl.innerText = isReloading ? "RELOADING..." : `AMMO: ${currentAmmo}/${maxAmmoPerMag} | RESERVES: ${reserveMagazines}`; let nervousEl = document.getElementById("hud-nervousness"); if (nervousEl) { const pct = Math.round(nervousnessLevel * 100); nervousEl.innerText = `NERVOUSNESS: ${pct}%`; nervousEl.style.color = pct > 75 ? '#ff3333' : pct > 40 ? '#ffcc00' : '#55ff55'; } let compassOverlay = document.getElementById("dw-compass-overlay"); if (compassOverlay) compassOverlay.style.display = "block"; islandBuildings.forEach(b => { let bElement = document.getElementById(`mini-b-${b.id}`); if (bElement) { let relX = (b.x - playerPos.x) * 0.35 + 70 - (b.w * 0.175); let relY = (b.y - playerPos.y) * 0.35 + 70 - (b.h * 0.175); bElement.style.left = `${relX}px`; bElement.style.top = `${relY}px`; } }); let radar = document.getElementById("mini-radar-map"); if (radar) { radar.style.transform = `rotate(${cameraLook.x * (180 / Math.PI)}deg)`; radar.style.transformOrigin = "70px 70px"; } let mCircle = document.getElementById("minimap-zone-circle"); if (mCircle) { let miniCssRadius = battleZone.radius * 0.35; let miniX = (battleZone.x - playerPos.x) * 0.35 + 70; let miniY = (battleZone.y - playerPos.y) * 0.35 + 70; mCircle.style.width = `${miniCssRadius * 2}px`; mCircle.style.height = `${miniCssRadius * 2}px`; mCircle.style.left = `${miniX}px`; mCircle.style.top = `${miniY}px`; } let compassStrip = document.getElementById("compass-strip"); if (compassStrip) { let degrees = (cameraLook.x * (180 / Math.PI)) % 360; if (degrees < 0) degrees += 360; let offset = -(degrees * 4) + 150; compassStrip.style.left = `${offset}px`; } // --- DAMAGE REGISTRATIE --- let zoneDx = playerPos.x - battleZone.x; let zoneDy = playerPos.y - battleZone.y; let distanceToCenter = Math.sqrt(zoneDx * zoneDx + zoneDy * zoneDy); if (distanceToCenter > battleZone.radius) { if (zoneTimer % 60 === 0 && currentHP > 0) { currentHP -= 5; // Verlies HP buiten de zone let hpElement = document.getElementById("hud-hp"); if (hpElement) hpElement.innerText = `HP: ${currentHP}`; if (currentHP <= 0) { currentHP = 0; if (hpElement) hpElement.innerText = `HP: 0`; if (document.pointerLockElement === canvas) document.exitPointerLock(); alert("GAME OVER! Je bent gestorven in de DangerRoyale."); exitActiveGame(); return; } } } // If you are in a menu, we stop the REST of the render loop here (like player movement), // maar we zorgen dat de engine wel blijft loopen zodat de zone-berekening hierboven blijft draaien! if (menuActive) { rafId = requestAnimationFrame(renderFrame); return; } if (canvas.width !== window.innerWidth || canvas.height !== window.innerHeight) { canvas.width = window.innerWidth; canvas.height = window.innerHeight; gl.viewport(0, 0, canvas.width, canvas.height); } const aspect = canvas.width / canvas.height; let speed = 0.25; // Slot 0 (mes) rennen: +10% mobiliteitsboost if (activeWeaponSlot === 0) { speed = speed * 1.10; } // wapengewicht-snelheidsmodificaties: schieter -15%, assault-rifles -5% if (activeWeaponSlot === 1 && currentWeapon) { const weaponKey = window.WEAPON_KEYS ? window.WEAPON_KEYS[activeWeaponSlot] : selectedLoadout?.primary; if (weaponKey === 'awp' || weaponKey === 'sniper_rifle') { speed = speed * 0.85; } else if (weaponKey === 'ar47_core' || weaponKey === 'm4a4_sandstorm') { speed = speed * 0.95; } } if (activeKeysPressed.SPRINT) { speed = speed * 1.50; } else if (activeKeysPressed.CROUCH) { speed = speed * 0.75; } // 🚫 INPUT BLOCKER: Blokkeer lopen met WASD als de chat openstaat óf als je in het ESC menu zit! const isChattingActive = document.getElementById("game-chat-input") && document.getElementById("game-chat-input").style.display === "block"; const isGamePausedESC = document.pointerLockElement === null && document.mozPointerLockElement === null; if (!isChattingActive && !isGamePausedESC) { if (activeKeysPressed.FORWARD) { playerPos.x -= Math.sin(cameraLook.x) * speed; playerPos.y -= Math.cos(cameraLook.x) * speed; } if (activeKeysPressed.BACKWARD) { playerPos.x += Math.sin(cameraLook.x) * speed; playerPos.y += Math.cos(cameraLook.x) * speed; } if (activeKeysPressed.LEFT) { playerPos.x -= Math.cos(cameraLook.x) * speed; playerPos.y += Math.sin(cameraLook.x) * speed; } if (activeKeysPressed.RIGHT) { playerPos.x += Math.cos(cameraLook.x) * speed; playerPos.y -= Math.sin(cameraLook.x) * speed; } } // NETWERKTIK: Stuur live positie naar de server (elke 3 frames) if (socket && zoneTimer % 3 === 0) { socket.emit('update_position', { x: playerPos.x, y: playerPos.y, yaw: cameraLook.x }); } gl.clearColor(0.53, 0.81, 0.92, 1.0); gl.clear(gl.COLOR_BUFFER_BIT | gl.DEPTH_BUFFER_BIT); gl.enable(gl.DEPTH_TEST); gl.disable(gl.CULL_FACE); let f = 1.0 / Math.tan(45 * Math.PI / 180); projMatrix[0] = f / aspect; projMatrix[1] = 0; projMatrix[2] = 0; projMatrix[3] = 0; projMatrix[4] = 0; projMatrix[5] = f; projMatrix[6] = 0; projMatrix[7] = 0; projMatrix[8] = 0; projMatrix[9] = 0; projMatrix[10] = 2000.1 / -1999.9; projMatrix[11] = -1; projMatrix[12] = 0; projMatrix[13] = 0; projMatrix[14] = 400.0 / -1999.9; projMatrix[15] = 0; let cosYaw = Math.cos(cameraLook.x); let sinYaw = Math.sin(cameraLook.x); let cosPitch = Math.cos(cameraLook.y); let sinPitch = Math.sin(cameraLook.y); // Geforceerd op 0 zetten: De grond is ALTIJD gras! if (uIsBuildingLoc) { gl.uniform1i(uIsBuildingLoc, 0); } // COLLISION & HOOGTE-COLLISION (Koppelt je camera live aan playerPos!) let groundHeight = 0.0; if (mapVertices.length > 0) { groundHeight = Math.sin(playerPos.x * 0.015) * Math.cos(playerPos.y * 0.015) * 12.0; } if (isNoclipActive && window.dangerUser?.isAdmin) { isGrounded = false; let noclipSpeed = 0.15; if (activeKeysPressed.SPRINT) noclipHeight -= noclipSpeed; if (activeKeysPressed.JUMP) noclipHeight += noclipSpeed; window.jumpOffset = 0.0; verticalVelocity = 0.0; } else { // Springen activeren op de Y-as if (activeKeysPressed.JUMP && isGrounded) { verticalVelocity = 0.85; isGrounded = false; } if (!isGrounded) { verticalVelocity -= 0.015; if (typeof jumpOffset === 'undefined') window.jumpOffset = 0.0; window.jumpOffset += verticalVelocity; if (window.jumpOffset <= 0.0) { window.jumpOffset = 0.0; verticalVelocity = 0.0; isGrounded = true; } } else { window.jumpOffset = 0.0; } } let eyeHeight = activeKeysPressed.CROUCH ? 0.5 : 1.2; let pY = isNoclipActive && window.dangerUser?.isAdmin ? (groundHeight + 4.0) + noclipHeight + eyeHeight : groundHeight + eyeHeight + (window.jumpOffset || 0.0) + 4.0; viewMatrix[0] = cosYaw; viewMatrix[1] = sinYaw * sinPitch; viewMatrix[2] = sinYaw * cosPitch; viewMatrix[3] = 0; viewMatrix[4] = 0; viewMatrix[5] = cosPitch; viewMatrix[6] = -sinPitch; viewMatrix[7] = 0; viewMatrix[8] = -sinYaw; viewMatrix[9] = cosYaw * sinPitch; viewMatrix[10] = cosYaw * cosPitch; viewMatrix[11] = 0; viewMatrix[12] = -(playerPos.x * cosYaw - playerPos.y * sinYaw); viewMatrix[13] = -(playerPos.x * sinYaw * sinPitch + pY * cosPitch + playerPos.y * cosYaw * sinPitch); viewMatrix[14] = -(playerPos.x * sinYaw * cosPitch - pY * sinPitch + playerPos.y * cosYaw * cosPitch); viewMatrix[15] = 1; gl.uniformMatrix4fv(uProjection, false, projMatrix); gl.uniformMatrix4fv(uView, false, viewMatrix); // Identiteits-modelmatrix voor terrein (model-logica verwerkt door WASM-core) if (uModel !== null) { gl.uniformMatrix4fv(uModel, false, identityMatrix); } if (mapVertices.length > 0 && typeof program !== 'undefined') { // Gebruik gecachte uniform/attribuutlocaties (ingesteld in initWebGLViewport) gl.uniform1i(uIsBuildingLoc, 0); // STATISCH BUFFER-SYSTEEM (VOORKOMT LAG & GEBRUIKT GPU-CACHING) // Detect map changes via object identity — if mapVertices was reassigned // (e.g. after loading a new map), the old GPU buffer is stale. if (!window.staticWorldMapBuffer || window.staticWorldMapVertexRef !== mapVertices || window.staticWorldMapSize !== mapVertices.length) { if (!window.staticWorldMapBuffer) { window.staticWorldMapBuffer = gl.createBuffer(); } window.staticWorldMapVertexRef = mapVertices; gl.bindBuffer(gl.ARRAY_BUFFER, window.staticWorldMapBuffer); // If mapVertices is a view into HEAPF32, copy to a detached plain buffer // so the upload is not affected by subsequent WASM heap growth. if (mapVertices instanceof Float32Array && mapVertices.buffer === Module.HEAPF32.buffer) { const copy = new Float32Array(mapVertices); gl.bufferData(gl.ARRAY_BUFFER, copy, gl.STATIC_DRAW); } else { gl.bufferData(gl.ARRAY_BUFFER, mapVertices, gl.STATIC_DRAW); } window.staticWorldMapSize = mapVertices.length; console.log(`[DangerRoyale Engine] Map buffer updated: ${mapVertices.length / 3} vertices`); } else { gl.bindBuffer(gl.ARRAY_BUFFER, window.staticWorldMapBuffer); } gl.enableVertexAttribArray(aPositionLoc); gl.vertexAttribPointer(aPositionLoc, 3, gl.FLOAT, false, 0, 0); if (typeof texCoordBuffer !== 'undefined' && texCoordBuffer !== null) { gl.bindBuffer(gl.ARRAY_BUFFER, texCoordBuffer); gl.enableVertexAttribArray(aTexCoordLoc); gl.vertexAttribPointer(aTexCoordLoc, 2, gl.FLOAT, false, 0, 0); } if (uObjectOffset) { gl.uniform3f(uObjectOffset, 0.0, 0.0, 0.0); } if (typeof mapTexture !== 'undefined' && mapTexture !== null) { gl.activeTexture(gl.TEXTURE0); gl.bindTexture(gl.TEXTURE_2D, mapTexture); if (sSamplerLoc) { gl.uniform1i(sSamplerLoc, 0); } } if (window.staticWorldMapBuffer) { gl.drawArrays(gl.TRIANGLES, 0, mapVertices.length / 3); } gl.bindTexture(gl.TEXTURE_2D, null); } if (uIsBuildingLoc) { gl.uniform1i(uIsBuildingLoc, 1); // Zet shader op BETON voor objecten } // [Mocht je handmatig de charBoxVertices of gebouwen hier renderen, // dan pakken ze nu automatisch de prachtige grijze betonlook!] // After drawing the objects, we set it back to 0 for safety if (uIsBuildingLoc) { gl.uniform1i(uIsBuildingLoc, 0); } // 💬 IN-GAME TRANSPARANTE CHAT OVERLAY (Met Directe Steam Nickname Match) if (!document.getElementById("dw-game-chat")) { let gameChat = document.createElement("div"); gameChat.id = "dw-game-chat"; gameChat.style.cssText = "position:absolute; bottom:120px; left:20px; width:350px; font-family:monospace; z-index:99999; pointer-events:none; text-align:left;"; let logs = document.createElement("div"); logs.id = "game-chat-logs"; logs.style.cssText = "height:120px; overflow:hidden; color:#00ffaa; font-size:13px; text-shadow:1px 1px 2px #000; margin-bottom:5px; padding-left:5px; display:flex; flex-direction:column; justify-content:end;"; logs.innerHTML = "
[Server] Welcome to DangerRoyale BR. Press Enter or T to chat.
"; let input = document.createElement("input"); input.id = "game-chat-input"; input.type = "text"; input.placeholder = "Typ een bericht of admin commando..."; input.style.cssText = "width:100%; background:rgba(0,0,0,0.6); border:none; border-bottom:2px solid #3a4454; color:#fff; padding:6px; font-family:monospace; font-size:13px; box-sizing:border-box; outline:none; display:none; pointer-events:auto;"; gameChat.appendChild(logs); gameChat.appendChild(input); document.body.appendChild(gameChat); document.addEventListener("keydown", function(e) { const cInput = document.getElementById("game-chat-input"); const cLogs = document.getElementById("game-chat-logs"); if (e.key === "Enter" || (e.key.toUpperCase() === "T" && document.activeElement !== cInput)) { if (cInput) { if (cInput.style.display === "none") { cInput.style.display = "block"; cInput.focus(); e.preventDefault(); } else if (e.key === "Enter") { let msg = cInput.value.trim(); cInput.value = ""; cInput.style.display = "none"; cInput.blur(); if (msg !== "" && cLogs) { // Direct doorsturen naar de Core Steam Engine onderaan! processInGameChatMessage(msg, cLogs); } } } } }); } // HTML CROSSHAIR INJECTOR (Pointerlock proof door fixed-center style!) if (!document.getElementById("dw-crosshair")) { let crosshair = document.createElement("div"); crosshair.id = "dw-crosshair"; crosshair.style.cssText = "position:fixed; top:50%; left:50%; width:8px; height:8px; background:#00ffaa; border-radius:50%; transform:translate(-50%,-50%); pointer-events:none; z-index:9999; border:2px solid #000000; box-shadow: 0 0 4px #000;"; document.body.appendChild(crosshair); } // FUNCTIONELE SHOOTING ENGINE (Inclusief Munitie & Reload-blocking) if (!window.isShootingScriptLoaded) { window.isShootingScriptLoaded = true; document.addEventListener("mousedown", function(e) { var canvas = document.getElementById("canvas"); if (canvas.style.display === "none" || document.pointerLockElement !== canvas) return; if (e.button === 0) { if (isReloading) return; // Wapen-ontplooi-cooldown: blokkeer vuren binnen 350ms na slotschakeling if (Date.now() - lastSlotSwitchTime < WEAPON_DEPLOY_COOLDOWN) return; // Mes - onmiddellijk treffen, geen munitie nodig if (activeWeaponSlot === 3) { if (socket) { socket.emit('player_attack', { type: 'melee', x: playerPos.x, z: playerPos.y, yaw: cameraLook.x, damage: currentWeapon?.damage || 40 }); } return; } // Granaat - gooi projectiel met kooktimer if (activeWeaponSlot === 4) { // Granaatmijn koken: start timer wanneer vuurknop vastgehouden if (!isGrenadeCooking) { isGrenadeCooking = true; grenadeCookStartTime = Date.now(); } const cookTime = Date.now() - grenadeCookStartTime; if (currentWeapon && (currentWeapon.magazine || 0) <= 0) return; if (currentWeapon) currentWeapon.magazine -= 1; // Koken beïnvloedt gooi-snelheid - langer koken = kortere gooi const cookFactor = Math.min(1, cookTime / 3500); const throwVelocity = 8.0 * (1 - cookFactor * 0.4); const throwHeight = 1.2 + cookFactor * 0.5; projectiles.push({ x: playerPos.x, y: throwHeight, z: playerPos.y, vx: -Math.sin(cameraLook.x) * Math.cos(Math.max(-0.5, Math.min(0.5, cameraLook.y))) * throwVelocity, vy: Math.max(0.5, Math.cos(cameraLook.y)) * (5.0 - cookFactor * 2.0), vz: -Math.cos(cameraLook.x) * Math.cos(Math.max(-0.5, Math.min(0.5, cameraLook.y))) * throwVelocity, gravity: true, time: Date.now(), explodeTime: Date.now() + (3000 - cookTime), // Reduced fuse based on cook time type: 'grenade' }); isGrenadeCooking = false; // Teken voorspellende trajectboog (parabool) drawGrenadeTrajectory(playerPos, cameraLook, throwVelocity); updateActiveWeapon(); return; } // Primary/Secondary weapon if (currentAmmo <= 0) return; currentAmmo--; let ammoElement = document.getElementById("hud-ammo"); if (ammoElement) { ammoElement.innerText = `AMMO: ${currentAmmo}/${maxAmmoPerMag} | RESERVES: ${reserveMagazines}`; } // Creëer projectiel if (socket) { socket.emit('player_shoot', { x: playerPos.x, y: 0, z: playerPos.y, yaw: cameraLook.x, pitch: cameraLook.y }); } // Native C++ WASM: roep processNativeFireWeapon aan met sub-tick-tijdstempel if (typeof Module !== 'undefined' && Module._processNativeFireWeapon) { try { const dirX = -Math.sin(cameraLook.x) * Math.cos(cameraLook.y); const dirY = -Math.sin(cameraLook.y); const dirZ = -Math.cos(cameraLook.x) * Math.cos(cameraLook.y); Module._processNativeFireWeapon( 0, // shooterId (local player = 0 in WASM) playerPos.x, 1.2, playerPos.y, dirX, dirY, dirZ ); } catch (e) { console.warn('[DREngine] Native fire weapon call failed:', e.message); } } projectiles.push({ x: playerPos.x, y: 1.2, z: playerPos.y, vx: -Math.sin(cameraLook.x) * Math.cos(cameraLook.y) * PROJECTILE_SPEED, vy: -Math.sin(cameraLook.y) * PROJECTILE_SPEED, vz: -Math.cos(cameraLook.x) * Math.cos(cameraLook.y) * PROJECTILE_SPEED, time: Date.now(), originX: playerPos.x, originY: 1.2, originZ: playerPos.y }); // Eject kamerhuls if (currentWeapon?.cartridge) { const ejectDir = -Math.cos(cameraLook.x) * 0.3 + Math.sin(cameraLook.x) * 0.1; ejectedCases.push({ x: playerPos.x + Math.sin(cameraLook.x) * 0.2, y: 1.0, z: playerPos.y - Math.cos(cameraLook.x) * 0.2, vx: Math.sin(cameraLook.x) * 3 + Math.cos(cameraLook.x) * 2, vy: 2.5, vz: -Math.cos(cameraLook.x) * 3 + Math.sin(cameraLook.x) * 2, vz2: -Math.cos(cameraLook.x) * 3 + Math.sin(cameraLook.x) * 2, gravity: true, time: Date.now(), lifetime: 3000, cartridge: currentWeapon.cartridge }); } var crosshair = document.getElementById("dw-crosshair"); if (crosshair) { crosshair.style.background = "#ffffff"; crosshair.style.boxShadow = "0 0 25px 15px #ffaa00, 0 0 10px 5px #ff5500"; crosshair.style.width = "14px"; crosshair.style.height = "14px"; setTimeout(() => { if (crosshair) { crosshair.style.background = "#00ffaa"; crosshair.style.boxShadow = "0 0 4px #000"; crosshair.style.width = "8px"; crosshair.style.height = "8px"; } }, 50); } } }); window.addEventListener("keydown", function(e) { const now = Date.now(); // Volg snelle toetsaanslagen if (now - lastKeyPressTime < 150) { rapidKeyPressCount++; } else { rapidKeyPressCount = 1; } lastKeyPressTime = now; rapidKeyPressCount = Math.min(rapidKeyPressCount, 10); const isMoving = activeKeysPressed.FORWARD || activeKeysPressed.BACKWARD || activeKeysPressed.LEFT || activeKeysPressed.RIGHT || activeKeysPressed.SPRINT || activeKeysPressed.JUMP; if (e.key.toUpperCase() === "R" && !isReloading && (activeWeaponSlot === 1 || activeWeaponSlot === 2) && currentAmmo < maxAmmoPerMag) { if (reserveMagazines <= 0) return; isReloading = true; let ammoElement = document.getElementById("hud-ammo"); if (ammoElement) ammoElement.innerText = "RELOADING..."; // Laat oude mageting vallen if (currentWeapon?.magModel) { droppedMags.push({ x: playerPos.x, y: 0.5, z: playerPos.y, vx: (Math.random() - 0.5) * 2, vy: 1.5, vz: (Math.random() - 0.5) * 2, gravity: true, time: Date.now(), lifetime: 5000, rotation: 0 }); } // Herlaadtijd: basis 2.0s + bewegingsstraf + spanningstraf let reloadTime = 2000; if (isMoving) { reloadTime += 600 + Math.random() * 400; } if (window.jumpOffset && window.jumpOffset > 0) { reloadTime += 300; } // Spanning van muisjitter + snelle toetsaanslagen reloadTime += nervousnessLevel * 1200; reloadTime += rapidKeyPressCount * 50; // Spanning veroorzaakt ook paniekshakes if (nervousnessLevel > 0.6) { cameraLook.x += (Math.random() - 0.5) * 0.1; cameraLook.y += (Math.random() - 0.5) * 0.05; } setTimeout(() => { const mistakeChance = 0.10 + nervousnessLevel * 0.30 + (isMoving ? 0.15 : 0); if (Math.random() < mistakeChance) { // Herlaad fout: mageting vallen, moet opgehaald worden isReloading = false; if (ammoElement) ammoElement.innerText = "MAG DROP! PANIC!"; setTimeout(() => { if (ammoElement) ammoElement.innerText = `AMMO: ${currentAmmo}/${maxAmmoPerMag} | RESERVES: ${reserveMagazines}`; }, 400); return; } isReloading = false; reserveMagazines--; currentAmmo = maxAmmoPerMag; if (ammoElement) { ammoElement.innerText = `AMMO: ${currentAmmo}/${maxAmmoPerMag} | RESERVES: ${reserveMagazines}`; } }, reloadTime); } }); } // Dummy-spelermodelbuffer - model-logica verwerkt door WASM-core, JS duwt alleen vertex-buffers gl.bindBuffer(gl.ARRAY_BUFFER, charBuffer); gl.bindBuffer(gl.ELEMENT_ARRAY_BUFFER, indexBuffer); if (typeof program !== 'undefined') { gl.enableVertexAttribArray(aPositionLoc); gl.vertexAttribPointer(aPositionLoc, 3, gl.FLOAT, false, 0, 0); } gl.useProgram(program); if (uIsBuildingLoc) gl.uniform1i(uIsBuildingLoc, 0); gl.uniform3f(uObjectOffset, playerPos.x, 0.0, playerPos.y); gl.uniform1i(uUseColor, true); if (isMdlLoaded) { gl.uniform4f(uColor, 0.35, 0.85, 0.35, 1.0); } else { gl.uniform4f(uColor, 1.0, 0.66, 0.0, 1.0); } gl.drawElements(gl.TRIANGLES, 36, gl.UNSIGNED_SHORT, 0); gl.uniform1i(uUseColor, false); // Projectielen gerenderd via WebGL-renderer in app.js // Render explosies (schaal, vervagende partikel-effecten) if (explosions.length > 0) { gl.uniform1i(uIsBuildingLoc, 0); gl.uniform1i(uUseColor, true); explosions.forEach(expl => { const intensity = Math.max(0, Math.min(1, expl.alpha)); const r = 1.0; const g = Math.max(0.3, 1.0 - intensity * 0.5); const b = Math.max(0.1, 1.0 - intensity * 0.8); gl.uniform4f(uColor, r, g, b, intensity); gl.uniform3f(uObjectOffset, expl.x, expl.y, expl.z); gl.drawElements(gl.TRIANGLES, 36, gl.UNSIGNED_SHORT, 0); }); gl.uniform1i(uUseColor, false); } // G ejecte kamerhuls en gevallene magetingen gerenderd via WebGL-renderer in app.js // Update en render geldstapels if (typeof window.dangerGameActive !== 'undefined' && window.dangerGameActive) { cashStacks.forEach((stack, index) => { if (stack.collected) return; const dx = playerPos.x - stack.x; const dz = playerPos.y - stack.z; const dist = Math.sqrt(dx*dx + dz*dz); if (dist < 2.5) { stack.collected = true; cashStacks.splice(index, 1); playerMoney += stack.value; const cashEl = document.getElementById("hud-cash"); if (cashEl) cashEl.innerText = `$${playerMoney}`;; } }); } rafId = requestAnimationFrame(renderFrame); } catch (e) { console.error("[DREngine] RenderFrame error:", e); rafId = requestAnimationFrame(renderFrame); } } function loginSteam() { window.location.href = '/auth/steam'; } function loginGoogle() { window.location.href = '/auth/google'; } function logoutSteam() { console.log("[DangerRoyale Core]: Logging out..."); fetch('/auth/dev-logout', { method: 'POST' }) .catch(() => {}) .then(() => { document.cookie = "dev_admin=; path=/; max-age=0"; window.location.href = '/'; }); } function logoutUser() { var logoutBtn = document.getElementById('logout-btn'); if (logoutBtn) logoutBtn.style.display = 'none'; var profileView = document.getElementById('profile-view'); if (profileView) profileView.style.display = 'none'; var loginForm = document.getElementById('login-form'); if (loginForm) loginForm.style.display = 'block'; window.dangerUser = null; localStorage.removeItem('danger_user_session'); } function isUserLoggedIn() { if (window.dangerUser && window.dangerUser.steamId) return true; var session = localStorage.getItem('danger_user_session'); return !!session; } function showLoginPrompt() { var loginBox = document.getElementById('login-form'); if (loginBox) { loginBox.style.display = 'block'; var prompt = document.createElement('div'); prompt.className = 'tactical-status error'; prompt.textContent = 'Please sign in to access the marketplace.'; prompt.style.marginTop = '8px'; loginBox.appendChild(prompt); setTimeout(function() { if (prompt.parentNode) prompt.parentNode.removeChild(prompt); }, 4000); } var navItems = document.querySelectorAll('.tactical-nav .nav-item'); navItems.forEach(function(i) { i.classList.remove('active'); }); var mmItem = document.querySelector('.nav-item[data-view="matchmaking"]'); if (mmItem) { mmItem.classList.add('active'); renderMatchmakingView(); } } // 🚫 IN-GAME ESCAPE MENU: Keuze tussen hervatten of terugkeren naar het hoofdmenu document.addEventListener("pointerlockchange", handleInGameEscapeMenu, false); document.addEventListener("mozpointerlockchange", handleInGameEscapeMenu, false); function handleInGameEscapeMenu() { var gameCanvas = document.getElementById("canvas"); if (window.dangerMapOpen || window.dangerSettingsOpen) return; if (gameCanvas && gameCanvas.style.display === "block") { // SITUATIE 1: De speler drukt op ESC (Muis wordt bevrijd) -> Open het keuzemenu! if ((document.pointerLockElement === null || document.mozPointerLockElement === null) && window.dangerGameActive) { if (!document.getElementById("dw-escape-panel")) { let panel = document.createElement("div"); panel.id = "dw-escape-panel"; panel.style.cssText = "position:absolute; top:45%; left:50%; transform:translate(-50%, -50%); display:flex; flex-direction:column; gap:15px; z-index:999999; text-align:center;"; // KNOP 1: Terugkeren naar het Hoofdmenu let returnBtn = document.createElement("button"); returnBtn.innerText = "← RETURN TO MAIN MENU"; returnBtn.style.cssText = "padding:15px 35px; background:#ff3333; color:white; border:2px solid #fff; border-radius:5px; font-family:monospace; font-size:16px; font-weight:bold; cursor:pointer; box-shadow:0 0 20px rgba(0,0,0,0.7); letter-spacing:1px;"; returnBtn.addEventListener("click", function(e) { let confirmLeave = window.confirm("Weet je zeker dat je de huidige match wilt verlaten?"); if (confirmLeave) { gameCanvas.style.display = "none"; const overlays = ["dw-hud-overlay", "dw-minimap-container", "dw-crosshair", "dw-game-chat", "dw-escape-panel"]; overlays.forEach(id => { let el = document.getElementById(id); if (el) el.remove(); }); isMapLoaded = false; setTimeout(() => { window.location.reload(); }, 50); } }); // KNOP 2: Terug de game in (Hervatten!) let resumeBtn = document.createElement("button"); resumeBtn.innerText = "▶ RESUME MATCH"; resumeBtn.style.cssText = "padding:15px 35px; background:#4b7d3a; color:white; border:2px solid #fff; border-radius:5px; font-family:monospace; font-size:16px; font-weight:bold; cursor:pointer; box-shadow:0 0 20px rgba(0,0,0,0.7); letter-spacing:1px;"; resumeBtn.addEventListener("click", function() { // Lock de muis direct terug in het scherm om verder te spelen! gameCanvas.requestPointerLock(); }); panel.appendChild(resumeBtn); panel.appendChild(returnBtn); document.body.appendChild(panel); } } // SITUATIE 2: De speler klikt op Resume (Muis wordt hervergrendeld) -> Ruim het menu netjes op! else { let existingPanel = document.getElementById("dw-escape-panel"); if (existingPanel) existingPanel.remove(); } } } // 💬 CORE STEAM CHAT ENGINE (Koppelt nickname en genereert een SteamID voor de backend logs) function processInGameChatMessage(msg, cLogs) { if (msg === "") return; // 1. Haal de werkelijke actieve Steam Nickname dynamic uit je profiel (bijv. 'bread') let pName = "Speler"; let nameEl = document.querySelector(".player-profile h3") || document.getElementById("username") || document.querySelector('h3'); if (nameEl) { pName = nameEl.innerText.replace("LOG OUT", "").replace("ADMIN PANEL", "").trim(); } // 2. Genereer of haal een werkelijk uniek SteamID64 op voor de backend logs if (!window.playerSteamID64) { window.playerSteamID64 = "7656119" + Math.floor(1000000000 + Math.random() * 9000000000); // Uniek gegenereerd ID } if (msg.startsWith("/")) { const isAdmin = document.getElementById("admin-panel-btn") || document.querySelector('[id*="ADMIN"]') || document.querySelector('[class*="admin"]'); if (msg.startsWith("/teleport ")) { if (!isAdmin) { let noAuth = document.createElement("div"); noAuth.style.color = "#ff5555"; noAuth.innerText = `[Server] Error: SteamID (${window.playerSteamID64}) has no admin rights.`; cLogs.appendChild(noAuth); } else { let parts = msg.split(" "); if (parts.length === 3) { let targetX = parseFloat(parts[1]); let targetY = parseFloat(parts[2]); if (!isNaN(targetX) && !isNaN(targetY)) { playerPos.x = targetX; playerPos.y = targetY; let success = document.createElement("div"); success.style.color = "#ffcc00"; success.innerText = `[Admin Console] Teleport executed for ${pName} (${window.playerSteamID64}) ➔ X: ${targetX}, Y: ${targetY}`; cLogs.appendChild(success); // Console-log simulatie naar je Node.js-backend Express-server console.log(`[BACKEND LOG] Admin ${pName} [${window.playerSteamID64}] teleported to: ${targetX}, ${targetY}`); } } else { let usage = document.createElement("div"); usage.style.color = "#ff5555"; usage.innerText = "[Server] Usage: /teleport X Y"; cLogs.appendChild(usage); } } } else if (msg.startsWith("/kick ") || msg.startsWith("/ban ")) { const cmd = msg.startsWith("/kick ") ? "kick" : "ban"; const parts = msg.split(" "); if (parts.length < 2) { let usage = document.createElement("div"); usage.style.color = "#ff5555"; usage.innerText = `[Server] Usage: /${cmd} `; cLogs.appendChild(usage); } else { const targetName = parts.slice(1).join(" "); if (socket) { socket.emit('server_admin_command', { command: cmd, targetName: targetName }); let feedback = document.createElement("div"); feedback.style.color = "#ffcc00"; feedback.innerText = `[Admin] ${cmd === 'kick' ? 'Kick' : 'Ban'} request sent for: ${targetName}`; cLogs.appendChild(feedback); } } } else { let unknownCmd = document.createElement("div"); unknownCmd.style.color = "#ff5555"; unknownCmd.innerText = "[Server] Unknown admin command."; cLogs.appendChild(unknownCmd); } } else { // NORMALE CHAT: Toon de echte naam in de HUD en stuur de data inclusief SteamID mee! let row = document.createElement("div"); row.style.color = "#ffffff"; row.style.textShadow = "1px 1px 2px #000"; row.innerText = `${pName}: ${msg}`; cLogs.appendChild(row); // Simulatie van verzending naar de dedicated server backend console.log(`[SERVER BACKEND INCOMING] ChatMessage van SteamID64: ${window.playerSteamID64} | Nickname: ${pName} | Bericht: ${msg}`); } } // Live update van het volume-percentage in de UI function updateVolumeLabel(val) { const label = document.getElementById("volume-value-label"); if (label) label.innerText = val + "%"; if (typeof window.AudioEngine !== 'undefined' && window.AudioEngine.setMusicVolume) { window.AudioEngine.setMusicVolume(val); } } // === MARKETPLACE & ADMIN SKIN UPLOAD HANDLERS === // === GLOBAL MARKETPLACE DATA === let skinCatalog = []; let playerMarketInventory = []; let marketplaceCoinBalance = 0; let selectedSkinDetail = null; let activeDropEvent = null; let dropCountdownTimer = null; let dropSequence = []; let dropSequenceIndex = 0; let currentBuyOrderSkinId = null; let chartData = {}; let orderBookData = { bids: [], asks: [] }; let activeFilters = { search: '', weaponType: 'all', rarity: 'all', wear: 'all' }; let selectedRechargeTier = null; const WEAR_TIERS = [ { key: 'factory_new', label: 'Factory New', color: '#00ff88', class: 'mp-wear-factory-new' }, { key: 'minimal_wear', label: 'Minimal Wear', color: '#ffaa00', class: 'mp-wear-minimal-wear' }, { key: 'field_tested', label: 'Field-Tested', color: '#4a90ff', class: 'mp-wear-field-tested' }, { key: 'well_worn', label: 'Well-Worn', color: '#ff6600', class: 'mp-wear-well-worn' }, { key: 'battle_scarred', label: 'Battle-Scarred', color: '#ff4d4d', class: 'mp-wear-battle-scarred' } ]; const RARITY_TIERS = [ { key: 'consumer', label: 'Consumer', color: '#b0b0b0' }, { key: 'industrial', label: 'Industrial', color: '#4dff4d' }, { key: 'restricted', label: 'Restricted', color: '#4a90ff' }, { key: 'covert', label: 'Covert', color: '#d244ff' }, { key: 'legendary', label: 'Legendary', color: '#ffaa00' }, { key: 'class_red', label: 'Classified', color: '#ff4d4d' } ]; const WEAPON_TYPES = ['Rifles', 'Pistols', 'Knives']; const RECHARGE_TIERS = [ { coins: 500, eur: 5.00 }, { coins: 1000, eur: 10.00 }, { coins: 2500, eur: 25.00 }, { coins: 5000, eur: 50.00 } ]; const MAX_WALLET_COINS = 50000; function getWearPercentage(wearRating) { return Math.round((1 - wearRating) * 100); } function getWearTier(wearRating) { const pct = getWearPercentage(wearRating); if (pct >= 95) return WEAR_TIERS[0]; if (pct >= 80) return WEAR_TIERS[1]; if (pct >= 50) return WEAR_TIERS[2]; if (pct >= 20) return WEAR_TIERS[3]; return WEAR_TIERS[4]; } function formatWearLabel(wearRating) { const pct = getWearPercentage(wearRating); return `${WEAR_TIERS.find(t => getWearTier(wearRating).key === t.key)?.label || 'Factory New'} (${pct}%)`; } function retrieveObfuscatedWallet() { if (typeof Module !== 'undefined' && Module !== null && typeof Module._retrieveObfuscatedWallet === 'function' && window.dangerUser?.userId) { try { const balance = Number(Module._retrieveObfuscatedWallet(window.dangerUser.userId)); return balance; } catch (e) { console.warn('[Marketplace] Native wallet retrieval failed, using fallback'); } } return marketplaceCoinBalance; } function openMarketplace() { renderMarketplaceView(); } function closeMarketplace() { var box = document.getElementById('main-content-box'); if (box) { box.innerHTML = ''; } const overlay = document.getElementById('marketplace-overlay'); if (overlay) overlay.style.display = 'none'; } function switchLoadoutSubTab(tab) { var subTabs = document.querySelectorAll('.loadout-sub-tab'); subTabs.forEach(t => t.classList.remove('active')); var activeTab = document.querySelector(`.loadout-sub-tab[onclick="switchLoadoutSubTab('${tab}')"]`); if (activeTab) activeTab.classList.add('active'); var loadoutGrid = document.getElementById('loadout-grid'); if (!loadoutGrid) return; var allItems = loadoutGrid.querySelectorAll('.loadout-item'); allItems.forEach(item => { item.style.display = 'none'; var itemType = item.getAttribute('data-type') || ''; if (tab === 'all' || itemType === tab) item.style.display = 'block'; }); } function openStripeModal() { if (!isUserLoggedIn()) { showMarketplaceToast('error', 'Sign in required for Stripe purchases.'); return; } var modal = document.getElementById('stripe-modal'); if (modal) modal.style.display = 'block'; var checkbox = document.getElementById('stripe-legal-compliance'); var checkoutBtn = document.getElementById('stripe-checkout-btn'); if (checkbox && checkoutBtn) { checkbox.checked = false; checkoutBtn.disabled = true; } } function closeStripeModal() { var modal = document.getElementById('stripe-modal'); if (modal) modal.style.display = 'none'; } function initiateStripeCheckout() { var checkbox = document.getElementById('stripe-legal-compliance'); if (!checkbox || !checkbox.checked) { showMarketplaceToast('error', 'Legal compliance checkbox required.'); return; } var amountSelect = document.getElementById('stripe-amount'); var amount = amountSelect ? Number(amountSelect.value) : 5; fetch('/api/stripe/checkout', { method: 'POST', credentials: 'include', headers: { 'Content-Type': 'application/json' }, body: JSON.stringify({ amount: amount, legalComplianceAccepted: true }) }).then(r => r.json()).then(data => { if (data.error) { showMarketplaceToast('error', data.error); } else if (data.checkoutUrl) { window.location.href = data.checkoutUrl; } }).catch(() => showMarketplaceToast('error', 'STRIPE CHECKOUT FAILED')); } function syncMarketplaceCoins() { var coinDisplay = document.getElementById('marketplace-coin-count'); if (!coinDisplay) return; const balance = retrieveObfuscatedWallet(); marketplaceCoinBalance = balance; coinDisplay.textContent = balance; const walletAmount = document.getElementById('wallet-amount'); if (walletAmount) walletAmount.textContent = balance; } // === MARKETPLACE TOAST NOTIFICATIONS === function showMarketplaceToast(type, message) { let container = document.getElementById('mp-toast-container'); if (!container) { container = document.createElement('div'); container.id = 'mp-toast-container'; container.className = 'mp-toast-container'; document.body.appendChild(container); } const toast = document.createElement('div'); toast.className = 'mp-toast ' + type; toast.textContent = message; container.appendChild(toast); setTimeout(() => { if (toast.parentNode) toast.parentNode.removeChild(toast); }, 5000); } // === RENDER THE FULL MARKETPLACE TRADING DASHBOARD === function renderMarketplaceView() { var box = document.getElementById('main-content-box'); if (!box) return; const featuredSkins = skinCatalog.slice(0, 3); box.innerHTML = `
WALLET: 0 MC
CEILING: 50,000 MC
${featuredSkins.map(s => { const change = (Math.random() * 20 - 10).toFixed(1); const isPositive = parseFloat(change) >= 0; return `
${s.name || s.skinId} ${isPositive ? '+' : ''}${change}%
`; }).join('')}
RECHARGE WALLET / KOOP COINS MC
STORE - ACTIVE LISTINGS REFRESH
YOUR ORDERS & INVENTORY LIST AN ITEM
`; loadMarketplaceCatalog(); loadMarketplaceInventory(); syncMarketplaceCoins(); } // === STOREFRONT: LIST SKINS FOR SALE BY OTHER USERS === function loadMarketplaceCatalog() { const grid = document.getElementById('mp-store-grid'); if (!grid) return; if (!skinCatalog || skinCatalog.length === 0) { grid.innerHTML = '
No items currently listed.
'; return; } grid.innerHTML = ''; const filtered = filterMarketItems(skinCatalog); filtered.forEach(skin => { const item = document.createElement('div'); item.className = 'mp-item-card'; const priceCoins = Math.round((skin.price || 0.99) * 100); const wearRating = skin.wearRating || Math.random() * 0.99; const wearTier = getWearTier(wearRating); const wearPct = getWearPercentage(wearRating); const rarity = skin.rarity || 'restricted'; const rarityColor = RARITY_TIERS.find(r => r.key === rarity)?.color || '#4a90ff'; const imageUrl = skin.previewImage || `/custom_assets/textures/weapons/v_models/${skin.skinId || skin.id}.png`; const assetId = skin.instanceId || skin.skinId || 'UNKNOWN'; const paintSeed = Math.floor(Math.random() * 4096); item.innerHTML = `
${skin.name} [${rarity.toUpperCase()}]
${skin.name || skin.skinId}
${skin.weaponType || ''} | SEED: #${paintSeed}
WEAR: ${wearRating.toFixed(4)} | FLOAT: ${wearRating.toFixed(4)}
${priceCoins} MC
`; grid.appendChild(item); }); if (filtered.length === 0) { grid.innerHTML = '
No items match your filters.
'; } } // === USER'S ORDERS & INVENTORY === function loadMarketplaceInventory() { const list = document.getElementById('mp-orders-list'); if (!list) return; let items = []; const localInventory = JSON.parse(localStorage.getItem('dw_player_inventory')) || []; items = localInventory.length > 0 ? localInventory : playerMarketInventory; if (items.length === 0) { list.innerHTML = '
Your inventory is empty. Buy items from the store!
'; return; } list.innerHTML = ''; items.forEach((item, index) => { const wearRating = item.wearRating || Math.random() * 0.99; const wearTier = getWearTier(wearRating); const wearPct = getWearPercentage(wearRating); const paintSeed = item.paintSeed || Math.floor(Math.random() * 4096); const assetId = item.instanceId || item.assetId || `ASSET-${index}`; const row = document.createElement('div'); row.className = 'mp-item-card'; row.style.padding = '8px'; row.style.flexDirection = 'column'; row.innerHTML = `
${item.name || ''}
${item.name || item.skinId || 'UNKNOWN ITEM'}
SEED: #${paintSeed} | ASSET: ${assetId}
WEAR: ${wearRating.toFixed(4)}
`; list.appendChild(row); }); } // === TRANSACTION GATEWAY: BUY NOW === function buyMarketSkin(skinId, assetId) { const priceCoins = Math.round((skinCatalog.find(s => (s.skinId || s.id) === skinId)?.price || 0.99) * 100); const userBalance = retrieveObfuscatedWallet(); if (userBalance < priceCoins) { showMarketplaceToast('error', `INSUFFICIENT FUNDS: Need ${priceCoins} MC, have ${userBalance}`); playClickSfx?.(); return; } if (window.dangerGameSocket && window.dangerGameSocket.connected) { window.dangerGameSocket.emit('buy_skin', { skinId: skinId, assetId: assetId }); } else { marketplaceCoinBalance = userBalance - priceCoins; syncMarketplaceCoins(); showMarketplaceToast('success', `PURCHASED: ${assetId} for ${priceCoins} MC`); } playClickSfx?.(); } // === BUY ORDER (LIMIT ORDER) MECHANIC === function openBuyOrderModal(skinId) { currentBuyOrderSkinId = skinId; const skin = skinCatalog.find(s => (s.skinId || s.id) === skinId); const skinName = skin?.name || skinId || 'UNKNOWN'; const currentPrice = Math.round((skin?.price || 0.99) * 100); let modal = document.getElementById('mp-buyorder-modal'); if (!modal) { modal = document.createElement('div'); modal.id = 'mp-buyorder-modal'; modal.className = 'mp-modal-overlay'; modal.innerHTML = `
PLACE BUY ORDER ×
`; document.body.appendChild(modal); } const nameEl = modal.querySelector('#mp-bo-skin-name'); const priceEl = modal.querySelector('#mp-bo-current-price'); const totalEl = modal.querySelector('#mp-bo-total-value'); const balanceEl = modal.querySelector('#mp-bo-wallet-balance'); if (nameEl) nameEl.textContent = skinName; if (priceEl) priceEl.textContent = `${currentPrice} MC`; if (balanceEl) balanceEl.textContent = `${retrieveObfuscatedWallet()} MC`; const priceInput = modal.querySelector('#mp-bo-price-input'); const qtyInput = modal.querySelector('#mp-bo-quantity-input'); const submitBtn = modal.querySelector('#mp-bo-submit-btn'); function validateAndCalculate() { const price = parseInt(priceInput.value) || currentPrice; const qty = parseInt(qtyInput.value) || 1; const total = price * qty; const wallet = retrieveObfuscatedWallet(); if (totalEl) totalEl.textContent = `${total} MC`; if (balanceEl) balanceEl.textContent = `${wallet} MC`; if (price > 0 && qty > 0 && total <= wallet && total <= wallet) { if (submitBtn) submitBtn.disabled = false; } else { if (submitBtn) submitBtn.disabled = true; } } if (priceInput) priceInput.oninput = validateAndCalculate; if (qtyInput) qtyInput.oninput = validateAndCalculate; modal.style.display = 'flex'; validateAndCalculate(); } function closeBuyOrderModal() { const modal = document.getElementById('mp-buyorder-modal'); if (modal) modal.style.display = 'none'; currentBuyOrderSkinId = null; } function submitBuyOrder() { const price = parseInt(document.getElementById('mp-bo-price-input')?.value) || 0; const qty = parseInt(document.getElementById('mp-bo-quantity-input')?.value) || 1; const total = price * qty; const wallet = retrieveObfuscatedWallet(); if (total > wallet) { showMarketplaceToast('error', `INSUFFICIENT FUNDS: Need ${total} MC, have ${wallet}`); return; } if (window.dangerGameSocket && window.dangerGameSocket.connected) { window.dangerGameSocket.emit('place_buy_order', { skinId: currentBuyOrderSkinId, price: price, quantity: qty, totalLocked: total }); } showMarketplaceToast('info', `BUY ORDER PLACED: ${qty}x ${currentBuyOrderSkinId} @ ${price} MC`); closeBuyOrderModal(); playClickSfx?.(); } // === ORDER BOOK (BID/ASK SPREAD) === function renderOrderBook(skinId) { const bids = orderBookData.bids || []; const asks = orderBookData.asks || []; let bidsHtml = bids.slice(0, 5).map(bid => `
${bid.price} MC ${bid.quantity}x
`).join(''); let asksHtml = asks.slice(0, 5).map(ask => `
${ask.price} MC ${ask.quantity}x
`).join(''); return `
ORDER BOOK
SELL ORDERS (ASK)QTY
${asksHtml || '
No asks
'}
BUY ORDERS (BID)QTY
${bidsHtml || '
No bids
'}
`; } function fetchOrderBook(skinId) { orderBookData = { bids: [ { price: Math.round(Math.random() * 800 + 200), quantity: Math.floor(Math.random() * 10) + 1 }, { price: Math.round(Math.random() * 700 + 150), quantity: Math.floor(Math.random() * 8) + 1 }, { price: Math.round(Math.random() * 600 + 100), quantity: Math.floor(Math.random() * 6) + 1 }, { price: Math.round(Math.random() * 500 + 50), quantity: Math.floor(Math.random() * 5) + 1 }, { price: Math.round(Math.random() * 400 + 20), quantity: Math.floor(Math.random() * 3) + 1 } ], asks: [ { price: Math.round(Math.random() * 300 + 1000), quantity: Math.floor(Math.random() * 5) + 1 }, { price: Math.round(Math.random() * 300 + 800), quantity: Math.floor(Math.random() * 4) + 1 }, { price: Math.round(Math.random() * 200 + 600), quantity: Math.floor(Math.random() * 6) + 1 }, { price: Math.round(Math.random() * 200 + 400), quantity: Math.floor(Math.random() * 8) + 1 }, { price: Math.round(Math.random() * 100 + 250), quantity: Math.floor(Math.random() * 10) + 1 } ] }; orderBookData.bids.sort((a, b) => b.price - a.price); orderBookData.asks.sort((a, b) => a.price - a.price); } // === PRICE CHART === function generateChartData(timeframe) { const dataPoints = timeframe === '24h' ? 24 : timeframe === '7d' ? 7 : 30; const points = []; let basePrice = 99; for (let i = 0; i < dataPoints; i++) { basePrice += (Math.random() - 0.5) * 10; basePrice = Math.max(20, basePrice); points.push(basePrice); } const high = Math.max(...points); const low = Math.min(...points); const volume = Math.floor(Math.random() * 5000 + 1000); chartData[timeframe] = { points, high, low, volume }; return { points, high, low, volume }; } function renderPriceChart(timeframe) { const canvas = document.getElementById('mp-chart-canvas'); if (!canvas) return; let data = chartData[timeframe]; if (!data) { data = generateChartData(timeframe); chartData[timeframe] = data; } const ctx = canvas.getContext('2d'); const width = canvas.width = canvas.offsetWidth * 2; const height = canvas.height = canvas.offsetHeight * 2; ctx.scale(2, 2); const w = canvas.width / 2; const h = canvas.height / 2; const points = data.points; const peak = Math.max(...points); const valley = Math.min(...points); const range = peak - valley || 1; ctx.clearRect(0, 0, w, h); ctx.fillStyle = 'rgba(10, 10, 12, 0.9)'; ctx.fillRect(0, 0, w, h); ctx.strokeStyle = 'rgba(255, 101, 0, 0.3)'; ctx.lineWidth = 1; for (let i = 1; i <= 4; i++) { const y = (h / 4) * i; ctx.beginPath(); ctx.moveTo(0, y); ctx.lineTo(w, y); ctx.stroke(); } ctx.strokeStyle = '#ff6500'; ctx.lineWidth = 2; ctx.shadowColor = 'rgba(255, 101, 0, 0.6)'; ctx.shadowBlur = 8; ctx.beginPath(); points.forEach((val, i) => { const x = (i / (points.length - 1)) * w; const y = h - ((val - valley) / range) * h; if (i === 0) ctx.moveTo(x, y); else ctx.lineTo(x, y); }); ctx.stroke(); ctx.shadowBlur = 0; ctx.fillStyle = 'rgba(255, 101, 0, 0.1)'; ctx.beginPath(); ctx.moveTo(0, h); points.forEach((val, i) => { const x = (i / (points.length - 1)) * w; const y = h - ((val - valley) / range) * h; ctx.lineTo(x, y); }); ctx.lineTo(w, h); ctx.fill(); ctx.fillStyle = 'rgba(255, 255, 255, 0.4)'; ctx.font = '8px Orbitron'; ctx.textAlign = 'right'; ctx.fillText(peak.toFixed(1), w - 4, 10); ctx.textAlign = 'left'; ctx.fillText(valley.toFixed(1), 4, h - 4); } function updateChartHUD(timeframe) { const highEl = document.getElementById('mp-chart-high'); const lowEl = document.getElementById('mp-chart-low'); const volEl = document.getElementById('mp-chart-volume'); const data = chartData[timeframe]; if (highEl && data) highEl.textContent = data.high.toFixed(2) + ' MC'; if (lowEl && data) lowEl.textContent = data.low.toFixed(2) + ' MC'; if (volEl && data) volEl.textContent = data.volume.toLocaleString(); } function switchChartTimeframe(tf) { if (!chartData[tf]) generateChartData(tf); renderPriceChart(tf); updateChartHUD(tf); document.querySelectorAll('.mp-chart-toolbar button').forEach(btn => { btn.classList.toggle('active', btn.dataset.tf === tf); }); } // === LIST AN ITEM MODAL === function openListItemModal() { let modal = document.getElementById('mp-listitem-modal'); if (!modal) { modal = document.createElement('div'); modal.id = 'mp-listitem-modal'; modal.className = 'mp-modal-overlay'; modal.innerHTML = `
LIST AN ITEM FOR SALE ×
`; document.body.appendChild(modal); } const select = modal.querySelector('#mp-li-skin-select'); if (select) { const localInventory = JSON.parse(localStorage.getItem('dw_player_inventory')) || []; const skinItems = localInventory.length > 0 ? localInventory : (typeof SKINS !== 'undefined' ? SKINS : []); select.innerHTML = ''; skinItems.forEach((skin, i) => { const opt = document.createElement('option'); opt.value = i; opt.textContent = `${skin.name || skin.skinId || 'Item'} [${skin.weaponType || 'N/A'}]`; select.appendChild(opt); }); } const priceInput = modal.querySelector('#mp-li-price-input'); const seedInput = modal.querySelector('#mp-li-seed-input'); if (priceInput) priceInput.value = '100'; if (seedInput) seedInput.value = Math.floor(Math.random() * 4096); modal.style.display = 'flex'; } function closeListItemModal() { const modal = document.getElementById('mp-listitem-modal'); if (modal) modal.style.display = 'none'; } function submitListItem() { const skinSelect = document.getElementById('mp-li-skin-select'); const priceInput = document.getElementById('mp-li-price-input'); const wearSelect = document.getElementById('mp-li-wear-select'); const seedInput = document.getElementById('mp-li-seed-input'); if (!skinSelect || !priceInput) return; const skinIndex = parseInt(skinSelect.value); if (isNaN(skinIndex) || skinIndex < 0) { showMarketplaceToast('error', 'PLEASE SELECT A SKIN'); return; } const localInventory = JSON.parse(localStorage.getItem('dw_player_inventory')) || []; const skinItems = localInventory.length > 0 ? localInventory : (typeof SKINS !== 'undefined' ? SKINS : []); const skin = skinItems[skinIndex]; if (!skin) { showMarketplaceToast('error', 'SKIN NOT FOUND IN INVENTORY'); return; } const price = parseInt(priceInput.value); if (!price || price <= 0) { showMarketplaceToast('error', 'PLEASE ENTER A VALID PRICE'); return; } const listing = { instanceId: 'LIST-' + Date.now(), skinId: skin.skinId || skin.id, name: skin.name, weaponType: skin.weaponType, price: price / 100, priceCoins: price, wearRating: Math.random() * 0.99, paintSeed: parseInt(seedInput?.value) || Math.floor(Math.random() * 4096), owner: window.dangerUser?.steamId || 'local', glbPath: skin.glbPath || skin.previewImage }; if (!skinCatalog) skinCatalog = []; skinCatalog.push(listing); if (window.dangerGameSocket && window.dangerGameSocket.connected) { window.dangerGameSocket.emit('list_item_for_sale', listing); } showMarketplaceToast('success', `LISTED: ${listing.name} for ${price} MC`); closeListItemModal(); loadMarketplaceCatalog(); playClickSfx?.(); } // === SELL INVENTORY ITEM === function sellInventoryItem(index) { const localInventory = JSON.parse(localStorage.getItem('dw_player_inventory')) || []; const skinItems = localInventory.length > 0 ? localInventory : playerMarketInventory; if (!skinItems[index]) return; const soldItem = skinItems[index]; const sellPrice = Math.round((soldItem.price || 0.50) * 50); skinItems.splice(index, 1); localStorage.setItem('dw_player_inventory', JSON.stringify(skinItems)); const wallet = retrieveObfuscatedWallet(); marketplaceCoinBalance = wallet + sellPrice; syncMarketplaceCoins(); showMarketplaceToast('success', `SOLD: ${soldItem.name || soldItem.skinId} for ${sellPrice} MC`); loadMarketplaceInventory(); playClickSfx?.(); } // === FILTER LOGIC === function filterMarketItems(items) { return items.filter(skin => { const name = (skin.name || skin.skinId || '').toLowerCase(); const weapon = (skin.weaponType || '').toLowerCase(); const rarity = (skin.rarity || '').toLowerCase(); if (activeFilters.search && !name.includes(activeFilters.search.toLowerCase())) return false; if (activeFilters.weaponType !== 'all') { if (activeFilters.weaponType === 'Rifles' && !weapon.includes('rifle') && !weapon.includes('ak') && !weapon.includes('m4')) return false; if (activeFilters.weaponType === 'Pistols' && !weapon.includes('gk18') && !weapon.includes('pistol') && !weapon.includes('usp') && !weapon.includes('p250')) return false; if (activeFilters.weaponType === 'Knives' && !weapon.includes('knife') && !weapon.includes('melee')) return false; } if (activeFilters.rarity !== 'all' && rarity !== activeFilters.rarity) return false; if (activeFilters.wear !== 'all') { const wearRating = skin.wearRating || 0; const wearTier = getWearTier(wearRating); if (wearTier.key !== activeFilters.wear) return false; } return true; }); } function onSearchFilterInput(value) { activeFilters.search = value; loadMarketplaceCatalog(); } function onWeaponTypeFilter(value) { activeFilters.weaponType = value; loadMarketplaceCatalog(); } function onRarityFilter(value) { activeFilters.rarity = value; loadMarketplaceCatalog(); } function onWearTierFilter(value) { activeFilters.wear = value; loadMarketplaceCatalog(); } function uploadAdminSkin() { if (!window._pinMasterKeyVerified) { alert('ERROR: Master key pin not verified. Upload .pin to unlock.'); return; } const input = document.getElementById('skin-upload-input'); const name = document.getElementById('skin-custom-name').value.trim(); const weaponType = document.getElementById('skin-weapon-type').value; const maxSupply = document.getElementById('skin-max-supply').value; if (!input.files || !input.files[0]) { alert('Please select a .glb file.'); return; } if (!name) { alert('Please enter a skin name.'); return; } const file = input.files[0]; const reader = new FileReader(); reader.onload = function(e) { const arrayBuffer = e.target.result; const base64 = arrayToBase64(arrayBuffer); if (window.dangerGameSocket && window.dangerGameSocket.connected) { window.dangerGameSocket.emit('admin_upload_global_skin', { customSkinName: name, weaponType: weaponType, maxSupply: parseInt(maxSupply, 10) || 999, skinFileBase64: base64 }); window.dangerGameSocket.on('skin_upload_result', (result) => { if (result.success) { appendChatMessage({ author: 'SYSTEM', text: `Skin "${result.skinId}" uploaded to global catalog. GLB path: ${result.glbPath}`, color: '#00ffaa', timestamp: Date.now() }); // Pipe to WASM virtual FS if (typeof Module !== 'undefined' && Module.FS) { Module.FS.writeFile('/drengine/assets/skins/' + result.skinId + '.glb', new Uint8Array(arrayBuffer)); } } else { appendChatMessage({ author: 'SYSTEM', text: 'Skin upload FAILED: ' + (result.error || 'unknown'), color: '#ff5555', timestamp: Date.now() }); } }); } }; reader.readAsArrayBuffer(file); } function arrayToBase64(buffer) { let binary = ''; const bytes = new Uint8Array(buffer); for (let i = 0; i < bytes.byteLength; i++) { binary += String.fromCharCode(bytes[i]); } return btoa(binary); } function addMapToPool() { const mapName = document.getElementById('map-pool-input').value.trim(); if (!mapName) return; if (window.dangerGameSocket && window.dangerGameSocket.connected) { window.dangerGameSocket.emit('server_admin_command', { command: 'changemap', targetName: mapName }); } } function resetMapPool() { if (window.dangerGameSocket && window.dangerGameSocket.connected) { window.dangerGameSocket.emit('server_admin_command', { command: 'changemap', targetName: 'Backrooms' }); } } function uploadCharacterModel() { const input = document.getElementById('model-upload-input'); const charId = document.getElementById('model-char-id').value.trim(); if (!input.files || !input.files[0] || !charId) { alert('Select a .glb file and enter a character ID.'); return; } const file = input.files[0]; const reader = new FileReader(); reader.onload = function(e) { const arrayBuffer = e.target.result; if (typeof Module !== 'undefined' && Module.FS) { Module.FS.writeFile('/drengine/assets/characters/' + charId + '.glb', new Uint8Array(arrayBuffer)); appendChatMessage({ author: 'SYSTEM', text: `Character model "${charId}" loaded into WASM virtual FS`, color: '#00ffaa', timestamp: Date.now() }); } }; reader.readAsArrayBuffer(file); } window.updateMusicVolumeLabel = function(val) { const label = document.getElementById("music-volume-value-label"); if (label) label.innerText = val + "%"; if (typeof window.AudioEngine !== 'undefined' && window.AudioEngine.setMusicVolume) { window.AudioEngine.setMusicVolume(val); } }; // === INITIALIZE MOCK SKIN CATALOG (for offline mode / initial load) === function initializeSkinCatalog() { if (skinCatalog.length === 0) { skinCatalog = [ { skinId: 'ar47_core_neonstrike', id: 'ar47_core_neonstrike', name: 'AR-47 | Neon Strike', weaponType: 'AR-47', price: 1.99, maxSupply: 333, rarity: 'covert', wearRating: 0.07, previewImage: '/custom_assets/textures/weapons/v_models/ar47_core.png' }, { skinId: 'm4a4_sandstorm', id: 'm4a4_sandstorm', name: 'M4A4-S | Sandstorm', weaponType: 'M4A4', price: 2.49, maxSupply: 333, rarity: 'covert', wearRating: 0.15, previewImage: '/custom_assets/textures/weapons/v_models/m4a4_sandstorm.png' }, { skinId: 'awp_phoenix', id: 'awp_phoenix', name: 'AWP | Phoenix Flame', weaponType: 'AWP', price: 4.99, maxSupply: 99, rarity: 'class_red', wearRating: 0.03, previewImage: '/custom_assets/textures/weapons/v_models/awp.png' }, { skinId: 'gk18_moonrise', id: 'gk18_moonrise', name: 'GK18-18 | Moonrise', weaponType: 'GK18', price: 0.99, maxSupply: 500, rarity: 'restricted', wearRating: 0.22, previewImage: '/custom_assets/textures/weapons/v_models/gk18.png' }, { skinId: 'usp_scorpion', id: 'usp_scorpion', name: 'USP-S | Scorpion', weaponType: 'USP', price: 1.49, maxSupply: 400, rarity: 'restricted', wearRating: 0.12, previewImage: '/custom_assets/textures/weapons/v_models/usp.png' }, { skinId: 'knife_dragon', id: 'knife_dragon', name: 'Shadow Knife | Dragon', weaponType: 'Knife', price: 9.99, maxSupply: 50, rarity: 'legendary', wearRating: 0.01, previewImage: '/custom_assets/textures/weapons/v_models/knife.png' } ]; // Initialize mock order book data fetchOrderBook('ar47_core_neonstrike'); // Pre-generate chart data generateChartData('24h'); generateChartData('7d'); generateChartData('30d'); } } // === STRIPE PAYMENT GATEWAY (CLOSED-LOOP SALDO) === function openRechargeWalletModal() { let modal = document.getElementById('mp-recharge-modal'); if (!modal) { modal = document.createElement('div'); modal.id = 'mp-recharge-modal'; modal.className = 'mp-modal-overlay'; modal.innerHTML = `
RECHARGE WALLET / KOOP COINS ×
Market Coins are non-refundable internal tokens. Max wallet: 50,000 MC
${RECHARGE_TIERS.map((tier, i) => `
${tier.coins} MC €${tier.eur.toFixed(2)}
`).join('')}
EU HERROEPINGSRECHT WAIVER (ART. 6(1)(b) + 16 T&Cs):
`; document.body.appendChild(modal); } selectedRechargeTier = null; modal.style.display = 'flex'; } function closeRechargeWalletModal() { const modal = document.getElementById('mp-recharge-modal'); if (modal) modal.style.display = 'none'; selectedRechargeTier = null; } function selectRechargeTier(index) { selectedRechargeTier = index; const btns = document.querySelectorAll('.mp-tier-btn'); btns.forEach((btn, i) => { btn.classList.toggle('selected', i === index); }); const customInput = document.getElementById('mp-custom-amount'); if (customInput) customInput.value = ''; toggleStripeButton(); } function toggleStripeButton() { const checkbox = document.getElementById('mp-legal-checkbox'); const stripeBtn = document.getElementById('mp-stripe-btn'); if (stripeBtn) { stripeBtn.disabled = !checkbox || !checkbox.checked; } } function getSelectedRechargeAmount() { if (selectedRechargeTier !== null && selectedRechargeTier >= 0) { return RECHARGE_TIERS[selectedRechargeTier].coins; } const custom = document.getElementById('mp-custom-amount'); if (custom && custom.value) { const val = parseInt(custom.value); if (val >= 100 && val <= MAX_WALLET_COINS) return val; } return 0; } async function executeStripeCheckout() { const amount = getSelectedRechargeAmount(); if (amount <= 0) { showMarketplaceToast('error', 'INVALID AMOUNT'); return; } const wallet = retrieveObfuscatedWallet(); if (wallet + amount > MAX_WALLET_COINS) { showMarketplaceToast('error', `MAXIMUM WALLET: 50,000 MC`); return; } const checkbox = document.getElementById('mp-legal-checkbox'); if (!checkbox || !checkbox.checked) { showMarketplaceToast('error', 'ACCEPT THE LEGAL WAIVER'); return; } const tier = selectedRechargeTier !== null ? RECHARGE_TIERS[selectedRechargeTier] : null; const eurAmount = tier ? tier.eur : (amount / 100); const stripeBtn = document.getElementById('mp-stripe-btn'); if (stripeBtn) { stripeBtn.disabled = true; stripeBtn.textContent = 'PROCESSING...'; } try { if (window.dangerGameSocket && window.dangerGameSocket.connected) { window.dangerGameSocket.emit('stripe_checkout_request', { coins: amount, eur: eurAmount, skinId: tier ? null : null }); } // Frontend simulation: map funds to user profile marketplaceCoinBalance = wallet + amount; syncMarketplaceCoins(); showMarketplaceToast('success', `${amount} MC ADDED TO WALLET via Stripe`); closeRechargeWalletModal(); // Notify native C++ core if (typeof Module !== 'undefined' && typeof Module._native_sync_market_coins === 'function') { Module._native_sync_market_coins(window.dangerUser?.userId || 0, marketplaceCoinBalance); } } catch (e) { showMarketplaceToast('error', 'CHECKOUT FAILED: ' + e.message); } if (stripeBtn) { stripeBtn.disabled = !checkbox?.checked || false; stripeBtn.textContent = 'PROCEED TO STRIPE CHECKOUT (iDEAL & CREDITCARD)'; } } // === DAILY CLAIM / ANTI-BOT SYSTEM === function claimDailyReward() { if (!isUserLoggedIn()) { showMarketplaceToast('error', 'Sign in required to claim rewards.'); return; } fetch('/api/claim-reward', { method: 'POST', credentials: 'include' }) .then(r => r.json()) .then(data => { if (data.success && typeof data.coinsAdded !== 'undefined') { marketplaceCoinBalance = Math.min(marketplaceCoinBalance + data.coinsAdded, MAX_WALLET_COINS); syncMarketplaceCoins(); showMarketplaceToast('success', `${data.coinsAdded} MC CLAIMED VIA DAILY REWARD`); if (typeof Module !== 'undefined' && Module._native_sync_market_coins) { Module._native_sync_market_coins(window.dangerUser?.userId || 0, marketplaceCoinBalance); } } else if (data.error) { showMarketplaceToast('error', data.error); } else { showMarketplaceToast('success', 'DAILY REWARD CLAIMED'); } }) .catch(() => showMarketplaceToast('error', 'CLAIM FAILED - CONNECTION ERROR')); } // === LIVE DROP CLAIM SYSTEM === function showDropAlert(dropInfo) { let alertEl = document.getElementById('mp-drop-alert'); if (!alertEl) { alertEl = document.createElement('div'); alertEl.id = 'mp-drop-alert'; alertEl.className = 'mp-drop-alert'; alertEl.innerHTML = `
TACTICAL DROP INCOMING
00:00:000
PRESS W-A-S-D TO CLAIM
`; document.body.appendChild(alertEl); } activeDropEvent = dropInfo; generateDropChallenge(); alertEl.classList.add('active'); startDropCountdown(dropInfo.expiresAt || (Date.now() + 30000)); } function generateDropChallenge() { const keys = ['W', 'A', 'S', 'D']; const shuffled = [...keys].sort(() => Math.random() - 0.5); dropSequence = shuffled; dropSequenceIndex = 0; const challengeEl = document.getElementById('mp-drop-challenge'); if (challengeEl) { challengeEl.innerHTML = `PRESS ${shuffled[0]} - ${shuffled[1]} - ${shuffled[2]} - ${shuffled[3]} TO CLAIM`; } // Attach key listener const keyHandler = (e) => { const key = e.key.toUpperCase(); if (dropSequenceIndex < dropSequence.length && key === dropSequence[dropSequenceIndex]) { dropSequenceIndex++; if (dropSequenceIndex >= dropSequence.length) { const btn = document.getElementById('mp-drop-claim-btn'); if (btn) btn.disabled = false; dropSequence = []; } } }; const existingHandler = window._dropKeyHandler; if (existingHandler) { document.removeEventListener('keydown', existingHandler); } window._dropKeyHandler = keyHandler; document.addEventListener('keydown', keyHandler); } function startDropCountdown(expiresAt) { if (dropCountdownTimer) clearInterval(dropCountdownTimer); dropCountdownTimer = setInterval(() => { const now = Date.now(); const remaining = expiresAt - now; const countdownEl = document.getElementById('mp-drop-countdown'); if (!countdownEl) return; if (remaining <= 0) { clearInterval(dropCountdownTimer); countdownEl.textContent = '00:00:000'; const btn = document.getElementById('mp-drop-claim-btn'); if (btn) { btn.disabled = true; btn.textContent = 'CLAIMED / OUT OF STOCK'; btn.classList.add('claimed'); } hideDropAlert(); } else { const seconds = Math.floor(remaining / 1000); const ms = remaining % 1000; countdownEl.textContent = `00:${String(seconds).padStart(2, '0')}:${String(ms).padStart(3, '0')}`; if (remaining < 5000) { countdownEl.style.color = '#ff0033'; countdownEl.style.textShadow = '0 0 12px rgba(255, 0, 51, 0.7)'; } } }, 1); } function hideDropAlert() { const alertEl = document.getElementById('mp-drop-alert'); if (alertEl) alertEl.classList.remove('active'); if (dropCountdownTimer) clearInterval(dropCountdownTimer); if (window._dropKeyHandler) { document.removeEventListener('keydown', window._dropKeyHandler); window._dropKeyHandler = null; } activeDropEvent = null; } function claimDropNative() { const btn = document.getElementById('mp-drop-claim-btn'); if (btn) { btn.disabled = true; btn.textContent = 'CLAIMED / OUT OF STOCK'; btn.classList.add('claimed'); } if (window.dangerGameSocket && window.dangerGameSocket.connected) { window.dangerGameSocket.emit('claim_drop', { dropId: activeDropEvent?.dropId || 'current', userId: window.dangerUser?.steamId }); } showMarketplaceToast('success', 'DROP CLAIM SUBMITTED'); setTimeout(hideDropAlert, 2000); } // === SKIN CARD WEAR BAR + ASSET ID (for SKINS inventory view) === function refreshPlayerSkinsList() { const listContainer = document.getElementById("player-skins-market-list"); if (!listContainer) return; let activeDrops = getActiveSkinDrops(); if (activeDrops.length === 0) { listContainer.innerHTML = `No active skin drops available. Connecting makes new skins available during play.`; return; } listContainer.innerHTML = ''; activeDrops.forEach((skin, index) => { const rarityColors = { 'consumer': '#b0b0b0', 'industrial': '#4dff4d', 'restricted': '#4a90ff', 'covert': '#d244ff', 'legendary': '#ffaa00', 'class-red': '#ff4d4d' }; const rarityColor = rarityColors[skin.rarity] || '#888'; const wearRating = skin.wearRating || (Math.random() * 0.99); const wearTier = getWearTier(wearRating); const wearPct = getWearPercentage(wearRating); const paintSeed = skin.paintSeed || Math.floor(Math.random() * 4096); const assetId = skin.id || `ASSET-${index}`; const imgSrc = skin.imagePath || "/custom_assets/textures/weapons/v_models/ar47_core.png"; const item = document.createElement("div"); item.className = "skin-drop-item"; item.style.cssText = "background:rgba(13,17,21,0.8); border:1px solid " + rarityColor + "; padding:10px; border-radius:6px; display:flex; align-items:center; gap:10px; margin-bottom:6px; position:relative;"; item.innerHTML = ` ${skin.name}
${skin.name} [${skin.rarity}]
ASSET: ${assetId} | SEED: #${paintSeed} | WEAR: ${wearRating.toFixed(4)}
`; listContainer.appendChild(item); }); } // === SKIN DETAIL VIEW WITH CHART + ORDER BOOK === function openSkinDetailView(skinId) { const skin = skinCatalog.find(s => (s.skinId || s.id) === skinId) || playerMarketInventory.find(s => (s.skinId || s.id) === skinId); if (!skin) { showMarketplaceToast('error', 'SKIN NOT FOUND'); return; } selectedSkinDetail = skin; const assetId = skin.instanceId || skin.skinId || skin.id; const priceCoins = Math.round((skin.price || 0.99) * 100); const wearRating = skin.wearRating || 0.07; const wearTier = getWearTier(wearRating); const wearPct = getWearPercentage(wearRating); const paintSeed = skin.paintSeed || Math.floor(Math.random() * 4096); const rarity = skin.rarity || 'restricted'; const rarityColor = RARITY_TIERS.find(r => r.key === rarity)?.color || '#4a90ff'; fetchOrderBook(skinId); var box = document.getElementById('main-content-box'); if (!box) return; box.innerHTML = `
WALLET: ${retrieveObfuscatedWallet()} MC
${skin.name}
${skin.name}
${skin.name}
${skin.weaponType}
ASSET: ${assetId} | SEED: #${paintSeed}
WEAR: ${wearRating.toFixed(4)} | FLOAT: ${wearRating.toFixed(4)}
${wearTier.label} (${wearPct}% condition)
${priceCoins} MC
24H HIGH:0.00 MC 24H LOW:0.00 MC ALL-TIME VOL:0
${renderOrderBook(skinId)}
RECHARGE WALLET MC
`; setTimeout(() => { switchChartTimeframe('24h'); }, 50); } // === WEBSOCKET EVENT LISTENERS FOR MARKETPLACE === function initMarketplaceSocketHandlers() { if (!window.dangerGameSocket) return; window.dangerGameSocket.on('skin_catalog', (data) => { if (data && data.skins) { skinCatalog = data.skins; loadMarketplaceCatalog(); } }); window.dangerGameSocket.on('player_inventory', (data) => { if (data && data.items) { playerMarketInventory = data.items; loadMarketplaceInventory(); } }); window.dangerGameSocket.on('get_coins_result', (data) => { if (data && typeof data.coins !== 'undefined') { marketplaceCoinBalance = data.coins; syncMarketplaceCoins(); } }); window.dangerGameSocket.on('purchase_success', (data) => { if (data && data.coins !== undefined) { marketplaceCoinBalance = data.coins; } syncMarketplaceCoins(); showMarketplaceToast('success', 'PURCHASE CONFIRMED: ' + (data.item?.name || 'ITEM')); }); window.dangerGameSocket.on('purchase_failed', (data) => { showMarketplaceToast('error', 'PURCHASE FAILED: ' + (data.reason || 'Unknown error')); }); window.dangerGameSocket.on('drop_incoming', (data) => { showDropAlert(data); }); window.dangerGameSocket.on('stripe_checkout_result', (data) => { if (data && data.success && typeof data.coinsAdded !== 'undefined') { marketplaceCoinBalance = Math.min(marketplaceCoinBalance + data.coinsAdded, MAX_WALLET_COINS); syncMarketplaceCoins(); showMarketplaceToast('success', `${data.coinsAdded} MC ADDED VIA STRIPE`); } else { showMarketplaceToast('error', 'STRIPE CHECKOUT FAILED'); } }); } // === CLEANUP JITTER BUFFER ON EXIT === function cleanupJitterBuffer() { PACKET_JITTER_BUFFER.clear(); } window.addEventListener('pagehide', () => { cleanupJitterBuffer(); }); // === PERFORMANCE MEMORY MONITOR === // Periodically checks heap growth and triggers GC to prevent OOM in long sessions. (function startMemoryMonitor() { if (typeof performance === 'undefined' || !performance.memory) return; let lastUsedMB = 0; let lastGCTime = 0; setInterval(() => { const mem = performance.memory; if (!mem) return; const usedMB = Math.round(mem.usedJSHeapSize / 1048576); const growthMB = usedMB - lastUsedMB; if (growthMB > 250) { if (window.gc && Date.now() - lastGCTime > 180000) { try { window.gc(); lastGCTime = Date.now(); console.log(`[DREngine] Memory monitor: forced GC after ${growthMB}MB growth (heap: ${usedMB}MB)`); } catch (e) { console.warn('[DREngine] GC trigger failed:', e.message); } } lastUsedMB = usedMB; } }, 60000); })();