68 lines
5.0 KiB
TypeScript
68 lines
5.0 KiB
TypeScript
'use client';
|
|
|
|
import type { Action, GameView } from '@/game/engine';
|
|
import { gholaLeaderClaimText, liveGholaLeaderPromises } from '@/game/ghola-leader-promises';
|
|
import { Button } from './ui/button';
|
|
|
|
type Props = { game: GameView; busy: boolean; send: (action: Action) => void };
|
|
const leaderName = (game: GameView, id: string) => id === 'kwisatz' ? 'Kwisatz Haderach'
|
|
: game.allLeaders.find(leader => leader.id === id)?.name ?? 'the named leader';
|
|
|
|
/** Forward only the exact current native card action from the private guide. */
|
|
export function GholaLeaderPromiseGuideButton(props: Props) {
|
|
const game = props.game, guide = game.gholaLeaderPromiseGuide;
|
|
if (!guide) return null;
|
|
const actor = game.me, turn = game.turn, phase = game.phase, action = guide.action;
|
|
const signature = JSON.stringify(action), quote = game.ghola, quoteSignature = JSON.stringify(quote);
|
|
const box = game.nullentropy, search = box?.search, boxSignature = JSON.stringify(box);
|
|
const card = game.players.find(player => player.id === actor)?.hand?.find(c => c.id === action.card);
|
|
const canAct = () => {
|
|
const current = props.game, me = current.players.find(player => player.id === actor);
|
|
const live = liveGholaLeaderPromises(current.gholaLeaderPromises, actor, turn).filter(promise => promise.answer);
|
|
const named = typeof action.leader === 'string' && live.some(promise => promise.leader === action.leader);
|
|
const ghola = action.type === 'card' && action.mode === undefined && typeof action.card === 'string' && !!named &&
|
|
!!card && card.kind === 'special' && card.effect === 'ghola' && !!me?.hand?.includes(card) &&
|
|
current.ghola === quote && JSON.stringify(quote) === quoteSignature && quote.available &&
|
|
quote.cards.includes(action.card) && (action.leader === 'kwisatz' ? quote.kwisatz
|
|
: quote.leaders.some(leader => leader.id === action.leader)) && !current.decision;
|
|
const startBox = action.type === 'card' && action.mode === undefined && typeof action.card === 'string' &&
|
|
!!card && card.kind === 'special' && card.effect === 'nullentropyBox' && !!me?.hand?.includes(card) &&
|
|
current.nullentropy === box && JSON.stringify(box) === boxSignature && !!box &&
|
|
box.card.id === card.id && !box.blocked && !box.search && !current.decision;
|
|
const chooseBox = action.type === 'decision' && typeof action.card === 'string' &&
|
|
current.decision?.kind === 'nullentropy' && current.decision.player === actor &&
|
|
current.nullentropy === box && JSON.stringify(box) === boxSignature && !!search &&
|
|
box?.search === search && search.event === action.event && !box.blocked &&
|
|
search.cards.some(candidate => candidate.id === action.card && candidate.effect === 'ghola');
|
|
return !props.busy && current === game && current.status === 'playing' && current.me === actor &&
|
|
current.turn === turn && current.phase === phase && current.gholaLeaderPromiseGuide === guide &&
|
|
guide.action === action && JSON.stringify(action) === signature && live.length > 0 && !!me && !me.autopilot &&
|
|
(ghola || startBox || chooseBox) && !current.truthtrance && !current.response && !current.phaseOpening &&
|
|
!current.automaticContinuationPending && !current.roomControl?.paused && !current.roomControl?.closed;
|
|
};
|
|
return <Button className="game-action min-h-11 whitespace-normal" data-testid="ghola-leader-promise-guide"
|
|
disabled={!canAct()} onClick={() => { if (canAct()) props.send(action); }}>
|
|
{action.type === 'decision' ? 'Recover Ghola from your paid Nullentropy Box search'
|
|
: card?.effect === 'nullentropyBox' ? 'Pay 2 spice to search with Nullentropy Box for your Ghola answer'
|
|
: `Revive ${leaderName(game, typeof action.leader === 'string' ? action.leader : '')} with Ghola while honoring your answer`}
|
|
</Button>;
|
|
}
|
|
|
|
export function GholaLeaderPromises(props: Props) {
|
|
const own = props.game.gholaLeaderPromises.filter(promise => promise.player === props.game.me);
|
|
if (!own.length) return null;
|
|
return <section className="notice space-y-3" aria-label="Your Truthtrance Ghola leader promises" id="ghola-leader-promise-guidance">
|
|
<h3>Your Ghola leader promises</h3>
|
|
<ul>{own.map(promise => <li key={promise.event}>
|
|
Turn {promise.turn}: You answered {promise.answer ? 'Yes' : 'No'} to {gholaLeaderClaimText(promise, id => leaderName(props.game, id))}.
|
|
{promise.released ? ' Released: this commitment is no longer possible.'
|
|
: promise.fulfilled ? ' Fulfilled: this commitment is complete.'
|
|
: promise.turn !== props.game.turn ? ' This turn has ended.' : ' This answer is binding for this turn.'}
|
|
</li>)}</ul>
|
|
<p>Yes requires an actual future return of the named leader with Ghola while it remains possible.
|
|
No forbids that particular Ghola leader return this turn. An ordinary return, a force return, or an earlier Ghola play does not fulfill it.</p>
|
|
<p className="fine">The original card timing and leader eligibility still apply. This question is a provisional interpretation of Truthtrance.</p>
|
|
<GholaLeaderPromiseGuideButton {...props} />
|
|
</section>;
|
|
}
|