89 lines
3.7 KiB
TypeScript
89 lines
3.7 KiB
TypeScript
import type { Action, GameView } from './engine';
|
|
import {
|
|
ecazOccupyDialOptions,
|
|
quoteEcazOccupyDial,
|
|
type EcazOccupyBattleProfile,
|
|
} from './ecaz-occupy-battle';
|
|
|
|
/** The selected lead plans and pays; the profile's force owner need not be that lead. */
|
|
export function ecazOccupyOwnProfile(game: GameView): EcazOccupyBattleProfile | null {
|
|
const battle = game.battle;
|
|
const occupy = battle?.ecazOccupy;
|
|
const profile = occupy?.profile;
|
|
return battle && occupy?.event === battle.event && profile?.planOwner === game.me
|
|
? profile : null;
|
|
}
|
|
|
|
export function ecazOccupyLeadOptions(game: GameView) {
|
|
const decision = game.decision;
|
|
const battle = game.battle;
|
|
if (decision?.kind !== 'ecazBattleLead' || !battle?.ecazOccupy ||
|
|
decision.event !== battle.event || decision.event !== battle.ecazOccupy.event) return [];
|
|
const chooser = game.players.find(player => player.id === decision.player)?.name ?? decision.player;
|
|
const blocked = game.status !== 'playing' || game.phase !== 6
|
|
? 'This battle is not awaiting a lead choice.'
|
|
: decision.player !== game.me ? `Only ${chooser} chooses the lead.` : null;
|
|
return decision.choices.map((lead) => ({
|
|
lead,
|
|
label: game.players.find((player) => player.id === lead)?.name ?? lead,
|
|
blocked,
|
|
action: { type: 'decision', event: decision.event, lead },
|
|
}));
|
|
}
|
|
|
|
function supportBudget(game: GameView, profile: EcazOccupyBattleProfile) {
|
|
const personalSpice = game.players.find((player) => player.id === game.me)?.spice ?? 0;
|
|
const bankSupport = game.battle?.strongholdEffects[profile.payer] === 'arrakeen' ? 2 : 0;
|
|
return {
|
|
personalSpice,
|
|
maximum: Math.min(profile.maxSupport, personalSpice + game.aid.available + bankSupport),
|
|
};
|
|
}
|
|
|
|
/** Only the lead's own projected funds enter affordability; never the other ally's spice. */
|
|
export function ecazOccupyDialChoices(game: GameView) {
|
|
const profile = ecazOccupyOwnProfile(game);
|
|
return profile ? ecazOccupyDialOptions(profile, supportBudget(game, profile).maximum) : [];
|
|
}
|
|
|
|
/** Numeric editor constraints and an explanation for an unsealable physical dial. */
|
|
export function ecazOccupyPlanControl(game: GameView, dial: number, support: number) {
|
|
const profile = ecazOccupyOwnProfile(game);
|
|
if (!profile) return null;
|
|
const budget = supportBudget(game, profile);
|
|
let blocked: string | null = null;
|
|
let variableDial: number | null = null;
|
|
try {
|
|
const quote = quoteEcazOccupyDial(profile, dial, support);
|
|
variableDial = quote.variableDial;
|
|
if (support > budget.maximum) blocked = 'The selected lead cannot afford this spice support.';
|
|
} catch (error) {
|
|
blocked = error instanceof Error ? error.message : 'Choose a legal total dial and spice support.';
|
|
}
|
|
return {
|
|
profile,
|
|
minimum: profile.fixedEcazDial,
|
|
maximum: profile.maxDial,
|
|
step: 0.5,
|
|
supportMaximum: budget.maximum,
|
|
personalSpice: budget.personalSpice,
|
|
fixedEcazDial: profile.fixedEcazDial,
|
|
variableDial,
|
|
blocked,
|
|
};
|
|
}
|
|
|
|
/**
|
|
* All four native policies share this legality adapter. Native candidate plans already
|
|
* use the selected lead's leaders/cards and TOTAL dials; no new strategy is introduced.
|
|
* It also serves prescience candidate generation before the ordinary plan editor opens.
|
|
*/
|
|
export function ecazOccupyPolicyActions(game: GameView, nativeCandidates: Action[] = []): Action[] {
|
|
if (game.decision?.kind === 'ecazBattleLead') {
|
|
return ecazOccupyLeadOptions(game).filter((option) => !option.blocked).map((option) => option.action);
|
|
}
|
|
if (!ecazOccupyOwnProfile(game)) return nativeCandidates;
|
|
return nativeCandidates.filter((action) => action.type !== 'battlePlan' ||
|
|
!ecazOccupyPlanControl(game, Number(action.dial), Number(action.support ?? 0))?.blocked);
|
|
}
|