Files
ark-stronghold-protocol/server/sim/content/kits/ops/op-huang.js
T
sganggs 29fa988604 feedback5 O5: 自选 operator kits for 左乐, 重岳 and 煌 — every skill at ranks 4 / 7, both talents, the trait and both modules at stages 1 / 3, from the data, PRTS 备注, the client's buff templates and the battle skeletons' clip events
- 左乐 (op-zuole.js): S1 next attack + 1 / 2 extra strikes below 80 / 50 % HP (each a damage instance and a trait heal); S2 流失 50 % current HP (never below 1), a barrier of scale × max HP stacking to 2×, ATK / block +1 / every blocked enemy, −200 per second once over; S3 7 slashes on ≤ 3 enemies of 3-2, the last ×2 + stun, the trait's heal turned into a barrier (70 × 2.5 / 3 per damage instance, ≤ 2× max HP, 15 s); 坚忍 ASPD / SP recovery; 守正自明 per damage instance (SBL-Y Lv3 ×1.2 on a success); SBL-X 庇护 (behind the barrier once 行险 resets it — PRTS S2 备注 — through the hpDamage hook); SBL-Y revive at 30 % once per deployment.
- 重岳 (op-chyue.js): 止戈 marks (normal-attack instances, ×1.65 / ×1.75 on all his damage); 万象为宾 SP at each skill / segment end, 阻回 ignored (FGT-X Lv3 4, else 1); S1 charges with the full-stack burst of 3; S2 x-4 ≤ 4 enemies (air too) + 浮空 2 s on the marked, the second segment 0.5 s later (skeleton events) on every 浮空 enemy; S3 strike + 0.8 splash, the 5th cast transforms him (x-6, double hits, +1 SP per attack, self-cast, the strike twice); FGT-X dodge, FGT-Y ASPD.
- 煌 (op-huang.js): CEN-X ×1.1 vs blocked enemies on every damage; CEN-Y −20 % physical above 50 %; 紧急除颤 不死 + heal + the 50 % HP floor through the hpDamage hook (CEN-Y Lv3: 50 % then 25 %, 8 s, DEF −150 above 50 %); 严酷训练 抵抗 after 15 s (CEN-X Lv3 ATK +6 % / ASPD +12 after 30 / 45 s); S1 next attack; S2 SP_FULL toggle with 2-2; S3 a 10 s 维持技能状态: cuts each second for 8 s, ramp to full at 8.95 s, at 9 s 流失 25 % max HP and the 3-6 burst (air too).
- kits/index.js OPERATOR_KIT_FILES lists the three files; test/content/diy.test.js's "owned 6★ without a kit" case takes the first kitless operator of diy.ownedPool (it named 煌).
- Golden: only test/golden/diy.json moved — the diy family packs the new kits: diy-003 / 004 field 左乐, diy-005 / 006 重岳, diy-007 / 008 煌 (tiers 5 / 6, every form × module × skill, no battle error); the prototypes and reserves moved from diy-003 … 006 to diy-009 … 012 (new ids, same packing); diy-001 / 002 (推进之王) and every other family unchanged.
- Tests: test/content/op_zuole.test.js (10), op_chyue.test.js (8), op_huang.test.js (10).
2026-10-06 00:13:05 +08:00

210 lines
15 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-huang.js — 煌 (char_017_huang) 自选 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_017_huang, 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); PRTS 煌 (talent and
// skill 备注, the CEN-X note); the client's buff templates (buff_template_data `huang_s_3`, `huang_t_1[heal]` / `[lock]`,
// `huang_e_003[lock]` / `[heal]`, `huang_t_2…`); Arknights Terra Wiki "Blaze" (the S3 notes).
// - Trait (强攻手) "同时攻击阻挡的所有敌人": the profession default (centurion `hitAllBlocked`, block 3). Module CEN-X
// "攻击被阻挡的敌人攻击力提升至110%" (trait bb atk_scale): PRTS "于每次计算伤害时检查目标是否处于阻挡/被阻挡状态…提升效果
// 为攻击力倍率提升" — every damage instance of hers whose target is blocked (by anyone) takes ×atk_scale on its ATK-based
// amount (before DEF): normal attacks, S1, the S3 cuts and burst. Module CEN-Y "生命值高于50%时受到的物理伤害降低20%"
// (trait bb hp_ratio / damage_resistance): physical damage taken × (1 − damage_resistance) while HP > hp_ratio.
// - T1 紧急除颤 "在生命值低于25%时,仅一次回复50%的生命值并在6秒内使生命值不低于50%" (bb hp_ratio,
// huang_t_1[heal].hp_ratio, huang_t_1[lock].min_hp_ratio / .duration). PRTS 备注: every deployment she holds 不死 until
// the first time her HP ratio is ≤ hp_ratio; then the heal (max HP × [heal].hp_ratio, a heal — huang_t_1[heal]
// HealViaMaxHpRatio) and for [lock].duration s every damage that would take her below [lock].min_hp_ratio × max HP is
// cut to what is left above it ("该伤害减少(伤害值-煌当前生命值+煌最大生命值×50%)点": the `hpDamage` hook after the
// shields — a 伤判效果, so a 流失 such as her S3's is not held). CEN-Y stage 3 (talent hp_ratio 0.5 + the hidden
// huang_e_003[lock] part): "在生命值首次低于25%、50%时…8秒内…": the talent fires at 50 %, and a second trigger at
// check_hp_ratio (25 %) with its own 不死 (huang_e_003[undead]) fires only once the first is spent ("生命值首次低于50%
// 始终先于…25%生效"), re-checking the HP after the first one's heal [ASSUMED: both read the current HP]; its heal is
// skipped while the first lock runs (huang_e_003[heal] "CheckContainsBuff huang_t_1[lock] → IfNot → heal"). The hidden
// part's hp_ratio / def_penetrate_fixed (CEN-Y stage 2+): "生命值高于50%时攻击无视目标150点防御力" — a defIgnoreFlat
// buff while HP > hp_ratio (every damage of hers).
// - T2 严酷训练 "在战场停留15秒后获得抵抗" (bb one_minus_status_resistance, interval): `interval` s after each deployment
// the engine 抵抗 (huang_t_2 creates status_resistance[inf]: control statuses last half as long) until she leaves.
// CEN-X stage 3 adds "30秒后攻击力+6%,45秒后攻速+12" (huang_t_2[e_002_atk].atk / .interval, [e_002_atk_speed]
// .attack_speed / .interval: an ATK MULTIPLIER and an ATTACK_SPEED ADDITION, INFINITY lifetime).
// - S1 强力击·γ型 (AUTO, attack SP): the next attack at atk_scale × ATK — every blocked enemy it hits (her trait); the
// data's DEFAULT trigger (an AUTO "next attack" waits for her attack).
// - S2 链锯延伸模块 (AUTO, 持续时间无限): ATK +atk, DEF +def and the skill range 2-2 for the rest of the deployment (PRTS 备注
// "攻击距离加长…使用技能提供的攻击范围(非攻击距离+1)"); an AUTO skill acting on herself only fires as soon as its SP is
// full (the owner's AUTO rule, kits/README.md checklist 5).
// - S3 沸腾爆裂 (MANUAL, data DEFAULT): a 维持技能状态 (PRTS 游戏数据基础: no normal attack while it runs; its cadence
// follows the animation — no attack speed change touches it — and a hard control (晕眩 / 冻结 …) ends it early).
// PRTS 备注 + huang_s_3: every second for the first 8 s the ATK / DEF bonus is updated, then one physical cut hits every
// ground enemy of her attack range ("对前方一格内的敌方单位进行切割" — her own 1-1 and the enemies she blocks; 不可对空;
// 100 % ATK [ASSUMED: no blackboard key — her attack's own damage]); the bonus is atk / def × min(1, t ÷ (duration −
// 1.05)) at the t-th update (huang_s_3 RemainingRatioToAttributeModifier `_endTime` 1.05: full at 8.95 s; the Terra
// Wiki's "reaches maximum after 8.95 seconds"); at 9 s it reaches the maximum and the finish follows (PRTS "9s时…";
// Terra "triggered at the 9 seconds mark"; her skeleton, whose Skill_2_* clips are S3's: the 3 s Skill_2_Loop_End's
// last OnAttack falls at 9 s of the 10 s run — Begin 1.4 s + the 0.4 s loops to 7 s): the huang_s_3 ON_SKILL_FINISH part, she loses hp_ratio ×
// MAX HP (DamageViaMaxHpRatio, undeadable — never below 1 HP; PRTS calls it 流失) and then every enemy of the 3 × 3
// area ahead (PRTS "前方3×3区域 3-6…碰撞判定,可对空": air units too) takes damage_by_atk_scale × her ATK at that
// moment, physical; the state runs on to its 10 s. A run cut short by a hard control finishes at once
// [ASSUMED: ON_SKILL_FINISH fires on any end but her knock-out]. The range the game shows while it runs is the
// burst's 3-6 (PRTS); the data has no skill range and the cuts use her own, so the card keeps her 1-1 and the trigger
// stays DEFAULT.
import { num, talentBb, moduleBb, traitBb, up, onHitBy, onHitOn, toggleBuff, skillRec } from '../shared/tier1.js';
import { absoluteRangeKeys } from '../../../targeting.js';
const S1 = 'skchr_huang_1';
const S2 = 'skchr_huang_2';
const S3 = 'skchr_huang_3';
/** S3's final area "前方3×3区域" (PRTS 备注 range 3-6; range_table): her tile, the two ahead and the rows beside them. */
const GRID_3_6 = Object.freeze([[1, 0], [1, 1], [1, 2], [0, 0], [0, 1], [0, 2], [-1, 0], [-1, 1], [-1, 2]]);
/** huang_s_3 RemainingRatioToAttributeModifier `_endTime`: the ramp is full this long before the run's nominal end. */
const S3_RAMP_END = 1.05;
/** S3 updates: eight cuts (1 s apart), then the finish at the ninth (PRTS "9s时"). */
const S3_CYCLES = 9;
const S3_BUFF = 'skill:huang:ramp';
const bbOf = (chess, id) => skillRec(chess, id)?.bb ?? {};
export default {
char_017_huang: (bb, chess, def) => {
const tb = traitBb(chess);
const t0 = talentBb(chess, 0), t1 = talentBb(chess, 1);
const hb = moduleBb(chess); // CEN-Y stage 2+: hp_ratio / def_penetrate_fixed; stage 3: huang_e_003[lock].*
const b1 = bbOf(chess, S1), b2 = bbOf(chess, S2), b3 = bbOf(chess, S3);
const s2 = skillRec(chess, S2);
const ownGrid = chess?.rangeGrid ?? def?.rangeGrid ?? [[0, 0], [0, 1]];
/** The ground enemies her own range holds and the ones she blocks (the S3 cuts). */
const cutTargets = (battle, unit) => {
const keys = absoluteRangeKeys(unit.rangeGrid || ownGrid, unit.tileR, unit.tileC, unit.dir, unit.s.rangeExtend || 0);
const list = battle.enemiesInKeys(keys, unit, { canHitFly: false });
for (const e of battle.blockedTargets(unit, { canHitFly: false })) if (!list.includes(e)) list.push(e);
return list;
};
/** S3's finish (huang_s_3 ON_SKILL_FINISH): the 流失 first (undeadable), then the burst at her ATK of that moment. */
const finish = (battle, unit, s) => {
s.done = true;
const loss = Math.min(unit.s.maxHp * num(b3.hp_ratio), unit.hp - 1);
if (loss > 0) battle.loseHp(unit, loss, { source: unit });
if (!up(unit)) return;
const atk = unit.s.atk;
const keys = absoluteRangeKeys(GRID_3_6, unit.tileR, unit.tileC, unit.dir, 0);
battle.fx('aoe', { x: unit.x, y: unit.y, radius: 1.5, id: unit.id, skill: 'huang:burst' });
for (const e of battle.enemiesInKeys(keys, unit, { canHitFly: true })) {
if (e.alive) battle.dealDamage(unit, e, { amount: atk * num(b3.damage_by_atk_scale, 1), type: 'phys', isSkill: true, tags: ['skill'] });
}
};
const setRamp = (battle, unit, r) => {
const b = unit.findBuff(S3_BUFF);
if (!b) return;
b.mods = { atkPct: num(b3.atk) * r, defPct: num(b3.def) * r };
unit.markDirty();
};
return {
skills: {
[S1]: { kind: 'instant', attack: { atkScale: num(b1.atk_scale, 1) } },
[S2]: {
kind: 'toggle', trigger: 'SP_FULL',
mods: { atkPct: num(b2.atk), defPct: num(b2.def) },
...(s2?.rangeGrid?.length ? { targeting: { rangeGrid: s2.rangeGrid } } : {}),
onStart({ battle, unit }) { battle.fx('buff', { x: unit.x, y: unit.y, id: unit.id, kind: 'atk' }); },
},
[S3]: {
kind: 'duration',
attack: { noAttack: true },
onStart({ battle, unit }) {
unit.mem.huangS3 = { t: 0, k: 0 };
battle.addBuff(unit, { key: S3_BUFF, mods: { atkPct: 0, defPct: 0 }, tags: ['skill'], visible: true });
},
onTick({ battle, unit, skill, dt }) {
const s = unit.mem.huangS3;
if (!s) return;
// 维持技能状态: a hard control ends it early (PRTS 游戏数据基础 "眩晕、冻结等硬性控制会提前结束")
if (!unit.canAct || unit.s.flags.sleep) { skill.end('interrupted'); return; }
s.t += dt;
while (s.k < S3_CYCLES && s.t >= s.k + 1 - 1e-9) {
s.k++;
setRamp(battle, unit, Math.min(1, s.k / Math.max(1e-6, skill.duration - S3_RAMP_END)));
if (s.k >= S3_CYCLES) { finish(battle, unit, s); return; }
const foes = cutTargets(battle, unit);
if (foes.length) battle.fx('aoe', { x: unit.x, y: unit.y, radius: 0.6, id: unit.id, skill: 'huang:cut' });
for (const e of foes) if (e.alive) battle.dealDamage(unit, e, { amount: unit.s.atk, type: 'phys', isSkill: true, tags: ['skill'] });
}
},
onEnd({ battle, unit, reason }) {
const s = unit.mem.huangS3;
unit.mem.huangS3 = null;
if (s && !s.done && reason !== 'death' && up(unit)) finish(battle, unit, s); // cut short: it finishes at once
battle.removeBuff(unit, S3_BUFF);
},
},
},
talents: [
{ install(battle, unit) { // 紧急除颤 (+ CEN-Y stage 3's second trigger and stage 2+'s 防御穿透)
const trig = num(t0.hp_ratio, 0.25);
const healA = num(t0['huang_t_1[heal].hp_ratio'], 0.5);
const floorA = num(t0['huang_t_1[lock].min_hp_ratio'], 0.5), durA = num(t0['huang_t_1[lock].duration'], 6);
const second = hb['huang_e_003[lock].check_hp_ratio'] != null;
const trigB = num(hb['huang_e_003[lock].check_hp_ratio'], 0.25);
const healB = num(hb['huang_e_003[lock].hp_ratio'], 0.5);
const floorB = num(hb['huang_e_003[lock].min_hp_ratio'], 0.5), durB = num(hb['huang_e_003[lock].duration'], 8);
const st = { undeadA: false, undeadB: false, lockA: -Infinity, lockB: -Infinity };
unit.mem.huangDefib = st;
battle.on('deploy', ({ unit: u }) => {
if (u !== unit) return;
st.undeadA = true;
st.undeadB = second;
st.lockA = st.lockB = -Infinity;
}, { owner: unit });
// 不死 while a trigger is unspent: a lethal hit leaves her at 1 HP (the trigger check below follows)
battle.on('fatal', (c) => { if (c.unit === unit && !c.prevented && (st.undeadA || st.undeadB)) c.prevented = true; }, { owner: unit });
battle.on('hpDamage', (c) => {
if (c.target !== unit) return;
const t = battle.time;
const floor = Math.max(t < st.lockA ? floorA : 0, t < st.lockB ? floorB : 0);
if (floor > 0) c.amount = Math.min(c.amount, Math.max(0, unit.hp - floor * unit.s.maxHp));
}, { owner: unit });
battle.on('damaged', (c) => {
if (c.target !== unit || !up(unit)) return;
if (st.undeadA && unit.hpRatio <= trig + 1e-9) {
st.undeadA = false;
st.lockA = battle.time + durA;
battle.heal(unit, unit, unit.s.maxHp * healA, { self: true, tags: ['talent'] });
battle.fx('heal', { x: unit.x, y: unit.y, id: unit.id });
}
if (st.undeadB && !st.undeadA && unit.hpRatio <= trigB + 1e-9) {
st.undeadB = false;
st.lockB = battle.time + durB;
if (!(battle.time < st.lockA)) battle.heal(unit, unit, unit.s.maxHp * healB, { self: true, tags: ['talent'] });
battle.fx('heal', { x: unit.x, y: unit.y, id: unit.id });
}
}, { owner: unit });
const pen = num(hb.def_penetrate_fixed);
if (pen > 0) toggleBuff(battle, unit, 'talent:huang:defPen', () => unit.hpRatio > num(hb.hp_ratio, 0.5), { defIgnoreFlat: pen });
} },
{ install(battle, unit) { // 严酷训练 (+ CEN-X stage 3: ATK after 30 s, ASPD after 45 s)
const resAt = num(t1['huang_t_2.interval'], num(t1.interval, 15));
const res = Math.min(0.95, Math.max(0, -num(t1['huang_t_2.one_minus_status_resistance'], num(t1.one_minus_status_resistance))));
const atk = num(t1['huang_t_2[e_002_atk].atk']), atkAt = num(t1['huang_t_2[e_002_atk].interval']);
const as = num(t1['huang_t_2[e_002_atk_speed].attack_speed']), asAt = num(t1['huang_t_2[e_002_atk_speed].interval']);
battle.on('deploy', ({ unit: u }) => {
if (u !== unit) return;
const seq = unit.deploySeq;
const at = (s, fn) => battle.after(s, () => { if (up(unit) && unit.deploySeq === seq) fn(); }, { owner: unit });
if (res > 0) at(resAt, () => battle.applyStatus(unit, 'resist', { value: res, source: unit }));
if (atk) at(atkAt, () => battle.addBuff(unit, { key: 'talent:huang:atk', mods: { atkPct: atk }, tags: ['talent'] }));
if (as) at(asAt, () => battle.addBuff(unit, { key: 'talent:huang:aspd', mods: { aspd: as }, tags: ['talent'] }));
}, { owner: unit });
} },
],
install(battle, unit) {
// CEN-X: ×atk_scale on her ATK-based damage to a blocked enemy (every damage calculation)
const blockedMul = num(tb.atk_scale, 1);
if (blockedMul !== 1) onHitBy(battle, unit, ({ target, dmg }) => { if (target.blockedBy) dmg.amount *= blockedMul; });
// CEN-Y: 生命值高于50%时受到的物理伤害降低20%
const dr = num(tb.damage_resistance);
if (dr > 0) onHitOn(battle, unit, ({ dmg }) => { if (dmg.type === 'phys' && unit.hpRatio > num(tb.hp_ratio, 0.5)) dmg.mul *= 1 - dr; });
},
};
},
};