// 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()}
Accept & Agree
`;
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}
Accept & Continue
`;
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 = `
`;
}
// 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.
TOGGLE LIVE RELEASE MODE
+BOT
KICK
NUKE
MAP
AMMO
SLAY
FREEZE
THAW
GRAVITY
TIMESCALE
WARMUP
SPECTATE
Commands execute via chat pipeline: type /cmd in chat or click above.
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 = `
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
S DEV LOGIN (STEAM)
Authorized admins can bypass the testing gate
DEATHMATCH
TEAM DEATHMATCH
DANGER ROYALE
⚙ USER SETTINGS
# FIND MATCH
@ SOLO SERVER
Login required to join or host servers
Load custom model
v CONFIRM MODEL
TICK:64Hz | HEAP:0MB | P:0 B:0 | WAITING
ALL
PRIMARY
SECONDARY
OPERATIVES
LIMITED DROPS
`;
}
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 = `
PRIMARY
SECONDARY
EQUIPMENT
`;
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 = `
Custom BSP Map
BROWSE
No custom BSP selected
`;
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 = `
DEATHMATCH
TEAM DEATHMATCH
DANGER ROYALE
# FIND MATCH
@ SOLO SERVER
Login required to join or host servers
`;
if (typeof loadDynamicGameAssets === 'function') setTimeout(loadDynamicGameAssets, 100);
bindQueueButtons();
refreshServerList();
}
function renderOperativesView() {
var box = document.getElementById('main-content-box');
if (!box) return;
box.innerHTML = `
Load custom model
v CONFIRM MODEL
`;
renderCharacterOptions();
updateCharacterSelection();
if (typeof initMainMenuCanvasPreview === 'function') initMainMenuCanvasPreview(box);
}
function renderLoadoutView() {
var box = document.getElementById('main-content-box');
if (!box) return;
box.innerHTML = ``;
populateWeaponSelectors();
if (typeof initMainMenuCanvasPreview === 'function') initMainMenuCanvasPreview(box);
}
function renderSkinsView() {
var box = document.getElementById('main-content-box');
if (!box) return;
box.innerHTML = ``;
refreshPlayerSkinsList();
}
function renderInventoryView() {
var box = document.getElementById('main-content-box');
if (!box) return;
box.innerHTML = ``;
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'}
Sell
`).join('');
}
function renderSettingsView() {
var box = document.getElementById('main-content-box');
if (!box) return;
box.innerHTML = ``;
}
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 `${WEAPON_DATA.primary[k].name} `;
}).join('');
}
if (secondarySelect) {
const current = secondarySelect.value;
secondarySelect.innerHTML = secondaryKeys.map(k => {
const selected = k === current ? 'selected' : '';
return `${WEAPON_DATA.secondary[k].name} `;
}).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"}
Connect
${isHost ? 'Stop Server ' : ''}
`;
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.rarity}]
Claim
`;
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 += `Buy `;
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 += `Buy `;
html += `
`;
}
}
html += `
`;
html += `Reload Ammo ($${ammoCost}) `;
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:
× Close
`;
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
RECHARGE
CLAIM DAILY
${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
ALL WEAPONS
${WEAPON_TYPES.map(wt => `${wt} `).join('')}
ALL RARITIES
${RARITY_TIERS.map(r => `${r.label} `).join('')}
ALL WEAR TIERS
${WEAR_TIERS.map(w => `${w.label} `).join('')}
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 = `
[${rarity.toUpperCase()}]
${skin.name || skin.skinId}
${skin.weaponType || ''} | SEED: #${paintSeed}
WEAR: ${wearRating.toFixed(4)} | FLOAT: ${wearRating.toFixed(4)}
${priceCoins} MC
BUY NOW
BUY ORDER
`;
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.skinId || 'UNKNOWN ITEM'}
SEED: #${paintSeed} | ASSET: ${assetId}
WEAR: ${wearRating.toFixed(4)}
SELL BACK
`;
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 = `
ITEM:
CURRENT MARKET PRICE:
LIMIT PRICE (MC):
QUANTITY:
TOTAL LOCK-IN: 0 MC
AVAILABLE BALANCE: ${retrieveObfuscatedWallet()} MC
CANCEL
PLACE 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 = `
`;
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 = 'Choose from inventory... ';
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 = `
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('')}
OR ENTER CUSTOM AMOUNT (100-50,000 MC):
CANCEL
PROCEED TO STRIPE CHECKOUT (iDEAL & CREDITCARD)
`;
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
CLAIM NOW
`;
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.rarity}]
ASSET: ${assetId} | SEED: #${paintSeed} | WEAR: ${wearRating.toFixed(4)}
Claim
`;
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.weaponType}
ASSET: ${assetId} | SEED: #${paintSeed}
WEAR: ${wearRating.toFixed(4)} | FLOAT: ${wearRating.toFixed(4)}
${wearTier.label} (${wearPct}% condition)
${priceCoins} MC
BUY NOW
PLACE BUY ORDER
24H
7D
30D
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);
})();