From c4e04609ea157e35e18139685e101f612a2863e8 Mon Sep 17 00:00:00 2001 From: sganggs Date: Tue, 6 Oct 2026 00:27:57 +0800 Subject: [PATCH] =?UTF-8?q?feedback5=20WK:=20=E8=87=AA=E9=80=89=20summon?= =?UTF-8?q?=20pieces=20=E2=80=94=20a=20test=20that=20a=20DIY=20=E9=B8=BF?= =?UTF-8?q?=E9=9B=AA=20(her=20operator=20kit)=20gets=20her=20=E6=89=93?= =?UTF-8?q?=E5=AD=97=E6=9C=BA=20stack=20in=20the=20hand=20when=20deployed?= =?UTF-8?q?=20in=20prep=20(the=20variant's=20deployLimit),=20places=20it,?= =?UTF-8?q?=20fields=20it=20in=20battle=20owned=20by=20her=20with=20the=20?= =?UTF-8?q?kit=20her=20install=20gives=20it=20(server=20and=20browser=20da?= =?UTF-8?q?ta=20sources),=20and=20loses=20it=20when=20she=20is=20sold;=20k?= =?UTF-8?q?its=20README=20documents=20how=20a=20kit=20finds=20its=20pieces?= =?UTF-8?q?=20(PlayerBattleInput=20token=20units=20with=20ownerUid,=20as?= =?UTF-8?q?=20op-bgsnow=20/=20op-cgbird=20do)?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit No new hook needed: the match already hands placed summon pieces to the battle with their owner. Golden unchanged. --- server/sim/content/kits/README.md | 12 ++++++-- test/match/diy-shop.test.js | 48 +++++++++++++++++++++++++++++++ 2 files changed, 58 insertions(+), 2 deletions(-) diff --git a/server/sim/content/kits/README.md b/server/sim/content/kits/README.md index b93244ae..8a69d869 100644 --- a/server/sim/content/kits/README.md +++ b/server/sim/content/kits/README.md @@ -182,8 +182,16 @@ pick chooses any of the three, so write every skill under `skills: { [skillId]: `talents`, `trait` and `install` apply under every skill. Read every number from `chess` / `bb` — the same file serves both tiers, both forms and both module stages. A 自选 piece has no 特质 and its bonds come from its factions: neither is the kit's business. Summons: `battle.tokenDef(tokenId, unit)` / `battle.spawnToken(unit, …)` resolve the variant of the -pick; a placeable summon comes to its player's hand like any operator's (server/match/player/diy.js `placeableTokens` -reads the owner form's variant). Potential is 0 [ASSUMED: no account]. A new kit makes the operator a legal pick at once: +pick. A **placeable** summon (data/backups.json `tokens[id].placeable`, made by the pick's skill or a talent — the +variant's `sources`) is a hand piece like any operator's: when the 自选 piece is deployed in prep its player gets one +stack of the variant's `deployLimit` (server/match/player/diy.js `placeableTokens`, PlayerState `grantTokensFor`), places +it on a legal tile (the token's position / `ownerRange`), loses it with its owner (sold, merged, moved back), and the +match hands every placed piece to the battle as a PlayerBattleInput token unit `{ kind: 'token', tokenId, ownerUid }` — +so a kit finds its pieces before the battle starts as `battle.allyUnits` with `kind === 'token'`, its `defId` and +`ownerUnit === unit`, still undeployed (e.g. `op-bgsnow.js` gives its 打字机 pieces their kit in a talent's `install` +with `battle._setupUnit(t, kit)`; `op-cgbird.js` counts its 幻影). A kit test fields a piece the same way: +`units: [op, { uid, kind: 'token', tokenId, ownerUid, row, col }]` (`test/content/op_bgsnow.test.js`); the match side is +covered by `test/match/diy-shop.test.js` (a DIY 鸿雪's 打字机 from prep to battle). Potential is 0 [ASSUMED: no account]. A new kit makes the operator a legal pick at once: the server's `welcome.diyKitted` lists `KITTED_CHARS`, so the 自选编队 picker offers it and `room.diy` keeps it. **The fidelity rule and the checklist** above apply item by item: every skill at rank 4 and 7, every talent, every diff --git a/test/match/diy-shop.test.js b/test/match/diy-shop.test.js index 878c04f0..7efa46df 100644 --- a/test/match/diy-shop.test.js +++ b/test/match/diy-shop.test.js @@ -357,3 +357,51 @@ test('a placeable 自选 summon comes to the hand like any operator\'s, places, h.invariants(); m.dispose(); }); + +test('a DIY 鸿雪 (her operator kit): her 打字机 piece comes to the hand in prep, places, fields with its kit; it leaves with her', REAL, () => { + const SNOW = 'char_4055_bgsnow'; + const TYPEWRITER = 'token_10026_bgsnow_subbow'; + if (!KITTED_CHARS.includes(SNOW)) return; // (her kit is on feedback5 since O6) + const h = diyMatch().start(); + const m = h.m; + h.toPrep(1); + const p0 = h.ps('p_0'); + assert.equal(p0.setDiy({ [T5B]: { charId: SNOW, skillIndex: 0, uniEquipId: null } }), true, 'a kitted pick: no widened list'); + p0.initDiyStock(new Set()); + clear(p0); + const snow = gain(p0, T5B); + assert.ok(!p0.hand.some((p) => p && p.kind === 'token'), 'in the hand she brings no summon yet'); + const tile = legalTileFor(m, p0, T5B); + assert.deepEqual(m.handle('p_0', { t: 'g.move', uid: snow.uid, to: { area: 'board', row: tile[0], col: tile[1] } }), { ok: true }); + const stack = p0.hand.find((p) => p && p.kind === 'token'); + assert.ok(stack, 'deployed, her 打字机 comes to the hand (PRTS 卫戍协议/帮助 §战斗部署)'); + assert.deepEqual([stack.id, stack.ownerUid, stack.count], [TYPEWRITER, snow.uid, DATA.backups.tokens[TYPEWRITER].variants[`${SNOW}@2/1/4/0`].stats.deployLimit]); + const t2 = legalTileFor(m, p0, T5B, new Set([`${tile[0]},${tile[1]}`])); + assert.deepEqual(m.handle('p_0', { t: 'g.move', uid: stack.uid, to: { area: 'board', row: t2[0], col: t2[1] } }), { ok: true }); + h.invariants(); + const input = p0.battleInput(); + const tok = input.units.find((u) => u.kind === 'token'); + assert.deepEqual([tok.tokenId, tok.ownerUid], [TYPEWRITER, snow.uid], 'the battle input carries the piece with its owner'); + const spec = wire(buildBattleSpec({ + battleId: 't.3', fieldId: 'n:p_0', kind: 'normal', seed: 9, modeId: m.modeId, round: 1, stageId: m.stageId, + rect: { ...GEO.NORMAL_RECT }, timeLimit: 20, players: [input], spawns: [], routes: m.wave.routes, + flags: { layerGainsEnabled: true, ...m.gd.dp }, enemyOverrides: {}, content: 'full', + })); + for (const ds of [m.ds, browserSource()]) { + const bt = createBattleFromSpec(wire(spec), ds, { quiet: true, recordEvents: false }); + bt.step(); + const u = bt.allyUnits.find((x) => x.uid === snow.uid); + const t = bt.allyUnits.find((x) => x.kind === 'token' && x.defId === TYPEWRITER); + assert.ok(u && !u.kit.generic, 'her operator kit'); + assert.ok(t, 'the 打字机 is fielded'); + assert.equal(t.ownerUnit, u, 'owned by her (the kit finds its piece by owner)'); + assert.equal(t.def.stats.maxHp, DATA.backups.tokens[TYPEWRITER].variants[`${SNOW}@2/1/4/0`].stats.maxHp, 'the token record of her form'); + assert.ok(t.kit && !t.kit.generic, 'the typewriter runs the kit her install gave it'); + assert.equal(bt.errors.length, 0); + } + // sold, the piece leaves with her + assert.deepEqual(m.handle('p_0', { t: 'g.sell', uid: snow.uid }), { ok: true }); + assert.ok(![...p0.board.values(), ...p0.hand, ...p0.temp].some((p) => p && p.kind === 'token'), 'her summon leaves with her'); + h.invariants(); + m.dispose(); +});