From 97edc3eac8cf3918c0f84dffe8d1d2dbe2b8a3df Mon Sep 17 00:00:00 2001 From: Quintine <15965533+Quintine@users.noreply.github.com> Date: Tue, 27 Oct 2026 14:06:29 +1100 Subject: [PATCH] Integrate Imperial Homeworld movement with Ornithopter --- components/homeworld-table.tsx | 54 ++++-- components/ornithopter-movement.tsx | 8 + docs/CURRENT_STATUS.md | 41 +++++ docs/HOMEWORLD_RULES.md | 11 ++ docs/IMPLEMENTATION_STATUS.md | 43 +++++ docs/RICHESE_ORNITHOPTER_ENGINE_AUDIT.md | 65 ++++++++ docs/RULE_DECISIONS.md | 15 ++ game/bots.ts | 4 +- game/engine.ts | 157 +++++++++++------- game/homeworld-options.ts | 125 ++++++++++---- game/ornithopter-homeworld.ts | 45 +++++ game/reference.ts | 11 +- tests/fixture-ornithopter-emperor-world.ts | 156 +++++++++++++++++ .../ornithopter-emperor-world-engine.test.ts | 147 ++++++++++++++++ tests/ornithopter-homeworld-controls.test.tsx | 125 ++++++++++++++ tests/ornithopter-homeworld-quote.test.ts | 150 +++++++++++++++++ 16 files changed, 1043 insertions(+), 114 deletions(-) create mode 100644 game/ornithopter-homeworld.ts create mode 100644 tests/fixture-ornithopter-emperor-world.ts create mode 100644 tests/ornithopter-emperor-world-engine.test.ts create mode 100644 tests/ornithopter-homeworld-controls.test.tsx create mode 100644 tests/ornithopter-homeworld-quote.test.ts diff --git a/components/homeworld-table.tsx b/components/homeworld-table.tsx index 963591d0..1aa00556 100644 --- a/components/homeworld-table.tsx +++ b/components/homeworld-table.tsx @@ -5,6 +5,9 @@ import type { Action, GameView } from '@/game/engine'; import { faction } from '@/game/catalog'; import { HOMEWORLD_CARDS } from '@/game/homeworld-cards'; 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'; @@ -148,7 +151,7 @@ export function EmperorHomeworldMovement({ act: (action: Action) => void; busy: boolean; }) { - const [origin, setOrigin] = useState('homeworld:emperor'); + const [origin, setOrigin] = useState('homeworld:emperor'); const [normal, setNormal] = useState(0); const [elite, setElite] = useState(0); const option = g.homeworldMove; @@ -156,9 +159,22 @@ export function EmperorHomeworldMovement({ 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 available = source.forces[g.me]; + 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) || @@ -177,16 +193,27 @@ export function EmperorHomeworldMovement({ 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} )} -
+