169 lines
11 KiB
JavaScript
169 lines
11 KiB
JavaScript
// server/sim/content/kits/ops/op-gdglow.js — 澄闪 (char_377_gdglow) 自选 operator kit: 6★ 驭械术师 (术师), an owned-6★ pick of the
|
||
// tier-5 and tier-6 自选 slots; every skill, both talents, the trait and both modules (FUN-X “辛劳之翼”, FUN-Y 梦想终将实现) at every
|
||
// form. Kit contract and the 自选 rules: ../README.md ("How to add an operator (自选)").
|
||
//
|
||
// Forms (data/backups.json units.char_377_gdglow): normal = E2 Lv1, skills at rank 4, no module; elite = E2 Lv60, rank 7, the
|
||
// picked module at stage 1 (tier 5) or 3 (tier 6) — the owner's decision of 2026-10-05. Full potential (the owner's decision of 2026-10-07).
|
||
// Sources: character_table / skill_table / battle_equip_table (zh_CN, as built into backups.json), PRTS 澄闪 (信标的愤怒 备注:
|
||
// "每层1.5%概率,初始1层;未成功自爆时对应浮游单元层数增加1层,叠加至40层后下次攻击必定自爆。对应浮游单元成功自爆后/澄闪部署时层数
|
||
// 重置。每个浮游单元自爆概率独立计算", "自爆伤害半径1.1;自爆不计入特性叠加;自爆伤害不受特性影响"; 精准导流 备注 "增加自身法术穿透
|
||
// 属性(直接加算)"; S3 备注 "停顿效果可应用于自爆"), PRTS 分支特性信息 驭械术师 ("浮游单元攻击不同目标/干员再部署时,上述的伤害立刻
|
||
// 恢复至初始值", "通过技能释放的浮游单元造成技能直接伤害,不受缴械类效果制约"), Arknights Terra Wiki Goldenglow, and the client's
|
||
// battle data read from the local install — charpack char_377_gdglow (the skill modes S1 / S2 / S3: an attack whose
|
||
// `_funnelActions` S1Funnel1–2 / S2Funnel1–2 / S3Funnel1–3 each throw a persistent funnel projectile at the attack's target
|
||
// (`_alwaysIncludeTarget`) and fire again only once it is gone (`_waitForProjectileInvalid`); S3's attack has no projectile
|
||
// of its own — "停止攻击" — and its funnels carry `sluggish`; OverrideTrait swaps the trait's Funnel trigger off in every
|
||
// skill mode; the talent config copies attack@prob / attack@atk_scale_2 into the skills), the funnel projectiles
|
||
// projectile_chr_gdglow_funnel_s1_1 … s3_3 (`_lifeTimeType` 2: until stopped, `_stopWhenSourceInvalid`, its crash
|
||
// AdvancedApplyDamage MAGICAL at atk_scale_2) and their s12 / s3 blast projectiles.
|
||
// - Trait (驭械术师) "操作浮游单元造成法术伤害;单元攻击同一敌人伤害提升(最高造成干员110%攻击力的伤害)" (trait bb init / delta /
|
||
// max_atk_scale): the engine's funnel profile (professions.js: her normal attack = one drone hit at init → +delta per hit on
|
||
// the same target → max). FUN-X: init 0.35 ("单元初始伤害提高"); FUN-Y: max 1.2. Ranged arts, 3-1, hits air (PRTS "可对空"),
|
||
// blocks 1, ground enemies target her.
|
||
// - Skill drones (the three skills "浮游单元+N,释放浮游单元锁定敌人攻击 … 锁敌后直至敌人被击杀、自爆或技能结束后返回干员身边"): while a
|
||
// skill runs she makes no attack of her own — every drone is out, 1 + attack@cnt of them — and each drone locks the target
|
||
// her targeting ranks first in her current range (the one her attack would take: the client's `_alwaysIncludeTarget`; so the
|
||
// drones mostly share one enemy), keeps it — in range or not — until it falls or turns unselectable, then takes her current
|
||
// first target again at once [ASSUMED: the client re-sends it with her next attack]. A drone attacks its enemy every attack
|
||
// interval of hers (her live ASPD), the first as it locks [ASSUMED: no flight time], for ATK × its own ramp (the trait's
|
||
// init, +delta per hit, ≤ max; every new lock starts at init [ASSUMED: a new funnel projectile]) — arts 技能直接伤害
|
||
// (isSkill, no normal attack: on-attack items skip it), and no 缴械 stops it (PRTS); [ASSUMED] nor do her stun / freeze /
|
||
// silence: the skill ticks on and so do the drones.
|
||
// - T1 信标的愤怒 "技能开启后浮游单元攻击时有10%概率自爆(回到干员身边)对小范围敌人造成澄闪300%攻击力的法术伤害" (full potential:
|
||
// 315 %; bb attack@prob / attack@atk_scale_2 / attack@max_stack_cnt; FUN-X stage 3: 375 %): each skill drone (by its slot,
|
||
// S3's third one too) holds a stack count from 1: on each of its attacks it self-destructs with prob × stacks (sure past
|
||
// attack@max_stack_cnt), else +1 stack; the blast — after that attack's hit [ASSUMED: in addition to it] — deals
|
||
// atk_scale_2 × her ATK arts (isSkill, no ramp) to every enemy within 1.1 of its target (中点判定, air units too), the drone
|
||
// goes back to her (its lock is free) and its count restarts at 1; every deployment of hers resets the counts.
|
||
// - T2 精准导流 "自身与浮游单元无视敌人15点法术抗性" (full potential: 18; bb magic_resist_penetrate_fixed; FUN-Y stage 3: 23):
|
||
// resIgnoreFlat on her (her drones' damage is hers).
|
||
// - S1 火花四溅 (MANUAL, data DEFAULT, 25 s): ATK +atk, ASPD +attack_speed, 2 drones.
|
||
// - S2 电流翻涌 (AUTO, 持续时间无限 — a toggle): range 3-18 (the skill's grid), ATK +atk, 2 drones. A self buff (no target to
|
||
// wait for): the owner's AUTO rule fires it as soon as its SP is full (`trigger: 'SP_FULL'`, kits/README.md checklist 5; as
|
||
// 黄's 链锯延伸模块 and 谬因's 连续映射).
|
||
// - S3 澄净闪耀 (MANUAL, data GDGLOW_SKILL_2 — "全场存在可选目标时释放技能", the rule named after it; 30 s): "停止攻击", range the
|
||
// whole field, ATK +atk, 3 drones; every drone hit — and every blast (PRTS 备注) — 停顿 attack@sluggish s.
|
||
|
||
import { num, talentBb, skillRec, statBuff } from '../shared/tier1.js';
|
||
import { WHOLE_FIELD } from '../shared/tier6.js';
|
||
import { canTargetEnemy, sortEnemyTargets } from '../../../targeting.js';
|
||
|
||
const S1 = 'skchr_gdglow_1';
|
||
const S2 = 'skchr_gdglow_2';
|
||
const S3 = 'skchr_gdglow_3';
|
||
/** 信标的愤怒's blast radius (PRTS 备注 "自爆伤害半径1.1"; no blackboard key). */
|
||
export const BLAST_RADIUS = 1.1;
|
||
export const DRONE_TAG = 'gdglow:drone';
|
||
export const BLAST_TAG = 'gdglow:blast';
|
||
|
||
const bbOf = (chess, id) => skillRec(chess, id)?.bb ?? {};
|
||
|
||
/** Her first-ranked target of her current range (and the enemies she blocks), or null. */
|
||
function firstTarget(battle, unit) {
|
||
const prof = 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);
|
||
if (!cands.length) return null;
|
||
sortEnemyTargets(battle, unit, cands, prof?.priority ?? null);
|
||
return cands[0];
|
||
}
|
||
|
||
export default {
|
||
char_377_gdglow: (bb, chess) => {
|
||
const t0 = talentBb(chess, 0), t1 = talentBb(chess, 1);
|
||
const prob = num(t0['attack@prob']), blastScale = num(t0['attack@atk_scale_2']);
|
||
const maxStack = Math.max(1, Math.floor(num(t0['attack@max_stack_cnt'], 40)));
|
||
const b1 = bbOf(chess, S1), b2 = bbOf(chess, S2), b3 = bbOf(chess, S3);
|
||
const s2 = skillRec(chess, S2);
|
||
const drones = (b) => 1 + Math.max(0, Math.floor(num(b['attack@cnt'])));
|
||
|
||
/** One drone of `unit` hits its enemy (and may self-destruct). */
|
||
const droneHit = (battle, unit, d, t, sluggish) => {
|
||
const f = unit.profile?.funnel || { init: 0.2, delta: 0.15, max: 1.1 };
|
||
d.ramp = d.rampId === t.id ? Math.min(f.max, d.ramp + f.delta) : f.init;
|
||
d.rampId = t.id;
|
||
battle.fx('drone', { x: t.x, y: t.y, id: unit.id });
|
||
battle.dealDamage(unit, t, { amount: unit.s.atk * unit.s.atkScaleMul * d.ramp, type: 'arts', isSkill: true, tags: ['skill', 'drone', DRONE_TAG] });
|
||
if (sluggish > 0 && t.alive) battle.applyStatus(t, 'sluggish', { duration: sluggish, source: unit });
|
||
if (!(prob > 0) || !(blastScale > 0) || !unit.alive) return;
|
||
// 信标的愤怒: prob × stacks, sure past the cap; a miss adds a stack
|
||
const st = unit.mem.gdStacks || (unit.mem.gdStacks = []);
|
||
const k = st[d.i] ?? 1;
|
||
if (!(k > maxStack || battle.rng.chance(Math.min(1, prob * k)))) { st[d.i] = Math.min(maxStack + 1, k + 1); return; }
|
||
st[d.i] = 1;
|
||
const x = t.x, y = t.y;
|
||
d.lock = null; // back to her
|
||
battle.fx('explode', { x, y, radius: BLAST_RADIUS, id: unit.id, dmgType: 'arts', skill: 'gdglow:blast' });
|
||
for (const e of battle.foesInRadius(x, y, BLAST_RADIUS, true)) {
|
||
if (!e.alive) continue;
|
||
battle.dealDamage(unit, e, { amount: unit.s.atk * blastScale, type: 'arts', isSkill: true, tags: ['skill', BLAST_TAG] });
|
||
if (sluggish > 0 && e.alive) battle.applyStatus(e, 'sluggish', { duration: sluggish, source: unit });
|
||
}
|
||
};
|
||
|
||
/** A skill's drones: `n` of them; S3's carry 停顿 `sluggish` s. */
|
||
const droneSpec = (n, sluggish) => ({
|
||
onStart({ battle, unit }) {
|
||
unit.mem.gdDrones = Array.from({ length: n }, (_, i) => ({ i, lock: null, cd: 0, rampId: null, ramp: 0 }));
|
||
battle.fx('drones', { x: unit.x, y: unit.y, id: unit.id, n });
|
||
},
|
||
onTick({ battle, unit, dt }) {
|
||
const D = unit.mem.gdDrones;
|
||
if (!D) return;
|
||
const ok = (e) => !!e && e.alive && !e.hidden && canTargetEnemy(unit, e, unit.profile);
|
||
for (const d of D) {
|
||
if (unit.mem.gdDrones !== D || !unit.alive) return;
|
||
d.cd = Math.max(0, d.cd - dt);
|
||
if (d.lock && !ok(d.lock)) d.lock = null;
|
||
if (!d.lock) {
|
||
const t = firstTarget(battle, unit);
|
||
if (!t) continue;
|
||
d.lock = t;
|
||
d.rampId = null;
|
||
battle.fx('droneLock', { x: t.x, y: t.y, id: t.id, src: unit.id });
|
||
}
|
||
if (d.cd > 1e-9) continue;
|
||
d.cd = unit.s.interval;
|
||
droneHit(battle, unit, d, d.lock, sluggish);
|
||
}
|
||
},
|
||
onEnd({ unit }) { unit.mem.gdDrones = null; },
|
||
});
|
||
|
||
return {
|
||
skills: {
|
||
[S1]: {
|
||
kind: 'duration',
|
||
mods: { atkPct: num(b1.atk), aspd: num(b1.attack_speed) },
|
||
attack: { noAttack: true },
|
||
...droneSpec(drones(b1), 0),
|
||
},
|
||
[S2]: {
|
||
kind: 'toggle',
|
||
trigger: 'SP_FULL',
|
||
mods: { atkPct: num(b2.atk) },
|
||
...(s2?.rangeGrid ? { targeting: { rangeGrid: s2.rangeGrid.map((p) => [p[0], p[1]]) } } : {}),
|
||
attack: { noAttack: true },
|
||
...droneSpec(drones(b2), 0),
|
||
},
|
||
[S3]: {
|
||
kind: 'duration',
|
||
mods: { atkPct: num(b3.atk) },
|
||
targeting: { rangeGrid: WHOLE_FIELD },
|
||
attack: { noAttack: true },
|
||
...droneSpec(drones(b3), num(b3['attack@sluggish'])),
|
||
},
|
||
},
|
||
talents: [
|
||
{ install(battle, unit) { // 信标的愤怒: the skill drones' self-destruct (droneHit); counts reset at every deployment
|
||
battle.on('deploy', (ctx) => { if (ctx.unit === unit) unit.mem.gdStacks = []; }, { owner: unit });
|
||
} },
|
||
{ install(battle, unit) { // 精准导流: 无视敌人 N 点法术抗性 (her drones' damage is hers)
|
||
statBuff(battle, unit, 'talent:gdglow:pen', { resIgnoreFlat: num(t1.magic_resist_penetrate_fixed) });
|
||
} },
|
||
],
|
||
};
|
||
},
|
||
};
|
||
|