- 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.
324 lines
21 KiB
JavaScript
324 lines
21 KiB
JavaScript
// server/sim/content/kits/ops/op-ray.js — 莱伊 (char_4117_ray) 自选 operator kit: 6★ 猎手 (狙击), an owned-6★ pick of the
|
||
// tier-5 and tier-6 自选 slots; every skill, both talents, the trait, both modules (HUN-X 《跳舞的月光》, HUN-Y 证明什么?) at
|
||
// every form, and the kit of her summon 沙地兽 (token_10034_ray_sndbst). Kit contract and the 自选 rules: ../README.md
|
||
// ("How to add an operator (自选)").
|
||
//
|
||
// Forms (data/backups.json units.char_4117_ray): 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 / the token's character_table row (zh_CN, as
|
||
// built into backups.json) and PRTS 莱伊 (巡哨伙伴 / 入神 / S1 / S2 / S3 备注), PRTS 沙地兽 (备注 "持有禁疗、无敌", "退场时返还1
|
||
// 个可部署的沙地兽", "莱伊退场时强制撤退场上的沙地兽"; its skill "仅在持有者携带技能2时才会携带"), PRTS 分支特性信息 猎手, BWIKI
|
||
// 莱伊 ("装填间隔即为攻击间隔,初始子弹数即为最大子弹数"), PRTS 卫戍协议/帮助 (a placed summon that leaves is deployed again on
|
||
// its tile "在满足条件后"); the client's battle data read from the local install — buff_template_data ray_tr / ray_tr_add /
|
||
// ray_tr_sub (the magazine), ray_s_1[konckback] (Knockback _useSourceDirection, _decreaseForceLevelWhenNotInDirection 1),
|
||
// ray_s_1[kill_add_bullet], ray_t_1[damage_scale] (PHYSICAL DamageScale on targets holding ray_sndbst_aura), ray_s_2[deck],
|
||
// ray_s_3[reload] / [check_full] / [sp], ray_sndbst_tr / ray_sndbst_collect, and charpack char_4117_ray.
|
||
// - Trait (猎手) "攻击时需要消耗子弹且攻击力提升至120%,不攻击时会缓慢地装填子弹(最多8发)" (trait bb value / atk_scale; the
|
||
// hunter profile's ×atk_scale on every bullet attack, skill attacks included). PRTS 分支特性信息 猎手: "入场时持有自身子弹上限
|
||
// 数的子弹" — full at every deployment; "存在子弹且攻击范围内存在目标时进行攻击,不存在子弹/子弹未满且攻击范围内无目标时进行
|
||
// 装弹,通常情况下每次装填1颗" and "装弹行为属于普通攻击行为,持续时间使用装填间隔,装填间隔默认为自身基础攻击间隔,不受攻速/
|
||
// 攻击间隔变化影响": a reload is an action of her attack loop lasting her BASE attack time (1.6 s; ASPD never shortens it),
|
||
// loading one bullet when it ends — taken when the magazine is empty (targets or not) or not full with nobody to shoot.
|
||
// This kit's own reload (kit trait install / canAttack / afterAttack); the engine's hunter profile (1 bullet per second
|
||
// after 1 s without attacking) stays for 雪猎. Can hit air units (PRTS "可对空"), blocks 1, ground enemies target her.
|
||
// HUN-X adds "子弹数量为空时下次装填额外加装1发子弹" (trait bb extra_add): a reload begun on an empty magazine loads 1 + that.
|
||
// HUN-Y raises the trait to atk_scale 1.33 (and ASPD +5 / +7 in the stats).
|
||
// - T1 巡哨伙伴 "可以在攻击范围内部署沙地兽于25秒内侦察一片区域延伸攻击范围,自身优先攻击该区域内的目标且对其造成的物理伤害提
|
||
// 高15%" (bb damage_scale; HUN-X stage 3: 20 % and "沙地兽的再部署时间-10秒" — the token's module stats, respawn 20 s).
|
||
// PRTS 备注: "攻击范围内" of her attacks, talents and skills means "自己或自己的召唤物的攻击范围内" — while a 沙地兽 scouts,
|
||
// the enemies of its x-4 (3×3) are in her range (Battle.setExtraRange) and in her skills' trigger range
|
||
// (skill.addTriggerRange); she picks a target of that area before any other (after the ones she blocks); her physical
|
||
// damage on an enemy there ×(1 + damage_scale) ("伤害倍率提升": DamageInfo mul).
|
||
// The 沙地兽 is a hand piece the player places in her range (data ownerRange): it deploys with the board, scouts for the
|
||
// token talent's `duration` (25 s) and then leaves [ASSUMED: the scouting ends with its stay — PRTS "退场时返还1个可部署
|
||
// 的沙地兽"]; it is untargetable (its trait text), 无敌 and 禁疗 (PRTS 备注), makes no attack (实体类型 装置) [ASSUMED], and
|
||
// is withdrawn when 莱伊 leaves the field (PRTS 备注). A 沙地兽 that left comes back on its tile once its redeploy time
|
||
// (×(1 + S2 respawn_time) while 广域警觉 runs) has passed, paying its cost (3 DP), while 莱伊 is on the field.
|
||
// - T2 入神 "攻击相同目标时每次攻击提高自身攻击力8%,最多3层" (full potential: 9 %; bb atk / max_stack_cnt; HUN-Y stage 3: 10 %, 4 layers). PRTS 备注:
|
||
// "每次攻击前:若目标与上次攻击目标不同,失去之前获得的增益;随后获得一层增益,持续时间无限" — before each attack (the special
|
||
// bullet of S1 too; never a reload), lost on a new target, gone when she leaves the field.
|
||
// - S1 脱身矢 (MANUAL, 2 charges, data DEFAULT): "立即用额外特殊子弹攻击目标,造成相当于攻击力N%的物理伤害并将其中等力度地推开,
|
||
// 若将其击倒则使下次装填额外加装1发子弹". PRTS 备注: "'额外特殊子弹'实际为无需消耗子弹的一次攻击,可受特性加成" — an attack
|
||
// at once (Battle.forceAttack; the cast's normal attack follows as usual) at atk_scale × ATK × the trait scale that takes no
|
||
// bullet; a 中力 (bb force) push along her facing, radial at 力度 −1 when the target is off it (PRTS "推动方向默认为自身朝
|
||
// 向,因角度过大变为径向推力时,力度仅-1": the engine's directional rule with a −1 instead of −2); a kill adds bb cnt bullets
|
||
// to the next reload ("额外装填效果可叠加并于下1次装填时一同生效"). With an empty magazine (no attack to wait for) the cast
|
||
// comes as soon as an enemy is in range [ASSUMED, as 雪猎's special bullets]. The 地穴 clause has nothing to act on here.
|
||
// - S2 广域警觉 (AUTO, attack SP; 持续时间无限 — a toggle): range 4-10, ATK +atk, the 沙地兽's redeploy time ×(1 +
|
||
// respawn_time). Self / summon effects only: it fires at full SP (`trigger: 'SP_FULL'`, the owner's AUTO rule).
|
||
// Passive "沙地兽撤退时回收命中该区域的子弹": while she carries S2, the bullets of her attacks that hit an enemy inside a
|
||
// scouting 沙地兽's area are counted (its own counter: ≤ its trait value 8 — ray_sndbst_tr) and come back to her magazine
|
||
// when it leaves, while she is on the field (ray_sndbst_collect → ray_tr_add: HUN-X's extra_add first on an empty
|
||
// magazine, capped at her maximum); S1's special bullet is no bullet. PRTS "装弹行为不触发攻击回复": no reload gives
|
||
// attack SP.
|
||
// - S3 “得见光芒” (MANUAL, 16 s, data ACTIVE_RANGE on its 3-8): "立即停止攻击直至子弹装满,装填间隔大幅缩短(-1.2)": at the cast
|
||
// she stops attacking and reloads (a reload under way is cut to the new interval) until her magazine is full, every reload
|
||
// while it runs lasting base attack time + reload_interval (0.4 s); range 3-8; every attack attack@atk_scale × ATK
|
||
// (× the trait scale) and 束缚 (bind) attack@unmove_duration s; a kill of hers during it ⇒ +sp SP when it ends, through
|
||
// 阻回 (PRTS 备注 "获得技力效果无视阻回").
|
||
|
||
import { num, talentBb, traitBb, skillRec, up } from '../shared/tier1.js';
|
||
import { acquireTargets, effectiveProfile } from '../../../ai.js';
|
||
import { sortEnemyTargets } from '../../../targeting.js';
|
||
import { bodyInKeys } from '../../../body.js';
|
||
import { PUSH_DIRECTIONAL_MIN_DIST } from '../../../constants.js';
|
||
import { startCountdown } from '../../tokens.js';
|
||
import { hypot } from '../../../detmath.js';
|
||
|
||
const S1 = 'skchr_ray_1';
|
||
const S2 = 'skchr_ray_2';
|
||
const S3 = 'skchr_ray_3';
|
||
export const SANDBEAST = 'token_10034_ray_sndbst';
|
||
/** 巡哨伙伴 (E2): the 沙地兽's scouting when its def carries no duration (PRTS: "持续25秒"). */
|
||
const LIFE_FALLBACK = 25;
|
||
/** Seconds between two tries of a 沙地兽 that left to come back (tile busy, DP short, 莱伊 away). */
|
||
const RETRY = 0.25;
|
||
/** Tag of S1's special bullet (no bullet of the magazine). */
|
||
export const SPECIAL_TAG = 'ray:special';
|
||
const TRANCE_KEY = 'talent:ray:trance';
|
||
|
||
const bbOf = (chess, id) => skillRec(chess, id)?.bb ?? {};
|
||
/** The magazine of the trait ("最多8发"). */
|
||
const ammoMax = (unit) => Math.max(1, Math.floor(num(unit.profile?.ammoMax, 8)));
|
||
/** First talent of a normalised token def holding `key` (token talents keep no index). */
|
||
const tokTalent = (def, key) => (def?.talents ?? []).find((t) => t && t.bb && t.bb[key] != null) ?? null;
|
||
|
||
/** The 沙地兽's kit: no attack, 无敌, 禁疗 (its untargetability comes from its trait text, simdata normalizeToken). */
|
||
function sandbeastKit() {
|
||
return {
|
||
skill: null,
|
||
trait: { noAttack: true },
|
||
talents: [],
|
||
install(battle, t) {
|
||
battle.addBuff(t, { key: 'ray:sandbeast', flags: { invulnerable: true, noHeal: true }, persist: true, allowDead: true });
|
||
},
|
||
};
|
||
}
|
||
|
||
export default {
|
||
char_4117_ray: (bb, chess) => {
|
||
const b1 = bbOf(chess, S1), b2 = bbOf(chess, S2), b3 = bbOf(chess, S3);
|
||
const s2 = skillRec(chess, S2), s3 = skillRec(chess, S3);
|
||
const t0 = talentBb(chess, 0), t1 = talentBb(chess, 1);
|
||
const extraAdd = Math.max(0, Math.floor(num(traitBb(chess).extra_add)));
|
||
const picked = chess?.skill?.skillId ?? null;
|
||
const tokenId = (chess?.talents ?? []).find((t) => t && t.tokenKey)?.tokenKey ?? SANDBEAST;
|
||
|
||
/** One reload: her base attack time, S3's reload_interval while it runs (PRTS: ASPD never changes it). */
|
||
const reloadInterval = (unit) => {
|
||
const s3on = !!(unit.skill && unit.skill.active && unit.skill.id === S3);
|
||
return Math.max(0.05, num(unit.base?.bat, 1.6) + (s3on ? num(b3.reload_interval) : 0));
|
||
};
|
||
const startReload = (unit) => {
|
||
const m = unit.mem;
|
||
m.rayReloading = true;
|
||
if ((unit.trait.ammo ?? 0) <= 0 && extraAdd > 0) m.rayLoadExtra = (m.rayLoadExtra ?? 0) + extraAdd; // HUN-X
|
||
unit.atkCd = reloadInterval(unit);
|
||
};
|
||
const finishReload = (unit) => {
|
||
const m = unit.mem;
|
||
m.rayReloading = false;
|
||
const add = 1 + Math.max(0, Math.floor(num(m.rayLoadExtra)));
|
||
m.rayLoadExtra = 0;
|
||
unit.trait.ammo = Math.min(ammoMax(unit), (unit.trait.ammo ?? 0) + add);
|
||
};
|
||
const resetTrait = (unit) => {
|
||
unit.trait.ammo = ammoMax(unit);
|
||
Object.assign(unit.mem, { rayReloading: false, rayLoadExtra: 0, rayFill: false });
|
||
};
|
||
|
||
return {
|
||
trait: {
|
||
// 猎手 (PRTS 分支特性信息): her own reload, an action of the attack loop (canAttack runs once the cooldown is over)
|
||
install(battle, unit) {
|
||
resetTrait(unit);
|
||
battle.on('deploy', (ctx) => { if (ctx.unit === unit) resetTrait(unit); }, { owner: unit });
|
||
},
|
||
canAttack(battle, unit) {
|
||
const m = unit.mem;
|
||
if (m.rayReloading) finishReload(unit);
|
||
const max = ammoMax(unit), ammo = unit.trait.ammo ?? 0;
|
||
if (m.rayFill) { // S3: no attack until the magazine is full
|
||
if (ammo < max) { startReload(unit); return false; }
|
||
m.rayFill = false;
|
||
}
|
||
if (ammo <= 0 || (ammo < max && !acquireTargets(battle, unit, effectiveProfile(unit)).length)) { startReload(unit); return false; }
|
||
return true;
|
||
},
|
||
afterAttack(battle, unit) { unit.trait.ammo = Math.max(0, (unit.trait.ammo ?? 0) - 1); },
|
||
},
|
||
skills: {
|
||
[S1]: {
|
||
kind: num(skillRec(chess, S1)?.maxChargeTime, 1) > 1 ? 'charges' : 'instant',
|
||
// the special bullet: an attack that spends no bullet (its own afterAttack) and takes the trait scale
|
||
attack: {
|
||
atkScale: num(b1.atk_scale, 1),
|
||
tags: ['skill', SPECIAL_TAG],
|
||
afterAttack() {},
|
||
onHit({ battle, unit, target }) {
|
||
if (!target) return;
|
||
if (!target.alive) { unit.mem.rayLoadExtra = num(unit.mem.rayLoadExtra) + Math.max(0, Math.floor(num(b1.cnt, 1))); return; }
|
||
const force = num(b1.force, 1);
|
||
const [fr, fc] = unit.fwd;
|
||
const vx = target.x - unit.x, vy = target.y - unit.y, d = hypot(vx, vy);
|
||
if (d >= PUSH_DIRECTIONAL_MIN_DIST && vx * fc + vy * fr >= d * Math.SQRT1_2) battle.push(target, force, { from: unit, dir: { x: fc, y: fr }, fixed: true });
|
||
else battle.push(target, force - 1, { from: unit });
|
||
},
|
||
},
|
||
onStart({ battle, skill, unit }) {
|
||
if (battle.forceAttack(unit)) return;
|
||
// nobody to shoot after all: no cast
|
||
skill.pending = false;
|
||
skill.addCharge(1);
|
||
},
|
||
},
|
||
[S2]: {
|
||
kind: 'toggle',
|
||
trigger: 'SP_FULL',
|
||
mods: { atkPct: num(b2.atk) },
|
||
targeting: s2?.rangeGrid ? { rangeGrid: s2.rangeGrid.map((p) => [p[0], p[1]]) } : undefined,
|
||
},
|
||
[S3]: {
|
||
kind: 'duration',
|
||
targeting: s3?.rangeGrid ? { rangeGrid: s3.rangeGrid.map((p) => [p[0], p[1]]) } : undefined,
|
||
attack: {
|
||
atkScale: num(b3['attack@atk_scale'], 1),
|
||
onHit({ battle, unit, target }) {
|
||
if (target && target.alive) battle.applyStatus(target, 'bind', { duration: num(b3['attack@unmove_duration'], 2), source: unit });
|
||
},
|
||
},
|
||
onStart({ unit }) {
|
||
const m = unit.mem;
|
||
m.rayFill = true;
|
||
m.rayKilled = false;
|
||
// 立即停止攻击: the reload starts now (one under way is cut to the new interval)
|
||
unit.atkCd = m.rayReloading ? Math.min(unit.atkCd, reloadInterval(unit)) : 0;
|
||
},
|
||
onEnd({ unit, reason }) {
|
||
const m = unit.mem;
|
||
m.rayFill = false;
|
||
if (m.rayKilled && reason !== 'death' && up(unit) && unit.skill) unit.skill.gainSp(num(b3.sp), 'init'); // 无视阻回
|
||
m.rayKilled = false;
|
||
},
|
||
},
|
||
},
|
||
talents: [
|
||
{ install(battle, unit) { // 巡哨伙伴 + the 沙地兽 (and S2's bullet recovery)
|
||
const m = unit.mem;
|
||
const mine = (t) => !!t && t.kind === 'token' && t.defId === tokenId && t.ownerUnit === unit;
|
||
// her placed 沙地兽 pieces run the 沙地兽's kit (set up here, before the battle starts — as 鸿雪's 打字机)
|
||
for (const t of battle.allyUnits) {
|
||
if (!mine(t) || t.alive || t.deployed) continue;
|
||
if (t.kit) battle.offOwner(t);
|
||
battle._setupUnit(t, sandbeastKit());
|
||
}
|
||
const ds = num(t0.damage_scale);
|
||
m.rayArea = null;
|
||
m.rayBullets = 0;
|
||
if (unit.skill) unit.skill.addTriggerRange(() => (m.rayAreaKeys ? [m.rayAreaKeys] : []));
|
||
const endArea = () => { m.rayArea = null; m.rayAreaKeys = null; m.rayBeast = null; battle.setExtraRange(unit, null); };
|
||
battle.on('deploy', (ctx) => {
|
||
const t = ctx.unit;
|
||
if (!mine(t)) return;
|
||
const life = num(tokTalent(t.def, 'duration')?.bb?.duration, LIFE_FALLBACK);
|
||
m.rayBeast = t;
|
||
m.rayAreaKeys = [...t.rangeKeys];
|
||
m.rayArea = new Set(m.rayAreaKeys);
|
||
m.rayBullets = 0;
|
||
m.rayCounted = 0;
|
||
battle.setExtraRange(unit, m.rayAreaKeys);
|
||
startCountdown(battle, t, life); // a countdown summon (content/tokens.js): its bar = the life left
|
||
const seq = t.deploySeq;
|
||
battle.after(life, () => { if (t.alive && t.deploySeq === seq) battle.retreat(t, { reason: 'expired', permanent: true }); }, { owner: unit });
|
||
}, { owner: unit });
|
||
battle.on('death', (ctx) => {
|
||
const t = ctx.unit;
|
||
if (t === unit) { // "莱伊退场时强制撤退场上的沙地兽"
|
||
for (const x of battle.allyUnits) if (mine(x) && x.alive) battle.retreat(x, { reason: 'retreat', permanent: true });
|
||
return;
|
||
}
|
||
if (!mine(t) || battle.finished) return;
|
||
t.removed = false; // the piece stays (Battle.redeploy needs it; its hooks are kept)
|
||
if (m.rayBeast === t) endArea();
|
||
// S2 passive: the bullets that hit in its area come back (ray_sndbst_collect → ray_tr_add: its count — ≤ its own
|
||
// trait value — onto her magazine, HUN-X's extra_add first when she is empty, capped at her maximum)
|
||
if (picked === S2 && m.rayBullets > 0 && up(unit)) {
|
||
const back = Math.min(m.rayBullets, Math.max(1, Math.floor(num(t.def?.traitBb?.value, ammoMax(unit)))));
|
||
const empty = (unit.trait.ammo ?? 0) <= 0 ? extraAdd : 0;
|
||
unit.trait.ammo = Math.min(ammoMax(unit), (unit.trait.ammo ?? 0) + empty + back);
|
||
}
|
||
m.rayBullets = 0;
|
||
const left = battle.time;
|
||
battle.every(RETRY, (b, sched) => {
|
||
if (t.alive || t.removed || b.finished) { sched.cancel(); return; }
|
||
const mul = unit.skill && unit.skill.active && unit.skill.id === S2 ? Math.max(0, 1 + num(b2.respawn_time)) : 1;
|
||
if (b.time + 1e-9 < left + Math.max(0, num(t.base.respawnTime)) * mul || !up(unit)) return;
|
||
if (b.redeploy(t, { free: false })) sched.cancel();
|
||
}, { owner: unit });
|
||
}, { owner: unit, priority: -10 });
|
||
// 自身优先攻击该区域内的目标 (after the enemies she blocks)
|
||
battle.on('beforeAttack', (ctx) => {
|
||
if (ctx.attacker !== unit || !m.rayArea || !ctx.targets.length) return;
|
||
const area = m.rayArea, prof = ctx.profile ?? unit.profile;
|
||
const all = battle.enemiesInKeys(unit.rangeKeys, unit, prof);
|
||
for (const e of battle.blockedTargets(unit, prof)) if (!all.includes(e)) all.push(e);
|
||
if (!all.some((e) => bodyInKeys(e, area))) return;
|
||
sortEnemyTargets(battle, unit, all, prof.priority ?? null);
|
||
const rank = (e) => (e.blockedBy === unit ? 0 : bodyInKeys(e, area) ? 1 : 2);
|
||
const order = all.map((e, i) => ({ e, i, r: rank(e) })).sort((a, b) => a.r - b.r || a.i - b.i).map((x) => x.e);
|
||
ctx.targets = order.slice(0, ctx.targets.length);
|
||
}, { owner: unit, priority: 10 });
|
||
// ×(1 + damage_scale) on her physical damage to an enemy of the area
|
||
if (ds > 0) {
|
||
battle.on('hit', (ctx) => {
|
||
if (ctx.source !== unit || !m.rayArea || !ctx.target || ctx.target.side !== 'enemy' || ctx.dmg.type !== 'phys') return;
|
||
if (bodyInKeys(ctx.target, m.rayArea)) ctx.dmg.mul *= 1 + ds;
|
||
}, { owner: unit });
|
||
}
|
||
// S2 passive: count the bullets of her attacks that hit an enemy of the area (one per attack)
|
||
if (picked === S2) {
|
||
battle.on('damaged', (ctx) => {
|
||
const d = ctx.dmg, e = ctx.target;
|
||
if (ctx.source !== unit || !m.rayArea || !d || !d.isAttack || !e || e.side !== 'enemy' || (d.tags && d.tags.includes(SPECIAL_TAG))) return;
|
||
if ((d.attackId && d.attackId === m.rayCounted) || !bodyInKeys(e, m.rayArea)) return;
|
||
m.rayCounted = d.attackId;
|
||
m.rayBullets++;
|
||
}, { owner: unit });
|
||
}
|
||
} },
|
||
{ install(battle, unit) { // 入神: +atk per attack on the same target (≤ max_stack_cnt), lost on a new target
|
||
const atk = num(t1.atk), max = Math.max(1, Math.floor(num(t1.max_stack_cnt, 3)));
|
||
if (!(atk > 0)) return;
|
||
const m = unit.mem;
|
||
m.rayTrance = null;
|
||
m.rayStacks = 0;
|
||
battle.on('deploy', (ctx) => { if (ctx.unit === unit) { m.rayTrance = null; m.rayStacks = 0; } }, { owner: unit });
|
||
battle.on('beforeAttack', (ctx) => {
|
||
const t = ctx.attacker === unit ? ctx.targets[0] : null;
|
||
if (!t) return;
|
||
if (m.rayTrance !== t) { m.rayTrance = t; m.rayStacks = 0; }
|
||
m.rayStacks = Math.min(max, m.rayStacks + 1);
|
||
battle.addBuff(unit, { key: TRANCE_KEY, mods: { atkPct: atk * m.rayStacks }, tags: ['talent'] });
|
||
}, { owner: unit, priority: -10 });
|
||
} },
|
||
],
|
||
install(battle, unit) {
|
||
// S3: 技能期间若击倒敌人 — a kill of hers while it runs
|
||
if (picked === S3) {
|
||
battle.on('kill', (ctx) => {
|
||
if (ctx.killer === unit && unit.skill && unit.skill.active && unit.skill.id === S3) unit.mem.rayKilled = true;
|
||
}, { owner: unit });
|
||
}
|
||
// S1 with an empty magazine (the attack loop only reloads then): cast once an enemy is in range [ASSUMED]
|
||
if (picked !== S1) return;
|
||
battle.on('tick', () => {
|
||
const sk = unit.skill;
|
||
if (!up(unit) || !unit.canAct || !sk || !sk.ready || sk.pending || sk.opCooling || unit.s.flags.silence || unit.s.flags.disarm) return;
|
||
if ((unit.trait.ammo ?? 1) > 0) return;
|
||
if (acquireTargets(battle, unit, effectiveProfile(unit)).length) sk.activate('DEFAULT');
|
||
}, { owner: unit });
|
||
},
|
||
};
|
||
},
|
||
};
|