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).
127 lines
7.5 KiB
JavaScript
127 lines
7.5 KiB
JavaScript
// 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 });
|
||
},
|
||
};
|
||
},
|
||
};
|