feedback5 WE1: 武者 trait heal on every damage instance, through 禁疗 (follow-up 1)

PRTS 分支特性信息 武者: "特性治疗于干员每次输出伤害时触发(不局限于攻击)", "通过自身特性/天赋/技能产生的作用于自身的治疗效果会无视
自身的禁疗" (utage_trait / helage_trait / zuole_trait: ON_OUTPUT_DAMAGE); PRTS 异常效果: an abnormal flag is one switch.

- professions.js installMushaHeal: one heal of the trait value per damage instance dealt to an enemy, at that instance
  (every hit of an attack, skill / item / bond damage); a dodged hit, a 流失, an element 损伤 or a talent / DoT damage heals
  nothing; ignoreHealFree. 收割者 keeps its rule (installReaperHeal) and ignores 禁疗 too (the same PRTS sentence).
- 赫拉格's kit: its second-hit heal patch is gone (redundant). 左乐's S1 reads his HP when it fires, before its strike's
  own heal [ASSUMED].

Golden (diy only): diy-007, diy-008, diy-009 (赫拉格: a multi-target attack heals 70 per target in separate heals instead
of one 70 × n heal — HP identical, heal hook / event counts move); diy-033, diy-034, diy-035 (左乐: heals land between
his S1 / S2 hits, so his HP-dependent T2 roll, 坚忍 and 庇护 read the healed HP from the second instance on). roster,
bonds, fields, matches, standins unchanged (宴 hits once per attack).

Tests: test/sim/feedback5-musha-heal.test.js (new, 4); op_helage, op_zuole, kits_t1t2, kits_t5, kits_alt_t5 pass.
Docs: SIM.md §Professions.
This commit is contained in:
sganggs committed 2026-10-06 15:32:30 +08:00
1 parent 89f730d05c
commit a7d62b5fc1
6 files changed
+192 -97

No files matched your search

+1 -1
View File
@@ -1222,7 +1222,7 @@ header of professions.js).
| craftsman | melee phys (support devices via kit) |
| shotprotector | ranged phys, can hit FLY, blocks 3 |
| fortress | melee single target while blocking, ranged 1.0 splash otherwise, ground only (never hits FLY) |
| unyield / musha / reaper | cannot be healed by others; the heal fires on every enemy the unit damages itself — a normal attack (musha 50 / bb value per hit; reaper 50 × min(hits, block)) and any damage it outputs that no buff produced (the official trait's ON_OUTPUT_DAMAGE, which is why 隐德来希's S2 血镰 cuts heal her while she is disarmed), 50 (bb value) per enemy, the reaper's capped at the block count per instant |
| unyield / musha / reaper | cannot be healed by others. musha: one heal of 50 (bb value) per damage instance it deals to an enemy (PRTS 分支特性信息 武者 "特性治疗于干员每次输出伤害时触发(不局限于攻击)"; the official `utage_trait` / `helage_trait` / `zuole_trait` ON_OUTPUT_DAMAGE): every hit of a normal attack — a double strike heals twice, a dodged hit not at all — and every skill / item / bond damage instance; not a 流失, an element 损伤 or a talent / DoT damage. reaper: 50 × min(enemies hit, block) per normal attack and 50 per other damage instance it outputs that no buff produced (the official trait's ON_OUTPUT_DAMAGE, which is why 隐德来希's S2 血镰 cuts heal her while she is disarmed), capped at the block count per instant. Both trait heals ignore 禁疗 (PRTS 分支特性信息 武者 / 收割者 "通过自身特性/天赋/技能产生的作用于自身的治疗效果会无视自身的禁疗") |
| centurion / crusher / pusher | hit every blocked enemy at once |
| hammer | 50 % splash (bb atk_scale_2) to others within 1 tile |
| instructor | ×1.2 (bb atk_scale) vs enemies it doesn't block |
+2 -13
View File
@@ -10,9 +10,8 @@
// 属性加成,损失一定比例时达最大加成(同类属性取最高)") and ba.protect 庇护 ("受到的物理和法术伤害降低相应比例(同名效果取最高)"), his
// battle skeleton char_188_helage.skel (Skill: two OnAttack, Skill_2: two, Skill_3_Loop: one).
// - Trait (武者) "不成为其他角色的治疗目标,每次攻击到敌人后回复自身70生命": the profession default (professions.js `musha`: no
// heal from others, value HP per enemy an attack hits), ground-only melee on his 1-1, block 1. The branch heals on every
// damage instance he deals: the engine heals once per enemy an attack strikes, so the second hit of S1 / S2's 二连击 on
// an enemy heals once more here (the trait's `value`, a self heal).
// heal from others, `value` HP on every damage instance he deals — PRTS 分支特性信息 武者 — so the second hit of S1 / S2's
// 二连击 heals too), ground-only melee on his 1-1, block 1.
// - T1 月盈星亏 "在场时,自身获得最高+100攻击速度的坚忍(损失70%生命值时达到最大加成)": ASPD + min_attack_speed × the share of
// the HP lost up to 1 − min_hp_ratio (linear, 坚忍), refreshed every tick. SBL-X stage 3: +130 at 50 % lost (the module
// talent change).
@@ -90,16 +89,6 @@ export default {
} },
],
install(battle, unit) {
// 武者: the trait heal on every damage instance — the engine heals once per enemy an attack strikes, so each further
// hit of the same attack on that enemy (S1, S2's 二连击) heals once more
battle.on('damaged', (c) => {
const d = c.dmg;
if (c.source !== unit || !unit.alive || !d || !d.isAttack || !d.attackId || !c.target || c.target.side !== 'enemy') return;
const m = unit.mem;
if (m.helageAttackId !== d.attackId) { m.helageAttackId = d.attackId; m.helageStruck = new Set(); }
if (!m.helageStruck.has(c.target.id)) { m.helageStruck.add(c.target.id); return; }
battle.heal(unit, unit, num(unit.profile?.selfHeal, num(tb.value, 70)), { self: true });
}, { owner: unit, priority: -10 });
// SBL-X “藏锋”: 生命值低于50%时,获得25%的庇护 (physical and arts damage taken)
const dr = num(t0.damage_resistance), drBelow = num(t0.hp_ratio);
if (dr > 0 && drBelow > 0) {
+9 -4
View File
@@ -9,8 +9,8 @@
// `[shield_core]`, `zuole_s_3` / `[shield]`, `zuole_e_002[resistance]`, `zuole_e_003_talent`, `zuole_e_003_trait`);
// Arknights Terra Wiki "Zuo Le" (the barrier caps count each skill's own barrier only).
// - Trait (武者) "不成为其他角色的治疗目标,每次攻击到敌人后回复自身70生命": the profession default (musha: no heal from
// others, `value` per enemy hit; zuole_trait heals on every damage he outputs — the profession heals a normal attack
// per target and any other damage per instance, so S1's extra strikes are separate damage instances).
// others; zuole_trait heals on every damage he outputs — the profession heals `value` per damage instance, PRTS 分支特性信息
// 武者, so S1's extra strikes heal too).
// - T1 秉烛照影 "在场时,自身获得最高+50攻击速度和技力自然回复速度+2/秒的坚忍(损失70%生命值时达到最大加成)" (bb
// min_attack_speed / min_sp_recovery_per_sec / min_hp_ratio): 坚忍 (ba.berserk "根据已损失的生命值获得相应比例的属性
// 加成") — ASPD and SP recovery × min(1, lost HP ÷ (1 − min_hp_ratio)) while he is deployed. SBL-X stage 3: +70 /
@@ -29,7 +29,8 @@
// not while a 不死 already holds him ("_dontConsumeWhenUndeadable": 坚固维式重锤's window, items/battle.js holdsUndying).
// - S1 破虏 (AUTO, 可充能 2 次): the next attack at atk_scale × ATK; "自身生命低于80%时额外攻击1次,低于50%时额外攻击2次"
// (hp_ratio_double / hp_ratio_tripple): one / two more strikes on that target at the same scale, separate damage
// instances [ASSUMED: the same target — nothing when it fell]. The data's DEFAULT trigger (an AUTO "next attack").
// instances [ASSUMED: the same target — nothing when it fell; the HP read when the skill fires, before the strike's
// own trait heal]. The data's DEFAULT trigger (an AUTO "next attack").
// - S2 行险 (MANUAL, 12 s): "立即流失50%当前生命" (zuole_s_2[shield] DamageViaCurHpRatio, undeadable: never below 1 HP;
// a 流失), then a barrier of scale × max HP (ba.barrier "可以吸收一定数值的伤害") added to the one he still has, capped at
// max_scale × max HP at each gain (PRTS "上限于每次获取屏障时实时发生变化"; Terra: the cap counts this skill's barrier
@@ -96,10 +97,14 @@ export default {
skills: {
[S1]: {
kind: num(s1?.maxChargeTime, 1) > 1 ? 'charges' : 'instant',
// the HP that decides the extra strikes: when the skill fires, before its strike lands (the trait heals at each
// damage instance — professions.js musha — so after the first strike he would read healed) [ASSUMED]
onStart({ unit }) { unit.mem.zuoleS1Hp = unit.hpRatio; },
attack: {
atkScale: num(b1.atk_scale, 1),
onHit({ battle, unit, target }) {
const r = unit.hpRatio;
const r = unit.mem.zuoleS1Hp ?? unit.hpRatio;
unit.mem.zuoleS1Hp = null;
const extra = (r < num(b1.hp_ratio_double, 0.8) ? 1 : 0) + (r < num(b1.hp_ratio_tripple, 0.5) ? 1 : 0);
for (let i = 0; i < extra && target && target.alive; i++) {
battle.dealDamage(unit, target, { amount: unit.s.atk * num(b1.atk_scale, 1), type: 'phys', isSkill: true, tags: ['skill'] });
+38 -18
View File
@@ -58,26 +58,46 @@ export const PROFESSION_DEFAULTS = Object.freeze({
// install helpers (per-unit hooks). All use engine helpers only; tunables come from unit.profile.
/**
* 武者 / 收割者 self-heal ("每次攻击到敌人回复自身50生命"; 收割者 adds "最大生效数等于阻挡数"). Normal attacks arrive as one
* 'attack' event whose `targets` are every enemy hit.
* 武者 (musha) trait "每次攻击到敌人后回复自身N生命" (bb value) with the branch rules of PRTS 分支特性信息 武者: "特性治疗于干员每次
* 输出伤害时触发(不局限于攻击)" and "常态持有禁疗;通过自身特性/天赋/技能产生的作用于自身的治疗效果会无视自身的禁疗". The official
* trait buffs (buff_template_data `utage_trait`, `helage_trait`, `zuole_trait`) fire on ON_OUTPUT_DAMAGE: one heal per damage
* instance the unit deals to an enemy — every hit of a normal attack (the second hit of a double strike, a splash or
* chain victim) and every skill / item / bond damage instance credited to it; a dodged hit deals none (no `damaged`).
* Not a 流失 (no damage dealt), not an element 损伤 (a gauge fill, no HP damage — as 咒愈师), not a buff-made damage
* (talent / DoT tags: attackType BUFF — [ASSUMED] as before; only 隐德来希's template filters it and no 武者 kit deals such
* damage). The heal ignores 禁疗 (`ignoreHealFree`: an abnormal flag is one switch whoever set it — PRTS 异常效果).
*/
const installMushaHeal = (battle, unit) => {
battle.on('damaged', (c) => {
if (c.source !== unit || !unit.alive || !c.target || c.target.side !== 'enemy') return;
const dmg = c.dmg;
if (!dmg || isHpLoss(dmg) || c.type === 'element') return;
const tags = dmg.tags || [];
if (tags.includes('talent') || tags.includes('dot') || tags.includes('periodic')) return;
battle.heal(unit, unit, unit.profile.selfHeal ?? 50, { self: true, ignoreHealFree: true });
}, { owner: unit, priority: -10 });
};
/**
* 收割者 (reaper) self-heal ("每次攻击到敌人回复自身50生命,最大生效数等于阻挡数"). Normal attacks arrive as one 'attack' event
* whose `targets` are every enemy hit.
*
* The official trait is a buff ON the operator that fires on ON_OUTPUT_DAMAGE — buff_template_data `etlchi_trait`,
* `excu2_trait`, `utage_trait`, `helage_trait`, `zuole_trait` all react to any damage the unit outputs, and 隐德来希's
* filters the attackType BUFF out (damage produced by a buff/talent — e.g. her own 萃血 DoT). So skill damage heals too:
* 隐德来希's S2 blood sickles restore her life although the skill stops her attacks (`attack: { noAttack: true }`) — the
* sickle's AOEDamage nodes are attackType NORMAL and PRTS 备注 says "伤害来源始终视为隐德来希". Every such hit arrives as
* one 'damaged' event per enemy, so the reaper cap ("最大生效数") is applied per instant here: the official `[heal_fake]`
* window is 0.05 s and its stack count is the block number (`SetStackCountViaBlockNum`). [ASSUMED] the sim uses the same
* `battle.time` as its window, so simultaneous hits (both 血镰, an AoE) share the block-count cap; a normal attack has its
* own event and its own cap, as before.
* `excu2_trait` react to any damage the unit outputs, and 隐德来希's filters the attackType BUFF out (damage produced by a
* buff/talent — e.g. her own 萃血 DoT). So skill damage heals too: 隐德来希's S2 blood sickles restore her life although the
* skill stops her attacks (`attack: { noAttack: true }`) — the sickle's AOEDamage nodes are attackType NORMAL and PRTS 备注
* says "伤害来源始终视为隐德来希". Every such hit arrives as one 'damaged' event per enemy, so the reaper cap ("最大生效数") is
* applied per instant here: the official `[heal_fake]` window is 0.05 s and its stack count is the block number
* (`SetStackCountViaBlockNum`; PRTS 分支特性信息 收割者 "触发后的0.05s内最多只能累加等于自身阻挡数的治疗次数"). [ASSUMED] the sim uses
* the same `battle.time` as its window, so simultaneous hits (both 血镰, an AoE) share the block-count cap; a normal attack
* has its own event and its own cap, as before. Like the 武者's, the heal ignores 禁疗 (PRTS 分支特性信息 收割者 "通过自身特性/
* 天赋/技能产生的作用于自身的治疗效果会无视自身的禁疗").
*/
const installSelfHealOnHit = (capByBlock) => (battle, unit) => {
const heal = (n) => { if (n > 0 && unit.alive) battle.heal(unit, unit, (unit.profile.selfHeal ?? 50) * n, { self: true }); };
const installReaperHeal = (battle, unit) => {
const heal = (n) => { if (n > 0 && unit.alive) battle.heal(unit, unit, (unit.profile.selfHeal ?? 50) * n, { self: true, ignoreHealFree: true }); };
battle.on('attack', (ctx) => {
if (ctx.attacker !== unit || !unit.alive) return;
let n = ctx.targets.length;
if (capByBlock) n = Math.min(n, Math.max(1, unit.s.blockCnt));
heal(n);
heal(Math.min(ctx.targets.length, Math.max(1, unit.s.blockCnt)));
}, { owner: unit, priority: -10 });
battle.on('damaged', (c) => {
if (c.source !== unit || !unit.alive || !c.target || c.target.side !== 'enemy') return;
@@ -87,7 +107,7 @@ const installSelfHealOnHit = (capByBlock) => (battle, unit) => {
if (tags.includes('talent') || tags.includes('dot') || tags.includes('periodic')) return; // attackType BUFF
const mem = unit.mem;
if (mem.selfHealAt !== battle.time) { mem.selfHealAt = battle.time; mem.selfHealN = 0; }
if (capByBlock && mem.selfHealN >= Math.max(1, unit.s.blockCnt)) return;
if (mem.selfHealN >= Math.max(1, unit.s.blockCnt)) return;
mem.selfHealN++;
heal(1);
}, { owner: unit, priority: -10 });
@@ -537,8 +557,8 @@ export const SUB = Object.freeze({
const [fr, fc] = frontOf(unit.tileR, unit.tileC, unit.dir);
return bodyOnTile(target, unit.tileR, unit.tileC) || bodyOnTile(target, fr, fc) ? 1 : (unit.profile.rangedScale ?? 0.8);
} }),
musha: P({ noHeal: true, install: installSelfHealOnHit(false) }),
reaper: P({ noHeal: true, allInRange: true, install: installSelfHealOnHit(true) }),
musha: P({ noHeal: true, install: installMushaHeal }),
reaper: P({ noHeal: true, allInRange: true, install: installReaperHeal }),
sword: P({ hits: 2 }),
artsfghter: P({ dmgType: 'arts' }),
swordmaster: P({ hits: 2 }),
+61 -61
View File
@@ -287,8 +287,8 @@
["-","~2","device","trap_1105_accrate",null,null,100,1,2,0,0,0,0,0,null]
],
"enemies": {"enemy_1006_shield_3":[21,0,21,35620,6038],"enemy_1043_zomsbr":[66,12,54,4385,116897],"enemy_1402_tgshd_2":[36,0,36,36780,9576]},
"hooks": [1,146,6574,427,427,593,589,6417,33,144,29,29,0,4685,11,50,123,111,1,33,0,0,4],
"events": {"atk":458,"deploy":21,"die":33,"dmg":589,"engage":12,"fx":197,"heal":192,"leak":111,"skill":58,"spawn":137,"status":41},
"hooks": [1,146,6574,427,427,593,589,6448,33,144,29,29,0,4685,11,50,123,111,1,33,0,0,4],
"events": {"atk":458,"deploy":21,"die":33,"dmg":589,"engage":12,"fx":197,"heal":215,"leak":111,"skill":58,"spawn":137,"status":41},
"fx": {"dodge":4,"expose":192,"slash":1},
"snaps": "c92d9074 b25de41b 83b87205 585bc8a5 ec7b191d 1ead25fd ab39b5e0 50b8e88f 406d11cf cb7cc1dd a6757124 871c5780 109309f9 3f5a1e7a 78d1573a 9f44ed0b 8aa93207 c62435a8 a34ba4ab 26499ef1 eea9267a f6e419be"
},
@@ -326,8 +326,8 @@
["p1",12,"op","chess_char_6_diy2_b",2,"none",0,0,84,0,3930,10224,3220,50,0]
],
"enemies": {"enemy_1166_dusbr_2":[39,7,32,17923,120234],"enemy_1169_duphlx_2":[57,1,56,79951,96856],"enemy_1170_dushld_2":[51,0,51,47715,17032]},
"hooks": [1,167,6569,1168,1168,1437,1421,1160,27,166,82,81,0,14678,0,27,147,139,1,31,0,0,16],
"events": {"atk":1283,"deploy":20,"die":27,"dmg":1421,"engage":12,"fx":20,"heal":995,"leak":139,"skill":163,"spawn":159,"status":147},
"hooks": [1,167,6569,1168,1168,1437,1421,1275,27,166,82,81,0,14678,0,27,147,139,1,31,0,0,16],
"events": {"atk":1283,"deploy":20,"die":27,"dmg":1421,"engage":12,"fx":20,"heal":1095,"leak":139,"skill":163,"spawn":159,"status":147},
"fx": {"dodge":16,"undying":4},
"snaps": "7b82b5b1 65791cc9 b9dff287 fb7bacb2 6dfce30d e78e095d f376523d 9466c3bb fc6d1c98 fa227963 7c675c6c 5e9c570e 552e59fe 0257df0c 59aa5f2e 3fe8638e f2894349 f59abe86 210b4b6c 7bc36047 a7187a45 06806640"
},
@@ -367,8 +367,8 @@
["-","~2","device","trap_1105_accrate",null,null,100,1,2,0,0,0,0,0,null]
],
"enemies": {"enemy_1267_nhpbr_2":[27,6,21,9609,62860],"enemy_1270_nhstlk_2":[36,8,28,35302,263906],"enemy_9006_actoxi":[39,16,23,91782,499764]},
"hooks": [1,122,6151,1264,1264,1849,1843,5883,40,112,97,97,0,18491,0,39,102,72,1,46,0,0,6],
"events": {"atk":1438,"deploy":18,"die":40,"dmg":1817,"engage":6,"fx":45,"heal":1018,"leak":72,"skill":194,"spawn":116,"status":216},
"hooks": [1,122,6151,1264,1264,1849,1843,6057,40,112,97,97,0,18491,0,39,102,72,1,46,0,0,6],
"events": {"atk":1438,"deploy":18,"die":40,"dmg":1817,"engage":6,"fx":45,"heal":1192,"leak":72,"skill":194,"spawn":116,"status":216},
"fx": {"dodge":6,"shield":11,"undying":6,"zone":22},
"snaps": "017c815a 30dd073f 5e7040ab 504fbcb6 8f54cda0 de1323f2 b79a53aa fc123f30 f1c9c547 2427819b b640058d 03b832e8 02cc55b8 a091843a 34961763 7e770c16 c42cd712 14732f12 2e4ab773 26025262 0e0d62b6"
},
@@ -1317,37 +1317,37 @@
"round": 13,
"seed": 2802554051,
"waveId": "act1autochess_h06",
"time": 220.67,
"ticks": 6620,
"time": 225.27,
"ticks": 6758,
"reason": "cleared",
"killed": 95,
"killed": 92,
"total": 96,
"unspawned": 0,
"bossHpLeft": null,
"rngDraws": 1046,
"rngDraws": 974,
"errors": {"count":0,"kinds":[]},
"players": {"p1":{"killed":95,"total":96,"perfect":false,"leaks":1,"leaksUncounted":0,"coins":0,"dmg":1576077,"bossDmg":0,"heal":45098,"deaths":4,"layerGains":{}}},
"players": {"p1":{"killed":92,"total":96,"perfect":false,"leaks":4,"leaksUncounted":0,"coins":0,"dmg":1502876,"bossDmg":0,"heal":40667,"deaths":6,"layerGains":{}}},
"units": [
["p1",1,"op","chess_char_5_diy1_a",0,null,0,0,75,9,37758,10266,4480,72,16],
["p1",2,"op","chess_char_5_diy2_a",1,null,1636.73,1,102,3,106690,12409,8260,101,5],
["p1",3,"op","chess_char_5_diy1_a",2,null,1287.81,1,6,10,153953,3014,1408,39,10],
["p1",4,"op","chess_char_5_diy2_b",0,"none",0,0,9,6,68121,9553,6020,55,25],
["p1",5,"op","chess_char_5_diy1_b",1,"none",1833.59,1,10,11,267874,17403,15704,179,8],
["p1",6,"op","chess_char_5_diy2_b",2,"none",275.83,1,12,25,369770,12483,9226,174,10],
["p1",1,"op","chess_char_5_diy1_a",0,null,0,0,75,9,32027,9496,3710,61,14],
["p1",2,"op","chess_char_5_diy2_a",1,null,2023.57,1,102,5,106561,12232,8470,104,5],
["p1",3,"op","chess_char_5_diy1_a",2,null,0,0,6,10,106424,4371,1478,42,6],
["p1",4,"op","chess_char_5_diy2_b",0,"none",0,0,9,6,66204,9273,5740,50,25],
["p1",5,"op","chess_char_5_diy1_b",1,"none",0,0,10,9,217468,16772,13239,160,6],
["p1",6,"op","chess_char_5_diy2_b",2,"none",3074.6,1,12,27,394691,8488,8029,168,10],
["p1",7,"op","chess_char_6_diy1_b",0,"none",1404,1,3,0,0,0,0,0,5],
["p1",8,"op","chess_char_6_diy2_b",1,"none",1404,1,4,10,124117,0,0,98,3],
["p1",8,"op","chess_char_6_diy2_b",1,"none",1404,1,4,8,126465,0,0,97,3],
["p1",9,"op","chess_char_6_diy1_b",2,"none",1404,1,13,3,52108,0,0,21,2],
["p1",10,"op","chess_char_6_diy2_b",0,"uniequip_002_veen",1594,1,14,0,64123,0,0,18,5],
["p1",11,"op","chess_char_6_diy1_b",1,"uniequip_002_veen",1594,1,5,7,147483,0,0,98,4],
["p1",12,"op","chess_char_6_diy2_b",2,"uniequip_002_veen",394,1,11,11,184082,1200,0,150,1],
["p1",11,"op","chess_char_6_diy1_b",1,"uniequip_002_veen",1594,1,5,5,148265,0,0,97,4],
["p1",12,"op","chess_char_6_diy2_b",2,"uniequip_002_veen",1594,1,11,9,188540,0,0,144,1],
["-","~1","device","trap_1105_accrate",null,null,100,1,1,0,0,0,0,0,null],
["-","~2","device","trap_1105_accrate",null,null,100,1,2,0,0,0,0,0,null]
],
"enemies": {"enemy_1229_darmy":[27,27,0,4006,211632],"enemy_1229_darmy_2":[57,56,1,21150,577910],"enemy_9007_acelem":[12,12,0,27068,806216]},
"hooks": [1,112,6620,1069,1069,4022,4085,970,99,100,94,89,34,39900,23,52,96,1,1,99,0,7,0],
"events": {"atk":1145,"deploy":14,"die":99,"dmg":3832,"engage":11,"fx":126,"heal":575,"leak":1,"skill":183,"spawn":110,"status":146},
"fx": {"aoe":20,"beam":19,"burst":7,"shield":13,"veenBounce":25,"veenVolley":42},
"snaps": "1000ac84 61b45deb c330d84e 3478aafc 688a1fd5 5fc55cc7 1a9ebd08 d73f2698 941941b6 a8677708 207a2bf7 dbe48a63 de744785 786cb545 4b8db9b9 94aac4dd e4208478 205d76b4 9e7bf630 1bc4f565 7f4f3e53 78c8fa29"
"enemies": {"enemy_1229_darmy":[27,26,1,1994,207861],"enemy_1229_darmy_2":[57,56,1,21055,583822],"enemy_9007_acelem":[12,10,2,25884,730367]},
"hooks": [1,112,6758,1026,1026,3902,3959,974,98,102,86,81,43,38912,19,52,96,4,1,98,0,6,0],
"events": {"atk":1086,"deploy":14,"die":98,"dmg":3711,"engage":11,"fx":125,"heal":588,"leak":4,"skill":167,"spawn":110,"status":127},
"fx": {"aoe":16,"beam":16,"burst":6,"shield":11,"veenBounce":30,"veenVolley":46},
"snaps": "1000ac84 61b45deb c330d84e 3478aafc 688a1fd5 5fc55cc7 1a9ebd08 d73f2698 941941b6 a8677708 207a2bf7 dbe48a63 bf542856 38597675 cef9ed1d fc29feec ff10b169 cdd4f91e bbfe8df5 76869883 18507da3 44d492a5 9ea2d59f"
},
"diy-034": {
"about": "5_diy1_b=zuole/S1/002_zuole 5_diy2_b=zuole/S2/002_zuole 5_diy1_b=zuole/S3/002_zuole 5_diy2_b=zuole/S1/003_zuole 5_diy1_b=zuole/S2/003_zuole 5_diy2_b=zuole/S3/003_zuole 6_diy1_a=zuole/S1 6_diy2_a=zuole/S2 6_diy1_a=zuole/S3 6_diy2_b=zuole/S1 6_diy1_b=zuole/S2 6_diy2_b=zuole/S3",
@@ -1361,35 +1361,35 @@
"time": 230,
"ticks": 6900,
"reason": "timeout",
"killed": 16,
"killed": 15,
"total": 147,
"unspawned": 0,
"bossHpLeft": null,
"rngDraws": 2147,
"rngDraws": 2492,
"errors": {"count":0,"kinds":[]},
"players": {"p1":{"killed":16,"total":147,"perfect":false,"leaks":131,"leaksUncounted":0,"coins":0,"dmg":1261990,"bossDmg":0,"heal":96175,"deaths":16,"layerGains":{}}},
"players": {"p1":{"killed":15,"total":147,"perfect":false,"leaks":132,"leaksUncounted":0,"coins":0,"dmg":1402868,"bossDmg":0,"heal":104954,"deaths":15,"layerGains":{}}},
"units": [
["p1",1,"op","chess_char_5_diy1_b",0,"uniequip_002_zuole",0,0,148,2,80200,20716,13650,90,65],
["p1",2,"op","chess_char_5_diy2_b",1,"uniequip_002_zuole",3533,1,171,2,79796,12666,5600,52,3],
["p1",3,"op","chess_char_5_diy1_b",2,"uniequip_002_zuole",1513.62,1,10,3,309833,10424,8404,188,13],
["p1",4,"op","chess_char_5_diy2_b",0,"uniequip_003_zuole",0,0,162,1,57220,25082,19590,107,80],
["p1",1,"op","chess_char_5_diy1_b",0,"uniequip_002_zuole",0,0,148,1,48186,17636,10570,70,50],
["p1",2,"op","chess_char_5_diy2_b",1,"uniequip_002_zuole",3533,1,171,1,64068,12176,5110,48,3],
["p1",3,"op","chess_char_5_diy1_b",2,"uniequip_002_zuole",1226.5,1,10,3,301700,9414,7107,187,11],
["p1",4,"op","chess_char_5_diy2_b",0,"uniequip_003_zuole",0,0,162,1,54243,23976,18610,106,75],
["p1",5,"op","chess_char_5_diy1_b",1,"uniequip_003_zuole",3833,1,170,1,91950,14798,8600,57,3],
["p1",6,"op","chess_char_5_diy2_b",2,"uniequip_003_zuole",1284.93,1,6,1,202371,15617,13069,195,15],
["p1",7,"op","chess_char_6_diy1_a",0,null,0,0,109,0,5125,12506,6720,49,30],
["p1",6,"op","chess_char_5_diy2_b",2,"uniequip_003_zuole",1692.08,1,6,0,220795,14247,12106,191,14],
["p1",7,"op","chess_char_6_diy1_a",0,null,0,0,109,0,3181,9496,3710,31,17],
["p1",8,"op","chess_char_6_diy2_a",1,null,0,0,4,0,18827,4573,1680,17,1],
["p1",9,"op","chess_char_6_diy1_a",2,null,0,0,8,1,9729,4503,1610,24,2],
["p1",10,"op","chess_char_6_diy2_b",0,"none",0,0,99,0,4589,11406,4340,28,21],
["p1",9,"op","chess_char_6_diy1_a",2,null,655.17,1,8,1,129138,14795,12557,193,17],
["p1",10,"op","chess_char_6_diy2_b",0,"none",0,0,99,3,80420,18616,11550,72,54],
["p1",11,"op","chess_char_6_diy1_b",1,"none",0,0,94,1,40980,10356,3290,30,2],
["p1",12,"op","chess_char_6_diy2_b",2,"none",2789.98,1,15,4,361369,10365,9622,179,15],
["p1",12,"op","chess_char_6_diy2_b",2,"none",2587.15,1,15,3,349380,11010,10065,187,16],
["-","~1","device","trap_1105_accrate",null,null,100,1,1,0,0,0,0,0,null],
["-","~2","device","trap_1105_accrate",null,null,100,1,2,0,0,0,0,0,null],
["-","~3","device","trap_1105_accrate",null,null,100,1,3,0,0,0,0,0,null]
],
"enemies": {"enemy_1166_dusbr_2":[39,12,27,17108,455191],"enemy_1169_duphlx_2":[57,3,53,44078,412497],"enemy_1170_dushld_2":[51,1,48,82327,394302]},
"hooks": [1,171,6900,1384,1384,2515,2524,2059,32,160,250,250,0,33148,97,41,147,128,1,36,0,0,0],
"events": {"atk":1476,"deploy":21,"die":32,"dmg":2352,"engage":12,"fx":58,"heal":1228,"leak":128,"skill":500,"spawn":162,"status":409},
"fx": {"aoe":45,"revive":4,"shield":9},
"snaps": "1e9ebc19 d4bd250b 4605de58 ece4b8e2 6779ab8e 98039abc 46e4c366 a5a081ed 5cb8201b e200e4c9 00478c83 6857f669 6f8bdb84 57f691c5 55d605b8 d3cc1811 faadb2c3 c31f25ab 48116784 52134e71 57b45c1e ec6d47b0 a5ee3a6b"
"enemies": {"enemy_1166_dusbr_2":[39,9,30,11627,468469],"enemy_1169_duphlx_2":[57,5,49,65204,575530],"enemy_1170_dushld_2":[51,1,49,75409,358869]},
"hooks": [1,171,6900,1629,1629,2932,2941,2496,30,158,263,263,0,37820,124,41,147,128,1,34,0,0,0],
"events": {"atk":1718,"deploy":21,"die":30,"dmg":2712,"engage":12,"fx":71,"heal":1448,"leak":128,"skill":526,"spawn":162,"status":473},
"fx": {"aoe":58,"revive":4,"shield":9},
"snaps": "1e9ebc19 d4bd250b 4605de58 ece4b8e2 72f0d0c2 508cc99b fa07e8e8 3e935320 527be598 84abcf85 52456235 51f00d0c d7233672 bb747210 eac77a3c 98f57f86 6950cf40 0cfe102c 158dfd05 fd89d38d 114ee0a9 d961a07e dca6566b"
},
"diy-035": {
"about": "6_diy1_b=zuole/S1/002_zuole 6_diy2_b=zuole/S2/002_zuole 6_diy1_b=zuole/S3/002_zuole 6_diy2_b=zuole/S1/003_zuole 6_diy1_b=zuole/S2/003_zuole 6_diy2_b=zuole/S3/003_zuole 5_diy1_a=chyue/S1 5_diy2_a=chyue/S2 5_diy1_a=chyue/S3 5_diy2_b=chyue/S1 5_diy1_b=chyue/S2 5_diy2_b=chyue/S3",
@@ -1400,35 +1400,35 @@
"round": 13,
"seed": 4056308894,
"waveId": "act1autochess_h06",
"time": 230,
"ticks": 6900,
"reason": "timeout",
"killed": 42,
"time": 218.43,
"ticks": 6553,
"reason": "cleared",
"killed": 45,
"total": 150,
"unspawned": 0,
"bossHpLeft": null,
"rngDraws": 850,
"rngDraws": 842,
"errors": {"count":0,"kinds":[]},
"players": {"p1":{"killed":42,"total":150,"perfect":false,"leaks":108,"leaksUncounted":0,"coins":0,"dmg":265746,"bossDmg":0,"heal":24599,"deaths":8,"layerGains":{}}},
"players": {"p1":{"killed":45,"total":150,"perfect":false,"leaks":105,"leaksUncounted":0,"coins":0,"dmg":268198,"bossDmg":0,"heal":24035,"deaths":8,"layerGains":{}}},
"units": [
["p1",1,"op","chess_char_6_diy1_b",0,"uniequip_002_zuole",2822.8,1,4,7,43385,15191,14481,195,73],
["p1",1,"op","chess_char_6_diy1_b",0,"uniequip_002_zuole",3370.6,1,4,8,46766,13664,13501,184,69],
["p1",2,"op","chess_char_6_diy2_b",1,"uniequip_002_zuole",3533,1,1,0,0,0,0,0,0],
["p1",3,"op","chess_char_6_diy1_b",2,"uniequip_002_zuole",3533,1,5,3,18153,0,0,0,5],
["p1",3,"op","chess_char_6_diy1_b",2,"uniequip_002_zuole",3533,1,5,3,20683,0,0,0,5],
["p1",4,"op","chess_char_6_diy2_b",0,"uniequip_003_zuole",3913,1,6,0,0,0,0,0,0],
["p1",5,"op","chess_char_6_diy1_b",1,"uniequip_003_zuole",1580.43,1,7,4,22748,8493,6160,88,6],
["p1",6,"op","chess_char_6_diy2_b",2,"uniequip_003_zuole",3806,1,9,15,93432,4065,3958,144,9],
["p1",7,"op","chess_char_5_diy1_a",0,null,2160,1,168,1,13166,4320,0,69,11],
["p1",5,"op","chess_char_6_diy1_b",1,"uniequip_003_zuole",3158.64,1,7,4,26111,8174,7420,110,6],
["p1",6,"op","chess_char_6_diy2_b",2,"uniequip_003_zuole",3913,1,9,15,87326,3114,3114,125,8],
["p1",7,"op","chess_char_5_diy1_a",0,null,2160,1,168,1,13329,4320,0,68,11],
["p1",8,"op","chess_char_5_diy2_a",1,null,2160,1,2,0,0,0,0,0,0],
["p1",9,"op","chess_char_5_diy1_a",2,null,2160,1,3,0,0,0,0,0,0],
["p1",10,"op","chess_char_5_diy2_b",0,"none",0,0,115,5,27468,4950,0,95,20],
["p1",11,"op","chess_char_5_diy1_b",1,"none",2475,1,169,7,35599,4950,0,84,8],
["p1",12,"op","chess_char_5_diy2_b",2,"none",2475,1,167,0,11794,4950,0,74,6]
["p1",10,"op","chess_char_5_diy2_b",0,"none",0,0,115,4,22246,4950,0,88,18],
["p1",11,"op","chess_char_5_diy1_b",1,"none",0,0,105,10,39960,4950,0,91,9],
["p1",12,"op","chess_char_5_diy2_b",2,"none",2475,1,167,0,11776,4950,0,74,6]
],
"enemies": {"enemy_1249_lysdb_2":[60,22,38,4031,128352],"enemy_1251_lysyta":[51,11,40,41,38148],"enemy_1329_cbshld":[39,9,29,34418,99246]},
"hooks": [1,169,6900,961,961,1163,1141,577,50,157,138,138,0,23691,14,45,150,107,1,50,0,0,0],
"events": {"atk":961,"deploy":19,"die":50,"dmg":1105,"engage":7,"fx":60,"heal":357,"leak":107,"skill":276,"spawn":162,"status":64},
"fx": {"aoe":26,"shield":6,"shieldBreak":28},
"snaps": "23a6b6b3 47e61689 6f8fe225 a3005a03 e548dbed 8cc09f82 7d0316d2 86f335cf 958e37e2 2dc604e4 b505d117 708f9fc7 ad63f1c4 7a8d199d 79dae266 e56649fb 423e4735 ee1a31bb a164c69b 67648206 48dda4f8 d6cff8cf d0e3c567"
"enemies": {"enemy_1249_lysdb_2":[60,23,37,4051,132480],"enemy_1251_lysyta":[51,13,38,41,38184],"enemy_1329_cbshld":[39,9,30,31856,97534]},
"hooks": [1,168,6553,940,940,1145,1123,558,53,158,132,132,0,22629,15,45,150,105,1,53,0,0,0],
"events": {"atk":940,"deploy":18,"die":53,"dmg":1089,"engage":7,"fx":61,"heal":348,"leak":105,"skill":264,"spawn":162,"status":60},
"fx": {"aoe":27,"shield":6,"shieldBreak":28},
"snaps": "23a6b6b3 5fc487bd 05667bb0 cd3ab6aa 62903576 50c474ef 6da9cfb6 d82c7dea 93708874 9712eb4d 1c4cdfef cae0caef 2c44b876 f7f42361 95d0d378 d8d4fdca 5874f7a9 8be8481a 1dd22cc3 5e311c7d 1d8d69c4 bc1809f8"
},
"diy-036": {
"about": "5_diy1_b=chyue/S1/002_chyue 5_diy2_b=chyue/S2/002_chyue 5_diy1_b=chyue/S3/002_chyue 5_diy2_b=chyue/S1/003_chyue 5_diy1_b=chyue/S2/003_chyue 5_diy2_b=chyue/S3/003_chyue 6_diy1_a=chyue/S1 6_diy2_a=chyue/S2 6_diy1_a=chyue/S3 6_diy2_b=chyue/S1 6_diy1_b=chyue/S2 6_diy2_b=chyue/S3",
+81
View File
@@ -0,0 +1,81 @@
// test/sim/feedback5-musha-heal.test.js — the 武者 trait heal on every damage instance (PRTS 分支特性信息 武者 "特性治疗于干员每次
// 输出伤害时触发(不局限于攻击)", "常态持有禁疗;通过自身特性/天赋/技能产生的作用于自身的治疗效果会无视自身的禁疗") —
// server/sim/professions.js installMushaHeal; 赫拉格's kit no longer patches his double strikes (op-helage.js), 左乐's S1 reads
// his HP when it fires (op-zuole.js).
// Run: node --test test/sim/feedback5-musha-heal.test.js
import { test } from 'node:test';
import assert from 'node:assert/strict';
import { makeBattle, enemyRec, chessRec, checkInvariants } from '../helpers/battleHarness.js';
const VALUE = 50;
/** A 武者 (trait bb value 50) hitting `hits` times per attack; an enemy that dodges `dodge` of physical hits. */
function field({ hits = 1, dodge = 0, kit = null } = {}) {
const musha = chessRec({ id: 't_musha', subProfessionId: 'musha', stats: { maxHp: 1e6, atk: 100, bat: 1, def: 0 }, skill: null });
musha.trait = { desc: '', bb: { value: VALUE } };
const medic = chessRec({ id: 't_medic', profession: 'MEDIC', stats: { maxHp: 1000, atk: 300, bat: 1 }, skill: null, rangeGrid: [[0, 0], [0, -1], [0, -2]] });
const h = makeBattle({
defs: { chess: { t_musha: musha, t_medic: medic }, enemies: { e: { ...enemyRec({ key: 'e', hp: 1e9, speed: 0 }) } } },
kits: { t_musha: () => ({ trait: { hits, ...(kit?.trait ?? {}) }, ...(kit?.rest ?? {}) }) },
units: [{ chessId: 't_musha', row: 10, col: 5 }, { chessId: 't_medic', row: 10, col: 7 }], enemies: [{ key: 'e', pos: [10, 6] }],
content: 'generic', autoFinish: false, timeLimit: 60, hooks: ['heal', 'damaged', 'attack', 'dodge'], captureNoisy: true,
setup: (b) => { if (dodge) b.on('enemySpawn', ({ enemy }) => { b.addBuff(enemy, { key: 'test:dodge', mods: { dodgePhys: dodge } }); }); },
});
h.step();
const u = h.unit('t_musha');
u.hp = u.s.maxHp / 2;
return { h, u, e: h.enemies()[0] };
}
const traitHeals = (h, u) => h.hooksOf('heal').filter((c) => c.source === u && c.target === u && c.opts?.self && !c.opts?.tags);
const done = (h) => { checkInvariants(h.b); assert.equal(h.b.errors.length, 0, JSON.stringify(h.b.errors[0])); };
test('a double strike heals twice: one heal of the trait value per damage instance of the attack (it used to heal once per enemy struck)', () => {
const { h, u } = field({ hits: 2 });
h.run(3.05);
const atks = h.hooksOf('attack').filter((c) => c.attacker === u).length;
const hits = h.hooksOf('damaged').filter((c) => c.source === u && c.dmg.isAttack).length;
assert.ok(atks >= 3 && hits === 2 * atks, `${atks} attacks, ${hits} hits`);
assert.equal(traitHeals(h, u).length, hits, 'a heal per hit');
assert.ok(traitHeals(h, u).every((c) => c.amount === VALUE));
done(h);
});
test('every damage instance it outputs heals, not only attacks; a dodged hit, a 流失, an element 损伤 and a talent / DoT damage do not', () => {
const { h, u, e } = field({ dodge: 1 });
h.run(2.05);
assert.ok(h.hooksOf('dodge').some((c) => c.source === u), 'its hits were dodged');
assert.equal(traitHeals(h, u).length, 0, 'a dodged hit deals no damage: no heal');
const n0 = traitHeals(h, u).length;
h.b.dealDamage(u, e, { amount: 100, type: 'arts', isSkill: true, canDodge: false, tags: ['skill'] });
h.b.dealDamage(u, e, { amount: 100, type: 'true', canDodge: false, tags: ['item', 'item:test'] });
assert.equal(traitHeals(h, u).length - n0, 2, 'skill and item damage heal');
h.b.loseHp(e, 100, { source: u });
h.b.dealDamage(u, e, { type: 'element', element: 'burn', amount: 100 });
h.b.dealDamage(u, e, { amount: 100, type: 'arts', canDodge: false, tags: ['talent', 'dot'] });
assert.equal(traitHeals(h, u).length - n0, 2, 'no heal for a 流失, an element 损伤 or a DoT');
done(h);
});
test('禁疗 does not stop the trait heal ("会无视自身的禁疗"); another unit\'s heal still does not reach it', () => {
const { h, u } = field();
h.b.addBuff(u, { key: 'test:healFree', status: 'healFree', flags: { noHeal: true, healFree: true } });
const hp0 = u.hp;
h.run(1.05);
assert.ok(traitHeals(h, u).length >= 1 && u.hp > hp0, 'healed through 禁疗');
assert.equal(h.hooksOf('heal').filter((c) => c.target === u && c.source !== u).length, 0, 'the medic never heals it');
assert.equal(h.b.heal(h.unit('t_medic'), u, 500), 0);
done(h);
});
test('宴 (the chess): one heal per hit of her attacks', () => {
const h = makeBattle({
defs: { enemies: { e: enemyRec({ key: 'e', hp: 1e9, speed: 0 }) } }, units: [{ chessId: 'chess_char_1_18_a', row: 10, col: 5 }],
enemies: [{ key: 'e', pos: [10, 6] }], autoFinish: false, timeLimit: 60, hooks: ['heal', 'damaged'], captureNoisy: true, seed: 5,
});
h.run(8);
const u = h.unit('chess_char_1_18_a');
const hits = h.hooksOf('damaged').filter((c) => c.source === u && c.target.side === 'enemy' && c.type !== 'element' && !(c.dmg.tags || []).includes('dot')).length;
assert.ok(hits >= 3);
assert.equal(traitHeals(h, u).length, hits);
done(h);
});