feat: room-entry reaction buffer, 1.5s perk-selection lock, nearby-item toasts, Sine Weave stagger perk
This commit is contained in:
1 parent
97ab316304
commit
5d5eb3d265
8 files changed
+122
-16
No files matched your search
+65
-4
@@ -61,7 +61,7 @@ for (const f of files) {
|
||||
}
|
||||
// const/let top-level bindings live in the context's lexical scope, not on the
|
||||
// global object — expose the ones the harness pokes at directly.
|
||||
vm.runInContext('Object.assign(this, { G, Input, WEAPONS, Music, Sfx, SfxBank, W, H, WALL, ITEMS, ACTIVES, PERKS, ENEMY_TYPES, BOSS_DEFS, SPRITE_MANIFEST, Sprites, ACTOR_ANIMS, PRESSURE_UNIT, PRESSURE_MIN, PRESSURE_MAX, PRESSURE_DIAL_MIN, PRESSURE_DIAL_MAX, ROOM_SHAPES, ROOM_THEMES, HELP_PAGES, HELP_RENDERERS, SPAWN_WARN, defaultPlayerStats, applyPressureDelta, debugRebuildStats, debugEnabled, DEBUG_PAGES })', sandbox);
|
||||
vm.runInContext('Object.assign(this, { G, Input, WEAPONS, Music, Sfx, SfxBank, W, H, WALL, ITEMS, ACTIVES, PERKS, ENEMY_TYPES, BOSS_DEFS, SPRITE_MANIFEST, Sprites, ACTOR_ANIMS, PRESSURE_UNIT, PRESSURE_MIN, PRESSURE_MAX, PRESSURE_DIAL_MIN, PRESSURE_DIAL_MAX, ROOM_SHAPES, ROOM_THEMES, HELP_PAGES, HELP_RENDERERS, SPAWN_WARN, ENTRY_WARN, ENTRY_BUFFER, STUN_UNIT, stunChance, separateEntryWave, defaultPlayerStats, applyPressureDelta, debugRebuildStats, debugEnabled, DEBUG_PAGES })', sandbox);
|
||||
|
||||
let simTime = 0;
|
||||
const ctx = sandbox;
|
||||
@@ -283,6 +283,11 @@ console.log('== XP / perks ==');
|
||||
ctx.gainXP(100);
|
||||
step(5, 16);
|
||||
check('levelup triggered', ctx.G.mode === 'levelup' || ctx.G.pendingLevelups > 0);
|
||||
check('perk selection is locked on first presentation', (ctx.G.selectionLock || 0) > 0);
|
||||
const lockedChoices = ctx.G.perkChoices;
|
||||
tap('1'); // ignored while the selection lock is active
|
||||
check('locked draft ignores selection input', ctx.G.perkChoices === lockedChoices);
|
||||
step(100, 16); // advance 1.6s in levelup mode, clearing the 1.5s selection lock
|
||||
let guard = 0;
|
||||
while (ctx.G.mode === 'levelup' && guard++ < 30) { tap('1'); step(2, 16); }
|
||||
check('perks drafted', ctx.G.mode === 'play');
|
||||
@@ -298,6 +303,7 @@ console.log('== rebalanced perks + automatic draft ==');
|
||||
ctx.G.pendingLevelups = 1; ctx.G.autoPerk = false; ctx.openPerkDraft();
|
||||
p.stats.rerolls = 1; tap('r');
|
||||
check('Reroll Rib currency refreshes a manual draft', p.stats.rerolls === 0 && ctx.G.perkChoices.length === 3);
|
||||
step(100, 16); // clear the 1.5s selection lock armed by openPerkDraft above
|
||||
tap('4');
|
||||
check('one-off random draft choice resolves normally', ctx.G.mode === 'play' && ctx.G.pendingLevelups === 0);
|
||||
const levelBefore = p.level;
|
||||
@@ -310,6 +316,24 @@ console.log('== rebalanced perks + automatic draft ==');
|
||||
}
|
||||
autoDraft = true; // later sections must not get stuck in the perk draft
|
||||
|
||||
console.log('== nearby-item description toast ==');
|
||||
{
|
||||
const p = ctx.G.player;
|
||||
ctx.G.pickups.length = 0;
|
||||
ctx.G.toasts.length = 0;
|
||||
ctx.spawnPickup('item', p.x + 50, p.y, { iid: 'hollowpoints' });
|
||||
step(1, 16); // within the 90px radius -> toast once
|
||||
check('nearby item toasts its description once',
|
||||
ctx.G.toasts.length === 1 && ctx.G.toasts[0].text === ctx.ITEMS.hollowpoints.name && ctx.G.toasts[0].sub === ctx.ITEMS.hollowpoints.desc);
|
||||
step(4, 16); // stay adjacent, must not re-fire
|
||||
check('proximity toast does not repeat while adjacent', ctx.G.toasts.length === 1);
|
||||
p.x += 500; step(1, 16); // leave the 90px radius -> re-arms
|
||||
p.x -= 420; step(1, 16); // back within 90 (outside pickup reach) -> fires again
|
||||
check('proximity toast re-arms after leaving', ctx.G.toasts.length === 2);
|
||||
ctx.G.pickups.length = 0;
|
||||
ctx.startRun(); step(3, 16);
|
||||
}
|
||||
|
||||
console.log('== shield heart perk ==');
|
||||
{
|
||||
const p = ctx.G.player;
|
||||
@@ -342,6 +366,16 @@ check('items applied', Object.keys(ctx.G.player.items).length === 4);
|
||||
check('orbital knives active', ctx.G.player.stats.orbitals >= 1);
|
||||
step(30, 16);
|
||||
|
||||
console.log('== Sine Weave diminishing stagger ==');
|
||||
{
|
||||
const s = {}; ctx.PERKS.find(p => p.id === 'sinew').apply(s, {});
|
||||
check('sine weave first stack is 3% stagger chance', Math.abs(ctx.stunChance(s.stunRaw) - 0.03) < 1e-6);
|
||||
check('sine weave chance never exceeds 50%', ctx.stunChance(1e6) === 0.5);
|
||||
check('sine weave gains diminishing returns', ctx.stunChance(s.stunRaw * 2) < 2 * ctx.stunChance(s.stunRaw));
|
||||
ctx.PERKS.find(p => p.id === 'sinew').apply(s, {});
|
||||
check('sine weave stacks raw rating', Math.abs(s.stunRaw - 2 * (0.03 / 0.97)) < 1e-9);
|
||||
}
|
||||
|
||||
console.log('== expanded perk and item rosters ==');
|
||||
{
|
||||
check('41 new items added (14 phase-2 + 12 phase-3 + 10 legendaries)', Object.keys(ctx.ITEMS).length === 82);
|
||||
@@ -997,7 +1031,7 @@ console.log('== damaged monster health pips ==');
|
||||
ctx.G.enemies = [];
|
||||
}
|
||||
|
||||
console.log('== 500ms wave spawn telegraph ==');
|
||||
console.log('== 750ms entry-wave spawn telegraph ==');
|
||||
{
|
||||
ctx.startRun(); step(3, 16);
|
||||
// enter a combat room to trigger a wave
|
||||
@@ -1010,7 +1044,7 @@ console.log('== 500ms wave spawn telegraph ==');
|
||||
const room2 = ctx.G.cur;
|
||||
if (room2.type === 'combat' && ctx.G.enemies.length > 0) {
|
||||
const spawner = ctx.G.enemies[0];
|
||||
check('wave spawn is telegraphing', spawner.warmT > 0.3 && spawner.warmT <= ctx.SPAWN_WARN);
|
||||
check('wave spawn is telegraphing', spawner.warmT > 0.3 && spawner.warmT <= ctx.ENTRY_WARN);
|
||||
const sx = spawner.x, sy = spawner.y;
|
||||
ctx.G.player.invT = 0;
|
||||
const hpBefore = ctx.G.player.hp;
|
||||
@@ -1018,7 +1052,7 @@ console.log('== 500ms wave spawn telegraph ==');
|
||||
step(20, 16); // ~320ms
|
||||
check('telegraphed spawn is frozen', Math.abs(spawner.x - sx) < 0.01 && Math.abs(spawner.y - sy) < 0.01);
|
||||
check('telegraphed spawn deals no contact damage', ctx.G.player.hp === hpBefore);
|
||||
step(25, 16); // past 500ms total
|
||||
step(30, 16); // past 750ms total
|
||||
check('spawn arms after the telegraph', spawner.warmT <= 0);
|
||||
ctx.G.player.x = ctx.W / 2; ctx.G.player.y = ctx.H / 2; ctx.G.player.invT = 1;
|
||||
} else {
|
||||
@@ -1036,6 +1070,33 @@ console.log('== 500ms wave spawn telegraph ==');
|
||||
ctx.startRun(); step(3, 16);
|
||||
}
|
||||
|
||||
console.log('== room-entry reaction buffer ==');
|
||||
{
|
||||
ctx.startRun(); step(3, 16);
|
||||
// enter a combat neighbor room so a first-entry wave is staged at the arrival door
|
||||
const sr = ctx.G.rooms['0,0'];
|
||||
const sd = Object.keys(sr.doors).find(d => sr.doors[d]);
|
||||
const DIRS3 = { n: [0, -1], s: [0, 1], e: [1, 0], w: [-1, 0] };
|
||||
const [edx, edy] = DIRS3[sd];
|
||||
ctx.enterRoom(sr.gx + edx, sr.gy + edy);
|
||||
const er = ctx.G.cur;
|
||||
if (er.type === 'combat' && ctx.G.enemies.length > 0) {
|
||||
// move the player to a far corner, then run the post-teleport separation
|
||||
const erA = ctx.G.arena;
|
||||
ctx.G.player.x = erA.x0 + 40; ctx.G.player.y = erA.y0 + 40;
|
||||
ctx.separateEntryWave();
|
||||
const buf = Math.min(ctx.ENTRY_BUFFER, Math.min(erA.w, erA.h) * 0.42);
|
||||
const ok = ctx.G.enemies.every(e =>
|
||||
e.boss || e.hp <= 0 || ((e.x - ctx.G.player.x) ** 2 + (e.y - ctx.G.player.y) ** 2) >= (buf * 0.9) ** 2);
|
||||
check('entry enemies stay past the reaction buffer', ok);
|
||||
check('first-entry wave telegraphs with ENTRY_WARN', ctx.G.enemies.every(e => (e.warmT || 0) <= ctx.ENTRY_WARN));
|
||||
} else {
|
||||
check('combat room staged for entry-buffer test (' + er.type + ')', true);
|
||||
}
|
||||
ctx.G.enemies.length = 0;
|
||||
ctx.startRun(); step(3, 16);
|
||||
}
|
||||
|
||||
console.log('== five pressure-monster archetypes ==');
|
||||
{
|
||||
const newTypes = ['censer', 'bulwark', 'choirmaster', 'flenserling', 'broodsac'];
|
||||
|
||||
Reference in new issue
Block a user