feedback5 K2: stand-in kits for Stormeye, Pith and Raidian — every skill, talent and module at every form
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).
This commit is contained in:
1 parent
eac9b9737d
commit
f317adf003
7 files changed
+1094
-1
No files matched your search
@@ -64,7 +64,7 @@ export const KIT_FILES = Object.freeze([
|
||||
* A stand-in without a file fights with the generic kit (content/generic.js) plus its unconditional stat talents
|
||||
* (genericTalents) — the eight 预备干员 need no file.
|
||||
*/
|
||||
export const STANDIN_KIT_FILES = Object.freeze([]);
|
||||
export const STANDIN_KIT_FILES = Object.freeze(['standin-acnipe.js', 'standin-accast.js', 'standin-acsupo.js']);
|
||||
|
||||
async function loadKitFile(file) {
|
||||
try {
|
||||
|
||||
@@ -0,0 +1,97 @@
|
||||
// server/sim/content/kits/ops/standin-accast.js — Pith (char_612_accast) 补位 stand-in kit: 6★ 扩散术师 for 烛煌 (T5) and
|
||||
// 焰影苇草 / 迷迭香 / 荒芜拉普兰德 (T6), all S3 + SPC-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 Pith(卫戍协议) (S3 备注 "※溅射范围扩大至1.2") and the client's battle data (charpack char_612_accast,
|
||||
// battle/buff_template_data.json accast_s_3[AOE]).
|
||||
// - 扩散术师 trait "攻击造成群体法术伤害": the splash around each struck target, radius = the trait's
|
||||
// attack@projectile_range (1.1, PRTS 溅射半径一览's branch value); a skill's attack@projectile_range replaces it while it runs.
|
||||
// - T1 "见我所见" "攻击无视目标12法术抗性": magic_resist_penetrate_fixed — a flat 法术穿透 attribute (charpack accast_t_1:
|
||||
// attribute 27 from the blackboard; PRTS Pith 备注 "无视法抗效果为增加自身法术穿透属性(直接加算)"): every arts damage she deals.
|
||||
// - T2 "授我所授" "自身及相邻四格的【术师】干员攻击力+10%" (module SPC-X level 2 / 3: +15 / +20 %): an aura on her own tile and
|
||||
// the 4 orthogonal ones, 【术师】 operators only (charpack: profession mask CASTER, removed when the target leaves);
|
||||
// several sources keep the strongest (installAura, [ASSUMED] like every same-named talent aura of the mode).
|
||||
// - S1 "书我所书" (自动触发, 25 s): ASPD +attack_speed, splash radius attack@projectile_range (1.5). Trigger: the data's
|
||||
// DEFAULT — an AUTO attack buff waits for her next attack (W5C's AUTO audit: "attack modes / attack buffs" keep waiting).
|
||||
// - S2 "为我所为" (35 s): her ASPD +attack_speed; "所有【术师】干员攻击力+atk" — an aura on every 【术师】 operator of the field
|
||||
// (her included) while it runs, refreshed every AURA_IV s; the strongest of several sources applies [ASSUMED].
|
||||
// - S3 "驭我所驭" (35 s): at the cast, one atk_scale_aoe × ATK arts hit on every enemy in her attack range (accast_s_3[AOE]:
|
||||
// AOEDamage MAGICAL, its s3_selector; a skill hit, not an attack); then attack@max_target targets (+1), ASPD
|
||||
// +attack_speed, splash attack@projectile_range (1.2, PRTS 备注).
|
||||
// - Module SPC-X "攻击范围扩大": her range becomes the module's own grid (the 3×3 caster range + the tile [0,3]; shared
|
||||
// tier5 moduleRangeUp), so the S3 burst covers it too; HP / ATK in the stats; T2 +20 % at level 3 (composed talents).
|
||||
|
||||
import { num, talentBb, skillRec, statBuff, installAura, up } from '../shared/tier1.js';
|
||||
import { moduleRangeUp } from '../shared/tier5.js';
|
||||
|
||||
const S1 = 'skchr_accast_1';
|
||||
const S2 = 'skchr_accast_2';
|
||||
const S3 = 'skchr_accast_3';
|
||||
/** Refresh period / length of the S2 aura buff (the non-stacking aura convention of the kits). */
|
||||
const AURA_IV = 0.25;
|
||||
const AURA_DUR = 0.4;
|
||||
const S2_KEY = 'accast:s2';
|
||||
|
||||
const bbOf = (chess, id) => skillRec(chess, id)?.bb ?? {};
|
||||
/** A 【术师】 operator (summons are never 【术师】 operators). */
|
||||
const isCaster = (a) => !!a && a.kind === 'op' && a.def?.profession === 'CASTER';
|
||||
/** One of the 4 tiles next to `u` (相邻四格). */
|
||||
const adjacent4 = (a, u) => Math.abs(a.tileR - u.tileR) + Math.abs(a.tileC - u.tileC) === 1;
|
||||
|
||||
/** S2: ATK +v on every 【术师】 operator unless a stronger one from another source is on it. */
|
||||
function casterAtk(battle, unit, v) {
|
||||
if (!(v > 0)) return;
|
||||
for (const a of battle.alliesFor(unit)) {
|
||||
if (!isCaster(a)) continue;
|
||||
const cur = a.findBuff(S2_KEY);
|
||||
if (cur && cur.source !== unit && (cur.data?.v ?? 0) > v && cur.timeLeft > 0.05) continue;
|
||||
battle.addBuff(a, { key: S2_KEY, duration: AURA_DUR, mods: { atkPct: v }, source: unit, data: { v }, tags: ['aura'] });
|
||||
}
|
||||
}
|
||||
|
||||
export default {
|
||||
char_612_accast: (bb, chess) => {
|
||||
const t0 = talentBb(chess, 0), t1 = talentBb(chess, 1);
|
||||
const tb = chess?.trait?.bb ?? {};
|
||||
const b1 = bbOf(chess, S1), b2 = bbOf(chess, S2), b3 = bbOf(chess, S3);
|
||||
return {
|
||||
trait: { splashRadius: num(tb['attack@projectile_range'], 1.1) },
|
||||
skills: {
|
||||
[S1]: { kind: 'duration', mods: { aspd: num(b1.attack_speed) }, attack: { splashRadius: num(b1['attack@projectile_range'], 1.5) } },
|
||||
[S2]: {
|
||||
kind: 'duration',
|
||||
mods: { aspd: num(b2.attack_speed) },
|
||||
onStart({ battle, unit }) { casterAtk(battle, unit, num(b2.atk)); },
|
||||
},
|
||||
[S3]: {
|
||||
kind: 'duration',
|
||||
mods: { aspd: num(b3.attack_speed) },
|
||||
targeting: { maxTargets: Math.max(1, Math.floor(num(b3['attack@max_target'], 2))) },
|
||||
attack: { splashRadius: num(b3['attack@projectile_range'], 1.2) },
|
||||
onStart({ battle, unit }) {
|
||||
const scale = num(b3.atk_scale_aoe, 1);
|
||||
for (const e of battle.enemiesInKeys(unit.rangeKeys, unit, unit.profile)) {
|
||||
battle.dealDamage(unit, e, { amount: unit.s.atk * scale, type: 'arts', isSkill: true, tags: ['skill', 'accast:aoe'] });
|
||||
battle.fx('aoe', { x: e.x, y: e.y, r: 0.5, id: unit.id, skill: 'accast' });
|
||||
}
|
||||
},
|
||||
},
|
||||
},
|
||||
talents: [
|
||||
{ install(battle, unit) { statBuff(battle, unit, 'talent:accast:res', { resIgnoreFlat: num(t0.magic_resist_penetrate_fixed) }); } },
|
||||
{ install(battle, unit) {
|
||||
const v = num(t1.atk);
|
||||
if (v) installAura(battle, unit, { key: 'talent:accast', value: v, select: (a) => isCaster(a) && (a === unit || adjacent4(a, unit)), mods: { atkPct: v } });
|
||||
} },
|
||||
],
|
||||
install(battle, unit) {
|
||||
moduleRangeUp(battle, unit, chess);
|
||||
if (unit.skill?.id !== S2) return;
|
||||
// S2's aura while it runs (its onStart gives the first application at the cast)
|
||||
const v2 = num(b2.atk);
|
||||
battle.every(AURA_IV, () => { if (up(unit) && unit.skill.active) casterAtk(battle, unit, v2); }, { owner: unit });
|
||||
},
|
||||
};
|
||||
},
|
||||
};
|
||||
@@ -0,0 +1,93 @@
|
||||
// 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 });
|
||||
},
|
||||
};
|
||||
},
|
||||
};
|
||||
@@ -0,0 +1,156 @@
|
||||
// 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 });
|
||||
},
|
||||
};
|
||||
},
|
||||
};
|
||||
Reference in new issue
Block a user