Connect Advanced Ecaz and Moritani special Karama first versions

This commit is contained in:
Quintine committed 2026-10-08 11:29:08 +11:00
1 parent 59a406830c
commit d438d923a3
17 files changed
+872 -3

No files matched your search

+96
View File
@@ -0,0 +1,96 @@
'use client';
import { useId, useState } from 'react';
import type { Action } from '@/game/engine';
import type { EcazSpecialKaramaQuote } from '@/game/ecaz-special-karama';
import { Button } from './ui/button';
import { CardInspector } from './card-inspector';
/** The parent supplies this private quote only to its Ecaz owner. */
export function EcazSpecialKarama({
quote,
act,
busy,
}: {
quote: EcazSpecialKaramaQuote | null;
act: (action: Action) => void;
busy: boolean;
}) {
const id = useId();
const [selection, setSelection] = useState('');
if (!quote) return null;
const karama = quote.cards.find((card) => card.id === selection) ?? quote.cards[0];
const reason = quote.blocked ??
(!quote.event ? 'No current battle event is available.' :
!karama ? 'You need an available Karama card.' : null);
return (
<details className="my-4" open={quote.declared}>
<summary className="min-h-11 cursor-pointer py-3">
Ecaz special Karama: leader-disc difference
</summary>
<div className="flex flex-col gap-4 py-3">
<p>
Before Battle Plans are revealed, spend one Karama to declare your
once-per-game special power. Your Battle Plan must contain neither a
weapon nor a defense. You may declare before sealing your plan.
</p>
<p className="fine">
Provisional prototype policy, not a publisher ruling: after reveal,
the absolute difference between the printed or copied leader-disc
values adds virtual dial strength. Other leader modifiers are excluded.
This adds no physical forces, spice support payments or casualties.
The bonus is not quoted from your opponent’s hidden plan.
</p>
{quote.declared ? (
<output className="notice">
Declared for this battle. Keep both your weapon and defense slots
empty; the leader-disc bonus is determined after reveal.
</output>
) : (
<>
{quote.cards.length > 0 && (
<>
<label htmlFor={`${id}-card`}>Karama to spend</label>
<select
id={`${id}-card`}
className="min-h-11"
value={karama?.id ?? ''}
disabled={busy || !!reason}
onChange={(event) => setSelection(event.target.value)}
>
{quote.cards.map((card) => (
<option value={card.id} key={card.id}>
{card.name}
</option>
))}
</select>
{karama && <CardInspector card={karama} />}
</>
)}
{reason && (
<p id={`${id}-reason`} className="notice">
{reason}
</p>
)}
<Button
className="min-h-11 whitespace-normal"
disabled={busy || !!reason}
aria-describedby={reason ? `${id}-reason` : undefined}
onClick={() => {
if (!busy && !reason && quote.event && karama)
act({
type: 'card',
mode: 'special',
card: karama.id,
event: quote.event,
});
}}
>
Spend Karama to declare the Ecaz leader-disc bonus
</Button>
</>
)}
</div>
</details>
);
}
+6
View File
@@ -69,6 +69,8 @@ import {
} from './stronghold-cards';
import { EcazPlacement, AmbassadorSupply } from './ecaz-ambassadors';
import { EcazEntry, AmbassadorInsights } from './ecaz-entry';
import { EcazSpecialKarama } from './ecaz-special-karama';
import { MoritaniSpecialKarama } from './moritani-special-karama';
import { RicheseAuctionDecision, RicheseAuctionLot } from './richese-auctions';
import { RicheseSpecialKarama } from './richese-special-karama';
import { RicheseGift } from './richese-gift';
@@ -2805,6 +2807,9 @@ export function GameTable({
/>
) : g.decision.kind === 'moritaniAssassinate' ? (
<MoritaniAssassinate key={g.decision.event} game={g} act={act} busy={busy} />
) : g.decision.kind === 'moritaniSpecialKarama' ? (
<MoritaniSpecialKarama quote={g.moritaniSpecialKarama ?? null} act={act} busy={busy}
declineAction={{ type: 'decision', event: g.decision.event, decline: true }} />
) : g.decision.kind === 'moritaniRetention' ? (
<MoritaniRetention game={g} act={act} busy={busy} />
) : g.decision.kind === 'moritaniTerror' ? (
@@ -5377,6 +5382,7 @@ export function GameTable({
<>
<ChoamCashIn game={g} act={act} busy={busy} />
<RicheseSpecialKarama game={g} act={act} busy={busy} />
<EcazSpecialKarama quote={g.ecazSpecialKarama ?? null} act={act} busy={busy} />
<RicheseGift game={g} act={act} busy={busy} />
<Distrans game={g} act={act} busy={busy} />
<JuiceOfSapho game={g} act={act} busy={busy} />
+127
View File
@@ -0,0 +1,127 @@
'use client';
import { useId, useState } from 'react';
import type { Action } from '@/game/engine';
import type { MoritaniSpecialKaramaView } from '@/game/moritani-special-karama';
import { Button } from './ui/button';
import { CardInspector, CardRules } from './card-inspector';
type Props = {
quote: MoritaniSpecialKaramaView | null;
act: (action: Action) => void;
busy: boolean;
/** The engine owns the opportunity's decline action; no Karama is spent. */
declineAction: Action;
};
type Disposition = 'unchanged' | 'keep' | 'discard';
export function MoritaniSpecialKarama({ quote, act, busy, declineAction }: Props) {
if (!quote) return null;
return (
<MoritaniSpecialKaramaChoice
key={`${quote.source.event}:${quote.source.owner}:${quote.source.opponent}`}
quote={quote}
act={act}
busy={busy}
declineAction={declineAction}
/>
);
}
function MoritaniSpecialKaramaChoice({ quote, act, busy, declineAction }: Omit<Props, 'quote'> & {
quote: MoritaniSpecialKaramaView;
}) {
const id = useId();
const [activation, setActivation] = useState('');
const [choices, setChoices] = useState<Record<string, Disposition>>({});
const karama = quote.karamas.find(card => card.id === activation) ?? quote.karamas[0];
const keep = quote.playedCards
.filter(card => choices[card.id] === 'keep' && quote.keepableIds.includes(card.id))
.map(card => card.id);
const discard = quote.playedCards.filter(card => choices[card.id] === 'discard').map(card => card.id);
const reason = !karama
? 'You need an available Karama Card.'
: keep.length + discard.length === 0
? 'Force at least one card to be kept or discarded, or decline without spending a Karama.'
: null;
return (
<section className="my-4 flex flex-col gap-4" aria-labelledby={`${id}-heading`}>
<h3 id={`${id}-heading`}>Moritani special Karama: opponent’s played cards</h3>
<p>
After your battle loss, spend one of your Karama Cards to force your opponent
to keep or discard selected publicly revealed cards they played. Unchanged
cards remain their choice. This uses your once-per-game special power.
</p>
<p className="fine">{quote.boundary}</p>
<label htmlFor={`${id}-activation`}>Karama to spend</label>
<select
id={`${id}-activation`}
className="min-h-11"
value={karama?.id ?? ''}
disabled={busy || !quote.karamas.length}
onChange={event => setActivation(event.target.value)}
>
{quote.karamas.map(card => (
<option value={card.id} key={card.id}>{card.name}</option>
))}
</select>
{karama && <CardInspector card={karama} />}
{quote.playedCards.map((card, index) => {
const keepable = quote.keepableIds.includes(card.id);
const selected = choices[card.id] ?? 'unchanged';
const disposition = selected === 'keep' && !keepable ? 'unchanged' : selected;
return (
<div className="flex flex-col gap-3" key={card.id}>
<label htmlFor={`${id}-card-${index}`}>{card.name}: disposition</label>
<select
id={`${id}-card-${index}`}
className="min-h-11"
value={disposition}
disabled={busy}
onChange={event => setChoices(previous => ({
...previous,
[card.id]: event.target.value as Disposition,
}))}
>
<option value="unchanged">Unchanged — opponent chooses normally</option>
<option value="keep" disabled={!keepable}>Force keep</option>
<option value="discard">Force discard</option>
</select>
{!keepable && <p className="fine">Force keep is unavailable under the provisional mandatory-disposal precedence.</p>}
<CardRules card={card} />
<CardInspector card={card} />
</div>
);
})}
{reason && <p className="notice" id={`${id}-reason`}>{reason}</p>}
<div className="flex flex-wrap gap-3">
<Button
className="min-h-11 whitespace-normal"
disabled={busy || !!reason}
aria-describedby={reason ? `${id}-reason` : undefined}
onClick={() => {
if (!busy && !reason && karama) act({
type: 'card',
mode: 'special',
card: karama.id,
event: quote.source.event,
keep,
discard,
});
}}
>
Spend Karama and force {keep.length + discard.length} card disposition(s)
</Button>
<Button
className="min-h-11 whitespace-normal"
disabled={busy}
onClick={() => { if (!busy) act(declineAction); }}
>
Decline — keep your Karama
</Button>
</div>
</section>
);
}