Files
ark-stronghold-protocol/server/sim/content/kits/ops/chess_char_4_25-cetsyr.js
T

151 lines
9.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/chess_char_4_25-cetsyr.js — 魔王 (char_4134_cetsyr) kit, tier 4.
// Conventions of the tier-4 kits: ../shared/tier4.js; kit contract and rules: ../README.md.
import { COLS } from '../../../constants.js';
import { canTargetEnemy } from '../../../targeting.js';
import { aggregateMods } from '../../../buffs.js';
import {
AURA, AURA_DUR, num, tbb, moduleBb, grid, enemyHasTag, whileDeployed, toggleBuff, skillActive, isSel, alt,
withDefaults,
} from '../shared/tier4.js';
/**
* 鼓舞 (ba.inspire "获得额外附加的基础属性加成(同类属性取最高)"): +`val` ATK (or max HP) on top of the target's own
* percentage multipliers (the flat mod is compensated, so an ATK+% skill does not scale it), the strongest source wins
* (the ATK key `inspire` is shared with 浊心斯卡蒂's 鼓舞); units flagged `mem.noInspire` (bards: 自身不受鼓舞影响) never
* receive it. Refreshed by an aura: lapses within AURA_DUR once the source stops.
*/
function inspire(battle, target, val, src, stat = 'atk') {
if (!(val > 0) || !target || !target.alive || target.mem?.noInspire) return;
const key = stat === 'hp' ? 'inspire:hp' : 'inspire';
const cur = target.findBuff(key);
if (cur && cur.data && cur.data.src !== src.id && cur.data.val > val && cur.timeLeft > 1e-6) return;
const { add, mul } = aggregateMods(target.buffs.filter((b) => b.key !== key));
const f = stat === 'hp' ? Math.max(0, 1 + (add.hpPct ?? 0)) * (mul.hpMul ?? 1) : Math.max(0, 1 + (add.atkPct ?? 0)) * (mul.atkMul ?? 1);
const flat = f > 1e-6 ? val / f : val;
battle.addBuff(target, { key, mods: stat === 'hp' ? { hpFlat: flat } : { atkFlat: flat }, duration: AURA_DUR, source: src, visible: true, data: { src: src.id, val } });
}
export default withDefaults({
// ===== 魔王 (bard) S3 编织重构现世 — wider range, trait 65 %, motes stay, 鼓舞 +65 % of her max HP, HP redistribution
// S1 往昔萦绕身旁 (SP_FULL toggle, 持续时间无限: trait 20 %/28 %, motes respawn in `talent_cool_down` s); S2 明日渺远不及
// (motes cap +3 and 6 at once, wider orbit — enemies within outside_radius — no longer touch operators: a mote
// hitting an enemy deals 220 %/260 % ATK true damage + 3 s bind and respawns like any mote [ASSUMED]; 鼓舞: other
// allies in range ATK + 65 %/80 % of her ATK)
chess_char_4_25_a: (bb, chess, def) => {
const t0 = tbb(def, 0), t1 = tbb(def, 1), mb = moduleBb(def);
const g = grid(def.skill?.rangeGrid);
const redistIv = Math.max(0.5, num(bb['attack@cetsyr_s_3[cal_hp_ratio].interval'], 2));
const S1 = isSel(def, 'skchr_cetsyr_1'), S2 = isSel(def, 'skchr_cetsyr_2'), S3 = isSel(def, 'skchr_cetsyr_3');
const baseCnt = Math.max(0, Math.floor(num(t0.cnt, 3)));
const MOTE_GAP = 0.5; // [ASSUMED] minimal time between two motes hitting the same enemy (s)
const setTrait = (unit, v) => { unit.mem.bardBase ??= unit.profile.auraRatio; unit.profile.auraRatio = num(v, unit.mem.bardBase); };
const resetTrait = (unit) => { if (unit.mem.bardBase != null) unit.profile.auraRatio = unit.mem.bardBase; };
return {
skills: alt(def, {
skchr_cetsyr_1: () => ({
kind: 'toggle',
trigger: 'SP_FULL', // 自动触发 (no 技能策略: the 吟游者 row is for MANUAL skills): on as soon as it is ready
onStart({ battle, unit }) { setTrait(unit, bb['attack@atk_to_hp_recovery_ratio']); battle.fx('mote', { x: unit.x, y: unit.y, id: unit.id }); },
onEnd({ unit }) { resetTrait(unit); },
}),
skchr_cetsyr_2: () => ({
kind: 'duration',
onStart({ battle, unit }) {
// “微尘”上限+3,立刻获得6枚“微尘”
unit.mem.motes = new Array(baseCnt + 3).fill(-Infinity);
unit.mem.moteHit = new Map();
battle.fx('reweave', { x: unit.x, y: unit.y, id: unit.id, n: unit.mem.motes.length });
},
onTick({ battle, unit }) { // 鼓舞: other allies in range ATK + x % of her ATK
for (const a of battle.alliesInGrid(unit)) if (a !== unit) inspire(battle, a, unit.s.atk * num(bb['attack@atk'], 0.65), unit);
},
onEnd({ battle, unit }) {
// back to the normal cap: the motes that come back first are kept
unit.mem.motes = (unit.mem.motes || []).slice().sort((a, b) => a - b).slice(0, baseCnt);
unit.mem.moteHit = null;
},
}),
}),
skill: {
kind: 'duration',
targeting: g ? { rangeGrid: g } : undefined,
onStart({ battle, unit }) {
setTrait(unit, bb['attack@atk_to_hp_recovery_ratio']);
unit.mem.redist = 0;
battle.fx('reweave', { x: unit.x, y: unit.y, id: unit.id });
},
onTick({ battle, unit, dt }) {
const others = battle.alliesInGrid(unit).filter((a) => a !== unit);
for (const a of others) inspire(battle, a, unit.s.maxHp * num(bb.max_hp, 0.65), unit, 'hp');
unit.mem.redist += dt;
if (unit.mem.redist + 1e-9 < redistIv) return;
unit.mem.redist -= redistIv;
const pool = battle.alliesInGrid(unit).filter((a) => !(a.s.flags.noHeal || a.profile?.noHeal) || a === unit);
let hp = 0, max = 0;
for (const a of pool) { hp += a.hp; max += a.s.maxHp; }
if (!(max > 0) || pool.length < 2) return;
const ratio = Math.min(1, hp / max);
for (const a of pool) a.hp = Math.max(1, Math.min(a.s.maxHp, a.s.maxHp * ratio));
battle.fx('redistribute', { x: unit.x, y: unit.y, id: unit.id, ratio: Math.round(ratio * 1000) / 1000 });
},
onEnd({ unit }) { if (unit.mem.bardBase != null) unit.profile.auraRatio = unit.mem.bardBase; },
},
talents: [
{ install(battle, unit) { // 过往尘埃: 3 motes; touching an operator ⇒ her trait heal ×1.5 on it for 6 s; mote back after 6 s
unit.mem.motes = new Array(baseCnt).fill(-Infinity);
unit.mem.noInspire = true; // trait: 自身不受鼓舞影响 (another bard's 鼓舞 skips her)
const cooldown = () => (S1 && skillActive(unit) ? num(bb.talent_cool_down, 3) : num(t0.cooldown, 6)); // S1: 重生速度加快
const takeMote = () => {
const i = unit.mem.motes.findIndex((t) => t <= battle.time + 1e-9);
if (i < 0) return false;
unit.mem.motes[i] = battle.time + cooldown();
return true;
};
whileDeployed(battle, unit, 0.25, () => {
if (S2 && skillActive(unit)) {
// S2: motes orbit wider and hit enemies instead of operators
const hitAt = unit.mem.moteHit || (unit.mem.moteHit = new Map());
const foes = battle.foesInRadius(unit.x, unit.y, num(bb.outside_radius, 2)).filter((e) => canTargetEnemy(unit, e, { canHitFly: true }))
.sort((a, b) => a.id - b.id);
for (const e of foes) {
if ((hitAt.get(e.id) ?? -Infinity) > battle.time + 1e-9) continue;
if (!takeMote()) break;
hitAt.set(e.id, battle.time + MOTE_GAP);
battle.dealDamage(unit, e, { amount: unit.s.atk * num(bb.atk_scale, 2.2), type: 'true', isSkill: true, tags: ['skill', 'mote'] });
if (e.alive) battle.applyStatus(e, 'bind', { duration: num(bb.unmoveable_duration, 3), source: unit });
battle.fx('mote', { x: e.x, y: e.y, id: e.id });
}
return;
}
const keep = S3 && skillActive(unit); // S3: “微尘”不再消失
for (const a of battle.alliesInRadius(unit.x, unit.y, num(t0.range_radius, 1.15), unit.ownerId)) {
if (a === unit || a.kind !== 'op' || a.findBuff(`cetsyr:mote:${unit.id}`)) continue;
if (!keep && !takeMote()) break;
battle.addBuff(a, { key: `cetsyr:mote:${unit.id}`, duration: num(t0.talent_duration, 6), visible: true, source: unit });
battle.fx('mote', { x: a.x, y: a.y, id: a.id });
}
});
battle.on('heal', (c) => {
if (c.source === unit && c.opts?.aura && c.target.findBuff(`cetsyr:mote:${unit.id}`)) c.amount *= num(t0['attack@trait_mul'], 1.5);
}, { owner: unit });
} },
{ install(battle, unit) { // 魔王残响: every ally takes −10 % damage from 萨卡兹 enemies (while she is in the squad)
battle.on('hit', (c) => {
if (c.target.side === 'ally' && c.target.ownerId === unit.ownerId && enemyHasTag(c.source, 'sarkaz')) c.dmg.mul *= 1 - num(t1.damage_resistance, 0.1);
}, { owner: unit });
} },
],
install(battle, unit) {
const a = num(mb.atk, 0), cnt = num(mb.cnt, 2); // module (elite): ≥ 2 other operators in her normal range ⇒ ATK +8 %
if (!a) return;
whileDeployed(battle, unit, AURA, () => {
const set = new Set(unit.baseRangeKeys || []);
const n = battle.allies(unit.ownerId).filter((o) => o !== unit && o.kind === 'op' && set.has(o.tileR * COLS + o.tileC)).length;
toggleBuff(battle, unit, 'cetsyr:module', n >= cnt, { atkPct: a });
});
},
};
},
});