Add adaptive progression and item expansion
This commit is contained in:
commit
8ab0857ee0
298 files changed
+8587
No files matched your search
@@ -0,0 +1,87 @@
|
||||
<!DOCTYPE html>
|
||||
<html lang="en">
|
||||
<head>
|
||||
<meta charset="UTF-8">
|
||||
<meta name="viewport" content="width=device-width, initial-scale=1">
|
||||
<title>MeatSlicer SFX Lab</title>
|
||||
<style>
|
||||
body { background:#090305; color:#eadfd3; font:14px monospace; margin:24px; }
|
||||
h1 { color:#ef3150; } h2 { color:#d6aa39; margin:24px 0 8px; }
|
||||
.grid { display:grid; grid-template-columns:repeat(auto-fill,minmax(190px,1fr)); gap:7px; }
|
||||
button { background:#251017; color:#eadfd3; border:1px solid #773142; padding:9px; text-align:left; cursor:pointer; }
|
||||
button:hover { background:#631526; border-color:#ef3150; }
|
||||
.controls { position:sticky; top:0; background:#090305e8; padding:8px 0; z-index:2; }
|
||||
#status { color:#8faeb2; margin-left:16px; }
|
||||
</style>
|
||||
<script>
|
||||
window.onerror = (msg, src, line) => { console.error('PAGEERROR:', msg, src, line); };
|
||||
</script>
|
||||
</head>
|
||||
<body>
|
||||
<h1>MEATSLICER // SFX LAB</h1>
|
||||
<div class="controls">
|
||||
<button id="init">INITIALIZE / PRELOAD</button>
|
||||
<button id="stop">STOP LOOPS</button>
|
||||
<label> SFX <input id="vol" type="range" min="0" max="1" step="0.05" value="0.45"></label>
|
||||
<span id="status">click initialize, then audition</span>
|
||||
</div>
|
||||
<div id="catalog"></div>
|
||||
<script src="../js/utils.js?v=23"></script>
|
||||
<script src="../js/state.js?v=23"></script>
|
||||
<script src="../js/sfx.js?v=23"></script>
|
||||
<script src="../js/sfxbank.js?v=23"></script>
|
||||
<script src="../js/weapons.js?v=23"></script>
|
||||
<script>
|
||||
G.player = { x: W / 2, y: H / 2 };
|
||||
const groups = {
|
||||
'16 WEAPONS': Object.values(WEAPONS).map(w => [w.name, () => Sfx.shoot(w)]),
|
||||
'WEAPON LOOPS': [
|
||||
['Bile loop', () => Sfx.syncWeaponLoop(WEAPONS.bile, true)],
|
||||
['Cauterizer loop', () => Sfx.syncWeaponLoop(WEAPONS.cauterizer, true)],
|
||||
['Red Hand loop', () => Sfx.syncWeaponLoop(WEAPONS.redhand, true)],
|
||||
['Spinal charge loop', () => Sfx.syncWeaponLoop(WEAPONS.spinaltap, true)],
|
||||
],
|
||||
'MONSTERS': ['shambler','runner','spitter','splitter','mini','exploder'].flatMap((type, i) => {
|
||||
const e = { type, x: 170 + i * 120, y: 300, elite: false };
|
||||
return [[type + ' death', () => Sfx.enemyDie(e)], ...(type === 'spitter' ? [['spitter attack', () => Sfx.spit(e)]] : [])];
|
||||
}).concat([
|
||||
['split burst', () => Sfx.split({x:480,y:300})], ['exploder fuse', () => Sfx.fuse({x:650,y:300})],
|
||||
['spawn', () => Sfx.spawn({x:250,y:300,elite:false})],
|
||||
]),
|
||||
'BOSSES': ['bonesaw','gorecrown','knifecrawl'].flatMap((bossKind, i) => {
|
||||
const e = { bossKind, x: 240 + i * 240, y: 260 };
|
||||
const events = bossKind === 'bonesaw' ? ['roar','charge','saws'] : bossKind === 'gorecrown' ? ['roar','volley','summon'] : ['roar','dash','knives'];
|
||||
return events.map(event => [bossKind + ' ' + event, () => Sfx.boss(e, event)]);
|
||||
}).concat([
|
||||
['boss enrage', () => Sfx.boss({bossKind:'bonesaw',x:480,y:260}, 'enrage')],
|
||||
['boss death', () => Sfx.boss({bossKind:'bonesaw',x:480,y:260}, 'death')],
|
||||
]),
|
||||
'PICKUPS / UI': ['gem','heart','ammo','weapon','item','levelup','perk','stairs','menu'].map(name => [name, () => Sfx[name] ? Sfx[name]() : Sfx.menu()]),
|
||||
'IMPACTS / PLAYER': [
|
||||
['flesh hit', () => Sfx.hit({x:480,y:250}, false)], ['critical hit', () => Sfx.hit({x:480,y:250}, true)],
|
||||
['ricochet left', () => Sfx.ricochet({x:80,y:300})], ['explosion right', () => Sfx.explode({x:880,y:300})],
|
||||
['trap set', () => Sfx.trapSet({x:480,y:300})], ['trap snap', () => Sfx.trapSnap({x:480,y:300})],
|
||||
['player hurt', () => Sfx.hurt()], ['player death', () => Sfx.playerDeath()], ['footstep', () => Sfx.footstep(1)],
|
||||
['shield break', () => Sfx.shieldBreak()], ['shield up', () => Sfx.shieldUp()],
|
||||
['door', () => Sfx.door({x:80,y:320})], ['room clear', () => Sfx.roomClear()],
|
||||
],
|
||||
};
|
||||
const catalog = document.getElementById('catalog');
|
||||
for (const [title, entries] of Object.entries(groups)) {
|
||||
const h = document.createElement('h2'); h.textContent = title; catalog.appendChild(h);
|
||||
const grid = document.createElement('div'); grid.className = 'grid';
|
||||
for (const [label, play] of entries) {
|
||||
const b = document.createElement('button'); b.textContent = label; b.onclick = () => { Sfx.resume(); play(); }; grid.appendChild(b);
|
||||
}
|
||||
catalog.appendChild(grid);
|
||||
}
|
||||
document.getElementById('init').onclick = () => {
|
||||
Sfx.init(); Sfx.resume(); SfxBank.preload();
|
||||
document.getElementById('status').textContent = 'loading ' + Object.keys(SfxBank.FILES).length + ' clips';
|
||||
setTimeout(() => document.getElementById('status').textContent = 'decoded ' + SfxBank.buffers.size + ' / failed ' + SfxBank.failed.size, 2500);
|
||||
};
|
||||
document.getElementById('stop').onclick = () => Sfx.stopAllLoops();
|
||||
document.getElementById('vol').oninput = e => Sfx.setVolume(Number(e.target.value));
|
||||
</script>
|
||||
</body>
|
||||
</html>
|
||||
@@ -0,0 +1,85 @@
|
||||
<!DOCTYPE html>
|
||||
<html lang="en">
|
||||
<head>
|
||||
<meta charset="UTF-8">
|
||||
<title>headless harness</title>
|
||||
<script>
|
||||
// error trap BEFORE anything else
|
||||
window.onerror = function (msg, src, line, col, err) {
|
||||
console.log('PAGEERROR: ' + msg + ' @ ' + (src || '').split('/').pop() + ':' + line + ':' + col +
|
||||
(err && err.stack ? ' | ' + err.stack.split('\n').slice(0, 3).join(' <- ') : ''));
|
||||
return false;
|
||||
};
|
||||
window.onunhandledrejection = function (e) {
|
||||
console.log('PAGEREJECTION: ' + (e.reason && e.reason.stack || e.reason));
|
||||
};
|
||||
// headless Chrome throttles rAF; drive it with timers so the game loop runs
|
||||
window.requestAnimationFrame = function (cb) { return setTimeout(function () { cb(performance.now()); }, 16); };
|
||||
</script>
|
||||
<link rel="stylesheet" href="../css/style.css">
|
||||
</head>
|
||||
<body>
|
||||
<div id="wrap"><canvas id="game" width="960" height="640"></canvas></div>
|
||||
<script src="../js/utils.js"></script>
|
||||
<script src="../js/state.js"></script>
|
||||
<script src="../js/input.js"></script>
|
||||
<script src="../js/sfx.js"></script>
|
||||
<script src="../js/audio.js"></script>
|
||||
<script src="../js/sprites.js"></script>
|
||||
<script src="../js/particles.js"></script>
|
||||
<script src="../js/weapons.js"></script>
|
||||
<script src="../js/bullets.js"></script>
|
||||
<script src="../js/pickups.js"></script>
|
||||
<script src="../js/items.js"></script>
|
||||
<script src="../js/perks.js"></script>
|
||||
<script src="../js/enemies.js"></script>
|
||||
<script src="../js/bosses.js"></script>
|
||||
<script src="../js/rooms.js"></script>
|
||||
<script src="../js/player.js"></script>
|
||||
<script src="../js/hud.js"></script>
|
||||
<script src="../js/main.js"></script>
|
||||
<script>
|
||||
window.addEventListener('load', function () {
|
||||
setTimeout(function () {
|
||||
var FLOOR = parseInt((location.search.match(/floor=(\d)/) || [0, '1'])[1], 10);
|
||||
console.log('DRIVER: starting run, floor ' + FLOOR);
|
||||
startRun();
|
||||
for (var i = 1; i < FLOOR; i++) nextFloor();
|
||||
var bossRoom = Object.values(G.rooms).find(function (r) { return r.type === 'boss'; });
|
||||
enterRoom(bossRoom.gx, bossRoom.gy);
|
||||
G.player.hp = 99999;
|
||||
console.log('DRIVER: boss = ' + (G.boss && G.boss.bossKind));
|
||||
var lastTime = -1, stalls = 0, ticks = 0, weaponIds = Object.keys(WEAPONS);
|
||||
var fight = setInterval(function () {
|
||||
G.player.hp = 99999;
|
||||
if (G.boss) { Input.mx = G.boss.x; Input.my = G.boss.y; }
|
||||
Input.mdown = true;
|
||||
if (ticks % 90 === 45) { // wiggle movement
|
||||
['w', 'a', 's', 'd'].forEach(function (k) { Input.keys[k] = false; });
|
||||
Input.keys[['w', 'a', 's', 'd'][Math.floor(Math.random() * 4)]] = true;
|
||||
}
|
||||
if (ticks % 800 === 700) { // cycle weapons
|
||||
G.player.weapon = { id: weaponIds[Math.floor(Math.random() * weaponIds.length)], ammo: 999 };
|
||||
console.log('DRIVER: weapon -> ' + G.player.weapon.id);
|
||||
}
|
||||
if (G.mode === 'levelup') choosePerk(0);
|
||||
if (G.time === lastTime && G.mode === 'play') {
|
||||
stalls++;
|
||||
if (stalls > 30) {
|
||||
console.log('DRIVER: GAME LOOP STALLED at G.time=' + G.time.toFixed(2) + ' mode=' + G.mode +
|
||||
' bossState=' + (G.boss && G.boss.state) + ' ebullets=' + G.ebullets.length);
|
||||
clearInterval(fight);
|
||||
}
|
||||
} else { stalls = 0; lastTime = G.time; }
|
||||
ticks++;
|
||||
if (ticks % 100 === 0) {
|
||||
console.log('DRIVER: t=' + G.time.toFixed(1) + ' bossHp=' + (G.boss ? Math.round(G.boss.hp) : 'dead') +
|
||||
' bossState=' + (G.boss && G.boss.state) + ' ebullets=' + G.ebullets.length + ' enemies=' + G.enemies.length);
|
||||
}
|
||||
if (ticks > 900) { console.log('DRIVER: DONE'); clearInterval(fight); }
|
||||
}, 100);
|
||||
}, 600);
|
||||
});
|
||||
</script>
|
||||
</body>
|
||||
</html>
|
||||
@@ -0,0 +1,118 @@
|
||||
// Long-fight soak for a specific boss. Usage: node test/repro_boss.js <floor 1-9>
|
||||
'use strict';
|
||||
const fs = require('fs');
|
||||
const path = require('path');
|
||||
const vm = require('vm');
|
||||
|
||||
const FLOOR = parseInt(process.argv[2] || '1', 10);
|
||||
|
||||
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;
|
||||
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();
|
||||
}
|
||||
}
|
||||
|
||||
for (const fn of (listeners['win:load'] || [])) fn();
|
||||
step(2);
|
||||
ctx.startRun();
|
||||
for (let i = 1; i < FLOOR; i++) ctx.nextFloor();
|
||||
const bossRoom = Object.values(ctx.G.rooms).find(r => r.type === 'boss');
|
||||
ctx.enterRoom(bossRoom.gx, bossRoom.gy);
|
||||
ctx.G.player.hp = 99999; // keep sim player alive (maxHp small for HUD)
|
||||
step(2);
|
||||
console.log('floor', FLOOR, '| boss:', ctx.G.boss && ctx.G.boss.bossKind, '| state:', ctx.G.boss && ctx.G.boss.state);
|
||||
if (!ctx.G.boss) { console.log('SETUP FAILED'); process.exit(1); }
|
||||
|
||||
const weaponIds = Object.keys(ctx.WEAPONS);
|
||||
const FRAMES = 12000;
|
||||
let prevState = 'idle', charges = 0, flingShots = 0, maxEbul = 0;
|
||||
try {
|
||||
for (let f = 0; f < FRAMES; f++) {
|
||||
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;
|
||||
}
|
||||
if (ctx.G.boss) { ctx.Input.mx = ctx.G.boss.x; ctx.Input.my = ctx.G.boss.y; }
|
||||
ctx.Input.mdown = true;
|
||||
if (f % 900 === 0 && f > 0) {
|
||||
ctx.G.player.weapon = { id: weaponIds[Math.floor(Math.random() * weaponIds.length)], ammo: 999 };
|
||||
}
|
||||
if (ctx.G.boss) ctx.G.boss.hp = Math.min(ctx.G.boss.maxHp, ctx.G.boss.hp + 3);
|
||||
ctx.G.player.hp = 99999;
|
||||
if (ctx.G.mode === 'levelup') ctx.choosePerk(0);
|
||||
const ebulBefore = ctx.G.ebullets.length;
|
||||
step(1);
|
||||
if (ctx.G.boss && ctx.G.boss.state !== prevState) {
|
||||
if (ctx.G.boss.state === 'charge') charges++;
|
||||
prevState = ctx.G.boss.state;
|
||||
}
|
||||
if (ctx.G.ebullets.length > ebulBefore) flingShots += ctx.G.ebullets.length - ebulBefore;
|
||||
maxEbul = Math.max(maxEbul, ctx.G.ebullets.length);
|
||||
if (f % 1000 === 999) {
|
||||
console.log(`f${f + 1}: mode=${ctx.G.mode} state=${ctx.G.boss ? ctx.G.boss.state : 'dead'} enemies=${ctx.G.enemies.length} ebul=${ctx.G.ebullets.length} bossHp=${ctx.G.boss ? Math.round(ctx.G.boss.hp) : 'dead'}`);
|
||||
}
|
||||
}
|
||||
} catch (e) {
|
||||
console.log('EXCEPTION in ' + SYS + ':', e.stack);
|
||||
process.exit(1);
|
||||
}
|
||||
console.log(`SOAK OK | charges=${charges} flingShots=${flingShots} maxEbul=${maxEbul}`);
|
||||
@@ -0,0 +1,150 @@
|
||||
// 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);
|
||||
}
|
||||
+1125
File diff suppressed because it is too large.
Load diff
Reference in new issue
Block a user