Resolve ambiguous Stone battles with sealed physical totals

Add a provisional post-reveal physical-count commitment for new outcome-changing Basic/Advanced Stone and copied Mirror battles. Keep all original sides sealed until complete and retain matching later typed losses. Preserve legacy, ordinary and invariant-winner timing. Carry the same constraint through card cleanup and native casualty consumers.

Validation: frozen 16 files146/146, types/lint/build and14 original-action/rendered-control programmes; four minimal policies make both owned choices legally. Source timing is explicitly provisional. Wider combinations, assurance and deployed acceptance remain open. No saved-game, reset, restart or deployment operation.
This commit is contained in:
Quintine committed 2026-10-18 12:13:06 +11:00
1 parent 677ad7aa3c
commit 6ccfaa1b4d
22 files changed
+1247 -83

No files matched your search

+7
View File
@@ -168,6 +168,7 @@ import {
} from '@/game/battle-cards';
import { validBattleSlotPair, usesNativeBattleSpecial } from '@/game/battle-card-slots';
import { stoneBurnerPlanBlock, stoneBurnerWithdrawalContext } from '@/game/stone-burner';
import { StoneBurnerAllocation } from './stone-burner-allocation';
import { mirrorWeaponModeBlock } from '@/game/mirror-weapon-mode';
import { TECH_TOKENS, ownedTech } from '@/game/tech-tokens';
import {
@@ -629,6 +630,7 @@ export function GameTable({
? 'attacker'
: 'defender',
stoneCandidateContext,
stoneContext?.allowAllocations,
),
)
.find((reason) => reason !== null) ?? null)));
@@ -3737,6 +3739,11 @@ export function GameTable({
accept: false,
})}
</>
) : g.decision.kind === 'stoneAllocation' ? (
g.battle?.stoneAllocation && <StoneBurnerAllocation event={g.decision.event}
totals={g.battle.stoneAllocation.totals}
onCommit={undialed => act({type:'decision',event:g.battle!.stoneAllocation!.event,undialed})}
busy={transportBusy || !!g.roomControl?.paused || !!g.roomControl?.closed || !!me.autopilot} />
) : g.decision.kind === 'discoveryEntry' ? (
<DiscoveryEntryDecision game={g} act={act} busy={transportBusy || (!!g.roomControl?.paused || !!g.roomControl?.closed) || !!me.autopilot} />
) : g.decision.kind === 'ecologicalStorm' ? (
+41
View File
@@ -0,0 +1,41 @@
'use client';
import { Button } from './ui/button';
export function StoneBurnerAllocation({
event,
totals,
onCommit,
busy,
}: {
event: string;
totals: readonly number[];
onCommit: (undialed: number) => void;
busy: boolean;
}) {
const choices = [...new Set(totals)].filter(total => Number.isSafeInteger(total) && total >= 0)
.sort((a, b) => a - b);
if (!event || !choices.length) return null;
return (
<section aria-label="Stone Burner undialed force commitment" className="notice space-y-3" data-event={event}>
<h4>Commit undialed physical forces</h4>
<p>
Provisional timing: choose the number of your undialed physical counters
for this Stone Burner comparison. Each ordinary or elite counter counts
as one. Choices stay sealed until everyone has committed.
</p>
<p>
The winner chooses their ordinary and elite losses afterward, consistent
with the committed total. This choice does not change your dial or spice.
</p>
<div className="flex flex-wrap gap-2">
{choices.map(total => (
<Button key={`${event}:${total}`} type="button" className="min-h-11 whitespace-normal text-base" disabled={busy}
onClick={() => { if (!busy) onCommit(total); }}>
Commit {total} undialed physical forces
</Button>
))}
</div>
</section>
);
}