Files
Dune/components/revival-promises.tsx
T

90 lines
6.1 KiB
TypeScript

'use client';
import type { Action, GameView } from '@/game/engine';
import { liveRevivalPromises, revivalClaimText, revivalPromiseModeSupported } from '@/game/revival-promises';
import { Button } from './ui/button';
type RevivalPromiseProps = { game: GameView; busy: boolean; send: (action: Action) => void };
/** Preserve the server's exact preparation or native return/Ready quote. The
* client checks only its live event and current owned action opportunity. */
export function RevivalPromiseGuideButton(props: RevivalPromiseProps) {
const game = props.game, guide = game.revivalPromiseGuide;
if (!guide) return null;
const actor = game.me, turn = game.turn, phase = guide.phase, order = game.revivalOrder;
const action = guide.action, event = action.revivalPromiseEvent, signature = JSON.stringify(action);
const canAct = () => {
const current = props.game, me = current.players.find(player => player.id === current.me);
const payment = phase === 3 && action.type === 'decision' &&
current.decision?.kind === 'auctionPayment' && current.decision.player === actor;
const native = current.richeseAuction;
const nativePass = phase === 3 && action.type === 'richeseBid' && native &&
native.event === action.event && !native.outcome && native.eligible.includes(actor) &&
(native.method === 'silent' ? !native.submitted.includes(actor) && action.amount === 0
: native.active === actor && action.amount === null) && action.allyPayment === 0;
const opportunity = phase === 2
? !current.ready.includes(actor) && (action.type === 'charity' || action.type === 'ready')
: phase === 3
? payment || nativePass || action.type === 'passBid' && !native && current.auction?.active === actor
: phase === 4 && !current.ready.includes(actor) && (action.type === 'revive' || action.type === 'ready') &&
current.revivalOrder === order && (!current.revivalOrder || current.revivalOrder.turn === turn &&
current.revivalOrder.remaining[0] === actor && current.active === actor);
return !props.busy && current === game && revivalPromiseModeSupported(current) &&
current.status === 'playing' && current.phase === phase && current.me === actor && current.turn === turn && opportunity &&
current.revivalPromiseGuide === guide && current.revivalPromiseGuide.phase === phase &&
current.revivalPromiseGuide.action === action && JSON.stringify(action) === signature && typeof event === 'string' &&
liveRevivalPromises(current.revivalPromises, actor, turn).some(promise => promise.event === event) && !!me && !me.autopilot &&
!current.truthtrance && !current.response && (!current.decision || payment) && !current.phaseOpening &&
!current.automaticContinuationPending && !current.roomControl?.paused && !current.roomControl?.closed;
};
return <Button className="game-action min-h-11 whitespace-normal" data-testid="revival-promise-guide"
disabled={!canAct()} onClick={() => { if (canAct()) props.send(action); }}>
{action.type === 'charity' ? 'Claim Charity for your later Revival'
: action.type === 'richeseBid' ? action.amount === 0
? 'Submit zero in this Silent auction while preserving your Revival answers'
: 'Pass this Richese bid while preserving your Revival answers'
: action.type === 'passBid' ? 'Pass this bid while preserving your Revival answers'
: action.type === 'decision' ? action.karama === true
? 'Use the quoted Karama auction payment for your later Revival'
: 'Pay the current auction price while preserving your Revival answers'
: action.type === 'revive' ? `Revive ${typeof action.amount === 'number' ? action.amount : 'the suggested number of'} forces while honoring your answers`
: phase === 2 ? 'Finish Charity while preserving your Revival answers'
: 'Finish Revival while honoring your answers'}
</Button>;
}
export function RevivalPromises(props: RevivalPromiseProps) {
const own = props.game.revivalPromises.filter(promise => promise.player === props.game.me);
if (!own.length) return null;
const live = liveRevivalPromises(own, props.game.me, props.game.turn).length > 0;
const other = props.game.revivalOrder?.remaining[0];
const waiting = live && (props.game.phase < 4 || props.game.phase === 4 && other && other !== props.game.me);
return <section className="notice space-y-3" aria-label="Your Truthtrance Revival promises" id="revival-promise-guidance">
<h3>Your Revival promises</h3>
<ul>{own.map(promise => <li key={promise.event}>
Turn {promise.turn}: You answered {promise.answer ? 'Yes' : 'No'} to {revivalClaimText(promise)}.
{' '}Recorded future ordinary returns: {promise.progress} {promise.counter === 'total' ? 'physical' : promise.counter} force{promise.progress === 1 ? '' : 's'}.
{promise.released ? ' Released: this commitment is no longer possible.'
: promise.fulfilled ? ' Fulfilled: this commitment is complete.' : ' This answer is binding for this turn.'}
</li>)}</ul>
<p>
Yes requires reaching the cumulative minimum through future ordinary native returns while it remains possible.
No keeps your cumulative future returns below that minimum, including across smaller split requests.
Prior returns, card effects, Ghola and Emperor extra revivals do not count.
</p>
<p className="fine">
Each normal or elite force counter counts once; total includes both types.
Original revival costs, limits and turn order still apply. Optional modules remain outside this commitment.
{' '}<a href="/rules#truthtrance-revival">Revival promise rules</a>.
</p>
{props.game.phase < 4 && <p>
A current preparation may claim Charity, pass an auction bid, or complete an already committed auction payment.
These steps preserve your later Revival; they do not count as returned forces.
</p>}
{waiting && <p>Your Revival opportunity is still ahead; {props.game.phase < 4
? 'continue the current phase and keep the required spice and forces available.'
: 'this promise remains visible while the current actor finishes.'}</p>}
<RevivalPromiseGuideButton {...props} />
</section>;
}