|
<!DOCTYPE html> |
|
<html lang="en"> |
|
<head> |
|
<meta charset="UTF-8"> |
|
<meta name="viewport" content="width=device-width, initial-scale=1.0"> |
|
<title>AI Game Multiverse</title> |
|
<style> |
|
body { |
|
font-family: Arial, sans-serif; |
|
margin: 0; |
|
padding: 0; |
|
background-color: #121212; |
|
color: #e0e0e0; |
|
display: flex; |
|
flex-direction: column; |
|
align-items: center; |
|
user-select: none; |
|
-webkit-user-select: none; |
|
-moz-user-select: none; |
|
-ms-user-select: none; |
|
overflow-x: hidden; |
|
} |
|
|
|
.container { |
|
width: 100%; |
|
max-width: 100%; |
|
display: flex; |
|
flex-direction: column; |
|
align-items: center; |
|
} |
|
|
|
.game-area { |
|
display: flex; |
|
flex-direction: column; |
|
align-items: center; |
|
width: 100%; |
|
max-height: 85vh; |
|
margin: 0; |
|
position: relative; |
|
} |
|
|
|
#mouse-tracking-area { |
|
position: relative; |
|
width: 100%; |
|
height: auto; |
|
cursor: pointer; |
|
display: flex; |
|
justify-content: center; |
|
align-items: center; |
|
max-height: 85vh; |
|
} |
|
|
|
#game-canvas { |
|
width: 100%; |
|
height: auto; |
|
max-height: 85vh; |
|
object-fit: contain; |
|
background-color: #000; |
|
pointer-events: none; |
|
-webkit-user-drag: none; |
|
-khtml-user-drag: none; |
|
-moz-user-drag: none; |
|
-o-user-drag: none; |
|
user-drag: none; |
|
} |
|
|
|
.controls { |
|
display: flex; |
|
justify-content: space-between; |
|
width: 100%; |
|
max-width: 1200px; |
|
padding: 10px; |
|
background-color: rgba(0, 0, 0, 0.5); |
|
position: absolute; |
|
bottom: 0; |
|
z-index: 10; |
|
box-sizing: border-box; |
|
} |
|
|
|
.panels-container { |
|
display: flex; |
|
width: 100%; |
|
max-width: 1200px; |
|
margin: 10px auto; |
|
gap: 10px; |
|
} |
|
|
|
.panel { |
|
flex: 1; |
|
background-color: #1E1E1E; |
|
border-radius: 5px; |
|
overflow: hidden; |
|
box-shadow: 0 2px 5px rgba(0, 0, 0, 0.2); |
|
transition: height 0.3s ease; |
|
} |
|
|
|
.panel-header { |
|
background-color: #272727; |
|
padding: 10px 15px; |
|
display: flex; |
|
justify-content: space-between; |
|
align-items: center; |
|
cursor: pointer; |
|
} |
|
|
|
.panel-title { |
|
font-weight: bold; |
|
color: #4CAF50; |
|
} |
|
|
|
.toggle-button { |
|
background: none; |
|
border: none; |
|
color: #e0e0e0; |
|
font-size: 18px; |
|
cursor: pointer; |
|
} |
|
|
|
.toggle-button:focus { |
|
outline: none; |
|
} |
|
|
|
.panel-content { |
|
padding: 15px; |
|
max-height: 300px; |
|
overflow-y: auto; |
|
transition: all 0.3s ease; |
|
} |
|
|
|
.collapsed .panel-content { |
|
max-height: 0; |
|
padding-top: 0; |
|
padding-bottom: 0; |
|
overflow: hidden; |
|
} |
|
|
|
button { |
|
background-color: #4CAF50; |
|
color: white; |
|
border: none; |
|
padding: 10px 15px; |
|
text-align: center; |
|
text-decoration: none; |
|
display: inline-block; |
|
font-size: 14px; |
|
border-radius: 5px; |
|
cursor: pointer; |
|
margin: 5px; |
|
transition: background-color 0.3s; |
|
} |
|
|
|
button:hover { |
|
background-color: #45a049; |
|
} |
|
|
|
button:disabled { |
|
background-color: #cccccc; |
|
cursor: not-allowed; |
|
} |
|
|
|
select { |
|
padding: 10px; |
|
border-radius: 5px; |
|
background-color: #2A2A2A; |
|
color: #e0e0e0; |
|
border: 1px solid #4CAF50; |
|
} |
|
|
|
.status { |
|
margin-top: 10px; |
|
color: #4CAF50; |
|
} |
|
|
|
.key-indicators { |
|
display: flex; |
|
justify-content: center; |
|
margin-top: 15px; |
|
} |
|
|
|
.key { |
|
width: 40px; |
|
height: 40px; |
|
margin: 0 5px; |
|
background-color: #2A2A2A; |
|
border: 1px solid #444; |
|
border-radius: 5px; |
|
display: flex; |
|
justify-content: center; |
|
align-items: center; |
|
font-weight: bold; |
|
transition: background-color 0.2s; |
|
} |
|
|
|
.key.active { |
|
background-color: #4CAF50; |
|
color: white; |
|
} |
|
|
|
.key-row { |
|
display: flex; |
|
justify-content: center; |
|
margin: 5px 0; |
|
} |
|
|
|
.spacebar { |
|
width: 150px; |
|
} |
|
|
|
.connection-info { |
|
font-family: monospace; |
|
height: 100%; |
|
overflow-y: auto; |
|
} |
|
|
|
.log-entry { |
|
margin: 5px 0; |
|
padding: 3px; |
|
border-bottom: 1px solid #333; |
|
} |
|
|
|
.fps-counter { |
|
position: absolute; |
|
top: 10px; |
|
right: 10px; |
|
background-color: rgba(0,0,0,0.5); |
|
color: #4CAF50; |
|
padding: 5px; |
|
border-radius: 3px; |
|
font-family: monospace; |
|
z-index: 20; |
|
} |
|
|
|
#mouse-position { |
|
position: absolute; |
|
top: 10px; |
|
left: 10px; |
|
background-color: rgba(0,0,0,0.5); |
|
color: #4CAF50; |
|
padding: 5px; |
|
border-radius: 3px; |
|
font-family: monospace; |
|
z-index: 20; |
|
} |
|
|
|
@media (max-width: 768px) { |
|
.panels-container { |
|
flex-direction: column; |
|
} |
|
} |
|
|
|
.header { |
|
text-align: center; |
|
padding: 15px; |
|
margin-bottom: 20px; |
|
} |
|
|
|
.header h1 { |
|
margin: 0; |
|
color: #4CAF50; |
|
font-size: 2rem; |
|
} |
|
|
|
.header p { |
|
margin-top: 5px; |
|
color: #aaa; |
|
} |
|
</style> |
|
</head> |
|
<body> |
|
<div class="header"> |
|
<h1>AI Game Multiverse</h1> |
|
<p>Play procedurally generated games using AI</p> |
|
</div> |
|
|
|
<div class="container"> |
|
<div class="game-area"> |
|
<div id="mouse-tracking-area"> |
|
<img id="game-canvas" src="data:image/png;base64,iVBORw0KGgoAAAANSUhEUgAAAAEAAAABCAQAAAC1HAwCAAAAC0lEQVR42mNk+A8AAQUBAScY42YAAAAASUVORK5CYII=" alt="Game Frame"> |
|
<div id="mouse-position">Mouse: 0.00, 0.00</div> |
|
<div class="fps-counter" id="fps-counter">FPS: 0</div> |
|
</div> |
|
|
|
<div class="controls"> |
|
<button id="connect-btn">Connect</button> |
|
<button id="start-stream-btn" disabled>Start Stream</button> |
|
<button id="stop-stream-btn" disabled>Stop Stream</button> |
|
<select id="scene-select" disabled> |
|
<option value="forest">Forest</option> |
|
<option value="desert">Desert</option> |
|
<option value="beach">Beach</option> |
|
<option value="hills">Hills</option> |
|
<option value="river">River</option> |
|
<option value="plain">Plain</option> |
|
</select> |
|
</div> |
|
</div> |
|
|
|
<div class="panels-container"> |
|
|
|
<div class="panel" id="controls-panel"> |
|
<div class="panel-header" onclick="togglePanel('controls-panel')"> |
|
<div class="panel-title">Keyboard Controls</div> |
|
<button class="toggle-button">−</button> |
|
</div> |
|
<div class="panel-content"> |
|
<div class="key-indicators"> |
|
<div class="key-row"> |
|
<div id="key-w" class="key">W</div> |
|
</div> |
|
<div class="key-row"> |
|
<div id="key-a" class="key">A</div> |
|
<div id="key-s" class="key">S</div> |
|
<div id="key-d" class="key">D</div> |
|
</div> |
|
<div class="key-row"> |
|
<div id="key-space" class="key spacebar">SPACE</div> |
|
</div> |
|
<div class="key-row"> |
|
<div id="key-shift" class="key">SHIFT</div> |
|
</div> |
|
</div> |
|
<p class="status"> |
|
W or ↑ = Forward, S or ↓ = Back, A or ← = Left, D or → = Right<br> |
|
Space = Jump, Shift = Attack<br> |
|
Click on game view to capture mouse (ESC to release)<br> |
|
Mouse = Look around |
|
</p> |
|
</div> |
|
</div> |
|
|
|
|
|
<div class="panel" id="log-panel"> |
|
<div class="panel-header" onclick="togglePanel('log-panel')"> |
|
<div class="panel-title">Connection Log</div> |
|
<button class="toggle-button">−</button> |
|
</div> |
|
<div class="panel-content"> |
|
<div class="connection-info" id="connection-log"> |
|
<div class="log-entry">Welcome to AI Game Multiverse. Click Connect to begin.</div> |
|
</div> |
|
</div> |
|
</div> |
|
</div> |
|
</div> |
|
|
|
<script> |
|
|
|
let socket = null; |
|
let userId = null; |
|
let isStreaming = false; |
|
let lastFrameTime = 0; |
|
let frameCount = 0; |
|
let fpsUpdateInterval = null; |
|
|
|
|
|
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'); |
|
|
|
|
|
const pointerLockSupported = 'pointerLockElement' in document || |
|
'mozPointerLockElement' in document || |
|
'webkitPointerLockElement' in document; |
|
|
|
|
|
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') |
|
}; |
|
|
|
|
|
const keyToAction = { |
|
'w': 'forward', |
|
'arrowup': 'forward', |
|
'a': 'left', |
|
'arrowleft': 'left', |
|
's': 'back', |
|
'arrowdown': 'back', |
|
'd': 'right', |
|
'arrowright': 'right', |
|
' ': 'jump', |
|
'shift': 'attack' |
|
}; |
|
|
|
|
|
const keyState = { |
|
'forward': false, |
|
'back': false, |
|
'left': false, |
|
'right': false, |
|
'jump': false, |
|
'attack': false |
|
}; |
|
|
|
|
|
const mouseState = { |
|
x: 0, |
|
y: 0, |
|
captured: false |
|
}; |
|
|
|
|
|
async function testServerConnectivity() { |
|
try { |
|
|
|
const basePath = window.location.pathname.replace(/\/+$/, ''); |
|
|
|
|
|
const response = await fetch(`${window.location.protocol}//${window.location.host}${basePath}/api/debug`); |
|
if (!response.ok) { |
|
throw new Error(`Server returned ${response.status}`); |
|
} |
|
|
|
const debugInfo = await response.json(); |
|
logMessage(`Server connection test successful! Server time: ${new Date(debugInfo.server_time * 1000).toLocaleTimeString()}`); |
|
|
|
|
|
if (debugInfo.all_routes && debugInfo.all_routes.length > 0) { |
|
logMessage(`Available routes: ${debugInfo.all_routes.join(', ')}`); |
|
} |
|
|
|
|
|
return debugInfo; |
|
} catch (error) { |
|
logMessage(`Server connection test failed: ${error.message}`); |
|
return null; |
|
} |
|
} |
|
|
|
|
|
async function connectWebSocket() { |
|
|
|
logMessage('Testing server connectivity...'); |
|
const debugInfo = await testServerConnectivity(); |
|
|
|
|
|
const protocol = window.location.protocol === 'https:' ? 'wss:' : 'ws:'; |
|
|
|
|
|
const basePath = window.location.pathname.replace(/\/+$/, ''); |
|
|
|
|
|
let serverUrl = `${protocol}//${window.location.hostname}${window.location.port ? ':' + window.location.port : ''}${basePath}/ws`; |
|
logMessage(`Attempting to connect to WebSocket at ${serverUrl}...`); |
|
|
|
|
|
const fallbackUrl = `${protocol}//${window.location.hostname}${window.location.port ? ':' + window.location.port : ''}/ws`; |
|
|
|
try { |
|
socket = new WebSocket(serverUrl); |
|
setupWebSocketHandlers(); |
|
|
|
|
|
setTimeout(() => { |
|
if (socket.readyState !== WebSocket.OPEN && socket.readyState !== WebSocket.CONNECTING) { |
|
logMessage(`Connection to ${serverUrl} failed. Trying fallback URL: ${fallbackUrl}`); |
|
socket = new WebSocket(fallbackUrl); |
|
setupWebSocketHandlers(); |
|
} |
|
}, 3000); |
|
} catch (error) { |
|
logMessage(`Error connecting to WebSocket: ${error.message}`); |
|
resetUI(); |
|
} |
|
} |
|
|
|
|
|
function setupWebSocketHandlers() { |
|
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}`); |
|
|
|
|
|
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': |
|
|
|
processFrame(message); |
|
break; |
|
|
|
case 'start_stream': |
|
if (message.success) { |
|
isStreaming = true; |
|
startStreamBtn.disabled = true; |
|
stopStreamBtn.disabled = false; |
|
logMessage(`Streaming started: ${message.message}`); |
|
|
|
|
|
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'); |
|
|
|
|
|
stopFpsCounter(); |
|
} else { |
|
logMessage(`Error stopping stream: ${message.error}`); |
|
} |
|
break; |
|
|
|
case 'pong': |
|
|
|
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 = (event) => { |
|
logMessage(`WebSocket connection closed (code: ${event.code}, reason: ${event.reason || 'none given'})`); |
|
resetUI(); |
|
}; |
|
|
|
socket.onerror = (error) => { |
|
logMessage(`WebSocket error. This is often caused by CORS issues or the server being inaccessible.`); |
|
console.error('WebSocket error:', error); |
|
resetUI(); |
|
}; |
|
} |
|
|
|
|
|
function disconnectWebSocket() { |
|
if (socket && socket.readyState === WebSocket.OPEN) { |
|
|
|
if (isStreaming) { |
|
sendStopStream(); |
|
} |
|
|
|
|
|
socket.close(); |
|
logMessage('Disconnected from server'); |
|
} |
|
} |
|
|
|
|
|
function sendStartStream() { |
|
if (socket && socket.readyState === WebSocket.OPEN) { |
|
socket.send(JSON.stringify({ |
|
action: 'start_stream', |
|
requestId: generateRequestId(), |
|
fps: 16 |
|
})); |
|
} |
|
} |
|
|
|
|
|
function sendStopStream() { |
|
if (socket && socket.readyState === WebSocket.OPEN) { |
|
socket.send(JSON.stringify({ |
|
action: 'stop_stream', |
|
requestId: generateRequestId() |
|
})); |
|
} |
|
} |
|
|
|
|
|
function sendKeyboardInput(key, pressed) { |
|
if (socket && socket.readyState === WebSocket.OPEN) { |
|
socket.send(JSON.stringify({ |
|
action: 'keyboard_input', |
|
requestId: generateRequestId(), |
|
key: key, |
|
pressed: pressed |
|
})); |
|
} |
|
} |
|
|
|
|
|
function sendMouseInput(x, y) { |
|
if (socket && socket.readyState === WebSocket.OPEN && isStreaming) { |
|
socket.send(JSON.stringify({ |
|
action: 'mouse_input', |
|
requestId: generateRequestId(), |
|
x: x, |
|
y: y |
|
})); |
|
} |
|
} |
|
|
|
|
|
function sendChangeScene(scene) { |
|
if (socket && socket.readyState === WebSocket.OPEN) { |
|
socket.send(JSON.stringify({ |
|
action: 'change_scene', |
|
requestId: generateRequestId(), |
|
scene: scene |
|
})); |
|
} |
|
} |
|
|
|
|
|
function processFrame(message) { |
|
|
|
const now = performance.now(); |
|
if (lastFrameTime > 0) { |
|
frameCount++; |
|
} |
|
lastFrameTime = now; |
|
|
|
|
|
if (message.frameData) { |
|
gameCanvas.src = `data:image/jpeg;base64,${message.frameData}`; |
|
} |
|
} |
|
|
|
|
|
function generateRequestId() { |
|
return Math.random().toString(36).substring(2, 15); |
|
} |
|
|
|
|
|
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; |
|
|
|
|
|
while (connectionLog.children.length > 100) { |
|
connectionLog.removeChild(connectionLog.firstChild); |
|
} |
|
} |
|
|
|
|
|
function startFpsCounter() { |
|
frameCount = 0; |
|
lastFrameTime = 0; |
|
|
|
|
|
fpsUpdateInterval = setInterval(() => { |
|
fpsCounter.textContent = `FPS: ${frameCount}`; |
|
frameCount = 0; |
|
}, 1000); |
|
} |
|
|
|
|
|
function stopFpsCounter() { |
|
if (fpsUpdateInterval) { |
|
clearInterval(fpsUpdateInterval); |
|
fpsUpdateInterval = null; |
|
} |
|
fpsCounter.textContent = 'FPS: 0'; |
|
} |
|
|
|
|
|
function resetUI() { |
|
connectBtn.textContent = 'Connect'; |
|
startStreamBtn.disabled = true; |
|
stopStreamBtn.disabled = true; |
|
sceneSelect.disabled = true; |
|
|
|
|
|
for (const key in keyElements) { |
|
keyElements[key].classList.remove('active'); |
|
} |
|
|
|
|
|
stopFpsCounter(); |
|
|
|
|
|
isStreaming = false; |
|
} |
|
|
|
|
|
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); |
|
}); |
|
|
|
|
|
document.addEventListener('keydown', (event) => { |
|
const key = event.key.toLowerCase(); |
|
|
|
|
|
let action = keyToAction[key]; |
|
if (!action && key === ' ') { |
|
action = keyToAction[' ']; |
|
} |
|
|
|
if (action && !keyState[action]) { |
|
keyState[action] = true; |
|
|
|
|
|
const keyElement = keyElements[key] || |
|
(key === ' ' ? keyElements['space'] : null) || |
|
(key === 'shift' ? keyElements['shift'] : null); |
|
|
|
if (keyElement) { |
|
keyElement.classList.add('active'); |
|
} |
|
|
|
|
|
sendKeyboardInput(action, true); |
|
} |
|
|
|
|
|
if (Object.keys(keyToAction).includes(key) || key === ' ') { |
|
event.preventDefault(); |
|
} |
|
}); |
|
|
|
document.addEventListener('keyup', (event) => { |
|
const key = event.key.toLowerCase(); |
|
|
|
|
|
let action = keyToAction[key]; |
|
if (!action && key === ' ') { |
|
action = keyToAction[' ']; |
|
} |
|
|
|
if (action && keyState[action]) { |
|
keyState[action] = false; |
|
|
|
|
|
const keyElement = keyElements[key] || |
|
(key === ' ' ? keyElements['space'] : null) || |
|
(key === 'shift' ? keyElements['shift'] : null); |
|
|
|
if (keyElement) { |
|
keyElement.classList.remove('active'); |
|
} |
|
|
|
|
|
sendKeyboardInput(action, false); |
|
} |
|
}); |
|
|
|
|
|
function requestPointerLock() { |
|
if (!mouseState.captured && pointerLockSupported) { |
|
mouseTrackingArea.requestPointerLock = mouseTrackingArea.requestPointerLock || |
|
mouseTrackingArea.mozRequestPointerLock || |
|
mouseTrackingArea.webkitRequestPointerLock; |
|
mouseTrackingArea.requestPointerLock(); |
|
logMessage('Mouse captured. Press ESC to release.'); |
|
} |
|
} |
|
|
|
function exitPointerLock() { |
|
if (mouseState.captured) { |
|
document.exitPointerLock = document.exitPointerLock || |
|
document.mozExitPointerLock || |
|
document.webkitExitPointerLock; |
|
document.exitPointerLock(); |
|
logMessage('Mouse released.'); |
|
} |
|
} |
|
|
|
|
|
document.addEventListener('pointerlockchange', pointerLockChangeHandler); |
|
document.addEventListener('mozpointerlockchange', pointerLockChangeHandler); |
|
document.addEventListener('webkitpointerlockchange', pointerLockChangeHandler); |
|
|
|
function pointerLockChangeHandler() { |
|
if (document.pointerLockElement === mouseTrackingArea || |
|
document.mozPointerLockElement === mouseTrackingArea || |
|
document.webkitPointerLockElement === mouseTrackingArea) { |
|
|
|
mouseState.captured = true; |
|
document.addEventListener('mousemove', handleMouseMovement); |
|
} else { |
|
|
|
mouseState.captured = false; |
|
document.removeEventListener('mousemove', handleMouseMovement); |
|
|
|
mouseState.x = 0; |
|
mouseState.y = 0; |
|
mousePosition.textContent = `Mouse: ${mouseState.x.toFixed(2)}, ${mouseState.y.toFixed(2)}`; |
|
throttledSendMouseInput(); |
|
} |
|
} |
|
|
|
|
|
function handleMouseMovement(event) { |
|
if (mouseState.captured) { |
|
|
|
const sensitivity = 0.005; |
|
mouseState.x += event.movementX * sensitivity; |
|
mouseState.y -= event.movementY * sensitivity; |
|
|
|
|
|
mouseState.x = Math.max(-1, Math.min(1, mouseState.x)); |
|
mouseState.y = Math.max(-1, Math.min(1, mouseState.y)); |
|
|
|
|
|
mousePosition.textContent = `Mouse: ${mouseState.x.toFixed(2)}, ${mouseState.y.toFixed(2)}`; |
|
|
|
|
|
throttledSendMouseInput(); |
|
} |
|
} |
|
|
|
|
|
mouseTrackingArea.addEventListener('click', () => { |
|
if (!mouseState.captured && isStreaming) { |
|
requestPointerLock(); |
|
} |
|
}); |
|
|
|
|
|
mouseTrackingArea.addEventListener('mousemove', (event) => { |
|
if (!mouseState.captured) { |
|
|
|
const rect = mouseTrackingArea.getBoundingClientRect(); |
|
const centerX = rect.width / 2; |
|
const centerY = rect.height / 2; |
|
|
|
|
|
const relX = (event.clientX - rect.left - centerX) / centerX; |
|
const relY = (event.clientY - rect.top - centerY) / centerY; |
|
|
|
|
|
const scaleFactor = 0.05; |
|
mouseState.x = relX * scaleFactor; |
|
mouseState.y = -relY * scaleFactor; |
|
|
|
|
|
mousePosition.textContent = `Mouse: ${mouseState.x.toFixed(2)}, ${mouseState.y.toFixed(2)}`; |
|
|
|
|
|
throttledSendMouseInput(); |
|
} |
|
}); |
|
|
|
|
|
const throttledSendMouseInput = (() => { |
|
let lastSentTime = 0; |
|
const interval = 50; |
|
|
|
return () => { |
|
const now = performance.now(); |
|
if (now - lastSentTime >= interval) { |
|
sendMouseInput(mouseState.x, mouseState.y); |
|
lastSentTime = now; |
|
} |
|
}; |
|
})(); |
|
|
|
|
|
function togglePanel(panelId) { |
|
const panel = document.getElementById(panelId); |
|
const button = panel.querySelector('.toggle-button'); |
|
|
|
if (panel.classList.contains('collapsed')) { |
|
|
|
panel.classList.remove('collapsed'); |
|
button.textContent = '−'; |
|
} else { |
|
|
|
panel.classList.add('collapsed'); |
|
button.textContent = '+'; |
|
} |
|
} |
|
|
|
|
|
resetUI(); |
|
|
|
|
|
document.querySelectorAll('.panel-header').forEach(header => { |
|
header.addEventListener('click', () => { |
|
const panelId = header.parentElement.id; |
|
togglePanel(panelId); |
|
}); |
|
}); |
|
</script> |
|
</body> |
|
</html> |