PRTS: 分支特性信息 吟游者 「特性为基于自身攻击力来增加受益者的“生命回复速度”属性」 (魔王, 浊心斯卡蒂); the 备注 of 调香师 熏衣草, 瑕光 S2, 铃兰 S3 and 锡人 S2 「生命恢复的提供方式为增加目标的“生命回复速度”属性…不受治疗加成和禁疗影响」 (铃兰: none in the first second, refreshed every second); 异常效果 禁疗 「增减生命回复速度…的效果不会被识别为治疗类能力」. These were heals from the provider, which damage.js refused on 收割者 / 不屈者 / 武者 (noHeal) and under 禁疗 — 隐德来希 gained nothing under 魔王. This is the separation proposed in PR #135 by @SpiritedAwayCN, written anew (the PR's patch is not used): - professions.js bardRegen: the trait is an hpRegen buff per bard refreshed every 0.25 s; 魔王's 微尘 ×1.5 goes through its `bardRegen` hook (both 魔王 kits); 浊心斯卡蒂's pulse and the 海嗣 token use it too. - 调香师 (every ally), 瑕光 S2 (the skill range, removed at its end), 铃兰 S3 (from the first full second, removed at its end), 锡人 (per 炼金单元, its cached ATK): hpRegen buffs instead of battle.heal. - damage.js: a `regen` tick takes no healing multiplier and a heal hook cannot change its amount (no 治疗加成 on any 生命回复速度); 流明's 凡人之愿 and 华法琳's S1 bonus skip a regen tick of their own. Direct heals are unchanged (still refused / scaled); 魔王 S3's redistribution and 纯烬's HoT are untouched. Other per-second recoveries without such a note stay heals (引星棘刺, 塞雷娅 S3, 流明 / 纯烬 HoTs; 余 闲云隐市 and 天马之盔 left open). Golden moved (every one fields 魔王 / 浊心斯卡蒂 / 调香师 / 瑕光 S2 / 铃兰 / 锡人): roster-004, roster-005, roster-006, roster-011, roster-013, roster-015, roster-016, roster-018, roster-023, roster-025, roster-030, roster-032, roster-035, roster-037, roster-039, roster-041, roster-042, roster-045, roster-046; bond-deputShip-high, bond-emptyShip-low, bond-emptyShip-high, bond-indomShip-high, bond-kazimierzShip-low, bond-kazimierzShip-high, bond-sargonShip-low, bond-sargonShip-high, bond-skillfulShip-high; boss-boss_1-pair, boss-boss_1-solo, boss-boss_2-solo, boss-boss_3-pair, boss-boss_3-solo, boss-boss_4-solo, boss-boss_5-pair, boss-boss_5-solo, boss-boss_6-solo, boss-boss_7-pair, boss-boss_7-solo, hidden-boss_8-solo, hidden-boss_9-pair, hidden-boss_9-solo, hidden-boss_10-solo, unite-1; solo-NORMAL-1, solo-HARD-1, solo-ABYSS-1, coop4-ABYSS-7, coop2-NORMAL-8-serverrun, coop2-NORMAL-14-standins. Docs: SIM.md (heal pipeline, bard row), PLAYING.md, kits/README.md (checklist item 11).
152 lines
9.2 KiB
JavaScript
152 lines
9.2 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 regen ×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 });
|
||
}
|
||
});
|
||
// "受到魔王特性效果提升至1.5倍": her trait's 生命回复速度 on that operator (professions.js bardRegen hook)
|
||
battle.on('bardRegen', (c) => {
|
||
if (c.unit === unit && c.target.findBuff(`cetsyr:mote:${unit.id}`)) c.value *= 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 });
|
||
});
|
||
},
|
||
};
|
||
},
|
||
});
|