Files
MeatSlicer/test/repro_boss2.js

151 lines
6.5 KiB
JavaScript

// Repro hunt: simulate a LONG Gore Crown (floor 2 boss) fight with a
// realistically behaving player, watching for exceptions / runaway growth.
'use strict';
const fs = require('fs');
const path = require('path');
const vm = require('vm');
function fakeCtx() {
return new Proxy({}, {
get(t, prop) {
if (prop === 'measureText') return () => ({ width: 50 });
if (typeof prop === 'string' && !(prop in t)) t[prop] = /^[a-z]/.test(prop) ? function () {} : undefined;
return t[prop];
},
set(t, prop, v) { t[prop] = v; return true; },
});
}
const listeners = {};
const canvasStub = {
width: 960, height: 640, style: {},
getContext: () => fakeCtx(),
addEventListener: (ev, fn) => { (listeners['canvas:' + ev] = listeners['canvas:' + ev] || []).push(fn); },
getBoundingClientRect: () => ({ left: 0, top: 0, width: 960, height: 640 }),
};
const sandbox = {
console,
performance: { now: () => simTime },
requestAnimationFrame: () => {},
localStorage: { getItem: () => null, setItem: () => {} },
document: { getElementById: () => canvasStub },
Image: class { constructor() { this.width = 32; this.height = 32; } set src(v) { this.onerror && this.onerror(); } },
Audio: class { constructor() { this.volume = 0; this.paused = true; } play() { return Promise.resolve(); } pause() {} },
AudioContext: undefined,
setTimeout: (fn) => fn(),
Math, JSON, Object, Array, Number, String, Boolean, Promise, Uint8ClampedArray, Proxy, Reflect, Set, Map, Date, Error, RegExp, parseInt, parseFloat, isNaN, Infinity, NaN, undefined,
};
sandbox.window = sandbox;
sandbox.globalThis = sandbox;
sandbox.window.addEventListener = (ev, fn) => { (listeners['win:' + ev] = listeners['win:' + ev] || []).push(fn); };
vm.createContext(sandbox);
for (const f of ['utils', 'state', 'input', 'sfx', 'audio', 'sprites', 'particles', 'weapons',
'bullets', 'pickups', 'items', 'perks', 'enemies', 'bosses', 'rooms', 'player', 'hud', 'main']) {
vm.runInContext(fs.readFileSync(path.join(__dirname, '..', 'js', f + '.js'), 'utf8'), sandbox, { filename: f + '.js' });
}
vm.runInContext('Object.assign(this, { G, Input, WEAPONS, Music, W, H, WALL })', sandbox);
let simTime = 0;
const ctx = sandbox;
let SYS = 'init';
function step(n) {
for (let i = 0; i < n; i++) {
simTime += 16;
// instrumented play-mode dispatch so the last label before a hang = culprit
if (ctx.G.mode === 'play') {
SYS = 'updatePlayer'; ctx.updatePlayer(0.016);
SYS = 'updateRoom'; ctx.updateRoom(0.016);
SYS = 'updateEnemies'; ctx.updateEnemies(0.016);
SYS = 'updateTelegraphs'; ctx.updateTelegraphs(0.016);
SYS = 'updateBullets'; ctx.updateBullets(0.016);
SYS = 'updateHazards'; ctx.updateHazards(0.016);
SYS = 'updatePickups'; ctx.updatePickups(0.016);
SYS = 'updateParticles'; ctx.updateParticles(0.016);
SYS = 'drawToastsUpdate'; ctx.drawToastsUpdate(0.016);
SYS = 'draw'; ctx.draw();
} else {
SYS = 'update(' + ctx.G.mode + ')';
ctx.update(0.016);
SYS = 'draw'; ctx.draw();
}
ctx.clearInputEdges();
}
}
process.on('SIGINT', () => { console.log('INT at', SYS); process.exit(1); });
for (const fn of (listeners['win:load'] || [])) fn();
step(2);
// start a run and go to floor 2's boss room
ctx.startRun();
ctx.nextFloor();
const bossRoom = Object.values(ctx.G.rooms).find(r => r.type === 'boss');
ctx.enterRoom(bossRoom.gx, bossRoom.gy);
// invincible sim player so the fight actually lasts (we're hunting freezes, not deaths)
// NOTE: keep maxHp small — drawHUD loops once per heart; huge maxHp = slow test
ctx.G.player.hp = 99999;
step(2);
console.log('boss:', ctx.G.boss && ctx.G.boss.bossKind);
if (!ctx.G.boss || ctx.G.boss.bossKind !== 'gorecrown') { console.log('SETUP FAILED'); process.exit(1); }
// realistic fight: move erratically, hold fire aimed at boss, cycle weapons,
// draft perks when offered, DON'T kill the boss (heal it) so the fight drags on
const weaponIds = Object.keys(ctx.WEAPONS);
let maxEnemies = 0, maxParts = 0, maxEbullets = 0, maxPickups = 0;
const FRAMES = 12000; // ~3.2 minutes of game time
try {
for (let f = 0; f < FRAMES; f++) {
// wiggle movement
if (f % 40 === 0) {
for (const k of ['w', 'a', 's', 'd']) ctx.Input.keys[k] = false;
ctx.Input.keys[['w', 'a', 's', 'd'][Math.floor(Math.random() * 4)]] = true;
}
// aim at boss and hold fire
if (ctx.G.boss) { ctx.Input.mx = ctx.G.boss.x; ctx.Input.my = ctx.G.boss.y; }
ctx.Input.mdown = true;
// swap weapons occasionally
if (f % 900 === 0 && f > 0) {
const wid = weaponIds[Math.floor(Math.random() * weaponIds.length)];
ctx.G.player.weapon = { id: wid, ammo: 999 };
}
// keep the boss alive: top up its hp each frame
if (ctx.G.boss) ctx.G.boss.hp = Math.min(ctx.G.boss.maxHp, ctx.G.boss.hp + 3);
// keep the sim player alive
ctx.G.player.hp = 99999;
// auto-draft perks
if (ctx.G.mode === 'levelup') ctx.choosePerk(0);
step(1);
maxEnemies = Math.max(maxEnemies, ctx.G.enemies.length);
maxParts = Math.max(maxParts, ctx.G.parts.length);
maxEbullets = Math.max(maxEbullets, ctx.G.ebullets.length);
maxPickups = Math.max(maxPickups, ctx.G.pickups.length);
if (f % 100 === 99) {
console.log(`f${f + 1}: mode=${ctx.G.mode} enemies=${ctx.G.enemies.length} parts=${ctx.G.parts.length} ebul=${ctx.G.ebullets.length} pick=${ctx.G.pickups.length} bossHp=${ctx.G.boss ? Math.round(ctx.G.boss.hp) : 'dead'} weapon=${ctx.G.player.weapon.id}`);
}
}
} catch (e) {
console.log('EXCEPTION DURING FIGHT:', e.stack);
process.exit(1);
}
console.log(`survived ${FRAMES} frames. maxEnemies=${maxEnemies} maxParts=${maxParts} maxEbullets=${maxEbullets} maxPickups=${maxPickups}`);
// now kill the boss while minions are alive, then mop up
try {
ctx.Input.mdown = false;
if (ctx.G.boss) ctx.damageEnemy(ctx.G.boss, 999999, 0, false);
step(5);
console.log('boss killed with', ctx.G.enemies.length, 'minions alive; room cleared =', ctx.G.cur.cleared);
for (const e of [...ctx.G.enemies]) ctx.damageEnemy(e, 999999, 0, false);
step(30);
console.log('after mop-up: cleared =', ctx.G.cur.cleared, '| stairs =', ctx.G.pickups.some(p => p.type === 'stairs'), '| floor =', ctx.G.floor);
const stairs = ctx.G.pickups.find(p => p.type === 'stairs');
if (stairs) {
ctx.G.player.x = stairs.x; ctx.G.player.y = stairs.y;
step(5);
console.log('floor after stairs =', ctx.G.floor, '| mode =', ctx.G.mode);
}
console.log('POST-FIGHT OK');
} catch (e) {
console.log('EXCEPTION AFTER FIGHT:', e.stack);
process.exit(1);
}