feedback5: merge O6 — 自选 kits for 黑 and 鸿雪 (with her 打字机); 焰狐龙梓兰's kit and test left out (a collab operator, out of the pool since 8287422); 黑 S3's tests and kit header follow its DEFAULT trigger (「攻击范围改为」); golden: only diy moved

This commit is contained in:
sganggs committed 2026-10-06 00:21:15 +08:00
commit 379822a04e
6 files changed
+1252 -117

No files matched your search

+2
View File
@@ -98,6 +98,8 @@ export const OPERATOR_KIT_FILES = Object.freeze([
'op-zuole.js',
'op-chyue.js',
'op-huang.js',
'op-shwaz.js',
'op-bgsnow.js',
]);
/**
+226
View File
@@ -0,0 +1,226 @@
// server/sim/content/kits/ops/op-bgsnow.js — 鸿雪 (char_4055_bgsnow) 自选 operator kit: 6★ 重射手 (狙击), an owned-6★ pick of
// the tier-5 and tier-6 自选 slots; every skill, both talents, the trait, both modules (ARC-Y 打字机色带, ARC-X 仪式感) at
// every form, and the kit of her summon “打字机” (token_10026_bgsnow_subbow). Kit contract and the 自选 rules: ../README.md
// ("How to add an operator (自选)").
//
// Forms (data/backups.json units.char_4055_bgsnow): 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)
// and PRTS 鸿雪 + “打字机” (点题 备注; the {{**|82%|下降18%}} marks = a final ×0.82; PRTS 伤害分类: "鸿雪2技能三段伤害的后2段"
// are 持续伤害).
// - Trait (重射手) "高精度的近距离射击": ranged physical arrows, 3-6, can hit air units (PRTS 分支特性信息 重射手 "可对空"),
// blocks 1, ground enemies target her (no flag).
// - Module ARC-Y “打字机色带” trait "攻击正前方的敌人时攻击力提升至105%且无视其物理闪避" (trait bb atk_scale): her damage on
// an enemy whose body is on her straight line ahead ×atk_scale, not dodgeable — 点题's two 持续伤害 hits included
// [ASSUMED: they are two of its "3次攻击"]; 正前方 = local row 0, column ≥ 0 (her own tile included, as the 3-2 "前方3格"
// lines count it) [ASSUMED]. Stage 2+ "属性更强的打字机": the typewriter's stats of the data (token module attributes).
// - Module ARC-X “仪式感”: "再部署时间减少" (attribute respawn_time −25, in the stats); stage 2+ upgrades 弱点速记 (below).
// - T1 自动打字机 "可使用一个持续25秒的“打字机”,“打字机”拥有和自己一样的技能,拥有独立的再部署时间": the typewriter is a
// hand piece the player places (the mode's talent summons, content/tokens.js header): it deploys with the board, leaves
// after 25 s (the token's hidden talent `interval`), and comes back on its tile after its own redeploy time (data 40 s; ×
// respawn_time while she carries S2), paying its cost (5 DP), while she is on the field [ASSUMED: as the 海嗣]. Its skill
// is the copy of hers the data gives (bySkill: sktok_bgsnow_1 / 2 / 3 — the 卫戍 strategy force-opens a summon's manual
// skill, PRTS 卫戍协议/帮助): free (spCost 0), once per deployment, by the data's DEFAULT. It is untargetable ("不会受到
// 攻击"), blocks nothing, hits air units. content/tokens.js has no kit for it: her install gives her pieces this one.
// - T2 弱点速记 "“打字机”的攻击会使命中目标的防御力下降18%,持续4秒;若“打字机”放在鸿雪周围四格则效果提升至23%": every
// damage the typewriter's attacks land cuts the target's DEF by bgsnow_token[def_down]_1 (_2 while the typewriter stands
// on one of the 4 tiles next to her — while she is on the field [ASSUMED]) for `duration` s: the catalogue `defDown`
// (final ×, 同名 keeps the strongest). ARC-X stage 2+: 28 % / 33 % and "周围八格" (the talent text names the ring).
// - S1 抑扬格 (AUTO, attack SP, data DEFAULT — an AUTO attack buff waits for her next attack): ATK +atk until she leaves the
// field ("持续时间无限": a toggle), and every attack meanwhile `prob` to deal ×atk_scale (one roll per attack).
// - S2 点题 (MANUAL, 2 charges, data SKILL_RANGE on its 3-1): passive — the typewriter's redeploy time ×respawn_time;
// active — 3 attacks at once ("立即": the cast takes the next attack now) on one target of the 3-1, atk_scale × ATK each:
// the first a physical 普通伤害 attack, the other two physical 持续伤害 (PRTS 伤害分类: tags 'dot', not dodgeable).
// PRTS 备注 "仅在干员自身范围内有可攻击目标时可释放": the data's SKILL_RANGE fires for any enemy of the 3-1, untargetable
// ones too; the cast then waits (pending) for an attackable target — reported as an open question (the 备注 reading
// of the strategy would be the basic one on her own range).
// - S3 锐笔速写 (MANUAL, data ACTIVE_RANGE on its 3-1): range 3-1 while it runs, attack interval base_attack_time s (−0.6 on
// 1.6 s), every attack atk_scale × ATK, bgsnow_s_3[atk_up].atk_scale × ATK on the 正前方3格 (the 3-2 line, her tile
// included [ASSUMED]).
import { num, up, traitBb, skillRec, batMod } from '../shared/tier1.js';
import { absoluteRangeKeys } from '../../../targeting.js';
import { bodyKeys, bodyInKeys } from '../../../body.js';
import { toLocal } from '../../../dir.js';
import { hasHp } from '../../../damage.js';
import { COLS } from '../../../constants.js';
const S1 = 'skchr_bgsnow_1';
const S2 = 'skchr_bgsnow_2';
const S3 = 'skchr_bgsnow_3';
export const TYPEWRITER = 'token_10026_bgsnow_subbow';
const TS1 = 'sktok_bgsnow_1';
const TS2 = 'sktok_bgsnow_2';
const TS3 = 'sktok_bgsnow_3';
/** 锐笔速写's 正前方3格: the 3-2 line (range_table) — her tile and the three ahead. */
const FRONT_3 = Object.freeze([[0, 0], [0, 1], [0, 2], [0, 3]]);
/** 点题's two 持续伤害 hits (PRTS 伤害分类 attackType BUFF). */
const TAG_PRECIS = 'bgsnow:precis';
/** 自动打字机: the typewriter's life when neither the token nor the talent carries it (PRTS: 持续25秒 at E2). */
const LIFE_FALLBACK = 25;
const bbOf = (chess, id) => skillRec(chess, id)?.bb ?? {};
/** First talent of a normalised token def holding `key` (token talents keep no index). */
const tokTalent = (def, key) => (def?.talents ?? []).find((t) => t && t.bb && t.bb[key] != null) ?? null;
/** ARC-Y 正前方: the enemy's body touches the unit's straight line ahead (local row 0, local column ≥ 0). */
function inFrontLine(unit, e) {
for (const k of bodyKeys(e)) {
const [lr, lc] = toLocal(Math.floor(k / COLS) - unit.tileR, (k % COLS) - unit.tileC, unit.dir);
if (lr === 0 && lc >= 0) return true;
}
return false;
}
/** 点题 / its copy: the two 持续伤害 hits after the attack's first (普通伤害) hit, on the same target. */
function precisFollowUps(battle, unit, target, scale) {
for (let i = 0; i < 2 && target && target.alive; i++) {
battle.dealDamage(unit, target, { amount: unit.s.atk * scale, type: 'phys', isSkill: true, canDodge: false, tags: ['skill', 'dot', TAG_PRECIS] });
}
}
/** 抑扬格 / its copy: while `on(unit)`, every attack `prob` to deal ×scale (one roll per attack). */
function installIamb(battle, unit, prob, scale, on) {
if (!(prob > 0) || !(scale > 0) || scale === 1) return;
battle.on('hit', (ctx) => {
const d = ctx.dmg, t = ctx.target;
if (ctx.source !== unit || !t || t.side !== 'enemy' || !d.isAttack || d.isSplash || d.type === 'element' || !on(unit)) return;
const m = unit.mem;
if (!d.attackId || m.iambRollId !== d.attackId) {
m.iambRollId = d.attackId;
m.iambProc = battle.rng.chance(prob);
if (m.iambProc) battle.fx('crit', { x: t.x, y: t.y, id: unit.id });
}
if (m.iambProc) d.amount *= scale;
}, { owner: unit });
}
/**
* The typewriter's kit: its skill (the copy of her skill the data gives), 弱点速记, its life and its redeploys.
* `owner` = 鸿雪, `chess` = her composed record.
*/
function typewriterKit(t, owner, chess) {
const def = t.def;
const sk = def.skill ?? null;
const tb = sk?.bb ?? {};
const weak = tokTalent(def, 'duration');
const wb = weak?.bb ?? {};
const cut1 = -num(wb['bgsnow_token[def_down]_1.def']), cut2 = -num(wb['bgsnow_token[def_down]_2.def'], -cut1);
const ring8 = /八格/.test(String(weak?.description ?? ''));
const ownTalent = (chess?.talents ?? []).find((x) => x && x.index === 0)?.bb ?? {};
const life = num(tokTalent(def, 'interval')?.bb?.interval, num(ownTalent.duration, LIFE_FALLBACK));
const respawnMul = owner.skill?.id === S2 ? num(bbOf(chess, S2).respawn_time, 1) : 1;
const near = () => {
if (!up(owner)) return false;
const dr = Math.abs(t.tileR - owner.tileR), dc = Math.abs(t.tileC - owner.tileC);
return ring8 ? Math.max(dr, dc) === 1 : dr + dc === 1;
};
const skillOn = (u) => !!(u.skill && u.skill.active);
let skill = null;
if (sk?.id === TS1) skill = { kind: 'toggle', mods: { atkPct: num(tb.atk) } };
else if (sk?.id === TS2) {
const scale = num(tb.atk_scale, 1);
skill = { kind: 'instant', attack: { atkScale: scale, onHit({ battle, unit, target }) { precisFollowUps(battle, unit, target, scale); } } };
} else if (sk?.id === TS3) {
skill = {
kind: 'duration',
targeting: { rangeGrid: sk.rangeGrid ?? [[0, 0], [0, 1], [0, 2], [0, 3], [0, 4]] },
mods: { batPct: batMod(tb.base_attack_time, { stats: def.stats }) },
attack: { atkScale: num(tb['attack@atk_scale'], 1) },
};
}
return {
skill,
talents: [{ install(battle, unit) { // 弱点速记: every damage of its attacks cuts the target's DEF (near her: more)
const dur = num(wb.duration, 4);
if (!(cut1 > 0) || !(dur > 0)) return;
battle.on('damaged', (ctx) => {
if (ctx.source !== unit || !ctx.target || ctx.target.side !== 'enemy' || !hasHp(ctx.target) || ctx.dmg?.type === 'element') return;
battle.applyStatus(ctx.target, 'defDown', { duration: dur, value: near() ? cut2 : cut1, source: unit });
}, { owner: unit });
} }],
install(battle, unit) {
if (sk?.id === TS1) installIamb(battle, unit, num(tb.prob), num(tb.atk_scale, 1), skillOn);
// 持续25秒: leaves `life` s after each deployment
battle.on('deploy', (ctx) => {
if (ctx.unit !== unit || !(life > 0)) return;
const seq = unit.deploySeq;
battle.after(life, () => { if (unit.alive && unit.deploySeq === seq) battle.retreat(unit, { reason: 'expired', permanent: true }); }, { owner: unit });
}, { owner: unit });
// 拥有独立的再部署时间: back on its tile after its redeploy time, paying its cost, while 鸿雪 stands (the piece is kept:
// `removed` false, as content/tokens.js enableRespawn)
battle.on('death', (ctx) => {
if (ctx.unit !== unit || battle.finished) return;
unit.removed = false;
const at = battle.time + Math.max(0, num(unit.base.respawnTime) * respawnMul);
unit.mem.respawnAt = at;
battle.every(0.25, (b, sched) => {
if (unit.alive || unit.removed) { sched.cancel(); return; }
if (b.time + 1e-9 < at || !up(owner)) return;
if (b.redeploy(unit, { free: false })) sched.cancel();
}, { owner: unit });
}, { owner: unit, priority: -10 });
},
};
}
export default {
char_4055_bgsnow: (bb, chess) => {
const b1 = bbOf(chess, S1), b2 = bbOf(chess, S2), b3 = bbOf(chess, S3);
const s2 = skillRec(chess, S2), s3 = skillRec(chess, S3);
const frontScale = num(traitBb(chess).atk_scale, 1);
const s2Scale = num(b2.atk_scale, 1);
const s3Scale = num(b3.atk_scale, 1), s3Front = num(b3['bgsnow_s_3[atk_up].atk_scale'], s3Scale);
return {
skills: {
[S1]: { kind: 'toggle', mods: { atkPct: num(b1.atk) } },
[S2]: {
kind: num(s2?.maxChargeTime, 1) > 1 ? 'charges' : 'instant',
// the 3-1 selects the target; her attack range stays her own (a 技能范围, not a range change)
targeting: { rangeGrid: s2?.rangeGrid ?? null, showOwnRange: true },
attack: { atkScale: s2Scale, onHit({ battle, unit, target }) { precisFollowUps(battle, unit, target, s2Scale); } },
onStart({ unit }) { unit.atkCd = 0; }, // "立即": the pending attack goes off now when a target is there
},
[S3]: {
kind: 'duration',
targeting: { rangeGrid: s3?.rangeGrid ?? null },
mods: { batPct: batMod(b3.base_attack_time, chess) },
attack: { atkScale: s3Scale },
},
},
talents: [
// 自动打字机 (and 弱点速记, the typewriter's own talent): her typewriter pieces run the typewriter's kit — set up
// here, before the battle starts (Battle._setupUnit with a kit, as spawnToken's `kit` option)
{ install(battle, unit) {
for (const t of battle.allyUnits) {
if (t.kind !== 'token' || t.defId !== TYPEWRITER || t.ownerUnit !== unit || t.alive || t.deployed) continue;
if (t.kit) battle.offOwner(t); // a piece set up before her: drop its generic kit's hooks
battle._setupUnit(t, typewriterKit(t, unit, chess));
}
} },
],
install(battle, unit) {
const sid = unit.skill?.id;
if (sid === S1) installIamb(battle, unit, num(b1.prob), num(b1.atk_scale, 1), (u) => !!u.skill?.active);
// 锐笔速写: 正前方3格 ⇒ atk_up's scale instead of the skill's
if (sid === S3 && s3Front !== s3Scale && s3Scale > 0) {
battle.on('hit', (ctx) => {
const d = ctx.dmg, t = ctx.target;
if (ctx.source !== unit || !t || t.side !== 'enemy' || !d.isAttack || !unit.skill?.active) return;
if (bodyInKeys(t, absoluteRangeKeys(FRONT_3, unit.tileR, unit.tileC, unit.dir, 0))) d.amount *= s3Front / s3Scale;
}, { owner: unit });
}
// ARC-Y 打字机色带: 攻击正前方的敌人时攻击力提升至105%且无视其物理闪避
if (frontScale > 0 && frontScale !== 1) {
battle.on('hit', (ctx) => {
const d = ctx.dmg, t = ctx.target;
if (ctx.source !== unit || !t || t.side !== 'enemy' || d.type === 'element') return;
if (!(d.isAttack || d.tags?.includes(TAG_PRECIS)) || !inFrontLine(unit, t)) return;
d.amount *= frontScale;
d.canDodge = false;
}, { owner: unit });
}
},
};
},
};
+162
View File
@@ -0,0 +1,162 @@
// server/sim/content/kits/ops/op-shwaz.js — 黑 (char_340_shwaz) 自选 operator kit: 6★ 重射手 (狙击), an owned-6★ pick of the
// tier-5 and tier-6 自选 slots; every skill, both talents, the trait and both modules (ARC-X 定制弩配件套装, ARC-Y 老剃刀) at
// every form. Kit contract and the 自选 rules: ../README.md ("How to add an operator (自选)").
//
// Forms (data/backups.json units.char_340_shwaz): 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) and PRTS 黑
// (破甲箭头 备注 "攻击力提升效果为提升攻击力倍率"; 战术的终结 "攻击间隔略微增大(+0.4)"; the {{**|80%|下降20%}} marks of the
// DEF cut = a final ×0.8).
// - Trait (重射手) "高精度的近距离射击": the plain closerange profile — ranged physical arrows, 3-6, can hit air units
// (PRTS 分支特性信息 重射手 "可对空"), blocks 1, ground enemies target her (no flag).
// - T1 破甲箭头 "攻击时,20%几率当次攻击的攻击力提升至160%,并使命中目标的防御力下降20%,持续5秒": one roll per attack
// (`prob`); on a proc the attack's damage ×atk_scale (an ATK-scale multiplier, PRTS 备注, so it multiplies the skills'
// own scales) and, once the hit landed (命中: a dodged hit does nothing), the target's DEF ×(1 + def) for
// defdown_duration s — the catalogue `defDown` (final ×0.8, PRTS; 同名 keeps the strongest). The skills raise the chance:
// S1's next attack to S1 talent@prob, S2 / S3's attacks while they run to their talent@prob (every attack a skill makes
// carries dmg.isSkill — S2's empty attack override marks its attacks for that). ARC-Y stage 2+ upgrades it (stage 3:
// 170 %, DEF −25 %) — read from the base talent and the module's NAMED change: ARC-Y's hidden talent part (talentIndex 0,
// isHideTalent, atk_scale 1.05, prefabKey "10") is the trait's 105 % as a talent of its own in the game, which the
// data's merge by talentIndex folds into 破甲箭头 at stage 1 (composed atk_scale 1.05) — reported, not used.
// - T2 交叉火力 "场上存在黑和另外至少一名【狙击】干员时,所有【狙击】干员的攻击力+8%": while she is on the field and another
// 【狙击】 operator is too, every 【狙击】 operator of the field (her included; a partner's in a shared field too [ASSUMED,
// as 推进之王's 万兽之王]) ATK +atk, refreshed every 0.5 s (lapses within 0.6 s when the condition ends); several sources
// keep the strongest. ARC-X stage 2+ "携带黑和另外至少一名【狙击】干员时…+13%": 携带 = in the squad — the player's own
// pieces of the battle, deployed or not — so with another 【狙击】 piece in her player's battle every 【狙击】 operator of
// that player ATK +13 % for the whole battle (persist; a partner's operators are not in her squad [ASSUMED]).
// - Module ARC-X “定制弩配件套装”: "再部署时间减少" (attribute respawn_time −25, in the stats: 70 → 45 s) + HP / ATK.
// - Module ARC-Y “老剃刀” trait "攻击正前方的敌人时攻击力提升至105%且无视其物理闪避" (trait bb atk_scale): her attack damage
// on an enemy whose body is on her straight line ahead ×atk_scale and not dodgeable (canDodge false — her damage is
// physical only). 正前方 = local row 0, column ≥ 0 in her facing — her own tile included, as S3's 3-2 "前方3格" counts
// it [ASSUMED: no source draws the line].
// - S1 强弩 (AUTO, attack SP 4, data DEFAULT): the next attack ×atk_scale (and the T1 chance above).
// - S2 暮眼锐瞳 (MANUAL, data DEFAULT): ATK +atk for 33 / 36 s, T1 chance talent@prob.
// - S3 战术的终结 (MANUAL): range → 3-2 (her tile + 3 ahead) while it runs, attack interval +base_attack_time s (a flat
// +0.4 on her 1.6 s, PRTS), ATK +atk, T1 chance 100 %. Trigger: the data's DEFAULT (the basic strategy) — "攻击范围改为前方
// 3格" is an attack-range change, not a 技能范围 (tools/build-data.mjs ATTACK_RANGE_CHANGE has "改为" since O6's report).
import { num, up, traitBb, moduleOn, skillRec, batMod } from '../shared/tier1.js';
import { bodyKeys } from '../../../body.js';
import { toLocal } from '../../../dir.js';
import { hasHp } from '../../../damage.js';
import { COLS } from '../../../constants.js';
const S1 = 'skchr_shwaz_1';
const S2 = 'skchr_shwaz_2';
const S3 = 'skchr_shwaz_3';
/** 交叉火力's buff (the field aura and the squad version share it: one effect per operator, the strongest). */
const CROSSFIRE = 'talent:shwaz:crossfire';
/** 交叉火力's refresh interval (s), as installAura. */
const AURA_EVERY = 0.5;
const bbOf = (chess, id) => skillRec(chess, id)?.bb ?? {};
/** A 【狙击】 operator (summons are never operators). */
const isSniperOp = (a) => !!a && a.kind === 'op' && a.def?.profession === 'SNIPER';
/** The module the record fights with (its active one), or null. */
function activeModule(chess) {
if (!moduleOn(chess)) return null;
return (chess.modules ?? []).find((m) => m && m.uniEquipId === chess.module.id) ?? null;
}
/**
* 破甲箭头's blackboard: the base talent (no module) with the module's NAMED changes of talent 0 only — never the hidden
* trait part (see the header).
*/
function pierceBb(chess) {
const base = (chess?.talentsBase ?? chess?.talents ?? []).find((t) => t && t.index === 0)?.bb ?? {};
const ups = (activeModule(chess)?.talentChanges ?? []).filter((t) => t && t.talentIndex === 0 && !t.hidden && t.name);
return Object.assign({}, base, ...ups.map((t) => t.bb || {}));
}
/** The record's 交叉火力 (talent 1, the module's change included): its blackboard and whether it reads 携带 (squad). */
function crossfireOf(chess) {
const t = (chess?.talents ?? []).find((x) => x && x.index === 1) ?? null;
return { atk: num(t?.bb?.atk), squad: /携带/.test(String(t?.desc ?? '')) };
}
/** ARC-Y 正前方: the enemy's body touches the unit's straight line ahead (local row 0, local column ≥ 0). */
function inFrontLine(unit, e) {
for (const k of bodyKeys(e)) {
const [lr, lc] = toLocal(Math.floor(k / COLS) - unit.tileR, (k % COLS) - unit.tileC, unit.dir);
if (lr === 0 && lc >= 0) return true;
}
return false;
}
export default {
char_340_shwaz: (bb, chess) => {
const b1 = bbOf(chess, S1), b2 = bbOf(chess, S2), b3 = bbOf(chess, S3);
const s3 = skillRec(chess, S3);
const t0 = pierceBb(chess);
const cross = crossfireOf(chess);
const frontScale = num(traitBb(chess).atk_scale, 1);
return {
skills: {
[S1]: { kind: 'instant', attack: { atkScale: num(b1.atk_scale, 1) } },
// (an empty attack override: the attacks made while it runs carry dmg.isSkill — 破甲箭头's raised chance)
[S2]: { kind: 'duration', mods: { atkPct: num(b2.atk) }, attack: {} },
[S3]: {
kind: 'duration',
targeting: { rangeGrid: s3?.rangeGrid ?? [[0, 0], [0, 1], [0, 2], [0, 3]] },
mods: { atkPct: num(b3.atk), batPct: batMod(b3.base_attack_time, chess) },
},
},
talents: [
{ install(battle, unit) { // 破甲箭头: one roll per attack; ×atk_scale on a proc, then DEF cut on the target hit
const scale = num(t0.atk_scale, 1), cut = -num(t0.def), dur = num(t0.defdown_duration, 5), base = num(t0.prob);
const sid = unit.skill?.id;
const raised = num(bbOf(chess, sid)['talent@prob'], base);
battle.on('hit', (ctx) => {
const d = ctx.dmg, t = ctx.target;
if (ctx.source !== unit || !t || t.side !== 'enemy' || !d.isAttack || d.isSplash || d.type === 'element') return;
const m = unit.mem;
if (!d.attackId || m.shwazRollId !== d.attackId) {
m.shwazRollId = d.attackId;
const p = d.isSkill ? raised : base;
m.shwazProc = p > 0 && battle.rng.chance(p);
if (m.shwazProc) battle.fx('crit', { x: t.x, y: t.y, id: unit.id });
}
if (!m.shwazProc) return;
d.amount *= scale;
d.shwazPierce = true;
}, { owner: unit });
battle.on('damaged', (ctx) => {
if (ctx.source !== unit || !ctx.dmg?.shwazPierce || !(cut > 0) || !hasHp(ctx.target) || ctx.target.side !== 'enemy') return;
battle.applyStatus(ctx.target, 'defDown', { duration: dur, value: cut, source: unit });
}, { owner: unit });
} },
{ install(battle, unit) { // 交叉火力: field aura, or (ARC-X stage 2+) the squad's snipers for the whole battle
const v = cross.atk;
if (!(v > 0)) return;
const give = (a, extra) => {
const cur = a.findBuff(CROSSFIRE);
if (cur && cur.source !== unit && (cur.data?.v ?? 0) > v && cur.timeLeft > 0.05) return;
battle.addBuff(a, { key: CROSSFIRE, mods: { atkPct: v }, source: unit, data: { v }, ...extra });
};
if (cross.squad) {
const squad = battle.allyUnits.filter((a) => isSniperOp(a) && a.ownerId === unit.ownerId);
if (squad.some((a) => a !== unit)) for (const a of squad) give(a, { persist: true, allowDead: true, tags: ['talent'] });
return;
}
battle.every(AURA_EVERY, () => {
if (!up(unit)) return;
const snipers = battle.alliesFor(unit).filter(isSniperOp);
if (!snipers.some((a) => a !== unit)) return;
for (const a of snipers) give(a, { duration: AURA_EVERY + 0.1, tags: ['aura'] });
}, { owner: unit, immediate: true });
} },
],
install(battle, unit) {
// ARC-Y 老剃刀: 攻击正前方的敌人时攻击力提升至105%且无视其物理闪避
if (!(frontScale > 0) || frontScale === 1) return;
battle.on('hit', (ctx) => {
const d = ctx.dmg, t = ctx.target;
if (ctx.source !== unit || !t || t.side !== 'enemy' || !d.isAttack || d.type === 'element' || !inFrontLine(unit, t)) return;
d.amount *= frontScale;
d.canDodge = false;
}, { owner: unit });
},
};
},
};