0.2.3: prepare follow-up fixes and client features (#407, #408, #410, #413, #431, #435, #436, #440, #444)
Add special-target activation without bypassing attack cadence, cooperative card preferences and emotes, skill sound banks, per-operator voice choices, text sizing, install prompts, closed-tab recovery and unanimous setup rerolls. Retain late deployment elapsed time and resolve echo/counter pulses without recursive hook overflow. Independent final review remains pending. Golden: roster-016, roster-035, roster-036, roster-045; boss-boss_1-pair, boss-boss_3-pair, boss-boss_5-pair, boss-boss_7-pair, hidden-boss_9-pair; solo-HARD-9-boosted: Lemuen attack cadence. diy-132: Necras extra activation targets. coop2-NORMAL-14-standins: cooperative purchase preference and simulation fixes. hidden-boss_10-solo: echo pulse FIFO resolution. See DESIGN 28.13-28.24 for assumptions, sources and contributor credit.
This commit is contained in:
1 parent
314564c139
commit
f3658a0dd4
104 files changed
+3674
-741
No files matched your search
@@ -0,0 +1,26 @@
|
||||
// Real reflected hits against a scaled hit-count enemy must finish without recursive hook overflow.
|
||||
import { test } from 'node:test';
|
||||
import assert from 'node:assert/strict';
|
||||
import { makeBattle, checkInvariants } from '../helpers/battleHarness.js';
|
||||
const ECHO = 'enemy_9023_acdums';
|
||||
for (const chessId of ['chess_char_1_06_b', 'chess_char_2_08_a']) {
|
||||
test(`${chessId}: a 67-hit echo resolves all counter pulses including its lethal hit`, () => {
|
||||
const h = makeBattle({ units: [{ chessId, row: 10, col: 4 }], autoFinish: false, captureNoisy: true, hooks: ['damaged'] });
|
||||
h.step();
|
||||
const u = h.unit(chessId);
|
||||
h.b.addBuff(u, { key: 'test:survive', persist: true, mods: { hpFlat: 1e8 } });
|
||||
u.hp = u.s.maxHp;
|
||||
u.skill.sp = u.skill.spCost; u.skill.charges = 1;
|
||||
assert.ok(u.skill.activate('manual'));
|
||||
const echo = h.spawn(ECHO, { pos: [10, 5], mods: { hpMul: 6.7, speedMul: 0 } });
|
||||
assert.equal(echo.s.maxHp, 67);
|
||||
const t = h.eventsOf('fx').length;
|
||||
h.b.dealDamage(u, echo, { amount: 1, type: 'arts', isAttack: true });
|
||||
assert.equal(echo.alive, false);
|
||||
assert.equal(h.b.errors.length, 0);
|
||||
assert.equal(h.eventsOf('fx').slice(t).filter((f) => f[1] === 'explode' && f[4]?.kind === 'echoPulse').length, 67);
|
||||
assert.equal(echo.mem.ab.pulsing, false);
|
||||
assert.deepEqual(echo.mem.ab.pulses, []);
|
||||
checkInvariants(h.b);
|
||||
});
|
||||
}
|
||||
@@ -59,3 +59,11 @@ test('immediate redeployment cannot attack through a cleared wanted range', () =
|
||||
h.step();assert.equal(u.stats.attacks, before, 'the allies phase sees the cleared range immediately');
|
||||
done(h);
|
||||
});
|
||||
|
||||
for (const skillIndex of [0, 1, 2]) test(`wanted S${skillIndex + 1} waits for the next attack, not the next tick`, () => {
|
||||
const { h, u, e } = field({ skillIndex });
|
||||
u.skill.sp = 0;h.run(8.5);assert.ok(e.findBuff('lemuen:wanted'));
|
||||
u.atkCd = 0.7;u.skill.gainSp(999);
|
||||
h.run(0.3);assert.equal(u.skill.activations, 0);
|
||||
h.run(0.6);assert.equal(u.skill.activations, 1);done(h);
|
||||
});
|
||||
@@ -0,0 +1,44 @@
|
||||
// #440: special targeting supplies the DEFAULT attack opportunity, without extending S2 links.
|
||||
import { test } from 'node:test';
|
||||
import assert from 'node:assert/strict';
|
||||
import { makeBattle, enemyRec, checkInvariants } from '../helpers/battleHarness.js';
|
||||
import { COLS } from '../../server/sim/constants.js';
|
||||
|
||||
function field(skillIndex, kind = 'unite') {
|
||||
const h = makeBattle({ kind, timeLimit: 90, autoFinish: false, seed: 5,
|
||||
flags: { dpPerSec: 0, dpMax: 999 }, hooks: ['skillStart', 'damaged'], captureNoisy: true,
|
||||
defs: { enemies: { weak: enemyRec({ key: 'weak', hp: 100, speed: 0, mass: 0 }),
|
||||
walker: enemyRec({ key: 'walker', hp: 1e9, speed: 0.5, mass: 0 }) } },
|
||||
units: [{ uid: 1, elite: true, row: 10, col: 3,
|
||||
diy: { slot: 'chess_char_6_diy1_a', charId: 'char_450_necras', skillIndex, uniEquipId: null } }],
|
||||
});
|
||||
h.step();
|
||||
const u = h.unit(1);u.skill.sp = 0;
|
||||
const weak = h.spawn('weak', { pos: [10, 6] });h.step();h.b.kill(weak, null);h.run(0.2);
|
||||
const summon = u.mem.necras.skels.find((s) => s.alive);assert.ok(summon);
|
||||
const e = h.spawn('walker', { pos: [10, 9] });
|
||||
assert.ok(h.runUntil(() => e.blockedBy === summon, 20));h.step(3);
|
||||
const key = Math.round(e.y) * COLS + Math.round(e.x);
|
||||
assert.ok(!u.baseRangeKeys.includes(key) && u.extraRangeKeys.includes(key));
|
||||
return { h, u, e, summon };
|
||||
}
|
||||
function done(h) { checkInvariants(h.b);assert.deepEqual(h.b.errors, []); }
|
||||
for (const kind of ['normal', 'unite']) for (const skillIndex of [0, 1]) {
|
||||
test(`Necras S${skillIndex + 1} starts on its next attack against a summon-blocked remote target (${kind})`, () => {
|
||||
const { h, u, e } = field(skillIndex, kind);
|
||||
u.skill.gainSp(999);u.atkCd = 0.7;
|
||||
h.run(0.3);assert.equal(u.skill.activations, 0, 'retain attack cadence');
|
||||
h.run(0.6);assert.equal(u.skill.activations, 1);
|
||||
if (skillIndex === 1) assert.ok(!u.mem.necrasLinks?.some((l) => l.e === e), 'remote trigger does not extend S2 effect range');
|
||||
done(h);
|
||||
});
|
||||
}
|
||||
test('remote Necras trigger needs a selectable enemy and the owning summon', () => {
|
||||
const { h, u, e, summon } = field(1);
|
||||
e.base.moveSpeed = 0;e.markDirty();
|
||||
h.b.applyStatus(e, 'sleep', { duration: 5 });u.skill.gainSp(999);u.atkCd = 0;
|
||||
h.run(1);assert.equal(u.skill.activations, 0, 'sleep cannot open DEFAULT');
|
||||
h.b.retreat(summon, { reason: 'test' });
|
||||
h.run(5);assert.equal(u.skill.activations, 0, 'expired block does not retain an extra trigger');
|
||||
done(h);
|
||||
});
|
||||
Reference in new issue
Block a user