62 lines
2.9 KiB
TypeScript
62 lines
2.9 KiB
TypeScript
import { coalitionOf, CoalitionAllianceError, type CoalitionState } from './coalition-alliance';
|
|
|
|
/** Only public roster and rule-profile data are required to select a side.
|
|
* Inspection answers and sealed plans are deliberately absent. */
|
|
export type CoalitionBattlePreparationContext = {
|
|
status: string;
|
|
players: readonly { id: string; faction: string; ally?: string | null; allySinceTurn?: number }[];
|
|
coalitionState?: CoalitionState;
|
|
advanced?: boolean;
|
|
expansions?: readonly unknown[];
|
|
homeworlds?: unknown;
|
|
nexusCards?: unknown;
|
|
leaderSkills?: unknown;
|
|
techTokens?: unknown;
|
|
strongholdCards?: unknown;
|
|
ecazTreachery?: unknown;
|
|
discoveryEnabled?: unknown;
|
|
};
|
|
|
|
const classicFactions = new Set(['atreides', 'beneGesserit', 'emperor', 'fremen', 'guild', 'harkonnen']);
|
|
|
|
/** Select the owner or a single allied combatant for native Voice or
|
|
* Prescience. The unexposed group extension is limited to the same bare Basic
|
|
* classic profile as its other faction powers. Ambiguous group battles fail
|
|
* closed. `requireMutualPair` preserves the stricter legacy Truthtrance quote;
|
|
* normal battle preparation historically accepted the owner's scalar ally. */
|
|
export function coalitionBattleBeneficiary(
|
|
game: CoalitionBattlePreparationContext,
|
|
ownerId: string,
|
|
attacker: string,
|
|
defender: string,
|
|
requireMutualPair = false,
|
|
): string | null {
|
|
if (!Array.isArray(game.players) || !ownerId || !attacker || !defender || attacker === defender) return null;
|
|
const owner = game.players.find(player => player.id === ownerId);
|
|
if (!owner || !game.players.some(player => player.id === attacker) ||
|
|
!game.players.some(player => player.id === defender)) return null;
|
|
const combatants = [attacker, defender];
|
|
if (game.coalitionState && game.status === 'playing' && game.advanced === false &&
|
|
Array.isArray(game.expansions) && game.expansions.length === 0 &&
|
|
!game.homeworlds && !game.nexusCards && !game.leaderSkills && !game.techTokens &&
|
|
!game.strongholdCards && !game.ecazTreachery && !game.discoveryEnabled &&
|
|
game.players.every(player => classicFactions.has(player.faction))) {
|
|
try {
|
|
const seats = game.players.map(player => ({id: player.id, ally: player.ally ?? null,
|
|
...(player.allySinceTurn !== undefined ? {allySinceTurn: player.allySinceTurn} : {})}));
|
|
const group = coalitionOf(seats, game.coalitionState, ownerId);
|
|
if (group && group.members.length >= 3) {
|
|
const members = combatants.filter(player => group.members.includes(player));
|
|
return members.length === 1 ? members[0] : null;
|
|
}
|
|
} catch (error) {
|
|
if (error instanceof CoalitionAllianceError) return null;
|
|
throw error;
|
|
}
|
|
}
|
|
if (combatants.includes(owner.id)) return owner.id;
|
|
if (!owner.ally || !combatants.includes(owner.ally)) return null;
|
|
if (requireMutualPair && !game.players.some(player => player.id === owner.ally && player.ally === owner.id)) return null;
|
|
return owner.ally;
|
|
}
|