Files
Dune/components/moritani-extortion.tsx
Quintine 6c83148ead Connect Sapho first and last to Extortion payer choices
Apply the printed ordered-action alternative provisionally to the unanswered native Extortion suffix. Preserve collected bank5, completed declines, payer membership, one payment3 and placement-first policy. Replay a public local-order receipt and retain it through ledger answers; expire the old payer event even when the same player remains next. Permit only the exact current Extortion decision as a clean Sapho window. Add persistent remaining-order/card controls and existing minimal policy participation. The typed Semuta suffix parks one updated payer decision and restores it without paying, advancing or recollecting.

Validation:9-file115/115, types/lint/build and six original public Basic/Advanced host/setup/paid-cache1/placed-Terror/paid-entry1/reveal/Mentat-placement-before-bank5 programmes. An actual earlier decline stays finished; first/payment3 and last/payment3 or all-decline preserve native retirement/recovery. Four profiles make eight legal holder/payer proposals and complete. Public order and private payer affordability render in actual GameTable plus enabled/busy controls. Pure replay/no-op/recovered reuse cases are separate. Bounded source/continuation review found no concrete defect.

Extortion/Sapho boundary is provisional. Direct combined Semuta claim/capture, broader combinations, complete modes, final assurance and browser/deployed acceptance remain open. No saved-room/database/reset/restart/deployment operation.
2026-10-18 14:07:08 +11:00

83 lines
2.7 KiB
TypeScript
Raw Permalink Blame History

This file contains ambiguous Unicode characters
This file contains Unicode characters that might be confused with other characters. If you think that this is intentional, you can safely ignore this warning. Use the Escape button to reveal them.
'use client';
import type { Action, GameView } from '@/game/engine';
import { Button } from './ui/button';
export function MoritaniExtortionNotice({ game }: { game: GameView }) {
const deferred = game.extortion?.deferred ?? 0;
const pending = game.extortion?.pending;
if (!deferred && !pending) return null;
const payer = pending && game.players.find((player) => player.id === pending.player);
return (
<section className="notice" aria-label="Moritani Extortion status">
{deferred > 0 && (
<p>
Moritani Extortion: {deferred} spice from the bank is set aside for
Moritani to collect during Mentat Pause. It is not yet spendable.
</p>
)}
{pending && (
<>
<p>
Moritani has collected the bank award. {payer?.name ?? 'The current player'}
{' '}may pay {pending.amount} spice to prevent Extortion’s return, or decline.
The first payment ends the opportunity; if everyone declines, the token
returns to Moritani’s hidden supply.
</p>
<p>Remaining choices: {pending.remaining.map(id => game.players.find(player => player.id === id)?.name ?? id).join(' → ')}.</p>
{pending.saphoReordered && <p>Juice of Sapho changed the remaining order. Earlier declines stay finished.</p>}
</>
)}
</section>
);
}
export function MoritaniExtortion({
game,
act,
busy,
}: {
game: GameView;
act: (action: Action) => void;
busy: boolean;
}) {
const decision = game.decision;
const pending = game.extortion?.pending;
if (
decision?.kind !== 'moritaniExtortion' ||
decision.player !== game.me ||
!pending ||
pending.player !== game.me ||
pending.event !== decision.event
) return null;
const disabled = busy || !!game.response || !!game.truthtrance;
return (
<section className="notice min-w-0 space-y-3" aria-label="Your Moritani Extortion payment choice">
<p>
Pay Moritani {pending.amount} spice to remove the Extortion token permanently,
or decline. The five-spice bank award has already been collected.
</p>
{!pending.canPay && (
<p className="fine">You do not have enough spice to pay. You may decline.</p>
)}
<Button
className="min-h-11 w-full whitespace-normal"
disabled={disabled || !pending.canPay}
onClick={() => act({ type: 'decision', event: decision.event, pay: true })}
>
Pay {pending.amount} spice to Moritani
</Button>
<Button
variant="outline"
className="min-h-11 w-full whitespace-normal"
disabled={disabled}
onClick={() => act({ type: 'decision', event: decision.event, pay: false })}
>
Decline payment
</Button>
</section>
);
}