/* ISLE SLINGERS — balance spec (Commander bug #3: "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) */ 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) 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); }