'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.
{state.tokens.map((token) => (
No-Field {token.value}
{deployed?.tokenId === token.id
? 'Concealed on the board'
: state.lastShipped === token.id
? 'Last used · cannot repeat'
: 'Available'}
))}
{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.