feedback5 O15: 自选 operator kits 森蚺, 斥罪 and 黍 — every skill, talent, trait and module (stage 1 / 3) at every form

- kits/ops/op-zumama.js (森蚺, 决战者): trait 阻回 unless blocking (charpack SpRecoveryStopped / SpRecovery); HES-X / HES-Y /
  RA-A turn it into an SP-recovery ratio (−0.8 / −0.999 / −1) while unblocked, HES-Y ATK / DEF +15 % blocking; RA-A's
  生息演算-only parts N/A (愈战愈勇 read from talentsBase); 勇冠三军 polled every 0.1 s (攻击力倍率 / 庇护); 愈战愈勇; S1 passive;
  S2 blockee stun past 抵抗; S3 hpRegenRatio + 5 s self-stun.
- kits/ops/op-judge.js (斥罪, 不屈者): 律法卫士 barrier (deploy / kill / S3, live 300 % cap, S2 ×(1 + shield_scale)); 荆棘环身
  thorns while barrier holds (the breaking hit too); UNY-X ×0.85 from blocked units, UNY-Y +8 % alone; S1 蓄力 (all SP per
  cast, ×1.8 + 3 s stun, attack-cycle reset); S2 no attack, 庇护, x-4 pulses at 0.9 + n s; S3 barrier / ATK / taunt.
- kits/ops/op-shu.js (黍, 守护者): 百谷长青 sown tiles (hpRegen + 庇护, S2 ×scale, GUA-X stage 3 born tile, cleared when she
  leaves); 天有四时 (professions, 4 【岁】 squad ATK + SP / 4 s); S1 as 塞雷娅 S1 (DEFAULT + an ally of x-4 at ≤ half); S2 heals
  of 2 on x-1; S3 range x-2 with Attack-else-Heal, the harvest aura and the 播种标记 teleport; GUA-X ×1.15 at ≤ 50 %. Her S3
  data rule TRY_SEARCH_ALLY_SKILL (no engine rule) is mapped onto SKILL_RANGE + allies on x-2 (reported).
- 庇护 in all three through battle.applyStrongest(unit, 'protect') (同名效果取最高 across these kits).
- kits/index.js: the three files appended to OPERATOR_KIT_FILES.
- test/content/op_zumama.test.js (8), op_judge.test.js (8), op_shu.test.js (9).
- docs/research/03-operators.md: 黍's trigger row (a 自选 pick now, the kit's mapping).
- golden: only diy moved — diy-049 … diy-052 now field 森蚺 / 斥罪 / 黍 and diy-053 … diy-058 are new (the 12-per-battle
  packing appends the new configurations; the prototype / reserve battles of 049 … 052 move to 055 … 058); diy-001 … 048
  and every other family unchanged.
This commit is contained in:
sganggs committed 2026-10-06 01:30:05 +08:00
1 parent fef4ea3d4b
commit 10bb226382
9 files changed
+1926 -99

No files matched your search

+3
View File
@@ -103,6 +103,9 @@ export const OPERATOR_KIT_FILES = Object.freeze([
'op-ifrit.js',
'op-aphris.js',
'op-lin.js',
'op-zumama.js',
'op-judge.js',
'op-shu.js',
]);
/**
+196
View File
@@ -0,0 +1,196 @@
// server/sim/content/kits/ops/op-judge.js — 斥罪 (char_4065_judge) 自选 operator kit: 6★ 不屈者 (重装), an owned-6★ pick of the
// tier-5 and tier-6 自选 slots; every skill, both talents, the trait and both modules at every form.
// Kit contract and the 自选 rules: ../README.md ("How to add an operator (自选)").
//
// Forms (data/backups.json units.char_4065_judge, the DIY slot statuses): normal = E2 Lv1, skills at rank 4, no module;
// elite = E2 Lv60, rank 7, the picked module at stage 1 (tier 5) or 3 (tier 6). Potential 0 [ASSUMED: no account].
// Sources: character_table / skill_table / battle_equip_table (zh_CN, as built into backups.json); PRTS 斥罪 (律法卫士 备注:
// "自身产生的屏障生效优先级为-1000,为单一BUFF,每次“获得”实际效果为补充屏障的吸收量…本天赋的屏障上限仅限制自身通过天赋和技能产生的
// 屏障量…上限会实时计算"; 荆棘环身 备注 "反伤效果会直接选中伤害来源,但仅对阵营为敌方的来源造成伤害"; S1 备注 on 蓄力; S2 备注
// "技能期间,自身丢失全部视野"; S3 备注 "技能获取的屏障为补充天赋屏障的吸收量,且不会令屏障总量超过天赋描述下的屏障获取上限"),
// gamedata_const ba.charged 蓄力 ("技力达到上限可继续回复,回复至上限2倍时进入蓄力状态,此时开启技能会触发额外效果(任何时候开启均消耗
// 全部技力)"); the client's battle data: buff templates judge_t_1 / judge_t_2 / judge_e_002_tr / judge_s_1_enhance_checker /
// judge_s_1[damage] / judge_s_1[mode] / judge_s_2[aoe] / judge_s_2[shield_scale] / judge_s_3[shield], the skill prefabs
// skchr_judge_1..3 and the module prefabs judge_equip_{1,2}_*; Arknights Terra Wiki "Penance".
// - Trait (不屈者) "无法被友方角色治疗": the profession default (professions.js unyield `noHeal`; the charpack's 'unhealable');
// an HP-regen attribute (黍's sown tiles, 吟游者) still reaches her (SIM.md §4). Melee physical, ground-only, block 3, 1-1.
// Module UNY-X “通往未来的荆棘之路” "受到来自自身阻挡单位的伤害降低15%" (trait bb damage_scale; judge_e_002_tr): every damage
// whose source she blocks ×damage_scale. Module UNY-Y “无罪” "周围8格没有友方干员时攻击力和防御力+8%" (trait bb atk / def; the
// prefab's checker counts operators only — professionMask 639): ATK / DEF + while no allied operator stands on the 8
// tiles around her.
// - T1 律法卫士 (judge_t_1): one barrier buff BARRIER. At every deployment +born_hp_ratio × max HP; every enemy she knocks
// out +kill_hp_ratio × max HP; S3 +hp_ratio × max HP — each gain × the shield scale (1, + S2's shield_scale while S2
// runs: judge_s_2[shield_scale]) and only while the barrier is below the cap max_hp_ratio × the CURRENT max HP, then
// capped there (a max-HP change leaves the barrier as it is). The buff ends when its absorb runs out (the client keeps
// an empty buff; nothing else depends on it here). [ASSUMED] it is spent in the engine's oldest-first order among
// several barriers (the client's priority −1000 puts it last); damage reductions (庇护, UNY-X) act before it as the
// client's LOW_PRIORITY. UNY-X stage 3: 70 % / 12 % (module talent change). higher_effect_hp_ratio only sizes the
// barrier's effect (judge_t_1[effect]) — no gameplay.
// - T2 荆棘环身 (judge_t_2, ON_TAKE_DAMAGE): every damage instance an enemy source deals her while she holds barrier (its
// judge_shield[mark]: the absorb before that hit — the hit that breaks it still counts, t_1 runs LOW_PRIORITY) deals
// atk_scale × her ATK arts back to that source (a 普通伤害, InverseDamage `attackType` NORMAL; × the 蓄力 攻击力倍率 while S1's
// 蓄力 attack is out — PRTS 备注), never for a 流失, an element 损伤, a 无来源 hit or reflected damage (tag 'counter'),
// undodgeable [ASSUMED like 星熊's counter]. UNY-Y stage 3: 58 %.
// - S1 一锤定音 (AUTO, time SP, data DEFAULT): the next attack adds atk_scale_2 × ATK arts (a 普通伤害 skill hit, after the
// physical one). 蓄力 (maxChargeTime 2): SP runs on to twice the cost; casting from there (the client's "available count
// ≥ 1" after the cast) also stuns the target `stun` s (before the arts hit) and raises every damage she deals until the
// attack is done ×judge_s_1_enhance_checker.atk_scale (an 攻击力倍率: atkScaleMul). Every cast spends ALL the SP
// (ClearCharacterSp). The moment the SP reaches the 蓄力 mark her attack cycle restarts (judge_s_1[mode] SwitchMode with
// restartFSM, "无关乎能否开启技能"; PRTS 备注 "存在可攻击目标且未被控制的情况下切换后会立刻触发技能"): atkCd = 0. The refund of a
// cast whose target died (_recoverSpIfTargetDead) is not modelled [ASSUMED]: a melee cast and its attack resolve in one
// tick here.
// - S2 坚心苦修 (MANUAL; data DEFAULT — the owner's 重装 exception of 2026-10-05, rawRule TAKE_DAMAGE): `duration` s, no
// attacks (她丢失全部视野), 庇护 damage_resistance (the shared applyStrongest key PROTECT, for the skill's time), and from
// S2_FIRST s on every S2_EVERY s (judge_s_2[aoe]: firstTriggerInterval 0.9, triggerInterval 1) atk_scale × ATK arts (×
// any 攻击力倍率) to every ground enemy she can select on the skill range x-4 (AOEDamage WALK_ONLY, attackType NORMAL); T1's
// gains ×(1 + shield_scale) meanwhile.
// - S3 披荆斩棘 (MANUAL, hurt SP; data DEFAULT, rawRule TAKE_DAMAGE): at once barrier +hp_ratio × max HP (topping T1's,
// capped), then `duration` s ATK +atk, attack interval +base_attack_time s (a flat +0.9 on 1.6 s), taunt +taunt_level.
import { num, traitBb, talentBb, skillRec, toggleBuff, batMod, up, enemiesInGrid } from '../shared/tier1.js';
import { isHpLoss } from '../../../damage.js';
const S1 = 'skchr_judge_1';
const S2 = 'skchr_judge_2';
const S3 = 'skchr_judge_3';
const BARRIER = 'talent:judge:barrier';
const CHARGED = 'skill:judge:charged';
/** S2's 技能范围 when the data carries none (x-4: her tile and the 8 around it). */
const X4 = Object.freeze([[1, -1], [1, 0], [1, 1], [0, -1], [0, 0], [0, 1], [-1, -1], [-1, 0], [-1, 1]]);
/** judge_s_2[aoe]: firstTriggerInterval 0.9 s, then triggerInterval 1 s. */
const S2_FIRST = 0.9;
const S2_EVERY = 1;
/** 庇护 (ba.protect): one "同名效果取最高" effect per unit, whoever grants it (battle.applyStrongest). */
const PROTECT = 'protect';
const protectMods = (v) => ({ physTakenMul: 1 - v, artsTakenMul: 1 - v });
const bbOf = (chess, id) => skillRec(chess, id)?.bb ?? {};
const tagged = (d, tag) => !!(d && Array.isArray(d.tags) && d.tags.includes(tag));
/** Her barrier left (0 without one). */
const barrierOf = (u) => u.findBuff(BARRIER)?.shield ?? 0;
export default {
char_4065_judge: (bb, chess) => {
const tb = traitBb(chess);
const t0 = talentBb(chess, 0), t1 = talentBb(chess, 1);
const b1 = bbOf(chess, S1), b2 = bbOf(chess, S2), b3 = bbOf(chess, S3);
const s2 = skillRec(chess, S2);
const capRatio = num(t0.max_hp_ratio, 3);
/** S2's 庇护 (damage_resistance[inf] for the skill's time). */
const protect = (battle, unit, skill) => {
const dr = num(b2.damage_resistance);
if (dr > 0) battle.applyStrongest(unit, PROTECT, { duration: Math.max(0.05, skill.timeLeft), value: dr, mods: protectMods, source: unit });
};
/** judge_t_1's shield_scale: 1, + S2's shield_scale while S2 runs. */
const shieldScale = (unit) => 1 + (unit.skill?.active && unit.skill.id === S2 ? num(b2.shield_scale) : 0);
/** Add `amount` to her barrier while it is below the cap (the CURRENT max HP), capped there. Returns the gain. */
const gain = (battle, unit, amount) => {
if (!up(unit) || !(amount > 0)) return 0;
const cap = unit.s.maxHp * capRatio;
const cur = unit.findBuff(BARRIER);
const have = cur ? cur.shield : 0;
if (!(cap > have + 1e-9)) return 0;
const next = Math.min(cap, have + amount);
if (cur) { cur.shield = next; unit.markDirty(); } else battle.addBuff(unit, { key: BARRIER, shield: next, visible: true, tags: ['talent'] });
battle.fx('shield', { x: unit.x, y: unit.y, id: unit.id });
return next - have;
};
return {
skills: {
[S1]: {
kind: 'charges',
attack: {
onHit({ battle, unit, target }) {
if (!target || !target.alive) return;
if (unit.mem.judgeCharged) battle.applyStatus(target, 'stun', { duration: num(b1.stun), source: unit });
if (!target.alive) return;
battle.dealDamage(unit, target, { amount: unit.s.atk * num(b1.atk_scale_2) * unit.s.atkScaleMul, type: 'arts', isSkill: true, tags: ['skill', 'judge:gavel'] });
},
},
onStart({ battle, unit, skill }) {
// 蓄力: the cast took one charge of a full stack; every cast spends ALL the SP (ClearCharacterSp)
const charged = skill.maxCharges > 1 && skill.charges >= skill.maxCharges - 1;
skill.charges = 0;
skill.sp = 0;
unit.mem.judgeCharged = charged;
if (charged) {
battle.addBuff(unit, { key: CHARGED, mods: { atkScaleMul: num(b1['judge_s_1_enhance_checker.atk_scale'], 1) }, tags: ['skill'] });
battle.fx('crit', { x: unit.x, y: unit.y, id: unit.id });
}
},
onEnd({ battle, unit }) { unit.mem.judgeCharged = false; battle.removeBuff(unit, CHARGED); },
},
[S2]: {
kind: 'duration',
attack: { noAttack: true },
onStart({ battle, unit, skill }) {
unit.mem.judgeS2Next = S2_FIRST;
protect(battle, unit, skill);
},
onTick({ battle, unit, skill, dt }) {
protect(battle, unit, skill);
unit.mem.judgeS2Next -= dt;
while (unit.mem.judgeS2Next <= 1e-9 && up(unit)) {
unit.mem.judgeS2Next += S2_EVERY;
const foes = enemiesInGrid(battle, unit, s2?.rangeGrid?.length ? s2.rangeGrid : X4, { canHitFly: false });
battle.fx('aoe', { x: unit.x, y: unit.y, radius: 1.5, id: unit.id, skill: 'judge:atonement' });
const amount = unit.s.atk * num(b2.atk_scale) * unit.s.atkScaleMul;
for (const e of foes) if (e.alive) battle.dealDamage(unit, e, { amount, type: 'arts', isSkill: true, tags: ['skill', 'judge:atonement'] });
}
},
onEnd({ battle, unit }) { if (unit.findBuff(PROTECT)?.source === unit) battle.removeBuff(unit, PROTECT); },
},
[S3]: {
kind: 'duration',
mods: { atkPct: num(b3.atk), batPct: batMod(b3.base_attack_time, chess), taunt: num(b3.taunt_level) },
onStart({ battle, unit }) { gain(battle, unit, unit.s.maxHp * num(b3.hp_ratio) * shieldScale(unit)); },
},
},
talents: [
{ install(battle, unit) { // 律法卫士: barrier at every deployment and per enemy she knocks out (≤ max_hp_ratio × max HP)
battle.on('deploy', (ctx) => {
if (ctx.unit === unit) gain(battle, unit, unit.s.maxHp * num(t0.born_hp_ratio) * shieldScale(unit));
}, { owner: unit });
battle.on('kill', ({ killer, victim }) => {
if (killer === unit && victim && victim.side === 'enemy') gain(battle, unit, unit.s.maxHp * num(t0.kill_hp_ratio) * shieldScale(unit));
}, { owner: unit });
} },
{ install(battle, unit) { // 荆棘环身: while she holds barrier, every damage from an enemy source reflects atk_scale × ATK arts
const scale = num(t1.atk_scale);
if (!(scale > 0)) return;
const armed = new WeakSet();
battle.on('hit', (c) => { if (c.target === unit && c.dmg && barrierOf(unit) > 1e-9) armed.add(c.dmg); }, { owner: unit });
battle.on('damaged', (c) => {
const d = c.dmg, src = c.source;
if (c.target !== unit || !d || !armed.has(d) || !src || src.side !== 'enemy' || !src.alive) return;
if (isHpLoss(d) || c.type === 'element' || tagged(d, 'counter')) return;
battle.dealDamage(unit, src, { amount: unit.s.atk * scale * unit.s.atkScaleMul, type: 'arts', canDodge: false, tags: ['talent', 'counter', 'judge:thorns'] });
battle.fx('thorns', { x: unit.x, y: unit.y, id: unit.id });
}, { owner: unit });
} },
],
install(battle, unit) {
// S1 蓄力: the moment the SP reaches twice the cost her attack cycle restarts (judge_s_1[mode], restartFSM)
if (unit.skill?.id === S1) {
let full = false;
battle.on('tick', () => {
const sk = unit.skill;
const now = !!sk && up(unit) && sk.maxCharges > 1 && sk.charges >= sk.maxCharges;
if (now && !full) unit.atkCd = 0;
full = now;
}, { owner: unit });
}
// UNY-X: 受到来自自身阻挡单位的伤害降低15%
const cut = num(tb.damage_scale, 1);
if (cut !== 1) battle.on('hit', (c) => { if (c.target === unit && c.source && c.source.blockedBy === unit) c.dmg.mul *= cut; }, { owner: unit });
// UNY-Y: 周围8格没有友方干员时攻击力和防御力+8% (operators only, her own tile aside)
const ma = num(tb.atk), md = num(tb.def);
if (ma || md) {
const alone = () => !battle.allyUnits.some((a) => a !== unit && a.kind === 'op' && a.alive && a.deployed && !a.hidden
&& Math.max(Math.abs(a.tileR - unit.tileR), Math.abs(a.tileC - unit.tileC)) === 1);
toggleBuff(battle, unit, 'trait:judge:alone', alone, { atkPct: ma, defPct: md });
}
},
};
},
};
+289
View File
@@ -0,0 +1,289 @@
// server/sim/content/kits/ops/op-shu.js — 黍 (char_2025_shu) 自选 operator kit: 6★ 守护者 (重装), an owned-6★ pick of the
// tier-5 and tier-6 自选 slots; every skill, both talents, the trait and her module at every form.
// Kit contract and the 自选 rules: ../README.md ("How to add an operator (自选)").
//
// Forms (data/backups.json units.char_2025_shu, the DIY slot statuses): normal = E2 Lv1, skills at rank 4, no module;
// elite = E2 Lv60, rank 7, GUA-X at stage 1 (tier 5) or 3 (tier 6). Potential 0 [ASSUMED: no account].
// Sources: character_table / skill_table / battle_equip_table (zh_CN, as built into backups.json); PRTS 黍 (百谷长青 备注
// "本天赋效果为永久地块效果,自身退场时清空所有地块的播种效果,不对装置职业的目标生效。生命恢复的提供方式为增加位于地块上干员的
// “生命回复速度”属性,不受治疗加成和禁疗影响"; 天有四时 修正 "在场时" / "编入队伍且"; S1 修正 "小于等于一半"; S2 备注 "实际效果为技能
// 期间普通攻击改为治疗友方单位(受攻速和缴械影响,非停止攻击),“特殊间隔”指攻击间隔+1.3"; S3 备注 "技能持续期间视野扩大至治疗范围,
// 自身普通攻击同时触发治疗,无攻击目标时普通攻击可直接进行治疗", the 【播种标记】 and its teleport, "攻击力/攻击速度提升效果无视孤立");
// PRTS 卫戍协议/帮助 §技能操作 (黍【离离枯荣】 "技能范围内存在可治疗的我方单位时释放技能"); the client's battle data: buff templates
// shu_t_1 / shu_t_1[char] / shu_farm_buff[char] / shu_s_2 / shu_s_3 / shu_s_3[in_range] / shu_s_3[heal_in_attack] /
// shu_s_3[enemy][enter|leave|mark] / heal_scale_up[hpratio][LE], her charpack (the S2 / S3 modes' Attack / Heal
// abilities, DoFarm / EnemyListener tile abilities and their selectors, the 天有四时 auras and deck part) and the skill
// prefabs skchr_shu_1..3; Arknights Terra Wiki "Shu".
// - Trait (守护者) "技能可以治疗友方单位": her normal attack is a melee physical strike (ground-only, block 3, range 0-1 — her own
// tile); only her skills heal. Module GUA-X “钦天司时” "治疗生命值低于50%的友方单位时治疗量提升15%" (trait bb heal_scale /
// hp_ratio): every heal of hers on a target at or below hp_ratio ×heal_scale (the client template is heal_scale_up
// [hpratio][LE]: at or below — the text says 低于), never an HP-regen tick.
// - T1 百谷长青 (shu_t_1): every heal she outputs (S1 / S2 / S3; never a regen tick) sows the healed unit's tile and the four
// next to it (DoFarm, range x-5) — tiles that can be deployed on or walked (the tile selector: buildable or passable). A
// sown tile stays sown until she leaves the field (death / retreat clears them all). Every allied unit standing on one —
// operators and summons, not 装置 (professionMask 767) nor a 孤立 unit (no ignoreTargetFree) — has 生命回复速度
// +hp_recovery_per_sec (an hpRegen buff, checklist 11: 禁疗 / 无法被友方治疗 do not stop it — 斥罪 too) and 庇护
// damage_resistance (the shared applyStrongest key PROTECT), refreshed every FARM_EVERY s (and at once on a sowing and at
// S2's start / end); both × S2's extra_extend_scale while S2 runs (shu_s_2 sets DoFarm's extra_extend_scale). Two 黍 of a
// shared field: the stronger regeneration and 庇护 hold. GUA-X stage 3: 80 / s, 15 % and "部署时立即给所处地块播种该效果"
// (bbStr born_range_id 0-1: her own tile at every deployment).
// - T2 天有四时: while she is deployed, ≥ PROF_NEEDED different professions among the allied operators on the field ⇒ every
// allied operator's max HP +max_hp; ≥ PROF_NEEDED operators of one profession ⇒ ASPD +attack_speed (charpack auras:
// operators only, 孤立 included); if her squad (her player's operators, deployed or not) holds ≥ SUI_NEEDED 【岁】 operators
// (character_table groupId 'sui': 年 夕 令 重岳 黍 余 望) ⇒ every operator of the squad ATK +atk for the battle and +sp SP
// every `interval` s of each deployment (modify_sp[trigger]; an SP gift — a 决战者's 阻回 refuses it), whether she is
// deployed or not (Terra). The counts 3 / 4 exist in the text and the charpack only (minCount / minCntToTrig).
// - S1 化被草木 (AUTO, time SP, 可充能 1 / 2 次; data DEFAULT): the client prefab is 塞雷娅 S1 急救's (a SkillTrigger on an
// ally of HP ≤ half — _maxHpRatio 0.5 — _cancelIfSearchTargetFailed), whose PRTS 备注 reads "此技能仅在周围有符合血量条件的友方
// 单位时可触发,触发时会替换当次攻击": the engine's DEFAULT with `allies` / `hpAtMost` on the skill range x-4 — at her attack,
// with an ally of x-4 at ≤ half HP, that attack heals the lowest such ally for heal_scale × ATK instead (as 塞雷娅's kit).
// - S2 嘉禾盈仓 (MANUAL; data DEFAULT — the owner's 重装 exception of 2026-10-05, rawRule TAKE_DAMAGE): `duration` s, her
// attacks become heals of up to attack@max_target injured allied units on the skill range x-1 (her range meanwhile:
// _rangeIdModeIndex 1) for ATK, ATK +atk, block +block_cnt, attack interval +base_attack_time s (a flat +1.3 on her 1.2
// s; ASPD and 缴械 still count), T1 ×extra_extend_scale.
// - S3 离离枯荣 (MANUAL): her range becomes the skill range x-2 (_rangeIdModeIndex 2; "视野扩大至治疗范围"), ATK +atk; every
// attack on an enemy also heals the most injured allied unit of that range other than herself for ATK
// (shu_s_3[heal_in_attack]: the Heal selector excluding her); with no enemy to attack her attack heals the most injured
// allied unit of the range, herself included (the mode's Attack-else-Heal ability). While a ground enemy stands on a
// sown tile every allied operator of her range — 孤立 ones too — ATK +e_atk and ASPD +e_attack_speed (× min(enemies,
// e_max_stack_cnt)); a ground enemy that enters a sown tile is marked with that tile (once) and each time it then enters
// a tile at a Manhattan distance ≥ max_distance from it, it is put back on that tile's centre (unblocked, its route
// re-planned from there; waypoints already passed stay passed), stealth or 无敌 notwithstanding; a 消失 (hidden) enemy loses
// its mark and every mark ends with the skill (the client attaches it to shu_s_3). No enemy of the mode holds
// 免疫传送 (data otherImmunities). Trigger: the data rule is TRY_SEARCH_ALLY_SKILL (her own row, PRTS "技能范围内存在可治疗的
// 我方单位时释放技能"), which the engine does not know (an unknown rule acts as DEFAULT): this kit maps it onto the engine's
// SKILL_RANGE + `allies` on x-2 (an injured, healable allied unit of the skill range, herself included; 古米 S1's option).
import { num, traitBb, skillRec, batMod, up, giveSp } from '../shared/tier1.js';
import { COLS } from '../../../constants.js';
import { effectiveProfile, acquireTargets, performAttack } from '../../../ai.js';
const S1 = 'skchr_shu_1';
const S2 = 'skchr_shu_2';
const S3 = 'skchr_shu_3';
/** The skill ranges when the data carries none: x-4 (S1, 周围), x-1 (S2), x-2 (S3) — range_table. */
const X4 = Object.freeze([[1, -1], [1, 0], [1, 1], [0, -1], [0, 0], [0, 1], [-1, -1], [-1, 0], [-1, 1]]);
const X1 = Object.freeze([[2, 0], [1, -1], [1, 0], [1, 1], [0, -2], [0, -1], [0, 0], [0, 1], [0, 2], [-1, -1], [-1, 0], [-1, 1], [-2, 0]]);
const X2 = Object.freeze([[2, -1], [2, 0], [2, 1], [1, -2], [1, -1], [1, 0], [1, 1], [1, 2], [0, -2], [0, -1], [0, 0], [0, 1], [0, 2],
[-1, -2], [-1, -1], [-1, 0], [-1, 1], [-1, 2], [-2, -1], [-2, 0], [-2, 1]]);
/** shu_t_1 sows the healed unit's tile and the four next to it (range x-5, symmetric). */
const SOW = Object.freeze([[0, 0], [1, 0], [-1, 0], [0, 1], [0, -1]]);
/** Range ids a talent's bbStr may name (GUA-X stage 3 born_range_id '0-1': her own tile). */
const RANGE_IDS = Object.freeze({ '0-1': Object.freeze([[0, 0]]) });
/** S1 "血量不足一半" — PRTS 修正 小于等于, the prefab's _maxHpRatio 0.5. */
const HALF_HP = 0.5;
/** T1's tile effect refresh and hold. */
const FARM_EVERY = 0.25;
const FARM_HOLD = 0.35;
/** T2 "三名不同 / 相同职业干员", "四名【岁】干员" (the charpack's minCount / minCntToTrig; no blackboard key). */
const PROF_NEEDED = 3;
const SUI_NEEDED = 4;
/** 【岁】: character_table groupId 'sui' (data/backups.json diy.operators powers carry it for the 自选 picks). */
const SUI = new Set(['char_2014_nian', 'char_2015_dusk', 'char_2023_ling', 'char_2024_chyue', 'char_2025_shu', 'char_2026_yu', 'char_2027_wang']);
/** 庇护 (ba.protect): one "同名效果取最高" effect per unit, whoever grants it (battle.applyStrongest). */
const PROTECT = 'protect';
const protectMods = (v) => ({ physTakenMul: 1 - v, artsTakenMul: 1 - v });
const FARM = 'talent:shu:farm';
const HARVEST = 'skill:shu:harvest';
const bbOf = (chess, id) => skillRec(chess, id)?.bb ?? {};
const gridOf = (chess, id, fallback) => { const g = skillRec(chess, id)?.rangeGrid; return g?.length ? g : fallback; };
/** The talent record with data index `i` (null when absent). */
const talentRec = (chess, i) => (chess?.talents ?? []).find((t) => t && t.index === i) ?? null;
/** The tile key of a unit's position (an enemy: its 判定中心). */
const tileKey = (u) => Math.round(u.y) * COLS + Math.round(u.x);
/** An allied unit her heals and tiles may take: on the field, not a device. */
const onField = (a) => !!a && a.alive && a.deployed && !a.hidden && a.kind !== 'device';
export default {
char_2025_shu: (bb, chess) => {
const tb = traitBb(chess);
const r0 = talentRec(chess, 0), t0 = r0?.bb ?? {}, t1 = talentRec(chess, 1)?.bb ?? {};
const born = RANGE_IDS[r0?.bbStr?.born_range_id] ?? null;
const b1 = bbOf(chess, S1), b2 = bbOf(chess, S2), b3 = bbOf(chess, S3);
const s1 = skillRec(chess, S1);
const g1 = gridOf(chess, S1, X4), g2 = gridOf(chess, S2, X1), g3 = gridOf(chess, S3, X2);
/** The sown tiles of her current deployment (a Set of tile keys). */
const sown = (unit) => {
if (unit.mem.shuSownSeq !== unit.deploySeq || !unit.mem.shuSown) { unit.mem.shuSown = new Set(); unit.mem.shuSownSeq = unit.deploySeq; }
return unit.mem.shuSown;
};
/** T1's tile effect on every allied unit standing on a sown tile (× S2's extra_extend_scale while it runs). */
const farm = (battle, unit) => {
if (!up(unit)) return;
const tiles = sown(unit);
if (!tiles.size) return;
const scale = unit.skill?.active && unit.skill.id === S2 ? num(b2.extra_extend_scale, 1) : 1;
const hp = num(t0.hp_recovery_per_sec) * scale, dr = num(t0.damage_resistance) * scale;
for (const a of battle.allyUnits) {
if (!onField(a) || !tiles.has(a.tileR * COLS + a.tileC) || !battle.allySelectable(a, unit)) continue;
const cur = a.findBuff(FARM);
if (hp > 0 && !(cur && cur.source !== unit && (cur.data?.v ?? 0) > hp && cur.timeLeft > 0.05)) {
battle.addBuff(a, { key: FARM, duration: FARM_HOLD, mods: { hpRegen: hp }, source: unit, data: { v: hp }, tags: ['talent'] });
}
if (dr > 0) battle.applyStrongest(a, PROTECT, { duration: FARM_HOLD, value: dr, mods: protectMods, source: unit });
}
};
/** Sow the tiles of `grid` around (r, c) — only tiles that can be deployed on or walked. Returns the new ones. */
const sow = (battle, unit, r, c, grid) => {
const tiles = sown(unit);
let n = 0;
for (const [dr, dc] of grid) {
const rr = r + dr, cc = c + dc;
if (!battle.grid.inRect(rr, cc)) continue;
const t = battle.grid.tile(rr, cc);
if (!t || (t.build === 'NONE' && t.pass !== 'ALL')) continue;
const k = rr * COLS + cc;
if (!tiles.has(k)) { tiles.add(k); n++; }
}
if (n > 0) { battle.fx('healField', { x: c, y: r, id: unit.id }); farm(battle, unit); }
return n;
};
/** S3: the most injured healable allied unit of her current range (`others`: not herself). */
const healTarget = (battle, unit, others) => {
const list = battle.injuredAlliesInKeys(unit.rangeKeySet || new Set(unit.rangeKeys || []), unit);
return others ? list.find((a) => a !== unit) ?? null : list[0] ?? null;
};
return {
skills: {
[S1]: {
kind: num(s1?.maxChargeTime, 1) > 1 ? 'charges' : 'instant',
trigger: { rule: 'DEFAULT', grid: g1, allies: true, hpAtMost: HALF_HP },
targeting: { rangeGrid: g1 },
attack: { dmgType: 'heal', heal: { mode: 'single', hpAtMost: HALF_HP }, healScale: num(b1.heal_scale, 1), projectile: 'none' },
},
[S2]: {
kind: 'duration',
mods: { atkPct: num(b2.atk), blockCnt: num(b2.block_cnt), batPct: batMod(b2.base_attack_time, chess) },
targeting: { rangeGrid: g2, maxTargets: Math.max(1, Math.floor(num(b2['attack@max_target'], 2))) },
attack: { dmgType: 'heal', heal: { mode: 'single' }, healScale: 1, projectile: 'none' },
onStart({ battle, unit }) { farm(battle, unit); },
onEnd({ battle, unit }) { farm(battle, unit); },
},
[S3]: {
kind: 'duration',
trigger: { rule: 'SKILL_RANGE', grid: g3, allies: true },
mods: { atkPct: num(b3.atk) },
targeting: { rangeGrid: g3 },
onStart({ unit }) { unit.mem.shuMarks = new Map(); },
onAttack({ battle, unit, targets }) {
// shu_s_3[heal_in_attack]: an attack on an enemy also heals the most injured allied unit of her range but her
if (!targets?.length || targets[0].side !== 'enemy') return;
const a = healTarget(battle, unit, true);
if (a) battle.heal(unit, a, unit.s.atk * unit.s.atkScaleMul);
},
onTick({ battle, unit }) {
if (!up(unit)) return;
const tiles = sown(unit);
// 有地面敌人处于播种地块时: the allied operators of her range ATK / ASPD + (孤立 ones too)
let onSown = 0;
for (const e of battle.enemies) if (e.alive && !e.hidden && !e.isFlying && tiles.has(tileKey(e))) onSown++;
const stacks = Math.min(onSown, Math.max(1, Math.floor(num(b3.e_max_stack_cnt, 1))));
if (stacks > 0) {
const keys = unit.rangeKeySet || new Set(unit.rangeKeys || []);
for (const a of battle.allyUnits) {
if (a.kind !== 'op' || !onField(a) || !keys.has(a.tileR * COLS + a.tileC)) continue;
battle.addBuff(a, { key: HARVEST, duration: 0.1, mods: { atkPct: num(b3.e_atk) * stacks, aspd: num(b3.e_attack_speed) * stacks }, source: unit, tags: ['skill'] });
}
}
// 【播种标记】: a ground enemy entering a sown tile records it; ≥ max_distance (Manhattan) away it is put back there
const marks = unit.mem.shuMarks ?? (unit.mem.shuMarks = new Map());
const far = Math.max(1, num(b3.max_distance, 4));
for (const e of battle.enemies) {
const m = marks.get(e.id);
if (!e.alive || e.hidden) { if (m) marks.delete(e.id); continue; }
const k = tileKey(e);
if (!m) {
if (!e.isFlying && tiles.has(k)) marks.set(e.id, { r: Math.round(e.y), c: Math.round(e.x), last: k });
continue;
}
if (k === m.last) continue;
m.last = k;
const dist = Math.abs(Math.floor(k / COLS) - m.r) + Math.abs((k % COLS) - m.c);
if (dist < far - 1e-9) continue;
e.x = m.c;
e.y = m.r;
battle._unblock(e);
if (e.route) e.route.pts = null;
m.last = m.r * COLS + m.c;
battle.fx('teleport', { x: m.c, y: m.r, id: e.id, src: unit.id });
}
// no enemy to attack: her attack heals the most injured allied unit of her range (herself included)
if (unit.atkCd > 0 || !unit.canAct || unit.s.flags.disarm || !unit.profile) return;
const prof = effectiveProfile(unit);
if (acquireTargets(battle, unit, prof).length) return;
const a = healTarget(battle, unit, false);
if (!a) return;
performAttack(battle, unit, { ...prof, dmgType: 'heal', heal: { mode: 'single' }, atkScale: 1, healScale: 1, projectile: 'none', splashRadius: 0, chain: null, hits: 1 }, [a]);
unit.atkCd = Math.max(unit.atkCd, unit.s.interval);
},
onEnd({ unit }) { unit.mem.shuMarks = null; },
},
},
talents: [
{ install(battle, unit) { // 百谷长青: every heal she outputs sows the healed unit's x-5; the sown tiles regenerate / 庇护
battle.on('heal', (c) => {
if (c.source !== unit || !up(unit) || c.opts?.regen || !(c.amount > 0) || !onField(c.target)) return;
sow(battle, unit, c.target.tileR, c.target.tileC, SOW);
}, { owner: unit, priority: -20 });
battle.on('deploy', (ctx) => {
if (ctx.unit !== unit) return;
sown(unit).clear();
if (born) sow(battle, unit, unit.tileR, unit.tileC, born); // GUA-X stage 3: 部署时立即给所处地块播种该效果
}, { owner: unit });
battle.on('death', (ctx) => { // 自身退场时清空所有地块的播种效果
if (ctx.unit !== unit) return;
sown(unit).clear();
for (const a of battle.allyUnits) if (a.findBuff(FARM)?.source === unit) battle.removeBuff(a, FARM);
}, { owner: unit });
battle.every(FARM_EVERY, () => farm(battle, unit), { owner: unit, immediate: true });
} },
{ install(battle, unit) { // 天有四时
const hpUp = num(t1.max_hp), asUp = num(t1.attack_speed);
battle.every(FARM_EVERY, () => {
if (!up(unit)) return;
const ops = battle.allyUnits.filter((a) => a.kind === 'op' && onField(a));
const per = new Map();
for (const a of ops) { const p = a.def?.profession ?? '?'; per.set(p, (per.get(p) ?? 0) + 1); }
const many = per.size >= PROF_NEEDED, same = Math.max(0, ...per.values()) >= PROF_NEEDED;
for (const a of ops) {
if (many && hpUp) battle.addBuff(a, { key: 'talent:shu:seasons:hp', duration: FARM_HOLD, mods: { hpPct: hpUp }, source: unit, tags: ['talent'] });
if (same && asUp) battle.addBuff(a, { key: 'talent:shu:seasons:aspd', duration: FARM_HOLD, mods: { aspd: asUp }, source: unit, tags: ['talent'] });
}
}, { owner: unit, immediate: true });
// 编入队伍且编队中有四名【岁】干员: her squad's operators ATK + for the battle, +sp SP every `interval` s deployed
const squadOf = () => battle.allyUnits.filter((a) => a.kind === 'op' && a.ownerId === unit.ownerId);
const atk = num(t1.atk), sp = num(t1.sp), iv = num(t1.interval);
const spBuff = (a) => {
if (sp > 0 && iv > 0) battle.addBuff(a, { key: 'talent:shu:suiSp', interval: iv, onTick: ({ unit: x }) => giveSp(x, sp), source: unit, tags: ['talent'] });
};
const apply = () => {
if (unit.mem.shuSuiDone) return;
unit.mem.shuSuiDone = true;
const squad = squadOf();
if (squad.filter((a) => SUI.has(a.def?.charId)).length < SUI_NEEDED) return;
const members = new Set(squad);
for (const a of squad) {
if (atk) battle.addBuff(a, { key: 'talent:shu:sui', mods: { atkPct: atk }, persist: true, allowDead: true, source: unit, tags: ['talent'] });
if (onField(a)) spBuff(a);
}
battle.on('deploy', (ctx) => { if (members.has(ctx.unit)) spBuff(ctx.unit); }, { owner: unit });
};
if (battle.started) apply();
else battle.on('battleStart', apply, { owner: unit });
} },
],
install(battle, unit) {
// GUA-X: 治疗生命值低于50%的友方单位时治疗量提升15% (heal_scale_up[hpratio][LE]: at or below hp_ratio)
const hs = num(tb.heal_scale, 1), hr = num(tb.hp_ratio);
if (hs !== 1 && hr > 0) {
battle.on('heal', (c) => {
if (c.source === unit && !c.opts?.regen && c.target && c.target.hpRatio <= hr + 1e-9) c.amount *= hs;
}, { owner: unit });
}
},
};
},
};
+125
View File
@@ -0,0 +1,125 @@
// server/sim/content/kits/ops/op-zumama.js — 森蚺 (char_416_zumama) 自选 operator kit: 6★ 决战者 (重装), an owned-6★ pick of
// the tier-5 and tier-6 自选 slots; every skill, both talents, the trait and all three modules at every form.
// Kit contract and the 自选 rules: ../README.md ("How to add an operator (自选)").
//
// Forms (data/backups.json units.char_416_zumama, the DIY slot statuses): normal = E2 Lv1, skills at rank 4, no module;
// elite = E2 Lv60, rank 7, the picked module at stage 1 (tier 5) or 3 (tier 6). Potential 0 [ASSUMED: no account].
// Sources: character_table / skill_table / battle_equip_table (zh_CN, as built into backups.json); PRTS 森蚺 (勇冠三军 备注
// "天赋具有0.1秒的检测周期;高于一半的效果实为在计算伤害时提升本次伤害的攻击力倍率"; S2 备注 "造成的晕眩效果无视目标可选性,持续
// 时间不会因抵抗等效果被缩短或延长"; S3 备注 "生命恢复的提供方式为增加“生命回复速度(百分比)”属性…不受治疗加成和禁疗影响");
// PRTS 分支特性信息 §决战者 ("常态持有阻回(停止任意形式的技力回复,技力也无法自然回复);阻挡敌人期间暂时免疫此效果"; "此分支的部分
// 干员拥有可以移除此阻回效果的模组,但会增加“技力回复速度倍率降低”的限制"); the client's battle data: her charpack (trait abilities
// SpRecoveryStopped / SpRecovery, the S2 mode's stun_ability), buff templates zumama_t_1 / zumama_t_1[atk] / zumama_s_3 /
// zumama_s_3[buff], the module prefabs zumama_equip_{1,2,3}_* and battle_equip_table `validInGameTag`; Arknights Terra
// Wiki "Eunectes" (S2: "unaffected by … Status Resistance and remains even if the target becomes invulnerable or
// untargetable").
// - Trait (决战者) "只有阻挡敌人时才能够回复技力": the charpack's trait — 阻回 (sp_recovery_stopped, overridden while she
// blocks at least one enemy): flag `noSp` while she blocks nobody — no SP of any kind then (time, gifts; skills.js), her
// initSp included at deployment ('init' passes it). The modules replace that 阻回 by a 技力回复速度倍率 (trait bb
// sp_recover_ratio, the equip prefab's attribute 30 on 'sp_e_recovery_stopped', overridden while blocking): HES-X
// “祖玛玛的工具箱” "缓慢回复技力,只有阻挡敌人时恢复技力回复速度" ×(1 − 0.8), HES-Y “小小的丑东西” "不阻挡敌人时无法自我回复技力"
// ×(1 − 0.999) — SP recovery ×(1 + ratio) while she blocks nobody (spRecoveryMul; SP gifts still land) —, RA-A
// 森蚺特限证章 ×(1 − 1) (its TRAIT part carries no validInGameTag: it holds in every mode). HES-Y also "阻挡敌人时…攻击力
// +15%,防御力+15%" (trait bb atk / def) while she blocks.
// - Module RA-A: every other part is 生息演算-only (battle_equip_table validInGameTag 'sandbox': the hidden talent's
// attraction / block / 物理脆弱 and 愈战愈勇's +0.4/s; trait text "在生息演算中…") — N/A here; its HP / ATK are in the stats. The
// composed record merges the sandbox 愈战愈勇 part (0.4) into `talents`, so this kit reads 愈战愈勇 from `talentsBase` under
// RA-A (+0.2/s, the module's own text "阻挡敌人时技力回复速度+0.2/秒;在生息演算中…").
// - T1 勇冠三军 (zumama_t_1, polled every T1_PERIOD s): HP ratio above hp_ratio ⇒ every damage she deals ×atk_scale as an
// 攻击力倍率 (atkScaleMul: applied to the ATK before DEF — PRTS 备注); at or below it ⇒ damage_resistance 庇护 (ba.protect
// "受到的物理和法术伤害降低相应比例(同名效果取最高)": the shared applyStrongest key PROTECT). HES-Y stage 3: 123 % / 28 % (the module
// talent change).
// - T2 愈战愈勇 "阻挡敌人时技力回复速度+0.2/秒": spRecoveryFlat while she blocks. HES-X stage 3: +0.55/s.
// - S1 轻型挂斧 (PASSIVE): ATK / DEF +atk / +def from every deployment.
// - S2 震慑劈砍 (MANUAL; data DEFAULT — the owner's 重装 exception of 2026-10-05, rawRule TAKE_DAMAGE): `duration` s, ATK
// +atk, attack interval +base_attack_time s (a flat +0.4 on her 1.6 s), and every enemy she blocks is stunned while it is
// blocked and the skill runs (the charpack's _buffsToBlockee 'stun'): a stun pulse of STUN_PULSE s renewed every tick
// past 抵抗 (`resistApplied`: "持续时间不会因抵抗等效果被缩短或延长") and refused only by a 晕眩 immunity — so it ends at most
// STUN_PULSE s after the block or the skill. [ASSUMED] a blockee that turns 无敌 and 无法选中 at once loses it after one pulse
// (the engine refuses statuses on such a unit; Terra: the official one stays).
// - S3 钢铁意志 (MANUAL; data DEFAULT, rawRule TAKE_DAMAGE): `duration` s, ATK / DEF +atk / +def, block +block_cnt,
// 生命回复速度 +hp_recovery_per_sec_by_max_hp_ratio × max HP (hpRegenRatio — checklist 11: no heal, 禁疗-proof); when it ends
// she is stunned `stun` s (zumama_s_3 ON_BUFF_FINISH 'stun'; a stunned operator blocks nobody).
// - Melee physical, ground-only (data canHitFly false), block 1, range 1-1; ground enemies target her (no 起飞 / 迷彩).
import { num, traitBb, skillRec, toggleBuff, batMod, up } from '../shared/tier1.js';
const S1 = 'skchr_zumama_1';
const S2 = 'skchr_zumama_2';
const S3 = 'skchr_zumama_3';
/** PRTS 勇冠三军 备注 "天赋具有0.1秒的检测周期". */
const T1_PERIOD = 0.1;
/** 庇护 refresh: a little longer than the check period, so it never lapses while it holds. */
const PROTECT_HOLD = T1_PERIOD + 0.05;
/** 庇护 (ba.protect): one "同名效果取最高" effect per unit, whoever grants it (battle.applyStrongest). */
const PROTECT = 'protect';
const protectMods = (v) => ({ physTakenMul: 1 - v, artsTakenMul: 1 - v });
/** S2's blockee stun, renewed every tick while it holds (three ticks long). */
const STUN_PULSE = 0.1;
const VALOR = 'talent:zumama:valor';
const bbOf = (chess, id) => skillRec(chess, id)?.bb ?? {};
/** The record's active module, or null. */
const activeModule = (chess) => (chess?.module?.active ? (chess.modules ?? []).find((m) => m && m.uniEquipId === chess.module.id) ?? null : null);
/** A module whose extra parts work 在生息演算中 only (RA-A): its talent changes do not apply here. */
const sandboxOnly = (mod) => /生息演算/.test(String(mod?.traitOverride?.moduleDesc ?? ''));
/** Blackboard of the talent with data index `i` in `list` ({} when absent). */
const talentAt = (list, i) => (list ?? []).find((t) => t && t.index === i)?.bb ?? {};
export default {
char_416_zumama: (bb, chess) => {
const mod = activeModule(chess);
const talents = mod && sandboxOnly(mod) ? chess.talentsBase : chess?.talents;
const t0 = talentAt(talents, 0), t1 = talentAt(talents, 1);
const tb = traitBb(chess);
const b1 = bbOf(chess, S1), b2 = bbOf(chess, S2), b3 = bbOf(chess, S3);
return {
skills: {
[S1]: { kind: 'passive', mods: { atkPct: num(b1.atk), defPct: num(b1.def) } },
[S2]: {
kind: 'duration',
mods: { atkPct: num(b2.atk), batPct: batMod(b2.base_attack_time, chess) },
onTick({ battle, unit }) {
for (const e of unit.blocking) {
if (e.alive && e.blockedBy === unit) battle.applyStatus(e, 'stun', { duration: STUN_PULSE, source: unit, resistApplied: true });
}
},
},
[S3]: {
kind: 'duration',
mods: { atkPct: num(b3.atk), defPct: num(b3.def), blockCnt: num(b3.block_cnt), hpRegenRatio: num(b3.hp_recovery_per_sec_by_max_hp_ratio) },
onEnd({ battle, unit }) {
if (up(unit) && num(b3.stun) > 0) battle.applyStatus(unit, 'stun', { duration: num(b3.stun), source: unit });
},
},
},
talents: [
{ install(battle, unit) { // 勇冠三军: above hp_ratio ×atk_scale (攻击力倍率), at or below it 庇护 — checked every T1_PERIOD s
const ratio = num(t0.hp_ratio, 0.5), scale = num(t0.atk_scale, 1), dr = num(t0.damage_resistance);
const check = () => {
const high = up(unit) && unit.hpRatio > ratio + 1e-9;
const has = unit.findBuff(VALOR);
if (high && !has && scale !== 1) battle.addBuff(unit, { key: VALOR, mods: { atkScaleMul: scale }, tags: ['talent'] });
else if (!high && has) battle.removeBuff(unit, VALOR);
if (up(unit) && !high && dr > 0) battle.applyStrongest(unit, PROTECT, { duration: PROTECT_HOLD, value: dr, mods: protectMods, source: unit });
};
battle.every(T1_PERIOD, check, { owner: unit, immediate: true });
battle.on('deploy', (ctx) => { if (ctx.unit === unit) check(); }, { owner: unit });
} },
{ install(battle, unit) { // 愈战愈勇: SP recovery +sp_recovery_per_sec while she blocks
const sp = num(t1.sp_recovery_per_sec);
if (sp > 0) toggleBuff(battle, unit, 'talent:zumama:fight', () => unit.blocking.length > 0, { spRecoveryFlat: sp });
} },
],
install(battle, unit) {
// 决战者: 阻回 while she blocks nobody; a module turns it into ×(1 + sp_recover_ratio) SP recovery instead
const notBlocking = () => unit.blocking.length === 0;
if (tb.sp_recover_ratio == null) toggleBuff(battle, unit, 'trait:zumama:duel', notBlocking, null, { flags: { noSp: true } });
else toggleBuff(battle, unit, 'trait:zumama:duel', notBlocking, { spRecoveryMul: Math.max(0, 1 + num(tb.sp_recover_ratio)) });
// HES-Y: 阻挡敌人时攻击力+15%,防御力+15%
const ma = num(tb.atk), md = num(tb.def);
if (ma || md) toggleBuff(battle, unit, 'trait:zumama:block', () => unit.blocking.length > 0, { atkPct: ma, defPct: md });
},
};
},
};