163 lines
10 KiB
JavaScript
163 lines
10 KiB
JavaScript
// 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 });
|
||
},
|
||
};
|
||
},
|
||
};
|