Apply ruled Grumman return and Cunning placement

This commit is contained in:
Quintine committed 2026-10-14 08:26:05 +11:00
1 parent 7994026a70
commit 2efa69d10f
24 files changed
+1246 -219

No files matched your search

+70 -57
View File
@@ -31,79 +31,92 @@ export function GrummanCollection({ game, act, busy }: Props) {
function CollectionChoice({ game, act, busy }: Props) {
const offer = game.grummanCollection!;
const [token, setToken] = useState(offer.tokens[0]?.id ?? '');
const [destination, setDestination] = useState(
offer.destinations[0]?.id ?? '',
const initialMode = offer.blocked && !offer.removeBlocked ? 'remove' : 'add';
const [mode, setMode] = useState<'add' | 'remove'>(initialMode);
const [token, setToken] = useState(
(initialMode === 'remove' ? offer.removable : offer.tokens)[0]?.id ?? '',
);
const [destination, setDestination] = useState(offer.destinations[0]?.id ?? '');
const [useCunning, setUseCunning] = useState(false);
const canAct = grummanCollectionCanAct(game);
const choice = grummanCollectionChoice(game, token, destination);
const disabled = busy || !canAct || !!offer.blocked;
const tokens = mode === 'remove' ? offer.removable : offer.tokens;
const destinations = useCunning && offer.cunning ? offer.cunning.destinations : offer.destinations;
const choice = grummanCollectionChoice(game, token, mode === 'add' ? destination : '', mode, useCunning);
const disabled = busy || !canAct;
return (
<section aria-label="Grumman collection" className="space-y-3">
<p>
Add one available Terror token to a stronghold to collect 4 spice. You
receive the spice only after the addition resolves.
Add one available Terror token or return one unrevealed placed token to
hidden supply to collect 4 spice. Only one completed operation pays this
Collection; your separate Mentat placement remains unchanged.
</p>
<label className="block space-y-1">
<span>Your Terror token</span>
<select
aria-label="Your Terror token"
className="w-full"
value={token}
disabled={disabled || !offer.tokens.length}
onChange={(event) => setToken(event.target.value)}
>
{!offer.tokens.length && <option value="">No available token</option>}
{offer.tokens.map((candidate) => (
<option key={candidate.id} value={candidate.id}>
{TERROR_DEFINITIONS[candidate.kind].name}
</option>
))}
<span>Grumman operation</span>
<select aria-label="Grumman operation" className="w-full" value={mode} disabled={disabled}
onChange={event => {
const next = event.target.value === 'remove' ? 'remove' : 'add';
setMode(next);
setUseCunning(false);
setToken((next === 'remove' ? offer.removable : offer.tokens)[0]?.id ?? '');
setDestination(offer.destinations[0]?.id ?? '');
}}>
<option value="add">Add from supply</option>
<option value="remove">Return placed token to hidden supply</option>
</select>
</label>
<label className="block space-y-1">
<span>Destination stronghold</span>
<select
aria-label="Destination stronghold"
className="w-full"
value={destination}
disabled={disabled || !offer.destinations.length}
onChange={(event) => setDestination(event.target.value)}
>
{!offer.destinations.length && (
<option value="">No available stronghold</option>
)}
{offer.destinations.map((target) => (
<option key={target.id} value={target.id}>
{target.name}
<span>Your Terror token</span>
<select aria-label="Your Terror token" className="w-full" value={token}
disabled={disabled || !tokens.length} onChange={event => setToken(event.target.value)}>
{!tokens.length && <option value="">No eligible token</option>}
{tokens.map(candidate => (
<option key={candidate.id} value={candidate.id}>
{TERROR_DEFINITIONS[candidate.kind].name}
{'name' in candidate && typeof candidate.name === 'string' ? ` · ${candidate.name}` : ''}
</option>
))}
</select>
</label>
{choice.blocked && (
<p className="fine" aria-live="polite">
{choice.blocked}
</p>
)}
<p className="fine">Removal unavailable: {offer.removeBlocked}</p>
{mode === 'add' && <>
{offer.cunning && <label className="decision-checkbox min-h-11">
<input type="checkbox" checked={useCunning} disabled={disabled || !!offer.cunning.blocked}
onChange={event => {
const use = event.target.checked;
setUseCunning(use);
const choices = use ? offer.cunning!.destinations : offer.destinations;
if (!choices.some(place => place.id === destination))
setDestination(choices[0]?.id ?? '');
}} />
<span>Use Moritani Nexus Cunning for this eligible addition</span>
</label>}
<label className="block space-y-1">
<span>{useCunning ? 'Destination Arrakis territory' : 'Destination stronghold'}</span>
<select aria-label={useCunning ? 'Destination Arrakis territory' : 'Destination stronghold'}
className="w-full" value={destination} disabled={disabled || !destinations.length}
onChange={event => setDestination(event.target.value)}>
{!destinations.length && <option value="">No eligible destination</option>}
{destinations.map(target => <option key={target.id} value={target.id}>{target.name}</option>)}
</select>
</label>
{offer.cunning && <p className="fine">
Cunning requires an ordinary eligible supply addition at a stronghold
already holding Terror. It spends the Nexus card, not your Mentat
placement, and never permits relocation, Homeworlds or the Hidden
Mobile Stronghold.
</p>}
</>}
{mode === 'remove' && <p className="fine">
The token stays unrevealed and becomes reusable in hidden supply.
Hidden supply identities rotate; other placed tokens remain where they are.
</p>}
{choice.blocked && <p className="fine" aria-live="polite">{choice.blocked}</p>}
<div className="flex flex-wrap gap-2">
<Button
className="game-action"
disabled={busy || !choice.action}
onClick={() => {
if (choice.action) act(choice.action);
}}
>
Add token and collect 4 spice
<Button className="game-action" disabled={busy || !choice.action}
onClick={() => { if (choice.action) act(choice.action); }}>
{mode === 'remove' ? 'Return token and collect 4 spice' : 'Add token and collect 4 spice'}
</Button>
<Button
className="game-action"
variant="outline"
disabled={busy || !canAct}
onClick={() =>
act({ type: 'decision', event: offer.event, decline: true })
}
>
<Button className="game-action" variant="outline" disabled={disabled}
onClick={() => act({ type: 'decision', event: offer.event, decline: true })}>
Pass Grumman collection
</Button>
</div>
+1 -1
View File
@@ -324,7 +324,7 @@ export function MoritaniPlacement({
onChange={(event) => setUseNexus(event.target.checked)} />
Use Moritani Nexus Cunning
</label>
<p className="fine">Cunning places one available supply token in an Arrakis territory, including one with Terror already present. It spends the Nexus card and this Mentat placement opportunity. Relocation, Grumman use, Homeworlds and the Hidden Mobile Stronghold are unavailable.</p>
<p className="fine">Cunning places one available supply token in an Arrakis territory, including one with Terror already present. It spends the Nexus card and this Mentat placement opportunity. It never enhances relocation or permits Homeworlds or the Hidden Mobile Stronghold. High-Grumman Cunning additions use their separate Collection controls and require an ordinary eligible addition.</p>
{game.nexusMoritani.blocked && <p className="fine">{game.nexusMoritani.blocked}</p>}
</>}
<div className="flex min-w-0 flex-col gap-3">