'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 { isBoardNoFieldLocation } from '@/game/richese-no-field'; 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 advisorGuidance = game.advanced && me.faction === 'beneGesserit'; const guildGuidance = !!game.nexusGuildCunning?.active; 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 = 'The Fremen two-territory advantage is unavailable. Choose a reachable destination within the remaining normal range.'; } 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 && isBoardNoFieldLocation(marker.location) ? { 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 imperial = key === 'homeworld:emperor' ? 'Kaitain' : key === 'homeworld:emperor:salusa' ? 'Salusa Secundus' : null; if (imperial) return (
  • {imperial}: {n} forces, including {active.cohort!.elites[key] ?? 0} Sardaukar. {' '}Use the Imperial movement panel for this group.
  • ); 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.

)} {guildGuidance && (

Provisional Guild Cunning combination: Ornithopter replaces one unused Hajr movement after the second shipment. Two-group mode keeps its second different group available until you finish it or end your turn. Ending early grants no movement for the abandoned group.

)} {advisorGuidance && (

Advanced Bene Gesserit advisors can use both card modes. Advisors in Arrakeen or Carthag do not grant the city’s movement range. This card’s one-group mode independently gives range three. Two-group mode uses your current normal range and different original forces. Normal entry and advisor conversion still apply.

)} {me.faction === 'choam' && game.choamMovementBonus > 0 && (

Provisional Kulon combination: the one-group card move stays capped at three territories. Kulon still extends each normal-range move in the two-group mode.

)} {reason && (

{reason}

)}
); }