Connect bounded Fremen Nexus worm-riding Betrayal
This commit is contained in:
1 parent
66a427ab55
commit
54cbf4d06f
14 files changed
+457
-55
No files matched your search
+46
-17
@@ -57,7 +57,7 @@ import { createNexusSardaukar, validateNexusSardaukar, type NexusSardaukarReceip
|
||||
import { createNexusMoritani, validateNexusMoritani, quoteNexusMoritaniPlacement, type NexusMoritaniReceipt } from './nexus-moritani';
|
||||
import { moritaniBetrayalOffer } from './nexus-moritani-betrayal';
|
||||
import { ecazBetrayalOffer, quoteEcazBetrayal, validateEcazBetrayalSnapshot, type EcazBetrayalSnapshot } from './nexus-ecaz-betrayal';
|
||||
import { fremenBetrayalOffer, fremenBetrayalSignature, validateFremenBetrayal, type FremenNexusBetrayal } from './nexus-fremen-betrayal';
|
||||
import { fremenBetrayalOffer, fremenBetrayalSignature, validateFremenBetrayal, type FremenNexusBetrayal, type StoredFremenNexusBetrayal } from './nexus-fremen-betrayal';
|
||||
import { atomicsAllianceChangeBlocked, atomicsAllianceStatus, atomicsShipmentBlocked, quoteMoritaniAtomics, type AtomicsAftermath, type AtomicsQuote } from './moritani-atomics';
|
||||
import { nexusMoritaniRecordSignature, terrorLocationAllowed, terrorEntryLocationAllowed } from './terror-location';
|
||||
import { CHOAM_NEXUS_EFFECTS, createNexusChoam, validateNexusChoam, type NexusChoamEffect, type NexusChoamReceipt } from './nexus-choam';
|
||||
@@ -592,6 +592,7 @@ export type Player = {
|
||||
ixMovementBlocked?: { turn: number; move: number };
|
||||
fremenMovementBlocked?: { turn: number; move: number };
|
||||
fremenNexusMovementBlockedTurn?: number;
|
||||
fremenNexusWormBlockedTurn?: number;
|
||||
/** Permanent printed one-card reduction after Atomics; bound to its Aftermath receipt. */
|
||||
atomicsHandLimitPenalty?: boolean;
|
||||
elites?: {
|
||||
@@ -1569,8 +1570,8 @@ export type Game = {
|
||||
}[];
|
||||
/** A spent Ecaz card binds one exact allied force group until Karama settles. */
|
||||
nexusEcazBetrayalHistory?: { snapshot: EcazBetrayalSnapshot; stage: 'pending' | 'complete' }[];
|
||||
/** A spent Fremen card suppresses the native two-territory advantage through its turn. */
|
||||
nexusFremenBetrayalHistory?: FremenNexusBetrayal[];
|
||||
/** Spent Fremen card receipts include the earlier movement-only saved form. */
|
||||
nexusFremenBetrayalHistory?: StoredFremenNexusBetrayal[];
|
||||
/** 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. */
|
||||
@@ -2135,41 +2136,60 @@ function nexusFremenBetrayalIntegrity(g: Game) {
|
||||
const history = g.nexusFremenBetrayalHistory;
|
||||
const fremen = byFaction(g, 'fremen');
|
||||
if (history === undefined) {
|
||||
requireRule(fremen?.fremenNexusMovementBlockedTurn === undefined,
|
||||
'Fremen movement suppression has no spent Nexus card receipt.');
|
||||
requireRule(fremen?.fremenNexusMovementBlockedTurn === undefined &&
|
||||
fremen?.fremenNexusWormBlockedTurn === undefined,
|
||||
'Fremen Betrayal restrictions require a spent Nexus card receipt.');
|
||||
return;
|
||||
}
|
||||
requireRule(g.nexusCards?.cards && fremen && Array.isArray(history) &&
|
||||
history.length > 0, 'Fremen Betrayal has lost its physical card module.');
|
||||
const turns = new Set<number>();
|
||||
let latestMovement: number | undefined;
|
||||
let latestWorm: number | undefined;
|
||||
let lastMode: FremenNexusBetrayal['mode'] = 'movement';
|
||||
for (const record of history) {
|
||||
nexusRule(() => validateFremenBetrayal(g, record));
|
||||
const mode = nexusRule(() => validateFremenBetrayal(g, record));
|
||||
requireRule(!turns.has(record.turn),
|
||||
'Fremen Betrayal cannot suppress movement twice in one turn.');
|
||||
'Fremen Betrayal cannot suppress two advantages in one turn.');
|
||||
turns.add(record.turn);
|
||||
if (mode === 'worm') latestWorm = record.turn;
|
||||
else latestMovement = record.turn;
|
||||
lastMode = mode;
|
||||
}
|
||||
const last = history.at(-1)!;
|
||||
requireRule(fremen.fremenNexusMovementBlockedTurn === last.turn,
|
||||
'Fremen Betrayal has lost its turn-long movement suppression.');
|
||||
if (last.turn === g.turn)
|
||||
requireRule(g.nexusCards.cards.discard.includes('fremen'),
|
||||
'Fremen Betrayal has lost its spent physical card.');
|
||||
requireRule(fremen.fremenNexusMovementBlockedTurn === latestMovement &&
|
||||
fremen.fremenNexusWormBlockedTurn === latestWorm,
|
||||
'Fremen Betrayal has lost its turn-long restriction.');
|
||||
if (last.turn === g.turn) {
|
||||
requireRule(lastMode !== 'worm' || (g.nexusCards.phase?.turn === g.turn &&
|
||||
!g.wormRides.length && !g.nexusFremenCunningOffer &&
|
||||
!g.nexusFremenCunningRides?.some(row =>
|
||||
row.occurrence.turn === g.turn && row.stage !== 'complete')),
|
||||
'Fremen Betrayal cannot leave a current worm ride or Cunning offer.');
|
||||
if (lastMode === 'movement' || g.nexusCards.phase?.stage === 'spice')
|
||||
requireRule(g.nexusCards.cards.discard.includes('fremen'),
|
||||
'Fremen Betrayal has lost its spent physical card.');
|
||||
}
|
||||
}
|
||||
function playNexusFremenBetrayal(g: Game, p: Player, action: Action) {
|
||||
const offer = fremenBetrayalOffer(g, p.id);
|
||||
requireRule(offer && !offer.blocked && action.event === offer.event &&
|
||||
Object.keys(action).sort().join(',') === 'event,type',
|
||||
'Use Fremen Betrayal before Fremen movement begins.');
|
||||
'Use Fremen Betrayal before its current printed advantage begins.');
|
||||
const record: FremenNexusBetrayal = {
|
||||
event: offer.event, owner: p.id, target: offer.target, turn: g.turn,
|
||||
phase: 5, signature: '',
|
||||
phase: offer.mode === 'worm' ? 1 : 5, mode: offer.mode, signature: '',
|
||||
};
|
||||
record.signature = fremenBetrayalSignature(record);
|
||||
g.nexusCards!.cards = nexusRule(() =>
|
||||
discardNexusCard(g.nexusCards!.cards!, p.id, g.players));
|
||||
getPlayer(g, offer.target).fremenNexusMovementBlockedTurn = g.turn;
|
||||
const fremen = getPlayer(g, offer.target);
|
||||
if (offer.mode === 'worm') fremen.fremenNexusWormBlockedTurn = g.turn;
|
||||
else fremen.fremenNexusMovementBlockedTurn = g.turn;
|
||||
(g.nexusFremenBetrayalHistory ??= []).push(record);
|
||||
log(g, `${p.name} spent Fremen Nexus Betrayal to suppress the Fremen two-territory movement advantage this turn. Independent ornithopters and movement abilities remain available.`,
|
||||
log(g, offer.mode === 'worm'
|
||||
? `${p.name} spent Fremen Nexus Betrayal before the first blow to suppress Fremen worm riding for this turn. Worm appearances and protection still resolve; ordinary and remote rides do not.`
|
||||
: `${p.name} spent Fremen Nexus Betrayal to suppress the Fremen two-territory movement advantage this turn. Independent ornithopters and movement abilities remain available.`,
|
||||
{ faction: p.faction, name: 'Fremen Nexus Betrayal' });
|
||||
}
|
||||
function bgBetrayalWindow(g: Game) {
|
||||
@@ -6105,6 +6125,7 @@ function requireFreshSetup(g: Game, allowIxElites = false) {
|
||||
p.noField === undefined && p.noFieldEvent === undefined && p.noFieldBlockedTurn === undefined &&
|
||||
p.advisors === undefined && p.gholaBlocked === undefined && p.ixMovementBlocked === undefined &&
|
||||
p.fremenMovementBlocked === undefined && p.fremenNexusMovementBlockedTurn === undefined &&
|
||||
p.fremenNexusWormBlockedTurn === undefined &&
|
||||
p.atomicsHandLimitPenalty === undefined && p.faceDancers === undefined &&
|
||||
p.faceDancerReplacedTurn === undefined && p.revealedTraitors === undefined;
|
||||
}), 'Starting setup requires unused native leaders and no prior alliance, revival or battle history.');
|
||||
@@ -7413,7 +7434,8 @@ function devour(
|
||||
for (const k of Object.keys(g.spice))
|
||||
if (splitLocation(k).territory === t) delete g.spice[k];
|
||||
const fremen = byFaction(g, 'fremen');
|
||||
if (fremen && at(fremen, t)) g.wormRides.push(t);
|
||||
if (fremen && fremen.fremenNexusWormBlockedTurn !== g.turn && at(fremen, t))
|
||||
g.wormRides.push(t);
|
||||
log(g, `Shai-Hulud appeared in ${territory(t).name}.`);
|
||||
}
|
||||
function beginWorm(g: Game, t: string, origin: 'natural' | 'additional' | 'summoned' = 'natural') {
|
||||
@@ -7429,6 +7451,7 @@ function beginWorm(g: Game, t: string, origin: 'natural' | 'additional' | 'summo
|
||||
!g.techTokens && !g.strongholdCards &&
|
||||
g.players.every(seat => ['atreides', 'harkonnen', 'emperor', 'guild', 'beneGesserit', 'fremen'].includes(seat.faction)) &&
|
||||
fremen && !fremen.ally && g.phase === 1 &&
|
||||
fremen.fremenNexusWormBlockedTurn !== g.turn &&
|
||||
Object.entries(fremen.forces).some(([key, amount]) => {
|
||||
if (amount <= 0) return false;
|
||||
const source = splitLocation(key);
|
||||
@@ -8021,6 +8044,7 @@ function nextWormRide(g: Game) {
|
||||
return;
|
||||
}
|
||||
const fremen = byFaction(g, 'fremen');
|
||||
if (fremen?.fremenNexusWormBlockedTurn === g.turn) g.wormRides.length = 0;
|
||||
while (g.wormRides.length) {
|
||||
const t = g.wormRides.shift()!;
|
||||
if (
|
||||
@@ -8038,6 +8062,10 @@ function nextWormRide(g: Game) {
|
||||
}
|
||||
for (const ride of g.nexusFremenCunningRides ?? []) {
|
||||
if (ride.stage !== 'queued' || ride.occurrence.turn !== g.turn) continue;
|
||||
if (fremen?.fremenNexusWormBlockedTurn === g.turn) {
|
||||
ride.stage = 'complete';
|
||||
continue;
|
||||
}
|
||||
const source = nexusRule(() => quoteFremenCunningRide(
|
||||
g, ride.authorization.owner, ride.occurrence, ride.authorization,
|
||||
));
|
||||
@@ -27734,6 +27762,7 @@ export function viewGame(state: Game, id: string) {
|
||||
(p.fremenMovementBlocked?.turn === g.turn &&
|
||||
p.fremenMovementBlocked.move === p.moved),
|
||||
fremenNexusMovementBlocked: p.fremenNexusMovementBlockedTurn === g.turn,
|
||||
fremenNexusWormBlocked: p.fremenNexusWormBlockedTurn === g.turn,
|
||||
ixMovementBlocked:
|
||||
p.ixMovementBlocked?.turn === g.turn &&
|
||||
p.ixMovementBlocked.move === p.moved,
|
||||
|
||||
Reference in new issue
Block a user