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,118 @@
// server/sim/content/kits/ops/chess_char_5_12-dusk.js — 夕 (char_2015_dusk) kit, tier 5.
// Conventions of the tier-5 kits: ../shared/tier5.js; kit contract and rules: ../README.md.
import { sortEnemyTargets } from '../../../targeting.js';
import {
HALF_HP, num, on, talent, talentRec, batPct, mods, selectedId, lazySkills, skillRange, moduleRangeUp,
} from '../shared/tier5.js';
/** 夕 S1 "下一次攻击溅射范围扩大" — expanded splash radius (tiles; the splash-caster default is 1.1). PRTS: "溅射半径扩大至1.7". */
const DUSK_SPLASH_RADIUS = 1.7;
/** 夕 S3 "攻击范围与溅射范围扩大" — the expanded splash radius, the same 1.7 as S1 (PRTS). [ASSUMED for S3] */
const DUSK_S3_SPLASH = DUSK_SPLASH_RADIUS;
export default {
// ---------------------------------------------------------------------------------------------------------------
// 夕 — S1 工笔入化 (2 charges): next attack 180 % ATK with an expanded splash. T1 化境: a kill by 夕 or 小自在 → ATK +2 %
// (×15). T2 点睛: first attack after deploying summons 小自在 (25 s) on the target's tile (deployable ground).
// Module (elite): 攻击范围扩大.
// S2 泼墨淋漓 (duration): skill range, ATK +, ASPD +, hits every enemy in range (one hit each: no splash on top);
// arts damage ×damage_scale on enemies below hp_ratio. S3 写意胜形 (duration): skill range, BAT +0.4 s, prefers
// unblocked enemies, splash 1.7, ATK +; every attack summons 小自在 on the target's tile (deployable ground) or moves /
// refreshes the one already out (deployLimit 1: "召唤/刷新一个"), 25 s (the 点睛 token duration).
chess_char_5_12_a: (bb, chess, def) => {
const t0 = talent(chess, 0), t1 = talent(chess, 1);
const sid = selectedId(chess, def);
const tokenId = talentRec(chess, 1)?.tokenKey ?? (chess?.tokens || [])[0] ?? 'token_10015_dusk_drgn';
const tokenDur = num(t1['attack@tokenduration'], 25);
const freeGround = (battle, r, c) => battle.grid.inRect(r, c) && battle.grid.canStand(r, c) && !battle.grid.isObstacle(r, c) && !battle.isReservedTile(r, c);
// 小自在 expiry follows tk.mem.duskUntil (a refresh pushes it back)
const expire = (battle, tk) => {
if (!tk.alive) return;
const left = num(tk.mem.duskUntil) - battle.time;
if (left > 1e-6) battle.after(left, () => expire(battle, tk), { owner: tk });
else battle.retreat(tk, { reason: 'expired', permanent: true });
};
/** Summon 小自在 on (r, c), or move / refresh hers when one is out. */
const summon = (battle, unit, r, c) => {
const mine = battle.allyUnits.find((x) => x.kind === 'token' && x.ownerUnit === unit && x.defId === tokenId && x.alive && x.deployed);
if (mine) {
if (mine.tileR !== r || mine.tileC !== c) {
const fromX = mine.x, fromY = mine.y;
if (!freeGround(battle, r, c) || !battle.relocate(mine, r, c)) return null;
battle.fx('teleport', { x: mine.x, y: mine.y, id: mine.id, fromX, fromY });
}
mine.mem.duskUntil = battle.time + tokenDur;
return mine;
}
if (!freeGround(battle, r, c)) return null;
const tk = battle.spawnToken(unit, tokenId, r, c);
if (!tk) return null;
tk.mem.duskUntil = battle.time + tokenDur;
battle.after(tokenDur, () => expire(battle, tk), { owner: tk });
battle.fx('summon', { x: tk.x, y: tk.y, id: tk.id, token: tokenId });
return tk;
};
return {
skills: lazySkills({
skchr_dusk_2: () => ({
kind: 'duration', mods: mods({ atkPct: num(bb.atk), aspd: num(bb.attack_speed) }),
targeting: skillRange(chess, def, { allInRange: true }),
attack: { splashRadius: 0 },
}),
skchr_dusk_3: () => ({
kind: 'duration', mods: mods({ atkPct: num(bb.atk), batPct: batPct(bb.base_attack_time, chess) }),
targeting: skillRange(chess, def),
attack: { splashRadius: DUSK_S3_SPLASH },
}),
}),
skill: { kind: 'charges', attack: { atkScale: num(bb.atk_scale, 1), splashRadius: DUSK_SPLASH_RADIUS } },
talents: [
{ install(battle, unit) { // 化境
battle.on('kill', (c) => {
const k = c.killer;
if (!k || c.victim.side !== 'enemy' || !(k === unit || (k.kind === 'token' && k.ownerUnit === unit)) || !on(unit)) return;
battle.addBuff(unit, { key: 'dusk:realm', refresh: 'stack', maxStacks: Math.max(1, num(t0.max_stack_cnt, 15)), mods: { atkPct: num(t0.atk) } });
}, { owner: unit });
} },
{ install(battle, unit) { // 点睛
battle.on('deploy', (c) => { if (c.unit === unit) unit.mem.duskSummoned = false; }, { owner: unit });
battle.on('attack', (c) => {
if (c.attacker !== unit || unit.mem.duskSummoned) return;
const t = c.targets.find((x) => x.side === 'enemy');
if (!t) return;
unit.mem.duskSummoned = true;
// "在目标位置(可部署地面)召唤": only on the target's own tile when it is free deployable ground — PRTS 备注:
// otherwise nothing is summoned, and the talent is spent for this deployment either way
summon(battle, unit, Math.round(t.y), Math.round(t.x));
}, { owner: unit });
} },
],
install(battle, unit) {
moduleRangeUp(battle, unit, chess);
if (sid === 'skchr_dusk_2') {
const cut = num(bb.hp_ratio, HALF_HP), mul = num(bb.damage_scale, 1);
battle.on('hit', (c) => {
if (c.source !== unit || !unit.skill?.active || c.dmg.type !== 'arts' || !c.target || c.target.side !== 'enemy') return;
if (c.target.hpRatio < cut) c.dmg.mul *= mul;
}, { owner: unit });
}
if (sid === 'skchr_dusk_3') {
// 优先攻击未阻挡的敌人
battle.on('beforeAttack', (c) => {
if (c.attacker !== unit || !unit.skill?.active || !c.targets.length || !c.targets[0].blockedBy) return;
const list = battle.enemiesInKeys(unit.rangeKeys, unit, unit.profile);
sortEnemyTargets(battle, unit, list, null);
const alt = list.find((e) => !e.blockedBy);
if (alt) c.targets = [alt, ...c.targets.slice(1).filter((x) => x !== alt)];
}, { owner: unit, priority: 10 });
battle.on('attack', (c) => {
if (c.attacker !== unit || !unit.skill?.active) return;
const t = c.targets.find((x) => x.side === 'enemy');
if (t) summon(battle, unit, Math.round(t.y), Math.round(t.x));
}, { owner: unit });
}
},
};
},
};