diff --git a/public/js/render/app/host.js b/public/js/render/app/host.js index 8be7e73a..fee19919 100644 --- a/public/js/render/app/host.js +++ b/public/js/render/app/host.js @@ -58,7 +58,8 @@ function makeData(src) { return rec; }; return { - chess: (id) => look('chess', id), token: (id) => look('tokens', id), item: (id) => look('items', id), + // (a 自选 operator's summons are data/backups.json tokens, 0.2.0) + chess: (id) => look('chess', id), token: (id) => look('tokens', id) || (backups()?.tokens && Object.hasOwn(backups().tokens, id) ? backups().tokens[id] : null), item: (id) => look('items', id), enemy: (id) => look('enemies', id), stage: (id) => look('stages', id), bond: (id) => look('bonds', id), standIn, diy, }; diff --git a/public/js/ui/detailPanel.js b/public/js/ui/detailPanel.js index 00a3019d..9115991e 100644 --- a/public/js/ui/detailPanel.js +++ b/public/js/ui/detailPanel.js @@ -32,7 +32,7 @@ // screen's 局内数值 section draws the same ones for the chosen skill / module, without a live entry (GitHub issue #64). import { html, Icon, TierChip, MicroLabel, Button, confirmDialog, useTicker } from './components.js'; -import { Img, RichText, UnitThumb, BondGlyph, GIcon } from './gameComponents.js'; +import { Img, RichText, UnitThumb, BondGlyph, GIcon, diyToken } from './gameComponents.js'; import { attackInterval, rangeGridBox, fmtNum, tileKey, chessLoadout, nextThreshold, bondTier, briefingBondTip, pieceBondIds, grantedBonds, morphPairings, fieldsStandIn, standInOf, standInLoadout, standInLabel, ownDiyRecord, ownDiyPick, diyRecordFor, pickGetter } from './gameLogic.js'; import { chessPortraitUrl, skillIconUrl, skillRecordIconUrl, profIconUrl, subProfIconUrl, itemIconUrl, enemyIconUrl, tokenAvatarUrl, factionIconUrl, uiUrl, moduleTypeIconUrl } from './assetUrls.js'; import { abilityRows } from './abilityLines.js'; @@ -634,7 +634,7 @@ export function resolveDetail(target, pieces, { priv = null, backups = data.get( if (!e) return null; const p = e.piece; if (p.kind === 'item') { const it = data.lookup('items', p.id); return it ? { type: 'item', item: it, piece: p } : null; } - if (p.kind === 'token') { const t = data.lookup('tokens', p.id); return t ? { type: 'token', token: t, piece: p, ownerId: tokenOwnerId(p, pieces) } : null; } + if (p.kind === 'token') { const t = data.lookup('tokens', p.id) || diyToken(p.id); return t ? { type: 'token', token: t, piece: p, ownerId: tokenOwnerId(p, pieces) } : null; } const c = data.lookup('chess', p.id); const d = ownDiy(c); if (d) return { type: 'chess', chess: d.chess, piece: p, standIn: null, diy: d.diy }; @@ -653,7 +653,7 @@ export function resolveDetail(target, pieces, { priv = null, backups = data.get( } if (target.kind === 'item') { const it = data.lookup('items', target.id); return it ? { type: 'item', item: it } : null; } if (target.kind === 'enemy') { const en = data.lookup('enemies', target.id); return en ? { type: 'enemy', enemy: en, count: target.count } : null; } - if (target.kind === 'token') { const t = data.lookup('tokens', target.id); return t ? { type: 'token', token: t } : null; } + if (target.kind === 'token') { const t = data.lookup('tokens', target.id) || diyToken(target.id); return t ? { type: 'token', token: t } : null; } if (target.kind === 'unit') { const u = target.unit || {}; const own = Number.isInteger(u.uid) ? pieces?.get(u.uid) : null; @@ -672,7 +672,7 @@ export function resolveDetail(target, pieces, { priv = null, backups = data.get( const dr = pick ? diyRecordFor(c, pick, dd) : null; if (dr) return { type: 'chess', chess: dr, piece: own?.piece || null, unitId: u.id, unitItems: Array.isArray(u.items) ? u.items : null, standIn: null, diy: pick }; if (c) return { type: 'chess', chess: c, piece: own?.piece || null, unitId: u.id, unitItems: Array.isArray(u.items) ? u.items : null, standIn: si }; - const t = data.lookup('tokens', u.defId); + const t = data.lookup('tokens', u.defId) || diyToken(u.defId); if (t) return { type: 'token', token: t, unitId: u.id, ownerId: tokenOwnerId(own?.piece, pieces) }; const en = data.lookup('enemies', u.defId); return en ? { type: 'enemy', enemy: en, unitId: u.id } : null; diff --git a/public/js/ui/gameComponents.js b/public/js/ui/gameComponents.js index 6ebc22f2..43ad27e0 100644 --- a/public/js/ui/gameComponents.js +++ b/public/js/ui/gameComponents.js @@ -26,6 +26,12 @@ export function useGameData() { return useMemo(() => makeLookups(ready), [ready]); } +/** A 自选 operator's summon record (data/backups.json `tokens`, 0.2.0), or null. */ +export function diyToken(id) { + const t = typeof id === 'string' ? data.get('backups')?.tokens : null; + return t && Object.hasOwn(t, id) ? t[id] : null; +} + /** Non-hook lookups (for event handlers). */ export function makeLookups(ready = true) { return { @@ -39,7 +45,8 @@ export function makeLookups(ready = true) { enemy: (k) => data.lookup('enemies', k), boss: (id) => data.lookup('bosses', id), stage: (id) => data.lookup('stages', id), - token: (id) => data.lookup('tokens', id), + // (a 自选 operator's summons are data/backups.json tokens — 0.2.0, the hand piece of a placeable one) + token: (id) => data.lookup('tokens', id) || diyToken(id), effect: (id) => data.lookup('effects', id), garrison: (id) => data.lookup('garrisons', id), factions: data.get('factions'), diff --git a/test/match/diy-shop.test.js b/test/match/diy-shop.test.js index e66c970e..878c04f0 100644 --- a/test/match/diy-shop.test.js +++ b/test/match/diy-shop.test.js @@ -321,3 +321,39 @@ test('a match keeps the picks its seat had at its start (the next match takes a assert.deepEqual(h2.ps('p_1').diy, { [T6A]: { charId: SIEGE, skillIndex: 1, uniEquipId: null } }); h2.m.dispose(); }); + +test('a placeable 自选 summon comes to the hand like any operator\'s, places, and fights as its owner\'s variant', REAL, () => { + const h = diyMatch().start(); + const m = h.m; + h.toPrep(1); + const p0 = h.ps('p_0'); + // 凯尔希 has no kit yet: the kit list is widened for this test (her Mon3tr is a placeable talent summon) + assert.equal(p0.setDiy({ [T6B]: { charId: 'char_003_kalts', skillIndex: 0, uniEquipId: null } }, { kitted: [...KITTED_CHARS, 'char_003_kalts'] }), true); + p0.initDiyStock(new Set()); + clear(p0); + const k = gain(p0, T6B); + const tile = legalTileFor(m, p0, T6B); + assert.deepEqual(m.handle('p_0', { t: 'g.move', uid: k.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, 'the Mon3tr stack is in the hand'); + assert.equal(stack.id, 'token_10002_kalts_mon3tr'); + assert.equal(stack.ownerUid, k.uid); + assert.equal(p0.gd.token(stack.id).name, 'Mon3tr', 'the player\'s data view finds the 自选 summon'); + const t2 = legalTileFor(m, p0, T6B, 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 }); + const input = p0.battleInput(); + const tok = input.units.find((u) => u.kind === 'token'); + assert.deepEqual([tok.tokenId, tok.ownerUid], ['token_10002_kalts_mon3tr', k.uid]); + const spec = wire(buildBattleSpec({ + battleId: 't.2', fieldId: 'n:p_0', kind: 'normal', seed: 5, 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', + })); + const bt = createBattleFromSpec(spec, m.ds, { quiet: true, recordEvents: false }); + bt.step(); + const mon = bt.allyUnits.find((u) => u.kind === 'token'); + assert.ok(mon && mon.def.name === 'Mon3tr', 'the summon is fielded'); + assert.equal(mon.ownerUnit?.def?.charId, 'char_003_kalts'); + h.invariants(); + m.dispose(); +}); diff --git a/test/match/harness.js b/test/match/harness.js index a459b318..532f0d8c 100644 --- a/test/match/harness.js +++ b/test/match/harness.js @@ -178,7 +178,8 @@ function legacyInvariants(m) { for (const [k, p] of ps.board) { const [r, c] = k.split(',').map(Number); assert.ok(r >= FIELD.r0 && r <= FIELD.r1 && c >= FIELD.c0 && c <= FIELD.c1, `piece outside the board ${k}`); - const rec = p.kind === 'token' ? m.gd.token(p.id) : m.gd.chess(p.id); + const pgd = ps.gd || m.gd; // the player's data view (0.2.0 自选: its summons are data/backups.json tokens) + const rec = p.kind === 'token' ? pgd.token(p.id) : pgd.chess(p.id); assert.ok(rec, `unknown board piece ${p.id}`); const cls = p.kind === 'chess' ? placeClass(ps, rec) : positionClass(rec); assert.ok(canPlace(dmap, cls, r, c), `illegal tile ${p.id} @ ${k}`);