'use client'; import { integerInputValue } from '../lib/integer-input'; import { useId, useState } from 'react'; import type { Action, GameView } from '@/game/engine'; import type { RicheseAuctionMethod } from '@/game/richese-auction'; import { Button } from './ui/button'; import { Input } from './ui/input'; import { CardInspector, CardRules } from './card-inspector'; type Props = { game: GameView; act: (action: Action) => void; busy: boolean }; const buttonClass = 'min-h-11 whitespace-normal motion-reduce:transition-none'; const methodName = { normal: 'Normal auction', onceAround: 'Once Around', silent: 'Silent auction', }; /** Only supplied owner-projected hand/cache identities are selectable. */ export function RicheseAuctionDecision({ game, act, busy }: Props) { const id = useId(); const [cardId, setCardId] = useState(''); const [method, setMethod] = useState('onceAround'); const [direction, setDirection] = useState<'clockwise' | 'counterclockwise'>( 'clockwise', ); const [claim, setClaim] = useState(''); const bidding = game.richeseBidding; const decision = game.decision; const me = game.players.find((player) => player.id === game.me); if ( !bidding || !me || decision?.player !== game.me || (decision.kind !== 'richeseCache' && bidding.owner !== game.me) || ![ 'richeseBlackMarket', 'richeseDeclaration', 'richeseCache', 'richeseCacheTerms', 'richeseUnbid', ].includes(decision.kind) ) return null; const send = (fields: Omit) => { if (!busy) act({ type: 'decision', event: bidding.event, ...fields }); }; if (decision.kind === 'richeseCacheTerms') { const cacheMethod = method === 'normal' ? 'onceAround' : method; return (

You remain the Richese seller. Choose the auction method and direction; the occupied Homeworld controller then chooses only the physical cache card.

{bidding.offerBlocked &&

{bidding.offerBlocked}

} {cacheMethod === 'onceAround' && ( <> )}
); } if (decision.kind === 'richeseDeclaration') return (

Choose whether your face-up cache card is auctioned before or after the ordinary cards. This declaration precedes Ixian auction inspection.

); if (decision.kind === 'richeseUnbid') { const room = (me.hand?.length ?? 0) < me.handLimit; return (

No player offered spice. Keep the cache card for free if your hand has room, or remove it from this game.

{game.richeseAuction?.card && ( <> )} {!room && (

Your hand is full; the card cannot be added.

)}
); } const blackMarket = decision.kind === 'richeseBlackMarket'; const cardOnly = decision.kind === 'richeseCache' && bidding.owner !== game.me; if (cardOnly && !bidding.cacheTerms) return {bidding.offerBlocked ?? 'Waiting for the native Richese seller to set cache auction terms.'}; const cards = blackMarket ? (me.hand ?? []) : (bidding.cache ?? []); const selected = cards.find((card) => card.id === cardId) ?? cards[0]; const chosenMethod = cardOnly ? bidding.cacheTerms!.method : !blackMarket && method === 'normal' ? 'onceAround' : method; return (

{blackMarket ? 'Offer a card from your hand without showing its face. Atreides may inspect it. An optional public claim may be truthful or a bluff. If no one pays spice, you keep the card.' : 'Choose one card from the original Richese cache. Its face is revealed to everyone when offered. Cache cards do not count toward a hand limit; Richese remains the seller.'}

{cardOnly && bidding.cacheTerms && (

Native Richese terms: {methodName[bidding.cacheTerms.method]} {bidding.cacheTerms.method === 'onceAround' ? ` · ${bidding.cacheTerms.direction}` : ''}. You choose the cache card only; the seller’s terms do not transfer.

)} {bidding.offerBlocked && (

{bidding.offerBlocked}

)} {cards.length ? ( <> {selected && ( <> )} {!cardOnly && ( <> {chosenMethod === 'onceAround' && ( <> )} )} {blackMarket && ( <> setClaim(event.target.value)} placeholder="What you say you are selling" /> )}

Karama cannot acquire this auction’s card.{' '} {chosenMethod === 'silent' ? 'Every eligible player submits once; zero is allowed. Equal positive bids use storm order.' : chosenMethod === 'onceAround' ? 'Each eligible player has one bidding opportunity, normally ending with Richese.' : 'Bidding continues in order until the other bidders pass.'}

) : (

No card is available to offer.

)} {blackMarket && ( )}
); } /** Only projected public bids and this viewer's own sealed submission are rendered. */ export function RicheseAuctionLot({ game, act, busy }: Props) { const id = useId(); const auction = game.richeseAuction; const [draft, setDraft] = useState({ event: '', amount: '', ally: '0' }); if (!auction) return null; const me = game.me; const silent = auction.method === 'silent'; const minimum = silent ? 0 : auction.bid + 1; const bidKey = `${auction.event}/${minimum}`; const amountText = draft.event === bidKey ? draft.amount : String(minimum); const allyText = draft.event === bidKey ? draft.ally : '0'; const amount = integerInputValue(amountText); const allyPayment = integerInputValue(allyText); const update = (part: Partial) => setDraft({ event: bidKey, amount: amountText, ally: allyText, ...part, }); const ownPayment = amount - allyPayment; const allowedTurn = !auction.outcome && auction.eligible.includes(me) && (silent ? !auction.submitted.includes(me) : auction.active === me); const canBid = allowedTurn && Number.isSafeInteger(amount) && amount >= minimum && Number.isSafeInteger(allyPayment) && allyPayment >= 0 && allyPayment <= auction.allyAvailable && ownPayment >= 0 && ownPayment <= auction.ownAvailable; const name = (player: string | null) => game.players.find((p) => p.id === player)?.name ?? 'Player'; const send = (bid: number | null, ally: number) => { if (!busy) act({ type: 'richeseBid', event: auction.event, amount: bid, allyPayment: ally, }); }; return (

{auction.source === 'cache' ? 'Richese cache' : 'Black Market'} ·{' '} {methodName[auction.method]}

{auction.card ? ( <>

{auction.card.name}

{auction.source === 'blackMarket' && (

This face is private to you through ownership or authorized inspection.

)} ) : (

The Black Market card remains face down.

)} {auction.inspectionBlocked && ( {auction.inspectionBlocked} )}

{name(auction.owner)} is selling. Karama cannot acquire this card.

{auction.source === 'blackMarket' && (

Provisional paid self-purchase: if the seller wins a positive bid, they retain this same card and pay the Emperor, or the Bank if absent. It counts as one sale and needs no additional hand space. An unbid card stays free and unsold.

)} {auction.claim && (

Seller’s public claim: {auction.claim}

)} {silent ? ( <>

{auction.submitted.length} of {auction.eligible.length} bids submitted.

{auction.ownBid !== null && (

Your sealed bid: {auction.ownBid} spice, including{' '} {auction.ownAllyPayment} pledged by your ally.

)} {auction.revealedBids && (
    {Object.entries(auction.revealedBids).map(([player, bid]) => (
  • {name(player)}: {bid} spice
  • ))}
)} ) : (

Highest bid: {auction.bid} spice {auction.bidder ? ` from ${name(auction.bidder)}` : ''}.

)} {allowedTurn ? ( <>

Available funding: {auction.ownAvailable} own spice and{' '} {auction.allyAvailable} pledged spice.

update({ amount: event.target.value })} /> {auction.allyAvailable > 0 && ( <> update({ ally: event.target.value })} /> )}

{canBid ? `Payment if you win: ${ownPayment} own spice + ${allyPayment} ally spice.` : 'Choose a valid bid and funding split within the available spice.'}{' '} {silent ? 'Your submitted amount stays private until all bids are in and cannot be changed.' : 'Raise the current bid or pass.'}

) : (

{auction.outcome ? 'The auction result is being resolved.' : silent && auction.submitted.includes(me) ? 'Your bid is sealed. Waiting for the other bidders.' : !auction.eligible.includes(me) ? 'You are not eligible to bid on this card.' : `Waiting for ${name(auction.active)} to bid.`}

)}
); }