104 lines
5.7 KiB
TypeScript
104 lines
5.7 KiB
TypeScript
'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>;
|
||
}
|