卫戍协议:盟约 web remake — initial import

Online co-op browser remake of Arknights' Stronghold Protocol: Alliance
(client-simulated combat, official waves/bonds/items/strategies, 3D board,
facing wheel, operator loadout, official emotes, deployment scripts).
This commit is contained in:
sganggs committed 2026-09-29 16:21:31 +08:00
commit 01a44cd288
408 files changed
+308986

No files matched your search

+990
View File
@@ -0,0 +1,990 @@
// server/sim/content/bosses.js — scripted leaders (boss_1…boss_10) and their parts (DESIGN §7, research 05 §5, 06 §10.1).
//
// install(battle) ensures the enemy dispatch hooks (enemies.js) and attaches a boss kit to every spawned leader / part
// listed in BOSS_KITS. Damage dealt TO a leader routes to the shared boss pool through the engine (tag 'boss' +
// sharedBoss); parts that "传递伤害" push a share of the damage they take into the pool with battle.loseHp(leader, …)
// credited to the attacker. Numbers come from the (template-overridden) talent / skill blackboards; the rest are named
// constants ([ASSUMED] or PRTS-sourced). Boss HP rules ("<20 %", "≤50 %") read the leader's HP ratio, which the engine
// syncs from the shared pool, so both mirrored copies switch phase together.
//
// Wave template: branches / extraRoutes / devices / patrol steps are read from the template the battle was built
// from — `opts.waveTemplate|template|templateId|waveId` when the match passes it, else inferred from data/waves.json
// (the template whose leader spawn matches and whose route list equals battle.routes). Without a template the
// fallbacks spawn summons at the leader and route them to the nearest goal. Branch spawns keep their literal key
// unless the match passes the round's placeholder replacements as `opts.slotKeys` ({ NF: key, N: key, … }).
// · patrol: a leader / part walking `patrol` steps loops them (never reaches the goal: 卢西恩 lpr 30, 铳, 碎铳之簧).
// · 'boss_battle_multi_player' (铳, 卢西恩): with ≥2 players on the field the mirrored copy is spawned from the branch
// (skipped if the match already spawned a second copy, or opts.bossMirror === false).
//
// Leaders (see each kit for details):
// boss_1/8 假想敌:胄 random arts beam (range 8); 灭顶之灾 刺胄之弹 at the highest-ATK operator (8-hit flying shell,
// 3×3 stun + phys DoT on arrival); <20 %: damage ×0.5 + 2 shells; 死亡集群 drones — a drone
// killed by an operator costs the leader 2 % max HP. boss_8 adds 斩胄之剑 / 破胄之锤.
// 斩胄之剑 / 破胄之锤 hover invulnerable next to 胄, AoE attacks; dive at the lowest-ATK operator (3×3 stun + DoT);
// 15 hits during the dive shoot it down: grounded 20 s, damage ×1.3, PART_TRANSFER to 胄.
// boss_2/9 假想敌:铳 unblockable; highest-DEF target in range; erosion; ASPD ramp (+80 × 5) on the same target
// (floor 20); 最终之罚 charge at the highest-DEF ground unit (disabled by the h07_02 override).
// boss_9: damage ×0.2 while springs live, 盲信之誓 links (100 phys/s on the lines), 末日布道 dash.
// 碎铳之簧 a/b/c arts barrier (absorbs arts after RES; phys ×0.1, counter phys + erosion) / element shield
// (phys+arts ×0.1, broken by its own element burst; every (spCost+1)-th attack a bouncing erosion
// shot) / 5-hit shield (every (spCost+1)-th attack a ten-hit combo); unblockable while shielded;
// shield back 25 s after breaking; PART_TRANSFER of damage to 铳 and, split, to the other springs.
// boss_3/10 假想敌:管 summons 余音 (dark) every 40 s (20 s < 50 %); normal attacks strike dark 余音 (their pulse hurts
// operators); 裂管之奏 3 strikes on every dark 余音 (AoE arts + apoptosis). boss_10 + 假想敌:弦
// (invulnerable, gold 余音, 断弦之奏 AoE + flips them dark; leaves with 管).
// 余音 10-hit unit, block ≥ 2 only; gold (ASPD +50) / dark (ATK +50 %, slower) forms that swap after
// 10 strikes; pulses on every hit taken; 合奏 every 5 s.
// boss_4 盐风主教昆图斯 2 highest-DEF targets (+ neural); 3 growth stages (33 % HP lost or 75 s / 200 s): 大潮 global
// arts + neural, 崩坍 delayed strikes on 2/4/8 highest-DEF units, 断裂生殖 tentacles (stun),
// 物种爆发 LP loss (stage 3).
// boss_5 卢西恩 evade 40 % while unblocked, DEF pen 40 %, neural on hit; AoE skill (radius 2); blinks past its
// blocker leaving a 不祥幻影 (same kit minus blink). Template overrides (0.2 / 0.12) apply.
// boss_6 阿利斯泰尔 王权号令 (SP 1/s, plus-shaped 200 % strike + random equipment drop); 莫非王土 animates the
// latest equipment on the field (2 s after it appears, then once per 王权号令 cycle) (sword strike / directional vest + barrier / wand disarm); ≤50 %: DEF +100 %,
// RES +45, animates 2; 斥退 at 999 s.
// boss_7 “萨米的意志” 冰凌 (its normal attack) hits a whole column; 自然涌动 stun + arts DoT; <50 %: damage taken
// ×(1−0.6), 2 targets; Doom at 600 s (LP −30). Targets via fairOrder (two players alternate).
// Every leader (tag boss) ignores 侵蚀 gauge damage ("最终攻势中,敌方领袖不会受到侵蚀损伤").
// LP effects ('lpLoss' hook + result.lpLoss) must be applied by the match (see the report of this module's owner).
// fx kinds: 'beam' 'shell' 'explode' 'telegraph' 'charge' 'link' 'dash' 'column' 'tide' 'rockfall' 'tentacle' 'equip'
// 'sword' 'vest' 'blink' 'summon' 'grow' 'phase' 'lpLoss' (x, y + extra {id, r, tiles, kind, tx, ty …}).
import { MOVE_SCALE } from '../constants.js';
import { canTargetAlly } from '../targeting.js';
import { compileRoute } from '../ai.js';
import { normalizeRoute } from '../simdata.js';
import {
ensureInstalled, abOf, attach, T, elem, hurt, alliesInTiles, targetsNear, allTargets, byPriority,
remainingRoute, stayRoute, stepToward, setHits, hitCount, lpLoss, blinkForward, canCast, absorbArts, nthOf,
} from './enemies.js';
// ---------------------------------------------------------------------------------------------------------------
// constants
/** Share of the damage a grounded 剑/锤 or a 碎铳之簧 passes to its leader ("以一定比例") [ASSUMED]. */
export const PART_TRANSFER = 0.5;
/** 卢西恩 / 不祥幻影 AoE radius (PRTS "半径2"). */
const LUCIEN_AOE_RADIUS = 2;
/** 余音 on-hit pulse radius (PRTS 1.6) and 合奏 radii per form (PRTS gold 0.8 / dark 1.6). */
const ECHO_PULSE_RADIUS = 1.6;
const ECHO_ENSEMBLE_RADIUS = Object.freeze({ gold: 0.8, dark: 1.6 });
/** "只能被阻挡数大于等于2的单位阻挡". */
const ECHO_BLOCK_WEIGHT = 2;
/** 裂管之奏 strike spacing (s). */
const PIPE_STRIKE_GAP = 0.4;
/** 断裂生殖 tentacle: stun duration and reach around its device tile [ASSUMED]. */
const TENTACLE_STUN = 5, TENTACLE_RADIUS = 1;
/** 崩坍 delay (PRTS 1 s), targets per growth stage (PRTS 2/4/8) and 物种爆发 delay (PRTS 4 s). */
const ROCKFALL_DELAY = 1, ROCKFALL_TARGETS = [2, 4, 8], DOOM_DELAY = 4;
/** 铳: minimum attack speed (PRTS "最低20") and charge hit radius (PRTS 0.35). */
const ASPD_FLOOR = 20, CHARGE_RADIUS = 0.35;
/** 盲信之誓 link half-width (PRTS 0.5). */
const LINK_WIDTH = 0.5;
/** 碎铳之簧 bullet bounce falloff / reach [ASSUMED], combo = 十连击; tag of the damage share passed between springs. */
const SPRING_BOUNCE_FALLOFF = 0.85, SPRING_BOUNCE_RANGE = 2, SPRING_COMBO_HITS = 10, SPRING_SHARE_TAG = 'springShare';
/** 阿利斯泰尔: enemy SP per second [ASSUMED] and 莫非王土 delay after each 王权号令 [ASSUMED]. */
const ENEMY_SP_PER_SEC = 1, ANIMATE_DELAY = 2;
const EQUIP_KEYS = Object.freeze(['enemy_10028_vtswd', 'enemy_10029_vtshld', 'enemy_10030_vtwand']);
const SHELL_KEY = 'enemy_9016_acstmr', ECHO_KEY = 'enemy_9023_acdums', PHANTOM_KEY = 'enemy_2017_csphts';
// ---------------------------------------------------------------------------------------------------------------
// per-battle state, template resolution
const STATE = new WeakMap();
function stOf(b) {
let st = STATE.get(b);
if (!st) { st = { tpl: undefined, mirrored: new Set() }; STATE.set(b, st); }
return st;
}
const pairEq = (a, b) => Array.isArray(a) && Array.isArray(b) && Math.abs(a[0] - b[0]) < 1e-6 && Math.abs(a[1] - b[1]) < 1e-6;
/** The wave template this battle was built from (or null). */
export function templateOf(b) {
const st = stOf(b);
if (st.tpl !== undefined) return st.tpl;
const o = b.opts || {};
for (const cand of [o.waveTemplate, o.template, o.templateId, o.waveId]) {
if (!cand) continue;
const t = typeof cand === 'string' ? b.data.getWave(cand) : cand;
if (t && typeof t === 'object') { st.tpl = t; return t; }
}
let best = null, bestScore = 0;
const ids = typeof b.data.waveIds === 'function' ? b.data.waveIds() : [];
for (const id of ids) {
const w = b.data.getWave(id);
if (!w || !Array.isArray(w.spawns) || !Array.isArray(w.routes)) continue;
if (!w.spawns.some((s) => s && s.tag === 'boss' && BOSS_KITS[s.key ?? s.enemyKey])) continue;
if (w.routes.length !== b.routes.length) continue;
let score = 1;
if (w.routes.every((r, i) => { const n = normalizeRoute(r); const m = b.routes[i]; return m && pairEq(n.start, m.start) && pairEq(n.end, m.end); })) score += 8;
if (/_s$/.test(id) === (b.players.length <= 1)) score += 1;
if (score > bestScore) { best = w; bestScore = score; }
}
st.tpl = bestScore >= 9 ? best : null;
return st.tpl;
}
const legSig = (legs) => legs.map((l) => `${l.t}:${l.r ?? ''},${l.c ?? ''},${l.time ?? ''}`).join('|');
/** Raw template route an enemy is walking (matched on compiled legs). */
function rawRouteOf(b, e, tpl) {
if (!tpl || !e.route || !Array.isArray(e.route.legs)) return null;
const sig = legSig(e.route.legs);
for (const r of [...(tpl.routes || []), ...(tpl.extraRoutes || [])]) {
if (!r) continue;
try { if (legSig(compileRoute(normalizeRoute(r), b.rect)) === sig) return r; } catch { /* skip */ }
}
return null;
}
/** Fallback route from a position to the nearest goal. */
function toGoal(b, x, y, motion = 'WALK') {
const ends = b.grid.specialTiles('end');
let best = ends[0] ?? [Math.round(y), Math.round(x)], bd = Infinity;
for (const p of ends) { const d = Math.hypot(p[0] - y, p[1] - x); if (d < bd) { bd = d; best = p; } }
return { motion, start: [y, x], end: best, checkpoints: [] };
}
/** Spawn one phase of a template branch. Returns the spawned enemies. */
function branchSpawn(b, tpl, name, phaseIdx, { fallback = null, mods = null, at = null } = {}) {
const br = tpl && tpl.branches && tpl.branches[name];
if (!Array.isArray(br) || !br.length) return fallback ? fallback() : [];
const phase = br[Math.max(0, Math.min(phaseIdx, br.length - 1))] || [];
const out = [];
for (const s of phase) {
if (!s || (s.action && String(s.action).toUpperCase() !== 'SPAWN')) continue;
// placeholder slots (NF/N…) of branch spawns: the match may pass the round's replacements as opts.slotKeys
const key = (s.slot && b.opts && b.opts.slotKeys && b.opts.slotKeys[s.slot]) || s.key || s.enemyKey;
if (!key || !String(key).startsWith('enemy_')) continue;
const route = tpl.extraRoutes && tpl.extraRoutes[s.routeIndex];
for (let i = 0; i < Math.max(1, s.count ?? 1); i++) {
const u = b.spawnEnemy(key, { route: route || (at ? toGoal(b, at.x, at.y) : undefined), pos: route ? undefined : at ? [at.y, at.x] : undefined, tag: s.tag ?? null, countInTotal: s.unharmful ? false : undefined, mods });
if (u) out.push(u);
}
}
return out;
}
// ---------------------------------------------------------------------------------------------------------------
// install
export function install(battle) {
ensureInstalled(battle);
battle.on('enemySpawn', ({ enemy }) => onSpawn(battle, enemy), { priority: 90 });
// "最终攻势中,敌方领袖不会受到侵蚀损伤" (research 06 §10.1): leaders ignore erosion gauge damage
battle.on('elementHit', (c) => { if (c.target && c.target.isBoss && c.dmg.element === 'erosion') c.dmg.cancel = true; }, { priority: 100 });
}
export function registerMeta() {}
function onSpawn(b, e) {
const kit = BOSS_KITS[e.defId];
if (typeof kit !== 'function') return;
const tpl = templateOf(b);
const ab = abOf(b, e);
let list = [];
try { list = kit(ab, e, b, tpl) || []; } catch (err) { b._handlerError('content:bosses', e, err); }
attach(b, e, list);
// patrol loop for leaders / parts
const raw = rawRouteOf(b, e, tpl);
if (raw && Array.isArray(raw.steps) && raw.steps.some((s) => s && s.t === 'patrol')) attach(b, e, [patrolLoop()]);
// mirrored leader for 2 players (boss_battle_multi_player)
if (e.isBoss || e.tag === 'boss') mirror(b, e, tpl);
}
function patrolLoop() {
return {
tick(b, e) {
const R = e.route;
if (!R || !Array.isArray(R.legs) || R.legs.length < 2) return;
if (R.legIdx >= R.legs.length - 1) { R.legIdx = 0; R.pts = null; R.waitLeft = null; }
},
};
}
function mirror(b, e, tpl) {
const st = stOf(b);
const br = tpl && tpl.branches && tpl.branches.boss_battle_multi_player;
if (!Array.isArray(br) || !br.length || st.mirrored.has(e.defId) || (b.opts && b.opts.bossMirror === false)) return;
if (b.players.length < 2 || e.mem.mirror) return;
st.mirrored.add(e.defId);
for (const s of br[0] || []) {
const key = s.key ?? s.enemyKey;
if (key !== e.defId) continue;
b.after(Math.max(0, (s.time ?? 0) - b.time), () => {
if (b.aliveEnemies().filter((o) => o.defId === key && o.isBoss).length >= 2) return; // the match already did it
const route = tpl.extraRoutes && tpl.extraRoutes[s.routeIndex];
const u = b.spawnEnemy(key, { route: route || undefined, tag: 'boss', mods: e.mods });
if (u) u.mem.mirror = true;
});
}
}
// ---------------------------------------------------------------------------------------------------------------
// shared bits
const opsOnly = (list) => list.filter((u) => u.kind === 'op');
const nearestOf = (b, e, pred) => {
let best = null, bd = Infinity;
for (const o of b.enemies) { if (!o.alive || o === e || !pred(o)) continue; const d = Math.hypot(o.x - e.x, o.y - e.y); if (d < bd) { bd = d; best = o; } }
return best;
};
/** Apply a floor to an enemy whose data attack speed is 0 (skill-driven leaders: 铳/管/弦). */
function aspdFloor(b, e, extra = 0) {
const rawAspd = e.def.raw && e.def.raw.stats ? Number(e.def.raw.stats.aspd) : NaN;
if (!(rawAspd === 0)) return;
b.addBuff(e, { key: 'boss:aspd', persist: true, mods: { aspd: Math.max(ASPD_FLOOR, rawAspd + extra) - e.base.aspd } });
}
/**
* Engine target priority (blocker → taunt → latest deployed), made fair for two players on one boss field: within the top
* priority tier the picks alternate between the players' units, starting with the other player each call. Both
* players deploy at t = 0, so "latest deployed" alone would always single out the second player [ASSUMED fairness
* rule]. `P.turn` keeps the rotation.
*/
export function fairOrder(b, e, list, P) {
const l = byPriority(e, list);
if (b.players.length < 2 || l.length < 2) return l;
const key = (u) => `${e.blockedBy === u ? 0 : 1}|${u.s.taunt || 0}`;
const top = key(l[0]);
const tier = l.filter((u) => key(u) === top), rest = l.filter((u) => key(u) !== top);
const ids = b.players.map((p) => p.playerId);
P.turn = ((P.turn ?? -1) + 1) % ids.length;
const queues = ids.map((pid) => tier.filter((u) => u.ownerId === pid));
const out = [];
for (let k = 0; out.length < tier.length && k < tier.length * ids.length + ids.length; k++) {
const q = queues[(P.turn + k) % ids.length];
if (q.length) out.push(q.shift());
}
for (const u of tier) if (!out.includes(u)) out.push(u);
return out.concat(rest);
}
/** Stun + phys DoT on the 3×3 around (r, c). */
function stunBlast(b, src, r, c, stun, dot, dur, kind) {
b.fx('explode', { x: c, y: r, r: 1.5, kind, tiles: 'box' });
for (const u of alliesInTiles(b, r, c, 'box', 1)) {
if (stun > 0) b.applyStatus(u, 'stun', { duration: stun, source: src });
if (dot > 0 && dur > 0) b.addBuff(u, { key: `boss:${kind}Dot`, duration: dur, refresh: 'replace', interval: 1, visible: true,
onTick: ({ battle, unit }) => battle.dealDamage(src, unit, { amount: dot, type: 'phys', canDodge: false, tags: ['enemyAbility', kind] }) });
}
}
/** 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));
}
// ---------------------------------------------------------------------------------------------------------------
// 假想敌:胄 (boss_1 / boss_8) + 刺胄之弹 + 斩胄之剑 / 破胄之锤
function kitHelm(ab, e, b, tpl) {
const s1 = ab.sk['1'], s2 = ab.sk['2'];
const lowRatio = T(ab, '1.hp_ratio') ?? 0, lowScale = T(ab, '1.damage_scale') ?? 1;
const hidden = /_2$/.test(e.defId);
const P = { low: false };
const range = () => e.base.rangeRadius || 8;
const shellTargets = (b2) => opsOnly(hidden ? targetsNear(b2, e, range()) : allTargets(b2, e));
return [
{
spawn(b2, e2, a, ab2) { ab2.atkType = 'arts'; },
before(c, b2, e2) { const l = targetsNear(b2, e2, range()); if (l.length) c.targets = [b2.rng.pick(l)]; }, // 随机选择目标
attack(c, b2, e2) { const t = c.targets[0]; if (t) b2.fx('beam', { x: e2.x, y: e2.y, from: e2.id, to: t.id, kind: 'helmRay' }); },
iv: 0.25,
tick(b2, e2) {
if (P.low || !(e2.hpRatio < lowRatio)) return;
P.low = true; // <20 %: damage taken ×0.5, extra shell
b2.addBuff(e2, { key: 'boss:helmGuard', persist: true, visible: true, mods: { physTakenMul: lowScale, artsTakenMul: lowScale } }); // 物理与法术伤害降低
b2.fx('phase', { x: e2.x, y: e2.y, id: e2.id, kind: 'helmLow' });
},
},
s1 && {
cd: s1.cd, icd: s1.icd, cond: (b2) => shellTargets(b2).length > 0,
fire(b2, e2) { // 【灭顶之灾】
const n = P.low ? 2 : 1;
const ts = shellTargets(b2).sort((p, q) => q.s.atk - p.s.atk || q.deploySeq - p.deploySeq).slice(0, n);
for (const t of ts) fireShell(b2, e2, t);
},
},
s2 && {
cd: s2.cd, icd: s2.icd,
fire(b2, e2) { // 【死亡集群】: drones; one killed by an operator costs the leader hp_ratio × max HP
const hpMul = s2.bb['summon.hp_ratio'] > 0 ? s2.bb['summon.hp_ratio'] : null;
const key = s2.bs.enemy_key ?? 'enemy_1005_yokai';
const drones = branchSpawn(b2, tpl, s2.bs.branch_id ?? 'boss_summon_enemy', 0, {
mods: hpMul ? { hpMul } : null, at: { x: e2.x, y: e2.y },
fallback: () => [b2.spawnEnemy(key, { pos: [e2.y, e2.x], route: toGoal(b2, e2.x, e2.y, 'FLY'), mods: hpMul ? { hpMul } : null })].filter(Boolean),
});
b2.fx('summon', { x: e2.x, y: e2.y, id: e2.id, key, n: drones.length });
const ratio = s2.bb.hp_ratio ?? 0;
for (const d of drones) attach(b2, d, [{
death(c, b3) {
if (c.reason !== 'killed' || !c.killer || c.killer.side !== 'ally' || !(ratio > 0)) return;
const boss = e2.alive ? e2 : b3.aliveEnemies().find((o) => o.isBoss && o.defId === e2.defId);
if (boss) { b3.loseHp(boss, boss.s.maxHp * ratio, { source: c.killer }); b3.fx('beam', { x: d.x, y: d.y, from: d.id, to: boss.id, kind: 'droneLink' }); }
},
}]);
},
},
];
}
/** Launch a 刺胄之弹 from `boss` at `target`'s tile. */
function fireShell(b, boss, target) {
const tr = target.tileR, tc = target.tileC;
const sh = b.spawnEnemy(SHELL_KEY, { pos: [boss.y, boss.x], route: { motion: 'FLY', start: [boss.y, boss.x], end: [tr, tc], steps: [{ t: 'wait', s: 99999 }] }, tag: 'part', countInTotal: false, ownerPlayerId: boss.ownerId });
if (!sh) return null;
sh.mem.ab.shell = { tr, tc, boss };
b.fx('shell', { x: boss.x, y: boss.y, id: sh.id, tx: tc, ty: tr, r: 1.5, kind: 'helmShell' });
return sh;
}
function kitShell(ab, e) {
const stun = T(ab, 'killed.duration') ?? 0, dot = T(ab, 'killed.value') ?? 0;
return [{
spawn(b, e2) { setHits(e2, e2.def.maxHp); hitCount(b, e2, true); }, // 需要数次攻击击倒
tick(b, e2, a, dt) {
const s = e2.mem.ab.shell;
if (!s || a.done) return;
if (!stepToward(e2, s.tc, s.tr, e2.s.moveSpeed * MOVE_SCALE * dt)) return;
a.done = true; // arrival: 3×3 long stun + phys DoT
stunBlast(b, s.boss && s.boss.alive ? s.boss : e2, s.tr, s.tc, stun, dot, stun, 'helmShell');
b.kill(e2, null);
},
}];
}
function kitBlade(ab, e, b, tpl) {
const s = ab.sk['1'];
const bb = s ? s.bb : {};
const P = { state: 'hover', home: { x: e.x, y: e.y }, hits: 0, target: null };
const leader = (b2) => nearestOf(b2, e, (o) => o.isBoss && /enemy_9013_acstmk/.test(o.defId));
const hover = (b2, on) => {
if (on) b2.addBuff(e, { key: 'boss:hover', persist: true, visible: true, flags: { invulnerable: true, untargetable: true } });
else b2.removeBuff(e, 'boss:hover');
};
return [
{
spawn(b2, e2) { hover(b2, true); b2.addBuff(e2, { key: 'boss:anchor', persist: true, flags: { noMove: true } }); },
before(c, b2, e2) { const l = targetsNear(b2, e2, e2.base.rangeRadius || 1.6, { ranged: false }); if (l.length) c.targets = l; }, // 范围物理伤害
taken(c, b2, e2) {
if (P.state === 'dive' && ++P.hits >= (bb.max_hit_cnt ?? Infinity)) {
// shot down: ground unit, damage ×damage_scale, stunned special_stun_duration, then flies home
P.state = 'down';
e2.motion = 'WALK';
b2.addBuff(e2, { key: 'boss:downed', duration: bb.special_stun_duration ?? 0, visible: true, mods: { dmgTakenMul: bb.damage_scale ?? 1 },
onExpire: () => { if (e2.alive) { e2.motion = 'FLY'; P.state = 'return'; } } });
b2.applyStatus(e2, 'stun', { duration: bb.special_stun_duration ?? 0, source: null, force: true });
b2.fx('phase', { x: e2.x, y: e2.y, id: e2.id, kind: 'shotDown' });
return;
}
if (P.state === 'down' && c.source && c.source.side === 'ally' && c.amount > 0) {
const L = leader(b2);
if (L) b2.loseHp(L, c.amount * PART_TRANSFER, { source: c.source, from: c.dmg }); // 被击落时受到伤害以一定比例传递给假想敌:胄
}
},
tick(b2, e2, a, dt) {
const sp = e2.s.moveSpeed * MOVE_SCALE * dt;
if (P.state === 'dive') {
const t = P.target;
if (!stepToward(e2, t.c, t.r, sp)) return;
stunBlast(b2, e2, t.r, t.c, bb.stun ?? 0, bb.dot_damage ?? 0, bb.dot_duration ?? 0, 'bladeDive');
P.state = 'return';
} else if (P.state === 'return') {
if (stepToward(e2, P.home.x, P.home.y, sp)) { P.state = 'hover'; hover(b2, true); }
}
},
},
s && {
cd: s.cd, icd: s.icd, cond: (b2) => P.state === 'hover' && opsOnly(allTargets(b2, e)).length > 0,
fire(b2, e2) { // fly at the lowest-ATK operator
const t = opsOnly(allTargets(b2, e2)).sort((p, q) => p.s.atk - q.s.atk || p.deploySeq - q.deploySeq)[0];
if (!t) return;
P.state = 'dive'; P.hits = 0; P.target = { r: t.tileR, c: t.tileC };
hover(b2, false);
b2.fx('charge', { x: e2.x, y: e2.y, id: e2.id, tx: t.tileC, ty: t.tileR, kind: 'bladeDive' });
},
},
];
}
// ---------------------------------------------------------------------------------------------------------------
// 假想敌:铳 (boss_2 / boss_9) + 碎铳之簧
const isGun = (o) => /enemy_9017_achunt/.test(o.defId);
const isSpring = (o) => /enemy_90(18|19|20)_actrp/.test(o.defId);
function kitGun(ab, e, b) {
const hidden = /_2$/.test(e.defId);
const step = T(ab, '2.attack_speed') ?? 0, maxN = T(ab, '2.max_stack_cnt') ?? 0;
const P = { last: null, n: 0, charge: null };
const range = () => e.base.rangeRadius || 2;
const setAspd = (b2) => aspdFloor(b2, e, step * P.n);
const s1 = ab.sk['1'], s2 = ab.sk['2'], s3 = ab.sk['3'];
const groundTargets = (b2) => allTargets(b2, e).filter((u) => u.ground);
const list = [
{
spawn(b2, e2) { b2.addBuff(e2, { key: 'boss:unblockable', persist: true, flags: { unblockable: true } }); setAspd(b2); },
before(c, b2, e2) { const l = targetsNear(b2, e2, range()); if (l.length) c.targets = [l.sort((p, q) => q.s.def - p.s.def || q.deploySeq - p.deploySeq)[0]]; }, // 优先攻击防御力最高的单位
attack(c, b2) {
const t = c.targets[0];
if (t === P.last) P.n = Math.min(maxN, P.n + 1); else { P.last = t; P.n = 0; }
setAspd(b2);
},
dealt(c, b2, e2) { elem(b2, e2, c.target, 'erosion', e2.s.atk * (T(ab, '1.ep_damage_ratio') ?? 0)); }, // 攻击时附带侵蚀损伤
tick(b2, e2, a, dt) {
const ch = P.charge;
if (!ch) return;
const t = ch.target;
const sp = (e2.s.moveSpeed + (T(ab, '3.move_speed') ?? 0)) * MOVE_SCALE * dt;
const nx = e2.x, ny = e2.y;
const arrived = stepToward(e2, t.x, t.y, sp);
if (!b2.grid.groundPassable(Math.round(e2.y), Math.round(e2.x))) { e2.x = nx; e2.y = ny; }
for (const u of b2.alliesInRadius(e2.x, e2.y, CHARGE_RADIUS)) if (!ch.hit.has(u)) { ch.hit.add(u); hurt(b2, e2, u, e2.s.atk * (T(ab, '3.atk_scale') ?? 1), 'phys'); }
if (arrived || b2.time >= ch.until || (e2.x === nx && e2.y === ny)) {
P.charge = null;
b2.removeBuff(e2, 'boss:charge');
if (e2.route) e2.route.pts = null;
}
},
},
s1 && {
cd: s1.cd, icd: s1.icd, cond: (b2) => groundTargets(b2).length > 0 && !P.charge,
fire(b2, e2) { // 【最终之罚】 charge at the highest-DEF ground unit
const t = groundTargets(b2).sort((p, q) => q.s.def - p.s.def || q.deploySeq - p.deploySeq)[0];
if (!t) return;
const dur = T(ab, '3.duration') ?? 0;
P.charge = { target: { x: t.x, y: t.y }, until: b2.time + dur, hit: new Set() };
b2.addBuff(e2, { key: 'boss:charge', duration: dur, visible: true, flags: { noMove: true, disarm: true } });
b2.fx('charge', { x: e2.x, y: e2.y, id: e2.id, tx: t.x, ty: t.y, kind: 'finalPenance' });
},
},
];
if (hidden) {
list.push({
iv: 0.25,
tick(b2, e2) { // 【未尽的告解】 physical / arts damage taken ×4.damage_scale while the 碎铳之簧 it came with remain
const springs = b2.enemies.some((o) => o.alive && isSpring(o));
if (springs) { const m = T(ab, '4.damage_scale') ?? 1; b2.addBuff(e2, { key: 'boss:confession', duration: 0.35, refresh: 'replace', mods: { physTakenMul: m, artsTakenMul: m } }); }
},
});
if (s3) list.push({
iv: s3.bb.interval ?? 1,
tick(b2, e2) { // 【盲信之誓】 links: phys per second on operators standing on a line
for (const sp of b2.enemies) {
if (!sp.alive || !isSpring(sp)) continue;
b2.fx('link', { x: e2.x, y: e2.y, from: e2.id, to: sp.id, kind: 'faithLink', dur: s3.bb.interval ?? 1 });
for (const u of b2.allies()) if (segDist(u.x, u.y, e2.x, e2.y, sp.x, sp.y) <= LINK_WIDTH) hurt(b2, e2, u, s3.bb.value ?? 0, 'phys');
}
},
});
if (s2) list.push({
cd: s2.cd, icd: s2.icd, cond: (b2) => b2.enemies.some((o) => o.alive && isSpring(o)),
fire(b2, e2) { // 【末日布道】 springs dash to 铳, invulnerable, trampling operators
for (const sp of b2.enemies) {
if (!sp.alive || !isSpring(sp) || !sp.mem.ab) continue;
sp.mem.ab.dash = { until: b2.time + (s2.bb.dog_duration ?? 0), mul: 1 + (s2.bb.move_speed ?? 0), gun: e2, hit: new Set() };
b2.addBuff(sp, { key: 'boss:dash', duration: s2.bb.dog_duration ?? 0, visible: true, flags: { invulnerable: true, noMove: true, unblockable: true } });
b2.fx('dash', { x: sp.x, y: sp.y, id: sp.id, tx: e2.x, ty: e2.y });
}
},
});
}
return list;
}
function kitSpring(ab, e) {
const kind = /9018/.test(e.defId) ? 'arts' : /9019/.test(e.defId) ? 'element' : 'hits';
const scale = T(ab, '1.damage_scale') ?? 1, regen = T(ab, '1.regenerate_duration') ?? 0;
const P = { up: false, barrier: 0, hits: 0, downAt: null };
const gun = (b) => nearestOf(b, e, (o) => isGun(o));
const raise = (b) => {
P.up = true;
if (kind === 'arts') P.barrier = e.s.maxHp * (T(ab, '1.shield_ratio') ?? 0);
if (kind === 'hits') P.hits = T(ab, '1.block_damage_max_times') ?? 0;
b.addBuff(e, { key: 'boss:springShield', persist: true, visible: true, flags: { unblockable: true } }); // 护盾生效时,自身无法被阻挡
b.fx('phase', { x: e.x, y: e.y, id: e.id, kind: `spring_${kind}` });
};
const drop = (b) => { P.up = false; P.downAt = b.time; b.removeBuff(e, 'boss:springShield'); b.fx('shieldBreak', { x: e.x, y: e.y, id: e.id }); };
const sk = ab.sk['1'];
return [
{
spawn(b) { raise(b); },
hitIn(c, b, e2) {
if (!P.up) return;
const ty = c.dmg.type;
if (kind === 'arts') { // 法术屏障: absorbs arts (after RES, like a shield); phys ×0.1 + counter
if (ty === 'arts') {
P.barrier -= absorbArts(c, P.barrier);
if (P.barrier <= 1e-6) { P.barrier = 0; drop(b); }
} else if (ty === 'phys') {
c.dmg.mul *= scale;
const s = c.source;
if (s && s.side === 'ally' && s.alive) { hurt(b, e2, s, e2.s.atk * (T(ab, '1.atk_scale') ?? 0), 'phys'); elem(b, e2, s, 'erosion', e2.s.atk * (T(ab, '1.ep_damage_ratio') ?? 0)); }
}
} else if (kind === 'element') { // 元素护盾: phys/arts heavily reduced
if (ty === 'phys' || ty === 'arts') c.dmg.mul *= scale;
} else if (ty === 'phys' || ty === 'arts' || ty === 'true') { // 频次护盾: negates N hits
c.dmg.cancel = true;
if (--P.hits <= 0) drop(b);
}
},
burst(c, b) { if (kind === 'element' && P.up) drop(b); }, // 自身元素损伤爆发时,护盾消失
taken(c, b, e2) {
if (!c.source || c.source.side !== 'ally' || !(c.amount > 0)) return;
if (c.dmg.tags && c.dmg.tags.includes(SPRING_SHARE_TAG)) return; // a share never passes on again
// 【盲信之誓】 受到伤害时以一定比例传递给假想敌:铳 (and, split, to the other springs — 隐秘核心 text)
const g = gun(b);
if (g) b.loseHp(g, c.amount * PART_TRANSFER, { source: c.source, from: c.dmg });
const others = b.enemies.filter((o) => o.alive && o !== e2 && isSpring(o));
for (const o of others) b.loseHp(o, (c.amount * PART_TRANSFER) / others.length, { source: c.source, from: c.dmg, tags: [SPRING_SHARE_TAG] });
},
tick(b, e2, a, dt) {
if (!P.up && P.downAt != null && b.time - P.downAt >= regen) raise(b);
const d = e2.mem.ab.dash;
if (!d) return;
const g = d.gun && d.gun.alive ? d.gun : gun(b);
const arrived = !g || stepToward(e2, g.x, g.y, e2.s.moveSpeed * d.mul * MOVE_SCALE * dt) || Math.hypot(g.x - e2.x, g.y - e2.y) < 1;
for (const u of b.alliesInRadius(e2.x, e2.y, 0.5)) if (!d.hit.has(u)) { d.hit.add(u); hurt(b, e2, u, e2.s.atk, 'phys'); }
if (arrived || b.time >= d.until) { e2.mem.ab.dash = null; b.removeBuff(e2, 'boss:dash'); if (e2.route) e2.route.pts = null; }
},
},
// while shielded, every (spCost+1)-th attack (enemy SP +1 per attack, as 粉碎攻坚手's official text) adds the shield's
// special attack on that attack's target
(kind === 'element' || kind === 'hits') && sk && {
attack(c, b, e2, a) {
if (!P.up) return;
a.n = (a.n ?? 0) + 1;
if (a.n % nthOf(sk)) return;
const t0 = c.targets.find((u) => u && u.alive);
if (!t0) return;
if (kind === 'element') { // bouncing element bullet: `times` targets, weaker after each bounce
const times = sk.bb.times || 1, ratio = sk.bb.ep_damage_ratio || 0;
let t = t0;
const hit = new Set();
for (let k = 0; k < times && t; k++) {
hit.add(t);
b.fx('beam', { x: e2.x, y: e2.y, from: e2.id, to: t.id, kind: 'springBullet' });
elem(b, e2, t, 'erosion', e2.s.atk * ratio * Math.pow(SPRING_BOUNCE_FALLOFF, k));
const prev = t;
t = b.alliesInRadius(prev.x, prev.y, SPRING_BOUNCE_RANGE).filter((u) => !hit.has(u)).sort((p, q) => Math.hypot(p.x - prev.x, p.y - prev.y) - Math.hypot(q.x - prev.x, q.y - prev.y) || q.deploySeq - p.deploySeq)[0];
}
} else { // 十连击
b.fx('beam', { x: e2.x, y: e2.y, from: e2.id, to: t0.id, kind: 'springCombo' });
for (let i = 0; i < SPRING_COMBO_HITS && t0.alive; i++) hurt(b, e2, t0, e2.s.atk * (sk.bb.atk_scale || 0), 'phys');
}
},
},
];
}
// ---------------------------------------------------------------------------------------------------------------
// 假想敌:管 (boss_3 / boss_10) + 假想敌:弦 + “余音”
const echoesOf = (b, form) => b.enemies.filter((o) => o.alive && o.defId === ECHO_KEY && o.mem.ab && o.mem.ab.form === form);
/** Switch an echo's form. */
export function setEchoForm(b, echo, form) {
const ab = echo.mem.ab;
if (!ab) return;
ab.form = form;
ab.strikes = 0;
const mods = form === 'gold' ? { aspd: T(ab, '1.attack_speed') ?? 0 } : { atkPct: T(ab, '2.atk') ?? 0, moveMul: 1 - (T(ab, '2.move_speed') ?? 0) };
b.addBuff(echo, { key: 'boss:echoForm', persist: true, visible: true, mods });
b.fx('phase', { x: echo.x, y: echo.y, id: echo.id, kind: `echo_${form}` });
}
/** An echo takes a strike/hit: pulse around it and count towards the form switch. */
export function echoHit(b, echo) {
const ab = echo.mem.ab;
if (!ab || !echo.alive) return;
const atk = echo.s.atk;
b.fx('explode', { x: echo.x, y: echo.y, r: ECHO_PULSE_RADIUS, kind: 'echoPulse' });
for (const u of b.alliesInRadius(echo.x, echo.y, ECHO_PULSE_RADIUS)) {
hurt(b, echo, u, atk * (T(ab, '3.atk_scale') ?? 0), 'arts');
elem(b, echo, u, 'apoptosis', atk * (T(ab, '3.ep_damage_ratio') ?? 0));
}
const need = ab.form === 'gold' ? T(ab, '1.hit_times_to_switch') : T(ab, '2.hit_times_to_switch');
ab.strikes = (ab.strikes ?? 0) + 1;
if (need > 0 && ab.strikes >= need) setEchoForm(b, echo, ab.form === 'gold' ? 'dark' : 'gold');
}
function kitEcho(ab, e) {
const s = ab.sk.Skill;
return [
{
spawn(b, e2) { setHits(e2, e2.def.maxHp); hitCount(b, e2, true); e2.blockWeight = ECHO_BLOCK_WEIGHT; setEchoForm(b, e2, 'dark'); },
taken(c, b, e2) { if (c.source && c.source.side === 'ally') echoHit(b, e2); }, // 受到伤害时以自身为中心造成一次范围伤害
},
s && {
cd: s.cd, icd: s.icd,
fire(b, e2) { // 合奏
const gold = e2.mem.ab.form === 'gold';
const r = gold ? ECHO_ENSEMBLE_RADIUS.gold : ECHO_ENSEMBLE_RADIUS.dark;
const ratio = gold ? T(ab, '4.ep_damage_ratio_passion') ?? 0 : T(ab, '4.ep_damage_ratio_depassion') ?? 0;
b.fx('explode', { x: e2.x, y: e2.y, r, kind: gold ? 'ensembleGold' : 'ensembleDark' });
for (const u of b.alliesInRadius(e2.x, e2.y, r)) { hurt(b, e2, u, e2.s.atk, 'arts'); elem(b, e2, u, 'apoptosis', e2.s.atk * ratio); }
},
},
];
}
/** 管 / 弦 core: summon loop, strikes on its own form of 余音, skill. */
function pipeCore(ab, e, b, tpl, { form, prefix }) {
const hi = T(ab, `1.${prefix}_t_1[high_hp].interval`) ?? 40, lo = T(ab, `1.${prefix}_t_1[low_hp].interval`) ?? hi, thr = T(ab, '1.hp_ratio') ?? 0;
const key = ab.tS['1.enemy_key'] ?? ECHO_KEY;
const P = { acc: 0, atk: Infinity }; // first strike as soon as an echo of its form exists (engine attack rule)
const summon = (b2, e2) => {
const got = branchSpawn(b2, tpl, 'summon_enemy', 0, { at: { x: e2.x, y: e2.y }, fallback: () => [b2.spawnEnemy(key, { pos: [e2.y, e2.x], route: toGoal(b2, e2.x, e2.y) })].filter(Boolean) });
for (const u of got) if (u.defId === ECHO_KEY && form === 'gold') setEchoForm(b2, u, 'gold');
b2.fx('summon', { x: e2.x, y: e2.y, id: e2.id, key, n: got.length });
};
return {
spawn(b2, e2) { e2.profile.noAttack = true; aspdFloor(b2, e2); },
tick(b2, e2, a, dt) {
P.acc += dt;
if (P.acc >= (e2.hpRatio < thr ? lo : hi)) { P.acc = 0; summon(b2, e2); }
if (!canCast(e2, false)) return;
P.atk += dt;
if (P.atk < e2.s.interval) return;
const t = b2.rng.pick(echoesOf(b2, form)); // 优先攻击…形态余音
if (!t) return;
P.atk = 0;
e2.lastAttackAt = b2.time;
e2.stats.attacks++;
b2.fx('beam', { x: e2.x, y: e2.y, from: e2.id, to: t.id, kind: `${prefix}Strike` });
echoHit(b2, t);
},
};
}
function kitPipe(ab, e, b, tpl) {
const s = ab.sk['1'];
return [
pipeCore(ab, e, b, tpl, { form: 'dark', prefix: 'acduml' }),
{
death(c, b2) { for (const o of b2.aliveEnemies()) if (o.defId === 'enemy_9022_acdumm') b2.kill(o, null); }, // 弦 leaves with 管
},
s && {
cd: s.cd, icd: s.icd, cond: (b2) => echoesOf(b2, 'dark').length > 0,
fire(b2, e2) { // 【裂管之奏】 three strikes on every dark echo
const r = s.bb.range_radius ?? 0, ep = s.bb.ep_damage_ratio ?? 0;
for (const echo of echoesOf(b2, 'dark')) for (let k = 0; k < 3; k++) b2.after(k * PIPE_STRIKE_GAP, () => {
if (!e2.alive || !echo.alive) return;
b2.fx('explode', { x: echo.x, y: echo.y, r, kind: 'pipeStrike' });
for (const u of b2.alliesInRadius(echo.x, echo.y, r)) { hurt(b2, e2, u, e2.s.atk, 'arts'); elem(b2, e2, u, 'apoptosis', e2.s.atk * ep); }
echoHit(b2, echo);
}, { owner: e2 });
},
},
];
}
function kitString(ab, e, b, tpl) {
const s = ab.sk['1'];
return [
pipeCore(ab, e, b, tpl, { form: 'gold', prefix: 'acdumm' }),
{ spawn(b2, e2) { b2.addBuff(e2, { key: 'boss:eternal', persist: true, flags: { invulnerable: true, untargetable: true } }); } }, // 永久无敌
s && {
cd: s.cd, icd: s.icd, cond: (b2) => echoesOf(b2, 'gold').length > 0,
fire(b2, e2) { // 【断弦之奏】 AoE on every gold echo, which then turns dark
const r = s.bb.range_radius ?? 0, ep = s.bb.ep_damage_ratio ?? 0;
for (const echo of echoesOf(b2, 'gold')) {
b2.fx('explode', { x: echo.x, y: echo.y, r, kind: 'stringStrike' });
for (const u of b2.alliesInRadius(echo.x, echo.y, r)) { hurt(b2, e2, u, e2.s.atk, 'arts'); elem(b2, e2, u, 'apoptosis', e2.s.atk * ep); }
setEchoForm(b2, echo, 'dark');
}
},
},
];
}
// ---------------------------------------------------------------------------------------------------------------
// 盐风主教昆图斯 (boss_4)
function kitQuintus(ab, e, b, tpl) {
const P = { stage: 0, t0: null, stageAt: null, tent: 0 };
const stageSkills = [['Tidewater', 'Rockfall', 'SummonTentac'], ['TidewaterG1', 'RockfallG1', 'SummonTentacG1'], ['TidewaterG2', 'RockfallG2', 'SummonTentacG2', 'Doom']];
const skills = [];
const atkRatio = T(ab, 'epdamage.attack@ep_damage_ratio') ?? 0;
const mk = (name, stage) => {
const s = ab.sk[name];
if (!s) return null;
const kind = name.replace(/G\d$/, '');
const a = { cd: s.cd, icd: s.icd, stage, cond: () => P.stage === stage, fire: (b2, e2) => QUINTUS[kind](b2, e2, s, P, tpl) };
skills.push(a);
return a;
};
const list = [{
spawn(b2, e2) { P.t0 = b2.time; P.stageAt = b2.time; e2.profile.maxTargets = 2; },
before(c, b2, e2) { // 同时攻击防御最高的两个单位
const l = targetsNear(b2, e2, e2.base.rangeRadius || 99).sort((p, q) => q.s.def - p.s.def || q.deploySeq - p.deploySeq);
if (l.length) c.targets = l.slice(0, 2);
},
dealt(c, b2, e2) { elem(b2, e2, c.target, 'neural', e2.s.atk * atkRatio); },
iv: 0.25,
tick(b2, e2) {
const lost = 1 - e2.hpRatio;
if (P.stage === 0 && (lost >= (T(ab, 'growup1.hp_ratio') ?? 1) || b2.time - P.t0 >= (T(ab, 'growup1.interval') ?? Infinity))) grow(b2, e2, 1);
else if (P.stage === 1 && (lost >= 2 * (T(ab, 'growup2.hp_ratio') ?? 1) || b2.time - P.stageAt >= (T(ab, 'growup2.interval') ?? Infinity))) grow(b2, e2, 2);
},
}];
const grow = (b2, e2, n) => {
P.stage = n; P.stageAt = b2.time;
b2.addBuff(e2, { key: 'boss:grow', persist: true, visible: true, mods: { atkPct: T(ab, `atkup${n}.atk`) ?? 0 } });
for (const a of skills) if (a.stage === n) a.left = a.icd; // the new form's skills start from their initial cooldown
b2.fx('grow', { x: e2.x, y: e2.y, id: e2.id, stage: n });
};
stageSkills.forEach((names, i) => names.forEach((n) => list.push(mk(n, i))));
return list;
}
const QUINTUS = {
Tidewater(b, e, s) { // 【大潮】 every operator: arts + neural
b.fx('tide', { x: e.x, y: e.y, id: e.id });
for (const u of b.allies()) { hurt(b, e, u, e.s.atk * (s.bb.atk_scale ?? 0), 'arts'); elem(b, e, u, 'neural', e.s.atk * (s.bb.ep_damage_ratio ?? 0)); }
},
Rockfall(b, e, s, P) { // 【崩坍】 delayed strikes on the N highest-DEF units
const n = ROCKFALL_TARGETS[Math.min(P.stage, ROCKFALL_TARGETS.length - 1)];
const ts = allTargets(b, e).sort((p, q) => q.s.def - p.s.def || q.deploySeq - p.deploySeq).slice(0, n);
for (const t of ts) {
b.fx('rockfall', { x: t.x, y: t.y, id: t.id, dur: ROCKFALL_DELAY });
b.after(ROCKFALL_DELAY, () => { if (e.alive && t.alive) hurt(b, e, t, e.s.atk * (s.bb.atk_scale ?? 0), 'phys'); }, { owner: e });
}
},
SummonTentac(b, e, s, P, tpl) { // 【断裂生殖】 tentacles at the template devices (phase i on the i-th cast)
const br = tpl && tpl.branches && tpl.branches[s.bs.branch_id ?? 'dslily_dstnta'];
let spots = [];
if (Array.isArray(br) && br.length) {
const phase = br[Math.min(P.tent, br.length - 1)] || [];
const devs = (tpl.devices || []);
for (const a of phase) { const d = devs.find((x) => x.alias === a.key); if (d && d.pos) spots.push(d.pos); }
}
if (!spots.length) { const t = b.rng.pick(allTargets(b, e)); if (t) spots = [[t.tileR, t.tileC]]; }
P.tent++;
for (const [r, c] of spots) {
b.fx('tentacle', { x: c, y: r, r: TENTACLE_RADIUS, dur: TENTACLE_STUN });
for (const u of b.alliesInRadius(c, r, TENTACLE_RADIUS)) b.applyStatus(u, 'stun', { duration: TENTACLE_STUN, source: e });
}
},
Doom(b, e, s) { // 【物种爆发】
b.fx('telegraph', { x: e.x, y: e.y, r: 99, dur: DOOM_DELAY, kind: 'speciesBurst', id: e.id });
b.after(DOOM_DELAY, () => { if (e.alive) lpLoss(b, Math.abs(s.bb.value ?? 0), 'speciesBurst', e); }, { owner: e });
},
};
// ---------------------------------------------------------------------------------------------------------------
// 卢西恩,“猩红血钻” (boss_5) + 不祥幻影
function lucienCore(ab, e) {
const pen = T(ab, 'penetrate.def_penetrate') ?? 0, prob = T(ab, 'evade.prob') ?? 0, epr = T(ab, 'combat.attack@ep_damage_ratio') ?? 0;
const s = ab.sk.aoe;
return [
{
spawn(b, e2) { if (pen > 0) b.addBuff(e2, { key: 'boss:pierce', persist: true, mods: { defIgnorePct: pen } }); },
tick(b, e2, a) { // 未被阻挡时有几率闪避物理与法术攻击
const on = !e2.blockedBy && prob > 0;
if (on === a.on) return;
a.on = on;
if (on) b.addBuff(e2, { key: 'boss:evade', persist: true, mods: { dodgePhys: prob, dodgeArts: prob } });
else b.removeBuff(e2, 'boss:evade');
},
dealt(c, b, e2) { elem(b, e2, c.target, 'neural', e2.s.atk * epr); },
},
s && {
cd: s.cd, icd: s.icd, cond: (b) => b.alliesInRadius(e.x, e.y, LUCIEN_AOE_RADIUS).length > 0,
fire(b, e2) {
b.fx('explode', { x: e2.x, y: e2.y, r: LUCIEN_AOE_RADIUS, kind: 'crimsonAoe' });
for (const u of b.alliesInRadius(e2.x, e2.y, LUCIEN_AOE_RADIUS)) { hurt(b, e2, u, e2.s.atk * (s.bb.atk_scale ?? 0), 'phys'); elem(b, e2, u, 'neural', e2.s.atk * (s.bb.ep_damage_ratio ?? 0)); }
},
},
];
}
function kitLucien(ab, e) {
const s = ab.sk.blink;
return [...lucienCore(ab, e), s && {
cd: s.cd, icd: s.icd, cond: () => !!e.blockedBy,
fire(b, e2) { // blink past the blocker, leaving a 不祥幻影 on its path
const route = remainingRoute(e2);
const from = blinkForward(b, e2, s.bb.dist ?? 1.5);
if (!from) return;
const ph = b.spawnEnemy(PHANTOM_KEY, { pos: [from.y, from.x], route, ownerPlayerId: e2.ownerId, sourcePlayerId: e2.sourcePlayerId });
if (ph) b.fx('summon', { x: from.x, y: from.y, id: e2.id, key: PHANTOM_KEY, n: 1 });
},
}];
}
// ---------------------------------------------------------------------------------------------------------------
// 阿利斯泰尔,帝国余晖 (boss_6)
function kitLion(ab, e, b) {
const eq = ab.sk.equip, eqA = ab.sk.equipA, st = ab.sk.store, stA = ab.sk.storeA, su = ab.sk.Suicide;
const P = { sp: 0, adv: false, nextAnim: null, wand: false };
const wandDef = b.data.getEnemy('enemy_10030_vtwand');
const disarm = wandDef && wandDef.talent ? wandDef.talent['1.disarmed_duration'] ?? 0 : 0;
const range = () => e.base.rangeRadius || 3.4;
const cost = eq ? (eq.sp > 0 ? eq.sp : 25) : Infinity;
/** Equipment lying on this field, oldest first. */
const equipment = (b2) => b2.enemies.filter((q) => q.alive && EQUIP_KEYS.includes(q.defId)).sort((p, q) => p.spawnSeq - q.spawnSeq);
const decree = (b2, e2) => { // 【王权号令】 plus-shaped strike + random equipment
const sk = P.adv && eqA ? eqA : eq;
const t = b2.rng.pick(targetsNear(b2, e2, range()));
if (!t) return false;
const r0 = t.tileR, c0 = t.tileC;
const tiles = [[r0, c0], [r0 + 1, c0], [r0 - 1, c0], [r0, c0 + 1], [r0, c0 - 1]];
b2.fx('telegraph', { x: c0, y: r0, r: 1, kind: 'royalDecree', tiles: 'plus', id: e2.id });
for (const u of alliesInTiles(b2, r0, c0, 'plus', 1)) hurt(b2, e2, u, e2.s.atk * ((sk && sk.bb.atk_scale) || 0), 'phys');
const taken = equipment(b2);
const free = tiles.filter(([r, c]) => b2.grid.inRect(r, c) && b2.grid.isLow(r, c) && b2.grid.groundPassable(r, c) && !b2.unitAt(r, c)
&& !taken.some((q) => Math.round(q.y) === r && Math.round(q.x) === c));
const spot = b2.rng.pick(free);
if (spot) {
const key = b2.rng.pick(EQUIP_KEYS);
const q = b2.spawnEnemy(key, { pos: spot, route: { motion: 'FLY', start: spot, end: spot, steps: [{ t: 'wait', s: 99999 }] }, tag: 'part', countInTotal: false, ownerPlayerId: e2.ownerId });
if (q) b2.fx('equip', { x: spot[1], y: spot[0], id: q.id, key });
}
return true;
};
const animate = (b2, e2) => { // 【莫非王土】 the latest equipment (2 at ≤50 %)
const n = P.adv && stA ? stA.bb.max_target ?? 2 : 1;
const sk = P.adv && stA ? stA : st;
for (const q of equipment(b2).slice(-n)) {
if (q.defId === 'enemy_10028_vtswd') {
const t = b2.rng.pick(targetsNear(b2, e2, range()));
if (t) { b2.fx('sword', { x: q.x, y: q.y, from: q.id, to: t.id }); hurt(b2, e2, t, e2.s.atk * ((sk && sk.bb.atk_scale) || 0), 'phys'); }
} else if (q.defId === 'enemy_10029_vtshld') {
const side = Math.sign(q.x - e2.x) || -1;
b2.addBuff(e2, { key: 'boss:vest', shield: (sk && sk.bb.dynamic) || 0, persist: true, visible: true, data: { side, cut: (sk && sk.bb.damage_resistance) || 0 } });
b2.fx('vest', { x: e2.x, y: e2.y, id: e2.id, side });
} else if (q.defId === 'enemy_10030_vtwand') {
P.wand = true; // [substitute for 黄金回响]: next attack becomes arts and disarms its target
b2.fx('equip', { x: q.x, y: q.y, id: q.id, key: q.defId, kind: 'wand' });
}
b2.kill(q, null);
}
};
return [
{
tick(b2, e2, a, dt) {
if (!P.adv && e2.hpRatio <= (T(ab, 'advance.hp_ratio') ?? 0)) { // ≤50 %: DEF +100 %, RES +45, animate two
P.adv = true;
b2.addBuff(e2, { key: 'boss:advance', persist: true, visible: true, mods: { defPct: T(ab, 'advance.def') ?? 0, resFlat: T(ab, 'advance.magic_resistance') ?? 0 } });
b2.fx('phase', { x: e2.x, y: e2.y, id: e2.id, kind: 'lionAdvance' });
}
if (!canCast(e2, false)) return;
P.sp += dt * ENEMY_SP_PER_SEC;
if (P.sp >= cost && decree(b2, e2)) P.sp = 0;
// 【莫非王土】: ANIMATE_DELAY s after equipment first lies on the field, then once per 王权号令 cycle
const has = equipment(b2).length > 0;
if (!has) { P.nextAnim = null; return; }
if (P.nextAnim == null) P.nextAnim = b2.time + ANIMATE_DELAY;
if (b2.time >= P.nextAnim) { animate(b2, e2); P.nextAnim = b2.time + (Number.isFinite(cost) ? cost : ANIMATE_DELAY); }
},
hitIn(c, b2, e2) { // vest: damage from its side ×(1 − damage_resistance) while the barrier holds
const v = e2.findBuff('boss:vest');
if (v && v.shield > 0 && c.source && Math.sign(c.source.x - e2.x) === v.data.side) c.dmg.mul *= 1 - v.data.cut;
},
hitOut(c, b2, e2) { if (P.wand && c.dmg.isAttack) c.dmg.type = 'arts'; },
dealt(c, b2, e2) { if (!P.wand) return; P.wand = false; if (disarm > 0) b2.applyStatus(c.target, 'disarm', { duration: disarm, source: e2 }); },
},
su && {
cd: su.cd, icd: su.icd,
fire(b2, e2) { // 【斥退】
b2.fx('telegraph', { x: e2.x, y: e2.y, r: 99, kind: 'repel', id: e2.id });
for (const u of b2.allies()) hurt(b2, e2, u, e2.s.atk * (su.bb.atk_scale ?? 0), 'arts');
lpLoss(b2, Math.abs(su.bb.value ?? 0), 'repel', e2);
},
},
];
}
function kitEquip() {
return [{ spawn(b, e) { b.addBuff(e, { key: 'boss:equip', persist: true, flags: { invulnerable: true, untargetable: true, noMove: true } }); } }];
}
// ---------------------------------------------------------------------------------------------------------------
// “萨米的意志” (boss_7)
function kitDeer(ab, e) {
const dr = T(ab, 'Madness.damage_resistance') ?? 0;
const atkN = T(ab, 'Madness.enemy_smdeer_mad[attack].max_cnt') ?? 1, skN = T(ab, 'Madness.enemy_smdeer_mad[skill].max_target') ?? 1;
const lasso = ab.sk.Lasso, doom = ab.sk.Doom;
const P = { low: false, acc: Infinity }; // like engine attacks, the first 冰凌 comes as soon as it has a target
return [
{
spawn(b, e2) { e2.profile.noAttack = true; },
tick(b, e2, a, dt) {
if (!P.low && e2.hpRatio < 0.5) { // <50 %: damage taken reduced, extra targets
P.low = true;
b.addBuff(e2, { key: 'boss:madness', persist: true, visible: true, mods: { physTakenMul: 1 - dr, artsTakenMul: 1 - dr } }); // 物理和法术伤害降低
b.fx('phase', { x: e2.x, y: e2.y, id: e2.id, kind: 'deerMadness' });
}
if (!canCast(e2, false)) return;
P.acc += dt;
if (P.acc < e2.s.interval) return;
const cands = fairOrder(b, e2, allTargets(b, e2), P);
if (!cands.length) return;
P.acc = 0;
const cols = [];
for (const t of cands) { if (!cols.includes(t.tileC)) cols.push(t.tileC); if (cols.length >= (P.low ? atkN : 1)) break; }
for (const c of cols) { // 【冰凌】 its normal attack: the whole column
const hit = b.allies().filter((u) => u.tileC === c);
b.fx('column', { x: c, y: e2.y, c, id: e2.id });
for (const u of hit) b.dealDamage(e2, u, { amount: e2.s.atk, type: 'phys', isAttack: true });
}
e2.lastAttackAt = b.time;
e2.stats.attacks++;
},
},
lasso && {
cd: lasso.cd, icd: lasso.icd, cond: (b) => allTargets(b, e).length > 0,
fire(b, e2) { // 【自然涌动】 stun + arts per second
const n = P.low ? skN : (lasso.bb.max_target ?? 1);
const dur = lasso.bb.projectile_life_time ?? 0;
for (const t of fairOrder(b, e2, allTargets(b, e2), P).slice(0, n)) {
b.fx('beam', { x: e2.x, y: e2.y, from: e2.id, to: t.id, kind: 'naturalSurge', dur });
b.applyStatus(t, 'stun', { duration: dur, source: e2 });
b.addBuff(t, { key: 'boss:surge', duration: dur, interval: 1, visible: true,
onTick: ({ battle, unit }) => battle.dealDamage(e2, unit, { amount: e2.s.atk * (lasso.bb.atk_scale ?? 0), type: 'arts', canDodge: false, tags: ['enemyAbility'] }) });
}
},
},
doom && {
cd: doom.cd, icd: doom.icd,
fire(b, e2) {
b.fx('telegraph', { x: e2.x, y: e2.y, r: 99, kind: 'samiDoom', id: e2.id });
for (const u of b.allies()) hurt(b, e2, u, e2.s.atk * (doom.bb.atk_scale ?? 0), 'arts');
lpLoss(b, Math.abs(doom.bb.value ?? 0), 'samiDoom', e2);
},
},
];
}
// ---------------------------------------------------------------------------------------------------------------
export const BOSS_KITS = Object.freeze({
enemy_9013_acstmk: kitHelm, // boss_1 假想敌:胄
enemy_9013_acstmk_2: kitHelm, // boss_8 假想敌:胄 (隐秘核心)
enemy_9014_acstma: kitBlade, // “斩胄之剑”
enemy_9015_acstmb: kitBlade, // “破胄之锤”
enemy_9016_acstmr: kitShell, // 刺胄之弹
enemy_9017_achunt: kitGun, // boss_2 假想敌:铳
enemy_9017_achunt_2: kitGun, // boss_9 假想敌:铳 (隐秘核心)
enemy_9018_actrpa: kitSpring, // “碎铳之簧” 法术护盾
enemy_9019_actrpb: kitSpring, // “碎铳之簧” 元素护盾
enemy_9020_actrpc: kitSpring, // “碎铳之簧” 频次护盾
enemy_9021_acduml: kitPipe, // boss_3 假想敌:管
enemy_9021_acduml_2: kitPipe, // boss_10 假想敌:管 (隐秘核心)
enemy_9022_acdumm: kitString, // 假想敌:弦
enemy_9023_acdums: kitEcho, // “余音”
enemy_1521_dslily: kitQuintus, // boss_4 盐风主教昆图斯
enemy_2016_csphtm: kitLucien, // boss_5 卢西恩,“猩红血钻”
enemy_2017_csphts: lucienCore, // 不祥幻影
enemy_9032_aclionk: kitLion, // boss_6 阿利斯泰尔,帝国余晖
enemy_10028_vtswd: kitEquip, // 未装配刀片 (inert until animated)
enemy_10029_vtshld: kitEquip, // 防护背心
enemy_10030_vtwand: kitEquip, // 冲击式施术单元
enemy_9033_acdeer: kitDeer, // boss_7 “萨米的意志”
});
export const BOSS_KEYS = Object.freeze(Object.keys(BOSS_KITS));