A five-way audit of the shipped build found the first thirty seconds are broken in three
independent ways. All three are verified, not inferred.
1. THE CONTROLS WERE NEVER STATED. The title said "press any key", and aim + fire are both gated
on pointer lock (input.js:45 onMouseMove early-returns unlocked; input.js:50 the first
mousedown only REQUESTS the lock) — so a player who dismissed with a key got no mouse look and
a cannon that silently did nothing. Worse: in an ARENA the forward axis belongs solely to
intent.throttle (player.js), so an untaught player coasts to a dead stop in front of L1's Mast
Cell and reasonably concludes the game is broken. Six arenas across L1/L3/L5/LS are the same
trap. The card now says CLICK TO FLY, takes the pointer lock on any dismissal, and prints the
legend that has existed — written and styled — in flight/dev.html since round 1, visible only
to whoever was running Lane B's harness.
2. DYING DELETED THE BOSS. boot's pump advanced `_ei` monotonically and never rewound. That
reasoning is right for `spawn` and `pickup` (re-firing double-spawns survivors and resurrects
banked pickups) but it silently applied to `boss` as well: die to any of the five, respawn,
fly through an empty lair, cross the gate, and collect a graded medal for a fight that never
happened. Dying was the fastest route past every boss in the game. Now a REARM set ({boss,
acid}) re-fires on respawn while everything else is retired into `_spent`. Verified by event
trace: start:mast_cell -> end:wipe -> start:mast_cell. Also despawns the sentinel's body on a
wipe, which the fix exposed as a duplicate (1 -> 2 Mast Cells per death).
3. L1'S TIDE WAS UNWINNABLE, AND MINE. I authored it this morning at speed 5 in a flow-2 segment,
reasoning "5 against 2 catches a dawdler". The player ceiling there is 2.8 u/s sustained
(throttleMax 1.4 x flow 2); boost peaks 5.2 for 0.55s on a 2.4s cooldown. C's own --sim prints
the verdict in plain text — "required 2.50x UNWINNABLE" — and I never ran it. Because
hazards.js grazes on gap<=catchRadius and the gap stays negative once passed, it was a ~74 dps
loop back onto the s=475 checkpoint: a brand-new player, in the default level, hard-stopped
three minutes in. Retuned to 2.4 (required 1.20x — the same shape as L2's teaching burp).
The validator was exempting it: `if (!c.lethal) continue` in levels/index.js meant qa.sh went
green over a death loop. An unwinnable chase is a design failure whatever its lethality — a
non-lethal one you cannot outrun is a continuous-damage trap. That exemption is gone, and all
six levels still validate.
Also: DEFAULT_LEVEL was still 'L2_esophagus' from when L2 was the only finished level, so every
new player skipped the tutorial entirely and started mid-campaign. LANE_C_NOTES §→ Lane F #2
said "change it if you would rather it be L1". It is L1 now.
Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
|
||
|---|---|---|
| .claude | ||
| docs | ||
| pipeline | ||
| tools | ||
| web | ||
| .gitignore | ||
| README.md | ||
GUTS — a fantastic voyage through the alimentary canal
Browser flight-combat game (three.js, zero build step). You pilot ENDO-1, a microscopic probe, mouth → rectum through five anatomically-grounded biomes: ride peristalsis down the esophagus, cross the acid sea of the stomach, thread the villi forest of the small intestine, survive the gas swamps of the colon. Fantastic Voyage × Star Fox × Descent.
- Design: docs/GDD.md · Look: docs/ART_BIBLE.md
- Architecture + contracts: docs/TECH.md
- Asset generation: docs/PIPELINE.md
- How we work (lanes/rounds): docs/PROCESS.md — read this first if you are a lane
- Current round: docs/LANES/ROUND1_INSTRUCTIONS.md
Run it
cd web && python3 -m http.server 8140
# http://localhost:8140 — the game
# http://localhost:8140/?stub=1&dbg=1 — stub world + debug HUD (works from day 0)
No bundler, no npm, no build. ES modules + import map; three.js r175 vendored in web/vendor/
(never CDN — the game must run offline). Before you finish a session: bash tools/qa.sh.
The one-paragraph tech pitch
The whole canal is a spline. World position is (s, θ, ρ) — distance along the canal,
angle and radius in the cross-section. Tube walls are chunked extrusions along
parallel-transport frames; peristalsis and breathing are vertex-shader displacement
(cheap, squishy, everywhere); collision is an analytic radius test (no physics engine).
Caverns (mouth, stomach) switch the same controller to free 6DOF. Look is synthetic
scanner — dark endoscope void, SEM-style monochrome walls with FLUX-generated detail
textures, emissive-coded enemies. Everything is seeded + deterministic, and every asset is
optional at runtime — the game boots and plays asset-free with procedural fallbacks.
Hard laws (all lanes)
- Determinism. No
Math.randomoutsidejs/core/rng.js. Seeded RNG only. qa.sh greps. - Assets are optional. Missing texture/model/audio ⇒ procedural fallback, never a crash.
- Eyeball everything. Visual work isn't done until a screenshot is in
docs/shots/laneX/. - Perf budget: ≤300 draw calls, 60 fps on M-series / mid laptop. Measure via
window.DBG. - Free-first. Local MODELBEAST generation before any paid API; fal.ai is a gated fallback (John's go). See PIPELINE.md.
- Git: stage only your exact paths, commit + push each session, never
git add ..
Deploy
Live: https://partly.party/gutsy/ — bash tools/deploy.sh (QA gate → rsync web/ minus
dev/ → docker cp into forum-nginx → verify). No build step; web/ ships as-is.
Two things to know before you touch it, both documented at length in the script header:
- It's ephemeral. Content lives in the container's own layer (like blobbo/glytch), so a
docker compose up --force-recreateor nginx image bump wipes it — re-run the script. The durable fix is a bind mount inforum/docker-compose.yml(what cratewars/roguelike use), which costs a forum-nginx recreate and brief partly.party downtime. - A 404 there returns HTTP 200. nginx's catch-all
try_files ... /index.htmlserves the arcade index for any missing file. Verify on content, never on status codes.
Origin: ssh://git@100.71.119.27:222/monster/guts.git.