Compose paired Nexus and native skills with signed marker shipments
This commit is contained in:
1 parent
54ce1de1df
commit
fba673d215
14 files changed
+1621
-36
No files matched your search
+20
-14
@@ -1,6 +1,6 @@
|
||||
import { classicNexusModulesProfile, pairedNexusModulesProfile } from './nexus-module-profile';
|
||||
import { nativeFactionTechProfile } from './faction-module-profile';
|
||||
import { advancedMoritaniLeaderSkillsProfile, advancedNativeLeaderSkillsProfile, classicNexusLeaderSkillsProfile, classicTechLeaderSkillsProfile, nativeExpansionLeaderSkillsProfile, nativeTechLeaderSkillsProfile, ordinaryLeaderSkillModeSupported, strongholdLeaderSkillsProfile } from './leader-skill-profile';
|
||||
import { advancedMoritaniLeaderSkillsProfile, advancedNativeLeaderSkillsProfile, classicNexusLeaderSkillsProfile, classicTechLeaderSkillsProfile, nativeExpansionLeaderSkillsProfile, nativeTechLeaderSkillsProfile, ordinaryLeaderSkillModeSupported, pairedNexusLeaderSkillsProfile, strongholdLeaderSkillsProfile } from './leader-skill-profile';
|
||||
import { bribeTimingBlock, maximumBribe, type BribeOptions } from './bribe-options';
|
||||
import { quoteSpicePlacement, stormExposesTerritory, stormSectorAfter, wormConsumesForces } from './disaster-rules';
|
||||
import { isStormCardDistance, type StormCardComponent } from './storm-cards';
|
||||
@@ -9400,7 +9400,7 @@ export function initializeLeaderSkillsGameForAudit(state: Game): Game {
|
||||
requireRule(!state.leaderSkills, 'Leader Skills cannot redeal existing skill cards.');
|
||||
const g = structuredClone(state);
|
||||
g.leaderSkills = createLeaderSkills(random);
|
||||
const initialized = initializeSetupGameForAudit(g, false, classicNexusLeaderSkillsProfile(g), false, false, true,
|
||||
const initialized = initializeSetupGameForAudit(g, false, classicNexusLeaderSkillsProfile(g) || pairedNexusLeaderSkillsProfile(g), false, false, true,
|
||||
g.expansions.length === 1 && g.expansions[0] === 'choam', nativeExpansionLeaderSkillsProfile(g));
|
||||
if (advancedMoritaniLeaderSkillsProfile(initialized)) initializeMoritaniAssassinateState(initialized);
|
||||
return initialized;
|
||||
@@ -10635,7 +10635,7 @@ function richeseBetrayalModeSupported(g: Game): boolean {
|
||||
return g.expansions.length === 1 && g.expansions[0] === 'choam' &&
|
||||
g.players.length >= 2 && g.players.length <= 6 &&
|
||||
g.players.some(p => p.faction === 'richese') && g.players.some(p => p.faction === 'choam') &&
|
||||
g.players.every(p => RICHESE_CUNNING_ROSTER.has(p.faction)) &&
|
||||
g.players.every(p => RICHESE_CUNNING_ROSTER[p.faction] === true) &&
|
||||
!!g.nexusCards && !g.homeworlds && !g.leaderSkills && !g.discoveryEnabled &&
|
||||
!g.ecazTreachery && !g.sandtrout && !g.techTokens && !g.strongholdCards &&
|
||||
!g.semutaPreview && !g.kullPreview;
|
||||
@@ -23962,11 +23962,13 @@ function leaderSkillNoFieldIntegrity(g: Game): boolean {
|
||||
for (const decision of guild) validateGuildShipmentDecision(g, decision);
|
||||
return true;
|
||||
}
|
||||
if (!(shipment?.smugglerCompanion !== undefined || shipment?.noFieldSkillProof !== undefined ||
|
||||
const pair = !!g.leaderSkills && !!shipment?.richesePair;
|
||||
if (!(pair || shipment?.smugglerCompanion !== undefined || shipment?.noFieldSkillProof !== undefined ||
|
||||
(g.leaderSkills && shipment?.noField) || responses.some(r => r.noFieldSkillProof !== undefined) ||
|
||||
decisions.some(d => 'noFieldSkillProof' in d))) return false;
|
||||
requireRule(shipment, 'The saved No-Field response lost its original shipment.');
|
||||
validateLeaderSkillNoFieldShipment(g, shipment);
|
||||
if (pair) currentRichesePairShipment(g, shipment);
|
||||
else validateLeaderSkillNoFieldShipment(g, shipment);
|
||||
const initial = responses.filter(r => r.kind === 'richeseNoField');
|
||||
const guild = decisions.filter(d => d.kind === 'guildShipment');
|
||||
requireRule(initial.length + guild.length === 1 &&
|
||||
@@ -23982,8 +23984,9 @@ function leaderSkillNoFieldIntegrity(g: Game): boolean {
|
||||
function validatePhysicalShipment(g: Game, shipment: PendingShipment) {
|
||||
requireRule(!atomicsShipmentBlocked(g.moritaniAtomics, shipment.territory),
|
||||
'Atomics Aftermath permanently blocks shipments into this territory.');
|
||||
if (shipment.smugglerCompanion !== undefined || shipment.noFieldSkillProof !== undefined ||
|
||||
(g.leaderSkills && shipment.noField)) validateLeaderSkillNoFieldShipment(g, shipment);
|
||||
// The pair has its original signed two-token validator below; skill proofs bind single markers.
|
||||
if (!shipment.richesePair && (shipment.smugglerCompanion !== undefined || shipment.noFieldSkillProof !== undefined ||
|
||||
(g.leaderSkills && shipment.noField))) validateLeaderSkillNoFieldShipment(g, shipment);
|
||||
if (shipment.smuggler) requireRule(
|
||||
!shipment.source && !shipment.noField && !shipment.alliedNoField &&
|
||||
!shipment.guildSecretEvent && !shipment.guildNexusEvent && !shipment.nexusEvent &&
|
||||
@@ -24920,13 +24923,14 @@ function nexusRicheseOffer(g: Game, owner: string) {
|
||||
else if (p.reserves < 1) blocked = 'You need physical forces in reserves.';
|
||||
return {event:JSON.stringify(['nexusRichese',g.turn,owner]),blocked,maxForces:Math.min(5,p.reserves)};
|
||||
}
|
||||
const RICHESE_CUNNING_ROSTER = new Set<FactionId>([
|
||||
'atreides', 'harkonnen', 'emperor', 'fremen', 'beneGesserit', 'guild', 'richese', 'choam',
|
||||
]);
|
||||
const RICHESE_CUNNING_ROSTER: Partial<Record<FactionId, true>> = {
|
||||
atreides: true, harkonnen: true, emperor: true, fremen: true,
|
||||
beneGesserit: true, guild: true, richese: true, choam: true,
|
||||
};
|
||||
function richeseCunningModeSupported(g: Game) {
|
||||
return !g.homeworlds && !g.leaderSkills && !g.discoveryEnabled &&
|
||||
return !g.homeworlds && (!g.leaderSkills || pairedNexusLeaderSkillsProfile(g)) && !g.discoveryEnabled &&
|
||||
!g.ecazTreachery && !g.sandtrout &&
|
||||
g.players.every(player => RICHESE_CUNNING_ROSTER.has(player.faction));
|
||||
g.players.every(player => RICHESE_CUNNING_ROSTER[player.faction] === true);
|
||||
}
|
||||
function nexusRicheseCunningOffer(g: Game, owner: string) {
|
||||
const p = g.players.find(player => player.id === owner);
|
||||
@@ -29780,8 +29784,10 @@ function applyActionInner(
|
||||
}),
|
||||
...(noField ? { noField } : {}),
|
||||
};
|
||||
if (noField && g.leaderSkills) shipment.noFieldSkillProof = noFieldSkillProof(shipment);
|
||||
if (noField && g.leaderSkills) validatePhysicalShipment(g, shipment);
|
||||
if (noField && g.leaderSkills && !richesePair) {
|
||||
shipment.noFieldSkillProof = noFieldSkillProof(shipment);
|
||||
validatePhysicalShipment(g, shipment);
|
||||
}
|
||||
if (nexusEvent) recordNexusRicheseShipment(g,shipment);
|
||||
bindGuildCunningShipment(g,p,'reserve',shipment);
|
||||
if (shipment.noField) {
|
||||
|
||||
@@ -33,19 +33,23 @@ export function noOtherLeaderSkillModules(game: LeaderSkillProfile): boolean {
|
||||
!game.discoveryEnabled && !game.strongholdCards && !game.techTokens && !game.ecazTreachery;
|
||||
}
|
||||
|
||||
function nexusLeaderSkillModulesSupported(game: LeaderSkillProfile): boolean {
|
||||
return !!game.nexusCards && typeof game.advanced === 'boolean' &&
|
||||
!!game.players && game.players.length >= 2 && game.players.length <= 6 &&
|
||||
!game.homeworlds && !game.discoveries && !game.discoveryEnabled &&
|
||||
!game.discoveryStash && !game.greatMaker && !game.strongholdCards && !game.techTokens &&
|
||||
!game.ecazTreachery && !game.semutaPreview && !game.moritaniAssassinatePreview &&
|
||||
!game.advancedPreview && !game.kullPreview && !game.nexusKullPreview &&
|
||||
!game.guildBetrayalPreview && !game.richeseBetrayalPreview &&
|
||||
!game.nexusIxianReplacementPreview && !game.nexusIxianBetrayalPreview &&
|
||||
!game.nexusHarkonnenBetrayalPreview && !game.mentatQuestionPreview &&
|
||||
!game.spiceBankerIncomePreview;
|
||||
}
|
||||
|
||||
/** Fresh classic Nexus/Skills only; other module and native compositions stay separate. */
|
||||
export function classicNexusLeaderSkillsProfile(game: LeaderSkillProfile): boolean {
|
||||
const players = game.players;
|
||||
if (!game.nexusCards || typeof game.advanced !== 'boolean' || game.expansions.length ||
|
||||
!players || players.length < 2 || players.length > 6 ||
|
||||
game.homeworlds || game.discoveries || game.discoveryEnabled ||
|
||||
game.discoveryStash || game.greatMaker || game.strongholdCards || game.techTokens ||
|
||||
game.ecazTreachery || game.semutaPreview || game.moritaniAssassinatePreview ||
|
||||
game.advancedPreview || game.kullPreview || game.nexusKullPreview ||
|
||||
game.guildBetrayalPreview || game.richeseBetrayalPreview ||
|
||||
game.nexusIxianReplacementPreview || game.nexusIxianBetrayalPreview ||
|
||||
game.nexusHarkonnenBetrayalPreview || game.mentatQuestionPreview ||
|
||||
game.spiceBankerIncomePreview) return false;
|
||||
if (game.expansions.length || !nexusLeaderSkillModulesSupported(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;
|
||||
@@ -56,9 +60,30 @@ export function classicNexusLeaderSkillsProfile(game: LeaderSkillProfile): boole
|
||||
return true;
|
||||
}
|
||||
|
||||
/** Both original natives in one E1/E2 family; independent decks and other overlays stay separate. */
|
||||
export function pairedNexusLeaderSkillsProfile(game: LeaderSkillProfile): boolean {
|
||||
if (game.expansions.length !== 1 || !nexusLeaderSkillModulesSupported(game)) 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;
|
||||
}
|
||||
|
||||
/** E1/E2 native skills may add Tech and Advanced Strongholds; original roster/decks remain. */
|
||||
function nativeLeaderSkillModulesSupported(game: LeaderSkillProfile): boolean {
|
||||
if (noOtherLeaderSkillModules(game)) return true;
|
||||
if (noOtherLeaderSkillModules(game) || pairedNexusLeaderSkillsProfile(game)) return true;
|
||||
const players = game.players;
|
||||
if ((!game.techTokens && !game.strongholdCards) || !players ||
|
||||
players.length < (game.techTokens ? 3 : 2) || players.length > 6 ||
|
||||
@@ -303,5 +328,6 @@ export function ordinaryLeaderSkillModeSupported(game: LeaderSkillProfile): bool
|
||||
return (noOtherLeaderSkillModules(game) &&
|
||||
(!game.expansions.length || nativeExpansionLeaderSkillsProfile(game))) ||
|
||||
classicTechLeaderSkillsProfile(game) || nativeTechLeaderSkillsProfile(game) ||
|
||||
strongholdLeaderSkillsProfile(game) || classicNexusLeaderSkillsProfile(game);
|
||||
strongholdLeaderSkillsProfile(game) || classicNexusLeaderSkillsProfile(game) ||
|
||||
pairedNexusLeaderSkillsProfile(game);
|
||||
}
|
||||
+3
-1
@@ -4368,6 +4368,7 @@ export const RULE_TOPICS: RuleTopic[] = [
|
||||
'Fresh local classic Nexus tables may preserve original Tech Tokens in Basic/Advanced three-through-six-seat lobbies and/or Stronghold Cards in Advanced two-through-six-seat lobbies; selecting both requires three or more. Original base33/all12 setup, closing Nexus deals, first-Storm Tech and actual end-Mentat Stronghold custody remain. Skills, Homeworlds in this composition, native expansions, unrelated previews, public starts and saved-game conversion stay separate.',
|
||||
'The same fresh local Nexus entry also composes Tech and/or Advanced Strongholds with paired Ixians + Tleilaxu OR CHOAM + Richese and classic opponents, one original family deck and no Skills. Tech needs three through six seats; Strongholds needs two through six Advanced seats. Real closing draws, native typed forces, physical Face Dancers and No-Fields, original payments/rewards and held effects remain. Mixed families, E3, Homeworlds, unrelated previews, public starts and unresolved rulings are not opened.',
|
||||
'The separate fresh local leader-skills --nexus-cards entry combines classic base33/all14 Skills/all12 Nexus in Basic or Advanced, two through six seats, without other overlays or public starts. Original starting hands and skill offers are continued, not redealt. Actual closing Nexus draws remain required. Extra/ordinary free-three returns, bank purchase and committed Traitor Deck exchanges preserve skill custody. Native Emperor Cunning composes ordinary skill posture, bonuses and Suk rescue without changing physical starred inventory or repeating support.',
|
||||
'The same fresh skill/Nexus entry also supports paired Ixians + Tleilaxu OR CHOAM + Richese with classics and one original family deck/all14/all12, Basic/Advanced two through six seats, without other overlays. Full Suboid Cunning composes typed skill rescue before equal actual Cyborg Tank substitution. Face Dance follows original skill/card/reward settlement, kills the matching trainer and returns its skill once without another bounty; partial Tleilaxu Cunning preserves ordinary Mentat rights. Original Richese pair and CHOAM printed fuel use their distinct physical operations. Mixed families, E3, companion/mixed-plan, native free-return accounting and other pending rulings stay separate.',
|
||||
'Atreides Cunning adds a second, different opposing plan element after the first native answer. Both remain binding. Use it in your own battle before sealing your plan; Karama may cancel the extra attempt while preserving the first disclosure.',
|
||||
'With Atreides absent, Secret Ally inspects one opposing element in your battle. A sealed plan supplies only that selected answer automatically. Otherwise the opponent chooses an answer that permits a legal plan. Other players receive no private answer.',
|
||||
'The No-Field restriction names Atreides, so Cunning cannot inspect that dial. A non-Atreides Secret Ally holder may request it without learning the token denomination or other plan elements.',
|
||||
@@ -4403,7 +4404,7 @@ export const RULE_TOPICS: RuleTopic[] = [
|
||||
'Fremen Betrayal has two bounded advance-declaration alternatives for an unallied holder in a classic Nexus table. Before the first Spice Blow or worm appearance, spend the physical card to prevent Fremen ordinary and Cunning worm rides for the turn; worm survival, destruction and Nexus still resolve. This clean timing is a product boundary, not an official post-appearance priority ruling. Instead, before Fremen move during Shipment and Movement, spend it to suppress their native two-territory advantage for that turn while ordinary movement and independent ornithopters remain. Only one alternative can be used even if the card is recycled later that turn. Owner controls show the currently available choice; combined modules and reactive post-worm timing remain gated.',
|
||||
'When Ecaz is absent, an unallied holder may spend its Nexus card at a quiet play boundary to ask whether any one player, including themself, holds one of the holder’s native faction leaders as an ordinary Traitor Card. The owner receives a private yes/no answer saved at the moment of use; no name or count is disclosed. Dead or captured native leaders count, but foreign Gholas, Duke Vidal, Cheap Hero and Face Dancers do not. Native-leader scope and private-only audience are explicit product interpretations, not publisher rulings; exceptional controlled-leader and public-disclosure combinations remain open.',
|
||||
'When Richese is absent, its Secret Ally replaces your ordinary reserve shipment with up to five physical forces charged as one. Select the explicit shipment checkbox; forces, elite identity, destination restrictions, Homeworld reserve sources and normal movement remain unchanged. Fremen remain free within their normal reinforcement range; Guild and an active Karama rate retain their existing tariff and payment routing. This is not a concealed No-Field or an extra shipment.',
|
||||
'Native Richese Cunning has a bounded two-No-Field shipment in genuine Richese/CHOAM Nexus games, including the supported fresh Tech/Advanced Stronghold composition without Skills. Choose distinct unused tokens: one reveals immediately into reserve-capped public forces, the other remains concealed; pay for one marker and spend the Nexus. The previous shipment token cannot be either selection; the concealed token becomes next last-used. Karama or Guild special prevention preserves physical tokens and unpaid spice, with the ordinary stopped-shipment allowance policy. An accepted off-planet pair accrues Heighliners once, collected only at phase end. Other module combinations and the full faceup-component display remain gated.',
|
||||
'Native Richese Cunning has a bounded two-No-Field shipment in genuine Richese/CHOAM Nexus games, including supported fresh Tech/Advanced Strongholds without Skills or the separate paired all14 Skills composition without other overlays. Choose distinct unused tokens: one reveals immediately into reserve-capped public forces, the other remains concealed; pay for one marker and spend the Nexus. The previous shipment token cannot be either selection; the concealed token becomes next last-used. The skill composition retains the original signed pair frame and native response/Guild continuation, not a single-marker skill proof or free companion. Karama or Guild special prevention preserves physical tokens and unpaid spice, with the ordinary stopped-shipment allowance policy. When Tech is actually selected, an accepted off-planet pair accrues Heighliners once at phase end. Other module combinations, mixed dialing and full faceup-component display remain gated.',
|
||||
'Richese Betrayal has a separate opt-in auction preview. Before payment, an unallied rival may spend its physical Richese Nexus card to discard a public self-cache win without any purchase debit, or redirect another buyer’s cache/Black Market payment to the bank while preserving delivery and earned bonus. Every publicly possible holder acknowledges the same opportunity; private Use eligibility cannot determine public timing. Normal hidden self-purchases and private special-Karama acquisitions are not connected. See the dedicated guide for its existing local runtime/browser evidence and remaining release gates; that evidence does not verify Nexus Kull.',
|
||||
'Guild Cunning is declared when finishing your ordinary Shipment and Movement turn. It spends the Nexus and opens one Karama response. Allowance grants a second native shipment at normal Guild prices, including supported cross-shipment, return and Homeworld routes. Cancellation leaves the completed first turn intact. Only an unused Hajr move can follow the second shipment; no ordinary movement or third move is granted. The physical Ornithopter card combination remains unavailable. The separate guild-betrayal profile admits only original native shipment producers and explicitly blocks this second-shipment offer before Nexus spending; Cunning outside that profile is unchanged.',
|
||||
'With Guild absent, its Secret Ally is an explicit optional source for one ordinary shipment at Guild prices. It supports physical reserve shipment, cross-shipment and return to abstract reserves, plus half-price independently legal world-to-world shipment. Fremen may retain free reinforcement or choose the paid card route beyond its normal range, outside the storm. Native faction, typed sources and normal movement stay unchanged. Homeworld reserve return awaits a ruling; arbitrary foreign-world transport and concealed No-Fields are not granted.',
|
||||
@@ -4606,6 +4607,7 @@ export const RULE_TOPICS: RuleTopic[] = [
|
||||
'Fresh three-through-six-seat Basic/Advanced development tables may compose all fourteen Skills with original Tech Tokens: classic factions, supported E1/E2 native families, or standalone Ecaz/Moritani with the exact ecaz deck and original roster exclusions. Starting cards/cache, skill/Traitor choices and printed/first-Storm assignment stay genuine. Existing leader-skills/banker-income entry preserves unused selected tokens; mixed E3 families, other overlays, public activation and saved-game conversion remain separate.',
|
||||
'Fresh Advanced classic or supported native two-through-six-seat skill tables may retain six original Stronghold Cards, with Tech at three or more seats. Original end-Mentat/local held effects remain. Arrakeen changes actual payer cost, not rescue or Banker eligibility; native CHOAM income retains bank support. Physical defenses, normal disciplines, holder ties and original dial/Worthless earnings remain. Standalone Ecaz Duke and Moritani assassination compose original rules; mixed E3, allied Occupy skills, copied Diplomat defenses, active skill/Stone and other guards stay separate.',
|
||||
'Fresh classic Leader Skills may explicitly add Nexus alone through the local leader-skills --nexus-cards entry, Basic/Advanced two through six players. Starting hands can already be dealt before skill choice; saved offers remain authoritative. Original closing Nexus draws, free-return ledgers, bank purchase and Traitor Deck declarations stay distinct from physical training. Native expansions, Tech/Strongholds/Homeworlds, Banker/Mentat previews, borrowed Smuggler shipment arithmetic and other unresolved interactions remain separate.',
|
||||
'The original paired Ixian/Tleilaxu or CHOAM/Richese native skill roster may also add Nexus alone through that explicit fresh local entry, with classics and one original family deck, Basic/Advanced two through six players. Full unsupported Suboids retain Cyborg support and skill posture. Suk physical rescue finishes before equal substitution of only actual Cyborg Tank losses; Face Dance then returns a killed trainer skill once without another bounty. Richese Cunning preserves its signed two-marker/one-invoice operation; ordinary skill marker proofs and companion exclusions remain separate. CHOAM Special fuel is discarded before an ordinary Planetologist move, without a second card effect or created combat Special.',
|
||||
'With this optional module, starting Treachery Cards are dealt after prediction and before skill assignment. Every faction privately receives two skills, keeps one, returns the other to the shuffled deck, and assigns its chosen card to an eligible leader. Skill and leader become public before traitor selection.',
|
||||
'Before Battle Plans, leave the skilled leader face up to keep its normal effect, or move it behind the shield for possible use. A face-up leader cannot be selected for the plan. Concealing it while choosing another leader gives neither effect. A leader that must be used because no alternative remains moves behind the shield automatically.',
|
||||
'Warmaster, Master of Assassins, Swordmaster of Ginaz, Killer Medic and Prana-Bindu Adept are connected. They give another leader +1 for the matching physical card role, or the surviving assigned leader +3. These bonuses do not stack to +4. They change battle strength, not the printed disc value or leader bounty.',
|
||||
|
||||
Reference in new issue
Block a user