Files
Dune/game/homeworld-revival-deployment-options.ts
T
Quintine 28ba1618ba Implement Homeworld mechanics through Caladan reinforcement checkpoint
Connect typed Homeworld custody, transport, battle, income, revival and Caladan reinforcement flows with private multiplayer recovery and controls. Preserve the optimized test workflow and keep unfinished rules modes gated. Verified with 3682 offline tests, typecheck, lint, production build and 40 HTTP tests; existing saved games remain unchanged.
2026-09-10 08:12:26 +10:00

107 lines
3.8 KiB
TypeScript

import { gameTerritories, splitLocation } from './board';
import type { Action, GameView } from './engine';
/** Use the server's source-specific timing boundary for the selected group. */
export function homeworldRevivalActionBlock(g: GameView, action: Action): string | null {
const me = g.players.find((player) => player.id === g.me);
if (!me || action.leader) return null;
const source = action.type === 'revive'
? 'normal'
: action.type === 'card' && me.hand?.some((card) => card.id === action.card && card.effect === 'ghola')
? 'ghola'
: null;
if (!source) return null;
const amount = action.amount === undefined && source === 'ghola'
? Math.min(5, me.tanks)
: Number(action.amount);
const elite = action.elite === undefined
? Math.max(0, amount - (me.tanks - (me.elites?.tanks ?? 0)))
: Number(action.elite);
return g.homeworldRevivalBlocks?.find((block) =>
block.source === source && block.amount === amount && block.elite === elite)?.reason ?? null;
}
/** Consume the actor's projected destinations; never reconstruct eligibility
* from reserves or another player's private state. */
export function homeworldRevivalDeploymentCanAct(g: GameView): boolean {
return !!(
g.status === 'playing' &&
g.homeworldRevivalDeployment?.player === g.me &&
g.decision?.kind === 'homeworldRevivalDeployment' &&
g.decision.player === g.me &&
g.decision.event === g.homeworldRevivalDeployment.event &&
!g.response &&
!g.truthtrance &&
!g.phaseOpening &&
!g.automaticContinuationPending
);
}
export function homeworldRevivalDeploymentChoice(
g: GameView,
destination: string,
): { blocked: string | null; action: Action | null } {
const offer = g.homeworldRevivalDeployment;
const target = offer?.destinations.find(
(option) => option.id === destination,
);
let blocked: string | null = null;
if (!homeworldRevivalDeploymentCanAct(g))
blocked = 'Wait for the current choice to finish.';
else if (offer!.blocked) blocked = offer!.blocked;
else if (!target) blocked = 'Choose a current revival destination.';
else if (target.blocked) blocked = target.blocked;
else if (
!Number.isSafeInteger(offer!.normal) ||
!Number.isSafeInteger(offer!.elite) ||
offer!.normal < 0 ||
offer!.elite < 0 ||
offer!.normal + offer!.elite <= 0 ||
offer!.normal + offer!.elite > 20
)
blocked = 'The revived group is no longer available.';
return {
blocked,
action: blocked
? null
: {
type: 'decision',
event: offer!.event,
destination,
amount: offer!.normal + offer!.elite,
},
};
}
/** All profiles share this legal fallback: reinforce an occupied stronghold,
* otherwise use the first permitted destination, or leave the group at home. */
export function homeworldRevivalDeploymentActions(g: GameView): Action[] {
if (!homeworldRevivalDeploymentCanAct(g)) return [];
const offer = g.homeworldRevivalDeployment!;
const me = g.players.find((player) => player.id === g.me);
const strongholds = new Set(
gameTerritories(g)
.filter((territory) => territory.type === 'stronghold')
.map((territory) => territory.id),
);
const occupied = new Set(
Object.entries(me?.forces ?? {})
.filter(([, count]) => count > 0)
.map(([key]) => splitLocation(key).territory),
);
const choices = offer.destinations
.map((target) => ({
target,
action: homeworldRevivalDeploymentChoice(g, target.id).action,
}))
.filter((choice) => choice.action);
const preferred = choices.find(
({ target }) =>
target.territory &&
strongholds.has(target.territory) &&
occupied.has(target.territory),
);
const action = preferred?.action ?? choices[0]?.action;
return [action ?? { type: 'decision', event: offer.event, decline: true }];
}