Files
ark-stronghold-protocol/server/sim/content/kits/ops/op-bgsnow.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

231 lines
14 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-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. Full potential (the owner's
// decision of 2026-10-07). 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 (full potential: ×0.8); 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%" (full potential:
// 20% for 5 s, 25%): 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 % (full potential: 30 % /
// 35 %) 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';
import { startCountdown } from '../../tokens.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 — a countdown summon (content/tokens.js startCountdown): 无敌,
// 禁疗 [ASSUMED], its bar = the life left
battle.on('deploy', (ctx) => {
if (ctx.unit !== unit || !(life > 0)) return;
startCountdown(battle, unit, life);
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 });
}
},
};
},
};