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.
106 lines
5.1 KiB
JavaScript
106 lines
5.1 KiB
JavaScript
// server/sim/content/kits/ops/chess_char_6_12-rosmon.js — 迷迭香 (char_391_rosmon) kit, tier 6.
|
||
// Conventions of the tier-6 kits: ../shared/tier6.js; kit contract and rules: ../README.md.
|
||
|
||
import { sortEnemyTargets } from '../../../targeting.js';
|
||
import { summonToken } from '../../tokens.js';
|
||
import { num, tbb, live, opsOf, selectedSkill, batOf } from '../shared/tier6.js';
|
||
|
||
// ------------------------------------------------------------------------------------------------------------------
|
||
// 迷迭香 chess_char_6_12 (投掷手) — S2 末梢阻断; 歼灭战装备; 感知稳定
|
||
|
||
/** S2 末梢阻断 "溅射范围扩大": radius 1.5 (PRTS 溅射半径一览, 技能: 迷迭香 末梢阻断 1.5; ×1.3 [ASSUMED] until 0.1.1). */
|
||
const ROSMON_S2_SPLASH = 1.5;
|
||
|
||
function rosmon(bb, chess, def) {
|
||
const t0 = tbb(def, 0), t1 = tbb(def, 1);
|
||
const sid = selectedSkill(chess, def);
|
||
const prob = num(bb['attack@prob']), stun = num(bb['attack@stun']);
|
||
const gearId = (chess?.tokens || []).find((t) => /rosmon_shield/.test(String(t))) || 'token_10012_rosmon_shield';
|
||
const skills = {
|
||
// S1 思维膨大 (attack SP): the next attack also deals extra_atk_scale × ATK arts to every enemy it hits
|
||
skchr_rosmon_1: {
|
||
kind: 'instant',
|
||
attack: {
|
||
onEachHit({ battle, unit, target, kind }) {
|
||
if (!target || !target.alive || (kind !== 'main' && kind !== 'splash')) return;
|
||
battle.dealDamage(unit, target, { amount: unit.s.atk * num(bb.extra_atk_scale, 1), type: 'arts', isSkill: true, tags: ['skill'] });
|
||
},
|
||
},
|
||
},
|
||
// S3 “如你所愿”: base attack time ×0.5 (akdata: 乘算负数), ATK +atk, 2 targets but blocked enemies only (install);
|
||
// two 战术装备 on melee tiles of her range (the token kit: appear stun, blocked enemies DEF −160, lifetime)
|
||
skchr_rosmon_3: {
|
||
kind: 'duration',
|
||
mods: { batPct: batOf(bb.base_attack_time, def, true), atkPct: num(bb.atk) },
|
||
targeting: { maxTargets: Math.max(1, Math.floor(num(bb['attack@max_target'], 2))) },
|
||
onStart({ battle, unit }) {
|
||
let n = 0;
|
||
for (let i = 0; i < 2; i++) if (summonToken(battle, unit, gearId, 'melee')) n++;
|
||
battle.fx('summon', { x: unit.x, y: unit.y, id: unit.id, n });
|
||
},
|
||
},
|
||
};
|
||
return {
|
||
skills,
|
||
install(battle, unit) {
|
||
if (sid !== 'skchr_rosmon_3') return;
|
||
battle.on('beforeAttack', (ctx) => { // "仅选择被阻挡的敌人作为目标"
|
||
if (ctx.attacker !== unit || !unit.skill?.active) return;
|
||
// her range, plus the enemies she blocks herself — always her targets (Battle.blockedTargets, DESIGN §20.3)
|
||
const c = battle.enemiesInKeys(unit.rangeKeys, unit, ctx.profile);
|
||
for (const e of battle.blockedTargets(unit, ctx.profile)) if (!c.includes(e)) c.push(e);
|
||
for (let i = c.length - 1; i >= 0; i--) if (!c[i].blockedBy) c.splice(i, 1);
|
||
sortEnemyTargets(battle, unit, c, ctx.profile?.priority ?? null);
|
||
ctx.targets = c.slice(0, Math.max(1, Math.floor(num(ctx.profile?.maxTargets, 2))));
|
||
}, { owner: unit });
|
||
},
|
||
skill: {
|
||
kind: 'duration',
|
||
mods: { batPct: batOf(bb.base_attack_time, def), atkPct: num(bb.atk) },
|
||
onStart({ unit }) {
|
||
const p = unit.profile;
|
||
unit.mem.rosSaved = { r: p.splashRadius, n: p.shockTimes };
|
||
p.splashRadius = Math.max(num(p.splashRadius, 0.9), ROSMON_S2_SPLASH);
|
||
p.shockTimes = num(p.shockTimes, 2) + Math.floor(num(bb.add_times));
|
||
},
|
||
onEnd({ unit }) {
|
||
const s = unit.mem.rosSaved;
|
||
if (s) { unit.profile.splashRadius = s.r; unit.profile.shockTimes = s.n; }
|
||
unit.mem.rosSaved = null;
|
||
},
|
||
},
|
||
talents: [
|
||
{ install(battle, unit) { // S2: attack & aftershock stun chance
|
||
battle.on('damaged', (ctx) => {
|
||
if (ctx.source !== unit || !unit.skill?.active || !(stun > 0) || !ctx.target.alive || ctx.target.side !== 'enemy') return;
|
||
if (!ctx.dmg?.isAttack && !ctx.dmg?.tags?.includes('aftershock')) return;
|
||
if (battle.rng() < prob) battle.applyStatus(ctx.target, 'stun', { duration: stun, source: unit });
|
||
}, { owner: unit });
|
||
} },
|
||
{ install(battle, unit) { // 歼灭战装备
|
||
const v = num(t0.def_penetrate_fixed);
|
||
if (v) battle.addBuff(unit, { key: 'rosmon:pierce', mods: { defIgnoreFlat: v }, persist: true, allowDead: true });
|
||
} },
|
||
{ install(battle, unit) { // 感知稳定
|
||
const atk = num(t1.atk);
|
||
battle.on('deploy', ({ unit: u }) => {
|
||
if (u !== unit || !atk) return;
|
||
battle.after(0, () => {
|
||
if (!live(unit)) return;
|
||
const casters = opsOf(battle, unit.ownerId).filter((a) => a.def?.profession === 'CASTER');
|
||
const pick = battle.rng.pick(casters);
|
||
if (!pick) return;
|
||
battle.addBuff(unit, { key: 'rosmon:stable', mods: { atkPct: atk } });
|
||
battle.addBuff(pick, { key: 'rosmon:stable', mods: { atkPct: atk } });
|
||
battle.fx('link', { x: pick.x, y: pick.y, id: pick.id, src: unit.id });
|
||
}, { owner: unit });
|
||
}, { owner: unit });
|
||
} },
|
||
],
|
||
};
|
||
}
|
||
|
||
export default {
|
||
chess_char_6_12_a: rosmon,
|
||
};
|