multiplayer added

This commit is contained in:
catarrh committed 2026-07-31 23:15:39 +03:00
1 parent df91e0aff8
commit 50718059e0
19 files changed
+1144 -111

No files matched your search

+2 -2
View File
@@ -1,5 +1,5 @@
import {PLAYER_MAX_ENERGY, PLAYER_INITIAL_POWER, PLAYER_ENERGY_POWER_MULTIPLIER} from './constants.js?v=15';
import {randomInt} from './math.js?v=15';
import {PLAYER_MAX_ENERGY, PLAYER_INITIAL_POWER, PLAYER_ENERGY_POWER_MULTIPLIER} from './constants.js?v=20';
import {randomInt} from './math.js?v=20';
export const AI_TYPES = {
+3 -3
View File
@@ -1,6 +1,6 @@
import {splitWords} from './utils.js?v=15';
import {parable} from './math.js?v=15';
import {FONT_HEIGHT, FONT_WIDTH} from './constants.js?v=15';
import {splitWords} from './utils.js?v=20';
import {parable} from './math.js?v=20';
import {FONT_HEIGHT, FONT_WIDTH} from './constants.js?v=20';
export function createCanvas(width, height, readable) {
const canvas = document.createElement('canvas');
+1 -1
View File
@@ -1,4 +1,4 @@
import {DEFAULT_KEYPRESS_DELAY, Z} from './constants.js?v=15';
import {DEFAULT_KEYPRESS_DELAY, Z} from './constants.js?v=20';
const input = {};
let lastKeypressTime = 0;
+699 -78
View File
File diff suppressed because it is too large. Load diff
+136
View File
@@ -0,0 +1,136 @@
import {MSG, HOST_MSG} from './net/protocol.js?v=20';
let socket = null;
let myId = null;
let isHost = false;
let room = null;
let myName = '';
let handlers = [];
let statusHandlers = [];
let status = 'disconnected';
function relayUrl() {
const params = new URLSearchParams(location.search);
const override = params.get('relay');
if (override) return override;
const proto = location.protocol === 'https:' ? 'wss:' : 'ws:';
return `${proto}//${location.host}/ws`;
}
export function netStatus() {
return status;
}
export function netIsConnected() {
return status === 'connected';
}
export function netIsHost() {
return isHost;
}
export function netRoom() {
return room;
}
export function netMyId() {
return myId;
}
export function netMakeRoomCode() {
const chars = 'ABCDEFGHJKLMNPQRSTUVWXYZ23456789';
let code = '';
for (let i = 0; i < 6; i++) code += chars[Math.floor(Math.random() * chars.length)];
return code;
}
export function netOnMessage(fn) {
handlers.push(fn);
}
export function netOnStatus(fn) {
statusHandlers.push(fn);
}
function emit(message) {
for (const fn of handlers) fn(message);
}
function setStatus(value) {
status = value;
for (const fn of statusHandlers) fn(value);
}
export function netConnect(name, code) {
return new Promise((resolve, reject) => {
if (socket) {
try { socket.close(); } catch {}
socket = null;
}
myName = name;
setStatus('connecting');
let joined = false;
socket = new WebSocket(relayUrl());
socket.onopen = () => {
socket.send(JSON.stringify({type: MSG.JOIN, room: code, name}));
};
socket.onmessage = (event) => {
let msg;
try {
msg = JSON.parse(event.data);
} catch {
return;
}
if (msg.type === MSG.JOINED) {
joined = true;
myId = msg.id;
isHost = msg.host;
room = msg.room;
setStatus('connected');
resolve({id: msg.id, host: msg.host, room: msg.room});
return;
}
emit(msg);
};
socket.onerror = () => {
setStatus('disconnected');
if (!joined) reject(new Error('connection failed'));
};
socket.onclose = () => {
setStatus('disconnected');
socket = null;
myId = null;
isHost = false;
room = null;
if (!joined) reject(new Error('connection closed'));
};
});
}
export function netSendCommand(cmd) {
if (!socket || status !== 'connected') return;
socket.send(JSON.stringify({type: MSG.COMMAND, playerId: myId, cmd}));
}
export function netBroadcast(payload) {
if (!socket || status !== 'connected' || !isHost) return;
socket.send(JSON.stringify({type: MSG.BROADCAST, payload}));
}
export function netDisconnect() {
if (socket) {
try {
socket.send(JSON.stringify({type: MSG.LEAVE}));
} catch {}
try { socket.close(); } catch {}
}
socket = null;
myId = null;
isHost = false;
room = null;
setStatus('disconnected');
}
+37
View File
@@ -0,0 +1,37 @@
// Wire protocol shared between the browser client, the host and the relay.
// All messages are JSON objects with a `type` field.
export const MSG = {
// client -> relay
JOIN: 'join', // {room, name}
LEAVE: 'leave', // {}
// relay -> client
JOINED: 'joined', // {id, host, room}
ROSTER: 'roster', // {players: [{id, name}]} (host only)
HOST_LEFT: 'host-left', // {}
ERROR: 'error', // {message}
// client -> relay -> host (non-host clients only)
COMMAND: 'command', // {playerId, cmd}
// host -> relay -> other clients (verbatim payload)
BROADCAST: 'broadcast', // {payload}
};
export const CMD = {
READY: 'ready', // lobby: client is ready
UNREADY: 'unready', // lobby: client no longer ready
CONFIG: 'config', // lobby: host updates lobby config {players, rounds, terrain}
START: 'start', // lobby: host starts the game
FIRE: 'fire', // {a, p, weaponIndex}
BUY: 'buy', // {item}
SELL: 'sell', // {item}
CHAT: 'chat', // {text}
};
export const HOST_MSG = {
LOBBY: 'lobby', // {players: [{id, name, ready}], config: {players, rounds, terrain}}
WORLD: 'world', // {snap}
TERRAIN: 'terrain', // {png: base64}
EVENT: 'event', // {kind, ...}
START_ROUND: 'start-round', // {} round N begins
END_GAME: 'end-game', // {reason}
};
+3 -2
View File
@@ -1,7 +1,8 @@
import {PANEL_WIDTH, PANEL_HEIGHT, PANEL_FONT_SIZE, PANEL_TITLE_FONT_SIZE, PANEL_BG_COLOR, PANEL_BORDER_COLOR, W, H, FONT_WIDTH} from './constants.js?v=15';
import {drawRect, drawLine} from './gfx.js?v=15';
import {PANEL_WIDTH, PANEL_HEIGHT, PANEL_FONT_SIZE, PANEL_TITLE_FONT_SIZE, PANEL_BG_COLOR, PANEL_BORDER_COLOR, W, H, FONT_WIDTH} from './constants.js?v=20';
import {drawRect, drawLine} from './gfx.js?v=20';
export const PANEL_X = (W - PANEL_WIDTH) / 2;
export {PANEL_WIDTH};
const PANEL_Y = (H - PANEL_HEIGHT) / 2;
export function drawButton(ctx, x, y, w, h, label, active) {
+7 -7
View File
@@ -1,10 +1,10 @@
import {H, PROJECTILE_ITERATIONS_PER_FRAME, PROJECTILE_ITERATION_PROGRESS, PROJECTILE_MAX_SOUND_FREQUENCY, PROJECTILE_MIN_SOUND_FREQUENCY, PROJECTILE_POWER_REDUCTION_FACTOR, PROJECTILE_WIND_REDUCTION_FACTOR, WEAPON_TYPES, PLAYER_TANK_Y_FOOTPRINT} from './constants.js?v=15';
import {checkLineWith, drawLineVirtual} from './gfx.js?v=15';
import {createParticles, isTank, isTankShield, spawnNapalm, tracerMode} from './main.js?v=15';
import {deg2rad, parable, rad2deg, wrap} from './math.js?v=15';
import {audio, createOsc} from './sound.js?v=15';
import {isTerrain, landHeight} from './terrain.js?v=15';
import {EXPLOSION_TYPES} from './weapons.js?v=15';
import {H, PROJECTILE_ITERATIONS_PER_FRAME, PROJECTILE_ITERATION_PROGRESS, PROJECTILE_MAX_SOUND_FREQUENCY, PROJECTILE_MIN_SOUND_FREQUENCY, PROJECTILE_POWER_REDUCTION_FACTOR, PROJECTILE_WIND_REDUCTION_FACTOR, WEAPON_TYPES, PLAYER_TANK_Y_FOOTPRINT} from './constants.js?v=20';
import {checkLineWith, drawLineVirtual} from './gfx.js?v=20';
import {createParticles, isTank, isTankShield, spawnNapalm, tracerMode} from './main.js?v=20';
import {deg2rad, parable, rad2deg, wrap} from './math.js?v=20';
import {audio, createOsc} from './sound.js?v=20';
import {isTerrain, landHeight} from './terrain.js?v=20';
import {EXPLOSION_TYPES} from './weapons.js?v=20';
export const PROJECTILE_TYPES = {
+4 -4
View File
@@ -1,7 +1,7 @@
import {SKY_COLORS} from './constants.js?v=15';
import {drawRect} from './gfx.js?v=15';
import {gradient} from './math.js?v=15';
import {sample} from './utils.js?v=15';
import {SKY_COLORS} from './constants.js?v=20';
import {drawRect} from './gfx.js?v=20';
import {gradient} from './math.js?v=20';
import {sample} from './utils.js?v=20';
export function generateSky(ctx) {
const {width, height} = ctx.canvas;
+1 -1
View File
@@ -1,4 +1,4 @@
import {SOUND_MUSIC_VOLUME, SOUND_SFX_VOLUME} from './constants.js?v=15';
import {SOUND_MUSIC_VOLUME, SOUND_SFX_VOLUME} from './constants.js?v=20';
// @ts-ignore
export const audio = new (window.AudioContext || window.webkitAudioContext)();
+3 -3
View File
@@ -1,6 +1,6 @@
import {clipCanvas, drawCircle, drawRect} from './gfx.js?v=15';
import {clamp, coords2index, random, randomInt} from './math.js?v=15';
import {sample} from './utils.js?v=15';
import {clipCanvas, drawCircle, drawRect} from './gfx.js?v=20';
import {clamp, coords2index, random, randomInt} from './math.js?v=20';
import {sample} from './utils.js?v=20';
let cachedImageData;
+6 -1
View File
@@ -1,4 +1,4 @@
import {randomInt} from './math.js?v=15';
import {randomInt} from './math.js?v=20';
export function sample(arr) {
return arr[randomInt(0, arr.length-1)];
@@ -29,6 +29,11 @@ export function nextId() {
return id;
}
export function newPlayerId() {
if (typeof crypto !== 'undefined' && crypto.randomUUID) return crypto.randomUUID();
return `p${Date.now().toString(36)}${Math.random().toString(36).slice(2, 10)}`;
}
export function splitWords(text) {
let words = [''];
for (let char of text) {
+5 -5
View File
@@ -1,8 +1,8 @@
import {PLAYER_TANK_BOUNDING_RADIUS, PLAYER_TANK_Y_FOOTPRINT, SHIELD_TYPES} from './constants.js?v=15';
import {drawCircle, plot} from './gfx.js?v=15';
import {clamp, cycle, distance, randomInt} from './math.js?v=15';
import {audio, createOsc} from './sound.js?v=15';
import {clipTerrain} from './terrain.js?v=15';
import {PLAYER_TANK_BOUNDING_RADIUS, PLAYER_TANK_Y_FOOTPRINT, SHIELD_TYPES} from './constants.js?v=20';
import {drawCircle, plot} from './gfx.js?v=20';
import {clamp, cycle, distance, randomInt} from './math.js?v=20';
import {audio, createOsc} from './sound.js?v=20';
import {clipTerrain} from './terrain.js?v=20';
export function drawExplosion(ctx, x, y, r) {
const step = Math.max(2, Math.floor(r / 6));