diff --git a/README.md b/README.md index 3eba50ab..3dbecf3f 100644 --- a/README.md +++ b/README.md @@ -189,6 +189,19 @@ Full hands and same-card redraw from an exhausted deck are supported. Harkonnen buyers and special sale origins remain guarded; see [verified source, privacy and runtime boundaries](docs/NEXUS_IXIAN_REPLACEMENT_RULES.md). +For local **Ixian Nexus Betrayal**, use +`node --import tsx tools/start-prototype.ts --profile ixian-betrayal` +on a fresh ready Basic/Advanced lobby with native Ixians, otherwise classic +factions or Tleilaxu, the forty-seven-card Ix Treachery Deck and Nexus alone. +Exact-version/private-backup safeguards apply; no existing-game retrofit or +public start is enabled. Native alliance-qualified Nexus rights still apply. +Original Karama counters resolve first; neutral rival acknowledgements then +offer **Pass** or privately eligible **Use Ixian Nexus** before the original +Bidding inspection/draw or Advanced Technology exchange. Use spends one +physical Nexus and prevents only that attempt; all-pass keeps its native +effect and continuation. See the +[bounded verified runtime](docs/NEXUS_CARD_RUNTIME.md#bounded-ixian-betrayal-local-runtime--1-october-2026). + Above the board, **Treachery draw pile** and **Spice draw pile** show live card counts separately from hands, auction cards and discards. See [draw-pile counts](docs/DRAW_PILES.md). diff --git a/components/game-table.tsx b/components/game-table.tsx index 4dbee962..757d0fd7 100644 --- a/components/game-table.tsx +++ b/components/game-table.tsx @@ -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({ Preview rules and limits

)} + {g.nexusIxianBetrayalPreview && ( +

+ 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.{' '} + Preview rules and limits +

+ )} {g.status !== 'lobby' && (