Add named Truthtrance leader-revival commitments
This commit is contained in:
1 parent
5627512d16
commit
a76effb932
20 files changed
+1681
-180
No files matched your search
@@ -27,6 +27,7 @@ import { ShipmentPromises } from './shipment-promises';
|
||||
import { AlliancePromises } from './alliance-promises';
|
||||
import { BribePromises } from './bribe-promises';
|
||||
import { RevivalPromises } from './revival-promises';
|
||||
import { LeaderRevivalPromises } from './leader-revival-promises';
|
||||
import { BidPromises } from './bid-promises';
|
||||
import { HomeworldShipment } from './homeworld-shipment';
|
||||
import { HomeworldRevivalDeployment } from './homeworld-revival-deployment';
|
||||
@@ -2036,6 +2037,7 @@ export function GameTable({
|
||||
)}
|
||||
{g.status === 'playing' && <BribePromises game={g} send={act} busy={busy} />}
|
||||
{g.status === 'playing' && <AlliancePromises game={g} send={act} busy={busy} />}
|
||||
{g.status === 'playing' && <LeaderRevivalPromises 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.guildBetrayalReaction ? (
|
||||
|
||||
@@ -0,0 +1,43 @@
|
||||
'use client';
|
||||
|
||||
import { parseLeaderRevivalClaim, type LeaderRevivalClaim } from '@/game/leader-revival-promises';
|
||||
|
||||
type LeaderChoice = { id: string; name: string };
|
||||
export function leaderRevivalClaimInputError(value: unknown, leaders?: readonly LeaderChoice[]): string | null {
|
||||
try {
|
||||
const claim = parseLeaderRevivalClaim(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 authorized public name choices.
|
||||
* Eligibility and the native return quote remain authoritative on the server. */
|
||||
export function LeaderRevivalClaimFields(props: {
|
||||
value: LeaderRevivalClaim;
|
||||
onChange: (claim: LeaderRevivalClaim) => void;
|
||||
leaders: readonly LeaderChoice[];
|
||||
disabled?: boolean;
|
||||
}) {
|
||||
const error = leaderRevivalClaimInputError(props.value, props.leaders);
|
||||
return <fieldset className="grid gap-2" disabled={props.disabled} aria-label="Named ordinary leader Revival condition">
|
||||
<legend>Future ordinary leader Revival</legend>
|
||||
<p>Turn {props.value.turn}</p>
|
||||
<label htmlFor="truth-leader-revival-leader">Named leader</label>
|
||||
<select id="truth-leader-revival-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 && !leaderRevivalClaimInputError(next, props.leaders)) props.onChange(parseLeaderRevivalClaim(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 through ordinary native Revival after the answer this turn.
|
||||
A request alone is not a return. Native revival cycles, costs and turn order still apply;
|
||||
Ghola, foreign leaders and special card returns do not count.
|
||||
</p>
|
||||
</fieldset>;
|
||||
}
|
||||
@@ -0,0 +1,96 @@
|
||||
'use client';
|
||||
|
||||
import type { Action, GameView } from '@/game/engine';
|
||||
import { leaderRevivalClaimText, leaderRevivalPromiseModeSupported, liveLeaderRevivalPromises } from '@/game/leader-revival-promises';
|
||||
import { Button } from './ui/button';
|
||||
|
||||
type LeaderRevivalPromiseProps = { 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 the exact projected preparation or ordinary return packet. The
|
||||
* client does not reconstruct leader eligibility, cycle restrictions or cost. */
|
||||
export function LeaderRevivalPromiseGuideButton(props: LeaderRevivalPromiseProps) {
|
||||
const game = props.game, guide = game.leaderRevivalPromiseGuide;
|
||||
if (!guide) return null;
|
||||
const actor = game.me, turn = game.turn, phase = guide.phase, order = game.revivalOrder;
|
||||
const action = guide.action, event = action.leaderRevivalPromiseEvent, 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 request = current.revivalRequests[actor];
|
||||
const acceptedQuote = action.type === 'acceptLeaderRevival' && !!request && !request.declined &&
|
||||
typeof request.price === 'number' && request.price === action.leaderRevivalPrice;
|
||||
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 === 'reviveLeader' || action.type === 'reviveKwisatz' || acceptedQuote || 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 && leaderRevivalPromiseModeSupported(current) &&
|
||||
current.status === 'playing' && current.phase === phase && current.me === actor && current.turn === turn && opportunity &&
|
||||
current.leaderRevivalPromiseGuide === guide && current.leaderRevivalPromiseGuide.phase === phase &&
|
||||
current.leaderRevivalPromiseGuide.action === action && JSON.stringify(action) === signature && typeof event === 'string' &&
|
||||
liveLeaderRevivalPromises(current.leaderRevivalPromises, 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="leader-revival-promise-guide"
|
||||
disabled={!canAct()} onClick={() => { if (canAct()) props.send(action); }}>
|
||||
{action.type === 'charity' ? 'Claim Charity for your later leader Revival'
|
||||
: action.type === 'richeseBid' ? action.amount === 0
|
||||
? 'Submit zero in this Silent auction while preserving your leader Revival answers'
|
||||
: 'Pass this Richese bid while preserving your leader Revival answers'
|
||||
: action.type === 'passBid' ? 'Pass this bid while preserving your leader Revival answers'
|
||||
: action.type === 'decision' ? action.karama === true
|
||||
? 'Use the quoted Karama auction payment for your later leader Revival'
|
||||
: 'Pay the current auction price while preserving your leader Revival answers'
|
||||
: action.type === 'reviveLeader' ? `Revive ${leaderName(game, typeof action.leader === 'string' ? action.leader : '')} while honoring your answers`
|
||||
: action.type === 'reviveKwisatz' ? 'Revive Kwisatz Haderach while honoring your answers'
|
||||
: action.type === 'acceptLeaderRevival' ? 'Accept the current quoted leader revival while honoring your answers'
|
||||
: action.type === 'revive' ? `Revive ${typeof action.amount === 'number' ? action.amount : 'the suggested number of'} ${action.amount === 1 ? 'force' : 'forces'} to prepare your leader Revival`
|
||||
: phase === 2 ? 'Finish Charity while preserving your leader Revival answers'
|
||||
: 'Finish Revival while honoring your named-leader answers'}
|
||||
</Button>;
|
||||
}
|
||||
|
||||
export function LeaderRevivalPromises(props: LeaderRevivalPromiseProps) {
|
||||
const own = props.game.leaderRevivalPromises.filter(promise => promise.player === props.game.me);
|
||||
if (!own.length) return null;
|
||||
const live = liveLeaderRevivalPromises(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 leader Revival promises" id="leader-revival-promise-guidance">
|
||||
<h3>Your leader Revival promises</h3>
|
||||
<ul>{own.map(promise => <li key={promise.event}>
|
||||
Turn {promise.turn}: You answered {promise.answer ? 'Yes' : 'No'} to {leaderRevivalClaimText(promise, id => leaderName(props.game, id))}.
|
||||
{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 the named leader’s future actual ordinary native return while it remains possible.
|
||||
No forbids that named ordinary return until your Revival opportunity ends this turn.
|
||||
A request alone is not a return. Prior returns, Ghola, foreign leaders and special card returns do not count.
|
||||
</p>
|
||||
<p className="fine">
|
||||
Original revival cycles, costs, limits and turn order still apply.
|
||||
Preparation may return forces or complete an earlier payment without returning the named leader.
|
||||
A guide may accept an existing, current Tleilaxu quote; it does not arrange a new agreement or promise future consent.
|
||||
{' '}<a href="/rules#truthtrance-leader-revival">Leader Revival promise rules</a>.
|
||||
</p>
|
||||
{props.game.phase < 4 && <p>
|
||||
Your leader Revival opportunity is still ahead. Continue the current phase and preserve its required resources;
|
||||
Charity and auction preparation do not themselves fulfill a named-leader answer.
|
||||
</p>}
|
||||
{waiting && props.game.phase === 4 && <p>Your leader Revival opportunity is still ahead; this promise remains visible while the current actor finishes.</p>}
|
||||
<LeaderRevivalPromiseGuideButton {...props} />
|
||||
</section>;
|
||||
}
|
||||
@@ -1,4 +1,6 @@
|
||||
'use client';
|
||||
import { LeaderRevivalClaimFields, leaderRevivalClaimInputError } from './leader-revival-claim-fields';
|
||||
import type { LeaderRevivalClaim } from '@/game/leader-revival-promises';
|
||||
import { MovementClaimFields } from './movement-claim-fields';
|
||||
import { AllianceClaimFields, allianceClaimInputError } from './alliance-claim-fields';
|
||||
import type { AllianceClaim } from '@/game/alliance-promises';
|
||||
@@ -61,8 +63,14 @@ export function Truthtrance({
|
||||
g.players.find((p) => p.id !== me.id)!.id,
|
||||
);
|
||||
const [kind, setKind] = useState<
|
||||
'fact' | 'freeform' | 'battlePlan' | 'shipment' | 'mixedShipment' | 'movement' | 'traitorCall' | 'bid' | 'revival' | 'bribe' | 'alliance'
|
||||
'fact' | 'freeform' | 'battlePlan' | 'shipment' | 'mixedShipment' | 'movement' | 'traitorCall' | 'bid' | 'revival' | 'bribe' | 'alliance' | 'leaderRevival'
|
||||
>('fact');
|
||||
const [revivalLeader, setRevivalLeader] = useState(g.allLeaders.find(leader =>
|
||||
leader.faction === g.players.find(player => player.id === target)?.faction)?.id ?? '');
|
||||
const leaderRevivalClaim: LeaderRevivalClaim = { turn: g.turn, leader: revivalLeader };
|
||||
const leaderRevivalFaction = g.players.find(player => player.id === target)?.faction;
|
||||
const leaderRevivalChoices = [...g.allLeaders.filter(leader => leader.faction === leaderRevivalFaction),
|
||||
...(g.advanced && leaderRevivalFaction === 'atreides' ? [{ id: 'kwisatz', name: 'Kwisatz Haderach' }] : [])];
|
||||
const [alliancePartner, setAlliancePartner] = useState(g.me);
|
||||
const allianceClaim: AllianceClaim = { turn: g.turn, partner: alliancePartner };
|
||||
const alliancePartners = g.players.filter(player => player.id !== target);
|
||||
@@ -113,6 +121,7 @@ export function Truthtrance({
|
||||
}
|
||||
: kind === 'traitorCall' ? { kind, target, battle: g.battle?.event ?? '', territory: g.battle?.territory ?? '' }
|
||||
: kind === 'movement' ? { kind, target, ...movement }
|
||||
: kind === 'leaderRevival' ? { kind, target, ...leaderRevivalClaim }
|
||||
: kind === 'alliance' ? { kind, target, ...allianceClaim }
|
||||
: kind === 'bribe' ? { kind, target, ...bribeClaim }
|
||||
: kind === 'revival' ? { kind, target, ...revivalClaim }
|
||||
@@ -224,6 +233,13 @@ export function Truthtrance({
|
||||
</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>
|
||||
{(g.truthLeaderRevivalAnswers ?? []).map(answer => <div key={answer}>
|
||||
{button(`Answer ${answer === 'yes' ? 'Yes' : answer === 'no' ? 'No' : 'unknown'}`, { type: 'truthAnswer', answer })}
|
||||
</div>)}
|
||||
</>
|
||||
) : w.question!.kind === 'alliance' ? (
|
||||
<>
|
||||
<p className="notice">This answer concerns actual membership when this Nexus closes. An offer alone does not form an alliance. Yes remains binding after formation; No requires ending without this named partner. Consent that is not yet available may be unknown, letting the holder ask again or save the card.</p>
|
||||
@@ -371,6 +387,7 @@ export function Truthtrance({
|
||||
<option value="movement">Bind ground movement this turn</option>
|
||||
<option value="alliance">Bind alliance membership at this Nexus close</option>
|
||||
<option value="bribe">Bind ordinary bribe payments this turn</option>
|
||||
<option value="leaderRevival">Bind a named leader revival this turn</option>
|
||||
<option value="revival">Bind ordinary force revivals this turn</option>
|
||||
<option value="bid">Bind a bid in this ordinary lot</option>
|
||||
<option value="traitorCall">Bind this battle’s Traitor declaration</option>
|
||||
@@ -388,7 +405,16 @@ export function Truthtrance({
|
||||
)}
|
||||
</select>
|
||||
</label>
|
||||
{kind === 'alliance' ? (
|
||||
{kind === 'leaderRevival' ? (
|
||||
<>
|
||||
<LeaderRevivalClaimFields value={leaderRevivalClaim} leaders={leaderRevivalChoices}
|
||||
disabled={busy || !g.truthLeaderRevivalTargets.includes(target)}
|
||||
onChange={claim => setRevivalLeader(claim.leader)} />
|
||||
<p className="fine">Only future actual native leader returns count. Keep the original cost, physical eligibility, revival cycle and allowance. Known owned preparation may precede the return; no future foreign agreement or price is assumed.</p>
|
||||
{!g.truthLeaderRevivalTargets.includes(target) && <p className="notice">Choose a supported participant with a remaining ordinary Revival opportunity this turn. Optional compositions remain unfinished.</p>}
|
||||
<p className="notice" aria-label="Public named leader revival question preview">{truthQuestionText(question, leaderName, locationName, name)}</p>
|
||||
</>
|
||||
) : kind === 'alliance' ? (
|
||||
<>
|
||||
<AllianceClaimFields value={allianceClaim} partners={alliancePartners} disabled={busy || !g.truthAllianceTargets.includes(target)}
|
||||
onChange={claim => setAlliancePartner(claim.partner)} />
|
||||
@@ -518,6 +544,7 @@ export function Truthtrance({
|
||||
'Ask publicly',
|
||||
{ type: 'truthAsk', question },
|
||||
(kind === 'movement' && (!validMovementClaim(movement) || !g.truthMovementTargets.includes(target))) ||
|
||||
(kind === 'leaderRevival' && (!!leaderRevivalClaimInputError(leaderRevivalClaim, leaderRevivalChoices) || !g.truthLeaderRevivalTargets.includes(target))) ||
|
||||
(kind === 'alliance' && (!!allianceClaimInputError(allianceClaim, alliancePartners) || !g.truthAllianceTargets.includes(target))) ||
|
||||
(kind === 'bribe' && (!!bribeClaimInputError(bribeClaim, bribeRecipients) || !g.truthBribeTargets.includes(target))) ||
|
||||
(kind === 'revival' && (!!revivalClaimInputError(revivalClaim) || !g.truthRevivalTargets.includes(target))) ||
|
||||
@@ -542,7 +569,7 @@ export function TruthHistory({ game: g }: { game: GameView }) {
|
||||
<details className="notice truthtrance-history">
|
||||
<summary>Truthtrance answers · Turn {g.turn}</summary>
|
||||
<p className="fine">
|
||||
Supported battle-plan, Nexus-alliance, bid, revival, bribe, movement and reserve-shipment
|
||||
Supported battle-plan, Nexus-alliance, bid, force/leader revival, bribe, movement and reserve-shipment
|
||||
answers are enforced. Current-turn freeform promises remain binding when possible,
|
||||
but their automatic enforcement is unfinished.
|
||||
</p>
|
||||
|
||||
Reference in new issue
Block a user