156 lines
5.1 KiB
TypeScript
156 lines
5.1 KiB
TypeScript
import type { Game } from './engine';
|
||
import { homeworldContext } from './homeworld-game';
|
||
import { homeworldPopulations } from './homeworld-population';
|
||
|
||
type HomeworldBenefitContext = Pick<
|
||
Game,
|
||
'advanced' | 'players' | 'homeworlds'
|
||
>;
|
||
export type HomeworldRevivalOpening = {
|
||
turn: number;
|
||
tleilaxu: { player: string; low: boolean };
|
||
};
|
||
export class HomeworldBenefitError extends Error {
|
||
constructor(message: string) {
|
||
super(message);
|
||
this.name = 'HomeworldBenefitError';
|
||
}
|
||
}
|
||
|
||
/** Current native population only. The caller owns benefit timing and all
|
||
* ordinary eligibility; Salusa never supplies another low-population bonus. */
|
||
export function homeworldLowBonus(
|
||
context: HomeworldBenefitContext,
|
||
playerId: string,
|
||
): 0 | 1 {
|
||
if (!context.homeworlds?.custody) return 0;
|
||
return homeworldPopulations(
|
||
homeworldContext(context),
|
||
context.homeworlds.custody,
|
||
).some(
|
||
(home) =>
|
||
home.native === playerId &&
|
||
home.card !== 'salusa_secundus' &&
|
||
home.side === 'low',
|
||
)
|
||
? 1
|
||
: 0;
|
||
}
|
||
|
||
/** Salusa's threshold counts Sardaukar at Salusa, not all Imperial reserves.
|
||
* Its high advantage affects the faction's Sardaukar wherever they fight. */
|
||
export function homeworldSardaukarFreeSupport(
|
||
context: HomeworldBenefitContext,
|
||
playerId: string,
|
||
): boolean {
|
||
if (!context.advanced || !context.homeworlds?.custody) return false;
|
||
if (
|
||
!context.players.some((p) => p.id === playerId && p.faction === 'emperor')
|
||
)
|
||
return false;
|
||
return homeworldPopulations(
|
||
homeworldContext(context),
|
||
context.homeworlds.custody,
|
||
).some(
|
||
(home) =>
|
||
home.native === playerId &&
|
||
home.card === 'salusa_secundus' &&
|
||
home.side === 'high',
|
||
);
|
||
}
|
||
|
||
/** Temporary release boundary: a population-changing return during combat
|
||
* must not invalidate sealed dials/support or earlier binding answers. Use
|
||
* public battle participation, never an opponent's hidden plan contents. */
|
||
export function homeworldSardaukarGholaBlock(
|
||
context: HomeworldBenefitContext & Pick<Game, 'battle'>,
|
||
playerId: string,
|
||
): string | null {
|
||
if (
|
||
!context.advanced ||
|
||
!context.homeworlds?.custody ||
|
||
!context.battle ||
|
||
![context.battle.attacker, context.battle.defender].includes(playerId) ||
|
||
!context.players.some((p) => p.id === playerId && p.faction === 'emperor')
|
||
)
|
||
return null;
|
||
return homeworldPopulations(
|
||
homeworldContext(context),
|
||
context.homeworlds.custody,
|
||
).some(
|
||
(home) =>
|
||
home.native === playerId &&
|
||
home.card === 'salusa_secundus' &&
|
||
home.population === 1,
|
||
)
|
||
? 'Ghola raising Salusa to high population during the Emperor’s battle awaits its battle-support timing ruling.'
|
||
: null;
|
||
}
|
||
|
||
const validTurn = (turn: unknown): turn is number =>
|
||
typeof turn === 'number' && Number.isSafeInteger(turn) && turn > 0;
|
||
|
||
/** Capture before any revival-opening effect can change native population.
|
||
* A saved receipt owns this phase's Tleilaxu free-revival income restriction. */
|
||
export function snapshotHomeworldRevival(
|
||
context: HomeworldBenefitContext & Pick<Game, 'turn'>,
|
||
): HomeworldRevivalOpening | null {
|
||
if (!context.homeworlds?.custody) return null;
|
||
const tleilaxu = context.players.find(
|
||
(player) => player.faction === 'tleilaxu',
|
||
);
|
||
if (!tleilaxu) return null;
|
||
if (!validTurn(context.turn))
|
||
throw new HomeworldBenefitError(
|
||
'The Homeworld revival opening needs a valid current turn.',
|
||
);
|
||
return {
|
||
turn: context.turn,
|
||
tleilaxu: {
|
||
player: tleilaxu.id,
|
||
low: homeworldLowBonus(context, tleilaxu.id) === 1,
|
||
},
|
||
};
|
||
}
|
||
|
||
const record = (value: unknown): value is Record<string, unknown> =>
|
||
!!value && typeof value === 'object' && !Array.isArray(value);
|
||
|
||
/** Read only the saved opening decision. Never infer a missing receipt from
|
||
* current populations: earlier revivals may already have crossed a threshold.
|
||
* This answers only the free-revival income gate, not Ghola purchase income. */
|
||
export function tleilaxuHomeworldFreeIncomeBlocked(
|
||
context: HomeworldBenefitContext &
|
||
Pick<Game, 'turn' | 'phase'> & {
|
||
homeworldRevival?: HomeworldRevivalOpening | null;
|
||
},
|
||
): boolean {
|
||
if (context.phase !== 4 || !context.homeworlds) return false;
|
||
const tleilaxu = context.players.filter(
|
||
(player) => player.faction === 'tleilaxu',
|
||
);
|
||
if (!tleilaxu.length) return false;
|
||
const receipt: unknown = context.homeworldRevival;
|
||
if (
|
||
tleilaxu.length !== 1 ||
|
||
!validTurn(context.turn) ||
|
||
!record(receipt) ||
|
||
Object.keys(receipt).length !== 2 ||
|
||
!Object.hasOwn(receipt, 'turn') ||
|
||
!Object.hasOwn(receipt, 'tleilaxu') ||
|
||
receipt.turn !== context.turn ||
|
||
!record(receipt.tleilaxu) ||
|
||
Object.keys(receipt.tleilaxu).length !== 2 ||
|
||
!Object.hasOwn(receipt.tleilaxu, 'player') ||
|
||
!Object.hasOwn(receipt.tleilaxu, 'low') ||
|
||
typeof receipt.tleilaxu.player !== 'string' ||
|
||
!receipt.tleilaxu.player.trim() ||
|
||
receipt.tleilaxu.player !== tleilaxu[0].id ||
|
||
typeof receipt.tleilaxu.low !== 'boolean'
|
||
)
|
||
throw new HomeworldBenefitError(
|
||
'The current Tleilaxu Homeworld revival-opening receipt is missing, stale or malformed.',
|
||
);
|
||
return receipt.tleilaxu.low;
|
||
}
|