Support Sapho ordering for shared arrivals
Verify and publish checkpoint container / container (push) Canceled after 0s
Verify and publish checkpoint container / container (push) Canceled after 0s
This commit is contained in:
1 parent
b18a767470
commit
e457478f7d
18 files changed
+808
-23
No files matched your search
@@ -1,4 +1,5 @@
|
||||
'use client';
|
||||
import { SaphoArrivalOrder } from './sapho-arrival-order';
|
||||
import { RicheseBidPromisePanel } from './richese-bid-promise-panel';
|
||||
import { EarlyRicheseBidPromises } from './early-richese-bid-promises';
|
||||
import { TravelPromises } from './travel-promises';
|
||||
@@ -385,8 +386,8 @@ export function GameTable({
|
||||
const harkonnenBetrayalOwnsControls = !!g.nexusHarkonnenBetrayalReaction &&
|
||||
!g.response && !g.decision && !g.truthtrance && !g.phaseOpening &&
|
||||
!g.automaticContinuationPending && !g.nexusCards?.waiting.length && !g.nexusTraitors?.pending;
|
||||
const busy = reactionBusy || !!g.victoryContinuation || !!g.nexusFremenRideBetrayal || g.automaticContinuationPending || !!g.semutaReaction || !!g.kullReaction || !!g.richeseBetrayalReaction || !!g.guildBetrayalReaction || !!g.nexusIxianReplacement || ixianBetrayalOwnsControls || harkonnenBetrayalOwnsControls;
|
||||
const reactionOwnsControls = !!g.nexusFremenRideBetrayal || !!g.kullReaction || !!g.kullCounterEvent || !!g.richeseBetrayalReaction || !!g.guildBetrayalReaction || !!g.nexusIxianReplacement || ixianBetrayalOwnsControls || harkonnenBetrayalOwnsControls;
|
||||
const busy = reactionBusy || !!g.saphoArrivalOrder || !!g.victoryContinuation || !!g.nexusFremenRideBetrayal || g.automaticContinuationPending || !!g.semutaReaction || !!g.kullReaction || !!g.richeseBetrayalReaction || !!g.guildBetrayalReaction || !!g.nexusIxianReplacement || ixianBetrayalOwnsControls || harkonnenBetrayalOwnsControls;
|
||||
const reactionOwnsControls = !!g.saphoArrivalOrder || !!g.nexusFremenRideBetrayal || !!g.kullReaction || !!g.kullCounterEvent || !!g.richeseBetrayalReaction || !!g.guildBetrayalReaction || !!g.nexusIxianReplacement || ixianBetrayalOwnsControls || harkonnenBetrayalOwnsControls;
|
||||
const canSpendKarama = (card: Card, purpose: KullPurpose) =>
|
||||
!kullPurposeBlock(g, purpose) && canUseAsKaramaRole(g, me, card);
|
||||
const canActivateKarama = (card: Card) => canSpendKarama(card, kullSpecialPurpose(me.faction));
|
||||
@@ -1514,15 +1515,19 @@ export function GameTable({
|
||||
<EcazLoyaltyCard loyalty={g.ecazLoyalty} />
|
||||
<MoritaniTerrorSupply game={g} />
|
||||
<MoritaniExtortionNotice game={g} />
|
||||
{g.saphoArrivalOrder && <SaphoArrivalOrder view={g.saphoArrivalOrder} me={g.me}
|
||||
busy={transportBusy || !!g.roomControl?.paused || !!g.roomControl?.closed || !!me.autopilot}
|
||||
name={id => g.players.find(player => player.id === id)?.name ?? id} act={act} />}
|
||||
{g.arrivalOverlap && (
|
||||
<section className="notice" aria-label="Ambassador and Terror response order">
|
||||
<p>
|
||||
{g.players.find((player) => player.id === g.arrivalOverlap!.entrant)?.name ?? 'The entrant'}
|
||||
{' '}entered {territory(g.arrivalOverlap.territory).name}. Ecaz Ambassador
|
||||
and Moritani Terror resolve in this table’s provisional storm order:{' '}
|
||||
and Moritani Terror use this table’s provisional reaction order:{' '}
|
||||
{g.arrivalOverlap.first === 'ambassador' ? 'Ecaz, then Moritani' : 'Moritani, then Ecaz'}.
|
||||
{g.semutaReaction
|
||||
? ' The current fresh-discard response must finish before the next arrival decision.'
|
||||
: g.saphoArrivalOrder ? ' The reaction owners are choosing their order.'
|
||||
: <> {g.arrivalOverlap.active === 'ambassador' ? 'Ecaz' : 'Moritani'} decides now.</>}
|
||||
{' '}The original entry is not repeated.
|
||||
</p>
|
||||
|
||||
@@ -0,0 +1,75 @@
|
||||
'use client';
|
||||
|
||||
import type { Action } from '@/game/engine';
|
||||
import { Button } from './ui/button';
|
||||
|
||||
/** Public order and responses; choices contain only this viewer's legal cards. */
|
||||
export type SaphoArrivalOrderView = {
|
||||
event: string;
|
||||
revision: number;
|
||||
order: string[];
|
||||
passed: string[];
|
||||
choices: { card: string; mode: 'first' | 'last' }[];
|
||||
};
|
||||
export type SaphoArrivalOrderProps = {
|
||||
view: SaphoArrivalOrderView;
|
||||
me: string;
|
||||
busy: boolean;
|
||||
name: (player: string) => string;
|
||||
act: (action: Action) => void;
|
||||
};
|
||||
|
||||
function current(props: SaphoArrivalOrderProps, view: SaphoArrivalOrderView,
|
||||
event: string, revision: number): boolean {
|
||||
return !props.busy && props.view === view && view.event === event &&
|
||||
view.revision === revision && typeof event === 'string' && event.length > 0 &&
|
||||
Number.isSafeInteger(revision) && revision >= 0 &&
|
||||
view.order.includes(props.me) && !view.passed.includes(props.me);
|
||||
}
|
||||
|
||||
/** Both original reaction owners can continue without revealing their hands. */
|
||||
export function SaphoArrivalOrderPass(props: SaphoArrivalOrderProps) {
|
||||
const { view } = props, event = view.event, revision = view.revision;
|
||||
const canAct = () => current(props, view, event, revision);
|
||||
return <Button className="game-action min-h-11 whitespace-normal" data-testid="sapho-arrival-order-pass"
|
||||
disabled={!canAct()} onClick={() => { if (canAct()) props.act({ type: 'arrivalOrderPass', event, revision }); }}>
|
||||
Continue without changing reaction order
|
||||
</Button>;
|
||||
}
|
||||
|
||||
export function SaphoArrivalOrderChoiceButton(props: SaphoArrivalOrderProps & {
|
||||
choice: SaphoArrivalOrderView['choices'][number];
|
||||
}) {
|
||||
const { view, choice } = props, event = view.event, revision = view.revision;
|
||||
const card = choice.card, mode = choice.mode;
|
||||
const canAct = () => current(props, view, event, revision) &&
|
||||
typeof card === 'string' && card.length > 0 && (mode === 'first' || mode === 'last') &&
|
||||
choice.card === card && choice.mode === mode &&
|
||||
view.choices.some(item => item.card === card && item.mode === mode);
|
||||
return <Button className="game-action min-h-11 whitespace-normal" data-testid={`sapho-arrival-order-${mode}`}
|
||||
disabled={!canAct()} onClick={() => {
|
||||
if (canAct()) props.act({ type: 'saphoArrivalOrder', event, revision, card, mode });
|
||||
}}>
|
||||
Use Juice of Sapho to react {mode}
|
||||
</Button>;
|
||||
}
|
||||
|
||||
/** Do not infer eligibility from a hand: the server supplies the public window
|
||||
* and only the current viewer's privately quoted card choices. */
|
||||
export function SaphoArrivalOrder(props: SaphoArrivalOrderProps) {
|
||||
const { view, me, name } = props;
|
||||
const eligible = view.order.includes(me), passed = view.passed.includes(me);
|
||||
return <section className="notice space-y-3" aria-label="Ambassador and Terror reaction order">
|
||||
<h3>Ambassador and Terror reaction order</h3>
|
||||
<p>Both reactions follow the same entry. Before either begins, their owners may continue or use an available order change.</p>
|
||||
<p>Current order: {view.order.map((player, index) =>
|
||||
<span key={player}>{index > 0 && ' → '}{name(player)}</span>)}.</p>
|
||||
<p>{view.passed.length} of {view.order.length} players have finished this choice.</p>
|
||||
<p className="fine">This ordering procedure is a provisional interpretation.</p>
|
||||
{eligible && !passed ? <>
|
||||
<SaphoArrivalOrderPass {...props} />
|
||||
{view.choices.map(choice => <SaphoArrivalOrderChoiceButton {...props}
|
||||
key={`${choice.card}:${choice.mode}`} choice={choice} />)}
|
||||
</> : <p>{eligible ? 'You have finished this choice.' : 'Waiting for the reaction owners.'}</p>}
|
||||
</section>;
|
||||
}
|
||||
Reference in new issue
Block a user