rook/val Public Public: anyone can read this; nobody can change it.
forked from benjivers/val
workbenchthis machine
Files 53c9cd79f21c24c95995672b7a3113c152df085c
| 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 | /** `owed` is how many kicks have been shown ahead of the server saying a shot happened. */ |
| 10 | export const makeGun = () => ({ kick: 0, mark: 0, ammo: null, owed: 0 }); |
| 11 | |
| 12 | /** Recoil and the hit marker both fade the same way: fast, and to zero. */ |
| 13 | export const fade = (value, dt, life) => Math.max(0, value - dt / life); |
| 14 | |
| 15 | /** A shot happened when the magazine went down, never when it was reloaded. */ |
| 16 | export const firedNow = (before, ammo, reloading) => |
| 17 | before !== null && !reloading && ammo < before; |
| 18 | |
| 19 | /** One of my shots landed: it started where I am standing and the server says it hit. */ |
| 20 | export function hitMarked(shots, me) { |
| 21 | for (const shot of shots) { |
| 22 | if (!shot.hit) continue; |
| 23 | if (Math.hypot(shot.x1 - me.x, shot.y1 - me.y) < 30) return true; |
| 24 | } |
| 25 | return false; |
| 26 | } |
| 27 | |
| 28 | /** |
| 29 | * The trigger was pulled here, now. If the gun can fire — alive, loaded, not |
| 30 | * reloading — it kicks at once rather than a round trip later, and the kick |
| 31 | * the server's snapshot would have given for the same shot is owed and not |
| 32 | * shown twice. Whether the shot lands, and whether it even happened, is the |
| 33 | * server's alone: a kick is the hand, not the bullet. |
| 34 | */ |
| 35 | export function gunFire(gun, snap) { |
| 36 | if (!snap.me.alive || snap.me.reloading || snap.me.ammo <= 0) return false; |
| 37 | if (gun.kick > 0.5) return false; // a held trigger, not a new pull |
| 38 | gun.kick = 1; |
| 39 | gun.owed += 1; |
| 40 | return true; |
| 41 | } |
| 42 | |
| 43 | /** Fold a fresh snapshot into the gun's state: kick on a shot, mark on a hit. */ |
| 44 | export function gunSnapshot(gun, snap) { |
| 45 | if (firedNow(gun.ammo, snap.me.ammo, snap.me.reloading)) { |
| 46 | if (gun.owed > 0) gun.owed -= 1; |
| 47 | else gun.kick = 1; |
| 48 | } |
| 49 | // A kick owed for longer than a shot could take to come back was refused. |
| 50 | if (gun.owed > 0 && gun.kick === 0) gun.owed = 0; |
| 51 | if (snap.me.alive && hitMarked(snap.shots, snap.me)) gun.mark = 1; |
| 52 | gun.ammo = snap.me.ammo; |
| 53 | } |
| 54 | |
| 55 | export function gunTick(gun, dt) { |
| 56 | gun.kick = fade(gun.kick, dt, 0.12); |
| 57 | gun.mark = fade(gun.mark, dt, 0.35); |
| 58 | } |
| 59 | |
| 60 | /** The crosshair, the weapon and the marker, in that order of importance. */ |
| 61 | export function drawGun(ctx, gun, width, height, snap) { |
| 62 | const cx = width / 2; |
| 63 | const cy = height / 2; |
| 64 | const gap = 6 + gun.kick * 10; |
| 65 | const arm = 9; |
| 66 | ctx.lineWidth = 2; |
| 67 | ctx.strokeStyle = "rgba(20,24,30,0.8)"; |
| 68 | ctx.beginPath(); |
| 69 | for (const [dx, dy] of [ |
| 70 | [1, 0], |
| 71 | [-1, 0], |
| 72 | [0, 1], |
| 73 | [0, -1], |
| 74 | ]) { |
| 75 | ctx.moveTo(cx + dx * gap, cy + dy * gap); |
| 76 | ctx.lineTo(cx + dx * (gap + arm), cy + dy * (gap + arm)); |
| 77 | } |
| 78 | ctx.stroke(); |
| 79 | ctx.strokeStyle = snap.me.alive ? "#8ef0c4" : "#888"; |
| 80 | ctx.lineWidth = 1.4; |
| 81 | ctx.stroke(); |
| 82 | |
| 83 | if (gun.mark > 0) { |
| 84 | ctx.strokeStyle = `rgba(255,120,110,${gun.mark})`; |
| 85 | ctx.lineWidth = 2.5; |
| 86 | ctx.beginPath(); |
| 87 | for (const [dx, dy] of [ |
| 88 | [1, 1], |
| 89 | [-1, -1], |
| 90 | [1, -1], |
| 91 | [-1, 1], |
| 92 | ]) { |
| 93 | ctx.moveTo(cx + dx * 7, cy + dy * 7); |
| 94 | ctx.lineTo(cx + dx * 15, cy + dy * 15); |
| 95 | } |
| 96 | ctx.stroke(); |
| 97 | } |
| 98 | |
| 99 | if (!snap.me.alive) return; |
| 100 | // The weapon: a barrel and a body, off to the right, rising with the kick. |
| 101 | const scale = height / 570; |
| 102 | const lift = gun.kick * 22 * scale + (snap.me.reloading ? 40 * scale : 0); |
| 103 | const bx = width * 0.66; |
| 104 | const by = height + lift; |
| 105 | ctx.fillStyle = "#23262e"; |
| 106 | ctx.beginPath(); |
| 107 | ctx.moveTo(bx - 34 * scale, by); |
| 108 | ctx.lineTo(bx - 20 * scale, by - 130 * scale); |
| 109 | ctx.lineTo(bx + 16 * scale, by - 150 * scale); |
| 110 | ctx.lineTo(bx + 30 * scale, by - 40 * scale); |
| 111 | ctx.lineTo(bx + 60 * scale, by); |
| 112 | ctx.closePath(); |
| 113 | ctx.fill(); |
| 114 | ctx.fillStyle = "#3b414e"; |
| 115 | ctx.fillRect(bx - 8 * scale, by - 160 * scale, 22 * scale, 26 * scale); |
| 116 | if (gun.kick > 0.35) { |
| 117 | ctx.fillStyle = `rgba(255,220,140,${gun.kick})`; |
| 118 | ctx.beginPath(); |
| 119 | ctx.arc(bx + 3 * scale, by - 162 * scale, 18 * scale * gun.kick, 0, 7); |
| 120 | ctx.fill(); |
| 121 | } |
| 122 | } |