Files
ark-stronghold-protocol/server/sim/content/kits/ops/chess_char_5_01-excu2.js
T
LimitlessHPPK 5a50f7b5fa feedback7 R22-GHSIM: the module trait line 「攻击范围内存在N名及以上敌人时攻击速度+X」 (REA-Y: 圣约送葬人, 隐德来希) is applied by the engine instead of the two kits — on and off on the tick the count changes; a scan of every chess, 补位 and 自选 loadout shows no other operator gains it (PR #293 by @LimitlessHPPK); golden: unchanged
The contributor's code (server/sim/content/traitMods.js, the battle/players.js _setupUnit wiring after kit.install, the
crowdAspd removal from shared/tier5.js and the two kits, test/sim/trait_attack_speed.test.js), applied from PR #293, so
they are the author of this commit. Maintainer additions: the coverage lock also scans the 补位 stand-ins (266) and every
legal 自选 combination (pool operator × slot form × skill × non-other-mode module) — none gains the rule; 黑键 / 维伊
MSC-Y 「拥有已储存的攻击能量时,攻击速度+30」 (their 自选 kits own it) is listed as kit-owned instead of 'unknown'; the
comment's history note named a commit this repository does not have, and its line-number references are now names;
docs/ARCHITECTURE.md and kits/README.md name the file.

Official: battle_equip_table uniequip_003_excu2 (display trait + hidden talent attack_speed 12, trigger_cnt[equip] 2) and
uniequip_003_etlchi (trait attack_speed 12, cnt 2), both 「攻击范围内存在2名及以上敌人时攻击速度+12」; 仇白's
uniequip_003_qiubai carries the same sentence on her named talent, which her kit keeps reading (not seen by the engine).

Before: shared/tier5.js crowdAspd polled every 0.2 s and gave a 0.3 s buff — measured +12 0.167 s after the second enemy
arrived and 0.267 s after it died (scratchpad repro); now one tick each. Golden: unchanged (no sampled snapshot moves).
2026-10-08 18:56:12 +08:00

127 lines
7.5 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_5_01-excu2.js — 圣约送葬人 (char_1032_excu2) kit, tier 5.
// Conventions of the tier-5 kits: ../shared/tier5.js; kit contract and rules: ../README.md.
import {
num, on, talent, batPct, mods, inFaction, isOp, selectedId, lazySkills, skillRange,
} from '../shared/tier5.js';
const isLaterano = (u) => inFaction(u, 'lateranoShip', ['laterano']);
/** Enemy `e` attacks `t` in melee (not a ranged shot), mirroring ai.js enemyAttack. */
function meleeAttacker(e, t) {
const melee = e.profile?.melee ?? e.def?.applyWay === 'MELEE';
const r = num(e.base?.rangeRadius, 0);
return melee || !(r > 0) || (e.blockedBy === t && r < 1);
}
export default {
// ---------------------------------------------------------------------------------------------------------------
// 圣约送葬人 — S2 近身铳斗 (ammo, attack SP): ATK/DEF +, block +1, melee hits dodged with prob (+ammo refill).
// T1 受选之人: extra attack chance (+prob_add per ammo spent in the skill). T2 铳弹共感: +ammo per Laterano op.
// S1 遗嘱执行 (ammo 8, attack SP): skill range, ATK +, ignores def_penetrate_fixed DEF.
// S3 圣约决裁 (ammo 16): skill range, BAT +0.5 s, ATK +; +attack@atk ATK per bullet spent (≤ attack@max_stack_cnt),
// reaper self-heal ×trait_ratio; at the end every enemy attacked during the skill takes attack@final_atk_scale × ATK phys.
// Module REA-Y (elite): ASPD +12 with ≥ 2 enemies in range.
chess_char_5_01_a: (bb, chess, def) => {
const t0 = talent(chess, 0), t1 = talent(chess, 1), tm = talent(chess, -1);
const sid = selectedId(chess, def);
const prob = num(bb.prob), refill = num(bb.recover_cnt, 1);
return {
skills: lazySkills({
skchr_excu2_1: () => ({
kind: 'ammo', ammo: num(bb['attack@trigger_time'], 8),
mods: mods({ atkPct: num(bb.atk), defIgnoreFlat: num(bb.def_penetrate_fixed) }),
targeting: skillRange(chess, def),
}),
skchr_excu2_3: () => ({
kind: 'ammo', ammo: num(bb['attack@trigger_time'], 16),
mods: mods({ atkPct: num(bb.atk), batPct: batPct(bb.base_attack_time, chess) }),
targeting: skillRange(chess, def),
attack: { onEachHit({ unit, target }) { if (target && target.side === 'enemy') unit.mem.verdict?.add(target); } },
onStart({ unit }) {
unit.mem.verdict = new Set();
// 特性的回复生命效果提高至2倍 (the reaper profile reads profile.selfHeal on every attack)
if (unit.profile) { unit.mem.verdictHeal0 ??= num(unit.profile.selfHeal, 50); unit.profile.selfHeal = unit.mem.verdictHeal0 * num(bb.trait_ratio, 1); }
},
onEnd({ battle, unit }) {
const hit = unit.mem.verdict;
unit.mem.verdict = null;
if (unit.profile && unit.mem.verdictHeal0 != null) unit.profile.selfHeal = unit.mem.verdictHeal0;
if (hit && unit.alive) {
for (const e of hit) {
if (e.alive) battle.dealDamage(unit, e, { amount: unit.s.atk * num(bb['attack@final_atk_scale']), type: 'phys', isSkill: true, tags: ['skill', 'verdict'] });
}
battle.fx('aoe', { x: unit.x, y: unit.y, id: unit.id, r: 1.5, skill: 'excu2' });
}
battle.removeBuff(unit, 'excu2:verdict');
},
}),
}),
skill: {
kind: 'ammo', ammo: num(bb['attack@trigger_time'], 12),
mods: mods({ atkPct: num(bb.atk), defPct: num(bb.def), blockCnt: num(bb.block_cnt) }),
},
// REA-Y: its trait part is DISPLAY-only (official battle_equip_table uniequip_003_excu2: target DISPLAY, the ASPD
// text "{value}" = 12; the ASPD rider itself is the talent −1 part). data/chess.json merges that display blackboard
// into trait.bb ({value: 12}), which the reaper profile would read as its per-hit heal: the heal stays the base
// trait's (50).
trait: tm['trigger_cnt[equip]'] != null && chess?.traitBase?.bb?.value != null ? { selfHeal: num(chess.traitBase.bb.value, 50) } : undefined,
talents: [
{ install(battle, unit) { // 受选之人
let spent = 0;
const reset = (c) => { if (c.unit === unit) spent = 0; };
battle.on('skillStart', reset, { owner: unit });
battle.on('skillEnd', reset, { owner: unit });
battle.on('ammoUsed', (c) => { if (c.unit === unit) spent++; }, { owner: unit });
// PRTS 备注: "触发时可额外回复1点攻击回复技力,不额外消耗弹药" — the extra attack is an attack (attack SP, trait
// heal) but consumes no ammo (engine forceAttack `noAmmo`: no bullet, no ammoUsed for any listener — the
// 7-bullet 特质 counter, items, bonds and the prob_add counter above)
battle.on('attack', (c) => {
if (c.attacker !== unit || unit.mem.extraAttack || !on(unit)) return;
const p = num(t0.prob) + (unit.skill?.active ? num(t0.prob_add) * spent : 0);
if (!(p > 0) || !battle.rng.chance(Math.min(1, p))) return;
const dep = unit.deploySeq;
battle.after(0, () => {
if (!on(unit) || unit.deploySeq !== dep || !unit.canAct || unit.s.flags.disarm) return;
unit.mem.extraAttack = true;
let hit = false;
try { hit = battle.forceAttack(unit, null, { noAmmo: true }); } finally { unit.mem.extraAttack = false; }
if (hit) battle.fx('extraAttack', { x: unit.x, y: unit.y, id: unit.id });
}, { owner: unit });
}, { owner: unit });
} },
{ install(battle, unit) { // 铳弹共感
battle.on('skillStart', (c) => {
if (c.unit !== unit || c.skill.kind !== 'ammo') return;
const n = battle.allies().filter((a) => isOp(a) && isLaterano(a)).length;
const stacks = Math.min(num(t1.add_count_max_stack, 4), n);
if (stacks > 0) c.skill.addAmmo(stacks * num(t1.add_count, 1));
}, { owner: unit });
} },
],
install(battle, unit) {
// REA-Y "攻击范围内存在2名及以上敌人时攻击速度+12": the ENGINE owns this trait line now
// (server/sim/content/traitMods.js, applied from battle/players.js _setupUnit for every operator) — a second
// implementation here would count the same 12 points twice.
if (sid === 'skchr_excu2_3') { // S3: +attack@atk ATK per bullet spent
battle.on('ammoUsed', (c) => {
if (c.unit !== unit || !unit.skill?.active) return;
battle.addBuff(unit, { key: 'excu2:verdict', refresh: 'stack', maxStacks: Math.max(1, num(bb['attack@max_stack_cnt'], 30)), mods: mods({ atkPct: num(bb['attack@atk']) }) });
}, { owner: unit });
}
if (sid && sid !== 'skchr_excu2_2') return;
// S2: melee attacks dodged with `prob`, each dodge refills ammo
battle.on('hit', (c) => {
if (c.target !== unit || !unit.skill?.active || !c.source || c.source.side !== 'enemy' || !c.dmg.isAttack) return;
if (!meleeAttacker(c.source, unit) || !(prob > 0) || !battle.rng.chance(prob)) return;
c.dmg.cancel = true;
battle.fx('dodge', { x: unit.x, y: unit.y, id: unit.id });
// a dodge like the engine's (listeners of the `dodge` hook — items/bonds — see it too)
if (battle.hasHook('dodge')) battle.emit('dodge', { source: c.source, target: unit, dmg: c.dmg });
unit.skill.addAmmo(refill);
}, { owner: unit });
},
};
},
};