rook/val Public Public: anyone can read this; nobody can change it.
forked from benjivers/val
workbenchthis machine
Files 230b84c268f985fd747740bbba16bff522b20d0b
| 1 | /** |
| 2 | * The gun in your hands: a crosshair, a shape at the bottom of the screen |
| 3 | * that kicks when you fire, and a marker when a shot of yours lands. |
| 4 | * |
| 5 | * The client never decides whether it hit anything — it reads that off the |
| 6 | * shots the server sent, the same ones the top-down view draws. |
| 7 | */ |
| 8 | |
| 9 | export const makeGun = () => ({ kick: 0, mark: 0, ammo: null }); |
| 10 | |
| 11 | /** Recoil and the hit marker both fade the same way: fast, and to zero. */ |
| 12 | export const fade = (value, dt, life) => Math.max(0, value - dt / life); |
| 13 | |
| 14 | /** A shot happened when the magazine went down, never when it was reloaded. */ |
| 15 | export const firedNow = (before, ammo, reloading) => |
| 16 | before !== null && !reloading && ammo < before; |
| 17 | |
| 18 | /** One of my shots landed: it started where I am standing and the server says it hit. */ |
| 19 | export function hitMarked(shots, me) { |
| 20 | for (const shot of shots) { |
| 21 | if (!shot.hit) continue; |
| 22 | if (Math.hypot(shot.x1 - me.x, shot.y1 - me.y) < 30) return true; |
| 23 | } |
| 24 | return false; |
| 25 | } |
| 26 | |
| 27 | /** Fold a fresh snapshot into the gun's state: kick on a shot, mark on a hit. */ |
| 28 | export function gunSnapshot(gun, snap) { |
| 29 | if (firedNow(gun.ammo, snap.me.ammo, snap.me.reloading)) gun.kick = 1; |
| 30 | if (snap.me.alive && hitMarked(snap.shots, snap.me)) gun.mark = 1; |
| 31 | gun.ammo = snap.me.ammo; |
| 32 | } |
| 33 | |
| 34 | export function gunTick(gun, dt) { |
| 35 | gun.kick = fade(gun.kick, dt, 0.12); |
| 36 | gun.mark = fade(gun.mark, dt, 0.35); |
| 37 | } |
| 38 | |
| 39 | /** The crosshair, the weapon and the marker, in that order of importance. */ |
| 40 | export function drawGun(ctx, gun, width, height, snap) { |
| 41 | const cx = width / 2; |
| 42 | const cy = height / 2; |
| 43 | const gap = 6 + gun.kick * 10; |
| 44 | const arm = 9; |
| 45 | ctx.lineWidth = 2; |
| 46 | ctx.strokeStyle = "rgba(20,24,30,0.8)"; |
| 47 | ctx.beginPath(); |
| 48 | for (const [dx, dy] of [ |
| 49 | [1, 0], |
| 50 | [-1, 0], |
| 51 | [0, 1], |
| 52 | [0, -1], |
| 53 | ]) { |
| 54 | ctx.moveTo(cx + dx * gap, cy + dy * gap); |
| 55 | ctx.lineTo(cx + dx * (gap + arm), cy + dy * (gap + arm)); |
| 56 | } |
| 57 | ctx.stroke(); |
| 58 | ctx.strokeStyle = snap.me.alive ? "#8ef0c4" : "#888"; |
| 59 | ctx.lineWidth = 1.4; |
| 60 | ctx.stroke(); |
| 61 | |
| 62 | if (gun.mark > 0) { |
| 63 | ctx.strokeStyle = `rgba(255,120,110,${gun.mark})`; |
| 64 | ctx.lineWidth = 2.5; |
| 65 | ctx.beginPath(); |
| 66 | for (const [dx, dy] of [ |
| 67 | [1, 1], |
| 68 | [-1, -1], |
| 69 | [1, -1], |
| 70 | [-1, 1], |
| 71 | ]) { |
| 72 | ctx.moveTo(cx + dx * 7, cy + dy * 7); |
| 73 | ctx.lineTo(cx + dx * 15, cy + dy * 15); |
| 74 | } |
| 75 | ctx.stroke(); |
| 76 | } |
| 77 | |
| 78 | if (!snap.me.alive) return; |
| 79 | // The weapon: a barrel and a body, off to the right, rising with the kick. |
| 80 | const scale = height / 570; |
| 81 | const lift = gun.kick * 22 * scale + (snap.me.reloading ? 40 * scale : 0); |
| 82 | const bx = width * 0.66; |
| 83 | const by = height + lift; |
| 84 | ctx.fillStyle = "#23262e"; |
| 85 | ctx.beginPath(); |
| 86 | ctx.moveTo(bx - 34 * scale, by); |
| 87 | ctx.lineTo(bx - 20 * scale, by - 130 * scale); |
| 88 | ctx.lineTo(bx + 16 * scale, by - 150 * scale); |
| 89 | ctx.lineTo(bx + 30 * scale, by - 40 * scale); |
| 90 | ctx.lineTo(bx + 60 * scale, by); |
| 91 | ctx.closePath(); |
| 92 | ctx.fill(); |
| 93 | ctx.fillStyle = "#3b414e"; |
| 94 | ctx.fillRect(bx - 8 * scale, by - 160 * scale, 22 * scale, 26 * scale); |
| 95 | if (gun.kick > 0.35) { |
| 96 | ctx.fillStyle = `rgba(255,220,140,${gun.kick})`; |
| 97 | ctx.beginPath(); |
| 98 | ctx.arc(bx + 3 * scale, by - 162 * scale, 18 * scale * gun.kick, 0, 7); |
| 99 | ctx.fill(); |
| 100 | } |
| 101 | } |