Files
ark-stronghold-protocol/server/sim/content/kits/ops/chess_char_6_05-pasngr.js
T
sganggs a0cd6826e5 feedback7 R22-GHSIM: 异客 S3's 雷暴 covers range x-1 — the 13-tile diamond — around the tile of the target's centre, not a 1.5-tile circle around its position (GitHub #322, PR #329 by @IceCodeNew, re-implemented); golden: roster-016/018/048, fields boss-boss_1/3/5-pair + hidden-boss_9-pair, matches solo-HARD-1, solo-ABYSS-1, coop2-HARD-4, coop2-ABYSS-10-boosted, coop2-NORMAL-70-diy move
PRTS 异客 S3 备注: 「开启技能时选择技能范围内的目标,以其判定中心所在格为中心生成一个x-1的雷暴区域」; range_table x-1 is the
13-tile diamond (|Δrow| + |Δcol| ≤ 2); x-3 (|技能范围|) stays the search range. The storm's tile is the rounding of the
target's centre (body.js posKey), fixed for its 4 s; a strike picks among the enemies she can select whose body is on
those tiles (enemiesInKeys, as her other range checks); each strike still chains on its own bounce radius. The fx ring is
drawn at radius 2 (visual only). Until now: [ASSUMED] radius 1.5 around the target's exact position — an enemy two tiles
straight off the target's tile was never struck, one at (+1.55, +1.05) off the tile centre was.

Re-implemented from the PR's claim (its golden files were not taken). Golden: every moved scenario fields 异客 casting
S3 — the zone holds other enemies and the random pick draws from another list.
2026-10-08 19:03:55 +08:00

120 lines
6.8 KiB
JavaScript
Raw Blame History

This file contains ambiguous Unicode characters
This file contains Unicode characters that might be confused with other characters. If you think that this is intentional, you can safely ignore this warning. Use the Escape button to reveal them.
// server/sim/content/kits/ops/chess_char_6_05-pasngr.js — 异客 (char_472_pasngr) kit, tier 6.
// Conventions of the tier-6 kits: ../shared/tier6.js; kit contract and rules: ../README.md.
import { absoluteRangeKeys, canTargetEnemy } from '../../../targeting.js';
import { CHAIN_RADIUS } from '../../../constants.js';
import { bodyDist, bodyInKeys } from '../../../body.js';
import { num, bv, tbb, live, ANY, enemiesIn, batOf, N4, aura } from '../shared/tier6.js';
import { powi } from '../../../detmath.js';
// ------------------------------------------------------------------------------------------------------------------
// 异客 chess_char_6_05 (链术师) — S3 辉煌裂片; 机理分析; 孤卒
/** S3 雷暴区域: PRTS 备注 「以其判定中心所在格为中心生成一个x-1的雷暴区域」 — range x-1 (range_table: the diamond of radius 2,
* 13 tiles) around the tile of the target's centre (GitHub #322, PR #329). Until 0.2.2 a 1.5-tile circle around the
* target's exact position [ASSUMED]. The chain of each strike bounces on its own radius, inside or outside the zone. */
const STORM_GRID = Object.freeze([[2, 0], [1, -1], [1, 0], [1, 1], [0, -2], [0, -1], [0, 0], [0, 1], [0, 2], [-1, -1], [-1, 0], [-1, 1], [-2, 0]]);
/** The storm fx's ring radius (tiles): the diamond's reach — drawn only, the zone is STORM_GRID. */
const STORM_FX_RADIUS = 2;
function pasngr(bb, chess, def) {
const t0 = tbb(def, 0), t1 = tbb(def, 1), tb = def?.traitBb || {};
const skillGrid = def?.skill?.rangeGrid?.length ? def.skill.rangeGrid : null;
const strike = (battle, unit, first, scale) => {
const ch0 = unit.profile?.chain || { count: 4, falloff: 0.15, radius: CHAIN_RADIUS };
// elite module (电磁调节器) upgrades the skill's chain too: skill@chain.atk_scale / skill@sluggish
const ch = { ...ch0, falloff: tb['skill@chain.atk_scale'] != null ? 1 - num(tb['skill@chain.atk_scale']) : num(ch0.falloff, 0.15) };
const count = Math.max(1, Math.floor(num(bb['chain.max_target'], ch.count || 4)));
const slug = num(tb['skill@sluggish'], num(bb.sluggish, ch.sluggish ?? 0));
const hit = new Set();
let prev = first;
for (let i = 0; i < count && prev; i++) {
hit.add(prev.id);
battle.fx('lightning', { x: prev.x, y: prev.y, id: prev.id, src: unit.id });
battle.dealDamage(unit, prev, { amount: unit.s.atk * scale * powi(1 - num(ch.falloff, 0.15), i), type: 'arts', isSkill: true, isAttack: true, tags: ['skill', 'storm'] });
if (slug > 0 && prev.alive) battle.applyStatus(prev, 'sluggish', { duration: slug, source: unit });
let best = null, bd = Infinity;
for (const x of battle.foesInRadius(prev.x, prev.y, ch.radius || CHAIN_RADIUS)) {
if (hit.has(x.id) || !canTargetEnemy(unit, x, ANY)) continue;
const d = bodyDist(x, prev.x, prev.y);
if (d < bd - 1e-9) { bd = d; best = x; }
}
prev = best;
}
};
// the chain of her profile (trait / module: bounce count, falloff, 停顿) with skill overrides
const chainOf = (unit, o) => ({ ...(unit.profile?.chain || { count: 4, falloff: 0.15, radius: CHAIN_RADIUS, sluggish: 0.5 }), ...o });
const skills = {
// S1 电能之触: next attack at atk_scale × ATK, bouncing over max_target enemies with a sluggish-s 停顿 (the module's
// skill@pasngr_s_1.chain.atk_scale sets its falloff)
skchr_pasngr_1: {
kind: 'instant',
attack: { atkScale: bv(bb, 'atk_scale', 1) },
onStart({ unit, skill }) {
const o = { count: Math.max(1, Math.floor(bv(bb, 'max_target', 4))), sluggish: bv(bb, 'sluggish', 0.5) };
if (tb['skill@pasngr_s_1.chain.atk_scale'] != null) o.falloff = 1 - num(tb['skill@pasngr_s_1.chain.atk_scale']);
skill.spec.attack.chain = chainOf(unit, o);
},
},
// S2 聚焦指令: range +1, ATK +atk, base attack time ×0.7/×0.6 (akdata: 乘算负数), bounces raised to attack@max_target
skchr_pasngr_2: {
kind: 'duration',
mods: { atkPct: num(bb.atk), batPct: batOf(bb.base_attack_time, def, true) },
targeting: { rangeExtend: num(bb.ability_range_forward_extend) },
attack: {},
onStart({ unit, skill }) { skill.spec.attack.chain = chainOf(unit, { count: Math.max(1, Math.floor(num(bb['attack@max_target'], 5))) }); },
},
};
return {
skills,
skill: {
kind: 'charges',
onStart({ battle, unit }) {
const keys = skillGrid ? absoluteRangeKeys(skillGrid, unit.tileR, unit.tileC, unit.dir, unit.s.rangeExtend) : unit.rangeKeys;
const cands = enemiesIn(battle, unit, keys);
if (!cands.length) return;
const tgt = cands.reduce((a, b) => (b.hp > a.hp ? b : a));
// 判定中心所在格: the tile of the target's centre (the rounding of body.js posKey); fixed for the storm's 4 s
const cx = Math.round(tgt.x), cy = Math.round(tgt.y);
const zoneKeys = absoluteRangeKeys(STORM_GRID, cy, cx, 1, 0);
const dur = num(bb.duration, 4), iv = Math.max(0.1, num(bb.interval, 0.5)), scale = num(bb.atk_scale, 1);
const n = Math.max(1, Math.round(dur / iv));
battle.fx('storm', { x: cx, y: cy, id: unit.id, duration: dur, r: STORM_FX_RADIUS });
let k = 0;
const seq = unit.deploySeq;
const h = battle.every(iv, () => {
if (!live(unit) || unit.deploySeq !== seq) { h.cancel(); return; } // the storm ends when she leaves the field
if (++k >= n) h.cancel();
const zone = enemiesIn(battle, unit, zoneKeys); // every enemy she can select whose body is on the 13 tiles
const e = battle.rng.pick(zone);
if (e) strike(battle, unit, e, scale);
}, { owner: unit });
},
},
talents: [
{ install(battle, unit) { // 机理分析
const thr = num(t0.hp_ratio, 0.8), ds = bv(t0, 'damage_scale', 1), dur = bv(t0, 'duration', 3);
const key = `pasngr:enhance:${unit.id}`;
battle.on('hit', (ctx) => {
const t = ctx.target;
if (ctx.source !== unit || !t || t.side !== 'enemy' || ctx.dmg.type === 'element') return;
if (ctx.dmg.isAttack && t.hpRatio >= thr - 1e-9) battle.addBuff(t, { key, duration: dur, refresh: 'replace', source: unit });
if (t.findBuff(key)) ctx.dmg.mul *= ds;
}, { owner: unit });
} },
{ install(battle, unit) { // 孤卒: no enemy on the 4 surrounding tiles
const atk = num(t1.atk), spr = num(t1.sp_recovery_per_sec);
aura(battle, unit, 0.25, () => {
const near = new Set(absoluteRangeKeys(N4, unit.tileR, unit.tileC, 1, 0));
if (battle.enemies.some((e) => e.alive && !e.hidden && bodyInKeys(e, near))) return;
battle.addBuff(unit, { key: 'pasngr:lone', mods: { atkPct: atk, spRecoveryFlat: spr }, duration: 0.4, refresh: 'replace' });
});
} },
],
};
}
export default {
chess_char_6_05_a: pasngr,
};