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.
192 lines
11 KiB
JavaScript
192 lines
11 KiB
JavaScript
// server/sim/content/kits/ops/chess_char_5_14-svash2.js — 凛御银灰 (char_1045_svash2) kit, tier 5.
|
||
// Conventions of the tier-5 kits: ../shared/tier5.js; kit contract and rules: ../README.md.
|
||
|
||
import { COLS } from '../../../constants.js';
|
||
import { bodyKeys } from '../../../body.js';
|
||
import { toLocal } from '../../../dir.js';
|
||
import {
|
||
AURA_IV, AURA_DUR, num, on, talent, talentRec, skillGrid, mods, inFaction, isOp, selectedId, lazySkills, instantKind,
|
||
skillRange, whileOn, enemiesInGrid,
|
||
} from '../shared/tier5.js';
|
||
|
||
const isKjerag = (u) => inFaction(u, 'kjeragShip', ['kjerag']);
|
||
|
||
export default {
|
||
// ---------------------------------------------------------------------------------------------------------------
|
||
// 凛御银灰 — S2 御敌的锋锐 (2 charges; PRTS 备注 "可对空"): the 6 front-most enemies in the skill range take 260 % ATK
|
||
// phys, cold + reveal for 4 s; the waiting (knocked-out) op nearest to 风雪之眼 by cost gets −11 redeploy cost (right
|
||
// side guard/caster/sniper first); waiting ops left of the eye (cost < eye cost) cast the AoE with their own ATK when
|
||
// they redeploy (≤2).
|
||
// T1 开放性开局: ops left of the eye and himself: +4 SP at deployment, redeploy time −20 %.
|
||
// T2 雪境先驱: Kjerag ops freeze-immune, DEF +60, 1.5 % max HP/s regen; doubled after 15 s on the field.
|
||
// The eye follows the selected skill (its overrideTokenKey: S1 eagle1 cost 16, S2 eagle2 14, S3 eagle3 19).
|
||
// S1 周旋的谋略 (instant): +cost DP at once; the waiting op nearest to the eye gets −[deck].cost redeploy cost (same pick
|
||
// as S2) and a barrier of [deck].shield × his max HP when it redeploys.
|
||
// S3 变革已至 (duration 48 s): skill range; enemies in it lose 隐匿; every attack hits every enemy of the range on the
|
||
// target's line (his facing) for bird_atk_scale × ATK phys + 脆弱 (damage_scale − 1, weak[limit] s [ASSUMED duration]);
|
||
// +[start_cost] DP at once, then +[cost].cost DP every [cost].interval s; the first activation of the battle swaps the
|
||
// base costs of the most (guard/caster/sniper first) and least expensive waiting ops. "风雪之眼变为可部署" is not
|
||
// modelled (nobody deploys by hand in the auto battle).
|
||
chess_char_5_14_a: (bb, chess, def) => {
|
||
const t0 = talent(chess, 0), t1 = talent(chess, 1);
|
||
const grid = skillGrid(chess, def) ?? [[0, 0], [0, 1]];
|
||
const eyeId = chess?.skill?.overrideTokenKey ?? talentRec(chess, 0)?.tokenKey ?? 'token_10057_svash2_eagle2';
|
||
const n = Math.max(1, num(bb.max_target, 6)), maxStacks = Math.max(1, num(bb.max_stack_cnt, 2));
|
||
const PREF = ['WARRIOR', 'CASTER', 'SNIPER'];
|
||
/** Waiting (knocked-out) ops of his player split around the eye cost, and the one nearest to the eye (right side first). */
|
||
const waitingArea = (battle, unit) => {
|
||
const eye = unit.mem.eyeCost ?? 0;
|
||
const waiting = battle.allyUnits.filter((a) => isOp(a) && a !== unit && a.ownerId === unit.ownerId && !a.alive && !a.removed);
|
||
const right = waiting.filter((a) => a.base.cost >= eye).sort((a, b) => a.base.cost - b.base.cost || a.id - b.id);
|
||
const left = waiting.filter((a) => a.base.cost < eye).sort((a, b) => b.base.cost - a.base.cost || a.id - b.id);
|
||
const pick = right.find((a) => PREF.includes(a.def?.profession)) ?? right[0] ?? left[0] ?? null;
|
||
return { waiting, right, left, pick };
|
||
};
|
||
const cutCost = (a, v) => {
|
||
if (!a || !(v > 0)) return;
|
||
a.mem.svashCostBase ??= a.base.cost;
|
||
a.base.cost = Math.max(0, a.base.cost - v);
|
||
};
|
||
const slash = (battle, caster, atk) => {
|
||
// PRTS 备注: the slash ignores 隐匿 and hits flying enemies (and its reveal makes them targetable afterwards)
|
||
for (const e of enemiesInGrid(battle, caster, grid, n, { ignoreStealth: true })) {
|
||
battle.dealDamage(caster, e, { amount: atk * num(bb.atk_scale, 1), type: 'phys', isSkill: true, tags: ['skill', 'svash2'] });
|
||
if (!e.alive) continue;
|
||
battle.applyStatus(e, 'cold', { duration: num(bb.cold), source: caster });
|
||
battle.applyStatus(e, 'reveal', { duration: num(bb.cold), source: caster });
|
||
}
|
||
battle.fx('aoe', { x: caster.x + caster.fwd[1] * 1.5, y: caster.y + caster.fwd[0] * 1.5, id: caster.id, r: 2, skill: 'svash2' });
|
||
};
|
||
return {
|
||
skills: lazySkills({
|
||
// (自动触发: an AUTO skill takes no 技能策略 — an AUTO DP skill fires as soon as it is ready, like 伺夜 S1)
|
||
skchr_svash2_1: () => ({
|
||
kind: instantKind(chess, def),
|
||
trigger: 'SP_FULL',
|
||
onStart({ battle, unit }) {
|
||
battle.addDp(unit.ownerId, num(bb.cost));
|
||
const { pick } = waitingArea(battle, unit);
|
||
cutCost(pick, num(bb['svash2_s_1[deck].cost']));
|
||
if (pick) pick.mem.svashShield = unit.s.maxHp * num(bb['svash2_s_1[deck].shield']);
|
||
battle.fx('aoe', { x: unit.x, y: unit.y, id: unit.id, r: 1, skill: 'svash2Plan' });
|
||
},
|
||
}),
|
||
skchr_svash2_3: () => ({
|
||
kind: 'duration',
|
||
targeting: skillRange(chess, def),
|
||
attack: {
|
||
atkScale: num(bb.bird_atk_scale, 1),
|
||
onEachHit({ battle, unit, target }) {
|
||
if (!target || !target.alive || target.side !== 'enemy') return;
|
||
const v = num(bb.damage_scale, 1) - 1;
|
||
if (v > 0) battle.applyStatus(target, 'fragile', { duration: num(bb['weak[limit]'], 2), value: v, source: unit });
|
||
},
|
||
},
|
||
onStart({ battle, unit }) {
|
||
battle.addDp(unit.ownerId, num(bb['svash2_s_3[start_cost].cost']));
|
||
unit.mem.svashDpAcc = 0;
|
||
unit.mem.svashReveal = AURA_IV;
|
||
if (!unit.mem.svashSwapped) { // 首次开启时交换…基础部署费用
|
||
unit.mem.svashSwapped = true;
|
||
const { waiting } = waitingArea(battle, unit);
|
||
const baseCost = (a) => a.mem.svashCostBase ?? a.base.cost;
|
||
const byCost = waiting.slice().sort((a, b) => baseCost(b) - baseCost(a) || a.id - b.id);
|
||
const hi = byCost.find((a) => PREF.includes(a.def?.profession)) ?? byCost[0];
|
||
const lo = byCost.filter((a) => a !== hi).pop();
|
||
if (hi && lo && baseCost(hi) !== baseCost(lo)) {
|
||
const ch = baseCost(hi), cl = baseCost(lo);
|
||
for (const [a, v] of [[hi, cl], [lo, ch]]) {
|
||
if (a.mem.svashCostBase != null) { a.base.cost = Math.max(0, v - (a.mem.svashCostBase - a.base.cost)); a.mem.svashCostBase = v; } else a.base.cost = v;
|
||
}
|
||
}
|
||
}
|
||
},
|
||
onTick({ battle, unit, dt }) {
|
||
const iv = Math.max(0.1, num(bb['svash2_s_3[cost].interval'], 2));
|
||
unit.mem.svashDpAcc += dt;
|
||
while (unit.mem.svashDpAcc >= iv - 1e-9) { unit.mem.svashDpAcc -= iv; battle.addDp(unit.ownerId, num(bb['svash2_s_3[cost].cost'], 1)); }
|
||
unit.mem.svashReveal += dt;
|
||
if (unit.mem.svashReveal < AURA_IV - 1e-9) return;
|
||
unit.mem.svashReveal = 0;
|
||
for (const e of enemiesInGrid(battle, unit, grid, 0, { ignoreStealth: true })) battle.applyStatus(e, 'reveal', { duration: AURA_DUR, source: unit });
|
||
},
|
||
}),
|
||
}),
|
||
skill: {
|
||
kind: 'charges', // DEFAULT (data): cast right before an attack on an enemy in his initial (melee) range
|
||
onStart({ battle, unit }) {
|
||
slash(battle, unit, unit.s.atk);
|
||
// cost cut: nearest to the eye, right side guard/caster/sniper preferred
|
||
const { left, pick } = waitingArea(battle, unit);
|
||
cutCost(pick, num(bb.cost));
|
||
for (const a of left) a.mem.svashCasts = Math.min(maxStacks, num(a.mem.svashCasts) + 1);
|
||
},
|
||
},
|
||
talents: [
|
||
{ install(battle, unit) { // 开放性开局
|
||
const sp = num(t0.sp), mul = num(t0.respawn_time, 1);
|
||
const left = (a) => isOp(a) && a.ownerId === unit.ownerId && (a === unit || a.base.cost < num(unit.mem.eyeCost));
|
||
battle.on('battleStart', () => {
|
||
if (!on(unit) || !(sp > 0)) return;
|
||
for (const a of battle.allies(unit.ownerId)) if (left(a) && a.skill) a.skill.gainSp(sp, 'talent');
|
||
}, { owner: unit });
|
||
battle.on('deploy', (c) => {
|
||
const a = c.unit;
|
||
if (c.initial || !left(a) || !(a === unit || on(unit))) return;
|
||
if (sp > 0 && a.skill) a.skill.gainSp(sp, 'talent');
|
||
}, { owner: unit });
|
||
battle.on('death', (c) => {
|
||
const a = c.unit;
|
||
if (!left(a) || a.removed || !(a === unit || on(unit)) || !Number.isFinite(a.respawnAt) || !(mul > 0 && mul < 1)) return;
|
||
a.respawnAt = a.deathAt + (a.respawnAt - a.deathAt) * mul;
|
||
}, { owner: unit });
|
||
} },
|
||
{ install(battle, unit) { // 雪境先驱
|
||
const defv = num(t1.def), regen = num(t1.hp_recovery_per_sec_by_max_hp_ratio), after = num(t1.interval, 15);
|
||
whileOn(battle, unit, AURA_IV, () => {
|
||
const m = battle.time - unit.deployedAt >= after - 1e-9 ? 2 : 1;
|
||
for (const a of battle.allies()) {
|
||
if (isOp(a) && isKjerag(a)) battle.addBuff(a, { key: 'svash2:snow', duration: AURA_DUR, mods: mods({ defFlat: defv * m, hpRegenRatio: regen * m }) });
|
||
}
|
||
});
|
||
battle.on('beforeStatus', (c) => {
|
||
if (c.status === 'freeze' && c.target.side === 'ally' && c.target.findBuff('svash2:snow')) c.cancel = true;
|
||
}, { owner: unit });
|
||
} },
|
||
],
|
||
install(battle, unit) {
|
||
const tok = battle.tokenDef(eyeId, unit); // the owner's skill variant (DESIGN §16)
|
||
unit.mem.eyeCost = num(tok?.stats?.cost, 14);
|
||
if (selectedId(chess, def) === 'skchr_svash2_3') {
|
||
// S3: every attack hits the enemies of the skill range on the target's line (his facing frame): the line of
|
||
// the target's position; another enemy is on it when its body is (a huge one: any tile — body.js)
|
||
battle.on('beforeAttack', (c) => {
|
||
if (c.attacker !== unit || !unit.skill?.active || !c.targets.length) return;
|
||
const main = c.targets[0];
|
||
const lat = (r, cc) => toLocal(r - unit.tileR, cc - unit.tileC, unit.dir)[0];
|
||
const l0 = lat(Math.round(main.y), Math.round(main.x));
|
||
const onLine = (e) => bodyKeys(e).some((k) => lat(Math.floor(k / COLS), k % COLS) === l0);
|
||
const line = battle.enemiesInKeys(unit.rangeKeys, unit, unit.profile).filter((e) => e !== main && onLine(e));
|
||
if (line.length) c.targets = [main, ...line];
|
||
}, { owner: unit, priority: 10 });
|
||
}
|
||
// waiting-area effects resolve when the op (re)deploys
|
||
battle.on('deploy', (c) => {
|
||
const a = c.unit;
|
||
if (!isOp(a) || a.ownerId !== unit.ownerId) return;
|
||
if (a.mem.svashCostBase != null) { a.base.cost = a.mem.svashCostBase; a.mem.svashCostBase = null; }
|
||
if (num(a.mem.svashShield) > 0 && !c.initial) { // S1: 部署后获得…屏障
|
||
battle.addBuff(a, { key: 'svash2:barrier', shield: a.mem.svashShield, visible: true });
|
||
a.mem.svashShield = 0;
|
||
}
|
||
const casts = num(a.mem.svashCasts);
|
||
if (casts > 0 && !c.initial) {
|
||
a.mem.svashCasts = 0;
|
||
for (let i = 0; i < casts; i++) slash(battle, a, a.s.atk);
|
||
}
|
||
}, { owner: unit });
|
||
},
|
||
};
|
||
},
|
||
};
|