Snipers
A browser-based multiplayer arena FPS built around one weapon: the sniper rifle. Inspired by Halo's Team Snipers — long sightlines, one-shot headshots, and movement that rewards positioning. Playable by anyone with a link, no install.
Play it: snipers.rorywillis.com — add ?room=<name> to the URL and share it for a private match. Design notes and architecture decisions live at docs.snipers.rorywillis.com.
Gameplay
- Team Snipers: two teams, first to 50 kills or best score in 10 minutes, with automatic warmup → countdown → match → scoreboard → rematch flow
- One weapon, deep skill: hitscan rifle — headshot kills instantly, body shots take two; 4-round mag, quick follow-up shots, punishing reload
- 5× hold-to-scope with flinch and forced descope when you take a hit, so the first accurate shot wins the duel
- Mouse & keyboard or gamepad (standard/Xbox mapping), usable interchangeably
- Kill feed, death cam, Tab scoreboard, footsteps, tracers, and procedural audio — no asset files anywhere
Netcode
The interesting part. Competitive sniping lives or dies on hit registration, so the game runs the full competitive-FPS stack:
- Authoritative server at a 60 Hz fixed tick; clients send inputs, never positions, and every input is validated
- Shared simulation: one TypeScript sim package imported by both client and server, which makes client-side prediction exact
- Client prediction + server reconciliation for your own movement (zero perceived input latency), snapshot interpolation (~100 ms) for everyone else
- Lag compensation: the server keeps ~1.5 s of hitbox history and rewinds targets to the tick you were actually seeing before tracing your shot — at 100 ms ping, shots that look like hits are hits
- WebTransport datagrams for inputs and snapshots (UDP-like: a lost packet is skipped, not retransmitted — no TCP head-of-line stalls on lossy connections), negotiated over the WebSocket, which stays as the universal fallback for browsers without it
- Built-in network simulator: append
?ping=150&jitter=30&loss=5to the URL to play under artificial latency, jitter, and packet loss
Tech stack
| Layer | Choice |
|---|---|
| Rendering | Hand-rolled WebGPU renderer (WGSL, flat-shaded low-poly) |
| Client | TypeScript + Vite, DOM-based HUD |
| Server | Node.js running TypeScript natively (type stripping); WebTransport datagrams with WebSocket fallback |
| Simulation | Shared TypeScript package, fixed timestep, custom AABB physics |
| Tooling | npm workspaces monorepo, zero runtime dependencies beyond ws |
| Infra | AWS CDK: Fargate + ALB (WSS), S3 + CloudFront, Route53 + ACM |
The map is a list of axis-aligned "brush" boxes that serves as both render geometry and collision geometry. The transport sits behind a small interface so WebSockets can be swapped for unreliable WebRTC DataChannels later without touching game code.
Running it
npm run dev # docker compose: client, game fleet, traefik, dynamodb-localOne command, no local npm install needed — dependencies install into a Docker volume on first run. It brings up the production topology with production's hostname layout, routed by traefik on port 80: the Vite client at http://localhost, two game server replicas round-robined at ws://ws.localhost (standing in for the ALB), and a DynamoDB room directory (dynamodb-local). Source is bind-mounted, so the server hot-restarts and the client hot-reloads on save. Traefik's dashboard is at http://localhost:8080.
Open http://localhost in two browser windows to start a match. A second player joining triggers the countdown.
Rooms live in one server's memory; the directory records which server owns which room, and mis-routed connections are forwarded to the owner — the same mechanism that gives zero-downtime drain-based deploys and auto-scaling in production.
Controls — WASD move, Space jump, mouse look, LMB fire, hold RMB to scope, R reload, Tab scoreboard. Gamepad: sticks move/look, RT fire, hold LT to scope, A jump, X reload, Back scoreboard. Gamepad look gets subtle aim assist (slowdown over visible enemies plus light tracking); mouse does not. Append ?assist=1 or ?assist=0 to the URL to force it on or off.
Requires a WebGPU-capable browser (current Chrome, Edge, Firefox, or Safari).
Models — the rifle and player models are Blender files in packages/assets/models, exported to the client by npm run models:build. Run npm run models:watch alongside npm run dev and saving in Blender updates the running game in place. See that folder's README for the conventions each model follows.
Status
Phases 0–7 of the build plan are complete: rendering, gameplay, networking, lag-compensated combat, match flow, and AWS deployment (Fargate game server behind TLS, CloudFront-hosted client, private room links, and CloudWatch metrics for tick time and snapshot size). Next up: a WebRTC transport upgrade, delta-compressed snapshots, and more maps.