Delivered by Hermies Qwen, gated and deployed by Relay. - Board tip/topple: full continuous rotation when a board's center of mass passes its support edge (Q1: smooth pivot swing, no discrete snap). Falling boards deal impact damage and can kill enemies in their fall path. - Falling-object impact damage: blocks and birds can now kill enemies from vertical drops and side plows (velocity-aware crush gate), chain momentum transfers correctly through stacks (mass-weighted contact), repeated impacts finish tougher enemies. - Landed-bird physics: a bird resting on a board remains a real physical body under continuous gravity (Q2: no lifetime timer). If its platform is destroyed it falls, damages what it lands on (chain reaction), and can kill enemies. - Repo modularized: folders = modules, subfolders = submodules, architecture.md + MODULES.md at root, an agents.md card in every module and submodule (17 total). - Test suite grown from 27 to 47 (20 new RED-watched tests for the above behaviors), all green. Golden differential against the original core confirmed the only behavioral changes are the intended ones. Live at http://10.244.136.121:8093/, independently re-verified by Relay (full suite re-run, module structure check, headless Chromium smoke) before this push. 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 RC-375D84 (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 (RC-375D84, 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 RC-375D84 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(27/27) and the golden-step hash (/opt/data/s2_golden.mjsagainst/opt/data/core.orig.js) when touching anything that predates RC-375D84 behavior. - 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.