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.
105 lines
6.5 KiB
JavaScript
105 lines
6.5 KiB
JavaScript
// server/sim/content/kits/ops/chess_char_5_13-ghost2.js — 归溟幽灵鲨 (char_1023_ghost2) kit, tier 5.
|
||
// Conventions of the tier-5 kits: ../shared/tier5.js; kit contract and rules: ../README.md.
|
||
|
||
import {
|
||
AURA_IV, AURA_DUR, num, talent, traitBb, skillGrid, batPct, mods, isAbyssal, isOp, selectedId, lazySkills, whileOn,
|
||
} from '../shared/tier5.js';
|
||
|
||
export default {
|
||
// ---------------------------------------------------------------------------------------------------------------
|
||
// 归溟幽灵鲨 — dollkeeper (professions.js installDollkeeper: the 替身 form, its switch animations, 阻回). Her <替身> makes
|
||
// no normal attack (PRTS 特性备注 "<替身>不进行普通攻击") and so casts no skill (kit trait `dollNoAttack`).
|
||
// S2 生存的渴望 (15/17 s): ATK/ASPD +, HP never below 1 (PRTS 备注: 不死 — a lethal hit does not switch her meanwhile);
|
||
// when it ends she switches to the 替身 at once (PRTS 修正 "技能结束后立刻切换为<替身>", the text's 视为被击倒: no lethal
|
||
// HP loss, so no 不死 / 复活 effect takes it). T1 拥抱自我: the 替身 slows nearby enemies −40 % and deals 40 % ATK
|
||
// arts/s to them (PRTS 备注 "伤害与减速不可对空": ground enemies only) — once it fights (not during its switch
|
||
// animation). T2 阿戈尔的深邃: Abyssal Hunters in the team max HP +20 %. Module (elite): substitute ATK +15 %.
|
||
// S1 生存的技巧 (duration): swaps HP ratios with the other operator of the skill area (周围) with the lowest HP ratio,
|
||
// ATK +. S3 生存的重压 (duration): BAT +1 s, hits every blocked enemy, ATK +, max HP +; an attacked enemy whose HP ratio
|
||
// is ≥ hers takes attack@atk_scale_ex × ATK phys more, otherwise she loses attack@hp_ratio of her max HP.
|
||
// Module PUM-Y (elite): substitute max HP +20 %.
|
||
chess_char_5_13_a: (bb, chess, def) => {
|
||
const t0 = talent(chess, 0), t1 = talent(chess, 1), tb = traitBb(chess);
|
||
const sid = selectedId(chess, def);
|
||
const aroundGrid = chess?.trait?.rangeGrid ?? [[1, -1], [1, 0], [1, 1], [0, -1], [0, 0], [0, 1], [-1, -1], [-1, 0], [-1, 1]];
|
||
return {
|
||
skills: lazySkills({
|
||
skchr_ghost2_1: () => ({
|
||
kind: 'duration', mods: mods({ atkPct: num(bb.atk) }),
|
||
onStart({ battle, unit }) {
|
||
const t = battle.unitsInGrid(unit, skillGrid(chess, def) ?? aroundGrid, { side: 'ally' }).filter((a) => a !== unit && isOp(a) && a.hp > 0)
|
||
.sort((a, b) => a.hpRatio - b.hpRatio || a.id - b.id)[0];
|
||
if (!t) return;
|
||
const mine = unit.hpRatio, theirs = t.hpRatio;
|
||
unit.hp = Math.max(1, Math.min(unit.s.maxHp, unit.s.maxHp * theirs));
|
||
t.hp = Math.max(1, Math.min(t.s.maxHp, t.s.maxHp * mine));
|
||
battle.fx('hpShare', { x: t.x, y: t.y, id: unit.id, to: t.id });
|
||
},
|
||
}),
|
||
skchr_ghost2_3: () => ({
|
||
kind: 'duration',
|
||
mods: mods({ atkPct: num(bb.atk), hpPct: num(bb.max_hp), batPct: batPct(bb.base_attack_time, chess) }),
|
||
attack: {
|
||
hitAllBlocked: true,
|
||
onEachHit({ battle, unit, target, kind }) {
|
||
if (kind !== 'main' || !target || target.side !== 'enemy') return;
|
||
if (unit.mem.ghostHeavy?.get(target)) {
|
||
if (target.alive) battle.dealDamage(unit, target, { amount: unit.s.atk * num(bb['attack@atk_scale_ex']), type: 'phys', isSkill: true, tags: ['skill', 'ghost2Weight'] });
|
||
} else if (unit.alive) battle.loseHp(unit, unit.s.maxHp * num(bb['attack@hp_ratio']), { source: unit });
|
||
},
|
||
},
|
||
}),
|
||
}),
|
||
skill: {
|
||
kind: 'duration',
|
||
mods: mods({ atkPct: num(bb.atk), aspd: num(bb.attack_speed) }),
|
||
// "技能结束后立刻切换为<替身>": nothing when the skill ended with her (death) or by her switch to the 替身, or she
|
||
// already is one
|
||
onEnd({ battle, unit, reason }) {
|
||
if (reason === 'death' || reason === 'substitute' || !unit.alive || !unit.deployed || unit.trait.doll) return;
|
||
battle.emit('dollSwitch', { unit, reason: 'skill', done: false });
|
||
},
|
||
},
|
||
trait: { dollNoAttack: true },
|
||
talents: [
|
||
{ install(battle, unit) { // 拥抱自我 (the 替身 fighting: not during a switch animation)
|
||
const slow = num(t0.move_speed), scale = num(t0.atk_scale);
|
||
whileOn(battle, unit, AURA_IV, () => {
|
||
if (!unit.trait.doll || unit.trait.dollSwitching || !slow) return;
|
||
for (const e of battle.unitsInGrid(unit, aroundGrid, { side: 'enemy' })) if (!e.isFlying) battle.addBuff(e, { key: 'ghost2:embrace', duration: AURA_DUR, mods: { moveMul: Math.max(0, 1 + slow) } });
|
||
});
|
||
whileOn(battle, unit, 1, () => {
|
||
if (!unit.trait.doll || unit.trait.dollSwitching || !(scale > 0)) return;
|
||
for (const e of battle.unitsInGrid(unit, aroundGrid, { side: 'enemy' })) if (!e.isFlying) battle.dealDamage(unit, e, { amount: unit.s.atk * scale, type: 'arts', tags: ['talent', 'embrace'] });
|
||
});
|
||
} },
|
||
{ install(battle, unit) { // 阿戈尔的深邃
|
||
const hp = num(t1.max_hp);
|
||
if (!hp) return;
|
||
battle.on('battleStart', () => {
|
||
for (const a of battle.getPlayer(unit.ownerId)?.units ?? []) if (isOp(a) && isAbyssal(a)) battle.addBuff(a, { key: 'ghost2:abyss', mods: { hpPct: hp }, persist: true, allowDead: true });
|
||
}, { owner: unit });
|
||
} },
|
||
],
|
||
install(battle, unit) {
|
||
// S2: HP never below 1 while it runs
|
||
if (!sid || sid === 'skchr_ghost2_2') battle.on('fatal', (c) => { if (c.unit === unit && unit.skill?.active) c.prevented = true; }, { owner: unit, priority: 10 });
|
||
if (sid === 'skchr_ghost2_3') { // S3: 生命比例高于或等于自身 — judged when the attack starts
|
||
battle.on('beforeAttack', (c) => {
|
||
if (c.attacker !== unit || !unit.skill?.active) return;
|
||
unit.mem.ghostHeavy = new Map(c.targets.filter(Boolean).map((t) => [t, t.hpRatio >= unit.hpRatio - 1e-9]));
|
||
}, { owner: unit, priority: -100 });
|
||
}
|
||
const hpUp = num(tb.max_hp); // PUM-Y: 替身…生命值提升
|
||
if (hpUp) whileOn(battle, unit, 0.2, () => { if (unit.trait.doll) battle.addBuff(unit, { key: 'ghost2:moduleY', duration: 0.3, mods: { hpPct: hpUp } }); });
|
||
const atk = num(tb.atk);
|
||
if (atk) {
|
||
whileOn(battle, unit, 0.2, () => {
|
||
if (unit.trait.doll) battle.addBuff(unit, { key: 'ghost2:module', duration: 0.3, mods: { atkPct: atk } });
|
||
});
|
||
}
|
||
},
|
||
};
|
||
},
|
||
};
|