'use client'; import { useId, useState } from 'react'; import type { Action, GameView } from '@/game/engine'; import { gameTerritories, location, splitLocation, } from '@/game/board'; import { sandmasterActionOrder, sandmasterDefaultChoice, sandmasterLeader, sandmasterModeSupported, sandmasterPathBlocked, sandmasterAdjacent, sandmasterRouteDistance, } from '@/game/sandmaster-movement'; import { planetologistFlightSelection as movementFlightSelection, type PlanetologistFlight as MovementFlight, } from '@/game/planetologist-flight'; import { SandmasterMovementAction, sandmasterMoveDraft, sandmasterMovementBlocked, sandmasterMovementSelection, } from './sandmaster-movement-action'; import { Button } from './ui/button'; export { sandmasterMoveDraft } from './sandmaster-movement-action'; type RouteDraft = { key: string; routes: Record; declined: string[]; }; function placeName(game: GameView, key: string) { const at = splitLocation(key); const name = gameTerritories(game).find((candidate) => candidate.id === at.territory) ?.name ?? at.territory; return `${name}, sector ${at.sector}`; } type Props = { game: GameView; move: Action | null; act: (action: Action) => void; busy: boolean; unavailableReason?: string | null; }; export function SandmasterMovement(props: Props) { const me = props.game.players.find(player => player.id === props.game.me); return ; } function SandmasterMovementDraft({ game, move, act, busy, unavailableReason, }: Props) { const id = useId(); const [flight, setFlight] = useState(undefined); const me = game.players.find((player) => player.id === game.me); const leader = sandmasterLeader(game, game.me); const selected = sandmasterMovementSelection(game, move, flight); const defaults = selected.move ? sandmasterDefaultChoice(game, game.me, selected.move) : null; const order = selected.move ? sandmasterActionOrder(game, game.me, selected.move) : null; const draftKey = JSON.stringify([ game.turn, me?.moved ?? 0, selected.move, defaults?.routes, ]); const [saved, setSaved] = useState(null); if ( !me || !leader || !sandmasterModeSupported(game) || !move ) return null; const routes = saved?.key === draftKey ? saved.routes : defaults?.routes ?? {}; const declined = saved?.key === draftKey ? saved.declined : []; const draft = sandmasterMoveDraft(game, move, Object.keys(routes).length ? routes : undefined, declined, flight); const updateRoutes = (next: Record) => setSaved({ key: draftKey, routes: next, declined }); const blocked = sandmasterMovementBlocked(game, draft, unavailableReason, selected.flight); const leaderName = game.allLeaders.find((candidate) => candidate.id === leader)?.name ?? leader; const destination = order ? location(order.to, order.sector) : null; return (

Sandmaster movement

{leaderName} may collect 1 spice in each distinct territory these forces enter or pass through. Choose one existing pile per territory, or decline that collection, and select the exact sector route for every source.

Provisional multi-pile policy: you choose which sector supplies the one spice; this is not a publisher allocation ruling.

{(game.ornithopter || game.discoveryOrnithopter) && ( <>

Provisional flight composition: the fixed-range card and Discovery flight allow routes up to three territories. Each two-group card movement keeps normal range and uses different original forces. Choose each actual route and optional spice collection before moving. Only one flight source may be used; Planetologist remains separate.

{game.ornithopter?.active && !selected.transport.blocked &&

{game.ornithopter.active.completed} card movements completed ·{' '} {game.ornithopter.active.remaining} remaining. Use the original unmoved cohort.

} )} {order?.noField &&

Provisional marker composition: your concealed No-Field moves as one force, including zero, and may collect along its route without revealing its value. A marker and physical forces in the same source sector share one route and collect only once per entered territory.

} {order && Object.keys(defaults?.routes ?? routes).map((from) => { const route = routes[from] ?? [from]; const last = route.at(-1)!; const extensions = last === destination ? [] : sandmasterAdjacent(game, last).filter( (next) => !route.includes(next) && !sandmasterPathBlocked(game, game.me, from, next) && sandmasterRouteDistance(game, [...route, next]) <= order.range, ); return (
{placeName(game, from)} route

{route.map((key) => placeName(game, key)).join(' → ')} ·{' '} {sandmasterRouteDistance(game, route)} / {order.range} territories

{last !== destination && ( )}
); })} {draft.collectible.length ? (
Optional spice collection {draft.collectible.map((key) => ( ))}
) : (

This route enters no territory with positive spice. You may still declare the route without collecting.

)} {blocked && ( {blocked} )}

Use the normal Move forces button to decline Sandmaster. A canceled movement collects no spice.

); }