Support grouped Truthtrance revival commitments
This commit is contained in:
1 parent
a76effb932
commit
f5ae61f382
20 files changed
+1730
-30
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 { RevivalExpressionPromises } from './revival-expression-promises';
|
||||
import { LeaderRevivalPromises } from './leader-revival-promises';
|
||||
import { BidPromises } from './bid-promises';
|
||||
import { HomeworldShipment } from './homeworld-shipment';
|
||||
@@ -2037,6 +2038,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' && <RevivalExpressionPromises 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} />}
|
||||
|
||||
@@ -0,0 +1,132 @@
|
||||
'use client';
|
||||
|
||||
import type { ReactElement } from 'react';
|
||||
import {
|
||||
parseRevivalExpression, REVIVAL_EXPRESSION_MAX_DEPTH, REVIVAL_EXPRESSION_MAX_LEAVES,
|
||||
type RevivalExpression,
|
||||
} from '@/game/revival-expression-promises';
|
||||
import { Button } from './ui/button';
|
||||
import { Input } from './ui/input';
|
||||
|
||||
type LeaderChoice = { id: string; name: string };
|
||||
const newLeaf = (): RevivalExpression => ({ kind: 'forces', counter: 'total', minimum: 1 });
|
||||
export function revivalExpressionInputError(value: unknown): string | null {
|
||||
try { parseRevivalExpression(value); return null; }
|
||||
catch (error) { return error instanceof Error ? error.message : 'Choose a valid ordinary Revival expression.'; }
|
||||
}
|
||||
|
||||
// Numeric drafts remain visible and repairable. Only structural controls use
|
||||
// this detached placeholder tree; the user's original numbers are retained.
|
||||
const structuralDraft = (node: RevivalExpression): RevivalExpression => node.kind === 'and' || node.kind === 'or'
|
||||
? { ...node, terms: node.terms.map(structuralDraft) }
|
||||
: node.kind === 'forces' ? { ...node, minimum: 1 } : node;
|
||||
const inputSize = (node: RevivalExpression): { leaves: number; depth: number } => {
|
||||
if (node.kind !== 'and' && node.kind !== 'or') return { leaves: 1, depth: 0 };
|
||||
const sizes = node.terms.map(inputSize);
|
||||
return { leaves: sizes.reduce((sum, child) => sum + child.leaves, 0), depth: 1 + Math.max(0, ...sizes.map(child => child.depth)) };
|
||||
};
|
||||
|
||||
/** Hook-free controlled tree, including invalid numeric drafts. Eligibility
|
||||
* and the whole-expression answer remain authoritative on the server. */
|
||||
export function RevivalExpressionFields({ value, leaders, disabled, onChange }: {
|
||||
value: RevivalExpression;
|
||||
leaders: readonly LeaderChoice[];
|
||||
disabled: boolean;
|
||||
onChange: (expression: RevivalExpression) => void;
|
||||
}) {
|
||||
const size = inputSize(value), error = revivalExpressionInputError(value);
|
||||
const commit = (next: RevivalExpression) => {
|
||||
if (!disabled && !revivalExpressionInputError(structuralDraft(next))) onChange(next);
|
||||
};
|
||||
const render = (node: RevivalExpression, path: string, replace: (next: RevivalExpression) => RevivalExpression,
|
||||
remove?: () => RevivalExpression): ReactElement => {
|
||||
const group = node.kind === 'and' || node.kind === 'or';
|
||||
const id = `truth-revival-expression-${path}`;
|
||||
const allowed = (next: RevivalExpression) => !disabled && !revivalExpressionInputError(structuralDraft(replace(next)));
|
||||
const change = (next: RevivalExpression) => commit(replace(next));
|
||||
const wrap = (kind: 'and' | 'or'): RevivalExpression => ({ kind, terms: [node, newLeaf()] });
|
||||
return <fieldset key={path} data-revival-expression-path={path} className="grid min-w-0 gap-2 p-2">
|
||||
<legend>{group ? `Revival group ${path} · ${node.kind.toUpperCase()}` : `Revival condition ${path}`}</legend>
|
||||
{group ? <>
|
||||
<label htmlFor={`${id}-meaning`}>Revival group {path} meaning</label>
|
||||
<select id={`${id}-meaning`} value={node.kind} disabled={disabled} onChange={event => {
|
||||
const kind = event.currentTarget.value;
|
||||
if (kind === 'and' || kind === 'or') change({ ...node, kind });
|
||||
}}>
|
||||
<option value="and">All conditions · AND</option>
|
||||
<option value="or">At least one condition · OR</option>
|
||||
</select>
|
||||
{node.terms.map((term, index) => render(term, `${path}.${index + 1}`,
|
||||
next => replace({ ...node, terms: node.terms.map((old, i) => i === index ? next : old) }),
|
||||
() => {
|
||||
const terms = node.terms.filter((_, i) => i !== index);
|
||||
return replace(terms.length === 1 ? terms[0] : { ...node, terms });
|
||||
}))}
|
||||
<div className="flex flex-wrap gap-2">
|
||||
<Button type="button" data-revival-expression-control={`add-${path}`}
|
||||
disabled={!allowed({ ...node, terms: [...node.terms, newLeaf()] })}
|
||||
onClick={() => change({ ...node, terms: [...node.terms, newLeaf()] })}>Add Revival condition</Button>
|
||||
{(['and', 'or'] as const).map(kind => {
|
||||
const next: RevivalExpression = { ...node, terms: [...node.terms, { kind, terms: [newLeaf(), newLeaf()] }] };
|
||||
return <Button key={kind} type="button" data-revival-expression-control={`nested-${kind}-${path}`}
|
||||
disabled={!allowed(next)} onClick={() => change(next)}>Add nested {kind.toUpperCase()} group</Button>;
|
||||
})}
|
||||
</div>
|
||||
</> : <>
|
||||
<label htmlFor={`${id}-kind`}>Revival condition type</label>
|
||||
<select id={`${id}-kind`} value={node.kind} disabled={disabled} onChange={event => {
|
||||
if (event.currentTarget.value === 'forces') change(newLeaf());
|
||||
else if (event.currentTarget.value === 'leader' && leaders.length) change({ kind: 'leader', leader: leaders[0].id });
|
||||
}}>
|
||||
<option value="forces">Cumulative physical forces</option>
|
||||
<option value="leader" disabled={!leaders.length}>Named ordinary leader</option>
|
||||
</select>
|
||||
{node.kind === 'forces' ? <>
|
||||
<label htmlFor={`${id}-counter`}>Physical force counters</label>
|
||||
<select id={`${id}-counter`} value={node.counter} disabled={disabled} onChange={event => {
|
||||
const counter = event.currentTarget.value;
|
||||
if (counter === 'total' || counter === 'normal' || counter === 'elite') change({ ...node, counter });
|
||||
}}>
|
||||
<option value="total">All physical forces</option>
|
||||
<option value="normal">Normal forces</option>
|
||||
<option value="elite">Elite forces</option>
|
||||
</select>
|
||||
<label htmlFor={`${id}-minimum`}>Minimum future ordinary returns</label>
|
||||
<Input id={`${id}-minimum`} type="number" min={1} max={20} step={1} required disabled={disabled}
|
||||
value={Number.isNaN(node.minimum) ? '' : node.minimum}
|
||||
aria-invalid={!!revivalExpressionInputError(node)} onChange={event => change({ ...node, minimum: event.currentTarget.valueAsNumber })} />
|
||||
</> : node.kind === 'leader' ? <>
|
||||
<label htmlFor={`${id}-leader`}>Named leader</label>
|
||||
<select id={`${id}-leader`} value={leaders.some(leader => leader.id === node.leader) ? node.leader : ''}
|
||||
disabled={disabled} onChange={event => {
|
||||
const leader = event.currentTarget.value;
|
||||
if (leaders.some(choice => choice.id === leader)) change({ ...node, leader });
|
||||
}}>
|
||||
<option value="">Choose a named leader</option>
|
||||
{leaders.map(leader => <option key={leader.id} value={leader.id}>{leader.name}</option>)}
|
||||
</select>
|
||||
</> : null}
|
||||
</>}
|
||||
<div className="flex flex-wrap gap-2">
|
||||
{(['and', 'or'] as const).map(kind => <Button key={kind} type="button" data-revival-expression-control={`${kind}-${path}`}
|
||||
disabled={!allowed(wrap(kind))} onClick={() => change(wrap(kind))}>Group with {kind.toUpperCase()}</Button>)}
|
||||
{remove && <Button type="button" data-revival-expression-control={`remove-${path}`} disabled={disabled}
|
||||
onClick={() => commit(remove())}>Remove {group ? 'group' : 'condition'}</Button>}
|
||||
</div>
|
||||
</fieldset>;
|
||||
};
|
||||
return <fieldset disabled={disabled} data-testid="revival-expression"
|
||||
className="grid min-w-0 gap-2 [&_select]:min-w-0 [&_select]:max-w-full [&_button]:max-w-full [&_button]:min-h-11 [&_button]:h-auto [&_button]:py-2 [&_button]:whitespace-normal">
|
||||
<legend>Ordinary Revival expression</legend>
|
||||
<p className="fine">
|
||||
AND requires every condition; OR requires at least one. No makes the whole expression false,
|
||||
not necessarily every condition. Legal alternatives remain available; the question does not pin a fixed branch.
|
||||
Count only future ordinary native force and named-leader returns after the answer this turn.
|
||||
</p>
|
||||
<p className="fine">App input limits: {REVIVAL_EXPRESSION_MAX_LEAVES} conditions and {REVIVAL_EXPRESSION_MAX_DEPTH} levels of grouping.
|
||||
Groups do not count as conditions. These are input limits, not additional printed rules.</p>
|
||||
<p className="fine">{size.leaves} / {REVIVAL_EXPRESSION_MAX_LEAVES} conditions · {size.depth} / {REVIVAL_EXPRESSION_MAX_DEPTH} levels of grouping</p>
|
||||
{error && <p role="alert">{error}</p>}
|
||||
{render(value, '1', next => next)}
|
||||
</fieldset>;
|
||||
}
|
||||
@@ -0,0 +1,97 @@
|
||||
'use client';
|
||||
|
||||
import type { Action, GameView } from '@/game/engine';
|
||||
import { liveRevivalExpressionPromises, revivalExpressionPromiseModeSupported, revivalExpressionText } from '@/game/revival-expression-promises';
|
||||
import { Button } from './ui/button';
|
||||
|
||||
type RevivalExpressionPromiseProps = { 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';
|
||||
|
||||
/** Preserve the authoritative whole-expression preparation or return quote.
|
||||
* A guide suggests a legal next action without committing to a fixed branch. */
|
||||
export function RevivalExpressionPromiseGuideButton(props: RevivalExpressionPromiseProps) {
|
||||
const game = props.game, guide = game.revivalExpressionPromiseGuide;
|
||||
if (!guide) return null;
|
||||
const actor = game.me, turn = game.turn, phase = guide.phase, order = game.revivalOrder;
|
||||
const action = guide.action, event = action.revivalExpressionPromiseEvent, 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 === 'revive' || action.type === 'reviveLeader' || action.type === 'reviveKwisatz' || acceptedQuote || 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 && revivalExpressionPromiseModeSupported(current) &&
|
||||
current.status === 'playing' && current.phase === phase && current.me === actor && current.turn === turn && opportunity &&
|
||||
current.revivalExpressionPromiseGuide === guide && current.revivalExpressionPromiseGuide.phase === phase &&
|
||||
current.revivalExpressionPromiseGuide.action === action && JSON.stringify(action) === signature && typeof event === 'string' &&
|
||||
liveRevivalExpressionPromises(current.revivalExpressionPromises, 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-expression-promise-guide"
|
||||
disabled={!canAct()} onClick={() => { if (canAct()) props.send(action); }}>
|
||||
{action.type === 'charity' ? 'Claim Charity while preserving your Revival expression answers'
|
||||
: action.type === 'richeseBid' ? action.amount === 0
|
||||
? 'Submit zero in this Silent auction while preserving your Revival expression answers'
|
||||
: 'Pass this Richese bid while preserving your Revival expression answers'
|
||||
: action.type === 'passBid' ? 'Pass this bid while preserving your Revival expression answers'
|
||||
: action.type === 'decision' ? action.karama === true
|
||||
? 'Use the quoted Karama auction payment while preserving your Revival expression answers'
|
||||
: 'Pay the current auction price while preserving your Revival expression answers'
|
||||
: action.type === 'reviveLeader' ? `Revive ${leaderName(game, typeof action.leader === 'string' ? action.leader : '')} while honoring the whole expression`
|
||||
: action.type === 'reviveKwisatz' ? 'Revive Kwisatz Haderach while honoring the whole expression'
|
||||
: action.type === 'acceptLeaderRevival' ? 'Accept the current quoted leader revival while honoring the whole expression'
|
||||
: action.type === 'revive' ? `Revive ${typeof action.amount === 'number' ? action.amount : 'the suggested number of'} ${action.amount === 1 ? 'force' : 'forces'} while honoring the whole expression`
|
||||
: phase === 2 ? 'Finish Charity while preserving your Revival expression answers'
|
||||
: 'Finish Revival while honoring the whole expression'}
|
||||
</Button>;
|
||||
}
|
||||
|
||||
export function RevivalExpressionPromises(props: RevivalExpressionPromiseProps) {
|
||||
const own = props.game.revivalExpressionPromises.filter(promise => promise.player === props.game.me);
|
||||
if (!own.length) return null;
|
||||
const live = liveRevivalExpressionPromises(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 expression promises" id="revival-expression-promise-guidance">
|
||||
<h3>Your Revival expression promises</h3>
|
||||
<ul>{own.map(promise => <li key={promise.event}>
|
||||
Turn {promise.turn}: You answered {promise.answer ? 'Yes' : 'No'} to {revivalExpressionText(promise.claim, id => leaderName(props.game, id))}.
|
||||
{' '}Future ordinary progress: {promise.progress.total} physical forces ({promise.progress.normal} normal, {promise.progress.elite} elite);
|
||||
{' '}returned leaders: {promise.progress.leaders.length ? promise.progress.leaders.map(id => leaderName(props.game, id)).join(', ') : 'none'}.
|
||||
{promise.released ? ' Released: this commitment is no longer possible.'
|
||||
: promise.fulfilled ? ' Fulfilled: this commitment is complete.' : ' This whole answer is binding for this turn.'}
|
||||
</li>)}</ul>
|
||||
<p>
|
||||
AND requires every condition; OR requires at least one. No negates the whole expression, not each condition separately.
|
||||
Legal alternatives remain available; no fixed branch is selected for you.
|
||||
Only future actual ordinary native force and named-leader returns after the answer count.
|
||||
Prior returns, requests alone, Ghola, foreign leaders and special card returns do not count.
|
||||
</p>
|
||||
<p className="fine">
|
||||
Physical force progress is cumulative across smaller returns; each normal or elite counter counts once.
|
||||
Original costs, revival cycles, limits and turn order still apply.
|
||||
A guide may accept an existing current Tleilaxu quote; it does not arrange new consent.
|
||||
{' '}<a href="/rules#truthtrance-revival-expression">Revival expression rules</a>.
|
||||
</p>
|
||||
{waiting && <p>Your Revival opportunity is still ahead; {props.game.phase < 4
|
||||
? 'continue the current phase and preserve the resources for your whole answer.'
|
||||
: 'this promise remains visible while the current actor finishes.'}</p>}
|
||||
<RevivalExpressionPromiseGuideButton {...props} />
|
||||
</section>;
|
||||
}
|
||||
@@ -1,4 +1,6 @@
|
||||
'use client';
|
||||
import { RevivalExpressionFields, revivalExpressionInputError } from './revival-expression-fields';
|
||||
import type { RevivalExpression } from '@/game/revival-expression-promises';
|
||||
import { LeaderRevivalClaimFields, leaderRevivalClaimInputError } from './leader-revival-claim-fields';
|
||||
import type { LeaderRevivalClaim } from '@/game/leader-revival-promises';
|
||||
import { MovementClaimFields } from './movement-claim-fields';
|
||||
@@ -63,7 +65,7 @@ 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' | 'leaderRevival'
|
||||
'fact' | 'freeform' | 'battlePlan' | 'shipment' | 'mixedShipment' | 'movement' | 'traitorCall' | 'bid' | 'revival' | 'bribe' | 'alliance' | 'leaderRevival' | 'revivalExpression'
|
||||
>('fact');
|
||||
const [revivalLeader, setRevivalLeader] = useState(g.allLeaders.find(leader =>
|
||||
leader.faction === g.players.find(player => player.id === target)?.faction)?.id ?? '');
|
||||
@@ -71,6 +73,13 @@ export function Truthtrance({
|
||||
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 [revivalExpression, setRevivalExpression] = useState<RevivalExpression>({ kind: 'and', terms: [
|
||||
{ kind: 'forces', counter: 'total', minimum: 1 }, { kind: 'leader', leader: revivalLeader },
|
||||
] });
|
||||
const revivalExpressionNamesValid = (node: RevivalExpression): boolean => node.kind === 'and' || node.kind === 'or'
|
||||
? node.terms.every(revivalExpressionNamesValid) : node.kind === 'forces' ||
|
||||
node.kind === 'leader' && leaderRevivalChoices.some(leader => leader.id === node.leader);
|
||||
const validRevivalExpressionNames = revivalExpressionNamesValid(revivalExpression);
|
||||
const [alliancePartner, setAlliancePartner] = useState(g.me);
|
||||
const allianceClaim: AllianceClaim = { turn: g.turn, partner: alliancePartner };
|
||||
const alliancePartners = g.players.filter(player => player.id !== target);
|
||||
@@ -121,6 +130,7 @@ export function Truthtrance({
|
||||
}
|
||||
: kind === 'traitorCall' ? { kind, target, battle: g.battle?.event ?? '', territory: g.battle?.territory ?? '' }
|
||||
: kind === 'movement' ? { kind, target, ...movement }
|
||||
: kind === 'revivalExpression' ? { kind, target, turn: g.turn, claim: revivalExpression }
|
||||
: kind === 'leaderRevival' ? { kind, target, ...leaderRevivalClaim }
|
||||
: kind === 'alliance' ? { kind, target, ...allianceClaim }
|
||||
: kind === 'bribe' ? { kind, target, ...bribeClaim }
|
||||
@@ -233,6 +243,13 @@ export function Truthtrance({
|
||||
</div>
|
||||
))}
|
||||
</>
|
||||
) : w.question!.kind === 'revivalExpression' ? (
|
||||
<>
|
||||
<p className="notice">Answer the whole AND/OR expression. Yes requires the complete statement to become true through actual future ordinary returns; No keeps the whole statement false. Legal alternatives remain available and no branch is fixed by this answer.</p>
|
||||
{(g.truthRevivalExpressionAnswers ?? []).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>
|
||||
@@ -387,6 +404,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="revivalExpression">Bind combined force and leader revivals</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>
|
||||
@@ -405,7 +423,15 @@ export function Truthtrance({
|
||||
)}
|
||||
</select>
|
||||
</label>
|
||||
{kind === 'leaderRevival' ? (
|
||||
{kind === 'revivalExpression' ? (
|
||||
<>
|
||||
<RevivalExpressionFields value={revivalExpression} leaders={leaderRevivalChoices}
|
||||
disabled={busy || !g.truthRevivalExpressionTargets.includes(target)} onChange={setRevivalExpression} />
|
||||
{!validRevivalExpressionNames && <p className="notice">Choose each named leader from the respondent’s public roster.</p>}
|
||||
{!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 === 'leaderRevival' ? (
|
||||
<>
|
||||
<LeaderRevivalClaimFields value={leaderRevivalClaim} leaders={leaderRevivalChoices}
|
||||
disabled={busy || !g.truthLeaderRevivalTargets.includes(target)}
|
||||
@@ -544,6 +570,7 @@ export function Truthtrance({
|
||||
'Ask publicly',
|
||||
{ type: 'truthAsk', question },
|
||||
(kind === 'movement' && (!validMovementClaim(movement) || !g.truthMovementTargets.includes(target))) ||
|
||||
(kind === 'revivalExpression' && (!!revivalExpressionInputError(revivalExpression) || !validRevivalExpressionNames || !g.truthRevivalExpressionTargets.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))) ||
|
||||
@@ -569,7 +596,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, force/leader revival, bribe, movement and reserve-shipment
|
||||
Supported battle-plan, Nexus-alliance, bid, force/leader and grouped 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