From 1cf90f1328ed4ef9d6fd9be93d899bbde886c52b Mon Sep 17 00:00:00 2001 From: Quintine <15965533+Quintine@users.noreply.github.com> Date: Thu, 29 Oct 2026 04:41:26 +1100 Subject: [PATCH] Allow chosen Reinforcements reserve costs --- components/game-table.tsx | 29 ++++- components/reinforcements-sources.tsx | 2 +- docs/CURRENT_STATUS.md | 11 ++ docs/REINFORCEMENTS_RUNTIME.md | 31 ++++- game/battle-resolution-quote.ts | 3 +- game/bots.ts | 2 + game/engine.ts | 37 ++++-- game/reference.ts | 8 +- game/reinforcements-homeworld.ts | 15 +-- game/reinforcements.ts | 23 +++- tests/no-field-reinforcements.test.ts | 5 +- tests/reinforcements-homeworld-choice.test.ts | 116 ++++++++++++++++++ tests/reinforcements-homeworld.test.ts | 5 +- ...nforcements-typed-choice-controls.test.tsx | 61 +++++++++ ...einforcements-typed-choice-runtime.test.ts | 98 +++++++++++++++ tests/reinforcements.test.ts | 17 ++- 16 files changed, 419 insertions(+), 44 deletions(-) create mode 100644 tests/reinforcements-homeworld-choice.test.ts create mode 100644 tests/reinforcements-typed-choice-controls.test.tsx create mode 100644 tests/reinforcements-typed-choice-runtime.test.ts diff --git a/components/game-table.tsx b/components/game-table.tsx index a5c1c4f0..8b8f7d49 100644 --- a/components/game-table.tsx +++ b/components/game-table.tsx @@ -515,6 +515,12 @@ export function GameTable({ selectedDefenseId === 'ecaz-reinforcements'; const harassSelected = (selectedWeaponId === 'ecaz-harass-withdraw' && !planetologistWeapon) || selectedDefenseId === 'ecaz-harass-withdraw'; + const [reinforcementsEliteDraft, setReinforcementsEliteDraft] = useState<{ key: string; elite: number } | null>(null); + const reinforcementsEliteKey = `${g.code}:${me.id}:${g.battle?.event ?? ''}`; + const reinforcementsEliteOptions = g.battle?.reinforcements?.eliteOptions ?? []; + const chosenReinforcementsElite = reinforcementsEliteDraft?.key === reinforcementsEliteKey && + reinforcementsEliteOptions.includes(reinforcementsEliteDraft.elite) + ? reinforcementsEliteDraft.elite : reinforcementsEliteOptions[0]; const [reinforcementsSourceDraft, setReinforcementsSourceDraft] = useState<{ key: string; index: number; } | null>(null); @@ -597,6 +603,8 @@ export function GameTable({ enabled: plan.planetologistSpecial === true, }); setKwisatz(!!plan.kwisatz); + setReinforcementsEliteDraft(plan.reinforcementsElite === undefined ? null + : { key: reinforcementsEliteKey, elite: plan.reinforcementsElite }); if (plan.reinforcementsSources) { const chosen = JSON.stringify(plan.reinforcementsSources); const index = reinforcementsSourceChoices?.findIndex(choice => JSON.stringify(choice) === chosen); @@ -5779,12 +5787,23 @@ export function GameTable({ Reinforcements may occupy either card slot but is neither a weapon nor a defense. It adds 2 to battle strength without changing the physical dial or spice support. The cost is three reserve forces sent to the Tanks on any revealed outcome. - Available for that quote: {g.battle.reinforcements.normal} normal,{' '} + Default fee: {g.battle.reinforcements.normal} normal,{' '} {g.battle.reinforcements.elite} elite. The card is discarded even after a traitor call or explosion. {g.battle.reinforcements.blocked && ` ${g.battle.reinforcements.blocked}`}

)} + {reinforcementsSelected && !g.homeworlds && reinforcementsEliteOptions.length > 0 && ( + + )} {reinforcementsSelected && reinforcementsSourceChoices && ( sum + forces.elite, 0) } + : reinforcementsSelected && chosenReinforcementsElite !== undefined + ? { reinforcementsElite: chosenReinforcementsElite } : {}), }), !!occupyControl?.blocked || !!stonePlanReason || !!harassControl.blocked || !!smugglerPlanReason || !!reinforcementsPlanReason || !battlePairValid, )} @@ -5922,6 +5944,9 @@ export function GameTable({ {g.battle.plans[me.id]?.planetologistSpecial && (

Planetologist alternate Special: +2 if the leader survives; no normal card effect.

)} + {g.battle.plans[me.id]?.reinforcementsElite !== undefined && !g.battle.plans[me.id]?.reinforcementsSources && ( +

Sealed reserve fee: {3 - g.battle.plans[me.id].reinforcementsElite!} normal + {g.battle.plans[me.id].reinforcementsElite} special.

+ )} {g.battle.plans[me.id]?.reinforcementsSources && (

Sealed reserve fee: {reinforcementsSourceLabel(g, g.battle.plans[me.id].reinforcementsSources!)}

)} diff --git a/components/reinforcements-sources.tsx b/components/reinforcements-sources.tsx index 74e4c0e7..78e01c67 100644 --- a/components/reinforcements-sources.tsx +++ b/components/reinforcements-sources.tsx @@ -19,7 +19,7 @@ export function ReinforcementsSources({ game, choices, selected, onChange, busy if (!choices.length) return null; return
Reinforcements reserve sources -

Three original native reserves go to the Tanks, normal first. These are not the invading army or future Harass returns.

+

Choose the three original native reserves sent to the Tanks. These are not the invading army or future Harass returns.

{choices.length > 1 ?