feedback5 O6: 自选 operator kits for 黑, 焰狐龙梓兰 and 鸿雪 (+ her “打字机”) — every skill, talent, trait and module at every form

- op-shwaz.js 黑: 破甲箭头 (one roll per attack, raised by S1 / S2 / S3; ×atk_scale, then defDown on a landed hit; numbers
  from the base talent + ARC-Y's named change — the data folds ARC-Y's hidden trait talent into talent 0 at stage 1),
  交叉火力 (field aura; ARC-X stage 3 "携带": her player's snipers for the battle), ARC-Y 正前方 ×1.05 undodgeable, S3 3-2 +
  interval +0.4; triggers from the data (S3 SKILL_RANGE — reported).
- op-orchd2.js 焰狐龙梓兰: 三连击 normal attack cut to 33.3 % after DEF (PRTS 特性备注), 强击瓶专家 stacks per round, 翔虫机动
  (respawn −15 / −18 s, ATK +15 / 20 % for 30 s on a redeploy at her mark), S1 刚射 / 刚连射 (charges, refund without a
  target, stun per arrow), S2 飞翔瞪射 (free cast at every deployment, 起飞, three volleys on the 4-4, landing on the 1-3,
  timed by the official skeleton), S3 龙之箭 (3 s charge, piercing arrow at 10 tiles/s, strikes every 0.25 tile, push).
- op-bgsnow.js 鸿雪: S1 toggle + proc, S2 点题 (立即, 普通 + 2 × 持续伤害), S3 3-1 with 正前方3格 scale, ARC-Y as 黑's; her
  placed “打字机” pieces get their kit at her install (25 s life, 40 s redeploy × S2's 65 / 60 %, 5 DP; the skill copies;
  弱点速记 DEF cut 18 / 23 %, ARC-X stage 3 28 / 33 % with 八格).
- kits/index.js OPERATOR_KIT_FILES: one line per file. Tests: test/content/op_shwaz / op_orchd2 / op_bgsnow (23).
- Golden: only the diy family moved — diy-003 … diy-006 now hold the 66 new 自选 configurations (黑, 焰狐龙梓兰, 鸿雪) in
  place of the prototype picks, diy-007 … diy-012 are new (the prototype battles moved down); every other family is
  byte-identical. test/golden/README.md: 12 diy battles, 61 of 156.
This commit is contained in:
sganggs committed 2026-10-06 00:10:34 +08:00
1 parent 742f478526
commit 4075338517
9 files changed
+1960 -103

No files matched your search

+6 -1
View File
@@ -78,7 +78,12 @@ export const STANDIN_KIT_FILES = Object.freeze(['standin-acguad.js', 'standin-sh
* The 自选 operator kit files (`ops/op-<codename>.js`, registry key = the operator's charId, an owned-6★ pick of
* data/backups.json; a new file is appended). An operator without a file is not offered as a 自选 pick (KITTED_CHARS).
*/
export const OPERATOR_KIT_FILES = Object.freeze(['op-siege.js']);
export const OPERATOR_KIT_FILES = Object.freeze([
'op-siege.js',
'op-shwaz.js',
'op-orchd2.js',
'op-bgsnow.js',
]);
/**
* The 预备干员 whose every skill the generic kit covers exactly (冲锋号令, 攻击力 / 防御力 / 治疗强化, 战术咏唱, 一击即退 —
+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 });
}
},
};
},
};
+306
View File
@@ -0,0 +1,306 @@
// server/sim/content/kits/ops/op-orchd2.js — 焰狐龙梓兰 (char_1048_orchd2) 自选 operator kit: 6★ 重射手 (狙击), an owned-6★
// pick of the tier-5 and tier-6 自选 slots; every skill, both talents, the trait and her module (ARC-X 梓兰特制箭靶) at every
// form. Kit contract and the 自选 rules: ../README.md ("How to add an operator (自选)").
//
// Forms (data/backups.json units.char_1048_orchd2): normal = E2 Lv1, skills at rank 4, no module; elite = E2 Lv60, rank 7,
// ARC-X 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 焰狐龙梓兰 (特性备注,
// the 备注 of both talents and all three skills, quoted where used); the official skeleton of char_1048_orchd2 (front .skel:
// the Skill_X animations and their OnAttack events) for the moments the skills act — PRTS 飞翔瞪射 备注 "三轮攻击的时间模式
// 均为跟随动画", 龙之箭 备注 (frame counts of the first / later casts). NB she came with a 联动寻访 (PRTS 获得方式; Monster
// Hunter); her team is 预备行动组 (reserve6), not one of build-data DIY_EXCLUDED_TEAMS, so the data offers her — reported.
// - Trait (重射手) "高精度的近距离射击": ranged physical arrows, 3-6, can hit air units (PRTS 分支特性信息 重射手 "可对空"),
// blocks 1, ground enemies target her except while airborne (S2). PRTS 特性备注: her normal attack is a 三连击 of 100 %
// ATK each, and every normal-attack damage is cut to 33.3 % after DEF / RES (hidden talent attack@damage_scale 0.333):
// profile hits 3 (the count is in no blackboard: NORMAL_HITS) and dmg.mul × damage_scale on her non-skill attack damage.
// - T1 强击瓶专家 "部署后首次开启技能时,接下来50次攻击的攻击力提升至115%": the first cast of each deployment gives
// power_attack_count stacks; each attack ROUND (备注: a normal attack's three hits, a skill volley, 飞翔瞪射's landing, one
// 龙之箭) spends one and all of that round's damage is ×power_attack_scale (an ATK-scale multiplier, 备注). A round spends
// it at its first hit, a 龙之箭 when it is shot [ASSUMED: the 备注 spends it "于弹道脱手前" — only a round whose arrows all
// fizzle differs]. The first cast also uses the longer Skill_X_Begin_First clip (备注): its waits below are the longer ones.
// - T2 翔虫机动 "再部署时间-15秒且不提高部署费用;部署至上次部署位置周围时,30秒内攻击力+15%…可以部署在近战位": the hidden
// talent's respawn_time (−15; ARC-X stage 3 −18) shortens her redeploy time; when she leaves the field a mark stays on her
// tile (备注) and a later deployment inside its range (bbStr ignore_build_type_target_range: x-1, ARC-X stage 3 x-2, from
// range_table) gives ATK +atk for atk_duration s — the mode's redeploy brings her back on that very tile, so every
// redeploy does. "不提高部署费用" and "可以部署在近战位": nothing in battle (the mode never raises a redeploy cost; the
// board places her).
// - Module ARC-X “梓兰特制箭靶”: "再部署时间减少" (attribute respawn_time −25, in the stats) + ATK / DEF; stage 3 upgrades T2.
// - S1 刚射 (MANUAL, charges 2 / 3, data DEFAULT): the cast's attack is 4 arrows of atk_scale_1 × ATK (the text's counts:
// "发射4支", "发射5支" — parsed); with a charge left it spends one more (备注: "消耗2次充能所需的技力") for 刚连射 1.167 s
// later (Skill_1_End_2: OnAttack at 0.667 / 1.833 s): 5 arrows of atk_scale_2 × ATK, each stun_prob to stun `stun` s;
// no target then ⇒ the skill stops and the charge comes back (备注). No normal attack until the end clip is over.
// "充能至最大层数时自动释放一次": the DEFAULT rule casts it as soon as she is about to attack — the 3 s operation cooldown
// never binds at max charges (a refill takes ≥ 10 s), so no extra rule.
// - S2 飞翔瞪射 (MANUAL, charges 2 / 3, data SKILL_RANGE on its 4-4): 起飞 (liftoff + blockFly, as 蒂比: no ground enemy
// blocked or attacking her) for 4.2 s, no normal attack (备注 "自身丢失全部视野"), three volleys at every target of the 4-4
// (air units too) of 3 / 4 / 5 arrows × attack@atk_scale_loop each, then she lands with attack@atk_scale_end × ATK on every
// enemy of the 1-3 (备注 "降落时伤害范围为1-3"; air units too [ASSUMED]). Timeline from the skeleton: volleys 0.167 s into
// each 0.833 s Skill_2_Loop after the 0.667 s Skill_2_Begin (first cast: Begin_First 1.5 s, the skill 0.833 s longer),
// the landing 0.233 s into Skill_2_End. "部署后立即释放一次": every deployment casts it at once, free (备注 "无视技力…不会消耗
// 技力").
// - S3 龙之箭 (MANUAL, charges 1 / 2, data DEFAULT): after 3 s (Skill_3_Begin 1.5 s + wait_duration; first cast
// Begin_First 2.167 s, 备注 ≈110 frames) and 0.1 s more (Skill_3_End OnAttack) a piercing arrow flies straight ahead from
// her at 10 tiles/s (备注), collision radius 0.5 (备注), until it leaves the field: every dist_interval tiles it deals
// atk_scale × ATK physical, then atk_scale_magic × ATK arts (备注 order) to every enemy around it (air units too
// [ASSUMED]), and it pushes each enemy it touches with 力度 `force` (中等力度) along its flight, once per
// knockback_duration s per enemy (备注). ATK is read when it is shot [ASSUMED]. Her range does not change (备注 "不影响
// 实际视野"). No normal attack until Skill_3_End is over (1.233 s).
// [ASSUMED] for the three skills: a stun / freeze does not cancel the scheduled volleys, bursts and arrow (no animation
// model); the wind-up of 刚射 before its first shot (Skill_1_Begin + 0.667 s) is not modelled, as no attack's wind-up is.
import { num, up, skillRec } from '../shared/tier1.js';
import { performAttack, acquireTargets } from '../../../ai.js';
import { absoluteRangeKeys } from '../../../targeting.js';
import { dirVec } from '../../../dir.js';
import { hasHp } from '../../../damage.js';
const S1 = 'skchr_orchd2_1';
const S2 = 'skchr_orchd2_2';
const S3 = 'skchr_orchd2_3';
/** PRTS 特性备注 "普通攻击为三连击": the hits of her normal attack (no blackboard key). */
export const NORMAL_HITS = 3;
/**
* The official skeleton of char_1048_orchd2 (front .skel), seconds: clip lengths and their OnAttack events.
* Skill_1: Begin 0.1 (First 1.0), End_1 1.5 (shot 0.667), End_2 2.667 (shots 0.667, 1.833); Skill_2: Begin 0.667 (First
* 1.5), three Loop clips 0.833 (shot 0.167), End 1.0 (landing 0.233); Skill_3: Begin 1.5 (First 2.167), End 1.233 (shot 0.1).
*/
export const CLIP = Object.freeze({
s1Shot: 0.667, s1BurstShot: 1.833, s1End1: 1.5, s1End2: 2.667,
s2Begin: 0.667, s2BeginFirst: 1.5, s2Loop: 0.833, s2LoopShot: 0.167, s2EndShot: 0.233,
s3Begin: 1.5, s3BeginFirst: 2.167, s3EndShot: 0.1, s3End: 1.233,
});
/** 龙之箭 (PRTS 备注): flight speed (tiles/s), collision radius (tiles), lifetime (s). */
export const ARROW = Object.freeze({ speed: 10, radius: 0.5, life: 30 });
/** 1-3 (range_table): 飞翔瞪射's landing area (PRTS 备注). */
const GRID_1_3 = Object.freeze([[1, 1], [0, 0], [0, 1], [-1, 1]]);
/** 翔虫机动's mark ranges (range_table): x-1 = |dr| + |dc| ≤ 2; x-2 = the 5 × 5 square without its corners. */
const MARK_RANGE = Object.freeze({
'x-1': (dr, dc) => Math.abs(dr) + Math.abs(dc) <= 2,
'x-2': (dr, dc) => Math.abs(dr) <= 2 && Math.abs(dc) <= 2 && !(Math.abs(dr) === 2 && Math.abs(dc) === 2),
});
const TAG_BURST = 'orchd2:burst';
const TAG_ARROW = 'orchd2:arrow';
const LOCK = 'orchd2:lock';
const LIFT = 'orchd2:liftoff';
const WIREBUG = 'talent:orchd2:wirebug';
const bbOf = (chess, id) => skillRec(chess, id)?.bb ?? {};
/** A talent of the record by a blackboard key it carries (index-independent: hidden talents included). */
const talentWith = (chess, key) => (chess?.talents ?? []).find((t) => t && t.bb && t.bb[key] != null) ?? null;
/** Numbers of a skill text ("发射4支…发射5支", "分别射出3、4、5支"), or `fallback`. */
function textCounts(desc, re, fallback) {
const m = re.exec(String(desc ?? ''));
if (!m) return fallback;
const out = m[1].split(/[、,,]/).map((s) => Number.parseInt(s, 10)).filter((n) => n > 0);
return out.length ? out : fallback;
}
/** No normal attack for `s` seconds (the rest of a skill clip). */
function lock(battle, unit, s) {
if (s > 0) battle.addBuff(unit, { key: LOCK, duration: s, flags: { disarm: true }, tags: ['skill'] });
}
export default {
char_1048_orchd2: (bb, chess) => {
const s1 = skillRec(chess, S1), s2 = skillRec(chess, S2), s3 = skillRec(chess, S3);
const b1 = bbOf(chess, S1), b2 = bbOf(chess, S2), b3 = bbOf(chess, S3);
const shots = [...String(s1?.desc ?? '').matchAll(/发射(\d+)支/g)].map((m) => Number(m[1]));
const arrows1 = shots[0] > 0 ? shots[0] : 4, arrows2 = shots[1] > 0 ? shots[1] : 5;
const volleys = textCounts(s2?.desc, /射出([\d、]+)支/, [3, 4, 5]);
const power = talentWith(chess, 'power_attack_count')?.bb ?? {};
const wire = talentWith(chess, 'atk_duration') ?? { bb: {}, bbStr: {} };
const respawn = num(talentWith(chess, 'respawn_time')?.bb?.respawn_time);
const normalCut = num(talentWith(chess, 'attack@damage_scale')?.bb?.['attack@damage_scale'], 1);
const markIn = MARK_RANGE[wire.bbStr?.ignore_build_type_target_range] ?? MARK_RANGE['x-1'];
/** The first cast of this deployment: T1's stacks and the longer first clip. */
const firstCast = (unit) => {
if (unit.mem.orchdCast) return false;
unit.mem.orchdCast = true;
unit.mem.orchdPower = Math.max(0, Math.floor(num(power.power_attack_count)));
return true;
};
/** T1: one stack for a round ⇒ its multiplier (1 without stacks). */
const takePower = (unit) => {
if (!(unit.mem.orchdPower > 0)) return 1;
unit.mem.orchdPower--;
return num(power.power_attack_scale, 1);
};
/** A skill attack profile: her own (targeting, projectile) with the skill's scale and hit count. */
const skillProfile = (unit, atkScale, hits, extra = {}) => ({ ...unit.profile, isSkill: true, atkScale, hits, tags: [], ...extra });
/** Targetable enemies of a facing-RIGHT grid around her (air units too: she can hit them). */
const inGrid = (battle, unit, grid) => battle.enemiesInKeys(absoluteRangeKeys(grid, unit.tileR, unit.tileC, unit.dir, 0), unit, { canHitFly: true });
/** The skill still runs this very activation (scheduled steps of a cast). */
const running = (unit, seq, act) => up(unit) && unit.deploySeq === seq && !!unit.skill?.active && unit.skill.activations === act;
/** 龙之箭: shot from her tile straight ahead (see the header). */
const shootArrow = (battle, unit) => {
const mul = takePower(unit);
const atk = unit.s.atk;
const [fr, fc] = dirVec(unit.dir);
const ux = fc, uy = fr; // x = column, y = row
const every = Math.max(0.05, num(b3.dist_interval, 0.25));
const maxDist = num(b3.max_dist, 99);
const force = num(b3.force, 1), cd = num(b3.knockback_duration, 1);
const phys = atk * num(b3.atk_scale) * mul, arts = atk * num(b3.atk_scale_magic) * mul;
const pushReady = new Map();
let x = unit.x, y = unit.y, acc = 0, flown = 0, age = 0;
battle.fx('dragonArrow', { x, y, id: unit.id, dx: ux, dy: uy, speed: ARROW.speed });
const strike = () => {
for (const e of battle.foesInRadius(x, y, ARROW.radius)) {
if (phys > 0) battle.dealDamage(unit, e, { amount: phys, type: 'phys', isSkill: true, tags: ['skill', TAG_ARROW] });
if (arts > 0 && e.alive) battle.dealDamage(unit, e, { amount: arts, type: 'arts', isSkill: true, tags: ['skill', TAG_ARROW] });
}
};
const h = battle.on('tick', ({ dt }) => {
const step = ARROW.speed * dt;
let moved = 0;
while (moved < step - 1e-9) {
const s = Math.min(step - moved, every - acc);
x += ux * s; y += uy * s; moved += s; acc += s; flown += s;
if (acc >= every - 1e-9) { acc = 0; strike(); }
}
// the collision (push) is checked once per frame (备注), each enemy at most once per knockback_duration
for (const e of battle.foesInRadius(x, y, ARROW.radius)) {
if ((pushReady.get(e) ?? -Infinity) > battle.time + 1e-9) continue;
pushReady.set(e, battle.time + cd);
battle.push(e, force, { from: { x, y }, dir: { x: ux, y: uy }, fixed: true });
}
age += dt;
if (age >= ARROW.life || flown >= maxDist || !battle.grid.inRect(Math.round(y), Math.round(x))) battle.off(h);
}, { owner: unit });
};
return {
trait: { hits: NORMAL_HITS },
skills: {
[S1]: {
kind: num(s1?.maxChargeTime, 1) > 1 ? 'charges' : 'instant',
attack: { atkScale: num(b1.atk_scale_1, 1), hits: arrows1 },
onStart({ battle, unit, skill }) {
firstCast(unit);
const burst = skill.charges >= 1;
if (burst) skill.charges -= 1; // "若还有充能则额外消耗1层施展刚连射"
lock(battle, unit, (burst ? CLIP.s1End2 : CLIP.s1End1) - CLIP.s1Shot);
battle.fx('quadShot', { x: unit.x, y: unit.y, id: unit.id });
if (!burst) return;
const seq = unit.deploySeq;
battle.after(CLIP.s1BurstShot - CLIP.s1Shot, () => {
if (!up(unit) || unit.deploySeq !== seq) return;
const prof = skillProfile(unit, num(b1.atk_scale_2, 1), arrows2, { tags: [TAG_BURST], maxTargets: 1, allInRange: false });
const targets = acquireTargets(battle, unit, prof).slice(0, 1);
if (!targets.length) { // "找不到目标,将立刻终止技能并返还与1次充能等额的技力"
unit.skill?.addCharge(1);
battle.removeBuff(unit, LOCK);
return;
}
performAttack(battle, unit, prof, targets);
}, { owner: unit });
},
},
[S2]: {
kind: 'duration',
attack: { noAttack: true },
onStart({ battle, unit, skill }) {
const lead = firstCast(unit) ? CLIP.s2BeginFirst : CLIP.s2Begin;
if (lead > CLIP.s2Begin) skill.extend(lead - CLIP.s2Begin);
battle.addBuff(unit, { key: LIFT, flags: { liftoff: true, blockFly: true }, tags: ['skill'] });
battle.releaseBlocked(unit);
battle.fx('takeoff', { x: unit.x, y: unit.y, id: unit.id });
const seq = unit.deploySeq, act = skill.activations;
const grid = s2?.rangeGrid ?? [];
volleys.forEach((n, i) => battle.after(lead + i * CLIP.s2Loop + CLIP.s2LoopShot, () => {
if (!running(unit, seq, act)) return;
const targets = inGrid(battle, unit, grid);
if (targets.length) performAttack(battle, unit, skillProfile(unit, num(b2['attack@atk_scale_loop'], 1), n, { allInRange: true }), targets);
}, { owner: unit }));
battle.after(lead + volleys.length * CLIP.s2Loop + CLIP.s2EndShot, () => {
if (!running(unit, seq, act)) return;
battle.removeBuff(unit, LIFT);
battle.releaseBlocked(unit);
const targets = inGrid(battle, unit, GRID_1_3);
battle.fx('aoe', { x: unit.x, y: unit.y, radius: 1, id: unit.id, skill: 'orchd2:landing' });
if (targets.length) performAttack(battle, unit, skillProfile(unit, num(b2['attack@atk_scale_end'], 1), 1, { allInRange: true, projectile: 'none', attack: 'melee' }), targets);
}, { owner: unit });
},
onEnd({ battle, unit }) {
if (unit.findBuff(LIFT)) { battle.removeBuff(unit, LIFT); battle.releaseBlocked(unit); }
},
},
[S3]: {
kind: num(s3?.maxChargeTime, 1) > 1 ? 'charges' : 'instant',
attack: { noAttack: true }, // the clip: the cast waits (pending) with no normal attack until it ends below
onStart({ battle, unit, skill }) {
const lead = (firstCast(unit) ? CLIP.s3BeginFirst : CLIP.s3Begin) + num(b3.wait_duration, 1.5);
const seq = unit.deploySeq, act = skill.activations;
battle.after(lead + CLIP.s3EndShot, () => { if (running(unit, seq, act)) shootArrow(battle, unit); }, { owner: unit });
battle.after(lead + CLIP.s3End, () => { if (running(unit, seq, act)) skill.end('clip'); }, { owner: unit });
},
},
},
talents: [
{ install(battle, unit) { // 强击瓶专家: a round's first hit spends one stack; every hit of that round ×scale
const rounds = new Map();
battle.on('hit', (ctx) => {
const d = ctx.dmg;
if (ctx.source !== unit || !ctx.target || ctx.target.side !== 'enemy' || !d.isAttack || !d.attackId || d.type === 'element') return;
let mul = rounds.get(d.attackId);
if (mul === undefined) {
mul = takePower(unit);
rounds.set(d.attackId, mul);
if (rounds.size > 16) rounds.delete(rounds.keys().next().value);
}
if (mul !== 1) d.amount *= mul;
}, { owner: unit });
} },
{ install(battle, unit) { // 翔虫机动: shorter redeploy; the mark on her last tile ⇒ ATK +atk for atk_duration s
if (respawn && !unit.mem.orchdRespawn) {
unit.mem.orchdRespawn = true;
unit.base.respawnTime = Math.max(0, unit.base.respawnTime + respawn);
}
const a = num(wire.bb?.atk), dur = num(wire.bb?.atk_duration);
battle.on('death', (ctx) => {
if (ctx.unit === unit) unit.mem.orchdMark = [unit.tileR, unit.tileC];
}, { owner: unit });
battle.on('deploy', (ctx) => {
if (ctx.unit !== unit) return;
const mark = unit.mem.orchdMark;
unit.mem.orchdMark = null; // "持续存在直至下次焰狐龙梓兰部署"
if (!mark || !(a > 0) || !(dur > 0) || !markIn(unit.tileR - mark[0], unit.tileC - mark[1])) return;
battle.addBuff(unit, { key: WIREBUG, duration: dur, mods: { atkPct: a }, visible: true, tags: ['talent'] });
battle.fx('buff', { x: unit.x, y: unit.y, id: unit.id, kind: 'wirebug' });
}, { owner: unit });
} },
],
install(battle, unit) {
// every deployment: T1 and the first-clip rule start over; S2 "部署后立即释放一次" (free, after the deployment
// is done — a 联防 SP carry is applied first)
battle.on('deploy', (ctx) => {
if (ctx.unit !== unit) return;
unit.mem.orchdCast = false;
unit.mem.orchdPower = 0;
if (unit.skill?.id !== S2) return;
const seq = unit.deploySeq;
battle.after(0, () => {
if (up(unit) && unit.deploySeq === seq && unit.skill && !unit.skill.active) unit.skill.activate('deploy', { free: true });
}, { owner: unit });
}, { owner: unit, priority: 10 });
// 特性备注: every normal-attack damage ×damage_scale after DEF / RES
if (normalCut !== 1) {
battle.on('hit', (ctx) => {
const d = ctx.dmg;
if (ctx.source === unit && d.isAttack && !d.isSkill && d.type !== 'element') d.mul *= normalCut;
}, { owner: unit });
}
// 刚连射: every arrow stun_prob to stun the target hit
if (unit.skill?.id === S1) {
const p = num(b1.stun_prob), s = num(b1.stun);
battle.on('damaged', (ctx) => {
if (ctx.source !== unit || !ctx.dmg?.tags?.includes(TAG_BURST) || !hasHp(ctx.target) || ctx.target.side !== 'enemy') return;
if (p > 0 && s > 0 && battle.rng.chance(p)) battle.applyStatus(ctx.target, 'stun', { duration: s, source: unit });
}, { owner: unit });
}
},
};
},
};
+163
View File
@@ -0,0 +1,163 @@
// 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 SKILL_RANGE on 3-2 — build-data reads "攻击范围
// 改为前方3格" as a 技能范围 of its own (its ATTACK_RANGE_CHANGE pattern has no "改为"); the strategy table's reading would
// be the basic strategy (DEFAULT) — reported as an open question, the data rule is kept.
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 });
},
};
},
};