// test/topple.test.mjs -- RC-375D84 S-3: board tip/topple physics // Q1 (Commander ruling, RC-375D84 turn 2): FULL CONTINUOUS ROTATION -- boards // rotate smoothly through the fall (no discrete settle-then-snap); COM past // the support edge triggers the tip; the tip lands with impact damage and // falling objects can kill enemies. // // Every arm was proven RED on the pre-S-3 tree: blocks carried no rotation // degree of freedom, so nothing could ever tip. import { createRequire } from 'node:module'; const require = createRequire(import.meta.url); const ISLE = require('../js/physics/index.js'); const DT = 1 / 120; let passed = 0, failed = 0; function check(name, cond, detail) { if (cond) { passed++; console.log(' PASS ' + name); } else { failed++; console.log(' FAIL ' + name + (detail ? ' -- ' + detail : '')); } } function step(g, secs) { const n = Math.round(secs / DT); for (let i = 0; i < n; i++) ISLE.step(g, DT); } // Real createGame, custom level definition (no birds needed for these arms). function game(def) { const lvl = Object.assign({ name: 'topple-test', sling: { x: 260, y: 560 }, birds: [], blocks: [], enemies: [] }, def); return ISLE.createGame(lvl); } // Geometry (verified against the pendulum prototype): // ground y=900. Stone pillar 200x200 at (1000,800): x 900..1100, top y=700. // Wood board 300x30 at (1200,685): x 1050..1350, COM at x=1200 -- 100px // PAST the pivot edge (pillar top-right corner x=1100, y=700). const PILLAR = { x: 1000, y: 800, w: 200, h: 200, mat: 'stone' }; const BOARD_OVER = { x: 1200, y: 685, w: 300, h: 30, mat: 'wood' }; const BOARD_CENTERED = { x: 1000, y: 685, w: 300, h: 30, mat: 'wood' }; // COM at x=1000, inside span 900..1100 // impact victim: ice 200x30 at (1250,885) -- under the board's fall path const VICTIM = { x: 1250, y: 885, w: 200, h: 30, mat: 'ice' }; console.log('== ISLE SLINGERS topple (S-3) =='); // ---- arm 1: COM past the support edge -> the board tips end-over-end ---- { const g = game({ blocks: [PILLAR, BOARD_OVER] }); const board = g.blocks[1]; step(g, 2.0); const ang = board.ang || 0; check('overhanging board tips (COM past support edge)', ang > 0.5, 'ang=' + ang + 'rad (' + (ang * 180 / Math.PI).toFixed(1) + 'deg)'); } // ---- arm 2: rotation is CONTINUOUS (Q1) -- smooth swing, intermediate // angles, no discrete settle-then-snap ---- { const g = game({ blocks: [PILLAR, BOARD_OVER] }); const board = g.blocks[1]; const samples = []; for (let i = 0; i < 96; i++) { ISLE.step(g, DT); samples.push(board.ang || 0); } let maxDelta = 0; for (let i = 1; i < samples.length; i++) maxDelta = Math.max(maxDelta, Math.abs(samples[i] - samples[i - 1])); const finalAng = samples[samples.length - 1]; const sawIntermediate = samples.some(a => a > 0.1 && a < finalAng - 0.1); check('rotation is continuous (smooth swing, intermediate angles)', sawIntermediate && maxDelta < 0.05, 'samples(0.8s, every 4th)=' + samples.filter((_, i) => i % 4 === 0).map(a => a.toFixed(3)).join(',') + ' maxStep=' + maxDelta.toFixed(4) + ' final=' + finalAng.toFixed(3)); } // ---- arm 3: a fully-supported board does NOT tip (negative control) ---- { const g = game({ blocks: [PILLAR, BOARD_CENTERED] }); const board = g.blocks[1]; step(g, 2.0); check('fully-supported board stays put (no tip)', (board.ang || 0) === 0 && (board.vy || 0) === 0, 'ang=' + board.ang + ' vy=' + board.vy); } // ---- arm 4: the tip lands with IMPACT that damages a block below ---- { const g = game({ blocks: [PILLAR, BOARD_OVER, VICTIM] }); const victim = g.blocks[2]; step(g, 3.0); check('tipping board impact damages a block in its fall path', victim.hp < ISLE.MATERIALS.ice.hp, 'victim hp=' + victim.hp + ' (full=' + ISLE.MATERIALS.ice.hp + ') boardAng=' + ((g.blocks[1].ang || 0) * 180 / Math.PI).toFixed(1) + 'deg'); } // ---- arm 5: a tipping board KILLS an enemy in its fall path (objective (b)) ---- { const g = game({ blocks: [PILLAR, BOARD_OVER], enemies: [{ x: 1250, y: 876, kind: 'mogg' }], // r=24, stands under the fall path }); const e = g.enemies[0]; step(g, 3.0); check('falling board kills an enemy in its fall path', e.dead, 'enemy hp=' + e.hp + ' dead=' + e.dead + ' boardAng=' + ((g.blocks[1].ang || 0) * 180 / Math.PI).toFixed(1) + 'deg'); } console.log('== ' + passed + ' passed, ' + failed + ' failed =='); process.exit(failed ? 1 : 0);