2026-09-29 09:23:12 -05:00
|
|
|
/* ISLE SLINGERS — balance spec (Commander bug #3:
|
2026-09-28 17:20:17 -05:00
|
|
|
"the blocks and pig bad guys too hard to kill").
|
|
|
|
|
*
|
|
|
|
|
* Measures with the REAL core (same integrator/colliders the game runs),
|
|
|
|
|
* repeated identical full-power DIRECT hits, and asserts hit-to-kill
|
|
|
|
|
* budgets. These numbers are what a human with a decent aim actually
|
|
|
|
|
* gets — not the suite's omniscient perfect archer.
|
|
|
|
|
*
|
|
|
|
|
* Pinned by this spec:
|
|
|
|
|
* wood breaks in <= 3 full-power direct hits
|
|
|
|
|
* ice breaks in <= 2 full-power direct hits
|
|
|
|
|
* stone breaks in <= 4 full-power direct hits (was ~11: unkillable)
|
|
|
|
|
* grum dies in <= 2 medium-speed direct hits (soft lobs were ~0.9 dmg)
|
|
|
|
|
* king must still SURVIVE one full-power wren hit (sim.test.mjs pins hp3)
|
|
|
|
|
*/
|
Physics upgrade + repo modularization (RC-375D84)
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>
2026-09-29 08:21:18 -05:00
|
|
|
import ISLE from '../js/physics/index.js'; // RC-375D84 S-2: physics module aggregator (was js/core.js)
|
|
|
|
|
import levelsModule from '../js/levels/index.js'; // RC-375D84 S-2: levels module aggregator (was js/levels.js)
|
2026-09-28 17:20:17 -05:00
|
|
|
const LEVELS = levelsModule.LEVELS;
|
|
|
|
|
let passed = 0, failed = 0;
|
|
|
|
|
const failures = [];
|
|
|
|
|
function test(name, fn) {
|
|
|
|
|
try { fn(); passed++; console.log(' ok ' + name); }
|
|
|
|
|
catch (err) {
|
|
|
|
|
failed++; failures.push({ name, err });
|
|
|
|
|
console.log(' FAIL ' + name + ' — ' + (err && err.message || err));
|
|
|
|
|
}
|
|
|
|
|
}
|
|
|
|
|
function ok(cond, msg) { if (!cond) throw new Error(msg); }
|
|
|
|
|
|
|
|
|
|
const DT = 1 / 120;
|
|
|
|
|
const SLING = LEVELS[0].sling;
|
|
|
|
|
const PULL = 150; // full power
|
|
|
|
|
|
|
|
|
|
// Full-power launch at `deg` above horizontal (toward +x).
|
|
|
|
|
function fire(g, deg) {
|
|
|
|
|
const r = deg * Math.PI / 180;
|
|
|
|
|
const v = PULL * ISLE.LAUNCH_POWER;
|
|
|
|
|
const b = ISLE.activeBird(g);
|
|
|
|
|
b.x = SLING.x; b.y = SLING.y;
|
|
|
|
|
b.vx = v * Math.cos(r); b.vy = -v * Math.sin(r);
|
|
|
|
|
b.state = 'flying'; b.launchT = g.time;
|
|
|
|
|
g.launched++;
|
|
|
|
|
let guard = 0;
|
|
|
|
|
while (g.status !== 'won' && g.status !== 'lost' && guard++ < 1600) {
|
|
|
|
|
ISLE.step(g, DT);
|
|
|
|
|
if (b.state === 'spent') break;
|
|
|
|
|
}
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
function settle(g, frames = 40) { for (let i = 0; i < frames; i++) ISLE.step(g, DT); }
|
|
|
|
|
|
|
|
|
|
// Best (most damaging) full-power direct angle against one isolated block.
|
|
|
|
|
function bestDirectAngle(mat) {
|
|
|
|
|
const mk = () => {
|
|
|
|
|
const g = ISLE.createGame({ ...LEVELS[0], enemies: [], blocks: [{ x: 1100, y: 900 - 35, w: 70, h: 70, mat }] });
|
|
|
|
|
settle(g);
|
|
|
|
|
return g;
|
|
|
|
|
};
|
|
|
|
|
let best = null;
|
|
|
|
|
for (let deg = 0; deg <= 25; deg += 1) {
|
|
|
|
|
const g = mk();
|
|
|
|
|
const hp0 = g.blocks[0].maxHp;
|
|
|
|
|
fire(g, deg);
|
|
|
|
|
const dmg = hp0 - g.blocks[0].hp;
|
|
|
|
|
if (!best || dmg > best.dmg) best = { deg, dmg };
|
|
|
|
|
}
|
|
|
|
|
return best;
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
// Hits-to-break: repeated identical direct hits, hp carried over between birds.
|
|
|
|
|
function hitsToBreak(mat, deg) {
|
|
|
|
|
const spec = ISLE.MATERIALS[mat];
|
|
|
|
|
let hp = spec.hp, hits = 0;
|
|
|
|
|
while (hp > 0 && hits < 20) {
|
|
|
|
|
const g = ISLE.createGame({ ...LEVELS[0], enemies: [], blocks: [{ x: 1100, y: 900 - 35, w: 70, h: 70, mat }] });
|
|
|
|
|
settle(g);
|
|
|
|
|
g.blocks[0].hp = hp;
|
|
|
|
|
fire(g, deg);
|
|
|
|
|
const dmg = hp - g.blocks[0].hp;
|
|
|
|
|
hp -= dmg; hits++;
|
|
|
|
|
if (dmg <= 0) break; // never connects — treat as unbreakable
|
|
|
|
|
}
|
|
|
|
|
return { hits, connected: hits > 0 && hp <= 0 };
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
// Enemy: repeated full-power direct hits from the sling (flat, 0° — the
|
|
|
|
|
// player's most reliable connection), hp carried over between birds.
|
|
|
|
|
function enemyHitsToDie(kind) {
|
|
|
|
|
const spec = ISLE.ENEMY_TYPES[kind];
|
|
|
|
|
let hp = spec.hp, hits = 0;
|
|
|
|
|
while (hp > 0 && hits < 10) {
|
|
|
|
|
const g = ISLE.createGame({ ...LEVELS[0], enemies: [{ x: 1100, y: 876, kind }], blocks: [], birds: ['wren'] });
|
|
|
|
|
settle(g, 10);
|
|
|
|
|
const e = g.enemies[0];
|
|
|
|
|
e.hp = hp;
|
|
|
|
|
fire(g, 0);
|
|
|
|
|
const dmg = hp - e.hp;
|
|
|
|
|
hp -= dmg; hits++;
|
|
|
|
|
if (dmg <= 0) break; // never connects — treat as unkillable
|
|
|
|
|
}
|
|
|
|
|
return { hits, connected: hp <= 0 };
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
console.log('\n== ISLE SLINGERS balance spec ==');
|
|
|
|
|
|
|
|
|
|
const W = bestDirectAngle('wood');
|
|
|
|
|
const I = bestDirectAngle('ice');
|
|
|
|
|
const S = bestDirectAngle('stone');
|
|
|
|
|
console.log(` (direct angles — wood ${W.deg}° ${W.dmg.toFixed(0)}dmg, ice ${I.deg}° ${I.dmg.toFixed(0)}dmg, stone ${S.deg}° ${S.dmg.toFixed(0)}dmg)`);
|
|
|
|
|
|
|
|
|
|
test('wood breaks in <= 3 full-power direct hits', () => {
|
|
|
|
|
const r = hitsToBreak('wood', W.deg);
|
|
|
|
|
ok(r.connected, 'wood block never broke');
|
|
|
|
|
ok(r.hits <= 3, `wood took ${r.hits} full-power direct hits (budget 3)`);
|
|
|
|
|
});
|
|
|
|
|
test('ice breaks in <= 2 full-power direct hits', () => {
|
|
|
|
|
const r = hitsToBreak('ice', I.deg);
|
|
|
|
|
ok(r.connected, 'ice block never broke');
|
|
|
|
|
ok(r.hits <= 2, `ice took ${r.hits} full-power direct hits (budget 2)`);
|
|
|
|
|
});
|
|
|
|
|
test('stone breaks in <= 4 full-power direct hits (was ~11: unkillable)', () => {
|
|
|
|
|
const r = hitsToBreak('stone', S.deg);
|
|
|
|
|
ok(r.connected, 'stone block never broke (unkillable wall)');
|
|
|
|
|
ok(r.hits <= 4, `stone took ${r.hits} full-power direct hits (budget 4)`);
|
|
|
|
|
});
|
|
|
|
|
test('grum dies in <= 2 full-power direct hits (soft lobs currently ~0.9 dmg)', () => {
|
|
|
|
|
const r = enemyHitsToDie('grum');
|
|
|
|
|
ok(r.connected, 'grum never died');
|
|
|
|
|
ok(r.hits <= 2, `grum took ${r.hits} full-power hits (budget 2)`);
|
|
|
|
|
});
|
|
|
|
|
test('king still survives one full-power flat wren hit (pinned hp3)', () => {
|
|
|
|
|
const h = ISLE.createGame({ ...LEVELS[0], enemies: [{ x: 1100, y: 876, kind: 'king' }], blocks: [], birds: ['wren'] });
|
|
|
|
|
settle(h, 10);
|
|
|
|
|
fire(h, 0); // full-power flat from the sling — the player's hardest reliable hit
|
|
|
|
|
const hp = h.enemies[0].hp;
|
|
|
|
|
ok(hp < 3, 'full-power flat wren shot did not connect with the king');
|
|
|
|
|
ok(hp > 0, `king died to a single full-power wren hit (hp ${hp}) — must survive (pinned by sim.test.mjs)`);
|
|
|
|
|
});
|
|
|
|
|
|
|
|
|
|
console.log(`\n== ${passed} passed, ${failed} failed ==`);
|
|
|
|
|
if (failed) {
|
|
|
|
|
process.exit(1);
|
|
|
|
|
}
|