Files
ark-stronghold-protocol/test/content/vulpis-first-hit.test.js
T

95 lines
5.0 KiB
JavaScript

// 追凶's first damage instance also carries its arts addition (PRTS 忍冬, talent remarks).
// Real full-content battles with controlled durable enemies; no HP/SP/ATK writes or forced casts.
import { test } from 'node:test';
import assert from 'node:assert/strict';
import { makeBattle, enemyRec, checkInvariants } from '../helpers/battleHarness.js';
const NORMAL = 'chess_char_3_18_a';
const ELITE = 'chess_char_3_18_b';
const ENEMY = 'enemy_vulpis_probe';
const approx = (a, b) => assert.ok(Math.abs(a - b) < 1e-6, `${a} vs ${b}`);
const hunt = (c) => c.dmg?.tags?.includes('vulpisHunt');
const finished = (h) => { assert.deepEqual(h.b.errors, []); checkInvariants(h.b); };
function scenario(entry = {}, enemy = {}) {
return makeBattle({
units: [{ chessId: NORMAL, row: 10, col: 4, ...entry }],
defs: { enemies: { [ENEMY]: enemyRec({ key: ENEMY, hp: 1e7, speed: 0, ...enemy }) } },
autoFinish: false, timeLimit: 40, flags: { startOpCooldown: 3 }, hooks: ['damaged'], captureNoisy: true,
});
}
const variants = [
{ label: 'normal', chessId: NORMAL },
{ label: 'elite without module', chessId: ELITE, moduleId: 'none' },
{ label: 'elite SOL-X', chessId: ELITE, moduleId: 'uniequip_002_vulpis' },
{ label: 'elite SOL-Y', chessId: ELITE, moduleId: 'uniequip_003_vulpis' },
];
for (const { label, ...entry } of variants) for (const skillIndex of [0, 1, 2]) {
test(`追凶: first ordinary hit includes one arts addition (${label}, S${skillIndex + 1})`, () => {
const h = scenario({ ...entry, skillIndex });
h.step();
const u = h.unit(entry.chessId);
if (entry.moduleId) assert.equal(u.def.loadout.moduleId, entry.moduleId, 'the requested module is equipped');
const e = h.spawn(ENEMY, { pos: [10, 5] });
const hp = e.hp;
assert.ok(h.runUntil(() => h.hooksOf('damaged').some((c) => c.source === u), 1));
const hits = h.hooksOf('damaged').filter((c) => c.source === u);
const main = hits.filter((c) => !hunt(c));
assert.equal(main.length, 1, 'one ordinary attack');
assert.equal(main[0].dmg.isSkill, false, 'no skill at the first hit');
assert.equal(main[0].type, 'phys');
assert.ok(e.alive && e.hp > 0, 'the first attack did not kill the target');
const riders = hits.filter(hunt);
assert.equal(riders.length, 1, 'the very first hit carries 追凶');
assert.equal(riders[0].t, main[0].t);
assert.equal(riders[0].type, 'arts');
assert.ok(riders[0].dmg.tags.includes('addition'));
approx(riders[0].amount, u.s.atk * u.def.talents[0].bb.atk_scale);
approx(hp - e.hp, main[0].amount + riders[0].amount);
assert.equal(h.eventsOf('dmg').filter((c) => c[1] === e.id).length, 2, 'both actual damage events exist');
finished(h);
});
}
for (const chessId of [NORMAL, ELITE]) for (const skillIndex of [0, 1, 2]) {
test(`追凶: first skill damage carries its addition (${chessId}, S${skillIndex + 1})`, () => {
const h = scenario({ chessId, skillIndex });
h.run(16); // every selected skill is naturally ready, with no enemies yet
const u = h.unit(chessId);
const e = h.spawn(ENEMY, { pos: [10, skillIndex === 1 ? 7 : skillIndex === 2 ? 6 : 5] });
const hp = e.hp;
assert.ok(h.runUntil(() => h.hooksOf('damaged').some((c) => c.source === u && c.dmg.isSkill), 2));
const hits = h.hooksOf('damaged').filter((c) => c.source === u && c.target === e);
const main = hits.filter((c) => !hunt(c));
assert.ok(main.some((c) => c.dmg.isSkill));
// S1 has a physical attack and a separate arts skill instance; S2/S3 have one main instance each.
assert.equal(main.length, skillIndex === 0 ? 2 : 1);
const riders = hits.filter(hunt);
assert.equal(riders.length, main.length, 'one addition per qualifying damage instance, including the first');
for (const c of riders) approx(c.amount, u.s.atk * u.def.talents[0].bb.atk_scale);
approx(hp - e.hp, hits.reduce((s, c) => s + c.amount, 0));
assert.ok(e.alive && e.hp > 0);
finished(h);
});
}
test('追凶: each enemy starts its own fixed window; repeated damage does not extend it', () => {
const h = scenario();
h.step();
const u = h.unit(NORMAL);
const e = h.spawn(ENEMY, { pos: [10, 5] });
h.run(12);
const main = h.hooksOf('damaged').filter((c) => c.source === u && c.target === e && !hunt(c));
const riders = h.hooksOf('damaged').filter((c) => c.source === u && c.target === e && hunt(c));
assert.ok(main.some((c) => c.t > main[0].t + 10));
assert.equal(riders.length, main.filter((c) => c.t - main[0].t <= 10 + 1e-9).length);
assert.ok(riders.every((c) => c.t - main[0].t <= 10 + 1e-9));
const another = h.spawn(ENEMY, { pos: [10, 6] });
// Naturally ready S3 can reach both enemies; the new target gets its own first-hit addition.
assert.ok(h.runUntil(() => h.hooksOf('damaged').some((c) => c.target === another), 10));
const first = h.hooksOf('damaged').filter((c) => c.target === another);
assert.equal(first.filter(hunt).length, 1);
assert.equal(first.filter(hunt)[0].t, first.find((c) => !hunt(c)).t);
finished(h);
});