Support Moritani assassination of captured and foreign-ghola targets

Retain printed native identity and valid original custody while projecting only
uniform potential bounty before commitment. Resolve living/dead targets once,
clear selected capture/concealment and retain ordinary Ghola return custody.
Selected concealed-outcome disclosure is explicitly provisional.

Verified: 14 affected offline files100/100, types/lint and production build;
six actual choice/gameplay/GameTable SSR journeys and twelve profile paths.
Native capture source47 actions; actual paid Ghola prefix140 actions followed
by explicit conserved seven-action battle. No saved-room/D1/reset/restart or
deployment operation. Runtime-only driver fixes reused unchanged passed checks.
This commit is contained in:
Quintine committed 2026-10-24 19:03:03 +11:00
1 parent 61828150b9
commit eff668b202
25 files changed
+1041 -57

No files matched your search

+31 -10
View File
@@ -17,6 +17,22 @@ export function MoritaniAssassinate({
busy: boolean;
}) {
const [selected, setSelected] = useState('');
return <MoritaniAssassinateChoice game={game} act={act} busy={busy} selected={selected} onSelect={setSelected} />;
}
export function MoritaniAssassinateChoice({
game,
act,
busy,
selected,
onSelect,
}: {
game: GameView;
act: (action: Action) => void;
busy: boolean;
selected: string;
onSelect: (card: string) => void;
}) {
const offer = game.moritaniAssassinate;
const decision = game.decision;
if (
@@ -30,9 +46,9 @@ export function MoritaniAssassinate({
const pending = offer.pending;
const card = pending.cards.find((candidate) => candidate.card === selected) ?? pending.cards[0];
const blocked = busy || !!game.response || !!game.truthtrance;
const revealBlocked = blocked || !!pending.blocked;
const opponent = game.players.find((player) => player.id === pending.opponent);
const factionName = opponent ? faction(opponent.faction).name : 'the opposing faction';
const selectedLeader = card && game.allLeaders.find((leader) => leader.id === card.card);
return (
<section className="min-w-0 space-y-3" aria-label="Moritani Assassinate Leaders">
<h3 className="font-serif text-xl">Assassinate Leaders</h3>
@@ -43,7 +59,10 @@ export function MoritaniAssassinate({
</p>
<p className="fine">
This advantage is available once against each faction. It cannot be canceled
with Karama. A dead target remains a legal choice, but yields no spice.
with Karama. The potential bounty is the leader’s printed strength and is
awarded only if the target is alive. An already-dead target remains a
legal choice and awards no spice. Revealing the physical Traitor Card
makes the actual outcome public.
</p>
{pending.blocked && <p className="notice">{pending.blocked}</p>}
{pending.cards.length > 0 ? (
@@ -54,31 +73,32 @@ export function MoritaniAssassinate({
aria-label="Traitor Card to reveal"
className="min-h-11 w-full rounded border border-[#65644b] bg-[#171b17] px-3 py-2"
value={card?.card ?? ''}
disabled={blocked}
onChange={(event) => setSelected(event.currentTarget.value)}
disabled={revealBlocked}
onChange={(event) => onSelect(event.currentTarget.value)}
>
{pending.cards.map((candidate) => (
<option key={candidate.card} value={candidate.card}>
{candidate.name}{candidate.dead ? ' · dead target · 0 spice' : ` · ${candidate.bounty} spice`}
{candidate.name} · {candidate.potentialBounty} spice potential bounty
</option>
))}
</select>
</label>
{card && (
<div className="rounded-lg border border-[#485542] p-4 space-y-2">
<p className="m-0"><strong>{card.name}</strong> · {card.dead ? '0 spice; target already dead' : `${card.bounty} spice bounty`}</p>
<p className="m-0 text-sm">Printed leader strength: {selectedLeader?.strength ?? 'unknown'}</p>
<p className="m-0"><strong>{card.name}</strong> · {card.potentialBounty} spice potential bounty</p>
<p className="m-0 text-sm">Printed leader strength: {card.potentialBounty}</p>
<LeaderInspector
kind="traitor"
assassination="choice"
identity={{ name: card.name, factionName, strength: selectedLeader?.strength ?? 0 }}
identity={{ name: card.name, factionName, strength: card.potentialBounty }}
/>
</div>
)}
<Button
data-testid="moritani-assassinate-reveal"
className="min-h-11 whitespace-normal"
disabled={blocked || !card}
onClick={() => card && !blocked && act({ type: 'decision', event: pending.event, card: card.card })}
disabled={revealBlocked || !card}
onClick={() => card && !revealBlocked && act({ type: 'decision', event: pending.event, card: card.card })}
>
Reveal {card?.name ?? 'selected Traitor Card'} publicly
</Button>
@@ -87,6 +107,7 @@ export function MoritaniAssassinate({
<p className="notice">No eligible opposing-faction Traitor Card is available for this advantage.</p>
)}
<Button
data-testid="moritani-assassinate-decline"
variant="outline"
className="min-h-11 whitespace-normal"
disabled={blocked}