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.
171 lines
12 KiB
JavaScript
171 lines
12 KiB
JavaScript
// 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 });
|
||
},
|
||
};
|
||
},
|
||
};
|