Support staged group Emperor aid
Verify and publish checkpoint container / container (push) Successful in 10m13s

This commit is contained in:
Quintine committed 2026-10-28 23:33:37 +11:00
1 parent b89c225d5b
commit 9081249574
10 files changed
+413 -17

No files matched your search

+103
View File
@@ -0,0 +1,103 @@
'use client';
import { useId, useState } from 'react';
import type { Player } from '@/game/engine';
import { eliteRevivalRemaining, paidForceRevivalCost } from '@/game/revival';
/** Public alliance members only; the engine remains the legality authority. */
export type CoalitionEmperorAlly = Pick<Player, 'id' | 'name' | 'faction' | 'tanks' | 'elites'>;
export type CoalitionEmperorAidAction =
| { type: 'emperorGift'; target: string; amount: number }
| { type: 'emperorRevival'; target: string; amount: number; elite: number };
function positiveInteger(raw: string): number | null {
if (!/^[1-9]\d*$/.test(raw)) return null;
const value = Number(raw);
return Number.isSafeInteger(value) ? value : null;
}
/** Render for an Emperor in a staged group alliance; pair controls stay separate. */
export function CoalitionEmperorAid({ allies, phase, advanced, availableSpice,
emperorExtra, revivalOrderHead, busy = false, act }: {
allies: readonly CoalitionEmperorAlly[];
phase: number;
advanced: boolean;
/** Spice not committed to coalition pledges or other obligations. */
availableSpice: number;
emperorExtra: Readonly<Record<string, number>>;
revivalOrderHead?: string | null;
busy?: boolean;
act: (action: CoalitionEmperorAidAction) => void;
}) {
const id = useId();
const [giftTarget, setGiftTarget] = useState('');
const [giftRaw, setGiftRaw] = useState('1');
const [revivalTarget, setRevivalTarget] = useState('');
const [revivalRaw, setRevivalRaw] = useState('1');
const [eliteRaw, setEliteRaw] = useState('0');
const giftAlly = allies.find(ally => ally.id === giftTarget);
const revivalAlly = allies.find(ally => ally.id === revivalTarget);
const gift = positiveInteger(giftRaw);
const amount = positiveInteger(revivalRaw);
const elite = /^(0|[1-9]\d*)$/.test(eliteRaw) && Number.isSafeInteger(Number(eliteRaw))
? Number(eliteRaw) : null;
const maximum = revivalAlly ? Math.max(0, Math.min(
3 - (emperorExtra[revivalAlly.id] ?? 0), revivalAlly.tanks,
Math.floor(availableSpice / 2),
)) : 0;
const cost = revivalAlly && amount && elite !== null
? paidForceRevivalCost(revivalAlly, amount, elite) : null;
const eliteTanks = revivalAlly?.elites?.tanks ?? 0;
const legalElite = revivalAlly && amount && elite !== null &&
elite <= Math.min(amount, eliteRevivalRemaining(revivalAlly, advanced)) &&
amount - elite <= revivalAlly.tanks - eliteTanks;
const canGift = !!giftAlly && gift !== null && gift <= availableSpice && !busy;
const canRevive = phase === 4 && !!revivalAlly && amount !== null && amount <= maximum &&
legalElite && cost !== null && cost <= availableSpice &&
(!revivalOrderHead || revivalOrderHead === revivalAlly.id) && !busy;
return <section className="notice min-w-0 space-y-3 break-words" aria-label="Emperor group alliance aid">
<h3>Emperor alliance aid</h3>
<p className="fine">Choose which ally receives your aid. Spice and revivals take effect after the Karama response.</p>
<label htmlFor={`${id}-gift-target`}>Ally to receive spice</label>
<select id={`${id}-gift-target`} value={giftAlly?.id ?? ''} disabled={busy}
onChange={event => setGiftTarget(event.target.value)}>
<option value="">Choose an ally</option>
{allies.map(ally => <option key={ally.id} value={ally.id}>{ally.name}</option>)}
</select>
<label htmlFor={`${id}-gift-amount`}>Spice to share</label>
<input id={`${id}-gift-amount`} type="number" inputMode="numeric" min={1} max={availableSpice}
step={1} value={giftRaw} disabled={busy} onChange={event => setGiftRaw(event.target.value)} />
<button type="button" className="game-action" disabled={!canGift}
onClick={() => { if (giftAlly && gift !== null && canGift) act({ type: 'emperorGift', target: giftAlly.id, amount: gift }); }}>
Share spice
</button>
{phase === 4 && <div className="space-y-2">
<label htmlFor={`${id}-revival-target`}>Ally to receive extra revivals</label>
<select id={`${id}-revival-target`} value={revivalAlly?.id ?? ''} disabled={busy}
onChange={event => { setRevivalTarget(event.target.value); setEliteRaw('0'); }}>
<option value="">Choose an ally</option>
{allies.map(ally => <option key={ally.id} value={ally.id}>{ally.name}</option>)}
</select>
{revivalAlly && <>
<p className="fine">{revivalAlly.name} has {revivalAlly.tanks} forces in the tanks and can receive up to {maximum} extra revivals now.</p>
{revivalOrderHead && revivalOrderHead !== revivalAlly.id && <p className="fine">Wait for this ally’s revival turn.</p>}
<label htmlFor={`${id}-revival-amount`}>Forces to revive</label>
<input id={`${id}-revival-amount`} type="number" inputMode="numeric" min={1} max={maximum}
step={1} value={revivalRaw} disabled={busy} onChange={event => setRevivalRaw(event.target.value)} />
{revivalAlly.elites && <>
<label htmlFor={`${id}-revival-elite`}>{revivalAlly.faction === 'ixians' ? 'Cyborgs' : 'Elite forces'}</label>
<input id={`${id}-revival-elite`} type="number" inputMode="numeric" min={0}
max={Math.min(amount ?? 0, eliteRevivalRemaining(revivalAlly, advanced))} step={1}
value={eliteRaw} disabled={busy} onChange={event => setEliteRaw(event.target.value)} />
</>}
{cost !== null && <p className="fine">You pay {cost} spice. This uses {revivalAlly.name}’s extra-revival allowance.</p>}
</>}
<button type="button" className="game-action" disabled={!canRevive}
onClick={() => { if (revivalAlly && amount !== null && elite !== null && canRevive)
act({ type: 'emperorRevival', target: revivalAlly.id, amount, elite }); }}>
Pay for extra ally revivals
</button>
</div>}
</section>;
}
+6
View File
@@ -286,6 +286,7 @@ import { MoritaniExtortion, MoritaniExtortionNotice } from './moritani-extortion
import { AdvancedAllySeparation } from './advanced-ally-separation';
import { CoalitionNexus } from './coalition-nexus';
import { CoalitionFunding, coalitionFundingContributions, type CoalitionFundingContribution } from './coalition-funding';
import { CoalitionEmperorAid } from './coalition-emperor-aid';
import { NexusEmperorPayment } from './nexus-emperor-payment';
import { NexusEmperorSellerPurchase } from './nexus-emperor-seller-purchase';
import { NexusEmperorAmbassadorPurchase } from './nexus-emperor-ambassador-purchase';
@@ -4539,6 +4540,11 @@ export function GameTable({
)}
</details>
)}
{g.coalitionEmperor && <CoalitionEmperorAid
allies={g.players.filter(player => g.coalitionEmperor!.allies.includes(player.id))}
phase={g.phase} advanced={g.advanced} availableSpice={g.coalitionEmperor.availableSpice}
emperorExtra={g.emperorExtra} revivalOrderHead={g.revivalOrder?.remaining[0]}
busy={busy} act={act} />}
{g.phase === 4 && (
<>
<RevivalTurn game={g} act={act} busy={busy} />