Send authoritative multiplayer inputs from browser

This commit is contained in:
Admin 2026-09-21 01:52:50 +00:00
parent 7cf6103544
commit 909abb1487

View File

@ -8,12 +8,15 @@ const KEYCLOAK_CONFIG = {
clientId: 'airship-web', clientId: 'airship-web',
}; };
const clamp = (value, min, max) => Math.max(min, Math.min(max, value));
const keycloak = new Keycloak(KEYCLOAK_CONFIG); const keycloak = new Keycloak(KEYCLOAK_CONFIG);
let socket = null; let socket = null;
let selfId = null; let selfId = null;
let reconnectTimer = null; let reconnectTimer = null;
let authenticated = false; let authenticated = false;
let playerCount = 0; let playerCount = 0;
let inputSequence = 0;
const statusButton = document.createElement('button'); const statusButton = document.createElement('button');
statusButton.id = 'multiplayer-status'; statusButton.id = 'multiplayer-status';
@ -30,7 +33,7 @@ style.textContent = `
left: max(10px, env(safe-area-inset-left)); left: max(10px, env(safe-area-inset-left));
z-index: 410; z-index: 410;
min-height: 38px; min-height: 38px;
max-width: 180px; max-width: 190px;
padding: 7px 11px; padding: 7px 11px;
border: 1px solid rgba(220, 168, 79, .72); border: 1px solid rgba(220, 168, 79, .72);
border-radius: 999px; border-radius: 999px;
@ -95,6 +98,7 @@ const connect = async () => {
socket = ws; socket = ws;
ws.addEventListener('open', () => { ws.addEventListener('open', () => {
inputSequence = 0;
setStatus('online', 'MULTIPLAYER • ONLINE'); setStatus('online', 'MULTIPLAYER • ONLINE');
emitConnection('online'); emitConnection('online');
}); });
@ -110,13 +114,24 @@ const connect = async () => {
} }
if (message.type === 'snapshot') { if (message.type === 'snapshot') {
const players = Array.isArray(message.players) const allPlayers = Array.isArray(message.players)
? message.players.filter((player) => player?.id && player.id !== selfId) ? message.players.filter((player) => player?.id)
: []; : [];
playerCount = players.length + 1; const self = allPlayers.find((player) => player.id === selfId) || null;
setStatus('online', `MULTIPLAYER • ${playerCount}`); const players = allPlayers.filter((player) => player.id !== selfId);
const projectiles = Array.isArray(message.projectiles)
? message.projectiles.filter((shot) => shot?.id)
: [];
playerCount = allPlayers.length;
const score = self ? ` • ${self.kills || 0}/${self.deaths || 0}` : '';
setStatus('online', `MULTIPLAYER • ${playerCount}${score}`);
window.dispatchEvent(new CustomEvent('airship:multiplayer-snapshot', { window.dispatchEvent(new CustomEvent('airship:multiplayer-snapshot', {
detail: { players, serverTime: message.serverTime || 0 }, detail: {
self,
players,
projectiles,
serverTime: message.serverTime || 0,
},
})); }));
} }
}); });
@ -134,15 +149,24 @@ const connect = async () => {
}); });
}; };
const sendState = (state) => { const sendInput = (input = {}) => {
if (socket?.readyState !== WebSocket.OPEN || !state) return false; if (socket?.readyState !== WebSocket.OPEN) return 0;
inputSequence += 1;
socket.send(JSON.stringify({ socket.send(JSON.stringify({
type: 'state', type: 'input',
x: Number(state.x) || 0, seq: inputSequence,
y: Number(state.y) || 0, throttle: clamp(Number(input.throttle) || 0, -1, 1),
angle: Number(state.angle) || 0, steer: clamp(Number(input.steer) || 0, -1, 1),
hull: Math.max(0, Math.round(Number(state.hull) || 0)), stop: Boolean(input.stop),
docked: Boolean(input.docked),
})); }));
return inputSequence;
};
const fire = (side) => {
if (socket?.readyState !== WebSocket.OPEN) return false;
const normalized = side === 'port' || side === -1 ? 'port' : 'starboard';
socket.send(JSON.stringify({ type: 'fire', side: normalized }));
return true; return true;
}; };
@ -158,10 +182,12 @@ statusButton.addEventListener('click', () => {
}); });
window.airshipMultiplayer = { window.airshipMultiplayer = {
sendState, sendInput,
fire,
login, login,
logout, logout,
connect, connect,
getSelfId: () => selfId,
isAuthenticated: () => authenticated, isAuthenticated: () => authenticated,
isConnected: () => socket?.readyState === WebSocket.OPEN, isConnected: () => socket?.readyState === WebSocket.OPEN,
}; };