'use client'; import { useId, useState } from 'react'; import type { Action, GameView } from '@/game/engine'; import { location, splitLocation, territory } from '@/game/board'; import { Button } from './ui/button'; import { Input } from './ui/input'; const bounded = (value: number | undefined, minimum: number, maximum: number) => Math.max( minimum, Math.min(maximum, Number.isSafeInteger(value) ? value! : minimum), ); /** A draft belongs to this Ambassador event, never to the entrant's normal move. */ export function AmbassadorMovement({ game, act, busy, }: { game: GameView; act: (action: Action) => void; busy: boolean; }) { const id = useId(); const [sourceId, setSourceId] = useState(''); const [destinationKey, setDestinationKey] = useState(''); const [forces, setForces] = useState>({}); const [elites, setElites] = useState>({}); const [takeMarker, setTakeMarker] = useState(false); const [fighters, setFighters] = useState(false); const entry = game.ambassadorEntry; if ( entry?.stage !== 'move' || entry.beneficiary !== game.me || game.decision?.kind !== 'ecazAmbassador' || game.decision.player !== game.me || !entry.movement ) return null; const sources = entry.movement.sources; const source = sources.find((s) => s.territory === sourceId) ?? sources[0]; if (!source) return (

No legal relocation is currently available.

); const destinations = source.destinations; const destination = destinations.find( (d) => location(d.territory, d.sector) === destinationKey, ) ?? destinations.find((d) => !d.blocked) ?? destinations[0]; const target = destination ? location(destination.territory, destination.sector) : ''; const rows = source.sectors.map((s) => { const count = s.key === target ? 0 : bounded(forces[s.key], 0, s.forces); const minimum = Math.max(0, count - (s.forces - s.elites)); return { ...s, count, minimum, maximum: Math.min(count, s.elites), elite: bounded(elites[s.key], minimum, Math.min(count, s.elites)), }; }); const selectedForces = Object.fromEntries( rows.filter((s) => s.count > 0).map((s) => [s.key, s.count]), ); const selectedElites = Object.fromEntries( rows.filter((s) => s.count > 0).map((s) => [s.key, s.elite]), ); const total = rows.reduce((n, s) => n + s.count, 0); const markerAtTarget = !!source.marker && location(source.territory, source.marker.sector) === target; const includesMarker = !!source.marker && takeMarker && !markerAtTarget; const reason = destination?.blocked ?? (!destination ? 'Choose an available destination.' : !total && !includesMarker ? 'Choose physical forces or the concealed No-Field to relocate.' : null); const chooseDestination = (key: string) => { setDestinationKey(key); setForces((old) => ({ ...old, [key]: 0 })); setElites((old) => ({ ...old, [key]: 0 })); setFighters(false); }; const targetTerritories = [...new Set(destinations.map((d) => d.territory))]; return (

Relocate a group from one territory. This does not spend your ordinary shipment or movement. The Ambassador has already been triggered.

{rows.map((s) => (
Sector {splitLocation(s.key).sector} · {s.forces} physical forces available {s.elites > 0 && ( )} {s.key === target && (

Forces already in the destination sector stay in place.

)}
))} {source.marker && (

The marker can move alone or with physical forces. Its concealed value stays unchanged. {markerAtTarget ? ' It is already in the destination sector and stays in place.' : ''}

)} {destination?.advisors && (

This group arrives as advisors.

)} {destination?.canFight && ( )}

Selected: {total} physical forces {includesMarker ? ' and one concealed No-Field' : ''}.

{reason && (

{reason}

)}
); }