Files
ark-stronghold-protocol/test/sim/feedback5-chain-heal.test.js
sganggs e1a7920482 feedback5 WE1: 链愈师 heal jumps within the 3×3 around the last one healed (x-4), a full-HP ally included (follow-up 27)
PRTS 分支特性信息 链愈师: "跳跃范围为x-4…不会对已跳跃过的单位重复跳跃", "优先跳跃至范围内生命比例最低>部署时间点最晚的我方单位。
可选择满生命我方单位为跳跃目标,但仍受禁疗制约" (range_table x-4 = the 3×3 around the unit).

- ai.js chainHealNext: an ally on the 3×3 of tiles around the last one healed, not yet in the chain, no device / hidden /
  孤立 / 禁疗 unit (the healer's healThrough summon excepted), the lowest HP ratio first — a full-HP ally too, the chain
  goes on from it — then the latest deployed. doHeal (莎草) and Mon3tr's chainFrom both use it (her fullProsts case is
  now general and gone). It used to be the most injured ally within 2.5 tiles. [ASSUMED] 禁疗 = not selected.

Golden: roster-004, roster-005, roster-023, roster-024, bond-sargonShip-low (莎草's chains: other jump targets, full-HP
links); diy-133, diy-134 (Mon3tr). Others unchanged (no match fields a chain healer).

Tests: test/sim/feedback5-chain-heal.test.js (new, 3); op_monstr, kits_t1t2, feedback2-surtr-stealth pass.
Docs: SIM.md §Professions (chainhealer row).
2026-10-06 16:00:20 +08:00

59 lines
3.9 KiB
JavaScript
Raw Permalink Blame History

This file contains ambiguous Unicode characters
This file contains Unicode characters that might be confused with other characters. If you think that this is intentional, you can safely ignore this warning. Use the Escape button to reveal them.
// test/sim/feedback5-chain-heal.test.js — the 链愈师 heal chain (follow-up 27; PRTS 分支特性信息 链愈师 "跳跃范围为x-4,无特殊说明
// 的场合一次治疗链不会对已跳跃过的单位重复跳跃", "优先跳跃至范围内生命比例最低>部署时间点最晚的我方单位。可选择满生命我方单位为跳跃目标,
// 但仍受禁疗制约") — server/sim/ai.js chainHealNext / doHeal (莎草's profession chain; Mon3tr's kit uses the same links). It used
// to jump to the most injured ally within 2.5 tiles (never a full-HP one, ties by list order).
// Run: node --test test/sim/feedback5-chain-heal.test.js
import { test } from 'node:test';
import assert from 'node:assert/strict';
import { makeBattle, chessRec, checkInvariants } from '../helpers/battleHarness.js';
const GRID = []; for (let r = -2; r <= 2; r++) for (let c = 0; c <= 6; c++) GRID.push([r, c]);
const HEALER = chessRec({ id: 't_chain', profession: 'MEDIC', subProfessionId: 'chainhealer', stats: { maxHp: 1e4, atk: 100, bat: 1 }, skill: null, rangeGrid: GRID });
const wall = (id) => chessRec({ id, stats: { maxHp: 1e4, atk: 0, def: 0 }, skill: null });
/** The healer on (10, 2) and walls at `at` = { id: [row, col, hpRatio] }; returns the chain of its first heal (ids → wall ids). */
function firstChain(at, setup = null) {
const ids = Object.keys(at);
const h = makeBattle({
defs: { chess: { t_chain: HEALER, ...Object.fromEntries(ids.map((id) => [id, wall(id)])) } },
units: [{ chessId: 't_chain', row: 10, col: 2 }, ...ids.map((id) => ({ chessId: id, row: at[id][0], col: at[id][1] }))],
content: 'none', autoFinish: false, timeLimit: 30, seed: 3,
});
h.step(); // deployed (the healer heals nobody yet: everyone is at full HP)
for (const id of ids) { const u = h.unit(id); u.hp = u.s.maxHp * at[id][2]; }
if (setup) setup(h);
const n0 = h.events.length;
for (let i = 0; i < 90 && !h.events.slice(n0).some((e) => e[0] === 'atk'); i++) h.step(); // (step drains the events)
const name = new Map(h.b.allyUnits.map((u) => [u.id, u.defId]));
const ev = h.events.slice(n0).filter((e) => e[0] === 'atk');
checkInvariants(h.b);
return { h, main: name.get(ev.find((e) => e[3] !== 'chainHeal')[2]), jumps: ev.filter((e) => e[3] === 'chainHeal').map((e) => [name.get(e[1]), name.get(e[2])]) };
}
test('the jump reaches the 3×3 around the last one healed (x-4), not an ally two tiles away; each jump goes on from where it landed', () => {
// A (most injured) on (10, 5); B two tiles right of it (inside 2.5 tiles, outside its 3×3), C on its diagonal
const r = firstChain({ t_a: [10, 5, 0.5], t_b: [10, 7, 0.6], t_c: [11, 6, 0.7] });
assert.equal(r.main, 't_a');
assert.deepEqual(r.jumps, [['t_a', 't_c'], ['t_c', 't_b']], 'A → C (its 3×3) → B (C\'s 3×3); it used to jump A → B → C');
});
test('a full-HP ally is a jump target when no injured one is in reach — healed for nothing, the chain jumps on from it', () => {
const r = firstChain({ t_a: [10, 5, 0.5], t_d: [10, 6, 1], t_e: [10, 7, 0.6] });
assert.equal(r.main, 't_a');
assert.deepEqual(r.jumps, [['t_a', 't_d'], ['t_d', 't_e']], 'A → D (full HP) → E');
assert.equal(r.h.unit('t_d').hp, r.h.unit('t_d').s.maxHp);
});
test('ties of the HP ratio: the latest deployed first; a 禁疗 unit is never a jump target', () => {
const tie = firstChain({ t_a: [10, 5, 0.5], t_f: [9, 5, 0.8], t_g: [11, 5, 0.8] });
const [f, g] = [tie.h.unit('t_f'), tie.h.unit('t_g')];
const later = f.aggroSeq > g.aggroSeq ? 't_f' : 't_g';
assert.deepEqual(tie.jumps[0], ['t_a', later], 'the later deployed of the two');
const banned = firstChain({ t_a: [10, 5, 0.5], t_n: [10, 6, 0.2], t_m: [11, 5, 0.9] }, (h) => {
h.b.addBuff(h.unit('t_n'), { key: 'test:healFree', status: 'healFree', flags: { noHeal: true, healFree: true } });
});
assert.equal(banned.main, 't_a', 'the 禁疗 unit is no main target either');
assert.deepEqual(banned.jumps[0], ['t_a', 't_m'], 'the 禁疗 unit is skipped');
});