diff --git a/components/battle-preparation.tsx b/components/battle-preparation.tsx
index c4377cae..4db23057 100644
--- a/components/battle-preparation.tsx
+++ b/components/battle-preparation.tsx
@@ -11,7 +11,62 @@ import {
import { useState } from 'react';
import { Button } from '@/components/ui/button';
import { Input } from '@/components/ui/input';
-import type { Action, GameView } from '@/game/engine';
+import type { Action, GameView, PlanField } from '@/game/engine';
+import type { CommittedPlanElement } from '@/game/battle-inspections';
+
+/** Read only the recipient's projected commitments, including an explicit null. */
+export function battlePlanCommitments(game: GameView): Partial> {
+ const battle = game.battle;
+ if (!battle) return {};
+ const commitments = battle.ownCommitments ?? (
+ battle.insight && battle.prescience && battle.prescience.player !== game.me &&
+ [battle.attacker, battle.defender].includes(game.me)
+ ? [{ source: 'native' as const, beneficiary: battle.prescience.player, target: game.me,
+ field: battle.insight.field, value: battle.insight.value }]
+ : []
+ );
+ return Object.fromEntries(commitments.filter((item) => item.target === game.me).map((item) => [item.field, item]));
+}
+
+export function bindBattlePlanCommitments(game: GameView, plan: Action): Action {
+ if (plan.type !== 'battlePlan') return plan;
+ return { ...plan, ...Object.fromEntries(Object.values(battlePlanCommitments(game)).map((item) => [item.field, item.value])) };
+}
+
+export function battlePreparationField(game: GameView): PlanField | null {
+ const battle = game.battle;
+ if (battle?.preparation?.kind === 'nexusPrescienceAnswer') {
+ const inspection = battle.nexusInspection;
+ return inspection?.stage === 'answer' && inspection.event === battle.event &&
+ inspection.target === battle.preparation.owner && inspection.owner === battle.preparation.beneficiary
+ ? inspection.field : null;
+ }
+ return battle?.preparation?.kind === 'prescienceAnswer' ? battle.prescience?.field ?? null : null;
+}
+
+export function battleInspectionAnswerAction(game: GameView, value: string): Action | null {
+ const battle = game.battle;
+ const field = battlePreparationField(game);
+ if (game.status !== 'playing' || game.phase !== 6 || !battle || battle.revealed ||
+ battle.preparation?.owner !== game.me || !field) return null;
+ const answer = field === 'dial' ? Number(value) : value || null;
+ if (field === 'dial' && (value.trim() === '' || !Number.isFinite(answer) || Number(answer) < 0 || !Number.isSafeInteger(Number(answer) * 2))) return null;
+ return battle.preparation.kind === 'nexusPrescienceAnswer'
+ ? { type: 'nexusPrescienceAnswer', event: battle.nexusInspection!.event, value: answer }
+ : { type: 'prescienceAnswer', value: answer };
+}
+
+export function NexusInspectionHistory({ game }: { game: GameView }) {
+ const battle = game.battle;
+ if (!battle?.nexusInspection || ![battle.nexusInspection.owner, battle.nexusInspection.target].includes(game.me) || !battle.nexusInsights?.length) return null;
+ return
+ Nexus inspection
+ {battle.nexusInsights.map((insight, index) =>
+ {insight.active ? (battle.nexusInspection!.target === game.me ? 'Your committed element' : 'Current answer') : 'Previous answer'}: {insight.field} = {insight.label}
+ {!insight.active && · No longer binding}
+
)}
+ ;
+}
export function BattlePreparation({
game,
@@ -134,7 +189,9 @@ export function BattlePreparation({
>
);
- const field = b.prescience!.field;
+ const field = battlePreparationField(game);
+ if (!field) return This inspection is no longer awaiting an answer.
;
+ const answerAction = battleInspectionAnswerAction(game, value);
const stoneReason =
field === 'weapon' && isStoneBurner(me.hand?.find((c) => c.id === value))
? b.stoneBurnerContext?.blocked
@@ -173,6 +230,7 @@ export function BattlePreparation({
)}
{field === 'dial' ? (
)}
-
{stoneReason && {stoneReason}
}
diff --git a/components/game-table.tsx b/components/game-table.tsx
index 4e97c969..e123f8e5 100644
--- a/components/game-table.tsx
+++ b/components/game-table.tsx
@@ -121,7 +121,7 @@ import {
} from 'lucide-react';
import { Button } from '@/components/ui/button';
import { Input } from '@/components/ui/input';
-import { BattlePreparation } from './battle-preparation';
+import { BattlePreparation, battlePlanCommitments, bindBattlePlanCommitments, NexusInspectionHistory } from './battle-preparation';
import { PrivateBattlePlan } from './private-battle-plan';
import { RevealedBattle } from './revealed-battle';
import { EliteCount } from './elite-count';
@@ -374,13 +374,10 @@ export function GameTable({
);
const [discardCards, setDiscardCards] = useState>({});
const stormLosses = g.decision?.kind === 'stormLosses' ? g.decision : null;
- const committed =
- g.battle?.insight && g.battle.prescience?.player !== me.id
- ? g.battle.insight
- : null;
+ const committed = battlePlanCommitments(g);
const selectedStone = me.hand?.find(
(c) =>
- c.id === (committed?.field === 'weapon' ? committed.value : weapon) &&
+ c.id === (committed.weapon ? committed.weapon.value : weapon) &&
isStoneBurner(c),
);
const stoneContext = g.battle?.stoneBurnerContext;
@@ -393,8 +390,8 @@ export function GameTable({
.map((pool) =>
stoneBurnerPlanBlock(
ownBattleForces,
- committed?.field === 'dial'
- ? Number(committed.value)
+ committed.dial
+ ? Number(committed.dial?.value)
: battleDial,
battleSupport,
pool,
@@ -1780,6 +1777,7 @@ export function GameTable({
faceDancerReplacement: 'Replace an unrevealed Face Dancer',
voice: 'The Voice',
prescience: 'Prescience',
+ nexusPrescience: 'Atreides Nexus Cunning',
emperorIncome:
g.response.source === 'ambassador'
? 'Emperor card-purchase income'
@@ -4038,6 +4036,7 @@ export function GameTable({
)}
{g.battle.revealed &&
{g.battle.insight && (
- {committed
+ {committed[g.battle.insight.field]
? 'Your committed element'
: 'Prescience'}
: {g.battle.insight.field} ={' '}
@@ -4133,11 +4132,11 @@ export function GameTable({
max={battleDialMaximum}
step={battleDialStep}
value={
- committed?.field === 'dial'
- ? Number(committed.value)
+ committed.dial
+ ? Number(committed.dial?.value)
: battleDial
}
- disabled={busy || committed?.field === 'dial'}
+ disabled={busy || !!committed.dial}
onChange={setDial}
/>
@@ -4216,11 +4215,11 @@ export function GameTable({