Files
Dune/game/recruits.ts
T
2026-09-08 07:37:52 +10:00

76 lines
2.3 KiB
TypeScript

/** A global card effect expires when the authoritative turn changes. */
export type RecruitsEffect = Readonly<{ turn: number }>;
export type RecruitsScope = Readonly<{
turn: number;
recruits?: RecruitsEffect | null;
}>;
/**
* Resolved facts, measured in physical forces, never battle strength.
* The caller resolves faction, ally, prevention and Homeworld interactions.
* freeAllowanceUsed is deliberately separate from total revived: the sources
* do not settle whether earlier paid revivals consume a newly raised free rate.
* freeRateCap is explicit because the ordinary Fremen cap example does not
* decide every interaction with an unlimited-revival faction.
*/
export type RecruitsRevivalFacts = Readonly<{
currentFreeRate: number;
currentLimit: number;
revived: number;
freeAllowanceUsed: number;
freeRateCap: number;
freeBlocked?: boolean;
revivalBlocked?: boolean;
}>;
export type RecruitsRevivalAllowance = Readonly<{
active: boolean;
freeRate: number;
limit: number;
freeRemaining: number;
remaining: number;
}>;
/**
* Pure resolved-facts arithmetic, not runtime-certified Recruits activation.
* It does not play/discard a card, reopen revival, refund spice, alter a pending
* quote, choose free elite units, or relax an elite unit's separate revival cap.
* Existing unlimited physical limits (currently 20) survive the ordinary raise.
*/
export function recruitsRevivalAllowance(
scope: RecruitsScope,
facts: RecruitsRevivalFacts,
): RecruitsRevivalAllowance {
for (const key of [
'currentFreeRate',
'currentLimit',
'revived',
'freeAllowanceUsed',
'freeRateCap',
] as const)
if (!Number.isSafeInteger(facts[key]) || facts[key] < 0)
throw new RangeError(`${key} must be a nonnegative integer force count.`);
const active = scope.recruits?.turn === scope.turn;
const limit = active ? Math.max(7, facts.currentLimit) : facts.currentLimit;
const freeRate = facts.freeBlocked
? 0
: Math.min(
facts.currentFreeRate * (active ? 2 : 1),
facts.freeRateCap,
limit,
);
const remaining = facts.revivalBlocked
? 0
: Math.max(0, limit - facts.revived);
return {
active,
freeRate,
limit,
remaining,
freeRemaining: Math.min(
remaining,
Math.max(0, freeRate - facts.freeAllowanceUsed),
),
};
}