From 5a045731f119398d95ddbb863e8222315c65f057 Mon Sep 17 00:00:00 2001 From: sganggs Date: Thu, 8 Oct 2026 19:31:40 +0800 Subject: [PATCH] =?UTF-8?q?feedback7=20R22-OSIM:=20=E6=9C=9B's=20placed=20?= =?UTF-8?q?=E6=A3=8B=E5=AD=90=20drop=20their=20=E9=93=B8=E5=AD=90=20?= =?UTF-8?q?=E8=B7=9F=E5=AD=90=20at=20the=20battle=20start=20(undeployable?= =?UTF-8?q?=20ground=20first,=20=E4=B8=8A=20>=20=E5=8F=B3=20>=20=E4=B8=8B?= =?UTF-8?q?=20>=20=E5=B7=A6);=20a=20deployment=20on=20a=20=E8=B7=9F?= =?UTF-8?q?=E5=AD=90's=20tile=20removes=20it;=20S3's=20overflow=20follows?= =?UTF-8?q?=20the=20same=20order;=20golden:=20unchanged?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit Community report relayed by the owner: 望的天赋以及3技能增强的天赋提供的额外棋子是可以出现(官方好像有出现的前后左右先后 顺序的规则)在不可部署地块的. The placement rule was already PRTS 望 铸子 备注's ("有敌人的>没有敌人的;不可部署地块>可部 署地面地块>可部署高台地块…相同优先级时按棋子上>右>下>左"), but only a 棋子's return dropped a 跟子: the battle-start deployment of the pieces the player placed — their 手动部署 in this mode (PRTS 卫戍协议/帮助: a placement in 休整 fires no deployment effect; the piece deploys when the combat starts) — dropped none, so no extra piece appeared, a lone 棋子 never connected and never set off. Now: - the stone kit's deploy hook drops one 跟子 when `ctx.initial` (a return still drops its own in scheduleReturn); - "跟子所在格进行部署(或进行类似部署的行为)时,该跟子会随之消失": any ally deploying on a 跟子's tile removes it (the battle start deploys one piece after another, so a 跟子 may first land on a piece still to come); - S3's overflow (备注 "采用相同的位置选择优先顺序"): enemy tiles first, then the tile class, then her target order / distance / tile [ASSUMED ties] — an enemy on undeployable floor now comes before one on the road. Her stock rule (the owner's decision of 2026-10-06) is unchanged; a 跟子 takes no stock. Tests: test/content/op_wang.test.js — the four tests that encoded "the battle start drops no 跟子" updated, a removal case and an S3 order test added (10; 5 fail on the parent). --- server/sim/content/kits/ops/op-wang.js | 54 +++++++++++-------- test/content/op_wang.test.js | 75 +++++++++++++++++++------- 2 files changed, 88 insertions(+), 41 deletions(-) diff --git a/server/sim/content/kits/ops/op-wang.js b/server/sim/content/kits/ops/op-wang.js index 12caf563..2f62dace 100644 --- a/server/sim/content/kits/ops/op-wang.js +++ b/server/sim/content/kits/ops/op-wang.js @@ -34,11 +34,14 @@ // 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. +// redeploy time (2 s), paying its cost and one of her stock, while 望 is on the field. 铸子 "手动部署棋子时,望在相邻位置额外 +// 部署一枚棋子": the 棋子 the player placed deploy at the battle start (PRTS 卫戍协议/帮助: a placement in 休整 fires no +// deployment effect, the piece deploys when the combat starts) — that deployment drops one 跟子 on a tile next to it (the +// PRTS order above; community report, 0.2.2 — until 0.2.1 the battle start dropped none, so a lone 棋子 never set off), at +// most attack@max_spawn_cnt on the field; each later return counts as one too [ASSUMED]. The battle start deploys one +// piece after another (Battle.start), so a 跟子 may land on the tile of a piece still to come: "跟子所在格进行部署(或进行类似 +// 部署的行为)时,该跟子会随之消失" (PRTS 铸子 备注) — any deployment on its tile removes it. 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: 15 % / 13. @@ -52,8 +55,9 @@ // 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 棋子 +// attacks, range 4-12; +cnt stock, the part over the cap placed at once as 跟子 on the tiles of her range by the 铸子 order +// (S3 备注 "采用相同的位置选择优先顺序"): enemy tiles first, then 不可部署 > 可部署地面 > 高台 [ASSUMED for the ties: her target +// order among enemy tiles, then 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. @@ -231,6 +235,9 @@ export default { 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) + // 铸子: the battle-start deployment of a placed 棋子 is its 手动部署 — one 跟子 next to it (a return drops its own in + // scheduleReturn) + if (ctx.initial && up(owner)) dropFollowers(battle, owner, unit.tileR, unit.tileC, 1); }, { owner: unit }); battle.on('death', (ctx) => { if (ctx.unit !== unit || battle.finished) return; @@ -264,28 +271,22 @@ export default { 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); + // "将超出上限的棋子优先部署在范围内敌人所在位置" and the 备注's "采用相同的位置选择优先顺序" (铸子): the free tiles of her + // (4-12) range, those holding an enemy she can target first, then 不可部署 > 可部署地面 > 高台; [ASSUMED] ties: her + // target order among enemy tiles, then nearest to her, then by tile + const 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 = []; + const cands = []; 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)) }); + const fi = foes.findIndex((e) => e.alive && bodyOnTile(e, r, c)); + cands.push({ r, c, k, foe: fi >= 0 ? 0 : 1, fi: fi >= 0 ? fi : 0, 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); + cands.sort((a, b) => a.foe - b.foe || a.cls - b.cls || a.fi - b.fi || a.d - b.d || a.k - b.k); + for (const t of cands.slice(0, n)) addFollower(battle, unit, t.r, t.c); }, onEnd({ unit, reason }) { const w = unit.mem.wang; @@ -360,6 +361,15 @@ export default { const w = unit.mem.wang; w.followers = w.followers.filter((f) => f !== n); }; + // "跟子所在格进行部署(或进行类似部署的行为)时,该跟子会随之消失" (PRTS 铸子 备注): any ally deploying on a 跟子's tile + // (priority 50: before that piece's own 铸子 drop) + battle.on('deploy', (ctx) => { + const a = ctx.unit, w = unit.mem.wang; + if (!a || a.side !== 'ally' || !w || !w.followers.length) return; + const n0 = w.followers.length; + w.followers = w.followers.filter((f) => f.r !== a.tileR || f.c !== a.tileC); + if (w.followers.length !== n0) battle.fx('disappear', { x: a.tileC, y: a.tileR, id: unit.id }); + }, { owner: unit, priority: 50 }); battle.on('tick', () => { if (!up(unit) || battle.finished) return; const nodes = piecesOf(battle, unit); diff --git a/test/content/op_wang.test.js b/test/content/op_wang.test.js index a3e21e3b..5b1af38b 100644 --- a/test/content/op_wang.test.js +++ b/test/content/op_wang.test.js @@ -101,12 +101,23 @@ test('望 in every 自选 form: her kit, stats + module attributes, 3-3, blocks assert.equal(validateDiyPicks({ [SLOT[6]]: { charId: WANG, skillIndex: 2, uniEquipId: X } }, { data, kitted: KITTED_CHARS }).ok, true); }); -test('铸子: a lone 棋子 never sets off; two side by side activate each other — an enemy (air too) on one sets it off and it is used up; it stays active once its partner is gone', () => { - { // lone - const { h, st } = field({ tier: 6, elite: true, skill: 1, stones: [[9, 6]] }); +test('铸子: the battle-start deployment of a placed 棋子 is its 手动部署 — one 跟子 next to it, so a lone 棋子 is live; with no free neighbour it never sets off; two side by side activate each other — an enemy (air too) on one sets it off and it is used up; it stays active once its partner is gone', () => { + { // lone: its 跟子 上 of it (its neighbours are road, 下 is off the field) — they connect + const { h, u, st } = field({ tier: 6, elite: true, skill: 1, stones: [[9, 6]] }); + assert.deepEqual(placed(h), [[10, 6]], 'the battle start drops one 跟子, 上 first among equal tiles'); + assert.equal(u.mem.wang.stock, 6, 'the 棋子 takes one of her 7; the 跟子 none'); + h.spawn('enemy_dummy', { pos: [9, 6] }); + h.step(); + assert.ok(!st[0].alive && stoneHits(h).length > 0, 'a lone 棋子 with its 跟子 sets off'); + done(h); + } + { // no free neighbour (operators on 上 / 右 / 左, 下 off the field): no 跟子, never sets off + const others = [[10, 6], [9, 7], [9, 5]].map(([row, col], i) => ({ uid: 10 + i, chessId: 'chess_char_1_01_a', row, col })); + const { h, st } = field({ tier: 6, elite: true, skill: 1, stones: [[9, 6]], others }); + assert.equal(placed(h).length, 0, 'nowhere to put it'); h.spawn('enemy_dummy', { pos: [9, 6] }); h.run(3); - assert.equal(stoneHits(h).length, 0, 'a lone 棋子'); + assert.equal(stoneHits(h).length, 0, 'a 棋子 with no partner'); assert.ok(st[0].alive); done(h); } @@ -115,7 +126,7 @@ test('铸子: a lone 棋子 never sets off; two side by side activate each other h.step(); const a = at(st, 9, 6), b = at(st, 9, 7); assert.ok(a.mem.wangNode.act && b.mem.wangNode.act, 'connected ⇒ active'); - assert.equal(placed(h).length, 0, 'the battle-start deployment drops no 跟子'); + assert.deepEqual(placed(h), [[10, 6], [10, 7]], 'the battle start drops one 跟子 上 of each'); const fl = h.spawn('enemy_fly', { pos: [9, 7] }); h.step(); assert.ok(!b.alive && a.alive, 'the flyer set off its 棋子, which is used up'); @@ -142,11 +153,10 @@ test('铸子: a spent 棋子 comes back on its tile 2 s later for its cost (TRP- const dp0 = h.b.players[0].dp; assert.ok(h.runUntil(() => b.alive, 1), 'back'); approx(h.b.players[0].dp, dp0 - (mod === X ? 2 : 3), `${mod ?? 'none'}: its cost`); - // the 跟子: 上 of (10,7) is (11,7) — deployable ground; no enemy anywhere ⇒ undeployable first: (10,7)'s neighbours on - // the flat stage are all deployable road but 右 (10,8) … the order 上 > 右 > 下 > 左 among equals - const f = u.mem.wang.followers; - assert.equal(f.length, 1, 'one 跟子'); - assert.deepEqual([f[0].r, f[0].c], [11, 7], '上 first among equal tiles'); + // the battle start dropped one 跟子 上 of each (equal road tiles: 上 > 右 > 下 > 左); the return of (10,7) finds its 上 + // (11,7) taken by its own and (10,6) by the other 棋子, so 右 (10,8) + assert.deepEqual(placed(h), [[11, 6], [11, 7], [10, 8]], `${mod ?? 'none'}: 上 at the battle start, then 右`); + assert.equal(u.mem.wang.followers.length, 3); done(h); } { // an enemy tile first; undeployable ground before deployable ground @@ -156,23 +166,36 @@ test('铸子: a spent 棋子 comes back on its tile 2 s later for its cost (TRP- h.step(); assert.ok(!b.alive); h.b.kill(h.b.enemies.find((x) => x.alive), null); + // the battle start: (11,9) deploys first (a column top to bottom) — its 右 (11,10) is undeployable floor, before its + // road 上; then (10,9) takes its floor 右 (10,10) + assert.deepEqual(placed(h), [[11, 10], [10, 10]], '不可部署地块 > 可部署地面地块 (before 上)'); const fl = h.spawn('enemy_fly', { pos: [9, 9] }); // 下 of (10,9) holds an enemy (a flyer: no block on the trap rule) assert.ok(h.runUntil(() => b.alive, 3), 'back (a flyer does not stop it)'); - assert.deepEqual(placed(h), [[9, 9]], 'the enemy tile first (before the undeployable 右 (10,10))'); + assert.deepEqual(placed(h)[2], [9, 9], 'the return: the enemy tile first'); h.step(); assert.ok(h.hooksOf('damaged').some((c) => c.target === fl && c.source === u && c.dmg.tags.includes('wang:stone')), 'the 跟子 sets off at once — its damage is 望\'s'); - assert.equal(u.mem.wang.followers.length, 0); + assert.equal(u.mem.wang.followers.length, 2, 'the two on the floor stay; the one on the flyer is spent'); done(h); } - { // no enemy around: undeployable ground (10,10) before the road + { // a return with no enemy around: undeployable ground (10,10) before the road const { h, u, st } = field({ tier: 6, elite: true, skill: 1, stones: [[10, 9], [11, 9]], dp: 50 }); const b = at(st, 10, 9); const e = h.spawn('enemy_dummy', { pos: [10, 9] }); h.step(); h.b.kill(e, null); + u.mem.wang.followers.length = 0; // (the battle start's 跟子 gone: the floor beside it is free again) + const n0 = placed(h).length; assert.ok(h.runUntil(() => b.alive, 3)); - assert.deepEqual(placed(h), [[10, 10]], '不可部署地块 > 可部署地面地块'); - void u; + assert.deepEqual(placed(h).slice(n0), [[10, 10]], '不可部署地块 > 可部署地面地块'); + done(h); + } + { // a 跟子 on the tile of a piece still to come at the battle start goes when that piece deploys there ("跟子所在格进行部署… + // 时,该跟子会随之消失"): (10,3)'s 上 holds an operator, so its 跟子 takes 右 (10,4) — the next 棋子's tile —, which + // deploys there and drops its own 上 (11,4) + const { h, u } = field({ tier: 6, elite: true, skill: 1, stones: [[10, 3], [10, 4]], others: [{ uid: 10, chessId: 'chess_char_1_01_a', row: 11, col: 3 }] }); + assert.deepEqual(placed(h), [[10, 4], [11, 4]]); + assert.deepEqual(u.mem.wang.followers.map((f) => [f.r, f.c]), [[11, 4]], 'the one on (10,4) is gone'); + assert.ok(h.eventsOf('fx').some((e) => e[1] === 'disappear' && e[2] === 4 && e[3] === 10), 'it vanishes'); done(h); } { // 望 away ⇒ her 棋子 leave and wait for her; her 跟子 are gone @@ -306,13 +329,14 @@ test('S2 连星 (AUTO, SP_FULL): a triggered piece hits every enemy on its conne const { h, u } = field({ tier, elite, skill: 1, stones: [[10, 6], [10, 7]] }); const onLine = h.spawn('enemy_fly', { pos: [10, 9] }); // 3 tiles right of (10,6)…(10,7)+2: on the horizontal line const beyond = h.spawn('enemy_dummy', { pos: [10, 11] }); // 4 tiles past (10,7) - const off = h.spawn('enemy_dummy', { pos: [11, 7] }); // off the line (vertical never activated) + const vertical = h.spawn('enemy_dummy', { pos: [12, 7] }); // its battle-start 跟子 (11,7) is 上 of it: the vertical line is on + const off = h.spawn('enemy_dummy', { pos: [9, 4] }); // on neither line const trig = h.spawn('enemy_runner', { pos: [10, 7] }); h.b.applyStatus(trig, 'stun', { duration: 99, force: true }); // it stays on the tile (the slow is read off its speed stat) h.step(); const hit = stoneHits(h); const victims = new Set(hit.map((c) => c.target)); - assert.ok(victims.has(trig) && victims.has(onLine), `T${tier}: the line, air included`); + assert.ok(victims.has(trig) && victims.has(onLine) && victims.has(vertical), `T${tier}: the lines, air included`); assert.ok(!victims.has(beyond) && !victims.has(off), `T${tier}: not beyond ± 3 / off the line`); for (const c of hit.filter((x) => x.target === trig)) approx(c.amount, u.s.atk * sk.bb['attack@atk_scale'] * 1.24, `T${tier}: 2 stacks`, 1e-4); const slow = trig.findBuff('wang:slow'); @@ -359,8 +383,9 @@ 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, 8, `T${tier}: +8, capped`); const fl = placed(h).map(([r, c]) => `${r},${c}`); - assert.equal(fl.length, 5, `T${tier}: (7 − 2 placed) + 8 − 8 over the cap`); - assert.deepEqual(fl.slice(0, 2).sort(), ['10,7', '9,5'], `T${tier}: the enemies' tiles first (${fl})`); + assert.deepEqual(fl.slice(0, 2), ['12,6', '12,7'], `T${tier}: the battle start's 跟子, 上 of each (${fl})`); + assert.equal(fl.length, 7, `T${tier}: then (7 − 2 placed) + 8 − 8 over the cap`); + assert.deepEqual(fl.slice(2, 4).sort(), ['10,7', '9,5'], `T${tier}: the overflow on 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 h.b.kill(a, null); h.b.kill(b, null); @@ -390,6 +415,18 @@ test('S3 天下劫 (MANUAL, ACTIVE_RANGE 4-12, 20 bullets): passive — the piec } }); +test('S3 天下劫: the overflow 跟子 take her range by the 铸子 order (S3 备注 "采用相同的位置选择优先顺序") — enemy tiles first, an enemy on undeployable floor before one on the road her target order puts first, then the floor', () => { + const { h, u } = field({ tier: 6, elite: true, skill: 2, row: 10, col: 6, stones: [[11, 4], [11, 5]], dp: 300 }); + assert.deepEqual(placed(h), [[12, 4], [12, 5]], 'the battle start'); + u.skill.gainSp(999); + h.spawn('enemy_dummy', { pos: [10, 10] }); // on tile_floor + h.spawn('enemy_dummy', { pos: [10, 8] }); // on the road, nearer the goal + assert.ok(h.runUntil(() => u.skill.active, 2)); + const fl = placed(h).slice(2).map(([r, c]) => `${r},${c}`); + assert.deepEqual(fl, ['10,10', '10,8', '9,10', '11,10', '12,10'], `(7 − 2) + 8 − 8 = 5: the enemies' tiles by class, then the undeployable tiles (${fl})`); + done(h); +}); + test('the hand: deploying her in prep brings 7 棋子 (TRP-X "可同时部署的陷阱数量提升": 8) — the match\'s data view reads the module variant\'s deploy limit', () => { const gd = new GameData(getData({ log: { warn() {}, error() {}, info() {} } }), 'mode_multi_hard'); const data = { chess: CHESS, backups: BACKUPS };