'use client'; import { useState, type CSSProperties } from 'react'; import { Dialog as DialogPrimitive } from '@base-ui/react/dialog'; import { Eye, Star, X } from 'lucide-react'; import { FACTIONS, faction, type FactionId } from '@/game/catalog'; import { forceCounterName, forceInventory, type ForcePool, type PublicForcePlayer, type PublicForceWorld } from '@/game/force-inventory'; import { Button } from './ui/button'; import { Dialog, DialogClose, DialogDescription, DialogOverlay, DialogPortal, DialogTitle, DialogTrigger } from './ui/dialog'; type CounterKind = 'force' | 'special' | 'advisor'; /** Original diagrammatic counter faces. Text stays crisp at enlarged sizes. */ export function ForceCounterFace({ factionId, kind }: { factionId: FactionId; kind: CounterKind }) { const house = faction(factionId); const name = forceCounterName(factionId, kind); return
{house.name}{name}
; } function Examples({ factionId, split = true }: { factionId: FactionId; split?: boolean }) { const special = ['emperor', 'fremen', 'ixians'].includes(factionId) && split; return
{special && } {factionId === 'beneGesserit' && }
; } function PoolList({ rows, player, split }: { rows: ForcePool[]; player: PublicForcePlayer; split: boolean }) { return
{rows.map(row =>
{row.name}
{row.total} {row.total === 1 ? 'counter' : 'counters'} {split && {row.normal} {player.faction === 'ixians' ? 'Suboid' : 'normal'} · {row.elite} {forceCounterName(player.faction, 'special')}} {row.advisors && Advisor face · included in this count}
)}
; } export function ForceInventoryDetails({ player, worlds = null }: { player: PublicForcePlayer; worlds?: readonly PublicForceWorld[] | null }) { const inventory = forceInventory(player, worlds); if (!inventory) return Force counts could not be reconciled. Refresh the table to read its latest state.; return

{inventory.total} physical force counters across reserves, the Tanks and deployed groups. This is a counter count, not battle strength.

{!inventory.split && ['emperor', 'fremen'].includes(player.faction) &&

This table shows a combined force count; starred counters are not listed separately.

} {inventory.nativeReserves.length > 0 &&

Reserves on native Homeworlds

These counters are already included in Reserves above.

} {player.faction === 'beneGesserit' &&

{inventory.advisors} deployed counters currently show their advisor face. Advisors are the same physical counters; reserves and the Tanks have no deployed stance.

} {inventory.marker &&

Concealed No-Field

{inventory.marker.name}

Counts as one force for board presence until revealed. It is a separate marker, not an extra physical force counter. Its hidden force value is not shown here. Revelation draws from the reserves available then.

}

Inspecting counters does not move them or commit a Battle Plan. Spice support, temporary powers and leaders may change battle strength without adding counters.

Force counter guide
; } export function ForceInspector({ player, worlds }: { player: PublicForcePlayer; worlds?: readonly PublicForceWorld[] | null }) { return } aria-label={`Inspect forces: ${player.name} · ${faction(player.faction).name}`}>
Public force counters}>
{/* oxlint-disable-next-line jsx-a11y/no-noninteractive-tabindex -- This separately scrolling inspection region needs keyboard access. */}
{player.name} · {faction(player.faction).name} Enlarged counter faces and the force locations visible to everyone at this table.
; } export function ForceReferenceGallery() { const [selected, setSelected] = useState('atreides'); return

Counter faces are enlarged diagrams. Each is one physical counter; the current rules and powers determine its strength. The gallery shows component types, not a saved table’s quantities.

{selected === 'beneGesserit' &&

Fighter and advisor are two faces of the same counter, not two separate forces.

}
; }