Files
Dune/game/homeworld-revival-destinations.ts

93 lines
5.0 KiB
TypeScript
Raw Permalink Blame History

This file contains ambiguous Unicode characters
This file contains Unicode characters that might be confused with other characters. If you think that this is intentional, you can safely ignore this warning. Use the Escape button to reveal them.
import type { Game } from './engine';
import { gameTerritories, location, MOBILE_STRONGHOLD, splitLocation } from './board';
import { presenceAt } from './force-presence';
import { territoryEntryBlock } from './occupancy';
import { homeworldContext } from './homeworld-game';
import { HOMEWORLD_CARDS } from './homeworld-cards';
import { homeworldForceGroups, quoteHomeworldCustody, HomeworldCustodyError } from './homeworld-custody';
import { quoteNativeReserveWithdrawal } from './homeworld-native-reserves';
import type { HomeworldRevivalDeploymentQuote } from './homeworld-revival-deployment';
type Context = Pick<Game, 'advanced' | 'players' | 'homeworlds' | 'storm' | 'mobileStronghold' | 'moritaniTerror'>;
export type HomeworldRevivalDestination = {
id: string;
name: string;
territory?: string;
sector?: number;
homeworld?: string;
blocked: string | null;
};
/** Legacy calls place the complete earned group in one destination. New Southern
* allocations may quote an exact subset of the original eligible stars.
* The engine owns provisional SEND-entry reactions and their continuation. No token face,
* opposing hand, spice balance or concealed denomination influences these lists. */
export function homeworldRevivalDestinations(
g: Context,
player: string,
grant: HomeworldRevivalDeploymentQuote,
): HomeworldRevivalDestination[] {
const p = g.players.find((seat) => seat.id === player);
if (!p || !g.homeworlds?.custody)
throw new HomeworldCustodyError('Revival placement needs its seated native Homeworld owner.');
const destinations: HomeworldRevivalDestination[] = gameTerritories(g).flatMap((t) => {
if (grant.kind === 'fedaykin' && presenceAt(p, t.id) === 0) return [];
return t.sectors.map((sector) => {
const effectiveSector = t.id === MOBILE_STRONGHOLD
? splitLocation(g.mobileStronghold!.location!).sector : sector;
let blocked = grant.blocked;
if (!blocked && t.id === MOBILE_STRONGHOLD)
blocked = grant.kind === 'fedaykin'
? 'Southern Hemisphere placement does not bypass the mobile stronghold’s entry route. Reach its pointing territory and enter by Force Movement.'
: 'Only Ixians may enter the mobile stronghold directly from off-planet reserves. Tleilax revival deployment does not grant that route.';
if (!blocked && effectiveSector !== 0 && effectiveSector === g.storm)
blocked = grant.kind === 'fedaykin'
? 'Southern Hemisphere placement must use a sector outside the storm. The Fremen storm-shipment exception does not grant revival placement.'
: 'Revival deployment cannot enter a sector in storm.';
blocked ??= territoryEntryBlock(g.players, p.id, t.id);
return { id: location(t.id, sector), name: `${t.name}${sector ? ` · sector ${sector}` : ''}`,
territory: t.id, sector, blocked };
});
});
if (grant.kind === 'tleilax') {
for (const home of homeworldForceGroups(homeworldContext(g), g.homeworlds.custody)) {
const native = g.players.find((seat) => seat.id === home.native)!;
const card = HOMEWORLD_CARDS.find((card) => card.faction === native.faction && (card.id === 'salusa_secundus') === home.secondary)!;
destinations.push({ id: home.id, name: card.name, homeworld: home.id,
blocked: grant.blocked ??
(p.ally === native.id && native.ally === p.id ? 'You cannot deploy to your ally’s Homeworld.' : null),
});
}
}
return destinations;
}
/** A custody quote only: the engine commits the physical arrival and its
* Ambassador continuation. No fee, new revival or shipment allowance is used. */
export function quoteHomeworldRevivalDestination(
g: Context,
player: string,
grant: HomeworldRevivalDeploymentQuote,
destination: string,
fedaykinAmount?: number,
) {
if (fedaykinAmount !== undefined && (grant.kind !== 'fedaykin' || grant.normal !== 0 ||
!Number.isSafeInteger(fedaykinAmount) || fedaykinAmount < 1 || fedaykinAmount > grant.elite))
throw new HomeworldCustodyError('Choose only a positive subset of the original revived Fedaykin grant.');
const selected = homeworldRevivalDestinations(g, player, grant).find((d) => d.id === destination);
if (!selected || selected.blocked)
throw new HomeworldCustodyError(selected?.blocked ?? 'Choose a currently eligible revival destination.');
const forces = { normal: grant.normal, elite: fedaykinAmount ?? grant.elite };
const context = homeworldContext(g);
if (selected.homeworld) {
const native = homeworldForceGroups(context, g.homeworlds!.custody!)
.find((home) => home.native === player && !home.secondary)!;
const transfer = quoteHomeworldCustody(context, g.homeworlds!.custody!, [
{ homeworld: native.id, player, withdraw: forces, deposit: { normal: 0, elite: 0 } },
{ homeworld: selected.homeworld, player, withdraw: { normal: 0, elite: 0 }, deposit: forces },
]);
return { selected, transfer };
}
return { selected, transfer: quoteNativeReserveWithdrawal(context, g.homeworlds!.custody!, player, forces) };
}