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.
147 lines
7.9 KiB
JavaScript
147 lines
7.9 KiB
JavaScript
// server/sim/content/kits/ops/chess_char_6_16-halo2.js — 溯光星源 (char_1047_halo2) kit, tier 6.
|
||
// Conventions of the tier-6 kits: ../shared/tier6.js; kit contract and rules: ../README.md.
|
||
|
||
import { canTargetEnemy } from '../../../targeting.js';
|
||
import { bodyDist, bodyInKeys } from '../../../body.js';
|
||
import {
|
||
num, bv, tbb, moduleBb, live, ANY, enemiesIn, onDefaultSkill, selectedSkill, batOf, pullToward, aura,
|
||
} from '../shared/tier6.js';
|
||
|
||
// ------------------------------------------------------------------------------------------------------------------
|
||
// 溯光星源 chess_char_6_16 (凝滞师) — S3 并流连锁; 数据建模; 能源解析
|
||
|
||
function halo2(bb, chess, def) {
|
||
const t0 = tbb(def, 0), t1 = tbb(def, 1), mod = moduleBb(chess);
|
||
const isDef = onDefaultSkill(chess), sid = selectedSkill(chess, def);
|
||
const skillGrid = def?.skill?.rangeGrid?.length ? def.skill.rangeGrid : null;
|
||
const n = Math.max(1, Math.floor(num(bb['attack@max_target'], 1))), share = num(bb['attack@atk_share']);
|
||
const slug = (unit) => unit.profile?.onHitStatus?.key === 'sluggish' ? num(unit.profile.onHitStatus.duration) : 0;
|
||
const skills = {
|
||
// S1 星图闪烁: ATK +atk; every attack then bounces attack@chain.max_target times between enemies (back and forth
|
||
// allowed, never twice in a row on one; attack@projectile_range tiles), each bounce a full hit with her 停顿
|
||
skchr_halo2_1: {
|
||
kind: 'duration',
|
||
mods: { atkPct: num(bb.atk) },
|
||
attack: {
|
||
onHit({ battle, unit, target }) {
|
||
if (!target) return;
|
||
const jumps = Math.max(0, Math.floor(num(bb['attack@chain.max_target'], 3))), R = num(bb['attack@projectile_range'], 1.7);
|
||
let prev = target, px = target.x, py = target.y;
|
||
for (let i = 0; i < jumps; i++) {
|
||
let best = null, bd = Infinity;
|
||
for (const e of battle.foesInRadius(px, py, R)) {
|
||
if (e === prev || !canTargetEnemy(unit, e, ANY)) continue;
|
||
const d = bodyDist(e, px, py);
|
||
if (d < bd - 1e-9) { bd = d; best = e; }
|
||
}
|
||
if (!best) break;
|
||
battle.dealDamage(unit, best, { amount: unit.s.atk, type: 'arts', isAttack: true, isSkill: true, tags: ['chain'] });
|
||
const sd = slug(unit);
|
||
if (sd > 0 && best.alive) battle.applyStatus(best, 'sluggish', { duration: sd, source: unit });
|
||
prev = best; px = best.x; py = best.y;
|
||
}
|
||
},
|
||
},
|
||
},
|
||
// S2 星束引力 (attack SP): the next attack targets the enemy of her range farthest from its goal (install), atk_scale
|
||
// × ATK arts with a `sluggish` s 停顿, and links up to max_target enemies within ability_range_radius of it: pulled
|
||
// toward it (force) and hit by atk_scale_link × ATK arts
|
||
skchr_halo2_2: {
|
||
kind: 'instant',
|
||
attack: {
|
||
atkScale: num(bb.atk_scale, 1),
|
||
onHitStatus: { key: 'sluggish', duration: num(bb.sluggish, 3) },
|
||
onHit({ battle, unit, target }) {
|
||
if (!target) return;
|
||
const R = num(bb.ability_range_radius, 2), k = Math.max(0, Math.floor(num(bb.max_target, 2)));
|
||
const near = battle.foesInRadius(target.x, target.y, R).filter((e) => e !== target && canTargetEnemy(unit, e, ANY))
|
||
.sort((a, b) => bodyDist(a, target.x, target.y) - bodyDist(b, target.x, target.y) || a.spawnSeq - b.spawnSeq)
|
||
.slice(0, k);
|
||
for (const e of near) {
|
||
battle.fx('link', { x: e.x, y: e.y, id: unit.id, ids: [target.id, e.id] });
|
||
pullToward(battle, { x: target.x, y: target.y }, e, num(bb.force), 0.3);
|
||
if (e.alive) battle.dealDamage(unit, e, { amount: unit.s.atk * num(bb.atk_scale_link, num(bb.atk_scale, 1)), type: 'arts', isSkill: true, tags: ['skill', 'link'] });
|
||
}
|
||
},
|
||
},
|
||
},
|
||
};
|
||
return {
|
||
skills,
|
||
install(battle, unit) {
|
||
if (sid !== 'skchr_halo2_2') return;
|
||
battle.on('beforeAttack', (ctx) => { // "选择攻击范围内距离目标点最远的1个敌人为目标"
|
||
if (ctx.attacker !== unit || !unit.skill?.active || !unit.skill.pending) return;
|
||
const c = battle.enemiesInKeys(unit.rangeKeys, unit, ctx.profile);
|
||
if (!c.length) return;
|
||
let best = c[0];
|
||
for (const e of c) if (battle.remainingDistance(e) > battle.remainingDistance(best) + 1e-9) best = e;
|
||
ctx.targets = [best];
|
||
}, { owner: unit });
|
||
},
|
||
skill: {
|
||
kind: 'duration',
|
||
mods: { atkPct: num(bb.atk), batPct: batOf(bb.base_attack_time, def) },
|
||
targeting: { maxTargets: n, ...(skillGrid ? { rangeGrid: skillGrid } : {}) },
|
||
onStart({ unit }) { unit.mem.haloLocks = []; },
|
||
onEnd({ unit }) { unit.mem.haloLocks = []; },
|
||
},
|
||
talents: [
|
||
{ install(battle, unit) { // S3: persistent locks + damage transfer between linked targets
|
||
if (!isDef) return;
|
||
battle.on('beforeAttack', (ctx) => {
|
||
if (ctx.attacker !== unit || !unit.skill?.active) return;
|
||
const valid = (e) => e && e.alive && canTargetEnemy(unit, e, ANY) && bodyInKeys(e, unit.rangeKeySet);
|
||
const keep = (unit.mem.haloLocks || []).filter(valid);
|
||
for (const e of ctx.targets) if (keep.length < n && !keep.includes(e)) keep.push(e);
|
||
ctx.targets = keep.slice(0, n);
|
||
const fresh = ctx.targets.some((e) => !(unit.mem.haloLocks || []).includes(e));
|
||
unit.mem.haloLocks = ctx.targets.slice();
|
||
if (fresh && ctx.targets.length > 1) battle.fx('link', { x: ctx.targets[0].x, y: ctx.targets[0].y, id: unit.id, ids: ctx.targets.map((e) => e.id) });
|
||
}, { owner: unit });
|
||
battle.on('hit', (ctx) => {
|
||
const locks = unit.mem.haloLocks;
|
||
if (!(share > 0) || !unit.skill?.active || !locks || locks.length < 2 || ctx.dmg.type !== 'arts' || ctx.dmg.tags.includes('link')) return;
|
||
if (!locks.includes(ctx.target) || unit.mem.haloLinking) return;
|
||
// re-entrancy guard: a transferred hit (or anything it causes, e.g. enemy damage sharing) never re-links
|
||
unit.mem.haloLinking = true;
|
||
try {
|
||
for (const o of locks) if (o !== ctx.target && o.alive) {
|
||
battle.dealDamage(ctx.source ?? unit, o, { amount: ctx.dmg.amount * share, type: 'arts', canDodge: false, isSkill: true, tags: ['skill', 'link'] });
|
||
}
|
||
} finally { unit.mem.haloLinking = false; }
|
||
}, { owner: unit });
|
||
} },
|
||
{ install(battle, unit) { // 数据建模 (+ module ATK at max stacks)
|
||
const as = num(t0.attack_speed), max = Math.max(1, Math.floor(num(t0.max_stack_cnt, 18))), fullAtk = num(mod.atk);
|
||
battle.on('deploy', ({ unit: u }) => { if (u === unit) unit.mem.haloStack = 0; }, { owner: unit });
|
||
battle.on('statusApplied', (ctx) => {
|
||
if (ctx.source !== unit || ctx.status !== 'sluggish' || !live(unit) || (unit.mem.haloStack || 0) >= max) return;
|
||
unit.mem.haloStack = (unit.mem.haloStack || 0) + 1;
|
||
const k = unit.mem.haloStack;
|
||
battle.addBuff(unit, { key: 'halo2:model', mods: { aspd: as * k, atkPct: k >= max ? fullAtk : 0 } });
|
||
}, { owner: unit });
|
||
} },
|
||
{ install(battle, unit) { // 能源解析
|
||
const base = bv(t1, 'damage_scale', 1), maxS = bv(t1, 'damage_scale_max', base), iv = bv(t1, 'interval', 7);
|
||
const stay = new WeakMap();
|
||
aura(battle, unit, 0.25, () => {
|
||
const inR = enemiesIn(battle, unit);
|
||
for (const e of battle.enemies) if (!inR.includes(e)) stay.delete(e);
|
||
for (const e of inR) {
|
||
const t = (stay.get(e) ?? 0) + 0.25;
|
||
stay.set(e, t);
|
||
const v = t > iv ? maxS : base;
|
||
// 脆弱 (ba.fragile, 同名效果取最高): the catalogue status, refreshed while the enemy stays in range
|
||
if (v > 1) battle.applyStatus(e, 'fragile', { duration: 0.4, value: v - 1, source: unit });
|
||
}
|
||
});
|
||
} },
|
||
],
|
||
};
|
||
}
|
||
|
||
export default {
|
||
chess_char_6_16_a: halo2,
|
||
};
|