Implement Emperor Nexus Cunning with physical casualty recovery
This commit is contained in:
1 parent
b05125de9a
commit
ba0b5647e1
27 files changed
+2487
-64
No files matched your search
@@ -181,11 +181,13 @@ export function BattlePromises({
|
||||
{index + 1}.{' '}
|
||||
{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` : ''}`}
|
||||
: action.type === 'nexusSardaukar'
|
||||
? 'Declare Emperor Cunning to count five ordinary forces as Sardaukar for this battle'
|
||||
: 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>
|
||||
))}
|
||||
|
||||
@@ -14,6 +14,7 @@ import { NexusTraitors } from './nexus-traitors';
|
||||
import { NexusTleilaxu } from './nexus-tleilaxu';
|
||||
import { NexusSuboids } from './nexus-suboids';
|
||||
import { NexusAdvisors } from './nexus-advisors';
|
||||
import { NexusSardaukar } from './nexus-sardaukar';
|
||||
import { TerrorBoardMarkers } from './terror-board-markers';
|
||||
import { homeworldRevivalActionBlock } from '@/game/homeworld-revival-deployment-options';
|
||||
import { GuildHomeworldShipment } from './guild-homeworld-shipment';
|
||||
@@ -27,7 +28,7 @@ import {
|
||||
} from './homeworld-table';
|
||||
import { nativeShipmentSources } from '@/game/homeworld-options';
|
||||
import { botBattleChoices } from '@/game/bot-battle-choices';
|
||||
import { specialForceName } from '@/game/combat';
|
||||
import { specialForceName, maxCombatDial, maxCombatSupport } from '@/game/combat';
|
||||
import type { NativeReserveSelections } from '@/game/homeworld-native-reserves';
|
||||
import {
|
||||
StrongholdCardGallery,
|
||||
@@ -325,8 +326,7 @@ export function GameTable({
|
||||
? fighterCount(me, g.battle.territory)
|
||||
: 0;
|
||||
const battleDialMaximum = ownBattleForces
|
||||
? ownBattleForces.normal * (ownBattleForces.normalFixedHalf ? 0.5 : 1) +
|
||||
ownBattleForces.elite * ownBattleForces.eliteStrength
|
||||
? maxCombatDial(ownBattleForces)
|
||||
: g.advanced || me.faction === 'ixians'
|
||||
? 40
|
||||
: battleForces;
|
||||
@@ -347,10 +347,7 @@ export function GameTable({
|
||||
const battleSupportMaximum = Math.min(
|
||||
(me.spice ?? 0) + g.aid.available + strongholdBankSupport,
|
||||
ownBattleForces
|
||||
? ownBattleForces.freeSupport
|
||||
? 0
|
||||
: (ownBattleForces.normalFixedHalf || ownBattleForces.normalFreeSupport ? 0 : ownBattleForces.normal) +
|
||||
(ownBattleForces.eliteFreeSupport ? 0 : ownBattleForces.elite)
|
||||
? maxCombatSupport(ownBattleForces)
|
||||
: Number.POSITIVE_INFINITY,
|
||||
);
|
||||
const battleSupport = Number.isFinite(support)
|
||||
@@ -1419,6 +1416,7 @@ export function GameTable({
|
||||
<NexusTleilaxu game={g} act={act} busy={transportBusy || !!me.autopilot} />
|
||||
<NexusSuboids game={g} act={act} busy={transportBusy || !!me.autopilot} />
|
||||
<NexusAdvisors game={g} act={act} busy={busy} />
|
||||
<NexusSardaukar game={g} act={act} busy={busy} />
|
||||
{!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) && (
|
||||
@@ -1805,6 +1803,7 @@ export function GameTable({
|
||||
worthlessKarama: 'Worthless card as Karama',
|
||||
advisorFlip: 'Bene Gesserit token flip',
|
||||
nexusAdvisorFlip: 'Bene Gesserit Nexus advisor conversion',
|
||||
nexusSardaukar: 'Emperor Nexus temporary Sardaukar',
|
||||
eliteStrength: 'Elite battle strength',
|
||||
fremenSupport: 'Fremen full strength without spice',
|
||||
capture: 'Harkonnen leader capture',
|
||||
@@ -1978,6 +1977,7 @@ export function GameTable({
|
||||
{g.response.intent &&
|
||||
g.response.source !== 'ambassador' &&
|
||||
g.response.kind !== 'nexusAdvisorFlip' &&
|
||||
g.response.kind !== 'nexusSardaukar' &&
|
||||
g.response.kind !== 'faceDancerReplacement' && (
|
||||
<p className="notice">{g.response.intent}</p>
|
||||
)}
|
||||
@@ -4152,6 +4152,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?.temporaryElite && (
|
||||
<p className="notice">
|
||||
{ownBattleForces.temporaryElite} ordinary counters count as Sardaukar for this battle. Their losses are ordinary counters, not starred Sardaukar.
|
||||
</p>
|
||||
)}
|
||||
{ownBattleForces?.normalFreeSupport && (
|
||||
<p className="notice">
|
||||
Your Suboids fight at full strength without spice support this turn. Spice support here applies only to Cyborgs.
|
||||
|
||||
@@ -118,7 +118,7 @@ export function NexusCards({ game, act, busy }: { game: GameView; act: (action:
|
||||
<div>
|
||||
<h3>Nexus cards</h3>
|
||||
<p className="fine">{offer.deckCount} in deck · {offer.discardCount} discarded · {Object.values(offer.held).filter((held) => held === true).length} held</p>
|
||||
<p>Playable effects include Atreides inspections, Harkonnen exchanges, and Tleilaxu, Ixian and Bene Gesserit Cunning. Use the controls shown at each effect’s timing; other effects remain unavailable.</p>
|
||||
<p>Playable effects include Atreides inspections, Harkonnen exchanges, and Tleilaxu, Ixian, Bene Gesserit and Emperor Cunning. Use the controls shown at each effect’s timing; other effects remain unavailable.</p>
|
||||
</div>
|
||||
{game.nexusAtreides && offer.card === 'atreides' && (
|
||||
<div aria-label="Atreides Nexus effect" className="space-y-2">
|
||||
|
||||
@@ -0,0 +1,63 @@
|
||||
'use client';
|
||||
|
||||
import type { Action, GameView } from '@/game/engine';
|
||||
import { nexusSardaukarAction } from '@/game/nexus-sardaukar-options';
|
||||
import { Button } from './ui/button';
|
||||
|
||||
type Props = { game: GameView; act: (action: Action) => void; busy: boolean };
|
||||
export function NexusSardaukar({ game, act, busy }: Props) {
|
||||
const state = game.nexusSardaukar;
|
||||
if (!state) return null;
|
||||
if (state.pending)
|
||||
return (
|
||||
<section aria-label="Emperor Nexus response" className="notice">
|
||||
<p>
|
||||
{game.players.find((p) => p.id === state.pending!.owner)?.name ??
|
||||
'Emperor'}{' '}
|
||||
declared Cunning. Karama may cancel the entire enhancement before the
|
||||
five counters count as Sardaukar.
|
||||
</p>
|
||||
</section>
|
||||
);
|
||||
if (state.active)
|
||||
return (
|
||||
<section aria-label="Emperor Nexus active" className="notice">
|
||||
<p>
|
||||
Five ordinary Emperor counters count as Sardaukar for this battle.
|
||||
They remain ordinary physical counters; any losses go to the ordinary
|
||||
Tanks.
|
||||
</p>
|
||||
</section>
|
||||
);
|
||||
const offer = state.offer;
|
||||
if (!offer) return null;
|
||||
const action = nexusSardaukarAction(game, offer.event);
|
||||
return (
|
||||
<section
|
||||
aria-label="Emperor Nexus Cunning"
|
||||
className="space-y-3 rounded-lg border border-[#a88b60]/50 p-3"
|
||||
>
|
||||
<h3>Emperor Nexus · Cunning</h3>
|
||||
<p>
|
||||
Count five of your ordinary forces as Sardaukar for this battle. Use
|
||||
this before submitting your plan, with no actual Sardaukar in the
|
||||
battle.
|
||||
</p>
|
||||
<p className="fine">
|
||||
This spends your Nexus card once. Karama may cancel the enhancement. The
|
||||
five counters keep their ordinary physical identity, including when
|
||||
lost.
|
||||
</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);
|
||||
}}
|
||||
>
|
||||
Use five forces as Sardaukar
|
||||
</Button>
|
||||
</section>
|
||||
);
|
||||
}
|
||||
Reference in new issue
Block a user