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).
This commit is contained in:
sganggs committed 2026-10-06 16:00:20 +08:00
1 parent 027c8477eb
commit e1a7920482
7 files changed
+227 -166

No files matched your search

+22 -7
View File
@@ -333,6 +333,27 @@ export function resolveHit(b, u, prof, target, info, x, y) {
}
}
/**
* The next unit of a 链愈师 heal chain of `healer` from `prev` — PRTS 分支特性信息 链愈师: "跳跃范围为x-4,无特殊说明的场合一次
* 治疗链不会对已跳跃过的单位重复跳跃", "优先跳跃至范围内生命比例最低>部署时间点最晚的我方单位。可选择满生命我方单位为跳跃目标,但仍受
* 禁疗制约": an ally on the 3×3 of tiles around prev's (range x-4), not yet in this chain (`seen` ids), no device, no hidden
* or 孤立 unit, no 禁疗 / 无法被治疗 one (as Battle.injuredAlliesInKeys: the healer's `healThrough` lets its own summon in —
* 凯尔希 / Mon3tr); the lowest HP ratio first — a full-HP ally too, healed for nothing, and the chain jumps on from it —,
* then the latest deployed (aggroSeq). Null when none. Shared by the profession (doHeal) and Mon3tr's kit.
*/
export function chainHealNext(b, healer, prev, seen) {
const through = healer && healer.profile && typeof healer.profile.healThrough === 'function' ? healer.profile.healThrough : null;
const r0 = prev.tileR, c0 = prev.tileC;
let best = null;
for (const a of b.allyUnits) {
if (!a.alive || !a.deployed || a.hidden || a.kind === 'device' || seen.has(a.id)) continue;
if (Math.abs(a.tileR - r0) > 1 || Math.abs(a.tileC - c0) > 1) continue;
if (a !== healer && (a.s.flags.isolated || (a.s.flags.noHeal && !(through && through(healer, a))) || (a.profile && a.profile.noHeal))) continue;
if (!best || a.hpRatio < best.hpRatio - 1e-12 || (Math.abs(a.hpRatio - best.hpRatio) <= 1e-12 && a.aggroSeq > best.aggroSeq)) best = a;
}
return best;
}
function doHeal(b, u, prof, t) {
const atk = u.s.atk;
const scale = (prof.atkScale ?? 1) * (prof.healScale ?? 1) * u.s.atkScaleMul;
@@ -346,13 +367,7 @@ function doHeal(b, u, prof, t) {
let prev = t;
const n = Math.max(1, h.count || 3);
for (let k = 1; k < n; k++) {
let best = null, bd = Infinity;
for (const a of b.alliesInRadius(prev.x, prev.y, 2.5, null)) {
// 禁疗 / noHeal units are no heal target for the bounces either (as injuredAlliesInKeys; 史尔特尔's 余烬, GitHub #52)
if (seen.has(a.id) || a.hp >= a.s.maxHp || a.kind === 'device' || a.s.flags.noHeal || (a.profile && a.profile.noHeal)) continue;
const d = a.hpRatio;
if (d < bd) { bd = d; best = a; }
}
const best = chainHealNext(b, u, prev, seen);
if (!best) break;
seen.add(best.id);
b._ev(['atk', prev.id, best.id, 'chainHeal']);
+10 -23
View File
@@ -27,8 +27,9 @@
// prosts_t_1[born_charge] BLOCK_CNT ×0, interval 0.1, firstTriggerInterval 1, FixedValueDamage PURE `damage` on itself);
// buff_template_data (monstr_t_2[atk_spd] ON_OUTPUT_MODIFIER IsHeal; monstr_s_3[undead] ON_BEFORE_TRY_SET_HP_ZERO).
// - Trait (链愈师) "恢复友方单位生命,且会在3个友方单位间跳跃,每次跳跃治疗量降低25%" — the chain is this kit's (the profession's
// 2.5-tile chain is replaced: heal mode single + `chainFrom`), as her heal projectiles have it: after the heal on the main
// target the heal jumps to the lowest-HP-ratio injured ally of the 3×3 around the last one healed (x-4), each jump ×
// chain is replaced: heal mode single + `chainFrom`, its links the profession's — ai.js chainHealNext): after the heal on
// the main target the heal jumps to the lowest-HP-ratio ally (a full-HP one too — PRTS 分支特性信息 链愈师; ties: the latest
// deployed) of the 3×3 around the last one healed (x-4), each jump ×
// attack@chain.atk_scale (0.75; XAH-X 0.85), up to attack@chain.max_target units healed (3). A 重构体 (and she herself while S3
// runs: monstr[free_jump]) is a free link: healing it uses no target of the count and the next jump does not decay (XAH-X
// stage 3: the next attack@chain.extra_cnt jumps after that neither). Her heals pass 禁疗 ("治疗效果无视禁疗"); her selection
@@ -63,6 +64,7 @@
import { num, talentBb, traitBb, skillRec, batMod, installAura, up } from '../shared/tier1.js';
import { COLS } from '../../../constants.js';
import { chainHealNext } from '../../../ai.js';
const S1 = 'skchr_monstr_1';
const S2 = 'skchr_monstr_2';
@@ -101,28 +103,13 @@ const prostsInRange = (battle, unit) => prostsOf(battle, unit).find((t) => up(t)
/** `a` stands on the 3×3 around `b` (x-4: a heal jump's reach, the 重构体's aura). */
const nearX4 = (a, b) => Math.abs(a.tileR - b.tileR) <= 1 && Math.abs(a.tileC - b.tileC) <= 1;
/**
* The next link of a chain heal from `cur`: the lowest-HP-ratio injured ally of its 3×3 not healed yet by this chain (her
* own 重构体 even at full HP when `fullProsts`); never a 禁疗 unit but her 重构体.
*/
function nextLink(battle, unit, cur, seen, fullProsts) {
let best = null;
for (const a of battle.allyUnits) {
if (!a.alive || !a.deployed || a.hidden || a.kind === 'device' || seen.has(a.id) || !nearX4(a, cur)) continue;
const mine = isProstsOf(a, unit);
if (a !== unit && ((a.s.flags.noHeal && !mine) || (a.profile && a.profile.noHeal))) continue;
if (!(a.hp < a.s.maxHp - 1e-6) && !(fullProsts && mine)) continue;
if (!best || a.hpRatio < best.hpRatio - 1e-12 || (Math.abs(a.hpRatio - best.hpRatio) <= 1e-12 && a.deploySeq < best.deploySeq)) best = a;
}
return best;
}
/**
* A chain heal of hers from `main` (already healed by her attack when `mainDone`): up to `count` units healed, × `step` per
* jump; a free link (her 重构体; herself with `selfFree`) uses no target of the count and the next 1 + `extra` jumps keep the
* amount.
* amount. Each link is the 链愈师 one (ai.js chainHealNext — the 3×3 around the last one healed, the lowest HP ratio a
* full-HP ally included, then the latest deployed; no 禁疗 unit but her 重构体, her `healThrough`).
*/
function chainFrom(battle, unit, main, base, { count, step, extra, fullProsts = false, mainDone = false, selfFree = false }) {
function chainFrom(battle, unit, main, base, { count, step, extra, mainDone = false, selfFree = false }) {
const free = (t) => isProstsOf(t, unit) || (selfFree && t === unit);
let amount = base;
if (!mainDone) battle.heal(unit, main, amount, HEAL_OPTS);
@@ -131,7 +118,7 @@ function chainFrom(battle, unit, main, base, { count, step, extra, fullProsts =
const seen = new Set([main.id]);
let cur = main;
while (counted < count) {
const next = nextLink(battle, unit, cur, seen, fullProsts);
const next = chainHealNext(battle, unit, cur, seen);
if (!next) break;
seen.add(next.id);
if (calm > 0) calm--; else amount *= step;
@@ -280,12 +267,12 @@ export default {
const base = unit.s.atk * (prof.atkScale ?? 1) * (prof.healScale ?? 1) * unit.s.atkScaleMul;
for (const t of c.targets || []) {
if (!t || t.side !== 'ally') continue;
chainFrom(battle, unit, t, base, { ...chain, count: s1on ? s1Count : chain.count, fullProsts: s2on, mainDone: true });
chainFrom(battle, unit, t, base, { ...chain, count: s1on ? s1Count : chain.count, mainDone: true });
// S2: 0.6 s after her attack healed her 重构体, a second chain on it (her ATK × 1)
if (s2on && isProstsOf(t, unit)) {
battle.after(S2_EXTRA_DELAY, () => {
if (!unit.alive || !up(t)) return;
chainFrom(battle, unit, t, unit.s.atk * unit.s.atkScaleMul, { ...chain, fullProsts: skillOn(unit, S2) });
chainFrom(battle, unit, t, unit.s.atk * unit.s.atkScaleMul, chain);
}, { owner: unit });
}
}