'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, PlanField } 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. Available actions appear at their required timing; unfinished effects remain unavailable.
)}
); } 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); } /** The server's private offer defines current fields and timing; no other * player's hand or sealed plan participates in client eligibility. */ export function nexusAtreidesAction(game: GameView, field?: PlanField): Action | null { const offer = game.nexusAtreides; if (!offer || offer.blocked || game.status !== 'playing' || game.phase !== 6 || game.nexusCards?.card !== 'atreides') return null; const owner = game.players.find((player) => player.id === game.me); if (!owner || nexusCardMode('atreides', owner.faction, game.players.map((player) => player.faction)) !== offer.mode) return null; const battle = game.battle; if (!battle || battle.event !== offer.event || battle.revealed) return null; if (offer.mode === 'betrayal') return field === undefined ? { type: 'nexusAtreides', event: offer.event, mode: offer.mode } : null; if (!field || !offer.fields.includes(field) || ![battle.attacker, battle.defender].includes(game.me) || battle.submitted.includes(game.me)) return null; return { type: 'nexusAtreides', event: offer.event, mode: offer.mode, field }; } 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

Playable effects include Atreides inspections, Harkonnen exchanges, and Tleilaxu, Ixian, Bene Gesserit, Emperor, CHOAM and Moritani Cunning, plus Richese Secret Ally reserve shipping and Guild Cunning’s second shipment or absent-Guild Secret Ally shipping. Use the controls shown at each effect’s timing. CHOAM’s Kull Wahad effect, Moritani Cunning relocation and Grumman combinations, and other unfinished effects remain unavailable.

{game.nexusAtreides && offer.card === 'atreides' && (

{NEXUS_PANEL_NAMES[game.nexusAtreides.mode]}

{game.nexusAtreides.blocked ?

{game.nexusAtreides.blocked}

: ( <>

{game.nexusAtreides.mode === 'betrayal' ? 'Discard this Nexus card to cancel the pending Atreides inspection.' : game.nexusAtreides.mode === 'cunning' ? 'Discard this Nexus card to inspect a second, different element of your opponent’s plan.' : 'Discard this Nexus card to inspect one element of your opponent’s plan.'}

{(game.nexusAtreides.mode === 'betrayal' ? [undefined] : game.nexusAtreides.fields).map((field) => { const action = nexusAtreidesAction(game, field); return ; })}
)}
)}
{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.

); }