Pure move: every line of a kit, of a helper and of the tier headers is copied verbatim (only the headers' first
line names the new file); a helper used by one kit moved into that kit's file. Glue only: the generated imports,
`export default { … }` around each registry entry, `export { … }` lists in shared/tier2..6.js, and tier4's
`(bb, chess, def)` default-argument wrapper as shared/tier4.js `withDefaults` (applied in each tier-4 kit file).
kits/index.js loads every kit file with a guarded dynamic import (browser-safe, no directory listing); tierN.js
re-export the tier registry (TIER_KITS) and their old named exports (tier1 helpers + tinmanKit, tier6
PITHST_ELEMENTS). The merged registry, every tier default and named export have the same keys, order, frozenness
and function sources as before. test/docs-consistency.test.js reads the moved code at its new paths.
Golden: GOLDEN_FULL=1 node --test test/golden.test.js unchanged.
147 lines
8.4 KiB
JavaScript
147 lines
8.4 KiB
JavaScript
// server/sim/content/kits/ops/chess_char_6_09-cello.js — 塑心 (char_245_cello) kit, tier 6.
|
||
// Conventions of the tier-6 kits: ../shared/tier6.js; kit contract and rules: ../README.md.
|
||
|
||
import { bodyInKeys } from '../../../body.js';
|
||
import { hasHp } from '../../../damage.js';
|
||
import {
|
||
num, tbb, moduleBb, live, isElite, enemiesIn, selectedSkill, skillGridOf, bstate, onElementHit, elementDmg, aura,
|
||
} from '../shared/tier6.js';
|
||
|
||
// ------------------------------------------------------------------------------------------------------------------
|
||
// 塑心 chess_char_6_09 (巫役) — S1 “黄金的狂喜”; 无词哀歌; 精神逆构; module 强弱法
|
||
|
||
function cello(bb, chess, def) {
|
||
const t0 = tbb(def, 0), t1 = tbb(def, 1), tb = def?.traitBb || {}, mod = moduleBb(chess);
|
||
const sid = selectedSkill(chess, def);
|
||
const isS1 = sid === 'skchr_cello_1' || !(chess?.skills?.length); // S1 = the default (no loadout info ⇒ default)
|
||
// "第二天赋效果提升至N倍" (S3): every 精神逆构 number scaled around 1 (delta × N) while it runs
|
||
const boost = (c) => (c.mem.celloBoost > 0 ? c.mem.celloBoost : 1);
|
||
const scaled = (v, c) => 1 + (v - 1) * boost(c);
|
||
/** S2 partner: the other operator of her range with the highest ATK (re-picked every 0.5 s). */
|
||
const pickPartner = (battle, unit) => {
|
||
unit.mem.celloPick = 0.5;
|
||
const ops = battle.alliesInGrid(unit).filter((a) => a !== unit && a.kind === 'op');
|
||
unit.mem.celloPartner = ops.reduce((best, a) => (!best || a.s.atk > best.s.atk + 1e-9 ? a : best), null);
|
||
};
|
||
const skills = {
|
||
// S2 “安魂的弥撒”: ASPD +, one extra target; while it runs every damage she or the highest-ATK other operator of her
|
||
// range deals to an enemy also adds ep_damage_ratio × her ATK 凋亡损伤 (install)
|
||
skchr_cello_2: {
|
||
kind: 'duration',
|
||
mods: { aspd: num(bb.attack_speed) },
|
||
targeting: { maxTargets: 2 },
|
||
onStart({ battle, unit }) { pickPartner(battle, unit); },
|
||
onTick({ battle, unit, dt }) {
|
||
unit.mem.celloPick -= dt;
|
||
if (unit.mem.celloPick > 0 && live(unit.mem.celloPartner)) return;
|
||
pickPartner(battle, unit);
|
||
},
|
||
onEnd({ unit }) { unit.mem.celloPartner = null; },
|
||
},
|
||
// S3 “自由的探戈”: stops attacking, skill range, ATK +atk, 精神逆构 ×scale_delta_to_one; the other operators of her
|
||
// range with the highest max HP / ATK / DEF get +20 % of that stat
|
||
skchr_cello_3: {
|
||
kind: 'duration',
|
||
mods: { atkPct: num(bb.atk) },
|
||
...(skillGridOf(def) ? { targeting: { rangeGrid: skillGridOf(def) } } : {}),
|
||
attack: { noAttack: true },
|
||
onStart({ unit }) { unit.mem.celloBoost = num(bb.scale_delta_to_one, 1); unit.mem.celloAcc = Infinity; },
|
||
onTick({ battle, unit, dt }) {
|
||
unit.mem.celloAcc += dt;
|
||
if (unit.mem.celloAcc < 0.5) return;
|
||
unit.mem.celloAcc = 0;
|
||
const ops = battle.alliesInGrid(unit).filter((a) => a !== unit && a.kind === 'op');
|
||
const top = (f) => ops.reduce((best, a) => (!best || f(a) > f(best) + 1e-9 ? a : best), null);
|
||
const give = (a, key, mods) => { if (a) battle.addBuff(a, { key, mods, duration: 0.75, refresh: 'replace', source: unit, visible: true }); };
|
||
give(top((a) => a.base.maxHp), 'cello:tango:hp', { hpPct: num(bb['cello_s_3[max_hp].max_hp']) });
|
||
give(top((a) => a.base.atk), 'cello:tango:atk', { atkPct: num(bb['cello_s_3[atk].atk']) });
|
||
give(top((a) => a.base.def), 'cello:tango:def', { defPct: num(bb['cello_s_3[def].def']) });
|
||
},
|
||
onEnd({ unit }) { unit.mem.celloBoost = 1; },
|
||
},
|
||
};
|
||
return {
|
||
skills,
|
||
// S1 "技能未开启时无法普通攻击" and its 未处于损伤爆发期间 target pick belong to S1 only
|
||
trait: isS1 ? { noAttackUnlessSkill: true, priority: 'notBurst' } : null,
|
||
install(battle, unit) {
|
||
if (sid !== 'skchr_cello_2') return;
|
||
const ratio = num(bb.ep_damage_ratio);
|
||
// PRTS 塑心 S2 备注: "受影响的干员即将造成伤害时,因该效果造成的凋亡损伤生效于当次触发的伤害之前,该造成的凋亡损伤的来源
|
||
// 始终为塑心" — a late `hit` handler (after every handler that may cancel the damage), so the 凋亡 lands while the
|
||
// target is alive and may burst before the damage; 无来源 bursts (source null) never trigger it. It rides the
|
||
// damage about to be dealt, so a hit dodged or absorbed afterwards still carried it [ASSUMED].
|
||
battle.on('hit', (ctx) => {
|
||
const s = ctx.source, t = ctx.target, d = ctx.dmg;
|
||
if (!(ratio > 0) || !unit.skill?.active || !live(unit) || !s || !t || t.side !== 'enemy' || !hasHp(t) || !d || d.cancel) return;
|
||
if (d.type === 'element' || d.type === 'elemental' || !(d.amount > 0) || (s !== unit && s !== unit.mem.celloPartner)) return;
|
||
elementDmg(battle, unit, t, 'apoptosis', unit.s.atk * ratio);
|
||
}, { owner: unit, priority: -1000 });
|
||
},
|
||
skill: {
|
||
kind: 'charges',
|
||
targeting: { priority: 'notBurst' },
|
||
attack: {
|
||
atkScale: num(bb.atk_scale, 1),
|
||
onHit({ battle, unit, target }) { if (target && target.alive) elementDmg(battle, unit, target, 'apoptosis', unit.s.atk * num(bb.ep_damage_ratio)); },
|
||
},
|
||
},
|
||
talents: [
|
||
{ install(battle, unit) { // 无词哀歌
|
||
const er = num(t0.ep_damage_ratio), slug = num(t0.sluggish);
|
||
aura(battle, unit, 1, () => {
|
||
for (const e of enemiesIn(battle, unit)) {
|
||
if (er > 0) elementDmg(battle, unit, e, 'apoptosis', unit.s.atk * er, ['talent']);
|
||
if (slug > 0 && e.alive) battle.applyStatus(e, 'sluggish', { duration: slug, source: unit });
|
||
}
|
||
});
|
||
} },
|
||
{ install(battle, unit) { // 精神逆构 (+ module element fragile / field-wide + DoT) and trait module (×1.18 vs elite/leader)
|
||
const amp = num(t1.ep_damage_scale, 1), frag = num(t1.damage_scale, 1) - 1, elite = num(tb.ep_damage_scale, 1);
|
||
// module 音乐家的旅程: "在场时,全场敌人…" (field-wide) + damage_value 元素伤害 per second during the burst
|
||
const dot = num(t1.damage_value), fieldWide = t1.damage_value != null, dotIv = Math.max(0.1, num(t1.interval, 1));
|
||
unit.mem.celloAmp = amp;
|
||
unit.mem.celloWide = fieldWide;
|
||
// one battle-wide handler: every apoptosis fill on an enemy inside some 塑心's range (or anywhere for a field-wide
|
||
// one) × the strongest 精神逆构 (the same effect from two 塑心 does not stack)
|
||
const S = bstate(battle);
|
||
if (!S.cellos) {
|
||
S.cellos = new Set();
|
||
battle.on('elementHit', (ctx) => {
|
||
const t = ctx.target, d = ctx.dmg;
|
||
if (!d || d.type !== 'element' || d.element !== 'apoptosis' || !t || t.side !== 'enemy') return;
|
||
let best = 1;
|
||
for (const c of S.cellos) {
|
||
if (!live(c) || !(c.mem.celloWide || bodyInKeys(t, c.rangeKeySet))) continue;
|
||
const v = scaled(c.mem.celloAmp, c);
|
||
if (v > best) best = v;
|
||
}
|
||
if (best > 1) d.mul *= best;
|
||
});
|
||
}
|
||
if (amp > 1) S.cellos.add(unit);
|
||
// 强弱法 (elite trait): her element damage vs elite / leader enemies ×1.18
|
||
if (elite > 1) onElementHit(battle, unit, (ctx) => (ctx.source === unit && ctx.target.side === 'enemy' && isElite(ctx.target) ? elite : 1));
|
||
// "凋亡损伤爆发期间受到5%的元素脆弱" — the 元素脆弱 status (同名效果取最高)
|
||
if (frag > 0) aura(battle, unit, 0.25, () => {
|
||
for (const e of enemiesIn(battle, unit)) if (e.findBuff('apoptosisBurst')) battle.applyStatus(e, 'elemFragile', { duration: 0.4, value: frag * boost(unit), source: unit });
|
||
});
|
||
if (dot > 0) aura(battle, unit, dotIv, () => {
|
||
for (const e of battle.enemies) {
|
||
if (e.alive && !e.hidden && e.findBuff('apoptosisBurst')) battle.dealDamage(unit, e, { amount: dot * boost(unit), type: 'elemental', element: 'apoptosis', canDodge: false, tags: ['talent', 'elementDmg'] });
|
||
}
|
||
});
|
||
// module 音乐家的旅程 trait: "攻击范围内敌人受到10%的元素脆弱"
|
||
const mFrag = num(mod.damage_scale, 1) - 1;
|
||
if (mFrag > 0) aura(battle, unit, 0.25, () => {
|
||
for (const e of enemiesIn(battle, unit)) battle.applyStatus(e, 'elemFragile', { duration: 0.4, value: mFrag, source: unit });
|
||
});
|
||
} },
|
||
],
|
||
};
|
||
}
|
||
|
||
export default {
|
||
chess_char_6_09_a: cello,
|
||
};
|