Files
ark-stronghold-protocol/server/sim/content/enemies.js
T
sganggs d4f868cd19 Playtest #4 fixes: tile picking, knocked-down state, element gauges, draft timer, live stats, shop items, drones, 赫默, 炎佑, UI flash
- #1 picking by the tile under the pointer (prep, battle, pen, equipment drops); the dragged model stays
  under the pointer (mouse and touch); pixel probe removed; phone ghost-click on the underframe fixed
- #2 two-tap confirm on 机变 cards (like the shop)
- #3 a 同盟 room with one human is untimed like 独立模拟 (no countdown outside battles)
- #4 strategy draft: one synced countdown, 30 s per human turn, timeout takes the highlighted band
- #5 special 维式重锤 (4) and 突变细胞 no longer sold; item card shows where they come from
- #6 operator skill-mode sounds no longer play on normal attacks (纯烬艾雅法拉 and 55 others)
- #7 detail card shows live battle stats and the prep-time battle-start stats (g.unitStats)
- #8 official element gauges on units; burst cooldown (阻回) per PRTS
- #9 knocked-down operators stay on their tile with a redeploy countdown ring
- #10 drone behaviour per PRTS (暴鸰, 先兆者 shells, 黑云, 萨科塔之眼)
- #11 赫默's drone only from her skill; placeable summons limited to talent tokens
- #12 炎佑 stays where it is with no enemies; 祛恶之焰 as a continuous channel
- #13 the occasional giant bond icon (loading texture scaled ×46) fixed
- docs (DESIGN §18), recordings, data and tests updated (2467 unit tests, all browser suites green)
2026-09-30 05:35:45 +08:00

2393 lines
138 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/enemies.js — enemy special types (特训敌人) and individual enemy abilities (DESIGN §7).
//
// Battle side: install(battle) registers ONE set of global dispatch hooks per battle (ensureInstalled — idempotent,
// bosses.js calls it too) and attaches an ability list to every spawned enemy whose key has a kit in KITS.
// Every number comes from the enemy's talent / skill blackboard (data/enemies.json, merged with the wave template's
// `overrides[key].talents/skills` — the engine only applies `overrides.stats`). Numbers that exist nowhere in the
// data are named constants below, marked [ASSUMED] or sourced from the official term table (gamedata_const
// termDescriptionDict) / PRTS.
//
// Runtime model: `enemy.mem.ab = { key, t (talent bb), tS (talent bbStr), sk {prefabKey → {cd, icd, sp, bb, bs}},
// list [abilities], times, hitShield, atkType, immune, … }`. An ability is a plain object with optional methods
// spawn(b,e,a,ab) · dealt(c,…) ('damaged' by one of e's ATTACKS on an ally, non-element) · taken(c,…) ('damaged' on e,
// non-element) · hitIn(c,…) ('hit' on e, late: after every other hit handler) · hitOut(c,…) ('hit' from e, early) ·
// before(c,…) (beforeAttack of e) · attack(c,…) · blocked(c,…) · killed(c,…) (return true = revived; the kill is then
// hidden from later 'kill' handlers) · death(c,…) · status(c,…) (statusApplied on e) · burst(c,…) (elementBurst on e)
// · tick(b,e,a,dt) every `iv` s (every tick when iv is 0) · skill: `cd`/`left`(=icd)/`cond`/`fire` (cooldown runs
// continuously; fires when ready, the enemy can act and `cond` holds). `sil: true` = the ability's handbook line
// is flagged SILENCE (沉默 disables it: exactly the data's `format: "SILENCE"` lines); stun / freeze / sleep block
// skills. Enemy SP skills ("数次攻击后…"): SP +1 per attack, the (spCost+1)-th attack is the skill attack (official
// 粉碎攻坚手 text: spCost 2 = "攻击2次后,下一次攻击").
// Shared mechanics: reborn() — "首次被击倒后重生 / 第二形态" (the first KO is hidden from kill credit and bounty; a
// rebirth of Reborn.duration s, untargetable and inert, then the new form); artsBarrier() — "吸收法术伤害的屏障"
// (absorbs arts after RES); frontGuard() — "来自正面的伤害降低" (facing = walking direction or the bigger crowd);
// unbalanced() — 失衡 detection (displacement beyond the enemy's own speed; engine displace() has no hook);
// attackLoop() — normal attacks for data-unarmed enemies once a form arms them.
//
// Special types (factions.json):
// FLY — engine (FLY motion, ranged-only targeting). Flyer kits below (御4, 护障, 寒霜, 萨科塔之翼/眼, 黑云 …).
// TIMES 频次 — "需要N次伤害击倒": maxHp := N hits; engine buff flag hitCount — every damage instance (any type, element
// bursts included) removes exactly 1 on its own DamageInfo (茶器 hitCountArts: phys removes 0); HP loss
// (loseHp) bypasses the rule. The faction's enemies
// create those units (death spawns, embers, blades, 再生).
// ELEMENT — element damage on hit = ATK × ep_damage_ratio into the ally's gauge. 侵蚀 (erosion) is an engine
// gauge whose burst is the official one (termDescription ba.dt.erosion: "永久降低100点防御力并受到800点物理伤害").
// DOT — HP-loss / arts damage zones (污染秽蚀, 毒雾, 燃烧区域), bleeding (removed by healing), pulsing auras.
// INVISIBLE — permanent `stealth` flag (engine: untargetable unless blocked or revealed; AoE still hits).
// REFLECTION — 折射 (ba.refraction "生效时,法术抗性+70"): RES +refracting.magic_resistance while NOT silenced
// (the ability line is SILENCE-flagged: silencing turns it off); 镜膜 also gets max HP +100 % while on.
// SPECIAL — mostly stats; prisoners, 穿刺手, 暴虐兵长, 镜卫, 动力装甲 … below.
// Every enemy key of data/enemies.json is either in KITS (this file), BOSS_KEYS (bosses.js) or STATS_ONLY (with the
// reason) — pinned by test/content/enemies_bosses.test.js. Bounty (悬赏) leaders are included: multi-form ones
// (巨大的丑东西, 杰斯顿, 自在, 锏, 扎罗) get their second form; event-only mechanics (芦苇, 摄影区, 狂欢时刻, 悬索桥, 唤血祭坛,
// 封冻/供暖器, 赘生甲壳, 锁链, 血债账款, 晦明) have no counterpart in this mode and are listed where they apply.
//
// fx kinds emitted (battle.fx(kind, {x, y, …})): 'explode' {r, kind} · 'zone' {r, dur, kind} · 'telegraph' {r, dur,
// kind, tiles?} (delayed strikes, charges, 'reborn') · 'beam' {from, to, kind} · 'summon' {id, key} · 'ember'/'revive'/
// 'stone' {id} · 'blink' {id, fx, fy} · 'charge' {id, tx, ty} · 'expose' {id} · 'shieldBreak' {id} · 'liberate' {id}
// · 'phase' {id, kind} (form / barrier changes) · 'lpLoss' {value, reason} · 'steal' · 'ignite'.
// Custom hook: 'lpLoss' {amount, reason, source} — leader "扣除目标生命" effects; also summed into result.lpLoss.
import { TICK, MOVE_SCALE, ELEMENT, ATTACK_PAUSE } from '../constants.js';
import { canTargetAlly, sortAllyTargets, sortEnemyTargets } from '../targeting.js';
import { mitigate } from '../damage.js';
// ---------------------------------------------------------------------------------------------------------------
// constants (numbers that exist nowhere in the data)
/** 侵蚀 → engine gauge. */
export const EROSION = 'erosion';
/** Official erosion burst on operators (gamedata_const termDescriptionDict ba.dt.erosion; engine constants ELEMENT.erosion.ally). */
export const EROSION_BURST = Object.freeze({ defDown: ELEMENT.erosion.ally.defDown, physDamage: ELEMENT.erosion.ally.damage });
/** Death-explosion radius when the enemy has no official radius [ASSUMED]. */
const BOOM_RADIUS = 1.25;
/** Pollution zone radius of 萨卡兹枯朽战车's ranged 污染秽蚀 [ASSUMED]. */
const TANK_ZONE_RADIUS = 1;
/** "数个目标" of 假想敌:骨刺 while stealthed [ASSUMED]. */
const ACBUNN_TARGETS = 3;
/** 假想敌:再生 shield aura radius ("周围一定距离") [ASSUMED]. */
const ACPUPP_AURA_RADIUS = 2;
/** 重弩突袭者 直击 reach along a row/column [ASSUMED]. */
const CROSS_REACH = 6;
/** 暴鸰 投弹: the target's tile and its 8 neighbours (PRTS "对目标及其周围八格的我方单位造成100%物理伤害"). */
const BOMB_REACH = 1;
/** 帝国炮火先兆者 shell (PRTS "普通攻击向目标所在位置发射一枚于3秒后命中的弹道,弹道对半径1.2范围内的所有我方单位造成攻击力
* 100%的无来源物理伤害 … ※弹道始终使用缓存攻击力"): flight time and blast radius. */
const SHELL_FLIGHT = 3, SHELL_RADIUS = 1.2;
/** 假想敌:黑云 抓取: the blackboard radius counts ×2.5 (PRTS "2.5倍可变半径": range_radius 1.5 → 3.75), at most 3 prey,
* "短暂延迟后" the 延迟吞噬 lands (delay [ASSUMED] 0.5 s); its SP (= 全弹发射 hits) caps at the data's spData.maxSp. */
const GRAB_RADIUS_SCALE = 2.5, GRAB_MAX_PREY = 3, GRAB_DELAY = 0.5;
/** 源石污染区 aura radius of an activated 奇美拉 [ASSUMED]. */
const CHIMERA_AURA_RADIUS = 1;
/** Bombardments per 自行炮 Cannon cast ("数次") [ASSUMED]. */
const CANNON_SHOTS = 3;
/** 枯朽之种 summoned per BornBugs cast ("数个") [ASSUMED]. */
const BUGS_PER_CAST = 3;
/** 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;
/** "短时间" DEF-down of 碎骨's grenade [ASSUMED]. */
const SKULSR_DEFDOWN_DUR = 3;
/** W's C4 fuse and blast radius [ASSUMED]. */
const C4_FUSE = 1, C4_RADIUS = 1;
/** Every Nth attack for "数次攻击后" abilities without an SP cost in the data [ASSUMED]; with a cost N = spCost + 1
* (official 粉碎攻坚手 text for spCost 2: "攻击2次后,下一次攻击" ⇒ 3rd; 码头水手 / 澪 / 清扫小队 spCost 3 ⇒ 4th). */
const NTH_ATTACK = 3;
export const nthOf = (s) => (s && s.sp > 0 ? s.sp + 1 : NTH_ATTACK);
/** 鼠王 【唱沙】 cross reach and 【沙狱】 "及其周围" radius [ASSUMED]. */
const DRIFT_REACH = 1, SANDSTORM_RADIUS = 1;
/** 巨大的丑东西 self-destruct radius ("大范围晕眩") [ASSUMED]. */
const MCM_BOMB_RADIUS = 2.5;
/** 自在 【纬地经天】: tiles covered along the row and the column of the centre ("十字型") [ASSUMED]. */
const XI_CROSS_REACH = 2;
/** 扎罗: form-2 "远程" attack radius, 【远古威慑】 aura radius / ASPD (no numbers in the data), 溶血骇惧 HP loss per second
* reached at the end of duration_bleed (share of max HP, rising linearly — "逐渐加速失去生命值") [ASSUMED]. */
const WOLF_RANGE = 2.5, WOLF_AWE_RADIUS = 2.5, WOLF_AWE_ASPD = -30, WOLF_BLEED_PEAK = 0.05;
/** 失衡 movement speed that turns 弧光锋卫's per-interval bleed into HP per tile moved [ASSUMED]. */
const UNBALANCE_SPEED = 5;
/** 乌顶巨角卢鲁 【角力对决】: the operator cannot be pushed (fixed tiles) ⇒ "更多伤害" multiplier [ASSUMED]. */
const ELK_FAIL_SCALE = 2;
// ---------------------------------------------------------------------------------------------------------------
// per-battle state + installation
const STATE = new WeakMap();
function stOf(b) {
let st = STATE.get(b);
if (!st) { st = { lpLoss: 0, floats: 0, freedAll: false, deathWatch: new Set() }; STATE.set(b, st); }
return st;
}
/** Register the global dispatch hooks once per battle (idempotent). */
export function ensureInstalled(b) {
const st = stOf(b);
if (st.installed) return st;
st.installed = true;
b.on('enemySpawn', ({ enemy }) => onSpawn(b, enemy), { priority: 100 });
b.on('hit', (c) => onHitOut(b, c), { priority: 200 });
b.on('hit', (c) => chaliceShare(b, c), { priority: -400 });
b.on('hit', (c) => onHitIn(b, c), { priority: -500 });
b.on('damaged', (c) => onDamaged(b, c), { priority: 50 });
b.on('beforeAttack', (c) => onBeforeAttack(b, c), { priority: 50 });
b.on('attack', (c) => { if (c.attacker && c.attacker.side === 'enemy') dispatch(b, c.attacker, 'attack', c); }, { priority: 50 });
b.on('blocked', (c) => dispatch(b, c.enemy, 'blocked', c), { priority: 50 });
b.on('kill', (c) => onKill(b, c), { priority: 1000 });
b.on('death', (c) => onDeath(b, c), { priority: 50 });
b.on('beforeStatus', (c) => {
const ab = c.target && c.target.mem && c.target.mem.ab;
if (!ab) return;
if (ab.immune && ab.immune.has(c.status)) c.cancel = true;
}, { priority: 50 });
b.on('statusApplied', (c) => { if (c.target && c.target.side === 'enemy') dispatch(b, c.target, 'status', c); }, { priority: 50 });
b.on('heal', (c) => {
// 逐腐兽 bleeding ends when the target receives healing (natural regeneration excluded)
if (c.amount > 0 && c.target && c.target.side === 'ally' && !(c.opts && c.opts.regen) && c.target.findBuff('ab:bleed')) b.removeBuff(c.target, 'ab:bleed');
}, { priority: -50 });
b.on('elementBurst', (c) => onBurst(b, c), { priority: 50 });
b.on('tick', (c) => onTick(b, c.dt), { priority: 50 });
b.on('battleEnd', ({ result }) => { if (st.lpLoss > 0 && result) result.lpLoss = (result.lpLoss ?? 0) + st.lpLoss; });
return st;
}
export function install(battle) { ensureInstalled(battle); }
export function registerMeta() {}
// ---------------------------------------------------------------------------------------------------------------
// ability bookkeeping
/** The enemy's ability record (created on demand). */
export function abOf(b, e) {
if (e.mem.ab) return e.mem.ab;
const key = e.defId;
const ov = (b.enemyOverrides && (b.enemyOverrides[key] ?? b.enemyOverrides[e.def && e.def.key])) || null;
const raw = (e.def && e.def.raw) || {};
const t = { ...((e.def && e.def.talent) || {}), ...((ov && ov.talents && ov.talents.bb) || {}) };
const tS = { ...((raw.talents && raw.talents.bbStr) || {}), ...((ov && ov.talents && ov.talents.bbStr) || {}) };
const sk = {};
for (const s of (ov && Array.isArray(ov.skills) ? ov.skills : (e.def && e.def.skills) || [])) {
if (!s || s.prefabKey == null) continue;
sk[s.prefabKey] = { cd: num(s.cooldown, 0), icd: num(s.initCooldown, 0), sp: num(s.spCost, 0), bb: s.bb || {}, bs: s.bbStr || {} };
}
e.mem.ab = { key, t, tS, sk, list: [], times: null, hitShield: 0, atkType: null, immune: null, origMaxHp: e.base.maxHp };
return e.mem.ab;
}
/** Attach abilities to an enemy (calls their spawn()). */
export function attach(b, e, list) {
const ab = abOf(b, e);
for (const a of list) {
if (!a) continue;
if (a.cd != null) a.left = num(a.left, num(a.icd, 0));
ab.list.push(a);
if (a.spawn) safe(b, e, () => a.spawn(b, e, a, ab));
}
return ab;
}
function safe(b, e, fn) {
try { return fn(); } catch (err) { b._handlerError('content:enemies', e, err); return undefined; }
}
function dispatch(b, e, name, c) {
const ab = e && e.mem && e.mem.ab;
if (!ab) return false;
let any = false;
for (const a of ab.list) {
const f = a[name];
if (!f) continue;
if (a.sil && e.s.flags.silence) continue;
try { if (f.call(a, c, b, e, a, ab)) any = true; } catch (err) { b._handlerError(`enemies:${name}`, e, err); }
}
return any;
}
const num = (v, d) => (typeof v === 'number' && Number.isFinite(v) ? v : d);
/** First finite talent value among keys. */
export function T(ab, ...keys) { for (const k of keys) { const v = ab.t[k]; if (typeof v === 'number' && Number.isFinite(v)) return v; } return undefined; }
export function silenced(e) { return !!e.s.flags.silence; }
export function canCast(e, sil) { return e.alive && !e.hidden && !e.s.flags.stun && !(sil && e.s.flags.silence); }
// ---------------------------------------------------------------------------------------------------------------
// global dispatchers
function onSpawn(b, e) {
const kit = KITS[e.defId];
if (typeof kit !== 'function') return;
const ab = abOf(b, e);
const list = safe(b, e, () => kit(ab, e, b)) || [];
attach(b, e, list);
}
function onHitOut(b, c) {
const s = c.source;
if (!s || s.side !== 'enemy' || !s.mem.ab || c.dmg.cancel) return;
const ab = s.mem.ab;
if (ab.atkType && c.dmg.isAttack && c.dmg.type !== 'element') c.dmg.type = ab.atkType;
dispatch(b, s, 'hitOut', c);
}
function onHitIn(b, c) {
const t = c.target;
if (!t || t.side !== 'enemy' || !t.mem.ab || c.dmg.cancel) return;
const ab = t.mem.ab;
dispatch(b, t, 'hitIn', c);
if (c.dmg.cancel) return;
const ty = c.dmg.type;
if (ab.hitShield > 0 && (ty === 'phys' || ty === 'arts')) {
// "可以抵挡一次物理或法术伤害" / 再生's aura shield: the whole instance is negated
c.dmg.cancel = true;
ab.hitShield--;
if (ab.hitShield <= 0) b.fx('shieldBreak', { x: t.x, y: t.y, id: t.id });
}
}
function onDamaged(b, c) {
const { source: s, target: t, dmg } = c;
if (!dmg || dmg.type === 'element') return;
if (t && t.side === 'enemy' && t.mem.ab) dispatch(b, t, 'taken', c);
if (s && s.side === 'enemy' && s.mem.ab && dmg.isAttack && t && t.side === 'ally') dispatch(b, s, 'dealt', c);
}
function onBeforeAttack(b, c) {
const a = c.attacker;
if (!a) return;
if (a.side === 'enemy') { if (a.mem.ab) dispatch(b, a, 'before', c); return; }
// 近地悬浮 (ba.float "无法被阻挡或近战攻击"): melee operators cannot hit floating enemies — they pick others instead
if (stOf(b).floats > 0 && a.profile && a.profile.attack !== 'ranged' && c.targets) {
const ok = (t) => !(t.mem && t.mem.ab && t.mem.ab.float);
const kept = c.targets.filter(ok);
if (kept.length === c.targets.length) return;
if (!kept.length && a.rangeKeys && c.targets.some((t) => t.side === 'enemy')) {
const prof = c.profile || a.profile;
const cands = b.enemiesInKeys(a.rangeKeys, a, prof).filter(ok);
sortEnemyTargets(b, a, cands, prof.priority);
kept.push(...cands.slice(0, Math.max(1, Math.floor(prof.maxTargets || 1))));
}
c.targets = kept;
}
}
function onKill(b, c) {
const v = c.victim;
if (!v || v.side !== 'enemy' || !v.mem.ab) return;
if (dispatch(b, v, 'killed', c) && v.hp > 0) c.stopPropagation = true; // revived: not a kill for anyone else
}
function onDeath(b, c) {
const u = c.unit;
const st = stOf(b);
if (u && u.side === 'enemy' && u.mem.ab) dispatch(b, u, 'death', c);
if (st.deathWatch.size) for (const w of [...st.deathWatch]) { if (!w.alive) { st.deathWatch.delete(w); continue; } dispatch(b, w, 'otherDeath', c); }
}
function onBurst(b, c) {
const t = c.target;
if (!t) return;
if (t.side === 'enemy') { if (t.mem.ab) dispatch(b, t, 'burst', c); return; }
// 假想敌:淤困 parasite: the host's burst spreads the same element to the 4 neighbouring allies
const par = t.findBuff('ab:parasite');
if (par && par.data && par.data.src) {
const src = par.data.src;
for (const o of alliesInTiles(b, t.tileR, t.tileC, 'plus', 1)) if (o !== t) elem(b, src, o, c.element, par.data.spread);
}
}
function onTick(b, dt) {
const list = b.enemies;
const n = list.length;
for (let i = 0; i < n; i++) {
const e = list[i];
if (!e || !e.alive || !e.mem.ab) continue;
const ab = e.mem.ab;
for (let j = 0; j < ab.list.length; j++) {
const a = ab.list[j];
if (!e.alive) break;
if (a.tick) {
if (!(a.sil && e.s.flags.silence)) {
if (!(a.iv > 0)) safe(b, e, () => a.tick(b, e, a, dt));
else {
a._acc = (a._acc ?? 0) + dt;
let k = 0;
while (a._acc >= a.iv - 1e-9 && k++ < 4 && e.alive) { a._acc -= a.iv; safe(b, e, () => a.tick(b, e, a, a.iv)); }
}
}
}
if (a.fire && a.cd != null) {
a.left -= dt;
if (a.left <= 1e-9 && canCast(e, a.sil) && (!a.cond || safe(b, e, () => a.cond(b, e, a)))) {
a.left = Math.max(TICK, a.cd);
a.casts = (a.casts ?? 0) + 1;
e.skillAnimUntil = b.time + 0.5;
safe(b, e, () => a.fire(b, e, a));
}
}
}
}
}
// ---------------------------------------------------------------------------------------------------------------
// helpers (exported for bosses.js)
/** Element damage (erosion mapped onto the engine gauge). */
export function elem(b, src, tgt, el, amount) {
if (!tgt || !tgt.alive || !(amount > 0)) return 0;
const element = el === 'erosion' ? EROSION : el;
return b.dealDamage(src, tgt, { type: 'element', element, amount, tags: ['enemyAbility'] });
}
/** Skill / ability damage (no dodge unless asked). */
export function hurt(b, src, tgt, amount, type = 'phys', { canDodge = false, tags = [], isSkill = true } = {}) {
if (!tgt || !tgt.alive || !(amount > 0)) return 0;
return b.dealDamage(src, tgt, { amount, type, canDodge, isSkill, tags: ['enemyAbility', ...tags] });
}
/** Allies whose tile is within `n` of (r, c): 'plus' = Manhattan (周围四格), 'box' = Chebyshev (周围8格). */
export function alliesInTiles(b, r, c, shape = 'plus', n = 1) {
const out = [];
for (const a of b.allies()) {
if (a.hidden) continue;
const dr = Math.abs(a.tileR - r), dc = Math.abs(a.tileC - c);
if (shape === 'plus' ? dr + dc <= n : Math.max(dr, dc) <= n) out.push(a);
}
return out;
}
/** Allies within radius that enemy `e` could target (ranged rules: stealth / untargetable skipped). */
export function targetsNear(b, e, r, { ranged = true, x = e.x, y = e.y } = {}) {
return b.alliesInRadius(x, y, r).filter((a) => canTargetAlly(e, a, ranged));
}
/** Every targetable ally on the field. */
export function allTargets(b, e) {
return b.allies().filter((a) => canTargetAlly(e, a, true));
}
/** Engine priority (blocker → taunt → latest deployed) on a copy. */
export function byPriority(e, list) { return sortAllyTargets(e, list.slice()); }
/** An HP-loss / damage zone. `tick(units)` runs every `iv` s for `life` s on the allies inside. */
export function zone(b, { x, y, r, life, iv = 1, kind = 'zone', tick }) {
b.fx('zone', { x, y, r, dur: life, kind });
let left = life;
const h = b.every(iv, () => {
tick(b.alliesInRadius(x, y, r));
left -= iv;
if (left <= 1e-6) h.cancel();
});
return h;
}
/** Remaining route of an enemy from its current position (for summons / phantoms that continue its path). */
export function remainingRoute(e) {
const R = e.route;
const here = [e.y, e.x];
if (!R || !Array.isArray(R.legs) || !R.legs.length) return { motion: e.motion === 'FLY' ? 'FLY' : 'WALK', start: here, end: here, checkpoints: [] };
const steps = [];
let end = null;
for (let i = Math.max(0, R.legIdx); i < R.legs.length; i++) {
const L = R.legs[i];
if (L.final) { end = [L.r, L.c]; break; }
if (L.t === 'move') steps.push({ t: 'move', p: [L.r, L.c] });
else if (L.t === 'wait') steps.push({ t: 'wait', s: i === R.legIdx && R.waitLeft != null ? R.waitLeft : L.time });
else if (L.t === 'disappear') steps.push({ t: 'disappear' });
else if (L.t === 'appear') steps.push({ t: 'appear', p: [L.r, L.c] });
}
if (!end) { const last = R.legs[R.legs.length - 1]; end = last && last.r != null ? [last.r, last.c] : [Math.round(e.y), Math.round(e.x)]; }
const route = { motion: e.motion === 'FLY' ? 'FLY' : 'WALK', start: here, end, checkpoints: [] };
if (steps.length) route.steps = steps;
return route;
}
/** A route that keeps an enemy in place (content moves it by hand). */
export function stayRoute(e, secs = 99999) {
const p = [Math.round(e.y), Math.round(e.x)];
return { motion: e.motion === 'FLY' ? 'FLY' : 'WALK', start: p, end: p, steps: [{ t: 'wait', s: secs }] };
}
/** Spawn `n` enemies at the parent's position that continue its route. Returns the spawned units. */
export function spawnChildren(b, parent, key, n, opts = {}) {
const out = [];
const route = opts.route ?? remainingRoute(parent);
const cnt = Math.max(0, Math.min(20, Math.floor(n)));
for (let i = 0; i < cnt; i++) {
const off = cnt > 1 ? (i - (cnt - 1) / 2) * 0.2 : 0;
const pos = opts.pos ?? [parent.y, parent.x + off];
const c = b.spawnEnemy(key, {
pos, route, mods: opts.mods ?? parent.mods ?? null, tag: opts.tag ?? null, countInTotal: opts.countInTotal,
ownerPlayerId: parent.ownerId ?? null, sourcePlayerId: parent.sourcePlayerId ?? null,
});
if (c) out.push(c);
}
if (out.length) b.fx('summon', { x: parent.x, y: parent.y, id: parent.id, key, n: out.length });
return out;
}
/** Move a unit straight toward (tx, ty) by `dist` tiles. Returns true on arrival. */
export function stepToward(u, tx, ty, dist) {
const dx = tx - u.x, dy = ty - u.y;
const d = Math.hypot(dx, dy);
if (d <= dist + 1e-9) { u.x = tx; u.y = ty; return true; }
u.x += (dx / d) * dist;
u.y += (dy / d) * dist;
return false;
}
/** Set a 频次 unit's hits (maxHp := hits, full). */
export function setHits(e, n) {
const h = Math.max(1, Math.round(n));
e.base.maxHp = h;
e.markDirty();
void e.s;
e.hp = h;
}
const HIT_COUNT_KEY = 'ab:hitCount';
/** 频次 on/off: every damage instance removes exactly 1 HP (engine flag; `artsOnly` = 茶器, physical instances remove 0). */
export function hitCount(b, e, on, artsOnly = false) {
if (!on) { b.removeBuff(e, HIT_COUNT_KEY); return; }
b.addBuff(e, { key: HIT_COUNT_KEY, persist: true, flags: artsOnly ? { hitCountArts: true } : { hitCount: true } });
}
export const isHitCount = (e) => !!e.findBuff(HIT_COUNT_KEY);
/** Leader "扣除目标生命" effects: recorded for the match (hook 'lpLoss' + result.lpLoss). */
export function lpLoss(b, amount, reason, source = null) {
if (!(amount > 0)) return;
const st = stOf(b);
st.lpLoss += amount;
b.fx('lpLoss', { x: source ? source.x : 0, y: source ? source.y : 0, value: amount, reason });
b.emit('lpLoss', { amount, reason, source });
}
/** 暴露: damage taken ×scale for `dur` s. */
export function expose(b, u, dur, scale) {
if (!u || !u.alive || !(dur > 0)) return;
b.addBuff(u, { key: 'ab:exposed', duration: dur, refresh: 'extend', mods: { dmgTakenMul: scale }, visible: true });
b.fx('expose', { x: u.x, y: u.y, id: u.id });
}
/** Final HP damage the pipeline would deal for a phys/arts/true `dmg` before shields (mitigation is linear in the amount). */
export function expectedFinal(src, tgt, dmg) {
const ts = tgt.s, ss = src && src.s ? src.s : null, ty = dmg.type;
const v = mitigate(dmg.amount, ty, ts, {
defIgnorePct: (dmg.defIgnorePct || 0) + (ss ? ss.defIgnorePct : 0), defIgnoreFlat: (dmg.defIgnoreFlat || 0) + (ss ? ss.defIgnoreFlat : 0),
resIgnorePct: (dmg.resIgnorePct || 0) + (ss ? ss.resIgnorePct : 0), resIgnoreFlat: (dmg.resIgnoreFlat || 0) + (ss ? ss.resIgnoreFlat : 0),
});
let mul = (dmg.mul ?? 1) * ts.dmgTakenMul;
if (ss) mul *= ss.dmgDealtMul * (ty === 'phys' ? ss.physDealtMul : ty === 'arts' ? ss.artsDealtMul : 1);
mul *= ty === 'phys' ? ts.physTakenMul : ty === 'arts' ? ts.artsTakenMul : ts.trueTakenMul;
const f = v * mul;
return Number.isFinite(f) && f > 0 ? f : 0;
}
/**
* "吸收法术伤害的屏障": absorb up to `left` of an arts instance in a `hit` handler (after RES, like a shield) by scaling
* the instance down (cancelled when fully absorbed). Returns the amount absorbed.
*/
export function absorbArts(c, left) {
if (!c || c.dmg.type !== 'arts' || !(left > 0) || c.dmg.cancel) return 0;
const f = expectedFinal(c.source, c.target, c.dmg);
if (!(f > 0)) return 0;
const take = Math.min(left, f);
if (take >= f - 1e-9) c.dmg.cancel = true;
else c.dmg.amount *= (f - take) / f;
return take;
}
/** Timed buff on each unit (same key never stacks: aura semantics). */
function auraBuff(b, u, key, iv, mods, flags = null, visible = false) {
b.addBuff(u, { key, duration: iv + 0.1, refresh: 'replace', mods, flags, visible });
}
/** Mark an ally/enemy ability as watching every death on the field. */
function watchDeaths(b, e) { stOf(b).deathWatch.add(e); }
const tileOf = (u) => [Math.round(u.y), Math.round(u.x)];
const onTerrain = (b, u, terrain) => { const [r, c] = tileOf(u); return b.grid.tile(r, c).terrain === terrain; };
// ---------------------------------------------------------------------------------------------------------------
// archetypes
/** 隐匿: permanent stealth. */
const stealth = () => ({ spawn(b, e) { b.addBuff(e, { key: 'ab:stealth', flags: { stealth: true }, persist: true }); } });
/** 无法被阻挡. */
const unblockable = () => ({ spawn(b, e) { b.addBuff(e, { key: 'ab:unblockable', flags: { unblockable: true }, persist: true }); } });
/** 频次: maxHp = hits. */
const times = (artsOnly = false) => ({ spawn(b, e) { setHits(e, e.def.maxHp); hitCount(b, e, true, artsOnly); } });
/** "只能被阻挡数大于等于N的单位阻挡". */
const blockWeight = (n) => ({ spawn(b, e) { e.blockWeight = n; } });
const taunt = (n) => ({ spawn(b, e) { if (n) b.addBuff(e, { key: 'ab:taunt', mods: { taunt: n }, persist: true }); } });
const maxTargets = (n) => ({ spawn(b, e) { if (n > 1) e.profile.maxTargets = n; } });
/** Element damage on every attack hit: ATK × ratio. */
const ep = (el, ratio, sil = false) => ({ sil, dealt(c, b, e) { if (ratio > 0) elem(b, e, c.target, el, e.s.atk * ratio); } });
/** Status on every attack hit. */
const onHitStatus = (key, dur, value) => ({ dealt(c, b, e) { if (dur > 0) b.applyStatus(c.target, key, { duration: dur, source: e, value }); } });
/** Attack hits every targetable ally within radius r (+ optional extra selector). */
const hitAllInRadius = (r) => ({ before(c, b, e) { const l = targetsNear(b, e, r); if (l.length) c.targets = byPriority(e, l); } });
/**
* 抵抗 (ba.buffres, the engine `resist` status: control durations halved, 麻痹 loses 1 stack every 5 s) + optional
* immunities. Value from the talent's `Buff.one_minus_status_resistance` when present (−0.5 ⇒ half).
*/
const resist = (immune = []) => ({
spawn(b, e, a, ab) {
const v = -(T(ab, 'Buff.one_minus_status_resistance') ?? -0.5);
b.applyStatus(e, 'resist', { source: e, value: v > 0 ? Math.min(1, v) : 0.5 });
if (immune.length) ab.immune = new Set([...(ab.immune || []), ...immune]);
},
});
const immuneTo = (...keys) => ({ spawn(b, e, a, ab) { ab.immune = new Set([...(ab.immune || []), ...keys]); } });
/** Every Nth attack: status on the targets ("攻击2次后,下一次攻击会晕眩目标"). */
const nthAttackStatus = (n, key, dur, sil = true) => ({
sil,
spawn(b, e, a) { a.n = 0; },
attack(c, b, e, a) {
a.n++;
if (a.n % n) return;
for (const t of c.targets) if (t.alive && dur > 0) b.applyStatus(t, key, { duration: dur, source: e });
},
});
/** Every Nth attack deals ×scale (+ optional element). */
const nthAttackPower = (n, scale, el = null, ratio = 0) => ({
spawn(b, e, a) { a.n = 0; a.power = false; },
before(c, b, e, a) { a.power = (a.n + 1) % n === 0; },
hitOut(c, b, e, a) { if (a.power && c.dmg.isAttack) c.dmg.amount *= scale; },
dealt(c, b, e, a) { if (a.power && el && ratio > 0) elem(b, e, c.target, el, e.s.atk * ratio); },
attack(c, b, e, a) { a.n++; },
});
/** Aura on other enemies within r: timed buff refreshed every iv s. */
const enemyAura = (r, key, mods, sil = true, iv = 0.5) => ({
sil, iv,
tick(b, e) { for (const o of b.enemiesInRadius(e.x, e.y, r)) if (o !== e) auraBuff(b, o, key, iv, mods); },
});
/** Aura on allies within r. */
const allyAura = (r, key, mods, sil = true, iv = 0.5, flags = null) => ({
sil, iv,
tick(b, e) { for (const u of b.alliesInRadius(e.x, e.y, r)) auraBuff(b, u, key, iv, mods, flags, true); },
});
/** Self ATK buff once HP first drops below ratio. */
const lowHpBuff = (ratio, mods, key = 'ab:lowhp') => ({
taken(c, b, e, a) {
if (a.on || !(e.hpRatio < ratio)) return;
a.on = true;
b.addBuff(e, { key, mods, persist: true, visible: true });
},
});
/** Death spawn ("被击倒后生成N个<X>"). */
const deathSpawn = (key, cnt, extra = {}) => ({
death(c, b, e) { if (c.reason === 'killed' && key && cnt > 0) spawnChildren(b, e, key, cnt, extra); },
});
/** Death explosion on allies within r. */
const deathBoom = ({ scale, type = 'phys', r = BOOM_RADIUS, status = null, sil = true, cond = null }) => ({
sil,
death(c, b, e, a) {
if (c.reason !== 'killed' || (cond && !cond(e, a))) return;
const atk = e.s.atk;
b.fx('explode', { x: e.x, y: e.y, r, kind: 'deathBoom', id: e.id });
for (const u of b.alliesInRadius(e.x, e.y, r)) {
if (scale > 0) hurt(b, e, u, atk * scale, type);
if (status && u.alive) b.applyStatus(u, status.key, { duration: status.dur, source: e, value: status.value });
}
},
});
/** 折射: RES +v (and optional max HP +hpPct) while not silenced. */
const refraction = (res, hpPct = 0) => ({
silAware: true, // SILENCE-flagged: silence switches it off in tick()
tick(b, e, a) {
const on = !e.s.flags.silence;
if (on === a.on) return;
a.on = on;
if (on) b.addBuff(e, { key: 'ab:refraction', mods: hpPct ? { resFlat: res, hpPct } : { resFlat: res }, persist: true, visible: true });
else b.removeBuff(e, 'ab:refraction');
},
});
/** 近地悬浮: unblockable, immune to melee attacks. */
const float = () => ({
spawn(b, e, a, ab) { ab.float = true; stOf(b).floats++; b.addBuff(e, { key: 'ab:float', flags: { unblockable: true }, persist: true }); },
hitIn(c, b, e) { const s = c.source; if (c.dmg.isAttack && s && s.side === 'ally' && s.profile && s.profile.attack !== 'ranged') c.dmg.cancel = true; },
death(c, b) { stOf(b).floats = Math.max(0, stOf(b).floats - 1); },
});
/** "生命值首次降至一半以下时,在数秒内陷入恐惧" (SelfFear). */
const selfFear = (ab) => ({
taken(c, b, e, a) {
if (a.done || !(e.hpRatio < 0.5)) return;
a.done = true;
const fear = T(ab, 'SelfFear.fear') ?? 0;
if (fear > 0) b.applyStatus(e, 'fear', { duration: fear, source: e });
// PRTS “萨科塔之翼/之眼”: "在5s内移动速度最终提升至150%" — move_speed 1.5 is the final multiplier
const ms = T(ab, 'SelfFear.move_speed') ?? 0, sd = T(ab, 'SelfFear.speed_duration') ?? 0;
if (ms > 0 && sd > 0) b.addBuff(e, { key: 'ab:fearRun', duration: sd, mods: { moveMul: ms }, flags: { unblockable: true } });
},
});
/** Prisoners (禁锢 → 解放). */
function prisoner(ab, { freeAll = false } = {}) {
const t = (k) => T(ab, k) ?? 0;
const liberate = (b, e, a) => {
if (a.free || !e.alive) return;
a.free = true;
b.removeBuff(e, 'ab:confined');
b.addBuff(e, { key: 'ab:liberty', persist: true, visible: true, mods: {
atkPct: t('liberty.atk'), defIgnorePct: t('liberty.def_penetrate'), resFlat: t('liberty.magic_resistance'), hpRegen: t('liberty.hp_recovery_per_sec') } });
b.fx('liberate', { x: e.x, y: e.y, id: e.id });
if (freeAll && !stOf(b).freedAll) {
// "第一次解放时同时解放全场敌人"
stOf(b).freedAll = true;
for (const o of b.aliveEnemies()) for (const x of (o.mem.ab ? o.mem.ab.list : [])) if (x.liberate && !x.free) x.liberate(b, o, x);
}
};
return {
liberate,
spawn(b, e, a) {
a.n = 0; a.free = false;
b.addBuff(e, { key: 'ab:confined', persist: true, visible: true, mods: { aspd: t('confinement.attack_speed'), defFlat: t('confinement.def') } });
},
attack(c, b, e, a) { if (!a.free && ++a.n >= (T(ab, 'confinement.times') ?? 4)) liberate(b, e, a); },
};
}
/** Free every confined prisoner on the field (重犯's first liberation, 杰斯顿's killer form). */
function freeAllPrisoners(b) {
for (const o of b.aliveEnemies()) for (const x of (o.mem.ab ? o.mem.ab.list : [])) if (x.liberate && !x.free) x.liberate(b, o, x);
}
/**
* "首次被击倒后重生 / 进入第二形态": the first knock-out is hidden (kill credit and bounty wait for the real death) and starts
* a rebirth of `dur` s — invulnerable, untargetable, released by its blocker, inert (no move / attack / skill).
* `onKo(b, e, a)` runs at once (self-destruct, freeing prisoners …), `during(b, e, a, elapsed)` every tick of it; then the
* enemy stands up with `hpRatio` of its max HP, `onReborn(b, e, a)` switches the form and `invincible` s of 无敌 follow.
* `a.state`: undefined → 'reborn' → 'form2'.
*/
function reborn({ dur = 0, hpRatio = 1, invincible = 0, onKo = null, during = null, onReborn = null, key = 'ab:reborn' } = {}) {
const finish = (b, e, a) => {
if (!e.alive || a.state !== 'reborn') return;
a.state = 'form2';
b.removeBuff(e, key);
e.profile.noAttack = a.noAtk;
if (onReborn) safe(b, e, () => onReborn(b, e, a));
e.hp = Math.max(1, e.s.maxHp * Math.max(0, Math.min(1, hpRatio)));
if (invincible > 0) b.addBuff(e, { key: `${key}:inv`, duration: invincible, visible: true, flags: { invulnerable: true } });
if (e.route) e.route.pts = null;
e.atkCd = 0;
b.fx('revive', { x: e.x, y: e.y, id: e.id, kind: 'reborn' });
};
return {
killed(c, b, e, a) {
if (a.state) return false;
a.state = 'reborn';
a.t0 = b.time;
a.noAtk = e.profile.noAttack;
e.hp = Math.min(1, e.s.maxHp);
if (onKo) safe(b, e, () => onKo(b, e, a));
if (!(dur > 0)) { finish(b, e, a); return true; }
e.profile.noAttack = true;
b.addBuff(e, {
key, duration: dur, visible: true, persist: true,
flags: { invulnerable: true, untargetable: true, unblockable: true, noMove: true, stun: true },
onTick: during ? ({ battle }) => during(battle, e, a, battle.time - a.t0) : null,
onExpire: ({ battle }) => finish(battle, e, a),
});
b.fx('telegraph', { x: e.x, y: e.y, r: 1, dur, kind: 'reborn', id: e.id });
return true;
},
};
}
/**
* A normal-attack loop for enemies whose data never attacks (dmgType none) once content arms them (转译基底 forms):
* the blocker first, else (ranged) the engine priority inside `radius`; `hits` instances of ATK per attack.
* Disabled until `a.on`.
*/
function attackLoop({ type = 'phys', melee = true, radius = 0, hits = 1 } = {}) {
return {
spawn(b, e, a) { a.cd = 0; a.on = false; },
tick(b, e, a, dt) {
if (!a.on || !e.alive || e.hidden) return;
const f = e.s.flags;
if (f.stun) return;
if (a.cd > 0) { a.cd -= dt; if (a.cd > 1e-9) return; }
if (f.disarm || f.fear || (f.tremble && e.blockedBy)) return;
let t = e.blockedBy && e.blockedBy.alive ? e.blockedBy : null;
if (!t && !melee && radius > 0) t = byPriority(e, targetsNear(b, e, radius))[0] ?? null;
if (!t) return;
a.cd = e.s.interval;
e.lastAttackAt = b.time;
e.stats.attacks++;
if (!e.blockedBy) e.pauseUntil = b.time + ATTACK_PAUSE;
if (!melee && t !== e.blockedBy) b.fx('beam', { x: e.x, y: e.y, from: e.id, to: t.id, kind: 'enemyShot' });
for (let i = 0; i < hits && t.alive; i++) b.dealDamage(e, t, { amount: e.s.atk, type, isAttack: true });
},
};
}
/** 正面减伤: phys/arts damage from allies in front of the enemy (`a.facing` side) × (1 − cut); `face` updates a.facing. */
function frontGuard(cut, face) {
return {
spawn(b, e, a) { a.facing = -1; a.px = e.x; },
tick(b, e, a) { face(b, e, a); },
hitIn(c, b, e, a) {
const s = c.source, ty = c.dmg.type;
if (!s || s.side !== 'ally' || (ty !== 'phys' && ty !== 'arts') || !(cut > 0)) return;
if ((s.x - e.x) * a.facing > 1e-6) c.dmg.mul *= Math.max(0, 1 - cut);
},
};
}
/** Faces its walking direction (horizontal component; keeps the last one while standing). */
const faceMove = (b, e, a) => { const dx = e.x - a.px; if (Math.abs(dx) > 1e-6) a.facing = dx > 0 ? 1 : -1; a.px = e.x; };
/** "始终面向我方干员数量较多的方向": the side with more deployed operators (ties keep the current facing). */
const faceCrowd = (b, e, a) => {
let l = 0, r = 0;
for (const u of b.allies()) { if (u.kind !== 'op') continue; if (u.x > e.x + 1e-6) r++; else if (u.x < e.x - 1e-6) l++; }
if (r !== l) a.facing = r > l ? 1 : -1;
};
/**
* 失衡 (unbalanced movement): being pushed / pulled by operators. battle.displace() has no hook, so it is detected as
* movement beyond the enemy's own route speed between two ticks (teleport legs excluded). `onMove(b, e, a, tiles)`.
*/
function unbalanced(onMove) {
return {
tick(b, e, a, dt) {
const px = a.px, py = a.py, hid = a.hid;
a.px = e.x; a.py = e.y; a.hid = e.hidden;
if (px == null || hid || e.hidden) return;
const own = e.s.moveSpeed * MOVE_SCALE * dt * 1.5 + 1e-3;
const extra = Math.hypot(e.x - px, e.y - py) - own;
a.lx = px; a.ly = py; // where the move started (direction for onMove)
if (extra > 0.05) onMove(b, e, a, extra);
},
};
}
/** An arts-only barrier ("吸收法术伤害的屏障") of `amount`; `a.left` = what is left, `refresh(b, e)` restores it. */
function artsBarrier(amount, { key = 'ab:artsBarrier', whileUp = null } = {}) {
return {
refresh(b, e, a) {
a.left = amount;
b.fx('phase', { x: e.x, y: e.y, id: e.id, kind: 'artsBarrier', value: amount });
if (whileUp) b.addBuff(e, { key, persist: true, visible: true, mods: whileUp });
},
spawn(b, e, a) { if (amount > 0) a.refresh(b, e, a); },
hitIn(c, b, e, a) {
if (!(a.left > 0)) return;
a.left -= absorbArts(c, a.left);
if (a.left > 1e-6) return;
a.left = 0;
b.fx('shieldBreak', { x: e.x, y: e.y, id: e.id });
// the bonus it grants ends once the overflow of the breaking hit has landed (never before: an HP bonus removed
// first would rescale HP and make the overflow count twice)
if (c.dmg.cancel) b.removeBuff(e, key); else a.breaking = true;
},
taken(c, b, e, a) { if (a.breaking) { a.breaking = false; b.removeBuff(e, key); } },
};
}
/** Revive as a stealthed 频次 husk (逐火 embers, 假想敌:再生). */
function husk({ hits, delay, stealthy = true, onHusk = null, key = 'ab:ember' }) {
return {
killed(c, b, e, a, ab) {
if (a.state === 'husk' || !(hits > 0) || !(delay > 0)) return false;
a.state = 'husk';
a.noAtk = e.profile.noAttack;
a.max = a.max ?? e.base.maxHp;
e.profile.noAttack = true;
setHits(e, hits);
hitCount(b, e, true);
b.addBuff(e, { key, visible: true, flags: { noMove: true, unblockable: true, ...(stealthy ? { stealth: true } : {}) } });
b.fx('ember', { x: e.x, y: e.y, id: e.id, hits, dur: delay });
if (onHusk) onHusk(b, e);
b.after(delay, () => {
if (!e.alive || a.state !== 'husk') return;
a.state = null;
hitCount(b, e, false);
e.base.maxHp = a.max;
e.markDirty();
void e.s;
e.hp = e.s.maxHp;
e.profile.noAttack = a.noAtk;
b.removeBuff(e, key);
if (e.route) e.route.pts = null;
b.fx('revive', { x: e.x, y: e.y, id: e.id });
}, { owner: e });
return true;
},
};
}
/** 守墓石像: first knock-out → statue (DEF/RES up, immobile) → reborn as a flyer. */
function statue(ab) {
const dur = T(ab, 'stone.duration') ?? 0;
return {
killed(c, b, e, a) {
if (a.done || !(dur > 0)) return false;
a.done = true;
a.noAtk = e.profile.noAttack;
e.profile.noAttack = true;
e.hp = e.s.maxHp;
b.fx('stone', { x: e.x, y: e.y, id: e.id, dur });
b.addBuff(e, {
key: 'ab:stone', duration: dur, visible: true, flags: { noMove: true },
mods: { defFlat: T(ab, 'stone.def') ?? 0, resFlat: T(ab, 'stone.magic_resistance') ?? 0 },
onExpire: ({ battle }) => {
if (!e.alive) return;
e.motion = 'FLY';
e.profile.noAttack = a.noAtk;
e.hp = e.s.maxHp;
battle.addBuff(e, { key: 'ab:liftoff', duration: 0.2, flags: { unblockable: true } }); // releases its blocker
if (e.route) e.route.pts = null;
battle.fx('revive', { x: e.x, y: e.y, id: e.id, kind: 'fly' });
},
});
return true;
},
};
}
/** Bleeding on hit (逐腐兽): arts damage per second, removed by healing. */
function bleed(ab) {
const dmg = T(ab, 'Bleeding.attack@bleeding_damage') ?? 0, dur = T(ab, 'Bleeding.attack@duration') ?? 0;
return {
dealt(c, b, e) {
if (!(dmg > 0 && dur > 0)) return;
b.addBuff(c.target, {
key: 'ab:bleed', duration: dur, refresh: 'replace', interval: 1, visible: true,
onTick: ({ battle, unit }) => battle.dealDamage(e, unit, { amount: dmg, type: 'arts', canDodge: false, tags: ['enemyAbility', 'bleed'] }),
});
},
};
}
/** 污染秽蚀 zone: HP loss per second, `low` on ground tiles, `high` on elevated tiles. */
function pollution(b, src, x, y, r, life, low, high) {
zone(b, { x, y, r, life, iv: 1, kind: 'pollution', tick(units) {
for (const u of units) { const v = u.ground ? low : high; if (v > 0) b.loseHp(u, v, { source: src }); }
} });
}
/** Damage zone (arts per tick). */
function dmgZone(b, src, x, y, r, life, iv, amount, type = 'arts', kind = 'zone', el = null, elAmount = 0) {
zone(b, { x, y, r, life, iv, kind, tick(units) {
for (const u of units) { hurt(b, src, u, amount, type); if (el && elAmount > 0) elem(b, src, u, el, elAmount); }
} });
}
/** A cooldown skill ability (`id` labels it for tests / other abilities). */
const skill = (s, fire, { cond = null, sil = false, cd = null, icd = null, id = null } = {}) => (s || cd != null) && fire ? ({
id: id ?? null, sil, cd: cd ?? s.cd, icd: icd ?? s.icd, cond, fire,
}) : null;
/** Blink past the blocker along the path (弑君者 / 卢西恩). Returns the start position. */
export function blinkForward(b, e, dist) {
const from = { x: e.x, y: e.y };
const R = e.route;
let left = dist;
if (R && R.pts && R.ptIdx < R.pts.length) {
let i = R.ptIdx;
while (left > 1e-9 && i < R.pts.length) {
const p = R.pts[i];
const d = Math.hypot(p.x - e.x, p.y - e.y);
if (d <= left) { e.x = p.x; e.y = p.y; left -= d; i++; } else { stepToward(e, p.x, p.y, left); left = 0; }
}
} else if (R && R.legs && R.legs[R.legIdx] && R.legs[R.legIdx].r != null) {
const L = R.legs[R.legIdx];
stepToward(e, L.c, L.r, dist);
}
// never end inside a wall
if (!b.grid.groundPassable(Math.round(e.y), Math.round(e.x)) && e.motion !== 'FLY') { e.x = from.x; e.y = from.y; return null; }
b.addBuff(e, { key: 'ab:blink', duration: 1, flags: { invulnerable: true, unblockable: true } });
if (R) R.pts = null;
b.fx('blink', { x: e.x, y: e.y, id: e.id, fx: from.x, fy: from.y });
return from;
}
// ---------------------------------------------------------------------------------------------------------------
// kits by archetype (functions reused by several keys)
const kitEp = (el, ...keys) => (ab) => [ep(el, T(ab, ...keys) ?? 0)];
const kitStealth = () => [stealth()];
const kitTimes = (artsOnly = false) => () => [times(artsOnly), unblockable()];
const kitRefraction = (ab) => [refraction(T(ab, 'refracting.magic_resistance', 'Refracting.magic_resistance') ?? 0)];
const kitPrisoner = (freeAll = false) => (ab) => [prisoner(ab, { freeAll })];
const kitDeathSpawn = (extra = []) => (ab) => [deathSpawn(ab.tS['DeadSpawn.enemy_key'], T(ab, 'DeadSpawn.cnt') ?? 0), ...extra];
const kitStun3 = (ab) => [nthAttackStatus(nthOf(ab.sk.stuncombat), 'stun', (ab.sk.stuncombat && ab.sk.stuncombat.bb.stun) || 0, true)];
const kitSelfFear = (ab) => [selfFear(ab)];
const kitEmber = (ab) => [husk({ hits: T(ab, 'Revive[Trigger].prop_max_hp'), delay: T(ab, 'Revive[Trigger].interval') })];
const kitPolluted = (ab) => [{
death(c, b, e) {
if (c.reason !== 'killed') return;
pollution(b, e, e.x, e.y, T(ab, 'PollutedDie.projectile_range') ?? 1, T(ab, 'PollutedDie.projectile_life_time') ?? 0,
T(ab, 'PollutedDie.polluted_damage_low') ?? 0, T(ab, 'PollutedDie.polluted_damage_high') ?? 0);
},
}];
function kitPhalanx(ab, e) {
const def = T(ab, 'auraDefup.def') ?? 0;
const r = (e.def.raw && e.def.raw.stats && e.def.raw.stats.rawRangeRadius) || 1.5;
return [...kitRefraction(ab), {
iv: 0.5,
tick(b, e2) {
let n = 0;
for (const o of b.enemiesInRadius(e2.x, e2.y, r)) if (o !== e2 && o.defId === e2.defId) n++;
if (n > 0) auraBuff(b, e2, 'ab:phalanx', 0.5, { defFlat: def * n });
},
}];
}
function kitBlades(ab) {
const cnt = T(ab, 'DeadSpawn.cnt') ?? 0, add = T(ab, 'DeadSpawn.cnt_add') ?? 0, atk = T(ab, 'Atkup.atk', 'AtkUp.atk') ?? 0;
const key = ab.tS['DeadSpawn.enemy_key'];
return [{
spawn(b, e, a) { a.used = 0; },
attack(c, b, e, a) {
if (a.used >= cnt) return;
a.used++;
b.addBuff(e, { key: 'ab:blade', refresh: 'stack', stacks: 1, maxStacks: Math.max(1, cnt), mods: { atkPct: atk }, persist: true, visible: true });
},
death(c, b, e, a) { if (c.reason === 'killed' && key) spawnChildren(b, e, key, Math.max(1, cnt + add * a.used)); },
}];
}
function kitDeepsea(ab, { swim = false, drown = false }) {
return [{
iv: 0.25,
spawn(b, e) { if (swim) e.mem.immuneDrown = true; },
tick(b, e, a, dt) {
const wet = onTerrain(b, e, 'deepsea');
if (swim) {
if (wet && !a.wet) b.addBuff(e, { key: 'ab:swim', mods: { atkPct: T(ab, 'Swim.atk') ?? 0 }, flags: { stealth: true }, visible: true });
if (!wet && a.wet) b.removeBuff(e, 'ab:swim');
if (wet) b.removeBuff(e, 'terrain:deepsea'); // 免疫水蚀: drop devices.js' deep-water debuff
}
if (drown && wet) { const v = T(ab, 'Drown.damage') ?? 0; if (v > 0) b.loseHp(e, v * dt, { source: null }); }
a.wet = wet;
},
hitIn(c, b, e, a) { if (swim && a.wet && c.dmg.tags && c.dmg.tags.some((t) => t === 'terrain' || t === 'deepsea' || t === 'drown')) c.dmg.cancel = true; },
}];
}
/** 源石污染区 (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 >= 1 - 1e-9;
if (pulse) a.acc -= 1;
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);
if (pulse && dmg > 0) b.loseHp(u, dmg, { source: e });
}
},
}];
}
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);
},
}];
}
function kitTidmag(ab) {
return [ep('erosion', T(ab, 'EpDamage.attack@ep_damage_ratio') ?? 0), {
before(c, b, e) {
const t0 = c.targets[0];
if (!t0) return;
const l = c.targets.slice();
for (const u of alliesInTiles(b, t0.tileR, t0.tileC, 'plus', 1)) if (!l.includes(u)) l.push(u);
c.targets = l;
},
}];
}
function kitDsubrl(ab, e) {
return [hitAllInRadius(e.base.rangeRadius || 1.6), ep('neural', T(ab, 'EpDamage.ep_damage_ratio') ?? 0), resist(['sluggish'])];
}
function kitTank(ab) {
const every = (T(ab, 'Empty.sp') ?? 2) + 1, melee = T(ab, 'Empty.attack@chuang_atk_scale') ?? 1;
const s = ab.sk.PollutedRangedAtk ? ab.sk.PollutedRangedAtk.bb : {};
return [{
spawn(b, e, a) { a.n = 0; },
before(c, b, e) { c.targets = c.targets.filter((u) => u.ground); }, // 只攻击地面单位
hitOut(c, b, e) { if (c.dmg.isAttack && c.target === e.blockedBy) c.dmg.amount *= melee; }, // 近战攻击造成更高伤害
attack(c, b, e, a) {
a.n++;
if (a.n % every) return;
const t = c.targets[0];
if (t) pollution(b, e, t.x, t.y, TANK_ZONE_RADIUS, s.projectile_life_time ?? 0, s.polluted_damage_low ?? 0, s.polluted_damage_high ?? 0);
},
}];
}
function kitDeathEye(ab, e) {
const s = ab.sk.DeathEye;
const r = e.base.rangeRadius || 2.5;
return [ep('apoptosis', T(ab, 'empty.attack@ep_damage_ratio') ?? 0), skill(s, (b, e2, a) => {
const t = byPriority(e2, targetsNear(b, e2, r))[0];
if (!t) return;
const dur = s.bb.hit_duration ?? 0;
b.addBuff(e2, { key: 'ab:channel', duration: dur, flags: { disarm: true } });
b.fx('beam', { x: e2.x, y: e2.y, from: e2.id, to: t.id, kind: 'deathEye', dur });
let n = 0;
const h = b.every(1, () => {
if (!e2.alive) { h.cancel(); return; }
// a stun / silence (SILENCE-flagged ability) or the locked target's fall interrupts the channel: no burst
if (e2.s.flags.stun || e2.s.flags.silence || !t.alive) { h.cancel(); b.removeBuff(e2, 'ab:channel'); b.fx('beam', { x: e2.x, y: e2.y, from: e2.id, to: t.id, kind: 'deathEyeEnd' }); return; }
hurt(b, e2, t, e2.s.atk * (s.bb.atk_scale ?? 0), 'arts');
if (++n >= dur) {
h.cancel();
b.fx('explode', { x: t.x, y: t.y, r: 1, kind: 'apoptosis' });
for (const u of b.alliesInRadius(t.x, t.y, 1)) elem(b, e2, u, 'apoptosis', e2.s.atk * (s.bb.ep_damage_ratio ?? 0));
}
}, { owner: e2 });
}, { sil: true, cond: (b, e2) => targetsNear(b, e2, r).length > 0 })];
}
function kitInvisShield(ab, e) {
const s = ab.sk.InvisibleShield;
const r = (e.def.raw && e.def.raw.stats && e.def.raw.stats.rawRangeRadius) || 2;
return [...kitDeathSpawn()(ab), skill(s, (b, e2) => {
const dur = s.bb.duration ?? T(ab, 'InvisibleShield.duration') ?? 0;
b.fx('telegraph', { x: e2.x, y: e2.y, r, kind: 'invisShield', id: e2.id });
for (const o of b.enemiesInRadius(e2.x, e2.y, r)) if (o !== e2 && dur > 0) b.addBuff(o, { key: 'ab:veiled', duration: dur, flags: { stealth: true }, visible: true });
})];
}
function kitCrossbow(ab) {
const s = ab.sk.CrossAttack;
const aligned = (b, e) => b.allies().filter((u) => canTargetAlly(e, u, true) && (Math.abs(u.y - e.y) < 0.5 || Math.abs(u.x - e.x) < 0.5) && Math.hypot(u.x - e.x, u.y - e.y) <= CROSS_REACH);
return [stealth(), skill(s, (b, e) => {
const t = aligned(b, e).sort((p, q) => Math.hypot(p.x - e.x, p.y - e.y) - Math.hypot(q.x - e.x, q.y - e.y))[0];
if (!t) return;
const bb = s.bb;
if (bb.duration > 0) b.addBuff(e, { key: 'ab:revealed', duration: bb.duration, flags: { reveal: true } });
b.addProjectile({ from: e, target: t, speed: 12, visual: 'enemy', source: e, onHit: (c) => {
if (!c.target || !c.target.alive) return;
hurt(b, e, c.target, e.s.atk * (bb.atk_scale ?? 1), 'arts');
if (bb.stun > 0 && c.target.alive) b.applyStatus(c.target, 'stun', { duration: bb.stun, source: e });
} });
}, { sil: true, cond: (b, e) => aligned(b, e).length > 0 })];
}
function kitShadowKiller(ab) {
const scale = (ab.sk.InvisibleCombat && ab.sk.InvisibleCombat.bb.atk_scale) || 1;
return [stealth(), {
blocked(c, b, e, a) { a.power = true; }, // 隐匿效果失效 (blocked) → next attack ×scale
hitOut(c, b, e, a) { if (a.power && c.dmg.isAttack) c.dmg.amount *= scale; },
attack(c, b, e, a) { a.power = false; },
}];
}
function kitJazz(ab, e) {
const s = ab.sk.fire;
const r = (e.def.raw && e.def.raw.stats && e.def.raw.stats.rawRangeRadius) || 2.5;
const revealed = (u) => !!(u.blockedBy || u.s.flags.reveal);
return [stealth(), {
spawn(b, e2) { e2.profile.noAttack = true; },
tick(b, e2) { e2.profile.noAttack = !revealed(e2); }, // 平时不攻击,失去隐匿时反击
}, s ? {
sil: true, cd: s.cd, icd: s.icd, cond: (b, e2) => revealed(e2),
fire(b, e2) {
const dur = s.bb['enemy_cnvsax[cd].duration'] ?? 0, iv = s.bb.hit_interval ?? 0.5;
let left = dur;
b.fx('zone', { x: e2.x, y: e2.y, r, dur, kind: 'jazzFire', id: e2.id });
const h = b.every(iv, () => {
if (!e2.alive || e2.s.flags.silence) { h.cancel(); return; }
for (const u of b.alliesInRadius(e2.x, e2.y, r)) { hurt(b, e2, u, e2.s.atk * (s.bb.atk_scale ?? 0), 'arts'); elem(b, e2, u, 'burn', e2.s.atk * (s.bb.ep_damage_ratio ?? 0)); }
left -= iv;
if (left <= 1e-6) h.cancel();
}, { owner: e2 });
},
} : null];
}
function kitShadowBlade(ab) {
const bat = ab.t['traitAbility.base_attack_time'] ?? 0;
return [stealth(), {
iv: 0.5,
tick(b, e) {
const partner = b.enemiesInRadius(e.x, e.y, 3).find((o) => /enemy_1174_duholy/.test(o.defId));
const r = partner && partner.mem.ab ? (T(partner.mem.ab, 'traitAbility.range_radius') ?? 1.1) : 1.1;
if (partner && Math.hypot(partner.x - e.x, partner.y - e.y) <= r + 1e-9 && e.base.bat > 0) auraBuff(b, e, 'ab:shadowSync', 0.5, { batPct: bat / e.base.bat });
},
}];
}
function kitHolyGuard(ab) {
const r = T(ab, 'traitAbility.range_radius') ?? 1.1, aspd = T(ab, 'traitAbility.attack_speed') ?? 0;
return [...kitRefraction(ab), {
iv: 0.5,
tick(b, e) {
const on = b.enemiesInRadius(e.x, e.y, r).some((o) => /enemy_1175_dushdo/.test(o.defId));
if (!on) return;
for (const u of b.alliesInRadius(e.x, e.y, r)) auraBuff(b, u, 'ab:forceField', 0.5, { aspd }, null, true);
},
}];
}
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, first = T(ab, 'firstattack.atk_scale') ?? 0;
return [{
iv,
spawn(b, e, a) { a.n = 0; },
tick(b, e, a) {
if (!e.moving || e.blockedBy || a.n >= max) return;
a.n++;
b.addBuff(e, { key: 'ab:rush', mods: { moveMul: 1 + ms * a.n }, persist: true });
},
blocked(c, b, e, a) { a.charge = a.n; },
hitOut(c, b, e, a) {
if (!c.dmg.isAttack || !(a.charge > 0) || !(max > 0)) return;
c.dmg.amount *= 1 + (first / 100) * (a.charge / max); // 被阻挡后的首次攻击: extra damage from the built-up speed
},
attack(c, b, e, a) { if (a.charge > 0) { a.charge = 0; a.n = 0; b.removeBuff(e, 'ab:rush'); } },
}];
}
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');
},
}];
}
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); } }];
}
function kitExposeOnDeath(ab) {
const dur = T(ab, 'Expose.weak[limit]') ?? 0, scale = T(ab, 'Expose.damage_scale') ?? 1, r = T(ab, 'Expose.range_radius') ?? 1;
return [{ sil: true, death(c, b, e) { if (c.reason !== 'killed') return; b.fx('explode', { x: e.x, y: e.y, r, kind: 'expose' }); for (const u of b.alliesInRadius(e.x, e.y, r)) expose(b, u, dur, scale); } }];
}
function kitParasite(ab) {
const scale = T(ab, '1.atk_scale') ?? 0, elMul = T(ab, '1.ep_damage_scale') ?? 1;
return [taunt(-1), {
iv: 0.2,
tick(b, e, a) {
const host = e.blockedBy && e.blockedBy.alive ? e.blockedBy : null;
if (a.host && a.host !== host) { b.removeBuff(a.host, 'ab:parasite'); a.host = null; }
if (!host || a.host === host) return;
a.host = host;
b.fx('beam', { x: e.x, y: e.y, from: e.id, to: host.id, kind: 'parasite' });
b.addBuff(host, {
key: 'ab:parasite', visible: true, interval: 1, mods: { elemTakenMul: elMul }, data: { src: e, spread: e.s.atk * elMul },
onTick: ({ battle, unit }) => { if (e.alive) battle.dealDamage(e, unit, { amount: e.s.atk * scale, type: 'arts', canDodge: false, tags: ['enemyAbility', 'parasite'] }); },
});
},
death(c, b, e, a) { if (a.host) b.removeBuff(a.host, 'ab:parasite'); },
}];
}
function kitBoneSpike() {
return [stealth(), {
before(c, b, e) {
if (e.blockedBy || e.s.flags.reveal) return; // 隐匿状态下同时攻击数个目标
const l = byPriority(e, targetsNear(b, e, e.base.rangeRadius));
if (l.length) c.targets = l.slice(0, ACBUNN_TARGETS);
},
}];
}
/**
* 假想敌:黑云 (PRTS): 抓取 (KillOthers, cd 10 / icd 5) "仅半径3.75范围内存在未持有【延迟吞噬】的敌方飞行普通单位时可触发:
* 选择至多3名满足上述条件的目标,短暂延迟后对其施加4秒【延迟吞噬】:束缚,效果结束时令给予方+1SP,随后强制击杀受予方 ※技能
* 持续4秒,期间持有束缚" (the nearest prey first [ASSUMED]); 全弹发射 (FireWeapon, cd 10 / icd 5, SP cost 1) "仅全场范围内存在
* 我方单位时可触发:清空自身SP,进行一次多连击,每击选择全场范围内的1名随机我方单位,对其造成攻击力130%的物理伤害 ※多连击的
* 连击次数等于本技能消耗的SP数量" — SP (技力上限 spData.maxSp 3) only comes from the grabs.
*/
function kitBlackCloud(ab, e) {
const k = ab.sk.KillOthers, f = ab.sk.FireWeapon;
const maxAmmo = Math.max(1, num(e.def.raw && e.def.raw.sp && e.def.raw.sp.maxSp, 3));
const grabR = (k ? num(k.bb.range_radius, 1.5) : 1.5) * GRAB_RADIUS_SCALE;
const dur = k ? num(k.bb.duration, 4) : 4;
const isPrey = (b, e2, o) => o !== e2 && o.alive && !o.hidden && o.isFlying && o.def.rank === 'NORMAL' && !o.isBoss && !o.findBuff('ab:devoured');
const prey = (b, e2) => b.enemiesInRadius(e2.x, e2.y, grabR).filter((o) => isPrey(b, e2, o));
return [
skill(k, (b, e2) => {
const list = prey(b, e2).sort((p, q) => Math.hypot(p.x - e2.x, p.y - e2.y) - Math.hypot(q.x - e2.x, q.y - e2.y)).slice(0, GRAB_MAX_PREY);
if (!list.length) return;
b.addBuff(e2, { key: 'ab:grabbing', duration: dur, flags: { bind: true, noMove: true } }); // 技能持续4秒,期间持有束缚
b.fx('beam', { x: e2.x, y: e2.y, from: e2.id, to: list[0].id, kind: 'devour' });
b.after(GRAB_DELAY, () => {
if (!e2.alive) return;
for (const o of list) {
if (!o.alive) continue;
b.addBuff(o, { key: 'ab:devoured', duration: dur, visible: true, flags: { bind: true, noMove: true, disarm: true } });
b.after(dur, () => {
if (!o.alive || !e2.alive) return;
e2.mem.ab.ammo = Math.min(maxAmmo, (e2.mem.ab.ammo ?? 0) + num(k.bb.sp, 1));
b.kill(o, null);
}, { owner: e2 });
}
}, { owner: e2 });
}, { sil: true, cond: (b, e2) => prey(b, e2).length > 0 }),
skill(f, (b, e2) => {
const hits = Math.floor(e2.mem.ab.ammo ?? 0);
if (!(hits > 0)) return;
e2.mem.ab.ammo = 0;
for (let i = 0; i < hits; i++) {
const t = b.rng.pick(allTargets(b, e2));
if (!t) break;
b.fx('beam', { x: e2.x, y: e2.y, from: e2.id, to: t.id, kind: 'fireWeapon' });
hurt(b, e2, t, e2.s.atk * num(f.bb.atk_scale, 1), 'phys');
}
}, { sil: true, cond: (b, e2) => (e2.mem.ab.ammo ?? 0) >= Math.max(1, num(f.sp, 1)) && allTargets(b, e2).length > 0 }),
];
}
function kitRegen(ab) {
const hits = T(ab, 'Revive[Trigger].prop_max_hp'), delay = T(ab, 'Revive[Trigger].interval'), block = T(ab, 'Aura.max_damage_block_cnt') ?? 0;
return [husk({
hits, delay, stealthy: false, key: 'ab:regen',
onHusk(b, e) {
// 被击倒后…使周围一定距离内其他敌人获得可以抵挡物理及法术伤害的护盾
b.fx('telegraph', { x: e.x, y: e.y, r: ACPUPP_AURA_RADIUS, kind: 'regenShield', id: e.id });
for (const o of b.enemiesInRadius(e.x, e.y, ACPUPP_AURA_RADIUS)) if (o !== e && block > 0) { const oab = abOf(b, o); oab.hitShield = Math.max(oab.hitShield, block); }
},
})];
}
function kitSteal(ab) {
const n = T(ab, 'DamageOrBullet.attack@minus_bullet') ?? 1;
return [selfFear(ab), {
// PRTS “萨科塔之眼”: "不会攻击飞行单位" (the 炎佑 is one) — its next ground target in range instead
before(c, b, e) {
if (!c.targets || !c.targets.some((t) => t.isFlying)) return;
const l = targetsNear(b, e, e.base.rangeRadius).filter((t) => !t.isFlying);
c.targets = byPriority(e, l).slice(0, 1);
},
hitOut(c, b, e) {
if (!c.dmg.isAttack) return;
const t = c.target;
// 攻击时尝试夺走目标1发弹药,若无法夺走弹药则造成高额物理伤害
if (t.skill && t.skill.active && t.skill.kind === 'ammo' && t.skill.ammoLeft > 0) {
t.skill.ammoLeft = Math.max(0, t.skill.ammoLeft - n);
if (t.skill.ammoLeft <= 0) t.skill.end('ammo');
} else if ((t.trait && t.trait.ammo) > 0) {
t.trait.ammo = Math.max(0, t.trait.ammo - n);
} else return;
c.dmg.cancel = true;
b.fx('steal', { x: t.x, y: t.y, id: t.id, from: e.id });
},
}];
}
function kitRoar(ab) {
const s = ab.sk.Roar;
return [selfFear(ab), skill(s, (b, e) => {
b.fx('telegraph', { x: e.x, y: e.y, r: 99, kind: 'roar', id: e.id });
for (const u of b.allies()) b.addBuff(u, { key: 'ab:roar', duration: s.bb.duration ?? 0, refresh: 'extend', mods: { aspd: s.bb.attack_speed ?? 0 }, visible: true });
}, { sil: true })];
}
/**
* 暴鸰 (PRTS): "不进行普通攻击"; 投弹 (boomb, cooldown / initCooldown 1) "仅攻击范围内存在我方单位时可触发:对目标及其周围八格
* 的我方单位造成100%物理伤害(对主目标造成物理普通伤害,对溅射目标造成物理溅射伤害,伤害无视迷彩)技能结束后移速最终提升至200%
* ※此技能仅能触发一次,不可沉默" — one bomb on the ranged target (engine priority), then move speed ×boomb.move_speed.
*/
function kitBombd(ab) {
const s = ab.sk.boomb;
const ms = (s && s.bb.move_speed) || 0;
const reach = (e) => e.base.rangeRadius || 2;
return [
{ spawn(b, e) { e.profile.noAttack = true; } },
skill(s, (b, e, a) => {
const t = byPriority(e, targetsNear(b, e, reach(e)))[0];
if (!t) return;
a.cd = Infinity; a.left = Infinity; // 仅能触发一次
b.fx('explode', { x: t.x, y: t.y, r: BOMB_REACH + 0.5, kind: 'bomb', tiles: 'box', id: e.id });
hurt(b, e, t, e.s.atk, 'phys');
for (const u of alliesInTiles(b, t.tileR, t.tileC, 'box', BOMB_REACH)) if (u !== t) hurt(b, e, u, e.s.atk, 'phys', { tags: ['splash'] });
if (ms > 0) b.addBuff(e, { key: 'ab:bombRun', mods: { moveMul: ms }, persist: true }); // 移速最终提升至200%
}, { cond: (b, e) => targetsNear(b, e, reach(e)).length > 0 }),
];
}
/**
* 帝国炮火先兆者 / 中枢先兆者 (PRTS): every normal attack fires a shell at the target's position that lands SHELL_FLIGHT s
* later and deals 100 % of the ATK at launch as physical damage without a source to every ally within SHELL_RADIUS
* (it may miss the target that moved away; camouflage does not matter). The attack itself is the engine's (cooldown,
* pause, 'atk' event of kind 'mortar'); the damage is the shell's (ai.js `profile.deferHit`).
*/
function kitShell() {
return [{
spawn(b, e) { e.profile.deferHit = true; e.profile.shot = 'mortar'; },
attack(c, b, e) {
const atk = e.s.atk * (e.profile.atkScale ?? 1);
for (const t of c.targets) {
const x = t.x, y = t.y;
b.fx('bombardShell', { x, y, id: e.id, r: SHELL_RADIUS, t: SHELL_FLIGHT });
b.after(SHELL_FLIGHT, () => {
b.fx('bombard', { x, y, r: SHELL_RADIUS, kind: 'emppnt' });
for (const u of b.alliesInRadius(x, y, SHELL_RADIUS)) hurt(b, null, u, atk, 'phys', { isSkill: false, tags: ['shell'] });
});
}
},
}];
}
function kitIgnitable(ab) {
return [deathBoom({ scale: T(ab, 'boom.atk_scale') ?? 0, type: 'arts', cond: (e) => !!e.mem.ab.ignited, sil: true })];
}
function kitFlameVine(ab) {
const add = T(ab, 'pow.add_max_atk') ?? 0, time = T(ab, 'pow.time') ?? 0;
const first = T(ab, 'pow.attack@ep_damage_ratio') ?? 0, normal = T(ab, 'pow.attack@ep_damage_ratio_normal') ?? 0;
return [{
iv: 0.5,
spawn(b, e, a) { a.t0 = b.time; a.first = true; },
tick(b, e, a) {
if (!a.first || !(time > 0)) return;
const k = Math.min(1, (b.time - a.t0) / time);
b.addBuff(e, { key: 'ab:pow', mods: { atkPct: add * k }, persist: true });
},
dealt(c, b, e, a) {
const t = c.target;
if (a.first) {
// 首次攻击对目标和周围造成法术伤害和灼燃损伤, then the ATK ramp ends
a.first = false;
b.fx('explode', { x: t.x, y: t.y, r: 1, kind: 'flameVine' });
for (const u of alliesInTiles(b, t.tileR, t.tileC, 'plus', 1)) { if (u !== t) hurt(b, e, u, e.s.atk, 'arts'); elem(b, e, u, 'burn', e.s.atk * first); }
b.removeBuff(e, 'ab:pow');
} else elem(b, e, t, 'burn', e.s.atk * normal);
},
attack(c, b, e) {
// ignites nearby 卷心籽
for (const o of b.enemiesInRadius(e.x, e.y, 1.5)) if (o.defId === 'enemy_10065_ftzlc' && o.mem.ab && !o.mem.ab.ignited) { o.mem.ab.ignited = true; b.fx('ignite', { x: o.x, y: o.y, id: o.id }); }
},
}];
}
function kitDekght(ab) {
const rage = { atkPct: T(ab, 'triggerrage.atk') ?? 0, aspd: T(ab, 'triggerrage.attack_speed') ?? 0, moveMul: 1 + (T(ab, 'triggerrage.move_speed') ?? 0) };
const partnerRage = {
otherDeath(c, b, e, a) {
const u = c.unit;
if (a.raged || !u || u === e || !/enemy_1513_dekght/.test(u.defId || '')) return;
a.raged = true;
b.addBuff(e, { key: 'ab:rage', mods: rage, persist: true, visible: true });
},
spawn(b, e) { watchDeaths(b, e); },
};
return partnerRage;
}
function kitTeapot(ab) {
return kitDeathSpawn([{
sil: true,
death(c, b, e) {
if (c.reason !== 'killed') return;
const aspd = T(ab, 'DeadBoom.attack_speed') ?? 0, dur = T(ab, 'DeadBoom.duration') ?? 0, iv = T(ab, 'DeadBoom.interval') ?? 1;
const r = e.base.rangeRadius || 2, atk = e.s.atk, x = e.x, y = e.y;
// 被击倒后爆炸造成范围法术伤害 (one blast of ATK ×1 [ASSUMED scale]) …
b.fx('explode', { x, y, r, kind: 'teaBoom', id: e.id });
for (const u of b.alliesInRadius(x, y, r)) hurt(b, null, u, atk, 'arts');
// … 使我方攻击速度大幅降低(可被抵抗): the steam lingers `duration` s; allies inside keep the ASPD cut (re-applied
// every `interval` s)
const slow = (units) => { for (const u of units) b.addBuff(u, { key: 'ab:teaBoom', duration: iv + 0.1, refresh: 'extend', mods: { aspd }, visible: true }); };
slow(b.alliesInRadius(x, y, r));
if (dur > 0) zone(b, { x, y, r, life: dur, iv, kind: 'teaSteam', tick: slow });
},
}])(ab);
}
function kitNucleus(ab) {
return [{
taken(c, b, e, a) { if (a.on) return; a.on = true; b.addBuff(e, { key: 'ab:combat', persist: true, visible: true, mods: { moveMul: T(ab, '0.move_speed') ?? 1 } }); },
iv: T(ab, '1.interval') ?? 1,
tick(b, e, a) { if (!a.on) return; for (const u of b.alliesInRadius(e.x, e.y, T(ab, '1.range_radius') ?? 0)) elem(b, e, u, 'neural', e.s.atk * (T(ab, '1.ep_damage_ratio') ?? 0)); },
}];
}
function kitLeaderMisc(key, ab, e) {
switch (key) {
case 'enemy_1050_lslime': {
const dot = { dmg: T(ab, 'dot.damage') ?? 0, iv: T(ab, 'dot.interval') ?? 1, dur: T(ab, 'dot.duration') ?? 0 };
return [maxTargets(4), lowHpBuff(T(ab, 'selfbuff.hp_ratio') ?? 0.5, { aspd: T(ab, 'selfbuff.attack_speed') ?? 0 }), {
dealt(c, b, e2) {
if (!(dot.dur > 0)) return;
b.addBuff(c.target, { key: 'ab:burnDot', duration: dot.dur, refresh: 'replace', interval: dot.iv, visible: true,
onTick: ({ battle, unit }) => battle.dealDamage(e2, unit, { amount: dot.dmg, type: 'arts', canDodge: false, tags: ['enemyAbility'] }) });
},
}, {
iv: T(ab, 'rangedamage.interval') ?? 10,
tick(b, e2) {
if (!e2.blockedBy) return; // 被阻挡时会进行自爆造成高额伤害
b.fx('explode', { x: e2.x, y: e2.y, r: 1, kind: 'selfBlast' });
for (const u of b.alliesInRadius(e2.x, e2.y, 1)) hurt(b, e2, u, T(ab, 'rangedamage.attack@damage') ?? 0, 'arts');
},
}];
}
case 'enemy_1500_skulsr':
return [lowHpBuff(T(ab, 'atkup.hp_ratio') ?? 0.5, { atkPct: T(ab, 'atkup.atk') ?? 0 }), {
dealt(c, b, e2) {
if (e2.blockedBy) return; // 未被阻挡时会发射榴弹
for (const u of b.alliesInRadius(c.target.x, c.target.y, 1)) {
if (u !== c.target) hurt(b, e2, u, e2.s.atk, 'phys');
b.applyStatus(u, 'defDown', { duration: SKULSR_DEFDOWN_DUR, source: e2, value: -(T(ab, 'defdown.def') ?? 0) });
}
},
}];
case 'enemy_1502_crowns': {
const s = ab.sk.blink;
return [skill(s, (b, e2) => blinkForward(b, e2, s.bb.dist ?? 1.5), { sil: true, cond: (b, e2) => !!e2.blockedBy })];
}
case 'enemy_1504_cqbw': {
const s = ab.sk.C4;
return [skill(s, (b, e2) => {
const l = byPriority(e2, targetsNear(b, e2, s.bb.range_radius ?? e2.base.rangeRadius));
const n = e2.hpRatio < 0.5 ? 2 : 1; // 生命值降至一半以下时炸药包的使用数量增加
for (const t of l.slice(0, n)) {
const x = t.x, y = t.y;
b.fx('telegraph', { x, y, r: C4_RADIUS, dur: C4_FUSE, kind: 'c4' });
b.after(C4_FUSE, () => { b.fx('explode', { x, y, r: C4_RADIUS, kind: 'c4' }); for (const u of b.alliesInRadius(x, y, C4_RADIUS)) hurt(b, e2, u, e2.s.atk * (s.bb.atk_scale ?? 1), 'phys'); });
}
}, { cond: (b, e2) => targetsNear(b, e2, s.bb.range_radius ?? e2.base.rangeRadius).length > 0 })];
}
case 'enemy_1509_mousek': {
// 出场时拥有能够吸收大量法术伤害的屏障,屏障存在时防御力大幅增加 (arts-only barrier + DEF while it holds)
const bar = artsBarrier(T(ab, 'shield.dynamic') ?? 0, { key: 'ab:sandDef', whileUp: { defFlat: T(ab, 'defup.def') ?? 0 } });
const ds = ab.sk.DriftSand, ss = ab.sk.SandStorm;
return [bar, lowHpBuff(T(ab, 'enrage.hp_ratio') ?? 0.5, { dmgDealtMul: T(ab, 'enrage.damage_scale') ?? 1 }),
// 【唱沙】 the highest-max-HP unit on the field and everything in its cross: physical `damage`
skill(ds, (b, e2) => {
const t = allTargets(b, e2).sort((p, q) => q.s.maxHp - p.s.maxHp || q.deploySeq - p.deploySeq)[0];
if (!t) return;
b.fx('telegraph', { x: t.tileC, y: t.tileR, r: DRIFT_REACH, kind: 'driftSand', tiles: 'plus', id: e2.id });
for (const u of alliesInTiles(b, t.tileR, t.tileC, 'plus', DRIFT_REACH)) hurt(b, e2, u, ds.bb.damage ?? 0, 'phys');
}, { cond: (b, e2) => allTargets(b, e2).length > 0 }),
// 【沙狱】 the lowest-max-HP unit and those around it: ATK −`atk` and `damage` arts per second for `duration` s
skill(ss, (b, e2) => {
const t = allTargets(b, e2).sort((p, q) => p.s.maxHp - q.s.maxHp || q.deploySeq - p.deploySeq)[0];
if (!t) return;
const dur = ss.bb.duration ?? 0, x = t.x, y = t.y;
b.fx('zone', { x, y, r: SANDSTORM_RADIUS, dur, kind: 'sandStorm', id: e2.id });
for (const u of b.alliesInRadius(x, y, SANDSTORM_RADIUS)) {
b.addBuff(u, { key: 'ab:sandStorm', duration: dur, refresh: 'replace', interval: 1, visible: true, mods: { atkPct: ss.bb.atk ?? 0 },
onTick: ({ battle, unit }) => battle.dealDamage(e2, unit, { amount: ss.bb.damage ?? 0, type: 'arts', canDodge: false, tags: ['enemyAbility', 'sandStorm'] }) });
}
}, { cond: (b, e2) => allTargets(b, e2).length > 0 }),
];
}
case 'enemy_1511_mdrock': {
// 拥有屏障吸收法术伤害,屏障存在时大幅提升生命上限与攻击速度; 周期性刷新屏障
const s = ab.sk.RefreshShield;
const bar = artsBarrier(s ? s.bb.dynamic ?? 0 : T(ab, 'shield.dynamic') ?? 0, { key: 'ab:rockPower',
whileUp: { hpPct: (s ? s.bb.max_hp : null) ?? T(ab, 'shield.max_hp') ?? 0, aspd: (s ? s.bb.attack_speed : null) ?? T(ab, 'shield.attack_speed') ?? 0 } });
return [bar, {
// 攻击时攻击力永久提升,最多六层 (the blackboard ATK is the 6-stack total [ASSUMED split])
attack(c, b, e2) { b.addBuff(e2, { key: 'ab:rockCharge', refresh: 'stack', stacks: 1, maxStacks: 6, persist: true, mods: { atkPct: (T(ab, 'charge.attack@enemy_mdrock_s_1[charge].atk') ?? 0) / 6 } }); },
}, skill(s, (b, e2) => bar.refresh(b, e2, bar))];
}
case 'enemy_1513_dekght': {
// 攻击时使目标与周围四格的单位受到物理伤害; 【蓄力攻击】 charge `duration` s, then ATK×atk_scale on the target's cross
const s = ab.sk.ChargeAttack;
return [kitDekght(ab), { before(c, b, e2) { const t = c.targets[0]; if (t) for (const u of alliesInTiles(b, t.tileR, t.tileC, 'plus', 1)) if (!c.targets.includes(u)) c.targets.push(u); } },
skill(s, (b, e2) => {
const t = e2.blockedBy;
const dur = s.bb.duration ?? 0, r = t.tileR, cc = t.tileC;
b.addBuff(e2, { key: 'ab:charging', duration: dur, visible: true, flags: { disarm: true } });
b.fx('telegraph', { x: cc, y: r, r: 1, dur, kind: 'chargeAttack', tiles: 'plus', id: e2.id });
b.after(dur, () => {
if (!e2.alive || e2.s.flags.stun) return;
b.fx('explode', { x: cc, y: r, r: 1, kind: 'chargeAttack', tiles: 'plus' });
for (const u of alliesInTiles(b, r, cc, 'plus', 1)) hurt(b, e2, u, e2.s.atk * (s.bb['dekght[aoe].atk_scale'] ?? s.bb.atk_scale ?? 1), 'phys');
}, { owner: e2 });
}, { cond: (b, e2) => !!(e2.blockedBy && e2.blockedBy.alive) })];
}
case 'enemy_1513_dekght_2': {
// 同时攻击两名目标; 爆炸箭: three targets, after `interval` s ATK×atk_scale arts on each target's cross
const s = ab.sk.TripleAttack;
const cands = (b, e2) => byPriority(e2, targetsNear(b, e2, e2.base.rangeRadius || 2.5));
return [kitDekght(ab), maxTargets(2), skill(s, (b, e2) => {
const delay = s.bb['dekght_2[aoe].interval'] ?? 0, scale = s.bb['dekght_2[aoe].atk_scale'] ?? 1;
for (const t of cands(b, e2).slice(0, 3)) {
const r = t.tileR, cc = t.tileC;
b.fx('telegraph', { x: cc, y: r, r: 1, dur: delay, kind: 'blastArrow', tiles: 'plus', id: e2.id });
b.after(delay, () => {
if (!e2.alive) return;
b.fx('explode', { x: cc, y: r, r: 1, kind: 'blastArrow', tiles: 'plus' });
for (const u of alliesInTiles(b, r, cc, 'plus', 1)) hurt(b, e2, u, e2.s.atk * scale, 'arts');
}, { owner: e2 });
}
}, { cond: (b, e2) => cands(b, e2).length > 0 })];
}
case 'enemy_1539_reid': {
// 生命值降至一半以下时攻击力大幅度提升; 首次被击倒后重生 (Reborn.duration s), 恢复一半生命值;
// 【冲锋】 a unit within range_radius ahead ⇒ move speed ×(1+move_speed) for `duration` s
const rush = ab.sk.Rush;
const prey = (b, e2) => targetsNear(b, e2, rush.bb.range_radius ?? 1.5, { ranged: false }).filter((u) => u.ground);
return [lowHpBuff(T(ab, 'atkup.hp_ratio') ?? 0.5, { atkPct: T(ab, 'AtkUp.atk', 'atkup.atk') ?? 0 }),
reborn({ dur: T(ab, 'Reborn.duration') ?? 0, hpRatio: T(ab, 'Reborn.hp_ratio') ?? 0.5, invincible: T(ab, 'Reborn.invincible') ?? 0 }),
skill(rush, (b, e2) => {
b.addBuff(e2, { key: 'ab:rush', duration: rush.bb.duration ?? 0, refresh: 'replace', visible: true, mods: { moveMul: 1 + (rush.bb.move_speed ?? 0) } });
b.fx('charge', { x: e2.x, y: e2.y, id: e2.id, kind: 'avengerRush' });
}, { cond: (b, e2) => !e2.blockedBy && prey(b, e2).length > 0 })];
}
case 'enemy_2003_rockman': {
const s = ab.sk.StunAttack;
const cands = (b, e2) => targetsNear(b, e2, (e2.def.raw.stats && e2.def.raw.stats.rawRangeRadius) || 2.5).filter((u) => !u.s.flags.stun);
return [skill(s, (b, e2) => {
const t = byPriority(e2, cands(b, e2))[0];
if (!t) return;
b.addProjectile({ from: e2, target: t, speed: 8, visual: 'lob', source: e2, onHit: (c) => {
if (!c.target || !c.target.alive) return;
hurt(b, e2, c.target, e2.s.atk * (s.bb.atk_scale ?? 1), 'phys');
if (c.target.alive) b.applyStatus(c.target, 'stun', { duration: s.bb.stun ?? 0, source: e2 });
} });
}, { cond: (b, e2) => cands(b, e2).length > 0 })];
}
case 'enemy_2004_balloon': {
const s = ab.sk.TakeOff;
return [skill(s, (b, e2) => {
b.addBuff(e2, { key: 'ab:takeoff', duration: s.bb.duration ?? 0, flags: { unblockable: true }, mods: { moveMul: 1 + (s.bb['balloon_s[fly].move_speed'] ?? 0) }, visible: true });
b.fx('telegraph', { x: e2.x, y: e2.y, r: 0.5, kind: 'takeoff', id: e2.id });
}, { cond: (b, e2) => !!e2.blockedBy })];
}
case 'enemy_2005_axetro':
return [{
attack(c, b, e2, a) {
a.last = b.time;
b.addBuff(e2, { key: 'ab:axeStack', refresh: 'stack', stacks: 1, maxStacks: T(ab, 'atkup.max_stack_cnt') ?? 1, persist: true, mods: { atkPct: T(ab, 'atkup.atk') ?? 0, aspd: T(ab, 'atkup.attack_speed') ?? 0 } });
},
iv: 0.5,
tick(b, e2, a) { if (a.last != null && b.time - a.last >= (T(ab, 'checker.delay') ?? 4)) { a.last = null; b.removeBuff(e2, 'ab:axeStack'); } },
}];
case 'enemy_2008_flking': {
// 使全战场我方所有单位攻击力、防御力减半 (text; the blackboard's atkdown.atk_scale is not the halving),
// 部署费用回复速度减半,再部署时间加倍; 周期性地添加正比于自身最大生命值的伤害防护屏障
const s = ab.sk.refreshshield;
return [{
iv: 0.5,
tick(b) {
for (const u of b.allies()) {
auraBuff(b, u, 'ab:tombstone', 0.5, { atkMul: 0.5, defMul: 0.5 }, null, true);
// persistent so it still counts when the unit falls (the redeploy timer is set at death)
b.addBuff(u, { key: 'ab:tombRedeploy', duration: 0.6, refresh: 'replace', persist: true, mods: { redeployMul: 2 } });
}
},
}, {
tick(b, e2, a, dt) { for (const p of b.players) b.addDp(p.playerId, -0.5 * b.flags.dpPerSec * dt); },
}, skill(s, (b, e2) => b.addBuff(e2, { key: 'ab:tombShield', shield: e2.s.maxHp * (s.bb.hp_ratio ?? 0), persist: true }))];
}
case 'enemy_2048_smgrd':
// 持续在自身周围8格生成【国度】 (the 3×3 around its tile), 大幅降低其中我方单位的攻击速度; 对【国度】中的我方单位造成高额物理伤害
return [{
iv: 0.5,
tick(b, e2) { for (const u of alliesInTiles(b, Math.round(e2.y), Math.round(e2.x), 'box', 1)) auraBuff(b, u, 'ab:blackFog', 0.5, { aspd: T(ab, 'BlackFog.attack_speed') ?? 0 }, null, true); },
hitOut(c, b, e2) {
const t = c.target;
if (c.dmg.isAttack && t.side === 'ally' && Math.max(Math.abs(t.tileR - Math.round(e2.y)), Math.abs(t.tileC - Math.round(e2.x))) <= 1) c.dmg.amount *= T(ab, 'DamageUp.atk_scale') ?? 1;
},
}];
case 'enemy_2050_smsha': {
const cold = T(ab, 'Attack.attack@freeze') ?? 0, n = T(ab, 'Attack.attack@chain.max_target') ?? 1, fall = T(ab, 'Attack.attack@chain.atk_scale') ?? 1;
const jr = T(ab, 'Attack.attack@projectile_range') ?? 1.6;
const cb = ab.sk.ChainBuff;
return [onHitStatus('cold', cold), {
dealt(c, b, e2) {
let prev = c.target;
const hit = new Set([prev]);
for (let k = 1; k < n; k++) {
const nx = b.alliesInRadius(prev.x, prev.y, jr).find((u) => !hit.has(u));
if (!nx) break;
hit.add(nx);
hurt(b, e2, nx, e2.s.atk * Math.pow(fall, k), 'arts');
if (nx.alive && cold > 0) b.applyStatus(nx, 'cold', { duration: cold, source: e2 });
prev = nx;
}
},
},
// 【反自然馈赠】 (SILENCE): "attacks" another enemy in range and jumps between up to chain.max_target enemies
// (projectile_range per jump), raising their move speed and ASPD for `duration` s
skill(cb, (b, e2) => {
const bb = cb.bb, jump = bb.projectile_range ?? jr, max = bb['chain.max_target'] ?? 3;
const near = (x, y, r, seen) => b.enemiesInRadius(x, y, r).filter((o) => o !== e2 && !seen.has(o))
.sort((p, q) => Math.hypot(p.x - x, p.y - y) - Math.hypot(q.x - x, q.y - y) || p.spawnSeq - q.spawnSeq)[0];
const seen = new Set();
let prev = e2, cur = near(e2.x, e2.y, e2.base.rangeRadius || 3.5, seen);
while (cur && seen.size < max) {
seen.add(cur);
b.fx('beam', { x: prev.x, y: prev.y, from: prev.id, to: cur.id, kind: 'chainBuff' });
b.addBuff(cur, { key: 'ab:chainBuff', duration: bb.duration ?? 0, refresh: 'replace', visible: true, mods: { moveMul: 1 + (bb.move_speed ?? 0), aspd: bb.attack_speed ?? 0 } });
prev = cur;
cur = near(cur.x, cur.y, jump, seen);
}
}, { sil: true, cond: (b, e2) => b.enemiesInRadius(e2.x, e2.y, e2.base.rangeRadius || 3.5).some((o) => o !== e2) })];
}
case 'enemy_2052_smgia': {
// 数次攻击后晕眩; 受到来自自然环境的伤害时 (terrain damage), 自身在短时间内获得高额脆弱 (damage taken ×damage_scale)
const lim = T(ab, 'Weak.weak[limit]') ?? 0, scale = T(ab, 'Weak.damage_scale') ?? 1;
return [nthAttackStatus(nthOf(ab.sk.StunAttack), 'stun', (ab.sk.StunAttack && ab.sk.StunAttack.bb.stun) || 0, false), {
taken(c, b, e2) {
if (!(lim > 0) || !(c.amount > 0) || !(c.dmg.tags && c.dmg.tags.includes('terrain'))) return;
b.addBuff(e2, { key: 'ab:natureWeak', duration: lim, refresh: 'replace', visible: true, mods: { dmgTakenMul: scale } });
},
}];
}
default:
return [];
}
}
// ---------------------------------------------------------------------------------------------------------------
// multi-form leaders (悬赏 bounty targets): the first knock-out switches them to their second form (reborn helper)
/** Distance from point p to segment a–b. */
function segDist(px, py, ax, ay, bx, by) {
const dx = bx - ax, dy = by - ay, L = dx * dx + dy * dy;
const t = L > 0 ? Math.max(0, Math.min(1, ((px - ax) * dx + (py - ay) * dy) / L)) : 0;
return Math.hypot(px - (ax + t * dx), py - (ay + t * dy));
}
/** A second normal hit on the same target ("二连击"): same damage type, no further on-hit procs. */
const doubleHit = (on = () => true, extra = null) => ({
dealt(c, b, e, a) {
if (!on(b, e, a) || !c.target.alive) return;
b.dealDamage(e, c.target, { amount: e.s.atk, type: c.dmg.type, canDodge: true, tags: ['enemyAbility', 'secondHit'], ...(extra ? extra(c, b, e) : {}) });
},
});
/** 巨大的丑东西 · melee ×rage; KO ⇒ self-destruct (big stun) and the 大祭司 pilot ejects: no attack, unblockable, own DEF/RES,
* faster, loses bird_run.damage true HP per second. (巨蕈/木桩 tiles and its ranged splash attack have no counterpart here:
* its data radius is 0.) */
function kitUglyThing(ab) {
const rage = T(ab, 'combat.attack@mcmstr_rage_attack.atk_scale') ?? 1;
const bomb = ab.sk['bomb[reborning]'] ? ab.sk['bomb[reborning]'].bb : {};
const run = { spd: T(ab, 'bird_run.move_speed') ?? 0, def: T(ab, 'bird_run.def'), res: T(ab, 'bird_run.magic_resistance'), dmg: T(ab, 'bird_run.damage') ?? 0 };
const P = { pilot: false };
return [
{ hitOut(c, b, e) { if (!P.pilot && c.dmg.isAttack && c.target === e.blockedBy) c.dmg.amount *= rage; } }, // 近战攻击造成更高伤害
reborn({
dur: T(ab, 'reborn.duration') ?? 0,
onKo(b, e) { // 生命值降至0后自爆,造成大范围晕眩
b.fx('explode', { x: e.x, y: e.y, r: MCM_BOMB_RADIUS, kind: 'mechBomb', id: e.id });
for (const u of b.alliesInRadius(e.x, e.y, MCM_BOMB_RADIUS)) {
hurt(b, e, u, e.s.atk * (bomb.atk_scale ?? 0), 'phys');
if (u.alive && bomb.stun > 0) b.applyStatus(u, 'stun', { duration: bomb.stun, source: e });
}
},
onReborn(b, e) { // 大祭司形态: 不进行攻击,无法被阻挡,每秒受到真实伤害
P.pilot = true;
e.profile.noAttack = true;
b.addBuff(e, {
key: 'ab:pilot', persist: true, visible: true, flags: { unblockable: true }, interval: 1,
mods: { moveMul: 1 + run.spd, defFlat: run.def != null ? run.def - e.base.def : 0, resFlat: run.res != null ? run.res - e.base.res : 0 },
onTick: ({ battle, unit }) => { if (run.dmg > 0) battle.dealDamage(null, unit, { amount: run.dmg, type: 'true', canDodge: false, tags: ['enemyAbility', 'pilotBurn'] }); },
});
},
}),
];
}
/** 杰斯顿·威廉姆斯 · 狱警: RES +enhance, ranged arts, every (sp+1)th attack stuns max_target units; KO ⇒ (reborn.duration s)
* 杀手: frees every prisoner, DEF/ATK/ASPD/speed up, melee physical, every (sp+1)th attack hits `times` × ignoring def_penetrate. */
function kitWarden(ab) {
const en = (k) => T(ab, `enhance.${k}`) ?? 0;
const iron = ab.sk.ironsandstorm, pierce = ab.sk.armorpiercing;
const P = { killer: false, n: 0, power: false };
const pen = () => (pierce && pierce.bb.def_penetrate) || 0;
return [{
spawn(b, e, a, ab2) {
ab2.atkType = 'arts'; // 攻击造成远程法术伤害
e.profile.melee = false;
b.addBuff(e, { key: 'ab:warden', persist: true, visible: true, mods: { resFlat: en('magic_resistance') } }); // 法术抗性大幅提升
},
before(c, b) { const s = P.killer ? pierce : iron; P.power = !!s && (P.n + 1) % nthOf(s) === 0; },
hitOut(c) { if (P.killer && P.power && c.dmg.isAttack) c.dmg.defIgnorePct = Math.min(1, (c.dmg.defIgnorePct || 0) + pen()); },
dealt(c, b, e) {
if (!P.killer || !P.power || !pierce) return;
for (let i = 1; i < (pierce.bb.times ?? 1) && c.target.alive; i++) b.dealDamage(e, c.target, { amount: e.s.atk, type: 'phys', defIgnorePct: pen(), canDodge: true, isSkill: true, tags: ['enemyAbility', 'armorPiercing'] });
},
attack(c, b, e) {
P.n++;
if (!P.power || P.killer || !iron) return;
// 能使多名单位晕眩: the skill attack stuns up to max_target units in its range (its target first)
const l = byPriority(e, targetsNear(b, e, e.base.rangeRadius || 2));
for (const t of c.targets) if (!l.includes(t)) l.unshift(t);
for (const u of l.slice(0, iron.bb.max_target ?? 1)) if (u.alive) b.applyStatus(u, 'stun', { duration: iron.bb.stun ?? 0, source: e });
},
}, reborn({
dur: T(ab, 'reborn.duration') ?? 0,
onKo(b) { freeAllPrisoners(b); }, // 进入杀手形态时解放全场敌人
onReborn(b, e) {
P.killer = true; P.n = 0;
e.mem.ab.atkType = null; // 攻击造成近战物理伤害
e.profile.melee = true;
b.removeBuff(e, 'ab:warden');
b.addBuff(e, { key: 'ab:killer', persist: true, visible: true,
mods: { defFlat: en('def'), atkFlat: en('atk'), batPct: en('base_attack_time') / (e.base.bat || 1), moveFlat: en('move_speed') } });
},
})];
}
/** “自在” · 【纬地经天】 arts cross on the nearest unit (form 2: + the farthest); 【破桎而出】 barrier — unbroken after its
* duration ⇒ ATK×atk_scale arts around; KO ⇒ second form (ATK up, double hits, stronger 破桎而出). 晦明 attributes n/a. */
function kitXi(ab) {
const cross = ab.sk.CrossAttack, sb = ab.sk.ShieldBurst, sb2 = ab.sk.ShieldBurstReborn;
const P = { form2: false };
const byDist = (b, e) => allTargets(b, e).sort((p, q) => Math.hypot(p.x - e.x, p.y - e.y) - Math.hypot(q.x - e.x, q.y - e.y) || q.deploySeq - p.deploySeq);
const crossAt = (b, e, t) => {
const r0 = t.tileR, c0 = t.tileC;
b.fx('telegraph', { x: c0, y: r0, r: XI_CROSS_REACH, kind: 'xiCross', tiles: 'plus', id: e.id });
for (const u of alliesInTiles(b, r0, c0, 'plus', XI_CROSS_REACH)) if (u.tileR === r0 || u.tileC === c0) hurt(b, e, u, e.s.atk * ((cross && cross.bb.atk_scale) ?? 1), 'arts');
};
const burst = (s) => (b, e) => {
const dur = s.bb.duration ?? 0, r = s.bb.range_radius ?? 0;
b.fx('telegraph', { x: e.x, y: e.y, r, dur, kind: 'breakFree', id: e.id });
b.addBuff(e, { key: 'ab:xiShield', duration: dur, shield: s.bb.dynamic ?? 0, visible: true,
// expiry (not breaking: a broken barrier is removed without onExpire) = the barrier held
onExpire: ({ battle }) => {
if (!e.alive) return;
battle.fx('explode', { x: e.x, y: e.y, r, kind: 'breakFree', id: e.id });
for (const u of battle.alliesInRadius(e.x, e.y, r)) hurt(battle, e, u, e.s.atk * (s.bb.atk_scale ?? 0), 'arts');
} });
};
const re = reborn({
dur: T(ab, 'reborn.duration') ?? 0, invincible: T(ab, 'reborn.invincible') ?? 0,
onKo(b, e) { b.removeBuff(e, 'ab:xiShield'); },
onReborn(b, e) {
P.form2 = true;
b.addBuff(e, { key: 'ab:xiReborn', persist: true, visible: true, mods: { atkPct: T(ab, 'reborn.atk') ?? 0 } }); // 攻击力提升
if (s2) s2.left = s2.icd;
},
});
const s1 = skill(sb, burst(sb), { cond: () => !P.form2 });
const s2 = skill(sb2, sb2 ? burst(sb2) : null, { cond: () => P.form2 });
return [re, s1, s2, doubleHit(() => P.form2), // 普通攻击进行两次
skill(cross, (b, e) => {
const l = byDist(b, e);
if (!l.length) return;
crossAt(b, e, l[0]);
if (P.form2 && l.length > 1) crossAt(b, e, l[l.length - 1]); // 额外对最远目标释放
}, { cond: (b, e) => allTargets(b, e).length > 0 })];
}
/** 锏 · 抵抗; DEF penetration against its blocker; 【速杀】 passes through its blocker hitting those on the way; 【肆虐风雪】
* AoE; KO ⇒ second form: stealth, more penetration, double hits, 【瞬息杀机】 at every 25 % HP lost (SP cleared + no attacks
* / heals around it). <降雪> absent. */
function kitMace(ab) {
const pen1 = T(ab, 'DefPenetrate.enemy_blkswb_t_2.def_penetrate') ?? 0, pen2 = T(ab, 'DefPenetrate.enemy_blkswb_t_2[reborn].def_penetrate') ?? pen1;
const cs = { ratio: T(ab, 'ClearSp.hp_ratio') ?? 0, dur: T(ab, 'ClearSp.duration') ?? 0, r: T(ab, 'ClearSp.range_radius') ?? 0 };
const P = { form2: false, next: 1 };
const penOn = (c, e) => (c.target === e.blockedBy ? (P.form2 ? pen2 : pen1) : 0);
const blink = (s) => (b, e) => {
const bl = e.blockedBy;
const from = blinkForward(b, e, s.bb.dist ?? 1.5);
if (!from) return;
for (const u of b.allies()) {
if (u !== bl && segDist(u.x, u.y, from.x, from.y, e.x, e.y) > 0.5) continue;
hurt(b, e, u, e.s.atk * (s.bb.atk_scale ?? 1), 'phys');
}
};
const circle = (s) => (b, e) => {
const r = s.bb.range_radius ?? 0;
b.fx('explode', { x: e.x, y: e.y, r, kind: 'blizzard', id: e.id });
for (const u of b.alliesInRadius(e.x, e.y, r)) hurt(b, e, u, e.s.atk * (s.bb.atk_scale ?? 1), 'phys');
};
const inR = (b, e, s) => b.alliesInRadius(e.x, e.y, (s && s.bb.range_radius) || 0).length > 0;
return [resist(), {
hitOut(c, b, e) { const p = penOn(c, e); if (c.dmg.isAttack && p > 0) c.dmg.defIgnorePct = Math.min(1, (c.dmg.defIgnorePct || 0) + p); },
taken(c, b, e) { // 【瞬息杀机】 every hp_ratio of max HP lost (second form)
if (!P.form2 || !(cs.ratio > 0)) return;
while (e.alive && e.hpRatio <= P.next - cs.ratio + 1e-9 && P.next - cs.ratio > 1e-9) {
P.next -= cs.ratio;
b.fx('explode', { x: e.x, y: e.y, r: cs.r, kind: 'clearSp', id: e.id });
for (const u of b.alliesInRadius(e.x, e.y, cs.r)) {
const sk = u.skill;
if (sk && !sk.noSkill && sk.kind !== 'passive' && !sk.active && sk.spCost > 0) { sk.sp = 0; sk.charges = 0; } // 清空技力 (stored charges too)
if (cs.dur > 0) b.applyStatus(u, 'disarm', { duration: cs.dur, source: e });
}
}
},
}, doubleHit(() => P.form2, (c, b, e) => ({ defIgnorePct: penOn(c, e) })),
skill(ab.sk.Blink, blink(ab.sk.Blink || { bb: {} }), { id: 'blink', cond: (b, e) => !P.form2 && !!e.blockedBy }),
skill(ab.sk.Blink2, blink(ab.sk.Blink2 || { bb: {} }), { id: 'blink2', cond: (b, e) => P.form2 && !!e.blockedBy }),
skill(ab.sk.CircleAttack, circle(ab.sk.CircleAttack || { bb: {} }), { id: 'circle', cond: (b, e) => !P.form2 && inR(b, e, ab.sk.CircleAttack) }),
skill(ab.sk.CircleAttack2, circle(ab.sk.CircleAttack2 || { bb: {} }), { id: 'circle2', cond: (b, e) => P.form2 && inR(b, e, ab.sk.CircleAttack2) }),
reborn({
dur: T(ab, 'Reborn.duration') ?? 0, invincible: T(ab, 'Reborn.invincible') ?? 0,
onReborn(b, e) { P.form2 = true; P.next = 1; b.addBuff(e, { key: 'ab:stealth', flags: { stealth: true }, persist: true }); },
})];
}
/** 扎罗,“狼之主” · first form: damage taken −30 %, not stunnable, 【溶血骇惧】 (3 units: ASPD −70, accelerating HP loss,
* cured once 扎罗 loses hp_ratio of its HP); KO ⇒ 【远古威慑】 rebirth (Reborn.duration s: ASPD down around it while its HP
* refills) ⇒ second form: ranged double hits, ATK/ASPD up, invincible at first. 血债账款 / 【狂暴怒嗥】 absent. */
function kitWolfLord(ab) {
const fc = ab.sk.FearCage;
const P = { form2: false };
const cure = (b) => { for (const u of b.allies()) { const d = u.findBuff('ab:fearCage'); if (d) b.removeBuff(u, d); } };
return [{
spawn(b, e, a, ab2) {
ab2.immune = new Set([...(ab2.immune || []), 'stun']); // 第一形态: 不可被晕眩
b.addBuff(e, { key: 'ab:wolfGuard', persist: true, visible: true, mods: { dmgTakenMul: 1 - (T(ab, 'Passive.damage_resistance') ?? 0) } });
},
iv: 0.25,
tick(b, e) { // afflicted units recover once 扎罗 lost hp_ratio since the cast
if (!fc) return;
for (const u of b.allies()) {
const d = u.findBuff('ab:fearCage');
if (d && d.data.src === e && e.hpRatio <= d.data.hp0 - (fc.bb.hp_ratio ?? 0) + 1e-9) b.removeBuff(u, d);
}
},
},
// 【溶血骇惧】 every spCost s (enemy SP +1/s [ASSUMED]), first form only
skill(fc, (b, e) => {
const l = b.rng.shuffle(allTargets(b, e).filter((u) => u.kind === 'op' && !u.findBuff('ab:fearCage'))).slice(0, fc.bb.max_target ?? 3);
const dur = fc.bb.duration_bleed ?? 0;
for (const u of l) {
b.fx('beam', { x: e.x, y: e.y, from: e.id, to: u.id, kind: 'fearCage' });
b.addBuff(u, { key: 'ab:fearCage', duration: dur, refresh: 'replace', visible: true, interval: 1, data: { src: e, hp0: e.hpRatio, t0: b.time },
mods: { aspd: fc.bb.attack_speed ?? 0 },
onTick: ({ battle, unit, buff }) => { const k = Math.min(1, (battle.time - buff.data.t0) / Math.max(1, dur)); battle.loseHp(unit, unit.s.maxHp * WOLF_BLEED_PEAK * k, { source: e }); } });
}
}, { cd: fc ? fc.sp || fc.cd : null, icd: fc ? fc.sp || fc.icd : null, cond: (b, e) => !P.form2 && allTargets(b, e).some((u) => u.kind === 'op') }),
doubleHit(() => P.form2), // 攻击变为远程二连击
reborn({
dur: T(ab, 'Reborn.duration') ?? 0, invincible: T(ab, 'Passive2.invincible_time') ?? 0,
onKo(b, e) {
cure(b);
b.removeBuff(e, 'ab:wolfGuard');
if (e.mem.ab.immune) e.mem.ab.immune.delete('stun');
},
during(b, e, a, t) { // 【远古威慑】: nearby operators slowed; its HP refills slowly
const dur = T(ab, 'Reborn.duration') ?? 1;
e.hp = Math.max(1, e.s.maxHp * Math.min(1, t / Math.max(TICK, dur)));
for (const u of b.alliesInRadius(e.x, e.y, WOLF_AWE_RADIUS)) auraBuff(b, u, 'ab:ancientAwe', 0.2, { aspd: WOLF_AWE_ASPD }, null, true);
},
onReborn(b, e) {
P.form2 = true;
e.profile.melee = false;
e.base.rangeRadius = Math.max(e.base.rangeRadius || 0, WOLF_RANGE);
b.addBuff(e, { key: 'ab:wolfRage', persist: true, visible: true, mods: { atkPct: T(ab, 'Passive2.atk') ?? 0, batPct: (T(ab, 'Passive2.base_attack_time') ?? 0) / (e.base.bat || 1) } });
},
})];
}
/** 转译基底·α · never attacks at first; 4 physical hits ⇒ 复仇 form (melee, heavy), 4 arts hits ⇒ 术士 form (ranged arts),
* blocked first ⇒ 幽灵 form (no stat change; unblockable [ASSUMED]). Stats from Mode_*_Passive. */
function kitTranslator(ab, e) {
const t = (k) => T(ab, k) ?? 0;
const P = { phys: 0, arts: 0, form: null };
const mods = (pre) => ({ hpFlat: t(`${pre}.max_hp`), atkFlat: t(`${pre}.atk`), defFlat: t(`${pre}.def`), resFlat: t(`${pre}.magic_resistance`),
moveMul: Math.max(0, 1 + t(`${pre}.move_speed`)), batPct: t(`${pre}.base_attack_time`) / (e.base.bat || 1) });
const melee = attackLoop({ type: 'phys', melee: true });
const caster = attackLoop({ type: 'arts', melee: false, radius: e.base.rangeRadius || (e.def.raw && e.def.raw.stats && e.def.raw.stats.rawRangeRadius) || 2.4 });
const change = (b, e2, form) => {
if (P.form) return;
P.form = form;
const pre = form === 'fuchou' ? 'Mode_Fuchou_Passive' : form === 'shushi' ? 'Mode_Shushi_Passive' : 'Mode_Youling_Passive';
b.addBuff(e2, { key: 'ab:form', persist: true, visible: true, mods: mods(pre), flags: form === 'youling' ? { unblockable: true } : null });
if (form === 'fuchou') melee.on = true;
if (form === 'shushi') caster.on = true;
b.fx('phase', { x: e2.x, y: e2.y, id: e2.id, kind: `translator_${form}` });
};
return [melee, caster, {
taken(c, b, e2) {
const s = c.source;
if (P.form || !s || s.side !== 'ally') return;
if (c.dmg.type === 'phys' && ++P.phys >= (t('Passive.phy_max_count') || 4)) change(b, e2, 'fuchou');
else if (c.dmg.type === 'arts' && ++P.arts >= (t('Passive.magic_max_count') || 4)) change(b, e2, 'shushi');
},
blocked(c, b, e2) { change(b, e2, 'youling'); },
}];
}
/** 乌顶巨角卢鲁 · 【角力对决】 when blocked: charges `duration` s (no normal attacks; a stun or 失衡 interrupts), then hits its
* blocker; operators hold fixed tiles, so the push always fails ⇒ extra damage and a fail_duration stun. */
function kitElk(ab) {
const s = ab.sk.skill;
const P = { ch: null };
const stop = (b, e) => { P.ch = null; b.removeBuff(e, 'ab:elkCharge'); };
return [{
tick(b, e) {
const ch = P.ch;
if (!ch) return;
if (e.s.flags.stun || !ch.t.alive || e.blockedBy !== ch.t) { stop(b, e); b.fx('phase', { x: e.x, y: e.y, id: e.id, kind: 'chargeBroken' }); return; }
if (b.time + 1e-9 < ch.until) return;
stop(b, e);
b.fx('explode', { x: ch.t.x, y: ch.t.y, r: 0.5, kind: 'elkClash' });
hurt(b, e, ch.t, e.s.atk * ((s && s.bb.atk_scale_s) ?? 1) * ELK_FAIL_SCALE, 'phys');
const st = T(ab, 'data.attack@fail_duration') ?? 0;
if (ch.t.alive && st > 0) b.applyStatus(ch.t, 'stun', { duration: st, source: e });
},
}, unbalanced((b, e) => { if (P.ch) stop(b, e); }),
skill(s, (b, e) => {
const dur = s.bb.duration ?? 0;
P.ch = { t: e.blockedBy, until: b.time + dur };
b.addBuff(e, { key: 'ab:elkCharge', duration: dur + 0.1, visible: true, flags: { disarm: true } });
b.fx('telegraph', { x: e.blockedBy.x, y: e.blockedBy.y, r: 0.5, dur, kind: 'elkCharge', id: e.id });
}, { cond: (b, e) => !P.ch && !!(e.blockedBy && e.blockedBy.alive) })];
}
// ---------------------------------------------------------------------------------------------------------------
// KITS: enemyKey → (ab, e, battle) => ability list. One line per key (name · what).
export const KITS = Object.freeze({
// --- INVISIBLE 隐匿
enemy_1009_lurker: kitStealth, // 潜伏者 · stealth
enemy_1019_jshoot: kitStealth, // 隐形弩手 · stealth
enemy_1019_jshoot_2: kitStealth, // 隐形弩手组长 · stealth
enemy_1023_jmage: kitStealth, // 隐形术师 · stealth
enemy_1283_sgkill: kitStealth, // 家族灭迹人 · stealth (清算时刻 n/a)
enemy_1283_sgkill_2: kitStealth, // 家族暗影灭迹人 · stealth
enemy_1299_ymkilr: kitShadowKiller, // 山海众头目 · stealth, 1st attack after being blocked ×InvisibleCombat.atk_scale
enemy_1299_ymkilr_2: kitShadowKiller, // 山海众秘使 · same
enemy_1389_winbab_2: kitStealth, // 访问团强攻冠军 · stealth (供暖器 priority n/a)
enemy_1404_msnip: kitCrossbow, // 重弩突袭者 · stealth + 直击 (row/column bolt: arts + stun, reveals itself)
enemy_10031_cnvsld: kitStealth, // 业余竞演者 · stealth
enemy_10034_cnvsax: kitJazz, // 节日爵士乐手 · stealth, attacks only when revealed; fire aura (arts + burn)
enemy_10042_prtrop: kitStealth, // 架桥船工 · stealth (bridges n/a)
enemy_10042_prtrop_2: kitStealth, // 扶桥老手 · stealth
enemy_9008_acbunn: kitBoneSpike, // 假想敌:骨刺 · stealth; hits 3 targets while stealthed
enemy_1175_dushdo_2: kitShadowBlade, // 深池伙友影刃精英 · stealth; BAT −1.3 s next to 卫队精英
enemy_2034_sythef: (ab) => [stealth(), onHitStatus('stun', T(ab, 'Combat.attack@stun'))], // 流泪小子 · stealth, attacks stun
enemy_2034_sythef_2: (ab) => [stealth(), onHitStatus('stun', T(ab, 'Combat.attack@stun'))], // 流泪小子 · same
// --- TIMES 频次 (hit-count units + their creators)
enemy_1196_msfyin: kitTimes(), // 木制瑞印 · 2 hits, unblockable
enemy_1196_msfyin_2: kitTimes(), // 红木瑞印 · 3 hits
enemy_1198_msfshu: kitTimes(), // 小说卷轴 · 3 hits
enemy_1198_msfshu_2: kitTimes(), // 诗画卷轴 · 4 hits
enemy_1200_msfjin: kitTimes(), // 青铜镜 · 30 hits
enemy_1200_msfjin_2: kitTimes(), // 黄铜镜 · 35 hits
enemy_1202_msfzhi: kitTimes(), // 木制镇纸 · 3 hits
enemy_1202_msfzhi_2: kitTimes(), // 红木镇纸 · 4 hits
enemy_1204_msfhu: kitTimes(true), // 青瓷茶器 · 4 arts/true hits
enemy_1204_msfhu_2: kitTimes(true), // 彩瓷茶器 · 6 arts/true hits
enemy_1208_msfji: kitTimes(), // 铜矛头 · 25 hits
enemy_1208_msfji_2: kitTimes(), // 铁矛头 · 30 hits
enemy_1210_msfden: kitTimes(), // 铜灯盘 · 35 hits
enemy_1210_msfden_2: kitTimes(), // 铁灯盘 · 45 hits
enemy_1195_sfyin: kitDeathSpawn(), // 磨砻 · death: 2 木制瑞印
enemy_1195_sfyin_2: kitDeathSpawn(), // 明鉴 · death: 2 红木瑞印
enemy_1197_sfshu: kitDeathSpawn(), // 俗心 · death: 3 小说卷轴
enemy_1197_sfshu_2: kitDeathSpawn(), // 雅气 · death: 3 诗画卷轴
enemy_1199_sfjin: kitDeathSpawn(), // 身观 · death: 1 青铜镜 (taunt from data)
enemy_1207_sfji: kitBlades, // 沉沙 · each attack spends a blade (+ATK); death: unspent blades → 铜矛头 (≥1)
enemy_1207_sfji_2: kitBlades, // 新硎 · same (铁矛头)
enemy_1209_sfden: kitInvisShield, // 清明 · veils nearby enemies (stealth) every 15 s; death: 铜灯盘
enemy_1209_sfden_2: kitInvisShield, // 堂皇 · same (铁灯盘)
enemy_1203_sfhu: kitTeapot, // 烹泉 · death: 4 青瓷茶器 + ASPD-down arts blast zone
enemy_1203_sfhu_2: kitTeapot, // 沏虹 · death: 4 彩瓷茶器 + blast zone
enemy_1288_duskls: kitEmber, // 深池逐火战士 · on KO: stealthed 5-hit ember, revives after 10 s
enemy_1288_duskls_2: kitEmber, // 深池逐火精锐战士 · same
enemy_1292_duskld: kitEmber, // 深池逐火护卫 · 10-hit ember
enemy_9010_acpupp: kitRegen, // 假想敌:再生 · 15-hit regen husk (15 s) + 5-hit shields on nearby enemies
// --- ELEMENT 元素
enemy_1148_dssbr: kitEp('neural', 'epdamage.attack@ep_damage_ratio'), // 底海滑动者 · neural on hit
enemy_1148_dssbr_2: kitEp('neural', 'epdamage.attack@ep_damage_ratio'), // 富营养的滑动者 · neural on hit
enemy_1158_divman: (ab) => kitDeepsea(ab, { swim: true }), // 潜水员 · in deep water: ATK up + stealth; immune to 水蚀
enemy_1160_hvyslr: (ab) => [...kitStun3(ab), ...kitDeepsea(ab, { drown: true })], // 码头水手 · every (spCost+1)th attack stuns; drowns in deep water
enemy_1160_hvyslr_2: (ab) => [...kitStun3(ab), ...kitDeepsea(ab, { drown: true })], // 码头水手长 · same
enemy_1161_tidmag: kitTidmag, // 控潮术师 · attack hits target + 4 neighbours, erosion
enemy_1161_tidmag_2: kitTidmag, // 领潮员 · same
enemy_1162_magmot: (ab) => [...kitTidmag(ab), deathSpawn(ab.tS['DeathRattle.enemy_key'], 1)], // 术师快艇 · same + 控潮术师 on death
enemy_1305_mhslim: kitEp('burn', 'EpDamage.attack@ep_damage_ratio'), // 灼热源石虫 · burn on hit
enemy_1305_mhslim_2: kitEp('burn', 'EpDamage.attack@ep_damage_ratio'), // 炽焰源石虫 · burn on hit
enemy_2021_syfish: kitEp('erosion', 'EpDamage.attack@ep_damage_ratio'), // 骨海漂流体 · erosion on hit
enemy_2025_syufo: (ab) => [float(), ep('erosion', T(ab, 'EpDamage.attack@ep_damage_ratio') ?? 0)], // 掠海漂移体 · 近地悬浮 + erosion
enemy_1229_darmy: kitEp('apoptosis', 'epdamage.attack@ep_damage_ratio'), // 萨卡兹王庭军战士 · apoptosis on hit
enemy_1229_darmy_2: kitEp('apoptosis', 'epdamage.attack@ep_damage_ratio'), // 萨卡兹王庭军精锐战士 · apoptosis on hit
enemy_1275_dwlock_2: kitDeathEye, // 萨卡兹王庭军精锐术师 · apoptosis on hit + DeathEye channel → group apoptosis
enemy_1439_dslntf: kitNucleus, // 元核孽生者 · first damage: combat state (speed ×, neural pulse around)
enemy_1439_dslntf_2: kitNucleus, // 异光体孽生者 · same
enemy_9007_acelem: kitParasite, // 假想敌:淤困 · taunt −1; parasitises its blocker (arts/s, element taken ×, burst spreads)
enemy_10065_ftzlc: kitIgnitable, // 卷心籽 · ignited by 灼藤 → arts death blast
enemy_10067_ftsjc: kitFlameVine, // 灼藤 · ATK ramps until the 1st attack (AoE arts + burn), burn on hit, ignites 卷心籽
// --- DOT 持续
enemy_1234_dsubrl: kitDsubrl, // 深溟巢涌者 · pulse hits every ally in range + neural; 抵抗, immune 停顿
enemy_1234_dsubrl_2: kitDsubrl, // 富营养的巢涌者 · same
enemy_1267_nhpbr: kitPolluted, // 萨卡兹枯朽战士 · death: 污染秽蚀 HP-loss zone
enemy_1267_nhpbr_2: kitPolluted, // 萨卡兹枯朽战士组长 · same
enemy_1270_nhstlk: (ab) => [bleed(ab)], // 逐腐兽 · bleeding (arts/s, cleared by healing)
enemy_1270_nhstlk_2: (ab) => [bleed(ab)], // 疯狂的逐腐兽 · same
enemy_1272_nhtank: kitTank, // 萨卡兹枯朽战车 · ground targets only, melee ×2, every 3rd attack a pollution zone
enemy_1272_nhtank_2: kitTank, // 尖端萨卡兹枯朽战车 · same
enemy_9006_actoxi: (ab) => [{ // 假想敌:蚀裂 · death: poison cloud on its killer
sil: true,
death(c, b, e) {
if (c.reason !== 'killed') return;
const k = c.killer && c.killer.side === 'ally' && c.killer.alive ? c.killer : null;
const atk = e.s.atk;
const cloud = (x, y) => dmgZone(b, null, x, y, T(ab, '1.projectile_range') ?? 0.8, T(ab, '1.projectile_life_time') ?? 0, T(ab, '1.interval') ?? 1, atk * (T(ab, '1.damage_atk_scale') ?? 0), 'arts', 'poison');
if (k) b.addProjectile({ from: e, target: k, speed: 8, visual: 'lob', source: null, hitDead: true, onHit: (h) => cloud(h.x ?? k.x, h.y ?? k.y) });
else cloud(e.x, e.y);
},
}],
enemy_10122_uacann_2: (ab) => [{ // 集团军重型火炮 · each hit leaves a burning zone
dealt(c, b, e) {
dmgZone(b, e, c.target.x, c.target.y, T(ab, 'ProjectileBoomRange.attack@projectile_range') ?? 1, T(ab, 'ProjectileBoomRange.attack@projectile_life_time') ?? 0, 1, T(ab, 'ProjectileBoomRange.attack@value') ?? 0, 'arts', 'burning');
},
}],
enemy_10054_cjhot: (ab, e) => [immuneTo('sluggish'), { // 鼎沸 · pulses arts + burn around itself; immune 停顿
iv: e.base.bat || 1,
tick(b, e2) {
const r = (e2.def.raw.stats && e2.def.raw.stats.rawRangeRadius) || 1.6;
b.fx('explode', { x: e2.x, y: e2.y, r, kind: 'boil' });
for (const u of b.alliesInRadius(e2.x, e2.y, r)) { hurt(b, e2, u, e2.s.atk * (T(ab, 'aoe.atk_scale') ?? 1), 'arts'); elem(b, e2, u, 'burn', e2.s.atk * (T(ab, 'aoe.ep_damage_ratio') ?? 0)); }
},
}],
// --- REFLECTION 折射
enemy_1165_duhond: kitRefraction, // 深池侦察犬 · refraction
enemy_1165_duhond_2: kitRefraction, // 深池侦察犬pro · refraction
enemy_1166_dusbr: kitRefraction, // 深池侦察兵 · refraction
enemy_1166_dusbr_2: kitRefraction, // 深池侦察队长 · refraction
enemy_1168_dumage: kitRefraction, // 深池暗影术师 · refraction
enemy_1168_dumage_2: kitRefraction, // 深池暗影术师队长 · refraction
enemy_1170_dushld: kitRefraction, // 深池重甲卫士 · refraction
enemy_1170_dushld_2: kitRefraction, // 深池重甲卫士队长 · refraction
enemy_1169_duphlx: kitPhalanx, // 深池方阵步兵 · refraction + DEF +200 per nearby same unit
enemy_1169_duphlx_2: kitPhalanx, // 深池方阵指挥官 · same
enemy_1172_dugago: (ab) => [...kitRefraction(ab), statue(ab)], // 守墓石像 · refraction; 1st KO → statue 10 s → flyer
enemy_1172_dugago_2: (ab) => [...kitRefraction(ab), statue(ab)], // 愤怒的守墓石像 · same
enemy_1174_duholy: kitHolyGuard, // 深池伙友卫队 · refraction; ASPD-down field next to 影刃 (taunt from data)
enemy_1174_duholy_2: kitHolyGuard, // 深池伙友卫队精英 · same
enemy_9011_acrefr: (ab) => [refraction(T(ab, 'Refracting.magic_resistance') ?? 0, T(ab, 'Refracting.max_hp') ?? 0)], // 假想敌:镜膜 · refraction + max HP while active
// --- FLY 飞行 (motion is engine; abilities)
enemy_1017_defdrn: (ab) => [enemyAura(T(ab, 'defup.range_radius') ?? 2.5, 'ab:defdrn', { defFlat: T(ab, 'defup.def') ?? 0 })], // 御4 · DEF aura
enemy_1355_mrfly: (ab, e) => [enemyAura(e.base.rangeRadius || 2.5, 'ab:mrfly', { resFlat: T(ab, 'magdef_add.magic_resistance') ?? 0 })], // 护障 · RES aura
enemy_1355_mrfly_2: (ab, e) => [enemyAura(e.base.rangeRadius || 2.5, 'ab:mrfly', { resFlat: T(ab, 'magdef_add.magic_resistance') ?? 0 })], // 护障·P · RES aura
enemy_1042_frostd: (ab) => [allyAura(T(ab, 'defup.range_radius') ?? 2.5, 'ab:frost', { aspd: (T(ab, 'atkSpeedDown.attack_speed') ?? 0) * 100 })], // 寒霜 · ASPD −50 aura on operators
enemy_1040_bombd: kitBombd, // 暴鸰 · no normal attack: ONE bomb (target + 8 tiles), then ×2 speed
enemy_10083_hlbird: kitSelfFear, // “萨科塔之翼” · fear + flee below half HP
enemy_10084_hlegle: kitSteal, // “萨科塔之眼” · fear below half; steals 1 ammo instead of hitting
enemy_10085_hllevi_2: kitRoar, // “萨科塔昂首” · fear below half; 祈祷邀约 global ASPD −30
enemy_1407_hummbd: kitExposeOnDeath, // 远眺 · death: exposes operators around (damage taken ×1.2)
enemy_9009_acfort: kitBlackCloud, // 假想敌:黑云 · devours ≤ 3 normal flyers for ammo, fires it all at random allies
enemy_1112_emppnt: kitShell, // 帝国炮火先兆者 · attacks are shells landing 3 s later (r 1.2)
enemy_1112_emppnt_2: kitShell, // 帝国炮火中枢先兆者 · same
enemy_1321_wdarft: (ab) => [skill(ab.sk.BornBugs, (b, e) => spawnChildren(b, e, 'enemy_1269_nhfly', BUGS_PER_CAST))], // 枯朽萃聚使徒 · spawns 枯朽之种
enemy_1269_nhfly: () => [{ // 枯朽之种 · no normal attack: dives onto a nearby operator and self-destructs
spawn(b, e) { e.profile.noAttack = true; },
tick(b, e, a, dt) {
if (a.t && !a.t.alive) { a.t = null; b.removeBuff(e, 'ab:dive'); }
if (!a.t) {
const l = targetsNear(b, e, e.base.rangeRadius || 1).sort((p, q) => Math.hypot(p.x - e.x, p.y - e.y) - Math.hypot(q.x - e.x, q.y - e.y));
if (!l.length) return;
a.t = l[0];
b.addBuff(e, { key: 'ab:dive', flags: { noMove: true } });
}
if (!stepToward(e, a.t.x, a.t.y, Math.max(e.s.moveSpeed, 0.5) * MOVE_SCALE * dt) && Math.hypot(a.t.x - e.x, a.t.y - e.y) > 0.3) return;
hurt(b, e, a.t, e.s.atk, 'phys');
b.fx('explode', { x: e.x, y: e.y, r: 0.5, kind: 'seed' });
b.kill(e, null);
},
}],
// --- 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) { b.loseHp(e, T(ab, 'periodic_damage.damage') ?? 0, { source: null }); } }], // 狂暴宿主组长 · loses HP over time
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) => [skill(ab.sk.Cannon, (b, e) => { // 高准度伦蒂尼姆城防自行炮 · locks the highest max-HP unit, bombards the highest HP% around it
const lock = allTargets(b, e).sort((p, q) => q.s.maxHp - p.s.maxHp)[0];
if (!lock) return;
const x = lock.x, y = lock.y;
b.fx('telegraph', { x, y, r: 1, dur: CANNON_SHOTS, kind: 'cannon' });
for (let i = 1; i <= CANNON_SHOTS; i++) b.after(i, () => {
if (!e.alive) return;
const t = b.alliesInRadius(x, y, 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) => allTargets(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' || Math.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(), { taken(c, b, e, a) { if (a.on) return; a.on = true; b.addBuff(e, { key: 'ab:duckRun', persist: true, mods: { moveMul: T(ab, 'run.attack@move_speed') ?? 1 } }); } }], // 鸭爵 · unblockable; runs once hit
enemy_2001_duckmi_2: (ab) => [unblockable(), { taken(c, b, e, a) { if (a.on) return; a.on = true; b.addBuff(e, { key: 'ab:duckRun', persist: true, mods: { moveMul: T(ab, 'run.attack@move_speed') ?? 1 } }); } }], // 鸭爵 · same
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++) {
const t = alliesInTiles(b, 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: (ab) => [float(), { taken(c, b, e, a) { if (a.on) return; a.on = true; b.addBuff(e, { key: 'ab:parrotRun', duration: T(ab, 'M0SpeedUp.duration') ?? 0, mods: { moveMul: 1 + (T(ab, 'M0SpeedUp.move_speed') ?? 0) } }); } }], // 吉兆飞鳞 · 近地悬浮; 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,
tick(b, e, a) { if (!(a.until > b.time)) return; for (const u of b.alliesInRadius(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'); },
};
return [spin, 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
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');
if (r > 0) for (const u of b.alliesInRadius(t.x, t.y, r)) if (u !== t) for (let i = 0; i < n; i++) hurt(b, e, u, e.s.atk, 'phys');
},
}],
enemy_1050_lslime: (ab, e) => kitLeaderMisc('enemy_1050_lslime', ab, e), // “庞贝” · 4 targets, burning DoT, self-blast when blocked, ASPD up below half
enemy_1500_skulsr: (ab, e) => kitLeaderMisc('enemy_1500_skulsr', ab, e), // 碎骨 · unblocked grenades (splash + DEF down); ATK up below half
enemy_1502_crowns: (ab, e) => kitLeaderMisc('enemy_1502_crowns', ab, e), // 弑君者 · blinks past its blocker
enemy_1504_cqbw: (ab, e) => kitLeaderMisc('enemy_1504_cqbw', ab, e), // W · C4 (2 below half)
enemy_1509_mousek: (ab, e) => kitLeaderMisc('enemy_1509_mousek', ab, e), // 鼠王 · opening barrier with DEF up; enrage below half
enemy_1511_mdrock: (ab, e) => kitLeaderMisc('enemy_1511_mdrock', ab, e), // 泥岩 · stacking ATK, refreshed barrier (+HP/ASPD while up)
enemy_1513_dekght: (ab, e) => kitLeaderMisc('enemy_1513_dekght', ab, e), // 腐败骑士 · plus-shaped hits; rage when 凋零骑士 dies
enemy_1513_dekght_2: (ab, e) => kitLeaderMisc('enemy_1513_dekght_2', ab, e), // 凋零骑士 · 2 targets; rage when 腐败骑士 dies
enemy_1539_reid: (ab, e) => kitLeaderMisc('enemy_1539_reid', ab, e), // “复仇者” · ATK up below half; revives once at 50 %
enemy_2003_rockman: (ab, e) => kitLeaderMisc('enemy_2003_rockman', ab, e), // 迷路的巨像 · long-stun boulder on a non-stunned unit
enemy_2004_balloon: (ab, e) => kitLeaderMisc('enemy_2004_balloon', ab, e), // 喷气人 · takes off (unblockable) when blocked
enemy_2005_axetro: (ab, e) => kitLeaderMisc('enemy_2005_axetro', ab, e), // “遗弃者” · attack stacks, reset after 4 s idle
enemy_2008_flking: (ab, e) => kitLeaderMisc('enemy_2008_flking', ab, e), // “墓碑” · operators' ATK/DEF halved; periodic barrier
enemy_2048_smgrd: (ab, e) => kitLeaderMisc('enemy_2048_smgrd', ab, e), // “邪魔的利刃” · 国度 ASPD-down around it, ×2.5 inside
enemy_2050_smsha: (ab, e) => kitLeaderMisc('enemy_2050_smsha', ab, e), // 陷落雪祀 · cold + 3-target chain
enemy_2052_smgia: (ab, e) => kitLeaderMisc('enemy_2052_smgia', ab, e), // 纠缠藤蔓 · every 3rd attack stuns 15 s; fragile after terrain damage
// --- multi-form leaders and other bounty (悬赏) targets
enemy_1512_mcmstr: kitUglyThing, // “巨大的丑东西” · melee ×3; KO ⇒ stun blast, fleeing 大祭司 (true HP loss/s)
enemy_1516_jakill: kitWarden, // 杰斯顿·威廉姆斯 · ranged arts + multi-stun ⇒ killer form (frees prisoners, armour piercing)
enemy_1517_xi: kitXi, // “自在” · cross arts, 破桎而出 barrier blast ⇒ form 2 (double hits, 2 crosses)
enemy_1525_blkswb: kitMace, // 锏 · 抵抗, DEF pen, 速杀 blink, AoE ⇒ form 2 (stealth, double hits, SP clear)
enemy_1535_wlfmster: kitWolfLord, // 扎罗 · −30 % damage, 溶血骇惧 ⇒ 远古威慑 rebirth ⇒ ranged double hits
enemy_10081_mpplai: kitTranslator, // 转译基底·α · 4 phys / 4 arts hits or being blocked ⇒ one of three forms
enemy_10144_xdelk_2: kitElk, // 乌顶巨角卢鲁 · 角力对决 charge on its blocker (push fails ⇒ damage + stun)
enemy_2085_skzjxd: (ab) => [frontGuard(T(ab, 'Weakness.damage_resistance') ?? 0, faceCrowd)], // 圆仔 · faces the bigger crowd, front damage −80 %
enemy_2085_skzjxd_2: (ab) => [frontGuard(T(ab, 'Weakness.damage_resistance') ?? 0, faceCrowd)], // 圆仔 · same
// 失衡 (pushed / pulled by operators)
enemy_1328_cbjedi: (ab) => [unbalanced((b, e, a, d) => { // 弧光锋卫 · loses HP in proportion to the distance moved while unbalanced
const v = T(ab, 'unbalanced_bleed.damage') ?? 0, iv = T(ab, 'unbalanced_bleed.interval') ?? 1;
if (v > 0 && iv > 0) b.loseHp(e, (v * d) / (iv * UNBALANCE_SPEED), { source: null, tags: ['unbalanced'] });
})],
enemy_10112_ymgds: (ab) => [unbalanced((b, e) => { // 冒失的小弟 · stunned after being unbalanced
const st = T(ab, 'StunAfterUnbalance.stun') ?? 0;
if (st > 0) b.applyStatus(e, 'stun', { duration: st, source: null });
})],
enemy_10138_xdsnow: (ab) => [unbalanced((b, e, a) => { // 雪孩子 · pushed / pulled into high ground ⇒ hitWall.value damage
const dx = e.x - a.lx, dy = e.y - a.ly, d = Math.hypot(dx, dy);
if (!(d > 0)) return;
const r = Math.round(e.y + (dy / d) * 0.6), c = Math.round(e.x + (dx / d) * 0.6);
if (b.grid.isLow(r, c) && b.grid.groundPassable(r, c)) return; // stopped by nothing: no collision
b.fx('explode', { x: e.x, y: e.y, r: 0.4, kind: 'wallHit', id: e.id });
hurt(b, null, e, T(ab, 'hitWall.value') ?? 0, 'true', { tags: ['wallHit'] });
})],
enemy_10141_xdpeng_2: (ab) => [unbalanced((b, e, a) => { // 拥霜羽兽 · unbalanced once ⇒ drops its egg: faster, unblockable
if (a.done) return;
a.done = true;
b.addBuff(e, { key: 'ab:noEgg', persist: true, visible: true, flags: { unblockable: true }, mods: { moveMul: 1 + (T(ab, 'speed.move_speed') ?? 0) } });
})],
});
/** Keys deliberately left to their data stats (reason). */
export const STATS_ONLY = Object.freeze({
enemy_1000_gopro_2: 'plain melee', enemy_1001_bigbo: 'plain leader (bounty)', enemy_1005_yokai: 'FLY, no attack (engine)',
enemy_1005_yokai_2: 'FLY ranged (engine)', enemy_1005_yokai_3: 'FLY ranged (engine)', enemy_1006_shield: 'heavy defender',
enemy_1006_shield_2: 'heavy defender', enemy_1006_shield_3: 'heavy defender', enemy_1007_slime: 'plain',
enemy_1010_demon: 'plain', enemy_1010_demon_2: 'plain', enemy_1041_lazerd: 'FLY arts (engine)', enemy_1041_lazerd_2: 'FLY arts (engine)',
enemy_1043_zomsbr: 'regen is data (hpRecoveryPerSec 80)', enemy_1061_zomshd: 'regen is data (hpRecoveryPerSec 200)',
enemy_1046_agent: 'plain', enemy_1071_dftman: 'plain', enemy_1092_mdgint: 'plain (bounty)',
enemy_1251_lysyta: 'R-series armour interaction absent', enemy_1251_lysyta_2: 'same',
enemy_1252_lysytb_2: 'same', enemy_1254_lypa_2: 'no listed ability', enemy_1325_cbgpro: 'plain', enemy_1325_cbgpro_2: 'plain',
enemy_1367_dseed: 'altar pulse absent (骸骨拷打者 leaves them inert, uncounted, in place)',
enemy_1381_winman: 'plain', enemy_1381_winman_2: 'plain', enemy_1387_winshd: '封冻/供暖器 zones absent', enemy_1415_mmkabi_2: 'chains absent',
enemy_1433_dsbasi: 'plain', enemy_1433_dsbasi_2: 'plain', enemy_1438_dspred: '赘生甲壳 absent',
enemy_2002_bearmi: 'plain', enemy_2002_bearmi_2: 'plain',
enemy_9012_acloon: '炎佑 is a bond summon (bonds.js)', enemy_10040_cnvbln: '狂欢时刻 absent', enemy_10043_sailor: 'bridges/cannons absent',
enemy_10073_mpcar: 'plain', enemy_10124_uashld_2: 'taunt is data; 矿工游击队 absent',
});
// 圣杯 damage sharing: ground enemies near a living chalice pass `share` of the damage they take to it.
function chaliceShare(b, c) {
const st = stOf(b);
if (!(st.chalices > 0)) return;
const t = c.target;
if (!t || t.side !== 'enemy' || t.isFlying || c.dmg.cancel || c.dmg.type === 'element' || (c.dmg.tags && c.dmg.tags.includes('chalice'))) return;
for (const o of b.enemies) {
if (!o.alive || o === t || o.defId !== 'enemy_1430_lrrook' || !o.mem.ab) continue;
const a = o.mem.ab.list[0];
if (!a || !(a.share > 0) || Math.hypot(o.x - t.x, o.y - t.y) > a.r) continue;
const part = c.dmg.amount * a.share;
c.dmg.amount -= part;
b.dealDamage(c.source, o, { amount: part, type: c.dmg.type, canDodge: false, tags: [...(c.dmg.tags || []), 'chalice'] });
break;
}
}