Allow chosen Reinforcements reserve costs
Verify and publish checkpoint container / container (push) Successful in 11m11s
Verify and publish checkpoint container / container (push) Successful in 11m11s
This commit is contained in:
1 parent
9dd386ff7a
commit
1cf90f1328
16 files changed
+419
-44
No files matched your search
@@ -80,6 +80,7 @@ export class StoneBurnerAllocationRequiredError extends BattleResolutionQuoteErr
|
||||
}
|
||||
}
|
||||
export type ResolutionPlan = {
|
||||
reinforcementsElite?: number;
|
||||
bankerSpice?: number;
|
||||
planetologistSpecial?: true;
|
||||
dial: number;
|
||||
@@ -473,7 +474,7 @@ function calculate(input: BattleResolutionInput): BattleResolutionQuote {
|
||||
const reserves = side.reinforcementsReserves!;
|
||||
requireQuote(!!reserves && typeof reserves === 'object' && !Array.isArray(reserves),
|
||||
'Reinforcements needs own typed reserves.');
|
||||
const cost = quoteReinforcements(reserves.normal, reserves.elite);
|
||||
const cost = quoteReinforcements(reserves.normal, reserves.elite, side.plan.reinforcementsElite);
|
||||
reinforcements.push({ player: side.id, card: REINFORCEMENTS_CARD, normal: cost.normal, elite: cost.elite });
|
||||
}
|
||||
const harass = slots.some((selectedCard, slot) =>
|
||||
|
||||
@@ -632,6 +632,7 @@ function candidatePlan(a: Action): Plan {
|
||||
};
|
||||
if (a.reinforcementsSources !== undefined)
|
||||
plan.reinforcementsSources = a.reinforcementsSources as NativeReserveSelections;
|
||||
if (typeof a.reinforcementsElite === 'number') plan.reinforcementsElite = a.reinforcementsElite;
|
||||
return plan;
|
||||
}
|
||||
function plans(g: GameView): Action[] {
|
||||
@@ -987,6 +988,7 @@ function plans(g: GameView): Action[] {
|
||||
}
|
||||
if (!selectedSources) continue;
|
||||
action.reinforcementsSources = selectedSources;
|
||||
action.reinforcementsElite = Object.values(selectedSources).reduce((sum, forces) => sum + forces.elite, 0);
|
||||
commitment = selectedCommitment;
|
||||
}
|
||||
if (typedForces && !commitment) continue;
|
||||
|
||||
+24
-13
@@ -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) {
|
||||
|
||||
+4
-4
@@ -1656,7 +1656,7 @@ export const RULE_TOPICS: RuleTopic[] = [
|
||||
'A native Richese ally may supply ordinary counters, a concealed No-Field or both under the selected Basic/Advanced provisional additive pool. Even a zero marker retains coalition presence. The selected actor gets its private variable pool/mixed notice; rivals do not. Both plans seal before one reserve-capped materialization, retaining ordinary counters and Richese-only variable losses. Ordinary Prescience follows the physical force owner; distinct Advanced special Karama keeps its private plan-read interpretation. Prevented Ecaz uses only its own pool; neither reading nor prevention reveals the marker early.',
|
||||
'Stone Burner compares physical undialed counters, not total dial or paid strength: active Occupy contributes floor(Ecaz forces / 2) plus the ally’s native undialed tokens. Canceled Occupy compares only the selected lead’s own pool. Physical Mirror copies use the same comparison and retain their original cleanup. Hidden opposing No-Field preflight uses public possible pools, not its actual denomination; genuinely winner-changing typed allocations remain blocked.',
|
||||
'With the independent variant, Harass returns only the actual card user’s undialed forces. An Ecaz user keeps the original mandatory ceil contribution committed at free full strength while its floor count returns; ally support is separate. An allied user subtracts fixed strength before choosing its native typed commitment. Canceled Occupy uses its selected lead’s own pool. Returned reserves are outside later casualties/explosion/Face Dance.',
|
||||
'Reinforcements consumes three card-user reserves normal-first and adds +2 normal score, never support or physical arrivals. Under the provisional native order, an own No-Field materializes before that original reserve fee and successful Harass returns; three reserves must remain after reveal before this plan may seal. Original/copied Stone ignores +2 score but not the fee. Existing all-revealed-outcome cost/disposal remains provisional. Unsupported co-side profiles stay guarded.',
|
||||
'Reinforcements consumes three chosen card-user reserves and adds +2 normal score, never support or physical arrivals. Under the provisional native order, an own No-Field materializes before that original reserve fee and successful Harass returns; three reserves must remain after reveal before this plan may seal. Original/copied Stone ignores +2 score but not the fee. Existing all-revealed-outcome cost/disposal remains provisional. Unsupported co-side profiles stay guarded.',
|
||||
'Advanced numbers come from the user-authorized revised PDF, physical pages 32 and 19, not a new publisher erratum. Basic follows the main printed paragraph provisionally. Original faction/deck prerequisites, separate ten-card Richese cache and independent three-card variant selection stay. Ordinary-plus-marker Basic/Advanced Occupy extends the provisional additive interpretation, not the unresolved FAQ cap. Fresh plain/card Ecaz and standalone Ecaz/classic Skills retain ordinary Arrakis Occupy, including independent cards without other modules. Bare Basic native mixed markers are connected; unsupported overlays and full-mode acceptance remain separate.',
|
||||
],
|
||||
checklist: [
|
||||
@@ -5514,7 +5514,7 @@ export const RULE_TOPICS: RuleTopic[] = [
|
||||
'Existing admitted World starts with Nexus and/or Discoveries also initialize original occupation benefits through fresh setup, optionally with Skills and original phase modules. Occupied Collection, ordinary deposits, nested entry, earned Nexus/Stronghold cards and deferred Banker retain their separate events. Original start/role limits remain; existing occupation history is never replaced and played games are not converted.',
|
||||
'Each faction uses its own card and matching world token. In Basic play the Emperor uses Kaitain alone. Advanced play adds Salusa Secundus for its Sardaukar; movement can transfer both force types between the two Emperor worlds.',
|
||||
'Provisional physical Ornithopter composition permits independently legal Imperial transfers as first or second groups, mixed with ground movement or another distinct typed World group. Original normal/starred pools prevent counter reuse after merging; fresh World-aware snapshots and the recorded older-flight inference retain the card event. Range3 grants no new World route. Only completed two-group use earns the extra movement credit; one card is discarded, and actual later Hajr is independent.',
|
||||
'Registered selected-family World all14 composes independent36/38/50-card setup and original Tech3+/Advanced Strongholds2+, including mixed E3. Clean bare World cards now use the registered selected-family profile with optional phase modules and original occupation state. Recruits uses current native rates/typed limits and recorded paid/free use; Banker and phase-end Axlotl remain separate. Reinforcements seals exact normal-first sources and excludes an own-World fee from dial/casualties while preserving original face/support. Suk rescues committed losses, not fees. Harass returns actual visitors through native custody and is forbidden on every own World. Original Nexus/Discovery/Semuta and role/effect guards remain.',
|
||||
'Registered selected-family World all14 composes independent36/38/50-card setup and original Tech3+/Advanced Strongholds2+, including mixed E3. Clean bare World cards now use the registered selected-family profile with optional phase modules and original occupation state. Recruits uses current native rates/typed limits and recorded paid/free use; Banker and phase-end Axlotl remain separate. Reinforcements seals exact chosen typed sources and excludes an own-World fee from dial/casualties while preserving original face/support. Suk rescues committed losses, not fees. Harass returns actual visitors through native custody and is forbidden on every own World. Original Nexus/Discovery/Semuta and role/effect guards remain.',
|
||||
'Native reserves determine high or low population. Salusa counts Sardaukar, while the other cards count the faction’s reserves there. Reaching the high face’s minimum restores high population. Occupation also applies the low penalty.',
|
||||
'Low population normally grants one additional free force revival and one additional bank spice when collecting CHOAM Charity. Salusa grants neither bonus; CHOAM’s Tupile reverses the usual pattern by granting an advantage while low and a penalty while high.',
|
||||
'A qualifying charity claim separates its ordinary payer from the extra bank spice. Inflation doubles both portions or prevents the whole collection. Canceling Bene Gesserit charity prevents both portions of that claim.',
|
||||
@@ -6653,9 +6653,9 @@ export const RULE_TOPICS: RuleTopic[] = [
|
||||
'In the independent Ecaz Treachery Cards variant, place your held Reinforcements card in either battle-card slot; it is neither a weapon nor a defense. A category Prescience answer is None, not the name of this card. One physical card cannot fill both slots.',
|
||||
'Have three own forces in reserves when sealing. Classic/paired Ecaz/Moritani Basic/Advanced, clean selected Ix/CHOAM native/base rosters and explicitly selected native Advanced Occupy prototypes spend ordinary reserves first and then elite reserves, moving exactly three to corresponding Tanks subpools at resolution. These are the actual card holder’s reserves, not the variable ally’s pool; no extra forces arrive.',
|
||||
'The normal-outcome score gains 2 without adding to the physical dial, spice support or on-board casualties. The card and its reserve cost are consumed even on a Traitor victory, a mutual Traitor result or a Lasgun/Shield explosion; successful Traitor calls still decide the outcome directly.',
|
||||
'All-outcome payment, normal-first selection and reserve transfer counting as battle losses remain provisional, not publisher rulings. Same-plan Harass pays the original three-reserve fee before returns; returned reserves cannot fund it. In native Advanced Occupy, original/copied Stone ignores +2 normal score but still charges the fee. Basic mixed-marker co-sides, unprofiled armies and other card-specific limits remain guarded.',
|
||||
'All-outcome payment and reserve transfer counting as battle losses remain provisional, not publisher rulings. The holder chooses an available ordinary/special reserve mix; older sealed plans without a choice retain normal-first payment. Same-plan Harass pays the original three-reserve fee before returns; returned reserves cannot fund it. In native Advanced Occupy, original/copied Stone ignores +2 normal score but still charges the fee. Basic mixed-marker co-sides, unprofiled armies and other card-specific limits remain guarded.',
|
||||
'With your own concealed No-Field, the provisional native order materializes its reserve-limited counters before this cost. At least three own reserves must remain after that reveal, before any future Harass return; otherwise Reinforcements is unavailable before the first plan seals. This checks only the actual card user, never its ally’s marker or reserves.',
|
||||
'Classic and selected Ix/CHOAM Basic/Advanced Worlds retain exact native normal-first sources, sealed when ambiguous; unique sources may derive. Own-World fees cannot also fund dial/casualties; original face/support stays, including explosion thresholds. Original phase modules preserve their own prerequisites and earned custody. Native-only World Traitors, typed Tanks and fee-before-Harass stay provisional composition, not a new card FAQ.',
|
||||
'Classic and selected Ix/CHOAM Basic/Advanced Worlds retain exact native sources for the chosen reserve mix, sealed when ambiguous; unique sources may derive. Own-World fees cannot also fund dial/casualties; original face/support stays, including explosion thresholds. Original phase modules preserve their own prerequisites and earned custody. Native-only World Traitors, typed Tanks and fee-before-Harass stay provisional composition, not a new card FAQ.',
|
||||
'One canonical physical-name registry now includes the independent cards for current facts and named battle conditions. Naming Reinforcements in a weapon slot does not make it a weapon category. Native questions use actual combat-location labels. Source-aware legal completion, private full-plan/read-only review and the fill button retain exact native sources.',
|
||||
'Selected Ix/CHOAM Worlds retain original38/50 setup and native stock, optionally with original Tech3+ and Advanced Strongholds2+. Cyborg substitution and Face Dance use their actual physical pools, never a duplicate fee origin. Native Richese must retain three World reserves AFTER its own projected No-Field reveal before R seals; rejection moves no counter or marker. Original phase rewards and earned held-card support stay; admitted Stone compositions retain their separate guards.',
|
||||
'Supported all14/card World/phase profiles keep the same physical fee and score. An own-World fee cannot fund the same physical dial/casualty; pre-fee native traits/face remain original. Suk rescues actual committed losses, never fee counters, keeping native saves at that home or using original typed external destinations. Earned Arrakeen never subsidizes a World or reserve fee. A Richese marker must leave three after materialization; future Harass cannot fund a refused seal. Native E3 card Worlds and unrelated guards remain.',
|
||||
|
||||
@@ -9,10 +9,10 @@ import {
|
||||
import { quoteReinforcements, ReinforcementsError } from './reinforcements';
|
||||
|
||||
type Cost = ReturnType<typeof quoteReinforcements>;
|
||||
function nativeCost(context: HomeworldCustodyContext, actor: string): Cost {
|
||||
function nativeCost(context: HomeworldCustodyContext, actor: string, selectedElite?: number): Cost {
|
||||
const seat = context.players.find(player => player.id === actor);
|
||||
if (!seat) throw new NativeReserveError('Reinforcements needs its seated native reserve owner.');
|
||||
return quoteReinforcements(seat.reserves - seat.eliteReserves, seat.eliteReserves);
|
||||
return quoteReinforcements(seat.reserves - seat.eliteReserves, seat.eliteReserves, selectedElite);
|
||||
}
|
||||
function sourceOptions(context: HomeworldCustodyContext, custody: HomeworldCustody,
|
||||
actor: string, cost: Cost): NativeReserveSelections[] {
|
||||
@@ -37,18 +37,19 @@ function sourceOptions(context: HomeworldCustodyContext, custody: HomeworldCusto
|
||||
return options;
|
||||
}
|
||||
|
||||
/** Public physical alternatives for the existing normal-first fee. Enumeration
|
||||
/** Public physical alternatives for the chosen typed fee, normal-first by default. Enumeration
|
||||
* does not select a source, move counters or grant permission to play a card. */
|
||||
export function reinforcementsHomeworldSources(context: HomeworldCustodyContext,
|
||||
custody: HomeworldCustody, actor: string): NativeReserveSelections[] {
|
||||
return sourceOptions(context, custody, actor, nativeCost(context, actor));
|
||||
custody: HomeworldCustody, actor: string, selectedElite?: number): NativeReserveSelections[] {
|
||||
return sourceOptions(context, custody, actor, nativeCost(context, actor, selectedElite));
|
||||
}
|
||||
|
||||
/** Exact native withdrawal; the caller commits matching Tanks and observes
|
||||
* population only after this whole fee. Ambiguous sources must be declared. */
|
||||
export function quoteReinforcementsHomeworldCost(context: HomeworldCustodyContext,
|
||||
custody: HomeworldCustody, actor: string, selections?: NativeReserveSelections) {
|
||||
const cost = nativeCost(context, actor);
|
||||
custody: HomeworldCustody, actor: string, selections?: NativeReserveSelections,
|
||||
selectedElite?: number) {
|
||||
const cost = nativeCost(context, actor, selectedElite);
|
||||
let sources = selections;
|
||||
if (sources === undefined) {
|
||||
const options = sourceOptions(context, custody, actor, cost);
|
||||
|
||||
+17
-6
@@ -17,15 +17,26 @@ export function isReinforcements(card: Card | undefined): boolean {
|
||||
ecazTreacheryDefinition(card)?.card.effect === 'reinforcements';
|
||||
}
|
||||
|
||||
/** Spend exactly three own reserve counters, ordinary before elite; never dial them. */
|
||||
/** Legal elite counts for the three-counter fee, in ordinary-first order. */
|
||||
export function reinforcementsEliteOptions(normalReserves: number, eliteReserves: number): number[] {
|
||||
if (![normalReserves, eliteReserves].every(n => typeof n === 'number' && Number.isSafeInteger(n) && n >= 0))
|
||||
throw new ReinforcementsError('Reinforcements needs valid nonnegative reserve counts.');
|
||||
const minimum = Math.max(0, 3 - normalReserves);
|
||||
const maximum = Math.min(3, eliteReserves);
|
||||
return minimum > maximum ? [] : Array.from({ length: maximum - minimum + 1 }, (_, i) => minimum + i);
|
||||
}
|
||||
|
||||
/** Spend exactly three own reserve counters; omitted choice retains the original ordinary-first rule. */
|
||||
export function quoteReinforcements(
|
||||
normalReserves: number,
|
||||
eliteReserves: number,
|
||||
selectedElite?: number,
|
||||
): { normal: number; elite: number; bonus: 2 } {
|
||||
if (![normalReserves, eliteReserves].every(n => typeof n === 'number' && Number.isSafeInteger(n) && n >= 0))
|
||||
throw new ReinforcementsError('Reinforcements needs valid nonnegative reserve counts.');
|
||||
if (normalReserves < 3 && eliteReserves < 3 - normalReserves)
|
||||
const options = reinforcementsEliteOptions(normalReserves, eliteReserves);
|
||||
if (!options.length)
|
||||
throw new ReinforcementsError('Reinforcements needs at least three own forces in reserves.');
|
||||
const normal = Math.min(3, normalReserves);
|
||||
return { normal, elite: 3 - normal, bonus: 2 };
|
||||
const elite = selectedElite === undefined ? options[0] : selectedElite;
|
||||
if (!Number.isSafeInteger(elite) || !options.includes(elite))
|
||||
throw new ReinforcementsError('Choose an available mix of three ordinary and elite reserve forces.');
|
||||
return { normal: 3 - elite, elite, bonus: 2 };
|
||||
}
|
||||
Reference in new issue
Block a user