Files
ark-stronghold-protocol/server/sim/content/kits/ops/op-shwaz.js
T
sganggs 806a047717 feedback6 R21P: every operator fights at full potential (潜能 6) — the owner's decision of 2026-10-07 (GitHub #252, PR #255; no host slider): tools/build-data.mjs OPERATOR_POTENTIAL adds every character_table potentialRanks attribute step (部署费用 / 再部署时间 / ATK / HP / DEF / RES / ASPD, all ADDITION, before rounding) to every chess (normal and elite), 补位 and 自选 unit form and picks talent / trait / module-talent candidates up to requiredPotentialRank 5 (「天赋效果增强」); a summon picks its talent candidates at its owner's potential (their requiredPotentialRank mirrors the owner's — PRTS 凯尔希: Mon3tr 3.5 s / 1400) and takes none of the owner's attribute steps; the 原型干员 stand-ins have no potential ranks (unchanged). 刺玫 413 / 17 → 435 / 15 as in the tournament video, 砾 18 → 16 s, 至简 80 → 70 s. data/chess.json (256 records), backups.json (the 71 owned picks, 8 summons), tokens.json (5 summons) and the data translations en / ja / ko / zh-TW rebuilt — only potential-driven fields (JSON-compared); the 干员调配 局内数值 and detail cards read the records. op-wang.js reads her stock cap 「最多拥有8(+1)枚」 through the potential step (the pattern fell back to 7: she sat at the cap from the start). Docs: DATA §2 / §18 / tokens, PLAYING (detail card), research 03, kits README, shared/diy.js / standIn.js, the 71 自选 kit headers and the kit comments that quoted potential-0 numbers. Tests: test/full-potential.test.js (new, fails on the parent); data.test.js re-derivation adds the potential steps (+ respawnTime); every test that hard-coded a potential-0 number carries the full-potential one (from the data and the test's own formula; no check loosened); golden: moves almost everywhere — roster 49/49, bonds 46/46, fields 22/22, matches 18/18, diy 133/138 (each battle parts on a stat: roster-001 / roster-020 at tick 1 on 角峰's HP (生命上限+250), bond-arcaneShip-high at 4.73 s and diy-001 at 3.70 s on an enemy's HP from a higher ATK; diy-110 / 111 also by 望's cap; the matches through those battles); unchanged: standins 10/10 and diy-135…138 (no potential ranks), diy-012 (twelve 夜莺 never hit, never redeployed)
2026-10-07 18:35:39 +08:00

165 lines
10 KiB
JavaScript
Raw Blame History

This file contains ambiguous Unicode characters
This file contains Unicode characters that might be confused with other characters. If you think that this is intentional, you can safely ignore this warning. Use the Escape button to reveal them.
// 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. Full potential (the owner's
// decision of 2026-10-07). 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%" (full potential: +10%): 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%" (full potential:
// +15%): 携带 = 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 +15 % 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: 66 → 41 s at full potential) + 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 });
},
};
},
};