Files
ark-stronghold-protocol/server/sim/content/kits/ops/op-phatm2.js
T
Cloudnyco ae22335cfe feedback7: the battle sim uses engine-independent math — Chrome, Firefox, Safari and the server agree bit for bit (PR #373 by @Cloudnyco)
- server/sim/detmath.js: hypot (sqrt of the squares, rescaled outside 2^±500), powi (integer powers in double-double,
  one rounding), sin / cos (fdlibm 5.3) and atan2 (fdlibm / FreeBSD msun) built only from correctly rounded IEEE
  operations; the sim's Math.hypot / sin / cos / atan2 / pow calls go through it (65 files); eslint.config.js forbids
  the approximated Math functions under server/sim and test/sim/detmath.test.js scans for them.
- sin / cos / atan2 are the algorithms V8 runs in Node, so the server's results stay; only hypot differs from V8's.
  Golden: 2 moved — diy-027 (a hypot distance, damage +80) and coop3-ABYSS-5 (a later card pick from round 9, same end).
- Applied unchanged after review (Grok static review: MERGE; the diff read in full: no network, no new dependency).
- Tests: broad node --test 5155 / 5155 with the new file tracked (detmath 7 / 7, package 10 / 10); lint 0 errors,
  typecheck, check:imports.
2026-10-08 18:35:55 +08:00

395 lines
26 KiB
JavaScript
Raw Blame History

This file contains ambiguous Unicode characters
This file contains Unicode characters that might be confused with other characters. If you think that this is intentional, you can safely ignore this warning. Use the Escape button to reveal them.
// server/sim/content/kits/ops/op-phatm2.js — 酒神 (char_1042_phatm2) 自选 operator kit: 6★ 巫役 (辅助), an owned-6★ pick of the
// tier-5 and tier-6 自选 slots; every skill, both talents, the trait, her module (RIT-X 酒神之心) at every form, and the kits of
// her summons 本能的召唤 (token_10054_phatm2_encdool) and 迷狂牢笼 (token_10055_phatm2_mndclv). Kit contract and the 自选
// rules: ../README.md ("How to add an operator (自选)").
//
// Forms (data/backups.json units.char_1042_phatm2): 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 / token_table / range_table (zh_CN, as built into backups.json),
// PRTS 酒神 (形为心役 备注 "溅射半径1.3(中点判定);先造成神经损伤与溅射,随后造成伤害"; 堕梦 备注 "于敌人成功普通攻击前之前触发
// (意味着触发的元素爆发可打断当次普攻,且不会触发成功攻击前的效果)"; 群体性谵妄 备注 on 本能的召唤; 空剧场 备注 "技能开始时,
// 对攻击范围内所有处于元素损伤爆发中的敌人触发一次第一天赋…尝试为其生成一次迷狂牢笼(不刷新已存在的牢笼)…迷狂牢笼只能生成于
// 不存在角色类单位的低地地块…所在地块已存在迷狂牢笼时改为将其刷新1次"), PRTS 本能的召唤 / 迷狂牢笼 (their 备注), and the
// client's battle data read from the local install — charpack char_1042_phatm2 (T1: phatm2_t_1 an active buff of every
// attack; T2: phatm2_t_2 on the enemies of her range, phatm2_t_2[global] on every enemy, phatm2_t_2[s3] in S3's range; S3:
// SpawnToken → phatm2_s_3[token] on the enemies of its range) and buff_template_data phatm2_t_1 / [range], phatm2_t_2 /
// [global] / [s3], phatm2_s_1[unmove], phatm2_s_2 / [damage] / [die], phatm2_s2_token_cache, token_phatm2_encdool_* ,
// phatm2_s_3[trigger] / [sanity] / [token] / [token][trigger] / [die] / [token_cooldown] / [token_clear_cooldown],
// token_phatm2_mndclv[passive].
// - Trait (巫役) "攻击造成法术伤害,可以造成元素损伤": ranged arts bolts (y-2, hits air units), blocks 1, ground enemies target
// her. RIT-X “酒神之心” (trait bb ep_damage_scale) "对精英和领袖敌人造成的元素损伤提升18%": every element fill she deals
// (her summon's too: "来源均为酒神自身") on an ELITE / BOSS / leader enemy ×1.18 — an `elementHit` multiplier (the kits'
// convention: 塑心 强弱法).
// - T1 形为心役 "攻击附带相当于攻击力30%的神经损伤,并对目标周围其他敌人造成一次相当于攻击力20%的神经损伤" (full potential: 33 % / 23 %): every damage
// instance of her normal attacks first gives its target attack@ep_damage_ratio × ATK 神经损伤 and every other selectable
// enemy within range_radius (1.3, 中点判定; S2 talent@range_radius 1.5) of it ep_damage_ratio × ATK — before the damage (a
// late `hit` handler, after any cancel; a hit dodged afterwards still carried it [ASSUMED], as 塑心 S2). S1's two hits
// each carry it [ASSUMED: per damage instance].
// - T2 堕梦 "在场时,全场处于神经损伤爆发期间的敌人攻击速度-12;攻击范围内的敌人普通攻击时受到70点神经损伤" (full potential: −16): while she is on
// the field every enemy in a 神经损伤 burst (its 爆发冷却, `neuralBurst`) has ASPD −|attack_speed| (one "同名" effect,
// the strongest — priorityBBKeys); an enemy of her attack range starting a normal attack first takes `value` 神经损伤 —
// the engine's `enemyAttackStart` hook, before 麻痹 is checked, so a burst it causes interrupts that very attack. RIT-X
// stage 2+: −24 / 90 (the talent change).
// - S1 暗夜回声 (AUTO, attack SP 3, data DEFAULT — a "next attack"): that attack picks an enemy not in a burst first
// (`notBurst`) and hits twice for atk_scale × ATK arts (times), then 束缚 (`bind`) for `unmove` s; while that root holds
// (its final duration after 抵抗) the target takes 神经损伤 ×ep_damage_scale from anyone (phatm2_s_1[unmove]
// EpDamageScale; one effect per target, the strongest) — the root lands after the two hits [ASSUMED].
// - S2 群体性谵妄 (AUTO, 持续时间无限 ⇒ a toggle until she leaves; an AUTO skill acting on herself and her summon: SP_FULL —
// the owner's AUTO rule): ASPD +attack_speed, T1's splash radius talent@range_radius; her placed 本能的召唤 piece takes the
// field on its tile when the skill starts and again each time it is ready ("退场时返还1个可部署的本能的召唤": its own 25 s
// redeploy time) while the skill runs (content/tokens.js releaseSkillSummon; like every placed skill summon it also deploys
// once, free, at the battle start — PRTS 卫戍协议/帮助); her leaving withdraws it ("酒神退场时强制撤退场上的本能的召唤").
// 本能的召唤 (kit below): untargetable, no attack, blocks nothing; every 0.1 s it 诱导s (the catalogue `attract`, to its
// tile, for the rest of its life) the enemies of its x-1 (13 tiles) it can lure — at most max_target in all, ELITE / leader
// first [ASSUMED: then the nearest], only "可达" ones (a ground path to its tile; flyers always); it leaves 10 s after its
// deployment or as soon as one of them is within ability_range_radius (0.4) of it; whenever it leaves (any reason) every
// selectable enemy of its x-1 is 停顿 `sluggish` s and for buff_time s takes, every interval_damage s from 0.1 s on,
// ep_damage_ratio_token × ATK 神经损伤 (fixed: "不受目标损伤抗性影响" — the 损伤抵抗 share added back) and atk_scale × ATK arts
// (持续伤害, not dodgeable) — her ATK when it was deployed ("缓存攻击力"), credited to her (无来源 once she left).
// - S3 空剧场 (MANUAL, ACTIVE_RANGE on y-8, 30 s): range y-8, ATK +atk, an enemy not in a burst first; every enemy she gives
// 神经损伤 meanwhile (a real gauge gain) takes ep_damage_ratio × her ATK 神经损伤 0.1 s later and every `interval` s after
// until it is in a burst (any element) or the skill ends; enemies of her range in a 神经损伤 爆发冷却 recover
// talent@ep_break_recover_speed (+50 %) faster (the cooldown runs 1.5×); an enemy of her range whose 神经损伤 bursts gets a
// 迷狂牢笼 on its ground tile (a low tile with no unit on it; an existing 牢笼 there is refreshed instead). At its start every
// enemy of her range already in an element burst sets T1's splash off around it once and, in a 神经损伤 burst, gets a 牢笼
// (none refreshed); 牢笼 left from an earlier cast stay. When it ends every 牢笼 of hers lasts
// phatm2_s_3[token_cooldown].interval (30) s more; her leaving withdraws them.
// 迷狂牢笼 (kit below): 孤立 + 禁疗, no attack, 迷彩 (only the enemies it blocks target it), every damage it takes from
// one of them is 1, any other damage (无来源 too) none — 3 HP: three hits; at its spawn / refresh it blocks every unblocked
// enemy on its tile, ground or air, whatever its capacity — none holding 不可阻挡 (恐惧, 诱导, 浮空, 沉睡: DESIGN §24.9) —,
// back to full HP, block count = their weight; it takes no other enemy by contact (flag `noNewBlock`); with nobody blocked
// it leaves at once.
// Statuses: 束缚 (S1), 停顿 + 诱导 (本能的召唤); 神经损伤 bursts (engine: 3 麻痹 + 6000 元素伤害).
import { num, up, traitBb, talentBb, skillRec, once } from '../shared/tier1.js';
import { isElite, onElementHit, elementDmg } from '../shared/tier6.js';
import { releaseSkillSummon, startCountdown } from '../../tokens.js';
import { absoluteRangeKeys } from '../../../targeting.js';
import { bodyInKeys } from '../../../body.js';
import { hasHp, burstLocked } from '../../../damage.js';
import { MIN_DAMAGE_RATIO } from '../../../constants.js';
import { hypot } from '../../../detmath.js';
const S1 = 'skchr_phatm2_1';
const S2 = 'skchr_phatm2_2';
const S3 = 'skchr_phatm2_3';
export const CALL = 'token_10054_phatm2_encdool';
export const CAGE = 'token_10055_phatm2_mndclv';
/** phatm2_s_1[unmove]: the root's 神经损伤 amplifier (one per target, the strongest). */
export const UNMOVE = 'phatm2:unmove';
/** phatm2_t_2[attack_speed]: the ASPD cut of enemies in a 神经损伤 burst (one per enemy, the strongest). */
export const DREAM = 'phatm2:dream';
/** The client's trigger period of her field / range effects. */
const IV = 0.1;
const HOLD = IV + 0.05;
/** 本能的召唤 / 迷狂牢笼 numbers the token records may lack (PRTS 本能的召唤: 10 s, 0.4; the S3 cooldown 30 s). */
const CALL_LIFE = 10, CALL_ARRIVE = 0.4, CAGE_AFTER = 30;
/** 本能的召唤's area when its record carries no grid: x-1 (range_table: the 13-tile diamond). */
const X1 = Object.freeze([[2, 0], [1, -1], [1, 0], [1, 1], [0, -2], [0, -1], [0, 0], [0, 1], [0, 2], [-1, -1], [-1, 0], [-1, 1], [-2, 0]]);
/** Selection profile of her effects on enemies: ground and air. */
const ANY = Object.freeze({ canHitFly: true });
const bbOf = (chess, id) => skillRec(chess, id)?.bb ?? {};
const skillOn = (u, id) => !!(u && u.skill && u.skill.id === id && u.skill.active);
const keysAt = (grid, u) => new Set(absoluteRangeKeys(grid, u.tileR, u.tileC, u.dir, 0));
/**
* 神经损伤 of a fixed value that ignores the target's 损伤抵抗 (PRTS 群体性谵妄 备注 "不受目标损伤抗性影响": the client's
* ApplyFixedElementDamage): the share the engine takes off (damage.js elementIntake) is added back first.
*/
function fixedNeural(battle, src, e, amount, sourceless = false) {
if (!(amount > 0) || !hasHp(e)) return 0;
const res = Math.min(1, Math.max(0, (Number(e.def?.epResistance) || 0) / 100));
const k = Math.max(MIN_DAMAGE_RATIO, 1 - res);
return battle.dealDamage(src, e, { type: 'element', element: 'neural', amount: amount / k, sourceless, tags: ['skill', 'phatm2:fixed'] });
}
/**
* 本能的召唤's kit (the skill-summon piece of her S2: a fresh one per piece). `owner` = 酒神, `b2` = her S2 blackboard
* (fallback for the token's own skill blackboard).
*/
function callKit(t, owner, b2) {
const sb = t.def?.skill?.bb ?? {};
const pick = (k, d) => num(sb[k], num(b2[k], d));
const life = num((t.def?.talents ?? []).find((x) => x && x.bb && x.bb.duration != null)?.bb?.duration, pick('interval', CALL_LIFE));
const cap = Math.max(1, Math.floor(pick('max_target', 4))), arrive = pick('ability_range_radius', CALL_ARRIVE);
const grid = t.def?.rangeGrid ?? X1;
return {
skill: null,
trait: { noAttack: true },
install(battle, tok) {
let lured = [], endAt = 0, atk = 0;
const mine = (e) => { const a = e.findBuff('attract'); return a && a.source === tok ? a : null; };
battle.on('deploy', (c) => {
if (c.unit !== tok) return;
lured = [];
endAt = battle.time + life;
atk = owner.s.atk; // 缓存攻击力: her ATK at the deployment (PRTS 备注)
startCountdown(battle, tok, life); // a countdown summon (content/tokens.js): 无敌, 禁疗 [ASSUMED], its bar = the life left
const seq = tok.deploySeq;
battle.after(life, () => { if (tok.alive && tok.deploySeq === seq) battle.retreat(tok, { reason: 'expired', permanent: true }); }, { owner: tok });
}, { owner: tok });
// 诱导 the enemies entering its x-1 (every 0.1 s, at most `cap` in all)
battle.every(IV, () => {
if (!up(tok) || lured.length >= cap) return;
const keys = keysAt(grid, tok);
const cands = battle.enemiesInKeys([...keys], tok, ANY).filter((e) => !lured.includes(e) && !e.s.flags.attract
&& (e.motion === 'FLY' || !!battle.grid.waypoints(Math.round(e.y), Math.round(e.x), tok.tileR, tok.tileC)));
cands.sort((a, b) => (isElite(b) - isElite(a)) || (hypot(a.x - tok.x, a.y - tok.y) - hypot(b.x - tok.x, b.y - tok.y)) || a.spawnSeq - b.spawnSeq);
for (const e of cands) {
if (lured.length >= cap) break;
if (battle.applyStatus(e, 'attract', { duration: Math.max(0.05, endAt - battle.time), source: tok, point: [tok.tileR, tok.tileC] })) {
lured.push(e);
battle.fx('lure', { x: e.x, y: e.y, id: e.id, src: tok.id });
}
}
}, { owner: tok });
// the first lured enemy within `arrive` of it: it leaves
battle.on('tick', () => {
if (!up(tok)) return;
if (lured.some((e) => e.alive && mine(e) && hypot(e.x - tok.x, e.y - tok.y) <= arrive + 1e-9)) battle.retreat(tok, { reason: 'expired', permanent: true });
}, { owner: tok });
// leaving (any reason): 停顿 + the damage over time on every selectable enemy of its x-1; the 诱导 ends
battle.on('death', (c) => {
if (c.unit !== tok || battle.finished) return;
for (const e of lured) { const a = e.alive ? mine(e) : null; if (a) battle.removeBuff(e, a); }
lured = [];
const keys = keysAt(grid, tok);
const slow = pick('sluggish', 6), dur = pick('buff_time', 6), iv = Math.max(0.1, pick('interval_damage', 0.5));
const ep = pick('ep_damage_ratio_token', 0), scale = pick('atk_scale', 1), a0 = atk;
battle.fx('aoe', { x: tok.x, y: tok.y, radius: 2, id: tok.id, skill: 'phatm2:call' });
for (const e of battle.enemiesInKeys([...keys], tok, ANY)) {
if (slow > 0) battle.applyStatus(e, 'sluggish', { duration: slow, source: owner });
battle.addBuff(e, {
key: `phatm2:delirium#${owner.id}`, duration: dur, source: owner, tags: ['skill'], data: { next: battle.time + IV },
onTick: ({ battle: b, unit: tgt, buff }) => {
if (b.time + 1e-9 < buff.data.next) return;
buff.data = { next: buff.data.next + iv };
const gone = !owner.alive;
fixedNeural(b, owner, tgt, a0 * ep, gone);
if (tgt.alive) b.dealDamage(owner, tgt, { amount: a0 * scale, type: 'arts', canDodge: false, isSkill: true, sourceless: gone, tags: ['skill', 'dot', 'phatm2:delirium'] });
},
});
}
}, { owner: tok, priority: 10 });
// "退场时返还1个可部署的本能的召唤": while her S2 runs it comes back after its own redeploy time (the dock's, −10, first)
battle.on('death', (c) => {
if (c.unit === tok && !battle.finished && skillOn(owner, S2) && up(owner)) releaseSkillSummon(battle, owner, CALL);
}, { owner: tok, priority: -20 });
},
};
}
/** 迷狂牢笼's kit (spawned by her S3: a fresh one per cage). */
function cageKit() {
return {
skill: null,
trait: { noAttack: true },
install(battle, cage) {
battle.addBuff(cage, { key: 'trait:cage', flags: { isolated: true, noHeal: true, camou: true, noNewBlock: true }, persist: true, allowDead: true });
// only the enemies it blocks hurt it, 1 per hit
battle.on('hit', (c) => { if (c.target === cage && (!c.source || !cage.blocking.includes(c.source))) c.dmg.cancel = true; }, { owner: cage, priority: 1000 });
battle.on('hpDamage', (c) => { if (c.target === cage && c.amount > 1) c.amount = 1; }, { owner: cage });
battle.on('tick', () => {
if (!up(cage)) return;
if (!cage.blocking.length || (cage.mem.cageUntil != null && battle.time + 1e-9 >= cage.mem.cageUntil)) battle.retreat(cage, { reason: 'expired', permanent: true });
}, { owner: cage });
},
};
}
/**
* A 牢笼 blocks every unblocked enemy on its tile (spawn / refresh): full HP again, block count = their weight. Never one
* holding 不可阻挡 — as Battle._checkBlock: the flag (恐惧 / 诱导 carry it), 浮空, 沉睡 (SLEEPING = 无法行动+无敌+不可阻挡, DESIGN
* §24.9: a sleeper on its tile stays free and is not counted).
*/
function cageBlock(battle, cage) {
let added = 0;
for (const e of battle.enemies) {
if (!e.alive || e.hidden || e.blockedBy || Math.round(e.y) !== cage.tileR || Math.round(e.x) !== cage.tileC) continue;
const f = e.s.flags;
if (f.unblockable || f.levitate || f.fear || f.sleep) continue;
e.blockedBy = cage;
cage.blocking.push(e);
e.moving = false;
added++;
if (battle.hasHook('blocked')) battle.emit('blocked', { blocker: cage, enemy: e });
}
if (added) {
let w = 0;
for (const e of cage.blocking) w += e.blockWeight ?? 1;
battle.addBuff(cage, { key: 'cage:block', mods: { blockCnt: w }, persist: true, allowDead: true });
cage.hp = cage.s.maxHp;
}
return added;
}
/** 空剧场: a 牢笼 on enemy `e`'s ground tile — or the one already there refreshed (`refresh`). */
function cageAt(battle, owner, e, refresh) {
const r = Math.round(e.y), c = Math.round(e.x);
if (!battle.grid.inRect(r, c)) return null;
const there = battle.allyUnits.find((t) => t.alive && t.deployed && t.kind === 'token' && t.defId === CAGE && t.tileR === r && t.tileC === c);
if (there) { if (refresh) cageBlock(battle, there); return there; }
if (!battle.grid.isLow(r, c) || battle.unitAt(r, c)) return null;
const cage = battle.spawnToken(owner, CAGE, r, c, { kit: cageKit() });
if (!cage) return null;
if (!skillOn(owner, S3)) cage.mem.cageUntil = battle.time + CAGE_AFTER;
cageBlock(battle, cage);
battle.fx('summon', { x: cage.x, y: cage.y, id: cage.id, token: CAGE });
return cage;
}
const callsOf = (battle, owner) => battle.allyUnits.filter((t) => t.kind === 'token' && t.defId === CALL && t.ownerUnit === owner);
const cagesOf = (battle, owner) => battle.allyUnits.filter((t) => t.kind === 'token' && t.defId === CAGE && t.ownerUnit === owner && t.alive);
export default {
char_1042_phatm2: (bb, chess) => {
const tb = traitBb(chess);
const t1 = talentBb(chess, 0), t2 = talentBb(chess, 1);
const b1 = bbOf(chess, S1), b2 = bbOf(chess, S2), b3 = bbOf(chess, S3);
const s3 = skillRec(chess, S3);
const eliteScale = num(tb.ep_damage_scale, 1);
const mainRatio = num(t1['attack@ep_damage_ratio']), splashRatio = num(t1.ep_damage_ratio);
const splashR = num(t1.range_radius, 1.3), s2R = num(b2['talent@range_radius'], splashR);
const dreamAspd = Math.abs(num(t2.attack_speed)), dreamValue = num(t2.value);
const s3Ratio = num(b3.ep_damage_ratio), s3Iv = Math.max(0.1, num(b3.interval, 1));
const s3Recover = num(b3['talent@ep_break_recover_speed']), s3After = num(b3['phatm2_s_3[token_cooldown].interval'], CAGE_AFTER);
const s3DotKey = (u) => `phatm2:s3dot#${u.id}`;
/** T1's splash around `t`: every other selectable enemy within the radius (中点判定). */
const splash = (battle, unit, t, r) => {
if (!(splashRatio > 0)) return;
for (const e of battle.foesInRadius(t.x, t.y, r, true)) if (e !== t) elementDmg(battle, unit, e, 'neural', unit.s.atk * splashRatio, ['talent']);
};
return {
skills: {
[S1]: {
kind: 'instant',
targeting: { priority: 'notBurst' },
attack: {
atkScale: num(b1.atk_scale, 1), hits: Math.max(1, Math.floor(num(b1.times, 2))),
onHit({ battle, unit, target }) {
if (!target || !target.alive || !battle.applyStatus(target, 'bind', { duration: num(b1.unmove), source: unit })) return;
const root = target.buffs.find((b) => b.status === 'bind');
const dur = root && Number.isFinite(root.timeLeft) ? root.timeLeft : num(b1.unmove);
battle.applyStrongest(target, UNMOVE, { duration: dur, value: num(b1.ep_damage_scale, 1), mods: () => ({}), source: unit });
},
},
},
[S2]: {
kind: 'toggle', trigger: 'SP_FULL', mods: { aspd: num(b2.attack_speed) },
onStart({ battle, unit }) { releaseSkillSummon(battle, unit, CALL); },
onEnd({ battle, unit }) { withdrawCalls(battle, unit); },
},
[S3]: {
kind: 'duration',
mods: { atkPct: num(b3.atk) },
targeting: { rangeGrid: s3?.rangeGrid ?? null, priority: 'notBurst' },
onStart({ battle, unit }) {
for (const c of cagesOf(battle, unit)) c.mem.cageUntil = null; // phatm2_s_3[token_clear_cooldown]
for (const e of [...battle.enemies]) {
if (!e.alive || e.hidden || !e.s.flags.burstLock || !bodyInKeys(e, unit.rangeKeySet)) continue;
splash(battle, unit, e, splashR);
if (e.alive && e.findBuff('neuralBurst')) cageAt(battle, unit, e, false);
}
},
onTick({ battle, unit, dt }) {
if (!(s3Recover > 0)) return;
for (const e of battle.enemies) {
if (!e.alive || e.hidden || !bodyInKeys(e, unit.rangeKeySet)) continue;
const lock = e.findBuff('neuralBurst');
if (lock && Number.isFinite(lock.timeLeft)) lock.timeLeft -= dt * s3Recover;
}
},
onEnd({ battle, unit }) {
const key = s3DotKey(unit);
for (const e of battle.enemies) if (e.findBuff(key)) battle.removeBuff(e, key);
for (const c of cagesOf(battle, unit)) c.mem.cageUntil = battle.time + s3After;
},
},
},
talents: [
{ install(battle, unit) { // 形为心役: each hit of her attacks — its target, then the enemies around it
battle.on('hit', (c) => {
const t = c.target, d = c.dmg;
if (c.source !== unit || !t || t.side !== 'enemy' || !d || d.cancel || !d.isAttack || d.isSplash || !hasHp(t)) return;
if (mainRatio > 0) elementDmg(battle, unit, t, 'neural', unit.s.atk * mainRatio, ['talent']);
splash(battle, unit, t, skillOn(unit, S2) ? s2R : splashR);
}, { owner: unit, priority: -1000 });
} },
{ install(battle, unit) { // 堕梦: ASPD cut in a 神经损伤 burst (the field); 神经损伤 on a normal attack (her range)
if (dreamAspd > 0) {
const slow = (e) => battle.applyStrongest(e, DREAM, { duration: HOLD, value: dreamAspd, mods: (v) => ({ aspd: -v }), source: unit });
battle.every(IV, () => {
if (!up(unit)) return;
for (const e of battle.enemies) if (e.alive && !e.hidden && e.findBuff('neuralBurst')) slow(e);
}, { owner: unit, immediate: true });
battle.on('elementBurst', (c) => { if (up(unit) && c.element === 'neural' && c.target && c.target.side === 'enemy' && c.target.alive) slow(c.target); }, { owner: unit });
}
if (dreamValue > 0) {
battle.on('enemyAttackStart', (c) => {
const e = c.enemy;
if (!up(unit) || !e || !hasHp(e) || !bodyInKeys(e, unit.rangeKeySet)) return;
elementDmg(battle, unit, e, 'neural', dreamValue, ['talent']);
}, { owner: unit });
}
} },
{ install(battle, unit) { // 本能的召唤: her placed pieces run its kit; her leaving withdraws it and every 迷狂牢笼
for (const t of callsOf(battle, unit)) {
if (t.alive || t.deployed) continue;
// (no offOwner for a piece set up before her: its hooks are content/tokens.js's dock hooks — the generic token
// kit registers none — and they must stay)
battle._setupUnit(t, callKit(t, unit, b2));
}
battle.on('death', (c) => {
if (c.unit !== unit) return;
withdrawCalls(battle, unit);
for (const t of cagesOf(battle, unit)) battle.retreat(t, { reason: 'expired', permanent: true });
}, { owner: unit });
} },
],
install(battle, unit) {
// RIT-X: her element damage on elite / leader enemies ×ep_damage_scale
if (eliteScale > 1) onElementHit(battle, unit, (c) => (c.source === unit && c.target.side === 'enemy' && isElite(c.target) ? eliteScale : 1));
// S1's root: 神经损伤 taken ×its value while it holds (one battle-wide handler, whoever rooted)
once(battle, 'phatm2:unmove', () => {
battle.on('elementHit', (c) => {
const d = c.dmg, t = c.target;
if (!d || d.type !== 'element' || d.element !== 'neural' || !t) return;
const v = t.findBuff(UNMOVE)?.data?.value;
if (v > 1) d.mul *= v;
});
});
if (unit.skill?.id !== S3) return;
// 空剧场: her 神经损伤 starts the per-second 神经损伤 (until a burst); a 神经损伤 burst in her range: a 牢笼
battle.on('damaged', (c) => {
const e = c.target;
if (!skillOn(unit, S3) || c.source !== unit || c.type !== 'element' || c.dmg?.element !== 'neural' || !(c.amount > 0)) return;
if (!e || e.side !== 'enemy' || !e.alive || e.findBuff(s3DotKey(unit)) || burstLocked(e)) return;
battle.addBuff(e, {
key: s3DotKey(unit), source: unit, tags: ['skill'], data: { next: battle.time + IV },
onTick: ({ battle: b, unit: tgt, buff }) => {
if (burstLocked(tgt)) { b.removeBuff(tgt, buff); return; }
if (b.time + 1e-9 < buff.data.next) return;
buff.data = { next: buff.data.next + s3Iv };
elementDmg(b, unit, tgt, 'neural', unit.s.atk * s3Ratio, ['skill', 'dot', 'phatm2:theatre']);
if (tgt.alive && burstLocked(tgt)) b.removeBuff(tgt, buff);
},
});
}, { owner: unit });
battle.on('elementBurst', (c) => {
const e = c.target;
if (c.element !== 'neural' || !skillOn(unit, S3) || !up(unit) || !e || e.side !== 'enemy' || !e.alive || !bodyInKeys(e, unit.rangeKeySet)) return;
cageAt(battle, unit, e, true);
}, { owner: unit });
},
};
},
};
/** Her S2's end or her leaving: the 本能的召唤 in stock is gone and the one on the field leaves. */
function withdrawCalls(battle, unit) {
const stock = unit.mem.summonStock;
if (stock) stock[CALL] = 0;
for (const t of callsOf(battle, unit)) if (t.alive) battle.retreat(t, { reason: 'expired', permanent: true });
}