A 咒愈师's trait heals on every damage she deals; 收割者 / 武者 heal on their skill hits too (#115)

The official trait buffs fire on output damage, not only on a normal attack (buff_template_data vendla_tr / reed2_tr / titi_tr ON_AFTER_OUTPUT_DAMAGE; etlchi_trait etc. ON_OUTPUT_DAMAGE, attackType BUFF filtered for 隐德来希). Skill damage now heals: 缇缇's 凝固的时光 ticks, 焰影苇草 S2 fireballs and 刺玫 S2's counter heal the one ally the skill names (DamageInfo.traitAlly); 隐德来希's S2 blood sickles heal her, capped at her block count per instant. PRTS 分支特性信息 §咒愈师: the heal depends only on the damage value. Contributed by @shanzhaikabi.
This commit is contained in:
shanzhaikabi authored and GitHub committed 2026-10-05 14:20:59 +08:00
1 parent bbff3514a8
commit 763e224b8d
7 files changed
+146 -18

No files matched your search

+56
View File
@@ -232,6 +232,17 @@ test('缇缇 S2 封护: no attacks; she and the lowest-HP op in range sleep (inv
approx(dream.at(-1).amount, u.s.atk * t0.damage_atk_scale * bb.talent_scale, 'T1 ×talent_scale');
assert.ok(h.runUntil(() => !u.skill.active, 30));
assert.equal(during(h, u, attacks(h, u)).length, 0, 'no attacks during the skill');
// …and every 凝固的时光 tick healed a 咒愈师 ally: the official trait buff (`titi_tr`) is ON_AFTER_OUTPUT_DAMAGE, so
// the heal follows ANY damage she deals. The sim ran the trait from the attack path only and she does not attack at
// all while this skill runs, so those ticks healed nothing (the same hook gap that left 隐德来希 S2 without heals).
const dreams = during(h, u, dealt(h, u, (c) => (c.dmg?.tags || []).includes('titiDream')));
const incHeals = during(h, u, heals(h, u, (c) => (c.opts?.tags || []).includes('incantation')));
assert.ok(dreams.length > 0, `${dreams.length} 凝固的时光 ticks`);
assert.equal(incHeals.length, dreams.length, 'one trait heal per tick');
for (const c of incHeals) {
approx(c.amount, dreams[0].amount * u.profile.healRatio, 'heal = 50 % of the tick');
assert.ok([hurt, fine, u].includes(c.target), 'on an ally in her range');
}
assert.ok(!u.findBuff('titi:ward') && !hurt.findBuff('titi:ward'), 'both wake at the end');
done(h);
}
@@ -417,6 +428,51 @@ test('隐德来希 S2 绯红壁合: no attacks; blood sickles on her and on the
}
});
test('隐德来希 S2 绯红壁合: every 血镰 cut heals the 收割者 trait (每攻击到一个敌人回复自身50生命)', () => {
for (const id of pair('06')) {
const h = run({
defs: { enemies: { enemy_dummy: dummy('enemy_dummy') } },
units: [entry(id, 'skchr_etlchi_2', { row: 10, col: 4 })],
enemies: [{ key: 'enemy_dummy', pos: [10, 5] }],
});
const u = sel(h, id, 'skchr_etlchi_2');
h.step();
cast(h, u);
u.hp = u.s.maxHp * 0.4; // wounded (and above 重盈's 25 %), so every heal shows
const hp0 = u.hp, t0 = h.b.time;
h.run(3);
const cuts = tagged(h, u, 'bloodSickle').filter((c) => c.t > t0 + 1e-9);
const healed = heals(h, u, (c) => c.target === u && c.t > t0 + 1e-9);
assert.ok(cuts.length >= 5, `${cuts.length} cuts`);
assert.equal(healed.length, cuts.length, 'one trait heal per cut (the trait fires on every damage she outputs)');
for (const c of healed) approx(c.amount, u.profile.selfHeal, 'per-hit heal = the trait 生命值');
approx(u.hp - hp0, healed.length * u.profile.selfHeal, 'HP restored');
done(h);
}
});
test('隐德来希 S2 绯红壁合: the 收割者 heal is capped by the block count per cut (最大生效数等于阻挡数)', () => {
const h = run({
defs: { enemies: { enemy_dummy: dummy('enemy_dummy') } },
units: [entry('chess_char_5_06_a', 'skchr_etlchi_2', { row: 10, col: 4 })],
enemies: [{ key: 'enemy_dummy', pos: [10, 5] }, { key: 'enemy_dummy', pos: [9, 4] }, { key: 'enemy_dummy', pos: [11, 4] }],
});
const u = sel(h, 'chess_char_5_06_a', 'skchr_etlchi_2');
h.step();
cast(h, u);
u.hp = u.s.maxHp * 0.4;
const hp0 = u.hp, t0 = h.b.time;
h.run(3);
const cuts = tagged(h, u, 'bloodSickle').filter((c) => c.t > t0 + 1e-9);
const healed = heals(h, u, (c) => c.target === u && c.t > t0 + 1e-9);
const ticks = cuts.length / 3; // three enemies stand in the sickle's ring
assert.ok(ticks >= 5 && Number.isInteger(ticks), `${cuts.length} cuts over ${ticks} ticks`);
assert.equal(healed.length, ticks * Math.max(1, u.s.blockCnt), `${healed.length} heals (阻挡数 ${u.s.blockCnt})`);
for (const c of healed) approx(c.amount, u.profile.selfHeal, 'per-hit heal');
approx(u.hp - hp0, healed.length * u.profile.selfHeal, 'HP restored');
done(h);
});
test('隐德来希 module REA-Y (玫瑰色故事集): ASPD +12 with ≥ 2 enemies in range', () => {
const h = run({
defs: { enemies: { enemy_dummy: dummy('enemy_dummy') } },
+10
View File
@@ -348,6 +348,16 @@ test('incantation medics heal an ally for 50 % of damage dealt; wandermedics cle
h.unit('t_tank').hp = 1000;
h.run(0.5);
approx(h.unit('t_tank').hp, 1000 + 500, 1e-6);
// …and not only for attacks: the official trait buff is ON_AFTER_OUTPUT_DAMAGE (`vendla_tr` / `reed2_tr` / `titi_tr`),
// so ANY damage the 咒愈师 deals heals. The sim used to run the trait from the attack path (`profile.afterHit`) only,
// which is why 缇缇's 凝固的时光 ticks — and every 咒愈师 skill that damages without an attack — healed nothing.
h.unit('t_tank').hp = 1000;
h.b.dealDamage(h.unit('t_inc'), h.enemy('enemy_dummy'), { amount: 400, type: 'arts', isSkill: true });
approx(h.unit('t_tank').hp, 1000 + 200, 1e-6, 'skill damage heals 50 %');
// a gauge fill removes no HP: it is not "伤害" for the trait
h.unit('t_tank').hp = 1000;
h.b.emit?.('damaged', { source: h.unit('t_inc'), target: h.enemy('enemy_dummy'), amount: 400, type: 'element', dmg: null });
approx(h.unit('t_tank').hp, 1000, 1e-6, 'element 损伤 does not heal');
const wm = chessRec({ id: 't_wm', profession: 'MEDIC', subProfessionId: 'wandermedic', attackKind: 'heal', dmgType: 'heal', rangeGrid: R3, skill: null, stats: { atk: 400 } });
const h2 = makeBattle({ defs: { chess: { t_wm: wm, t_tank: tank } }, units: [{ chessId: 't_wm', row: 10, col: 4 }, { chessId: 't_tank', row: 10, col: 5 }], content: 'none' });
h2.step();