Support unused selected decks with Nexus and Discovery
Verify and publish checkpoint container / container (push) Canceled after 0s

This commit is contained in:
Quintine committed 2026-10-29 20:19:58 +11:00
1 parent c8dbd44975
commit 91d45d1dec
14 files changed
+405 -17

No files matched your search

+4 -5
View File
@@ -1,5 +1,5 @@
import { FACTIONS } from './catalog';
import { independentDiscoveryProfile, mixedE3NexusRosterProfile, selectedPhaseModuleRoster, type FactionModuleProfile } from './faction-module-profile';
import { independentDiscoveryProfile, mixedE3NexusRosterProfile, selectedNexusWithoutCardsProfile, selectedPhaseModuleRoster, type FactionModuleProfile } from './faction-module-profile';
import type { LeaderSkillProfile } from './leader-skill-profile';
/** Public unused-lobby admission only. The original initializer owns physical
@@ -17,7 +17,7 @@ export function homeworldDiscoveryProfile(game: FactionModuleProfile): boolean {
/** The same roster/module contract during play; live Discovery frames are expected. */
export function discoveryModeSupported(game: FactionModuleProfile): boolean {
if (independentDiscoveryProfile(game)) return true;
if (game.nexusCards) return standaloneE3DiscoveryNexusProfile(game) || singleE1E2DiscoveryNexusProfile(game) ||
if (game.nexusCards) return (game.discoveryEnabled === true && selectedNexusWithoutCardsProfile(game)) || standaloneE3DiscoveryNexusProfile(game) || singleE1E2DiscoveryNexusProfile(game) ||
mixedE3DiscoveryNexusProfile(game) || mixedE1E2DiscoveryNexusProfile(game) || pairedE3DiscoveryNexusProfile(game);
if (typeof game.advanced !== 'boolean' || game.discoveryEnabled !== true ||
game.players.length < 2 || game.players.length > 6 ||
@@ -41,7 +41,7 @@ export function discoveryModeSupported(game: FactionModuleProfile): boolean {
if (e3 ? expansion !== 'ecaz' : expansion !== 'ix' && expansion !== 'choam') return false;
for (let j = 0; j < i; j++) if (game.expansions[j] === expansion) return false;
}
let native = 0, moritani = false, harkonnen = false;
let moritani = false, harkonnen = false;
for (let i = 0; i < game.players.length; i++) {
const player = game.players[i];
for (let j = 0; j < i; j++) if (game.players[j].faction === player.faction) return false;
@@ -52,12 +52,11 @@ export function discoveryModeSupported(game: FactionModuleProfile): boolean {
if (expansion !== 'base') {
if (e3 ? player.faction !== 'ecaz' && player.faction !== 'moritani'
: !game.expansions.includes(expansion)) return false;
native++;
}
if (player.faction === 'moritani') moritani = true;
if (player.faction === 'harkonnen') harkonnen = true;
}
return (!e3 || native >= 1 && native <= 2) && !(game.advanced && moritani && harkonnen);
return !(game.advanced && moritani && harkonnen);
}
/** The original E3 pair can use selected decks with Discovery without Nexus or cards. */
+9 -8
View File
@@ -99,7 +99,7 @@ import { movementShipmentPreparations } from './movement-shipment-preparation';
import { MovementClaimError, liveMovementPromises, movementEventAllowed, movementPromiseModeSupported, movementPromiseTargetAvailable, settleMovementEvent, finishMovementPromises, validMovementClaim, type MovementClaim, type MovementPromise } from './movement-promises';
import { findMovementCompletion, type MovementCompletion } from './movement-promise-search';
import { classicNexusModulesProfile, mixedE1E2NexusProfile, mixedE3NexusProfile, pairedE3NexusProfile, pairedNexusModulesProfile, singleE1E2NexusProfile, standaloneE3NexusProfile } from './nexus-module-profile';
import { independentDiscoveryProfile, independentNexusProfile, nexusCoreProfile, nativeFactionTechProfile, selectedFactionStrongholdProfile, selectedIndependentCardsProfile, selectedPhaseModuleRoster } from './faction-module-profile';
import { independentDiscoveryProfile, independentNexusProfile, nexusCoreProfile, nativeFactionTechProfile, selectedFactionStrongholdProfile, selectedIndependentCardsProfile, selectedNexusWithoutCardsProfile, selectedPhaseModuleRoster } from './faction-module-profile';
import { homeworldOccupationLeaderSkillsProfile, homeworldOccupationModulesProfile, homeworldPhaseModulesProfile } from './homeworld-module-profile';
import { RevivalPromiseError, revivalPromiseModeSupported, liveRevivalPromises, recordPromisedRevival,
revivalPromisesSatisfied, validateRevivalPromises, type RevivalClaim, type RevivalPromise } from './revival-promises';
@@ -11382,7 +11382,7 @@ function e3StrongholdFactionProfile(g: Game): boolean {
return native !== null && (native !== 'moritani' || !byFaction(g, 'harkonnen'));
}
function moritaniAssassinateModeSupported(g: Game) {
if (independentNexusProfile(g) || independentDiscoveryProfile(g)) return g.advanced && !!byFaction(g, 'moritani');
if (independentNexusProfile(g) || independentDiscoveryProfile(g) || selectedNexusWithoutCardsProfile(g)) return g.advanced && !!byFaction(g, 'moritani');
if (mixedE3NexusProfile(g) || mixedE3NexusLeaderSkillsProfile(g) ||
standaloneE3NexusProfile(g) || pairedE3NexusProfile(g) || pairedE3NexusLeaderSkillsProfile(g))
return g.advanced && g.players.some(player => player.faction === 'moritani');
@@ -12477,10 +12477,11 @@ export function initializeNexusGameForAudit(state: Game): Game {
const standalone = standaloneE3NexusProfile(state);
const pairedE3 = pairedE3NexusProfile(state);
const mixedE3 = mixedE3NexusProfile(state);
const native = mixedE3 || standalone || pairedE3 || singleE1E2NexusProfile(state) || mixedE1E2NexusProfile(state);
const selected = selectedNexusWithoutCardsProfile(state);
const native = selected || mixedE3 || standalone || pairedE3 || singleE1E2NexusProfile(state) || mixedE1E2NexusProfile(state);
const discovery = classicDiscoveryNexusProfile(state) || standaloneE3DiscoveryNexusProfile(state) ||
mixedE3DiscoveryNexusProfile(state) || singleE1E2DiscoveryNexusProfile(state) ||
mixedE1E2DiscoveryNexusProfile(state) || pairedE3DiscoveryNexusProfile(state);
mixedE1E2DiscoveryNexusProfile(state) || pairedE3DiscoveryNexusProfile(state) || (selected && !!state.discoveryEnabled);
if (state.discoveryEnabled) {
requireRule(discovery,
'Discovery Nexus requires a fresh supported original classic or native roster and module envelope.');
@@ -12488,8 +12489,8 @@ export function initializeNexusGameForAudit(state: Game): Game {
requireFreshFactionInventory(state);
}
const g = initializeSetupGameForAudit(state, !!state.homeworlds, true, false, discovery, false, false, native);
if ((mixedE3 || standalone || pairedE3) && byFaction(g, 'ecaz')) g.ecazOccupyPreview = true;
if ((mixedE3 || standalone || pairedE3) && g.advanced && byFaction(g, 'moritani')) initializeMoritaniAssassinateState(g);
if ((selected || mixedE3 || standalone || pairedE3) && byFaction(g, 'ecaz')) g.ecazOccupyPreview = true;
if ((selected || mixedE3 || standalone || pairedE3) && g.advanced && byFaction(g, 'moritani')) initializeMoritaniAssassinateState(g);
return g;
}
/** Narrow original standalone admission; normal classics share fresh effect state. */
@@ -13452,7 +13453,7 @@ function ecazOccupyCompositionSupported(g: Game): boolean {
const basicNatives = g.advanced === false && !g.strongholdCards &&
((!g.ecazTreachery && (!g.techTokens || nativeFactionTechProfile(g))) || selectedIndependentCardsProfile(g) ||
g.kullPreview === true && selectedIndependentCardsProfile({...g,kullPreview:undefined}));
if ((independentNexusProfile(g) || independentDiscoveryProfile(g)) && byFaction(g, 'ecaz')) return true;
if ((independentNexusProfile(g) || independentDiscoveryProfile(g) || selectedNexusWithoutCardsProfile(g)) && byFaction(g, 'ecaz')) return true;
if (g.leaderSkills && nativeEcazLeaderSkillsProfile(g)) return true;
if (g.ecazOccupyPreview === true && (mixedE3NexusProfile(g) || standaloneE3NexusProfile(g) || pairedE3NexusProfile(g)) &&
g.players.some(player => player.faction === 'ecaz')) return true;
@@ -25170,7 +25171,7 @@ function arrivalOverlapModeSupported(g: Game, revivalArrival?: RevivalEntryParen
if (homeworldPhaseModulesProfile(g) && byFaction(g, 'ecaz') && byFaction(g, 'moritani')) return true;
if (revivalArrival && g.homeworlds && byFaction(g, 'ecaz') && byFaction(g, 'moritani')) return true;
if (bareBasicSemutaProfile(g) && byFaction(g, 'ecaz') && byFaction(g, 'moritani')) return true;
if ((independentNexusProfile(g) || independentDiscoveryProfile(g)) && byFaction(g, 'ecaz') && byFaction(g, 'moritani')) return true;
if ((independentNexusProfile(g) || independentDiscoveryProfile(g) || selectedNexusWithoutCardsProfile(g)) && byFaction(g, 'ecaz') && byFaction(g, 'moritani')) return true;
if ((mixedE3NexusLeaderSkillsProfile(g) || mixedE3NexusProfile(g)) &&
byFaction(g, 'ecaz') && byFaction(g, 'moritani')) return true;
if (g.ecazTreachery && byFaction(g, 'ecaz') && byFaction(g, 'moritani') &&
+15 -2
View File
@@ -59,10 +59,23 @@ export function independentDiscoveryNexusProfile(game: NexusCoreConfiguration):
return !!game.nexusCards && independentDiscoveryProfile(game);
}
/** Common consumers of the classic and selected-independent fresh Nexus core. */
/** Original selected decks do not require their native factions or the
* independent three-card variant. Keep Skills and hidden E3/Harkonnen
* compositions on their existing separately scoped paths. */
export function selectedNexusWithoutCardsProfile(game: NexusCoreConfiguration): boolean {
const players = game.players;
if (game.ecazTreachery || game.leaderSkills || !game.nexusCards || !players ||
!game.expansions.length || !nexusCoreRosterProfile(game, true, game.discoveryEnabled === true)) return false;
if (game.advanced && players.some(player => player.faction === 'harkonnen') &&
players.some(player => player.faction === 'ecaz' || player.faction === 'moritani')) return false;
return game.expansions.some(expansion => !players.some(player =>
FACTIONS.some(faction => faction.id === player.faction && faction.expansion === expansion)));
}
/** Common consumers of supported classic and selected-deck Nexus games. */
export function nexusCoreProfile(game: NexusCoreConfiguration): boolean {
return classicNexusCoreProfile(game) || selectedIndependentNexusProfile(game) ||
independentDiscoveryNexusProfile(game);
independentDiscoveryNexusProfile(game) || selectedNexusWithoutCardsProfile(game);
}
function nexusCoreRosterProfile(game: NexusCoreConfiguration, selectedDecks: boolean, discovery = false): boolean {
+2 -1
View File
@@ -5925,7 +5925,7 @@ export const RULE_TOPICS: RuleTopic[] = [
summary: 'A development preview connects Great Maker, six Discovery spice blows, private token inspection, stash rewards, revealed locations, next-turn entry, Cistern income, bounded Orgiz transfers, Ornithopter movement, Jacurutu income, Testing Station and Shrine. Supported public starts remain previews; remaining interpretations and complete-mode acceptance stay open.',
searchText: 'Hiereg Smuggler Jacurutu Sietch Cistern Ecological Testing Station Shrine Orgiz Processing Station Treachery Card Stash Spice Stash Ornithopter',
steps: [
'The Discovery-only fresh preview supports classics and selected Ixian/Tleilaxu, CHOAM/Richese and Ecaz/Moritani decks in Basic/Advanced2–6, including mixed selections with unused decks. It supplies seven extra Spice Cards/eight tokens; optional Tech requires3+ and Strongholds require Advanced. Nexus and independent cards are not required. Existing Advanced Moritani/Harkonnen and deeper interaction restrictions remain; complete-mode acceptance stays separate.',
'The Discovery-only fresh preview supports classics and selected Ixian/Tleilaxu, CHOAM/Richese and Ecaz/Moritani decks in Basic/Advanced2–6, including unused selected decks. It supplies seven extra Spice Cards/eight tokens; optional Tech requires3+ and Strongholds require Advanced. Nexus and independent cards are not required. Existing Advanced Moritani/Harkonnen and deeper interaction restrictions remain; complete-mode acceptance stays separate.',
'Fresh Discovery retains original Homeworlds and native reserves in classic and supported E1/E2 or E3 tables, Basic/Advanced2–6, optional Tech3+/Advanced Strongholds. Original Nexus entries additionally admit natives from BOTH Ix+CHOAM families/union47 or BOTH Ecaz+Moritani/ecaz33 with classics, with or without all14 Skills. Maker, actual parent/nested entry, source-local payments/rescue, phase-end industry and original-winner cleanup/Tech remain. Held benefits do not migrate into nested locations. Original capture/roster and unresolved-effect guards remain.',
'The original leader-skills --nexus-cards --discoveries entry composes all14 offered training/all12 Nexus/Discovery7/8 for classics/base33, single or paired one-family E1/E2, natives from BOTH selected Ix+CHOAM families/union47, or standalone OR paired Ecaz/Moritani/ecaz33 with classics. Original Homeworlds may be retained; Tech requires3+ and Strongholds Advanced. Hands/offers, native components and actual Maker/both-pile/alliance-change closing draws remain authoritative. Shared Duke never trains; Advanced paired E3/Moritani excludes Harkonnen. Mixed E3/three-family and pending effects/public starts/save conversion remain separate.',
'Connected native outcomes use original sources: Suboid Cunning and winning trained Suk settle typed fixed casualties before one equal actual Cyborg Tank substitution; original cleanup and mandatory winner Tech precede matching Face Dance, retaining its foreign-Homeworld suppression. Signed Richese two-marker one-invoice physical shipment requires a currently revealed nested site. Ecaz-selected coalition armies and Moritani skill-first assassination remain distinct from shared-Duke training. These programmes do not certify every skill or card band.',
@@ -6294,6 +6294,7 @@ export const RULE_TOPICS: RuleTopic[] = [
'Entering any alliance discards your held Nexus Card, including alliances made through an Ambassador or Terror token. An offer that has not been accepted does not discard it.',
'Use Cunning for your own faction, Betrayal when another player controls the printed faction, and Secret Ally when that faction is absent. Used cards go to the separate Nexus discard pile. Each printed effect has its own timing.',
'Fresh original Nexus tables may retain Homeworlds, Discovery7/8, Tech3+ Basic/Advanced and Strongholds2+ Advanced, optionally all14 through leader-skills --nexus-cards. Original roster/deck envelopes include classics/base33, selected E1/E2 natives/union47 and standalone, paired or mixed E3 native families. Printed/native setup, first-Storm Tech, actual end-Mentat claims and all12 closing draws remain. Original no-Skills E3 Nexus now admits Advanced Harkonnen; Skills retains its Harkonnen exclusion and shared Duke never trains. Public starts, played-save conversion and pending effects remain separate.',
'Fresh selected-deck Nexus previews also admit an unseated selected family without independent Ecaz cards, with optional Discovery. Leader Skills and Advanced Harkonnen with Ecaz or Moritani retain their separate restrictions; wider interactions and complete-mode acceptance remain open.',
'Fresh nexus --discoveries uses the original no-Skills envelopes above with optional Homeworlds, Tech3+ and Advanced Strongholds. Seven Discovery Spice Cards/eight tokens, protected nested sites, next-turn physical entry and actual Maker losses/votes/reserve ride remain. The card draw follows both Advanced piles and actual settled alliance change; earlier Shai-Hulud losses never repeat. Source-clear quotas, borrowed Guild tariffs and signed native one-invoice pairs at revealed sites use original controls, not new transport adapters.',
'Without Skills, the existing generic Nexus initializer handles single and mixed selected E1/E2 natives, while paired one-family and paired E3 entries use their original paired initializer and require BOTH original natives. Native HMS/Face Dancers/Auditor/Richese cache, Ecaz ordinary discs/Loyalty/shared Duke and Advanced Moritani assassination remain physical. New overlays do not create a first Nexus hand, free native return, arbitrary marker companion or played-game conversion.',
'The separate fresh local leader-skills --nexus-cards entry combines classic base33/all14 Skills/all12 Nexus in Basic or Advanced, two through six seats, optionally adding original Discovery7/8 through --discoveries. Original Tech requires three or more seats; Stronghold Cards require Advanced. Starting hands/offers, actual end-Spice closing draws, first-Storm Tech and end-Mentat held custody remain. Extra/ordinary free-three returns earn original phase-end Axlotl without changing skill custody. Emperor Cunning composes skill posture, support and Suk rescue; Arrakeen reduces payer cost, not committed support or rescue eligibility. Winner cleanup precedes mandatory Tech reward. Unrelated overlays and public starts remain guarded.',