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,177 @@
// server/sim/content/kits/ops/chess_char_5_02-titi.js — 缇缇 (char_4056_titi) kit, tier 5.
// Conventions of the tier-5 kits: ../shared/tier5.js; kit contract and rules: ../README.md.
import { COLS } from '../../../constants.js';
import {
AURA_IV, AURA_DUR, RING1, num, on, talent, mods, dist, inFaction, isOp, selectedId, lazySkills, whileOn,
} from '../shared/tier5.js';
const isSargonMinos = (u) => inFaction(u, 'sargonShip', ['sargon', 'minos']);
/** Enemy not moving along its route (blocked, stunned, rooted, waiting…). */
const isStill = (e) => !!(e.blockedBy || !e.moving || e.s.flags.stun || e.s.flags.noMove || !(e.s.moveSpeed > 0));
export default {
// ---------------------------------------------------------------------------------------------------------------
// 缇缇 — S3 旧日绽放 (25 s): ATK +, 2 targets, sleeps non-sleeping targets; during the skill every enemy waking up (or
// knocked out asleep) takes arts damage growing with the sleep time (linear min_atk_scale → max_atk_scale over `sleep` s,
// PRTS 备注) and sends the highest-aggro enemy within range_radius to sleep; other allies in range fall asleep (小睡)
// instead of dying. T1 凝固的时光: can attack sleeping enemies, +15 % ATK arts vs non-moving
// enemies, sleeping enemies take 30 % ATK arts/s. T2 勇气的报偿: Sargon/Minos ops above 50 % HP get +20 ASPD.
// S1 缓蚀 (duration): ATK +, attack@prob chance per attack to sleep the target attack@sleep s.
// S2 封护 (duration): no attacks, ATK +; she and the lowest-HP-ratio operator in her range sleep until the skill ends
// (沉睡 = invulnerable), enemies around either of them keep falling asleep; T1 ×talent_scale.
chess_char_5_02_a: (bb, chess, def) => {
const t0 = talent(chess, 0), t1 = talent(chess, 1);
const sid = selectedId(chess, def);
const hitSleep = num(bb['attack@sleep'], num(bb.sleep, 5));
const minS = num(bb.min_atk_scale, 1), maxS = num(bb.max_atk_scale, minS);
const chainSleep = num(bb.sleep, hitSleep), radius = num(bb.range_radius, 1.5), chainN = Math.max(1, num(bb.max_target, 1));
// S2 封护: "第一天赋效果提升至N倍" while it runs
const t0Scale = (unit) => (sid === 'skchr_titi_2' && unit.skill?.active ? num(bb.talent_scale, 1) : 1);
const ward = (battle, unit, a) => {
const entered = !a.buffs.some((b) => (b.status ?? b.key) === 'sleep');
battle.addBuff(a, { key: 'titi:ward', flags: { sleep: true }, visible: true, status: 'sleep', data: { src: unit } });
battle.releaseBlocked(a);
if (battle.hasHook('statusApplied')) battle.emit('statusApplied', { source: unit, target: a, status: 'sleep', duration: Infinity, entered });
};
return {
skills: lazySkills({
skchr_titi_1: () => ({
kind: 'duration', mods: mods({ atkPct: num(bb.atk) }),
attack: {
onHit({ battle, unit, target }) {
if (!target || !target.alive || target.s.flags.sleep || !battle.rng.chance(num(bb['attack@prob']))) return;
battle.applyStatus(target, 'sleep', { duration: num(bb['attack@sleep']), source: unit });
},
},
}),
skchr_titi_2: () => ({
kind: 'duration', mods: mods({ atkPct: num(bb.atk) }),
attack: { noAttack: true },
onStart({ battle, unit }) {
const pick = battle.alliesInGrid(unit).filter((a) => a !== unit && isOp(a) && a.hp > 0)
.sort((a, b) => a.hpRatio - b.hpRatio || a.id - b.id)[0] ?? null;
unit.mem.titiWard = pick ? [unit, pick] : [unit];
for (const a of unit.mem.titiWard) ward(battle, unit, a);
unit.mem.titiWardAcc = AURA_IV;
battle.fx('sleepGuard', { x: unit.x, y: unit.y, id: unit.id });
},
onTick({ battle, unit, dt }) {
unit.mem.titiWardAcc += dt;
if (unit.mem.titiWardAcc < AURA_IV - 1e-9) return;
unit.mem.titiWardAcc = 0;
for (const a of unit.mem.titiWard || []) {
if (!a.alive || !a.deployed || !a.findBuff('titi:ward')) continue;
for (const e of battle.foesInRadius(a.x, a.y, RING1)) if (e.alive) battle.applyStatus(e, 'sleep', { duration: AURA_DUR, source: unit });
}
},
onEnd({ battle, unit }) {
for (const a of unit.mem.titiWard || []) {
const b = a.findBuff('titi:ward');
if (b && b.data.src === unit) battle.removeBuff(a, b);
}
unit.mem.titiWard = null;
},
}),
}),
trait: { hitSleep: true }, // 凝固的时光 (a): she targets and damages sleeping enemies (沉睡 = 无敌 for everyone else)
skill: {
kind: 'duration',
mods: mods({ atkPct: num(bb.atk) }),
attack: {
maxTargets: Math.max(1, num(bb['attack@max_target'], 2)),
onHit({ battle, unit, target }) {
if (!target || !target.alive || target.s.flags.sleep) return;
battle.applyStatus(target, 'sleep', { duration: hitSleep, source: unit });
},
},
onEnd({ battle, unit }) {
for (const a of battle.allyUnits) {
const b = a.findBuff('titi:allySleep');
if (b && b.data.src === unit) battle.removeBuff(a, b);
}
},
},
talents: [
{ install(battle, unit) { // 凝固的时光 — (a) is the `hitSleep` trait above
// (b) +extra_atk_scale arts vs non-moving enemies
battle.on('damaged', (c) => {
if (c.source !== unit || !c.dmg?.isAttack || c.dmg.isSplash || c.type === 'element') return;
const e = c.target;
if (e.side !== 'enemy' || !e.alive || !isStill(e)) return;
battle.dealDamage(unit, e, { amount: unit.s.atk * num(t0.extra_atk_scale) * t0Scale(unit), type: 'arts', tags: ['talent', 'titiStill'] });
}, { owner: unit });
// (c) sleeping enemies take damage_atk_scale × ATK arts per second
whileOn(battle, unit, 1, () => {
const amount = unit.s.atk * num(t0.damage_atk_scale) * t0Scale(unit);
for (const e of battle.enemies) {
if (e.alive && !e.hidden && e.s.flags.sleep) battle.dealDamage(unit, e, { amount, type: 'arts', tags: ['talent', 'titiDream'] });
}
});
} },
{ install(battle, unit) { // 勇气的报偿: 精力充沛
whileOn(battle, unit, AURA_IV, () => {
for (const a of battle.allies()) {
if (isOp(a) && isSargonMinos(a) && a.hpRatio > num(t1.hp_ratio, 0.5)) battle.addBuff(a, { key: 'titi:vigor', duration: AURA_DUR, mods: { aspd: num(t1.attack_speed) } });
}
});
} },
],
install(battle, unit) {
if (sid && sid !== 'skchr_titi_3') return;
// S3: wake-up burst chain (sleep applied by anyone counts)
const asleep = new Map();
// PRTS 备注: "向1.5半径内除该单位外仇恨值最高的敌方单位施加沉睡" — highest aggro (taunt) first, then the nearest
const sleepOthers = (from) => {
const cands = battle.foesInRadius(from.x, from.y, radius).filter((o) => o !== from && !o.s.flags.sleep && !o.mem?.candleOwner)
.sort((a, b) => (b.s.taunt || 0) - (a.s.taunt || 0) || dist(a, from) - dist(b, from) || a.spawnSeq - b.spawnSeq);
let n = 0;
for (const o of cands) {
if (n >= chainN) break;
if (battle.applyStatus(o, 'sleep', { duration: chainSleep, source: unit })) n++;
}
};
battle.on('statusApplied', (c) => {
if (c.status === 'sleep' && c.target.side === 'enemy' && !asleep.has(c.target)) asleep.set(c.target, battle.time);
}, { owner: unit });
battle.on('tick', () => {
if (!asleep.size) return;
for (const [e, t0s] of asleep) {
if (!e.alive) { asleep.delete(e); continue; }
if (e.s.flags.sleep) continue;
asleep.delete(e);
if (!unit.skill?.active || !on(unit)) continue;
const scale = minS + (maxS - minS) * Math.min(1, (battle.time - t0s) / Math.max(0.1, chainSleep));
battle.dealDamage(unit, e, { amount: unit.s.atk * scale, type: 'arts', isSkill: true, tags: ['skill', 'titiWake'] });
battle.fx('wake', { x: e.x, y: e.y, id: e.id, scale: Math.round(scale * 100) / 100 });
sleepOthers(e);
}
}, { owner: unit });
battle.on('kill', (c) => {
const v = c.victim;
if (v.side !== 'enemy' || !asleep.has(v)) return;
asleep.delete(v);
if (unit.skill?.active && on(unit)) sleepOthers(v);
}, { owner: unit });
// S3: other allies in range fall asleep instead of dying, until healed to full or the skill ends
battle.on('fatal', (c) => {
const a = c.unit;
if (c.prevented || a.side !== 'ally' || a === unit) return;
const guard = a.findBuff('titi:allySleep');
if (guard) { if (guard.data.src === unit) c.prevented = true; return; }
if (!isOp(a) || !unit.skill?.active || !on(unit) || !unit.rangeKeySet?.has(a.tileR * COLS + a.tileC)) return;
c.prevented = true;
const entered = !a.buffs.some((b) => (b.status ?? b.key) === 'sleep');
battle.addBuff(a, {
key: 'titi:allySleep', flags: { sleep: true }, visible: true, status: 'sleep', data: { src: unit }, interval: 0.2,
onTick: ({ unit: x, buff }) => { if (x.hp >= x.s.maxHp - 1e-6) battle.removeBuff(x, buff); },
});
// other content (特质 "干员进入沉睡时") observes it like any sleep status
if (battle.hasHook('statusApplied')) battle.emit('statusApplied', { source: unit, target: a, status: 'sleep', duration: Infinity, entered });
battle.fx('sleepGuard', { x: a.x, y: a.y, id: a.id });
}, { owner: unit, priority: 5 });
},
};
},
};