Plan the breach, run it live, then share the raid as a URL that re-simulates bit-for-bit in someone else's browser.
English · 한국어 · 日本語 · 简体中文 · 繁體中文 · Español · Deutsch · Français · Português (Brasil) · Русский · Italiano · Bahasa Indonesia · Türkçe
Important
Apache-2.0 covers source code only. Art, audio, models, textures, and captures are governed by the records in CREDITS.md, and those records take precedence over Apache-2.0 for those files. Media that is not listed there is undocumented, not free to reuse.
Fatal Funnel is a top-down tactical CQB game that runs in a browser: stop time, draw an entry line for each operator, then execute everyone at once. It is a playable vertical slice on trunk — 12 hand-authored campaign missions, a seeded mission generator, an in-browser editor, and a leaderboard service — not a finished commercial release. The engineering centre of the project is a zero-dependency simulation package that runs at a fixed 30 Hz off a seeded PRNG and hashes its whole world state, so a finished run can be serialised into a ?replay= URL and reproduced exactly by a stranger who clicks it. That claim is proved by a headless harness on every run of the gate, not asserted in prose.
- Fixed-step deterministic core.
TICK_HZ = 30inpackages/sim/src/world.ts, mulberry32 seeded streams, and an FNV-1a hash over raw float bits of the entire world. - Bit-exact replay links, proved end to end.
tools/replay-check.mjsruns 10 checks: it plays a scripted raid, serialises it to a share URL, boots that URL cold in a second page, injects sabotage input mid-replay, and requires the end-tick state hash to match exactly. - Determinism is enforced, not hoped for.
tools/check-sim-purity.mjsfails the build if the simulation or content packages touch any of 11 banned non-deterministic or engine-variant APIs;packages/sim/src/dmath.tsreplaces the transcendentals with routines that use only IEEE-exact operations. - 55 test files — 29 in the simulation package, 20 in content, 6 in the renderer — plus a golden-replay fixture whose checkpoint hashes must reproduce exactly or the suite fails.
- 40 weapons defined in
packages/content/src/weapons.ts, each carrying its real trigger group, so a weapon with no full-auto position never gets one. - 12 hand-authored campaign missions across two acts, alongside a seeded generator and an editor that shares a built mission as a
?mission=URL. - 40 headless harnesses in
tools/, including a 16-check gameplay smoke run and a 4-camera screenshot pass with luma sanity checks. - Recorded audio layered over synthesis, with a synthesis fallback: 6 gunshot blast layers, 3 ambient tension beds, and 51 locally rendered radio clips across 7 event kinds. If a pack fails to load the engine falls back to pure synthesis instead of going silent.
- Licence evidence is archived, not linked: 12 licence-page snapshots live in
LICENSES/, because a URL can be edited after you read it. - Trunk-only snapshot. This publication contains 0 weapon GLB files: the procedural weapon renderer is what ships on
main, and in-progress model work on feature branches is deliberately not mixed into a trunk release.
Both captures below were produced by node tools/shots.mjs against the production bundle of this snapshot — they are frames of the running game, not mock-ups.
| Fog of war, committed plan line, go-code hold | Pre-mission insertion |
|---|---|
![]() |
![]() |
Play the current web build at fatal-funnel.vercel.app. No install and no account; progress is stored in the browser.
The in-game briefing (?) is the canonical control table and the keys are rebindable in settings. Defaults:
| Input | Action |
|---|---|
Space |
Stop time — the planning state |
| Left click | Run to a point (kicks a closed door — loud) |
| Right click | Walk to a point (opens a door quietly) |
| Drag | Box-select operators |
Q / Tab |
Select all / cycle operator |
Shift+click |
Append a waypoint to the selected operator's plan |
[ / ] |
Change storey, then click to route through the connector |
F |
Set the facing to hold at the last waypoint (overwatch) |
G / H |
Throw utility slot 1 / 2 (flash, frag, smoke) |
B |
Place a breaching charge on a door near the last waypoint |
1 2 3 |
Attach a go-code A/B/C hold to the last waypoint |
Enter |
Execute every operator's plan simultaneously |
8 9 0 |
Fire go-code A/B/C — the simultaneous-entry mechanic |
X |
Call for surrender; standing beside a surrendered target arrests them |
V / E |
Bodycam spectate / take first-person control of an operator |
C |
Stack up on the lead operator |
R |
Instant restart, keeping the plan |
Esc / ? |
Cancel or pause menu / briefing |
Share links only behave as links on a real origin: /?gen=SEED reproduces an exact generated operation, /?screen=editor&mission=… opens a shared mission, and /?replay=… boots the deterministic observer.
Prerequisites: Node.js 20 or newer, and pnpm through Corepack. The commands below were verified on Node.js v26.3.1 with pnpm 11.6.0.
git clone <your-fork-url> fatal-funnel
cd fatal-funnel
corepack enable
pnpm install --frozen-lockfile
pnpm devOpen the URL Vite prints. Single-player needs no server, database, or environment file; only the optional leaderboard and co-op paths talk to a backend.
flowchart LR
I[Mouse, keyboard, or replay log] --> C[Command log]
C --> S["@ff/sim - fixed 30 Hz, zero dependency"]
D["@ff/content - missions, weapons, generator"] --> S
S --> H[FNV-1a world state hash]
S --> E[State and events]
E --> R["@ff/renderer - Three.js, VFX, audio"]
H --> V[Replay URL: seed, log, end tick, end hash]
V --> S
P[check-sim-purity] -.-> S
P -.-> D
Four rules hold the project together:
- The simulation owns outcomes; nothing else does.
@ff/simis headless and has no dependencies. It cannot import Three.js or React — the purity checker fails the build if it tries. - Determinism is a property of the pair
@ff/simand@ff/content. The mission generator and entry planner live in content and feed the hashed simulation, so a non-portableMath.hypotthere would fork the state hash across JavaScript engines exactly as it would inside the simulation. Both packages are scanned, and both usedmathinstead of the transcendentals. - A replay is a seed plus a command log, never a state dump. That is small enough to fit in a URL, and reproducing it re-runs the real simulation instead of playing back recorded positions — which is why an observer's input cannot perturb it, and why a mismatch is detectable at all.
- Rendering is downstream and disposable. Models, VFX, and audio consume simulation state and events; a missing model or audio pack degrades presentation and changes no outcome.
Golden replay fixtures are treated as evidence: intentional simulation changes are re-blessed with node tools/regold.mjs and reviewed as a diff, never hand-edited.
packages/
sim/ zero-dependency deterministic core: world, ballistics, vision, nav, hash, rng
content/ missions, campaign acts, weapons, generator, entry planner, progression
renderer/ Three.js scene, models, VFX, and the shipped model and texture payloads
apps/
game/ browser shell: screens, HUD, audio, comms, replay, service worker
services/
leaderboard/ run-submission verification shared with the edge function
supabase/
functions/ leaderboard submission endpoint
api/ share-card and share-link endpoints
desktop/ Electron wrapper and its URL policy
tools/ 40 headless harnesses: smoke, replay, shots, purity, regold, bundle budgets
LICENSES/ archived licence pages for every third-party asset
docs/
i18n/ translated README set
screenshots/ captures produced by tools/shots.mjs
The workflow is AI-augmented but gate-first, and the gates are what make the agent output trustworthy rather than the other way round.
- Agent relay with a written contract. Work was split into bounded lanes and run as a relay: one agent implemented or extended a lane, a second agent reviewed the diff adversarially, and the owner supplied playtest judgment. A self-reported success from an agent was never accepted as evidence — harness output was.
- Deterministic gates instead of review by reading. Every behavioural change has to survive the state-hash tests, the golden-replay checkpoints, and the purity scan. That combination catches the specific defect an agent is most likely to introduce: a plausible-looking
Math.sinorDate.now()that quietly desynchronises replays on somebody else's browser engine. - Browser QA as probes, not screenshots judged by eye. Behavioural claims are made by harnesses that assert. The smoke run makes 16 assertions about ticking, pausing, pathfinding, contact, casualties, go-code gating, and first-person control; the replay harness proves cross-page bit-exactness with sabotage input injected mid-replay; the screenshot pass measures average luma and blown-highlight ratio, so a black or blown frame fails instead of being quietly committed.
- Asset licensing is fail-closed and mechanical. Nothing enters the tree without a licence verified on the asset's own page; that page is snapshotted into
LICENSES/at acquisition time because links rot; every shipped file's SHA-256 is recorded; and the fetch tools refuse to write if a source page stops declaring the licence they expect. Several genuinely convenient assets were rejected on licence or model-lineage grounds, and the rejections are kept inTHIRD_PARTY_ASSETS.mdso the reasoning survives. - Publication is its own gated step. This snapshot is produced by a sanitising publisher that never writes to the development repository: it archives a ref, applies exclusions and publication-only documents, then runs secret scanning, credential-shape and identity checks, an excluded-path check, a build, the full test suite, and a claims gate that re-derives every number in this README from the tree. A stale figure here fails publication instead of shipping.
pnpm test # 55 test files across the three packages + the determinism purity scan
pnpm build # game bundle, service worker, and the bundle-split budget contract
pnpm smoke # 16 headless gameplay assertions against a served build
pnpm replaycheck # 10 checks, ending in cross-page bit-exact reproduction
pnpm shots # 4 fixed cameras with luma sanity checks
pnpm purity # the 11 banned non-deterministic and non-portable APIs
pnpm regold # re-bless golden replay checkpoints (review the diff)The browser harnesses take a base URL, so build and serve first — for example pnpm build, then npx serve apps/game/dist -l 4266, then pnpm smoke http://localhost:4266. They run headless by design and never open a window.
Behaviour changes should arrive with a deterministic regression test, keep tuning in the content data where possible, and update CREDITS.md plus the hash ledger for any media change.
The English README is canonical. Translations live in docs/i18n/ and are indexed in README-INDEX.md; commands, paths, numbers, and links are kept identical across all of them. The game itself has no runtime i18n layer: the HUD, briefing, and mission text are authored in Korean with English callsigns and screen titles, and the radio voice pack is English. Adding a language today would mean extracting those inline strings first — that work has not been done, and this README does not claim otherwise.
CREDITS.md is the effective media licence record: a four-tier redistribution register that promotes the existing ledgers in THIRD_PARTY_ASSETS.md, apps/game/public/THIRD_PARTY_NOTICES.txt, and LICENSES/. Its asset-specific terms take precedence over the code licence for art, audio, models, and textures.
Source code is licensed under the Apache License 2.0; attribution and modification notices are in NOTICE. Media files are not licensed by Apache-2.0 and remain governed by CREDITS.md.


