'use client'; import { useEffect, useRef, useState } from 'react'; import { faction } from '@/game/catalog'; import type { Action, GameView } from '@/game/engine'; import { Button } from '@/components/ui/button'; type Deal = GameView['deals'][number]; /** The whole table can read an offer and its history; only its parties act. */ export function PublicDealList({ game, act, disabled }: { game: GameView; act: (action: Action) => void; disabled: boolean; }) { const name = (id: string) => { const player = game.players.find(candidate => candidate.id === id); return player ? `${player.name} (${faction(player.faction).name})` : 'Unknown player'; }; const canAct = game.status === 'playing' && !game.players.find(player => player.id === game.me)?.bot; const deals: Deal[] = game.deals ?? []; if (!deals.length) return

No public deals have been proposed.

; return
    {deals.map(deal => { const party = game.me === deal.from || game.me === deal.to; const voted = deal.releaseVotes.includes(game.me); const partner = game.me === deal.from ? deal.to : deal.from; return
  1. {name(deal.from)} → {name(deal.to)} · Turn {deal.turn} · {deal.status}

    {deal.terms}

    {deal.status === 'active' && deal.releaseVotes.length > 0 &&

    Release requested by {deal.releaseVotes.map(name).join(', ')}. Both parties must agree to release the deal.

    } {canAct && deal.status === 'offered' && deal.to === game.me &&
    } {canAct && deal.status === 'offered' && deal.from === game.me && } {canAct && deal.status === 'active' && party && }
  2. ; })}
; } export function PublicDeals({ game, act, busy }: { game: GameView; act: (action: Action) => void; busy: boolean; }) { const [target, setTarget] = useState(''); const [terms, setTerms] = useState(''); const needsResponse = game.status === 'playing' && game.deals.some(deal => deal.status === 'offered' && deal.to === game.me || deal.status === 'active' && (deal.from === game.me || deal.to === game.me) && !deal.releaseVotes.includes(game.me) && deal.releaseVotes.length > 0); const panel = useRef(null); useEffect(() => { if (needsResponse && panel.current) panel.current.open = true; }, [needsResponse]); if (game.status !== 'playing' && game.status !== 'finished') return null; const me = game.players.find(player => player.id === game.me); const targets = game.players.filter(player => me && player.id !== me.id && !player.bot && player.id !== me.ally && player.ally !== me.id); const recipient = targets.find(player => player.id === target); const disabled = busy || !!game.roomControl?.paused || !!game.roomControl?.closed || !!me?.autopilot; const mayOffer = game.status === 'playing' && me && !me.bot; return
Public deals{needsResponse ? ' · your response requested' : game.deals?.some(deal => deal.status === 'offered' || deal.status === 'active') ? ' · proposals or active deals' : ''}

Deals are public. A recipient must accept an offer. Players must honor valid terms; an impossible part is void, and both parties can agree to release a deal. The game records the terms but cannot automatically enforce free-text promises or determine when they become impossible.

Do not trade cards, forces, leaders or faction powers, or make terms that contradict the rules or faction powers. A deal does not automatically transfer spice; use the Bribes control to make an eligible payment. Bribe rules

{mayOffer &&
{ event.preventDefault(); const text = terms.trim(); if (!disabled && recipient && text && text.length <= 500) act({ type: 'dealOffer', target: recipient.id, terms: text }); }}>