diff --git a/client.html b/client.html index ac5f575..2ff9fc0 100644 --- a/client.html +++ b/client.html @@ -38,6 +38,36 @@ font-size: 13px; z-index: 10; } + #idle-warning { + display: none; + position: fixed; + top: 10px; + left: 50%; + transform: translateX(-50%); + background: rgba(180,60,0,0.85); + padding: 6px 14px; + border-radius: 4px; + font-size: 13px; + z-index: 20; + } + #idle-warning.visible { display: block; } + + #sound-toggle { + position: fixed; + top: 10px; + right: 10px; + background: rgba(0,0,0,0.7); + color: #fff; + border: 1px solid #444; + padding: 6px 12px; + border-radius: 4px; + font-size: 13px; + font-family: monospace; + cursor: pointer; + z-index: 10; + } + #sound-toggle:hover { background: rgba(40,40,40,0.85); } + #sound-toggle.unmuted { color: #4fc; border-color: #4fc; } #gameCanvas { width: 1280px; height: 720px; @@ -105,6 +135,9 @@
However be aware that the Ingame server browser is not usable as it is filled with fake redirect servers.
The IP is 51.195.188.106:27015.
+The zombie escape IP: 51.195.188.106:27015.
+The zombie escape2 IP: 51.195.188.106:27035.
+The zombie riot IP: 51.195.188.106:27016.
+The minigame IP: 51.195.188.106:27017.
We have our own self hosted stoat instance: https://unloze.com/stoat/
For the sake of optimizing game performance we sometimes dont render far distance.
-Press ESC to release mouse control, then press ESC again to open the menu.
@@ -197,6 +229,8 @@ const hintEl = document.getElementById('hint'); const queueList = document.getElementById('queue-list'); const loadingEl = document.getElementById('loading-overlay'); + const idleWarnEl = document.getElementById('idle-warning'); + const soundToggleEl = document.getElementById('sound-toggle'); let sigWs = null; let pc = null; @@ -205,6 +239,7 @@ let isControlling = false; let isInQueue = false; let isWaitingForRestart = false; + let idleWarnTimer = null; function setStatus(msg) { statusEl.textContent = msg; } @@ -212,6 +247,36 @@ loadingEl.classList.toggle('visible', visible); } + // Mirrors the server's IDLE_TIMEOUT (60s); warns 15s before release so + // losing control is never a silent surprise. + const IDLE_TIMEOUT_MS = 60000; + const IDLE_WARN_MS = 15000; + function resetIdleWarning() { + idleWarnEl.classList.remove('visible'); + if (idleWarnTimer) clearTimeout(idleWarnTimer); + if (!isControlling) return; + idleWarnTimer = setTimeout(() => { + idleWarnEl.classList.add('visible'); + }, IDLE_TIMEOUT_MS - IDLE_WARN_MS); + } + + function pingActive() { + resetIdleWarning(); + if (sigWs && sigWs.readyState === WebSocket.OPEN) { + sigWs.send(JSON.stringify({ type: 'controller_active' })); + } + } + + function setMuted(muted) { + canvas.muted = muted; + soundToggleEl.textContent = muted ? '🔇 Unmute' : '🔊 Mute'; + soundToggleEl.classList.toggle('unmuted', !muted); + } + + soundToggleEl.addEventListener('click', () => { + setMuted(!canvas.muted); + }); + function updateQueuePanel(controller, queue, restarting) { let html = ''; if (restarting) { @@ -296,9 +361,10 @@ isInQueue = false; isWaitingForRestart = false; canvas.classList.replace('watching', 'controlling'); - canvas.muted = false; + setMuted(false); canvas.requestPointerLock(); hintEl.textContent = 'Mouse captured — ESC to release'; + resetIdleWarning(); } function exitControl() { @@ -308,6 +374,8 @@ canvas.classList.replace('controlling', 'watching'); hintEl.textContent = 'Click to take control'; stopMic(); + if (idleWarnTimer) clearTimeout(idleWarnTimer); + idleWarnEl.classList.remove('visible'); if (document.pointerLockElement === canvas) document.exitPointerLock(); } @@ -421,7 +489,7 @@ canvas.addEventListener('click', () => { if (isControlling) { - canvas.muted = false; + setMuted(false); canvas.requestPointerLock(); } else { requestControl(); @@ -434,9 +502,7 @@ hintEl.textContent = 'Mouse released — click to recapture or wait to release control'; } else if (pointerLocked) { hintEl.textContent = 'Mouse captured — ESC to release'; - if (sigWs && sigWs.readyState === WebSocket.OPEN) { - sigWs.send(JSON.stringify({ type: 'controller_active' })); - } + pingActive(); } }); @@ -450,7 +516,7 @@ setInterval(() => { if (pointerLocked && isControlling && sigWs && sigWs.readyState === WebSocket.OPEN) { - sigWs.send(JSON.stringify({ type: 'controller_active' })); + pingActive(); } }, 3000); @@ -461,9 +527,7 @@ if (now - lastMouseMove < 16) return; lastMouseMove = now; sendInput({ type: 'mouse_move', dx: e.movementX, dy: e.movementY }); - if (sigWs && sigWs.readyState === WebSocket.OPEN) { - sigWs.send(JSON.stringify({ type: 'controller_active' })); - } + pingActive(); }); canvas.addEventListener('mousedown', e => { diff --git a/streaming-agent/signaling-server.js b/streaming-agent/signaling-server.js index 082f9c4..516ed57 100644 --- a/streaming-agent/signaling-server.js +++ b/streaming-agent/signaling-server.js @@ -22,7 +22,7 @@ const CF_APP_ID = process.env.CF_APP_ID || ''; const CF_APP_TOKEN = process.env.CF_APP_TOKEN || ''; const CF_BASE = `https://rtc.live.cloudflare.com/v1/apps/${CF_APP_ID}`; const CONTAINER = process.env.CONTAINER_NAME || 'css-test'; -const IDLE_TIMEOUT = 10000; // 10 seconds +const IDLE_TIMEOUT = 60000; // 60 seconds const NAME_SALT = process.env.NAME_SALT || 'change-this-secret'; // How long to wait after relaunching CSS before granting control @@ -77,8 +77,10 @@ const controlQueue = []; let containerRestarting = false; function broadcastQueueState() { - const queueNames = controlQueue.map(e => e.visitorName); const controllerName = currentController ? currentController.visitorName : null; + const queueNames = controlQueue + .map(e => e.visitorName) + .filter(name => name !== controllerName); const msg = JSON.stringify({ type: 'queue_state', controller: controllerName, @@ -99,6 +101,12 @@ function resetIdleTimer(entry) { } async function applyController(entry) { + // Defensive: if this exact entry is sitting in the queue (e.g. a race + // between request_control calls), pull it out so it's never displayed + // as both controller and queued. + const qIdx = controlQueue.indexOf(entry); + if (qIdx !== -1) controlQueue.splice(qIdx, 1); + containerRestarting = true; broadcastQueueState(); @@ -287,6 +295,10 @@ wss.on('connection', ws => { if (!currentController) { applyController(entry); + } else if (currentController.visitorName === visitorName) { + // Same identity already controlling (e.g. duplicate tab) — don't queue + // behind yourself. + return; } else { controlQueue.push(entry); console.log(`[sig] ${visitorName} joined queue position ${controlQueue.length}`);