feedback5 R4: one file per operator kit — kits/tier1..6.js split into kits/ops/<chessId>-<codename>.js (129 kits), kits/shared/tier1..6.js (the helpers two or more kits of a tier use, and each tier's header notes) and kits/index.js (the registry, same keys in the same order); the tier files become re-export shims

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.
This commit is contained in:
sganggs committed 2026-10-05 16:13:56 +08:00
1 parent 6b489b1776
commit 1169102d46
143 files changed
+12924 -11784

No files matched your search

@@ -0,0 +1,144 @@
// server/sim/content/kits/ops/chess_char_6_20-agoat2.js — 纯烬艾雅法拉 (char_1016_agoat2) 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, parseN, WHOLE_FIELD, bstate, aura } from '../shared/tier6.js';
// ------------------------------------------------------------------------------------------------------------------
// 纯烬艾雅法拉 chess_char_6_20 (行医) — S3 火山回响; 氤氲; 火山灰疗愈
function agoat2(bb, chess, def) {
const t0 = tbb(def, 0), t1 = tbb(def, 1), tb = def?.traitBb || {};
const hs = num(bb['attack@heal_scale'], 1);
const shots = Math.max(1, parseN(def?.skill?.description, /(\d+)连发/, 1));
const elemLoad = (a) => (a.elem ? a.elem.burn + a.elem.neural + a.elem.necrosis + a.elem.apoptosis + a.elem.erosion : 0);
const skills = {
// S1 无声润物 (toggle, heal): ATK +atk, one extra heal target, every ally of her range recovers ep_heal_ratio × ATK
// 元素损伤 per second
skchr_agoat2_1: {
kind: 'toggle',
heal: true,
mods: { atkPct: num(bb.atk) },
targeting: { maxTargets: 2 },
onStart({ unit }) { unit.mem.agoatAcc = 0; },
onTick({ battle, unit, dt }) {
unit.mem.agoatAcc = (unit.mem.agoatAcc || 0) + dt;
if (unit.mem.agoatAcc + 1e-9 < 1) return;
unit.mem.agoatAcc -= 1;
const v = unit.s.atk * bv(bb, 'ep_heal_ratio');
if (v > 0) for (const a of battle.alliesInGrid(unit)) battle.reduceElement(a, v);
},
},
// S2 云霭荫佑 (heal): one heal (her normal amount + element recovery) on every ally of her range, then a barrier
// over that range for `duration` s absorbing agoat2_s_2[shield].atk_scale × ATK of 元素损伤 in all (install)
skchr_agoat2_2: {
kind: 'instant',
heal: true,
onStart({ battle, unit }) {
const er = num(unit.profile?.heal?.elementHealRatio, num(tb.ep_heal_ratio, 0.5));
const allies = battle.alliesInGrid(unit);
for (const a of allies) {
if (er > 0) battle.reduceElement(a, unit.s.atk * er);
if (a.hp < a.s.maxHp) battle.heal(unit, a, unit.s.atk, { skillHeal: true });
}
const S = bstate(battle);
S.agoatVeils ??= [];
S.agoatVeils.push({ keys: new Set(unit.rangeKeys || []), pool: unit.s.atk * bv(bb, 'atk_scale', 5), until: battle.time + num(bb.duration, 12), src: unit });
battle.fx('shield', { x: unit.x, y: unit.y, id: unit.id, duration: num(bb.duration, 12) });
if (!S.agoatVeilHook) {
S.agoatVeilHook = true;
battle.on('elementHit', (ctx) => { // the barrier absorbs 元素损伤 of the allies inside it
const t = ctx.target, d = ctx.dmg;
if (!t || t.side !== 'ally' || !d || d.type !== 'element' || d.cancel) return;
S.agoatVeils = S.agoatVeils.filter((v) => v.pool > 1e-9 && battle.time < v.until);
const k = t.tileR * COLS + t.tileC;
for (const v of S.agoatVeils) {
if (!v.keys.has(k)) continue;
const eff = d.amount * d.mul;
if (!(eff > 0)) return;
const take = Math.min(v.pool, eff);
v.pool -= take;
if (take >= eff - 1e-9) { d.cancel = true; return; }
d.mul *= (eff - take) / eff;
}
}, { priority: -50 });
}
},
},
};
return {
skills,
install(battle, unit) {
// module 想要留下的生命: "攻击范围内存在受到元素损伤的友方单位时,攻击速度+8"
const as = num(tb.attack_speed);
if (as) aura(battle, unit, 0.25, () => {
if (battle.alliesInGrid(unit).some((a) => elemLoad(a) > 0)) battle.addBuff(unit, { key: 'agoat2:linger', mods: { aspd: as }, duration: 0.4, refresh: 'replace' });
});
},
skill: {
kind: 'duration',
heal: true,
targeting: { rangeGrid: WHOLE_FIELD, maxTargets: shots },
attack: { healScale: hs },
onStart({ skill, unit }) {
const h = unit.profile?.heal || { mode: 'single' };
skill.spec.attack.heal = { ...h, elementHealRatio: num(h.elementHealRatio) * hs };
},
},
talents: [
{ install(battle, unit) { // S3: 5 shots, different targets first
battle.on('beforeAttack', (ctx) => {
if (ctx.attacker !== unit || !unit.skill?.active || !ctx.targets.length || ctx.targets.length >= shots) return;
const base = ctx.targets.slice();
const out = [];
for (let i = 0; i < shots; i++) out.push(base[i % base.length]);
ctx.targets = out;
}, { owner: unit });
} },
{ install(battle, unit) { // 氤氲
// "普通治疗使目标每秒额外受到一次治疗量和元素损伤回复量为10%的增益治疗,持续6秒(最多叠加3层)" — PRTS 备注: "本天赋受
// 特性治疗倍率影响,使用缓存攻击力;叠加时,重置持续时间并更新缓存攻击力": a 10 % heal shaped like her normal one —
// HP heal_scale × ATK and 元素损伤 recovery heal_scale × the trait's ep_heal_ratio × ATK (0.5 / 0.6: 5 % / 6 %,
// it used to be the full 10 %) — per stack, with the ATK of the last application; a new stack refreshes them all.
const sc = num(t0.heal_scale), dur = num(t0.duration, 6), max = Math.max(1, Math.floor(num(t0.max_stack_cnt, 3)));
if (!(sc > 0)) return;
const epRatio = () => num(unit.profile?.heal?.elementHealRatio, num(tb.ep_heal_ratio, 0.5));
battle.on('heal', (ctx) => {
const t = ctx.target;
if (ctx.source !== unit || ctx.opts?.hot || ctx.opts?.aura || ctx.opts?.regen || ctx.opts?.skillHeal || !t) return;
// keyed per 纯烬: two of them (co-op) each keep their own stacks, ATK and heal source
const b = battle.addBuff(t, {
key: `agoat2:mist:${unit.id}`, duration: dur, refresh: 'stack', stacks: 1, maxStacks: max, interval: 1, source: unit, data: {},
onTick: ({ unit: a, buff }) => {
const amt = num(buff.data.atk, unit.s.atk) * sc * Math.max(1, buff.stacks);
battle.reduceElement(a, amt * epRatio());
if (a.hp < a.s.maxHp) battle.heal(unit, a, amt, { hot: true });
},
});
if (b) b.data.atk = unit.s.atk;
}, { owner: unit });
} },
{ install(battle, unit) { // 火山灰疗愈 (×talent_scale during S3)
// "受到的元素损伤降低" is a 元素损伤 multiplier applied on the element hit (PRTS 元素 "…后续可应用元素损伤倍率
// 提升/降低等效果"), not `elemTakenMul` (元素伤害 / 元素脆弱): the aura buff carries the cut, `elementHit` applies it
const hp = num(t1.max_hp), er = num(t1.ep_damage_resistance), mulS = num(bb.talent_scale, 1);
aura(battle, unit, 0.5, () => {
const f = unit.skill?.active ? mulS : 1;
for (const a of battle.alliesInGrid(unit)) {
battle.addBuff(a, { key: 'agoat2:ash', mods: { hpPct: hp * f }, data: { epCut: Math.min(1, er * f) }, source: unit, duration: 0.75, refresh: 'replace' });
}
});
battle.on('elementHit', (ctx) => {
const d = ctx.dmg, b = ctx.target?.findBuff?.('agoat2:ash');
if (!b || b.source !== unit || !d || d.type !== 'element') return;
const cut = num(b.data?.epCut);
if (cut > 0) d.mul *= Math.max(0, 1 - cut);
}, { owner: unit, priority: 20 });
} },
],
};
}
export default {
chess_char_6_20_a: agoat2,
};