feedback7 G-UI: retreat with a full bench counts the slot of the own summon stack (PR #361 by @Xinglan233)
Applied as-is. The client precheck for withdrawing a board operator onto an occupied hand slot now treats that operator's own hand summon stack as a free slot, which the server already accepted. Temp stacks, board summons, another player's tokens, and an item that only carries ownerUid stay HAND_FULL. stack.count is not extra slots. Dropping on a chess still checks the swap first. Tests: node --test test/ui/gameLogic.test.js test/ui/facing.test.js test/ui/uiFixes.test.js test/match/feedback1-placement.test.js test/match/feedback5-we2-placement.test.js — 116 pass, 0 fail. Before this commit: npm run -s lint — 0 errors (134 existing warnings); npm run -s typecheck — pass; npm run -s check:imports — pass; node --test test/docs-consistency.test.js test/docs-paths.test.js test/version.test.js test/package.test.js — 47 pass, 0 fail. No new UI strings, so the i18n check was not run. No golden update.
This commit is contained in:
1 parent
bc22f9cbe6
commit
5f0a06cc70
2 files changed
+59
-3
No files matched your search
@@ -301,7 +301,8 @@ function unitAllowed(ctx, piece, row, col, owner = null) {
|
||||
* must be legal on the source tile); from hand/temp the deploy cap applies unless a chess occupant swaps out.
|
||||
* board ← token: legal tile; from the hand the tile must be empty and its summoner deployed; board ↔ board swaps.
|
||||
* hand ← chess from the board: empty slot (withdraw), chess occupant (swap, legal on the source tile), otherwise
|
||||
* it goes to any free slot (HAND_FULL when none). hand ← token from the board: always (back onto its stack).
|
||||
* it uses a free slot or one freed by its own summon stack (HAND_FULL when none). hand ← token from the board:
|
||||
* always (back onto its stack).
|
||||
* hand ← hand/temp piece: move / swap. hand ← EQUIP item onto a chess: equip; onto a non-chess: swap.
|
||||
* board ← EQUIP item: equip the chess on that tile. board ← MAGIC (Arts): used on that tile.
|
||||
* @param {ReturnType<typeof placementContext>} ctx
|
||||
@@ -334,8 +335,11 @@ export function canPlace(ctx, uid, target) {
|
||||
if (!tileAllows(ctx, occ.piece, src.row, src.col)) return no('BAD_TILE', t('交换后的单位无法部署在原位置'));
|
||||
return { ok: true, action: 'swap' };
|
||||
}
|
||||
// the withdrawn operator goes to another free slot
|
||||
const free = [...Array(GEO.HAND_SIZE).keys()].some((i) => !ctx.handAt.has(i));
|
||||
// Withdrawal removes the operator's own summon stacks, freeing their hand slots too.
|
||||
const free = [...Array(GEO.HAND_SIZE).keys()].some((i) => {
|
||||
const p = ctx.handAt.get(i)?.piece;
|
||||
return !p || (p.kind === 'token' && p.ownerUid === piece.uid);
|
||||
});
|
||||
return free ? { ok: true, action: 'move' } : no('HAND_FULL', t('整备区已满'));
|
||||
}
|
||||
return { ok: true, action: 'swap' };
|
||||
|
||||
@@ -442,6 +442,58 @@ describe('placement mirror (canPlace)', () => {
|
||||
assert.equal(canPlace(ctx, h0.uid, { area: 'hand', idx: 10 }).ok, false);
|
||||
assert.equal(canPlace(ctx, h0.uid, { area: 'hand', idx: -1 }).ok, false);
|
||||
});
|
||||
test('summoner withdrawal: a full hand accepts a drop onto its own summon stack', () => {
|
||||
const b = { ...piece('chess_char_2_02_a'), row: 10, col: 4 };
|
||||
const stack = { uid: ++uid, kind: 'token', id: 'token_10000_silent_healrb', ownerUid: b.uid, count: 1 };
|
||||
const ctx = ctxFor(privWith({ board: [b], hand: [...Array.from({ length: 9 }, () => item(EQUIP)), stack] }));
|
||||
const to = { area: 'hand', idx: 9 };
|
||||
assert.deepEqual(canPlace(ctx, b.uid, to), { ok: true, action: 'move' });
|
||||
assert.deepEqual(dropIntent(ctx, b.uid, to), { t: 'g.move', fields: { uid: b.uid, to } });
|
||||
assert.equal(dropFailureReason(ctx, b.uid, to), null);
|
||||
});
|
||||
test('summoner withdrawal: its own stack frees space when dropped onto another item or token', () => {
|
||||
const b = { ...piece('chess_char_2_02_a'), row: 10, col: 4 };
|
||||
const stack = { uid: ++uid, kind: 'token', id: 'token_10000_silent_healrb', ownerUid: b.uid, count: 1 };
|
||||
const other = { ...stack, uid: ++uid, ownerUid: ++uid };
|
||||
const ctx = ctxFor(privWith({ board: [b], hand: [...Array.from({ length: 8 }, () => item(EQUIP)), other, stack] }));
|
||||
for (const idx of [0, 8]) {
|
||||
const to = { area: 'hand', idx };
|
||||
assert.deepEqual(canPlace(ctx, b.uid, to), { ok: true, action: 'move' });
|
||||
assert.deepEqual(dropIntent(ctx, b.uid, to), { t: 'g.move', fields: { uid: b.uid, to } });
|
||||
assert.equal(dropFailureReason(ctx, b.uid, to), null);
|
||||
}
|
||||
});
|
||||
test('summoner withdrawal: a full hand without its own hand stack still refuses the move', () => {
|
||||
const b = { ...piece('chess_char_2_02_a'), row: 10, col: 4 };
|
||||
const stack = { uid: ++uid, kind: 'token', id: 'token_10000_silent_healrb', ownerUid: b.uid, count: 1 };
|
||||
const other = { ...stack, uid: ++uid, ownerUid: ++uid };
|
||||
const hand = Array.from({ length: 10 }, () => item(EQUIP));
|
||||
const cases = [
|
||||
privWith({ board: [b], hand }),
|
||||
privWith({ board: [{ ...b, id: MELEE }], hand }),
|
||||
privWith({ board: [b], hand: [...hand.slice(0, 9), other] }),
|
||||
privWith({ board: [b], hand, temp: [stack] }),
|
||||
privWith({ board: [b, { ...stack, row: 9, col: 3 }], hand }),
|
||||
privWith({ board: [b], hand: [...hand.slice(0, 9), { ...item(EQUIP), ownerUid: b.uid }] }),
|
||||
];
|
||||
for (const priv of cases) {
|
||||
const ctx = ctxFor(priv);
|
||||
for (const idx of [0, 9]) {
|
||||
const to = { area: 'hand', idx };
|
||||
assert.equal(canPlace(ctx, b.uid, to).code, 'HAND_FULL');
|
||||
assert.equal(dropIntent(ctx, b.uid, to), null);
|
||||
assert.equal(dropFailureReason(ctx, b.uid, to), '整备区已满');
|
||||
}
|
||||
}
|
||||
});
|
||||
test('summoner withdrawal: freed space does not bypass an illegal chess swap', () => {
|
||||
const b = { ...piece('chess_char_2_02_a'), row: 10, col: 4 };
|
||||
const stack = { uid: ++uid, kind: 'token', id: 'token_10000_silent_healrb', ownerUid: b.uid, count: 1 };
|
||||
const ctx = ctxFor(privWith({ board: [b], hand: [piece(MELEE), ...Array.from({ length: 8 }, () => item(EQUIP)), stack] }));
|
||||
const to = { area: 'hand', idx: 0 };
|
||||
assert.equal(canPlace(ctx, b.uid, to).code, 'BAD_TILE');
|
||||
assert.equal(dropIntent(ctx, b.uid, to), null);
|
||||
});
|
||||
test('items: equip on chess (board or hand), arts on tiles, not on tokens', () => {
|
||||
const b = { ...piece(MELEE), row: 9, col: 3 };
|
||||
const hChess = piece(RANGED);
|
||||
|
||||
Reference in new issue
Block a user