'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', 'loan', 'copy', 'cards', 'move', 'ship', 'revival'].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. In Basic, an accepted alliance can be followed by an optional Duke loan when the disc is available.

) : ( <>

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. In Basic, Ecaz can then optionally lend an available Duke Vidal. Your remaining actions resume after those choices.

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

Lend Duke Vidal to your new ally for this turn, or continue without lending him. Basic prototype: one battle; an unused living Duke is set aside at turn end. Return and capture interactions remain provisional.

{entry.dukeLoan?.blocked &&

{entry.dukeLoan.blocked}

} ) : 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 === 'revival' ? ( <>

Revive one eligible own leader, or return up to {entry.revival?.maximum ?? 0} {' '}physical forces from the Tanks for free. This independent grant leaves ordinary allowances and income unchanged provisionally. Repeat-death and shared-leader interactions remain unfinished.

{entry.revival?.blocked && (

{entry.revival.blocked}

)} {!entry.revival?.blocked && entry.revival?.leaders.map(leader => ( ))} {!entry.revival?.blocked && (entry.revival?.maximum ?? 0) > 0 && (
Forces to return {Array.from( { length: entry.revival!.maximum }, (_, index) => index + 1, ).map((count) => ( ))}
)} ) : 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'}

)}
); })}
); }