Files
ark-stronghold-protocol/server/sim/content/bands/battle.js
T
sganggs 221e6b8dd2 0.2.5-dev: community round 30 and follow-ups
- Stock: fixed trait gifts (缪尔赛思 / 变形者集群 变形同构体, 耶拉 不融冰, 蜜蜡 / 卡涅利安 迅捷作战粮, 诗怀雅 盟约之币) still occupy stock but are granted past the cap; an operator card already shown in the shop or a reward can be taken after the pool runs out and overdraws (GitHub #498, the owner's decision); item buys and item rewards keep the stock check. 洛洛 / 焰尾 sub-pools use the measured weights (PR #497 by @2321Robin; the occupancy idea of PR #496 by @2321Robin re-implemented differently).
- Boss pairs are drawn at random once per match and fixed to battle start; the Hidden Core reuses them; a leaver does not split a full pair (the owner's decision).
- 阿戈尔 in 联防 no longer re-applies the opening devour HP loss or a false revive; Touch's team medic stays for the match after its holder leaves (owner-approved, official lifetime unverified).
- Auto-skill ranges: 司霆惊蛰 S2, 斩业星熊 S3 against air, 予愿安洁莉娜 S3; 自在's post-rebirth invulnerability is also untargetable (#510); Horn / Clementia / catapult / 忍冬 first hit (PR #502 by @Xinglan233); 不融冰 / tear gas trigger on real damage.
- Bond layer-up sound in round start / draft / prep / battle (#482): one channel, a new cue replaces the old, no extra cooldown; a prep update that adds layers and activates a bond plays one cue.
- Client: multi-swing fix (#506), refresh single-flight (PR #507 by @robll-v1), LP tower keeps the settled value with a −N tick (PR #504 by @robll-v1), export with only 潜能 / 练度 changes (PR #503 by @Xinglan233), spectator rosters list DIY members (PR #505 by @Xinglan233), banned members beside bond tooltips (PR #494 by @YuriRestia), recovery action above the away overlay (PR #511 by @RainoutOvO), the /dev/sim-check.html consistency page (PR #492 by @Cloudnyco), Korean (PR #490 by @D-Roote) and English Victoria wording (PR #499 by @YuriRestia).
- golden: 45 of 287 scenarios move against 0.2.4 (roster 17, bonds 3, fields 6, matches 17, standins 2), each attributed in docs/history/0.2.5.md §30.9 / §30.16.
2026-10-12 14:07:08 +08:00

273 lines
14 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/bands/battle.js — battle side of the strategies (bands, data/bands.json; research 01 §11).
//
// Each player of a battle installs the IN_BATTLE parts of its own band, dispatched on the band buff's bbStr.key
// (numbers from the blackboard, never hard-coded). Band buffs apply in every battle kind (normal / 联防 / boss /
// hidden); IN_BATTLE layer gains are no-ops where the match disables them (support.gainLayers).
//
// act1autochess_band2_buff 阿米娅 众志合一 ≥ value_i active bonds (highest i) ⇒ every operator ATK +atk_i,
// HP +max_hp_i (直接乘算 — support directMods —, bonds snapshot at combat start)
// act1autochess_band28_buff 埃芒加德 命结之秘 the first max_respawn_cnt operator knock-downs of the battle revive at
// once — PRTS 备注 "“复活”的实现方式为:受益者因移动之外的原因退场时下次部署
// 的再部署时间和费用归零": the knock-out stands (被击倒时 effects run) and the
// operator redeploys at once, free, where it lies (部署时 effects run, SP
// reset): items reviveNow, a `death` hook after M3茧甲's and before 阿戈尔
// 5 / 不屈 (in place until 0.2.0, community report 「…砾和瑕光这种死亡和部
// 署的叠层效果,如果有艾芒加德的3次复活似乎是无法触发」)
// act1autochess_band13_buff 克莱门莎 崇高牺牲 a <bond_id> operator knocked down ⇒ +its tier (等阶) <bond_id> layers
// (bond_add_type by_charlevel; no "已激活" in the text ⇒ requireActive false)
// act1autochess_band16_buff 大帝 加急调派 "每次部署后再部署时间减少50%": every deployment of an operator stacks one
// redeploy ×(1 + respawn_time) for the rest of the battle, with no cap (PRTS
// 卫戍协议:盟约 下半 / PRTS盟约记录 备注 "※该策略效果可无限叠加"; a 20-stack cap
// until 0.2.2 — GitHub #328, PR #329)
// act1autochess_band17_buff 桑葚 药枚实验 at combat start the player's units on the front-most (最右边) column: each
// attack has `prob` to gain 1 shield layer (max 1)
// act1autochess_band18_buff 休谟斯 回收利用 a ground (地面) operator's skill ends ⇒ a random operator on its 4
// neighbouring tiles gains `sp` SP
// act1autochess_band15_buff 卡莱莎 食腐之蝶 an operator knocked down ⇒ every other unit of the player on the field
// ATK +atk × n, n ≤ max_stack_cnt (+20 % … +200 %, additive stacks),
// until that unit is knocked down itself or the battle ends
// act1autochess_band19_buff 陈 以己之长 the player's operators' physical / arts damage becomes 弱点伤害
// act2autochess_band12_buff 夕 墨色真颜 at combat start operators of which ≥ 2 of the same name (same base chess,
// elite or not) stand on the field get ATK +atk
// act2autochess_band3_buff 伊奥莱塔 统御号令 at combat start n = elite operators on the field ⇒ every elite operator
// ATK +atk_per_cnt × n, HP +max_hp_per_cnt × n (all 直接乘算)
// auto_chess_change_map Touch 外勤医疗 "所有玩家场地上出现一名<预备干员-医疗>": once selected in this match, its
// retained team grant gives every player's field the map character of the
// variant whose common_condition holds for that player's elites on the field
// (condition_golden_chess_le cnt → 预备干员-医疗, _ge cnt → Touch), spawned at
// combat start on its stage slot (tokens.spawnMapChar)
// Prep-only bands (economy / shop / grants / layers at prep end or round start) live in bands/meta.js.
import {
num, buffsOf, bandRecord, isOp, onField, isElite, tierOf, unitBonds, activeBondIds, playerOps, passiveBuff, fxOn,
matchBands, gainLayers, alliesAround, N4, baseChessId, isGroundOp, directMods,
} from '../support/index.js';
import { weaknessRetype, addShieldLayer, PRIO_RESPAWN, reviveNow } from '../items/battle.js';
import { spawnMapChar } from '../tokens.js';
export const AMEDIC_BAND = 'band_amedic';
/**
* 'death' priority of 埃芒加德's revive: a knock-out (so after every `fatal` saver — kits', 坚固维式重锤's 不死), after the
* operator's own M3茧甲 (PRIO_RESPAWN 13), before 阿戈尔 5's first-knock-out revive (11) and 不屈 (10).
*/
export const PRIO_BAND_REVIVE = PRIO_RESPAWN - 1;
const keyOf = (bandId, part = '') => `band:${bandId}${part ? `:${part}` : ''}`;
const deployedOps = (battle, pid) => playerOps(battle, pid, { fieldOnly: true });
// ---------------------------------------------------------------------------------------------------------------
// band buff behaviours: (battle, ps, params, bandId)
const BY_KEY = {
// 阿米娅 众志合一
act1autochess_band2_buff(battle, ps, p, bandId) {
const n = activeBondIds(battle, ps.playerId).length;
let atk = 0, hp = 0;
for (let i = 1; i <= 9 && p[`value_${i}`] != null; i++) {
if (n >= num(p[`value_${i}`], Infinity)) { atk = num(p[`atk_${i}`]); hp = num(p[`max_hp_${i}`]); }
}
if (!(atk > 0 || hp > 0)) return;
const mods = directMods({ atk, hp });
for (const u of playerOps(battle, ps.playerId)) passiveBuff(battle, u, keyOf(bandId), mods);
},
// 埃芒加德 命结之秘
act1autochess_band28_buff(battle, ps, p, bandId) {
const max = Math.floor(num(p.max_respawn_cnt, 3));
if (!(max > 0)) return;
let used = 0;
battle.on('death', (c) => {
const u = c.unit;
if (used >= max || !isOp(u) || u.ownerId !== ps.playerId || !reviveNow(battle, c, 'band')) return;
used++;
fxOn(battle, 'revive', u, keyOf(bandId), bandId, { left: max - used });
}, { priority: PRIO_BAND_REVIVE });
},
// 克莱门莎 崇高牺牲: PRTS requires the 阿戈尔 bond to be active.
act1autochess_band13_buff(battle, ps, p, bandId) {
const bond = typeof p.bond_id === 'string' && p.bond_id ? p.bond_id : null;
if (!bond) return;
const byLevel = p.bond_add_type == null || p.bond_add_type === 'by_charlevel';
battle.on('death', (c) => {
const u = c.unit;
if (c.reason !== 'killed' || !isOp(u) || u.ownerId !== ps.playerId || !unitBonds(u).includes(bond)) return;
const n = byLevel ? tierOf(u) : Math.max(1, Math.floor(num(p.value, 1)));
if (gainLayers(battle, { playerId: ps.playerId, bonds: bond, n, requireActive: true, source: u, reason: 'band' }) > 0) {
fxOn(battle, 'layer', u, keyOf(bandId), bandId, { bond, n });
}
});
},
// 大帝 加急调派
act1autochess_band16_buff(battle, ps, p, bandId) {
const mul = Math.max(0, 1 + num(p.respawn_time, 0));
if (mul === 1) return;
const key = keyOf(bandId);
battle.on('deploy', (c) => {
const u = c.unit;
if (!isOp(u) || u.ownerId !== ps.playerId) return;
battle.addBuff(u, { key, mods: { redeployMul: mul }, refresh: 'stack', stacks: 1, maxStacks: Infinity, persist: true, allowDead: true });
});
},
// 桑葚 药枚实验
act1autochess_band17_buff(battle, ps, p, bandId) {
const pr = num(p.prob);
if (!(pr > 0)) return;
const key = keyOf(bandId, 'shield');
const chosen = new Set();
battle.on('battleStart', () => {
const units = battle.allies(ps.playerId);
// "最右边的所有我方单位": the rightmost column of the player's own board (a board position, whatever the units'
// directions; the mirrored Final Assault right side counts from the field's left)
const boardCol = (u) => (ps.mirror ? -u.tileC : u.tileC);
let front = -Infinity;
for (const u of units) front = Math.max(front, boardCol(u));
for (const u of units) if (boardCol(u) === front) chosen.add(u);
});
battle.on('attack', (c) => {
const u = c.attacker;
if (!chosen.has(u) || !u.alive) return;
if (!(pr >= 1 || battle.rng() < pr)) return;
if (addShieldLayer(battle, u, key, 1)) fxOn(battle, 'shield', u, keyOf(bandId), bandId);
});
},
// 休谟斯 回收利用 ("地面干员技能结束时…": a melee-position operator on any tile — support isGroundOp)
act1autochess_band18_buff(battle, ps, p, bandId) {
const sp = num(p.sp);
if (!(sp > 0)) return;
battle.on('skillEnd', (c) => {
const u = c.unit;
if (!isOp(u) || u.ownerId !== ps.playerId || c.reason === 'death' || !onField(u) || !isGroundOp(u)) return;
const cands = alliesAround(battle, u, N4).filter((a) => a.kind === 'op' && a.skill && !a.skill.noSkill && a.skill.kind !== 'passive');
const t = cands.length ? battle.rng.pick(cands) : null;
if (!t) return;
t.skill.gainSp(sp, 'band');
fxOn(battle, 'spGain', t, keyOf(bandId), bandId, { n: sp, from: u.id });
});
},
// 卡莱莎 食腐之蝶
act1autochess_band15_buff(battle, ps, p, bandId) {
const step = num(p.atk);
const cap = Math.max(1, Math.floor(num(p.max_stack_cnt, 10)));
if (!(step > 0)) return;
const key = keyOf(bandId);
battle.on('death', (c) => {
const d = c.unit;
if (c.reason !== 'killed' || !isOp(d) || d.ownerId !== ps.playerId) return;
for (const a of battle.allies(ps.playerId)) {
if (a === d) continue;
const cur = a.findBuff(key);
const n = Math.min(cap, (cur && cur.data ? cur.data.n : 0) + 1);
battle.addBuff(a, { key, mods: directMods({ atk: step * n }), refresh: 'replace', data: { n }, visible: true });
}
});
},
// 陈 以己之长
act1autochess_band19_buff(battle, ps) {
battle.on('hit', (c) => {
const s = c.source;
if (!isOp(s) || s.ownerId !== ps.playerId || !c.target || c.target.side !== 'enemy') return;
weaknessRetype(c.dmg, s, c.target);
}, { priority: 5 });
},
// 夕 墨色真颜
act2autochess_band12_buff(battle, ps, p, bandId) {
const atk = num(p.atk);
if (!atk) return;
battle.on('battleStart', () => {
const groups = new Map();
for (const u of deployedOps(battle, ps.playerId)) {
const b = baseChessId(u);
if (!groups.has(b)) groups.set(b, []);
groups.get(b).push(u);
}
for (const list of groups.values()) {
if (list.length < 2) continue;
for (const u of list) passiveBuff(battle, u, keyOf(bandId), directMods({ atk }));
}
});
},
// 伊奥莱塔 统御号令
act2autochess_band3_buff(battle, ps, p, bandId) {
const a = num(p.atk_per_cnt), h = num(p.max_hp_per_cnt);
if (!(a > 0 || h > 0)) return;
battle.on('battleStart', () => {
const elites = deployedOps(battle, ps.playerId).filter(isElite);
const n = elites.length;
if (!n) return;
const mods = directMods({ atk: a * n, hp: h * n });
for (const u of elites) passiveBuff(battle, u, keyOf(bandId), mods);
});
},
};
// ---------------------------------------------------------------------------------------------------------------
// Touch 外勤医疗 (auto_chess_change_map, every player's field)
/** Map-character variants of band_amedic: [{ cond, cnt, chars: [tokenId…] }]. */
export function amedicVariants() {
const out = [];
for (const b of buffsOf(bandRecord(AMEDIC_BAND))) {
if (b.key !== 'auto_chess_change_map') continue;
const chars = new Set();
for (const [k, v] of Object.entries(b.p)) if (k.includes('#') && num(v) > 0) chars.add(k.split('#')[0]);
out.push({ cond: String(b.p.common_condition || ''), cnt: num(b.p.cnt, 0), chars: [...chars] });
}
return out;
}
/** Map characters for a player with `elites` elite operators on the field. */
export function amedicCharsFor(elites, variants = amedicVariants()) {
for (const v of variants) {
const ok = /_ge$/.test(v.cond) ? elites >= v.cnt : /_le$/.test(v.cond) ? elites <= v.cnt : false;
if (ok) return v.chars;
}
return [];
}
function installMapChars(battle) {
const variants = amedicVariants();
if (!variants.length) return;
const holders = battle.players.filter((ps) => matchBands(battle, ps.playerId).includes(AMEDIC_BAND));
if (!holders.length) return;
battle.on('battleStart', () => {
for (const ps of holders) {
const elites = deployedOps(battle, ps.playerId).filter(isElite).length;
for (const id of amedicCharsFor(elites, variants)) {
const u = spawnMapChar(battle, ps.playerId, id);
if (u) fxOn(battle, 'summon', u, keyOf(AMEDIC_BAND), AMEDIC_BAND);
}
}
});
}
// ---------------------------------------------------------------------------------------------------------------
/** Band buff keys with a battle part (tests / tooling). */
export const BATTLE_BUFF_KEYS = Object.freeze([...Object.keys(BY_KEY), 'auto_chess_change_map']);
/** Does this band have a battle part? */
export function hasBattlePart(bandId) {
return buffsOf(bandRecord(bandId)).some((b) => (b.bbKey && BY_KEY[b.bbKey]) || b.key === 'auto_chess_change_map');
}
export function install(battle) {
for (const ps of battle.players) {
const band = ps.bandId ? bandRecord(ps.bandId) : null;
if (!band) continue;
for (const b of buffsOf(band)) {
const f = b.bbKey ? BY_KEY[b.bbKey] : null;
if (!f) continue;
try { f(battle, ps, b.p, ps.bandId); } catch (e) { battle._handlerError?.(`band:${ps.bandId}`, null, e); }
}
}
installMapChars(battle);
}