Support staged group Emperor aid
Verify and publish checkpoint container / container (push) Successful in 10m13s
Verify and publish checkpoint container / container (push) Successful in 10m13s
This commit is contained in:
1 parent
b89c225d5b
commit
9081249574
10 files changed
+413
-17
No files matched your search
@@ -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>;
|
||||
}
|
||||
@@ -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} />
|
||||
|
||||
Reference in new issue
Block a user