feedback5 O11: 自选 operator kits 薇薇安娜, 怒潮凛冬 and 赫德雷 — every skill, talent, trait and module (stage 1 / 3) at every form
- kits/ops/op-vvana.js, op-headb2.js, op-hodrer.js (registered in OPERATOR_KIT_FILES), checked against the data, PRTS and the client's battle data (charpacks, [uc]skills / [uc]equips prefabs, buff templates, 怒潮凛冬's Skill_3 clip events). - Engine (additive): ai.js enemyAttack marks a shot that flies as a projectile isProjectile (远程途径: 薇薇安娜's 散华 护盾 blocks melee-path hits only); targeting.js priority 'elite' (薇薇安娜 S3 "优先攻击精英或领袖敌人"); docs/SIM.md §4 / §8. - Tests: test/content/op_vvana.test.js (13), op_headb2.test.js (10), op_hodrer.test.js (8). - Golden: only diy moved — diy-049 … 054 now field the three operators in every form × module × skill, the prototypes follow in diy-054 … 058 (same packing); diy-001 … 048 and every other family unchanged.
This commit is contained in:
1 parent
fef4ea3d4b
commit
e0cce79f98
11 files changed
+2128
-113
No files matched your search
@@ -0,0 +1,176 @@
|
||||
// server/sim/content/kits/ops/op-hodrer.js — 赫德雷 (char_4088_hodrer) 自选 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_4088_hodrer, 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 赫德雷 (备注 of 及锋而试
|
||||
// and 死境硝烟); PRTS 卫戍协议/帮助 §技能操作 ("携带状态切换类技能的干员…每次部署后仅开启一次技能"); gamedata_const ba.protect 庇护,
|
||||
// ba.stun 晕眩, ba.root 束缚; PRTS 伤害分类 (无来源真实持续伤害); the client's battle data read from the local install —
|
||||
// charpack char_4088_hodrer, battle/prefabs [uc]skills skchr_hodrer_1/2/3 and [uc]equips hodrer_equip_*, buff_template_data
|
||||
// hodrer_* (named below).
|
||||
// - Trait (重剑手) "同时攻击阻挡的所有敌人": the profession (melee physical, ground only, block 2, 2.5 s).
|
||||
// - Module CRU-X 新的生活: trait "受到的治疗效果提升20%" (trait bb heal_scale; equip_heal_scale_up): healingTakenMul ×heal_scale —
|
||||
// his own S1 / S3 heals included. Stage 3: 余火之氅 28 % and 物理伤害 +10 % (below).
|
||||
// - Module CRU-Y 笔迹: trait "对被阻挡的敌人伤害提升至110%" (trait bb damage_scale; hodrer_e_003_tr: ON_OUTPUT_DAMAGE, CheckBlocked
|
||||
// by the buff's source — him): ×damage_scale (a damage multiplier) on every damage of his on an enemy HE blocks. Stage 3:
|
||||
// 及锋而试 130 % / 160 % and its first-hit stun (below).
|
||||
// - T1 及锋而试 "攻击敌人时攻击力提升至110%,若目标处于晕眩、束缚则改为提升至140%" (atk_scale_2 / atk_scale; hodrer_t_1:
|
||||
// ON_CALCULATE_DAMAGE, abnormal flags STUNNED / UNMOVABLE → AtkScaleUp): each damage instance of his attacks ×atk_scale_2, or
|
||||
// ×atk_scale on a target under a 晕眩 or 束缚 status (冻结 is neither) — an ATK scale, before DEF. PRTS 备注 "本天赋于每次攻击的
|
||||
// 附加效果后、计算伤害前判断并生效": the attack's riders come first, in the same `hit` step — S2's stun and S3's stun roll (the
|
||||
// attacks' `_activeBuffs`), then CRU-Y stage 3's "且对阻挡的敌人首次造成伤害时对其造成2.5秒晕眩效果" (hodrer_e_003_t, event
|
||||
// priority 2000 — before 及锋而试: an enemy he blocks without his mark gets it and stuns `stun` s; the marks are derived from
|
||||
// the module talent's buff, so they go when he leaves the field and a redeployed 赫德雷 stuns anew) — so a hit that stuns
|
||||
// counts as on a stunned target; a rider lands when the hit is then dodged [ASSUMED: the client applies them in the attack's
|
||||
// damage calculation].
|
||||
// - T2 余火之氅 "使自身与身后一格的友军获得18%的庇护" (damage_resistance, talent grid b-1: his tile and the one behind; CRU-X
|
||||
// stage 3 "28%…且造成的物理伤害提升10%" — hodrer_equip_1_3_p2 gives damage_resistance[inf] and hodrer_equip[damage_scale_up]
|
||||
// (ON_OUTPUT_DAMAGE PHYSICAL ×damage_scale) to the same targets, ally side, any motion, removed when they leave): 庇护
|
||||
// (ba.protect "受到的物理和法术伤害降低相应比例(同名效果取最高)") = phys / artsTakenMul 1 − value as one instance per ally
|
||||
// (applyStrongest) with physDealtMul ×damage_scale, renewed every PROTECT_IV s on the allies of the grid while he is deployed.
|
||||
// - S1 重锋不熄 (AUTO, hit SP: INCREASE_WHEN_ATTACK, data DEFAULT): the next attack (every enemy he blocks) at atk_scale × ATK;
|
||||
// hodrer_s[heal] (ON_ABILITY_SPELL_ON, HealViaMaxHpRatio) heals hp_ratio of his max HP once for that attack.
|
||||
// - S2 余烬重荷 (MANUAL, data DEFAULT): "被动效果:攻击力+16%" (atk; the skill's extra ability hodrer_s_2[passive]) while it is
|
||||
// his skill; "主动触发可以在下列状态和初始状态间切换" — a 状态切换类 skill, switched on once per deployment and kept (the official
|
||||
// auto-battle rule, `toggle`): attack interval +base_attack_time s (flat), block +block_cnt, every attack stuns its targets
|
||||
// attack@stun s.
|
||||
// - S3 死境硝烟 (MANUAL, data ACTIVE_RANGE on its running range, 70 / 70 s): max HP +max_hp, ATK +atk, 攻击距离
|
||||
// +ability_range_forward_extend; each attack heals attack@hp_ratio of his max HP (hodrer_s[heal], once per attack) and stuns
|
||||
// each target attack@stun s with attack@buff_prob. hodrer_s_3[damage] (triggerInterval 1 s, the first after 1 s): he loses
|
||||
// attack@value HP every second (FixedValueDamage that skips the damage events: a 流失 here [ASSUMED: it may knock him out —
|
||||
// no floor is named]); every damage he outputs on an enemy, and every damage an enemy deals him, marks that enemy if it is
|
||||
// not 无敌 (one hodrer_s_3[burn] per enemy and 赫德雷, PRTS 备注 "仅判定本次技能期间满足条件的非无敌敌方单位…不可叠加"): from
|
||||
// then on it takes attack@damage 无来源 true damage (NoSourceDamage PURE: damage.js periodicDamage, credited to him) every
|
||||
// second of its own (triggerInterval 1 s from its mark) until the skill ends (a derived buff of the skill's).
|
||||
|
||||
import { num, talentBb, talentGrid, moduleBb, traitBb, skillRec, statBuff, up, batMod, alliesInGridOf } from '../shared/tier1.js';
|
||||
import { periodicDamage } from '../../../damage.js';
|
||||
|
||||
const S1 = 'skchr_hodrer_1';
|
||||
const S2 = 'skchr_hodrer_2';
|
||||
const S3 = 'skchr_hodrer_3';
|
||||
/** 余火之氅's grid when the data carries none: his tile and the one behind him (range b-1). */
|
||||
const BEHIND = Object.freeze([Object.freeze([0, -1]), Object.freeze([0, 0])]);
|
||||
const PROTECT_IV = 0.25;
|
||||
const PROTECT_KEY = 'talent:hodrer:protect';
|
||||
const S2_PASSIVE_KEY = 'skill:hodrer:s2passive';
|
||||
/** hodrer_s_3[damage] / hodrer_s_3[burn]: triggerInterval 1 s, waitFirstTriggerInterval. */
|
||||
const SMOKE_IV = 1;
|
||||
/** The riders and 及锋而试 before the other `hit` handlers read the hit. */
|
||||
const T1_PRIORITY = 10;
|
||||
|
||||
const bbOf = (chess, id) => skillRec(chess, id)?.bb ?? {};
|
||||
/** Under a 晕眩 or 束缚 status (及锋而试: abnormal flags STUNNED / UNMOVABLE). */
|
||||
const held = (e) => e.buffs.some((b) => (b.status ?? b.key) === 'stun' || (b.status ?? b.key) === 'bind');
|
||||
/** The S3 burn of one 赫德雷 on an enemy (independentCharacterSource: one per source). */
|
||||
const smokeKey = (unit) => `skill:hodrer:smoke:${unit.id}`;
|
||||
|
||||
export default {
|
||||
char_4088_hodrer: (bb, chess) => {
|
||||
const t0 = talentBb(chess, 0), t1 = talentBb(chess, 1);
|
||||
const hidden = moduleBb(chess); // CRU-Y stage 3: stun (the first-hit stun on an enemy he blocks)
|
||||
const tb = traitBb(chess);
|
||||
const b1 = bbOf(chess, S1), b2 = bbOf(chess, S2), b3 = bbOf(chess, S3);
|
||||
const selfHeal = (battle, unit, ratio) => { if (ratio > 0) battle.heal(unit, unit, unit.s.maxHp * ratio, { self: true }); };
|
||||
/** S3: mark `e` (not 无敌) for this cast — its own 1 s burn until the skill ends. */
|
||||
const mark = (battle, unit, e) => {
|
||||
const marks = unit.mem.hodrerMarks;
|
||||
if (!marks || marks.has(e) || !e.alive || e.s.flags.invulnerable) return;
|
||||
marks.add(e);
|
||||
const dmg = num(b3['attack@damage']);
|
||||
battle.addBuff(e, {
|
||||
key: smokeKey(unit), interval: SMOKE_IV, source: unit, tags: ['skill'],
|
||||
onTick: ({ battle: b, unit: target }) => { if (dmg > 0) b.dealDamage(unit, target, periodicDamage(dmg)); },
|
||||
});
|
||||
};
|
||||
return {
|
||||
skills: {
|
||||
[S1]: {
|
||||
kind: 'instant',
|
||||
attack: { atkScale: num(b1.atk_scale, 1) },
|
||||
onAttack({ battle, unit }) { selfHeal(battle, unit, num(b1.hp_ratio)); },
|
||||
},
|
||||
[S2]: {
|
||||
kind: 'toggle',
|
||||
mods: { batPct: batMod(b2.base_attack_time, chess), blockCnt: num(b2.block_cnt) },
|
||||
},
|
||||
[S3]: {
|
||||
kind: 'duration',
|
||||
mods: { hpPct: num(b3.max_hp), atkPct: num(b3.atk) },
|
||||
targeting: { rangeExtend: num(b3.ability_range_forward_extend) },
|
||||
onStart({ battle, unit, skill }) {
|
||||
const n0 = skill.activations;
|
||||
unit.mem.hodrerMarks = new Set();
|
||||
unit.mem.hodrerSmoke = battle.every(SMOKE_IV, (b, sc) => {
|
||||
if (!skill.active || skill.activations !== n0 || !up(unit)) { sc.cancel(); return; }
|
||||
battle.loseHp(unit, num(b3['attack@value']), { source: unit, tags: ['hodrer:smoke'] });
|
||||
}, { owner: unit });
|
||||
},
|
||||
onAttack({ battle, unit }) { selfHeal(battle, unit, num(b3['attack@hp_ratio'])); },
|
||||
onEnd({ battle, unit }) {
|
||||
unit.mem.hodrerSmoke?.cancel();
|
||||
unit.mem.hodrerSmoke = null;
|
||||
for (const e of unit.mem.hodrerMarks ?? []) battle.removeBuff(e, smokeKey(unit));
|
||||
unit.mem.hodrerMarks = null;
|
||||
},
|
||||
},
|
||||
},
|
||||
talents: [
|
||||
{ install(battle, unit) { // 及锋而试 (+ the riders that precede it): ×atk_scale_2, ×atk_scale on a stunned / bound target
|
||||
const firstStun = num(hidden.stun);
|
||||
let stunnedOnce = new WeakSet(); // CRU-Y stage 3's marks, gone with his deployment
|
||||
battle.on('deploy', (c) => { if (c.unit === unit) stunnedOnce = new WeakSet(); }, { owner: unit });
|
||||
battle.on('hit', (c) => {
|
||||
const d = c.dmg, e = c.target;
|
||||
if (c.source !== unit || !d.isAttack || d.cancel || !e || e.side !== 'enemy') return;
|
||||
const sk = unit.skill;
|
||||
if (sk?.active && sk.id === S2) battle.applyStatus(e, 'stun', { duration: num(b2['attack@stun']), source: unit });
|
||||
if (sk?.active && sk.id === S3 && e.alive && battle.rng.chance(num(b3['attack@buff_prob']))) {
|
||||
battle.applyStatus(e, 'stun', { duration: num(b3['attack@stun']), source: unit });
|
||||
}
|
||||
if (firstStun > 0 && e.alive && e.blockedBy === unit && !stunnedOnce.has(e)) {
|
||||
stunnedOnce.add(e);
|
||||
battle.applyStatus(e, 'stun', { duration: firstStun, source: unit });
|
||||
}
|
||||
d.amount *= held(e) ? num(t0.atk_scale, 1) : num(t0.atk_scale_2, 1);
|
||||
}, { owner: unit, priority: T1_PRIORITY });
|
||||
} },
|
||||
{ install(battle, unit) { // 余火之氅: 庇护 (and CRU-X stage 3 物理伤害 +) on his tile and the one behind
|
||||
const dr = num(t1.damage_resistance), ds = num(t1.damage_scale, 1);
|
||||
const grid = talentGrid(chess, 1) ?? BEHIND;
|
||||
if (!(dr > 0) && ds === 1) return;
|
||||
const mods = (v) => ({ physTakenMul: 1 - v, artsTakenMul: 1 - v, ...(ds !== 1 ? { physDealtMul: ds } : {}) });
|
||||
const give = () => {
|
||||
if (!up(unit)) return;
|
||||
for (const a of alliesInGridOf(battle, unit, grid)) battle.applyStrongest(a, PROTECT_KEY, { duration: PROTECT_IV + 0.1, value: dr, mods, source: unit });
|
||||
};
|
||||
battle.every(PROTECT_IV, give, { owner: unit });
|
||||
battle.on('battleStart', give, { owner: unit });
|
||||
battle.on('deploy', (c) => { if (c.unit === unit && battle.started) give(); }, { owner: unit });
|
||||
} },
|
||||
],
|
||||
install(battle, unit) {
|
||||
// CRU-X: 受到的治疗效果提升20%
|
||||
statBuff(battle, unit, 'trait:hodrer:heal', { healingTakenMul: num(tb.heal_scale, 1) });
|
||||
// CRU-Y: 对被(自身)阻挡的敌人伤害提升至110%
|
||||
const vs = num(tb.damage_scale, 1);
|
||||
if (vs !== 1) {
|
||||
battle.on('hit', (c) => {
|
||||
if (c.source === unit && c.target && c.target.side === 'enemy' && c.target.blockedBy === unit) c.dmg.mul *= vs;
|
||||
}, { owner: unit });
|
||||
}
|
||||
// S2's passive part: 攻击力+16% while 余烬重荷 is his skill
|
||||
if (unit.skill?.id === S2) statBuff(battle, unit, S2_PASSIVE_KEY, { atkPct: num(b2.atk) });
|
||||
// S3: every damage either way between him and an enemy marks the enemy for the running cast
|
||||
if (unit.skill?.id === S3) {
|
||||
battle.on('hit', (c) => {
|
||||
if (!unit.mem.hodrerMarks) return;
|
||||
if (c.source === unit && c.target && c.target.side === 'enemy') mark(battle, unit, c.target);
|
||||
else if (c.target === unit && c.source && c.source.side === 'enemy') mark(battle, unit, c.source);
|
||||
}, { owner: unit });
|
||||
}
|
||||
},
|
||||
};
|
||||
},
|
||||
};
|
||||
Reference in new issue
Block a user