'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 { territory } from '@/game/board'; import { faction } from '@/game/catalog'; export const AMBASSADOR_COVERAGE = 'Development coverage: Ambassador placement, including the placed Hidden Mobile Stronghold, Ecaz’s direct Duke acquisition and consensual alliance, Emperor, Atreides, Harkonnen, CHOAM, Ixian, Richese, Fremen, Guild and Tleilaxu entry effects are supported, plus Bene Gesserit copies. The Tleilaxu effect offers physical forces or a first-death own leader, including Atreides’ separate Kwisatz Haderach token. Basic offers an optional new-ally Duke loan; Advanced automatically grants the available Duke after accepted alliance consent. One-battle loan tenure is provisional; an unused living loan is set aside at turn end. Ordinary revival accounting, repeat-return/shared-leader cases and competing arrivals remain for refinement; Ecaz starts remain labelled previews.'; function tokenLocation(token: AmbassadorToken) { if (token.zone === 'placed') return `Placed in ${token.location ? territory(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(''); return ( ); } /** Uses only the current projected supply and destinations, including the placed HMS. */ export function EcazPlacementChoice({ game, act, busy, id, selectedToken, selectedTerritory, onSelectToken, onSelectTerritory, }: { game: GameView; act: (action: Action) => void; busy: boolean; id: string; selectedToken: string; selectedTerritory: string; onSelectToken: (token: string) => void; onSelectTerritory: (territory: string) => void; }) { 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 = state.destinations; 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 stronghold is currently available for an Ambassador.

)} {!affordable && (

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

)}

{AMBASSADOR_COVERAGE}

); }