From 69b494d0547b411fb2faaa42979e30e59e497286 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?=D0=90=D0=BD=D1=82=D0=BE=D0=BD=20=D0=A2=D1=80=D0=BE=D1=88?= =?UTF-8?q?=D0=B8=D0=BD?= Date: Sun, 23 Aug 2026 12:15:33 +0300 Subject: [PATCH] multiplayer fixes: trajectory dedupe by seq, reset client carve state on terrain resync, traceId backfill --- src/main.js | 10 ++++++++-- src/projectiles.js | 5 +++-- src/terrain.js | 1 + 3 files changed, 12 insertions(+), 4 deletions(-) diff --git a/src/main.js b/src/main.js index a2f4fae..0505df2 100644 --- a/src/main.js +++ b/src/main.js @@ -804,7 +804,9 @@ function applyWorld(snap) { projectiles = snap.projectiles || []; explosions = (snap.explosions || []).map(e => ({...e, osc: NOOP_OSC})); if (networkMode && !netIsHost()) { - const incoming = snap.trajectories || []; + let lastSeq = 0; + for (const t of trajectories) if (t.seq > lastSeq) lastSeq = t.seq; + const incoming = (snap.trajectories || []).filter(t => t.seq > lastSeq); trajectories = [...trajectories, ...incoming].slice(-600); } else { trajectories = snap.trajectories || []; @@ -922,6 +924,9 @@ function interpolateWorld() { } function applyTerrain(grid) { + carveQueue.length = 0; + seenClientExplosions.clear(); + clientCollapseState = null; decodeTerrain(terrain, grid); } @@ -962,7 +967,7 @@ function broadcastWorld() { napalmParticles: napalmParticles.map(p => ({x: p.x, y: p.y})), fireCells: fireCells.map(f => ({x: f.x, y: f.y, timeLeft: f.timeLeft})), smokeParticles: smokeParticles.map(s => ({x: s.x, y: s.y, vx: s.vx, vy: s.vy, alpha: s.alpha, lifetime: s.lifetime})), - trajectories: trajectories.slice(-300).filter((_, i) => i % 3 === 0).map(t => ({x: t.x, y: t.y, a: t.a, c: t.c, traceId: t.traceId})), + trajectories: trajectories.slice(-300).filter((_, i) => i % 3 === 0).map(t => ({x: t.x, y: t.y, a: t.a, c: t.c, traceId: t.traceId, seq: t.seq})), }; netBroadcast({type: HOST_MSG.WORLD, snap}); } @@ -1585,6 +1590,7 @@ function update() { } else if (state === 'shoot') { + for (const p of projectiles) if (!p.traceId) p.traceId = nextTraceId++; for (let i=projectiles.length-1; i>=0; i--) { const projectile = projectiles[i]; const projectileType = PROJECTILE_TYPES[projectile.type]; diff --git a/src/projectiles.js b/src/projectiles.js index ed9dfcd..f7e19e5 100644 --- a/src/projectiles.js +++ b/src/projectiles.js @@ -6,6 +6,7 @@ import {audio, createOsc} from './sound.js?v=26'; import {isTerrain, landHeight} from './terrain.js?v=26'; import {EXPLOSION_TYPES} from './weapons.js?v=26'; +let trajectorySeq = 1; export const PROJECTILE_TYPES = { normal: { @@ -109,7 +110,7 @@ export const PROJECTILE_TYPES = { trajectory .slice(0, trajectory.length-1) - .map(x => ({...x, a:255, traceId: projectile.traceId})) + .map(x => ({...x, a:255, traceId: projectile.traceId, seq: trajectorySeq++})) .forEach(x => trajectories.push(x)); } @@ -269,7 +270,7 @@ export const PROJECTILE_TYPES = { trajectory .slice(0, trajectory.length-1) - .map(x => ({...x, a:255, traceId: projectile.traceId})) + .map(x => ({...x, a:255, traceId: projectile.traceId, seq: trajectorySeq++})) .forEach(x => trajectories.push(x)); } diff --git a/src/terrain.js b/src/terrain.js index 9b202c5..f1468c2 100644 --- a/src/terrain.js +++ b/src/terrain.js @@ -74,6 +74,7 @@ export function encodeTerrain(ctx) { } export function decodeTerrain(ctx, grid) { + if (!grid || !grid.tops || !grid.colors || !Array.isArray(grid.palette)) return; const {width, height} = ctx.canvas; const paletteRgb = grid.palette.map(hexToRgb); const tops = new Uint16Array(fromBase64(grid.tops).buffer);