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(); +});