Isle-Slingers/test/landedbird.test.mjs
Relay dc68c6e3dd 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 13:21:18 +00:00

151 lines
6 KiB
JavaScript

// test/landedbird.test.mjs -- RC-375D84 S-5: landed bird remains a physical body.
// Commander (verbatim): "When a board is broken from underneath a bird it just
// floats. Gravity needs to work for the birds as well so if the platform falls
// from underneath them they can fall causing more damage to any boards that they
// fall on like a chain reaction and the birds can also kill more enemy's that way
// as well. That is AFTER a bird has been shot from a slingshot and another bird is
// already active."
// Q2 (Commander ruling): REAL CONTINUOUS GRAVITY, no fixed lifetime timer -- the
// bird is a body as long as it exists.
//
// The pre-S-5 defect: spendBird() marked a settled bird 'spent' and zeroed its
// velocity; stepPhysics only integrated the ACTIVE bird, so a settled bird was a
// visual corpse -- when its platform broke it FLOATED in place (never fell).
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 game(def) {
const lvl = Object.assign({ name: 'lb-test', sling: { x: 260, y: 560 }, birds: [], blocks: [], enemies: [] }, def);
return ISLE.createGame(lvl);
}
function step(g, secs) {
const n = Math.round(secs / DT);
for (let i = 0; i < n; i++) ISLE.step(g, DT);
}
// helper: put a bird in a settled/landed state on a board, with a queued second
// bird active (the "another bird is already active" window).
function setupPerched() {
// board 200x30 at (1200,765): x 1100..1300, top 750.
const g = game({
blocks: [{ x: 1200, y: 765, w: 200, h: 30, mat: 'wood' }],
birds: ['wren', 'wren'], // [0] will be perched, [1] the 'already active' next bird
});
// bird 0: perched on the board, settled (no velocity).
const b0 = g.birds[0];
b0.state = 'landed';
b0.x = 1200; b0.y = 750 - b0.r; // resting on the board top
b0.vx = 0; b0.vy = 0;
// bird 1: active, aiming on the sling.
g.activeBird = 1;
g.birds[1].state = 'aiming';
g.birds[1].x = g.level.sling.x; g.birds[1].y = g.level.sling.y;
return g;
}
// arm 1 (THE S-5 behaviour): the perched bird FALLS when its board breaks.
// The board rests on two stone pillars (so the platform is stable); the bird
// is perched on the board. A bird that is a corpse (pre-S-5) would stay at its
// perch forever while the board falls away. The landed bird must drop toward
// the ground (y rises well past the perch).
{
const g = game({
blocks: [
{ x: 1120, y: 840, w: 40, h: 120, mat: 'stone' }, // pillar L (top 780)
{ x: 1280, y: 840, w: 40, h: 120, mat: 'stone' }, // pillar R (top 780)
{ x: 1200, y: 765, w: 200, h: 30, mat: 'wood' } // board 750..780 on the pillars
],
birds: ['wren', 'wren']
});
const b0 = g.birds[0];
b0.state = 'landed';
b0.x = 1200; b0.y = 750 - b0.r;
b0.vx = 0; b0.vy = 0;
g.activeBird = 1; g.birds[1].state = 'aiming';
g.birds[1].x = g.level.sling.x; g.birds[1].y = g.level.sling.y;
const board = g.blocks[2];
step(g, 0.5);
const yIntact = b0.y;
board.hp = 0; // break the board out from under the bird
step(g, 2.0);
check('perched bird FALLS when its platform breaks (Q2 real gravity)', b0.y > yIntact + 80,
'y intact=' + yIntact.toFixed(0) + ' -> after break=' + b0.y.toFixed(0) + ' (ground=' + ISLE.WORLD.ground + ')');
}
// arm 2: a landed bird on a stable board (pillars + board) does NOT drift.
{
const g = game({
blocks: [
{ x: 1120, y: 840, w: 40, h: 120, mat: 'stone' },
{ x: 1280, y: 840, w: 40, h: 120, mat: 'stone' },
{ x: 1200, y: 765, w: 200, h: 30, mat: 'wood' }
],
birds: ['wren', 'wren']
});
const b0 = g.birds[0];
b0.state = 'landed';
b0.x = 1200; b0.y = 750 - b0.r;
b0.vx = 0; b0.vy = 0;
g.activeBird = 1; g.birds[1].state = 'aiming';
g.birds[1].x = g.level.sling.x; g.birds[1].y = g.level.sling.y;
const x0 = b0.x, y0 = b0.y;
step(g, 2.0);
check('landed bird on an intact board stays put (no drift, no float-away)', Math.abs(b0.x - x0) < 2 && Math.abs(b0.y - y0) < 3,
'x ' + x0.toFixed(1) + '->' + b0.x.toFixed(1) + ' y ' + y0.toFixed(1) + '->' + b0.y.toFixed(1));
}
// arm 3 (chain reaction): the falling bird damages the board it lands on.
// Bird perched high, board beneath it breaks, a lower board catches it -- the
// lower board must take impact damage.
{
const g = game({
blocks: [
{ x: 1200, y: 600, w: 120, h: 30, mat: 'ice' }, // upper perch (breaks)
{ x: 1200, y: 880, w: 200, h: 40, mat: 'ice' } // lower catcher
],
birds: ['wren', 'wren']
});
const b0 = g.birds[0];
b0.state = 'landed';
b0.x = 1200; b0.y = 585 - b0.r; // on the upper perch top (585)
b0.vx = 0; b0.vy = 0;
g.activeBird = 1;
g.birds[1].state = 'aiming';
g.birds[1].x = g.level.sling.x; g.birds[1].y = g.level.sling.y;
const catcher = g.blocks[1];
const catcherHp0 = catcher.hp;
g.blocks[0].hp = 0; // break the perch
step(g, 2.0);
check('falling bird damages the board it lands on (chain reaction)', catcher.hp < catcherHp0,
'catcher hp ' + catcherHp0 + ' -> ' + catcher.hp.toFixed(0));
}
// arm 4 (kill): the falling bird can kill an enemy in its fall path.
{
const g = game({
blocks: [{ x: 1200, y: 600, w: 120, h: 30, mat: 'ice' }],
enemies: [{ x: 1200, y: 876, kind: 'mogg' }],
birds: ['wren', 'wren']
});
const b0 = g.birds[0];
b0.state = 'landed';
b0.x = 1200; b0.y = 585 - b0.r;
b0.vx = 0; b0.vy = 0;
g.activeBird = 1;
g.birds[1].state = 'aiming';
g.birds[1].x = g.level.sling.x; g.birds[1].y = g.level.sling.y;
const e = g.enemies[0];
g.blocks[0].hp = 0; // break the perch -> bird drops onto the mogg
step(g, 2.5);
check('falling bird KILLS an enemy in its fall path', e.dead,
'mogg hp=' + e.hp + ' dead=' + e.dead + ' birdY=' + b0.y.toFixed(0));
}
console.log('== ' + passed + ' passed, ' + failed + ' failed ==');
process.exit(failed ? 1 : 0);