'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 { NexusEmperorSecretAlly } from './nexus-emperor-secret-ally'; import { NexusFremenRevival } from './nexus-fremen-revival'; import {NexusTleilaxuRevival} from './nexus-tleilaxu-revival'; 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

Connected controls include Atreides inspections, Harkonnen exchanges, native Cunning, bounded Ecaz Duke acquisition, Emperor bank purchases and extra revivals, Tleilaxu free-leader/paid-force revival, Fremen ordinary free revival or worm protection, Bene Gesserit borrowed Voice, and Richese/Guild shipment effects. Use only the controls offered at each effect’s timing; provisional interpretations are labelled there. {game.nexusKullPreview ? 'CHOAM Nexus Kull is reactive: choose its fuel at the Karama opportunity.' : 'CHOAM Nexus Kull is unavailable outside its explicit preview.'} Moritani Cunning enhances supply placement, not relocation. Unsupported combinations and complete public modes remain gated.

{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 ; })}
)}
)} {game.nexusEmperorBetrayal && offer.card === 'emperor' && (

Emperor Betrayal · Sardaukar

Spend your Nexus card during this battle response to prevent the Emperor’s Sardaukar strength advantage. Its starred forces remain physical Sardaukar.

)} {game.nexusBgBetrayal && offer.card === 'beneGesserit' && (

Bene Gesserit Betrayal · Voice

Spend your Nexus card to prevent this declared Voice before battle plans are sealed. Prescience and other battle powers still proceed.

)}
{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.

); }