Connect Tleilaxu Nexus free leader and paid force revival

This commit is contained in:
Quintine committed 2026-10-14 23:27:30 +11:00
1 parent 4bf6cfc875
commit 72e37a8978
22 files changed
+1589 -80

No files matched your search

+49 -5
View File
@@ -309,6 +309,7 @@ import {
DiscoveryCollectionError,
type DiscoveryCollectionQuote,
} from './discovery-collection';
import {tleilaxuSecretAllyOptions,quoteTleilaxuSecretAlly} from './nexus-tleilaxu-revival';
import {
quoteMovementCancellation,
validateAmbassadorRelocationContext,
@@ -3269,9 +3270,10 @@ function nexusFaceDancerIntegrity(g: Game) {
}
function projectedNexusTleilaxu(g: Game, id: string) {
if (!g.nexusCards?.cards) return null;
if (g.nexusCards.cards.hands[id] !== 'tleilaxu') return { cunning: null };
if (g.nexusCards.cards.hands[id] !== 'tleilaxu') return { cunning: null, secretAlly: null };
const p = getPlayer(g, id);
if (p.faction !== 'tleilaxu') return { cunning: null };
if (p.faction !== 'tleilaxu')
return { cunning: null, secretAlly: byFaction(g,'tleilaxu')?null:tleilaxuSecretAllyOptions(g,p) };
const count = p.faceDancers?.filter(card => card.revealed).length ?? 0;
let blocked: string | null = null;
if (g.status !== 'playing') blocked = 'Use Cunning during an active game.';
@@ -3282,7 +3284,7 @@ function projectedNexusTleilaxu(g: Game, id: string) {
else if (pendingNexusTraitors(g)) blocked = 'Finish the current private Traitor Deck return first.';
else if (!count) blocked = 'No revealed Face Dancers are available to replace.';
else if ((g.traitorReserve?.length ?? 0) < count) blocked = 'The Traitor Deck cannot supply the full replacement draw.';
return { cunning: { event: JSON.stringify(['nexusFaceDancers', g.turn, g.phase, g.nexusFaceDancerHistory?.length ?? 0, id]), count, blocked } };
return { cunning: { event: JSON.stringify(['nexusFaceDancers', g.turn, g.phase, g.nexusFaceDancerHistory?.length ?? 0, id]), count, blocked }, secretAlly: null };
}
function playNexusFaceDancers(g: Game, p: Player, action: Action) {
requireRule(Object.keys(action).every(key => ['type', 'event'].includes(key)),
@@ -3299,6 +3301,46 @@ function playNexusFaceDancers(g: Game, p: Player, action: Action) {
{ faction: p.faction, name: 'Nexus Face Dancer replacement' });
}
function playNexusTleilaxuRevival(g: Game, p: Player, action: Action) {
requireRule(Object.keys(action).every(key=>['type','event','leader','amount','elite'].includes(key)),
'Use only the owned Nexus revival selection; its price and source are authoritative.');
const offer=projectedNexusTleilaxu(g,p.id)?.secretAlly;
requireRule(offer&&!offer.blocked&&action.event===offer.event,
offer?.blocked??'Choose the current Tleilaxu Secret Ally offer.');
const quote=nexusRule(()=>quoteTleilaxuSecretAlly(g,p,{
leader:stringField(action.leader),amount:integer(action.amount,0,5,'Forces'),
elite:integer(action.elite,0,5,'Elite forces'),
}));
const group={amount:quote.amount,elite:quote.elite,free:0};
const grant=quote.amount>0?requireHomeworldRevivalGrant(g,p,'nexusTleilaxu',group):null;
const ordinary=leaderRevivalOptions(g,p);
const payment=g.spiceBankerIncomePreview&&quote.cost>0
?bankerIncomePayment(g,'force-revival',[{payer:p.id,amount:quote.cost,recipient:'bank'}]):undefined;
g.nexusCards!.cards=nexusRule(()=>discardNexusCard(g.nexusCards!.cards!,p.id,g.players));
g.revivalRules??=newRevivalRules();
(g.revivalRules.nexusTleilaxu??=[]).push(p.id);
if(quote.kind!=='duke'||ordinary.cycle>p.revivalCycle)p.revivalCycle=ordinary.cycle;
// The card quote establishes a free own return; the original mutation keeps
// Kwisatz/Duke custody, one-leader usage and skill revival lifecycle.
finishRevival(g,{
pending:{player:p.id,kind:quote.kind==='kwisatz'?'kwisatz':'leader',
...(quote.kind==='kwisatz'?{}:{leader:quote.leader}),normalCost:0,cost:0,free:0,checks:[]},
outcome:'revive',
});
if(quote.amount>0) {
p.spice-=quote.cost;
addNativeReserves(g,p,quote.amount,quote.elite);
p.tanks-=quote.amount;
if(p.freeForcesRevived!==undefined||p.revived===0)p.freeForcesRevived??=0;
p.revived+=quote.amount;
if(p.elites){p.elites.tanks-=quote.elite;p.elites.revived+=quote.elite}
if(payment)awardBankerIncome(g,payment);
stageHomeworldRevivalReturn(g,p,'nexusTleilaxu',group,grant);
}
log(g,`${p.name} spent Tleilaxu Nexus Secret Ally: one own leader returned free and ${quote.amount} physical forces returned for ${quote.cost} bank spice. The ordinary leader slot and shared five-force allowance apply.`,
{faction:p.faction,name:'Tleilaxu Nexus revival'});
}
function nexusSuboidIntegrity(g: Game) {
if (g.nexusSuboidHistory === undefined) {
requireRule(!g.nexusSuboidLast, 'The Suboid Nexus effect has lost its saved history.');
@@ -28796,6 +28838,7 @@ function applyActionInner(
requireRule(g.nexusCards?.phase?.stage !== 'drawing', 'Finish the closing Nexus card choices first.');
if (t === 'nexusTraitorDraw') { playNexusTraitorDraw(g, p, action); return g; }
if (t === 'nexusFaceDancers') { playNexusFaceDancers(g, p, action); return g; }
if (t === 'nexusTleilaxuRevival') { playNexusTleilaxuRevival(g,p,action); return g; }
if (t === 'nexusSuboids') { playNexusSuboids(g, p, action); return g; }
if (t === 'nexusAdvisors') { playNexusAdvisors(g, p, action); return g; }
if (t === 'nexusSardaukar') { playNexusSardaukar(g,p,action); return g; }
@@ -31472,8 +31515,9 @@ function applyActionInner(
requireRule(quote.cost <= p.spice, 'Not enough spice for revival.');
const checks: PendingRevival['checks'] = [];
const choamBenefit = p.faction === 'choam' && !g.revivalRules?.choamBlocked;
const uncancelableLimit =
g.recruits?.turn === g.turn ? 7 : Math.max(3, freeRevivalRate(g, p));
const uncancelableLimit = Math.max(
g.recruits?.turn === g.turn ? 7 : Math.max(3, freeRevivalRate(g, p)),
g.revivalRules?.nexusTleilaxu?.includes(p.id) ? 5 : 0);
if (choamBenefit && (n > quote.free || p.revived + n > uncancelableLimit))
checks.push('choamRevival');
if (!choamBenefit && p.revived + n > uncancelableLimit)
+3 -2
View File
@@ -16,7 +16,8 @@ export type HomeworldRevivalDeploymentContext = Pick<
export type HomeworldRevivalDeploymentSource =
| 'normal'
| 'emperorExtra'
| 'ghola';
| 'ghola'
| 'nexusTleilaxu';
export type HomeworldRevivalDeploymentGroup = {
amount: number;
elite: number;
@@ -61,7 +62,7 @@ export function quoteHomeworldRevivalDeployment(
group: HomeworldRevivalDeploymentGroup,
): HomeworldRevivalDeploymentQuote | null {
if (
!['normal', 'emperorExtra', 'ghola'].includes(source) ||
!['normal', 'emperorExtra', 'ghola', 'nexusTleilaxu'].includes(source) ||
!group ||
typeof group !== 'object' ||
Array.isArray(group) ||
+1 -1
View File
@@ -136,7 +136,7 @@ function validateOriginal(frame: Original): void {
!text(frame.player) ||
!count(frame.turn, 1, Number.MAX_SAFE_INTEGER) ||
!count(frame.phase, 0, 8) ||
!['normal', 'emperorExtra', 'ghola'].includes(frame.source) ||
!['normal', 'emperorExtra', 'ghola', 'nexusTleilaxu'].includes(frame.source) ||
(frame.source === 'ghola' ? !text(frame.card) : frame.card !== undefined) ||
!keys(frame.group, ['amount', 'elite', 'free']) ||
!count(frame.group.amount, 1) ||
+22
View File
@@ -1,5 +1,19 @@
import type { Action, GameView } from './engine';
export function nexusTleilaxuRevivalAction(
game:GameView,event:string,leader:string,amount:number,elite:number,
):Action|null {
const offer=game.nexusTleilaxu?.secretAlly;
const owner=game.players.find(p=>p.id===game.me);
if(!offer||offer.blocked||!owner||game.status!=='playing'||game.phase!==4||
owner.ally||game.nexusCards?.card!=='tleilaxu'||offer.event!==event||
!offer.leaders.some(candidate=>candidate.id===leader)||
!Number.isSafeInteger(amount)||amount<0||amount>offer.maxForces||amount>(owner.spice??0)||
!Number.isSafeInteger(elite)||elite<0||elite>amount||elite>offer.maxElite||
amount-elite>owner.tanks-(owner.elites?.tanks??0))return null;
return {type:'nexusTleilaxuRevival',event,leader,amount,elite};
}
/** The server's private offer binds the complete revealed replacement group.
* Do not reconstruct eligibility from private Face Dancer hands. */
export function nexusFaceDancersAction(
@@ -31,6 +45,14 @@ export function nexusFaceDancersAction(
/** All profiles share the legal replacement policy. There is no post-draw
* choice and no access to another player's hand or the reserve order. */
export function nexusTleilaxuBotActions(game: GameView): Action[] {
const revival=game.nexusTleilaxu?.secretAlly;
if(revival&&!revival.blocked&&revival.leaders.length) {
const owner=game.players.find(p=>p.id===game.me)!;
const amount=Math.min(revival.maxForces,owner.spice??0);
const elite=Math.max(0,amount-(owner.tanks-(owner.elites?.tanks??0)));
const action=nexusTleilaxuRevivalAction(game,revival.event,revival.leaders[0].id,amount,elite);
if(action)return [action];
}
const offer = game.nexusTleilaxu?.cunning;
const action = offer ? nexusFaceDancersAction(game, offer.event) : null;
return action ? [action] : [];
+342
View File
@@ -0,0 +1,342 @@
import type { Game, Player } from './engine';
import { DUKE_VIDAL_ID } from './duke-vidal';
import { nexusCardMode } from './nexus-cards';
import {
eliteRevivalRemaining,
leaderRevivalOptions,
normalForceRevivalLimit,
} from './revival';
/** The panel's own force group is fixed at five exact counters. */
export const NEXUS_TLEILAXU_FORCE_GROUP_CAP = 5;
/** Every counter of that group, typed or not, costs one bank spice; none is free. */
export const NEXUS_TLEILAXU_FORCE_PRICE = 1;
/**
* Provisional composed policy, not a publisher or user numerical ruling.
* `docs/NEXUS_TLEILAXU_RULES.md` leaves three questions open (independent versus
* ordinary force allowance, waived leader prerequisites, optional leader
* portion). This module deliberately answers them the conservative way and
* publishes the answer instead of narrowing the effect silently.
*/
export const NEXUS_TLEILAXU_POLICY =
'Provisional composed policy. One combined action during a quiet Revival by the unallied actual holder of the Tleilaxu Nexus card in a game where Tleilaxu is absent: exactly one own dead leader (or an eligible Kwisatz Haderach / own shared Duke) returns free and uses the ordinary one-leader slot, and that leader portion is mandatory, so a force-only return is unavailable. The card waives only the unavailable-roster trigger, so repeated-death/face-down cycles, the one-leader slot and every special identity custody rule still gate candidates. Up to five exact physical counters return from the same shared ordinary ceiling, which this grant raises to at least five (earlier ordinary returns count against it, CHOAM\'s larger native ceiling is not reduced, and Recruits never doubles this fixed group); each counter costs one bank spice, typed counters included, and none is free or Cyborg-priced. Zero forces is permitted. Advanced Fedaykin/Sardaukar keep the one-per-turn cap across producers; Ixian Cyborgs have no matching cap. Paid card counters are not free forces.';
export type TleilaxuSecretAllyKind = 'leader' | 'kwisatz' | 'duke';
export type TleilaxuSecretAllyLeader = {
id: string;
name: string;
kind: TleilaxuSecretAllyKind;
/** True only when the card's waiver, rather than the ordinary roster trigger, offers this candidate. */
early: boolean;
};
export type TleilaxuSecretAllyOptions = {
event: string;
blocked: string | null;
leaders: TleilaxuSecretAllyLeader[];
maxForces: number;
maxElite: number;
policy: string;
};
export type TleilaxuSecretAllySelection = {
leader: string;
amount: number;
elite: number;
};
export type TleilaxuSecretAllyQuote = {
leader: string;
kind: TleilaxuSecretAllyKind;
amount: number;
elite: number;
/** Exact bank spice for this group; the leader adds nothing. */
cost: number;
/** Maximum legal counter count for the same state, so callers never guess the bound. */
limit: number;
};
/** Physical counter bounds of the card group for one holder and state. */
export type TleilaxuSecretAllyForceBounds = {
/** Ordinary ceiling after this grant's floor of five. */
ceiling: number;
normalTanks: number;
eliteTanks: number;
/** Typed counters that may return under the one-per-turn cap and physical inventory. */
eliteRoom: number;
maxForces: number;
maxElite: number;
};
export type TleilaxuSecretAllyFailure =
| 'source'
| 'timing'
| 'leader'
| 'forces'
| 'price';
export class TleilaxuSecretAllyError extends Error {
constructor(
message: string,
readonly reason: TleilaxuSecretAllyFailure,
) {
super(message);
this.name = 'TleilaxuSecretAllyError';
}
}
const leaderBlock =
'Tleilaxu Secret Ally needs one own dead leader, and zero forces does not replace it.';
const noSource =
'Only the actual holder of the Tleilaxu Nexus card may use Tleilaxu Secret Ally.';
const whole = (n: unknown): n is number =>
Number.isSafeInteger(n) && (n as number) >= 0;
/** Cheap Hero/Heroine are Treachery cards, never leader discs in custody. */
const discId = (id: string) => !id.startsWith('treachery-');
export const tleilaxuSecretAllyEvent = (
g: Pick<Game, 'turn' | 'phase'>,
p: Pick<Player, 'id'>,
) => JSON.stringify(['nexusTleilaxuSecretAlly', g.turn, g.phase, p.id]);
function heldCard(g: Game, p: Player) {
return g.nexusCards?.cards?.hands?.[p.id] === 'tleilaxu';
}
/** Quiet Revival: no other interaction, reaction or pending revival owns the board. */
function interrupted(g: Game) {
return !!(
g.response ||
g.decision ||
g.phaseOpening ||
g.pendingRevival ||
g.pendingKarama ||
g.pendingTreacheryDiscard ||
g.pendingNullentropy ||
g.pendingExchange ||
g.pendingRicheseGift ||
g.pendingAmbassador ||
g.pendingCapture ||
g.battle ||
g.nexusTraitorPending ||
g.nexusTraitorParent ||
g.nexusCards?.phase?.stage === 'drawing' ||
(g.homeworldRevivalReturn && g.homeworldRevivalReturn.stage !== 'complete')
);
}
/** Actual held card, printed mode, unallied actor and quiet Revival only.
* A projected offer is not authority: validate the physical held card again. */
function sourceState(
g: Game,
p: Player,
): { reason: TleilaxuSecretAllyFailure; message: string } | null {
if (!heldCard(g, p)) return { reason: 'source', message: noSource };
if (p.ally)
return { reason: 'source', message: 'An allied player cannot use a Nexus card.' };
let mode: string;
try {
mode = nexusCardMode(
'tleilaxu',
p.faction,
g.players.map((seat) => seat.faction),
);
} catch {
return { reason: 'source', message: 'The Nexus custody does not describe this table.' };
}
if (mode !== 'secretAlly')
return {
reason: 'source',
message:
'Tleilaxu Secret Ally needs Tleilaxu absent from the seated roster and a non-Tleilaxu holder.',
};
if (g.status !== 'playing')
return { reason: 'timing', message: 'Nexus effects belong to the started game.' };
if (g.phase !== 4)
return { reason: 'timing', message: 'Use Tleilaxu Secret Ally during Revival.' };
if (interrupted(g))
return {
reason: 'timing',
message: 'Finish the current interaction before using Tleilaxu Secret Ally.',
};
return null;
}
export function tleilaxuSecretAllyBlock(g: Game, p: Player): string | null {
return sourceState(g, p)?.message ?? null;
}
/** An own physical disc, one of the only three leader identities this grant may return. */
function ownDisc(p: Player, id: string) {
const leader = p.leaders.find((candidate) => candidate.id === id);
return !!leader &&
discId(leader.id) &&
leader.faction === p.faction &&
leader.dead === true &&
!leader.capturedBy &&
!leader.gholaBy;
}
/**
* Canonical ordinary/Duke/Kwisatz eligibility plus the card's single waiver.
* The waiver is expressed by pretending the ordinary cycle is open at level one;
* repeated-death matching, the ordinary one-leader slot, the Auditor's own
* special projection, Duke custody and every other eligibility rule stay the
* canonical ones. Nothing here is written back: the caller must never store the
* synthetic cycle, or an early grant would open later ordinary returns.
*/
function leaderCandidates(
g: Game,
p: Player,
): { leaders: TleilaxuSecretAllyLeader[]; dukeBlocked: string | null; cycleBlock: string | null } {
const canonical = leaderRevivalOptions(g, p);
const waived = leaderRevivalOptions(g, {
...p,
revivalCycle: Math.max(1, p.revivalCycle),
});
const canonicalIds = new Set(canonical.leaders.map((option) => option.id));
const blockedEarly = (id: string) =>
!!g.revivalRules?.earlyBlocked?.includes(`${p.id}:${id}`);
const leaders: TleilaxuSecretAllyLeader[] = [];
const seen = new Set<string>();
const push = (id: string, name: string, early: boolean) => {
if (seen.has(id) || (early && blockedEarly(id))) return;
// The shared Duke is not an own disc: its control/custody was already
// resolved by the canonical Ecaz quote, so only own discs are re-checked.
if (id !== DUKE_VIDAL_ID && !ownDisc(p, id)) return;
seen.add(id);
leaders.push({
id,
name,
kind: id === DUKE_VIDAL_ID ? 'duke' : 'leader',
early,
});
};
for (const option of canonical.leaders)
push(option.id, option.name, !canonicalIds.has(option.id));
for (const option of waived.leaders)
push(option.id, option.name, !canonicalIds.has(option.id));
if (canonical.kwisatz || waived.kwisatz) {
const early = !canonical.kwisatz && !blockedEarly('kwisatz');
if (early || canonical.kwisatz)
leaders.push({ id: 'kwisatz', name: 'Kwisatz Haderach', kind: 'kwisatz', early });
}
return {
leaders,
dukeBlocked: canonical.dukeBlocked,
cycleBlock: canonical.cycleBlock,
};
}
/**
* Shared ordinary ceiling raised to at least five, minus earlier ordinary
* returns, restricted to the exact physical typed counters in Tanks and to the
* Advanced one-per-turn typed cap. Force and elite maxima are always jointly
* attainable, so a caller may use both at once.
*/
export function tleilaxuSecretAllyForceBounds(
g: Game,
p: Player,
): TleilaxuSecretAllyForceBounds {
const ceiling = Math.max(
normalForceRevivalLimit(g, p),
NEXUS_TLEILAXU_FORCE_GROUP_CAP,
);
const tanks = Math.max(0, p.tanks);
const eliteTanks = Math.max(0, p.elites?.tanks ?? 0);
const normalTanks = Math.max(0, tanks - eliteTanks);
const eliteRoom = Math.min(eliteTanks, eliteRevivalRemaining(p, g.advanced));
const maxForces = Math.min(
NEXUS_TLEILAXU_FORCE_GROUP_CAP,
Math.max(0, ceiling - p.revived),
normalTanks + eliteRoom,
);
return {
ceiling,
normalTanks,
eliteTanks,
eliteRoom,
maxForces,
maxElite: Math.min(maxForces, eliteRoom),
};
}
/** Owner-only projection input; blocked reasons name the real current limitation. */
export function tleilaxuSecretAllyOptions(
g: Game,
p: Player,
): TleilaxuSecretAllyOptions {
const event = tleilaxuSecretAllyEvent(g, p);
const blocked = tleilaxuSecretAllyBlock(g, p);
if (blocked)
return {
event,
blocked,
leaders: [],
maxForces: 0,
maxElite: 0,
policy: NEXUS_TLEILAXU_POLICY,
};
const bounds = tleilaxuSecretAllyForceBounds(g, p);
const { leaders, dukeBlocked, cycleBlock } = leaderCandidates(g, p);
return {
event,
blocked: leaders.length
? null
: dukeBlocked ?? cycleBlock ?? leaderBlock,
leaders,
maxForces: bounds.maxForces,
maxElite: bounds.maxElite,
policy: NEXUS_TLEILAXU_POLICY,
};
}
/**
* Pure quote, no mutation, no randomness, no stored offer. Throws when the
* physical card, actor, phase, quiet window, leader identity, exact typed group
* or bank spice cannot support the selection, so a committed action always has
* a legal quote. `limit` repeats the maximum legal amount for the same state.
*/
export function quoteTleilaxuSecretAlly(
g: Game,
p: Player,
selection: TleilaxuSecretAllySelection,
): TleilaxuSecretAllyQuote {
const source = sourceState(g, p);
if (source) throw new TleilaxuSecretAllyError(source.message, source.reason);
const { leaders, dukeBlocked, cycleBlock } = leaderCandidates(g, p);
const chosen = leaders.find((leader) => leader.id === selection?.leader);
if (!chosen)
throw new TleilaxuSecretAllyError(
leaders.length ? 'Choose one offered own leader.' : dukeBlocked ?? cycleBlock ?? leaderBlock,
'leader',
);
const amount = selection.amount;
const elite = selection.elite;
const bounds = tleilaxuSecretAllyForceBounds(g, p);
if (!whole(amount) || !whole(elite) || elite > amount)
throw new TleilaxuSecretAllyError(
'The card group needs a whole normal/typed counter split.',
'forces',
);
if (
amount > bounds.maxForces ||
elite > bounds.eliteRoom ||
amount - elite > bounds.normalTanks
)
throw new TleilaxuSecretAllyError(
`The card group must fit ${bounds.maxForces} physical counters (at most ${bounds.maxElite} typed) from the shared ordinary ceiling of ${bounds.ceiling}.`,
'forces',
);
const cost = amount * NEXUS_TLEILAXU_FORCE_PRICE;
if (cost > p.spice)
throw new TleilaxuSecretAllyError(
'Not enough spice for one bank spice per returned counter.',
'price',
);
return {
leader: chosen.id,
kind: chosen.kind,
amount,
elite,
cost,
limit: bounds.maxForces,
};
}
+5 -5
View File
@@ -4562,7 +4562,7 @@ export const RULE_TOPICS: RuleTopic[] = [
'Ixian Cunning is used in your battle before submitting your Battle Plan. Your Suboids then count as strength one without spice support in every battle for the rest of the turn, in Basic and Advanced. Cyborg strength and support are unchanged. Earlier inspected or promised plan elements remain binding; you cannot activate the card if the new strength would make them impossible.',
'Ixian Secret Ally has a fresh local ixian-replacement profile for normal paid or printed Karama-paid auction purchases in classic base-deck/Nexus Basic or Advanced games. A supported unallied buyer may spend its actual Nexus to discard exactly the just-purchased card and draw the real deck top privately; Pass keeps both cards. Full-hand replacement and empty-deck same-card redraw are allowed, and original payment/income/auction continuation occur once. Harkonnen buyers, cache/Black Market/special origins and combined modules remain guarded. The neutral buyer choice is an inferred privacy convention, not publisher timing. See the dedicated guide for bounded verified native, private, recovery and phone evidence; this is not full-module certification.',
'Ixian Betrayal has a separate fresh native-Ixian ixian-betrayal profile for both original Bidding inspection/draw and Advanced Technology attempts. Native counters resolve first; then every publicly possible unallied rival held-Nexus seat acknowledges uniformly before the effect. A legal Use spends one physical Ixian Nexus to deny only that attempt; all-pass preserves the native effect once. No completed-card undo or whole-phase ban is added. This bounded ordering/privacy is an inference, not universal publisher priority; see its bounded verified native/private/recovery/phone evidence and unchanged Richese/module/public/deployment gates.',
'Tleilaxu Secret Ally revival remains unavailable pending its ordinary-allowance, leader-eligibility and optional-leader rulings. The printed fixed force price does not resolve those questions.',
'With Tleilaxu absent, its actual unallied Nexus holder may use a quiet Revival control to return one mandatory own dead leader free and up to five exact physical counters for one bank spice each. The provisional policy uses the ordinary leader slot and expands the shared ordinary ceiling to at least five, counting prior returns and retaining larger native/Recruits allowance. Only the unopened roster trigger is waived: repeated deaths, actual stars/Cyborgs and special custody retain canonical rules, and ordinary cycle history is not fabricated. Zero forces is legal; force-only use is not. Spend the physical card once; native Tleilaxu income and cancellation are not imported.',
'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.',
@@ -4596,11 +4596,11 @@ export const RULE_TOPICS: RuleTopic[] = [
],
related: ['ecaz-modules', 'spice-blow', 'alliance-funding', 'choam-worthless', 'choam-kull', 'nexus-choam-kull', 'nexus-richese-betrayal', 'nexus-guild-betrayal', 'nexus-ixian-replacement', 'nexus-ixian-betrayal'],
checklist: [
{ area: 'Implementation', status: 'Partial', detail: 'Physical lifecycle, Atreides inspections, Harkonnen exchanges/declarations, Face Dancers, Suboids, advisors, native Voice, Emperor Sardaukar and Betrayal, bounded Fremen revival/Cunning, CHOAM effects, Moritani Cunning/Betrayal, Ecaz Betrayal, native Duke Cunning and private Secret Ally inquiry, Richese shipment and bounded auction Betrayal, and bounded Emperor bank-auction purchase are integrated. Ecaz Cunning transfers the existing Duke at a quiet Battle boundary with a real card and distinct turn-end expiry. Ecaz inquiry scope/audience and Emperor purchase timing are provisional. Borrowed Voice, exceptional Duke custody, other reactions, 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/nexus-ecaz-inquiry.ts', 'game/nexus-ecaz-duke.ts', 'game/duke-vidal.ts', 'game/nexus-emperor-secret-ally.ts', 'game/choam-power-options.ts', 'game/nexus-moritani-options.ts', 'game/nexus-moritani-betrayal.ts', 'game/nexus-richese-options.ts', 'game/nexus-richese-betrayal.ts', 'game/nexus-guild-cunning-options.ts', 'game/nexus-guild-secret-ally-options.ts', 'game/nexus-module-profile.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, Moritani Betrayal, Ecaz Betrayal and owner-only Secret Ally inquiry, native Duke Cunning, Richese shipment and bounded auction Betrayal, Emperor purchase and Fremen Cunning controls are connected. 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/nexus-ecaz-inquiry.tsx', 'components/nexus-ecaz-duke.tsx', 'components/nexus-richese-betrayal.tsx', 'components/game-table.tsx'] },
{ area: 'AI', status: 'Partial', detail: 'All four profiles choose legal actions through private offers, including Moritani/Ecaz Betrayal, Ecaz Secret Ally inquiry and native Duke Cunning, Fremen Cunning, Emperor purchase and bounded Richese auction Betrayal. 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-ecaz-inquiry-options.ts', 'game/nexus-ecaz-duke-options.ts', 'game/nexus-richese-options.ts', 'game/bot-nexus-richese-betrayal.ts', 'game/nexus-guild-cunning-options.ts', 'game/bots.ts'] },
{ area: 'Implementation', status: 'Partial', detail: 'Physical lifecycle and existing native/borrowed effects remain connected. Tleilaxu Secret Ally now returns one free mandatory own leader with up to five exact paid counters, sharing ordinary force allowance and the leader slot under an explicit provisional policy. Original cycle/custody, typed caps, bank payment, skill return and Homeworld force source remain connected. Borrowed Voice, exceptional custody, worm-protection responses and broader combinations remain open.', 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/nexus-ecaz-inquiry.ts', 'game/nexus-ecaz-duke.ts', 'game/duke-vidal.ts', 'game/nexus-emperor-secret-ally.ts', 'game/choam-power-options.ts', 'game/nexus-moritani-options.ts', 'game/nexus-moritani-betrayal.ts', 'game/nexus-richese-options.ts', 'game/nexus-richese-betrayal.ts', 'game/nexus-guild-cunning-options.ts', 'game/nexus-guild-secret-ally-options.ts', 'game/nexus-module-profile.ts', 'game/engine.ts', 'game/nexus-tleilaxu-revival.ts'] },
{ area: 'Player controls', status: 'Partial', detail: 'Existing private lifecycle and effect controls remain. Tleilaxu Secret Ally adds owner-only free-leader and exact paid-force/elite selection, authoritative price, zero-force use and a visible provisional-policy explanation. The spent card removes the offer and original Revival Ready remains usable. Remaining borrowed/reactive effects and complete modes are open.', 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/nexus-ecaz-inquiry.tsx', 'components/nexus-ecaz-duke.tsx', 'components/nexus-richese-betrayal.tsx', 'components/game-table.tsx', 'components/nexus-tleilaxu-revival.tsx'] },
{ area: 'AI', status: 'Partial', detail: 'All four legal profiles retain existing private effect policies and now execute actual Tleilaxu Secret Ally own-leader/five-paid-counter returns through the shared offered selection. Native Cunning priority/private boundaries remain. Complete module strategy and calibration are deferred and 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-ecaz-inquiry-options.ts', 'game/nexus-ecaz-duke-options.ts', 'game/nexus-richese-options.ts', 'game/bot-nexus-richese-betrayal.ts', 'game/nexus-guild-cunning-options.ts', 'game/bots.ts'] },
{ area: 'Documentation', status: 'Partial', detail: 'Common rules, twelve printed faces and bounded runtime contracts retain explicit unresolved Ecaz Duke capture/Tanks/Ghola and inquiry audience/scope, Fremen nested Cunning, Great Maker, combined modules, fresh advisors, storm and borrowed Voice. Richese auction Betrayal has a separate cost/prepayment/privacy contract and excluded-producer boundaries. Guild Betrayal separately records full funded payment, own pre-funding, donor escrow once, the printed full-Junction override, four native adapters and neutral privacy; ordinary occupation/rounding and general response-priority questions remain unresolved.', 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_RICHESE_RULES.md', 'docs/NEXUS_GUILD_CUNNING_RUNTIME.md', 'docs/NEXUS_GUILD_RULES.md'] },
{ area: 'Verification', status: 'Partial', detail: 'Mixed E1/E2 and paired E3 adds31 physical programmes plus2 Loyalty-stock regressions, final489/489 affected checks across42 files and types/lint. All64 original six-seat families finish258/288 games with338931/339426 actions and9013 JSON across three frozen trees and preserved-prefix continuations. Native Ecaz assassination, separate physical Loyalty stock and exact paired overlap admission are repaired; all30 remaining traces retain printed source guards. Actual390px original human Ixian offer/training, typed shipment/free Suboid plan and paired Ecaz foreign arrival/living shared Duke preserve20 counters. Earlier evidence remains canonical; not complete modes, new assurance or protected deployed acceptance.', evidence: ['tests/mixed-nexus-modules-runtime.test.ts', 'tests/paired-e3-nexus-modules-runtime.test.ts', 'tests/sample-custody.test.ts', 'tests/ambassador-terror-overlap-engine.test.ts', 'tests/single-nexus-e1-runtime.test.ts', 'tests/single-nexus-e2-runtime.test.ts', 'tests/e3-nexus-ecaz-runtime.test.ts', 'tests/e3-nexus-moritani-runtime.test.ts', 'tests/homeworld-classic-nexus-skills-runtime.test.ts', 'tests/homeworld-classic-discovery-nexus-runtime.test.ts', 'tests/nexus-cards.test.ts', 'tests/nexus-card-engine.test.ts', 'tests/nexus-fremen-cunning.test.ts', 'tests/nexus-fremen-cunning-recovery.test.ts', 'tests/nexus-fremen-cunning-summon.test.ts', 'tests/nexus-fremen-cunning-summon-recovery.test.ts', 'tests/nexus-ecaz-betrayal-quotes.test.ts', 'tests/nexus-ecaz-betrayal.test.ts', 'tests/nexus-ecaz-betrayal-recovery.test.ts', 'tests/nexus-ecaz-inquiry.test.ts', 'tests/nexus-ecaz-duke-quote.test.ts', 'tests/nexus-ecaz-duke-engine.test.ts', 'tests/nexus-ecaz-duke-controls.test.ts', 'tests/nexus-ecaz-duke-recovery.test.ts', 'tests/nexus-traitor-recovery.test.ts', 'tests/nexus-face-dancer-engine.test.ts', 'tests/nexus-suboids-engine.test.ts', 'tests/normal-free-support.test.ts', 'tests/nexus-suboid-controls.test.ts', 'tests/nexus-advisor-controls.test.ts', 'tests/nexus-advisor-bots.test.ts', 'tests/nexus-sardaukar-controls.test.ts', 'tests/nexus-sardaukar-bots.test.ts', 'tests/nexus-choam-controls.test.ts', 'tests/nexus-choam-bots.test.ts', 'tests/nexus-moritani-controls.test.ts', 'tests/nexus-moritani-bots.test.ts', 'tests/nexus-moritani-betrayal.test.ts', 'tests/nexus-richese-controls.test.ts', 'tests/nexus-richese-bots.test.ts', 'tests/nexus-richese-betrayal-engine.test.ts', 'tests/nexus-richese-betrayal-recovery.test.ts', 'tests/nexus-guild-cunning-controls.test.ts', 'tests/nexus-guild-cunning-bots.test.ts', 'tests/nexus-module-payments-runtime.test.ts', 'tests/nexus-module-battles-runtime.test.ts', 'tests/paired-ix-nexus-modules-runtime.test.ts', 'tests/paired-choam-nexus-modules-runtime.test.ts', 'tests/classic-nexus-skills-payments-runtime.test.ts', 'tests/classic-nexus-skills-battles-runtime.test.ts', 'tests/nexus-skills-modules-payments-runtime.test.ts', 'tests/nexus-skills-modules-battles-runtime.test.ts', 'tests/paired-ix-nexus-skills-modules-runtime.test.ts', 'tests/paired-choam-nexus-skills-modules-runtime.test.ts', 'tests/ecaz-nexus-skills-modules-runtime.test.ts', 'tests/moritani-nexus-skills-modules-runtime.test.ts', 'docs/NEXUS_CARD_RULES.md'] },
{ area: 'Verification', status: 'Partial', detail: 'Current Tleilaxu revival batch passes141/141 in9 related files, types/lint and build. Genuine Basic/Advanced weapon death/eight-counter exchange, worm alliance and actual Nexus draw feed leader/paid/zero returns, both shared-allowance orders, stale/invalid rejection and the next real turn. Actual390px Dr. Yueh/five return changes spice11 to6, Tanks8 to3 and reserves10 to15 with original cycle0/deaths1, one card discard and Ready restored; no overflow/errors. Four policies execute the actual return. Earlier mixed-family evidence stays source-bound; complete modes, assurance and deployed acceptance remain open.', evidence: ['tests/mixed-nexus-modules-runtime.test.ts', 'tests/paired-e3-nexus-modules-runtime.test.ts', 'tests/sample-custody.test.ts', 'tests/ambassador-terror-overlap-engine.test.ts', 'tests/single-nexus-e1-runtime.test.ts', 'tests/single-nexus-e2-runtime.test.ts', 'tests/e3-nexus-ecaz-runtime.test.ts', 'tests/e3-nexus-moritani-runtime.test.ts', 'tests/homeworld-classic-nexus-skills-runtime.test.ts', 'tests/homeworld-classic-discovery-nexus-runtime.test.ts', 'tests/nexus-cards.test.ts', 'tests/nexus-card-engine.test.ts', 'tests/nexus-fremen-cunning.test.ts', 'tests/nexus-fremen-cunning-recovery.test.ts', 'tests/nexus-fremen-cunning-summon.test.ts', 'tests/nexus-fremen-cunning-summon-recovery.test.ts', 'tests/nexus-ecaz-betrayal-quotes.test.ts', 'tests/nexus-ecaz-betrayal.test.ts', 'tests/nexus-ecaz-betrayal-recovery.test.ts', 'tests/nexus-ecaz-inquiry.test.ts', 'tests/nexus-ecaz-duke-quote.test.ts', 'tests/nexus-ecaz-duke-engine.test.ts', 'tests/nexus-ecaz-duke-controls.test.ts', 'tests/nexus-ecaz-duke-recovery.test.ts', 'tests/nexus-traitor-recovery.test.ts', 'tests/nexus-face-dancer-engine.test.ts', 'tests/nexus-suboids-engine.test.ts', 'tests/normal-free-support.test.ts', 'tests/nexus-suboid-controls.test.ts', 'tests/nexus-advisor-controls.test.ts', 'tests/nexus-advisor-bots.test.ts', 'tests/nexus-sardaukar-controls.test.ts', 'tests/nexus-sardaukar-bots.test.ts', 'tests/nexus-choam-controls.test.ts', 'tests/nexus-choam-bots.test.ts', 'tests/nexus-moritani-controls.test.ts', 'tests/nexus-moritani-bots.test.ts', 'tests/nexus-moritani-betrayal.test.ts', 'tests/nexus-richese-controls.test.ts', 'tests/nexus-richese-bots.test.ts', 'tests/nexus-richese-betrayal-engine.test.ts', 'tests/nexus-richese-betrayal-recovery.test.ts', 'tests/nexus-guild-cunning-controls.test.ts', 'tests/nexus-guild-cunning-bots.test.ts', 'tests/nexus-module-payments-runtime.test.ts', 'tests/nexus-module-battles-runtime.test.ts', 'tests/paired-ix-nexus-modules-runtime.test.ts', 'tests/paired-choam-nexus-modules-runtime.test.ts', 'tests/classic-nexus-skills-payments-runtime.test.ts', 'tests/classic-nexus-skills-battles-runtime.test.ts', 'tests/nexus-skills-modules-payments-runtime.test.ts', 'tests/nexus-skills-modules-battles-runtime.test.ts', 'tests/paired-ix-nexus-skills-modules-runtime.test.ts', 'tests/paired-choam-nexus-skills-modules-runtime.test.ts', 'tests/ecaz-nexus-skills-modules-runtime.test.ts', 'tests/moritani-nexus-skills-modules-runtime.test.ts', 'docs/NEXUS_CARD_RULES.md', 'tests/nexus-tleilaxu-revival.test.ts', 'tests/fixture-nexus-tleilaxu-secret-ally.ts', 'tests/nexus-tleilaxu-secret-ally-runtime.test.ts'] },
],
},
{
+3
View File
@@ -17,6 +17,8 @@ type RevivalRateContext = Pick<
>;
export type RevivalRules = {
expanded: string[];
/** Provisional shared five-return permission from the actual absent-faction Nexus grant. */
nexusTleilaxu?: string[];
choamBlocked?: boolean;
freeBlocked?: string[];
limitBlocked: boolean;
@@ -38,6 +40,7 @@ export function forceRevivalLimit(
p: Pick<Player, 'id' | 'faction'>,
) {
if (p.faction === 'choam' && !g.revivalRules?.choamBlocked) return 20;
if (g.revivalRules?.nexusTleilaxu?.includes(p.id)) return 5;
if (g.revivalRules?.limitBlocked) return 3;
if (p.faction === 'tleilaxu') return 20;
return g.revivalRules?.expanded.includes(p.id) ? 5 : 3;