Files
ark-stronghold-protocol/server/sim/content/kits/ops/op-closur.js
T
sganggs 806a047717 feedback6 R21P: every operator fights at full potential (潜能 6) — the owner's decision of 2026-10-07 (GitHub #252, PR #255; no host slider): tools/build-data.mjs OPERATOR_POTENTIAL adds every character_table potentialRanks attribute step (部署费用 / 再部署时间 / ATK / HP / DEF / RES / ASPD, all ADDITION, before rounding) to every chess (normal and elite), 补位 and 自选 unit form and picks talent / trait / module-talent candidates up to requiredPotentialRank 5 (「天赋效果增强」); a summon picks its talent candidates at its owner's potential (their requiredPotentialRank mirrors the owner's — PRTS 凯尔希: Mon3tr 3.5 s / 1400) and takes none of the owner's attribute steps; the 原型干员 stand-ins have no potential ranks (unchanged). 刺玫 413 / 17 → 435 / 15 as in the tournament video, 砾 18 → 16 s, 至简 80 → 70 s. data/chess.json (256 records), backups.json (the 71 owned picks, 8 summons), tokens.json (5 summons) and the data translations en / ja / ko / zh-TW rebuilt — only potential-driven fields (JSON-compared); the 干员调配 局内数值 and detail cards read the records. op-wang.js reads her stock cap 「最多拥有8(+1)枚」 through the potential step (the pattern fell back to 7: she sat at the cap from the start). Docs: DATA §2 / §18 / tokens, PLAYING (detail card), research 03, kits README, shared/diy.js / standIn.js, the 71 自选 kit headers and the kit comments that quoted potential-0 numbers. Tests: test/full-potential.test.js (new, fails on the parent); data.test.js re-derivation adds the potential steps (+ respawnTime); every test that hard-coded a potential-0 number carries the full-potential one (from the data and the test's own formula; no check loosened); golden: moves almost everywhere — roster 49/49, bonds 46/46, fields 22/22, matches 18/18, diy 133/138 (each battle parts on a stat: roster-001 / roster-020 at tick 1 on 角峰's HP (生命上限+250), bond-arcaneShip-high at 4.73 s and diy-001 at 3.70 s on an enemy's HP from a higher ATK; diy-110 / 111 also by 望's cap; the matches through those battles); unchanged: standins 10/10 and diy-135…138 (no potential ranks), diy-012 (twelve 夜莺 never hit, never redeployed)
2026-10-07 18:35:39 +08:00

394 lines
25 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/kits/ops/op-closur.js — 可露希尔 (char_4228_closur) 自选 operator kit: 6★ 战术家 (先锋), an owned-6★ pick of
// the tier-5 and tier-6 自选 slots; every skill, both talents, the trait, the module TAC-X 给自己的小奖杯 at every form, and the
// kit of her 援军 指挥中心 (token_10066_closur_ourbase). Kit contract and the 自选 rules: ../README.md ("How to add an operator
// (自选)").
//
// Forms (data/backups.json units.char_4228_closur): normal = E2 Lv1, skills at rank 4, no module; elite = E2 Lv60, rank 7, the
// module at stage 1 (tier 5) or 3 (tier 6) — the owner's decision of 2026-10-05. Full potential (the owner's decision of 2026-10-07). Sources:
// character_table / skill_table / battle_equip_table / the token's character_table row (zh_CN, as built into backups.json),
// PRTS 可露希尔 (精准投放 备注 "战术点对位于战术点攻击范围内的所有我方干员/召唤物付与可露希尔的援军标记…在攻击这些单位阻挡的敌人时同样
// 可以触发特性效果"; S1 备注 "回费间隔 = 持续时间 / 回费量,首次回费间隔为计算所得的回费间隔的一半"; S2 备注 "2秒固定间隔回复费用",
// the 【返费】 "回复干员当前部署费用属性的40%(向上取整)", "上一次撤退/离场的原因为移动…不会生效"; S3 备注 "1.66秒的固定间隔", "攻击
// 范围变为自身攻击范围+战术点的攻击范围+所有“援军”视野范围的复合范围", "能攻击到自身援军阻挡的敌人", "仅能对敌人类敌方单位施加迟钝"),
// PRTS 指挥中心 (备注: 持有禁疗; 对攻击范围内的所有我方干员/召唤物施加援军标记; x-5 / x-4 / x-6 by her skill; 撤退或被击倒 ⇒ 战术点
// 形态; 持有者离场后强制撤退), PRTS 分支特性信息 战术家, PRTS 术语释义 护盾 / 迟钝 / 部署费用下限, and the client's battle data read
// from the local install — charpack char_4228_closur (trait closur_tr; T2 deck buff closur_t_2[atk] ATK MULTIPLIER on the
// filterTag rhodes cards with `_minCostExtremeSetting`; S2 / S3 selectors: her range + the enemies blocked by a closur_friend
// holder; S3's attack buff closur[slow_down]: overrideType, refreshRemainingTimeWhenStackMax, clearAllStackCntWhenTimeUp),
// the token prefab token_10066_closur_ourbase (modes S1 / S2 / S3: closur_ourbase_aura on ground allies, operators and
// summons (professionMask 767), itself too; closur_passive with 禁疗 (abnormal flag 7); the 战术点形态 "Recharge" mode:
// 不死 / 无敌, block ×0, back at full HP — closur_ourbase_t_1[listener] / [trigger_switch] / [switch]), the skill prefabs
// skchr_closur_1 (ApplyShield: closur_friend holders, damage_block_once, one layer, `overrideType`) / _2 (ActiveAura:
// closur_s_2[friend] DEF MULTIPLIER + BLOCK_CNT, the born-state refund) / _3, buff_template_data closur_tr (AtkScaleUp vs a
// target blocked by her summon or by a closur_friend holder), closur_e_002_tr (TAC-X DamageScale vs the damage of a unit the
// holder blocks), closur_ourbase_aura (closur_friend from the host), closur_s_1[cost] / [trigger_modify_cost],
// closur_s_2[friend], closur_s_3[data], slow_down.
// - Trait (战术家) "可以在攻击范围内选择一次战术点来召唤援军,自身攻击援军阻挡的敌人时攻击力提升至150%" (trait bb atk_scale): her
// 援军 is the 指挥中心 (T1); her damage on an enemy blocked by one of her 援军 — the 指挥中心, or any ally holding her mark — ×
// atk_scale (closur_tr; every attack of hers, skill attacks included). Ranged physical, 3-3, hits air (PRTS 分支特性信息 战术家
// "普通攻击可对空"), blocks 1, ground enemies target her. TAC-X adds "援军受到来自自身阻挡单位的伤害降低15%" (hidden module talent
// damage_scale 0.85): damage on one of her 援军 from an enemy that 援军 blocks ×damage_scale.
// - 援军 (her mark, closur_friend): while her 指挥中心 stands, it and every ally of its range — operators and summons (the aura's
// ground-motion targets: every unit of our side), any player's, never a 孤立 one — hold her mark (re-read every tick). Her
// own tile counts too when the player puts the 指挥中心 next to her [ASSUMED: nothing in the text leaves her out].
// - T1 精准投放 "可以在战术点召唤指挥中心协助作战,其被击败后会在15秒后自动刷新;战术点效果范围随携带技能变化": the 指挥中心 is a hand
// piece the player places in her range (data ownerRange); her deployment brings it on its tile (deployed early when she comes
// first in the battle-start deployment, as 伺夜's pack); without a placed piece it is summoned on the tactical point
// (Battle.findTacticalPoint — tier3 tacticalPoint, the 伺夜 rule). It never attacks (ATK 0), blocks 2, holds 禁疗, and its
// range is the one of her skill (the variant's bySkill: x-5 / x-4 / x-6 — its passive skill's range here, so the card shows
// it). Knocked out (or withdrawn) it enters its 战术点形态 — off the fight, its tile kept (伺夜's 狼群 model) — for the token's
// hidden talent `interval` (15 s), then it is back on its tile at full HP; her leaving the field withdraws it for good (no
// 战术点形态: "持有者离场后强制撤退") and her next deployment brings a new one.
// - T2 极限调度 "携带可露希尔时,部署费用下限降低3,【罗德岛】干员攻击力+4%" (full potential: 降低4; bb cost / atk; TAC-X stage 3:
// −6 / +8 %): from the battle start every operator of her player whose nation is 罗德岛 (character_table nationId rhodes —
// the client's filterTag) ATK +atk for the whole battle (a deck buff: on the field or not, herself included). The
// 部署费用下限 has no counterpart here: the sim's deploy costs never go below 0 (the kits' cost cuts stop there) and only the
// auto-redeploy spends DP [ASSUMED: no effect].
// - S1 递归策略 (AUTO, 8 s): the 援军 of the moment get one 护盾 layer (shieldHits 1, 不叠加: back to one; until used) and she
// gains n = min(cost + uses × cost_per_add, cost_add_max) DP over the duration — uses = her earlier casts of this deployment —
// one at a time, every 8 / n s from half that interval. An AUTO skill acting on her side only: it fires as soon as its SP is
// full (`trigger: 'SP_FULL'`, the owner's AUTO rule, kits/README.md checklist 5).
// - S2 模型扩展 (MANUAL, data SP_FULL — the 战术家 row's ALWAYS; 30 s): +cost DP at once, 15 more one per 2 s; ATK +atk, 2 targets,
// and the enemies her 援军 block are her targets too (Battle.setExtraRange: their tiles); every 援军 DEF +def (MULTIPLIER =
// Σpct) and block +block_cnt while it runs; an operator of her player deployed on a tile of the 指挥中心's range meanwhile
// gives back ceil(cost_return × its deploy cost) DP — not after a 【移动】.
// - S3 Q.E.D. (MANUAL, SP_FULL, 30 s): 18 DP one per 1.66 s; attack interval base_attack_time (a flat −0.5 s on 1.0 s); her range
// is her own + the 指挥中心's + every 援军's own range (their running grid, not their extra keys — PRTS "仅累加其复合范围中的基础
// 范围") + the tiles of the enemies her 援军 block; every hit attack@atk_scale × ATK physical and one 迟钝 stack of
// attack@slow_down on an enemy (≤ attack@max_stack_cnt, cap attack@slow_down_max), the stacks sharing one
// attack@slow_down_time timer; every attack_trigger_cnt attacks one more target (max_trigger_cnt times).
// - 迟钝 (ba.slowdown "移动速度、攻击速度…降低相应比例(同名效果取最高)"; PRTS "移动速度、攻击速度、迟钝乘数均降低相应数值(直接乘算)",
// "同名同源效果叠加并刷新时间(不超过上限);不同名/不同源迟钝效果间取当前数值最高"): one 'slowdown' effect per enemy
// (Battle.applyStrongest) of value v: move speed ×(1 − v) and attack interval ×1 / (1 − v) (ASPD ×(1 − v); the engine has
// no ASPD Σpct bucket — exact while no other attack-interval change is on). Its effect on enemy ability cooldowns and on the
// time of resistible statuses is not modelled [ASSUMED].
import { num, talentBb, moduleBb, traitBb, skillRec, batMod, up } from '../shared/tier1.js';
import { tacticalPoint } from '../shared/tier3.js';
import { COLS, FORCED_EXIT } from '../../../constants.js';
const S1 = 'skchr_closur_1';
const S2 = 'skchr_closur_2';
const S3 = 'skchr_closur_3';
export const BASE = 'token_10066_closur_ourbase';
/** 精准投放 "其被击败后会在15秒后自动刷新" when the token def carries no interval. */
const REFRESH_FALLBACK = 15;
/** Seconds between two tries of a 指挥中心 whose 战术点形态 is over but whose tile is taken. */
const RETRY = 0.25;
/** Removal reasons that put the 指挥中心 in its 战术点形态 ("撤退或被击倒"); 'expired' = her leaving / the deploy limit. */
const TAC_EXITS = new Set(['killed', 'retreat', FORCED_EXIT]);
export const SHIELD_KEY = 'closur:s1';
export const S2_KEY = 'closur:s2';
export const S3_KEY = 'closur:s3';
export const RHODES_KEY = 'talent:closur:rhodes';
export const SLOWDOWN = 'slowdown';
const bbOf = (chess, id) => skillRec(chess, id)?.bb ?? {};
const tileKey = (r, c) => r * COLS + c;
/** First talent of a normalised token def holding `key` (token talents keep no index). */
const tokTalent = (def, key) => (def?.talents ?? []).find((t) => t && t.bb && t.bb[key] != null) ?? null;
/** 迟钝 v: move speed ×(1 − v), attack interval ×1 / (1 − v). */
const slowMods = (v) => ({ moveMul: Math.max(0, 1 - v), batPct: v < 0.99 ? v / (1 - v) : 99 });
/**
* One 迟钝 stack of `unit` on enemy `e` (S3): the stacks of one source add up (≤ maxStacks, ≤ cap) and share one timer
* refreshed by each stack (cleared together when it runs out); the strongest source wins (applyStrongest).
*/
export function slowdownStack(battle, unit, e, { step, maxStacks, cap, duration }) {
if (!e || !e.alive || e.side !== 'enemy' || e.kind !== 'enemy' || !(step > 0) || !(duration > 0)) return;
const key = `closurSlow:${unit.id}`;
const cur = e.mem[key];
const n = Math.min(Math.max(1, Math.floor(maxStacks) || 1), (cur && cur.until > battle.time + 1e-9 ? cur.n : 0) + 1);
e.mem[key] = { n, until: battle.time + duration };
battle.applyStrongest(e, SLOWDOWN, { duration, value: Math.min(cap > 0 ? cap : Infinity, step * n), source: unit, mods: slowMods });
}
/**
* A skill's DP over time (the client's periodic_cost / closur_s_1[trigger_modify_cost]): `step` DP every `iv` s from
* `first` s on, `total` in all; a skill that runs its full duration pays the rest at its end.
*/
function dpTicker(unit, { total, step = 1, iv, first = iv }) {
unit.mem.closurDp = { t: 0, next: first, given: 0, total, step, iv };
}
function dpTick(battle, unit, dt) {
const d = unit.mem.closurDp;
if (!d || !(d.iv > 0) || !(d.step > 0)) return;
d.t += dt;
while (d.t + 1e-9 >= d.next && d.given + d.step <= d.total + 1e-9) {
d.given += d.step;
d.next += d.iv;
battle.addDp(unit.ownerId, d.step);
}
}
function dpEnd(battle, unit, reason) {
const d = unit.mem.closurDp;
unit.mem.closurDp = null;
if (!d) return;
const rest = d.total - d.given;
if (reason === 'duration' && rest > 1e-9) { battle.addDp(unit.ownerId, rest); d.given += rest; }
if (d.given > 0) battle.fx('dp', { x: unit.x, y: unit.y, n: d.given, id: unit.id });
}
export default {
char_4228_closur: (bb, chess) => {
const t1 = talentBb(chess, 1);
const hidden = moduleBb(chess); // TAC-X: damage_scale (the 援军's guard)
const scale = num(traitBb(chess).atk_scale, 1.5);
const guard = num(hidden.damage_scale, 1);
const b1 = bbOf(chess, S1), b2 = bbOf(chess, S2), b3 = bbOf(chess, S3);
const s1 = skillRec(chess, S1);
const tokenId = (chess?.talents ?? []).find((t) => t && t.tokenKey)?.tokenKey ?? BASE;
/** Her 指挥中心 units (board pieces and summons). */
const basesOf = (battle, unit) => battle.allyUnits.filter((t) => t.kind === 'token' && t.defId === tokenId && t.ownerUnit === unit);
/** Her 援军 now (cached per tick): her standing 指挥中心 and every operator / summon of its range (no 孤立 one). */
const reinfOf = (battle, unit) => {
const m = unit.mem;
if (m.closurReinf && m.closurReinfT === battle.time) return m.closurReinf;
const set = new Set();
for (const base of basesOf(battle, unit)) {
if (!up(base)) continue;
set.add(base);
const keys = base.rangeKeySet ?? new Set(base.rangeKeys ?? []);
for (const a of battle.allyUnits) {
if (!up(a) || a.hidden || (a.kind !== 'op' && a.kind !== 'token')) continue;
if (!battle.allySelectable(a, unit) || !keys.has(tileKey(a.tileR, a.tileC))) continue;
set.add(a);
}
}
m.closurReinf = set;
m.closurReinfT = battle.time;
return set;
};
/** Tiles of the enemies her 援军 block (S2 / S3: "能攻击到自身援军阻挡的敌人"). */
const blockedTiles = (battle, unit, reinf) => {
const keys = [];
for (const r of reinf) for (const e of r.blocking) if (e.alive && e.blockedBy === r) keys.push(tileKey(Math.round(e.y), Math.round(e.x)));
return keys;
};
const setExtra = (battle, unit, keys) => {
const cur = unit.extraRangeKeys ?? [];
const uniq = [...new Set(keys)].sort((a, b) => a - b);
if (uniq.length === cur.length && uniq.every((k, i) => k === cur[i])) return;
battle.setExtraRange(unit, uniq.length ? uniq : null);
};
/** The 指挥中心's kit: no attack, 禁疗, the range of her skill, its 战术点形态. `def` = its def for her loadout. */
const baseKit = (owner, def) => {
const grid = def?.skill?.rangeGrid ?? def?.rangeGrid ?? [[0, 0]];
const refresh = num(tokTalent(def, 'interval')?.bb?.interval, REFRESH_FALLBACK);
return {
skill: { kind: 'passive', targeting: { rangeGrid: grid.map((p) => [p[0], p[1]]) } },
trait: { noAttack: true },
talents: [],
install(battle, t) {
battle.addBuff(t, { key: 'closur:base', flags: { noHeal: true }, persist: true, allowDead: true });
// 撤退 / 被击倒 ⇒ 战术点形态: off the fight for `refresh` s, its tile kept, then back at full HP (while she stands)
battle.on('death', (ctx) => {
if (ctx.unit !== t || battle.finished || !TAC_EXITS.has(ctx.reason) || !up(owner)) return;
t.removed = false;
const seq = (t.mem.closurTacSeq ?? 0) + 1;
t.mem.closurTacSeq = seq;
t.mem.closurTac = { seq, until: battle.time + refresh };
battle.fx('disappear', { x: t.x, y: t.y, id: t.id });
const back = () => {
const tp = t.mem.closurTac;
if (!tp || tp.seq !== seq || battle.finished || t.alive || t.removed) return;
if (!up(owner)) { t.mem.closurTac = null; return; }
if (battle.redeploy(t, { free: true })) { t.mem.closurTac = null; battle.fx('summon', { x: t.x, y: t.y, id: t.id, src: owner.id }); return; }
battle.after(RETRY, back, { owner: t });
};
battle.after(refresh, back, { owner: t });
}, { owner: t, priority: -10 });
},
};
};
return {
// the 援军 is the 指挥中心 (T1): no engine generic 援军; ×atk_scale vs an enemy one of her 援军 blocks
trait: {
install() {},
dmgMul(battle, unit, target) {
const b = target && target.blockedBy;
return b && reinfOf(battle, unit).has(b) ? scale : 1;
},
},
skills: {
[S1]: {
kind: 'duration',
trigger: 'SP_FULL',
onStart({ battle, unit }) {
const uses = unit.mem.closurS1Uses ?? 0;
unit.mem.closurS1Uses = uses + 1;
const n = Math.max(0, Math.min(num(b1.cost_add_max, Infinity), num(b1.cost) + uses * num(b1.cost_per_add)));
const dur = num(s1?.duration, 8);
if (n > 0) dpTicker(unit, { total: n, step: 1, iv: dur / n, first: dur / n / 2 });
const layers = Math.max(1, Math.floor(num(b1.shield_cnt, 1)));
for (const a of reinfOf(battle, unit)) {
battle.addBuff(a, { key: `${SHIELD_KEY}:${unit.id}`, shieldHits: layers, source: unit, tags: ['skill'] });
battle.fx('shield', { x: a.x, y: a.y, id: a.id, src: unit.id });
}
},
onTick({ battle, unit, dt }) { dpTick(battle, unit, dt); },
onEnd({ battle, unit, reason }) { dpEnd(battle, unit, reason); },
},
[S2]: {
kind: 'duration',
mods: { atkPct: num(b2.atk) },
targeting: { maxTargets: Math.max(1, Math.floor(num(b2['attack@max_target'], 2))) },
onStart({ battle, unit }) {
const cost = num(b2.cost);
if (cost > 0) { battle.addDp(unit.ownerId, cost); battle.fx('dp', { x: unit.x, y: unit.y, n: cost, id: unit.id }); }
dpTicker(unit, { total: num(b2.cost_period), step: num(b2['closur_s_2[add_cost_period].cost'], 1), iv: num(b2['closur_s_2[add_cost_period].interval'], 2) });
unit.mem.closurS2Set = new Set();
},
onTick({ battle, unit, dt }) {
dpTick(battle, unit, dt);
// every 援军 DEF +def, block +block_cnt; the enemies they block are her targets
const reinf = reinfOf(battle, unit);
const key = `${S2_KEY}:${unit.id}`;
const had = unit.mem.closurS2Set ?? new Set();
const mods = { defPct: num(b2.def), blockCnt: num(b2.block_cnt) };
for (const a of reinf) if (!a.findBuff(key)) battle.addBuff(a, { key, mods, source: unit, tags: ['skill'] });
for (const a of had) if (!reinf.has(a)) battle.removeBuff(a, key);
unit.mem.closurS2Set = new Set(reinf);
setExtra(battle, unit, blockedTiles(battle, unit, reinf));
},
onEnd({ battle, unit, reason }) {
dpEnd(battle, unit, reason);
for (const a of unit.mem.closurS2Set ?? []) battle.removeBuff(a, `${S2_KEY}:${unit.id}`);
unit.mem.closurS2Set = null;
battle.setExtraRange(unit, null);
},
},
[S3]: {
kind: 'duration',
mods: { batPct: batMod(b3.base_attack_time, chess) },
attack: {
atkScale: num(b3['attack@atk_scale'], 1),
onEachHit({ battle, unit, target }) {
slowdownStack(battle, unit, target, {
step: num(b3['attack@slow_down']), maxStacks: num(b3['attack@max_stack_cnt'], 10),
cap: num(b3['attack@slow_down_max']), duration: num(b3['attack@slow_down_time'], 3),
});
},
},
onStart({ battle, unit }) {
dpTicker(unit, { total: num(b3.cost_period), step: num(b3['closur_s_3[add_cost_period].cost'], 1), iv: num(b3['closur_s_3[add_cost_period].interval'], 1.66) });
unit.mem.closurS3 = { attacks: 0, extra: 0 };
battle.removeBuff(unit, S3_KEY);
},
onAttack({ battle, unit }) {
const q = unit.mem.closurS3;
if (!q) return;
q.attacks++;
if (q.extra < num(b3.max_trigger_cnt, 6) && q.attacks >= num(b3.attack_trigger_cnt, 9)) {
q.attacks = 0;
q.extra++;
battle.addBuff(unit, { key: S3_KEY, mods: { maxTargets: q.extra }, tags: ['skill'] });
}
},
onTick({ battle, unit, dt }) {
dpTick(battle, unit, dt);
// her own range + the 指挥中心's + every 援军's own range + the tiles of the enemies they block
const reinf = reinfOf(battle, unit);
const keys = [];
for (const r of reinf) {
const extra = r.extraRangeKeys ? new Set(r.extraRangeKeys) : null;
for (const k of r.rangeKeys ?? []) if (!extra || !extra.has(k)) keys.push(k);
}
keys.push(...blockedTiles(battle, unit, reinf));
setExtra(battle, unit, keys);
},
onEnd({ battle, unit, reason }) {
dpEnd(battle, unit, reason);
unit.mem.closurS3 = null;
battle.removeBuff(unit, S3_KEY);
battle.setExtraRange(unit, null);
},
},
},
talents: [
{ install(battle, unit) { // 精准投放: the 指挥中心
// her placed pieces run its kit (set up here, before the battle starts — as 鸿雪's 打字机)
for (const t of basesOf(battle, unit)) {
if (t.alive || t.deployed) continue;
if (t.kit) battle.offOwner(t);
battle._setupUnit(t, baseKit(unit, t.def));
}
const summon = () => {
if (!up(unit)) return;
const ps = basesOf(battle, unit);
const live = ps.find((t) => up(t));
if (live) { unit.trait.reinforcement = live; return; }
if (ps.some((t) => !t.alive && !t.removed && t.mem.closurTac)) return; // in its 战术点形态: back by itself
const waiting = ps.find((t) => !t.alive && !t.removed);
if (waiting && battle.redeploy(waiting, { free: true })) {
unit.trait.reinforcement = waiting;
battle.fx('summon', { x: waiting.x, y: waiting.y, id: waiting.id, src: unit.id, token: tokenId });
return;
}
const board = ps.find((t) => t.uid != null);
const tile = tacticalPoint(battle, unit, board ? [board.homeR, board.homeC] : null);
if (!tile) return;
const def = battle.tokenDef(tokenId, unit);
const t = battle.spawnToken(unit, tokenId, tile[0], tile[1], { kit: baseKit(unit, def) });
if (!t) return;
unit.trait.reinforcement = t;
battle.fx('summon', { x: t.x, y: t.y, id: t.id, src: unit.id, token: tokenId });
};
battle.on('deploy', (ctx) => {
if (ctx.unit === unit) { unit.mem.closurS1Uses = 0; summon(); return; }
if (ctx.unit.kind === 'token' && ctx.unit.defId === tokenId && ctx.unit.ownerUnit === unit) unit.trait.reinforcement = ctx.unit;
}, { owner: unit });
// "持有者离场后强制撤退场上所有的指挥中心" (no 战术点形态); one in its 战术点形态 ends it there and waits for her
battle.on('death', (ctx) => {
if (ctx.unit !== unit) return;
for (const t of basesOf(battle, unit)) {
if (t.alive) battle.retreat(t, { reason: 'expired', permanent: true });
else if (t.mem.closurTac) t.mem.closurTac = null;
}
unit.trait.reinforcement = null;
}, { owner: unit });
// TAC-X: damage on one of her 援军 from an enemy that 援军 blocks ×damage_scale
if (guard !== 1) {
battle.on('hit', (ctx) => {
const tg = ctx.target, src = ctx.source;
if (!tg || !src || src.side !== 'enemy' || src.blockedBy !== tg || !up(unit) || !reinfOf(battle, unit).has(tg)) return;
ctx.dmg.mul *= guard;
}, { owner: unit });
}
// S2: 在战术点效果范围内部署干员时,立即返还部署费用的40%(向上取整) — not after a 【移动】
if (unit.skill?.id === S2) {
const ret = num(b2.cost_return);
battle.on('deploy', (ctx) => {
const a = ctx.unit;
if (!(ret > 0) || ctx.initial || ctx.move || !a || a.kind !== 'op' || a.ownerId !== unit.ownerId || a === unit) return;
if (!unit.skill?.active || unit.skill.id !== S2 || !up(unit)) return;
const inRange = basesOf(battle, unit).some((t) => up(t) && (t.rangeKeySet ?? new Set(t.rangeKeys)).has(tileKey(a.tileR, a.tileC)));
if (!inRange) return;
const back = Math.ceil(num(a.base.cost) * ret - 1e-9);
if (back > 0) { battle.addDp(unit.ownerId, back); battle.fx('dp', { x: a.x, y: a.y, n: back, id: unit.id }); }
}, { owner: unit });
}
} },
{ install(battle, unit) { // 极限调度: the 【罗德岛】 operators of her player ATK +atk for the battle (携带可露希尔时)
const atk = num(t1.atk);
if (!(atk > 0)) return;
const give = () => {
for (const a of battle.allyUnits) {
if (a.kind !== 'op' || a.ownerId !== unit.ownerId || (a.def?.raw?.nationId ?? a.def?.nationId) !== 'rhodes') continue;
const cur = a.findBuff(RHODES_KEY);
if (cur && num(cur.data?.v) >= atk) continue;
battle.addBuff(a, { key: RHODES_KEY, mods: { atkPct: atk }, persist: true, allowDead: true, data: { v: atk }, source: unit, tags: ['talent'] });
}
};
if (battle.started) give();
else battle.on('battleStart', give, { owner: unit });
} },
],
};
},
};