Containment failed. Survive the waves.
A first-person, pixel-art zombie survival shooter that runs in the browser. Play it live: https://noah-zipit.github.io/rot-protocol/ Ten waves, three bosses, two maps, one finale. Built with Vite + TypeScript + Three.js. No backend, no accounts, no downloads beyond the first page load.
- Pick a map (Graveyard / City) and a loadout (any 2 of Pistol, Shotgun, SMG, Rifle — melee knife is always on V).
- Survive 10 waves. Waves 3, 6 and 9 are boss fights: THE ROT KING (AoE slam + summons), THE RIPPER (telegraphed dash), THE BROODMOTHER (spawns minions, shielded while they live).
- Wave 10 is PROTOCOL OMEGA: all three bosses at once. Kill them all to win — "CONTAINMENT RESTORED".
| Input | Action |
|---|---|
| WASD | Move |
| Mouse | Look (pointer lock) |
| Left click | Shoot |
| Shift | Sprint |
| Space | Jump |
| C / Ctrl | Slide (while sprinting — momentum, i-frames, FOV kick) |
| R | Reload |
| 1 / 2 | Swap weapons |
| V | Melee |
| M | Mute |
| Esc | Pause |
Touch devices are auto-detected and get touch controls instead of the keyboard list:
- Left joystick — move · SPRINT toggle — sprint
- Drag right half of screen — look
- FIRE (big, right thumb), SLIDE (sprinting only), JUMP, RLD (reload), 1/2 (swap), MELEE, pause, sound buttons
Tips: headshots deal 2x damage and freeze the action briefly. The shotgun's spread tightens while sliding. The Broodmother is immune while any minion lives — clear the adds first. Slide out of the Rot King's red ring.
npm install
npm run dev # local dev server
npm run build # tsc + vite build -> dist/
npm run preview # serve dist/ locally for a smoke test
npm run verify # headless sim checks: wave sim NaN scan + expedition
# mode end-to-end (extract, death, upgrades, regression)Stack: Vite 7, TypeScript 5, Three.js (npm). No frameworks.
src/
main.ts boot, screen state machine, fixed-step loop, event wiring
sim.ts 60Hz simulation: player, enemies, waves, weapons (no three.js)
world.ts three.js renderer: arenas, models, view models, particles, FX
ui.ts DOM HUD, screens, loadout, map select, boss bars
input.ts keyboard/mouse + touch input -> one InputState per tick
audio.ts 100% procedural WebAudio SFX
style.css all UI styling
public/
models/enemies/ zombie + monster models
models/guns/ FPS view models
favicon.svg
The simulation is already netcode-ready by construction:
sim.tshas zero rendering imports. All state is plain data (GameState).- The sim advances on a fixed 60Hz timestep via
stepSim(state, input, dt). - All player intent enters through the explicit
InputStatestruct ininput.ts(moveX/moveY/lookDX/lookDY + edge-triggered flags). - Every side effect the sim wants to show leaves as a
GameEvent(shots, hits, deaths, waves, boss attacks) — the renderer and audio are pure consumers of these events. Math.random()is used for spread/AI jitter; a future netcode build would replace these with a seeded RNG and send onlyInputStateper tick.- Rendering (
world.ts) only reads state and never mutates it, so a remote snapshot could be rendered the same way.
To add netcode later: serialize GameState snapshots, exchange InputState
per tick, run the same stepSim on both ends. No rewrite of game logic.
npm run build produces dist/ — a fully static site. The live game at
https://noah-zipit.github.io/rot-protocol/ is served from the gh-pages
branch, which holds a built copy of dist/ plus a .nojekyll file
(Pages is set to deploy from that branch). To publish a new build:
npm run build
git worktree add /tmp/rot-pages gh-pages
cp -r dist/. /tmp/rot-pages/ # .nojekyll already lives on the branch
git -C /tmp/rot-pages add -A
git -C /tmp/rot-pages commit -m "pages: rebuild"
git -C /tmp/rot-pages push origin gh-pages
git worktree remove /tmp/rot-pagesAlternatives: Vercel CLI (vercel --prod from this directory, no config
needed) or the Vercel dashboard (framework preset "Vite", build command
npm run build, output directory dist); any static host works too
(npx serve dist, Netlify, CF Pages).
No environment variables, no server functions, no database.
| Asset | Source | License |
|---|---|---|
| Zombie models (Shambler/Runner/Brute) | Quaternius (quaternius.com) | CC-BY — credit Quaternius |
| Skeleton (Rattler) | Quaternius via poly.pizza | Public Domain (CC0) — credit Quaternius |
| Bat "Goleling" (Shrieker) | Quaternius via poly.pizza | Public Domain (CC0) — credit Quaternius |
| Demon (The Rot King) | Quaternius via poly.pizza | Public Domain (CC0) — credit Quaternius |
| Blue Demon (The Ripper) | Quaternius via poly.pizza | Public Domain (CC0) — credit Quaternius |
| Giant (The Broodmother) | Quaternius via poly.pizza | Public Domain (CC0) — credit Quaternius |
| FPS pack (AKM rig, Glock rig, FPS arms, Mossberg 590A1, Combat Knife) | "FPS pack" bundle by J-Toastie via poly.pizza | CC-BY — credit J-Toastie |
| Graveyard arena kit (14 pieces: tombstones, dead trees, crypt, fences, gate, lanterns, coffins) | "Graveyard Kit" by Kenney (kenney.nl) | CC0 — credit Kenney |
| City arena kit: Building, Car, SUV, Police Car, Path Straight | "City Pack" by dreamdev via poly.pizza — models by Quaternius | CC0 — credit Quaternius |
| City arena kit: Large Building, Large Building 2 | "City Pack" by dreamdev via poly.pizza — models by Kenney | CC0 — credit Kenney |
| City arena kit: Road Bits, Traffic light | "City Pack" by dreamdev via poly.pizza — models by Kay Lousberg | CC0 — credit Kay Lousberg |
| SMG view model | Built procedurally in code (matches J-Toastie style) | own |
| All audio | Procedural WebAudio (no audio files) | — |
| Font | "Press Start 2P" via @fontsource (bundled, offline-safe) | OFL |
The same credits appear on the in-game title screen.
- Internal render resolution 426x240 upscaled with
image-rendering: pixelated. - DevicePixelRatio capped at 1 on mobile; zombie cap 8 (touch) vs 14 (desktop); reduced particle pools on touch; tight fog to limit draw distance.
- Auto quality scaler: if FPS stays under ~30 for 3 seconds, effects and particle counts step down (2 levels); recovers when FPS stabilizes.
- Total download target: under ~12MB.