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,105 @@
// server/sim/content/kits/ops/chess_char_6_12-rosmon.js — 迷迭香 (char_391_rosmon) kit, tier 6.
// Conventions of the tier-6 kits: ../shared/tier6.js; kit contract and rules: ../README.md.
import { sortEnemyTargets } from '../../../targeting.js';
import { summonToken } from '../../tokens.js';
import { num, tbb, live, opsOf, selectedSkill, batOf } from '../shared/tier6.js';
// ------------------------------------------------------------------------------------------------------------------
// 迷迭香 chess_char_6_12 (投掷手) — S2 末梢阻断; 歼灭战装备; 感知稳定
/** S2 末梢阻断 "溅射范围扩大": radius 1.5 (PRTS 溅射半径一览, 技能: 迷迭香 末梢阻断 1.5; ×1.3 [ASSUMED] until 0.1.1). */
const ROSMON_S2_SPLASH = 1.5;
function rosmon(bb, chess, def) {
const t0 = tbb(def, 0), t1 = tbb(def, 1);
const sid = selectedSkill(chess, def);
const prob = num(bb['attack@prob']), stun = num(bb['attack@stun']);
const gearId = (chess?.tokens || []).find((t) => /rosmon_shield/.test(String(t))) || 'token_10012_rosmon_shield';
const skills = {
// S1 思维膨大 (attack SP): the next attack also deals extra_atk_scale × ATK arts to every enemy it hits
skchr_rosmon_1: {
kind: 'instant',
attack: {
onEachHit({ battle, unit, target, kind }) {
if (!target || !target.alive || (kind !== 'main' && kind !== 'splash')) return;
battle.dealDamage(unit, target, { amount: unit.s.atk * num(bb.extra_atk_scale, 1), type: 'arts', isSkill: true, tags: ['skill'] });
},
},
},
// S3 “如你所愿”: base attack time ×0.5 (akdata: 乘算负数), ATK +atk, 2 targets but blocked enemies only (install);
// two 战术装备 on melee tiles of her range (the token kit: appear stun, blocked enemies DEF −160, lifetime)
skchr_rosmon_3: {
kind: 'duration',
mods: { batPct: batOf(bb.base_attack_time, def, true), atkPct: num(bb.atk) },
targeting: { maxTargets: Math.max(1, Math.floor(num(bb['attack@max_target'], 2))) },
onStart({ battle, unit }) {
let n = 0;
for (let i = 0; i < 2; i++) if (summonToken(battle, unit, gearId, 'melee')) n++;
battle.fx('summon', { x: unit.x, y: unit.y, id: unit.id, n });
},
},
};
return {
skills,
install(battle, unit) {
if (sid !== 'skchr_rosmon_3') return;
battle.on('beforeAttack', (ctx) => { // "仅选择被阻挡的敌人作为目标"
if (ctx.attacker !== unit || !unit.skill?.active) return;
// her range, plus the enemies she blocks herself — always her targets (Battle.blockedTargets, DESIGN §20.3)
const c = battle.enemiesInKeys(unit.rangeKeys, unit, ctx.profile);
for (const e of battle.blockedTargets(unit, ctx.profile)) if (!c.includes(e)) c.push(e);
for (let i = c.length - 1; i >= 0; i--) if (!c[i].blockedBy) c.splice(i, 1);
sortEnemyTargets(battle, unit, c, ctx.profile?.priority ?? null);
ctx.targets = c.slice(0, Math.max(1, Math.floor(num(ctx.profile?.maxTargets, 2))));
}, { owner: unit });
},
skill: {
kind: 'duration',
mods: { batPct: batOf(bb.base_attack_time, def), atkPct: num(bb.atk) },
onStart({ unit }) {
const p = unit.profile;
unit.mem.rosSaved = { r: p.splashRadius, n: p.shockTimes };
p.splashRadius = Math.max(num(p.splashRadius, 0.9), ROSMON_S2_SPLASH);
p.shockTimes = num(p.shockTimes, 2) + Math.floor(num(bb.add_times));
},
onEnd({ unit }) {
const s = unit.mem.rosSaved;
if (s) { unit.profile.splashRadius = s.r; unit.profile.shockTimes = s.n; }
unit.mem.rosSaved = null;
},
},
talents: [
{ install(battle, unit) { // S2: attack & aftershock stun chance
battle.on('damaged', (ctx) => {
if (ctx.source !== unit || !unit.skill?.active || !(stun > 0) || !ctx.target.alive || ctx.target.side !== 'enemy') return;
if (!ctx.dmg?.isAttack && !ctx.dmg?.tags?.includes('aftershock')) return;
if (battle.rng() < prob) battle.applyStatus(ctx.target, 'stun', { duration: stun, source: unit });
}, { owner: unit });
} },
{ install(battle, unit) { // 歼灭战装备
const v = num(t0.def_penetrate_fixed);
if (v) battle.addBuff(unit, { key: 'rosmon:pierce', mods: { defIgnoreFlat: v }, persist: true, allowDead: true });
} },
{ install(battle, unit) { // 感知稳定
const atk = num(t1.atk);
battle.on('deploy', ({ unit: u }) => {
if (u !== unit || !atk) return;
battle.after(0, () => {
if (!live(unit)) return;
const casters = opsOf(battle, unit.ownerId).filter((a) => a.def?.profession === 'CASTER');
const pick = battle.rng.pick(casters);
if (!pick) return;
battle.addBuff(unit, { key: 'rosmon:stable', mods: { atkPct: atk } });
battle.addBuff(pick, { key: 'rosmon:stable', mods: { atkPct: atk } });
battle.fx('link', { x: pick.x, y: pick.y, id: pick.id, src: unit.id });
}, { owner: unit });
}, { owner: unit });
} },
],
};
}
export default {
chess_char_6_12_a: rosmon,
};