Suspend Voice response controls during Nexus Traitor return
This commit is contained in:
1 parent
8956cf0be7
commit
0d01d18ddd
3 files changed
+86
-37
No files matched your search
+43
-33
@@ -2205,37 +2205,45 @@ export function GameTable({
|
||||
g.response.kind !== 'faceDancerReplacement' && (
|
||||
<p className="notice">{g.response.intent}</p>
|
||||
)}
|
||||
{me.hand
|
||||
?.filter((c) => g.responseControls?.cancelCards.includes(c.id))
|
||||
.map((c) => (
|
||||
<Button
|
||||
key={c.id}
|
||||
className="game-action"
|
||||
disabled={busy}
|
||||
onClick={() =>
|
||||
act({ type: 'card', card: c.id, mode: 'cancel' })
|
||||
}
|
||||
>
|
||||
{c.kind === 'worthless'
|
||||
? `Use ${c.name} as Karama`
|
||||
: 'Cancel with Karama'}
|
||||
</Button>
|
||||
))}
|
||||
{(g.responseControls?.cancelCards.length ||
|
||||
(g.nexusCards && (
|
||||
(g.response.kind === 'eliteStrength' &&
|
||||
g.players.some((player) => player.id === g.response?.owner && player.faction === 'emperor')) ||
|
||||
(g.response.kind === 'voice' &&
|
||||
g.players.some((player) => player.id === g.response?.owner && player.faction === 'beneGesserit'))
|
||||
))) &&
|
||||
!g.responseControls?.hasPassed ? (
|
||||
actionButton('Allow this power', { type: 'passResponse' })
|
||||
) : (
|
||||
<p className="muted">
|
||||
{g.responseControls?.hasPassed
|
||||
? 'You have allowed this power. Waiting for the response to finish.'
|
||||
: 'Waiting for the response to finish.'}
|
||||
{g.nexusTraitors?.pending ? (
|
||||
<p className="notice">
|
||||
Finish the private Nexus Traitor card return before responding to this power.
|
||||
</p>
|
||||
) : (
|
||||
<>
|
||||
{me.hand
|
||||
?.filter((c) => g.responseControls?.cancelCards.includes(c.id))
|
||||
.map((c) => (
|
||||
<Button
|
||||
key={c.id}
|
||||
className="game-action"
|
||||
disabled={busy}
|
||||
onClick={() =>
|
||||
act({ type: 'card', card: c.id, mode: 'cancel' })
|
||||
}
|
||||
>
|
||||
{c.kind === 'worthless'
|
||||
? `Use ${c.name} as Karama`
|
||||
: 'Cancel with Karama'}
|
||||
</Button>
|
||||
))}
|
||||
{(g.responseControls?.cancelCards.length ||
|
||||
(g.nexusCards && (
|
||||
(g.response.kind === 'eliteStrength' &&
|
||||
g.players.some((player) => player.id === g.response?.owner && player.faction === 'emperor')) ||
|
||||
(g.response.kind === 'voice' &&
|
||||
g.players.some((player) => player.id === g.response?.owner && player.faction === 'beneGesserit'))
|
||||
))) &&
|
||||
!g.responseControls?.hasPassed ? (
|
||||
actionButton('Allow this power', { type: 'passResponse' })
|
||||
) : (
|
||||
<p className="muted">
|
||||
{g.responseControls?.hasPassed
|
||||
? 'You have allowed this power. Waiting for the response to finish.'
|
||||
: 'Waiting for the response to finish.'}
|
||||
</p>
|
||||
)}
|
||||
</>
|
||||
)}
|
||||
</>
|
||||
) : g.decision ? (
|
||||
@@ -5027,9 +5035,11 @@ export function GameTable({
|
||||
: richeseCardActionBlock(c)) ??
|
||||
(canUseAsKaramaRole(g, me, c)
|
||||
? g.response
|
||||
? !g.responseControls?.cancelCards.includes(c.id)
|
||||
? 'This card cannot cancel the current power.'
|
||||
: null
|
||||
? g.nexusTraitors?.pending
|
||||
? 'Finish the private Nexus Traitor card return before responding to this power.'
|
||||
: !g.responseControls?.cancelCards.includes(c.id)
|
||||
? 'This card cannot cancel the current power.'
|
||||
: null
|
||||
: g.decision
|
||||
? 'Finish the pending decision before taking an auction card.'
|
||||
: g.phase !== 3 || !g.auction || g.richeseAuction
|
||||
|
||||
@@ -50,8 +50,10 @@ removes only the Voice; any already selected Prescience preparation and
|
||||
unchanged plan promises continue. The spent-card battle receipt, private
|
||||
holder offer and exact response survive JSON and room recovery; other seats
|
||||
see the public response but not who holds the card. Bot profiles play or pass
|
||||
their own reaction. An active Truthtrance or unfinished private Harkonnen
|
||||
Nexus Traitor return suspends the offer until that interaction finishes.
|
||||
their own reaction. An active Truthtrance suspends the private offer. An
|
||||
unfinished Harkonnen Nexus Traitor return suspends both the offer and Voice
|
||||
response actions, including the hand-card Karama cancellation, until the
|
||||
private card return finishes.
|
||||
|
||||
This does not grant absent-BG Secret Ally Voice or change Cunning's advisor
|
||||
conversion. Basic and Advanced retain their native Voice legality; the
|
||||
|
||||
@@ -1,6 +1,7 @@
|
||||
import test from 'node:test';
|
||||
import assert from 'node:assert/strict';
|
||||
import { registerHooks } from 'node:module';
|
||||
import { readFileSync } from 'node:fs';
|
||||
import { createElement } from 'react';
|
||||
import { renderToStaticMarkup } from 'react-dom/server';
|
||||
import { applyAction, viewGame, type GameView } from '../game/engine';
|
||||
@@ -8,15 +9,27 @@ import { nexusBgBetrayalFixture } from './fixture-nexus-bg-betrayal';
|
||||
|
||||
const aliases = registerHooks({
|
||||
resolve(specifier, context, next) {
|
||||
return next(specifier === 'next/image' ? 'vinext/shims/image' : specifier, context);
|
||||
return next(specifier === 'next/image' ? 'vinext/shims/image' : specifier === 'next/link' ? 'vinext/shims/link' : specifier, context);
|
||||
},
|
||||
load(url, context, next) {
|
||||
if (!url.endsWith('.module.css')) return next(url, context);
|
||||
const css = readFileSync(new URL(url), 'utf8');
|
||||
const classes = Object.fromEntries([...css.matchAll(/\.([a-zA-Z_][\w-]*)/g)].map((match) => [match[1], match[1]]));
|
||||
return { format: 'module', source: `export default ${JSON.stringify(classes)}`, shortCircuit: true };
|
||||
},
|
||||
});
|
||||
// NexusCards imports next/image; the test-only alias must exist before module loading.
|
||||
// Install Next and CSS-module shims before loading the UI components.
|
||||
const { NexusCards } = await import('../components/nexus-cards');
|
||||
const { GameTable } = await import('../components/game-table');
|
||||
|
||||
function markup(view: GameView, busy = false) {
|
||||
return renderToStaticMarkup(createElement(NexusCards, { game: view, act() {}, busy }));
|
||||
}
|
||||
function table(view: GameView) {
|
||||
return renderToStaticMarkup(createElement(GameTable, {
|
||||
game: view, send: async () => {}, onExit() {}, busy: false,
|
||||
}));
|
||||
}
|
||||
|
||||
void test('private BG holder alone sees an enabled Voice prevention control', () => {
|
||||
const initial = nexusBgBetrayalFixture();
|
||||
@@ -29,4 +42,28 @@ void test('private BG holder alone sees an enabled Voice prevention control', ()
|
||||
assert.match(markup(viewGame(g, 'r'), true), /disabled=""[^>]*>Prevent Voice with Betrayal/);
|
||||
});
|
||||
|
||||
void test('a private Traitor return suspends Voice pass and Karama controls until completion', () => {
|
||||
let g = applyAction(nexusBgBetrayalFixture('r', false, 'harkonnen'),
|
||||
'p', { type: 'voice', kind: 'poison', must: false });
|
||||
const karamaIndex = g.deck.findIndex(card => card.effect === 'karama');
|
||||
assert.ok(karamaIndex >= 0);
|
||||
const karama = g.deck.splice(karamaIndex, 1)[0];
|
||||
g.players.find(player => player.id === 'r')!.hand.push(karama);
|
||||
assert.match(table(viewGame(g, 'r')), /Allow this power/);
|
||||
assert.match(table(viewGame(g, 'r')), /Cancel with Karama/);
|
||||
const offer = viewGame(g, 'q').nexusTraitors!.offer!;
|
||||
g = applyAction(g, 'q', { type: 'nexusTraitorDraw', event: offer.event, mode: offer.mode });
|
||||
assert.throws(() => applyAction(g, 'r', { type: 'passResponse' }));
|
||||
assert.doesNotMatch(table(viewGame(g, 'r')), /Allow this power/);
|
||||
assert.doesNotMatch(table(viewGame(g, 'r')), /Cancel with Karama/);
|
||||
assert.match(table(viewGame(g, 'r')), /disabled=""[^>]*>Cancel this power<\/button>/);
|
||||
const pending = viewGame(g, 'q').nexusTraitors!.pending!;
|
||||
g = applyAction(g, 'q', {
|
||||
type: 'nexusTraitorReturn', event: pending.event,
|
||||
cards: pending.choices.filter(card => card.drawn).map(card => card.id),
|
||||
});
|
||||
assert.match(table(viewGame(g, 'r')), /Allow this power/);
|
||||
assert.match(table(viewGame(g, 'r')), /Cancel with Karama/);
|
||||
});
|
||||
|
||||
aliases.deregister();
|
||||
Reference in new issue
Block a user