Implement Richese Nexus Secret Ally shipment and recovery

This commit is contained in:
Quintine committed 2026-09-10 13:03:53 +10:00
1 parent ae66668e2e
commit 6ba1050a92
21 files changed
+2286 -103

No files matched your search

+36 -14
View File
@@ -109,6 +109,7 @@ import { canUseAsKarama } from '@/game/karama';
import { LeaderPortrait } from './leader-portrait';
import { ShipmentQuote } from './shipment-quote';
import { reserveShipmentCost } from '@/game/shipment-price';
import { nexusRicheseAction, nexusRicheseQuote } from '@/game/nexus-richese-options';
import { guildTransportQuote } from '@/game/transport-quote';
import { fremenReserveEntry, botGroundMoveAllowed } from '@/game/bot-mobility';
import { location as boardLocation } from '@/game/board';
@@ -270,6 +271,8 @@ export function GameTable({
: 'arrakeen';
const [sector, setSector] = useState(10);
const [amount, setAmount] = useState(1);
const [richeseShipmentEvent, setRicheseShipmentEvent] = useState('');
const useRicheseShipment = !!g.nexusRichese && richeseShipmentEvent === g.nexusRichese.event;
const [bidDraft, setBidDraft] = useState({ auction: '', value: 1 });
const auctionKey = `${g.code}/${g.turn}/${g.auction?.remaining ?? 0}`;
const minimumBid = (g.auction?.bid ?? 0) + 1;
@@ -406,6 +409,8 @@ export function GameTable({
const payment =
!me.ally || allyPayment === '' ? {} : { allyPayment: Number(allyPayment) };
const shipmentProblems: string[] = [];
if (useRicheseShipment && g.nexusRichese?.blocked) shipmentProblems.push(g.nexusRichese.blocked);
const shipmentMaximum = useRicheseShipment ? Math.min(me.reserves, 5, g.nexusRichese!.maxForces) : me.reserves;
if (homeworldSources) {
const homes = g.homeworlds!.worlds!.filter((w) => w.native === me.id);
if (
@@ -440,10 +445,10 @@ export function GameTable({
`${territory(selected).name} is beyond the Fremen reinforcement area. Choose the Great Flat or a territory within two territories of it.`,
);
const validShipmentAmount =
Number.isInteger(amount) && amount > 0 && amount <= me.reserves;
Number.isInteger(amount) && amount > 0 && amount <= shipmentMaximum;
if (!validShipmentAmount)
shipmentProblems.push(
`Choose a whole number from 1 to ${me.reserves} available reserves.`,
`Choose a whole number from 1 to ${shipmentMaximum} available reserves.`,
);
if (me.elites && validShipmentAmount) {
const minimum = Math.max(0, amount - (me.reserves - me.elites.reserves));
@@ -465,8 +470,12 @@ export function GameTable({
g.karamaShipping?.player === me.id,
};
const shipmentCost = validShipmentAmount
? reserveShipmentCost(shipmentRate, territory(selected).type, amount)
? useRicheseShipment
? (nexusRicheseQuote(g, selected, amount)?.cost ?? null)
: reserveShipmentCost(shipmentRate, territory(selected).type, amount)
: null;
if (useRicheseShipment && shipmentCost === null && !g.nexusRichese?.blocked)
shipmentProblems.push('Choose a current Richese Secret Ally shipment of up to five physical forces.');
const selectedAllyPayment =
shipmentCost === null
? 0
@@ -719,7 +728,7 @@ export function GameTable({
id="forces"
type="number"
min={includesNoField ? 0 : 1}
max={20}
max={useRicheseShipment && !me.shipped ? shipmentMaximum : 20}
value={amount}
onChange={(e) => setAmount(Number(e.target.value))}
/>
@@ -3664,6 +3673,19 @@ export function GameTable({
/>
</details>
))}
{!me.shipped && g.nexusRichese && (
<div className="notice">
<label className="decision-checkbox min-h-11">
<input type="checkbox" checked={useRicheseShipment}
disabled={busy || !!g.nexusRichese.blocked}
onChange={(event) => setRicheseShipmentEvent(event.target.checked ? g.nexusRichese!.event : '')} />
Use Richese Nexus Secret Ally
</label>
<p>Spend the Nexus to ship up to five physical forces from reserves at the price of one. This uses your ordinary shipment; movement remains available.</p>
{g.nexusRichese.blocked && <p>{g.nexusRichese.blocked}</p>}
{useRicheseShipment && <p>Physical forces selected: {amount}. Forces charged: 1. Ordinary destination and faction restrictions still apply.</p>}
</div>
)}
{destination}
{amountInput}
{g.karamaShipping?.player === me.id && (
@@ -3689,6 +3711,7 @@ export function GameTable({
/>
<ShipmentQuote
id="reserve-shipment-quote"
physicalForces={useRicheseShipment ? amount : undefined}
quote={shipmentQuote}
funding={{
ownSpice: me.spice ?? 0,
@@ -3704,17 +3727,16 @@ export function GameTable({
!!g.truthtrance ||
shipmentProblems.length > 0
}
onClick={() =>
act({
type: 'ship',
elite: eliteAmount,
...payment,
territory: selected,
sector,
amount,
onClick={() => {
const shipment: Action = {
type: 'ship', elite: eliteAmount, ...payment,
territory: selected, sector, amount,
...(homeworldSources ? { homeworldSources } : {}),
})
}
};
const action = useRicheseShipment
? nexusRicheseAction(g, richeseShipmentEvent, shipment) : shipment;
if (action) act(action);
}}
>
Ship from reserves <ArrowRight size={15} />
</Button>
+1 -1
View File
@@ -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, Bene Gesserit, Emperor, CHOAM and Moritani Cunning. Use the controls shown at each effect’s timing. CHOAM’s Kull Wahad effect, Moritani Cunning relocation and Grumman combinations, and other unfinished effects remain unavailable.</p>
<p>Playable effects include Atreides inspections, Harkonnen exchanges, and Tleilaxu, Ixian, Bene Gesserit, Emperor, CHOAM and Moritani Cunning, plus Richese Secret Ally reserve shipping. Use the controls shown at each effect’s timing. CHOAM’s Kull Wahad effect, Moritani Cunning relocation and Grumman combinations, and other unfinished effects remain unavailable.</p>
</div>
{game.nexusAtreides && offer.card === 'atreides' && (
<div aria-label="Atreides Nexus effect" className="space-y-2">