Stormeye (char_611_acnipe; 空弦, 莫斯提马, 百炼嘉维尔 / 妮芙, 蕾缪安 / 异客): 风坠 rolls on every attack instance (acnipe_t_1: ATK × atk_scale before DEF; S3 talent@prob), 风雨欲来 +0.2 SP/s after `delay` s without an attack [ASSUMED reading], S1 ATK / ASPD / 100 DEF ignored, S2 an endless toggle (ATK +5 %, 2 targets), S3 3 targets × double hit plus one physical ATK × 1.0 instance on a target at >= 90 % HP on the attack's first hit (acnipe_s_3[extra], no 风坠 / MAR-X on it); MAR-X stays the fastshot profile's flyScale. Pith (char_612_accast; 烛煌, 焰影苇草 / 迷迭香 / 荒芜拉普兰德): splash from the trait blackboard, 12 RES ignored, ATK +10 / +20 % on herself and the 【术师】 of the 4 tiles next to her, S1 ASPD + splash 1.5, S2 ASPD + ATK on every 【术师】, S3 one ATK × atk_scale_aoe arts hit on every enemy of her range at the cast, then 2 targets, ASPD, splash 1.2 (PRTS 备注); SPC-X range (moduleRangeUp). Raidian (char_614_acsupo; 灵知, 夕 / 安洁莉娜, 浊心斯卡蒂 / 仇白): 同调 on herself and the operators next to her (× talent_scale in S3), 诱引 per PRTS 命中率 — one roll per enemy attack in her range, a miss cancels the whole attack before the enemy riders (hit priority 300), S1 / S2 from the blackboards (S2 flat −0.1 / −0.2 s, 停顿 1.1 / 1.2 s), S3 y-4 + ATK + 脆弱 / 虚弱 aura with the 虚弱 stacking as 90 / 80 % (stackAs); S3 casts by the data's ACTIVE_RANGE; DEC-X +0.2 SP/s with an enemy in range. Triggers stay the data's (AUTO attack buffs Stormeye S2 / Pith S1 keep DEFAULT, as the W5C AUTO audit). No golden scenario fields a stand-in: digests unchanged. Tests: test/content/standin_acnipe / _accast / _acsupo (27).
157 lines
9.6 KiB
JavaScript
157 lines
9.6 KiB
JavaScript
// server/sim/content/kits/ops/standin-acsupo.js — Raidian (char_614_acsupo) 补位 stand-in kit: 6★ 凝滞师 for 灵知 (T4, S2),
|
||
// 夕 / 安洁莉娜 (T5, S3 + DEC-X) and 浊心斯卡蒂 / 仇白 (T6, S3 + DEC-X); every skill at every form.
|
||
// Kit contract and the stand-in rules: ../README.md ("Stand-in kits").
|
||
//
|
||
// Sources: data/backups.json (character_table / skill_table / battle_equip_table at E2 Lv1 rank 4 and E2 Lv60 rank 7),
|
||
// PRTS Raidian(卫戍协议) (S3 备注 on its 虚弱), PRTS 命中率 and the client's battle data (charpack char_614_acsupo).
|
||
// - 凝滞师 trait "攻击造成法术伤害,并对敌人造成短暂的停顿": arts, 停顿 `sluggish` s on every target (professions.js slower,
|
||
// from the trait blackboard); S2 lengthens it (attack@sluggish).
|
||
// - T1 同调 "攻击速度+15,相邻的干员攻击速度+10" (module DEC-X level 2 / 3: 18 / 11, 20 / 12): her own ASPD (charpack
|
||
// acsupo_t_1[self]) and an aura on the operators of the 4 tiles next to her (acsupo_t_1[ally]: profession mask of every
|
||
// operator class — no summon —, removed when the target leaves); several sources keep the strongest [ASSUMED]. S3
|
||
// multiplies both by talent_scale (the S3 mode's `_applyTalentScale`).
|
||
// - T2 诱引 "攻击范围内敌人的物理和法术命中率降低10%" (damage_hitrate_physical / damage_hitrate_magical): PRTS 命中率 — every
|
||
// unit has 100 % 物理 / 法术命中率; an attack of that damage type made at a lower rate rolls once when it starts, and a
|
||
// failed roll misses as a whole: every target and hit of that attack, its splash, and the effects it would carry
|
||
// ("攻击未命中…在输出伤害事件前就算作处理失败", no 晕眩 / 停顿 …). Here: one roll per enemy attack (its `attackId`) while the
|
||
// attacker stands in a Raidian's current attack range (S3's included), made by a battle-wide `hit` handler at
|
||
// LURE_PRIORITY — before the enemy content's own hit riders (content/enemies.js, priority 200) and everything else —
|
||
// which cancels every damage instance of a missed attack and stops the hook there. A target's own 闪避 is a separate roll
|
||
// in the damage pipeline. [ASSUMED] the roll is made at the attack's first damage instance (a ranged enemy's at its
|
||
// projectile's arrival; it stands still through its attack clip) rather than at its wind-up; two Raidians' cuts never
|
||
// add up (the stronger one counts, like the mode's other same-named effects — the charpack's `disableOverride` might
|
||
// mean they coexist); true damage is never affected (PRTS: neither 物理 nor 法术).
|
||
// - S1 双声 (24 s): ATK +atk, attack@max_target targets.
|
||
// - S2 三形 (14 / 17 s): base attack time + base_attack_time s (PRTS "略微缩短(-0.1)" / "缩短(-0.2)": a flat change, batMod),
|
||
// attack@max_target targets, 停顿 attack@sluggish s.
|
||
// - S3 信号跃动 (30 / 35 s): range y-4 (the skill's rangeGrid) — its auto-cast is the data's ACTIVE_RANGE (the owner's rule
|
||
// of 2026-10-05: an enemy inside the larger range casts it; tools/build-data.mjs resolveTrigger, not this kit) —, ATK
|
||
// +acsupo_s_3.atk, T1 × talent_scale, and every enemy in her attack range takes 脆弱 damage_scale − 1 and 虚弱 1 − atk:
|
||
// the catalogue statuses (同名效果取最高), refreshed every AURA_IV s while the skill runs. The 虚弱 stacks as `atk`
|
||
// (0.9 / 0.8: PRTS 备注 "此技能的虚弱在叠加时视为90%(1级~6级)/80%(7级~专精二)…的虚弱(仅影响叠加优先级,不影响实际效果)"
|
||
// — applyStatus `stackAs`), so it outranks every weaker 虚弱 while it lasts.
|
||
// - Module DEC-X "攻击范围内存在敌人时技力自然恢复速度+0.2/秒" (hidden talent sp_recovery_per_sec; acsupo_e_002_t: one
|
||
// +0.2 whatever the number of enemies); HP / ATK in the stats; T1 changes at level 2 / 3 (composed talents).
|
||
|
||
import { canTargetEnemy } from '../../../targeting.js';
|
||
import { bodyInKeys } from '../../../body.js';
|
||
import { num, talentBb, moduleBb, skillRec, statBuff, spTimeBonus, enemyInRange, up, batMod } from '../shared/tier1.js';
|
||
|
||
const S1 = 'skchr_acsupo_1';
|
||
const S2 = 'skchr_acsupo_2';
|
||
const S3 = 'skchr_acsupo_3';
|
||
/** Refresh period / length of the auras (T1 on allies, S3 on enemies): the non-stacking aura convention of the kits. */
|
||
const AURA_IV = 0.25;
|
||
const AURA_DUR = 0.4;
|
||
const T1_KEY = 'acsupo:t1';
|
||
/** 诱引's battle-wide `hit` handler runs before every other one (the enemy content's riders sit at 200). */
|
||
const LURE_PRIORITY = 300;
|
||
const ANY = Object.freeze({ canHitFly: true });
|
||
|
||
const bbOf = (chess, id) => skillRec(chess, id)?.bb ?? {};
|
||
const count = (v, d) => Math.max(1, Math.floor(num(v, d)));
|
||
const adjacent4 = (a, u) => Math.abs(a.tileR - u.tileR) + Math.abs(a.tileC - u.tileC) === 1;
|
||
const s3On = (unit) => !!(unit.skill && unit.skill.active && unit.skill.id === S3);
|
||
|
||
// ---- 诱引 (T2): one battle-wide handler over every Raidian of the battle -------------------------------------------
|
||
/** battle → { srcs: [{ unit, phys, arts }], last: WeakMap(enemy → { id, miss }) }. */
|
||
const LURE = new WeakMap();
|
||
|
||
/** The hit-rate cut (0–1) `e`'s attack of damage type `type` suffers: the strongest of the Raidians whose range holds it. */
|
||
function lureCut(st, e, type) {
|
||
let cut = 0;
|
||
for (const s of st.srcs) {
|
||
const u = s.unit, v = Math.abs(type === 'arts' ? s.arts : s.phys);
|
||
if (v > cut && up(u) && u.rangeKeySet && canTargetEnemy(u, e, ANY) && bodyInKeys(e, u.rangeKeySet)) cut = v;
|
||
}
|
||
return Math.min(1, cut);
|
||
}
|
||
|
||
function lureHit(battle, st, ctx) {
|
||
const e = ctx.source, d = ctx.dmg;
|
||
if (!e || e.side !== 'enemy' || !d || !d.isAttack || d.cancel || (d.type !== 'phys' && d.type !== 'arts')) return;
|
||
// one roll per attack: every damage instance of it (each target, a multi-hit) shares the first instance's result
|
||
const prev = d.attackId ? st.last.get(e) : null;
|
||
let miss;
|
||
if (prev && prev.id === d.attackId) miss = prev.miss;
|
||
else {
|
||
const cut = lureCut(st, e, d.type);
|
||
miss = cut > 0 && battle.rng() < cut;
|
||
if (d.attackId) st.last.set(e, { id: d.attackId, miss });
|
||
}
|
||
if (!miss) return;
|
||
d.cancel = true;
|
||
ctx.stopPropagation = true;
|
||
if (ctx.target) battle.fx('dodge', { x: ctx.target.x, y: ctx.target.y, id: ctx.target.id });
|
||
}
|
||
|
||
function installLure(battle, unit, phys, arts) {
|
||
if (!phys && !arts) return;
|
||
let st = LURE.get(battle);
|
||
if (!st) {
|
||
st = { srcs: [], last: new WeakMap() };
|
||
LURE.set(battle, st);
|
||
// no owner: it serves every Raidian of the battle, whichever is on the field
|
||
battle.on('hit', (ctx) => lureHit(battle, st, ctx), { priority: LURE_PRIORITY });
|
||
}
|
||
st.srcs.push({ unit, phys, arts });
|
||
}
|
||
|
||
// ---- the kit ---------------------------------------------------------------------------------------------------------
|
||
export default {
|
||
char_614_acsupo: (bb, chess) => {
|
||
const t0 = talentBb(chess, 0), t1 = talentBb(chess, 1);
|
||
const b1 = bbOf(chess, S1), b2 = bbOf(chess, S2), b3 = bbOf(chess, S3);
|
||
const grid3 = skillRec(chess, S3)?.rangeGrid;
|
||
const self = num(t0.attack_speed), ally = num(t0['acsupo_t_1[ally].attack_speed']);
|
||
const scale = num(b3.talent_scale, 1);
|
||
const fragile = num(b3.damage_scale, 1) - 1, atkMul = num(b3.atk, 1);
|
||
/** S3: 脆弱 / 虚弱 on every enemy of her (skill) range — at the cast, then every AURA_IV s while it runs. */
|
||
const s3Aura = (battle, unit) => {
|
||
for (const e of battle.enemiesInKeys(unit.rangeKeys, unit, ANY)) {
|
||
if (fragile > 0) battle.applyStatus(e, 'fragile', { duration: AURA_DUR, value: fragile, source: unit });
|
||
if (atkMul > 0 && atkMul < 1 && e.alive) battle.applyStatus(e, 'weaken', { duration: AURA_DUR, value: 1 - atkMul, stackAs: atkMul, source: unit });
|
||
}
|
||
};
|
||
return {
|
||
skills: {
|
||
[S1]: { kind: 'duration', mods: { atkPct: num(b1.atk) }, targeting: { maxTargets: count(b1['attack@max_target'], 2) } },
|
||
[S2]: {
|
||
kind: 'duration',
|
||
mods: { batPct: batMod(b2.base_attack_time, chess) },
|
||
targeting: { maxTargets: count(b2['attack@max_target'], 3) },
|
||
attack: { onHitStatus: { key: 'sluggish', duration: num(b2['attack@sluggish'], 1.1) } },
|
||
},
|
||
[S3]: {
|
||
kind: 'duration',
|
||
// T1 × talent_scale: her own part here, the allies' in the T1 aura
|
||
mods: { atkPct: num(b3['acsupo_s_3.atk']), aspd: self * (scale - 1) },
|
||
...(grid3 && grid3.length ? { targeting: { rangeGrid: grid3.map((p) => [p[0], p[1]]) } } : {}),
|
||
onStart({ battle, unit }) { s3Aura(battle, unit); },
|
||
},
|
||
},
|
||
talents: [
|
||
{ install(battle, unit) { // 同调
|
||
statBuff(battle, unit, 'talent:acsupo', { aspd: self });
|
||
if (!ally) return;
|
||
battle.every(AURA_IV, () => {
|
||
if (!up(unit)) return;
|
||
const v = ally * (s3On(unit) ? scale : 1);
|
||
for (const a of battle.alliesFor(unit)) {
|
||
if (a === unit || a.kind !== 'op' || !adjacent4(a, unit)) continue;
|
||
const cur = a.findBuff(T1_KEY);
|
||
if (cur && cur.source !== unit && (cur.data?.v ?? 0) > v && cur.timeLeft > 0.05) continue;
|
||
battle.addBuff(a, { key: T1_KEY, duration: AURA_DUR, mods: { aspd: v }, source: unit, data: { v }, tags: ['aura'] });
|
||
}
|
||
}, { owner: unit, immediate: true });
|
||
} },
|
||
{ install(battle, unit) { installLure(battle, unit, num(t1.damage_hitrate_physical), num(t1.damage_hitrate_magical)); } }, // 诱引
|
||
],
|
||
install(battle, unit) {
|
||
spTimeBonus(battle, unit, num(moduleBb(chess).sp_recovery_per_sec), () => enemyInRange(battle, unit)); // DEC-X
|
||
if (unit.skill?.id === S3) battle.every(AURA_IV, () => { if (up(unit) && s3On(unit)) s3Aura(battle, unit); }, { owner: unit });
|
||
},
|
||
};
|
||
},
|
||
};
|