diff --git a/components/game-table.tsx b/components/game-table.tsx index be2552df..4e97c969 100644 --- a/components/game-table.tsx +++ b/components/game-table.tsx @@ -9,6 +9,7 @@ import { HomeworldRevivalDeployment } from './homeworld-revival-deployment'; import { CaladanReinforcement } from './caladan-reinforcement'; import { GrummanCollection } from './grumman-collection'; import { TupileIntelligence } from './tupile-intelligence'; +import { NexusCards } from './nexus-cards'; import { TerrorBoardMarkers } from './terror-board-markers'; import { homeworldRevivalActionBlock } from '@/game/homeworld-revival-deployment-options'; import { GuildHomeworldShipment } from './guild-homeworld-shipment'; @@ -1415,7 +1416,8 @@ export function GameTable({ {g.status === 'finished' ? 'FINAL OUTCOME' : 'YOUR NEXT DECISION'} - + + {!g.nexusCards?.waiting.length && } {g.biddingEnd && } {g.junctionTransport && [g.junctionTransport.owner, g.junctionTransport.recipient].includes(me.id) && ( @@ -1436,7 +1438,9 @@ export function GameTable({ sector={sector} onSectorChange={setSector} /> - {g.truthtrance ? ( + {g.nexusCards?.waiting.length ? ( +

The next phase begins when the remaining Nexus card choices are finished.

+ ) : g.truthtrance ? ( 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. +
+
+
+
+
+ )} +
Ecaz & Moritani · Original Nexus effects
+
+ ); +} + +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.

+ +
+
+ ); +} diff --git a/components/rules-reference.tsx b/components/rules-reference.tsx index 5605824d..6ff62961 100644 --- a/components/rules-reference.tsx +++ b/components/rules-reference.tsx @@ -2,6 +2,7 @@ import Link from 'next/link'; import { StrongholdCardGallery } from './stronghold-cards'; import { HomeworldCardGallery } from './homeworld-cards'; +import { NexusCardGallery } from './nexus-cards'; import { createStrongholdCards } from '@/game/stronghold-cards'; import { useSearchParams } from 'next/navigation'; import { Suspense, useState, useEffect, useRef } from 'react'; @@ -187,6 +188,7 @@ function RulesReferenceContent() { /> )} {topic.id === 'homeworlds' && } + {topic.id === 'nexus-cards' && } {topic.id === 'richese-cards' && (