'use client'; import { useId, useState } from 'react'; import type { Action, GameView } from '@/game/engine'; import { AMBASSADOR_REFERENCE } from '@/game/ambassador-reference'; import { territory } from '@/game/board'; import { faction } from '@/game/catalog'; import { leaderStrengthLabel } from '@/game/cards'; import { CHEAP_HERO_TRAITOR } from '@/game/traitors'; import { Button } from './ui/button'; import { CardInspector, CardRules } from './card-inspector'; import { LeaderInspector } from './leader-inspector'; import { AmbassadorInspector, AMBASSADOR_COVERAGE } from './ecaz-ambassadors'; import { AmbassadorMovement } from './ambassador-movement'; import { AmbassadorShipment } from './ambassador-shipment'; const buttonClass = 'min-h-11 whitespace-normal motion-reduce:transition-none'; export function EcazEntry({ game, act, busy, }: { game: GameView; act: (action: Action) => void; busy: boolean; }) { const id = useId(); const [selected, setSelected] = useState([]); const entry = game.ambassadorEntry; if ( !entry || game.decision?.kind !== 'ecazAmbassador' || game.decision.player !== game.me || !['offer', 'allianceReply', 'copy', 'cards', 'move', 'ship'].includes( entry.stage, ) ) return null; const me = game.players.find((p) => p.id === game.me)!; const guide = AMBASSADOR_REFERENCE[entry.effect]; const token = game.ambassadors?.tokens.find((t) => t.effect === entry.effect); const send = (fields: Omit) => { if (!busy) act({ type: 'decision', event: entry.event, ...fields }); }; const choices = entry.cards.flatMap((option) => { const card = me.hand?.find((card) => card.id === option.card); return card ? [{ card, blocked: option.blocked }] : []; }); const chosen = choices.filter( (choice) => !choice.blocked && selected.includes(choice.card.id), ); const legalSelection = entry.effect === 'choam' || chosen.length === 1; const dukeBlock = entry.dukeAcquisition?.blocked ?? (entry.dukeAcquisition ? null : 'Duke acquisition is unavailable for this opportunity.'); const allianceBlock = entry.allianceOffer?.blocked ?? (entry.allianceOffer ? null : 'This alliance opportunity is unavailable.'); const duke = game.dukeVidal?.leader; return (

{guide.name} Ambassador · {territory(entry.territory).name}

{game.players.find((p) => p.id === entry.entrant)?.name} entered this stronghold. {guide.gameplay}

{token && } {entry.stage === 'offer' ? ( <> {entry.effect === 'ecaz' ? ( <>

Acquire Duke Vidal for Ecaz. The Ambassador returns to your supply. Ecaz keeps Duke until battle use or until Moritani takes him.

{duke && ( )} {dukeBlock && (

{dukeBlock}

)} {allianceBlock && (

{allianceBlock}

)}

Both factions must be unallied. The entrant chooses whether to accept. The triggered Ambassador returns to supply even if the offer is refused. Duke loans remain unfinished.

) : ( <>

Trigger the Ambassador for yourself or an eligible ally, or leave it in place. Triggering commits the token before the beneficiary resolves its effect.

{entry.effect === 'richese' && (

Triggering automatically spends 3 spice from the selected beneficiary and draws a card if the purchase can be completed. Otherwise no spice is spent and no card is drawn. The token is used either way; there is no later purchase choice.

)} {entry.beneficiaries.map((option) => (
{option.blocked && (

{option.blocked}

)}
))} )} ) : entry.stage === 'allianceReply' ? ( <>

{game.players.find((p) => p.id === entry.owner)?.name} offers you an alliance. Accepting activates both factions’ alliance abilities immediately. Duke Vidal is not included in this offer. Your remaining actions resume after your reply.

) : entry.stage === 'copy' ? ( <>

Choose an effect outside the original supply for this group. The Bene Gesserit token has already been permanently removed.

{entry.copies.map((option) => { const copied = AMBASSADOR_REFERENCE[option.effect]; const face = game.ambassadors?.tokens.find( (t) => t.effect === option.effect, ); return (

{copied.name}

{copied.gameplay}

{option.effect === 'richese' && (

This automatically spends 3 of your spice and draws a card if the purchase can be completed. Otherwise no spice is spent and no card is drawn. The Bene Gesserit token remains removed.

)} {face && } {option.blocked && (

{option.blocked}

)}
); })} ) : entry.stage === 'ship' ? ( ) : entry.stage === 'move' ? ( ) : ( <>

{entry.effect === 'choam' ? 'Choose any number of available cards, including none. Each discarded card earns three spice.' : 'Choose exactly one available card to discard and draw its replacement.'}

Your cards available for this effect {choices.map(({ card, blocked }) => (
{blocked &&

{blocked}

}
))}
)}

{AMBASSADOR_COVERAGE}

); } /** Only recipient-projected snapshots are read. Local disclosures never send a game action. */ export function AmbassadorInsights({ game }: { game: GameView }) { const snapshots = game.ambassadorInsights.filter( (entry) => entry.viewer === game.me, ); if (!snapshots.length) return null; return (

Private Ambassador inspections

These are snapshots of what you inspected at that moment, not a live view of another hand. Only you receive them. Opening or closing an inspection does not take a game action.

{snapshots.map((snapshot, index) => { const target = game.players.find((p) => p.id === snapshot.target); const leader = game.allLeaders.find((l) => l.id === snapshot.traitor); const cheapHero = snapshot.traitor === CHEAP_HERO_TRAITOR; return (
Turn {snapshot.turn} · {target?.name ?? 'Player'} ·{' '} {snapshot.effect === 'atreides' ? 'Treachery hand' : target?.faction === 'tleilaxu' ? 'Face Dancer identity' : 'Traitor identity'} {snapshot.effect === 'atreides' ? (
{!snapshot.cards.length && (

The inspected hand was empty.

)} {snapshot.cards.map((card) => (

{card.name}

))}
) : cheapHero || leader ? (
) : (

Recorded identity: {snapshot.traitor ?? 'None'}

)}
); })}
); }