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).
94 lines
6.2 KiB
JavaScript
94 lines
6.2 KiB
JavaScript
// server/sim/content/kits/ops/standin-acnipe.js — Stormeye (char_611_acnipe) 补位 stand-in kit: 6★ 速射手 for 空弦 (T3, S3),
|
||
// 莫斯提马 (T4, S2), 百炼嘉维尔 / 妮芙 (T5, S3 + MAR-X) and 蕾缪安 / 异客 (T6, S3 + MAR-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)
|
||
// and the client's battle data (charpack char_611_acnipe, battle/buff_template_data.json — the templates quoted below).
|
||
// - 速射手 trait 优先攻击空中单位 (data `targetPriority` 'fly', `canHitFly`); module MAR-X "攻击空中单位时攻击力提升至110%"
|
||
// (trait atk_scale 1.1, template acnipe_e_002_trait: CheckMotionMode FLY → AtkScaleUp on every damage calculation) is
|
||
// the fastshot profile's own (professions.js fastshot `flyScale` from the trait blackboard): no kit code. The module's
|
||
// HP / ATK are in the stats; at level 3 it raises 风坠 to 190 % (the composed talents).
|
||
// - T1 风坠 "攻击时有25%的概率攻击力提升至180%" — acnipe_t_1: ON_CALCULATE_DAMAGE → Dice(prob) → AtkScaleUp(atk_scale): a roll
|
||
// on EVERY damage instance of an attack (each target, each hit of S3's double hit), ATK × atk_scale before DEF / RES.
|
||
// S3 raises `prob` to its talent@prob.
|
||
// - T2 风雨欲来 "自身未进行攻击时,技力回复速度+0.2/秒" (sp_recovery_per_sec, delay 2): +0.2 SP/s once she has made no attack
|
||
// for `delay` s — [ASSUMED] the reading of `delay` (no text or PRTS note gives it), counted from her deployment too.
|
||
// - S1 破空: ATK +atk, ASPD +attack_speed, 无视 def_penetrate_fixed DEF (defIgnoreFlat) for its 20 s.
|
||
// - S2 心手合一 (自动触发, 持续时间无限): ATK +atk and attack@max_target targets until she is knocked out (a toggle). Trigger:
|
||
// the data's DEFAULT — an AUTO attack buff waits for her next attack like 能天使 S3 / 乌尔比安 S2 (W5C's AUTO audit:
|
||
// "attack modes / attack buffs" keep waiting); being endless it changes nothing but the moment of the cast.
|
||
// - S3 旋臂: attack@max_target targets, attack@times hits each (二连击), 风坠 at talent@prob; and "攻击目标生命值高于90%时额外
|
||
// 造成1次伤害": the S3 attack carries acnipe_s_3[extra] (`_onlyFeedActiveBuffToFirstOne`: the first hit of the double hit
|
||
// only) — FilterByTargetHpRatio GE attack@hp_ratio, then AdvancedApplyDamage PHYSICAL attack@atk_scale_extra × ATK with
|
||
// `_emitSourceOnCalculateDamage` false: one extra physical instance per target per attack, which neither 风坠 nor the
|
||
// MAR-X trait touches (no isAttack: no "攻击时" effect sees it). [ASSUMED] the HP ratio is read when that first hit lands,
|
||
// before its damage (as 异客's 机理分析 kit reads it; the order of the buff and the hit is not in the data).
|
||
|
||
import { num, talentBb, skillRec, spTimeBonus } from '../shared/tier1.js';
|
||
|
||
const S1 = 'skchr_acnipe_1';
|
||
const S2 = 'skchr_acnipe_2';
|
||
const S3 = 'skchr_acnipe_3';
|
||
/** Tag of S3's extra damage instance (not an attack: 风坠 skips it). */
|
||
const EXTRA = 'acnipe:extra';
|
||
|
||
const bbOf = (chess, id) => skillRec(chess, id)?.bb ?? {};
|
||
const count = (v, d) => Math.max(1, Math.floor(num(v, d)));
|
||
const s3On = (unit) => !!(unit.skill && unit.skill.active && unit.skill.id === S3);
|
||
|
||
export default {
|
||
char_611_acnipe: (bb, chess) => {
|
||
const t0 = talentBb(chess, 0), t1 = talentBb(chess, 1);
|
||
const b1 = bbOf(chess, S1), b2 = bbOf(chess, S2), b3 = bbOf(chess, S3);
|
||
return {
|
||
skills: {
|
||
[S1]: { kind: 'duration', mods: { atkPct: num(b1.atk), aspd: num(b1.attack_speed), defIgnoreFlat: num(b1.def_penetrate_fixed) } },
|
||
[S2]: { kind: 'toggle', mods: { atkPct: num(b2.atk) }, targeting: { maxTargets: count(b2['attack@max_target'], 2) } },
|
||
[S3]: {
|
||
kind: 'duration',
|
||
targeting: { maxTargets: count(b3['attack@max_target'], 3) },
|
||
attack: {
|
||
hits: count(b3['attack@times'], 2),
|
||
// the extra instance, after the attack's hits on that target (marked by the install's first-hit check)
|
||
onEachHit({ battle, unit, target, kind }) {
|
||
const due = unit.mem.acnipeExtra;
|
||
if (kind !== 'main' || !due || !due.has(target)) return;
|
||
due.delete(target);
|
||
if (target.alive) battle.dealDamage(unit, target, { amount: unit.s.atk * num(b3['attack@atk_scale_extra'], 1), type: 'phys', isSkill: true, tags: ['skill', EXTRA] });
|
||
},
|
||
},
|
||
},
|
||
},
|
||
talents: [
|
||
{ install(battle, unit) { // 风坠: every damage instance of an attack rolls
|
||
const scale = num(t0.atk_scale, 1), base = num(t0.prob);
|
||
if (!(scale > 0) || scale === 1) return;
|
||
battle.on('hit', (ctx) => {
|
||
const d = ctx.dmg;
|
||
if (ctx.source !== unit || !ctx.target || ctx.target.side !== 'enemy' || !d.isAttack || d.type === 'element' || (d.tags || []).includes(EXTRA)) return;
|
||
const p = s3On(unit) ? num(b3['talent@prob'], base) : base;
|
||
if (p > 0 && battle.rng.chance(p)) d.amount *= scale;
|
||
}, { owner: unit });
|
||
} },
|
||
{ install(battle, unit) { // 风雨欲来: no attack for `delay` s (her deployment counts as the start) ⇒ +SP/s
|
||
const delay = num(t1.delay, 0);
|
||
spTimeBonus(battle, unit, num(t1.sp_recovery_per_sec), () => battle.time - Math.max(unit.lastAttackAt, unit.deployedAt) >= delay - 1e-9);
|
||
} },
|
||
],
|
||
install(battle, unit) {
|
||
if (unit.skill?.id !== S3) return;
|
||
// S3 [extra]: the first hit of each attack on a target checks its HP ratio before that hit's damage
|
||
const ratio = num(b3['attack@hp_ratio'], 0.9);
|
||
battle.on('hit', (ctx) => {
|
||
const t = ctx.target, d = ctx.dmg;
|
||
if (ctx.source !== unit || !t || t.side !== 'enemy' || !d.isAttack || d.isSplash || !s3On(unit)) return;
|
||
const seen = unit.mem.acnipeSeen ??= new WeakMap();
|
||
if (d.attackId && seen.get(t) === d.attackId) return;
|
||
seen.set(t, d.attackId);
|
||
if (t.hpRatio >= ratio - 1e-9) (unit.mem.acnipeExtra ??= new Set()).add(t);
|
||
}, { owner: unit });
|
||
},
|
||
};
|
||
},
|
||
};
|