'use client'; import { useState } from 'react'; import Image from 'next/image'; import { Dialog as DialogPrimitive } from '@base-ui/react/dialog'; import { faction, type FactionId } from '@/game/catalog'; import type { Action, GameView } from '@/game/engine'; import { HOMEWORLD_CARDS } from '@/game/homeworld-cards'; import { nexusCardMode, type NexusCardMode } from '@/game/nexus-cards'; import type { NexusCardChoice } from '@/game/nexus-card-phase'; import { nexusCardAction } from '@/game/nexus-card-options'; import { NEXUS_CARD_REFERENCE, NEXUS_PANEL_NAMES, nexusCardReference } from '@/game/nexus-card-reference'; import { Button } from './ui/button'; import { Dialog, DialogClose, DialogDescription, DialogOverlay, DialogPortal, DialogTitle, DialogTrigger } from './ui/dialog'; const panels: NexusCardMode[] = ['betrayal', 'cunning', 'secretAlly']; /** Only an authorized projected card may be passed as a private held card. */ export function NexusCardFace({ card, mode, enlarged = false }: { card: FactionId; mode?: NexusCardMode; enlarged?: boolean; }) { const identity = faction(card); const reference = nexusCardReference(card); const world = HOMEWORLD_CARDS.find((candidate) => candidate.faction === card)!; return (

Nexus card

{identity.name}

{panels.map((panel) => (

{NEXUS_PANEL_NAMES[panel]} {mode === panel && Your applicable mode}

{panel === 'cunning' ? `${identity.name} player only` : panel === 'betrayal' ? `Another player controls ${identity.name}` : `${identity.name} is not in this game`}

{reference[panel]}

))}
{!enlarged && (
} aria-label={`Inspect ${identity.name} Nexus card`}> Inspect card
{identity.name} Nexus card }>Close
All three Nexus effects and their faction requirements. Card effects are not playable yet.
)}
); } export function NexusCardGallery() { return
{NEXUS_CARD_REFERENCE.map((card) => )}
; } /** The server supplies current choices. The client binds the exact offered * turn and held identity; it neither infers an opponent's card nor draws one. */ export function nexusCardChoiceAction(game: GameView, choice: NexusCardChoice, ownRedraws: 0 | 1 | 2 = 0): Action | null { const offer = game.nexusCards; if (!offer || game.status !== 'playing' || game.phase !== 1 || offer.turn !== game.turn || !offer.waiting.includes(game.me) || !offer.choices.includes(choice) || ![0, 1, 2].includes(ownRedraws)) return null; const owner = game.players.find((player) => player.id === game.me); if (!owner || (choice === 'draw' && offer.card !== null) || (choice === 'replace' && offer.card === null)) return null; return nexusCardAction(game, choice, ownRedraws); } export function NexusCards({ game, act, busy }: { game: GameView; act: (action: Action) => void; busy: boolean }) { const [ownRedraws, setOwnRedraws] = useState<0 | 1 | 2>(0); const offer = game.nexusCards; if (!offer) return null; const owner = game.players.find((player) => player.id === game.me); if (!owner) return null; const mode = offer.card ? nexusCardMode(offer.card, owner.faction, game.players.map((player) => player.faction)) : undefined; const labels: Record = { draw: 'Draw a Nexus card', replace: 'Discard and replace', keep: offer.card ? 'Keep this card' : 'Skip this draw', }; const waiting = offer.waiting.filter((id) => id !== game.me) .map((id) => game.players.find((player) => player.id === id)?.name).filter(Boolean); return (

Nexus cards

{offer.deckCount} in deck · {offer.discardCount} discarded · {Object.values(offer.held).filter((held) => held === true).length} held

Drawing and keeping cards is supported; their effects are not playable yet.

{offer.card ? :

You do not hold a Nexus card.

}
{(offer.choices.includes('draw') || offer.choices.includes('replace')) && ( )}
{[...offer.choices].sort((a, b) => Number(a === 'keep') - Number(b === 'keep')).map((choice) => { const action = nexusCardChoiceAction(game, choice, ownRedraws); return ; })}
{!!waiting.length &&

Waiting for Nexus choices: {waiting.join(', ')}.

}
View all 12 Nexus cards

This reference includes every faction. Other players’ held card identities stay secret.

); }