{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.
diff --git a/components/sapho-arrival-order.tsx b/components/sapho-arrival-order.tsx new file mode 100644 index 00000000..8659295a --- /dev/null +++ b/components/sapho-arrival-order.tsx @@ -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 ; +} + +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 ; +} + +/** 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); + returnBoth reactions follow the same entry. Before either begins, their owners may continue or use an available order change.
+Current order: {view.order.map((player, index) => + {index > 0 && ' → '}{name(player)})}.
+{view.passed.length} of {view.order.length} players have finished this choice.
+This ordering procedure is a provisional interpretation.
+ {eligible && !passed ? <> +{eligible ? 'You have finished this choice.' : 'Waiting for the reaction owners.'}
} +