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.
This commit is contained in:
sganggs committed 2026-10-06 01:21:14 +08:00
1 parent fef4ea3d4b
commit 57d4d8476a
8 files changed
+2110 -104

No files matched your search

+170
View File
@@ -0,0 +1,170 @@
// 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 });
},
};
},
};