Implement Tleilaxu and Ixian Nexus Cunning with durable private recovery

This commit is contained in:
Quintine committed 2026-09-10 10:49:18 +10:00
1 parent d5830d260c
commit 40912498b1
32 files changed
+2761 -24

No files matched your search

+7 -5
View File
@@ -179,11 +179,13 @@ export function BattlePromises({
{b.compliantPreparation.actions.map((action, index) => (
<p key={index}>
{index + 1}.{' '}
{action.mode === 'special'
? `Spend Karama and discard ${(action.cards as string[]).map(name).join(', ')} to gain ${(action.cards as string[]).length * 3} spice`
: typeof action.leader === 'string'
? `Play Ghola to revive ${action.leader === 'kwisatz' ? 'Kwisatz Haderach' : name(action.leader)}`
: `Play Ghola to revive ${Number(action.amount)} force${Number(action.amount) === 1 ? '' : 's'}${Number(action.elite) ? `, including ${Number(action.elite)} elite` : ''}`}
{action.type === 'nexusSuboids'
? 'Use Ixian Cunning to give Suboids full strength without spice support'
: action.mode === 'special'
? `Spend Karama and discard ${(action.cards as string[]).map(name).join(', ')} to gain ${(action.cards as string[]).length * 3} spice`
: typeof action.leader === 'string'
? `Play Ghola to revive ${action.leader === 'kwisatz' ? 'Kwisatz Haderach' : name(action.leader)}`
: `Play Ghola to revive ${Number(action.amount)} force${Number(action.amount) === 1 ? '' : 's'}${Number(action.elite) ? `, including ${Number(action.elite)} elite` : ''}`}
.
</p>
))}
+10 -1
View File
@@ -11,6 +11,8 @@ import { GrummanCollection } from './grumman-collection';
import { TupileIntelligence } from './tupile-intelligence';
import { NexusCards } from './nexus-cards';
import { NexusTraitors } from './nexus-traitors';
import { NexusTleilaxu } from './nexus-tleilaxu';
import { NexusSuboids } from './nexus-suboids';
import { TerrorBoardMarkers } from './terror-board-markers';
import { homeworldRevivalActionBlock } from '@/game/homeworld-revival-deployment-options';
import { GuildHomeworldShipment } from './guild-homeworld-shipment';
@@ -346,7 +348,7 @@ export function GameTable({
ownBattleForces
? ownBattleForces.freeSupport
? 0
: (ownBattleForces.normalFixedHalf ? 0 : ownBattleForces.normal) +
: (ownBattleForces.normalFixedHalf || ownBattleForces.normalFreeSupport ? 0 : ownBattleForces.normal) +
(ownBattleForces.eliteFreeSupport ? 0 : ownBattleForces.elite)
: Number.POSITIVE_INFINITY,
);
@@ -1416,6 +1418,8 @@ export function GameTable({
<PrivateBattlePlan game={g} />
<NexusCards game={g} act={act} busy={busy} />
<NexusTraitors game={g} act={act} busy={transportBusy || !!me.autopilot} />
<NexusTleilaxu game={g} act={act} busy={transportBusy || !!me.autopilot} />
<NexusSuboids game={g} act={act} busy={transportBusy || !!me.autopilot} />
{!g.nexusCards?.waiting.length && <TupileIntelligence game={g} act={act} busy={busy} />}
{g.biddingEnd && <BiddingEnd game={g} act={act} busy={busy} />}
{g.junctionTransport && [g.junctionTransport.owner, g.junctionTransport.recipient].includes(me.id) && (
@@ -4147,6 +4151,11 @@ export function GameTable({
? `Your concealed No-Field can reveal ${battleForces} physical forces from your current reserves. This estimate is private.`
: `You have ${battleForces} fighting forces here.`}
</p>
{ownBattleForces?.normalFreeSupport && (
<p className="notice">
Your Suboids fight at full strength without spice support this turn. Spice support here applies only to Cyborgs.
</p>
)}
{ownBattleForces?.eliteFreeSupport && (
<p className="notice">
Salusa Secundus is at high population. Your
+49
View File
@@ -0,0 +1,49 @@
'use client';
import type { Action, GameView } from '@/game/engine';
import { nexusSuboidsAction } from '@/game/nexus-suboid-options';
import { Button } from './ui/button';
type Props = { game: GameView; act: (action: Action) => void; busy: boolean };
export function NexusSuboids({ game, act, busy }: Props) {
const state = game.nexusSuboids;
if (!state?.offer && !state?.active) return null;
if (state.active)
return (
<section aria-label="Ixian Nexus active">
<p className="notice">
Ixian Suboids fight at full strength without spice support for the
rest of this turn. Cyborgs keep their usual strength and support
rules.
</p>
</section>
);
const offer = state.offer!;
const action = nexusSuboidsAction(game, offer.event);
return (
<section
aria-label="Ixian Nexus Cunning"
className="space-y-3 rounded-lg border border-[#a88b60]/50 p-3"
>
<h3>Ixian Nexus · Cunning</h3>
<p>
For the rest of this turn, your Suboids fight at full strength without
spice support. Cyborgs keep their usual strength and support rules.
</p>
<p className="fine">
Use this before submitting your battle plan. This spends your Ixian
Nexus card.
</p>
{offer.blocked && <p className="notice">{offer.blocked}</p>}
<Button
className="game-action min-h-11 whitespace-normal"
disabled={busy || !action}
onClick={() => {
if (!busy && action) act(action);
}}
>
Strengthen Suboids for this turn
</Button>
</section>
);
}
+60
View File
@@ -0,0 +1,60 @@
'use client';
import type { Action, GameView } from '@/game/engine';
import { nexusFaceDancersAction } from '@/game/nexus-tleilaxu-options';
import { nexusCardMode } from '@/game/nexus-cards';
import { Button } from './ui/button';
type Props = { game: GameView; act: (action: Action) => void; busy: boolean };
export function NexusTleilaxu({ game, act, busy }: Props) {
const offer = game.nexusTleilaxu?.cunning;
if (!offer) {
const owner = game.players.find((player) => player.id === game.me);
if (!owner || game.nexusCards?.card !== 'tleilaxu') return null;
const mode = nexusCardMode(
'tleilaxu',
owner.faction,
game.players.map((player) => player.faction),
);
if (mode === 'cunning') return null;
return (
<section aria-label="Tleilaxu Nexus availability">
<p className="notice">
{mode === 'secretAlly'
? 'Secret Ally revival is unavailable while its revival-limit ruling is pending.'
: 'Betrayal is unavailable while its private response timing is pending.'}
</p>
</section>
);
}
const action = nexusFaceDancersAction(game, offer.event);
return (
<section
aria-label="Tleilaxu Nexus Cunning"
className="space-y-3 rounded-lg border border-[#a88b60]/50 p-3"
>
<h3>Tleilaxu Nexus · Cunning</h3>
<p>
Set aside all your revealed Face Dancers and secretly draw their
replacements. Then shuffle the set-aside cards into the Traitor Deck.
Your unrevealed Face Dancers stay in your hand.
</p>
<p className="fine">
This uses your Tleilaxu Nexus card. All {offer.count} revealed{' '}
{offer.count === 1 ? 'card is' : 'cards are'} replaced together.
</p>
{offer.blocked && <p className="notice">{offer.blocked}</p>}
<Button
className="game-action min-h-11 whitespace-normal"
disabled={busy || !action}
onClick={() => {
if (!busy && action) act(action);
}}
>
Replace {offer.count} revealed Face{' '}
{offer.count === 1 ? 'Dancer' : 'Dancers'}
</Button>
</section>
);
}