'use client'; import { useId, useState } from 'react'; import type { Action, GameView } from '@/game/engine'; import { validateCohortSelection, type OrnithopterMode, } from '@/game/ornithopter'; import { location, splitLocation, gameTerritories } from '@/game/board'; import { isAdvisor } from '@/game/advisors'; import { botGroundMoveAllowed } from '@/game/bot-mobility'; import { Button } from './ui/button'; import { CardInspector, CardRules } from './card-inspector'; /** The parent supplies its existing ordinary/elite/multi-sector/marker draft. */ export function OrnithopterMovement({ game, act, busy, move, unavailableReason, }: { game: GameView; act: (action: Action) => void; busy: boolean; move: Action | null; unavailableReason?: string; }) { const id = useId(); const [selected, setSelected] = useState('range3'); const card = game.ornithopter; const me = game.players.find((p) => p.id === game.me); if (!card || !me) return null; const active = card.active; const mode = active?.mode ?? ( card.modes.find( (option) => option.mode === selected && !option.blocked, ) ?? card.modes.find((option) => !option.blocked) ?? card.modes[0] )?.mode; let reason = unavailableReason ?? card.blocked ?? (!active ? card.modes.find((option) => option.mode === mode)?.blocked : null) ?? (!move || move.type !== 'move' ? 'Select a valid movement group and destination above.' : null); if (!reason && mode === 'twoGroups' && me.fremenMovementBlocked && move) { const selectedForces = typeof move.from === 'string' ? { [move.from]: Number(move.amount) } : (move.forces as Record | undefined) ?? {}; if (Object.entries(selectedForces).some(([from, count]) => count > 0 && !botGroundMoveAllowed(game, me, from, location(String(move.territory), Number(move.sector)), 0))) reason = 'Karama removed this group’s two-territory advantage. Choose a reachable destination within the remaining normal range.'; } const physicalSource = typeof move?.from === 'string' ? move.from : move?.forces && typeof move.forces === 'object' ? Object.keys(move.forces)[0] : undefined; if ( !reason && physicalSource && isAdvisor(me, splitLocation(physicalSource).territory) ) reason = 'Advanced advisor use of the Ornithopter card awaits its ruling.'; if (!reason && active?.cohort && move) { const forces = (move.forces as Record | undefined) ?? (typeof move.from === 'string' ? { [move.from]: Number(move.amount) } : {}); const elites = (move.eliteForces as Record | undefined) ?? (typeof move.from === 'string' ? { [move.from]: Number(move.elite ?? 0) } : {}); const marker = game.richeseNoField?.owner === game.me ? game.richeseNoField.private?.deployed : null; try { validateCohortSelection( active.cohort, me.forces, me.elites?.forces ?? {}, forces, elites, typeof move.noField === 'string' && marker ? { tokenId: move.noField, event: typeof move.event === 'string' ? move.event : '', from: location(marker.location.territory, marker.location.sector), } : undefined, ); } catch (error) { reason = error instanceof Error && /cohort|original unmoved|marker/i.test(error.message) ? 'Choose forces that have not moved with this card. The first group cannot move again.' : 'The selected force counts are no longer available. Choose an available group.'; } } return (

{active ? 'Ornithopter movement in progress' : 'Use Ornithopter for this movement'}

{active ? ( <>

{active.completed} group moved · {active.remaining} remaining.{' '} {active.mode === 'twoGroups' ? 'The next group must use different original forces.' : 'This group has a maximum range of three territories.'}

{!!active.cohort && (

Original unmoved forces available for selection:

    {Object.entries(active.cohort.forces) .filter(([, n]) => n > 0) .map(([key, n]) => { const source = splitLocation(key); return (
  • {gameTerritories(game).find( (t) => t.id === source.territory, )?.name ?? source.territory} , sector {source.sector}: {n} forces{me.elites ? `, including ${active.cohort!.elites[key] ?? 0} elite` : ''}.
  • ); })} {active.cohort.noField && (
  • One original unmoved concealed No-Field; select its current source above.
  • )}
)} ) : ( <> {card.modes .filter((option) => option.blocked) .map((option) => (

{option.mode === 'range3' ? 'Three-territory mode' : 'Two-group mode'} : {option.blocked}

))}

Uses the forces, elite mix, No-Field and destination selected above. The card is played with the first move and discarded when its use ends. Storm and entry rules still apply.

)} {reason && (

{reason}

)}
); }