A nanoscale arcade game. Shoot charge across the room, wrestle atoms together until they snap, and contain a chain reaction before it eats the field. Vanilla JS + Canvas 2D, no dependencies, no build step. Core systems: - Charge: give/take as the only two verbs. Opposites attract, likes repel. - Laserhands: ranged proton/vacuum bolts with real travel time. Bolts are charged bodies, so they curve through the field — bank shots are real. Give spends from an 8-proton ring, take earns them back. - The wrestle: bonds can't form on their own. Every pair has an activation barrier that parks them at arm's length; Molly's hands are the only way over it. Hold, squeeze, release -> SNAP. - Radicals: the enemy, with no mind. Every 0.32s one grabs its nearest neighbour, completes itself, and hands the wound on. Can't be shot dead — terminated by wrestling two together (barrierless, so 0.38s vs 1.56s). Two that drift into contact self-quench, which gives outbreaks a natural equilibrium instead of unbounded growth. - Overload: keep feeding one atom and it goes critical. The blast tips its neighbours, so density decides whether you get one pop (34 atoms) or 84 detonations from a single seed (116 atoms). - CASCADE: eight arenas of escalating density with finite ignition waves. Clear the field to advance; sustained neglect melts it down. - Membrane, sour/soapy zones, and the seven wordless tutorial rooms. Non-obvious constraints, documented at each constant: - BARRIER_FORCE must exceed MAX_CHARGE_FORCE, or atoms bond themselves and the wrestle (the whole game) gets skipped. - Bond lock distance scales with atom radius. A flat value meant two carbons could never bond at all, since collision held them further apart than the lock — invisible because hydrogen worked fine. - RAD_SPLIT_R must exceed the field's natural packing distance (set by the barrier), or outbreaks can only crawl and never bloom. Testing: 39 headless sim tests run the real physics in Node — no browser, no canvas. The sim draws all randomness from a seeded RNG so runs are reproducible. Perf is 0.32ms/sim-step at 116 atoms. Dev server strips a /v<timestamp>/ path prefix that index.html adds to its entry import, because browsers cache the *parsed* ES module by URL and no-cache headers alone don't touch it. Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
43 lines
1.2 KiB
JavaScript
43 lines
1.2 KiB
JavaScript
import { CFG } from './config.js';
|
|
import { FX } from './fx.js';
|
|
|
|
// ============================================================
|
|
// THE JUICE BUDGET.
|
|
// Written FIRST, before bolts/detonations/cascades can stack on it.
|
|
// Without a cap, a chain reaction reads as "the game crashed"
|
|
// rather than "the game is going off".
|
|
// ============================================================
|
|
|
|
const win = []; // [{t, ms}] hitstop spend inside the window
|
|
let clock = 0;
|
|
|
|
export const Juice = {
|
|
tick(dt) {
|
|
clock += dt;
|
|
while (win.length && clock - win[0].t > CFG.JUICE_WINDOW) win.shift();
|
|
},
|
|
|
|
spent() {
|
|
let s = 0;
|
|
for (const e of win) s += e.ms;
|
|
return s;
|
|
},
|
|
|
|
// request hitstop; returns what was actually granted
|
|
hitstop(ms) {
|
|
const room = Math.max(0, CFG.JUICE_HITSTOP_CAP - this.spent());
|
|
const grant = Math.min(ms, room);
|
|
if (grant > 0) {
|
|
win.push({ t: clock, ms: grant });
|
|
FX.freeze(grant / 1000);
|
|
}
|
|
return grant;
|
|
},
|
|
|
|
// trauma is self-limiting (clamped 0..1 + decay) but funnel it here
|
|
// so every source is auditable in one place
|
|
trauma(camera, v) { camera.addTrauma(v); },
|
|
|
|
reset() { win.length = 0; clock = 0; },
|
|
};
|