Prototype Bene Gesserit Nexus Voice betrayal response
This commit is contained in:
1 parent
22a81c6f7a
commit
8956cf0be7
10 files changed
+384
-11
No files matched your search
+7
-3
@@ -1422,6 +1422,8 @@ function policyActions(g: GameView): Action[] {
|
||||
}
|
||||
if (g.nexusEmperorBetrayal)
|
||||
return [{ type: 'nexusEmperorBetrayal', event: g.nexusEmperorBetrayal.event }];
|
||||
if (g.nexusBgBetrayal)
|
||||
return [{ type: 'nexusBgBetrayal', event: g.nexusBgBetrayal.event }];
|
||||
if (g.response) {
|
||||
const benefitEnemy =
|
||||
g.response.owner !== me.id && g.response.owner !== me.ally;
|
||||
@@ -1473,9 +1475,11 @@ function policyActions(g: GameView): Action[] {
|
||||
g.response.intent === 'double'))))
|
||||
)
|
||||
return [{ type: 'card', card: card.id, mode: 'cancel' }];
|
||||
if (g.response.kind === 'eliteStrength' && g.nexusCards &&
|
||||
g.players.some((player) => player.id === g.response!.owner && player.faction === 'emperor') &&
|
||||
!g.responseControls?.hasPassed)
|
||||
if (g.nexusCards && !g.responseControls?.hasPassed &&
|
||||
((g.response.kind === 'eliteStrength' &&
|
||||
g.players.some((player) => player.id === g.response!.owner && player.faction === 'emperor')) ||
|
||||
(g.response.kind === 'voice' &&
|
||||
g.players.some((player) => player.id === g.response!.owner && player.faction === 'beneGesserit'))))
|
||||
return [{ type: 'passResponse' }];
|
||||
return !g.responseControls?.cancelCards.length ||
|
||||
g.responseControls.hasPassed
|
||||
|
||||
+63
-3
@@ -1468,6 +1468,9 @@ export type Game = {
|
||||
nexusEcazBetrayalHistory?: { snapshot: EcazBetrayalSnapshot; stage: 'pending' | 'complete' }[];
|
||||
/** One actual Sardaukar battle advantage suppressed by a spent Emperor card. */
|
||||
nexusEmperorBetrayalHistory?: { event: string; turn: number; territory: string; owner: string; target: string }[];
|
||||
/** Original native Voice canceled by a spent Bene Gesserit Nexus card. */
|
||||
nexusBgBetrayalHistory?: { event: string; turn: number; territory: string; owner: string;
|
||||
target: string; voice: NonNullable<Battle['voice']> }[];
|
||||
nexusFaceDancerHistory?: NexusFaceDancerReceipt[];
|
||||
nexusSuboidHistory?: NexusSuboidReceipt[];
|
||||
nexusSuboidLast?: { event: string; owner: string; turn: number };
|
||||
@@ -1762,12 +1765,12 @@ function emperorBetrayalWindow(g: Game) {
|
||||
return !!(g.nexusCards?.cards && g.status === 'playing' && g.phase === 6 &&
|
||||
battle && !battle.revealed && battle.event && response?.kind === 'eliteStrength' &&
|
||||
response.owner === byFaction(g, 'emperor')?.id &&
|
||||
[battle.attacker, battle.defender].includes(response.owner) &&
|
||||
(response.owner === battle.attacker || response.owner === battle.defender) &&
|
||||
!battle.eliteBlocked?.includes(response.owner));
|
||||
}
|
||||
function nexusEmperorBetrayalOffer(g: Game, owner: string) {
|
||||
const player = g.players.find((p) => p.id === owner);
|
||||
if (g.truthtrance || !emperorBetrayalWindow(g) || !player || player.ally ||
|
||||
if (g.truthtrance || pendingNexusTraitors(g) || !emperorBetrayalWindow(g) || !player || player.ally ||
|
||||
owner === g.response!.owner || g.response!.passed.includes(owner) ||
|
||||
g.nexusCards!.cards!.hands[owner] !== 'emperor') return null;
|
||||
return { event: g.battle!.event!, target: g.response!.owner };
|
||||
@@ -1811,6 +1814,60 @@ function playNexusEmperorBetrayal(g: Game, p: Player, action: Action) {
|
||||
log(g, `${p.name} spent Emperor Nexus Betrayal to suppress ${getPlayer(g, record.target).name}'s Sardaukar strength in this battle. Actual starred counters remain Sardaukar.`,
|
||||
{ faction: p.faction, name: 'Emperor Nexus Betrayal' });
|
||||
}
|
||||
function bgBetrayalWindow(g: Game) {
|
||||
const response = g.response, battle = g.battle;
|
||||
return !!(g.nexusCards?.cards && g.status === 'playing' && g.phase === 6 &&
|
||||
battle && !battle.revealed && battle.event && battle.voice &&
|
||||
response?.kind === 'voice' && response.owner === byFaction(g, 'beneGesserit')?.id &&
|
||||
(battle.voice.target === battle.attacker || battle.voice.target === battle.defender));
|
||||
}
|
||||
function nexusBgBetrayalOffer(g: Game, owner: string) {
|
||||
const player = g.players.find(p => p.id === owner);
|
||||
if (g.truthtrance || pendingNexusTraitors(g) || !bgBetrayalWindow(g) || !player || player.ally ||
|
||||
owner === g.response!.owner || g.response!.passed.includes(owner) ||
|
||||
g.nexusCards!.cards!.hands[owner] !== 'beneGesserit') return null;
|
||||
return { event: g.battle!.event!, target: g.response!.owner };
|
||||
}
|
||||
function nexusBgBetrayalIntegrity(g: Game) {
|
||||
const history = g.nexusBgBetrayalHistory;
|
||||
if (history === undefined) return;
|
||||
requireRule(g.nexusCards?.cards && Array.isArray(history) && history.length > 0,
|
||||
'Bene Gesserit Betrayal has lost its physical card module.');
|
||||
const events = new Set<string>();
|
||||
for (const record of history) {
|
||||
requireRule(record && typeof record.event === 'string' && record.event.length > 0 &&
|
||||
Number.isInteger(record.turn) && record.turn > 0 && record.turn <= g.turn &&
|
||||
typeof record.territory === 'string' && record.territory.length > 0 &&
|
||||
record.owner !== record.target &&
|
||||
g.players.some(p => p.id === record.owner) &&
|
||||
g.players.some(p => p.id === record.target && p.faction === 'beneGesserit') &&
|
||||
record.voice && typeof record.voice.must === 'boolean' &&
|
||||
VOICE_KINDS.includes(record.voice.kind) &&
|
||||
g.players.some(p => p.id === record.voice.target) && !events.has(record.event),
|
||||
'Bene Gesserit Betrayal has a malformed or repeated Voice receipt.');
|
||||
events.add(record.event);
|
||||
}
|
||||
const last = history.at(-1)!;
|
||||
if (last.turn === g.turn) requireRule(g.nexusCards.cards.discard.includes('beneGesserit'),
|
||||
'Bene Gesserit Betrayal has lost its spent physical card.');
|
||||
if (g.battle?.event === last.event) requireRule(g.battle.territory === last.territory &&
|
||||
!g.battle.voice,
|
||||
'The current battle has regained its canceled Voice.');
|
||||
}
|
||||
function playNexusBgBetrayal(g: Game, p: Player, action: Action) {
|
||||
const offer = nexusBgBetrayalOffer(g, p.id);
|
||||
requireRule(offer && action.event === offer.event &&
|
||||
Object.keys(action).sort().join(',') === 'event,type',
|
||||
'Choose your current Bene Gesserit Voice response.');
|
||||
const record = { event: offer.event, turn: g.turn, territory: g.battle!.territory,
|
||||
owner: p.id, target: g.response!.owner, voice: structuredClone(g.battle!.voice!) };
|
||||
g.nexusCards!.cards = nexusRule(() =>
|
||||
discardNexusCard(g.nexusCards!.cards!, p.id, g.players));
|
||||
(g.nexusBgBetrayalHistory ??= []).push(record);
|
||||
finishResponse(g, true);
|
||||
log(g, `${p.name} spent Bene Gesserit Nexus Betrayal to prevent ${getPlayer(g, record.target).name}'s Voice in this battle. The battle and any later Prescience remain available.`,
|
||||
{ faction: p.faction, name: 'Bene Gesserit Nexus Betrayal' });
|
||||
}
|
||||
function nexusEmperorSecretAllyOffer(g:Game,owner:string) {
|
||||
const p = g.players.find(p => p.id === owner);
|
||||
if (!p || g.nexusCards?.cards?.hands[owner] !== 'emperor' || byFaction(g,'emperor')) return null;
|
||||
@@ -14875,6 +14932,7 @@ function marketGholaIntegrity(g: Game) {
|
||||
nexusCardsIntegrity(g);
|
||||
nexusEmperorSecretIntegrity(g);
|
||||
nexusEmperorBetrayalIntegrity(g);
|
||||
nexusBgBetrayalIntegrity(g);
|
||||
nexusFremenRevivalIntegrity(g);
|
||||
nexusFremenCunningIntegrity(g);
|
||||
nexusEcazBetrayalIntegrity(g);
|
||||
@@ -21374,7 +21432,7 @@ function settleAutomaticContinuations(g: Game) {
|
||||
g.players.some(
|
||||
(p) =>
|
||||
!response.passed.includes(p.id) &&
|
||||
(emperorBetrayalWindow(g) ||
|
||||
(emperorBetrayalWindow(g) || bgBetrayalWindow(g) ||
|
||||
responseCancelCards(g, p, response).length > 0),
|
||||
)
|
||||
)
|
||||
@@ -21536,6 +21594,7 @@ function applyActionInner(
|
||||
if (t === 'nexusChoamTrade') { playNexusChoamTrade(g, p, action); return g; }
|
||||
if (t === 'nexusEmperorRevive') { playNexusEmperorRevive(g,p,action); return g; }
|
||||
if (t === 'nexusEmperorBetrayal') { playNexusEmperorBetrayal(g,p,action); return g; }
|
||||
if (t === 'nexusBgBetrayal') { playNexusBgBetrayal(g,p,action); return g; }
|
||||
if (t === 'nexusFremenRevive') { playNexusFremenRevive(g,p,action); return g; }
|
||||
if (t === 'nexusMoritaniBetrayal') { playMoritaniBetrayal(g, p, action); return g; }
|
||||
if (t === 'nexusEcazBetrayal') { playEcazBetrayal(g, p, action); return g; }
|
||||
@@ -25453,6 +25512,7 @@ export function viewGame(state: Game, id: string) {
|
||||
nexusChoamTrade: currentNexusChoamTrade(g, id),
|
||||
nexusEmperorSecretAlly: nexusEmperorSecretAllyOffer(g,id),
|
||||
nexusEmperorBetrayal: nexusEmperorBetrayalOffer(g, id),
|
||||
nexusBgBetrayal: nexusBgBetrayalOffer(g, id),
|
||||
nexusFremenRevival: fremenNexusRevivalOffer(g,id),
|
||||
nexusMoritani: nexusMoritaniOffer(g,id),
|
||||
nexusMoritaniBetrayal: moritaniBetrayalOffer(g,id,
|
||||
|
||||
+2
-1
@@ -4006,6 +4006,7 @@ export const RULE_TOPICS: RuleTopic[] = [
|
||||
'In Advanced play, Bene Gesserit Cunning converts selected whole advisor territories during your own Shipment and Movement action. All your counters in each selected territory become fighters together across sectors. It spends the Nexus card once, with no spice, shipment or movement cost.',
|
||||
'One Karama response covers the entire declared territory set. Cancellation keeps every selected group as advisors and leaves the Nexus card spent. Existing territory restrictions still apply; fresh accompanied advisors and storm-related conversion remain unavailable pending their rulings.',
|
||||
'Bene Gesserit Secret Ally borrowed Voice remains unavailable pending its cancellation and timing interpretation. Its printed permission does not make the other unfinished effects available.',
|
||||
'Bene Gesserit Betrayal can prevent the native Voice while the ordinary pre-plan response is open. In Nexus games every seat responds even without a canceling card, so the window does not reveal the hidden holder. An unallied opponent spends one physical Bene Gesserit Nexus card to remove only that declared Voice; Prescience and plan commitments continue. Truthtrance has priority. This path does not grant borrowed Voice.',
|
||||
'Emperor Cunning requires an Advanced battle with at least five ordinary Emperor counters and no actual Sardaukar, before submitting your plan. The played Nexus card opens one Karama response. Allowing it makes five ordinary counters count as Sardaukar for this battle; cancellation prevents the entire enhancement and leaves the Nexus card spent.',
|
||||
'Temporary Sardaukar use the applicable strength and support rules, including the Fremen exception, but remain physically ordinary counters. Their losses go to Tanks as ordinary counters, without changing starred reserves or Tanks. Existing inspected or promised plans remain binding. The role ends after this battle; fewer-than-five groups remain unavailable. The separate Secret Ally revival has its own connected controls; Emperor purchase modes remain unfinished.',
|
||||
'Emperor Betrayal has a battle alternative in Advanced play: an unallied opponent holding the Emperor Nexus card can spend it during the ordinary pre-plan Sardaukar response to suppress actual Sardaukar strength for this battle. Every seat receives the same response opportunity even with no card, keeping hidden custody private. The starred pieces remain physical Sardaukar; no card grants a new force or cancels their separate support benefit.',
|
||||
@@ -4025,7 +4026,7 @@ export const RULE_TOPICS: RuleTopic[] = [
|
||||
],
|
||||
related: ['ecaz-modules', 'spice-blow', 'alliance-funding'],
|
||||
checklist: [
|
||||
{ area: 'Implementation', status: 'Partial', detail: 'Physical lifecycle, Atreides inspections, Harkonnen exchanges/declarations, revealed Face Dancer refresh, turn-wide Suboid strength, whole-territory advisor conversion, temporary Emperor Sardaukar and actual Sardaukar Betrayal suppression, bounded Fremen Secret Ally free-three revival and native empty-worm Cunning, five source-aware CHOAM Cunning effects, Moritani Cunning placement and Betrayal token return, Ecaz Betrayal allied-force return, and Richese Secret Ally reserve shipping priced as one. Borrowed Voice, other Betrayal reactions, Emperor purchase effects, Tleilaxu revival, Fremen worm protection and combined Cunning interactions remain unfinished.', evidence: ['game/nexus-cards.ts', 'game/nexus-card-phase.ts', 'game/nexus-traitor-exchange.ts', 'game/traitor-declarations.ts', 'game/nexus-face-dancers.ts', 'game/nexus-suboids.ts', 'game/nexus-advisors.ts', 'game/nexus-sardaukar-options.ts', 'game/nexus-fremen-revival.ts', 'game/nexus-fremen-cunning.ts', 'game/nexus-ecaz-betrayal.ts', 'game/choam-power-options.ts', 'game/nexus-moritani-options.ts', 'game/nexus-moritani-betrayal.ts', 'game/nexus-richese-options.ts', 'game/nexus-guild-cunning-options.ts', 'game/nexus-guild-secret-ally-options.ts', 'game/engine.ts'] },
|
||||
{ area: 'Implementation', status: 'Partial', detail: 'Physical lifecycle, Atreides inspections, Harkonnen exchanges/declarations, revealed Face Dancer refresh, turn-wide Suboid strength, whole-territory advisor conversion and native Voice Betrayal suppression, temporary Emperor Sardaukar and actual Sardaukar Betrayal suppression, bounded Fremen Secret Ally free-three revival and native empty-worm Cunning, five source-aware CHOAM Cunning effects, Moritani Cunning placement and Betrayal token return, Ecaz Betrayal allied-force return, and Richese Secret Ally reserve shipping priced as one. Borrowed Voice, other Betrayal reactions, Emperor purchase effects, Tleilaxu revival, Fremen worm protection and combined Cunning interactions remain unfinished.', evidence: ['game/nexus-cards.ts', 'game/nexus-card-phase.ts', 'game/nexus-traitor-exchange.ts', 'game/traitor-declarations.ts', 'game/nexus-face-dancers.ts', 'game/nexus-suboids.ts', 'game/nexus-advisors.ts', 'game/nexus-sardaukar-options.ts', 'game/nexus-fremen-revival.ts', 'game/nexus-fremen-cunning.ts', 'game/nexus-ecaz-betrayal.ts', 'game/choam-power-options.ts', 'game/nexus-moritani-options.ts', 'game/nexus-moritani-betrayal.ts', 'game/nexus-richese-options.ts', 'game/nexus-guild-cunning-options.ts', 'game/nexus-guild-secret-ally-options.ts', 'game/engine.ts'] },
|
||||
{ area: 'Player controls', status: 'Partial', detail: 'Private lifecycle, inspections, exchanges, Tleilaxu refresh, Ixian activation, advisor selection, temporary Sardaukar and CHOAM effects with native Karama responses, Moritani Betrayal token selection, Ecaz Betrayal shared-territory choice, Richese shipment pricing and Fremen Cunning offer/remote ride. Remaining effect controls are unfinished.', evidence: ['components/nexus-cards.tsx', 'components/nexus-traitors.tsx', 'components/nexus-tleilaxu.tsx', 'components/nexus-suboids.tsx', 'components/nexus-advisors.tsx', 'components/nexus-sardaukar.tsx', 'components/choam-power-cost.tsx', 'components/moritani-terror.tsx', 'components/nexus-moritani-betrayal.tsx', 'components/nexus-ecaz-betrayal.tsx', 'components/game-table.tsx'] },
|
||||
{ area: 'AI', status: 'Partial', detail: 'All profiles handle integrated effects through private offers, including Moritani Betrayal public-token selection, Ecaz Betrayal public-territory selection and legal Fremen Cunning offer/ride decisions. Complete module strategy and calibration remain unfinished.', evidence: ['game/nexus-card-options.ts', 'game/nexus-traitor-options.ts', 'game/nexus-tleilaxu-options.ts', 'game/nexus-suboid-options.ts', 'game/nexus-advisor-options.ts', 'game/nexus-sardaukar-options.ts', 'game/choam-power-options.ts', 'game/nexus-moritani-options.ts', 'game/nexus-moritani-betrayal-options.ts', 'game/nexus-ecaz-betrayal-options.ts', 'game/nexus-richese-options.ts', 'game/nexus-guild-cunning-options.ts', 'game/bots.ts'] },
|
||||
{ area: 'Documentation', status: 'Partial', detail: 'Common rules, twelve printed faces and integrated effect families retain explicit unresolved boundaries, including Ecaz Duke/inquiry, a second Cunning offer during an interrupted Fremen control, Great Maker and combined modules, fresh advisors, storm and borrowed Voice.', evidence: ['docs/NEXUS_CARD_RULES.md', 'docs/NEXUS_ATREIDES_RULES.md', 'docs/NEXUS_HARKONNEN_RULES.md', 'docs/NEXUS_TLEILAXU_RULES.md', 'docs/NEXUS_IXIAN_CUNNING_RULES.md', 'docs/NEXUS_CUNNING_RUNTIME.md', 'docs/NEXUS_BENE_GESSERIT_RULES.md', 'docs/NEXUS_ADVISOR_RUNTIME.md', 'docs/NEXUS_EMPEROR_RULES.md', 'docs/NEXUS_SARDAUKAR_RUNTIME.md', 'docs/NEXUS_FREMEN_RULES.md', 'docs/NEXUS_ECAZ_RULES.md', 'docs/NEXUS_CHOAM_RULES.md', 'docs/NEXUS_CHOAM_RUNTIME.md', 'docs/NEXUS_MORITANI_RULES.md', 'docs/NEXUS_MORITANI_RUNTIME.md', 'docs/NEXUS_RICHESE_RUNTIME.md', 'docs/NEXUS_GUILD_CUNNING_RUNTIME.md'] },
|
||||
|
||||
Reference in new issue
Block a user