Bind current Traitor declarations with Truthtrance
This commit is contained in:
1 parent
724eabf99c
commit
86608f7ea9
20 files changed
+1362
-35
No files matched your search
@@ -1,4 +1,5 @@
|
||||
'use client';
|
||||
import { TraitorDecisionPromises } from './traitor-decision-promises';
|
||||
import { MovementPromises } from './movement-promises';
|
||||
import { integerInputValue } from '../lib/integer-input';
|
||||
import { EcazSetup } from './ecaz-setup';
|
||||
@@ -362,7 +363,7 @@ export function GameTable({
|
||||
: traitorBattle.attacker
|
||||
]
|
||||
: undefined;
|
||||
const canRevealTraitor = !!(
|
||||
const canRevealTraitor = traitorBattle?.traitorCallChoices?.includes(true) ?? !!(
|
||||
traitorBattle?.traitorVoters.includes(me.id) &&
|
||||
traitorPlan &&
|
||||
!traitorPlan.kwisatz &&
|
||||
@@ -372,7 +373,9 @@ export function GameTable({
|
||||
traitorBattle.cards.find((card) => card.id === traitorPlan.leader),
|
||||
)
|
||||
);
|
||||
const noTraitorReason = traitorPlan?.kwisatz
|
||||
const canDeclineTraitor = traitorBattle?.traitorCallChoices?.includes(false) ?? true;
|
||||
const noTraitorReason = traitorBattle?.ownTraitorPromises?.some(promise => !promise.answer && !promise.fulfilled && !promise.released)
|
||||
? 'Your accepted Truthtrance answer requires declining this Traitor declaration.' : traitorPlan?.kwisatz
|
||||
? 'Kwisatz Haderach prevents this leader from turning traitor.'
|
||||
: 'You do not hold the opposing leader as a traitor.';
|
||||
const [guildSecretEvent, setGuildSecretEvent] = useState('');
|
||||
@@ -5307,6 +5310,7 @@ export function GameTable({
|
||||
)}
|
||||
{g.battle.traitorVoters.includes(me.id) && !g.battle.traitorSubmitted.includes(me.id) && (
|
||||
<>
|
||||
<TraitorDecisionPromises game={g} />
|
||||
{actionButton(
|
||||
'Reveal traitor',
|
||||
{
|
||||
@@ -5321,7 +5325,7 @@ export function GameTable({
|
||||
{actionButton('No traitor — resolve', {
|
||||
type: 'traitorCall',
|
||||
call: false,
|
||||
})}
|
||||
}, !canDeclineTraitor)}
|
||||
</>
|
||||
)}
|
||||
</>
|
||||
@@ -5719,6 +5723,7 @@ export function GameTable({
|
||||
</p>
|
||||
{!g.battle.traitorSubmitted.includes(me.id) ? (
|
||||
<>
|
||||
<TraitorDecisionPromises game={g} />
|
||||
{actionButton(
|
||||
'Reveal ally’s opponent as traitor',
|
||||
{
|
||||
@@ -5733,7 +5738,7 @@ export function GameTable({
|
||||
{actionButton('Decline traitor support', {
|
||||
type: 'traitorCall',
|
||||
call: false,
|
||||
})}
|
||||
}, !canDeclineTraitor)}
|
||||
</>
|
||||
) : (
|
||||
<p className="muted">
|
||||
|
||||
@@ -165,7 +165,7 @@ function ReturnCards({ game, act, busy }: Props) {
|
||||
className="shrink-0"
|
||||
checked={checked}
|
||||
disabled={
|
||||
blocked || (!checked && selected.length >= pending.count)
|
||||
blocked || (!checked && (!!choice.blocked || selected.length >= pending.count))
|
||||
}
|
||||
onChange={() =>
|
||||
setSelected((old) =>
|
||||
@@ -184,6 +184,7 @@ function ReturnCards({ game, act, busy }: Props) {
|
||||
</span>
|
||||
</span>
|
||||
</label>
|
||||
{choice.blocked && <p className="notice">{choice.blocked}</p>}
|
||||
<LeaderInspector
|
||||
identity={identity}
|
||||
kind={dancers ? 'faceDancer' : 'traitor'}
|
||||
|
||||
@@ -0,0 +1,22 @@
|
||||
'use client';
|
||||
import type { Action, GameView } from '@/game/engine';
|
||||
import { Button } from './ui/button';
|
||||
|
||||
export function TraitorDecisionAnswers({ game, act, busy }: { game: GameView; act: (action: Action) => void; busy: boolean }) {
|
||||
if (game.truthtrance?.stage !== 'answer' || game.truthtrance.question?.kind !== 'traitorCall' ||
|
||||
game.truthtrance.question.target !== game.me) return null;
|
||||
return <section aria-label="Your Truthtrance Traitor answer">
|
||||
<p className="notice">Yes binds Reveal for this original Traitor decision; No binds decline. This concerns your declaration, so a later Karama or betrayal response can still prevent its effect.</p>
|
||||
{(game.truthTraitorAnswers ?? []).map(answer => <Button key={answer} className="game-action min-h-11" disabled={busy}
|
||||
onClick={() => { if (!busy) act({ type: 'truthAnswer', answer }); }}>Answer {answer === 'yes' ? 'Yes' : 'No'}</Button>)}
|
||||
</section>;
|
||||
}
|
||||
|
||||
export function TraitorDecisionPromises({ game }: { game: GameView }) {
|
||||
const promises = game.battle?.ownTraitorPromises?.filter(promise => !promise.fulfilled && !promise.released) ?? [];
|
||||
if (!promises.length) return null;
|
||||
return <section className="notice" aria-label="Your Traitor declaration promise">
|
||||
<p>Your Truthtrance answer requires {promises[0].answer ? 'Reveal traitor' : 'No traitor'} for this battle.</p>
|
||||
<p className="fine">The original declaration fulfills your answer. Later Karama or betrayal prevention can still change its effect.</p>
|
||||
</section>;
|
||||
}
|
||||
@@ -1,5 +1,6 @@
|
||||
'use client';
|
||||
import { MovementClaimFields } from './movement-claim-fields';
|
||||
import { TraitorDecisionAnswers } from './traitor-decision-promises';
|
||||
import { validMovementClaim, type MovementClaim } from '@/game/movement-promises';
|
||||
import { ShipmentClaimFields, invalidShipmentClause, type ShipmentClauseInput } from './shipment-claim-fields';
|
||||
import { BattleClaimFields } from './battle-promises';
|
||||
@@ -52,7 +53,7 @@ export function Truthtrance({
|
||||
g.players.find((p) => p.id !== me.id)!.id,
|
||||
);
|
||||
const [kind, setKind] = useState<
|
||||
'fact' | 'freeform' | 'battlePlan' | 'shipment' | 'mixedShipment' | 'movement'
|
||||
'fact' | 'freeform' | 'battlePlan' | 'shipment' | 'mixedShipment' | 'movement' | 'traitorCall'
|
||||
>('fact');
|
||||
const [movement, setMovement] = useState<MovementClaim>({ territory: 'carthag', minimum: 6 });
|
||||
const [shipmentJoin, setShipmentJoin] = useState<'single' | 'and' | 'or'>('single');
|
||||
@@ -108,6 +109,7 @@ export function Truthtrance({
|
||||
? planClauses[0]
|
||||
: { kind: join, terms: planClauses },
|
||||
}
|
||||
: kind === 'traitorCall' ? { kind, target, battle: g.battle?.event ?? '', territory: g.battle?.territory ?? '' }
|
||||
: kind === 'movement' ? { kind, target, ...movement }
|
||||
: kind === 'shipment'
|
||||
? {
|
||||
@@ -223,6 +225,8 @@ export function Truthtrance({
|
||||
<div key={answer}>{button(answer === 'unknown' ? 'I don’t know yet' : `Answer ${answer === 'yes' ? 'Yes' : 'No'}`, { type: 'truthAnswer', answer })}</div>
|
||||
))}
|
||||
</>
|
||||
) : w.question!.kind === 'traitorCall' ? (
|
||||
<TraitorDecisionAnswers game={g} act={act} busy={busy} />
|
||||
) : w.question!.kind === 'battlePlan' ? (
|
||||
<>
|
||||
<p className="notice">
|
||||
@@ -331,6 +335,7 @@ export function Truthtrance({
|
||||
Verified current facts
|
||||
</option>
|
||||
<option value="movement">Bind ground movement this turn</option>
|
||||
<option value="traitorCall">Bind this battle’s Traitor declaration</option>
|
||||
<option value="shipment">
|
||||
Bind a shipment from reserves
|
||||
</option>
|
||||
@@ -345,7 +350,13 @@ export function Truthtrance({
|
||||
)}
|
||||
</select>
|
||||
</label>
|
||||
{kind === 'movement' ? (
|
||||
{kind === 'traitorCall' ? (
|
||||
<>
|
||||
<p className="notice">Choose a still-unsubmitted voter in the current revealed battle. A definite answer binds that player’s original declaration, without reopening any sealed vote or promising that a later response will allow it.</p>
|
||||
{!g.truthTraitorTargets.includes(target) && <p>Reveal both plans and choose a voter who has not submitted their Traitor decision.</p>}
|
||||
{g.battle?.revealed && <p className="notice" aria-label="Public Traitor question preview">{truthQuestionText(question, leaderName, locationName)}</p>}
|
||||
</>
|
||||
) : kind === 'movement' ? (
|
||||
<>
|
||||
<MovementClaimFields id={`${factId}-movement`} value={movement} onChange={setMovement} />
|
||||
<p className="fine">One ground movement of the chosen physical minimum, from now until the respondent’s Movement turn ends. Shipment, HMS relocation and Ambassador relocation do not count. Owned Hajr, Ornithopter, native Kulon and paid Box recovery of a known discard can prepare a later move. No stays binding after earlier nonmatching moves.</p>
|
||||
@@ -496,6 +507,7 @@ export function Truthtrance({
|
||||
'Ask publicly',
|
||||
{ type: 'truthAsk', question },
|
||||
(kind === 'movement' && (!validMovementClaim(movement) || !g.truthMovementTargets.includes(target))) ||
|
||||
(kind === 'traitorCall' && !g.truthTraitorTargets.includes(target)) ||
|
||||
(kind === 'shipment' &&
|
||||
(invalidShipment || !shipmentAvailable)) ||
|
||||
(kind === 'mixedShipment' &&
|
||||
|
||||
Reference in new issue
Block a user