151 lines
9.1 KiB
JavaScript
151 lines
9.1 KiB
JavaScript
// 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 });
|
||
});
|
||
},
|
||
};
|
||
},
|
||
});
|