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.
127 lines
6.6 KiB
JavaScript
127 lines
6.6 KiB
JavaScript
// server/sim/content/kits/ops/chess_char_6_14-lumen.js — 流明 (char_4042_lumen) kit, tier 6.
|
||
// Conventions of the tier-6 kits: ../shared/tier6.js; kit contract and rules: ../README.md.
|
||
|
||
import { COLS } from '../../../constants.js';
|
||
import {
|
||
num, bv, tbb, live, onDefaultSkill, instantKind, ABNORMAL, hasAbnormal, cleanseAbnormal,
|
||
} from '../shared/tier6.js';
|
||
|
||
// ------------------------------------------------------------------------------------------------------------------
|
||
// 流明 chess_char_6_14 (疗养师) — S3 灯火不灭; 凡人之愿; 应急处理; module 纯铜单筒望远镜
|
||
|
||
function lumen(bb, chess, def) {
|
||
const t0 = tbb(def, 0), t1 = tbb(def, 1), tb = def?.traitBb || {};
|
||
const isDef = onDefaultSkill(chess);
|
||
const hs = num(bb.heal_scale, 1);
|
||
const abnormalAllies = (battle, unit) => battle.alliesInGrid(unit).filter((a) => hasAbnormal(a)).sort((a, b) => a.hpRatio - b.hpRatio || a.deploySeq - b.deploySeq);
|
||
const cleanse = (battle, t) => { cleanseAbnormal(battle, t); };
|
||
const skills = {
|
||
// S1 沐雨: the next heal also gives its target and the allies around it (8 tiles) aura.heal_scale × ATK healing
|
||
// per second for aura.projectile_life_time s
|
||
skchr_lumen_1: {
|
||
kind: 'instant',
|
||
heal: true,
|
||
attack: { healScale: 1 },
|
||
onHit({ battle, unit, target }) {
|
||
if (!target) return;
|
||
const sc = bv(bb, 'heal_scale'), life = bv(bb, 'projectile_life_time', 4), iv = Math.max(0.2, bv(bb, 'interval', 1));
|
||
if (!(sc > 0) || !(life > 0)) return;
|
||
battle.fx('healField', { x: target.x, y: target.y, id: unit.id });
|
||
for (const a of battle.alliesInRadius(target.x, target.y, 1.5, target.ownerId)) {
|
||
if (a.kind === 'device' || a.s.flags.noHeal || a.profile?.noHeal) continue; // a heal: never on 禁疗 / 孤立
|
||
battle.addBuff(a, {
|
||
key: `lumen:rain:${unit.id}`, duration: life, interval: iv, refresh: 'replace', visible: true, source: unit,
|
||
onTick: ({ unit: x }) => { if (x.hp < x.s.maxHp) battle.heal(unit, x, unit.s.atk * sc, { hot: true }); },
|
||
});
|
||
}
|
||
},
|
||
},
|
||
// S2 沛霖 (2 charges): heal up to max_target allies of her range for heal_scale × ATK; cast with full charges ⇒
|
||
// also cleanse their 异常状态 (蓄力额外效果)
|
||
skchr_lumen_2: {
|
||
kind: instantKind(def),
|
||
heal: true,
|
||
onStart({ battle, unit, skill }) {
|
||
const full = skill.maxCharges > 1 && skill.charges >= skill.maxCharges - 1; // (this cast already spent one)
|
||
const inj = battle.injuredAlliesInKeys(unit.rangeKeys, unit);
|
||
const abn = full ? abnormalAllies(battle, unit).filter((a) => !inj.includes(a)) : [];
|
||
const tgts = inj.concat(abn).slice(0, Math.max(1, Math.floor(num(bb.max_target, 2))));
|
||
for (const t of tgts) {
|
||
battle.heal(unit, t, unit.s.atk * num(bb.heal_scale, 1), { skillHeal: true });
|
||
if (full) cleanse(battle, t);
|
||
}
|
||
battle.fx('heal', { x: unit.x, y: unit.y, id: unit.id, n: tgts.length, charged: full });
|
||
},
|
||
},
|
||
};
|
||
return {
|
||
skills,
|
||
skill: {
|
||
kind: 'ammo',
|
||
heal: true,
|
||
ammo: Math.max(1, Math.floor(num(bb['attack@trigger_time'], 4))),
|
||
mods: { atkPct: num(bb.atk), aspd: num(bb.attack_speed) },
|
||
attack: { healScale: 1 },
|
||
onHit({ battle, unit, target }) { if (unit.mem.lumenAbn && target) cleanse(battle, target); },
|
||
onAttack(ctx) { // only heals on abnormal targets consume a bullet (engine: ctx.noAmmo — no bullet, no ammoUsed)
|
||
if (!ctx.unit.mem.lumenAbn) ctx.noAmmo = true;
|
||
ctx.unit.mem.lumenAbn = false;
|
||
},
|
||
},
|
||
talents: [
|
||
{ install(battle, unit) { // S3 targeting / bullet bookkeeping
|
||
if (!isDef) return;
|
||
battle.on('beforeAttack', (ctx) => {
|
||
if (ctx.attacker !== unit || !unit.skill?.active) return;
|
||
const abn = abnormalAllies(battle, unit);
|
||
unit.mem.lumenAbn = abn.length > 0;
|
||
if (abn.length) ctx.targets = [abn[0]];
|
||
if (ctx.profile && ctx.profile !== unit.profile) ctx.profile.healScale = abn.length ? hs : 1;
|
||
}, { owner: unit });
|
||
battle.on('tick', () => { // abnormal allies at full HP are healed too
|
||
const sk = unit.skill;
|
||
if (!sk?.active || !unit.canAct || unit.atkCd > 0 || unit.s.flags.disarm) return;
|
||
if (battle.injuredAlliesInKeys(unit.rangeKeys, unit).length) return;
|
||
const abn = abnormalAllies(battle, unit);
|
||
if (!abn.length) return;
|
||
battle.forceAttack(unit, [abn[0]]);
|
||
unit.atkCd = unit.s.interval;
|
||
}, { owner: unit });
|
||
} },
|
||
{ install(battle, unit) { // 凡人之愿: healed targets gain 抵抗
|
||
const base = bv(t0, 'status_resistance[limit]', 0), special = num(t0['lumen_t_1[special].status_resistance[limit]'], base);
|
||
const res = -num(t0.one_minus_status_resistance, -0.5), thr = num(t0.hp_ratio, 0.75);
|
||
battle.on('heal', (ctx) => {
|
||
const t = ctx.target;
|
||
if (ctx.source !== unit || !t || !(res > 0) || !(base > 0)) return;
|
||
const after = Math.min(t.s.maxHp, t.hp + ctx.amount);
|
||
battle.applyStatus(t, 'resist', { duration: after / t.s.maxHp > thr ? special : base, value: Math.min(1, res), source: unit });
|
||
}, { owner: unit, priority: -10 });
|
||
} },
|
||
{ install(battle, unit) { // 应急处理
|
||
const sc = num(t1.heal_scale), cd = num(t1.duration, 12);
|
||
let ready = -Infinity;
|
||
battle.on('statusApplied', (ctx) => {
|
||
const t = ctx.target;
|
||
if (!live(unit) || !t || t.side !== 'ally' || !ABNORMAL.has(ctx.status) || battle.time < ready || !(sc > 0)) return;
|
||
if (!unit.rangeKeySet?.has(t.tileR * COLS + t.tileC)) return;
|
||
ready = battle.time + cd;
|
||
battle.heal(unit, t, unit.s.atk * sc);
|
||
battle.fx('emergency', { x: t.x, y: t.y, id: t.id, src: unit.id });
|
||
}, { owner: unit });
|
||
} },
|
||
{ install(battle, unit) { // elite module: taunt −1 and 抵抗
|
||
const taunt = num(tb.taunt_level), res = -num(tb.one_minus_status_resistance);
|
||
if (taunt) battle.addBuff(unit, { key: 'lumen:scope', mods: { taunt }, persist: true, allowDead: true });
|
||
// a permanent 抵抗 (survives death/redeploy): any buff with status 'resist' counts (engine resistOf — the
|
||
// strongest one applies, never compounding with 凡人之愿 / 灵知)
|
||
if (res > 0) battle.addBuff(unit, { key: 'lumen:scopeResist', status: 'resist', visible: true, persist: true, allowDead: true, data: { value: Math.min(1, res) } });
|
||
} },
|
||
],
|
||
};
|
||
}
|
||
|
||
export default {
|
||
chess_char_6_14_a: lumen,
|
||
};
|