feedback5 WH: the first 自选 operator kit, 推进之王 (kits/ops/op-siege.js, registered in OPERATOR_KIT_FILES) — S1 冲锋号令·γ型 +12 DP at full SP, S2 跃空锤 next attack 250 / 280 % ATK on every ground enemy of her x-5 + 3 DP (2 / 3 charges), S3 碎颅击 interval +1.0 s with 290 / 320 % ATK and 40 % stun 0.8 / 1.1 s; T1 万兽之王 field 【先锋】 ATK / DEF +8 % (SOL-X stage 3: her own +8 % more), T2 粉碎 +1 SP per enemy falling on her x-5 (flyers, under 阻回; SOL-Y stage 3: +3 and +1 to another 先锋); SOL-X trait ATK / DEF +8 % while blocking; SOL-Y's 首次部署 −4 has no battle effect; tests in test/content/op_siege.test.js over every form (tiers 5 / 6, normal / elite × none / SOL-X / SOL-Y)

No golden scenario moved (none fields a 自选 piece).
This commit is contained in:
sganggs committed 2026-10-05 19:54:32 +08:00
1 parent 2b72d4deb0
commit ee74401901
3 files changed
+363 -1

No files matched your search

+1 -1
View File
@@ -78,7 +78,7 @@ 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([]);
export const OPERATOR_KIT_FILES = Object.freeze(['op-siege.js']);
/**
* The 预备干员 whose every skill the generic kit covers exactly (冲锋号令, 攻击力 / 防御力 / 治疗强化, 战术咏唱, 一击即退 —
+127
View File
@@ -0,0 +1,127 @@
// server/sim/content/kits/ops/op-siege.js — 推进之王 (char_112_siege) 自选 operator kit: 6★ 尖兵 (先锋), an owned-6★ pick
// of the tier-5 and tier-6 自选 slots; every skill, both talents, the trait and both modules at every form.
// Kit contract and the 自选 rules: ../README.md ("How to add an operator (自选)").
//
// Forms (data/backups.json units.char_112_siege, the DIY slot statuses): 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). Potential 0 [ASSUMED: no account].
// Sources: character_table / skill_table / battle_equip_table (zh_CN, as built into backups.json) and PRTS 推进之王
// (talent 修正 "在场时"; 粉碎 备注 "本天赋无视自身的阻回,可响应飞行单位的死亡"; S2 备注 "技能固定动画不受攻击速度影响";
// S3 "攻击间隔增大(+1.0)").
// - Trait (尖兵) "能够阻挡两个敌人": the profession default (block 2). Module SOL-X “糖果盒” overrides it with
// "阻挡敌人时攻击力和防御力各+8%" (trait bb atk / def): +atk / +def while she blocks at least one enemy.
// - T1 万兽之王 "在场时,所有【先锋】职业干员的攻击力和防御力各+8%": an aura on every 【先锋】 operator of the field (her
// included) while she is deployed (installAura: the kits' convention for field-wide talents — teammates' pioneers in a
// shared field too; several sources keep the strongest). SOL-X stage 2+ adds "自身攻击力和防御力额外+N%" (the module
// talent change's bb, +8 % at stage 3): a self buff on top; the aura value stays the base talent's (talentsBase).
// - T2 粉碎 "周围四格内有敌人倒下时获得1点技力" (talent range x-5: her tile and the four next to it): +sp SP when an enemy
// dies there — flying enemies too, and under her own 阻回 (PRTS 备注) — never while a timed skill runs (AK: no SP during
// a skill). SOL-Y stage 2+ raises it to sp (3 at stage 3) "场上随机另一名【先锋】职业干员获得1点技力": the hidden module
// talent's sp to one other 【先锋】 operator of the field, drawn uniformly [ASSUMED: any deployed other pioneer, a draw that
// cannot take SP wastes it].
// - Module SOL-Y “初诺” "首次部署时部署费用-4" (hidden runtime_cost −4): the first deployment of a battle costs nothing in
// this mode ⇒ no effect (as 德克萨斯's 战术快递 elite note); its HP / ATK are in the stats.
// - S1 冲锋号令·γ型 (AUTO, no target): +cost DP at once; an AUTO skill acting on nobody fires as soon as its SP is full
// (`trigger: 'SP_FULL'`, as 德克萨斯's kit casts the same skill — the owner's AUTO rule, kits/README.md checklist 5).
// - S2 跃空锤 (AUTO, 可充能 2 / 3 次): the next attack hits every enemy around her — the skill range x-5 — for atk_scale × ATK
// physical and gives cost DP; the data's DEFAULT trigger (an AUTO "next attack" waits for her attack). The AoE is the
// attack itself, so it reaches the ground enemies a melee attack can [ASSUMED: no source says it reaches air]. The fixed
// skill animation (PRTS 备注) does not change the sim's attack timing.
// - S3 碎颅击 (MANUAL, 18 / 21 s): attack interval +base_attack_time s (a flat +1.0 on her 1.05 s), each attack at
// attack@atk_scale × ATK with attack@buff_prob to stun the target attack@stun s.
import { num, talentBb, talentGrid, moduleBb, traitBb, moduleOn, skillRec, statBuff, toggleBuff, installAura, batMod,
up, skillBusy, giveSp } from '../shared/tier1.js';
import { absoluteRangeKeys } from '../../../targeting.js';
import { bodyInKeys } from '../../../body.js';
const S1 = 'skcom_charge_cost[3]';
const S2 = 'skchr_siege_2';
const S3 = 'skchr_siege_3';
/** The four tiles around her and her own (talent / skill range x-5) when the data carries no grid. */
const X5 = Object.freeze([[1, 0], [0, -1], [0, 0], [0, 1], [-1, 0]]);
const bbOf = (chess, id) => skillRec(chess, id)?.bb ?? {};
/** A 【先锋】 operator (summons are never 【先锋】 operators). */
const isPioneer = (a) => !!a && a.kind === 'op' && a.def?.profession === 'PIONEER';
/** 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;
}
export default {
char_112_siege: (bb, chess) => {
const base0 = talentBb({ talents: chess?.talentsBase ?? chess?.talents }, 0); // 万兽之王 without the module change
const mod = activeModule(chess);
const self0 = mod?.talentChanges?.find((t) => t && t.talentIndex === 0)?.bb ?? {}; // SOL-X stage 2+: 自身额外
const t1 = talentBb(chess, 1);
const grid1 = talentGrid(chess, 1) ?? X5;
const hidden = moduleBb(chess); // SOL-Y: runtime_cost (no effect), sp to another pioneer (stage 2+)
const tb = traitBb(chess);
const b1 = bbOf(chess, S1), b2 = bbOf(chess, S2), b3 = bbOf(chess, S3);
const s2 = skillRec(chess, S2);
return {
skills: {
[S1]: {
kind: 'instant', trigger: 'SP_FULL',
onStart({ battle, unit }) {
const n = num(b1.cost);
battle.addDp(unit.ownerId, n);
battle.fx('dp', { x: unit.x, y: unit.y, n, id: unit.id });
},
},
[S2]: {
kind: num(s2?.maxChargeTime, 1) > 1 ? 'charges' : 'instant',
targeting: { rangeGrid: s2?.rangeGrid ?? X5, allInRange: true },
attack: { atkScale: num(b2.atk_scale, 1) },
onStart({ battle, unit }) {
const n = num(b2.cost);
battle.addDp(unit.ownerId, n);
battle.fx('dp', { x: unit.x, y: unit.y, n, id: unit.id });
battle.fx('aoe', { x: unit.x, y: unit.y, radius: 1, id: unit.id, skill: 'siege:hammer' });
},
},
[S3]: {
kind: 'duration',
mods: { batPct: batMod(b3.base_attack_time, chess) },
attack: {
atkScale: num(b3['attack@atk_scale'], 1),
onHit({ battle, unit, target }) {
if (target && target.alive && target.side === 'enemy' && battle.rng.chance(num(b3['attack@buff_prob']))) {
battle.applyStatus(target, 'stun', { duration: num(b3['attack@stun']), source: unit });
}
},
},
},
},
talents: [
{ install(battle, unit) { // 万兽之王: the field's 【先锋】 ATK / DEF +atk / +def; SOL-X stage 2+: her own extra
const a = num(base0.atk), d = num(base0.def);
if (a || d) installAura(battle, unit, { key: 'talent:siege:pioneers', value: a, select: isPioneer, mods: { atkPct: a, defPct: d } });
statBuff(battle, unit, 'talent:siege:self', { atkPct: num(self0.atk), defPct: num(self0.def) });
} },
{ install(battle, unit) { // 粉碎: an enemy falls on her x-5 ⇒ +sp SP (阻回 ignored); SOL-Y: +1 SP to another pioneer
const sp = num(t1.sp), other = num(hidden.sp);
if (!(sp > 0)) return;
battle.on('death', (ctx) => {
const e = ctx.unit;
if (e.side !== 'enemy' || ctx.reason !== 'killed' || !up(unit) || !unit.skill) return;
if (!bodyInKeys(e, new Set(absoluteRangeKeys(grid1, unit.tileR, unit.tileC, unit.dir, 0)))) return;
// "无视自身的阻回": the 'init' reason passes 阻回; a running timed skill still takes no SP
if (!skillBusy(unit)) unit.skill.gainSp(sp, 'init');
if (other > 0) {
const mates = battle.alliesFor(unit).filter((a) => a !== unit && isPioneer(a));
if (mates.length) giveSp(battle.rng.pick(mates), other);
}
}, { owner: unit });
} },
],
install(battle, unit) {
// SOL-X “糖果盒”: 阻挡敌人时攻击力和防御力各+8%
const ma = num(tb.atk), md = num(tb.def);
if (ma || md) toggleBuff(battle, unit, 'trait:siege:block', () => unit.blocking.length > 0, { atkPct: ma, defPct: md });
},
};
},
};