feedback5 O19: 自选 kits for 老鲤, 温蒂 (with her 工程蓄水炮) and 望 (with her 棋子 and 跟子)

Three owned 6★ picks get their operator kits (kits/ops/op-lmlee.js, op-weedy.js, op-wang.js, appended to
OPERATOR_KIT_FILES): every skill at rank 4 / 7, both talents, the trait and every module at stage 1 / 3, and the kits of
the two summons (set up by the owner's talent, as 鸿雪's 打字机).

- 老鲤 (行商): MER-X 2 DP, MER-Y ATK +4 % per payment (5 stacks); 和气生财 (±14, ×2 alone; MER-Y Lv3 ±20); 有备无患 through
  the merchantPay hook (5 DP for a guard against the next 晕眩 / 冻结, counter-stun 3 s; MER-X Lv3 4 DP / 4 s); S1 / S3 fire
  at full SP (the owner's AUTO rule), S2's mark bursts per its PRTS notes, S3's push and outside-range dodge.
- 温蒂 (推击手): S1 next attack (push + stun), S2 water mode (×3.2 interval, range +1, 0.9 splash, pushes), S3 liquid
  nitrogen (1.2 splash, push, 移动创伤 1200 per tile); the cannon (20 s, back on its tile, nearest target, pushes, its
  forced skill, fires with her S3 beside her); 蓄水炮强化 SP (PUS-X Lv3 every 2 s and ATK +20 %); PUS-X refund on a 高台,
  PUS-Y +1 force while blocking two; her composed record takes a 高台 (shared/highGround.js unchanged).
- 望 (陷阱师): hits air; 棋子 activation, trigger, in-place return (2 s, cost; TRP-X 2 DP), 跟子 in the PRTS order, 料敌机先
  stacks, S1 DoT + 停顿, S2 line damage + additive slow, S3 x-6 passive and the 天下劫 overflow / bullets.

Match: the 自选 hand count of a placeable summon reads the active module's byModule deploy limit (server/match/player/
diy.js placeableTokens) — 望 with TRP-X gets 7 棋子; nobody else changes (多萝西's 13 stays at the 9 cap).
Docs: PLAYING.md §4 (温蒂 on a 高台), SIM.md §8 (陷阱师 air through the data), kits/README.md (module deploy limit).

Golden: only diy moved — diy-106 … diy-109 are now the 老鲤 / 温蒂 battles (the prototype battles they held move on),
diy-110 … diy-115 are new (望, then the prototypes, seeds per index); roster, bonds, fields, matches, standins unchanged.
Tests: test/content/op_lmlee.test.js (8), op_weedy.test.js (10), op_wang.test.js (8).
This commit is contained in:
sganggs committed 2026-10-06 05:09:27 +08:00
1 parent 996f5d20fe
commit 8627791e53
12 files changed
+2293 -101

No files matched your search

+403
View File
@@ -0,0 +1,403 @@
// 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 and without using her stock (stock = cnt at the start, at most STOCK_CAP; the skills add to it). 棋子 / 跟子
// 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, while 望 is on the field — its own card returns to the stock as it leaves, so the
// stock does not change [ASSUMED]. 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)) return;
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;
unit.mem.wangNode = { seq: ++owner.mem.wang.seq, act: false, h: false, v: false, stacks: 0 };
}, { 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;
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 });
},
};
},
};