Connect owned multi-pile battle skill choices
This commit is contained in:
1 parent
8e204218be
commit
a1517ec949
20 files changed
+705
-71
No files matched your search
@@ -78,6 +78,8 @@ import { JuiceOfSapho } from './juice-of-sapho';
|
||||
import { LeaderSkillsPanel } from './leader-skills';
|
||||
import { LeaderSkillBattleGuide } from './leader-skill-battle-guide';
|
||||
import { smugglerBattleModeSupported, smugglerBattlePlanBlock } from '@/game/smuggler-battle';
|
||||
import { SmugglerBattleCollection } from './smuggler-battle-collection';
|
||||
import { SandmasterVictorySpice } from './sandmaster-victory-spice';
|
||||
import { RihaniChoice, RihaniHistory } from './rihani-decipherer';
|
||||
import { MentatQuestion, MentatHistory } from './mentat-question';
|
||||
import { BureaucratPayment } from './bureaucrat-payment';
|
||||
@@ -2537,6 +2539,10 @@ export function GameTable({
|
||||
? g.decision.stage === 'offer' ? 'Rihani Decipherer — optional draw' : 'Choose your Traitor exchange'
|
||||
: g.decision.kind === 'sukRescue'
|
||||
? 'Save your battle casualties'
|
||||
: g.decision.kind === 'smugglerCollection'
|
||||
? 'Choose Smuggler collection sectors'
|
||||
: g.decision.kind === 'sandmasterVictorySpice'
|
||||
? 'Choose Sandmaster victory spice sector'
|
||||
: g.decision.kind === 'harassWithdraw'
|
||||
? 'Choose undialed forces to return'
|
||||
: g.decision.kind === 'diplomatRetreat'
|
||||
@@ -2989,6 +2995,10 @@ export function GameTable({
|
||||
mode: 'ignore',
|
||||
})}
|
||||
</>
|
||||
) : g.decision.kind === 'smugglerCollection' ? (
|
||||
<SmugglerBattleCollection key={g.decision.event} offer={g.decision} act={act} busy={busy} />
|
||||
) : g.decision.kind === 'sandmasterVictorySpice' ? (
|
||||
<SandmasterVictorySpice offer={g.decision} act={act} busy={busy} />
|
||||
) : g.decision.kind === 'harassWithdraw' ? (
|
||||
<>
|
||||
<HarassWithdrawChoice key={g.decision.event} game={g} act={act} busy={busy} />
|
||||
@@ -4933,8 +4943,8 @@ export function GameTable({
|
||||
g.battle.revealed ? (
|
||||
<>
|
||||
{g.battle.smugglerCollection && <p className="notice">
|
||||
{g.players.find(p => p.id === g.battle!.smugglerCollection!.player)?.name}’s Smuggler has {g.battle.smugglerCollection.amount} spice pending from the reveal-time pile.
|
||||
Collection is automatic if the leader survives, even after a defeat. That spice is not yet spendable.
|
||||
{g.players.find(p => p.id === g.battle!.smugglerCollection!.player)?.name}’s Smuggler has {g.battle.smugglerCollection.amount} spice pending from the reveal-time piles.
|
||||
If the leader survives, collection resolves before battle settlement, even after defeat. A partial collection across several piles requires its owner’s sector choice. That spice is not yet spendable.
|
||||
</p>}
|
||||
{!g.battle.traitorSubmitted.includes(me.id) && (
|
||||
<>
|
||||
|
||||
@@ -17,10 +17,10 @@ export function LeaderSkillBattleGuide({ game, leader }: { game: GameView; leade
|
||||
return <div className="notice" aria-label="Leader Skill battle effect">
|
||||
{mentat && <p>Mentat adds 2 to this leader’s battle strength if the leader survives.</p>}
|
||||
{bureaucrat && <p>Bureaucrat subtracts {leaderSkillStrongholdCount(game, opponent)} from your opponent’s battle total if your leader survives. Count each occupied stronghold once, including contested strongholds. Advisors do not count.</p>}
|
||||
{sandmaster && <p>If this Sandmaster survives and wins, add 3 spice to the territory’s existing spice pile automatically. No spice is added when the territory has no spice.</p>}
|
||||
{sandmaster && <p>If this Sandmaster survives and wins, add 3 spice to an existing pile in the territory. One pile resolves automatically; with several, provisionally choose its sector. No existing spice means no addition.</p>}
|
||||
{rihani && <p>If this Rihani Decipherer survives and wins, you may draw two Traitors and exchange an unused old card for one of them. A native Rihani also privately inspects two deck cards first.</p>}
|
||||
{smuggler && <p>{game.battle!.smugglerCollectionEnabled
|
||||
? 'Smuggler records spice in this territory when both plans reveal, capped by this leader’s unmodified strength. If the leader survives, collect it automatically even if you lose or have no forces left. Pending spice cannot fund this plan. Strength modifiers and combined modules remain under review.'
|
||||
? 'Smuggler records the territory’s positive piles when both plans reveal, capped in total by this leader’s unmodified strength. A surviving leader collects even if you lose or have no forces left. A partial collection across several piles provisionally asks you for sector amounts; single-pile and all/none collections stay automatic. Pending spice cannot fund this plan. Strength modifiers and other combined modules remain under review.'
|
||||
: 'This saved battle predates automatic Smuggler collection. Its original battle behavior is preserved; new battles use the current collection flow.'}</p>}
|
||||
{(mentat || bureaucrat) && <p className="fine">These score effects resolve automatically. They do not change the leader’s printed value or spice bounty.</p>}
|
||||
</div>;
|
||||
|
||||
@@ -0,0 +1,42 @@
|
||||
'use client';
|
||||
|
||||
import { splitLocation, territory } from '@/game/board';
|
||||
import type { Action } from '@/game/engine';
|
||||
import type { SandmasterVictorySpiceOffer } from '@/game/leader-skill-battle-board';
|
||||
import { Button } from './ui/button';
|
||||
|
||||
type Props = {
|
||||
offer: SandmasterVictorySpiceOffer | null | undefined;
|
||||
busy: boolean;
|
||||
act: (action: Action) => void;
|
||||
};
|
||||
|
||||
export function SandmasterVictorySpice({ offer, busy, act }: Props) {
|
||||
if (!offer) return null;
|
||||
const name = territory(offer.territory).name;
|
||||
return (
|
||||
<section className="notice min-w-0 space-y-3" aria-label="Your Sandmaster victory spice placement">
|
||||
<h3>Sandmaster · victory spice · {name}</h3>
|
||||
<p>Place the mandatory +3 board spice on one existing pile. This grants no direct faction income.</p>
|
||||
<p className="fine">Provisional allocation policy: the winning owner selects an existing pile. This is a prototype choice, not a publisher sector-placement ruling.</p>
|
||||
<div className="space-y-2">
|
||||
{offer.piles.map((pile) => {
|
||||
const sector = splitLocation(pile.key).sector;
|
||||
return (
|
||||
<div key={pile.key} className="space-y-1">
|
||||
<p>{name}, sector {sector}: {pile.before} → {pile.after} spice (+3).</p>
|
||||
<Button
|
||||
className="game-action min-h-11 w-full whitespace-normal"
|
||||
disabled={busy}
|
||||
data-testid={`sandmaster-victory-spice-${pile.key}`}
|
||||
onClick={() => { if (!busy) act({ type: 'decision', event: offer.event, key: pile.key }); }}
|
||||
>
|
||||
Place +3 spice in sector {sector}
|
||||
</Button>
|
||||
</div>
|
||||
);
|
||||
})}
|
||||
</div>
|
||||
</section>
|
||||
);
|
||||
}
|
||||
@@ -0,0 +1,76 @@
|
||||
'use client';
|
||||
|
||||
import { useId, useState } from 'react';
|
||||
import { splitLocation, territory } from '@/game/board';
|
||||
import { defaultSmugglerBattleAllocation, type SmugglerBattleCollectionOffer } from '@/game/smuggler-battle';
|
||||
import { Button } from './ui/button';
|
||||
import { Input } from './ui/input';
|
||||
|
||||
function initialAllocation(offer: SmugglerBattleCollectionOffer): Record<string, string> {
|
||||
const allocations = defaultSmugglerBattleAllocation(offer.piles, offer.amount);
|
||||
return Object.fromEntries(Object.keys(offer.piles).map(key => [key, String(allocations[key] ?? 0)]));
|
||||
}
|
||||
|
||||
export function SmugglerBattleCollection({ offer, busy, act }: {
|
||||
offer: SmugglerBattleCollectionOffer;
|
||||
busy: boolean;
|
||||
act: (action: { type: 'decision'; event: string; allocations: Record<string, number> }) => void;
|
||||
}) {
|
||||
const id = useId();
|
||||
const [selection, setSelection] = useState(() => ({ event: offer.event, values: initialAllocation(offer) }));
|
||||
const values = selection.event === offer.event ? selection.values : initialAllocation(offer);
|
||||
const entries = Object.entries(offer.piles).sort(([a], [b]) => a.localeCompare(b));
|
||||
const allocations: Record<string, number> = {};
|
||||
let total = 0;
|
||||
let legal = true;
|
||||
for (const [key, available] of entries) {
|
||||
const text = values[key] ?? '';
|
||||
const amount = Number(text);
|
||||
if (!text.trim() || !Number.isSafeInteger(amount) || amount < 0 || amount > available) legal = false;
|
||||
if (amount > 0) allocations[key] = amount;
|
||||
total += amount;
|
||||
}
|
||||
legal = legal && total === offer.amount;
|
||||
|
||||
return (
|
||||
<section aria-label="Smuggler battle collection sectors" className="notice space-y-3">
|
||||
<h4>Choose Smuggler collection sectors</h4>
|
||||
<p>
|
||||
Your Smuggler survived. Collect exactly {offer.amount} spice from the
|
||||
revealed piles in {territory(offer.territory).name} before battle settlement.
|
||||
</p>
|
||||
<p className="text-sm">
|
||||
<strong>Provisional allocation policy:</strong> you choose the sectors.
|
||||
This prototype policy is not a publisher ruling; the total and unmodified
|
||||
leader-strength cap do not change.
|
||||
</p>
|
||||
{entries.map(([key, available], index) => (
|
||||
<label key={key} htmlFor={`${id}-${index}`} className="block">
|
||||
Sector {splitLocation(key).sector}: collect 0–{available} spice
|
||||
<Input
|
||||
id={`${id}-${index}`}
|
||||
type="number"
|
||||
min={0}
|
||||
max={available}
|
||||
step={1}
|
||||
disabled={busy}
|
||||
value={values[key] ?? ''}
|
||||
onChange={(event) => setSelection({
|
||||
event: offer.event,
|
||||
values: { ...values, [key]: event.target.value },
|
||||
})}
|
||||
/>
|
||||
</label>
|
||||
))}
|
||||
<output className="block" aria-live="polite">
|
||||
{legal ? `${total} of ${offer.amount} spice selected.` :
|
||||
`Choose whole spice within each pile's limit, totaling exactly ${offer.amount}.`}
|
||||
</output>
|
||||
<Button disabled={busy || !legal} onClick={() => {
|
||||
if (legal && !busy) act({ type: 'decision', event: offer.event, allocations });
|
||||
}}>
|
||||
Collect {offer.amount} spice
|
||||
</Button>
|
||||
</section>
|
||||
);
|
||||
}
|
||||
Reference in new issue
Block a user