Allow chosen Reinforcements reserve costs
Verify and publish checkpoint container / container (push) Successful in 11m11s

This commit is contained in:
Quintine committed 2026-10-29 04:41:26 +11:00
1 parent 9dd386ff7a
commit 1cf90f1328
16 files changed
+419 -44

No files matched your search

+24 -13
View File
@@ -776,7 +776,7 @@ import {
type SpiceCard,
} from './cards';
import { ecazTreacheryCards, ecazTreacheryDefinition } from './ecaz-cards';
import { REINFORCEMENTS_CARD, isReinforcements, quoteReinforcements, ReinforcementsError } from './reinforcements';
import { REINFORCEMENTS_CARD, isReinforcements, quoteReinforcements, reinforcementsEliteOptions, ReinforcementsError } from './reinforcements';
import { quoteReinforcementsHomeworldCost, reinforcementsHomeworldSources,
reinforcementsBattleForces } from './reinforcements-homeworld';
import {
@@ -873,12 +873,14 @@ export type Plan = {
support: number;
allyPayment?: number;
kwisatz?: boolean;
/** Explicit number of special reserve counters in the three-counter fee. */
reinforcementsElite?: number;
/** Exact native reserve fee sources; only a Homeworld Reinforcements plan. */
reinforcementsSources?: NativeReserveSelections;
};
export type PlanField = Exclude<
keyof Plan,
'support' | 'kwisatz' | 'allyPayment' | 'bankerSpice' | 'reinforcementsSources' | 'planetologistSpecial'
'support' | 'kwisatz' | 'allyPayment' | 'bankerSpice' | 'reinforcementsSources' | 'reinforcementsElite' | 'planetologistSpecial'
>;
export type DiplomatDefenseReceipt = DiplomatDefenseQuote & {
event: string;
@@ -10814,7 +10816,7 @@ function planCombatForces(g: Game, p: Player, opponent: Player,
if (!home || home.native !== p.id) return forces;
const plan = g.battle!.plans[p.id];
const cost = reserveCost ?? (plan && usesNativeBattleSpecial(plan, REINFORCEMENTS_CARD)
? currentReinforcementsCost(g, p, plan.reinforcementsSources) : undefined);
? currentReinforcementsCost(g, p, plan.reinforcementsSources, plan.reinforcementsElite) : undefined);
return reinforcementsBattleForces(forces, cost?.sources?.[home.id]);
}
function takeBattleLosses(g: Game, p: Player, t: string, losses: Casualties) {
@@ -18717,8 +18719,10 @@ function ecazTreacheryIntegrity(g: Game) {
[plan.weapon, plan.defense].every(id => id === null || !!cardOf(owner, id)) &&
validBattleSlotPair(cardOf(owner, plan.weapon), cardOf(owner, plan.defense), true),
'The sealed Reinforcements plan lost its physical card or legal slot.');
currentReinforcementsCost(g, owner, plan.reinforcementsSources);
currentReinforcementsCost(g, owner, plan.reinforcementsSources, plan.reinforcementsElite);
}
requireRule(plan.reinforcementsElite === undefined || usesNativeBattleSpecial(plan, REINFORCEMENTS_CARD),
'A typed reserve fee requires the physical Reinforcements card.');
requireRule(plan.reinforcementsSources === undefined ||
(g.homeworlds && usesNativeBattleSpecial(plan, REINFORCEMENTS_CARD)),
'Native reserve fee sources require the physical Reinforcements card in a Homeworld plan.');
@@ -18799,7 +18803,7 @@ type ReinforcementsCost = ReturnType<typeof quoteReinforcements> & {
native?: ReturnType<typeof quoteNativeReserveWithdrawal>;
};
function currentReinforcementsCost(g: Game, p: Player,
selections?: NativeReserveSelections): ReinforcementsCost {
selections?: NativeReserveSelections, selectedElite?: number): ReinforcementsCost {
requireRule(reinforcementsModeSupported(g),
'Reinforcements requires the independent Ecaz card variant in a supported native faction composition.');
const combinedBlock = combinedCardBattleBlock(g);
@@ -18812,11 +18816,11 @@ function currentReinforcementsCost(g: Game, p: Player,
requireRule(g.homeworlds.custody, 'Reinforcements needs its original native reserve custody.');
// Richese materializes ordinary primary-World reserves before this fee.
if (materializes)
quoteReinforcements(p.reserves - materializes - (p.elites?.reserves ?? 0), p.elites?.reserves ?? 0);
quoteReinforcements(p.reserves - materializes - (p.elites?.reserves ?? 0), p.elites?.reserves ?? 0, selectedElite);
return homeworldRule(() => quoteReinforcementsHomeworldCost(
homeworldContext(g), g.homeworlds!.custody!, p.id, selections));
homeworldContext(g), g.homeworlds!.custody!, p.id, selections, selectedElite));
}
return quoteReinforcements(p.reserves - materializes - (p.elites?.reserves ?? 0), p.elites?.reserves ?? 0);
return quoteReinforcements(p.reserves - materializes - (p.elites?.reserves ?? 0), p.elites?.reserves ?? 0, selectedElite);
} catch (error) {
if (error instanceof ReinforcementsError) throw new RuleError(error.message);
throw error;
@@ -18842,9 +18846,11 @@ function reinforcementsPreview(g: Game, p: Player) {
blocked = materializes
? 'Your No-Field reveals before Reinforcements; three own reserves must remain after materialization.'
: 'Reinforcements needs three physical forces in your reserves.';
return { blocked, normal, elite,
const eliteOptions = blocked ? [] : reinforcementsEliteOptions(
reservesAfterReveal - (p.elites?.reserves ?? 0), p.elites?.reserves ?? 0);
return { blocked, normal, elite, eliteOptions,
...(!blocked && g.homeworlds?.custody ? { homeworldSources: homeworldRule(() =>
reinforcementsHomeworldSources(homeworldContext(g), g.homeworlds!.custody!, p.id)) } : {}),
eliteOptions.flatMap(choice => reinforcementsHomeworldSources(homeworldContext(g), g.homeworlds!.custody!, p.id, choice))) } : {}),
...(noFieldAtCombatLocation(p,battle.territory)
? { noField: { materializes, reservesAfterReveal } } : {}) };
}
@@ -31181,8 +31187,12 @@ function validatePlan(
const selectedReinforcements = usesNativeBattleSpecial(input, REINFORCEMENTS_CARD);
requireRule(input.reinforcementsSources === undefined || selectedReinforcements,
'Native reserve fee sources require the physical Reinforcements card.');
requireRule(input.reinforcementsElite === undefined || selectedReinforcements &&
typeof input.reinforcementsElite === 'number',
'A typed reserve fee requires the physical Reinforcements card.');
const reserveCost = selectedReinforcements
? currentReinforcementsCost(g, p, input.reinforcementsSources as NativeReserveSelections | undefined)
? currentReinforcementsCost(g, p, input.reinforcementsSources as NativeReserveSelections | undefined,
input.reinforcementsElite as number | undefined)
: undefined;
const forces = planCombatForces(g, p, opponent, reserveCost);
const profile = ecazBattleProfile(g);
@@ -31215,6 +31225,7 @@ function validatePlan(
...(input.planetologistSpecial === true ? { planetologistSpecial: true as const } : {}),
...(allyPayment ? { allyPayment } : {}),
...(reserveCost?.sources ? { reinforcementsSources: structuredClone(reserveCost.sources) } : {}),
...(input.reinforcementsElite !== undefined ? { reinforcementsElite: input.reinforcementsElite as number } : {}),
dial,
leader:
typeof input.leader === 'string' && input.leader ? input.leader : null,
@@ -33951,7 +33962,7 @@ function resolveBattle(g: Game, retreat?: DiplomatRetreatSelection) {
const reduced = casualtyCommitment.forces.temporaryElite !== sardaukar.receipt.count;
const plan = b.plans[winner.id];
const fee = reduced && usesNativeBattleSpecial(plan,REINFORCEMENTS_CARD)
? currentReinforcementsCost(g,winner,plan.reinforcementsSources).sources?.[b.territory] : undefined;
? currentReinforcementsCost(g,winner,plan.reinforcementsSources,plan.reinforcementsElite).sources?.[b.territory] : undefined;
requireRule(!reduced || (fee && g.homeworlds && homeworldBattleLocation(g,b.territory)?.native === winner.id),
'Reduced temporary Sardaukar require the original native Reinforcements fee.');
sardaukar.casualties = {...structuredClone(casualtyCommitment),
@@ -33979,7 +33990,7 @@ function resolveBattle(g: Game, retreat?: DiplomatRetreatSelection) {
}
for (const cost of quote.reinforcements ?? []) {
const player = getPlayer(g, cost.player);
const current = currentReinforcementsCost(g, player, b.plans[player.id].reinforcementsSources);
const current = currentReinforcementsCost(g, player, b.plans[player.id].reinforcementsSources, b.plans[player.id].reinforcementsElite);
requireRule(cost.normal === current.normal && cost.elite === current.elite,
'Reinforcements lost its original physical reserve cost.');
if (current.native) {