// MatrixGame WebSocket Client // WebSocket connection let socket = null; let userId = null; let isStreaming = false; let lastFrameTime = 0; let frameCount = 0; let fpsUpdateInterval = null; // DOM Elements const connectBtn = document.getElementById('connect-btn'); const startStreamBtn = document.getElementById('start-stream-btn'); const stopStreamBtn = document.getElementById('stop-stream-btn'); const sceneSelect = document.getElementById('scene-select'); const gameCanvas = document.getElementById('game-canvas'); const connectionLog = document.getElementById('connection-log'); const mousePosition = document.getElementById('mouse-position'); const fpsCounter = document.getElementById('fps-counter'); const mouseTrackingArea = document.getElementById('mouse-tracking-area'); // Keyboard DOM elements const keyElements = { 'w': document.getElementById('key-w'), 'a': document.getElementById('key-a'), 's': document.getElementById('key-s'), 'd': document.getElementById('key-d'), 'space': document.getElementById('key-space'), 'shift': document.getElementById('key-shift') }; // Key mapping to action names const keyToAction = { 'w': 'forward', 'a': 'left', 's': 'back', 'd': 'right', ' ': 'jump', 'shift': 'attack' }; // Key state tracking const keyState = { 'forward': false, 'back': false, 'left': false, 'right': false, 'jump': false, 'attack': false }; // Mouse state const mouseState = { x: 0, y: 0 }; // Connect to WebSocket server function connectWebSocket() { // Use secure WebSocket (wss://) if the page is loaded over HTTPS const protocol = window.location.protocol === 'https:' ? 'wss:' : 'ws:'; // Get base path by extracting path from the script tag's src attribute let basePath = ''; const scriptTags = document.getElementsByTagName('script'); for (const script of scriptTags) { if (script.src.includes('client.js')) { const url = new URL(script.src); basePath = url.pathname.replace('/assets/client.js', ''); break; } } const serverUrl = `${protocol}//${window.location.hostname}${window.location.port ? ':' + window.location.port : ''}${basePath}/ws`; logMessage(`Connecting to ${serverUrl}...`); socket = new WebSocket(serverUrl); socket.onopen = () => { logMessage('WebSocket connection established'); connectBtn.textContent = 'Disconnect'; startStreamBtn.disabled = false; sceneSelect.disabled = false; }; socket.onmessage = (event) => { const message = JSON.parse(event.data); switch (message.action) { case 'welcome': userId = message.userId; logMessage(`Connected with user ID: ${userId}`); // Update scene options if server provides them if (message.scenes && Array.isArray(message.scenes)) { sceneSelect.innerHTML = ''; message.scenes.forEach(scene => { const option = document.createElement('option'); option.value = scene; option.textContent = scene.charAt(0).toUpperCase() + scene.slice(1); sceneSelect.appendChild(option); }); } break; case 'frame': // Process incoming frame processFrame(message); break; case 'start_stream': if (message.success) { isStreaming = true; startStreamBtn.disabled = true; stopStreamBtn.disabled = false; logMessage(`Streaming started: ${message.message}`); // Start FPS counter startFpsCounter(); } else { logMessage(`Error starting stream: ${message.error}`); } break; case 'stop_stream': if (message.success) { isStreaming = false; startStreamBtn.disabled = false; stopStreamBtn.disabled = true; logMessage('Streaming stopped'); // Stop FPS counter stopFpsCounter(); } else { logMessage(`Error stopping stream: ${message.error}`); } break; case 'pong': // Server responded to ping break; case 'change_scene': if (message.success) { logMessage(`Scene changed to ${message.scene}`); } else { logMessage(`Error changing scene: ${message.error}`); } break; default: logMessage(`Received message: ${JSON.stringify(message)}`); } }; socket.onclose = () => { logMessage('WebSocket connection closed'); resetUI(); }; socket.onerror = (error) => { logMessage(`WebSocket error: ${error}`); resetUI(); }; } // Disconnect from WebSocket server function disconnectWebSocket() { if (socket && socket.readyState === WebSocket.OPEN) { // Stop streaming if active if (isStreaming) { sendStopStream(); } // Close the socket socket.close(); logMessage('Disconnected from server'); } } // Start streaming frames function sendStartStream() { if (socket && socket.readyState === WebSocket.OPEN) { socket.send(JSON.stringify({ action: 'start_stream', requestId: generateRequestId(), fps: 16 // Default FPS })); } } // Stop streaming frames function sendStopStream() { if (socket && socket.readyState === WebSocket.OPEN) { socket.send(JSON.stringify({ action: 'stop_stream', requestId: generateRequestId() })); } } // Send keyboard input to server function sendKeyboardInput(key, pressed) { if (socket && socket.readyState === WebSocket.OPEN) { socket.send(JSON.stringify({ action: 'keyboard_input', requestId: generateRequestId(), key: key, pressed: pressed })); } } // Send mouse input to server function sendMouseInput(x, y) { if (socket && socket.readyState === WebSocket.OPEN && isStreaming) { socket.send(JSON.stringify({ action: 'mouse_input', requestId: generateRequestId(), x: x, y: y })); } } // Change scene function sendChangeScene(scene) { if (socket && socket.readyState === WebSocket.OPEN) { socket.send(JSON.stringify({ action: 'change_scene', requestId: generateRequestId(), scene: scene })); } } // Process incoming frame function processFrame(message) { // Update FPS calculation const now = performance.now(); if (lastFrameTime > 0) { frameCount++; } lastFrameTime = now; // Update the canvas with the new frame if (message.frameData) { gameCanvas.src = `data:image/jpeg;base64,${message.frameData}`; } } // Generate a random request ID function generateRequestId() { return Math.random().toString(36).substring(2, 15); } // Log message to the connection info panel function logMessage(message) { const logEntry = document.createElement('div'); logEntry.className = 'log-entry'; const timestamp = new Date().toLocaleTimeString(); logEntry.textContent = `[${timestamp}] ${message}`; connectionLog.appendChild(logEntry); connectionLog.scrollTop = connectionLog.scrollHeight; // Limit number of log entries while (connectionLog.children.length > 100) { connectionLog.removeChild(connectionLog.firstChild); } } // Start FPS counter updates function startFpsCounter() { frameCount = 0; lastFrameTime = 0; // Update FPS display every second fpsUpdateInterval = setInterval(() => { fpsCounter.textContent = `FPS: ${frameCount}`; frameCount = 0; }, 1000); } // Stop FPS counter updates function stopFpsCounter() { if (fpsUpdateInterval) { clearInterval(fpsUpdateInterval); fpsUpdateInterval = null; } fpsCounter.textContent = 'FPS: 0'; } // Reset UI to initial state function resetUI() { connectBtn.textContent = 'Connect'; startStreamBtn.disabled = true; stopStreamBtn.disabled = true; sceneSelect.disabled = true; // Reset key indicators for (const key in keyElements) { keyElements[key].classList.remove('active'); } // Stop FPS counter stopFpsCounter(); // Reset streaming state isStreaming = false; } // Event Listeners connectBtn.addEventListener('click', () => { if (socket && socket.readyState === WebSocket.OPEN) { disconnectWebSocket(); } else { connectWebSocket(); } }); startStreamBtn.addEventListener('click', sendStartStream); stopStreamBtn.addEventListener('click', sendStopStream); sceneSelect.addEventListener('change', () => { sendChangeScene(sceneSelect.value); }); // Keyboard event listeners document.addEventListener('keydown', (event) => { const key = event.key.toLowerCase(); // Map key to action let action = keyToAction[key]; if (!action && key === ' ') { action = keyToAction[' ']; // Handle spacebar } if (action && !keyState[action]) { keyState[action] = true; // Update visual indicator const keyElement = keyElements[key] || (key === ' ' ? keyElements['space'] : null) || (key === 'shift' ? keyElements['shift'] : null); if (keyElement) { keyElement.classList.add('active'); } // Send to server sendKeyboardInput(action, true); } // Prevent default actions for game controls if (Object.keys(keyToAction).includes(key) || key === ' ') { event.preventDefault(); } }); document.addEventListener('keyup', (event) => { const key = event.key.toLowerCase(); // Map key to action let action = keyToAction[key]; if (!action && key === ' ') { action = keyToAction[' ']; // Handle spacebar } if (action && keyState[action]) { keyState[action] = false; // Update visual indicator const keyElement = keyElements[key] || (key === ' ' ? keyElements['space'] : null) || (key === 'shift' ? keyElements['shift'] : null); if (keyElement) { keyElement.classList.remove('active'); } // Send to server sendKeyboardInput(action, false); } }); // Mouse tracking mouseTrackingArea.addEventListener('mousemove', (event) => { // Calculate normalized coordinates relative to the center of the tracking area const rect = mouseTrackingArea.getBoundingClientRect(); const centerX = rect.width / 2; const centerY = rect.height / 2; // Calculate relative position from center (-1 to 1) const relX = (event.clientX - rect.left - centerX) / centerX; const relY = (event.clientY - rect.top - centerY) / centerY; // Scale down for smoother movement (similar to conditions.py) const scaleFactor = 0.05; mouseState.x = relX * scaleFactor; mouseState.y = -relY * scaleFactor; // Invert Y for intuitive camera control // Update display mousePosition.textContent = `Mouse: ${mouseState.x.toFixed(2)}, ${mouseState.y.toFixed(2)}`; // Send to server (throttled) throttledSendMouseInput(); }); // Throttle mouse movement to avoid flooding the server const throttledSendMouseInput = (() => { let lastSentTime = 0; const interval = 50; // milliseconds return () => { const now = performance.now(); if (now - lastSentTime >= interval) { sendMouseInput(mouseState.x, mouseState.y); lastSentTime = now; } }; })(); // Initialize the UI resetUI();