'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 && (
)}
);
}
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.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.'}