194 lines
13 KiB
JavaScript
194 lines
13 KiB
JavaScript
// server/sim/content/kits/ops/op-judge.js — 斥罪 (char_4065_judge) 自选 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_4065_judge, 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 斥罪 (律法卫士 备注:
|
||
// "自身产生的屏障生效优先级为-1000,为单一BUFF,每次“获得”实际效果为补充屏障的吸收量…本天赋的屏障上限仅限制自身通过天赋和技能产生的
|
||
// 屏障量…上限会实时计算"; 荆棘环身 备注 "反伤效果会直接选中伤害来源,但仅对阵营为敌方的来源造成伤害"; S1 备注 on 蓄力; S2 备注
|
||
// "技能期间,自身丢失全部视野"; S3 备注 "技能获取的屏障为补充天赋屏障的吸收量,且不会令屏障总量超过天赋描述下的屏障获取上限"),
|
||
// gamedata_const ba.charged 蓄力 ("技力达到上限可继续回复,回复至上限2倍时进入蓄力状态,此时开启技能会触发额外效果(任何时候开启均消耗
|
||
// 全部技力)"); the client's battle data: buff templates judge_t_1 / judge_t_2 / judge_e_002_tr / judge_s_1_enhance_checker /
|
||
// judge_s_1[damage] / judge_s_1[mode] / judge_s_2[aoe] / judge_s_2[shield_scale] / judge_s_3[shield], the skill prefabs
|
||
// skchr_judge_1..3 and the module prefabs judge_equip_{1,2}_*; Arknights Terra Wiki "Penance".
|
||
// - Trait (不屈者) "无法被友方角色治疗": the profession default (professions.js unyield `noHeal`; the charpack's 'unhealable');
|
||
// an HP-regen attribute (黍's sown tiles, 吟游者) still reaches her (SIM.md §4). Melee physical, ground-only, block 3, 1-1.
|
||
// Module UNY-X “通往未来的荆棘之路” "受到来自自身阻挡单位的伤害降低15%" (trait bb damage_scale; judge_e_002_tr): every damage
|
||
// whose source she blocks ×damage_scale. Module UNY-Y “无罪” "周围8格没有友方干员时攻击力和防御力+8%" (trait bb atk / def; the
|
||
// prefab's checker counts operators only — professionMask 639): ATK / DEF + while no allied operator stands on the 8
|
||
// tiles around her.
|
||
// - T1 律法卫士 (judge_t_1): one barrier buff BARRIER. At every deployment +born_hp_ratio × max HP; every enemy she knocks
|
||
// out +kill_hp_ratio × max HP; S3 +hp_ratio × max HP — each gain × the shield scale (1, + S2's shield_scale while S2
|
||
// runs: judge_s_2[shield_scale]) and only while the barrier is below the cap max_hp_ratio × the CURRENT max HP, then
|
||
// capped there (a max-HP change leaves the barrier as it is). The buff ends when its absorb runs out (the client keeps
|
||
// an empty buff; nothing else depends on it here). [ASSUMED] it is spent in the engine's oldest-first order among
|
||
// several barriers (the client's priority −1000 puts it last); damage reductions (庇护, UNY-X) act before it as the
|
||
// client's LOW_PRIORITY. UNY-X stage 3: 70 % / 12 % (module talent change). higher_effect_hp_ratio only sizes the
|
||
// barrier's effect (judge_t_1[effect]) — no gameplay.
|
||
// - T2 荆棘环身 (judge_t_2, ON_TAKE_DAMAGE): every damage instance an enemy source deals her while she holds barrier (its
|
||
// judge_shield[mark]: the absorb before that hit — the hit that breaks it still counts, t_1 runs LOW_PRIORITY) deals
|
||
// atk_scale × her ATK arts back to that source (a 普通伤害, InverseDamage `attackType` NORMAL; × the 蓄力 攻击力倍率 while S1's
|
||
// 蓄力 attack is out — PRTS 备注), never for a 流失, an element 损伤, a 无来源 hit or reflected damage (tag 'counter'),
|
||
// undodgeable [ASSUMED like 星熊's counter]. UNY-Y stage 3: 58 %.
|
||
// - S1 一锤定音 (AUTO, time SP, data DEFAULT): the next attack adds atk_scale_2 × ATK arts (a 普通伤害 skill hit, after the
|
||
// physical one). 蓄力 (maxChargeTime 2): SP runs on to twice the cost; casting from there (the client's "available count
|
||
// ≥ 1" after the cast) also stuns the target `stun` s (before the arts hit) and raises every damage she deals until the
|
||
// attack is done ×judge_s_1_enhance_checker.atk_scale (an 攻击力倍率: atkScaleMul). Every cast spends ALL the SP
|
||
// (ClearCharacterSp). The moment the SP reaches the 蓄力 mark her attack cycle restarts (judge_s_1[mode] SwitchMode with
|
||
// restartFSM, "无关乎能否开启技能"; PRTS 备注 "存在可攻击目标且未被控制的情况下切换后会立刻触发技能"): atkCd = 0. The refund of a
|
||
// cast whose target died (_recoverSpIfTargetDead) is not modelled [ASSUMED]: a melee cast and its attack resolve in one
|
||
// tick here.
|
||
// - S2 坚心苦修 (MANUAL; data DEFAULT — the owner's 重装 exception of 2026-10-05, rawRule TAKE_DAMAGE): `duration` s, no
|
||
// attacks (她丢失全部视野), 庇护 damage_resistance (the shared applyStrongest key PROTECT, for the skill's time), and from
|
||
// S2_FIRST s on every S2_EVERY s (judge_s_2[aoe]: firstTriggerInterval 0.9, triggerInterval 1) atk_scale × ATK arts (×
|
||
// any 攻击力倍率) to every ground enemy she can select on the skill range x-4 (AOEDamage WALK_ONLY, attackType NORMAL); T1's
|
||
// gains ×(1 + shield_scale) meanwhile.
|
||
// - S3 披荆斩棘 (MANUAL, hurt SP; data DEFAULT, rawRule TAKE_DAMAGE): at once barrier +hp_ratio × max HP (topping T1's,
|
||
// capped), then `duration` s ATK +atk, attack interval +base_attack_time s (a flat +0.9 on 1.6 s), taunt +taunt_level.
|
||
|
||
import { num, traitBb, talentBb, skillRec, toggleBuff, batMod, up, enemiesInGrid, holdProtect, PROTECT } from '../shared/tier1.js';
|
||
import { isHpLoss } from '../../../damage.js';
|
||
|
||
const S1 = 'skchr_judge_1';
|
||
const S2 = 'skchr_judge_2';
|
||
const S3 = 'skchr_judge_3';
|
||
const BARRIER = 'talent:judge:barrier';
|
||
const CHARGED = 'skill:judge:charged';
|
||
/** S2's 技能范围 when the data carries none (x-4: her tile and the 8 around it). */
|
||
const X4 = Object.freeze([[1, -1], [1, 0], [1, 1], [0, -1], [0, 0], [0, 1], [-1, -1], [-1, 0], [-1, 1]]);
|
||
/** judge_s_2[aoe]: firstTriggerInterval 0.9 s, then triggerInterval 1 s. */
|
||
const S2_FIRST = 0.9;
|
||
const S2_EVERY = 1;
|
||
|
||
const bbOf = (chess, id) => skillRec(chess, id)?.bb ?? {};
|
||
const tagged = (d, tag) => !!(d && Array.isArray(d.tags) && d.tags.includes(tag));
|
||
/** Her barrier left (0 without one). */
|
||
const barrierOf = (u) => u.findBuff(BARRIER)?.shield ?? 0;
|
||
|
||
export default {
|
||
char_4065_judge: (bb, chess) => {
|
||
const tb = traitBb(chess);
|
||
const t0 = talentBb(chess, 0), t1 = talentBb(chess, 1);
|
||
const b1 = bbOf(chess, S1), b2 = bbOf(chess, S2), b3 = bbOf(chess, S3);
|
||
const s2 = skillRec(chess, S2);
|
||
const capRatio = num(t0.max_hp_ratio, 3);
|
||
/** S2's 庇护 (damage_resistance[inf] for the skill's time). */
|
||
const protect = (battle, unit, skill) => {
|
||
const dr = num(b2.damage_resistance);
|
||
holdProtect(battle, unit, dr, Math.max(0.05, skill.timeLeft), unit); // 庇护: the shared effect (同名效果取最高)
|
||
};
|
||
/** judge_t_1's shield_scale: 1, + S2's shield_scale while S2 runs. */
|
||
const shieldScale = (unit) => 1 + (unit.skill?.active && unit.skill.id === S2 ? num(b2.shield_scale) : 0);
|
||
/** Add `amount` to her barrier while it is below the cap (the CURRENT max HP), capped there. Returns the gain. */
|
||
const gain = (battle, unit, amount) => {
|
||
if (!up(unit) || !(amount > 0)) return 0;
|
||
const cap = unit.s.maxHp * capRatio;
|
||
const cur = unit.findBuff(BARRIER);
|
||
const have = cur ? cur.shield : 0;
|
||
if (!(cap > have + 1e-9)) return 0;
|
||
const next = Math.min(cap, have + amount);
|
||
if (cur) { cur.shield = next; unit.markDirty(); } else battle.addBuff(unit, { key: BARRIER, shield: next, visible: true, tags: ['talent'] });
|
||
battle.fx('shield', { x: unit.x, y: unit.y, id: unit.id });
|
||
return next - have;
|
||
};
|
||
return {
|
||
skills: {
|
||
[S1]: {
|
||
kind: 'charges',
|
||
attack: {
|
||
onHit({ battle, unit, target }) {
|
||
if (!target || !target.alive) return;
|
||
if (unit.mem.judgeCharged) battle.applyStatus(target, 'stun', { duration: num(b1.stun), source: unit });
|
||
if (!target.alive) return;
|
||
battle.dealDamage(unit, target, { amount: unit.s.atk * num(b1.atk_scale_2) * unit.s.atkScaleMul, type: 'arts', isSkill: true, tags: ['skill', 'judge:gavel'] });
|
||
},
|
||
},
|
||
onStart({ battle, unit, skill }) {
|
||
// 蓄力: the cast took one charge of a full stack; every cast spends ALL the SP (ClearCharacterSp)
|
||
const charged = skill.maxCharges > 1 && skill.charges >= skill.maxCharges - 1;
|
||
skill.charges = 0;
|
||
skill.sp = 0;
|
||
unit.mem.judgeCharged = charged;
|
||
if (charged) {
|
||
battle.addBuff(unit, { key: CHARGED, mods: { atkScaleMul: num(b1['judge_s_1_enhance_checker.atk_scale'], 1) }, tags: ['skill'] });
|
||
battle.fx('crit', { x: unit.x, y: unit.y, id: unit.id });
|
||
}
|
||
},
|
||
onEnd({ battle, unit }) { unit.mem.judgeCharged = false; battle.removeBuff(unit, CHARGED); },
|
||
},
|
||
[S2]: {
|
||
kind: 'duration',
|
||
attack: { noAttack: true },
|
||
onStart({ battle, unit, skill }) {
|
||
unit.mem.judgeS2Next = S2_FIRST;
|
||
protect(battle, unit, skill);
|
||
},
|
||
onTick({ battle, unit, skill, dt }) {
|
||
protect(battle, unit, skill);
|
||
unit.mem.judgeS2Next -= dt;
|
||
while (unit.mem.judgeS2Next <= 1e-9 && up(unit)) {
|
||
unit.mem.judgeS2Next += S2_EVERY;
|
||
const foes = enemiesInGrid(battle, unit, s2?.rangeGrid?.length ? s2.rangeGrid : X4, { canHitFly: false });
|
||
battle.fx('aoe', { x: unit.x, y: unit.y, radius: 1.5, id: unit.id, skill: 'judge:atonement' });
|
||
const amount = unit.s.atk * num(b2.atk_scale) * unit.s.atkScaleMul;
|
||
for (const e of foes) if (e.alive) battle.dealDamage(unit, e, { amount, type: 'arts', isSkill: true, tags: ['skill', 'judge:atonement'] });
|
||
}
|
||
},
|
||
onEnd({ battle, unit }) { if (unit.findBuff(PROTECT)?.source === unit) battle.removeBuff(unit, PROTECT); },
|
||
},
|
||
[S3]: {
|
||
kind: 'duration',
|
||
mods: { atkPct: num(b3.atk), batPct: batMod(b3.base_attack_time, chess), taunt: num(b3.taunt_level) },
|
||
onStart({ battle, unit }) { gain(battle, unit, unit.s.maxHp * num(b3.hp_ratio) * shieldScale(unit)); },
|
||
},
|
||
},
|
||
talents: [
|
||
{ install(battle, unit) { // 律法卫士: barrier at every deployment and per enemy she knocks out (≤ max_hp_ratio × max HP)
|
||
battle.on('deploy', (ctx) => {
|
||
if (ctx.unit === unit) gain(battle, unit, unit.s.maxHp * num(t0.born_hp_ratio) * shieldScale(unit));
|
||
}, { owner: unit });
|
||
battle.on('kill', ({ killer, victim }) => {
|
||
if (killer === unit && victim && victim.side === 'enemy') gain(battle, unit, unit.s.maxHp * num(t0.kill_hp_ratio) * shieldScale(unit));
|
||
}, { owner: unit });
|
||
} },
|
||
{ install(battle, unit) { // 荆棘环身: while she holds barrier, every damage from an enemy source reflects atk_scale × ATK arts
|
||
const scale = num(t1.atk_scale);
|
||
if (!(scale > 0)) return;
|
||
const armed = new WeakSet();
|
||
battle.on('hit', (c) => { if (c.target === unit && c.dmg && barrierOf(unit) > 1e-9) armed.add(c.dmg); }, { owner: unit });
|
||
battle.on('damaged', (c) => {
|
||
const d = c.dmg, src = c.source;
|
||
if (c.target !== unit || !d || !armed.has(d) || !src || src.side !== 'enemy' || !src.alive) return;
|
||
if (isHpLoss(d) || c.type === 'element' || tagged(d, 'counter')) return;
|
||
battle.dealDamage(unit, src, { amount: unit.s.atk * scale * unit.s.atkScaleMul, type: 'arts', canDodge: false, tags: ['talent', 'counter', 'judge:thorns'] });
|
||
battle.fx('thorns', { x: unit.x, y: unit.y, id: unit.id });
|
||
}, { owner: unit });
|
||
} },
|
||
],
|
||
install(battle, unit) {
|
||
// S1 蓄力: the moment the SP reaches twice the cost her attack cycle restarts (judge_s_1[mode], restartFSM)
|
||
if (unit.skill?.id === S1) {
|
||
let full = false;
|
||
battle.on('tick', () => {
|
||
const sk = unit.skill;
|
||
const now = !!sk && up(unit) && sk.maxCharges > 1 && sk.charges >= sk.maxCharges;
|
||
if (now && !full) unit.atkCd = 0;
|
||
full = now;
|
||
}, { owner: unit });
|
||
}
|
||
// UNY-X: 受到来自自身阻挡单位的伤害降低15%
|
||
const cut = num(tb.damage_scale, 1);
|
||
if (cut !== 1) battle.on('hit', (c) => { if (c.target === unit && c.source && c.source.blockedBy === unit) c.dmg.mul *= cut; }, { owner: unit });
|
||
// UNY-Y: 周围8格没有友方干员时攻击力和防御力+8% (operators only, her own tile aside)
|
||
const ma = num(tb.atk), md = num(tb.def);
|
||
if (ma || md) {
|
||
const alone = () => !battle.allyUnits.some((a) => a !== unit && a.kind === 'op' && a.alive && a.deployed && !a.hidden
|
||
&& Math.max(Math.abs(a.tileR - unit.tileR), Math.abs(a.tileC - unit.tileC)) === 1);
|
||
toggleBuff(battle, unit, 'trait:judge:alone', alone, { atkPct: ma, defPct: md });
|
||
}
|
||
},
|
||
};
|
||
},
|
||
};
|