'use client'; import { useId, useState } from 'react'; import type { Action, GameView } from '@/game/engine'; 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 { ShipmentQuote } from './shipment-quote'; import { Button } from './ui/button'; /** This renderer receives public player data only, never the private token model. */ export function NoFieldBoardMarkers({ players, }: { players: readonly { id: string; name: string; faction: string; noField?: RicheseNoFieldView | null; }[]; }) { return players.flatMap((player) => { const marker = player.noField?.deployed; if (!marker) 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 [ {label} NF , ]; }); } export function RicheseNoFieldControls({ game, act, busy, destination, sector, allyPayment, }: { game: GameView; act: (action: Action) => void; busy: boolean; destination: string; sector: number; allyPayment: string; }) { const id = useId(); const [selection, setSelection] = useState(''); const info = game.richeseNoField; const me = game.players.find((player) => player.id === game.me); if (!info || info.owner !== game.me || !info.private || !me) return null; const state = info.private; const available = state.tokens.filter( (token) => token.id !== state.lastShipped, ); const selected = available.find((token) => token.id === selection) ?? available[0]; const deployed = state.deployed; const selectedTerritory = territory(destination); const rate = { faction: me.faction, halfRate: me.faction === 'guild' || game.players.some( (player) => player.faction === 'guild' && player.id === me.ally, ) || game.karamaShipping?.player === me.id, }; const cost = reserveShipmentCost(rate, selectedTerritory.type, 1); const share = !me.ally || allyPayment === '' ? Math.max(0, cost - (me.spice ?? 0)) : Number(allyPayment); const validShare = Number.isSafeInteger(share) && share >= 0 && share <= cost; const problems: string[] = []; if (!info.canShip) problems.push(info.shipBlock ?? 'No-Field shipment is not available now.'); if (!selected) problems.push('No different No-Field token is available.'); if (destination === MOBILE_STRONGHOLD) problems.push('You cannot ship directly into the mobile stronghold.'); if ( selectedTerritory.type === 'stronghold' && game.players.filter( (player) => player.id !== me.id && fighterCount(player, destination) > 0, ).length >= 2 ) problems.push('Two other factions already occupy this stronghold.'); if (!selectedTerritory.sectors.includes(sector)) problems.push('Select a sector in the destination territory.'); if (sector === game.storm) problems.push('You cannot ship into the storm sector.'); if (!validShare) problems.push(`Choose an ally payment from 0 to ${cost}.`); else { if (share > game.aid.available) problems.push( `Only ${game.aid.available} pledged ally spice is available.`, ); if (cost - share > (me.spice ?? 0)) problems.push('Your spice does not cover the chosen payment.'); } const showShipment = game.phase === 5 && game.active === me.id && !me.shipped; return (
Your No-Field tokens

Your token values are private until revealed. A concealed marker counts as one force, even when its value is zero. Physical forces remain in reserves until revelation.

{deployed && ( <>

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.

{!info.canReveal && (

{info.revealBlock ?? 'Voluntary revelation is not available now.'}

)} )} {showShipment && ( <>

Destination: {selectedTerritory.name}, sector {sector}. Choose another destination on the map or in the shipment controls. This replaces your normal shipment at the price of one force.

)}
); }