feedback5 WL: 生命回复速度 is not 治疗 — the 吟游者 trait, 调香师, 瑕光 S2, 铃兰 S3 and 锡人 give an hpRegen buff, so 无法被友方治疗 / 禁疗 units get it and 治疗加成 does not scale it (GitHub #96 / #137)

PRTS: 分支特性信息 吟游者 「特性为基于自身攻击力来增加受益者的“生命回复速度”属性」 (魔王, 浊心斯卡蒂); the 备注 of 调香师 熏衣草,
瑕光 S2, 铃兰 S3 and 锡人 S2 「生命恢复的提供方式为增加目标的“生命回复速度”属性…不受治疗加成和禁疗影响」 (铃兰: none in the
first second, refreshed every second); 异常效果 禁疗 「增减生命回复速度…的效果不会被识别为治疗类能力」. These were heals from
the provider, which damage.js refused on 收割者 / 不屈者 / 武者 (noHeal) and under 禁疗 — 隐德来希 gained nothing under 魔王.
This is the separation proposed in PR #135 by @SpiritedAwayCN, written anew (the PR's patch is not used):
- professions.js bardRegen: the trait is an hpRegen buff per bard refreshed every 0.25 s; 魔王's 微尘 ×1.5 goes through
  its `bardRegen` hook (both 魔王 kits); 浊心斯卡蒂's pulse and the 海嗣 token use it too.
- 调香师 (every ally), 瑕光 S2 (the skill range, removed at its end), 铃兰 S3 (from the first full second, removed at its
  end), 锡人 (per 炼金单元, its cached ATK): hpRegen buffs instead of battle.heal.
- damage.js: a `regen` tick takes no healing multiplier and a heal hook cannot change its amount (no 治疗加成 on any
  生命回复速度); 流明's 凡人之愿 and 华法琳's S1 bonus skip a regen tick of their own.
Direct heals are unchanged (still refused / scaled); 魔王 S3's redistribution and 纯烬's HoT are untouched. Other
per-second recoveries without such a note stay heals (引星棘刺, 塞雷娅 S3, 流明 / 纯烬 HoTs; 余 闲云隐市 and 天马之盔 left open).

Golden moved (every one fields 魔王 / 浊心斯卡蒂 / 调香师 / 瑕光 S2 / 铃兰 / 锡人): roster-004, roster-005, roster-006,
roster-011, roster-013, roster-015, roster-016, roster-018, roster-023, roster-025, roster-030, roster-032, roster-035,
roster-037, roster-039, roster-041, roster-042, roster-045, roster-046; bond-deputShip-high, bond-emptyShip-low,
bond-emptyShip-high, bond-indomShip-high, bond-kazimierzShip-low, bond-kazimierzShip-high, bond-sargonShip-low,
bond-sargonShip-high, bond-skillfulShip-high; boss-boss_1-pair, boss-boss_1-solo, boss-boss_2-solo, boss-boss_3-pair,
boss-boss_3-solo, boss-boss_4-solo, boss-boss_5-pair, boss-boss_5-solo, boss-boss_6-solo, boss-boss_7-pair,
boss-boss_7-solo, hidden-boss_8-solo, hidden-boss_9-pair, hidden-boss_9-solo, hidden-boss_10-solo, unite-1;
solo-NORMAL-1, solo-HARD-1, solo-ABYSS-1, coop4-ABYSS-7, coop2-NORMAL-8-serverrun, coop2-NORMAL-14-standins.
Docs: SIM.md (heal pipeline, bard row), PLAYING.md, kits/README.md (checklist item 11).
This commit is contained in:
sganggs committed 2026-10-05 21:13:15 +08:00
1 parent 696085ac8e
commit 5c6524e1ad
27 files changed
+943 -721

No files matched your search

+22 -8
View File
@@ -457,7 +457,7 @@ test('号角 S3: cast with an enemy in range (DEFAULT, DESIGN §21.29); ATK +25
});
// ------------------------------------------------------------------------------------------------------------------
test('魔王 S3: inspire +65 % of her max HP to others in range, HP equalised every 2 s; T1 orbiting motes ×1.5 aura heal; T2 −10 % from Sarkaz', () => {
test('魔王 S3: inspire +65 % of her max HP to others in range, HP equalised every 2 s; T1 orbiting motes ×1.5 trait (生命回复速度); T2 −10 % from Sarkaz', () => {
const sark = dummy('enemy_sark');
sark.tags = ['sarkaz'];
const h = makeBattle({
@@ -469,17 +469,22 @@ test('魔王 S3: inspire +65 % of her max HP to others in range, HP equalised ev
const a = h.unit('t_a'), b = h.unit('t_b'), c = h.unit('t_c');
const bb = bbOf(u), t0 = tal(u, 0), t1 = tal(u, 1);
const aura = () => u.s.atk * u.def.traitBb['attack@atk_to_hp_recovery_ratio'];
// the trait: 生命回复速度 on the allies in range (an hpRegen buff — PRTS 分支特性信息 吟游者; professions.js bardRegen)
const trait = (x) => x.findBuff(`trait:bard:${u.id}`)?.mods.hpRegen ?? 0;
h.step();
a.hp = 2000; b.hp = 8000; c.hp = 2000;
h.run(1.05);
// the 3 motes orbit at range_radius 1.15 (dynamic_spd 30°/s, 120° apart): none sits on the left neighbour at t = 1 s
assert.ok(!a.hasBuff('cetsyr:mote'), 'motes orbit: not on the left neighbour yet');
approx(a.hp - 2000, aura(), 1e-6, 'plain aura before the mote arrives');
approx(b.hp - 8000, aura(), 1e-6, 'plain aura (2 tiles away)');
approx(trait(a), aura(), 1e-6, 'plain trait before the mote arrives');
approx(trait(b), aura(), 1e-6, 'plain trait (2 tiles away)');
assert.ok(a.hp > 2000 && b.hp > 8000, 'regenerating');
assert.ok(h.runUntil(() => a.hasBuff('cetsyr:mote'), 2), 'an orbiting mote reaches the adjacent operator');
h.run(0.3);
approx(trait(a), aura() * t0['attack@trait_mul'], 1e-6, 'mote ×1.5');
const hpA = a.hp;
h.run(1);
approx(a.hp - hpA, aura() * t0['attack@trait_mul'], 1e-6, 'mote ×1.5');
assert.ok(Math.abs(a.hp - hpA - aura() * t0['attack@trait_mul']) <= 1.5, `mote ×1.5: +${a.hp - hpA} in 1 s`);
assert.ok(h.runUntil(() => c.hasBuff('cetsyr:mote'), 12), 'a diagonal neighbour (1.41 tiles) is on the orbit too');
h.run(8);
assert.ok(!fxOf(h, 'mote').some((e) => e[4].id === b.id), 'two tiles away: off the orbit');
@@ -502,7 +507,7 @@ test('魔王 S3: inspire +65 % of her max HP to others in range, HP equalised ev
});
// ------------------------------------------------------------------------------------------------------------------
test('铃兰 T2 画地为牢 fragile 20 % on sluggish enemies (×1.4 in S3); S3 no attack, range-wide sluggish, heals; T1 Supporter SP aura', () => {
test('铃兰 T2 画地为牢 fragile 20 % on sluggish enemies (×1.4 in S3); S3 no attack, range-wide sluggish, 生命回复速度 (none in the first second); T1 Supporter SP aura', () => {
const h = makeBattle({
defs: { enemies: { enemy_dummy: dummy() }, chess: { t_sup: chessRec({ id: 't_sup', profession: 'SUPPORT', stats: { atk: 0 } }), t_hurt: ally('t_hurt') } },
units: [{ chessId: 'chess_char_5_10_a', row: 10, col: 4 }, { chessId: 't_sup', row: 12, col: 3 }, { chessId: 't_hurt', row: 11, col: 5 }],
@@ -521,12 +526,21 @@ test('铃兰 T2 画地为牢 fragile 20 % on sluggish enemies (×1.4 in S3); S3
hurt.hp = 5000;
u.skill.gainSp(1000);
assert.ok(h.runUntil(() => u.skill.active, 5));
const lastAtk = u.lastAttackAt;
h.run(2.1);
const lastAtk = u.lastAttackAt, cast = h.b.time;
// PRTS 技能3 备注: an hpRegen buff (no heal), 0 in the first second, its amount refreshed every second
const fox = () => hurt.findBuff(`lisa:fox:${u.id}`);
h.run(0.9);
assert.equal(fox(), null, 'nothing in the first second');
assert.equal(hurt.hp, 5000);
h.run(1.2);
assert.equal(u.lastAttackAt, lastAtk, 'no attacks during S3');
assert.ok(e.s.flags && e.findBuff('sluggish'), 'enemies in range are sluggish');
approx(e.s.dmgTakenMul, 1 + (t1.damage_scale - 1) * bb.scale_delta_to_one, 1e-9, 'T2 ×1.4');
approx(hurt.hp - 5000, 2 * u.s.atk * bb['attack@atk_to_hp_recovery_ratio'], 1e-6, 'heals 9 % ATK/s');
const v = u.s.atk * bb['attack@atk_to_hp_recovery_ratio'];
approx(fox().mods.hpRegen, v, 1e-9, '生命回复速度 +9 % ATK');
assert.ok(Math.abs(hurt.hp - 5000 - v * (h.b.time - cast - 1)) <= 2, `regenerated ${hurt.hp - 5000} from the first second on`);
assert.ok(h.runUntil(() => !u.skill.active, 40));
assert.equal(fox(), null, 'gone with the skill');
clean(h);
});