HALL PASS — read this before you touch anything
Three.js survivors-like. Students hunt curses in a school from 9 PM to First Bell at 7 AM. Live at https://hallpass.cc. Studio: Very Bad Games. Canon guard: ten rooms, one hour each; a run is one hour and its bell is just the top of the next hour. Only the tenth room ends at First Bell with a sunrise. Never write daybreak, sun, or First Bell into a mid-night hour.
Chris's tasks and feedback are on the Hall Pass board (private Trello in
the free "Very Bad" workspace, beside Salty's:
https://trello.com/b/eNxiGuGk/hall-pass). Hand-driven sessions read it at the
start and whenever they run out of their own work: npm run relay (status).
A card in 📥 To do is Chris's ask and comes before self-chosen work. Never
act on 💡 Ideas & Backlog (the first list): read it only to see where Chris is
heading. Every command is npm run relay -- <cmd>: take <id> when you
pick one up, comment <id> "…" as you go, attachments <id> <dir> to save
the card's screenshots, done <id> "<commit, live link>" shot.png once it's
verified and shipped (a card you filed goes to ✅ Done (Claude)); 👍 Approved
is Chris's, never move a card there. ask <id> "<recommendation first>" for
anything that needs Chris's yes (money, anything outward, a real design
fork); file "<title>" "<body>" for your own work if it runs long. The
client is tools/relay/relay.mjs, a copy of Salty's: keep the two in step.
The tuner, playtest and registrar loops stay in their lanes and don't take
cards; the registrar's decision cards stay in npm run idea -- --cards.
This file is loaded into every agent session automatically. Keep it short.
Depth lives in docs/MECHANICS.md (systems), HANDOFF.md (project state) and
docs/SESSION-LOG.md (dated session history, spend, vendor facts, gotchas).
If you change a system, update docs/MECHANICS.md in the same commit.
Before touching music loops read docs/MUSIC-ENGINE.md first: the looping
primitive is NOT the problem and four rounds were lost to assuming it was.
Ask the graph before reading the tree: graphify-out/graph.json maps every
module, function and doc (1,226 nodes). graphify query "<question>" answers
architecture questions for a fraction of the tokens of a grep-and-read crawl;
GRAPH_REPORT.md lists the god nodes and communities. Rebuild after big
changes with /graphify G:\Skunkworks\hallpass --update.
Browser rigs start chrome-headless-shell, never full Chrome (no
channel: 'chrome', no temp profile): a full browser with a new profile
locked Chris's Windows account on Ghost (2026-10-13). Table and the one
exception in docs/BROWSER-RIGS.md.
Everything an agent needs travels in this repo. Skills live in
.claude/skills/, the dev-server config in .claude/launch.json, session
history in docs/SESSION-LOG.md. Nothing load-bearing may live only in a
machine-local ~/.claude — if a session learns something durable, write it
here in that same session. Only .claude/settings.local.json (per-machine
permissions) stays untracked.
src/config.js belongs to the tuning loop. Do not edit it in a hand-driven
session without checking npm run tuner first — it says whether a robot is
mid-landing. Everything else in src/ and all assets are yours; the tuner
touches that one file and nothing else, so staying off it makes a collision
impossible. playtest/** belongs to the playtest loop. Full map:
playtest/OWNERSHIP.md.
The playtest lab. Two agents (Claude and Codex) measure this game under
one set of rules: playtest/PROTOCOL.md. Read it before running or judging
any experiment. scripts/playtest-runner.mjs is the stable interface and owns
the metric definitions; each agent writes its own harness on top
(scripts/insight.mjs is Claude's). Shared seed banks make both agents
measure the same deterministic nights. Records are append-only manifests in
_hallpass-archive/experiments/; conclusions in _hallpass-archive/reports/INSIGHTS.md.
Claude's loop is /hallpass-playtest under /loop, from the repo root.
The registrar (/hallpass-triage, cheap model) judges everything on the
board that is unapproved or parked on a human: it approves tuning with the
lever named, approves harness bugs, rejects the superseded, and escalates only
design as one-question decision cards. Chris answers cards
(npm run idea -- --cards, --decide=<id>:<letter>); nothing else reaches him.
Rooms are edited in 3D, not by hand — docs/LEVEL-EDITOR.md. npm run dev
plus npm run editor, then /level-editor.html: drag props, stickers and
floor patches, save straight into src/maps.js. It renders with the real
buildWorld(), so what you drag is what ships. Dev-only, gated on !prod.
MAPS moved to src/maps.js — config.js re-exports it, so imports are
unchanged, but the tuner keeps config.js and the editor owns maps.js.
Directed gameplay footage is a solved problem — docs/CINECAM.md. Shots
live in cine/*.json, the rig in scripts/cine{,-client}.mjs|js. It is
dev-only and never enters the bundle. Read it before hand-recording anything:
takes are reproducible only because four specific leaks are plugged, and
cine.verify() is how you find out you broke one.
Heavy artifacts never enter this repo. Raw run JSON, screenshots, video
and browser profiles go to ../_hallpass-archive/, which is its OWN private
repo (MisterSands/hallpass-archive) with its own README — so telemetry is
retained and versioned without riding along on every game commit. All of it
regenerates from (seed, commit) because the sim is deterministic. Bulk
artifacts in git already cost this repo 1.25GB of history and broke pushes
once — never git add -A here.
Non-negotiables
npm testmust stay green. The suite includes determinism lockstep pairs and a snapshot round-trip — they are the canaries for the whole sim. If they fail, you changed rng consumption, and a saved run will no longer replay.- The sim is deterministic and the view is not.
src/sim.jsowns logic and consumesmulberry32in a fixed order.src/view.jsowns rendering and may useMath.random()freely. Never callMath.random()in sim. Never put game state in view. - Difficulty comes from enemy HP and speed, never spawn counts. More spawns make a survivors-like EASIER — spawns are the XP economy. This was measured: Night II at spawnMult 1.15 hit dawn at the median.
- Deploy only from
main, only fromdist/:npm run ship(=npm run build:prod, thennpx netlify-cli deploy --prod --dir dist --site 84baf5f8-bf39-4a6f-8439-93b4e22ec37c, thennode scripts/cf-purge.mjs, which purges Cloudflare's edge once it is in front and is a harmless warning until then —docs/CLOUDFLARE-PLAN.md). Standing rule (Chris, 2026-09-01): ship straight to production after every landed game change — no draft URLs, no asking — until there are live players (expected a few weeks out). Revisit then; draft URLs (netlify deploywith no--prod) come back for experiments. APK rule (Chris, 2026-09-09): do not rebuild or publish a new APK unless Chris explicitly requests one for a major release. Web changes still ship. - Never regenerate the public sound library on build.
scripts/sound-library.pyruns on demand only, or it silently re-cuts reference takes that collaborators already downloaded. - No text baked into generated video. Titles and names are composited in post so a clip can be re-cut in any language. Only diegetic text on props.
Tripwires that have actually bitten
Each of these cost real time. They are not hypothetical.
- Edit the file that owns the behaviour, not the file you expect. The "E
triggers Domain" bug survived two fixes in
game.jsbecause the binding was ininput.js. Grep for the symptom before editing. #ui > *setspointer-events: none; only.btnre-enables it. Any new interactive element needspointer-events: autoor it is silently dead.justify-content: centerdoes not centre the scrolling.screenflex containers — it clips overflow instead. Use auto margins /.flexspacer.- A
.foo.bigvariant outranks a.foooverride. Short-landscape blocks written as.headingnever applied to.heading.big, so phones got a 119px title in a 393px viewport. - Atlas frames are 128px (64 on mobile). Past ~9 world units a sprite reads
as a smear. Use
fx.ring/burst/glow— geometry scales losslessly. SeeSPRITE_MAXinview.jscase 'domain'. .screen::before(the ghost word) hangs 37px below the box and makes screens scrollable even when the content fits.- Video layers reveal on
'playing', nevercanplay. iOS blocks autoplay in Low Power Mode and an unrendered<video>fades over the art. preload="none"is safe only for files served over HTTP. With data URIs it plays nothing at all.- iPhone Safari has no element Fullscreen API. Add to Home Screen is the only zero-chrome path. Do not claim otherwise.
How to prove a change works here
The embedded browser pane does not composite. It cannot screenshot, does not run rAF, and freezes CSS transitions. So:
- Drive the loop manually with
g.frame(1/60);window.__HALLPASS__exposes{game, view, input, sound, ui, sim, save}. - Disable transitions before measuring layout after a resize, or you read a stale transform that is not really there.
- Prove a button is reachable with
document.elementFromPoint, not by its coordinates. - Prove input works with real dispatched
KeyboardEvent/PointerEvent. Injecting intoinput.queuebypasses the handler and proves nothing. - Balance claims need
node scripts/balance.mjs, not intuition.
Conventions
- The credits are one entry: Chris Sands / SOLO DEVELOPER (Chris, 2026-10-08). No one else is credited. "Remy Price" is Chris's music name and never appears on this game.
- Deliver files to
C:\Users\Operator\Downloads, not chat attachments. - Shipaton contest material does not live here: the campaign plan and BIP posts are in
../shipaton-intel/campaign/, the submission video in../hallpass-shipaton-video/, the daily watch in../shipaton-watch/.store/keeps only the game's own store work. - Envato masters are git-ignored — licensed per account, not redistributable.
research/IS tracked, corpus included, except the oversized Reddit JSONL corpus: it lives in../_hallpass-archive/research/reddit/(override withHALLPASS_REDDIT_RAW). Its reports and harvesting script remain tracked. Seeresearch/reddit/README.md. Media SOURCE is not:assets-src/(8.7GB) andshots/stay ignored. Only the optimizedassets/ship.- Gameplay captures live OUTSIDE the repo at
G:\Skunkworks\_archive\hallpass-captures\— 1.4GB, with its own README. They were in git twice by accident and cost 1.25GB of history; do not bring them back. Regenerate with the?drive=1harness instead of restoring.python scripts/archive-captures.pysweeps new recordings out of the repo into it, dates them, and updates the manifest;--verifychecksums it.