# Molly Cool — Vertical Slice Build Plan **For the executing agent (Opus 4.8). Read this whole file before writing code.** You are building a playable **2D browser prototype** of the first ten minutes of *Molly Cool*. This is a **feel-first vertical slice**: the entire point is to prove that the core mechanics are *fun to hold a controller for*. If the moment-to-moment feel isn't satisfying, nothing else matters. Prioritize game feel over completeness at every fork. This is **not** a science app. There is no real chemistry validation, no accuracy requirement, no molecule database, no pedagogy. "Protons," "charge," "bonds," "acidity" are flavor for mechanics — nothing more. Do not add educational text, quizzes, or explanations. **The game teaches entirely through play, with zero words on screen.** --- ## 1. The design in one screen Molly is a nanoscale character in a permanently shaking fluid world. She has two hands: - **Give** — push a proton into a thing → it goes **positive** (hot pink). - **Take** — rip a proton out → it goes **negative** (cyan). - Leave it alone → **neutral** (dim gray-white), and it drifts free. From those two verbs everything follows: - **Opposites attract, likes repel.** Charged things lunge at their opposite and stick. - **Bonds** aren't built with a button — you *drag two atoms together, wrestle them through the resistance, and let go.* They snap. Big juice. - **The membrane wall** blocks anything charged and ignores anything neutral. No guard, no switch — you get through by *becoming neutral*. - **Zones** (sour / soapy / neutral) re-tune your hands: a sour zone keeps shoving things positive, so neutral won't stay neutral; soapy drains charge away. - **Heat** shakes everything in a radius (crowbar). **Light** flips one thing (scalpel). Some setups react to them in *opposite* directions. **The heist:** you're too small to open doors, so you change what things *are*. Strip a thing neutral → it drifts through the wall → the sour room on the far side charges it back up → now it's trapped. You never opened the vault. You made the loot un-leavable. --- ## 2. Non-negotiable pillars 1. **The snap must feel incredible.** The bond hold-and-release (§8) is the heartbeat of the game. Spend disproportionate effort here. If you only get one thing feeling perfect, make it this. 2. **Zero on-screen text during play.** No tutorials, no labels, no tooltips. Every lesson is a thing that happens to the player. (A single title card at the start and an end card are fine.) 3. **Two buttons carry the whole game.** Give + Take, plus Grab, plus two openers (Heat, Light). Resist adding mechanics. Depth comes from combining these, not from more verbs. 4. **The world is always alive.** Everything jitters constantly (Brownian shimmer). Nothing sits perfectly still, including Molly at rest. No coasting — there's drag on everything. 5. **Readable at a glance.** Charge state is legible instantly by both **color AND shape** (see §6), so it reads even for colorblind players and even in motion. --- ## 3. Explicit non-goals (do not build these) - No 3D, no WebGL, no molecular rendering libraries, no Babylon/Three. - No real chemistry engine, no force fields, no molecule data files, no PubChem. - No accuracy claims, no correct bond angles, no periodic table. Charge is an integer; that's the whole model. - No build framework (no React, no bundler required). No npm dependencies. See §4. - No menus beyond a start screen, no settings, no save system, no networking, no analytics. - No art assets, no audio files. Everything is drawn with Canvas primitives and synthesized with WebAudio. --- ## 4. Tech stack - **Vanilla JavaScript, ES modules.** No dependencies, no bundler, no build step. - **HTML5 Canvas 2D** for all rendering. - **WebAudio API** for all sound (synthesized at runtime — no files). - **Gamepad API** for controller + haptic rumble (the hold-resistance tell). - Runs by opening `index.html` through any static file server (e.g. `python3 -m http.server`). Must work in current Chrome/Edge/Safari. Must hold **60 fps** on modest hardware. Logical resolution **1280×720**, letterboxed/scaled to fit the window (integer or fractional scale, preserve aspect). All coordinates in logical pixels. --- ## 5. Project structure (suggested — adapt if you have a better idea) ``` index.html # canvas + module entry, title card, start button src/ main.js # boot, game loop, state machine (title → play → end) config.js # ALL tunable feel constants live here. One file. input.js # keyboard + mouse + gamepad → unified InputState, rumble camera.js # follow + screenshake (trauma model) + world↔screen audio.js # WebAudio synth: pop, hum, snap, ambience, bounce world.js # holds particles, membranes, zones; steps the sim particle.js # Particle entity + charge/jitter/bond state molly.js # player: movement, hands, grab, self-charge physics.js # forces: jitter, charge interaction, zones, membrane, bonds bonds.js # the hold-and-release mechanic + bond constraints render.js # draw world, particles, glow/shape by charge, effects fx.js # particles/sparks, hitstop, flashes, trauma helpers rooms.js # room definitions (data) + RoomManager rooms.data.js # the ten scripted beats as data ``` Keep `config.js` authoritative for every feel number. Feel is tuned by editing that one file; don't scatter magic numbers through the code. --- ## 6. Core data model **Particle** (a mote / atom): ``` { pos: {x, y}, vel: {x, y}, charge: -1 | 0 | +1, // slice uses three states; allow ±2 later displayCharge: float, // lerps toward `charge` for smooth color/shape radius: ~10, slots: 1..4, // "valence" dots drawn around it (visual only in slice) bonds: [particleRef, ...], // spring-linked partners zoneTimer: float, // accumulates while in a re-tuning zone grabbedBy: null | 'L' | 'R', // which hand is holding it fixed: bool, // scenery anchors (raft targets, etc.) tag: string // e.g. 'key', 'debris', for room scripting } ``` **Molly** (player): ``` { pos, vel, charge: -1|0|+1, displayCharge, aim:{x,y}, radius:~14, handL: {holding: particleRef|null}, handR: {holding: particleRef|null} } ``` **Membrane:** a line segment `{a:{x,y}, b:{x,y}, thickness}` that repels charged things, ignores neutral ones. **Zone:** `{shape:'rect'|'circle', ...bounds, type:'sour'|'soapy'|'neutral', tint}`. **Room:** `{ id, molly:{x,y}, particles:[...], membranes:[...], zones:[...], exit:{rect}, ambient:{hum, tint}, onEnter?:fn, onExit?:fn }`. **Charge → appearance** (both color and shape, always redundant): | Charge | Color | Shape signature | Halo | |---|---|---|---| | **+1 positive** | hot pink `#FF2D9B` | **spiky** outline (outward points) | tight bright rim | | **0 neutral** | dim gray-white `#C8D0D8` | smooth circle, flat | none, slightly translucent | | **−1 negative** | cyan `#2DB8FF` | smooth circle, **soft pulsing** halo | wide gentle glow | Positive = aggressive/spiky, negative = smooth/glowing, neutral = quiet/flat. This makes charge readable without color, and just reads better in motion. `displayCharge` lerps over ~150 ms so transitions feel physical, not instant. --- ## 7. The simulation (physics.js) — update order per frame Run at a fixed timestep (e.g. accumulate real time, step at `dt = 1/120 s`, render interpolated). Order each step: 1. **Jitter (the storm).** Every non-grabbed particle gets a random impulse: `vel += randUnit() * JITTER_ACCEL * dt`. Molly gets a much smaller idle jitter so she never sits dead still. Tune `JITTER_ACCEL` (start ~400 px/s²) so the world *shimmers* — alive, not chaotic. 2. **Charge interaction.** For each pair of *charged* particles within `CHARGE_RANGE` (~180 px): Coulomb-ish force `F = K_CHARGE * q1 * q2 / max(d, D_MIN)²`, attractive for opposite signs, repulsive for like. Cap magnitude to avoid explosions. Skip neutral–neutral pairs (cheap). Start `K_CHARGE` so two opposites within ~120 px visibly lunge together over ~0.3 s. 3. **Zone re-tuning.** For each particle inside a zone: - **sour:** if `charge < +1`, accrue `zoneTimer += dt`; when it exceeds `ZONE_FLIP_TIME` (~1.2 s), bump `charge += 1` and reset. (So neutral won't *stay* neutral here — the sour air keeps shoving it positive. This IS the "your take hand has to fight the current" feel.) - **soapy:** if `charge != 0`, accrue timer; on threshold move `charge` one step toward 0. - **neutral zone / outside:** decay `zoneTimer` toward 0. - Reset a particle's `zoneTimer` whenever the player just acted on it, so player action feels responsive and the zone "reclaims" it afterward. 4. **Membrane repulsion.** For each *charged* particle (and Molly if charged) near a membrane within `thickness`: apply a strong force along the segment normal, pushing it back to its current side. Neutral particles get **zero** force — they pass freely. Tune so charged things clearly bounce and cannot muscle through; neutral things ignore it entirely. 5. **Bond springs.** For each bonded pair: spring toward `BOND_LENGTH` (~24 px), stiff, damped, so molecules hold shape but wobble slightly in the storm. 6. **Grab springs.** Grabbed particles are pulled toward their hand anchor (§8). 7. **Integrate.** `vel *= damping` (drag — start so speed halves ~every 0.1 s at rest), `pos += vel * dt`. Clamp to world bounds. Keep it cheap: a few dozen particles per room max. No spatial hashing needed at this scale. --- ## 8. THE SIGNATURE MECHANIC: bond hold-and-release (bonds.js) This is the game. Get it feeling *physical and earned*. The challenge is the **wrestle**, not the click. **Grab.** Hold the Grab input near a particle → it tethers to a *hand anchor* that tracks the aim reticle via a stiff spring. Molly can carry it. Grab again → second particle on the other hand (hold up to two). The two anchors sit slightly apart so the player positions each. **The activation barrier (this is what makes it fun).** When two grabbed opposite-charge particles approach: - Beyond ~60 px: mild attraction, easy. - Between ~25–45 px: a **repulsive bump** they must be shoved *through* — plus the constant jitter knocking them off line. This is the resistance. The player has to *force* them together and hold against the push-back. - Below `BOND_LOCK_DIST` (~22 px): they **snap** — bond forms, both auto-release from the hands, and the snap event fires (§9). **Feedback while wrestling:** - **Rumble ramps** with effort — low as they approach, peaking at the barrier. (Gamepad haptics; on keyboard, substitute a rising audio hum + a subtle screen tremor.) - A **rising hum** (audio.js) climbs in pitch/gain as they near the barrier. - The particles visibly *shove back* against the player's drag at the barrier. **Release / fail.** Let go of Grab while still fighting → the particles fly apart on the jitter, no bond, no punishment. Just try again. Failure is instant and cheap. **The payoff — snap event:** - **Hitstop** ~50 ms (freeze the sim, keep rendering). - **Screenshake:** add ~0.6 trauma (§9). - **Bass thunk + noise burst** (audio.js). - **Spark burst** at the bond point, colored by the two charges. - **Molly knockback:** shove her velocity away from the bond point. - The new bond glows at the seam for ~0.5 s. Tune the whole sequence until you'd do it fifty times just because it feels good. That's the bar. --- ## 9. Juice spec (fx.js, camera.js) Feel lives here. Starting values — tune freely. - **Screenshake (trauma model, à la "The Art of Screenshake").** Keep a `trauma` float 0..1. Offset = `MAX_SHAKE * trauma² * noise()`, plus small rotation `MAX_ROT * trauma² * noise()`. `MAX_SHAKE` ~20 px, `MAX_ROT` ~2°. Decay `trauma -= 1.5 * dt`. Snap adds ~0.6; membrane bounce ~0.15; charge pop ~0.05. - **Hitstop.** On big events set `freeze = 0.05 s`; during freeze `dt = 0` for the sim but rendering + effects continue. Sells impact. - **Particles/sparks.** Cheap circles/lines with velocity + fade. Bursts on snap, charge change, membrane bounce, heat pulse. - **Charge transitions.** `displayCharge` lerps ~150 ms; color and shape morph with it. A quick bright flash on the moment of change. - **Trails.** Fast-moving motes leave a short fading trail (a few ghost positions or additive alpha). Sells speed and the fluid. - **Ambient bloom.** Charged particles have an additive glow. Use `globalCompositeOperation = 'lighter'` for glows/sparks over a dark ground. **Audio (all synthesized, no files):** - **Charge pop:** short sine blip; pitch *up* for Give, *down* for Take. - **Hold hum:** oscillator whose gain + pitch ramp with bond proximity; kill on release/snap. - **Snap:** sine sweeping 80→50 Hz over ~120 ms + a filtered white-noise burst + short decay tail. The money sound — make it satisfying. - **Membrane bounce:** dull filtered thud. - **Zone ambience:** low drone / filtered noise, timbre tinted per zone; crossfade on zone change. --- ## 10. Zones & membrane recap (behavior already specced in §7) - **Zones** apply a subtle full-screen **tint** (sour = yellow-green, soapy = blue-violet, neutral = none) and swap the **ambient hum**. The gameplay effect is the recharge/discharge timer in §7.3. Keep the visual subtle — a wash, not a filter that hurts readability. - **Membrane** renders as a shimmering oily band. When a charged thing bounces, ripple + bounce thud. When a neutral thing passes, a soft "phase-through" shimmer. Molly passes only while neutral — desaturate the screen and drop the hum while she's neutral to sell "gone quiet." --- ## 11. Controls **Keyboard + mouse (must be fully playable):** - Move: WASD / arrows. Aim: mouse reticle. - **Give (right hand):** Left Mouse. **Take (left hand):** Right Mouse. - **Grab (hold):** Space (or hold Left Mouse variant — pick what feels best). - **Heat:** Q. **Light:** E. - **Self-charge:** if the reticle isn't over a particle, Give/Take act on **Molly herself** (this is how she strips neutral to cross the wall). Clean and discoverable. **Gamepad (support + rumble):** - Move: left stick. Aim: right stick (reticle offset from Molly). - Give: RT. Take: LT. Grab: A/× (hold). Heat: LB. Light: RB. - Rumble ramps during the bond wrestle (the core use of haptics). Unify both into a single `InputState` each frame so game code never checks device type. --- ## 12. Heat & Light (openers) - **Heat:** area impulse at the reticle, radius ~150 px — kicks all particles outward + spikes their jitter briefly; can break a bond if it stretches past `BOND_BREAK_DIST`. Loud, indiscriminate. Big trauma, big noise. - **Light:** single target under the reticle — flips its "mood." In the slice, Light toggles a target's charge sign (or triggers a scripted state on a special object). Silent, precise, one thing. - **The opposite-directions hook:** include at least one scripted "lock" object that moves its state one way under Heat and the other way under Light. It can be a stretch beat (Milestone G) — wire the hook even if only one puzzle uses it. --- ## 13. The scripted sequence (rooms.data.js) Ten beats. Each is a small hand-authored room (data). Camera follows Molly; reaching a room's `exit` rect loads the next. Some beats can merge if it plays better — use judgment. **No text in any of them.** | # | Room | What the player learns | How it's taught (no words) | |---|---|---|---| | 0 | **Wake** | Give/Take = push/pull | Dark, one drifting mote. Poking it charges it and it pushes away / pulls back. Fade in from black. | | 1 | **First toy** | Opposites grab | A cluster; charge one +, one −, they lunge together and stick. A gap to reach behind them. | | 2 | **The raft** | Bonds = hold-and-release | Two raft-halves drift apart, jittering. Wrestle them together → SNAP → ride the raft across a gap. | | 3 | **The wall** | The membrane reads charge | Throw charged debris → it bounces, no reaction. Drift a neutral bit → it passes through. | | 4 | **Go quiet** | Neutral = sneak | Strip Molly neutral (Take on herself) → screen desaturates → drift through the membrane. | | 5 | **Sour** | Zones re-tune your hands | World tints yellow-green. A mote you neutralize keeps snapping back to +. | | 6 | **The vault** | (setup) | A second membrane with the exit behind it, and a "key" object. Sour air present. | | 7 | **The heist** | Combine everything | Key is charged → bounces. Strip it neutral → it drifts through → sour air on the far side charges it → **trapped.** Exit opens. | | 8 | **Scale** | (tease) | Zoom/parallax reveal: this was one droplet; thousands more; something huge pulsing far off. Fade to end card. | Give each room optional `onEnter`/`onExit` hooks for the few scripted moments (Room 0 fade-in, Room 8 reveal). Don't build a scripting engine — just callbacks. --- ## 14. Build order — milestones (each independently runnable & testable) Build in this order. **After each milestone, stop and verify the stated test before moving on.** Do not build later milestones until the current one feels right. - **A — Grey box & life.** Canvas, fixed-timestep loop, letterbox scaling. Molly moves with drag + idle jitter; camera follows. A dozen particles drift with the storm jitter. No charge yet. *Test: does moving around and watching the shimmer feel alive and fluid, not floaty or dead?* - **B — Give/Take + charge + opposites-grab.** The core toy. Reticle targeting, Give/Take change charge, color+shape by charge (§6), charge interaction force (§7.2), charge pop audio. *Test (the kill-test): poke a mote and it charges; make two opposites lunge together and stick. Is just this fun to mess with for a minute?* - **C — Bond hold-and-release + full snap juice.** Grab/anchor springs, the activation barrier, rumble/hum ramp, hitstop, screenshake, sparks, knockback, snap audio (§8, §9). **Spend the most time here.** *Test: would you trigger the snap fifty times for fun? If not, keep tuning before proceeding.* - **D — Membrane.** Charged repulsion, neutral pass-through, Molly self-charge to cross, bounce/phase FX. *Test: charged things bounce, neutral things pass, and going neutral to sneak through feels sneaky.* - **E — Zones.** Sour/soapy/neutral recharge fields, tint wash, ambient hum crossfade. *Test: in a sour zone a neutralized mote visibly refuses to stay neutral.* - **F — Rooms & the sequence.** RoomManager, `rooms.data.js`, exit triggers, room transitions, wire beats 0–7. The heist must be solvable using only mechanics the earlier rooms taught. *Test: play start→heist with no instructions and no text. Does the heist click as "my own idea"?* - **G — Heat & Light + the opposite-directions lock.** The two openers and at least one puzzle using the heat-vs-light hook. *Test: heat feels like a crowbar, light like a scalpel; the opposite-reaction lock reads.* - **H — Bookends & polish.** Title card + start, Room 8 scale reveal, end card, audio mix pass, one full feel-tuning pass across `config.js`. *Test: the whole ten minutes plays clean, start to finish, 60 fps.* If time is short, **A–F is the real slice** (it contains the whole thesis). G and H are enhancement. --- ## 15. Definition of done - All beats 0–7 are playable start to finish, **with zero text on screen** during play. - The **bond snap feels genuinely good** (the fifty-times bar). - Charge state is readable instantly by **color and shape**, even in motion. - Keyboard+mouse fully playable; gamepad supported with rumble during the wrestle. - Holds **60 fps** in Chrome on modest hardware. - **All feel constants live in `config.js`** and can be tuned without touching logic. - Runs from a plain static server with no build step and no dependencies. --- ## 16. Tuning notes for whoever plays it next The feel lives in `config.js`. The three knobs that matter most, in order: 1. **`JITTER_ACCEL`** — the life of the world. Too low = dead; too high = unplayable soup. 2. **The bond barrier** (`BOND_ATTEMPT_RANGE`, barrier strength, `BOND_LOCK_DIST`) — the wrestle. This is the whole game's feel in three numbers. 3. **Snap juice** (`MAX_SHAKE`, hitstop duration, snap audio envelope) — the payoff. Tune those first, by feel, before touching anything else. --- *This is a prototype to answer one question: is the core loop fun? Build the smallest thing that answers it honestly. Everything else waits.*