From 985cf85e12a78714a8b6a465dea9b8b63f15153a Mon Sep 17 00:00:00 2001 From: Quintine <15965533+Quintine@users.noreply.github.com> Date: Thu, 15 Oct 2026 04:40:46 +1100 Subject: [PATCH] Connect BG Nexus fresh and storm advisor conversion --- components/game-table.tsx | 30 ++- components/nexus-advisors.tsx | 5 + docs/CURRENT_STATUS.md | 19 ++ docs/IMPLEMENTATION_STATUS.md | 46 +++++ docs/NEXUS_ADVISOR_RUNTIME.md | 55 +++++- docs/NEXUS_BENE_GESSERIT_RULES.md | 38 +++- docs/NEXUS_CARD_RULES.md | 12 +- docs/RULE_DECISIONS.md | 1 + game/bot-mobility.ts | 20 +- game/engine.ts | 2 +- game/nexus-advisors.ts | 69 +------ game/planetologist-movement.ts | 2 +- game/reference.ts | 12 +- tests/fixture-nexus-advisor-fresh.ts | 183 +++++++++++++++++ tests/fixture-nexus-advisor-storm.ts | 230 ++++++++++++++++++++++ tests/fixture-nexus-fremen-protection.ts | 4 +- tests/nexus-advisor-controls.test.ts | 160 +-------------- tests/nexus-advisor-fresh-runtime.test.ts | 132 +++++++++++++ tests/nexus-advisor-storm-runtime.test.ts | 169 ++++++++++++++++ tests/nexus-advisors-quote.test.ts | 45 ----- tests/nexus-advisors.test.ts | 18 -- 21 files changed, 936 insertions(+), 316 deletions(-) create mode 100644 tests/fixture-nexus-advisor-fresh.ts create mode 100644 tests/fixture-nexus-advisor-storm.ts create mode 100644 tests/nexus-advisor-fresh-runtime.test.ts create mode 100644 tests/nexus-advisor-storm-runtime.test.ts diff --git a/components/game-table.tsx b/components/game-table.tsx index be51b584..89bd69c9 100644 --- a/components/game-table.tsx +++ b/components/game-table.tsx @@ -95,6 +95,7 @@ import { NexusHarkonnenBetrayal } from './nexus-harkonnen-betrayal'; import { OrnithopterMovement } from './ornithopter-movement'; import { DiscoveryOrnithopterMovement } from './discovery-flight-movement'; import { PlanetologistMovement } from './planetologist-movement'; +import { selectedOriginElites } from '@/game/planetologist-movement'; import { SandmasterMovement } from './sandmaster-movement'; import { SandmasterWormChoice } from './sandmaster-worm'; import { sandmasterWormCollection } from '@/game/sandmaster-worm'; @@ -177,7 +178,7 @@ import { SmugglerShipmentChoice } from './smuggler-shipment'; import { nexusGuildSecretAllyAction, nexusGuildSecretAllyQuote } from '@/game/nexus-guild-secret-ally-options'; import { nexusRicheseAction, nexusRicheseQuote } from '@/game/nexus-richese-options'; import { guildTransportQuote } from '@/game/transport-quote'; -import { fremenReserveEntry, botGroundMoveAllowed } from '@/game/bot-mobility'; +import { fremenReserveEntry, botGroundMoveAllowed, botMovementRange, groundMoveRouteDistance } from '@/game/bot-mobility'; import { isAlliedShipmentVisit } from '@/game/allied-separation'; import { location as boardLocation } from '@/game/board'; import { fighterCount, isAdvisor } from '@/game/advisors'; @@ -814,6 +815,15 @@ export function GameTable({ count > 0 && !botGroundMoveAllowed(g, me, key, boardLocation(selected, sector), 0), ); + const movementTarget = boardLocation(selected, sector); + const selectedMovementElite = combineSectors + ? selectedOriginElites(physicalDraftEntries, eliteForces, splitLocation(source).territory) + : eliteAmount; + const ordinaryMovementRange = botMovementRange(g, me, selectedMovementElite); + const ordinaryMovementRouteBlocked = physicalDraftEntries.some(([key, count]) => + count > 0 && groundMoveRouteDistance(g, me, key, movementTarget) > ordinaryMovementRange) || + (!!includesNoField && !!ownNoField && groundMoveRouteDistance(g, me, + boardLocation(ownNoField.location.territory, ownNoField.location.sector), movementTarget) > ordinaryMovementRange); const act = (a: Action) => { // Control of one's own seat is independent of every gameplay response. if (a.type === 'setAutopilot') { void send(a); return; } @@ -4674,6 +4684,7 @@ export function GameTable({ ordinaryMovementAction, !source || canceledFremenRouteBlocked || + ordinaryMovementRouteBlocked || selectedDestinationInStorm || (me.moved ?? 0) >= (me.movesAllowed ?? 1) || (includesNoField @@ -4681,9 +4692,11 @@ export function GameTable({ : (me.forces[source] ?? 0) === 0), selectedDestinationInStorm ? 'movement-storm-unavailable' - : canceledFremenRouteBlocked - ? 'movement-range-unavailable' - : undefined, + : ordinaryMovementRouteBlocked + ? 'movement-route-unavailable' + : canceledFremenRouteBlocked + ? 'movement-range-unavailable' + : undefined, )} {canceledFremenRouteBlocked && !selectedDestinationInStorm && ( @@ -4697,6 +4710,13 @@ export function GameTable({ territory, or use available ornithopters.

)} + {ordinaryMovementRouteBlocked && !selectedDestinationInStorm && ( +

+ Movement unavailable: this force group has no unblocked + ground route within its ordinary range. Choose a reachable + source or destination outside the storm and blocked strongholds. +

+ )} {selectedDestinationInStorm && (

= (me.movesAllowed ?? 1) ? 'No ordinary movement remains this turn.' + : ordinaryMovementRouteBlocked + ? 'Choose an unblocked source and route outside the storm.' : canceledFremenRouteBlocked ? 'Choose an ordinary route within your available range.' : selectedDestinationInStorm diff --git a/components/nexus-advisors.tsx b/components/nexus-advisors.tsx index d54c0b33..0c328993 100644 --- a/components/nexus-advisors.tsx +++ b/components/nexus-advisors.tsx @@ -79,6 +79,11 @@ function AdvisorSelection({ game, act, busy }: Props) { This spends your Nexus card once. Karama may cancel the entire selection. It costs no spice and uses no shipment or movement allowance.

+

+ Provisional card exception: Cunning may convert freshly accompanied or + storm-bound advisors in place. Ordinary flipping, storm movement and + battle restrictions remain unchanged. +

{offer.blocked &&

{offer.blocked}

}