'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'; import { EcazDukeAcquisitionChoices } from './ecaz-duke-acquisition-choices'; 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 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}

{game.advanced && entry.effect === 'ecaz' && (

Advanced source override: supplied rulebook, physical page 30, adds an available Duke after accepted alliance consent automatically. The publisher token guide’s optional loan wording above remains Basic guidance.

)} {token && } {entry.stage === 'offer' ? ( <> {entry.effect === 'ecaz' ? ( <> {duke && ( )} {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.{' '} {game.advanced ? 'In Advanced, accepting also adds an available Duke to the entrant’s pool automatically. The first version uses one-battle tenure and sets an unused living Duke aside at turn end.' : '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.{' '} {game.advanced ? 'An available Duke Vidal is added to your pool automatically, without a second acknowledgement. One-battle tenure is provisional; an unused living Duke is set aside at turn end.' : 'In Basic, Ecaz can then optionally lend an available Duke Vidal.'}{' '} Your remaining actions then resume.

) : 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 leader or return up to {entry.revival?.maximum ?? 0} {' '}physical forces for free. This independent grant leaves ordinary allowances unchanged provisionally. When Tleilaxu is seated, a positive return can earn it one bank spice, sharing the beneficiary’s once-per-turn free-revival reward. Canceling income keeps the return. This income interpretation is provisional. First-death leaders and the first-killed Kwisatz Haderach need not wait for the ordinary revival cohort. Further returns must satisfy their ordinary death condition under the provisional interpretation. Kwisatz Haderach keeps its separate next-cohort marker. Other revival interactions remain under development.

{entry.revival?.leaders.some(leader => leader.destination === 'setAside') &&

Duke returns alive to the shared area, unclaimed. A separate legal acquisition is needed before he can fight.

} {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.revival?.eliteBlock &&

{entry.revival.eliteBlock}

} ) : 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'}

)}
); })}
); }