Connect owned Sandmaster destination pile choices
This commit is contained in:
1 parent
24863480cb
commit
0040ed1921
17 files changed
+458
-89
No files matched your search
@@ -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 } : {}),
|
||||
}
|
||||
: {}),
|
||||
});
|
||||
}}
|
||||
>
|
||||
|
||||
@@ -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,
|
||||
|
||||
@@ -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>
|
||||
|
||||
Reference in new issue
Block a user