Prototype named Ghola leader Truthtrance promises
Verify and publish checkpoint container / container (push) Successful in 10m45s
Verify and publish checkpoint container / container (push) Successful in 10m45s
This commit is contained in:
1 parent
f8078ed5e2
commit
c1ffa1c28a
16 files changed
+689
-9
No files matched your search
@@ -46,6 +46,7 @@ import { MixedBidPromises } from './mixed-bid-promises';
|
||||
import { RevivalPromises } from './revival-promises';
|
||||
import { RevivalExpressionPromises } from './revival-expression-promises';
|
||||
import { LeaderRevivalPromises } from './leader-revival-promises';
|
||||
import { GholaLeaderPromises } from './ghola-leader-promises';
|
||||
import { BidPromises } from './bid-promises';
|
||||
import { PhaseBidPromises } from './phase-bid-promises';
|
||||
import { HomeworldShipment } from './homeworld-shipment';
|
||||
@@ -2158,6 +2159,7 @@ export function GameTable({
|
||||
{g.status === 'playing' && <RevivalExpressionPromises game={g} send={act} busy={busy} />}
|
||||
{g.status === 'playing' && <MixedRevivalPromises game={g} send={act} busy={busy} />}
|
||||
{g.status === 'playing' && <LeaderRevivalPromises game={g} send={act} busy={busy} />}
|
||||
{g.status === 'playing' && <GholaLeaderPromises game={g} send={act} busy={busy} />}
|
||||
{g.status === 'playing' && <RevivalPromises game={g} send={act} busy={busy} />}
|
||||
{g.status === 'playing' && <BidPromises game={g} send={act} busy={busy} />}
|
||||
{g.status === 'playing' && <PhaseBidPromises game={g} send={act} busy={busy} />}
|
||||
|
||||
@@ -0,0 +1,46 @@
|
||||
'use client';
|
||||
|
||||
import { parseGholaLeaderClaim, type GholaLeaderClaim } from '@/game/ghola-leader-promises';
|
||||
|
||||
type LeaderChoice = { id: string; name: string };
|
||||
|
||||
export function gholaLeaderClaimInputError(value: unknown, leaders?: readonly LeaderChoice[]): string | null {
|
||||
try {
|
||||
const claim = parseGholaLeaderClaim(value);
|
||||
return leaders && !leaders.some(leader => leader.id === claim.leader)
|
||||
? 'Choose one of the current named leaders.' : null;
|
||||
} catch (error) {
|
||||
return error instanceof Error ? error.message : 'Choose the current turn and a named leader.';
|
||||
}
|
||||
}
|
||||
|
||||
/** The parent supplies the current turn and public leader names. Server rules
|
||||
* decide whether the named return is possible and whether a response is legal. */
|
||||
export function GholaLeaderClaimFields(props: {
|
||||
value: GholaLeaderClaim;
|
||||
onChange: (claim: GholaLeaderClaim) => void;
|
||||
leaders: readonly LeaderChoice[];
|
||||
disabled?: boolean;
|
||||
}) {
|
||||
const error = gholaLeaderClaimInputError(props.value, props.leaders);
|
||||
return <fieldset className="grid gap-2" disabled={props.disabled} aria-label="Named Ghola leader return condition">
|
||||
<legend>Future Ghola leader return</legend>
|
||||
<p>Turn {props.value.turn}</p>
|
||||
<label htmlFor="truth-ghola-leader">Named leader</label>
|
||||
<select id="truth-ghola-leader" disabled={props.disabled}
|
||||
value={props.leaders.some(leader => leader.id === props.value.leader) ? props.value.leader : ''}
|
||||
onChange={event => {
|
||||
const next = { ...props.value, leader: event.currentTarget.value };
|
||||
if (!props.disabled && !gholaLeaderClaimInputError(next, props.leaders)) props.onChange(parseGholaLeaderClaim(next));
|
||||
}}>
|
||||
<option value="">Choose a named leader</option>
|
||||
{props.leaders.map(leader => <option key={leader.id} value={leader.id}>{leader.name}</option>)}
|
||||
</select>
|
||||
{error && <p role="alert">{error}</p>}
|
||||
<p className="fine">
|
||||
Ask whether this named leader will actually return using Tleilaxu Ghola after the answer this turn.
|
||||
Playing Ghola to return forces, ordinary revival, or a return before the answer does not count.
|
||||
The card and its normal eligibility rules still apply.
|
||||
</p>
|
||||
</fieldset>;
|
||||
}
|
||||
@@ -0,0 +1,55 @@
|
||||
'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 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 named = typeof action.leader === 'string' && liveGholaLeaderPromises(current.gholaLeaderPromises, actor, turn)
|
||||
.some(promise => promise.answer && promise.leader === action.leader);
|
||||
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 && action.type === 'card' &&
|
||||
action.mode === undefined && typeof action.card === 'string' && !!named && !!me && !me.autopilot &&
|
||||
!!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.truthtrance && !current.response && !current.decision && !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); }}>
|
||||
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>;
|
||||
}
|
||||
@@ -33,6 +33,7 @@ import { bribeExpressionRecipients, type BribeExpression } from '@/game/bribe-ex
|
||||
import { RevivalExpressionFields, revivalExpressionInputError } from './revival-expression-fields';
|
||||
import type { RevivalExpression } from '@/game/revival-expression-promises';
|
||||
import { LeaderRevivalClaimFields, leaderRevivalClaimInputError } from './leader-revival-claim-fields';
|
||||
import { GholaLeaderClaimFields, gholaLeaderClaimInputError } from './ghola-leader-claim-fields';
|
||||
import type { LeaderRevivalClaim } from '@/game/leader-revival-promises';
|
||||
import { MovementClaimFields } from './movement-claim-fields';
|
||||
import { AllianceClaimFields, allianceClaimInputError } from './alliance-claim-fields';
|
||||
@@ -131,7 +132,7 @@ export function Truthtrance({
|
||||
g.players.find((p) => p.id !== me.id)!.id,
|
||||
);
|
||||
const [kind, setKind] = useState<
|
||||
'fact' | 'freeform' | 'charity' | 'battlePlan' | 'futureBattleLeader' | 'shipment' | 'mixedShipment' | 'movement' | 'traitorCall' | 'bid' | 'phaseBid' | 'richeseBid' | 'earlyRicheseBid' | 'revival' | 'bribe' | 'pledgeAid' | 'alliance' | 'leaderRevival' | 'revivalExpression' | 'bribeExpression' | 'movementExpression' | 'mixedMovement' | 'mixedBribe' | 'mixedRevival' | 'mixedBid' | 'allianceExpression' | 'mixedAlliance' | 'mixedBattlePlan' | 'mixedTraitorCall' | 'travel' | 'turnActions'
|
||||
'fact' | 'freeform' | 'charity' | 'battlePlan' | 'futureBattleLeader' | 'shipment' | 'mixedShipment' | 'movement' | 'traitorCall' | 'bid' | 'phaseBid' | 'richeseBid' | 'earlyRicheseBid' | 'revival' | 'bribe' | 'pledgeAid' | 'alliance' | 'leaderRevival' | 'gholaLeader' | 'revivalExpression' | 'bribeExpression' | 'movementExpression' | 'mixedMovement' | 'mixedBribe' | 'mixedRevival' | 'mixedBid' | 'allianceExpression' | 'mixedAlliance' | 'mixedBattlePlan' | 'mixedTraitorCall' | 'travel' | 'turnActions'
|
||||
>('fact');
|
||||
const [revivalLeader, setRevivalLeader] = useState(g.allLeaders.find(leader =>
|
||||
leader.faction === g.players.find(player => player.id === target)?.faction)?.id ?? '');
|
||||
@@ -270,7 +271,7 @@ export function Truthtrance({
|
||||
: kind === 'movement' ? { kind, target, ...movement }
|
||||
: kind === 'mixedRevival' ? { kind, target, turn: g.turn, mixed: mixedRevival }
|
||||
: kind === 'revivalExpression' ? { kind, target, turn: g.turn, claim: revivalExpression }
|
||||
: kind === 'leaderRevival' ? { kind, target, ...leaderRevivalClaim }
|
||||
: kind === 'leaderRevival' || kind === 'gholaLeader' ? { kind, target, ...leaderRevivalClaim }
|
||||
: kind === 'allianceExpression' ? { kind, target, turn: g.turn, claim: allianceExpression }
|
||||
: kind === 'mixedAlliance' ? { kind, target, turn: g.turn, mixed: mixedAlliance }
|
||||
: kind === 'alliance' ? { kind, target, ...allianceClaim }
|
||||
@@ -425,6 +426,13 @@ export function Truthtrance({
|
||||
{button(`Answer ${answer === 'yes' ? 'Yes' : answer === 'no' ? 'No' : 'unknown'}`, { type: 'truthAnswer', answer })}
|
||||
</div>)}
|
||||
</>
|
||||
) : w.question!.kind === 'gholaLeader' ? (
|
||||
<>
|
||||
<p className="notice">This provisional question concerns a future return of the named leader using Ghola this turn. A force return, ordinary revival, or play before this answer does not fulfill it.</p>
|
||||
{(g.truthGholaLeaderAnswers ?? []).map(answer => <div key={answer}>
|
||||
{button(`Answer ${answer === 'yes' ? 'Yes' : answer === 'no' ? 'No' : 'unknown'}`, { type: 'truthAnswer', answer })}
|
||||
</div>)}
|
||||
</>
|
||||
) : w.question!.kind === 'leaderRevival' ? (
|
||||
<>
|
||||
<p className="notice">This concerns a future actual ordinary return of the named leader before your Revival opportunity ends. Native cost, eligibility, cycle and order still apply. A request alone, Ghola card, special or foreign-ghola return does not fulfill it.</p>
|
||||
@@ -670,6 +678,7 @@ export function Truthtrance({
|
||||
<option value="mixedRevival">Combine current facts and ordinary Revival</option>
|
||||
<option value="revivalExpression">Bind combined force and leader revivals</option>
|
||||
<option value="leaderRevival">Bind a named leader revival this turn</option>
|
||||
<option value="gholaLeader">Bind a named leader return with Ghola this turn (preview)</option>
|
||||
<option value="revival">Bind ordinary force revivals this turn</option>
|
||||
<option value="bid">Bind a bid in this ordinary lot</option>
|
||||
<option value="charity">Bind a Charity claim this turn</option>
|
||||
@@ -768,6 +777,15 @@ export function Truthtrance({
|
||||
{!g.truthRevivalExpressionTargets.includes(target) && <p className="notice">Choose a supported participant with an ordinary Revival opportunity still ahead this turn.</p>}
|
||||
<p className="notice" aria-label="Public grouped revival question preview">{truthQuestionText(question, leaderName, locationName, name)}</p>
|
||||
</>
|
||||
) : kind === 'gholaLeader' ? (
|
||||
<>
|
||||
<GholaLeaderClaimFields value={leaderRevivalClaim} leaders={leaderRevivalChoices}
|
||||
disabled={busy || !g.truthGholaLeaderTargets.includes(target)}
|
||||
onChange={claim => setRevivalLeader(claim.leader)} />
|
||||
<p className="fine">This question is a provisional preview. Only a named leader returned through Ghola after the answer counts.</p>
|
||||
{!g.truthGholaLeaderTargets.includes(target) && <p className="notice">Choose a supported participant for this question.</p>}
|
||||
<p className="notice" aria-label="Public named Ghola leader question preview">{truthQuestionText(question, leaderName, locationName, name)}</p>
|
||||
</>
|
||||
) : kind === 'leaderRevival' ? (
|
||||
<>
|
||||
<LeaderRevivalClaimFields value={leaderRevivalClaim} leaders={leaderRevivalChoices}
|
||||
@@ -988,6 +1006,7 @@ export function Truthtrance({
|
||||
(kind === 'mixedRevival' && (!!mixedRevivalError || !g.truthMixedRevivalTargets.includes(target))) ||
|
||||
(kind === 'revivalExpression' && (!!revivalExpressionInputError(revivalExpression) || !validRevivalExpressionNames || !g.truthRevivalExpressionTargets.includes(target))) ||
|
||||
(kind === 'leaderRevival' && (!!leaderRevivalClaimInputError(leaderRevivalClaim, leaderRevivalChoices) || !g.truthLeaderRevivalTargets.includes(target))) ||
|
||||
(kind === 'gholaLeader' && (!!gholaLeaderClaimInputError(leaderRevivalClaim, leaderRevivalChoices) || !g.truthGholaLeaderTargets.includes(target))) ||
|
||||
(kind === 'mixedBattlePlan' && (!!mixedBattleError || !g.truthMixedBattleTargets.includes(target))) ||
|
||||
(kind === 'allianceExpression' && (!!allianceExpressionError || !g.truthAllianceExpressionTargets.includes(target))) ||
|
||||
(kind === 'mixedAlliance' && (!!mixedAllianceError || !g.truthMixedAllianceTargets.includes(target))) ||
|
||||
|
||||
Reference in new issue
Block a user