'use client'; import { useId, useState } from 'react'; import { Dialog as DialogPrimitive } from '@base-ui/react/dialog'; import { Eye, X } from 'lucide-react'; import { Button } from '@/components/ui/button'; import { Dialog, DialogClose, DialogDescription, DialogOverlay, DialogPortal, DialogTitle, DialogTrigger, } from '@/components/ui/dialog'; import type { Action, GameView } from '@/game/engine'; import type { AmbassadorToken } from '@/game/ecaz-ambassadors'; import { AMBASSADOR_COMMON_GUIDANCE, AMBASSADOR_REFERENCE, } from '@/game/ambassador-reference'; import { TERRITORIES } from '@/game/board'; import { faction } from '@/game/catalog'; export const AMBASSADOR_COVERAGE = 'Development coverage: Ambassador placement, Ecaz’s direct Duke acquisition and consensual alliance, Emperor, Atreides, Harkonnen, CHOAM, Ixian, Richese, Fremen and Guild entry effects are supported, plus Bene Gesserit copies of those eight ordinary effects. Duke loan choices, other effects and some competing arrival reactions remain unfinished; full Ecaz starts remain disabled.'; function tokenLocation(token: AmbassadorToken) { if (token.zone === 'placed') return `Placed in ${TERRITORIES.find((t) => t.id === token.location)?.name ?? 'a stronghold'}`; return { pool: 'Unused pool', supply: 'Available supply', used: 'Triggered · set aside', removed: 'Permanently removed', }[token.zone]; } function AmbassadorArt({ token, large = false, }: { token: AmbassadorToken; large?: boolean; }) { const identity = faction(token.effect); return ( ); } /** Inspection accepts one already-public token; no private deck or game state is consulted. */ export function AmbassadorInspector({ token }: { token: AmbassadorToken }) { const guide = AMBASSADOR_REFERENCE[token.effect]; const status = tokenLocation(token); return ( } aria-label={`Inspect ${guide.name} Ambassador: ${status}`} > {guide.name} {status}
{guide.name} Ambassador Gameplay guide · {status}
} aria-label="Close Ambassador inspection" >

{guide.gameplay}

Using Ambassadors

{AMBASSADOR_COMMON_GUIDANCE.map((paragraph) => (

{paragraph}

))}

{AMBASSADOR_COVERAGE}

Original illustration and gameplay explanation.

); } export function AmbassadorSupply({ game }: { game: GameView }) { if (!game.ambassadors) return null; const supplyCount = game.ambassadors.tokens.filter( (token) => token.zone === 'supply', ).length; const placedCount = game.ambassadors.tokens.filter( (token) => token.zone === 'placed', ).length; const zones: AmbassadorToken['zone'][] = [ 'supply', 'placed', 'used', 'removed', 'pool', ]; const names = { supply: 'Available supply', placed: 'On the board', used: 'Triggered this group', removed: 'Permanently removed', pool: 'Unused pool', }; return (
Ecaz Ambassadors{' '} · {supplyCount} in supply · {placedCount} placed · Inspect tokens

Inspect any token to read its effect and current location.

{zones.map((zone) => { const tokens = game.ambassadors!.tokens.filter( (token) => token.zone === zone, ); if (!tokens.length) return null; return (

{names[zone]} · {tokens.length}

{tokens.map((token) => ( ))}
); })}

{AMBASSADOR_COVERAGE}

); } export function EcazPlacement({ game, act, busy, }: { game: GameView; act: (action: Action) => void; busy: boolean; }) { const id = useId(); const [selectedToken, setSelectedToken] = useState(''); const [selectedTerritory, setSelectedTerritory] = useState(''); const me = game.players.find((player) => player.id === game.me); const state = game.ambassadors; if ( !state || me?.faction !== 'ecaz' || game.decision?.kind !== 'ecazPlacement' || game.decision.player !== me.id ) return null; const supply = state.tokens.filter((token) => token.zone === 'supply'); const destinations = TERRITORIES.filter( (territory) => territory.type === 'stronghold' && !territory.sectors.includes(game.storm) && !state.tokens.some( (token) => token.zone === 'placed' && token.location === territory.id, ), ); const token = supply.find((candidate) => candidate.id === selectedToken); const destination = destinations.find( (candidate) => candidate.id === selectedTerritory, ); const affordable = (me.spice ?? 0) >= state.nextCost; const disabled = busy || state.blocked || !token || !destination || !affordable; return (

Place an Ambassador

Next placement: {state.nextCost} spice · Your available spice: {me.spice ?? 0}. Each placement this turn increases the next cost by one.

{token && ( <>

{AMBASSADOR_REFERENCE[token.effect].gameplay}

)} {state.blocked && (

Ambassador placement is blocked for this turn.

)} {!supply.length && (

No Ambassador is available in your supply.

)} {!destinations.length && (

No ordinary stronghold is currently available for an Ambassador.

)} {!affordable && (

You need {state.nextCost - (me.spice ?? 0)} more spice for this placement.

)}

{AMBASSADOR_COVERAGE}

); }