Removes internal process references so the repo is presentable to an outside audience: dropped the internal build-retrospective doc (process log with internal terms, not useful to an outside reader), and reworded remaining internal references in code comments and module docs (agents.md cards, architecture.md, MODULES.md, index.html, package.json) to generic descriptions of what changed and when, with no behavior change -- full 47/47 suite re-run green after the scrub, every edit verified comment/doc-only via diff. Co-Authored-By: Claude Sonnet 5 <noreply@anthropic.com>
8.4 KiB
Isle Slingers — Architecture
Original architecture for a fully offline, canvas-rendered slingshot game.
No frameworks, no build step, no external assets. As of the physics + modularization update (S-2) the
JavaScript is a modularized tree: folders are modules, subfolders are
submodules, and every module/submodule carries its own agents.md.
Runtime model (plain <script> load, single page)
There is no bundler and no import/export in the browser. Modules are ordinary
UMD-style files loaded in a strict order by index.html; the test harness
(test/page.test.mjs) replays that exact order in a DOM stub, and the
headless suites (test/sim.test.mjs, test/balance.test.mjs) load the same
modules through Node's CommonJS require branch of each UMD wrapper.
Two kinds of module:
- Shared-scope modules — a group of submodules that must share one
lexical scope (the way a single file used to). Each submodule exports an
installer function
P -> void; the aggregator builds one shared object (Pfor physics,Lfor levels) and calls every installer against it. Submodules therefore reference each other's units asP.<unit>/L.<unit>— never as free names. This preserves the monolith's single-closure semantics exactly (proven by the golden-step hash: identical scripted playthroughs produce byte-identical state hashes). In the browser, submodules push their installer onto a global part list (ISLE_PHYSICS_PARTS/ISLE_LEVEL_PARTS); the aggregator (the last<script>of the group) consumes the list in order. - Self-contained modules —
audio,render,app. One file, one global (ISLE_AUDIO,ISLE_RENDERER, plus the page state object onwindow), no shared scope.
Load order (contract — do not reorder)
physics submodules : constants, state, aim, abilities, damage, step,
collision, topple, rules, camera (shared scope P)
physics/index.js : aggregator -> window.ISLE_CORE (the physics API)
levels/helpers : constructors + SLING anchor (shared scope L)
levels/data/* : one level definition each (shared scope L)
levels/index.js : aggregator -> window.ISLE_LEVELS {LEVELS, SLING}
audio/index.js : window.ISLE_AUDIO {AudioEngine}
render/index.js : window.ISLE_RENDERER {Renderer, VIEW_W, VIEW_H}
app/index.js : the page — input, screens, main loop
app (the former main.js) is the only module that touches DOM wiring;
render draws from the state object returned by ISLE_CORE.createGame;
audio plays the events the core appends to state.events.
Physics model (landed 2026-09-29)
The simulation is a deterministic impulse-ish engine: bodies integrate under
gravity at fixed 1/120 s, contacts resolve positionally, and damage routes
through two funnels (damageBlock, damageEnemy). What each physics-pass step
added, and where it lives:
- Boards tip and topple (S-3, Q1 = full continuous rotation). Blocks carry
a rotational DOF:
ang,angVel,pivoting. A resting block whose centre of mass lies outside the x-span of its support (ground, or the top face of a block below —topple.blockSupportSpan) pivots about the support-edge corner under gravity (point-mass COM,alpha = G·(comX−pivotX)/r²), a smooth continuous swing — no discrete snap. Two endings: a far corner reaches the ground line → it lands, impact speed|angVel|·rdeals self-damage and footprint damage (topplePathDamage: blocks take impact, enemies in the footprint can be killed); or the pivot is too high for any corner to reach the ground → the board is a pendulum and swings back, settling flat on its support (settleBack, with anti-livelock: it re-tips only on a real knock ≥ 6 px or a damaging hit). - Falling objects kill (S-4). Crush is velocity-direction-aware: the
moving box must be the body plowing into the enemy (
boxAlong > 30), so vertical drops and side plows both register and a merely resting box never does. One landing = one crush instance (per-enemy 0.12 s cooldown). Stacked block contact is mass-weighted inelastic, so a heavy block landing on a light plank transfers real momentum through the stack (chain-crush). - Landed birds stay physical (S-5, Q2 = real continuous gravity, no
lifetime timer). A bird that settles while ANOTHER bird is in play
(queued, or flying/aiming/landed) becomes
landed: it keeps integrating under gravity, falls when its support breaks, chain-damages the boards it lands on, and can kill enemies — for as long as the level runs. The LAST bird still spends exactly as before, so win/lose resolution is unchanged;rulesmust not declarelostwhile a landed bird is in the world, and level end clears landed bodies.
Data flow
input (drag/keys) -> app -> ISLE_CORE.launch / ISLE_CORE.triggerAbility
|
main loop (rAF, fixed 1/120s accumulator) -> ISLE_CORE.step(state, dt)
|
state = { birds, blocks, enemies, events, score, camera, status, ... }
|
Renderer.draw(ctx, state) + AudioEngine.play(event)
The core is deterministic and DOM-free: createGame(levelDef) returns a
plain state object; step(state, dt) mutates it and is safe to call from a
headless loop (that is how all physics tests run).
Invariants (the load-bearing contracts)
ISLE_COREpublic API is key-for-key stable (seejs/physics/index.js): consumers useISLE_CORE.WORLD,MATERIALS,BIRD_TYPES,ENEMY_TYPES,createGame,activeBird,aimVector,trajectory,launch,triggerAbility,step,starsFor,birdsRemaining,LAUNCH_POWER,MAX_PULL,MIN_PULL,BIRD_BONUS,AIR_DRAG,VIEW.ISLE_LEVELSpublic API:{ LEVELS, SLING }. Level geometry uses block centers; enemies sit with their bottom aty + rresting on ground (y=900) or a flat block top — the level-integrity test enforces this.- Physics timestep: 1/120 s in tests and the page accumulator; collision
response, crush damage and settle logic all assume
dtat that scale. - One global per module, attached to
windowin the browser and exported viamodule.exportsin Node. Shared-scope part lists (ISLE_PHYSICS_PARTS,ISLE_LEVEL_PARTS) are browser-only transport; Node resolves parts viarequire.
Test surface
test/sim.test.mjs(15) — headless: parses every file underjs/, DOM-id wiring, level integrity, determinism, stability, trajectory, abilities, win/lose, all 5 levels clearable, fort rest, aim framing.test/page.test.mjs(7) — DOM stub: boots the real page in browser order, click-through the UI, a full 8 s launch + sim, pause/mute/keys, launch direction, sling-band release.test/balance.test.mjs(5) — the balance spec: per-material break budgets and enemy kill budgets under full-power direct hits.test/topple.test.mjs(5) — S-3: overhanging board tips (COM past support edge), rotation is continuous, a fully-supported board never tips (negative control), tip impact damages a block in the fall path, falling board kills an enemy in the fall path.test/knifeedge.test.mjs(6) — S-3 regression: a knife-edge overhang (COM a hair past the edge, pivot too high to reach the ground) tips once and settles back to rest — no livelock; a real knock re-arms it.test/fallingkill.test.mjs(5) — S-4: chain-kill through a knocked plank, direct one-hit kill, repeated-impact kill (grum), one landing = one crush instance, and a resting block never crushes.test/landedbird.test.mjs(4) — S-5: perched bird falls when its platform breaks (Q2 real gravity), no drift on an intact board, chain-damages the board it lands on, kills an enemy in its fall path.
Total: 47/47. New physics behavior must ship with RED-watched tests in this same surface (every new spec was watched RED on the pre-change tree first).
Conventions for agents working in this tree
- Change a physics rule -> edit the one submodule that owns it (see each
agents.md); re-runnpm test(47/47) and re-check the golden differential against the pre-modularization core when touching anything that predates this modularization/physics pass. - Never re-introduce a flat file for a unit that has a submodule folder.
- Keep
index.htmlscript order in lockstep withtest/page.test.mjs'sFILESlist — both encode the load contract. - No new runtime dependencies; the game must stay fully offline.