import { FACTIONS } from './catalog'; import { mixedE3NexusRosterProfile } from './faction-module-profile'; import { classicDiscoveryLeaderSkillsProfile } from './discovery-module-profile'; /** Public configuration only; never gate a skill on a hidden card or random deal. */ export type LeaderSkillProfile = { expansions: readonly string[]; advanced?: boolean; players?: readonly { faction: string }[]; homeworlds?: unknown; nexusCards?: unknown; discoveries?: unknown; discoveryEnabled?: unknown; discoveryStash?: unknown; greatMaker?: unknown; strongholdCards?: unknown; techTokens?: unknown; ecazTreachery?: unknown; semutaPreview?: unknown; moritaniAssassinatePreview?: unknown; advancedPreview?: unknown; kullPreview?: unknown; nexusKullPreview?: unknown; guildBetrayalPreview?: unknown; richeseBetrayalPreview?: unknown; nexusIxianReplacementPreview?: unknown; nexusIxianBetrayalPreview?: unknown; nexusHarkonnenBetrayalPreview?: unknown; spiceBankerIncomePreview?: unknown; }; export function noOtherLeaderSkillModules(game: LeaderSkillProfile): boolean { return !game.homeworlds && !game.nexusCards && !game.discoveries && !game.discoveryEnabled && !game.strongholdCards && !game.techTokens && !game.ecazTreachery; } function nexusLeaderSkillModulesSupported(game: LeaderSkillProfile, assassination = false, discovery = false, betrayal = false, bankerIncome = false): boolean { return !!game.nexusCards && typeof game.advanced === 'boolean' && !!game.players && game.players.length >= 2 && game.players.length <= 6 && ((discovery && game.discoveryEnabled === true) || (!game.discoveries && !game.discoveryEnabled && !game.discoveryStash && !game.greatMaker)) && (!game.techTokens || game.players.length >= 3) && (!game.strongholdCards || game.advanced === true) && !game.ecazTreachery && (!game.semutaPreview || (semutaLeaderSkillContext(game) && !game.homeworlds && !game.discoveryEnabled && !game.discoveries)) && (!game.moritaniAssassinatePreview || assassination) && !game.advancedPreview && !game.kullPreview && !game.nexusKullPreview && (!game.guildBetrayalPreview || betrayal) && !game.richeseBetrayalPreview && !game.nexusIxianReplacementPreview && !game.nexusIxianBetrayalPreview && !game.nexusHarkonnenBetrayalPreview && (!game.spiceBankerIncomePreview || bankerIncome); } /** Fresh classic Nexus/Skills, optionally Homeworlds/Discovery/Tech/Advanced Strongholds. */ export function classicNexusLeaderSkillsProfile(game: LeaderSkillProfile): boolean { if (game.expansions.length || !nexusLeaderSkillModulesSupported(game, false, true, false, true)) return false; if (game.spiceBankerIncomePreview && (game.homeworlds || game.discoveryEnabled || game.discoveries || game.discoveryStash || game.greatMaker)) return false; const players = game.players!; for (let i = 0; i < players.length; i++) { for (let j = 0; j < i; j++) if (players[j].faction === players[i].faction) return false; let classic = false; for (const faction of FACTIONS) if (faction.id === players[i].faction) { classic = faction.expansion === 'base'; break; } if (!classic) return false; } return true; } /** Fresh classic Nexus/Skills composed only with the bounded Guild Betrayal preview. */ export function guildBetrayalNexusLeaderSkillsProfile(game: LeaderSkillProfile): boolean { if (game.expansions.length || !game.guildBetrayalPreview || game.homeworlds || game.techTokens || game.strongholdCards || game.semutaPreview || !nexusLeaderSkillModulesSupported(game, false, false, true)) return false; const players = game.players!; for (let i = 0; i < players.length; i++) { for (let j = 0; j < i; j++) if (players[j].faction === players[i].faction) return false; let classic = false; for (const faction of FACTIONS) if (faction.id === players[i].faction) { classic = faction.expansion === 'base'; break; } if (!classic) return false; } return players.some(player => player.faction === 'guild'); } /** Both original natives in one E1/E2 family, optionally Homeworlds/Discovery/Tech/Strongholds. */ export function pairedNexusLeaderSkillsProfile(game: LeaderSkillProfile): boolean { if (game.expansions.length !== 1 || !nexusLeaderSkillModulesSupported(game, false, true)) return false; const expansion = game.expansions[0]; if (expansion !== 'ix' && expansion !== 'choam') return false; const primary = expansion === 'ix' ? 'ixians' : 'choam'; const secondary = expansion === 'ix' ? 'tleilaxu' : 'richese'; const players = game.players!; let hasPrimary = false, hasSecondary = false; for (let i = 0; i < players.length; i++) { for (let j = 0; j < i; j++) if (players[j].faction === players[i].faction) return false; let family: string | undefined; for (const faction of FACTIONS) if (faction.id === players[i].faction) { family = faction.expansion; break; } if (family !== 'base' && family !== expansion) return false; if (players[i].faction === primary) hasPrimary = true; if (players[i].faction === secondary) hasSecondary = true; } return hasPrimary && hasSecondary; } /** Exactly one original E1/E2 native with classics; paired/mixed rosters stay separate. */ export function singleE1E2NexusLeaderSkillsProfile(game: LeaderSkillProfile): boolean { if (game.expansions.length !== 1 || !nexusLeaderSkillModulesSupported(game, false, true)) return false; const expansion = game.expansions[0]; if (expansion !== 'ix' && expansion !== 'choam') return false; const players = game.players!; let native = 0; for (let i = 0; i < players.length; i++) { for (let j = 0; j < i; j++) if (players[j].faction === players[i].faction) return false; let family: string | undefined; for (const faction of FACTIONS) if (faction.id === players[i].faction) { family = faction.expansion; break; } if (family !== 'base' && family !== expansion) return false; if (family === expansion) native++; } return native === 1; } /** One original E3 native with classics and optional Homeworlds/Discovery; no shared Duke or mixed family. */ export function standaloneE3NexusLeaderSkillsProfile(game: LeaderSkillProfile): boolean { if (game.expansions.length !== 1 || game.expansions[0] !== 'ecaz' || !nexusLeaderSkillModulesSupported(game, game.advanced === true, true)) return false; const players = game.players!; let native: 'ecaz' | 'moritani' | null = null, harkonnen = false; for (let i = 0; i < players.length; i++) { for (let j = 0; j < i; j++) if (players[j].faction === players[i].faction) return false; const player = players[i]; if (player.faction === 'ecaz' || player.faction === 'moritani') { if (native !== null) return false; native = player.faction; } else { let classic = false; for (const faction of FACTIONS) if (faction.id === player.faction) { classic = faction.expansion === 'base'; break; } if (!classic) return false; if (player.faction === 'harkonnen') harkonnen = true; } } return native !== null && !(game.advanced && harkonnen) && (native === 'moritani' || !game.moritaniAssassinatePreview); } /** Original E1/E2 natives from both selected families with classics and Nexus. */ export function mixedE1E2NexusLeaderSkillsProfile(game: LeaderSkillProfile): boolean { if (game.expansions.length !== 2 || !game.expansions.includes('ix') || !game.expansions.includes('choam') || !nexusLeaderSkillModulesSupported(game, false, true)) return false; const players = game.players!; let ix = false, choam = false; for (let i = 0; i < players.length; i++) { for (let j = 0; j < i; j++) if (players[j].faction === players[i].faction) return false; let family: string | undefined; for (const faction of FACTIONS) if (faction.id === players[i].faction) { family = faction.expansion; break; } if (family !== 'base' && family !== 'ix' && family !== 'choam') return false; if (family === 'ix') ix = true; if (family === 'choam') choam = true; } return ix && choam; } /** Both original E3 natives and classic opponents; the shared Duke is never a training disc. */ export function pairedE3NexusLeaderSkillsProfile(game: LeaderSkillProfile): boolean { if (game.expansions.length !== 1 || game.expansions[0] !== 'ecaz' || !nexusLeaderSkillModulesSupported(game, game.advanced === true, true)) return false; const players = game.players!; let ecaz = false, moritani = false; for (let i = 0; i < players.length; i++) { for (let j = 0; j < i; j++) if (players[j].faction === players[i].faction) return false; const player = players[i]; if (player.faction === 'ecaz') ecaz = true; else if (player.faction === 'moritani') moritani = true; else { if (game.advanced && player.faction === 'harkonnen') return false; let classic = false; for (const faction of FACTIONS) if (faction.id === player.faction) { classic = faction.expansion === 'base'; break; } if (!classic) return false; } } return ecaz && moritani; } /** Original two/three selected families with native E3, Nexus and optional original modules. */ export function mixedE3NexusLeaderSkillsProfile(game: LeaderSkillProfile): boolean { return nexusLeaderSkillModulesSupported(game, game.advanced === true, true) && mixedE3NexusRosterProfile(game, true); } /** Discovery keeps original native roster predicates; only this module envelope * differs. Live token/entry/Great Maker frames are original consumers. */ function discoveryLeaderSkillModulesSupported(game: LeaderSkillProfile): boolean { if (game.homeworlds) return game.discoveryEnabled === true && homeworldLeaderSkillModulesSupported(game); const players = game.players; return typeof game.advanced === 'boolean' && game.discoveryEnabled === true && !!players && players.length >= 2 && players.length <= 6 && (!game.techTokens || players.length >= 3) && (!game.strongholdCards || game.advanced) && game.expansions.length >= 1 && game.expansions.length <= 2 && !game.homeworlds && !game.nexusCards && !game.ecazTreachery && !game.semutaPreview && !game.advancedPreview && !game.kullPreview && !game.nexusKullPreview && !game.guildBetrayalPreview && !game.richeseBetrayalPreview && !game.nexusIxianReplacementPreview && !game.nexusIxianBetrayalPreview && !game.nexusHarkonnenBetrayalPreview && !game.spiceBankerIncomePreview; } /** Recognize only the explicit overlay; native predicates still own roster/module legality. */ function semutaLeaderSkillContext(game: LeaderSkillProfile): boolean { return game.semutaPreview === true && !game.spiceBankerIncomePreview && game.expansions.includes('choam') && !!game.players?.some(p => p.faction === 'richese'); } /** E1/E2 native skills may add Tech and Advanced Strongholds; original roster/decks remain. */ function nativeLeaderSkillModulesSupported(game: LeaderSkillProfile): boolean { if (game.homeworlds) return homeworldLeaderSkillModulesSupported(game); if (noOtherLeaderSkillModules(game) || pairedNexusLeaderSkillsProfile(game) || singleE1E2NexusLeaderSkillsProfile(game) || mixedE1E2NexusLeaderSkillsProfile(game)) return true; if (game.discoveryEnabled || game.discoveries) return discoveryLeaderSkillModulesSupported(game); const players = game.players; if ((!game.techTokens && !game.strongholdCards) || !players || players.length < (game.techTokens ? 3 : 2) || players.length > 6 || (game.strongholdCards && game.advanced !== true) || game.expansions.length < 1 || game.expansions.length > 2 || game.homeworlds || game.nexusCards || game.discoveries || game.discoveryEnabled || game.ecazTreachery || (game.semutaPreview && !semutaLeaderSkillContext(game)) || game.moritaniAssassinatePreview || game.advancedPreview || game.kullPreview || game.nexusKullPreview || game.guildBetrayalPreview || game.richeseBetrayalPreview || game.nexusIxianReplacementPreview || game.nexusIxianBetrayalPreview || game.nexusHarkonnenBetrayalPreview) return false; for (const expansion of game.expansions) if (expansion !== 'ix' && expansion !== 'choam') return false; for (let i = 0; i < players.length; i++) for (let j = 0; j < i; j++) if (players[j].faction === players[i].faction) return false; return true; } /** Standalone E3 native predicates retain their own roster, Duke and assassination limits. */ function e3LeaderSkillModulesSupported(game: LeaderSkillProfile): boolean { if (game.homeworlds) return homeworldLeaderSkillModulesSupported(game); if (noOtherLeaderSkillModules(game) || standaloneE3NexusLeaderSkillsProfile(game) || pairedE3NexusLeaderSkillsProfile(game)) return true; if (game.discoveryEnabled || game.discoveries) return discoveryLeaderSkillModulesSupported(game); const players = game.players; if ((!game.techTokens && !game.strongholdCards) || !players || players.length < (game.techTokens ? 3 : 2) || players.length > 6 || (game.strongholdCards && game.advanced !== true) || game.homeworlds || game.nexusCards || game.discoveries || game.discoveryEnabled || game.ecazTreachery || game.semutaPreview || game.advancedPreview || game.kullPreview || game.nexusKullPreview || game.guildBetrayalPreview || game.richeseBetrayalPreview || game.nexusIxianReplacementPreview || game.nexusIxianBetrayalPreview || game.nexusHarkonnenBetrayalPreview) return false; for (let i = 0; i < players.length; i++) for (let j = 0; j < i; j++) if (players[j].faction === players[i].faction) return false; return true; } /** Classic three-to-six-seat Skills/Tech composition; native and other modules stay separate. */ export function classicTechLeaderSkillsProfile(game: LeaderSkillProfile): boolean { const players = game.players; if (!game.techTokens || typeof game.advanced !== 'boolean' || game.expansions.length || !players || players.length < 3 || players.length > 6 || game.homeworlds || game.nexusCards || game.discoveries || game.discoveryEnabled || game.strongholdCards || game.ecazTreachery || game.semutaPreview || game.moritaniAssassinatePreview || game.advancedPreview || game.kullPreview || game.nexusKullPreview || game.guildBetrayalPreview || game.richeseBetrayalPreview || game.nexusIxianReplacementPreview || game.nexusIxianBetrayalPreview || game.nexusHarkonnenBetrayalPreview) return false; for (let i = 0; i < players.length; i++) { for (let j = 0; j < i; j++) if (players[j].faction === players[i].faction) return false; let classic = false; for (const faction of FACTIONS) if (faction.id === players[i].faction && faction.expansion === 'base') { classic = true; break; } if (!classic) return false; } return true; } /** Original Advanced Strongholds, with optional Tech, for classic or supported native skills. */ export function strongholdLeaderSkillsProfile(game: LeaderSkillProfile): boolean { if (!game.strongholdCards || game.advanced !== true) return false; if (game.expansions.length) return advancedNativeLeaderSkillsProfile(game) || advancedMoritaniLeaderSkillsProfile(game) || nativeEcazLeaderSkillsProfile(game); const players = game.players; if (!players || players.length < (game.techTokens ? 3 : 2) || players.length > 6 || game.homeworlds || game.nexusCards || game.discoveries || game.discoveryEnabled || game.ecazTreachery || game.semutaPreview || game.moritaniAssassinatePreview || game.advancedPreview || game.kullPreview || game.nexusKullPreview || game.guildBetrayalPreview || game.richeseBetrayalPreview || game.nexusIxianReplacementPreview || game.nexusIxianBetrayalPreview || game.nexusHarkonnenBetrayalPreview) return false; for (let i = 0; i < players.length; i++) { for (let j = 0; j < i; j++) if (players[j].faction === players[i].faction) return false; let classic = false; for (const faction of FACTIONS) if (faction.id === players[i].faction && faction.expansion === 'base') { classic = true; break; } if (!classic) return false; } return true; } /** Bounded integration of the ordinary Moritani roster, not Advanced assassination. */ export function basicMoritaniLeaderSkillsProfile(game: LeaderSkillProfile): boolean { return game.advanced === false && game.expansions.length === 1 && game.expansions[0] === 'ecaz' && e3LeaderSkillModulesSupported(game) && !!game.players?.some(player => player.faction === 'moritani') && game.players.every(player => player.faction === 'moritani' || FACTIONS.some(faction => faction.id === player.faction && faction.expansion === 'base')); } /** Existing bounded Advanced assassination roster, composed with native skills. */ export function advancedMoritaniLeaderSkillsProfile(game: LeaderSkillProfile): boolean { const players = game.players; if (game.advanced !== true || game.expansions.length !== 1 || game.expansions[0] !== 'ecaz' || !players || players.length < 2 || players.length > 6 || !e3LeaderSkillModulesSupported(game) || game.semutaPreview || game.advancedPreview || game.kullPreview || game.nexusKullPreview || game.guildBetrayalPreview || game.richeseBetrayalPreview || game.nexusIxianReplacementPreview || game.nexusIxianBetrayalPreview || game.nexusHarkonnenBetrayalPreview) return false; let native = false; for (let i = 0; i < players.length; i++) { const player = players[i]; for (let j = 0; j < i; j++) if (players[j].faction === player.faction) return false; if (player.faction === 'moritani') native = true; else { if (player.faction === 'harkonnen') return false; let classic = false; for (const faction of FACTIONS) if (faction.id === player.faction && faction.expansion === 'base') { classic = true; break; } if (!classic) return false; } } return native; } /** Ecaz's five native discs; the separate temporarily controlled Duke is not an assignment disc. */ export function nativeEcazLeaderSkillsProfile(game: LeaderSkillProfile): boolean { const players = game.players; if (typeof game.advanced !== 'boolean' || game.expansions.length !== 1 || game.expansions[0] !== 'ecaz' || !players || players.length < 2 || players.length > 6 || !e3LeaderSkillModulesSupported(game) || game.semutaPreview || game.moritaniAssassinatePreview || game.advancedPreview || game.kullPreview || game.nexusKullPreview || game.guildBetrayalPreview || game.richeseBetrayalPreview || game.nexusIxianReplacementPreview || game.nexusIxianBetrayalPreview || game.nexusHarkonnenBetrayalPreview) return false; let native = false; for (let i = 0; i < players.length; i++) { const player = players[i]; for (let j = 0; j < i; j++) if (players[j].faction === player.faction) return false; if (player.faction === 'ecaz') native = true; else { if (game.advanced && player.faction === 'harkonnen') return false; let classic = false; for (const faction of FACTIONS) if (faction.id === player.faction && faction.expansion === 'base') { classic = true; break; } if (!classic) return false; } } return native; } /** Native Basic Tleilaxu, including Face Dancers; foreign gholas are Advanced. */ export function basicTleilaxuLeaderSkillsProfile(game: LeaderSkillProfile): boolean { return game.advanced === false && game.expansions.length === 1 && game.expansions[0] === 'ix' && nativeLeaderSkillModulesSupported(game) && !!game.players?.some(player => player.faction === 'tleilaxu') && game.players.every(player => player.faction === 'tleilaxu' || FACTIONS.some(faction => faction.id === player.faction && faction.expansion === 'base')); } /** Basic Ixians, optionally with native Tleilaxu, keep the ordinary Ix inventories. */ export function basicIxLeaderSkillsProfile(game: LeaderSkillProfile): boolean { return game.advanced === false && game.expansions.length === 1 && game.expansions[0] === 'ix' && nativeLeaderSkillModulesSupported(game) && !!game.players?.some(player => player.faction === 'ixians') && game.players.every(player => ['ixians', 'tleilaxu'].includes(player.faction) || FACTIONS.some(faction => faction.id === player.faction && faction.expansion === 'base')); } /** Basic CHOAM's ordinary leaders and economy; Auditor belongs to Advanced. */ export function basicChoamLeaderSkillsProfile(game: LeaderSkillProfile): boolean { return game.advanced === false && game.expansions.length === 1 && game.expansions[0] === 'choam' && nativeLeaderSkillModulesSupported(game) && !!game.players?.some(player => player.faction === 'choam') && game.players.every(player => player.faction === 'choam' || FACTIONS.some(faction => faction.id === player.faction && faction.expansion === 'base')); } /** Native Basic Richese/CHOAM setup, without unrelated optional overlays. */ export function basicRicheseLeaderSkillsProfile(game: LeaderSkillProfile): boolean { const players = game.players; if (game.advanced !== false || game.expansions.length !== 1 || game.expansions[0] !== 'choam' || !players || players.length < 2 || players.length > 6 || !nativeLeaderSkillModulesSupported(game) || (game.semutaPreview && !semutaLeaderSkillContext(game)) || game.moritaniAssassinatePreview || game.advancedPreview || game.kullPreview || game.nexusKullPreview || game.guildBetrayalPreview || game.richeseBetrayalPreview || game.nexusIxianReplacementPreview || game.nexusIxianBetrayalPreview || game.nexusHarkonnenBetrayalPreview) return false; let native = false; for (let i = 0; i < players.length; i++) { const player = players[i]; for (let j = 0; j < i; j++) if (players[j].faction === player.faction) return false; if (player.faction === 'richese') native = true; else if (player.faction !== 'choam') { let classic = false; for (const faction of FACTIONS) if (faction.id === player.faction && faction.expansion === 'base') { classic = true; break; } if (!classic) return false; } } return native; } export function basicExpansionLeaderSkillsProfile(game: LeaderSkillProfile): boolean { return basicMoritaniLeaderSkillsProfile(game) || basicTleilaxuLeaderSkillsProfile(game) || basicIxLeaderSkillsProfile(game) || basicChoamLeaderSkillsProfile(game) || basicRicheseLeaderSkillsProfile(game); } /** Source-clear Advanced native factions; optional decks never imply a seated faction. */ export function advancedNativeLeaderSkillsProfile(game: LeaderSkillProfile): boolean { const players = game.players; if (game.advanced !== true || !players || players.length < 2 || players.length > 6 || game.expansions.length < 1 || game.expansions.length > 2 || (game.expansions.length === 2 && game.expansions[0] === game.expansions[1]) || !nativeLeaderSkillModulesSupported(game) || (game.semutaPreview && !semutaLeaderSkillContext(game)) || game.moritaniAssassinatePreview || game.advancedPreview || game.kullPreview || game.nexusKullPreview || game.guildBetrayalPreview || game.richeseBetrayalPreview || game.nexusIxianReplacementPreview || game.nexusIxianBetrayalPreview || game.nexusHarkonnenBetrayalPreview) return false; for (const expansion of game.expansions) if (expansion !== 'ix' && expansion !== 'choam') return false; let native = false; for (let i = 0; i < players.length; i++) { const player = players[i]; // Six seats at most: check prior seats without allocating per-rule quote sets. for (let j = 0; j < i; j++) if (players[j].faction === player.faction) return false; if (player.faction === 'ixians' || player.faction === 'tleilaxu' || player.faction === 'choam' || player.faction === 'richese') { if (!game.expansions.includes(player.faction === 'choam' || player.faction === 'richese' ? 'choam' : 'ix')) return false; native = true; } else { let classic = false; for (const faction of FACTIONS) if (faction.id === player.faction && faction.expansion === 'base') { classic = true; break; } if (!classic) return false; } } return native; } export function nativeExpansionLeaderSkillsProfile(game: LeaderSkillProfile): boolean { return mixedE3NexusLeaderSkillsProfile(game) || mixedE1E2NexusLeaderSkillsProfile(game) || pairedE3NexusLeaderSkillsProfile(game) || basicExpansionLeaderSkillsProfile(game) || advancedNativeLeaderSkillsProfile(game) || advancedMoritaniLeaderSkillsProfile(game) || nativeEcazLeaderSkillsProfile(game); } /** Fresh/live original native all14/Discovery; existing family limits stay authoritative. */ export function nativeDiscoveryLeaderSkillsProfile(game: LeaderSkillProfile): boolean { return game.discoveryEnabled === true && nativeExpansionLeaderSkillsProfile(game); } /** Only the original eligible native skill families gain the Tech combination. */ export function nativeTechLeaderSkillsProfile(game: LeaderSkillProfile): boolean { return !!game.techTokens && nativeExpansionLeaderSkillsProfile(game); } /** Original Homeworld module envelope; native roster/deck predicates stay authoritative. */ function homeworldLeaderSkillModulesSupported(game: LeaderSkillProfile): boolean { const players = game.players; if (!game.homeworlds || typeof game.advanced !== 'boolean' || !players || players.length < 2 || players.length > 6 || (game.nexusCards && !classicNexusLeaderSkillsProfile(game) && !pairedNexusLeaderSkillsProfile(game) && !singleE1E2NexusLeaderSkillsProfile(game) && !mixedE1E2NexusLeaderSkillsProfile(game) && !pairedE3NexusLeaderSkillsProfile(game) && !standaloneE3NexusLeaderSkillsProfile(game)) || (game.discoveryEnabled !== true && (game.discoveryEnabled || game.discoveries || game.discoveryStash || game.greatMaker)) || (game.techTokens && players.length < 3) || (game.strongholdCards && !game.advanced) || game.ecazTreachery || game.semutaPreview || game.advancedPreview || (game.moritaniAssassinatePreview && (!game.advanced || game.expansions.length !== 1 || game.expansions[0] !== 'ecaz' || !players.some(player => player.faction === 'moritani'))) || game.kullPreview || game.nexusKullPreview || game.guildBetrayalPreview || game.richeseBetrayalPreview || game.nexusIxianReplacementPreview || game.nexusIxianBetrayalPreview || game.nexusHarkonnenBetrayalPreview || (game.spiceBankerIncomePreview && (game.expansions.length || game.nexusCards || game.discoveryEnabled || game.discoveries || game.discoveryStash || game.greatMaker || game.techTokens || game.strongholdCards))) return false; return true; } /** Original Homeworld/Skills under the existing classic or native Nexus roster contracts. */ export function homeworldLeaderSkillsProfile(game: LeaderSkillProfile): boolean { if (!game.homeworlds) return false; if (game.expansions.length) return nativeExpansionLeaderSkillsProfile(game); if (!homeworldLeaderSkillModulesSupported(game)) return false; const players = game.players!; for (let i = 0; i < players.length; i++) { for (let j = 0; j < i; j++) if (players[j].faction === players[i].faction) return false; let classic = false; for (const faction of FACTIONS) if (faction.id === players[i].faction) { classic = faction.expansion === 'base'; break; } if (!classic) return false; } return true; } /** Shared by rule quotes, controls and minimal legal bot participation. */ export function ordinaryLeaderSkillModeSupported(game: LeaderSkillProfile): boolean { return (noOtherLeaderSkillModules(game) && (!game.expansions.length || nativeExpansionLeaderSkillsProfile(game))) || classicTechLeaderSkillsProfile(game) || nativeTechLeaderSkillsProfile(game) || strongholdLeaderSkillsProfile(game) || classicNexusLeaderSkillsProfile(game) || guildBetrayalNexusLeaderSkillsProfile(game) || singleE1E2NexusLeaderSkillsProfile(game) || mixedE3NexusLeaderSkillsProfile(game) || mixedE1E2NexusLeaderSkillsProfile(game) || pairedE3NexusLeaderSkillsProfile(game) || pairedNexusLeaderSkillsProfile(game) || standaloneE3NexusLeaderSkillsProfile(game) || classicDiscoveryLeaderSkillsProfile(game) || nativeDiscoveryLeaderSkillsProfile(game) || homeworldLeaderSkillsProfile(game); }