Compose standalone E3 Skills and independent cards with native power defaults
Restore fresh Ecaz coalition and Advanced Moritani initialization; retain printed native continuations and selected-family card eligibility. Validate Mentat fallback hands against explicitly selected independent cards. Frozen25-file274/274, types/lint/build and genuine runtime/mobile entry proofs pass. No save retrofit or deployment.
This commit is contained in:
1 parent
2ad947af54
commit
fde44baab5
25 files changed
+1838
-103
No files matched your search
+38
-30
@@ -8581,14 +8581,18 @@ function moritaniAssassinateModeSupported(g: Game) {
|
||||
return g.advanced && !!g.discoveries &&
|
||||
(discoveryModeSupported(g) || nativeDiscoveryLeaderSkillsProfile(g)) &&
|
||||
g.players.some(player => player.faction === 'moritani');
|
||||
return g.advanced && g.expansions.length === 1 && g.expansions[0] === 'ecaz' &&
|
||||
return g.advanced && g.expansions.includes('ecaz') && g.expansions.length <= 3 &&
|
||||
g.expansions.every((id, index) => (id === 'ix' || id === 'choam' || id === 'ecaz') &&
|
||||
g.expansions.indexOf(id) === index) &&
|
||||
g.players.some(p => p.faction === 'moritani') &&
|
||||
g.players.every(p => p.faction === 'moritani' || (p.faction !== 'harkonnen' && CLASSIC_FACTIONS[p.faction] === true)) &&
|
||||
g.players.every(p => FACTIONS.some(f => f.id === p.faction &&
|
||||
(f.expansion === 'base' || g.expansions.includes(f.expansion)))) &&
|
||||
(!g.nexusCards || (!!g.leaderSkills && standaloneE3NexusLeaderSkillsProfile(g))) &&
|
||||
(!g.leaderSkills || advancedMoritaniLeaderSkillsProfile(g)) &&
|
||||
(!g.strongholdCards || e3StrongholdFactionProfile(g) ||
|
||||
(!!g.leaderSkills && strongholdLeaderSkillsProfile(g))) &&
|
||||
(!g.homeworlds || (!!g.leaderSkills && homeworldLeaderSkillsProfile(g))) &&
|
||||
(!g.homeworlds || g.homeworldOccupationPreview === true ||
|
||||
(!!g.leaderSkills && homeworldLeaderSkillsProfile(g))) &&
|
||||
!g.discoveryEnabled && !g.discoveries &&
|
||||
(!g.techTokens || nativeFactionTechProfile(g) || (!!g.leaderSkills && nativeTechLeaderSkillsProfile(g)));
|
||||
}
|
||||
@@ -9592,6 +9596,7 @@ export function initializeHomeworldOccupationGameForAudit(state: Game, leaderSki
|
||||
g.homeworldEcazVictoryPreview = true;
|
||||
g.ecazOccupyPreview = true;
|
||||
}
|
||||
if (moritaniAssassinateModeSupported(g)) initializeMoritaniAssassinateState(g);
|
||||
return g;
|
||||
}
|
||||
/** Shared classic/supported-native Nexus setup; existing composition guards apply. */
|
||||
@@ -10513,8 +10518,8 @@ export function initializeFactionExpansionsGameForAudit(state: Game): Game {
|
||||
'The faction prototype excludes other optional modules; Tech Tokens require a supported native three-through-six-seat lobby.');
|
||||
requireFreshFactionInventory(state);
|
||||
const initialized = initializeSetupGameForAudit(state, false, false, false, false, false, false, true);
|
||||
if (initialized.advanced && nativeFactionTechProfile(initialized) && byFaction(initialized, 'moritani'))
|
||||
initializeMoritaniAssassinateState(initialized);
|
||||
if (byFaction(initialized, 'ecaz')) initialized.ecazOccupyPreview = true;
|
||||
if (moritaniAssassinateModeSupported(initialized)) initializeMoritaniAssassinateState(initialized);
|
||||
return initialized;
|
||||
}
|
||||
function ecazOccupyCompositionSupported(g: Game): boolean {
|
||||
@@ -10541,7 +10546,7 @@ function ecazOccupyCompositionSupported(g: Game): boolean {
|
||||
g.expansions.every(id => ['ecaz', 'ix', 'choam'].includes(id)) &&
|
||||
new Set(g.expansions).size === g.expansions.length &&
|
||||
(g.advanced || (g.expansions.length === 1 && g.expansions[0] === 'ecaz' &&
|
||||
!g.ecazTreachery && g.players.every(p => p.faction === 'ecaz' ||
|
||||
g.players.every(p => p.faction === 'ecaz' ||
|
||||
p.faction === 'moritani' || CLASSIC_FACTIONS[p.faction]))) &&
|
||||
g.players.length >= 2 && g.players.length <= 6 &&
|
||||
g.players.some(p => p.faction === 'ecaz') &&
|
||||
@@ -10557,11 +10562,12 @@ function ecazOccupyCompositionSupported(g: Game): boolean {
|
||||
/** Fresh native Occupy profile; Basic odd counts use the main printed rule provisionally. */
|
||||
export function initializeEcazOccupyGameForAudit(state: Game): Game {
|
||||
requireRule(ecazOccupyCompositionSupported(state),
|
||||
'Occupy requires supported native factions and their distinct selected decks; Basic uses Ecaz/classic/optional Moritani without optional modules.');
|
||||
'Occupy requires supported native factions and their selected decks; Basic uses Ecaz/classic/optional Moritani and may include the independent cards.');
|
||||
requireFreshBaseRuntime(state);
|
||||
requireFreshFactionInventory(state);
|
||||
const g = initializeSetupGameForAudit(state, false, false, false, false, false, false, true, state.ecazTreachery === true);
|
||||
g.ecazOccupyPreview = true;
|
||||
if (moritaniAssassinateModeSupported(g)) initializeMoritaniAssassinateState(g);
|
||||
return g;
|
||||
}
|
||||
/** Original E1/E2 Stronghold roster; shared by fresh setup and Semuta validation. */
|
||||
@@ -10611,14 +10617,13 @@ function initializeMoritaniAssassinateState(g: Game): void {
|
||||
g.moritaniAssassinate = {version:1,owner:byFaction(g,'moritani')!.id,normalTraitorCall:false,opportunities:[]};
|
||||
g.moritaniAssassinateCallEvents = [];
|
||||
}
|
||||
/** Genuine, explicitly opted-in preview; no player action enables this profile. */
|
||||
/** Focused fresh native Moritani entry through the same printed-power setup. */
|
||||
export function initializeMoritaniAssassinateGameForAudit(state: Game): Game {
|
||||
requireRule(!state.moritaniAssassinatePreview && !state.moritaniAssassinate && !state.moritaniAssassinateResume && !state.moritaniAssassinateCallEvents,
|
||||
'The assassination preview starts only from a fresh lobby.');
|
||||
requireRule(moritaniAssassinateModeSupported(state), 'The assassination preview needs Advanced Moritani with ordinary base opponents except Harkonnen, the Ecaz expansion and no optional modules.');
|
||||
const g = initializeFactionExpansionsGameForAudit(state);
|
||||
initializeMoritaniAssassinateState(g);
|
||||
return g;
|
||||
requireRule(moritaniAssassinateModeSupported(state),
|
||||
'Assassination requires Advanced Moritani in a supported selected native roster and module composition.');
|
||||
return initializeFactionExpansionsGameForAudit(state);
|
||||
}
|
||||
/** Development-only Semuta with original native modules/skills, separate bounded
|
||||
* Homeworld occupation, or Basic Moritani/E3. Public starts stay gated. */
|
||||
@@ -10703,7 +10708,7 @@ export function initializeLeaderSkillsGameForAudit(state: Game): Game {
|
||||
const homeworlds = homeworldLeaderSkillsProfile(state);
|
||||
const independent = independentTreacheryLeaderSkillsProfile(state);
|
||||
if (state.ecazTreachery) {
|
||||
requireRule(independent, 'Independent Treachery with Leader Skills requires a fresh classic or selected Ix/CHOAM lobby without other modules.');
|
||||
requireRule(independent, 'Independent Treachery with Leader Skills requires a fresh classic, selected Ix/CHOAM or standalone Ecaz/Moritani lobby without other modules.');
|
||||
requireFreshBaseRuntime(state);
|
||||
requireFreshFactionInventory(state);
|
||||
}
|
||||
@@ -10733,19 +10738,19 @@ export function initializeLeaderSkillsGameForAudit(state: Game): Game {
|
||||
if ((nativeEcazLeaderSkillsProfile(initialized) || mixedE3 || homeworlds || nativeDiscoveryLeaderSkillsProfile(initialized) || pairedE3) && byFaction(initialized, 'ecaz')) initialized.ecazOccupyPreview = true;
|
||||
return initialized;
|
||||
}
|
||||
/** Independent cards keep the actual selected base/E1/E2 physical families. */
|
||||
/** Independent cards keep every selected native family's physical counters. */
|
||||
function independentCardRoster(g: Game): boolean {
|
||||
if (g.expansions.length > 2 ||
|
||||
(g.expansions.length === 2 && g.expansions[0] === g.expansions[1]) ||
|
||||
g.expansions.some(expansion => expansion !== 'ix' && expansion !== 'choam')) return false;
|
||||
if (g.expansions.length > 3 || g.expansions.some((expansion, index) =>
|
||||
(expansion !== 'ix' && expansion !== 'choam' && expansion !== 'ecaz') ||
|
||||
g.expansions.indexOf(expansion) !== index)) return false;
|
||||
return g.players.every(player => {
|
||||
const family = faction(player.faction).expansion;
|
||||
return family === 'base' || ((family === 'ix' || family === 'choam') && g.expansions.includes(family));
|
||||
return family === 'base' || g.expansions.includes(family);
|
||||
});
|
||||
}
|
||||
function ecazHomeworldProfile(g: Game): boolean {
|
||||
return !!g.homeworlds && g.ecazTreachery === true && independentCardRoster(g) &&
|
||||
g.players.length >= 2 && g.players.length <= 6 &&
|
||||
!g.expansions.includes('ecaz') && g.players.length >= 2 && g.players.length <= 6 &&
|
||||
!g.nexusCards && !g.leaderSkills &&
|
||||
((!g.techTokens && !g.strongholdCards) || independentCardModulesProfile(g)) &&
|
||||
!g.discoveryEnabled && !g.discoveries && !g.discoveryStash && !g.greatMaker && !g.sandtrout &&
|
||||
@@ -10756,7 +10761,7 @@ function ecazHomeworldProfile(g: Game): boolean {
|
||||
}
|
||||
/** Independent cards compose original phase modules, not unrelated previews. */
|
||||
function independentCardModulesProfile(g: Game): boolean {
|
||||
if (!g.ecazTreachery || (!g.techTokens && !g.strongholdCards) || !independentCardRoster(g) ||
|
||||
if (!g.ecazTreachery || (!g.techTokens && !g.strongholdCards) || !independentCardRoster(g) || g.expansions.includes('ecaz') ||
|
||||
g.players.length < (g.techTokens ? 3 : 2) || g.players.length > 6 ||
|
||||
(g.strongholdCards && !g.advanced) ||
|
||||
g.nexusCards || g.leaderSkills ||
|
||||
@@ -10775,7 +10780,10 @@ export function initializeEcazTreacheryGameForAudit(state: Game): Game {
|
||||
const homeworlds = !!g.homeworlds;
|
||||
requireRule(!homeworlds || ecazHomeworldProfile(g),
|
||||
'Independent cards with Homeworlds need a fresh classic or selected Ix/CHOAM roster with only original Tech or Advanced Strongholds.');
|
||||
return initializeSetupGameForAudit(g, homeworlds, false, false, false, false, false, true, true);
|
||||
const initialized = initializeSetupGameForAudit(g, homeworlds, false, false, false, false, false, true, true);
|
||||
if (byFaction(initialized, 'ecaz')) initialized.ecazOccupyPreview = true;
|
||||
if (moritaniAssassinateModeSupported(initialized)) initializeMoritaniAssassinateState(initialized);
|
||||
return initialized;
|
||||
}
|
||||
function initializeSetupGameForAudit(state: Game, homeworlds: boolean, nexus = false, ix = false, discovery = false, leaderSkills = false, choam = false, factions = false, ecazTreachery = false, strongholdFactions = false): Game {
|
||||
nexusCardsIntegrity(state);
|
||||
@@ -12836,7 +12844,7 @@ function createMentatQuestion(g: Game) {
|
||||
const event = crypto.randomUUID();
|
||||
const receipt: MentatQuestionReceipt = {event,battle:b.event!,turn:g.turn,territory:b.territory,
|
||||
owner,target,leader:trained.leader,stage:'name',weapon:null,card:null,
|
||||
hand:mentatRule(() => mentatHand(getPlayer(g,target).hand,g.expansions)),signature:''};
|
||||
hand:mentatRule(() => mentatHand(getPlayer(g,target).hand,g.expansions,g.ecazTreachery === true)),signature:''};
|
||||
if (!receipt.hand.length) {receipt.stage = 'unavailable';log(g,MENTAT_EMPTY_HAND,{faction:getPlayer(g,owner).faction,name:'Mentat'});}
|
||||
receipt.signature = mentatSignature(receipt);
|
||||
b.mentatQuestion = receipt;
|
||||
@@ -12857,7 +12865,7 @@ function mentatQuestionIntegrity(g: Game) {
|
||||
g.players.some(p => p.id === observation.owner && p.leaders.some(l => l.id === observation.leader)) &&
|
||||
mentatWeaponNames(g.expansions).includes(observation.weapon) && observation.signature === mentatSignature(observation),
|
||||
'The saved Mentat observation changed its question, audience or event.');
|
||||
mentatRule(() => mentatHand([observation.card],g.expansions));
|
||||
mentatRule(() => mentatHand([observation.card],g.expansions,g.ecazTreachery === true));
|
||||
}
|
||||
const b = g.battle, receipt = b?.mentatQuestion;
|
||||
const decisions = homeworldSavedDecisions(g).filter(d => d.kind === 'mentatQuestion');
|
||||
@@ -12874,7 +12882,7 @@ function mentatQuestionIntegrity(g: Game) {
|
||||
[b.attacker,b.defender].includes(receipt.owner) && [b.attacker,b.defender].includes(receipt.target) && receipt.owner !== receipt.target &&
|
||||
receipt.signature === mentatSignature(receipt),
|
||||
'The saved Mentat question changed its battle, audience or original event.');
|
||||
mentatRule(() => mentatHand(receipt.hand,g.expansions));
|
||||
mentatRule(() => mentatHand(receipt.hand,g.expansions,g.ecazTreachery === true));
|
||||
requireRule(['name','reveal','answered','declined','unavailable'].includes(receipt.stage), 'The saved Mentat question has an invalid stage.');
|
||||
requireRule(receipt.stage === 'unavailable' ? receipt.hand.length === 0 : receipt.hand.length > 0,
|
||||
'The saved Mentat opportunity changed its empty-hand boundary.');
|
||||
@@ -12882,7 +12890,7 @@ function mentatQuestionIntegrity(g: Game) {
|
||||
requireRule(receipt.weapon === null && receipt.card === null, 'The unasked Mentat question cannot contain an answer.');
|
||||
else {
|
||||
requireRule(typeof receipt.weapon === 'string', 'The saved Mentat weapon question is missing.');
|
||||
const quote = mentatRule(() => quoteMentatReveal(receipt.hand,receipt.weapon!,g.expansions));
|
||||
const quote = mentatRule(() => quoteMentatReveal(receipt.hand,receipt.weapon!,g.expansions,g.ecazTreachery === true));
|
||||
requireRule(receipt.stage === 'reveal' ? receipt.card === null :
|
||||
receipt.card && quote.cards.some(card => JSON.stringify(card) === JSON.stringify(receipt.card)),
|
||||
'The saved Mentat disclosure does not answer its exact original question.');
|
||||
@@ -12919,7 +12927,7 @@ function projectedMentat(g: Game, viewer: string): MentatView {
|
||||
player:pending.stage === 'name' ? pending.owner : pending.target,stage:pending.stage as 'name'|'reveal',weapon:pending.weapon,
|
||||
weapons:pending.stage === 'name' && viewer === pending.owner && pending.hand.length ? mentatWeaponNames(g.expansions) : [],
|
||||
cards:pending.stage === 'reveal' && viewer === pending.target ?
|
||||
mentatRule(() => quoteMentatReveal(pending.hand,pending.weapon!,g.expansions)).cards : [],
|
||||
mentatRule(() => quoteMentatReveal(pending.hand,pending.weapon!,g.expansions,g.ecazTreachery === true)).cards : [],
|
||||
blocked:pending.stage === 'name' && !pending.hand.length ? MENTAT_EMPTY_HAND : null} : null,
|
||||
history:(g.mentatHistory ?? []).filter(observation => [observation.owner,observation.target].includes(viewer))
|
||||
.map(({signature: _signature,...observation}) => structuredClone(observation))};
|
||||
@@ -12935,7 +12943,7 @@ function actMentatQuestion(g: Game, decision: Extract<Decision,{kind:'mentatQues
|
||||
} else {
|
||||
requireRule(Object.keys(action).every(key => ['type','event','weapon'].includes(key)), 'Name only one specific Mentat weapon.');
|
||||
const weapon = stringField(action.weapon);
|
||||
mentatRule(() => quoteMentatReveal(receipt.hand,weapon,g.expansions));
|
||||
mentatRule(() => quoteMentatReveal(receipt.hand,weapon,g.expansions,g.ecazTreachery === true));
|
||||
receipt.weapon = weapon;
|
||||
log(g,`${getPlayer(g,receipt.owner).name} asked ${getPlayer(g,receipt.target).name} about ${weapon} with Mentat. The card shown is private and need not be played.`);
|
||||
// Every question uses the same target-owned response, so public timing
|
||||
@@ -12944,7 +12952,7 @@ function actMentatQuestion(g: Game, decision: Extract<Decision,{kind:'mentatQues
|
||||
}
|
||||
} else {
|
||||
requireRule(Object.keys(action).every(key => ['type','event','card'].includes(key)), 'Show exactly one held Treachery Card for Mentat.');
|
||||
const quote = mentatRule(() => quoteMentatReveal(receipt.hand,receipt.weapon!,g.expansions));
|
||||
const quote = mentatRule(() => quoteMentatReveal(receipt.hand,receipt.weapon!,g.expansions,g.ecazTreachery === true));
|
||||
const card = quote.cards.find(card => card.id === action.card);
|
||||
requireRule(card, 'Choose a held card that answers this Mentat question.');
|
||||
receipt.card = {...card}; receipt.stage = 'answered';
|
||||
@@ -13845,7 +13853,7 @@ function combinedCardBattleBlock(g: Game): string | null {
|
||||
(battle.attacker === ecaz.id || battle.defender === ecaz.id ||
|
||||
battle.attacker === ally.id || battle.defender === ally.id) &&
|
||||
fighterCount(ecaz, battle.territory) > 0 && fighterCount(ally, battle.territory) > 0)
|
||||
return 'Combined card armies require the supported Advanced Occupy battle profile.';
|
||||
return 'Combined card armies require the supported native Occupy battle profile.';
|
||||
return null;
|
||||
}
|
||||
function reinforcementsModeSupported(g: Game) {
|
||||
@@ -13937,7 +13945,7 @@ function recruitsPlayBlock(g: Game, owner: Player, card: Card): string | null {
|
||||
if (!g.ecazTreachery || !ecazTreacheryDefinition(card) || card.id !== RECRUITS_CARD_ID)
|
||||
return 'Choose the physical Recruits card from the enabled Ecaz Treachery Cards variant.';
|
||||
if (!recruitsModeSupported(g))
|
||||
return 'Recruits requires a supported independent-card profile: faction-only, classic all14 Skills without other modules, or the existing native Homeworld and phase-module compositions.';
|
||||
return 'Recruits requires a supported independent-card profile: faction-only, supported all14 Skills without other modules, or the existing native Homeworld and phase-module compositions.';
|
||||
if (g.status !== 'playing' || g.phase !== 4)
|
||||
return 'Play Recruits during Revival.';
|
||||
if (g.recruits?.turn === g.turn)
|
||||
|
||||
@@ -241,8 +241,8 @@ function nativeLeaderSkillModulesSupported(game: LeaderSkillProfile): boolean {
|
||||
/** 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 (noOtherLeaderSkillModules(game) || independentTreacheryLeaderSkillsProfile(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 ||
|
||||
@@ -508,32 +508,40 @@ export function homeworldLeaderSkillsProfile(game: LeaderSkillProfile): boolean
|
||||
return true;
|
||||
}
|
||||
|
||||
/** Independent utility cards with original classic or selected E1/E2 all14 setup. */
|
||||
/** Independent cards with classic, selected E1/E2 or standalone E3 all14 setup. */
|
||||
export function independentTreacheryLeaderSkillsProfile(game: LeaderSkillProfile): boolean {
|
||||
const e3 = game.expansions.length === 1 && game.expansions[0] === 'ecaz';
|
||||
if (game.ecazTreachery !== true || game.expansions.length > 2 ||
|
||||
typeof game.advanced !== 'boolean' || !game.players ||
|
||||
game.players.length < 2 || game.players.length > 6 ||
|
||||
game.homeworlds || game.nexusCards || game.discoveries || game.discoveryEnabled ||
|
||||
game.discoveryStash || game.greatMaker || game.techTokens || game.strongholdCards ||
|
||||
game.semutaPreview || game.moritaniAssassinatePreview || game.advancedPreview ||
|
||||
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 < game.expansions.length; i++) {
|
||||
if (game.expansions[i] !== 'ix' && game.expansions[i] !== 'choam') return false;
|
||||
if (!e3 && game.expansions[i] !== 'ix' && game.expansions[i] !== 'choam') return false;
|
||||
for (let j = 0; j < i; j++) if (game.expansions[j] === game.expansions[i]) return false;
|
||||
}
|
||||
const players = game.players;
|
||||
let e3Natives = 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 selected = false;
|
||||
for (const faction of FACTIONS)
|
||||
if (faction.id === players[i].faction) {
|
||||
selected = faction.expansion === 'base' || game.expansions.includes(faction.expansion);
|
||||
if (faction.expansion === 'ecaz') e3Natives++;
|
||||
break;
|
||||
}
|
||||
if (!selected) return false;
|
||||
}
|
||||
// Preserve the original standalone native Skills/Duke roster boundaries.
|
||||
if (e3 && (e3Natives > 1 || (e3Natives > 0 && game.advanced &&
|
||||
players.some(player => player.faction === 'harkonnen')))) return false;
|
||||
if (game.moritaniAssassinatePreview && (!e3 || !game.advanced ||
|
||||
!players.some(player => player.faction === 'moritani'))) return false;
|
||||
return true;
|
||||
}
|
||||
|
||||
|
||||
@@ -1,5 +1,6 @@
|
||||
import { treacheryDeck, type Card } from './cards';
|
||||
import { richeseCards } from './richese-cards';
|
||||
import { ecazTreacheryCards } from './ecaz-cards';
|
||||
import { quoteTupileIntelligenceAnswer } from './tupile-intelligence-answer';
|
||||
import { ordinaryLeaderSkillModeSupported, type LeaderSkillProfile } from './leader-skill-profile';
|
||||
|
||||
@@ -42,17 +43,18 @@ export type MentatView = {
|
||||
export function mentatQuestionModeSupported(game: LeaderSkillProfile): boolean {
|
||||
return ordinaryLeaderSkillModeSupported(game);
|
||||
}
|
||||
function inventory(expansions: readonly string[]): Card[] {
|
||||
function inventory(expansions: readonly string[], ecazTreachery: boolean): Card[] {
|
||||
return [
|
||||
...treacheryDeck(expansions),
|
||||
...(expansions.includes('choam') ? richeseCards() : []),
|
||||
...(ecazTreachery ? ecazTreacheryCards() : []),
|
||||
];
|
||||
}
|
||||
/** Printed/default held-card roles, shared with Tupile; never battle-slot legality. */
|
||||
export function mentatWeaponNames(expansions: readonly string[]): string[] {
|
||||
return [
|
||||
...new Set(
|
||||
inventory(expansions)
|
||||
inventory(expansions, false)
|
||||
.filter(
|
||||
(card) =>
|
||||
quoteTupileIntelligenceAnswer([card], 0, 'weapons').count === 1,
|
||||
@@ -64,9 +66,10 @@ export function mentatWeaponNames(expansions: readonly string[]): string[] {
|
||||
export function mentatHand(
|
||||
hand: readonly Card[],
|
||||
expansions: readonly string[],
|
||||
ecazTreachery: boolean,
|
||||
): Card[] {
|
||||
const canonical = new Map(
|
||||
inventory(expansions).map((card) => [card.id, card]),
|
||||
inventory(expansions, ecazTreachery).map((card) => [card.id, card]),
|
||||
);
|
||||
if (
|
||||
!Array.isArray(hand) ||
|
||||
@@ -87,10 +90,11 @@ export function quoteMentatReveal(
|
||||
hand: readonly Card[],
|
||||
weapon: string,
|
||||
expansions: readonly string[],
|
||||
ecazTreachery: boolean,
|
||||
): { cards: Card[] } {
|
||||
if (!mentatWeaponNames(expansions).includes(weapon))
|
||||
throw new MentatQuestionError('Name one specific canonical weapon.');
|
||||
const held = mentatHand(hand, expansions);
|
||||
const held = mentatHand(hand, expansions, ecazTreachery);
|
||||
if (!held.length) throw new MentatQuestionError(MENTAT_EMPTY_HAND);
|
||||
const matching = held.filter((card) => card.name === weapon);
|
||||
return {
|
||||
|
||||
+6
-6
@@ -1407,8 +1407,8 @@ export const RULE_TOPICS: RuleTopic[] = [
|
||||
'A native Richese ally may supply ordinary counters, a concealed No-Field, or both under the selected Advanced provisional additive pool. The selected actor gets its private variable pool/mixed notice; rivals do not. Richese materializes once after both seals, including zero/caps. Ordinary dial Prescience follows the actual force owner; distinct Advanced special Karama may privately inspect its complete sealed plan under the provisional power distinction. Prevented Ecaz uses only its own pool; neither private read nor prevention reveals the marker early.',
|
||||
'Stone Burner compares physical undialed counters, not total dial or paid strength: active Occupy contributes floor(Ecaz forces / 2) plus the ally’s native undialed tokens. Canceled Occupy compares only the selected lead’s own pool. Physical Mirror copies use the same comparison and retain their original cleanup. Hidden opposing No-Field preflight uses public possible pools, not its actual denomination; genuinely winner-changing typed allocations remain blocked.',
|
||||
'With the independent variant, Harass returns only the actual card user’s undialed forces. An Ecaz user keeps the original mandatory ceil contribution committed at free full strength while its floor count returns; ally support is separate. An allied user subtracts fixed strength before choosing its native typed commitment. Canceled Occupy uses its selected lead’s own pool. Returned reserves are outside later casualties/explosion/Face Dance.',
|
||||
'Reinforcements consumes three card-user reserves normal-first and adds +2 normal score, never support or physical arrivals. Under the provisional native order, an own No-Field materializes before that original reserve fee and successful Harass returns; three reserves must remain after reveal before this plan may seal. Original/copied Stone ignores +2 score but not the fee. Existing all-revealed-outcome cost/disposal remains provisional. Basic/unprofiled co-side cards stay guarded.',
|
||||
'Advanced numbers come from the user-authorized revised PDF, physical pages 32 and 19, not a new publisher erratum. Basic follows the main printed paragraph provisionally. Original faction/deck prerequisites, separate ten-card Richese cache and independent three-card variant selection stay. Ordinary-plus-marker Advanced Occupy extends the provisional additive interpretation, not the unresolved FAQ cap. Native Ecaz/classic Skills and supported World previews now retain ordinary Arrakis Occupy; Basic mixed-co-side cards, unsupported optional overlays and full-mode acceptance remain separate.',
|
||||
'Reinforcements consumes three card-user reserves normal-first and adds +2 normal score, never support or physical arrivals. Under the provisional native order, an own No-Field materializes before that original reserve fee and successful Harass returns; three reserves must remain after reveal before this plan may seal. Original/copied Stone ignores +2 score but not the fee. Existing all-revealed-outcome cost/disposal remains provisional. Unsupported co-side profiles stay guarded.',
|
||||
'Advanced numbers come from the user-authorized revised PDF, physical pages 32 and 19, not a new publisher erratum. Basic follows the main printed paragraph provisionally. Original faction/deck prerequisites, separate ten-card Richese cache and independent three-card variant selection stay. Ordinary-plus-marker Advanced Occupy extends the provisional additive interpretation, not the unresolved FAQ cap. Fresh plain/card Ecaz and standalone Ecaz/classic Skills retain ordinary Arrakis Occupy, including independent cards without other modules. Basic mixed-co-side markers, unsupported overlays and full-mode acceptance remain separate.',
|
||||
],
|
||||
checklist: [
|
||||
{ area: 'Implementation', status: 'Partial', detail: 'Combined-side enumeration, lead choice, native Occupy response and owner-labelled physical outcomes are connected. Basic odd counts now follow the main printed paragraph provisionally; Advanced retains its authorized arithmetic. Optional/exotic composition refinement remains.', evidence: ['game/ecaz-occupy-battle.ts', 'game/board-resolution-quote.ts', 'game/battle-resolution-quote.ts', 'game/engine.ts'] },
|
||||
@@ -3038,7 +3038,7 @@ export const RULE_TOPICS: RuleTopic[] = [
|
||||
summary:
|
||||
'Six hidden tokens threaten stronghold entrants. Bounded Atomics revelation is available in classic-plus-Moritani tables; complete Moritani starts remain unavailable.',
|
||||
steps: [
|
||||
'Placement and isolated entry decisions have controls for Robbery, Sabotage, Sneak Attack, Extortion, ordinary native-leader Assassination and bounded Atomics. You may instead leave a token hidden. Arrivals with unsupported competing reactions remain gated; Moritani starts remain disabled.',
|
||||
'Placement and isolated entry decisions have controls for Robbery, Sabotage, Sneak Attack, Extortion, ordinary native-leader Assassination and bounded Atomics. You may instead leave a token hidden. Supported native Moritani starts use labelled previews; unsupported competing entry reactions remain guarded.',
|
||||
'Begin with all six Terror tokens in your hidden supply. Once during Mentat Pause, you may either place one from supply or move one already placed to another eligible stronghold. There is no spice cost.',
|
||||
'The destination must have no Terror token. A stronghold under storm is eligible, but the Hidden Mobile Stronghold and Homeworlds are excluded.',
|
||||
'When another faction ships or moves into the marked stronghold, you may reveal its token and apply the effect to that entrant. Your ally does not trigger this opportunity. Bene Gesserit advisors do.',
|
||||
@@ -3228,7 +3228,7 @@ export const RULE_TOPICS: RuleTopic[] = [
|
||||
coverage: 'Partial',
|
||||
developmentStage: 'Prototyped',
|
||||
summary:
|
||||
'An opted-in Advanced Moritani preview reveals a different opposing-faction Traitor Card after a loss and replaces it during Mentat Pause.',
|
||||
'Supported native Advanced Moritani reveals a different opposing-faction Traitor Card after a loss and replaces it during Mentat Pause.',
|
||||
steps: [
|
||||
'After you lose a battle, this advanced advantage is available only if the opposing leader disc survived and no Traitor was called.',
|
||||
'You may reveal a Traitor Card for that opposing faction, but it must name a different leader from the one you just fought. If that named leader is not in the Tanks, kill it and collect spice for its value.',
|
||||
@@ -3236,7 +3236,7 @@ export const RULE_TOPICS: RuleTopic[] = [
|
||||
'A Mentat replacement is an ordinary physical Traitor Card, including the original Ix Hero identity when present. After original owned initialization, retire the victim permanently. The replacement itself may later be legally exchanged, returned and drawn again; keep its old receipt unchanged and count one live card, not one per historical reference.',
|
||||
'The authorized revised Advanced PDF, physical page35, requires that you have not revealed your Traitor Card normally this game. A normal own-card reveal therefore forfeits Assassinate Leaders for the rest of this game, across turns and opponents. Existing state preserves that forfeiture; Karama has no effect against the ability. This is the authorized source, not a new GF9 erratum.',
|
||||
'The surviving actual played disc need not be native to its victorious controller. Native Ecaz/Duke6, Tleilaxu/foreign Guild5, CHOAM/Auditor2 and Harkonnen captives use the winner’s printed-faction targets. Captivity or Ghola control never rewrites native identity. Auditor has a matching Traitor outside CHOAM’s ordinary five; it can be a different native target for two but not the disc just played. Duke has no Traitor, and Cheap Hero is not a physical opposing disc. Exceptional captured/Ghola targets remain separate.',
|
||||
'Terror Assassination chooses a random leader when its token is triggered. It does not use this battle-loss condition, different-leader Traitor Card or once-per-faction allowance. Moritani starts remain unavailable in Basic and Advanced.',
|
||||
'Terror Assassination chooses a random leader when its token is triggered. It does not use this battle-loss condition, different-leader Traitor Card or once-per-faction allowance. Fresh supported plain/card Advanced Moritani starts initialize this printed advantage automatically; Basic never gains it. Independent cards also compose standalone Moritani/classic all14 Skills without other modules, retaining the Advanced Harkonnen and E3 pair/mixed Skills guards. Played saves do not gain invented prior-call history.',
|
||||
],
|
||||
checklist: [
|
||||
{area:'Implementation',status:'Partial',detail:'Canonical played-disc/native target semantics remain. Moritani replacement accepts physical Hero separately; permanent victim cards join the shared removed census, while historical replacement IDs may move/recur legally. Exchange, Face Dancer, Betrayal and Rihani consumers bind their own source zone. Current authorization/census, revealed-card reservation and original parent remain.',evidence:['game/moritani-assassinate.ts','game/nexus-traitor-exchange.ts','game/nexus-face-dancers.ts','game/nexus-harkonnen-betrayal.ts','game/rihani-decipherer.ts','game/engine.ts']},
|
||||
@@ -4931,7 +4931,7 @@ export const RULE_TOPICS: RuleTopic[] = [
|
||||
'Explicit guild-betrayal --leader-skills retains the original classic native-Guild Nexus profile without Homeworlds, Tech, Strongholds, Discovery, Semuta or other overlays. All14 training remains native. Ordinary skill quotes now admit the composition, restoring actual Mentat private questions before posture and Sandmaster ground collection rather than only enabling Bureaucrat. Native card custody, paid forces, original Betrayal Use/Pass and movement/plan continuation stay unchanged. Other combinations and complete-mode acceptance remain separate.',
|
||||
'Basic Moritani retains base opponents including Harkonnen. Its separate bounded Advanced classic/non-Harkonnen roster now composes the original assassination preview and all fourteen skills: winner Suk rescue and Rihani choice finish first, then an eligible different trained disc may be assassinated with its exact card return and printed bounty. No casualties repeat; original set-aside and one private Mentat replacement remain. See Moritani with Leader Skills for the exact scope.',
|
||||
'Fresh Advanced profiles compose native Ixians, Tleilaxu, CHOAM and/or Richese with classic opponents and distinct required family decks. All fourteen skills and native starts remain. Auditor stays unskilled; cyborg casualties, original CHOAM support and Richese capped marker revelation remain physical. Foreign revival creates no new skill. Supplied Advanced p34 excludes Richese auctions from Ixian Technology; own mixed No-Field, captured replacement, Advanced Atreides Suk/KH and unrelated overlays retain their guards.',
|
||||
'The independent Recruits, Reinforcements and Harass & Withdraw cards may be selected with all fourteen Skills in fresh classic or selected Ix/CHOAM native/base Basic/Advanced games, without other modules. Original36/38/50 ordinary decks, any separate Richese cache, native setup, free-revival exceptions and paid Cyborg prices stay. Native typed fee/withdrawal/Suk outcomes and ordinary Smuggler, Planetologist and Sandmaster capabilities use one shared admission; the card variant does not silently remove those skills. Other faction/module combinations retain their explicit guards.',
|
||||
'The independent Recruits, Reinforcements and Harass & Withdraw cards may be selected with all fourteen Skills in fresh classic, selected Ix/CHOAM native/base or standalone Ecaz OR Moritani/classic Basic/Advanced games, without other modules. Original36/38/50 decks, Richese cache, native setup, free-revival exceptions and paid Cyborg prices stay. Card fee/withdrawal/Suk and ordinary Smuggler, Planetologist and Sandmaster capabilities share admission. Ecaz retains selected-lead/separate armies; Advanced Moritani keeps skill-first assassination and one Mentat replacement. Advanced Harkonnen, E3 native-pair/mixed Skills and other module guards remain.',
|
||||
'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/Arrakis coalition and Moritani assassination compose original rules; mixed E3, exceptional copied defenses, active skill/Stone and other guards stay separate.',
|
||||
'Fresh classic Leader Skills may explicitly add Nexus through local leader-skills --nexus-cards, Basic/Advanced two through six players, retaining selected original Tech at three or more seats and/or Advanced Strongholds. --discoveries additionally composes original Discovery7/8 in this classic profile. Saved hands/offers remain authoritative; the actual end-Spice closing draw follows Maker votes/typed rides, both Advanced piles and settled alliance. Free-return ledgers, bank purchase and Traitor Deck declarations stay distinct from training. Original phase-end industry, held payer support/physical defenses, physical Suk rescue and subsequent mandatory winner Tech rewards retain their own timing. Homeworlds, Banker/Mentat previews, borrowed Smuggler arithmetic and unresolved interactions remain separate.',
|
||||
|
||||
Reference in new issue
Block a user