Files
ark-stronghold-protocol/server/sim/content/kits/ops/op-wang.js
T

415 lines
26 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-wang.js — 望 (char_2027_wang) 自选 operator kit: 6★ 陷阱师 (特种), an owned-6★ pick of the
// tier-5 and tier-6 自选 slots; every skill, both talents, the trait, the module TRP-X 兽形棋盒 at every form, and the kit of
// her summon 棋子 (token_10064_wang_stone1) with its 跟子. Kit contract and the 自选 rules: ../README.md ("How to add an
// operator (自选)").
//
// Forms (data/backups.json units.char_2027_wang, the DIY slot statuses): normal = E2 Lv1, skills at rank 4, no module;
// elite = E2 Lv60, rank 7, TRP-X at stage 1 (tier 5) or 3 (tier 6). Potential 0 [ASSUMED: no account].
// Sources: character_table / skill_table / battle_equip_table / token_table (zh_CN, as built into backups.json; the 棋子
// owner-form / skill / module variants) and PRTS 望 (铸子 备注: the 跟子 are 望's own projectiles — no unit, they take no
// tile, act and connect like a 棋子; they go on deployable tiles and on undeployable ground with no character unit; order
// "有敌人的>没有敌人的;不可部署地块>可部署地面地块>可部署高台地块…相同优先级时按棋子上>右>下>左"; a 棋子 deals its damage as
// itself, a 跟子 as 望. 料敌机先 备注: the RES ignore is the source's 法术穿透 for that damage. S1 备注 "棋子持有库存达到上限的情况下
// 具有阻回 / 法术伤害效果可独立叠加生效(每个效果会独立记录触发棋子的第二天赋层数)…可对空 / …由望依自身属性预计算伤害后、以无来源
// 的形式施加给目标". S2 备注 "若相连棋子/跟子在上下侧则激活垂直效果范围(自身及上下3格),左右侧则激活水平效果范围…效果范围不因相连
// 棋子/跟子消失而取消激活 / 移动速度降低为直接乘算,可无限叠加,每层独立计时(但移动速度存在0.1数值下限)". S3 备注 "棋子与跟子的
// 攻击范围/触发范围为…,触发时对范围内所有敌方单位造成伤害(可对空) / 技能开启时自动“部署”的为第一天赋的跟子"), PRTS 棋子 (备注
// "技能伤害始终借用持有者的攻击力计算,但伤害来源于自身"), PRTS 分支特性信息 陷阱师 ("可对空 / “敌人已在”…地面行动敌人… / 干员离场
// 后,附属的陷阱随之消失"), PRTS 卫戍协议/帮助 §作战阶段 (placed summons deploy with the board "无视所属干员的持有状态,不消耗持有数量";
// "若召唤物在战斗期间退场,将在满足条件后立即原地再部署1个").
// - Trait (陷阱师): ranged physical arrows, 3-3, hits air units (PRTS 分支特性信息 "可对空"), blocks 1, ground-targetable.
// "陷阱无法放置于敌人已在的格子中": a 棋子 does not come back while a ground enemy stands on its tile (S3: inside her range it
// may). Module TRP-X "部署费用更低(-1)…可同时部署的陷阱数量提升(+1)": the 棋子 variant's cost 2 (the DP of its returns) and
// deploy limit 7 (the hand count — the match's); its attributes in the stats.
// - T1 铸子 (cnt / attack@max_spawn_cnt; "最多拥有7枚"): the 棋子 are hand pieces the player places; they deploy with the board
// for free. Her stock (持有库存: the 棋子 in her hand — cnt at the start, at most STOCK_CAP; the skills add to it) counts
// them — the owner's decision of 2026-10-06, a deliberate deviation from PRTS 卫戍协议/帮助 (placed summons "无视所属干员的
// 持有状态,不消耗持有数量", which left her stock at cnt: one S1 / S2 cast reached the cap and 阻回 held her SP for the rest
// of the battle): every deployment of a 棋子 (the battle start's and each return) takes one from it, like one she deployed
// herself (never below 0 [ASSUMED: a 7th placed piece of TRP-X's deploy limit takes none]), and a 棋子 that leaves the
// field (set off, or gone with her) goes back into it at once by the official summon rule — its card back in her hand
// while its redeploy time runs (as shared/summoner.js's recall) —, at most the cap [ASSUMED: one over the cap is lost].
// So "立即获得两枚棋子" and the 阻回 at the cap keep cycling through the battle. 棋子 / 跟子
// that touch on a side (上下 / 左右) activate each other and stay active once activated [ASSUMED from the S2 备注]; an
// enemy (air units too) on an active one's tile sets it off: its effect, then it is used up (a 棋子 leaves the field; a
// 跟子 is gone) [ASSUMED: a trap is spent by its trigger]. A spent 棋子 comes back on its tile ("原地再部署") after its
// redeploy time (2 s), paying its cost and one of her stock, while 望 is on the field. That return is the mode's
// deployment of her card: it counts as a 手动部署 for 铸子 —
// one 跟子 on a tile next to it (the PRTS order above), at most attack@max_spawn_cnt on the field [ASSUMED: the battle-start
// deployment does not count]. When 望 leaves the field her 棋子 and 跟子 vanish (PRTS 分支特性信息); the 棋子 come back
// with her.
// - T2 料敌机先 (attack@per_atk_scale / attack@per_magic_resist_penetrate_fixed / attack@max_trigger_cnt): an active 棋子 / 跟子
// gets one stack per piece of the unbroken line it is part of (itself included; the longer of its lines; ≤ the cap),
// kept once reached [ASSUMED]; its damage ×(1 + n × per) and n × pen RES ignored. TRP-X stage 3: 13 % / 12.
// - S1 取势 (AUTO): passive — a triggered piece puts 停顿 attack@sluggish s on the enemy that set it off and a 法术 DoT of
// attack@atk_scale × her ATK every second for attack@sluggish s (precomputed at the trigger with the piece's stacks and
// her damage multipliers; 无来源, credited to her; each one independent). Active — "立即获得两枚棋子": +cnt stock (≤ the
// cap); with the stock full she holds 阻回. An AUTO skill acting on nobody fires at full SP (`trigger: 'SP_FULL'`, the
// owner's AUTO rule).
// - S2 连星 (AUTO, SP_FULL): passive — a triggered piece deals attack@atk_scale × her ATK arts to every enemy on its active
// lines (its tile ± 3 along each side it was ever connected on; air units too) and a 直接乘算 slow attack@move_speed for
// attack@duration s, each one on its own timer, added up (speed ≥ 0.1). Active as S1.
// - S3 天下劫 (MANUAL, data ACTIVE_RANGE on its 4-12; ammo trigger_time): passive — the pieces' trigger and damage area is the
// token skill's range (x-6 in the data; PRTS writes x-1), atk_scale × her ATK arts on every enemy there. Active — no
// attacks, range 4-12; +cnt stock, the part over the cap placed at once as 跟子 on the enemies of her range first (her
// target order), then on other tiles of it by the 铸子 order [ASSUMED for the ties: nearest to her, then by tile]; a 棋子
// coming back inside her range during it drops up to 3 跟子, one bullet each ("第一天赋额外至多部署3枚棋子并消耗等量弹药");
// it ends when the bullets are spent ("棋子耗尽" [ASSUMED]); bullets left at another end return to the stock.
import { num, talentBb, skillRec, up } from '../shared/tier1.js';
import { absoluteRangeKeys, sortEnemyTargets } from '../../../targeting.js';
import { bodyOnTile } from '../../../body.js';
import { COLS, ROWS } from '../../../constants.js';
const S1 = 'skchr_wang_1';
const S2 = 'skchr_wang_2';
const S3 = 'skchr_wang_3';
export const STONE = 'token_10064_wang_stone1';
/** 铸子 "最多拥有7枚" (the 棋子's max deck stack at E2; the talent text). */
export const STOCK_CAP_FALLBACK = 7;
/** 料敌机先's stack cap when the data carries none. */
const T2_CAP_FALLBACK = 3;
/** 连星 "两侧3格" (the token blackboard's tile_count 6, both sides). */
const LINE_FALLBACK = 6;
/** The S3 token skill's range when the data carries none (range_table x-6). */
const X6 = Object.freeze([[2, 0], [1, 0], [0, -2], [0, -1], [0, 0], [0, 1], [0, 2], [-1, 0], [-2, 0]]);
/** 跟子 neighbours in the PRTS order 上 > 右 > 下 > 左 (UP = +row; the mirrored field swaps 右 / 左). */
const SIDES = Object.freeze([[1, 0], [0, 1], [-1, 0], [0, -1]]);
const SIDES_MIRRORED = Object.freeze([[1, 0], [0, -1], [-1, 0], [0, 1]]);
/** "额外至多部署3枚棋子" (S3, no blackboard key). */
const S3_EXTRA = 3;
/** Seconds between two tries of a spent 棋子 to come back. */
const RETRY = 0.25;
const SLOW_KEY = 'wang:slow';
const DOT_KEY = 'wang:qushi';
const TAG = 'wang:stone';
/** Profile the pieces select enemies with: air units too. */
const ANY = Object.freeze({ canHitFly: true });
const bbOf = (chess, id) => skillRec(chess, id)?.bb ?? {};
const keyOf = (r, c) => (r >= 0 && r < ROWS && c >= 0 && c < COLS ? r * COLS + c : -1);
/**
* The 跟子 placement class of a tile: 0 undeployable ground (walkable, nothing may be deployed there), 1 deployable
* ground, 2 deployable 高台; null where no 跟子 may go (outside the field, walls, flyer-only tiles that take nobody).
*/
function tileClass(battle, r, c) {
if (!battle.grid.inRect(r, c)) return null;
const t = battle.grid.tile(r, c);
if (t.build === 'NONE') return t.height === 'LOW' && t.pass === 'ALL' ? 0 : null;
if (t.height === 'LOW' && (t.build === 'ALL' || t.build === 'MELEE')) return 1;
if (t.build === 'RANGED' || t.build === 'ALL') return 2;
return null;
}
/**
* 连星's slow (PRTS 备注 "移动速度降低为直接乘算,可无限叠加,每层独立计时(但移动速度存在0.1数值下限)"): one buff per enemy holding
* every application (its value, its end); moveMul = max(0.1 ÷ base speed, 1 + Σ values).
*/
function addSlow(battle, src, e, value, duration) {
if (!e.alive || !(duration > 0) || !value) return;
const refresh = (unit, b) => {
const now = battle.time;
b.data.list = b.data.list.filter((x) => x.until > now + 1e-9);
if (!b.data.list.length) { battle.removeBuff(unit, b); return; }
const sum = b.data.list.reduce((a, x) => a + x.v, 0);
const floor = Math.min(1, 0.1 / Math.max(1e-6, unit.base.moveSpeed));
const mul = Math.max(floor, 1 + sum);
if (b.mods?.moveMul !== mul) { b.mods = { moveMul: mul }; unit.markDirty(); }
};
let b = e.findBuff(SLOW_KEY);
if (!b) b = battle.addBuff(e, { key: SLOW_KEY, source: src, tags: ['skill'], data: { list: [] }, mods: { moveMul: 1 }, onTick: ({ unit, buff }) => refresh(unit, buff) });
if (!b) return;
b.data.list.push({ v: value, until: battle.time + duration });
refresh(e, b);
}
export default {
char_2027_wang: (bb, chess) => {
const t0 = talentBb(chess, 0); // 铸子: cnt, attack@max_spawn_cnt
const t1 = talentBb(chess, 1); // 料敌机先 (TRP-X stage 2+: its change)
const t0desc = String((chess?.talents ?? []).find((t) => t && t.index === 0)?.desc ?? '');
const capText = /最多拥有(\d+)枚/.exec(t0desc);
const b1 = bbOf(chess, S1), b2 = bbOf(chess, S2), b3 = bbOf(chess, S3);
const s3 = skillRec(chess, S3);
const per = num(t1['attack@per_atk_scale']), pen = num(t1['attack@per_magic_resist_penetrate_fixed']);
const t2cap = Math.max(1, Math.floor(num(t1['attack@max_trigger_cnt'], T2_CAP_FALLBACK)));
const followCap = Math.max(0, Math.floor(num(t0['attack@max_spawn_cnt'])));
const stock0 = Math.max(0, Math.floor(num(t0.cnt)));
const stockCap = capText ? +capText[1] : STOCK_CAP_FALLBACK;
/** Every live piece of 望: her deployed 棋子 and her 跟子, in the order they came. */
const piecesOf = (battle, unit) => {
const out = [];
for (const a of battle.allyUnits) {
if (a.kind !== 'token' || a.defId !== STONE || a.ownerUnit !== unit || !a.alive || !a.deployed || !a.mem.wangNode) continue;
const n = a.mem.wangNode;
n.r = a.tileR; n.c = a.tileC; n.stone = a;
out.push(n);
}
for (const f of unit.mem.wang.followers) out.push(f);
return out.sort((a, b) => a.seq - b.seq);
};
const groundFoeOn = (battle, r, c) => battle.enemies.some((e) => e.alive && !e.hidden && !e.isFlying && bodyOnTile(e, r, c));
const anyFoeOn = (battle, r, c) => battle.enemies.some((e) => e.alive && !e.hidden && bodyOnTile(e, r, c));
/** Place a 跟子 on (r, c) (the caller checked the tile). */
const addFollower = (battle, unit, r, c) => {
const w = unit.mem.wang;
const f = { r, c, seq: ++w.seq, act: false, h: false, v: false, stacks: 0, follower: true };
w.followers.push(f);
battle.fx('summon', { x: c, y: r, id: unit.id, src: unit.id, token: STONE, follower: true });
return f;
};
/** Tiles a 跟子 may take: in the field, a class, no character unit or knocked-out operator, no piece. */
const followerFree = (battle, unit, r, c, taken) => {
const k = keyOf(r, c);
return k >= 0 && !taken.has(k) && tileClass(battle, r, c) != null && !battle.unitAt(r, c) && !battle.downOn(r, c);
};
/** 铸子: up to `n` 跟子 next to (r, c) in the PRTS order; returns how many were placed. */
const dropFollowers = (battle, unit, r, c, n) => {
const w = unit.mem.wang;
let placed = 0;
for (let i = 0; i < n && w.followers.length < followCap; i++) {
const taken = new Set(piecesOf(battle, unit).map((p) => keyOf(p.r, p.c)));
const sides = battle.getPlayer(unit.ownerId)?.mirror ? SIDES_MIRRORED : SIDES;
const cands = [];
sides.forEach(([dr, dc], idx) => {
const rr = r + dr, cc = c + dc;
if (!followerFree(battle, unit, rr, cc, taken)) return;
cands.push({ rr, cc, rank: [anyFoeOn(battle, rr, cc) ? 0 : 1, tileClass(battle, rr, cc), idx] });
});
if (!cands.length) break;
cands.sort((a, b) => a.rank[0] - b.rank[0] || a.rank[1] - b.rank[1] || a.rank[2] - b.rank[2]);
addFollower(battle, unit, cands[0].rr, cands[0].cc);
placed++;
}
return placed;
};
/** S3 bullets: spend `n` (the skill ends with the last). */
const spendAmmo = (battle, unit, n) => {
const sk = unit.skill, w = unit.mem.wang;
if (!sk || sk.id !== S3 || !sk.active || !(n > 0)) return;
w.ammo = Math.max(0, w.ammo - n);
sk.ammoLeft = w.ammo;
if (battle.hasHook('ammoUsed')) battle.emit('ammoUsed', { unit, left: w.ammo, skill: sk });
if (w.ammo <= 0) sk.end('ammo');
};
const inHerRange = (unit, r, c) => !!unit.rangeKeySet?.has(keyOf(r, c));
/** A spent 棋子 comes back on its tile ("原地再部署"); its return drops the 铸子 跟子. */
const scheduleReturn = (battle, unit, stone) => {
const at = battle.time + Math.max(0, num(stone.base.respawnTime));
battle.every(RETRY, (b, sched) => {
if (stone.alive || stone.removed || b.finished) { sched.cancel(); return; }
if (b.time + 1e-9 < at || !up(unit) || unit.mem.wang.stock < 1) return; // (one of her stock: the deployment takes it)
const r = stone.homeR, c = stone.homeC;
const s3on = unit.skill?.id === S3 && unit.skill.active && inHerRange(unit, r, c);
if (groundFoeOn(b, r, c) && !s3on) return; // "陷阱无法放置于敌人已在的格子中"
// (a 跟子 on its tile goes: "跟子所在格进行部署…时,该跟子会随之消失")
const w = unit.mem.wang;
const before = w.followers.length;
w.followers = w.followers.filter((f) => f.r !== r || f.c !== c);
if (!b.redeploy(stone, { free: false })) { if (w.followers.length !== before) b.fx('disappear', { x: c, y: r, id: unit.id }); return; }
sched.cancel();
if (s3on) spendAmmo(b, unit, dropFollowers(b, unit, r, c, Math.min(S3_EXTRA, w.ammo)));
else dropFollowers(b, unit, r, c, 1);
}, { owner: unit });
};
/** The 棋子's kit: no attack, untargetable, its node data per deployment, its return when spent. */
const stoneKit = (owner) => ({
skill: null,
trait: { noAttack: true },
install(battle, unit) {
// [ASSUMED] a trap: no enemy selects it
battle.addBuff(unit, { key: 'trait:wangStone', flags: { untargetable: true }, persist: true, allowDead: true });
battle.on('deploy', (ctx) => {
if (ctx.unit !== unit) return;
const w = owner.mem.wang;
unit.mem.wangNode = { seq: ++w.seq, act: false, h: false, v: false, stacks: 0 };
w.stock = Math.max(0, w.stock - 1); // it occupies her stock (the owner's decision of 2026-10-06)
}, { owner: unit });
battle.on('death', (ctx) => {
if (ctx.unit !== unit || battle.finished) return;
unit.removed = false; // the piece is kept (Battle.redeploy accepts it; its hooks stay)
unit.mem.wangNode = null;
const w = owner.mem.wang;
w.stock = Math.min(stockCap, w.stock + 1); // its card goes back into her stock (the official summon rule)
scheduleReturn(battle, owner, unit);
}, { owner: unit, priority: -10 });
},
});
return {
skills: {
[S1]: {
kind: 'instant', trigger: { rule: 'SP_FULL' },
onStart({ unit }) { const w = unit.mem.wang; w.stock = Math.min(stockCap, w.stock + Math.max(0, Math.floor(num(b1.cnt)))); },
},
[S2]: {
kind: 'instant', trigger: { rule: 'SP_FULL' },
onStart({ unit }) { const w = unit.mem.wang; w.stock = Math.min(stockCap, w.stock + Math.max(0, Math.floor(num(b2.cnt)))); },
},
[S3]: {
kind: 'ammo',
ammo: Math.max(1, Math.floor(num(b3.trigger_time, 20))),
targeting: { rangeGrid: s3?.rangeGrid ?? null },
attack: { noAttack: true },
onStart({ battle, unit, skill }) {
const w = unit.mem.wang;
w.ammo = skill.ammoLeft;
const total = w.stock + Math.max(0, Math.floor(num(b3.cnt)));
const over = Math.max(0, total - stockCap);
w.stock = Math.min(stockCap, total);
// "将超出上限的棋子优先部署在范围内敌人所在位置": enemies of her (4-12) range first, then other tiles of it
let n = Math.min(over, followCap - w.followers.length);
if (!(n > 0)) return;
const taken = new Set(piecesOf(battle, unit).map((p) => keyOf(p.r, p.c)));
const foes = sortEnemyTargets(battle, unit, battle.enemiesInKeys(unit.rangeKeys, unit, ANY), unit.profile?.priority ?? null);
for (const e of foes) {
if (n <= 0) break;
const r = Math.round(e.y), c = Math.round(e.x);
if (!followerFree(battle, unit, r, c, taken)) continue;
taken.add(keyOf(r, c));
addFollower(battle, unit, r, c);
n--;
}
if (n <= 0) return;
const rest = [];
for (const k of unit.rangeKeys ?? []) {
const r = Math.floor(k / COLS), c = k % COLS;
if (!followerFree(battle, unit, r, c, taken)) continue;
rest.push({ r, c, k, cls: tileClass(battle, r, c), d: Math.max(Math.abs(r - unit.tileR), Math.abs(c - unit.tileC)) });
}
rest.sort((a, b) => a.cls - b.cls || a.d - b.d || a.k - b.k);
for (const t of rest.slice(0, n)) addFollower(battle, unit, t.r, t.c);
},
onEnd({ unit, reason }) {
const w = unit.mem.wang;
if (reason !== 'ammo' && w.ammo > 0) w.stock = Math.min(stockCap, w.stock + w.ammo); // "剩余的弹药返还为棋子"
w.ammo = 0;
},
},
},
talents: [
// 铸子 (and the 棋子's own kit): her 棋子 pieces run the stone kit — set up here, before the battle starts
{ install(battle, unit) {
unit.mem.wang = { stock: stock0, followers: [], seq: 0, ammo: 0 };
for (const t of battle.allyUnits) {
if (t.kind !== 'token' || t.defId !== STONE || t.ownerUnit !== unit || t.alive || t.deployed) continue;
if (t.kit) battle.offOwner(t);
battle._setupUnit(t, stoneKit(unit));
}
} },
{ install() {} }, // 料敌机先 — the pieces' stacks (kit install)
],
install(battle, unit) {
const sid = unit.skill?.id;
const tok = battle.tokenDef(STONE, unit);
const tbb = tok?.skill?.bb ?? {};
const line = Math.max(0, Math.floor(num(tbb.tile_count, LINE_FALLBACK) / 2));
const trigGrid = sid === S3 ? (tok?.skill?.rangeGrid ?? X6) : null;
/** Trigger / damage tiles of a piece: its own tile, or S3's area around it. */
const trigKeys = (n) => (trigGrid ? absoluteRangeKeys(trigGrid, n.r, n.c, 'RIGHT', 0) : [keyOf(n.r, n.c)].filter((k) => k >= 0));
/** 连星's lines of a piece: its tile ± `line` along each side it was connected on. */
const lineKeys = (n) => {
const out = new Set();
for (let k = -line; k <= line; k++) {
if (n.h) { const x = keyOf(n.r, n.c + k); if (x >= 0) out.add(x); }
if (n.v) { const x = keyOf(n.r + k, n.c); if (x >= 0) out.add(x); }
}
return [...out];
};
const hitOpts = (n) => ({ type: 'arts', isSkill: true, mul: 1 + per * n.stacks, resIgnoreFlat: pen * n.stacks, tags: ['skill', TAG] });
/** A piece sets off: its effect (the picked skill's passive) on `foes` (the enemies of its trigger tiles). */
const fire = (n, foes) => {
const src = n.stone ?? unit;
battle.fx('explode', { x: n.c, y: n.r, r: trigGrid ? 2 : 0.5, id: src.id, src: unit.id, dmgType: 'arts', skill: sid === S3 ? 'wang_3' : sid === S2 ? 'wang_2' : 'wang_1' });
if (sid === S1) {
const e = sortEnemyTargets(battle, src, foes.slice(), null)[0];
if (!e) return;
const dur = num(b1['attack@sluggish']);
battle.applyStatus(e, 'sluggish', { duration: dur, source: unit });
// precomputed by 望 at the trigger (her ATK and damage multipliers, the piece's stacks), dealt as 无来源
const s = unit.s;
const amount = s.atk * num(b1['attack@atk_scale']) * (1 + per * n.stacks) * s.dmgDealtMul * s.artsDealtMul;
const rif = pen * n.stacks + s.resIgnoreFlat, rip = s.resIgnorePct;
if (amount > 0 && dur > 0) {
battle.addBuff(e, {
key: DOT_KEY, refresh: 'independent', maxStacks: 999, duration: dur, interval: 1, source: unit, tags: ['skill', 'dot'],
onTick: ({ unit: t }) => { if (t.alive) battle.dealDamage(unit, t, { amount, type: 'arts', canDodge: false, isSkill: true, sourceless: true, resIgnoreFlat: rif, resIgnorePct: rip, tags: ['skill', 'dot', DOT_KEY] }); },
});
}
return;
}
const victims = sid === S2 ? battle.enemiesInKeys(lineKeys(n), src, ANY) : foes;
const scale = sid === S2 ? num(b2['attack@atk_scale']) : num(b3.atk_scale);
for (const e of victims) {
if (!e.alive) continue;
battle.dealDamage(src, e, { amount: unit.s.atk * scale, ...hitOpts(n) });
if (sid === S2 && e.alive) addSlow(battle, unit, e, num(b2['attack@move_speed']), num(b2['attack@duration']));
}
};
/** The piece is spent: a 棋子 leaves the field (it comes back on its own), a 跟子 is gone. */
const spend = (n) => {
n.gone = true;
if (n.stone) { if (n.stone.alive) battle.retreat(n.stone, { reason: 'expired', permanent: true }); return; }
const w = unit.mem.wang;
w.followers = w.followers.filter((f) => f !== n);
};
battle.on('tick', () => {
if (!up(unit) || battle.finished) return;
const nodes = piecesOf(battle, unit);
if (!nodes.length) return;
const at = new Map(nodes.map((n) => [keyOf(n.r, n.c), n]));
for (const n of nodes) { // 棋子相连时相互激活 (kept once active)
for (const [dr, dc] of SIDES) {
const k = keyOf(n.r + dr, n.c + dc);
if (k < 0 || !at.has(k)) continue;
n.act = true;
if (dr) n.v = true; else n.h = true;
}
}
if (per > 0 || pen > 0) {
for (const n of nodes) { // 料敌机先: one stack per piece of its unbroken line (≤ the cap)
if (!n.act) continue;
for (const [dr, dc] of [[0, 1], [1, 0]]) {
let run = 1; // (keyOf is −1 off the stage: never a piece)
for (let k = 1; at.has(keyOf(n.r + dr * k, n.c + dc * k)); k++) run++;
for (let k = 1; at.has(keyOf(n.r - dr * k, n.c - dc * k)); k++) run++;
if (run >= 2) n.stacks = Math.max(n.stacks, Math.min(t2cap, run));
}
}
}
for (const n of nodes) {
if (!n.act || n.gone) continue;
const src = n.stone ?? unit;
const foes = battle.enemiesInKeys(trigKeys(n), src, ANY);
if (!foes.length) continue;
fire(n, foes);
spend(n);
}
}, { owner: unit });
// 阻回 while the stock is full (S1 / S2 备注)
if (sid === S1 || sid === S2) {
battle.on('tick', () => {
const want = up(unit) && unit.mem.wang.stock >= stockCap;
const has = unit.findBuff('wang:stockFull');
if (want && !has) battle.addBuff(unit, { key: 'wang:stockFull', flags: { noSp: true }, tags: ['skill'] });
else if (!want && has) battle.removeBuff(unit, 'wang:stockFull');
}, { owner: unit });
}
// "干员离场后,附属的陷阱随之消失": her 棋子 leave (and come back with her), her 跟子 are gone
battle.on('death', (ctx) => {
if (ctx.unit !== unit || battle.finished) return;
unit.mem.wang.followers = [];
for (const a of battle.allyUnits) {
if (a.kind === 'token' && a.defId === STONE && a.ownerUnit === unit && a.alive) battle.retreat(a, { reason: 'expired', permanent: true });
}
}, { owner: unit });
},
};
},
};