2026-09-29 11:38:06 -05:00
|
|
|
// test/landedbird.test.mjs -- landed bird remains a physical body.
|
|
|
|
|
// Design spec (verbatim): "When a board is broken from underneath a bird it just
|
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 08:21:18 -05:00
|
|
|
// 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."
|
2026-09-29 11:38:06 -05:00
|
|
|
// Q2 (ruling): REAL CONTINUOUS GRAVITY, no fixed lifetime timer -- the
|
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 08:21:18 -05:00
|
|
|
// 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);
|