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.
138 lines
7.8 KiB
JavaScript
138 lines
7.8 KiB
JavaScript
// server/sim/content/kits/ops/chess_char_6_19-blkkgt.js — 锏 (char_4116_blkkgt) kit, tier 6.
|
||
// Conventions of the tier-6 kits: ../shared/tier6.js; kit contract and rules: ../README.md.
|
||
|
||
import { sortEnemyTargets, canTargetEnemy } from '../../../targeting.js';
|
||
import { num, tbb, parseN, live, ANY, enemiesIn, instantKind, pullToward } from '../shared/tier6.js';
|
||
|
||
// ------------------------------------------------------------------------------------------------------------------
|
||
// 锏 chess_char_6_19 (剑豪) — S3 归于宁静; “天生的武者”; 活着的传奇; module “过往的注脚”
|
||
|
||
function blkkgt(bb, chess, def) {
|
||
const t0 = tbb(def, 0), t1 = tbb(def, 1), tb = def?.traitBb || {};
|
||
const slashes = Math.max(1, parseN(def?.skill?.description, /总计(\d+)次/, 10));
|
||
const iv = Math.max(0.05, num(bb.d_hit_interval, 0.3)), pullIv = Math.max(0.1, num(bb.p_hit_interval, 1));
|
||
const maxT = Math.max(1, Math.floor(num(bb.max_target, 1)));
|
||
const skillGrid = def?.skill?.rangeGrid?.length ? def.skill.rangeGrid : null;
|
||
// the skill range (x-1): her range while S3 runs — also for the finisher, since onEnd runs before the engine
|
||
// removes the skill's range
|
||
const skillKeys = (unit) => unit.rangeKeys || [];
|
||
// S3 hits and pulls air units too — PRTS 锏 S3 备注 "※可对空。不会拖拽自身中心半径0.6708范围内的敌人" (her attacks and S1 /
|
||
// S2 stay ground-only: "地面敌人"); the air units of the mode are 静态刚体, so the pull leaves them in place (Battle.pull)
|
||
const victims = (battle, unit) => {
|
||
const c = battle.enemiesInKeys(skillKeys(unit), unit, { ...unit.profile, canHitFly: true, groundOnly: false });
|
||
sortEnemyTargets(battle, unit, c, null);
|
||
return c.slice(0, maxT);
|
||
};
|
||
const slash = (battle, unit, scale) => {
|
||
const v = victims(battle, unit);
|
||
if (v.length) battle.fx('slash', { x: unit.x, y: unit.y, id: unit.id, n: v.length });
|
||
for (const e of v) battle.dealDamage(unit, e, { amount: unit.s.atk * scale, type: 'phys', isSkill: true, tags: ['skill', 'slash'] });
|
||
};
|
||
// "持续将敌人中等力度地拖拽至自身中心,之后…较大力地拖拽至自身": the official 力度 − 重量 pulls (PRTS 推与拉: the last one
|
||
// aims at her own tile centre, the others at the usual 拉力起点 half a tile ahead; 急停 0.6708 around her)
|
||
const pull = (battle, unit, force, last = false) => {
|
||
for (const e of enemiesIn(battle, unit, skillKeys(unit))) {
|
||
if (last) pullToward(battle, unit, e, force); else battle.pullToFront(e, unit, force);
|
||
}
|
||
};
|
||
const skills = {
|
||
// S1 纯粹的武力 (attack SP): the next attack hits up to max_target ground enemies of the 3×3 around her, each twice
|
||
// (her trait) at atk_scale_s1 × ATK
|
||
skchr_blkkgt_1: {
|
||
kind: 'instant',
|
||
attack: { atkScale: num(bb.atk_scale_s1, 1) },
|
||
targeting: { maxTargets: Math.max(1, Math.floor(num(bb.max_target, 5))), ...(skillGrid ? { rangeGrid: skillGrid } : {}) },
|
||
},
|
||
// S2 无声的嘲笑 (attack SP, 2 charges): up to max_target ground enemies of the front skill range take 2 slashes
|
||
// (3 when blocked) of dot_scale × ATK; the talent procs at 100 % meanwhile (skill active + bb.prob)
|
||
skchr_blkkgt_2: {
|
||
kind: instantKind(def),
|
||
onStart({ battle, unit }) {
|
||
const grid = skillGrid || [[0, 0], [0, 1]];
|
||
const c = battle.unitsInGrid(unit, grid, { side: 'enemy' }).filter((e) => !e.isFlying && canTargetEnemy(unit, e, unit.profile));
|
||
sortEnemyTargets(battle, unit, c, null);
|
||
const v = c.slice(0, Math.max(1, Math.floor(num(bb.max_target, 5))));
|
||
const nFree = Math.floor(num(bb['blkkgt_s_2[not_blocked].trig_cnt'], 2)), nBlocked = Math.floor(num(bb['blkkgt_s_2[blocked].trig_cnt'], 3));
|
||
if (v.length) battle.fx('slash', { x: unit.x, y: unit.y, id: unit.id, n: v.length });
|
||
for (const e of v) {
|
||
const n = e.blockedBy ? nBlocked : nFree;
|
||
for (let i = 0; i < n && e.alive; i++) battle.dealDamage(unit, e, { amount: unit.s.atk * num(bb.dot_scale, 1), type: 'phys', isSkill: true, tags: ['skill', 'slash'] });
|
||
}
|
||
},
|
||
},
|
||
};
|
||
// module 新合同: trait "攻击时无视敌人70点的防御力"; talent 活着的传奇 "攻击力+8%…地面敌人首次进入自身攻击范围时,使其战栗6秒"
|
||
const penFlat = num(tb.def_penetrate_fixed), legendAtk = num(t1.atk), legendTremble = num(t1.not_combat);
|
||
return {
|
||
skills,
|
||
install(battle, unit) {
|
||
if (penFlat > 0 || legendAtk) battle.addBuff(unit, { key: 'blkkgt:contract', mods: { defIgnoreFlat: penFlat, atkPct: legendAtk }, persist: true, allowDead: true });
|
||
// S3 slashes: 晕眩免疫, 冻结免疫 (PRTS 备注)
|
||
battle.on('beforeStatus', (c) => {
|
||
if (c.target === unit && unit.mem.dgb && (c.status === 'stun' || c.status === 'freeze')) c.cancel = true;
|
||
}, { owner: unit });
|
||
if (legendTremble > 0) {
|
||
const seen = new WeakSet();
|
||
battle.on('tick', () => {
|
||
if (!live(unit)) return;
|
||
for (const e of battle.enemiesInKeys(unit.rangeKeys, unit, ANY)) {
|
||
if (e.isFlying || seen.has(e)) continue;
|
||
seen.add(e);
|
||
battle.applyStatus(e, 'tremble', { duration: legendTremble, source: unit });
|
||
}
|
||
}, { owner: unit });
|
||
}
|
||
},
|
||
skill: {
|
||
kind: 'duration',
|
||
duration: slashes * iv,
|
||
...(skillGrid ? { targeting: { rangeGrid: skillGrid } } : {}),
|
||
attack: { noAttack: true },
|
||
// PRTS 备注 "※多段斩击期间,自身获得无敌,晕眩免疫,冻结免疫": invulnerable while the slashes run (removed in onEnd,
|
||
// before the finisher); stun / freeze refused by her `beforeStatus` hook (install) while `mem.dgb` is set
|
||
onStart({ battle, unit }) {
|
||
unit.mem.dgb = { n: 1, acc: 0, pacc: 0 };
|
||
battle.addBuff(unit, { key: 'blkkgt:slashes', duration: slashes * iv + 1, flags: { invulnerable: true }, visible: true, source: unit });
|
||
slash(battle, unit, num(bb.d_atk_scale, 1));
|
||
},
|
||
onTick({ battle, unit, dt }) {
|
||
const m = unit.mem.dgb;
|
||
if (!m) return;
|
||
m.acc += dt;
|
||
m.pacc += dt;
|
||
while (m.acc + 1e-9 >= iv && m.n < slashes) { m.acc -= iv; m.n++; slash(battle, unit, num(bb.d_atk_scale, 1)); }
|
||
if (m.pacc + 1e-9 >= pullIv) { m.pacc -= pullIv; pull(battle, unit, num(bb.p_force)); }
|
||
},
|
||
onEnd({ battle, unit, reason }) {
|
||
unit.mem.dgb = null;
|
||
battle.removeBuff(unit, 'blkkgt:slashes');
|
||
if (reason === 'death' || !unit.alive) return;
|
||
battle.fx('finale', { x: unit.x, y: unit.y, id: unit.id });
|
||
unit.mem.dgbFinale = true; // the finisher is part of the skill (talent at 100 %, module +10 %)
|
||
try { slash(battle, unit, num(bb.e_atk_scale_end, 1)); } finally { unit.mem.dgbFinale = false; }
|
||
pull(battle, unit, num(bb.e_force), true);
|
||
},
|
||
},
|
||
talents: [
|
||
{ install(battle, unit) { // 活着的传奇 → “天生的武者” → module (skill damage +10 %)
|
||
const prob = num(t0.prob), sc = num(t0.atk_scale, 1), tr = num(t0.not_combat), pen = num(t1.def_penetrate), skillMul = num(tb.damage_scale, 1);
|
||
battle.on('hit', (ctx) => {
|
||
const t = ctx.target, d = ctx.dmg;
|
||
if (ctx.source !== unit || !t || t.side !== 'enemy' || d.type === 'element') return;
|
||
if (pen > 0 && t.findBuff('tremble')) d.defIgnorePct += pen;
|
||
const p = unit.skill?.active || unit.mem.dgbFinale ? num(bb.prob, prob) : prob;
|
||
if (p > 0 && (p >= 1 || battle.rng() < p)) {
|
||
d.amount *= sc;
|
||
if (tr > 0 && t.alive) battle.applyStatus(t, 'tremble', { duration: tr, source: unit });
|
||
}
|
||
if (skillMul > 1 && d.isSkill) d.mul *= skillMul;
|
||
}, { owner: unit });
|
||
} },
|
||
],
|
||
};
|
||
}
|
||
|
||
export default {
|
||
chess_char_6_19_a: blkkgt,
|
||
};
|