slight improvements to the controller

This commit is contained in:
jenz 2026-06-30 13:00:09 +01:00
parent 12b25e8821
commit 7c63796358
2 changed files with 93 additions and 17 deletions

View File

@ -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 @@
</head>
<body>
<div id="status">Connecting...</div>
<button id="sound-toggle">🔇 Unmute</button>
<div id="idle-warning">Idle — control will be released soon. Move your mouse to stay in control.</div>
<div id="queue-panel">
<h3>Controller</h3>
@ -136,17 +169,16 @@
<p>However be aware that the Ingame server browser is not usable as it is filled with fake redirect servers.</p>
</div>
<div class="faq-item">
<h3>How can i join the server?</h3>
<p>The IP is 51.195.188.106:27015.</p>
<h3>How can i join the servers?</h3>
<p>The zombie escape IP: 51.195.188.106:27015.</p>
<p>The zombie escape2 IP: 51.195.188.106:27035.</p>
<p>The zombie riot IP: 51.195.188.106:27016.</p>
<p>The minigame IP: 51.195.188.106:27017.</p>
</div>
<div class="faq-item">
<h3>Want to chat with us?</h3>
<p>We have our own self hosted stoat instance: https://unloze.com/stoat/</p>
</div>
<div class="faq-item">
<h3>Render Distance</h3>
<p>For the sake of optimizing game performance we sometimes dont render far distance.</p>
</div>
<div class="faq-item">
<h3>Opening the in-game menu</h3>
<p>Press ESC to release mouse control, then press ESC again to open the menu.</p>
@ -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 => {

View File

@ -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}`);