// test/landedbird.test.mjs -- landed bird remains a physical body. // Design spec (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 (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);