PRTS 分支特性信息 秘术师: "能量储存与攻击占用相同的攻击间隔:在进行攻击判定时…若无有效目标且能量储存数未满,则改为储存一份 攻击能量(属于攻击行为);仅没有有效目标,且能量储存数已满的情况下才进入待机状态", "攻击被打断且弹道未能成功生成的情况下,储存的 能量不会被消耗"; PRTS 异常效果 缴械 "秘术师储存能量同样无法进行". - ai.js updateAlly: a ready unit (able to act, not disarmed) whose attack check finds no valid target — no target, or its profile's canAttack false (a 秘术师 kit's target rule: 深靛's bound enemies, 维伊's marked ones) — calls the profile's storeEnergy; a stored energy restarts the attack interval, a full store idles with the attack ready. A DEFAULT cast at the check that leaves no target stores in the same check. - professions.js mystic: storeEnergy (up to bb times), releaseEnergy (performAttack, after beforeAttack: a cancelled shot keeps them) and hitsFn(b, u, info) — the energies leave with the attack and land with its main hit; the old separate store timer (it started only once the attack was ready and never restarted the interval) is gone. [ASSUMED] a redeployment holds no energy. - 维伊 / 黑键 kits: their own tick stores become the kit's storeEnergy on the shared check; 深靛 / 维伊 no longer write hadTarget themselves. Golden: roster-003, roster-021, roster-022, bond-preciShip-high (深靛 stores at her check); matches solo-FUNNY-1, solo-NORMAL-2, solo-HARD-2, solo-ABYSS-1, solo-ABYSS-2, coop2-NORMAL-3, coop2-HARD-4, coop4-FUNNY-6, coop4-ABYSS-7, coop2-NORMAL-8-serverrun, solo-HARD-9-boosted, coop2-ABYSS-10-boosted, coop2-NORMAL-14-standins (each fields 深靛); diy-032 (维伊) and diy-114 (黑键): the first store after a redeployment comes at the next tick's attack check instead of the end of the redeploy tick (with the old tick store added back as a fallback the diy family matches the old digests). fields, standins unchanged. Tests: test/sim/feedback5-mystic-store.test.js (new, 4); op_veen, op_ebnhlz, kits_t1t2, kits_alt_t1 pass unchanged. Docs: SIM.md §Professions (mystic row, profile hooks).
72 lines
4.1 KiB
JavaScript
72 lines
4.1 KiB
JavaScript
// server/sim/content/kits/ops/chess_char_1_17-indigo.js — 深靛 (char_469_indigo) kit, tier 1.
|
||
// Conventions of the tier-1 kits: ../shared/tier1.js; kit contract and rules: ../README.md.
|
||
|
||
import { sortEnemyTargets } from '../../../targeting.js';
|
||
import { num, talentBb, skillRec, batMod } from '../shared/tier1.js';
|
||
|
||
export default {
|
||
// ---------------------------------------------------------------------------------------------------------------
|
||
// 1_17 深靛 光影迷宫: attack interval ×base_attack_time, talent chance ×talent_scale, bound enemies in range take
|
||
// indigo_s_2[damage].atk_scale × ATK arts every indigo_s_2[damage].interval s.
|
||
// 柔光缚目: attacks bind the target `duration` s with `prob`; bound enemies are never chosen as targets.
|
||
// Alternate S1 灯塔守卫者: for `duration` s the skill range (4-1), attack interval ×(1 + base_attack_time) (PRTS:
|
||
// "大幅度缩短(-80%)" ⇒ ×0.2 — a flat −0.8 s on 3 s would be less than S2's "略微缩短 ×0.7"), every attack hits for
|
||
// attack@atk_scale × ATK arts. Elite module MSC-X (store 4) is the mystic profile of the module trait (TUNE.mystic);
|
||
// the store itself is the shared 秘术师 profile (professions.js installMystic: at the attack check, PRTS 分支特性信息).
|
||
chess_char_1_17_a: (bb, chess, def) => {
|
||
const t = talentBb(chess, 0);
|
||
const dScale = num(bb['indigo_s_2[damage].atk_scale']);
|
||
const dIv = num(bb['indigo_s_2[damage].interval'], 0.5);
|
||
const bound = (e) => !!e.s.flags.bind;
|
||
const r1 = skillRec(chess, 'skchr_indigo_1');
|
||
const b1 = r1?.bb ?? {};
|
||
return {
|
||
skills: {
|
||
skchr_indigo_1: {
|
||
kind: 'duration', mods: { batPct: Math.max(-0.95, Math.min(0, num(b1.base_attack_time))) },
|
||
targeting: { rangeGrid: r1?.rangeGrid ?? null },
|
||
attack: { atkScale: num(b1['attack@atk_scale'], 1) },
|
||
},
|
||
},
|
||
trait: {
|
||
// with only bound enemies in range (or blocked by her — always her targets, Battle.blockedTargets) she has no valid
|
||
// target: she holds her fire and the mystic trait stores an energy at the attack check (professions.js
|
||
// installMystic; a bind on her only target included)
|
||
canAttack(battle, u) {
|
||
return battle.enemiesInKeys(u.rangeKeys, u, u.profile).some((e) => !bound(e)) || battle.blockedTargets(u, u.profile).some((e) => !bound(e));
|
||
},
|
||
afterHit(battle, u, target) {
|
||
if (!target || !target.alive || target.side !== 'enemy') return;
|
||
const p = num(t.prob) * (u.skill?.active ? num(bb.talent_scale, 1) : 1);
|
||
if (p > 0 && battle.rng.chance(Math.min(1, p))) battle.applyStatus(target, 'bind', { duration: num(t.duration), source: u });
|
||
},
|
||
},
|
||
skill: {
|
||
kind: 'duration', mods: { batPct: batMod(bb.base_attack_time, chess, def?.skill?.description ?? chess?.skill?.desc) },
|
||
onStart({ unit }) { unit.mem.indigoAcc = 0; },
|
||
onTick({ battle, unit, dt }) {
|
||
if (!(dScale > 0) || !(dIv > 0)) return;
|
||
unit.mem.indigoAcc = (unit.mem.indigoAcc ?? 0) + dt;
|
||
while (unit.mem.indigoAcc >= dIv - 1e-9) {
|
||
unit.mem.indigoAcc -= dIv;
|
||
for (const e of battle.enemiesInKeys(unit.rangeKeys, unit, { canHitFly: true })) {
|
||
if (bound(e)) battle.dealDamage(unit, e, { amount: unit.s.atk * dScale, type: 'arts', isSkill: true, canDodge: false, tags: ['dot'] });
|
||
}
|
||
}
|
||
},
|
||
},
|
||
talents: [{ install(battle, unit) {
|
||
battle.on('beforeAttack', (ctx) => {
|
||
if (ctx.attacker !== unit || !ctx.targets.some(bound)) return;
|
||
const prof = ctx.profile || unit.profile;
|
||
const cands = battle.enemiesInKeys(unit.rangeKeys, unit, prof);
|
||
for (const e of battle.blockedTargets(unit, prof)) if (!cands.includes(e)) cands.push(e);
|
||
for (let i = cands.length - 1; i >= 0; i--) if (bound(cands[i])) cands.splice(i, 1);
|
||
sortEnemyTargets(battle, unit, cands, prof.priority);
|
||
ctx.targets = cands.slice(0, Math.max(1, ctx.targets.length));
|
||
}, { owner: unit });
|
||
} }],
|
||
};
|
||
},
|
||
};
|