Connect Advanced Ecaz and Moritani special Karama first versions
This commit is contained in:
1 parent
59a406830c
commit
d438d923a3
17 files changed
+872
-3
No files matched your search
@@ -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>
|
||||
);
|
||||
}
|
||||
@@ -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} />
|
||||
|
||||
@@ -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>
|
||||
);
|
||||
}
|
||||
Reference in new issue
Block a user