Integrate Ixian Nexus Betrayal at original native attempts

This commit is contained in:
Quintine committed 2026-10-01 21:58:07 +10:00
1 parent 49c99de3e2
commit cdfe0e5370
25 files changed
+2824 -63

No files matched your search

+29 -3
View File
@@ -82,6 +82,7 @@ import { ChoamKull } from './choam-kull';
import { NexusRicheseBetrayal } from './nexus-richese-betrayal';
import { NexusGuildBetrayal } from './nexus-guild-betrayal';
import { NexusIxianReplacement } from './nexus-ixian-replacement';
import { NexusIxianBetrayal } from './nexus-ixian-betrayal';
import { OrnithopterMovement } from './ornithopter-movement';
import { DiscoveryOrnithopterMovement } from './discovery-flight-movement';
import { PlanetologistMovement } from './planetologist-movement';
@@ -306,8 +307,11 @@ export function GameTable({
);
const reactionBusy =
transportBusy || !!g.roomControl?.paused || !!g.roomControl?.closed || !!me.autopilot;
const busy = reactionBusy || g.automaticContinuationPending || !!g.semutaReaction || !!g.kullReaction || !!g.richeseBetrayalReaction || !!g.guildBetrayalReaction || !!g.nexusIxianReplacement;
const reactionOwnsControls = !!g.kullReaction || !!g.kullCounterEvent || !!g.richeseBetrayalReaction || !!g.guildBetrayalReaction || !!g.nexusIxianReplacement;
const ixianBetrayalOwnsControls = !!g.nexusIxianBetrayalReaction &&
!g.response && !g.decision && !g.truthtrance && !g.phaseOpening &&
!g.automaticContinuationPending && !g.nexusCards?.waiting.length && !g.nexusTraitors?.pending;
const busy = reactionBusy || g.automaticContinuationPending || !!g.semutaReaction || !!g.kullReaction || !!g.richeseBetrayalReaction || !!g.guildBetrayalReaction || !!g.nexusIxianReplacement || ixianBetrayalOwnsControls;
const reactionOwnsControls = !!g.kullReaction || !!g.kullCounterEvent || !!g.richeseBetrayalReaction || !!g.guildBetrayalReaction || !!g.nexusIxianReplacement || ixianBetrayalOwnsControls;
const canActivateKarama = (card: Card) =>
!g.karamaBlocked && canUseAsKaramaRole(g, me, card);
const traitorBattle = g.battle;
@@ -784,6 +788,15 @@ export function GameTable({
!botGroundMoveAllowed(g, me, key, boardLocation(selected, sector), 0),
);
const act = (a: Action) => {
// Control of one's own seat is independent of every gameplay response.
if (a.type === 'setAutopilot') { void send(a); return; }
const ixianReaction = g.nexusIxianBetrayalReaction;
if (ixianReaction && ixianBetrayalOwnsControls) {
if (reactionBusy || g.status !== 'playing' || !ixianReaction.canPass || ixianReaction.hasPassed ||
(a.type !== 'nexusIxianBetrayalPass' && a.type !== 'nexusIxianBetrayalUse') ||
a.event !== ixianReaction.event ||
(a.type === 'nexusIxianBetrayalUse' && (!ixianReaction.canUse || ixianReaction.blocked))) return;
}
const guildReaction = g.guildBetrayalReaction;
if (guildReaction) {
if (reactionBusy || g.status !== 'playing' || !guildReaction.canPass || guildReaction.hasPassed ||
@@ -1029,6 +1042,8 @@ export function GameTable({
faction(g.players.find((p) => p.id === id)!.faction).name,
)
.join(' + ') || 'No winner'
: ixianBetrayalOwnsControls
? 'Native Ixian advantage acknowledgement'
: g.guildBetrayalReaction
? 'Shipment payment acknowledgement'
: g.richeseBetrayalReaction
@@ -1093,6 +1108,15 @@ export function GameTable({
<a href="/rules?topic=nexus-ixian-replacement#nexus-ixian-replacement">Preview rules and limits</a>
</p>
)}
{g.nexusIxianBetrayalPreview && (
<p className="notice" role="status">
Ixian Nexus Betrayal development preview · native Ixian Bidding extra-card
inspection/draw and Advanced Technology exchange prevention. Native Ixians,
permitted classic/Tleilaxu seats, physical Ixian Treachery and Nexus only.
Native Karama responses finish first. Not complete Ixian or certified Nexus rules.{' '}
<a href="/rules?topic=nexus-ixian-betrayal#nexus-ixian-betrayal">Preview rules and limits</a>
</p>
)}
{g.status !== 'lobby' && (
<nav className="phase-track" aria-label="Turn phases">
{PHASES.map((p, i) => (
@@ -1125,7 +1149,7 @@ export function GameTable({
<SpiceCardInspector card={g.spicePeek} context="Private foresight" />
</section>
)}
<SeatAutopilot game={g} act={act} busy={transportBusy || !!g.guildBetrayalReaction} />
<SeatAutopilot game={g} act={act} busy={transportBusy} />
<DukeVidal game={g} />
<EcazLoyaltyCard loyalty={g.ecazLoyalty} />
<MoritaniTerrorSupply game={g} />
@@ -1739,6 +1763,8 @@ export function GameTable({
<ChoamKull game={g} act={act} busy={reactionBusy} />
) : g.nexusIxianReplacement ? (
<NexusIxianReplacement game={g} act={act} busy={reactionBusy} />
) : ixianBetrayalOwnsControls ? (
<NexusIxianBetrayal game={g} act={act} busy={reactionBusy} />
) : g.nexusCards?.waiting.length ? (
<p className="muted">The next phase begins when the remaining Nexus card choices are finished.</p>
) : g.truthtrance ? (
+60
View File
@@ -0,0 +1,60 @@
'use client';
import type { Action, GameView } from '@/game/engine';
import { nexusCardMode } from '@/game/nexus-cards';
import { NexusCardFace } from './nexus-cards';
import { Button } from './ui/button';
export function NexusIxianBetrayal({ game, act, busy }: {
game: GameView;
act: (action: Action) => void;
busy: boolean;
}) {
const reaction = game.nexusIxianBetrayalReaction;
if (!reaction) return null;
const own = game.players.find((player) => player.id === game.me);
const provider = game.players.find((player) => player.id === reaction.provider);
const canRespond = !!reaction.event && reaction.canPass && !reaction.hasPassed;
const canUse = canRespond && reaction.canUse && !reaction.blocked;
const priorityBlocked = game.automaticContinuationPending || !!game.truthtrance ||
!!game.response || !!game.decision || !!game.phaseOpening ||
!!game.nexusCards?.waiting.length || !!game.nexusTraitors?.pending;
const disabled = busy || game.status !== 'playing' || game.phase !== 3 || priorityBlocked ||
!!game.roomControl?.paused || !!game.roomControl?.closed || !!own?.autopilot;
const ownMode = game.nexusCards?.card === 'ixians' && own
? nexusCardMode('ixians', own.faction, game.players.map((player) => player.faction))
: undefined;
const advantage = reaction.kind === 'bidding' ? 'Bidding' : 'Technology';
return (
<section aria-label="Native Ixian advantage acknowledgement" data-testid="nexus-ixian-betrayal" className="notice min-w-0 space-y-3">
<h3>Native Ixian advantage acknowledgement · {advantage}</h3>
<p>{provider?.name ?? 'The Ixian player'} has a pending native {advantage} attempt.
{reaction.kind === 'bidding'
? ' The extra-card draw and inspection have not happened.'
: ' The declared exchange has not happened.'}</p>
<p className="fine">Every publicly possible unallied responder acknowledges this boundary, regardless of their secret Nexus identity.
After all acknowledgements, the original advantage continues once. Preventing it closes only this attempt, not the whole phase.
The native Karama response finishes first; this acknowledgement is not another Karama counter window.</p>
{game.nexusCards?.card === 'ixians' && <>
<NexusCardFace card="ixians" mode={ownMode} />
{ownMode === 'betrayal' && <p className="fine">Spend your own Ixian Nexus to prevent this one native {advantage} advantage.
{' '}<a href="/rules?topic=nexus-ixian-betrayal#nexus-ixian-betrayal">Betrayal timing and preview limits</a>
</p>}
</>}
{!canRespond ? (
<p aria-live="polite">{reaction.hasPassed ? 'You have passed. Waiting for the remaining acknowledgements.' : 'Waiting for the required acknowledgements.'}</p>
) : <>
{reaction.blocked && <p className="fine">{reaction.blocked}</p>}
<Button data-testid="nexus-ixian-betrayal-pass" className="game-action min-h-11 whitespace-normal" disabled={disabled}
onClick={() => { if (!disabled) act({ type: 'nexusIxianBetrayalPass', event: reaction.event }); }}>
Pass · allow the original advantage
</Button>
{canUse && <Button data-testid="nexus-ixian-betrayal-use" className="game-action min-h-11 whitespace-normal" disabled={disabled}
onClick={() => { if (!disabled) act({ type: 'nexusIxianBetrayalUse', event: reaction.event }); }}>
Use Ixian Nexus · prevent this {advantage} advantage
</Button>}
</>}
</section>
);
}