Files
ark-stronghold-protocol/server/sim/content/kits/ops/op-nian.js
T
sganggs 57d4d8476a feedback5 O14: 自选 operator kits 年, 斩业星熊 and 珊比 — every skill, talent, trait and module (stage 1 / 3) at every form
Kits server/sim/content/kits/ops/op-nian.js, op-hsgma2.js and op-thumpy.js, appended to OPERATOR_KIT_FILES; tests
test/content/op_nian.test.js (10), op_hsgma2.test.js (11), op_thumpy.test.js (12). Kit-local code only: no engine or other
shared file changed.

- 年: S1 锡灼 (DEF / ATK, arts attacks), S2 铜印 (no attack, DEF, block +1, counter + 3 s silence on every enemy attack),
  S3 铁御 (ATK; the other allied operators of her x-2: DEF, block +1, 抵抗); 积甲成山 (编入队伍时: her team's 重装 max HP
  +16 / 21 %), 干明可鉴 (3 护盾 layers per deployment; PRO-X Lv3: ATK / DEF +7 % and +3 SP per broken layer); PRO-X DEF +20 %
  while blocking, PRO-Y block 4 and at Lv3 max HP / healing received +4 % per 重装 on the field (≤ 3).
- 斩业星熊: physical attacks with no skill on, arts while one runs (the data's dmgType is the skill-on one); S1 恶业苦果
  (toggle, fired at full SP: an AUTO self buff), S2 无始无明 (the throw, then the 365° shield: PRTS's 1.2 tile start, 90°/s,
  −80 % near a target, 0.5 s pulses with 停顿 and the heal through 禁疗), S3 地狱变相 (x-2, HP / ATK, 2 hits on ≤ 2, air too;
  a 主动关闭 keeps it 11 s as the 临死模式 — 4 hits, 阻回 / 沉默 / 不死, allies of her range 不死 with the excess as her 流失, then
  a withdrawal; no strategy of the mode closes it since 下半); 业火 我执 (negative HP to 200 %, 禁疗, the 5 %/s
  生命回复速度 after 5 s quiet), 鬼之架势 坚忍 (AST-X Lv3: full at 50 %, +15 % ATK); AST-X extra / returned 10 % ATK arts.
- 珊比: S1 (every blocked enemy struck and pushed radially with 小力), S2 (PRTS's glue order on her tile + 4, 0.7 s pulses of
  停顿 then the hit, the 15 % 侵蚀 rider on other operators' physical damage), S3 (block +2, the 4-tile conveyor at 0.8 × 20 /
  30 tile/s for unblocked ground enemies of 重量 ≤ 4, 1 s damage on her tile + 4 ahead + blocked, −30 DEF on a 侵蚀 burst
  there); 探险理论 (元素损伤 taken ×0.85; 10 % ATK 侵蚀 per physical hit, or the burst −1 s), 坚硬脚板 (marks, DEF / ATK stack
  and one growing barrier ≤ 300 % max HP); PRP-X ×1.15 element damage while blocking.

Golden: only test/golden/diy.json moved — diy-049 / 050 now field 年 (tiers 5 / 6), diy-051 斩业星熊 tier 5, diy-052 斩业星熊
tier 6 and 珊比's tier-5 normal entries, diy-053 the rest of 珊比 (every form × module × skill); the prototypes and reserves
moved from diy-049 … 052 to diy-054 … 057 (same packing). diy-001 … 048 and the roster, bonds, fields, matches and standins
families are unchanged.
2026-10-06 01:21:14 +08:00

171 lines
12 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-nian.js — 年 (char_2014_nian) 自选 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_2014_nian, 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), gamedata_const
// ba.shield 护盾 ("每层护盾可以抵挡一次伤害") and ba.buffres 抵抗, PRTS 年 (积甲成山 备注 "根据重装干员数量获得的治疗效果加成与
// 生命上限加成叠加时以加法叠加"; 干明可鉴 备注 "攻击力与防御力增益持续时间无限"; 铜印 备注 "沉默效果与反伤效果会直接选中伤害来源,
// 但仅对阵营为敌方的来源产生效果"; 铁御 技能范围 x-2).
// - Trait (铁卫) "能够阻挡三个敌人": the profession default (block 3 from the data stats), ground-only physical melee on her
// 1-1. Module PRO-X 混沌阵列 adds "阻挡敌人时防御力+20%" (trait bb def): DEF + while she blocks at least one enemy.
// Module PRO-Y “请勿叩门” "能够阻挡四个敌人" (attr block_cnt +1, already in the stats). From stage 2 its trait part carries
// the blackboard of 积甲成山's module text (battle_equip_table: TRAIT part max_hp / heal_scale / heal_scale_addition /
// heal_scale_max_value / max_stack_cnt, the talent part only the team bonus) "场上每有一名重装干员,自身受到治疗效果和生命上限
// 额外提升4%,最多叠加3层": n = the 重装 operators on the field, her included [ASSUMED: "每有一名重装干员" counts every 重装 on
// the field] and a teammate's in a shared field (alliesFor), at most max_stack_cnt — max HP +max_hp × n (直接乘算 with her
// other "+%" bonuses) and healing received ×(heal_scale + heal_scale_addition × n), at most heal_scale_max_value (the
// PRTS note's 加法叠加: the stacks add up, 104 / 108 / 112 %). Re-counted every tick.
// - T1 积甲成山 "编入队伍时,所有【重装】职业干员的生命上限+16%": every 重装 operator of her player's team (her included) max HP
// +max_hp for the whole battle, deployed or not (the kits' 编入队伍时 convention: 斯卡蒂 深海掠食者, 早露 学生楷模). PRO-Y
// stage 3: +21 % (the module talent change).
// - T2 干明可鉴 "部署后立即获得3层护盾": `times` 护盾 layers at every deployment (a hit-negating barrier: buff shieldHits — each
// layer negates one damage instance). PRO-X stage 2+ (talent change atk / def / sp / max_stack_cnt) "每层护盾破裂时,攻击力
// +7%,防御力+7%且获得3点技力": each broken layer one more stack of ATK / DEF + (at most max_stack_cnt; "持续时间无限": until she
// leaves the field [ASSUMED: a deployment's layers give a deployment's stacks]) and +sp SP (none while a skill runs: AK's
// no-SP-during-a-skill rule, tier1 giveSp).
// - S1 锡灼 (MANUAL, time SP; data DEFAULT — the owner's 重装 cast-in-range exception, rawRule TAKE_DAMAGE): `duration` s of
// DEF +def and ATK +atk; normal attacks deal arts damage.
// - S2 铜印 (MANUAL; data DEFAULT): `duration` s — stops attacking; DEF +def, block +block_cnt; every enemy attack she takes
// (each damage instance of one, as 星熊 荆棘 / 泡泡) deals atk_scale × ATK arts to the attacker and silences it `silence`
// s ("失去特殊能力"): a direct pick of the damage source (ignoreSelect), enemies only (PRTS 备注), even when a 护盾 layer
// negated the hit.
// - S3 铁御 (MANUAL; data DEFAULT — its x-2 is a 技能范围 for the allies, no attack-range change): `duration` s of ATK
// +[self].atk (block +[self].block_cnt = 0); every other allied operator on her x-2 ("周围其他友方干员": summons are no 干员
// [ASSUMED]) gets DEF +[ally].def, block +[ally].block_cnt and 抵抗 (the engine `resist` status, value
// −one_minus_status_resistance), refreshed every AURA s while the skill runs; two 年 of a shared field keep the stronger
// (installAura's rule). The DEF / block buff ends with the skill, the 抵抗 within AURA_DUR.
import { num, talentBb, traitBb, skillRec, statBuff, toggleBuff, up, giveSp } from '../shared/tier1.js';
import { absoluteRangeKeys } from '../../../targeting.js';
import { COLS } from '../../../constants.js';
const S1 = 'skchr_nian_1';
const S2 = 'skchr_nian_2';
const S3 = 'skchr_nian_3';
/** 铁御's aura: refresh period and buff lifetime (s) — the tier-4 kits' AURA / AURA_DUR. */
const AURA = 0.2;
const AURA_DUR = 0.25;
/** 铁御's 技能范围 x-2 when the data carries no grid. */
const X2 = Object.freeze([[2, -1], [2, 0], [2, 1], [1, -2], [1, -1], [1, 0], [1, 1], [1, 2], [0, -2], [0, -1], [0, 0], [0, 1],
[0, 2], [-1, -2], [-1, -1], [-1, 0], [-1, 1], [-1, 2], [-2, -1], [-2, 0], [-2, 1]]);
const SHIELD = 'talent:nian:shield';
const BREAK = 'talent:nian:shieldBreak';
const GUARD = 'skill:nian:ironGuard';
const FORT = 'trait:nian:fortify';
const bbOf = (chess, id) => skillRec(chess, id)?.bb ?? {};
/** A 【重装】 operator (summons are never 重装 operators). */
const isTank = (a) => !!a && a.kind === 'op' && a.def?.profession === 'TANK';
/** The mods of `o` without its zero entries. */
const nz = (o) => Object.fromEntries(Object.entries(o).filter(([, v]) => Number.isFinite(v) && v !== 0));
export default {
char_2014_nian: (bb, chess) => {
const t0 = talentBb(chess, 0), t1 = talentBb(chess, 1);
const tb = traitBb(chess);
const b1 = bbOf(chess, S1), b2 = bbOf(chess, S2), b3 = bbOf(chess, S3);
const guardGrid = skillRec(chess, S3)?.rangeGrid ?? X2;
const allyDef = num(b3['nian_s_3[ally].def']), allyBlock = num(b3['nian_s_3[ally].block_cnt']);
const resist = Math.min(0.95, Math.max(0, -num(b3.one_minus_status_resistance)));
/** 铁御: the other allied operators on her x-2 get the DEF / block buff and 抵抗 for AURA_DUR. */
const ironGuard = (battle, unit) => {
const keys = new Set(absoluteRangeKeys(guardGrid, unit.tileR, unit.tileC, unit.dir, 0));
for (const a of battle.alliesFor(unit)) {
if (a === unit || a.kind !== 'op' || a.hidden || !keys.has(a.tileR * COLS + a.tileC)) continue;
const cur = a.findBuff(GUARD);
if (!(cur && cur.source !== unit && (cur.data?.v ?? 0) > allyDef && cur.timeLeft > 0.05)) {
battle.addBuff(a, { key: GUARD, duration: AURA_DUR, mods: nz({ defPct: allyDef, blockCnt: allyBlock }), source: unit, data: { v: allyDef }, tags: ['skill', 'aura'] });
}
if (resist > 0) battle.applyStatus(a, 'resist', { duration: AURA_DUR, value: resist, source: unit });
}
};
return {
skills: {
[S1]: { kind: 'duration', mods: nz({ defPct: num(b1.def), atkPct: num(b1.atk) }), attack: { dmgType: 'arts' } },
[S2]: { kind: 'duration', mods: nz({ defPct: num(b2.def), blockCnt: num(b2.block_cnt) }), attack: { noAttack: true } },
[S3]: {
kind: 'duration',
mods: nz({ atkPct: num(b3['nian_s_3[self].atk']), blockCnt: num(b3['nian_s_3[self].block_cnt']) }),
onStart({ battle, unit }) {
unit.mem.nianGuardAcc = 0;
ironGuard(battle, unit);
battle.fx('buff', { x: unit.x, y: unit.y, id: unit.id, kind: 'nian:ironGuard' });
},
onTick({ battle, unit, dt }) {
unit.mem.nianGuardAcc = (unit.mem.nianGuardAcc ?? 0) + dt;
if (unit.mem.nianGuardAcc + 1e-9 < AURA) return;
unit.mem.nianGuardAcc -= AURA;
ironGuard(battle, unit);
},
onEnd({ battle, unit }) {
for (const a of battle.allyUnits) {
const b = a.findBuff(GUARD);
if (b && b.source === unit) battle.removeBuff(a, b);
}
},
},
},
talents: [
{ install(battle, unit) { // 积甲成山: every 重装 operator of her team max HP +max_hp (PRO-Y stage 3: the module's value)
const hp = num(t0.max_hp);
if (!(hp > 0)) return;
for (const a of battle.allyUnits) if (isTank(a) && a.ownerId === unit.ownerId) statBuff(battle, a, 'talent:nian:armor', { hpPct: hp });
} },
{ install(battle, unit) { // 干明可鉴: `times` 护盾 layers at every deployment; PRO-X stage 2+: ATK / DEF stack + SP per broken layer
const times = Math.max(0, Math.floor(num(t1.times)));
if (!(times > 0)) return;
const atk = num(t1.atk), def = num(t1.def), sp = num(t1.sp), cap = Math.max(1, Math.floor(num(t1.max_stack_cnt, 1)));
battle.on('deploy', (c) => {
if (c.unit !== unit) return;
unit.mem.nianLayers = times;
battle.addBuff(unit, { key: SHIELD, shieldHits: times, visible: true, tags: ['talent'] });
battle.fx('shield', { x: unit.x, y: unit.y, id: unit.id, n: times });
}, { owner: unit });
if (!(atk || def || sp)) return;
battle.on('damaged', (c) => {
if (c.target !== unit || !unit.alive) return;
const left = unit.findBuff(SHIELD)?.shieldHits ?? 0;
const broken = (unit.mem.nianLayers ?? 0) - left;
if (!(broken > 0)) return;
unit.mem.nianLayers = left;
for (let i = 0; i < broken; i++) {
if (atk || def) battle.addBuff(unit, { key: BREAK, refresh: 'stack', stacks: 1, maxStacks: cap, mods: nz({ atkPct: atk, defPct: def }), visible: true, tags: ['talent'] });
giveSp(unit, sp);
}
battle.fx('shieldBreak', { x: unit.x, y: unit.y, id: unit.id });
}, { owner: unit });
} },
],
install(battle, unit) {
// PRO-X 混沌阵列: 阻挡敌人时防御力+20%
const blockDef = num(tb.def);
if (blockDef) toggleBuff(battle, unit, 'trait:nian:block', () => unit.blocking.length > 0, { defPct: blockDef });
// PRO-Y stage 2+: per 重装 operator on the field max HP +max_hp and healing received +heal_scale_addition (≤ the cap)
const perHp = num(tb.max_hp), healAdd = num(tb.heal_scale_addition), stackCap = Math.floor(num(tb.max_stack_cnt));
if ((perHp > 0 || healAdd > 0) && stackCap > 0) {
const healBase = num(tb.heal_scale, 1), healMax = num(tb.heal_scale_max_value, Infinity);
battle.on('tick', () => {
const cur = unit.findBuff(FORT);
const n = up(unit) ? Math.min(stackCap, battle.alliesFor(unit).filter(isTank).length) : 0;
if ((cur?.data?.n ?? 0) === n) return;
if (!(n > 0)) { battle.removeBuff(unit, FORT); return; }
battle.addBuff(unit, { key: FORT, mods: nz({ hpPct: perHp * n, healingTakenMul: Math.min(healMax, healBase + healAdd * n) }), data: { n }, tags: ['trait'] });
}, { owner: unit });
}
// S2 铜印: every enemy attack she takes ⇒ atk_scale × ATK arts back to the attacker + silence `silence` s
const scale = num(b2.atk_scale), sil = num(b2.silence);
battle.on('damaged', (c) => {
const src = c.source;
if (c.target !== unit || !unit.alive || unit.skill?.id !== S2 || !unit.skill.active) return;
if (!src || src.side !== 'enemy' || !src.alive || !c.dmg?.isAttack) return;
if (scale > 0) battle.dealDamage(unit, src, { amount: unit.s.atk * scale, type: 'arts', canDodge: false, isSkill: true, ignoreSelect: true, tags: ['skill', 'counter'] });
if (sil > 0 && src.alive) battle.applyStatus(src, 'silence', { duration: sil, source: unit });
battle.fx('counter', { x: src.x, y: src.y, id: unit.id });
}, { owner: unit });
},
};
},
};