122 lines
6.2 KiB
JavaScript
122 lines
6.2 KiB
JavaScript
|
|
// test/fallingkill.test.mjs -- RC-375D84 S-4: falling-object impact damage.
|
||
|
|
// Commander (verbatim in the RC): "falling objects need to be able to kill the
|
||
|
|
// enemy's as well not just the birds". Under the RC-0D193E-tuned balance
|
||
|
|
// (block maxVel clamps: stone 480, ice 560, wood 520) blocks kill a mogg in
|
||
|
|
// one impact (1.07-1.45) and a grum (hp 2) in two impacts (1.07 x 2 = 2.14).
|
||
|
|
// S-4 fixes the two gaps that made falling blocks unable to kill, plus a
|
||
|
|
// double-fire defect the fix exposed:
|
||
|
|
// (a) crush direction gate -- the old "ny < -0.3" (enemy ABOVE the box) meant
|
||
|
|
// a block dropped STRAIGHT DOWN onto an enemy never registered. Now the
|
||
|
|
// box must simply be the body moving into the enemy (boxAlong > 30).
|
||
|
|
// (b) chain momentum -- resolveBoxPair used a mass-less (a.vy+c.vy)/2, so a
|
||
|
|
// heavy stone landing on a light plank handed the plank only half the
|
||
|
|
// speed (240 px/s, UNDER the 330 mogg threshold). Mass-weighted inelastic
|
||
|
|
// contact transfers the real momentum (plank -> 466 px/s, OVER), so a
|
||
|
|
// knocked plank is itself a live projectile that can kill.
|
||
|
|
// (c) crush cooldown + inelastic transfer -- with (a) in place a box resting
|
||
|
|
// on an enemy at terminal velocity would otherwise re-fire damage every
|
||
|
|
// frame (4 frames = a king gone). One landing = one crush instance
|
||
|
|
// (0.12s per-enemy cooldown, same idea as the bird-hit gate); the box
|
||
|
|
// also sheds a mass-scaled share of its normal velocity so it does not
|
||
|
|
// phase through the enemy.
|
||
|
|
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: 'fk-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);
|
||
|
|
}
|
||
|
|
|
||
|
|
// arm 1 (chain kill): heavy stone 200x200 (mass 62.9) dropped onto an ice
|
||
|
|
// plank 120x30 (mass 1.8) that overhangs a mogg. Mass-weighted contact drives
|
||
|
|
// the plank to ~466 px/s (the old mass-less average would give ~240, UNDER the
|
||
|
|
// 330 threshold) -> the plank crushes the mogg for 1.01 >= hp 1. KILL through
|
||
|
|
// the stack: the falling stone kills the enemy via the plank it knocks.
|
||
|
|
{
|
||
|
|
const g = game({
|
||
|
|
blocks: [
|
||
|
|
{ x: 1300, y: 300, w: 200, h: 200, mat: 'stone' }, // bottom 400
|
||
|
|
{ x: 1300, y: 767, w: 120, h: 30, mat: 'ice' } // 752..782, overhangs mogg at 1340
|
||
|
|
],
|
||
|
|
enemies: [{ x: 1340, y: 876, kind: 'mogg' }]
|
||
|
|
});
|
||
|
|
step(g, 5.0);
|
||
|
|
const e = g.enemies[0];
|
||
|
|
check('chain: heavy stone drives a plank fast enough to KILL the mogg it sweeps', e.dead,
|
||
|
|
'mogg hp=' + e.hp + ' dead=' + e.dead);
|
||
|
|
}
|
||
|
|
|
||
|
|
// arm 2 (direct one-hit kill): stone dropped straight onto a mogg.
|
||
|
|
{
|
||
|
|
const g = game({ blocks: [{ x: 1200, y: 280, w: 120, h: 120, mat: 'stone' }],
|
||
|
|
enemies: [{ x: 1200, y: 876, kind: 'mogg' }] });
|
||
|
|
step(g, 4.0);
|
||
|
|
const e = g.enemies[0];
|
||
|
|
check('direct: a falling stone kills the mogg it lands on (one hit)', e.dead,
|
||
|
|
'mogg hp=' + e.hp + ' dead=' + e.dead);
|
||
|
|
}
|
||
|
|
|
||
|
|
// arm 3 (repeated-impact kill): a grum (hp 2) dies to two ice drops landing at
|
||
|
|
// DISJOINT spots (offset 90px, blocks are 120 wide so they never touch each
|
||
|
|
// other). 1.07 x 2 = 2.14 >= 2.
|
||
|
|
{
|
||
|
|
const g = game({ enemies: [{ x: 1200, y: 868, kind: 'grum' }] });
|
||
|
|
g.blocks.push({ id: 90, x: 1160, y: 280, w: 120, h: 120, mat: 'ice', hp: 480, maxHp: 480, vx: 0, vy: 0, rest: 1, ang: 0, angVel: 0, pivoting: false, mass: 0.7 * 120 * 120 / 1400 });
|
||
|
|
step(g, 4.0); // first impact at x=1160: 1.07
|
||
|
|
const afterFirst = g.enemies[0].hp;
|
||
|
|
g.blocks.push({ id: 91, x: 1250, y: 280, w: 120, h: 120, mat: 'ice', hp: 480, maxHp: 480, vx: 0, vy: 0, rest: 1, ang: 0, angVel: 0, pivoting: false, mass: 0.7 * 120 * 120 / 1400 });
|
||
|
|
step(g, 4.0); // second impact at x=1250: 1.07
|
||
|
|
const e = g.enemies[0];
|
||
|
|
check('repeated: a grum dies to two disjoint ice drops (1.07 x 2 >= 2)', e.dead,
|
||
|
|
'after 1st=' + afterFirst.toFixed(2) + ' final hp=' + e.hp + ' dead=' + e.dead);
|
||
|
|
}
|
||
|
|
|
||
|
|
// arm 4 (one impact, no machine-gun): a LOW stone drop (70px fall -> v=458,
|
||
|
|
// below stone's 480 clamp) deals 0.96 < 1: the mogg survives with EXACTLY one
|
||
|
|
// damage instance even though the stone rests on it for the whole window.
|
||
|
|
// Without the cooldown, the resting box (still registered at terminal speed)
|
||
|
|
// would re-fire every frame and finish it.
|
||
|
|
{
|
||
|
|
const g = game({ blocks: [{ x: 1200, y: 722, w: 120, h: 120, mat: 'stone' }],
|
||
|
|
enemies: [{ x: 1200, y: 876, kind: 'mogg' }] });
|
||
|
|
let crushEvents = 0;
|
||
|
|
const origPush = g.events.push.bind(g.events);
|
||
|
|
g.events.push = function (ev) { if (ev.type === 'crush') crushEvents++; return origPush(ev); };
|
||
|
|
step(g, 4.0);
|
||
|
|
const e = g.enemies[0];
|
||
|
|
// impact ~462 px/s (70px fall + ~5px detection overlap): dmg ~0.99, survives
|
||
|
|
// with a hair of hp -- and the resting box must not re-fire (exactly 1 event).
|
||
|
|
check('one landing = one crush instance (no per-frame re-fire)',
|
||
|
|
crushEvents === 1 && !e.dead && e.hp > 0 && e.hp < 0.05,
|
||
|
|
'crushEvents=' + crushEvents + ' mogg hp=' + e.hp.toFixed(3));
|
||
|
|
}
|
||
|
|
|
||
|
|
// arm 5 (negative: under threshold): a wood block dropped only 36px
|
||
|
|
// (v=328 < the 330 mogg crushVel) must deal zero crush damage.
|
||
|
|
{
|
||
|
|
// A block resting ON the mogg with no impact velocity must never crush it:
|
||
|
|
// the crush gate is velocity-based (boxAlong > 30), so a box that is merely
|
||
|
|
// sitting there (boxAlong ~ 0) deals nothing. Guards the per-frame
|
||
|
|
// machine-gun failure mode from the other direction.
|
||
|
|
const g = game({ blocks: [{ x: 1200, y: 832, w: 60, h: 40, mat: 'wood', hp: 1300, maxHp: 1300, vx: 0, vy: 0, rest: 1, ang: 0, angVel: 0, pivoting: false, mass: 1.0 * 60 * 40 / 1400 }],
|
||
|
|
enemies: [{ x: 1200, y: 876, kind: 'mogg' }] });
|
||
|
|
step(g, 4.0);
|
||
|
|
const e = g.enemies[0];
|
||
|
|
check('negative: a block RESTING on the mogg (no impact velocity) never crushes it', e.hp === 1 && !e.dead,
|
||
|
|
'mogg hp=' + e.hp);
|
||
|
|
}
|
||
|
|
|
||
|
|
console.log('== ' + passed + ' passed, ' + failed + ' failed ==');
|
||
|
|
process.exit(failed ? 1 : 0);
|