/** * The gun in your hands: a crosshair, a shape at the bottom of the screen * that kicks when you fire, and a marker when a shot of yours lands. * * The client never decides whether it hit anything — it reads that off the * shots the server sent, the same ones the top-down view draws. */ /** `owed` is how many kicks have been shown ahead of the server saying a shot happened. */ export const makeGun = () => ({ kick: 0, mark: 0, ammo: null, owed: 0 }); /** Recoil and the hit marker both fade the same way: fast, and to zero. */ export const fade = (value, dt, life) => Math.max(0, value - dt / life); /** A shot happened when the magazine went down, never when it was reloaded. */ export const firedNow = (before, ammo, reloading) => before !== null && !reloading && ammo < before; /** One of my shots landed: it started where I am standing and the server says it hit. */ export function hitMarked(shots, me) { for (const shot of shots) { if (!shot.hit) continue; if (Math.hypot(shot.x1 - me.x, shot.y1 - me.y) < 30) return true; } return false; } /** * The trigger was pulled here, now. If the gun can fire — alive, loaded, not * reloading — it kicks at once rather than a round trip later, and the kick * the server's snapshot would have given for the same shot is owed and not * shown twice. Whether the shot lands, and whether it even happened, is the * server's alone: a kick is the hand, not the bullet. */ export function gunFire(gun, snap) { if (!snap.me.alive || snap.me.reloading || snap.me.ammo <= 0) return false; if (gun.kick > 0.5) return false; // a held trigger, not a new pull gun.kick = 1; gun.owed += 1; return true; } /** Fold a fresh snapshot into the gun's state: kick on a shot, mark on a hit. */ export function gunSnapshot(gun, snap) { if (firedNow(gun.ammo, snap.me.ammo, snap.me.reloading)) { if (gun.owed > 0) gun.owed -= 1; else gun.kick = 1; } // A kick owed for longer than a shot could take to come back was refused. if (gun.owed > 0 && gun.kick === 0) gun.owed = 0; if (snap.me.alive && hitMarked(snap.shots, snap.me)) gun.mark = 1; gun.ammo = snap.me.ammo; } export function gunTick(gun, dt) { gun.kick = fade(gun.kick, dt, 0.12); gun.mark = fade(gun.mark, dt, 0.35); } /** The crosshair, the weapon and the marker, in that order of importance. */ export function drawGun(ctx, gun, width, height, snap) { const cx = width / 2; const cy = height / 2; const gap = 6 + gun.kick * 10; const arm = 9; ctx.lineWidth = 2; ctx.strokeStyle = "rgba(20,24,30,0.8)"; ctx.beginPath(); for (const [dx, dy] of [ [1, 0], [-1, 0], [0, 1], [0, -1], ]) { ctx.moveTo(cx + dx * gap, cy + dy * gap); ctx.lineTo(cx + dx * (gap + arm), cy + dy * (gap + arm)); } ctx.stroke(); ctx.strokeStyle = snap.me.alive ? "#8ef0c4" : "#888"; ctx.lineWidth = 1.4; ctx.stroke(); if (gun.mark > 0) { ctx.strokeStyle = `rgba(255,120,110,${gun.mark})`; ctx.lineWidth = 2.5; ctx.beginPath(); for (const [dx, dy] of [ [1, 1], [-1, -1], [1, -1], [-1, 1], ]) { ctx.moveTo(cx + dx * 7, cy + dy * 7); ctx.lineTo(cx + dx * 15, cy + dy * 15); } ctx.stroke(); } if (!snap.me.alive) return; // The weapon: a barrel and a body, off to the right, rising with the kick. const scale = height / 570; const lift = gun.kick * 22 * scale + (snap.me.reloading ? 40 * scale : 0); const bx = width * 0.66; const by = height + lift; ctx.fillStyle = "#23262e"; ctx.beginPath(); ctx.moveTo(bx - 34 * scale, by); ctx.lineTo(bx - 20 * scale, by - 130 * scale); ctx.lineTo(bx + 16 * scale, by - 150 * scale); ctx.lineTo(bx + 30 * scale, by - 40 * scale); ctx.lineTo(bx + 60 * scale, by); ctx.closePath(); ctx.fill(); ctx.fillStyle = "#3b414e"; ctx.fillRect(bx - 8 * scale, by - 160 * scale, 22 * scale, 26 * scale); if (gun.kick > 0.35) { ctx.fillStyle = `rgba(255,220,140,${gun.kick})`; ctx.beginPath(); ctx.arc(bx + 3 * scale, by - 162 * scale, 18 * scale * gun.kick, 0, 7); ctx.fill(); } }