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