'use client';
import { useState } from 'react';
import type { Action, GameView } from '@/game/engine';
import { specialForceName } from '@/game/combat';
import {
homeworldName,
homeworldShipmentChoice,
richeseHomeworldMarkerChoice,
type HomeworldShipmentSources,
} from '@/game/homeworld-shipment-options';
import { Button } from './ui/button';
import { Input } from './ui/input';
/** This private control is separate from physical-source shipment: the marker
* costs one but does not withdraw a reserve counter until it is revealed. */
export function HomeworldNoFieldShipment({ game, act, busy }: {
game: GameView; act: (action: Action) => 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.
Destination Homeworld for No-Field
setSelectedDestination(event.target.value)}>
{!targets.length && No eligible destination }
{targets.map(world =>
{homeworldName(game, world.id)} · {game.players.find(player => player.id === world.native)?.name}
)}
Private No-Field token
setSelectedToken(event.target.value)}>
{!tokens.length && No different token available }
{tokens.map(candidate => No-Field {candidate.value} )}
{me.ally && Allied spice contribution · {game.aid.available} pledged
setAllyPayment(Number(event.target.value))} />
}
One marker · one spice total · you pay {quote.ownPayment}, ally pays {quote.allyPayment}.
{quote.blocked && ` ${quote.blocked}`}
{ if (quote.action) act(quote.action); }}>
Ship No-Field to {homeworldName(game, destination?.id ?? '')}
;
}
export function HomeworldShipment({
game: g,
act,
busy,
}: {
game: GameView;
act: (action: Action) => void;
busy: boolean;
}) {
const worlds = g.homeworlds?.worlds ?? [];
const me = g.players.find((player) => player.id === g.me)!;
const available = worlds.filter((world) => {
const force = world.forces[g.me];
return force && force.normal + force.elite > 0;
});
const targets = worlds.filter(
(world) =>
(world.native !== g.me || me.faction === 'guild') &&
!(g.guildTransportCanceled && me.faction === 'guild' && world.native === me.id) &&
world.native !== me.ally,
);
const [origin, setOrigin] = useState(available[0]?.id ?? '');
const [destination, setDestination] = useState(
targets.find((world) => world.id !== available[0]?.id)?.id ?? '',
);
const [sources, setSources] = useState({});
const [allyPayment, setAllyPayment] = useState(0);
const [nexusEvent, setNexusEvent] = useState('');
const useNexus =
g.karamaShipping?.player !== g.me &&
!!g.nexusGuildSecretAlly &&
nexusEvent === g.nexusGuildSecretAlly.event;
const option = g.homeworldShipment;
if (!option) return null;
const imperial = g.advanced && me.faction === 'emperor';
const selectedOrigins = available.filter((world) =>
origin === 'imperial-pair' ? world.native === me.id : world.id === origin,
);
const selected = Object.fromEntries(
selectedOrigins.flatMap((world) => {
const choice = sources[world.id];
return choice && choice.normal + choice.elite !== 0
? [[world.id, choice]]
: [];
}),
);
const quote = homeworldShipmentChoice(
g,
destination,
selected,
allyPayment,
useNexus ? nexusEvent : undefined,
);
const special = specialForceName(me.faction);
return (
Ship to another Homeworld
Use your shipment to send forces from one Homeworld to another. Native
reserves and foreign garrisons have separate source pools.{' '}
{me.faction === 'guild'
? g.guildTransportCanceled
? 'The native Guild return permission is canceled: Junction cannot be a destination, even with a purchased price.'
: 'Guild may also return a foreign garrison to Junction.'
: 'Your own Homeworld cannot be a destination.'}{' '}
Allied Homeworlds cannot be destinations.
{useNexus
? 'Selected Guild Nexus price: one spice per two forces, rounded up.'
: g.karamaShipping?.player === g.me
? 'Purchased Karama price: one spice per two forces, rounded up, paid entirely to the bank even if the native Guild rate is canceled.'
: me.faction === 'guild'
? g.guildRateCanceled
? 'The native Guild rate is canceled: pay one spice per force.'
: 'Guild pays one spice per two forces, rounded up.'
: 'Pay one spice per force.'}{' '}
This uses your shipment; your normal movement remains available.
{g.nexusGuildSecretAlly && (
setNexusEvent(
event.target.checked ? g.nexusGuildSecretAlly!.event : '',
)
}
/>
Use Guild Nexus Secret Ally for Homeworld shipment
Spend the Nexus for its independent price of one spice per two
forces, rounded up; it does not stack with a purchased Karama price.
Existing source and destination restrictions still apply.
{g.nexusGuildSecretAlly.blocked && (
{g.nexusGuildSecretAlly.blocked}
)}
)}
{option.blocked && (
{option.blocked}
)}
Source Homeworld
{
setOrigin(event.target.value);
setSources({});
}}
>
{!available.length && (
No forces on Homeworlds
)}
{available.map((world) => (
{homeworldName(g, world.id)} ·{' '}
{world.native === me.id
? 'native reserves'
: 'foreign garrison'}
))}
{imperial &&
available.filter((world) => world.native === me.id).length ===
2 && (
Kaitain + Salusa Secundus · combined native shipment
)}
{selectedOrigins.map((world) => {
const pool = world.forces[me.id];
const chosen = sources[world.id] ?? { normal: 0, elite: 0 };
return (
{homeworldName(g, world.id)} ·{' '}
{world.native === me.id
? 'native reserves'
: 'foreign garrison'}
Available: {pool.normal} normal
{pool.elite > 0 ? ` + ${pool.elite} ${special}` : ''}.
{(['normal', 'elite'] as const)
.filter((kind) => kind === 'normal' || pool.elite > 0)
.map((kind) => (
{kind === 'normal' ? 'Normal forces' : special} from{' '}
{homeworldName(g, world.id)}
setSources({
...sources,
[world.id]: {
...chosen,
[kind]: Number(event.target.value),
},
})
}
/>
))}
);
})}
Destination Homeworld
setDestination(event.target.value)}
>
{!targets.length && (
No eligible destination
)}
{targets.map((world) => (
source.id === world.id,
)}
>
{homeworldName(g, world.id)} ·{' '}
{world.native === me.id
? 'return to your Homeworld'
: g.players.find((player) => player.id === world.native)
?.name}
))}
{me.ally && (
Allied spice contribution · {g.aid.available} pledged
setAllyPayment(Number(event.target.value))}
/>
)}
Your available spice: {me.spice ?? 0}. Allied pledge:{' '}
{g.aid.available}.
{quote.amount} forces · {quote.cost} spice total
{quote.amount > 0
? ` · you pay ${quote.ownPayment}, ally pays ${quote.allyPayment}`
: ''}
.{quote.blocked && ` ${quote.blocked}`}
{
if (quote.action) act(quote.action);
}}
>
Ship to {homeworldName(g, destination)}
);
}