97 lines
4.3 KiB
JavaScript
97 lines
4.3 KiB
JavaScript
|
|
// 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);
|