- 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.
422 lines
28 KiB
JavaScript
422 lines
28 KiB
JavaScript
// server/sim/content/enemies/special.js — SPECIAL 特异 kits (prisoners, 穿刺手, 暴虐兵长, 镜卫, chimeras, 圣杯, the act-2 specials
|
||
// …) and their part of KITS (split from content/enemies.js).
|
||
|
||
import { ALLY_COLLIDER_RADIUS } from '../../constants.js';
|
||
import { canTargetAlly, areaSelectable } from '../../targeting.js';
|
||
import { periodicDamage } from '../../damage.js';
|
||
import {
|
||
BOOM_RADIUS, nthOf, stOf, T, elem, hurt, targetsNear, allTargets, areaAllies, areaAlliesInTiles, byPriority,
|
||
stayRoute, spawnChildren, expose, auraBuff, watchDeaths, onTerrain,
|
||
} from './helpers.js';
|
||
import {
|
||
unblockable, runWhenHit, blockWeight, taunt, maxTargets, ep, nthAttackPower, lowHpBuff, deathBoom, setFloat, float,
|
||
reborn, frontGuard, faceMove, unbalanced, skill, kitEp, kitPrisoner, kitStun3,
|
||
} from './archetypes.js';
|
||
import { hypot } from '../../detmath.js';
|
||
|
||
// ---------------------------------------------------------------------------------------------------------------
|
||
// constants (numbers that exist nowhere in the data)
|
||
|
||
/** 孽罪奇美拉 污染模式 aura (PRTS 天赋 "自身半径1.2范围内的所有单位…每0.5秒受到50真实持续伤害(同类效果取最高)"): radius,
|
||
* damage period (s). */
|
||
const CHIMERA_AURA_RADIUS = 1.2, CHIMERA_AURA_EVERY = 0.5;
|
||
|
||
/** 自行炮 Cannon (PRTS 高准度伦蒂尼姆城防自行炮 技能 "以攻击范围内生命上限最高的我方单位为中心,生成9格的炮击区域,6s内每0.5s对炮击
|
||
* 区域中生命比例最高的我方单位进行攻击…(最多进行10次攻击)"): shot interval (s), shot cap; 3 shots 1 s apart [ASSUMED] until
|
||
* 0.1.1. 备注: during the skill animation 失衡免疫 + 晕眩 / 冻结 / 浮空 / 沉睡免疫 — held over the bombardment [ASSUMED length]. */
|
||
const CANNON_EVERY = 0.5, CANNON_SHOTS = 10;
|
||
|
||
const CANNON_IMMUNE = Object.freeze(['stun', 'freeze', 'cold', 'levitate', 'sleep']);
|
||
|
||
/** Radius of 骸骨拷打者's "周围" death sensing when its talent has no Attack.range_radius [ASSUMED]. */
|
||
const TORTURER_RADIUS = 1;
|
||
|
||
/** Barrel zone radius of 咸鳞汁推荐者 [ASSUMED]. */
|
||
const BARREL_RADIUS = 1.5;
|
||
|
||
// ---------------------------------------------------------------------------------------------------------------
|
||
// kits
|
||
|
||
/** 源石污染区 (act1 m04 infection tiles). */
|
||
function infectionArts() {
|
||
return { hitOut(c, b, e) { if (c.dmg.isAttack && onTerrain(b, e, 'infection')) c.dmg.type = 'arts'; } };
|
||
}
|
||
|
||
function kitChimera(ab) {
|
||
const dmg = T(ab, 'OrigAura.damage') ?? 0, spr = T(ab, 'OrigAura.sp_recover_ratio') ?? 0;
|
||
const activate = (b, e, a) => {
|
||
if (a.on || !onTerrain(b, e, 'infection')) return;
|
||
a.on = true;
|
||
e.mem.ab.atkType = 'arts';
|
||
b.fx('telegraph', { x: e.x, y: e.y, r: CHIMERA_AURA_RADIUS, kind: 'chimera', id: e.id });
|
||
};
|
||
return [{
|
||
hitOut(c, b, e, a) { if (c.dmg.isAttack && !a.on) { activate(b, e, a); if (a.on) c.dmg.type = 'arts'; } },
|
||
iv: 0.25,
|
||
tick(b, e, a) {
|
||
activate(b, e, a);
|
||
if (!a.on) return;
|
||
a.acc = (a.acc ?? 0) + 0.25;
|
||
const pulse = a.acc >= CHIMERA_AURA_EVERY - 1e-9;
|
||
if (pulse) a.acc -= CHIMERA_AURA_EVERY;
|
||
for (const u of b.alliesInRadius(e.x, e.y, CHIMERA_AURA_RADIUS)) {
|
||
auraBuff(b, u, 'ab:originium', 0.25, { spRecoveryMul: Math.max(0, 1 + spr) }, null, true);
|
||
// PRTS "持续视为受到源石污染区影响…每0.5秒受到50真实持续伤害": a damage instance (受击回复, the 重装 trigger), not a
|
||
// 流失 — 无来源 like the 源石污染区 terrain it stands for [ASSUMED], the chimera keeps the credit; "同类效果取最高": one
|
||
// tick per unit per period however many chimeras reach it (`mem.chimeraAt`; every chimera's is 50). A terrain
|
||
// effect, not an area selection: 隐匿 allies inside are reached too ("所有单位" [ASSUMED], DESIGN §22.12)
|
||
if (!pulse || !(dmg > 0) || b.time - (u.mem.chimeraAt ?? -Infinity) < CHIMERA_AURA_EVERY - 1e-6) continue;
|
||
u.mem.chimeraAt = b.time;
|
||
b.dealDamage(e, u, { ...periodicDamage(dmg), tags: ['dot', 'pollution'] });
|
||
}
|
||
},
|
||
}];
|
||
}
|
||
|
||
function kitNazg(ab, e) {
|
||
const r = (e.def.raw && e.def.raw.stats && e.def.raw.stats.rawRangeRadius) || e.base.rangeRadius || 1.3;
|
||
return [{
|
||
before(c, b, e2) {
|
||
const l = targetsNear(b, e2, r);
|
||
if (onTerrain(b, e2, 'infection')) for (const u of b.allies()) if (!l.includes(u) && onTerrain(b, u, 'infection') && canTargetAlly(e2, u, true)) l.push(u);
|
||
if (l.length) c.targets = byPriority(e2, l);
|
||
},
|
||
}];
|
||
}
|
||
|
||
/** 吉兆飞鳞 【失温坠落】: the stun when a freeze ends (PRTS "冻结结束时,获得0.05秒晕眩"). */
|
||
const PARROT_CHILL_STUN = 0.05;
|
||
|
||
/**
|
||
* 吉兆飞鳞 (PRTS): 近地悬浮 (失衡免疫), no normal attack; 受晕眩/无法行动/沉睡/缚地影响后进入晕眩模式 — the float is off, it
|
||
* cannot be blocked and is stunned `Stun.duration` (8) s — and floats again once none of those effects holds it
|
||
* (初始模式); 冻结 grounds it only when the freeze ends (【失温坠落】: 0.05 s stun). 首次受到伤害后 its speed is ×
|
||
* `M0SpeedUp.move_speed` for `M0SpeedUp.duration` s ("最终提升至300%"; never in 晕眩模式, dropped when its mode
|
||
* changes). 搬运模式 (carrying a 寻险水手) needs a level checkpoint (`ThrowEnemy.checkpoint` 0 = never) — not in this mode.
|
||
*/
|
||
function kitParrot(ab) {
|
||
const down = T(ab, 'Stun.duration') ?? 0;
|
||
return [float(), {
|
||
taken(c, b, e, a) {
|
||
if (a.ran || a.down) return;
|
||
a.ran = true;
|
||
b.addBuff(e, { key: 'ab:parrotRun', duration: T(ab, 'M0SpeedUp.duration') ?? 0, mods: { moveMul: T(ab, 'M0SpeedUp.move_speed') ?? 1 } });
|
||
},
|
||
status(c, b, e, a) {
|
||
if (c.status === 'freeze') { a.chill = true; return; }
|
||
if (a.down || !(c.status === 'stun' || c.status === 'sleep' || c.status === 'groundbind')) return;
|
||
a.down = true;
|
||
setFloat(b, e, false);
|
||
b.removeBuff(e, 'ab:parrotRun');
|
||
b.addBuff(e, { key: 'ab:parrotDown', flags: { unblockable: true }, persist: true });
|
||
if (down > 0) b.applyStatus(e, 'stun', { duration: down, source: null });
|
||
b.fx('phase', { x: e.x, y: e.y, id: e.id, kind: 'grounded' });
|
||
},
|
||
tick(b, e, a) {
|
||
if (a.chill && !e.s.flags.freeze) { a.chill = false; b.applyStatus(e, 'stun', { duration: PARROT_CHILL_STUN, source: null }); }
|
||
// "离开上述异常效果影响后进入初始模式": s.flags.stun also holds while asleep / frozen (units.js), so a sleep that
|
||
// outlasts the mode stun keeps it down; so does a 缚地 (flag `groundbind`)
|
||
if (a.down && !e.s.flags.stun && !e.s.flags.groundbind) {
|
||
a.down = false;
|
||
b.removeBuff(e, 'ab:parrotDown');
|
||
b.removeBuff(e, 'ab:parrotRun');
|
||
setFloat(b, e, true);
|
||
b.fx('phase', { x: e.x, y: e.y, id: e.id, kind: 'float' });
|
||
}
|
||
},
|
||
}];
|
||
}
|
||
|
||
/**
|
||
* 萨卡兹穿刺手 (PRTS 天赋; the battle prefab's Rush / FirstAttack abilities — templates dlancer_t_listener[a] / [b] / [c],
|
||
* dlancer_t[trigger], dlancer_t_atk): every RUSH_LISTEN (0.1) s — 晕眩 (STUNNED) or 束缚 (UNMOVABLE) ends the acceleration
|
||
* and its speed layers with it; not blocked and not accelerating starts it. While it accelerates, every
|
||
* `rush.dlancer_t[trigger].interval` (0.5) s — the first one 0.5 s after the start — one more layer of move speed
|
||
* +`move_speed` (50 %, MULTIPLIER, the layers add up), at most `trig_cnt` (25) tries — blocked or not, frozen, asleep or
|
||
* slowed or not (only the two flags stop it). Every normal attack carries dlancer_t_atk: while it accelerates, the hit
|
||
* adds 当前移动速度 × `firstattack.atk_scale` (600) physical damage — a second instance, melee, no 受击回复 (ignoreForSp) —
|
||
* and ends the acceleration, so once blocked only its first hit has it. Until 0.2.0 it kept accelerating through a stun
|
||
* and kept the speed after it, and the first hit after a block multiplied the attack instead (×(1 + 6 × layers / 25)),
|
||
* a stun in between included (community report of 2026-10-06, item 36).
|
||
*/
|
||
const RUSH_LISTEN = 0.1;
|
||
|
||
function kitRush(ab) {
|
||
const ms = T(ab, 'rush.dlancer_t[trigger].move_speed') ?? 0, iv = T(ab, 'rush.dlancer_t[trigger].interval') ?? 0.5;
|
||
const max = T(ab, 'rush.dlancer_t[trigger].trig_cnt') ?? 0, scale = T(ab, 'firstattack.atk_scale') ?? 0;
|
||
// 晕眩 / 束缚 only (the client's STUNNED / UNMOVABLE flags: not 冻结, 沉睡 or 浮空, which hold flags.stun here too)
|
||
const held = (e) => e.buffs.some((x) => x.status === 'stun' || x.status === 'bind');
|
||
const stop = (b, e, a) => { a.on = false; a.n = 0; b.removeBuff(e, 'ab:rush'); };
|
||
return [{
|
||
iv: RUSH_LISTEN,
|
||
// the listeners first look at once (waitFirstTriggerInterval 0): a walker spawned unblocked starts at t = 0
|
||
spawn(b, e, a) { a.on = !e.blockedBy && !held(e); a.n = 0; a.tries = 0; a.acc = 0; },
|
||
tick(b, e, a, dt) {
|
||
if (held(e)) { if (a.on) stop(b, e, a); return; }
|
||
if (!a.on) {
|
||
if (!e.blockedBy) { a.on = true; a.tries = 0; a.acc = 0; }
|
||
return;
|
||
}
|
||
if (a.tries >= max || !(iv > 0)) return;
|
||
a.acc += dt;
|
||
let grew = false;
|
||
while (a.acc >= iv - 1e-9 && a.tries < max) { a.acc -= iv; a.tries++; a.n++; grew = true; }
|
||
if (grew) b.addBuff(e, { key: 'ab:rush', mods: { moveMul: 1 + ms * a.n }, persist: true });
|
||
},
|
||
dealt(c, b, e, a) {
|
||
if (!a.on) return;
|
||
const t = c.target;
|
||
const amount = e.s.moveSpeed * scale;
|
||
stop(b, e, a);
|
||
if (t && t.alive && amount > 0) b.dealDamage(e, t, { amount, type: 'phys', canDodge: false, isSkill: true, noSp: true, tags: ['enemyAbility', 'rush'] });
|
||
},
|
||
}];
|
||
}
|
||
|
||
/**
|
||
* 萨卡兹悖谬暴虐兵长: blocked only by a blocker with ≥ 3 free block; its first hit also strikes the units around its target
|
||
* (PRTS 技能0 暴击 "对目标和周围4格的我方单位造成攻击力150%的物理普通伤害(无视无法选择,无视迷彩)※此技能仅能触发一次"):
|
||
* the splash ignores 无法选择, so it reaches an airborne 起飞 ally too (`ignoreSelect`).
|
||
*/
|
||
function kitFirstAoe(ab) {
|
||
const scale = T(ab, 'AOEAttack.atk_scale') ?? 0;
|
||
return [blockWeight(3), {
|
||
dealt(c, b, e, a) {
|
||
if (a.done) return;
|
||
a.done = true;
|
||
b.fx('explode', { x: c.target.x, y: c.target.y, r: 1, kind: 'aoeAttack' });
|
||
for (const u of b.alliesInRadius(c.target.x, c.target.y, 1)) {
|
||
if (u !== c.target) hurt(b, e, u, e.s.atk * scale, 'phys', { ignoreSelect: true, tags: ['aoeAttack'] });
|
||
}
|
||
},
|
||
}];
|
||
}
|
||
|
||
function kitDefDecay(ab) {
|
||
const max = T(ab, 'def_reduce.max_stack_cnt') ?? 0, def = T(ab, 'def_reduce.def') ?? 0, res = T(ab, 'def_reduce.magic_resistance') ?? 0;
|
||
return [{
|
||
taken(c, b, e) {
|
||
if (!(max > 0)) return;
|
||
b.addBuff(e, { key: 'ab:defDecay', refresh: 'stack', stacks: 1, maxStacks: max, persist: true, mods: { defFlat: def / max, resFlat: res / max } });
|
||
},
|
||
}];
|
||
}
|
||
|
||
function kitExposeOnHit(ab) {
|
||
const dur = T(ab, 'Expose.weak[limit]') ?? 0, scale = T(ab, 'Expose.damage_scale') ?? 1;
|
||
return [{ sil: true, taken(c, b) { const s = c.source; if (s && s.side === 'ally' && s.alive && s.kind !== 'device') expose(b, s, dur, scale); } }];
|
||
}
|
||
|
||
// ---------------------------------------------------------------------------------------------------------------
|
||
// this family's part of KITS (content/enemies.js spreads the parts in this order)
|
||
|
||
export const SPECIAL_KITS = Object.freeze({
|
||
// --- SPECIAL 特异 / others
|
||
enemy_1045_hammer: kitStun3, // 粉碎攻坚手 · every 3rd attack stuns
|
||
enemy_1045_hammer_2: kitStun3, // 粉碎攻坚组长 · same
|
||
enemy_1116_liprr: kitPrisoner(), // 普通囚犯 · confined (ASPD −50) → freed after 4 attacks (ATK +50 %)
|
||
enemy_1116_liprr_2: kitPrisoner(), // 老练囚犯 · same
|
||
enemy_1118_lidbox_2: kitPrisoner(), // 拳师囚犯 · + DEF penetration when freed
|
||
enemy_1119_vofsd: kitPrisoner(), // 强壮囚犯 · + RES and regen when freed
|
||
enemy_1121_lifbos: kitPrisoner(true), // 重犯 · confined DEF up; first liberation frees every prisoner
|
||
enemy_1121_lifbos_2: kitPrisoner(true), // 传奇重犯 · same
|
||
enemy_1072_dlancer: kitRush, // 萨卡兹穿刺手 · accelerates while walking; first hit after block ×speed
|
||
enemy_1320_wdrrl_2: kitFirstAoe, // 萨卡兹悖谬暴虐兵长 · block ≥3 only; first attack splashes
|
||
enemy_1302_ymtro_2: () => [blockWeight(4)], // “越长尘” · block ≥4 only (passengers n/a)
|
||
enemy_1329_cbshld: kitDefDecay, // 弧光镜卫 · DEF/RES drop with every damage instance
|
||
enemy_1329_cbshld_2: kitDefDecay, // 弧光镜卫长 · same
|
||
enemy_1249_lysdb_2: (ab) => [{ spawn(b, e, a, ab2) { ab2.hitShield = T(ab, 'Shield.max_block_damage_cnt') ?? 0; } }], // 莱茵生命防卫科高级成员 · blocks one phys/arts hit
|
||
enemy_1402_tgshd_2: kitExposeOnHit, // 重装侦察兵 · exposes its attackers 5 s
|
||
enemy_1081_sotisd: (ab) => [taunt(T(ab, 'taunt.taunt_level') ?? 0)], // 游击队盾卫 · taunt +1
|
||
enemy_1427_lrnazg: kitNazg, // “灵幛” · attack hits everyone around it (+ on infection tiles)
|
||
enemy_1422_lrsldr: () => [infectionArts()], // 萨卡兹枯朽前锋 · arts attacks on 源石污染区 (infection tiles)
|
||
enemy_1422_lrsldr_2: () => [infectionArts()], // 萨卡兹枯朽辟路前锋 · same
|
||
enemy_1425_lrcmra: kitChimera, // 孽罪奇美拉 · activated on infection: arts attacks + pollution aura
|
||
enemy_1425_lrcmra_2: kitChimera, // 渎罪奇美拉 · same
|
||
enemy_1430_lrrook: (ab) => [{ // 愧悔魂灵圣杯 · shares damage taken by nearby ground enemies
|
||
spawn(b, e, a) { a.r = T(ab, 'takeDmg.range_radius') ?? 1.7; a.share = T(ab, 'takeDmg.damage_scale') ?? 0; stOf(b).chalices = (stOf(b).chalices ?? 0) + 1; },
|
||
death(c, b) { stOf(b).chalices = Math.max(0, (stOf(b).chalices ?? 1) - 1); },
|
||
}],
|
||
enemy_1025_reveng: (ab) => [lowHpBuff(0.5, { atkPct: T(ab, 'atkup.atk') ?? 0 })], // 寻仇者 · ATK up below half HP
|
||
enemy_1021_bslime: (ab) => [deathBoom({ scale: T(ab, 'boom.atk_scale') ?? 0 })], // 高能源石虫 · death: phys blast
|
||
enemy_1067_snslime: (ab, e) => [deathBoom({ scale: T(ab, 'boom.atk_scale') ?? 0, r: (e.def.raw.stats && e.def.raw.stats.rawRangeRadius) || BOOM_RADIUS, status: { key: 'cold', dur: T(ab, 'boom.freeze') ?? 0 } })], // 冰爆源石虫 · death: phys blast + cold
|
||
enemy_1069_icebrk_2: (ab) => [{ hitOut(c, b, e) { if (c.dmg.isAttack && c.target.s.flags.freeze) c.dmg.amount *= T(ab, 'atkup.atk_scale') ?? 1; } }], // 雪怪小队破冰者 · ×3 vs frozen
|
||
enemy_1026_aghost: () => [unblockable()], // 幽灵组长 · unblockable
|
||
enemy_1062_rager_2: (ab) => [{ iv: 1, tick(b, e) { const v = T(ab, 'periodic_damage.damage') ?? 0; if (v > 0) b.dealDamage(null, e, periodicDamage(v)); } }], // 狂暴宿主组长 · "自身每秒受到500无来源真实伤害" (damage, not 流失)
|
||
enemy_1183_mlasrt: (ab) => [ep('erosion', T(ab, 'EpDamage.attack@ep_damage_ratio') ?? 0), nthAttackPower(nthOf(ab.sk.PowerAttack), (ab.sk.PowerAttack && ab.sk.PowerAttack.bb.atk_scale) || 1)], // 无胄盟清扫小队 · erosion; every 4th attack ×1.5
|
||
enemy_1273_stmgun_2: (ab) => { // 高准度伦蒂尼姆城防自行炮 · locks the highest max-HP unit in range, bombards the highest HP% on its 9 tiles
|
||
// the allies in its range (the engine's ranged reach: radius + the ally collider)
|
||
const inRange = (b, e) => allTargets(b, e).filter((a) => hypot(a.x - e.x, a.y - e.y) <= (e.base.rangeRadius || 0) + ALLY_COLLIDER_RADIUS + 1e-9);
|
||
return [skill(ab.sk.Cannon, (b, e) => {
|
||
const lock = inRange(b, e).sort((p, q) => q.s.maxHp - p.s.maxHp)[0];
|
||
if (!lock) return;
|
||
const r0 = Math.round(lock.y), c0 = Math.round(lock.x);
|
||
const dur = CANNON_SHOTS * CANNON_EVERY;
|
||
b.fx('telegraph', { x: c0, y: r0, r: 1.5, dur, kind: 'cannon' });
|
||
b.addBuff(e, { key: 'ab:cannon', duration: dur, flags: { noDisplace: true } });
|
||
const ab2 = e.mem.ab;
|
||
const had = new Set(ab2.immune || []);
|
||
ab2.immune = new Set([...had, ...CANNON_IMMUNE]);
|
||
for (let i = 1; i <= CANNON_SHOTS; i++) b.after(i * CANNON_EVERY, () => {
|
||
if (!e.alive) return;
|
||
if (i === CANNON_SHOTS) ab2.immune = had;
|
||
// each shot picks inside the 9-tile zone (格子判定): an area selection — a 迷彩 ally may be shot, an unblocking 隐匿
|
||
// one not (areaSelectable; it used to skip 迷彩 like a normal attack)
|
||
const t = b.allies().filter((a) => areaSelectable(e, a) && Math.abs(Math.round(a.y) - r0) <= 1 && Math.abs(Math.round(a.x) - c0) <= 1)
|
||
.sort((p, q) => q.hpRatio - p.hpRatio)[0];
|
||
if (t) { b.fx('explode', { x: t.x, y: t.y, r: 0.5, kind: 'cannon' }); hurt(b, e, t, e.s.atk * (ab.sk.Cannon.bb.atk_scale ?? 0), 'arts'); }
|
||
}, { owner: e });
|
||
}, { cond: (b, e) => inRange(b, e).length > 0 })];
|
||
},
|
||
// 萨卡兹骸骨拷打者 · heals + ATK stack when a unit within Attack.range_radius is knocked out; leaves 2 血珀 on death — they
|
||
// stay where it fell and wait for a 唤血祭坛 that this mode has none of (inert, not counted)
|
||
enemy_1364_spnaxe_2: (ab) => [{
|
||
death(c, b, e) {
|
||
const key = ab.tS['Summon.enemy_key'], n = T(ab, 'Summon.cnt') ?? 0;
|
||
if (c.reason !== 'killed' || !key || !(n > 0)) return;
|
||
spawnChildren(b, e, key, n, { route: stayRoute(e), countInTotal: false, mods: null });
|
||
},
|
||
}, {
|
||
spawn(b, e) { watchDeaths(b, e); },
|
||
otherDeath(c, b, e) {
|
||
const u = c.unit;
|
||
if (!u || u === e || c.reason !== 'killed' || hypot(u.x - e.x, u.y - e.y) > (T(ab, 'Attack.range_radius') ?? TORTURER_RADIUS) + 1e-9) return;
|
||
b.heal(e, e, e.s.maxHp * (T(ab, 'Attack.hp_ratio') ?? 0), { self: true });
|
||
b.addBuff(e, { key: 'ab:torture', refresh: 'stack', stacks: 1, maxStacks: T(ab, 'Attack.max_stack_cnt') ?? 1, persist: true, mods: { atkPct: T(ab, 'Attack.atk') ?? 0 } });
|
||
},
|
||
hitOut(c, b, e) { if (c.dmg.isAttack && c.target !== e.blockedBy) c.dmg.amount *= T(ab, 'Attack.attack@ranged_atk_scale') ?? 1; },
|
||
}],
|
||
enemy_1501_demonk: () => [maxTargets(2)], // 萨卡兹百夫长 · attacks 2 targets
|
||
enemy_10018_sgrobh: () => [maxTargets(2)], // “独轮车玩具” · attacks 2 targets
|
||
enemy_2001_duckmi: (ab) => [unblockable(), runWhenHit(T(ab, 'run.attack@move_speed') ?? 0)], // 鸭爵 · unblockable, no attack; runs +400 % once hurt
|
||
enemy_2001_duckmi_2: (ab) => [unblockable(), runWhenHit(T(ab, 'run.attack@move_speed') ?? 0)], // 鸭爵 (鸭爵 strategy) · same, +300 %
|
||
enemy_10159_mntrjn: () => [unblockable()], // 伊利昂的木驮兽 · unblockable (passengers n/a)
|
||
enemy_2009_csaudc: kitEp('neural', 'combat.attack@ep_damage_ratio'), // 骇笑看客 · neural on hit
|
||
enemy_2010_csdcr: (ab) => [ep('neural', T(ab, 'attack.attack@ep_damage_ratio') ?? 0), { // 绯红歌伶 · neural on hit; every 20 hits taken: global enemy ASPD up
|
||
taken(c, b, e, a) {
|
||
a.n = (a.n ?? 0) + 1;
|
||
if (a.n < (T(ab, 'AttackSpeedUp.stack_cnt') ?? Infinity)) return;
|
||
a.n = 0;
|
||
b.fx('telegraph', { x: e.x, y: e.y, r: 99, kind: 'songOfWar', id: e.id });
|
||
for (const o of b.aliveEnemies()) b.addBuff(o, { key: 'ab:songOfWar', duration: T(ab, 'AttackSpeedUp.duration') ?? 0, refresh: 'extend', mods: { aspd: T(ab, 'AttackSpeedUp.attack_speed') ?? 0 }, visible: true });
|
||
},
|
||
}],
|
||
enemy_10001_trslim: (ab) => [skill(ab.sk.StartRun, (b, e) => { // 简饲源石虫 · below half: runs (faster, unblockable 3 s)
|
||
const s = ab.sk.StartRun.bb;
|
||
b.addBuff(e, { key: 'ab:run', duration: s.block_free_time ?? 0, mods: { moveMul: 1 + (s.move_speed ?? 0) }, flags: { unblockable: true }, visible: true });
|
||
}, { sil: true, cond: (b, e) => e.hpRatio < 0.5 })],
|
||
enemy_10027_vtsk: (ab) => { // “帝国的甲胄” · entrance barrage on the highest-HP unit; ranged ×0.8; 3-hit charge attack
|
||
const ap = ab.sk.Appear, mc = ab.sk.MultiCombat;
|
||
return [{
|
||
spawn(b, e) {
|
||
const n = ap ? ap.bb.times ?? 0 : 0;
|
||
b.after(0.5, () => {
|
||
if (!e.alive || !n) return;
|
||
const lock = allTargets(b, e).sort((p, q) => q.hp - p.hp)[0];
|
||
if (!lock) return;
|
||
b.fx('telegraph', { x: lock.x, y: lock.y, r: 1, kind: 'barrage', id: e.id });
|
||
for (let i = 0; i < n; i++) {
|
||
// "对碰撞范围内的1名当前生命值最高的我方单位" — an area selection: never an airborne 起飞 ally nor an unblocking
|
||
// 隐匿 one (PRTS “帝国的甲胄”; areaAlliesInTiles)
|
||
const t = areaAlliesInTiles(b, e, lock.tileR, lock.tileC, 'box', 1).sort((p, q) => q.hp - p.hp)[0];
|
||
if (t) hurt(b, e, t, e.s.atk, 'phys');
|
||
}
|
||
}, { owner: e });
|
||
},
|
||
hitOut(c, b, e) { if (c.dmg.isAttack && c.target !== e.blockedBy) c.dmg.amount *= T(ab, 'range.attack@atk_scale_range') ?? 1; },
|
||
}, mc ? {
|
||
cd: mc.cd, icd: mc.icd, fire(b, e) { e.mem.ab.multi = mc.bb.times ?? 1; },
|
||
dealt(c, b, e) {
|
||
const m = e.mem.ab.multi;
|
||
if (!(m > 1)) return;
|
||
e.mem.ab.multi = 0;
|
||
for (let i = 1; i < m; i++) hurt(b, e, c.target, e.s.atk, 'phys');
|
||
},
|
||
} : null];
|
||
},
|
||
enemy_10044_wintun: (ab) => { // 咸鳞汁推荐者 · fast with the barrel; first attack (SILENCE): big hit + buff zone for enemies
|
||
const s = ab.sk.BlockedBoom ? ab.sk.BlockedBoom.bb : {};
|
||
return [{
|
||
sil: true, // silenced: the barrel is kept (and its speed) until an unsilenced attack
|
||
spawn(b, e) { b.addBuff(e, { key: 'ab:barrel', persist: true, mods: { moveMul: T(ab, '1.move_speed') ?? 1 } }); },
|
||
hitOut(c, b, e, a) { if (!a.done && c.dmg.isAttack) c.dmg.amount *= s.blockee_atk_scale ?? 1; },
|
||
attack(c, b, e, a) {
|
||
if (a.done) return;
|
||
a.done = true;
|
||
b.removeBuff(e, 'ab:barrel');
|
||
const x = e.x, y = e.y, life = s.fixed_duration ?? 0;
|
||
b.fx('zone', { x, y, r: BARREL_RADIUS, dur: life, kind: 'barrel' });
|
||
let left = life;
|
||
const h = b.every(0.5, () => {
|
||
for (const o of b.enemiesInRadius(x, y, BARREL_RADIUS)) auraBuff(b, o, 'ab:barrelZone', 0.5, { aspd: s.attack_speed ?? 0, dodgePhys: s.prob ?? 0 });
|
||
left -= 0.5;
|
||
if (left <= 0) h.cancel();
|
||
});
|
||
},
|
||
}];
|
||
},
|
||
enemy_10045_parrot: kitParrot, // 吉兆飞鳞 · 近地悬浮 (grounded 8 s when stunned); sprints when first hit
|
||
enemy_10087_hlchgr: (ab) => [{ // 圣堂剑士 · spends ammo (every 6 s, max 5) for permanent speed/ATK
|
||
iv: T(ab, 'SkillTrigger.interval') ?? 6,
|
||
tick(b, e, a) {
|
||
a.n = (a.n ?? 0) + 1;
|
||
if (a.n > 5) return;
|
||
const s = ab.sk.ForeverEnhance ? ab.sk.ForeverEnhance.bb : {};
|
||
b.addBuff(e, { key: 'ab:enhance', refresh: 'stack', stacks: 1, maxStacks: 5, persist: true, mods: { moveFlat: (s.move_speed_add ?? 0) * e.base.moveSpeed, atkPct: s.atk_add ?? 0 } });
|
||
},
|
||
}],
|
||
enemy_10094_crstf: kitEp('neural', 'ep.ep_damage_ratio'), // 临时收音师 · neural on hit (filming zones n/a → always)
|
||
enemy_10097_crshd: (ab) => [ // 心虚设计师 · neural to its blocker every s; front damage −80 % (faces its walk)
|
||
{ iv: 1, tick(b, e) { if (e.blockedBy) elem(b, e, e.blockedBy, 'neural', e.s.atk * (T(ab, 'block.ep_damage_ratio') ?? 0)); } },
|
||
frontGuard(T(ab, 'weakness.damage_resistance') ?? 0, faceMove), // 减少来自正面的物理/法术伤害 (cameras absent: it never turns)
|
||
],
|
||
enemy_10098_crhro: (ab) => [ep('neural', T(ab, 'inside.attack@ep_damage_ratio') ?? 0), // 主角阵营角色 · neural on hit; 重生 once (reborn.duration s, full HP)
|
||
reborn({ dur: T(ab, 'reborn.duration') ?? 0, invincible: T(ab, 'reborn.invincible') ?? 0 })],
|
||
enemy_10099_crvln: kitEp('neural', 'inside.attack@ep_damage_ratio'), // 反派阵营角色 · neural on hit
|
||
enemy_10116_ymgtop: (ab) => { // 水遁忍者 · spinning phase (SILENCE): phys AoE every s; 失衡 stops it
|
||
const spin = {
|
||
sil: true, cd: ab.sk.SwitchModeTrigger ? ab.sk.SwitchModeTrigger.cd : 60, icd: ab.sk.SwitchModeTrigger ? ab.sk.SwitchModeTrigger.icd : 10,
|
||
fire(b, e, a) { a.until = b.time + (T(ab, 'EndRotate.rotate_duration') ?? 0); b.fx('telegraph', { x: e.x, y: e.y, r: 1, kind: 'spin', id: e.id }); },
|
||
iv: T(ab, 'RotateDamage.interval') ?? 1,
|
||
// "每秒对半径1.0范围内的所有我方单位造成…(无视迷彩,不可对空),自身受阻止攻击类异常效果影响期间无法造成此伤害": an area
|
||
// selection, skipped while 晕眩 / 冻结 / 浮空 (flag stun), 沉睡 or 缴械 hold it (PRTS 异常效果 §阻止攻击; until 0.1.3 it spun on)
|
||
tick(b, e, a) {
|
||
if (!(a.until > b.time) || e.s.flags.stun || e.s.flags.sleep || e.s.flags.disarm) return;
|
||
for (const u of areaAllies(b, e, e.x, e.y, T(ab, 'RotateDamage.attack@range_radius') ?? 1)) hurt(b, e, u, e.s.atk * (T(ab, 'RotateDamage.attack@atk_scale') ?? 1), 'phys');
|
||
},
|
||
};
|
||
// 漩涡形态 "不进行普通攻击" (PRTS 天赋): no blocked attack while it spins (until 0.1.3 its blocker took both)
|
||
const noAtk = { tick(b, e) { e.profile.noAttack = spin.until > b.time; } };
|
||
return [spin, noAtk, unbalanced((b, e) => { if (spin.until > b.time) { spin.until = 0; b.fx('phase', { x: e.x, y: e.y, id: e.id, kind: 'spinStop' }); } })];
|
||
},
|
||
enemy_10118_ymgprc: (ab) => { // 澪 · double hits; 寒晖: every 4th attack (spCost 3) is a ×1.5 double hit
|
||
const scale = (ab.sk.PowerAttack && ab.sk.PowerAttack.bb.atk_scale) || 1;
|
||
const pw = nthAttackPower(nthOf(ab.sk.PowerAttack), scale);
|
||
return [pw, { dealt(c, b, e) { hurt(b, e, c.target, e.s.atk * (pw.power ? scale : 1), 'phys', { isSkill: false }); } }];
|
||
},
|
||
enemy_10127_rkmbst_2: (ab) => [taunt(1), { spawn(b, e) { // 异质裂兽·α · M0 shield (damage ×0.5, ASPD +100, barrier)
|
||
b.addBuff(e, { key: 'ab:m0', persist: true, visible: true, mods: { dmgTakenMul: 1 - (T(ab, 'M0Shield.damage_resistance') ?? 0), aspd: T(ab, 'M0Shield.attack_speed') ?? 0 } });
|
||
const sh = e.s.maxHp * (T(ab, 'M0Shield.init_shield_hp_ratio') ?? 0);
|
||
if (sh > 0) b.addBuff(e, { key: 'ab:m0barrier', shield: sh, persist: true });
|
||
} }],
|
||
enemy_10156_mncrer: (ab) => [{ sil: true, death(c, b, e) { // 新手祭司学徒 · death: heals nearby enemies
|
||
if (c.reason !== 'killed') return;
|
||
const r = T(ab, 'Boom.projectile_range') ?? 1, amt = e.s.atk * (T(ab, 'Boom.heal_scale') ?? 0);
|
||
b.fx('explode', { x: e.x, y: e.y, r, kind: 'heal' });
|
||
for (const o of b.enemiesInRadius(e.x, e.y, r)) if (o !== e) b.heal(e, o, amt);
|
||
} }],
|
||
enemy_10162_mnctpt: (ab) => [{ // 自制投石机 · 3-hit attacks with small splash; 索敌不受阻挡影响
|
||
// PRTS 天赋 「索敌不受阻挡影响,且不会因丢失目标而结束攻击」: its target selection ignores its block — a 隐匿 blocker (an
|
||
// operator on the 排气格栅) is no target, so with nobody else in range it does not attack (the official game, community
|
||
// report of 2026-10-06, item 24); it picks by 仇恨值, not its blocker first
|
||
spawn(b, e) { e.profile.blockFree = true; },
|
||
dealt(c, b, e) {
|
||
const n = T(ab, 'Attack.attack@times') ?? 1, r = T(ab, 'Attack.attack@projectile_range') ?? 0;
|
||
const t = c.target;
|
||
for (let i = 1; i < n; i++) hurt(b, e, t, e.s.atk, 'phys');
|
||
// the stone's splash ("碰撞无视迷彩"): an area selection — no unblocking 隐匿 ally
|
||
if (r > 0) for (const u of areaAllies(b, e, t.x, t.y, r)) if (u !== t) for (let i = 0; i < n; i++) hurt(b, e, u, e.s.atk, 'phys');
|
||
},
|
||
}],
|
||
});
|