Isle-Slingers/test/topple.test.mjs

97 lines
4.3 KiB
JavaScript
Raw Permalink Normal View History

2026-09-29 09:24:13 -05:00
// test/topple.test.mjs -- S-3: board tip/topple physics
// Q1 (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);