'use client';
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';
import { imperialFlightCohort } from '@/game/ornithopter-homeworld';
import { HomeworldFace } from './homeworld-cards';
import { EcazPoisonIncome } from './ecaz-poison-income';
import { Button } from './ui/button';
import { Input } from './ui/input';
export function HomeworldTable({ game: g }: { game: GameView }) {
const worlds = g.homeworlds?.worlds;
if (!worlds) return null;
return (
Homeworlds
Native forces here are your reserves. Salusa Secundus counts Sardaukar
for its population.
{worlds.map((world) => {
const card = HOMEWORLD_CARDS.find((c) => c.id === world.card)!;
const owner = g.players.find((p) => p.id === world.native)!;
return (
{card.name}
{world.side} population · {world.population}
{faction(owner.faction).name} · {owner.name}
{Object.entries(world.forces).map(([id, forces]) => (
{id === world.native
? 'Native reserves'
: g.players.find((p) => p.id === id)?.name}
: {forces.normal} normal
{forces.elite > 0
? ` + ${forces.elite} ${owner.faction === 'emperor' && id === owner.id ? 'Sardaukar' : 'special'}`
: ''}
))}
{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
);
})}
);
}
/** The complementary Kaitain allocation is explicit in the submitted order. */
export function NativeShipmentChoice({
game: g,
amount,
elite,
sources,
onChange,
busy,
}: {
game: GameView;
amount: number;
elite: number;
sources: NativeReserveSelections | null;
onChange: (sources: NativeReserveSelections) => void;
busy: boolean;
}) {
const homes = g.homeworlds?.worlds?.filter((w) => w.native === g.me);
if (!homes || homes.length !== 2 || !sources) return null;
const salusa = homes.find((w) => w.secondary)!;
const kaitain = homes.find((w) => !w.secondary)!;
const chosen = sources[salusa.id];
const requested = { normal: amount - elite, elite };
return (
Shipment sources
Choose the counters leaving Salusa. The remainder leaves Kaitain in the
same shipment.
{(['normal', 'elite'] as const).map((kind) => {
const minimum = Math.max(
0,
requested[kind] - kaitain.forces[g.me][kind],
);
const maximum = Math.min(requested[kind], salusa.forces[g.me][kind]);
return (
{kind === 'normal' ? 'Normal forces' : 'Sardaukar'} from Salusa (
{minimum}–{maximum})
{
const count = Number(e.target.value);
onChange({
...sources,
[salusa.id]: { ...chosen, [kind]: count },
[kaitain.id]: {
...sources[kaitain.id],
[kind]: requested[kind] - count,
},
});
}}
/>
);
})}
Kaitain: {sources[kaitain.id].normal} normal +{' '}
{sources[kaitain.id].elite} Sardaukar. Salusa: {chosen.normal} normal +{' '}
{chosen.elite} Sardaukar.
);
}
export function EmperorHomeworldMovement({
game: g,
act,
busy,
}: {
game: GameView;
act: (action: Action) => void;
busy: boolean;
}) {
const [origin, setOrigin] = useState('homeworld:emperor');
const [normal, setNormal] = useState(0);
const [elite, setElite] = useState(0);
const option = g.homeworldMove;
const homes = g.homeworlds?.worlds?.filter((w) => w.native === g.me);
if (!option || !homes || homes.length !== 2) return null;
const source = homes.find((w) => w.id === origin)!;
const destination = homes.find((w) => w.id !== origin)!;
const active = g.ornithopter?.active;
let available = source.forces[g.me];
if (active?.mode === 'twoGroups' && active.completed === 1 && active.cohort) {
try {
const cohort = imperialFlightCohort(active.cohort, active.homeworldCohort === true, homes, g.me);
available = { normal: Math.min(available.normal, (cohort.forces[origin] ?? 0) - (cohort.elites[origin] ?? 0)),
elite: Math.min(available.elite, cohort.elites[origin] ?? 0) };
} catch { available = { normal: 0, elite: 0 }; }
}
const order: EmperorHomeworldMove = { origin, forces: { normal, elite } };
const move = emperorHomeworldMoveAction(g, order);
const cardMoves = active ? [] : (['range3', 'twoGroups'] as const).map(mode => ({ mode,
action: emperorHomeworldMoveAction(g, order, mode) }));
const name = (id: string) =>
id === 'homeworld:emperor' ? 'Kaitain' : 'Salusa Secundus';
const disabled = busy || !!option.blocked || !!g.players.find(player => player.id === g.me)?.autopilot;
const invalid =
!Number.isSafeInteger(normal) ||
!Number.isSafeInteger(elite) ||
normal < 0 ||
elite < 0 ||
normal > available.normal ||
elite > available.elite ||
normal + elite === 0;
return (
Move between Imperial Homeworlds
Move normal forces or Sardaukar between Kaitain and Salusa Secundus.
Costs one movement and no spice. Moving first passes your unused
shipment.
{g.ornithopter &&
Provisional Ornithopter composition: the native Kaitain–Salusa route and no-spice cost stay unchanged.
Range three gives no extra World range or new route. Two groups must use different original typed
counters; a World group may be paired with a ground group. Existing shipment history and completed
movement stay recorded. {active ? 'The card is already on the table; continue its current flight.'
: 'Play the held physical card with the first group.'}
}
{active &&
Ornithopter: {active.completed} group moved · {active.remaining} remaining.
Select only original unmoved normal forces or Sardaukar for the next group.
}
{option.blocked && (
{option.blocked}
)}
From
{
setOrigin(e.target.value as EmperorHomeworldMove['origin']);
setNormal(0);
setElite(0);
}}
className="w-full"
>
{homes.map((home) => (
{name(home.id)}
))}
Normal forces (0–{available.normal})
setNormal(Number(e.target.value))}
/>
Sardaukar (0–{available.elite})
setElite(Number(e.target.value))}
/>
{option.remaining} movement{option.remaining === 1 ? '' : 's'}{' '}
remaining.
{ if (!disabled && !invalid && move) act(move); }}
>
{active ? 'Move next Ornithopter group to' : 'Move to'} {name(destination.id)}
{!active && g.ornithopter && cardMoves.map(({ mode, action }) => { if (!disabled && !invalid && action) act(action); }}>
Play Ornithopter ({mode === 'range3' ? 'range three' : 'two groups'}) and move to {name(destination.id)}
)}
);
}