feedback5 O18: 自选 kits for 阿, 傀影 (with his 镜中虚影) and 弑君者

Three owned-6★ 自选 picks get operator kits (kits/ops/op-haak.js, op-phatom.js, op-crosly.js; appended to
OPERATOR_KIT_FILES): every skill, both talents, the trait and every module at stage 1 / 3 in every DIY form, from the
data, PRTS (备注) and the client's battle logic (charpacks, skill / equip prefabs, buff templates).

- 阿: 怪杰 drain (profession default), 混合药物射击 (one of four per attack, GEE-X stage 2+ "all four" roll), 药剂扩散
  (healing received, GEE-Y stage 3 deploy heal on the ally ahead), S1 ASPD, S2 / S3 爆发剂 (15 hits of 500 fixed ATK on
  the ally of his range ahead, then the buff on both), GEE-X SP recovery while HP >= 80 %.
- 傀影: S1 dodge + physical-only barrier, S2 stacked ATK used per damage instance, S3 burst (x-4, ground, radial 小力
  push, one random status per cast); the twin's kit (its skill copy, killed when he leaves, back on its tile once its
  own redeploy time has run from its last deployment and he stands, 5 DP), 虚影精通, EXE-X (refund N/A), EXE-Y
  (lonely ATK, stage 3 ATK on both), ISW-A attributes only.
- 弑君者: the smoke's 命中率 cut (x-4, S3 x-1, S2 x2; battle-wide hit handler), 弑君者威名 (fresh ground enemies x1.2,
  EXE-Y stage 3 x1.35 and stun x1.5), S1 ATK / dodge, S2 no attacks + taunt -1 + end blast, S3 隐匿 + block 0 +
  2 s strikes with 6 s marks.

Tests: test/content/op_haak.test.js (10), op_phatom.test.js (8), op_crosly.test.js (8).
Golden: only diy moved — diy-094..diy-100 field the 78 new configurations (阿 24, 傀影 30, 弑君者 24; no summon is
placed by the family), diy-101..diy-104 are the 预备干员 / prototype battles re-packed after them (new index, stage
and seed). roster, bonds, fields, matches and standins are unchanged.
This commit is contained in:
sganggs committed 2026-10-06 03:04:57 +08:00
1 parent 78251f499c
commit b55d4f9d04
8 files changed
+1909 -99

No files matched your search

+3
View File
@@ -127,6 +127,9 @@ export const OPERATOR_KIT_FILES = Object.freeze([
'op-jesca2.js',
'op-mcnist.js',
'op-kalts.js',
'op-haak.js',
'op-phatom.js',
'op-crosly.js',
]);
/**
+230
View File
@@ -0,0 +1,230 @@
// server/sim/content/kits/ops/op-crosly.js — 弑君者 (char_1502_crosly) 自选 operator kit: 6★ 处决者 (特种), an owned-6★ pick
// of the tier-5 and tier-6 自选 slots; every skill, both talents, the trait and both modules (EXE-X 弑君刃, EXE-Y 木炭画) at
// every form. Kit contract and the 自选 rules: ../README.md ("How to add an operator (自选)").
//
// Forms (data/backups.json units.char_1502_crosly, 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) — the owner's decision of 2026-10-05.
// Potential 0 [ASSUMED: no account]. Sources: character_table / skill_table / battle_equip_table (zh_CN, as built into
// backups.json); PRTS 弑君者 (吞咽苦厄 备注 "天赋生效范围为x-4"; 弑君者威名 备注 "<Y模组>任何来源为弑君者的晕眩…均可触发模组的额外
// 效果" / "<Y模组>额外效果在弑君者付与晕眩效果时检查目标是否伤害过自身,若没有则令该次晕眩Buff的持续时间变为1.25倍/1.5倍"; 硝烟震爆
// 备注 "技能持续期间获得嘲讽等级-1"; 烽烟行刑场 备注 "部署时阻挡数立刻归零;不会改变攻击范围/视野范围" / "“6秒内只会触发一次”的
// 实际效果为受到弑君者伤害的单位获得持续6秒的“已攻击”标记Buff,可因任何来源于弑君者的伤害触发;技能期间弑君者始终不攻击具有“已攻击”
// 标记的单位" / "此技能包含隐匿的Buff被隐匿反制解除时,失去“阻挡数变为0”的效果"); PRTS 命中率; the client's battle data —
// charpack char_1502_crosly (modes Default / S1 / S2 (no attack, Talents/t1 `_applyTalentScale`) / S3 (attack: cooldown
// `s3_cd`, `atk_scale_s3`, `_additionalTimes` 1, active buff stun, selector: ground enemies without her crosly_s_3[mark];
// `_rangeToShow` 1-1) / S2_End (attack `atk_scale_s2` on every ground enemy of the x-4, `_allowNoTarget`); Talents/t1 per
// mode: the hit-rate aura over ground enemies (attributes 33 / 34, range x-4 — S3: the skill's range); Talents/t2:
// crosly_t_2), the skill prefabs skchr_crosly_1 / _2 / _3 (S1: crosly_s_1[mode] ATK + crosly_s_1[evade] (evade: physical
// and arts) for `duration`; S2: crosly_s_2[start] (taunt level) for `duration`, then mode S2_End; S3: crosly_s_3[mode]
// for `duration` + crosly_s_3[invisible] (abnormal flag 隐匿, block count ×0)), the equips crosly_equip_2_* (the EXE-Y
// trait: ATK while no allied unit of profession mask 639 — the eight operator professions — stands at Manhattan distance 1)
// / crosly_equip_2_2/3_p2 (crosly_e_003_t2[status-resist] on every enemy without her crosly_t_2[mark], every 0.1 s);
// buff_template_data (crosly_t_2: an enemy whose non-continuous, uncancelled damage she takes gets crosly_t_2[mark] — a
// derived buff, gone with her talent buff when she leaves the field; DamageScale `damage_scale` on PHYSICAL damage she
// deals to a target without it; crosly_s_2[end] (finished at once when she is STUNNED / FROZEN / DISARMED — no blast);
// crosly_s_3[mode] (her damage marks the target crosly_s_3[mark] for mark_duration, derived: gone when the skill ends);
// crosly_e_003_t2[status-resist] (a stun of hers on such an enemy: duration × |1 + one_minus_status_resistance|)).
// - Trait (处决者) "再部署时间大幅度减少": the data's respawnTime (22 s); melee physical, 1-1, blocks 1, ground-only (data
// canHitFly false), attacks the enemies it blocks first (the engine's blocked-first rule); ground enemies target her.
// - Module EXE-X: "撤退时返还大量该次部署费用" — no manual retreat in battle in this mode ⇒ no effect (as 砾's EXE-X); stats;
// stage 2+ changes T1 (−23 % / −25 %). EXE-Y: "周围四格没有友方干员时攻击力+10%" (trait bb atk): ATK +atk while no allied
// operator — summons and devices do not count — stands on the 4 tiles beside her (op-phatom.js noOperatorBeside); stats;
// stage 2+ changes T2 (below).
// - T1 吞咽苦厄 "技能持续期间在自身周围产生烟雾,使其中的地面敌人物理与法术命中率-20%": while any of her skills runs, the smoke covers
// her x-4 (PRTS 备注; S3: the skill's x-1, "第一天赋的生效范围扩大"); each physical / arts attack of a selectable ground
// enemy whose body is in it misses with |damage_hitrate_physical| / |damage_hitrate_magical| (×talent_scale while S2
// runs): PRTS 命中率 — one roll per attack (its first damage instance) that cancels every hit of it, a battle-wide `hit`
// handler as Raidian's / 阿斯卡纶's (the strongest smoke holding the attacker; other kinds of 命中率 cut roll apart
// [ASSUMED, as theirs]).
// - T2 弑君者威名 "对未伤害过自身的地面敌人造成的物理伤害提升20%": physical damage she deals to a ground enemy that has not damaged
// her during this deployment ×damage_scale (a final multiplier, dmg.mul); an enemy is marked by any damage of it she
// takes — not a 持续伤害 tick, not a 流失, not an element 损伤, not a dodged hit — and the marks go when she leaves the field.
// EXE-Y stage 2+ (crosly_e_003_t2[status-resist]): "且造成的晕眩效果影响时间+50%" — a stun of hers (any source, PRTS 备注)
// on an enemy without that mark lasts ×(1 + one_minus_status_resistance) (the hidden module talent; 1.5 at stage 3).
// - S1 尘烟蔽目 (被动, at each deployment, the skill's duration 10 s): ATK +atk, physical and arts dodge `prob`.
// - S2 硝烟震爆 (被动, at each deployment, bb duration 8 s): no attacks, taunt level +taunt_level (−1), T1 ×talent_scale; at its
// end — unless she cannot act (晕眩 / 冻结 / …) or is 缴械 then (crosly_s_2[end]) — every selectable ground enemy of the
// x-4 smoke takes attack@atk_scale_s2 × ATK physical skill damage. [ASSUMED] the blast lands at once (the end clip's
// wind-up is not modelled).
// - S3 烽烟行刑场 (被动, at each deployment, the skill's duration 16 s): 隐匿 and block count 0 (one buff: no enemy counters an
// ally's 隐匿 in this mode); her normal attacks stop; her attack is a strike — ready at the start, then every
// attack@s3_cd (2) s [ASSUMED: not shortened by attack speed] — on one selectable ground enemy of the x-1 smoke without
// her mark (the engine's default target order): attack@times (2) hits of attack@atk_scale_s3 × ATK physical (a skill
// attack: Battle.forceAttack) and 晕眩 attack@stun s; every damage she deals meanwhile marks its target for mark_duration
// (6) s, the marks gone when the skill ends. Her attack range stays 1-1 (PRTS "不会改变攻击范围").
// Triggers: the three skills are 被动 ON_DEPLOY — timed deployment skills (activateOnDeploy, trigger NEVER).
import { num, talentBb, moduleBb, traitBb, skillRec, toggleBuff, up } from '../shared/tier1.js';
import { absoluteRangeKeys, canTargetEnemy, sortEnemyTargets } from '../../../targeting.js';
import { bodyInKeys } from '../../../body.js';
import { noOperatorBeside } from './op-phatom.js';
const S1 = 'skchr_crosly_1';
const S2 = 'skchr_crosly_2';
const S3 = 'skchr_crosly_3';
/** 吞咽苦厄's smoke (PRTS 备注 "天赋生效范围为x-4"): her tile and the eight around it. */
const X4 = Object.freeze([[1, -1], [1, 0], [1, 1], [0, -1], [0, 0], [0, 1], [-1, -1], [-1, 0], [-1, 1]]);
/** 烽烟行刑场's smoke when its record carries no grid: range x-1 (the diamond of radius 2). */
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]]);
/** The smoke's 命中率 handler runs after Raidian's (300) and 阿斯卡纶's (290), before the enemy content's riders (200). */
const MISS_PRIORITY = 280;
const GROUND = Object.freeze({ canHitFly: false, groundOnly: true });
const TIMED = Object.freeze({ kind: 'duration', activateOnDeploy: true, spCost: 0, spType: 'none', trigger: 'NEVER' });
const TAG_BLAST = 'crosly:blast';
const skillOn = (u, id) => !!(u.skill && u.skill.active && (id == null || u.skill.id === id));
// ---- T1 命中率: one battle-wide handler over every 弑君者 of the battle ---------------------------------------------
/** battle → { srcs: [{ unit, phys, arts, scale, s3Grid }], last: WeakMap(enemy → { id, miss }) }. */
const SMOKE = new WeakMap();
/** The smoke's tiles of `u` now (S3: the skill's x-1, else her x-4). */
const smokeKeys = (u, s3Grid) => absoluteRangeKeys(skillOn(u, S3) ? s3Grid : X4, u.tileR, u.tileC, u.dir, 0);
/** The 命中率 cut (0–1) that ground enemy `e`'s attack of damage type `type` suffers: the strongest smoke holding it. */
function smokeCut(st, e, type) {
let cut = 0;
for (const s of st.srcs) {
const u = s.unit;
if (!up(u) || !skillOn(u)) continue;
const v = (type === 'arts' ? s.arts : s.phys) * (skillOn(u, S2) ? s.scale : 1);
if (!(v > cut) || !canTargetEnemy(u, e, GROUND) || !bodyInKeys(e, smokeKeys(u, s.s3Grid))) continue;
cut = v;
}
return Math.min(1, cut);
}
function smokeHit(battle, st, ctx) {
const e = ctx.source, d = ctx.dmg;
if (!e || e.side !== 'enemy' || e.isFlying || !d || !d.isAttack || d.cancel || (d.type !== 'phys' && d.type !== 'arts')) return;
// one roll per attack: every damage instance of it shares the first instance's result
const prev = d.attackId ? st.last.get(e) : null;
let miss;
if (prev && prev.id === d.attackId) miss = prev.miss;
else {
const cut = smokeCut(st, e, d.type);
miss = cut > 0 && battle.rng() < cut;
if (d.attackId) st.last.set(e, { id: d.attackId, miss });
}
if (!miss) return;
d.cancel = true;
ctx.stopPropagation = true;
if (ctx.target) battle.fx('dodge', { x: ctx.target.x, y: ctx.target.y, id: ctx.target.id });
}
function installSmoke(battle, src) {
if (!(src.phys > 0) && !(src.arts > 0)) return;
let st = SMOKE.get(battle);
if (!st) {
st = { srcs: [], last: new WeakMap() };
SMOKE.set(battle, st);
// no owner: it serves every 弑君者 of the battle, whichever is on the field
battle.on('hit', (ctx) => smokeHit(battle, st, ctx), { priority: MISS_PRIORITY });
}
st.srcs.push(src);
}
// ---- S3 strikes -------------------------------------------------------------------------------------------------------
/** The strike's target: a selectable ground enemy of the x-1 smoke without her mark, the engine's default order. */
function strikeTarget(battle, unit, s3Grid) {
const marks = unit.mem.croslyMarks;
const list = battle.enemiesInKeys(smokeKeys(unit, s3Grid), unit, GROUND)
.filter((e) => !(marks && marks.get(e) > battle.time + 1e-9));
if (!list.length) return null;
sortEnemyTargets(battle, unit, list, null);
return list[0];
}
export default {
char_1502_crosly: (bb, chess) => {
const t0 = talentBb(chess, 0); // 吞咽苦厄 (EXE-X stage 2+: −0.25)
const t1 = talentBb(chess, 1); // 弑君者威名 (EXE-Y stage 2+: 1.35)
const osr = num(moduleBb(chess).one_minus_status_resistance); // EXE-Y stage 2+: the stun duration share
const lonelyAtk = num(traitBb(chess).atk); // EXE-Y trait (EXE-X: withdraw_cost_recover_ratio)
const r1 = skillRec(chess, S1), r2 = skillRec(chess, S2), r3 = skillRec(chess, S3);
const b1 = r1?.bb ?? {}, b2 = r2?.bb ?? {}, b3 = r3?.bb ?? {};
const s3Grid = Array.isArray(r3?.rangeGrid) && r3.rangeGrid.length ? r3.rangeGrid.map((p) => [p[0], p[1]]) : X1;
const cd3 = Math.max(0.1, num(b3['attack@s3_cd'], 2));
const markDur = num(b3.mark_duration, 6);
return {
skills: {
[S1]: { ...TIMED, duration: num(r1?.duration, 10), mods: { atkPct: num(b1.atk), dodgePhys: num(b1.prob), dodgeArts: num(b1.prob) } },
[S2]: {
...TIMED, duration: num(b2.duration, 8), mods: { taunt: num(b2.taunt_level) }, attack: { noAttack: true },
onEnd({ battle, unit, reason }) {
// crosly_s_2[end]: STUNNED / FROZEN / DISARMED when it ends ⇒ no blast
if (reason !== 'duration' || !up(unit) || !unit.canAct || unit.s.flags.disarm) return;
const scale = num(b2['attack@atk_scale_s2']);
for (const e of battle.enemiesInKeys(absoluteRangeKeys(X4, unit.tileR, unit.tileC, unit.dir, 0), unit, GROUND)) {
if (e.alive && scale > 0) battle.dealDamage(unit, e, { amount: unit.s.atk * scale, type: 'phys', isSkill: true, tags: ['skill', TAG_BLAST] });
}
battle.fx('aoe', { x: unit.x, y: unit.y, radius: 1, id: unit.id, skill: TAG_BLAST });
},
},
[S3]: {
...TIMED, duration: num(r3?.duration, 16),
flags: { stealth: true }, mods: { blockCnt: -99 },
attack: {
noAttack: true, atkScale: num(b3['attack@atk_scale_s3'], 1), hits: Math.max(1, Math.floor(num(b3['attack@times'], 2))),
onHitStatus: { key: 'stun', duration: num(b3['attack@stun']) },
},
onStart({ battle, unit }) {
unit.mem.croslyMarks = new Map();
unit.mem.croslyNext = battle.time; // the strike is ready at the start
battle.fx('stealth', { x: unit.x, y: unit.y, id: unit.id });
},
onTick({ battle, unit }) {
if (!unit.canAct || unit.s.flags.disarm || battle.time + 1e-9 < num(unit.mem.croslyNext, 0)) return;
const t = strikeTarget(battle, unit, s3Grid);
if (!t) return;
if (battle.forceAttack(unit, [t])) {
unit.mem.croslyNext = battle.time + cd3;
battle.fx('strike', { x: t.x, y: t.y, id: unit.id, target: t.id });
}
},
onEnd({ unit }) { unit.mem.croslyMarks = null; },
},
},
talents: [
{ install(battle, unit) { // 吞咽苦厄: the smoke's 命中率 cut (×talent_scale while S2 runs)
installSmoke(battle, { unit, phys: Math.abs(num(t0.damage_hitrate_physical)), arts: Math.abs(num(t0.damage_hitrate_magical)), scale: num(b2.talent_scale, 1), s3Grid });
} },
{ install(battle, unit) { // 弑君者威名 (+ EXE-Y stage 2+: the stun duration)
const ds = num(t1.damage_scale, 1);
battle.on('deploy', (c) => { if (c.unit === unit && !c.move) unit.mem.croslyHurtBy = new WeakSet(); }, { owner: unit, priority: 10 });
battle.on('damaged', (c) => {
const e = c.source, d = c.dmg;
if (c.target !== unit || !e || e.side !== 'enemy' || c.type === 'element' || !d) return;
if (d.tags && (d.tags.includes('dot') || d.tags.includes('periodic') || d.tags.includes('hpLoss'))) return;
(unit.mem.croslyHurtBy ??= new WeakSet()).add(e);
}, { owner: unit });
const fresh = (e) => !(unit.mem.croslyHurtBy && unit.mem.croslyHurtBy.has(e));
if (ds !== 1) {
battle.on('hit', (c) => {
const t = c.target, d = c.dmg;
if (c.source !== unit || !t || t.side !== 'enemy' || t.isFlying || !d || d.type !== 'phys' || !fresh(t)) return;
d.mul *= ds;
}, { owner: unit });
}
if (osr > 0) {
battle.on('beforeStatus', (c) => {
if (c.source !== unit || c.status !== 'stun' || !c.target || c.target.side !== 'enemy' || !fresh(c.target)) return;
c.duration *= Math.abs(1 + osr);
}, { owner: unit });
}
} },
],
install(battle, unit) {
// 烽烟行刑场: every damage she deals while it runs marks its target for mark_duration s
if (chess?.skill?.skillId === S3) {
battle.on('damaged', (c) => {
const t = c.target;
if (c.source !== unit || !t || t.side !== 'enemy' || c.type === 'element' || !skillOn(unit, S3) || !unit.mem.croslyMarks) return;
unit.mem.croslyMarks.set(t, battle.time + markDur);
}, { owner: unit });
}
// EXE-Y "周围四格没有友方干员时攻击力+10%"
if (lonelyAtk > 0) toggleBuff(battle, unit, 'trait:crosly:lonely', () => noOperatorBeside(battle, unit), { atkPct: lonelyAtk });
},
};
},
};
+184
View File
@@ -0,0 +1,184 @@
// server/sim/content/kits/ops/op-haak.js — 阿 (char_225_haak) 自选 operator kit: 6★ 怪杰 (特种), an owned-6★ pick of the
// tier-5 and tier-6 自选 slots; every skill, both talents, the trait and both modules (GEE-X 什锦果味医用箱, GEE-Y 毒医残章) at
// every form. Kit contract and the 自选 rules: ../README.md ("How to add an operator (自选)").
//
// Forms (data/backups.json units.char_225_haak, 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) — the owner's decision of 2026-10-05.
// Potential 0 [ASSUMED: no account]. Sources: character_table / skill_table / battle_equip_table (zh_CN, as built into
// backups.json); PRTS 阿 (混合药物射击 备注 "每次攻击必定触发随机效果,4种效果的概率平均分布" / "<X模组>先判断是否“同时触发”所有
// 效果,未触发情况下再判断随机效果"; 爆发剂 备注 "进行完整的15次攻击后阿与目标获得持续时间相当于技能持续时间的增益;15次攻击均为无途径
// 攻击,但可满足近战/远程攻击相关的触发条件"); PRTS 分支特性信息 怪杰 ("可对空", "每秒流失最大生命值1%的生命值,不会致命且最低保留1
// 生命值"); the client's battle data — charpack char_225_haak (Talents/1: the additive active buff haak_t_1; Talents/2:
// heal_scale; Trait: periodic_damage_by_hp_ratio[skip_modifier_fix][undeadable] every 1 s), buff_template_data
// (haak_t_1: RandomCreateBuff of haak_t_atk (AtkScaleUp `atk_scale` on ON_CALCULATE_DAMAGE) / sluggish / stun / haak_t_heal
// (HealViaMaxHpRatio), weight 1 each; haak_e_002[dice]: Dice `prob` ⇒ all four, else the random one), the skill prefabs
// skchr_haak_2 / _3 (Attack + Fake_Attack: 15 shots — `_additionalTimes` 14, `_triggerDelta` 0.03 s — of
// instant_damage_physical[add_sp] = FixedValueDamage PHYSICAL `damage`; the target's buff fed with the last shot, his own
// by the Buff ability after the volley; ally selector `_targetSide` 1, `_excludeOwner`, `_postFilter` 21, his attack
// range) and the equips haak_equip_1_* (the hidden sp_recovery talent while his HP ratio is in [0.8, 1]) / haak_equip_2_2,
// _2_3 (heal_scale + the ability healinrange: one ally of his range, the same selector, healed healinrange.heal_scale ×
// ATK at once); Terra Wiki Aak (爆发剂: "an ally within range (prioritizes allies directly in front)", "the one in the
// frontal row/column from the closest to the furthest").
// - Trait (怪杰) "自身生命会不断流失": the profession default (professions.js geek, installHpDrain: hp_ratio × max HP every
// second, never below 1 HP), ranged physical arrows that hit air units (data canHitFly; PRTS "可对空"), blocks 1, 3-3;
// ground enemies target him (no flag). Module GEE-X adds "生命值高于80%时,技力自然回复速度+0.25/秒" (the hidden module
// talent sp_recovery_per_sec: SP recovery +0.25/s while his HP ratio is at least 80 % — the equip checker's [0.8, 1]);
// GEE-Y "再部署时间减少" is its attribute respawn_time −15 (in the stats, as its ATK / ASPD).
// - T1 混合药物射击 "每次攻击时会随机触发下列效果之一": every attack of his on an enemy rolls once, at its hit (the talent's
// additive active buff rides the attack onto the target — not damage-missable: a dodged hit still rolls), one of four
// with equal weight: heal himself hp_ratio × max HP (a 治疗: T2 scales it), that attack's damage ×atk_scale (an ATK scale,
// before mitigation — "当次攻击力提升至150%"), 停顿 `sluggish` s or 晕眩 `stun` s on the target. GEE-X stage 2+: first
// `prob` to trigger all four at once (haak_e_002[dice]), else the random one; stage 3's numbers are the module's.
// [ASSUMED] his 爆发剂 shots at an ally never roll (PRTS names no such effect); the client's haak_t_atk lingers until his
// next roll and could scale damage he deals meanwhile — here it scales the attack that rolled it only.
// - T2 药剂扩散 "自身受到的治疗量+20%": healing received ×heal_scale. GEE-Y stage 2+ (1.25 at stage 3) adds "部署后立即恢复攻击
// 范围内一名友方干员(优先选取正前方)的生命值,恢复量为阿攻击力的150%": at each deployment, after the deploy-time buffs, a heal
// of healinrange.heal_scale × ATK on the ally frontAlly picks among the healable ones [ASSUMED: injured or not — the
// selector has no HP filter; summons count, as for 爆发剂's identical selector].
// - S1 快速射击 (MANUAL, data DEFAULT): ASPD +attack_speed for its duration.
// - S2 爆发剂·γ型 / S3 爆发剂·榴莲味 (MANUAL, data DEFAULT; timed: 30 / 20 s): at the cast, `stimulant`: the ally frontAlly
// picks takes 15 physical hits of `damage` (500) fixed attack power each (FixedValueDamage: DEF applies, 5 % minimum;
// 普通伤害, skill damage — 受击回复 SP and the 重装 TAKE_DAMAGE trigger answer them like any damage taken: the
// template's "[add_sp]"); then he and — still standing — the target get max HP +max_hp / DEF +def (S2) or ATK +atk / ASPD
// +attack_speed (S3) for the skill's duration (per 阿 on the target: independentCharacterSource). [ASSUMED] the volley
// lands at once (its 0.42 s of shots and flight are not modelled); with no ally in his range the cast does nothing (the
// client's sequence stops when its attack finds no target) — his skill still runs and spends its SP: the data's DEFAULT
// trigger needs an enemy, not an ally (reported as an open question).
import { num, talentBb, moduleBb, moduleOn, skillRec, statBuff, toggleBuff, up } from '../shared/tier1.js';
import { toLocal } from '../../../dir.js';
import { COLS } from '../../../constants.js';
const S1 = 'skchr_haak_1';
const S2 = 'skchr_haak_2';
const S3 = 'skchr_haak_3';
/** 爆发剂's shots when the text names none ("攻击15次"; skill prefab `_additionalTimes` 14 + the first). */
const SHOTS_FALLBACK = 15;
/** GEE-X "生命值高于80%时" when the trait text names none (the equip checker's `_minHpRatio`). */
const GEEX_HP_FALLBACK = 0.8;
/** 混合药物射击's four effects, in the client's RandomCreateBuff order (weight 1 each). */
const DICE = Object.freeze(['atk', 'sluggish', 'stun', 'heal']);
const TAG_STIM = 'haak:stim';
const bbOf = (chess, id) => skillRec(chess, id)?.bb ?? {};
/**
* 爆发剂 / 药剂扩散's target (the client selector `_postFilter` 21; Terra Wiki Aak): an ally — an operator or a summon,
* never a device, never a 孤立 one (Battle.allySelectable) — standing in his current attack range, himself excluded; the
* ones on his straight line ahead (local row 0) first, each group nearest first; ties: the higher taunt level, then the
* latest deployed [ASSUMED: the Terra Wiki's "highest aggression level"], then the unit id. `heal` = a heal selector: no
* 禁疗 / 无法被友方治疗 ally (PRTS 选择器 "若掩码中禁疗为1,且选择器属于治疗行为,则不可选中").
*/
export function frontAlly(battle, unit, heal = false) {
const keys = unit.rangeKeySet || new Set(unit.rangeKeys || []);
let best = null, bk = null;
for (const a of battle.allyUnits) {
if (a === unit || !a.alive || !a.deployed || a.hidden || a.kind === 'device') continue;
if (!keys.has(a.tileR * COLS + a.tileC) || !battle.allySelectable(a, unit)) continue;
if (heal && (a.s.flags.noHeal || a.s.flags.healFree || a.profile?.noHeal)) continue;
const dr = a.tileR - unit.tileR, dc = a.tileC - unit.tileC;
const [lr, lc] = toLocal(dr, dc, unit.dir);
const k = [lr === 0 && lc > 0 ? 0 : 1, Math.hypot(dr, dc), -(a.s.taunt || 0), -(a.aggroSeq || a.deploySeq || 0), a.id];
let less = !bk;
for (let i = 0; !less && i < k.length; i++) {
if (k[i] === bk[i]) continue;
less = k[i] < bk[i];
break;
}
if (less) { best = a; bk = k; }
}
return best;
}
/**
* 爆发剂 at the cast: `shots` hits of `damage` fixed attack power on frontAlly's pick, then `mods` on him and on the
* target (if it still stands) for `dur` s. No ally in range: nothing [ASSUMED, header].
*/
function stimulant(battle, unit, { damage, shots, dur, mods, key }) {
const t = frontAlly(battle, unit);
if (!t) return;
for (let i = 0; i < shots && t.alive && unit.alive; i++) {
battle.dealDamage(unit, t, { amount: damage, type: 'phys', isSkill: true, tags: ['skill', TAG_STIM] });
}
battle.fx('volley', { x: t.x, y: t.y, id: unit.id, target: t.id, n: shots, skill: key });
if (!(dur > 0)) return;
if (unit.alive) battle.addBuff(unit, { key: `${key}:self`, duration: dur, mods, source: unit, tags: ['skill'] });
if (t.alive && t !== unit) {
battle.addBuff(t, { key: `${key}:${unit.id}`, duration: dur, mods, source: unit, tags: ['skill'] });
battle.fx('buff', { x: t.x, y: t.y, id: t.id, kind: key });
}
}
/**
* 混合药物射击: one roll per attack on an enemy (its `attackId`), at its first hit — `all` (GEE-X stage 2+) for the four
* at once, else one of DICE uniformly; the heal and the statuses then, the ATK scale on every damage instance of it.
*/
function installDice(battle, unit, t0) {
const hr = num(t0.hp_ratio), scale = num(t0.atk_scale, 1), slug = num(t0.sluggish), stun = num(t0.stun), all = num(t0.prob);
battle.on('hit', (ctx) => {
const d = ctx.dmg, t = ctx.target;
if (ctx.source !== unit || !d || !d.isAttack || d.isSplash || d.type === 'element' || !t || t.side !== 'enemy') return;
const m = unit.mem;
if (!d.attackId || m.haakDiceId !== d.attackId) {
m.haakDiceId = d.attackId;
m.haakDice = all > 0 && battle.rng.chance(all) ? DICE : [DICE[battle.rng.int(DICE.length)]];
for (const e of m.haakDice) {
if (e === 'heal' && hr > 0) battle.heal(unit, unit, unit.s.maxHp * hr, { self: true });
else if (e === 'sluggish' && slug > 0) battle.applyStatus(t, 'sluggish', { duration: slug, source: unit });
else if (e === 'stun' && stun > 0) battle.applyStatus(t, 'stun', { duration: stun, source: unit });
}
if (m.haakDice.includes('atk') && scale !== 1) battle.fx('crit', { x: t.x, y: t.y, id: unit.id });
}
if (m.haakDice.includes('atk')) d.amount *= scale;
}, { owner: unit });
}
export default {
char_225_haak: (bb, chess) => {
const t0 = talentBb(chess, 0); // 混合药物射击 (GEE-X stage 2+: + prob, its own numbers)
const t1 = talentBb(chess, 1); // 药剂扩散 (GEE-Y stage 2+: heal_scale 1.25, healinrange.heal_scale)
const hidden = moduleBb(chess); // GEE-X: sp_recovery_per_sec
const b1 = bbOf(chess, S1);
const stim = (id, key, mods) => {
const rec = skillRec(chess, id);
const b = rec?.bb ?? {};
const shots = Math.max(1, Math.floor(num(+(/攻击(\d+)次/.exec(String(rec?.desc ?? ''))?.[1]), SHOTS_FALLBACK)));
const dur = num(rec?.duration, 0);
return {
kind: 'duration',
onStart({ battle, unit }) { stimulant(battle, unit, { damage: num(b.damage, 500), shots, dur, mods: mods(b), key }); },
};
};
return {
skills: {
[S1]: { kind: 'duration', mods: { aspd: num(b1.attack_speed) } },
[S2]: stim(S2, 'haak:s2', (b) => ({ hpPct: num(b.max_hp), defPct: num(b.def) })),
[S3]: stim(S3, 'haak:s3', (b) => ({ atkPct: num(b.atk), aspd: num(b.attack_speed) })),
},
talents: [
{ install(battle, unit) { installDice(battle, unit, t0); } }, // 混合药物射击
{ install(battle, unit) { // 药剂扩散: healing received ×heal_scale; GEE-Y stage 2+: the deploy heal
statBuff(battle, unit, 'talent:haak:heal', { healingTakenMul: num(t1.heal_scale, 1) });
const hs = num(t1['healinrange.heal_scale']);
if (!(hs > 0)) return;
battle.on('deploy', (c) => {
if (c.unit !== unit || c.move || !up(unit)) return;
const t = frontAlly(battle, unit, true);
if (!t) return;
battle.heal(unit, t, unit.s.atk * hs);
battle.fx('heal', { x: t.x, y: t.y, id: t.id, src: unit.id });
}, { owner: unit, priority: -10 });
} },
],
install(battle, unit) {
// GEE-X "生命值高于80%时,技力自然回复速度+0.25/秒"
const sp = num(hidden.sp_recovery_per_sec);
if (sp > 0 && moduleOn(chess)) {
const pct = +(/生命值高于(\d+)%/.exec(String(chess?.trait?.moduleDesc ?? ''))?.[1]);
const thr = Number.isFinite(pct) ? pct / 100 : GEEX_HP_FALLBACK;
toggleBuff(battle, unit, 'module:haak:sp', () => unit.hp >= unit.s.maxHp * thr - 1e-9, { spRecoveryFlat: sp });
}
},
};
},
};
+227
View File
@@ -0,0 +1,227 @@
// server/sim/content/kits/ops/op-phatom.js — 傀影 (char_250_phatom) 自选 operator kit: 6★ 处决者 (特种), an owned-6★ pick of
// the tier-5 and tier-6 自选 slots; every skill, both talents, the trait, his modules (EXE-X 克里斯汀小姐的毛毡, EXE-Y “如影随形”,
// ISW-A 傀影特限证章) at every form, and the kit of his summon 镜中虚影 (token_10007_phatom_twin). Kit contract and the 自选
// rules: ../README.md ("How to add an operator (自选)").
//
// Forms (data/backups.json units.char_250_phatom; the twin: tokens.token_10007_phatom_twin, variants by owner form with
// bySkill / byModule): 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) — the owner's decision of 2026-10-05. Potential 0 [ASSUMED: no account]. Sources:
// character_table / skill_table / battle_equip_table / token_table (zh_CN, as built into backups.json); PRTS 傀影 (镜中虚影
// 备注 "虚影再部署与拥有复数召唤物的干员相同,即“部署召唤物后立刻开始计算再部署时间,结束后可以部署下一个”"; 血色乐章 备注 "触发消耗
// 层数效果的行动节点为“成功造成伤害后”。若造成的伤害被伤判效果归零则不会消耗层数"; 夜幕突袭 备注 "不可对空"; EXE-X "撤退时返还大量
// (80%)该次部署费用"); PRTS 分支特性信息 处决者; the client's battle data — charpack char_250_phatom (Talents/1:
// charge_token[born] (RechargeToken on his deployment) + die_to_kill_token (KillTokens when he leaves), CommonAbilities
// KillTokens; modes S2 / S3), token prefab token_10007_phatom_twin (charge_token[finish]; the same modes), the skill
// prefabs skchr_phatom_1 / _2 / _3 and sktok_phatom_1 / _2 / _3 (S1: evade_physical + phatom_s_1[shield_physical] = a
// shield_physical of hp_ratio × max HP, both `duration`; S2: phatom_s_2 = `times` stacked phatom_s_2[atk] (ATK MULTIPLIER
// `atk`), one stack finished per ON_AFTER_OUTPUT_DAMAGE; S3: RandomCastAbility of the sluggish / stun / root attack
// abilities — one drawn per cast, each hitting every ground enemy of the x-4 with `atk_scale` physical, knockback[relative]
// (`force`) and its status), the equips phatom_equip_2_* (the EXE-Y trait: ATK while every allied unit of profession mask
// 639 — the eight operator professions, no summon — stands at Manhattan distance ≥ 2) / phatom_equip_2_2/3_p2 (the twin's
// hidden talent phatom_equip_token[atk] → phatom_equip_host[atk] on him) / phatom_equip_3_* (`_checkRoguelikeMode`: ISW-A
// is 集成战略-only); buff_template_data (die_to_kill_token, charge_token[born] / [finish], phatom_equip_token[host]).
// - Trait (处决者) "再部署时间大幅度减少": the data's respawnTime (18 s); melee physical, 1-1, blocks 1, ground-only (data
// canHitFly false), attacks the enemies it blocks first (the engine's blocked-first rule); ground enemies target him.
// - Module EXE-X: "撤退时返还大量该次部署费用" — no manual retreat in battle in this mode ⇒ no effect (as 砾's EXE-X); HP / ATK
// in the stats; stage 2+ changes T2 (below). EXE-Y: "周围四格没有友方干员时攻击力+10%" (trait bb atk): ATK +atk while no
// allied operator — summons and devices do not count (mask 639) — stands on the 4 tiles beside him (noOperatorBeside);
// stage 2+ changes T1 (below); ATK / DEF in the stats. ISW-A: its trait ("在集成战略中,攻击时使自身和召唤物下次再部署时间减少
// 1秒") and talent ("在集成战略中,虚影不占用部署位,且部署虚影时自身再次释放技能") are 集成战略-only: N/A here, the trait
// blackboard it carries is not used; ATK / ASPD in the stats.
// - T1 镜中虚影 "可以使用一个属性更强的虚影,虚影拥有和自己一样的技能,拥有独立的再部署时间": the twin is a hand piece the player
// places (a talent summon: tokens `placeable`, not owner-range); it deploys with the board after the operators and fights
// with its own stats (the variant of his form; EXE-Y stage 3: "属性进一步增强" = the byModule stats) as an executor (melee,
// blocks 1, ground-only), its copy of his picked skill (bySkill sktok_phatom_1 / 2 / 3) running at each of its own
// deployments. When he leaves the field (knocked out, withdrawn, forced out) it is killed (KillTokens); a fallen twin comes
// back on its tile — paying its cost (5 DP) as a deployment, the 卫戍 auto redeploy of a placed summon [ASSUMED, as 鸿雪's
// 打字机 / 凯尔希's Mon3tr] — once its redeploy time has passed since its LAST DEPLOYMENT (PRTS 备注: the timer starts at the
// summon's deployment) and only while he stands (it is recharged by his deployment and killed by his leaving)
// [ASSUMED: his redeployment does not cut the twin's own timer short — the client's RechargeToken NORMAL; the 凯尔希 kit
// reads the same charge_token[born] as "readies at once"]. EXE-Y stage 2+ "本体和虚影同时在场时,攻击力各+10%": the twin's
// hidden module talent `atk` — ATK +atk on both while both are on the field.
// - T2 虚影精通 "虚影的再部署时间-10秒" (EXE-X stage 3: −16): the twin's own talent respawn_time, added to its 45 s.
// - S1 暗夜魅影 (被动, at each deployment): physical dodge `prob` and a 屏障 of hp_ratio × max HP that absorbs physical damage
// only (shieldType 'phys'), both for `duration` (10) s.
// - S2 血色乐章 (被动, at each deployment): `times` stacks of ATK +atk each (直接乘算, summed); each damage instance he deals
// uses one, after it is dealt (a dodged or cancelled hit — invulnerable, 限伤 — uses none; a hit a barrier absorbs does);
// the stacks go when he leaves the field.
// - S3 夜幕突袭 (被动, at each deployment, after the deploy-time buffs): every selectable ground enemy of the x-4 (PRTS "不可
// 对空") takes atk_scale × ATK physical skill damage, is pushed away from him radially with 力度 `force` (0 = 小力,
// knockback[relative]: Battle.push) and gets the one status drawn for this cast — 停顿 / 晕眩 / 束缚 for its bb duration
// (the client draws one of the three abilities per cast, so every enemy gets the same) [ASSUMED: the push and the status
// land on a dodged hit too — the abilities' buffs are not damage-missable]. At the battle-start deployment no enemy is
// on the field yet: it hits nothing (as Misery's 空间的归依).
import { num, traitBb, skillRec, toggleBuff, up } from '../shared/tier1.js';
import { absoluteRangeKeys } from '../../../targeting.js';
const S1 = 'skchr_phatom_1';
const S2 = 'skchr_phatom_2';
const S3 = 'skchr_phatom_3';
export const TWIN = 'token_10007_phatom_twin';
const TS1 = 'sktok_phatom_1';
const TS2 = 'sktok_phatom_2';
const TS3 = 'sktok_phatom_3';
/** 夜幕突袭's area when a record carries no grid: range x-4, his tile and the eight around it. */
const X4 = Object.freeze([[1, -1], [1, 0], [1, 1], [0, -1], [0, 0], [0, 1], [-1, -1], [-1, 0], [-1, 1]]);
/** The four tiles beside a unit (EXE-Y "周围四格"). */
const BESIDE = Object.freeze([[1, 0], [-1, 0], [0, 1], [0, -1]]);
/** 血色乐章's stack cap (buff_template_data phatom_s_2[atk] max_stack_cnt: the ISW-α stacking limit, never reached here). */
const OPUS_CAP = 80;
/** 夜幕突袭's statuses: the client's RandomCastAbility list (sluggish, stun, root) and the blackboard key of each duration. */
const RAID = Object.freeze([{ status: 'sluggish', key: 'sluggish' }, { status: 'stun', key: 'stun' }, { status: 'bind', key: 'root' }]);
/** How often a fallen twin tries to come back once its conditions may hold. */
const RETRY = 0.25;
const TAG_RAID = 'phatom:raid';
/** First talent of a normalised token def whose blackboard holds `key` (token talents keep no index). */
const tokTalentBb = (def, key) => (def?.talents ?? []).find((t) => t && t.bb && t.bb[key] != null)?.bb ?? {};
const isTwinOf = (t, owner) => !!t && t.kind === 'token' && t.defId === TWIN && t.ownerUnit === owner;
const twinsOf = (battle, owner) => battle.allyUnits.filter((t) => isTwinOf(t, owner));
/**
* Module EXE-Y "周围四格没有友方干员" (the equip checker over profession mask 639 = the eight operator professions): no
* allied operator — of any player of the field — stands on one of the 4 tiles beside `unit`; summons and devices do not
* count. Shared with 弑君者's EXE-Y (op-crosly.js).
*/
export function noOperatorBeside(battle, unit) {
for (const [dr, dc] of BESIDE) {
const r = unit.tileR + dr, c = unit.tileC + dc;
if (!battle.grid.inBounds(r, c)) continue;
const o = battle.unitAt(r, c);
if (o && o !== unit && o.side === 'ally' && o.kind === 'op') return false;
}
return true;
}
/** 暗夜魅影 / its copy at a deployment of `u`: physical dodge and a physical-only 屏障 of hp_ratio × its max HP. */
function nightPhantom(battle, u, b, key) {
const dur = num(b.duration, 10), p = num(b.prob), sh = u.s.maxHp * num(b.hp_ratio);
if (!(dur > 0)) return;
if (p > 0) battle.addBuff(u, { key: `${key}:evade`, duration: dur, mods: { dodgePhys: p }, tags: ['skill'] });
if (sh > 0) {
battle.addBuff(u, { key: `${key}:shield`, duration: dur, shield: sh, shieldType: 'phys', tags: ['skill'] });
battle.fx('shield', { x: u.x, y: u.y, id: u.id });
}
}
/** 血色乐章 / its copy at a deployment of `u`: `times` stacks of ATK +atk. */
function bloodyOpus(battle, u, b, key) {
const n = Math.floor(num(b.times)), a = num(b.atk);
if (!(n > 0) || !a) return;
battle.addBuff(u, { key, refresh: 'stack', stacks: n, maxStacks: Math.max(n, OPUS_CAP), mods: { atkPct: a }, tags: ['skill'] });
battle.fx('buff', { x: u.x, y: u.y, id: u.id, kind: 'atk' });
}
/** 血色乐章: every damage instance `u` deals — after it is dealt — uses one stack of buff `key`. */
function installOpusUse(battle, u, key) {
battle.on('damaged', (c) => {
if (c.source !== u || c.type === 'element' || !c.target || c.target.side === u.side) return;
const b = u.findBuff(key);
if (!b) return;
if (b.stacks > 1) { b.stacks -= 1; u.markDirty(); } else battle.removeBuff(u, b);
}, { owner: u });
}
/** 夜幕突袭 / its copy at a deployment of `u` (record `rec`: its blackboard and x-4). */
function nightRaid(battle, u, rec) {
const b = rec?.bb ?? {};
const grid = Array.isArray(rec?.rangeGrid) && rec.rangeGrid.length ? rec.rangeGrid : X4;
const foes = battle.enemiesInKeys(absoluteRangeKeys(grid, u.tileR, u.tileC, u.dir, 0), u, { canHitFly: false, groundOnly: true });
const pick = RAID[battle.rng.int(RAID.length)]; // one ability per cast (RandomCastAbility)
const dur = num(b[pick.key]), scale = num(b.atk_scale), force = num(b.force, 0);
for (const e of foes) {
if (!e.alive) continue;
if (scale > 0) battle.dealDamage(u, e, { amount: u.s.atk * scale, type: 'phys', isSkill: true, tags: ['skill', TAG_RAID] });
if (e.alive) battle.push(e, force, { from: u });
if (e.alive && dur > 0) battle.applyStatus(e, pick.status, { duration: dur, source: u });
}
battle.fx('aoe', { x: u.x, y: u.y, radius: 1, id: u.id, skill: TAG_RAID });
}
/** The deploy-time part of skill record `rec` (his or the twin's copy) on `u`; `key` names its buffs. */
function deployEffect(battle, u, rec, key) {
const id = rec?.skillId ?? rec?.id ?? null;
if (id === S1 || id === TS1) nightPhantom(battle, u, rec.bb ?? {}, key);
else if (id === S2 || id === TS2) bloodyOpus(battle, u, rec.bb ?? {}, key);
else if (id === S3 || id === TS3) nightRaid(battle, u, rec);
}
/**
* The twin's kit (`owner` = 傀影): its skill copy at each of its deployments, EXE-Y stage 2+'s shared ATK, and its own
* redeploys (killed with him; back on its tile once its timer has run from its last deployment, he stands and the DP is
* there).
*/
function twinKit(t, owner) {
const rec = t.def?.skill ?? null;
const key = 'phatom:twin:skill';
const cut = num(tokTalentBb(t.def, 'respawn_time').respawn_time); // 虚影精通 (−10; EXE-X stage 3: −16)
const both = num(tokTalentBb(t.def, 'atk').atk); // EXE-Y stage 2+: 本体和虚影同时在场时
return {
skill: { kind: 'passive' },
install(battle, unit) {
if (rec?.id === TS2) installOpusUse(battle, unit, key);
battle.on('deploy', (c) => {
if (c.unit !== unit || c.move || !up(unit)) return;
unit.mem.twinReadyAt = battle.time + Math.max(0, num(unit.base.respawnTime) + cut); // timer from its deployment
deployEffect(battle, unit, rec ? { ...rec, skillId: rec.id } : null, key);
}, { owner: unit, priority: -10 });
if (both > 0) toggleBuff(battle, unit, 'module:phatom:both', () => up(owner), { atkPct: both });
battle.on('death', (c) => {
if (c.unit !== unit || battle.finished) return;
unit.removed = false; // the piece stays (Battle.redeploy accepts it; its hooks are kept)
if (unit.mem.twinRetry) return;
unit.mem.twinRetry = battle.every(RETRY, (b, sched) => {
const stop = () => { sched.cancel(); unit.mem.twinRetry = null; };
if (unit.alive || unit.removed || b.finished) { stop(); return; }
if (!up(owner) || b.time + 1e-9 < num(unit.mem.twinReadyAt, 0)) return;
if (b.redeploy(unit, { free: false })) stop();
}, { owner: unit });
}, { owner: unit, priority: -10 });
},
};
}
export default {
char_250_phatom: (bb, chess) => {
const lonelyAtk = num(traitBb(chess).atk); // EXE-Y trait (EXE-X: withdraw_cost_recover_ratio, ISW-A: 集成战略-only)
const t0desc = String((chess?.talents ?? []).find((t) => t && t.index === 0)?.desc ?? '');
const recOf = (id) => skillRec(chess, id);
return {
skills: {
[S1]: { kind: 'passive' },
[S2]: { kind: 'passive' },
[S3]: { kind: 'passive' },
},
talents: [
{ install(battle, unit) { // 镜中虚影: his twin pieces run the twin's kit; killed when he leaves; EXE-Y stage 2+: both +ATK
for (const t of twinsOf(battle, unit)) {
if (t.alive || t.deployed) continue;
if (t.kit) battle.offOwner(t); // a piece set up before him: drop its generic kit's hooks
battle._setupUnit(t, twinKit(t, unit));
}
battle.on('death', (c) => {
if (c.unit !== unit) return;
for (const t of twinsOf(battle, unit)) if (t.alive) battle.kill(t, null); // KillTokens
}, { owner: unit });
// 本体和虚影同时在场时,攻击力各+N%: the twin's hidden module talent (his text when no twin def resolves)
const both = num(tokTalentBb(battle.tokenDef(TWIN, unit), 'atk').atk, /本体和虚影同时在场时/.test(t0desc)
? num(+(/攻击力各\+(\d+)%/.exec(t0desc)?.[1]) / 100) : 0);
if (both > 0) toggleBuff(battle, unit, 'module:phatom:both', () => twinsOf(battle, unit).some(up), { atkPct: both });
} },
// 虚影精通 is the twin's own talent (its redeploy time): twinKit
],
install(battle, unit) {
// the picked skill's deploy-time effects (S1 dodge + barrier, S2 stacks, S3 burst), after the deploy-time buffs
const rec = recOf(chess?.skill?.skillId);
if (rec?.skillId === S2) installOpusUse(battle, unit, 'phatom:skill');
battle.on('deploy', (c) => {
if (c.unit !== unit || c.move || !up(unit)) return;
deployEffect(battle, unit, rec, 'phatom:skill');
}, { owner: unit, priority: -10 });
// EXE-Y "周围四格没有友方干员时攻击力+10%"
if (lonelyAtk > 0) toggleBuff(battle, unit, 'trait:phatom:lonely', () => noOperatorBeside(battle, unit), { atkPct: lonelyAtk });
},
};
},
};