diff --git a/components/game-table.tsx b/components/game-table.tsx index bcb9ce20..46d73c71 100644 --- a/components/game-table.tsx +++ b/components/game-table.tsx @@ -226,6 +226,7 @@ import { guildTransportQuote } from '@/game/transport-quote'; import { fremenReserveEntry, botGroundMoveAllowed, botMovementRange, groundMoveRouteDistance } from '@/game/bot-mobility'; import { isAlliedShipmentVisit } from '@/game/allied-separation'; import { location as boardLocation } from '@/game/board'; +import { isBoardNoFieldLocation } from '@/game/richese-no-field'; import { fighterCount, isAdvisor } from '@/game/advisors'; import { useState } from 'react'; import { @@ -825,10 +826,13 @@ export function GameTable({ ), } : { from: source, amount, elite: eliteAmount }; - const ownNoField = + const ownedDeployedNoField = g.richeseNoField?.owner === me.id ? g.richeseNoField.private?.deployed : null; + const ownNoField = ownedDeployedNoField && isBoardNoFieldLocation(ownedDeployedNoField.location) + ? { ...ownedDeployedNoField, location: ownedDeployedNoField.location } + : null; const markerAtSource = !!ownNoField && !!source && @@ -1958,19 +1962,18 @@ export function GameTable({ )), )} - {g.players - .filter( - (p) => p.noField?.deployed?.location.territory === selected, - ) - .map((p) => ( - - {faction(p.faction).name}: concealed No-Field in sector{' '} - {p.noField!.deployed!.location.sector} · counts as one force - - ))} + {g.players.flatMap((p) => { + const marker = p.noField?.deployed; + if (!marker || !isBoardNoFieldLocation(marker.location) || marker.location.territory !== selected) + return []; + return [ + {faction(p.faction).name}: concealed No-Field in sector{' '} + {marker.location.sector} · counts as one force + ]; + })} {g.status !== 'lobby' && (
void; busy: boolean; +}) { + const me = game.players.find(player => player.id === game.me); + const info = game.richeseNoField; + const [selectedDestination, setSelectedDestination] = useState(''); + const [selectedToken, setSelectedToken] = useState(''); + const [allyPayment, setAllyPayment] = useState(0); + if (!me || me.faction !== 'richese' || info?.owner !== me.id || !info.private) return null; + const targets = (game.homeworlds?.worlds ?? []).filter(world => + world.native !== me.id && world.native !== me.ally); + const destination = targets.find(world => world.id === selectedDestination) ?? targets[0]; + const tokens = info.private.tokens.filter(token => token.id !== info.private!.lastShipped); + const token = tokens.find(candidate => candidate.id === selectedToken) ?? tokens[0]; + const quote = richeseHomeworldMarkerChoice(game, destination?.id ?? '', token?.id ?? '', allyPayment); + return
+ Ship a concealed No-Field +

Provisional Homeworld rule: ship one concealed marker from your supply to + another non-allied Homeworld for one spice. Its value stays private and its physical forces + remain in your reserves until the marker is revealed. This uses your normal shipment.

+ + + {me.ally && } + + One marker · one spice total · you pay {quote.ownPayment}, ally pays {quote.allyPayment}. + {quote.blocked && ` ${quote.blocked}`} + + +
; +} + export function HomeworldShipment({ game: g, act, @@ -265,6 +320,7 @@ export function HomeworldShipment({ Ship to {homeworldName(g, destination)} + ); } diff --git a/components/homeworld-table.tsx b/components/homeworld-table.tsx index 1aa00556..4e431233 100644 --- a/components/homeworld-table.tsx +++ b/components/homeworld-table.tsx @@ -4,6 +4,7 @@ import { useState } from 'react'; import type { Action, GameView } from '@/game/engine'; import { faction } from '@/game/catalog'; import { HOMEWORLD_CARDS } from '@/game/homeworld-cards'; +import { isHomeworldNoFieldLocation } from '@/game/richese-no-field'; import type { NativeReserveSelections } from '@/game/homeworld-native-reserves'; import { emperorHomeworldMoveAction } from '@/game/homeworld-options'; import type { EmperorHomeworldMove } from '@/game/homeworld-emperor-move'; @@ -54,6 +55,14 @@ export function HomeworldTable({ game: g }: { game: GameView }) { : ''}

))} + {g.players.flatMap(player => { + const marker = player.noField?.deployed; + if (!marker || !isHomeworldNoFieldLocation(marker.location) || marker.location.homeworld !== world.id) + return []; + return [

+ {player.name}: concealed No-Field · counts as one force +

]; + })}
{card.name} card diff --git a/components/mobile-stronghold.tsx b/components/mobile-stronghold.tsx index 4a927404..297cf8ce 100644 --- a/components/mobile-stronghold.tsx +++ b/components/mobile-stronghold.tsx @@ -13,6 +13,7 @@ import { import type { Action, GameView } from '@/game/engine'; import { quoteSandmasterStronghold } from '@/game/sandmaster-movement'; import { mobileStrongholdCollectionCapacity } from '@/game/mobile-stronghold-collection'; +import { isBoardNoFieldLocation } from '@/game/richese-no-field'; const label = (key: string) => { const loc = splitLocation(key); return `${territory(loc.territory).name}${loc.sector ? ` · sector ${loc.sector}` : ''}`; @@ -56,7 +57,12 @@ export function MobileStronghold({ return chosen === false ? [] : [typeof chosen === 'string' && keys.includes(chosen) ? chosen : keys[0]]; }) : []; const remaining: Record = {}; - const capacity = mobileStrongholdCollectionCapacity(g.players); + const capacity = mobileStrongholdCollectionCapacity(g.players.map(player => { + const marker = player.noField?.deployed; + return { ...player, noField: marker && isBoardNoFieldLocation(marker.location) + ? { deployed: { controller: marker.controller, location: marker.location } } + : null }; + })); const collections = route.map((key, index) => { const before = remaining[key] ?? Math.max(0, (g.spice[key] ?? 0) - Number(skillPiles.includes(key))); const maximum = Math.min(before, capacity); diff --git a/components/ornithopter-movement.tsx b/components/ornithopter-movement.tsx index cc3b94f7..a0c99f62 100644 --- a/components/ornithopter-movement.tsx +++ b/components/ornithopter-movement.tsx @@ -7,6 +7,7 @@ import { type OrnithopterMode, } from '@/game/ornithopter'; import { location, splitLocation, gameTerritories } from '@/game/board'; +import { isBoardNoFieldLocation } from '@/game/richese-no-field'; import { botGroundMoveAllowed } from '@/game/bot-mobility'; import { Button } from './ui/button'; import { CardInspector, CardRules } from './card-inspector'; @@ -82,7 +83,7 @@ export function OrnithopterMovement({ me.elites?.forces ?? {}, forces, elites, - typeof move.noField === 'string' && marker + typeof move.noField === 'string' && marker && isBoardNoFieldLocation(marker.location) ? { tokenId: move.noField, event: typeof move.event === 'string' ? move.event : '', diff --git a/components/planetologist-movement-action.tsx b/components/planetologist-movement-action.tsx index b213f2d9..75467add 100644 --- a/components/planetologist-movement-action.tsx +++ b/components/planetologist-movement-action.tsx @@ -12,6 +12,7 @@ import { botGroundMoveAllowed } from '@/game/bot-mobility'; import { isAdvisor } from '@/game/advisors'; import { quoteAdvisorGatherMatching, AdvisorGatherMatchingError } from '@/game/advisor-gather-matching'; import { location, splitLocation, validGameLocation } from '@/game/board'; +import { isBoardNoFieldLocation } from '@/game/richese-no-field'; import { nexusGuildMovementAvailable } from '@/game/nexus-guild-cunning-options'; import { planetologistFlightSelection, type PlanetologistFlight } from '@/game/planetologist-flight'; import { validateCohortSelection } from '@/game/ornithopter'; @@ -30,8 +31,9 @@ export function planetologistNoFieldMarker(game: GameView) { const state = info.private; const marker = state?.deployed; const publicMarker = info.public.deployed; - if (!state || !marker || marker.controller !== me.id || + if (!state || !marker || !isBoardNoFieldLocation(marker.location) || marker.controller !== me.id || !publicMarker || publicMarker.controller !== me.id || + !isBoardNoFieldLocation(publicMarker.location) || marker.location.territory !== publicMarker.location.territory || marker.location.sector !== publicMarker.location.sector || marker.tokenId !== state.lastShipped || diff --git a/components/richese-no-field.tsx b/components/richese-no-field.tsx index a91a95c4..9379915e 100644 --- a/components/richese-no-field.tsx +++ b/components/richese-no-field.tsx @@ -6,7 +6,8 @@ import { faction } from '@/game/catalog'; import { territory, MOBILE_STRONGHOLD } from '@/game/board'; import { fighterCount } from '@/game/advisors'; import { reserveShipmentCost } from '@/game/shipment-price'; -import type { RicheseNoFieldView } from '@/game/richese-no-field'; +import { isBoardNoFieldLocation, isHomeworldNoFieldLocation, type RicheseNoFieldView } from '@/game/richese-no-field'; +import { HOMEWORLD_CARDS } from '@/game/homeworld-cards'; import { quoteSmugglerNoField } from '@/game/smuggler-no-field'; import { quoteRicheseHomeworldForces, quoteRicheseHomeworldPair } from '@/game/richese-homeworld-shipment'; import { RicheseHomeworldForces } from './richese-homeworld-forces'; @@ -58,7 +59,7 @@ export function NoFieldBoardMarkers({ }) { return players.flatMap((player) => { const marker = player.noField?.deployed; - if (!marker) return []; + if (!marker || !isBoardNoFieldLocation(marker.location)) return []; const t = territory(marker.location.territory); const label = `${player.name}: concealed No-Field in ${t.name}, sector ${marker.location.sector}. Counts as one force until revealed.`; return [ @@ -135,6 +136,10 @@ export function RicheseNoFieldControls({ ) ?? available.find((token) => token.id !== pairConcealed?.id); const nexus = game.nexusRicheseCunning; const deployed = state.deployed; + const deployedHomeworldId = deployed && isHomeworldNoFieldLocation(deployed.location) + ? deployed.location.homeworld : null; + const deployedHomeworld = game.homeworlds?.worlds?.find(world => world.id === deployedHomeworldId); + const deployedHomeworldName = HOMEWORLD_CARDS.find(card => card.id === deployedHomeworld?.card)?.name ?? 'a Homeworld'; const selectedTerritory = territory(destination); const rate = { faction: me.faction, @@ -225,7 +230,7 @@ export function RicheseNoFieldControls({ No-Field {token.value} {deployed?.tokenId === token.id - ? 'Concealed on the board' + ? 'Concealed in play' : state.lastShipped === token.id ? 'Last used · cannot repeat' : 'Available'} @@ -235,11 +240,14 @@ export function RicheseNoFieldControls({ {deployed && ( <> -

+ {isBoardNoFieldLocation(deployed.location) ?

Concealed at {territory(deployed.location.territory).name}, sector{' '} {deployed.location.sector}. Use the movement controls to move the marker alone or with physical forces from that territory. -

+

:

+ Concealed at {deployedHomeworldName}. + The marker counts as one force until it is revealed. +

}