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,168 @@
// server/sim/content/kits/ops/chess_char_4_04-ines.js — 伊内丝 (char_4087_ines) kit, tier 4.
// Conventions of the tier-4 kits: ../shared/tier4.js; kit contract and rules: ../README.md.
import { bodyDist } from '../../../body.js';
import { canTargetEnemy } from '../../../targeting.js';
import { AURA, num, tbb, grid, enemiesOnRange, pulse, isSel, alt, instantKind, withDefaults } from '../shared/tier4.js';
export default withDefaults({
// ===== 伊内丝 (agent) S2 暗夜无明 — stealth, +ATK, +1 DP & steal ASPD per attack; talents 影织 / 影哨
// S1 淬影突袭 (next attack: 3 s arts DoT 40 %/55 % ATK per s, not stacking, +2 DP); S3 独影归途 (passive: the
// first deployment leaves a 影哨 and retreats with the redeploy timer refreshed; every deployment: ATK up for
// the skill duration, the sentry is recalled through ≤ 4/5 enemies for 110 %/140 % phys, +1 DP per damage)
chess_char_4_04_a: (bb, chess, def) => {
const t0 = tbb(def, 0), t1 = tbb(def, 1);
const tb = def.traitBb || {};
const g = grid(def.skill?.rangeGrid);
const step = num(bb['attack@steal_atk_speed'], 5), maxSteal = num(bb['attack@steal_atk_speed_max'], 50);
const S3 = isSel(def, 'skchr_ines_3');
const clearAspd = (battle, unit) => {
for (const id of unit.mem.inesAspdVictims || []) { const e = battle.unitById(id); if (e) battle.removeBuff(e, `ines:aspd:${unit.id}`); }
unit.mem.inesAspdVictims = [];
unit.mem.inesAspd = 0;
battle.removeBuff(unit, 'ines:aspdGain');
};
const s3Dur = num(def.skill?.duration, 11);
return {
skills: alt(def, {
skchr_ines_1: () => ({
kind: instantKind(def),
attack: {
onHit({ battle, unit, target }) {
battle.addDp(unit.ownerId, num(bb.cost, 2));
if (!target || !target.alive || target.side !== 'enemy') return;
const scale = num(bb.bleed_atk_scale, 0.4);
// (不叠加): a new DoT replaces the running one
battle.addBuff(target, { key: `ines:bleed:${unit.id}`, duration: num(bb.bleed_duration, 3) + 1e-6, interval: 1, source: unit, visible: true, refresh: 'extend',
onTick: ({ unit: t }) => battle.dealDamage(unit, t, { amount: unit.s.atk * scale, type: 'arts', isSkill: true, tags: ['skill', 'dot'] }) });
},
},
}),
skchr_ines_3: () => ({
kind: 'passive',
onStart({ battle, unit }) {
if (!unit.mem.inesS3Placed) {
// 首次部署: place a 影哨 (her talent's sentry, left on retreat) and leave; 立刻刷新再部署时间 — the redeploy
// (auto, paying her DP cost like every redeploy) follows as soon as it is affordable
unit.mem.inesS3Placed = true;
battle.after(0, () => {
if (!unit.alive || !unit.deployed) return;
battle.retreat(unit, { reason: 'retreat' });
unit.respawnAt = battle.time;
battle.fx('sentry', { x: unit.x, y: unit.y, id: unit.id });
}, { owner: unit });
return;
}
battle.addBuff(unit, { key: 'ines:s3', duration: s3Dur, mods: { atkPct: num(bb.atk) }, visible: true });
// 立刻收回影哨: the sentry flies back to her and hits ≤ max_target enemies on its way (within
// projectile_range of the segment, [ASSUMED] nearest to its start first)
const from = unit.mem.inesSentryAt;
if (!unit.mem.sentry || !from) return;
unit.mem.sentry = null;
unit.mem.inesSentryAt = null;
const ax = from.x, ay = from.y, bx = unit.x, by = unit.y;
const L2 = (bx - ax) ** 2 + (by - ay) ** 2;
const w = num(bb.projectile_range, 1.4);
const hits = [];
for (const e of battle.aliveEnemies()) {
if (!canTargetEnemy(unit, e, { canHitFly: true })) continue;
const t = L2 > 1e-9 ? Math.max(0, Math.min(1, ((e.x - ax) * (bx - ax) + (e.y - ay) * (by - ay)) / L2)) : 0;
const d = bodyDist(e, ax + t * (bx - ax), ay + t * (by - ay));
if (d <= w + 1e-9) hits.push({ e, t, d });
}
hits.sort((a, b) => a.t - b.t || a.d - b.d || a.e.id - b.e.id);
const amount = unit.s.atk * num(bb.atk_scale, 1.1); // one projectile: ATK taken when it flies back
for (const { e } of hits.slice(0, Math.max(1, Math.floor(num(bb.max_target, 4))))) {
battle.dealDamage(unit, e, { amount, type: 'phys', isSkill: true, tags: ['skill', 'sentryRecall'] });
}
battle.fx('beam', { x: ax, y: ay, tx: bx, ty: by, id: unit.id });
},
}),
}),
skill: {
kind: 'duration',
mods: { atkPct: num(bb.atk) },
flags: { stealth: true },
targeting: g ? { rangeGrid: g } : undefined,
onStart({ battle, unit }) { clearAspd(battle, unit); battle.fx('stealth', { x: unit.x, y: unit.y, id: unit.id }); },
onAttack({ battle, unit }) { battle.addDp(unit.ownerId, num(bb.cost, 1)); },
onHit({ battle, unit, target }) {
if (!target || !target.alive || target.side !== 'enemy') return;
const room = maxSteal - (unit.mem.inesAspd ?? 0);
if (room <= 0) return;
const s = Math.min(step, room);
unit.mem.inesAspd = (unit.mem.inesAspd ?? 0) + s;
const key = `ines:aspd:${unit.id}`;
const cur = target.findBuff(key);
battle.addBuff(target, { key, mods: { aspd: (cur?.mods?.aspd ?? 0) - s }, source: unit });
if (!(unit.mem.inesAspdVictims ||= []).includes(target.id)) unit.mem.inesAspdVictims.push(target.id);
battle.addBuff(unit, { key: 'ines:aspdGain', mods: { aspd: unit.mem.inesAspd } });
},
onEnd({ battle, unit }) { clearAspd(battle, unit); },
},
talents: [
{ install(battle, unit) { // 影织: first damage on each enemy → bind 5 s + steal 90 ATK until it dies or Ines leaves
unit.mem.woven = new Set();
unit.mem.atkVictims = [];
const refresh = () => {
unit.mem.atkVictims = unit.mem.atkVictims.filter((id) => battle.unitById(id)?.alive);
const gain = Math.min(num(t0.steal_atk_max, Infinity), num(t0.steal_atk, 90) * unit.mem.atkVictims.length);
if (unit.alive) { if (gain > 0) battle.addBuff(unit, { key: 'ines:atkGain', mods: { atkFlat: gain } }); else battle.removeBuff(unit, 'ines:atkGain'); }
};
battle.on('damaged', (c) => {
const e = c.target;
if (c.source !== unit || !unit.alive || e.side !== 'enemy' || c.type === 'element' || unit.mem.woven.has(e.id)) return;
unit.mem.woven.add(e.id);
if (!e.alive) return;
battle.applyStatus(e, 'bind', { duration: num(t0.duration, 5), source: unit });
battle.addBuff(e, { key: `ines:atkSteal:${unit.id}`, mods: { atkFlat: -num(t0.steal_atk, 90) }, source: unit });
unit.mem.atkVictims.push(e.id);
refresh();
battle.fx('shadowWeave', { x: e.x, y: e.y, id: e.id });
}, { owner: unit });
battle.on('death', (c) => {
if (c.unit.side === 'enemy' && unit.mem.atkVictims.includes(c.unit.id)) refresh();
if (c.unit !== unit) return;
for (const id of unit.mem.atkVictims) { const e = battle.unitById(id); if (e) battle.removeBuff(e, `ines:atkSteal:${unit.id}`); }
unit.mem.atkVictims = [];
unit.mem.woven = new Set();
}, { owner: unit });
} },
{ install(battle, unit) { // 影哨: reveal + −30 % move speed in range; a sentry keeps it after she leaves (max 1)
const mods = { moveMul: Math.max(0, 1 + num(t1.move_speed, -0.3)) };
battle.on('death', (c) => {
if (c.unit !== unit || c.reason === 'expired') return;
unit.mem.sentry = new Set(unit.baseRangeKeys || unit.rangeKeys || []);
battle.fx('sentry', { x: unit.x, y: unit.y, id: unit.id });
}, { owner: unit });
battle.every(AURA, () => {
const set = new Set();
if (unit.alive && unit.deployed) for (const k of unit.rangeKeys || []) set.add(k);
if (unit.mem.sentry) for (const k of unit.mem.sentry) set.add(k);
if (!set.size) return;
for (const e of enemiesOnRange(battle, unit, set)) pulse(battle, e, `ines:sentry:${unit.id}`, mods, { flags: { reveal: true } });
}, { owner: unit });
} },
],
install(battle, unit) {
if (S3) {
// where the talent's 影哨 stays (every leave but an expiry), for the recall of the next deployment
battle.on('death', (c) => { if (c.unit === unit && c.reason !== 'expired') unit.mem.inesSentryAt = { x: unit.x, y: unit.y }; }, { owner: unit, priority: 5 });
// 技能期间每对一个敌人造成伤害就获得1点部署费用
battle.on('damaged', (c) => {
if (c.source !== unit || c.target.side !== 'enemy' || !(c.amount > 0) || c.type === 'element' || !unit.findBuff('ines:s3')) return;
battle.addDp(unit.ownerId, num(bb.cost, 1));
}, { owner: unit });
}
// module (elite): the first retreat's redeploy time is 35 % shorter
const rt = num(tb.respawn_time, 0);
if (!rt) return;
battle.on('death', (c) => {
if (c.unit !== unit || unit.mem.firstRetreatDone || !Number.isFinite(unit.respawnAt)) return;
unit.mem.firstRetreatDone = true;
unit.respawnAt = unit.deathAt + (unit.respawnAt - unit.deathAt) * Math.max(0, 1 + rt);
}, { owner: unit });
},
};
},
});