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,316 @@
// server/sim/content/kits/ops/chess_char_6_11-mlyss.js — 缪尔赛思 (char_249_mlyss) kit, tier 6.
// Conventions of the tier-6 kits: ../shared/tier6.js; kit contract and rules: ../README.md.
import {
num, bv, tbb, tdesc, moduleBb, parseN, live, isTok, onDefaultSkill, selectedSkill, AROUND8, N4, bstate, bestTile,
pullToward,
} from '../shared/tier6.js';
/** 莱茵生命 (character_table groupId "rhine") members of this season's pool (checked against the official table). */
const RHINE = new Set(['char_108_silent', 'char_128_plosis', 'char_202_demkni', 'char_249_mlyss', 'char_1047_halo2']);
// ------------------------------------------------------------------------------------------------------------------
// 缪尔赛思 chess_char_6_11 (战术家) — S3 浅层非熵适应; 净水即生命 (流形); 开源节流; module 梳妆流形
/**
* The operator a 流形 copies: the nearest (Chebyshev tiles) living operator of its player on the field, not the summoner;
* ties → higher base ATK → lower id. Same pick as content/tokens.js (unmanaged 流形), so both paths agree.
*/
function pickCopyTarget(battle, owner, t) {
let best = null, bs = null;
for (const a of battle.allyUnits) {
if (a.kind !== 'op' || !live(a) || a === owner || a.ownerId !== owner.ownerId) continue;
const s = [Math.max(Math.abs(a.tileR - t.tileR), Math.abs(a.tileC - t.tileC)), -a.base.atk, a.id];
let less = !bs;
if (!less) for (let i = 0; i < 3; i++) { if (s[i] < bs[i]) { less = true; break; } if (s[i] > bs[i]) break; }
if (less) { best = a; bs = s; }
}
return best;
}
/**
* 流形 copy (token talent "复制目标90%的生命值、攻击力、防御力、法抗,以及阻挡数、攻击间隔、攻击范围、初始伤害类型(不攻击、
* 治疗类型则不继承)"): scale × HP/ATK/DEF/RES, block count, BAT/ASPD, range and damage type. The attack itself stays the
* token's single-target attack (melee or ranged after the copied position) — splash/chain/multi-hit shapes are not
* listed and not copied. `ranged` (近/远程位) decides the special trait (steal / split).
*/
function copyInto(battle, t, src, scale, ranged) {
const sb = src.base, b = t.base;
b.maxHp = Math.max(1, num(sb.maxHp, b.maxHp) * scale);
for (const k of ['atk', 'def', 'res']) if (Number.isFinite(sb[k])) b[k] = sb[k] * scale;
for (const k of ['blockCnt', 'bat', 'aspd']) if (Number.isFinite(sb[k])) b[k] = sb[k];
if (Array.isArray(src.rangeGrid) && src.rangeGrid.length) t.rangeGrid = src.rangeGrid;
const sp = src.profile || {};
const p = t.profile;
if (p) {
p.attack = ranged ? 'ranged' : 'melee';
// (a 阵法术师 / 轰击术师's instant 'beam' is their every-enemy-in-range shape: the copy fires a plain bolt)
p.projectile = ranged ? (sp.projectile && sp.projectile !== 'none' && sp.projectile !== 'orb' && sp.projectile !== 'beam' ? sp.projectile : 'bolt') : 'none';
p.canHitFly = ranged ? true : !!sp.canHitFly;
if (!(sp.dmgType === 'heal' || sp.dmgType === 'none' || sp.noAttack || sp.noAttackUnlessSkill)) p.dmgType = sp.dmgType;
p.heal = null;
p.noAttack = false;
p.noAttackUnlessSkill = false;
}
battle.removeBuff(t, 'mlyss:stolen');
t.mem.mlyss = { ranged, from: src.id, stolenAtk: 0, stolenDef: 0, attacks: 0 }; // "复制后重置"
t.markDirty();
t.hp = t.s.maxHp;
battle.refreshRange(t); // the copied grid: current + initial range, trigger keys
}
function mlyss(bb, chess, def) {
const tokId = def?.talents?.[0]?.tokenKey || (chess?.tokens || [])[0] || 'token_10030_mlyss_wtrman';
const t1 = tbb(def, 1), mod = moduleBb(chess);
const isDef = onDefaultSkill(chess), sid = selectedSkill(chess, def);
const reinfCut = parseN(chess?.trait?.moduleDesc, /伤害降低(\d+(?:\.\d+)?)%/, 0) / 100;
// S3 (default) only: melee pulse interval, ranged-copy bind
const pulseIv = Math.max(0.2, bv(bb, 'interval', 2)), bindDur = isDef ? bv(bb, 'duration', 1.5) : 0;
// "自身与流形攻击力+N%(攻击速度+N)" — every skill of hers buffs the 流形 too while it runs
const manifoldMods = { atkPct: num(bb.atk), ...(num(bb.attack_speed) ? { aspd: num(bb.attack_speed) } : {}) };
const buffManifold = (battle, t, dur) => battle.addBuff(t, { key: 'mlyss:s3', mods: manifoldMods, duration: dur });
// module 落叶四季 (开源节流): copying a 莱茵生命 operator ⇒ she gains `sp`; deploying the copied one ⇒ it gains `sp_other`
const rhineSp = num(mod.sp), rhineSpOther = num(mod.sp_other);
const tokTal = (t, key) => { for (const x of t.def?.talents || []) if (x?.bb && typeof x.bb[key] === 'number') return x.bb; return {}; };
// "其被击败后会在25秒后自动刷新" (token talent interval; the talent text as a fallback)
const respawnOf = (t) => num(tokTal(t, 'scale').interval, parseN(tdesc(def, 0), /(\d+(?:\.\d+)?)秒后自动刷新/, 25));
const isRanged = (src) => (src.def?.position ? src.def.position === 'RANGED' : src.profile?.attack === 'ranged');
const copyNearest = (battle, unit, t) => {
const src = pickCopyTarget(battle, unit, t);
if (!src) { // nobody to copy: the copy skill is not spent — it ends and stays ready (no attack without a copy)
if (t.skill?.active) { t.skill.end('noTarget'); t.skill.gainSp(t.skill.spCost, 'refund'); }
return;
}
copyInto(battle, t, src, num(tokTal(t, 'scale').scale, 1), isRanged(src));
battle.fx('copy', { x: t.x, y: t.y, id: t.id, src: src.id });
if (rhineSp > 0 && RHINE.has(src.def?.charId) && unit.skill) unit.skill.gainSp(rhineSp, 'talent');
};
const mineLive = (battle, unit) => battle.allyUnits.filter((x) => isTok(x, tokId, unit) && live(x));
const skills = {
// S1 渐进性润化: fake_cost DP over the skill (1 every mlyss_s_1[cost].interval s, the rest at its end), she and her
// 流形 ATK / ASPD +
skchr_mlyss_1: {
kind: 'duration',
mods: { atkPct: num(bb.atk), aspd: num(bb.attack_speed) },
onStart({ battle, unit, skill }) {
unit.mem.mlyDp = { acc: 0, got: 0 };
for (const t of mineLive(battle, unit)) buffManifold(battle, t, skill.timeLeft);
},
onTick({ battle, unit, dt }) {
const m = unit.mem.mlyDp;
if (!m) return;
const iv = Math.max(0.1, bv(bb, 'interval', 1.364)), total = Math.floor(num(bb.fake_cost, 11)), per = bv(bb, 'cost', 1);
m.acc += dt;
while (m.acc + 1e-9 >= iv && m.got < total) { m.acc -= iv; m.got++; battle.addDp(unit.ownerId, per); }
},
onEnd({ battle, unit, reason }) {
const m = unit.mem.mlyDp;
unit.mem.mlyDp = null;
const total = Math.floor(num(bb.fake_cost, 11));
if (m && reason === 'duration' && m.got < total) battle.addDp(unit.ownerId, (total - m.got) * bv(bb, 'cost', 1));
},
},
// S2 生态耦合: +cost DP, she and her 流形 ATK +; melee copies regenerate hp ratio / s and take 庇护; ranged copies
// attack twice at random targets of their range (install)
skchr_mlyss_2: {
kind: 'duration',
mods: { atkPct: num(bb.atk) },
onStart({ battle, unit, skill }) {
battle.addDp(unit.ownerId, num(bb.cost));
battle.fx('dp', { x: unit.x, y: unit.y, id: unit.id, n: num(bb.cost) });
for (const t of mineLive(battle, unit)) buffManifold(battle, t, skill.timeLeft);
},
},
};
return {
skills,
install(battle, unit) {
if (sid === 'skchr_mlyss_2') {
const regen = num(bb.hp_recovery_per_sec_by_max_hp_ratio), dr = num(bb.damage_resistance);
battle.every(0.25, () => {
const on = !!unit.skill?.active && live(unit);
for (const t of battle.allyUnits) {
if (!isTok(t, tokId, unit) || !t.profile) continue;
const m = t.mem.mlyss;
if (t.mem.mlyssHits == null) t.mem.mlyssHits = Math.max(1, Math.floor(num(t.profile.hits, 1)));
const ranged = !!(m && m.ranged);
t.profile.hits = on && ranged && live(t) ? t.mem.mlyssHits * 2 : t.mem.mlyssHits; // 二连击
if (on && m && !ranged && live(t)) {
battle.addBuff(t, { key: 'mlyss:eco', duration: 0.4, refresh: 'replace', visible: true, mods: { hpRegenRatio: regen, physTakenMul: 1 - dr, artsTakenMul: 1 - dr } });
}
}
}, { owner: unit });
battle.on('beforeAttack', (ctx) => { // ranged copies: 随机攻击范围内的目标
const t = ctx.attacker;
if (!isTok(t, tokId, unit) || !t.mem.mlyss?.ranged || !unit.skill?.active) return;
const c = battle.enemiesInKeys(t.rangeKeys, t, ctx.profile);
if (c.length) ctx.targets = [battle.rng.pick(c)];
}, { owner: unit });
}
if (rhineSpOther > 0) battle.on('deploy', ({ unit: u, initial }) => { // the copied 莱茵生命 operator is deployed
if (initial || !u || u.kind !== 'op' || u.ownerId !== unit.ownerId || !RHINE.has(u.def?.charId) || !u.skill) return;
if (battle.allyUnits.some((t) => isTok(t, tokId, unit) && live(t) && t.mem.mlyss?.from === u.id)) u.skill.gainSp(rhineSpOther, 'talent');
}, { owner: unit });
},
skill: {
kind: 'duration',
mods: { atkPct: num(bb.atk) },
onStart({ battle, unit, skill }) {
battle.addDp(unit.ownerId, num(bb.cost));
battle.fx('dp', { x: unit.x, y: unit.y, id: unit.id, n: num(bb.cost) });
const S = unit.mem;
let ranged = false;
for (const t of battle.allyUnits.filter((x) => isTok(x, tokId, unit) && live(x))) {
buffManifold(battle, t, skill.timeLeft);
if (t.mem.mlyss?.ranged) ranged = true;
}
if (ranged) { // 刷新所有流形: heal the living ones, respawn the destroyed ones now
for (const t of battle.allyUnits.filter((x) => isTok(x, tokId, unit) && live(x))) t.hp = t.s.maxHp;
for (const p of (S.mlyssPending || []).splice(0)) { p.timer?.cancel?.(); p.spawn(); }
}
S.mlyssPulse = battle.every(pulseIv, () => { // melee copies: pull the 8 surrounding tiles + stun blocked units
if (!unit.skill?.active) return;
for (const t of battle.allyUnits) {
if (!isTok(t, tokId, unit) || !live(t) || !t.mem.mlyss || t.mem.mlyss.ranged) continue;
for (const e of battle.unitsInGrid(t, AROUND8, { side: 'enemy' })) pullToward(battle, t, e, num(t.def?.skill?.bb?.force));
for (const e of t.blocking) if (e.alive) battle.applyStatus(e, 'stun', { duration: pulseIv + 0.1, source: unit });
battle.fx('pulse', { x: t.x, y: t.y, id: t.id });
}
}, { owner: unit, immediate: true });
},
onEnd({ unit }) { if (unit.mem.mlyssPulse) unit.mem.mlyssPulse.cancel(); unit.mem.mlyssPulse = null; },
},
talents: [
{ install(battle, unit) { // 净水即生命: 流形 copy / respawn / melee steal / ranged split
unit.mem.mlyssPending = [];
const S = bstate(battle);
S.robbed ??= new WeakMap(); // enemy → { atk, def } stolen by any 流形
const mine = (t) => isTok(t, tokId, unit);
// her 流形 on the field (split clones aside: deployLimit 1 counts the 流形 itself)
const standing = () => battle.allyUnits.some((t) => mine(t) && live(t) && !t.mem.mlyssClone);
const cancelPending = () => { for (const p of unit.mem.mlyssPending.splice(0)) p.timer?.cancel?.(); };
battle.on('deploy', ({ unit: t }) => {
if (!mine(t)) return;
const clone = unit.mem.mlyssCloneOf;
if (clone) { // a split copy: same stats as its origin, never copies/splits/respawns itself
unit.mem.mlyssCloneOf = null;
t.mem.mlyssClone = true;
t.mem.isClone = true; // content/tokens.js skips clones too (her redeploy must bring a real 流形, not adopt a clone)
copyInto(battle, t, clone, 1, true);
return;
}
cancelPending(); // a 流形 stands again (tactical re-summon, S3 refresh): no second one later
t.mem.mlyssHome = [t.tileR, t.tileC];
const sp = num(tokTal(t, 'sp').sp);
if (sp > 0 && !unit.mem.mlyssFirst && t.skill) t.skill.gainSp(sp, 'talent'); // elite: first deployment +5 SP
unit.mem.mlyssFirst = true;
if (unit.skill?.active && unit.skill.isTimed) buffManifold(battle, t, unit.skill.timeLeft);
}, { owner: unit });
battle.on('skillStart', ({ unit: t }) => { if (mine(t) && !t.mem.mlyssClone) copyNearest(battle, unit, t); }, { owner: unit });
// trigger rule MLYSS_WTRMAN ("全部技能,受流形影响") [ASSUMED]: DEFAULT (engine), and an enemy inside the range of one
// of her copied 流形 also satisfies it (her S3 acts through the 流形: pull/stun or bind around them)
if (unit.skill) unit.skill.addTriggerRange(() => battle.allyUnits.filter((t) => mine(t) && live(t) && t.mem.mlyss));
battle.on('tick', () => { // without a dedicated token kit the copy fires once ready and someone can be copied
for (const t of battle.allyUnits) {
if (!mine(t) || !live(t) || t.mem.mlyssClone || !t.kit?.generic || !t.skill || t.skill.active || !t.skill.ready || t.skill.opCooling) continue;
if (pickCopyTarget(battle, unit, t)) t.skill.activate('MLYSS_WTRMAN');
}
}, { owner: unit });
// "其被击败后会在25秒后自动刷新": one pending respawn at a time, on its tile, only while she is on the field and no
// 流形 of hers stands (her redeploy summons it again as her 援军, tokens.js); expired / replaced ones do not return
battle.on('death', ({ unit: t, reason }) => {
if (t === unit) { cancelPending(); return; }
if (!mine(t) || t.mem.mlyssClone || reason !== 'killed' || !t.mem.mlyssHome || standing()) return;
cancelPending();
const [r, c] = t.mem.mlyssHome;
const entry = { spawn: null, timer: null };
let tries = 0;
entry.spawn = () => {
const i = unit.mem.mlyssPending.indexOf(entry);
if (i >= 0) unit.mem.mlyssPending.splice(i, 1);
if (battle.finished || !live(unit) || standing() || tries++ > 120) return;
if (battle.isReservedTile(r, c) || !battle.spawnToken(unit, tokId, r, c)) { // its tile is taken: retry in 1 s
entry.timer = battle.after(1, entry.spawn, { owner: unit });
unit.mem.mlyssPending.push(entry);
}
};
entry.timer = battle.after(respawnOf(t), entry.spawn, { owner: unit });
unit.mem.mlyssPending.push(entry);
}, { owner: unit });
battle.on('attack', ({ attacker: t, targets }) => {
if (!mine(t) || !t.mem.mlyss || !live(t)) return;
const tal = tokTal(t, 'steal_atk');
const m = t.mem.mlyss;
if (!m.ranged) { // melee copy: 每次攻击偷取敌方10点攻击力与防御力(最高250点)— a capped copy steals nothing more
const sa = num(tal.steal_atk), sd = num(tal.steal_def, sa);
const capA = num(tal.steal_atk_max, Infinity), capD = num(tal.steal_def_max, capA);
let changed = false;
for (const e of targets) {
if (!e.alive || e.side !== 'enemy') continue;
const ga = Math.max(0, Math.min(sa, capA - m.stolenAtk)), gd = Math.max(0, Math.min(sd, capD - m.stolenDef));
if (!(ga > 0 || gd > 0)) break;
m.stolenAtk += ga; m.stolenDef += gd;
const rb = S.robbed.get(e) || { atk: 0, def: 0 };
rb.atk += ga; rb.def += gd;
S.robbed.set(e, rb);
battle.addBuff(e, { key: 'mlyss:robbed', mods: { atkFlat: -rb.atk, defFlat: -rb.def }, source: t });
changed = true;
}
if (changed) battle.addBuff(t, { key: 'mlyss:stolen', mods: { atkFlat: m.stolenAtk, defFlat: m.stolenDef } });
} else if (!t.mem.mlyssClone) { // ranged copy: split every N attacks (the clone lasts `interval` s)
const every = Math.max(1, Math.floor(bv(tal, 'max_stack_cnt', 10)));
if (++m.attacks % every !== 0) return;
const tiles = N4.slice(1).map(([dr, dc]) => [t.tileR + dr, t.tileC + dc])
.filter(([r, c]) => battle.grid.inRect(r, c) && !battle.isReservedTile(r, c) && battle.grid.canStand(r, c, { ranged: true }));
const tile = bestTile(battle, tiles);
if (!tile) return;
unit.mem.mlyssCloneOf = t;
let cl = null;
try { cl = battle.spawnToken(unit, tokId, tile[0], tile[1], { duration: num(tal.interval, 25), kit: { skill: null } }); } finally { unit.mem.mlyssCloneOf = null; }
if (cl) battle.fx('split', { x: cl.x, y: cl.y, id: cl.id, src: t.id });
}
}, { owner: unit });
battle.on('damaged', (ctx) => { // S3, ranged copies: attacks bind
const t = ctx.source;
if (!mine(t) || !t.mem.mlyss?.ranged || !unit.skill?.active || !ctx.dmg?.isAttack || !ctx.target.alive || !(bindDur > 0)) return;
battle.applyStatus(ctx.target, 'bind', { duration: bindDur, source: unit });
}, { owner: unit });
battle.on('hit', (ctx) => { // module: 援军 / 流形 take less damage from the enemies they block
const t = ctx.target, s = ctx.source;
if (!t || !s || s.side !== 'enemy' || s.blockedBy !== t) return;
if (mine(t)) { // 流形 (her 援军): token module part, unless its token kit (content/tokens.js) applies it
if (!t.kit?.fromTokens) { const ds = num(tokTal(t, 'damage_scale').damage_scale, 1); if (ds > 0 && ds < 1) ctx.dmg.mul *= ds; }
} else if (reinfCut > 0 && t === unit.trait?.reinforcement) ctx.dmg.mul *= 1 - reinfCut; // engine 援军
}, { owner: unit });
// module 落叶四季: "援军阻挡的敌人更容易受到我方的攻击" — the 流形's module part (token talent taunt_level: +1) on
// the enemies it blocks: our operators pick higher-taunt enemies first (targeting.js sortEnemyTargets)
battle.every(0.25, () => {
for (const t of battle.allyUnits) {
if (!mine(t) || !live(t) || !t.blocking.length) continue;
const lv = num(tokTal(t, 'taunt_level').taunt_level);
if (!(lv > 0)) continue;
for (const e of t.blocking) if (e.alive) battle.addBuff(e, { key: 'mlyss:exposed', mods: { taunt: lv }, duration: 0.4, refresh: 'replace', source: t });
}
}, { owner: unit });
} },
{ install(battle, unit) { // 开源节流: 莱茵生命 ops of the owner cost less DP to redeploy
const S = bstate(battle);
S.rhine ??= new Set();
if (S.rhine.has(unit.ownerId)) return;
S.rhine.add(unit.ownerId);
const cut = num(t1.cost), first = num(t1.runtime_cost);
battle.on('battleStart', () => {
const rh = battle.allyUnits.filter((a) => a.kind === 'op' && a.ownerId === unit.ownerId && RHINE.has(a.def?.charId)).sort((a, b) => a.deploySeq - b.deploySeq);
rh.forEach((a, i) => { a.base.cost = Math.max(0, a.base.cost + cut + (i === 0 ? first : 0)); });
});
} },
],
};
}
export default {
chess_char_6_11_a: mlyss,
};