Connect owned Sandmaster destination pile choices

This commit is contained in:
Quintine committed 2026-10-09 04:51:12 +11:00
1 parent 24863480cb
commit 0040ed1921
17 files changed
+458 -89

No files matched your search

+44 -1
View File
@@ -31,6 +31,11 @@ export function AmbassadorMovement({
const [takeMarker, setTakeMarker] = useState(false);
const [fighters, setFighters] = useState(false);
const [collectSandmaster, setCollectSandmaster] = useState(true);
const [sandmasterSelection, setSandmasterSelection] = useState<{
source: string;
target: string;
key: string;
} | null>(null);
const entry = game.ambassadorEntry;
if (
entry?.stage !== 'move' ||
@@ -83,6 +88,12 @@ export function AmbassadorMovement({
const sandmaster = !includesMarker && destination
? sandmasterDestinationCollection(game, game.me, source.territory, destination.territory, destination.sector)
: null;
const sandmasterPile =
sandmasterSelection?.source === source.territory &&
sandmasterSelection.target === target &&
sandmaster?.piles.some((pile) => pile.key === sandmasterSelection.key)
? sandmasterSelection.key
: sandmaster?.key;
const reason =
destination?.blocked ??
(!destination
@@ -98,6 +109,7 @@ export function AmbassadorMovement({
setForces((old) => ({ ...old, [key]: 0 }));
setElites((old) => ({ ...old, [key]: 0 }));
setFighters(false);
setSandmasterSelection(null);
};
const targetTerritories = [...new Set(destinations.map((d) => d.territory))];
return (
@@ -123,6 +135,7 @@ export function AmbassadorMovement({
setElites({});
setTakeMarker(false);
setFighters(false);
setSandmasterSelection(null);
}}
>
{sources.map((s) => (
@@ -286,6 +299,31 @@ export function AmbassadorMovement({
{sandmaster.blocked ?? 'Collect 1 spice here with Sandmaster before arrival reactions'}
</label>
)}
{sandmaster && !sandmaster.blocked && sandmaster.piles.length > 1 && (
<label className="flex flex-col gap-2" htmlFor={`${id}-sandmaster-pile`}>
Sandmaster debit sector — provisional owner allocation
<select
id={`${id}-sandmaster-pile`}
className="min-h-11"
disabled={busy || !collectSandmaster}
value={sandmasterPile ?? ''}
onChange={(event) => setSandmasterSelection({
source: source.territory,
target,
key: event.target.value,
})}
>
{sandmaster.piles.map((pile) => (
<option key={pile.key} value={pile.key}>
Sector {splitLocation(pile.key).sector}: {pile.before} → {pile.before - 1}
</option>
))}
</select>
</label>
)}
{sandmaster && !sandmaster.blocked && sandmaster.piles.length > 1 && (
<p className="fine">You choose the one-spice debit sector. This provisional allocation policy is not a publisher ruling.</p>
)}
<p className="m-0 text-sm" aria-live="polite">
Selected: {total} physical forces
{includesMarker ? ' and one concealed No-Field' : ''}.
@@ -317,7 +355,12 @@ export function AmbassadorMovement({
}
: {}),
...(fighters && destination.canFight ? { fighters: true } : {}),
...(sandmaster?.blocked === null ? { sandmasterCollect: collectSandmaster } : {}),
...(sandmaster?.blocked === null
? {
sandmasterCollect: collectSandmaster,
...(collectSandmaster && sandmasterPile ? { sandmasterPile } : {}),
}
: {}),
});
}}
>
+12 -3
View File
@@ -493,7 +493,13 @@ export function GameTable({
const [moveForces, setMoveForces] = useState<Record<string, number>>({});
const [rideForces, setRideForces] = useState<Record<string, number>>({});
const [collectSandmasterRide, setCollectSandmasterRide] = useState(true);
const sandmasterRide = sandmasterWormCollection(g, me.id, selected, sector);
const [sandmasterRidePile, setSandmasterRidePile] = useState<{ target: string; key: string } | null>(null);
const sandmasterRideTarget = JSON.stringify([g.turn, g.decision?.kind === 'wormRide' ? g.decision : null, selected, sector]);
const baseSandmasterRide = sandmasterWormCollection(g, me.id, selected, sector);
const selectedRidePile = sandmasterRidePile?.target === sandmasterRideTarget
? baseSandmasterRide?.piles.find(pile => pile.key === sandmasterRidePile.key) : null;
const sandmasterRide = selectedRidePile && baseSandmasterRide
? { ...baseSandmasterRide, key: selectedRidePile.key, before: selectedRidePile.before } : baseSandmasterRide;
const [exchangeCards, setExchangeCards] = useState<Record<string, boolean>>(
{},
);
@@ -3646,10 +3652,13 @@ export function GameTable({
/>
))}
<SandmasterWormChoice quote={sandmasterRide} collect={collectSandmasterRide}
onChange={setCollectSandmasterRide} />
onChange={setCollectSandmasterRide}
onPileChange={key => setSandmasterRidePile({ target: sandmasterRideTarget, key })} />
{actionButton('Ride Shai-Hulud', {
type: 'decision',
...(sandmasterRide?.blocked === null ? { sandmasterCollect: collectSandmasterRide } : {}),
...(sandmasterRide?.blocked === null
? { sandmasterCollect: collectSandmasterRide,
...(collectSandmasterRide ? { sandmasterPile: sandmasterRide.key } : {}) } : {}),
eliteForces,
accept: true,
territory: selected,
+17 -3
View File
@@ -1,17 +1,31 @@
import { splitLocation } from '@/game/board';
import type { SandmasterDestinationCollection } from '@/game/sandmaster-movement';
export function SandmasterWormChoice({ quote, collect, onChange }: {
export function SandmasterWormChoice({ quote, collect, onChange, onPileChange }: {
quote: SandmasterDestinationCollection | null;
collect: boolean;
onChange: (collect: boolean) => void;
onPileChange: (key: string) => void;
}) {
if (!quote) return null;
return <div className="notice" aria-label="Sandmaster worm collection">
return <div className="notice space-y-3" aria-label="Sandmaster worm collection">
{quote.blocked ? <p>{quote.blocked} You may still ride without collecting.</p> : <>
<label><input type="checkbox" checked={collect}
<label className="flex min-h-11 items-start gap-2"><input className="mt-1 shrink-0" type="checkbox" checked={collect}
onChange={event => onChange(event.target.checked)} />
Collect 1 spice with Sandmaster at the destination
</label>
{quote.piles.length > 1 && <>
<label className="flex flex-col gap-2">Destination spice pile
<select className="min-h-11 w-full" disabled={!collect} value={quote.key ?? ''} onChange={event => onPileChange(event.target.value)}>
{quote.piles.map(pile => <option key={pile.key} value={pile.key}>
Sector {splitLocation(pile.key).sector}: {pile.before} → {pile.before - 1} spice
</option>)}
</select>
</label>
<p className="fine">Provisional multi-pile policy: you choose which sector supplies
the one spice; this is not a publisher allocation ruling.</p>
</>}
<p className="fine">Sector {splitLocation(quote.key!).sector}: {quote.before} → {quote.before - 1} spice if collected.</p>
<p className="fine">Collect once for this ride, even when several forces or source
sectors participate. The source and intervening territories earn nothing.
This does not use your normal movement.</p>