diff --git a/docs/SIM.md b/docs/SIM.md index 70bd6278..c543acac 100644 --- a/docs/SIM.md +++ b/docs/SIM.md @@ -121,7 +121,8 @@ its loadout's `skillIndex` / `moduleId` and no equipment (the copies' items went Token pieces: `{ kind:'token', tokenId, ownerUid, row, col, dir? }` — the manually deployable summons the player placed (data `placeable`, PRTS 卫戍协议/帮助 §战斗部署; user playtest #6). A piece marks the tile its summon deploys on: a talent summon the owner holds from the start (狼群, 海嗣, 流形, 凯瑟琳's 爬行号·防护单元 — facing the operator it shields) deploys -with the board; a skill's summon (赫默 S2 医疗探机, 巫恋 S2 诅咒娃娃, "获得一个…") deploys once at the battle start, free +with the board (望's 棋子 too, each one occupying her stock while it stands and going back into it as it leaves — the +owner's decision of 2026-10-06, kits/ops/op-wang.js); a skill's summon (赫默 S2 医疗探机, 巫恋 S2 诅咒娃娃, "获得一个…") deploys once at the battle start, free and regardless of the holding (PRTS §作战阶段 "所有手动部署的召唤物,无视所属干员的持有状态…作战开始时立即部署一次"), then waits on its tile and takes the field there each time the skill gives one (stock ≤ 1, after the token's redeploy time once it left, free; never while its owner is off the field [ASSUMED] — a stocked one deploys as soon as the owner is diff --git a/server/sim/content/kits/ops/op-wang.js b/server/sim/content/kits/ops/op-wang.js index 2519c3f9..1101594c 100644 --- a/server/sim/content/kits/ops/op-wang.js +++ b/server/sim/content/kits/ops/op-wang.js @@ -22,12 +22,19 @@ // 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). 棋子 / 跟子 +// 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, 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 铸子 — +// 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. @@ -195,7 +202,7 @@ export default { 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; + 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; // "陷阱无法放置于敌人已在的格子中" @@ -219,12 +226,16 @@ export default { 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 }; + 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 }); }, diff --git a/test/content/op_wang.test.js b/test/content/op_wang.test.js index 9f67a7e9..ade79485 100644 --- a/test/content/op_wang.test.js +++ b/test/content/op_wang.test.js @@ -216,7 +216,8 @@ test('S1 取势 (AUTO, SP_FULL): the triggering enemy is 停顿 6.5 s and takes const sk = skillOf(tier, elite, S1); assert.deepEqual([sk.spCost, sk.bb.cnt, sk.bb['attack@atk_scale'], sk.bb['attack@sluggish']], elite ? [20, 2, 1.2, 6.5] : [21, 2, 1.05, 6.5], `T${tier}`); const { h, u, st } = field({ tier, elite, skill: 0, stones: [[10, 6], [10, 7]] }); - assert.deepEqual([u.skill.kind, u.skill.rule, u.mem.wang.stock], ['instant', 'SP_FULL', 6]); + // her stock: 6 less the 2 placed 棋子 (the owner's decision of 2026-10-06 — see the stock test below) + assert.deepEqual([u.skill.kind, u.skill.rule, u.mem.wang.stock], ['instant', 'SP_FULL', 4]); const e = h.spawn('enemy_dummy', { pos: [10, 7] }); h.step(); const slug = h.hooksOf('statusApplied').find((c) => c.target === e && c.status === 'sluggish'); @@ -238,7 +239,7 @@ test('S1 取势 (AUTO, SP_FULL): the triggering enemy is 停顿 6.5 s and takes h.step(); h.step(); assert.equal(e2.buffs.filter((b) => b.key === 'wang:qushi').length, 2, `T${tier}: independent`); - // the active part: +2 stock (6 → 7: the cap), then 阻回 + // the active part: +2 stock (6 → 7: the cap — both pieces are off the field, their cards back in her hand), then 阻回 u.skill.gainSp(999); assert.ok(h.runUntil(() => u.skill.activations === 1, 1), `T${tier}: cast at full SP`); assert.equal(u.mem.wang.stock, 7, `T${tier}: 最多拥有7枚`); @@ -248,6 +249,56 @@ test('S1 取势 (AUTO, SP_FULL): the triggering enemy is 停顿 6.5 s and takes } }); +test('her stock (the owner\'s decision of 2026-10-06): the placed 棋子 occupy it like ones she deployed; a 棋子 that goes off goes back into it at once and its return takes one again; at the cap one that goes off is lost to the cap, so 阻回 lifts with its return and S1 / S2 keep casting', () => { + for (const skill of [0, 1]) { + const S = `S${skill + 1}`; + const { h, u, st } = field({ tier: 6, elite: true, skill, stones: [[10, 6], [10, 7]] }); + const w = u.mem.wang; + assert.equal(w.stock, 6 - 2, `${S}: 6 less the 2 placed`); + const a = at(st, 10, 6), b = at(st, 10, 7); + let e = h.spawn('enemy_dummy', { pos: [10, 7] }); + h.step(); + assert.ok(!b.alive, `${S}: set off`); + assert.equal(w.stock, 5, `${S}: its card is back in her stock at once`); + h.b.kill(e, null); + assert.ok(h.runUntil(() => b.alive, 5), `${S}: back on its tile`); + assert.equal(w.stock, 4, `${S}: its return takes one`); + // two casts: 4 → 6 → 7 (8, over the cap); 阻回 at the cap only + u.skill.gainSp(999); + h.step(); + assert.deepEqual([u.skill.activations, w.stock], [1, 6], `${S}: 立即获得两枚棋子`); + h.step(); + assert.ok(!u.s.flags.noSp, `${S}: below the cap, no 阻回`); + u.skill.gainSp(999); + h.step(); + assert.deepEqual([u.skill.activations, w.stock], [2, 7], `${S}: 最多拥有7枚`); + h.step(); + assert.ok(u.s.flags.noSp, `${S}: 阻回 at the cap`); + const sp0 = u.skill.sp; + h.run(2); + assert.equal(u.skill.sp, sp0, `${S}: no SP under 阻回`); + // at the cap: the piece that goes off is lost to the cap, its return takes one ⇒ 阻回 lifts, SP comes back + e = h.spawn('enemy_dummy', { pos: [10, 6] }); + h.step(); + assert.ok(!a.alive, `${S}: set off at the cap`); + assert.equal(w.stock, 7, `${S}: still the cap`); + h.b.kill(e, null); + assert.ok(h.runUntil(() => a.alive, 5), `${S}: back`); + assert.equal(w.stock, 6, `${S}: its return took one`); + h.step(); + assert.ok(!u.s.flags.noSp, `${S}: 阻回 lifted`); + const sp1 = u.skill.sp; + h.run(2); + assert.ok(u.skill.sp > sp1, `${S}: her SP recovers again`); + done(h); + } + // TRP-X: 7 placed 棋子 (its deploy limit) on a stock of 6 ⇒ 0, never below + const { h, u, st } = field({ tier: 6, elite: true, mod: X, skill: 0, stones: [[10, 3], [10, 4], [10, 5], [10, 6], [10, 7], [10, 8], [10, 9]] }); + assert.ok(st.length === 7 && st.every((s) => s.alive), 'TRP-X: all 7 deploy'); + assert.equal(u.mem.wang.stock, 0, 'TRP-X: 7 placed on a stock of 6'); + done(h); +}); + test('S2 连星 (AUTO, SP_FULL): a triggered piece hits every enemy on its connected lines (its tile ± 3; air too) for 420 % / 480 % ATK arts and slows them 35 % / 40 % for 6 s — each slow on its own timer, added up, speed ≥ 0.1', () => { for (const [tier, elite] of [[5, false], [6, true]]) { const sk = skillOf(tier, elite, S2); @@ -308,7 +359,7 @@ test('S3 天下劫 (MANUAL, ACTIVE_RANGE 4-12, 20 bullets): passive — the piec assert.equal(u.skill.attackOverride()?.noAttack, true, `T${tier}: 停止攻击`); assert.equal(u.mem.wang.stock, 7, `T${tier}: +8, capped`); const fl = placed(h).map(([r, c]) => `${r},${c}`); - assert.equal(fl.length, 7, `T${tier}: 6 + 8 − 7 over the cap`); + assert.equal(fl.length, 5, `T${tier}: (6 − 2 placed) + 8 − 7 over the cap`); assert.deepEqual(fl.slice(0, 2).sort(), ['10,7', '9,5'], `T${tier}: the enemies' tiles first (${fl})`); assert.ok(h.hooksOf('damaged').some((c) => c.target === a && c.source === u && c.dmg.tags.includes('wang:stone')), `T${tier}: they set off`); // inside her range a 棋子 comes back onto an enemy ("可部署至敌人所在位置") — and sets off again