Skip to content

Latest commit

 

History

1 Commit

Folders and files

NameName
Last commit message
Last commit date
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 

Repository files navigation

FATAL FUNNEL

Plan the breach, run it live, then share the raid as a URL that re-simulates bit-for-bit in someone else's browser.

License Three.js TypeScript pnpm Tests

English · 한국어 · 日本語 · 简体中文 · 繁體中文 · Español · Deutsch · Français · Português (Brasil) · Русский · Italiano · Bahasa Indonesia · Türkçe

Play now · Quickstart · Architecture · Credits

Fatal Funnel share card — a real captured frame of the running build

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.

What this is

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.

Highlights

  • Fixed-step deterministic core. TICK_HZ = 30 in packages/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.mjs runs 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.mjs fails the build if the simulation or content packages touch any of 11 banned non-deterministic or engine-variant APIs; packages/sim/src/dmath.ts replaces 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.

Screenshots

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
Top-down view with unexplored area dark, explored area dimmed, a visible cone, a plan polyline and a go-code badge on an operator Pre-mission insertion screen with the objective and a countdown to entry

Play it

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.

Quickstart

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 dev

Open 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.

Architecture

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
Loading

Four rules hold the project together:

  1. The simulation owns outcomes; nothing else does. @ff/sim is headless and has no dependencies. It cannot import Three.js or React — the purity checker fails the build if it tries.
  2. Determinism is a property of the pair @ff/sim and @ff/content. The mission generator and entry planner live in content and feed the hashed simulation, so a non-portable Math.hypot there would fork the state hash across JavaScript engines exactly as it would inside the simulation. Both packages are scanned, and both use dmath instead of the transcendentals.
  3. 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.
  4. 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.

Project layout

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

How this was built

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.sin or Date.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 in THIRD_PARTY_ASSETS.md so 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.

Development

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.

Localization

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

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.

License

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.

About

Top-down tactical CQB in the browser: stop time, draw an entry line per operator, then execute everyone at once. The core is a zero-dependency simulation at a fixed 30 Hz off a seeded PRNG that hashes its world state, so a finished raid serialises into a replay URL and re-simulates bit-for-bit for a stranger. 12 missions.

Topics

Resources

Stars

0 stars

Watchers

0 watching

Forks

Releases

Packages

Contributors

Languages