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