Files
ark-stronghold-protocol/server/sim/content/kits/ops/standin-acpion.js
T

145 lines
8.1 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/standin-acpion.js — 郁金香 (char_608_acpion) 补位 stand-in kit: 3 瑕光 (S3), 4 焰尾 (S2),
// 5 凛御银灰 / 6 圣聆初雪 / 缪尔赛思 / 耀骑士临光 (S3 + module SOL-X on the elite). Stand-in contract: ../README.md.
import { absoluteRangeKeys, sortEnemyTargets } from '../../../targeting.js';
import { num, talentBb, traitBb, skillRec, statBuff, toggleBuff, up } from '../shared/tier1.js';
const S1 = 'skchr_acpion_1', S2 = 'skchr_acpion_2', S3 = 'skchr_acpion_3';
/**
* S3 只余芬芳's slashes: her Spine `Skill` clip (char_608_acpion.skel, 1.567 s) carries 8 OnAttack events — one per
* slash, `times` 8 — at 0.400 s and then every 1/6 s (0.567 … 1.567 s); the `Skill_End` clip adds 0.233 s. The skill
* holds its effects (below) from the cast to the end of `Skill_End` [ASSUMED: the clips play at 1×, the effects last the
* two clips].
*/
const SLASH_FIRST = 0.4;
const SLASH_GAP = 1 / 6;
const SLASH_END_CLIP = 0.233;
/** S3's buff while it plays: 无敌, 无法阻挡, no normal attack (the skill clip plays) — stun / freeze refused by a hook. */
const FRAGRANCE_KEY = 'acpion:fragrance';
/** Enemies S3 slashes: every selectable enemy on the x-1 skill range, air units included (PRTS 备注 "※可对空"). */
const SLASH_PROFILE = Object.freeze({ canHitFly: true });
export default {
// 郁金香 (尖兵, blocks 2) — numbers from data/backups.json (normal E2 Lv1 rank 4, elite E2 Lv60 rank 7; skill_table and
// PRTS 郁金香(卫戍协议) agree). DP (部署费用) is the player's redeploy currency here (DESIGN §5.5 "DP").
// 无垠之心: from each deployment until her 2nd skill activation of it, SP recovery +sp_recovery_per_sec (module SOL-X Lv3:
// +1/s) — `cnt` 2.
// 浪潮之心: ATK +atk; each enemy she kills +cost DP.
// S1 钻心 (instant): +cost DP, then ATK × atk_scale physical damage to up to max_target enemies of her attack range
// (the enemies she blocks included, ground only like her attack) [ASSUMED: at the cast — her S1 clip `Skill_2`
// carries no hit event].
// S2 迅瞬 (10 s): ASPD +attack_speed, ignores def_penetrate of the target's DEF, and "逐渐获得" trig_cnt × cost DP: one
// every `interval` s from the cast (6 × 1.6 s / 7 × 1.4 s, all inside the 10 s) [ASSUMED: none at the cast, and the
// DP not yet given is lost if the skill ends early].
// S3 只余芬芳 (instant): +cost DP, then `times` slashes on the enemies around her (the x-1 skill range), each ATK ×
// atk_scale physical ignoring def_penetrate (50 %) of DEF, timed by her Spine clip (SLASH_*). PRTS 郁金香 (the 集成战略
// character char_513_apionr, whose S1 只余芬芳 has the same text, blackboard and x-1 range) 备注: "※可对空" and
// "※技能生效期间,持有效果:无敌,无法阻挡,眩晕免疫,冻结免疫" [ASSUMED: the 卫戍 copy skchr_acpion_3 behaves the same].
// Module SOL-X (elite, trait override): "阻挡敌人时攻击力和防御力各+8%" — while she blocks (none during S3: 无法阻挡).
// Triggers (data, nothing set here): S1 / S2 DEFAULT; S3 SKILL_RANGE on its x-1 (any enemy in it, air units too).
char_608_acpion: (bb, chess) => {
const t0 = talentBb(chess, 0), t1 = talentBb(chess, 1), tb = traitBb(chess);
const r1 = skillRec(chess, S1), r2 = skillRec(chess, S2), r3 = skillRec(chess, S3);
const dp = (battle, unit, n) => {
if (!(n > 0)) return;
battle.addDp(unit.ownerId, n);
battle.fx('dp', { x: unit.x, y: unit.y, id: unit.id, n });
};
const skills = {};
if (r1) {
const b1 = r1.bb ?? {};
skills[S1] = {
kind: 'instant',
onStart({ battle, unit }) {
dp(battle, unit, num(b1.cost));
const prof = unit.profile;
const foes = battle.enemiesInKeys(unit.rangeKeys || [], unit, prof);
for (const e of battle.blockedTargets(unit, prof)) if (!foes.includes(e)) foes.push(e);
sortEnemyTargets(battle, unit, foes, prof?.priority ?? null);
const v = foes.slice(0, Math.max(1, Math.floor(num(b1.max_target, 2))));
if (v.length) battle.fx('slash', { x: unit.x, y: unit.y, id: unit.id, n: v.length });
for (const e of v) battle.dealDamage(unit, e, { amount: unit.s.atk * num(b1.atk_scale, 1), type: 'phys', isSkill: true, tags: ['skill'] });
},
};
}
if (r2) {
const b2 = r2.bb ?? {};
const total = Math.max(0, Math.floor(num(b2.trig_cnt, 0))), iv = Math.max(0.1, num(b2.interval, 1)), per = num(b2.cost, 1);
skills[S2] = {
kind: 'duration',
mods: { aspd: num(b2.attack_speed), defIgnorePct: num(b2.def_penetrate) },
onStart({ unit }) { unit.mem.acpionDp = { acc: 0, n: 0 }; },
onTick({ battle, unit, dt }) {
const m = unit.mem.acpionDp;
if (!m) return;
m.acc += dt;
while (m.acc + 1e-9 >= iv && m.n < total) { m.acc -= iv; m.n++; dp(battle, unit, per); }
},
onEnd({ unit }) { unit.mem.acpionDp = null; },
};
}
if (r3) {
const b3 = r3.bb ?? {};
const times = Math.max(1, Math.floor(num(b3.times, 8))), scale = num(b3.atk_scale, 1), pen = num(b3.def_penetrate, 0);
const grid = r3.rangeGrid?.length ? r3.rangeGrid : null;
const lastAt = SLASH_FIRST + (times - 1) * SLASH_GAP;
const slash = (battle, unit) => {
const keys = grid ? absoluteRangeKeys(grid, unit.tileR, unit.tileC, unit.dir, 0) : unit.rangeKeys || [];
const v = battle.enemiesInKeys(keys, unit, SLASH_PROFILE);
if (v.length) battle.fx('slash', { x: unit.x, y: unit.y, id: unit.id, n: v.length });
for (const e of v) battle.dealDamage(unit, e, { amount: unit.s.atk * scale, type: 'phys', defIgnorePct: pen, isSkill: true, tags: ['skill', 'slash'] });
};
skills[S3] = {
kind: 'instant',
onStart({ battle, unit }) {
dp(battle, unit, num(b3.cost));
const seq = unit.deploySeq;
battle.addBuff(unit, {
key: FRAGRANCE_KEY, duration: lastAt + SLASH_END_CLIP,
flags: { invulnerable: true, noBlock: true, disarm: true }, visible: true, source: unit, tags: ['skill'],
});
battle.releaseBlocked(unit);
for (let i = 0; i < times; i++) {
battle.after(SLASH_FIRST + i * SLASH_GAP, () => {
if (up(unit) && unit.deploySeq === seq) slash(battle, unit);
}, { owner: unit });
}
},
};
}
return {
skills,
talents: [
{ install(battle, unit) { // 无垠之心
const per = num(t0.sp_recovery_per_sec), cnt = Math.max(1, Math.floor(num(t0.cnt, 2)));
if (!(per > 0)) return;
battle.on('deploy', ({ unit: u }) => {
if (u !== unit) return;
unit.mem.acpionCasts = 0;
battle.addBuff(unit, { key: 'acpion:boundless', mods: { spRecoveryFlat: per }, tags: ['talent'] });
}, { owner: unit });
battle.on('skillStart', ({ unit: u }) => {
if (u !== unit) return;
unit.mem.acpionCasts = (unit.mem.acpionCasts ?? 0) + 1;
if (unit.mem.acpionCasts >= cnt) battle.removeBuff(unit, 'acpion:boundless');
}, { owner: unit });
} },
{ install(battle, unit) { // 浪潮之心
statBuff(battle, unit, 'acpion:tide', { atkPct: num(t1.atk) });
const n = num(t1.cost);
if (n > 0) battle.on('kill', ({ killer, victim }) => { if (killer === unit && victim?.side === 'enemy') dp(battle, unit, n); }, { owner: unit });
} },
],
install(battle, unit) {
const ma = num(tb.atk, 0), md = num(tb.def, 0); // module SOL-X: 阻挡敌人时攻击力和防御力各+8%
if (ma || md) toggleBuff(battle, unit, 'acpion:sol', () => unit.blocking.length > 0, { atkPct: ma, defPct: md });
// S3: 眩晕免疫, 冻结免疫 while its effects last
if (r3) battle.on('beforeStatus', (c) => {
if (c.target === unit && (c.status === 'stun' || c.status === 'freeze') && unit.findBuff(FRAGRANCE_KEY)) c.cancel = true;
}, { owner: unit });
},
};
},
};