Files
Dune/components/nullentropy-box.tsx
T

120 lines
4.6 KiB
TypeScript

'use client';
import { useId } from 'react';
import type { Action, GameView } from '@/game/engine';
import { Button } from './ui/button';
import { CardInspector, CardRules } from './card-inspector';
const fullHandPolicy = 'Provisional full-hand exchange: retrieval and the played Box discard settle atomically, leaving your final hand at its limit. The printed intermediate-capacity timing remains unresolved.';
const shipmentPricePolicy = 'Provisional refund rule: the purchased shipping price, the player who bought it and its shipment beneficiary stay unchanged. Recovering its original card cost prevents a refund if the Guild later stops that shipment with special Karama, even after that card is discarded again. Taking another card leaves any still-unclaimed cost under its usual refund rule.';
type BoxProps = {
game: GameView;
act: (action: Action) => void;
busy: boolean;
};
/** Before payment this renders only the owned Box, never discard candidates. */
export function NullentropyBox({ game, act, busy }: BoxProps) {
const id = useId();
const box = game.nullentropy;
if (!box) return null;
if (box.search)
return (
<p className="notice">
Your Nullentropy Box search is paid. Complete the private card choice in
the decision panel above.
{box.fullHandExchange && <span className="block">{fullHandPolicy}</span>}
{game.karamaShipping && <span className="block">{shipmentPricePolicy}</span>}
</p>
);
return (
<details className="my-4 min-w-0">
<summary className="min-h-11 cursor-pointer py-3">
Nullentropy Box: paid discard search
</summary>
<div className="flex min-w-0 flex-col gap-4 py-3">
<p>
Pay 2 spice to the bank to privately search the Treachery discard
pile. Choose one card other than any Nullentropy Box. The remaining
pile is shuffled, then your played Box is discarded on top.
</p>
<p className="fine">
Payment begins the search. There is no free cancellation after seeing
the cards. If only one card is eligible, the server takes it
automatically.
</p>
{game.karamaShipping && <p className="fine">{shipmentPricePolicy}</p>}
{box.fullHandExchange && <p className="notice">{fullHandPolicy}</p>}
<CardRules card={box.card} />
<CardInspector card={box.card} />
{box.blocked && (
<p id={`${id}-reason`} className="notice">
{box.blocked}
</p>
)}
<Button
className="min-h-11 whitespace-normal motion-reduce:transition-none"
disabled={busy || !!box.blocked}
aria-describedby={box.blocked ? `${id}-reason` : undefined}
onClick={() => {
if (!busy && !box.blocked) act({ type: 'card', card: box.card.id });
}}
>
Pay 2 spice and search privately
</Button>
</div>
</details>
);
}
/** Only the paid, entitled projection supplies these temporary discard faces. */
export function NullentropySearch({ game, act, busy }: BoxProps) {
const search = game.nullentropy?.search;
if (
game.decision?.kind !== 'nullentropy' ||
game.decision.player !== game.me ||
!search
)
return null;
return (
<div className="flex min-w-0 flex-col gap-4">
<p>
You have paid 2 spice. Choose one card for your hand. This finishes the
search and discards your Box on top of the shuffled remainder.
</p>
<p className="fine">
These discard cards are visible only to you during this paid search.
Inspection does not select a card.
</p>
{game.karamaShipping && <p className="fine">{shipmentPricePolicy}</p>}
{game.nullentropy?.fullHandExchange && <p className="notice">{fullHandPolicy}</p>}
<div
className="grid min-w-0 gap-4"
style={{ gridTemplateColumns: 'repeat(auto-fit, minmax(min(100%, 16rem), 1fr))' }}
>
{search.cards.map((card) => (
<article
key={card.id}
className="flex min-w-0 flex-col gap-3 rounded-lg border border-[#65644b] p-4"
>
<h3>{card.name}</h3>
<CardRules card={card} />
<CardInspector card={card} />
<Button
className="min-h-11 whitespace-normal motion-reduce:transition-none"
disabled={busy}
onClick={() => {
if (!busy)
act({ type: 'decision', event: search.event, card: card.id });
}}
>
Take {card.name} and finish search
</Button>
</article>
))}
</div>
</div>
);
}