Connect bounded Moritani Atomics Aftermath
This commit is contained in:
1 parent
b2603f6715
commit
66a427ab55
17 files changed
+1036
-68
No files matched your search
+20
-11
@@ -42,6 +42,7 @@ import { NexusAdvisors } from './nexus-advisors';
|
||||
import { NexusSardaukar } from './nexus-sardaukar';
|
||||
import { TerrorBoardMarkers } from './terror-board-markers';
|
||||
import { homeworldRevivalActionBlock } from '@/game/homeworld-revival-deployment-options';
|
||||
import { atomicsAllianceChangeBlocked } from '@/game/moritani-atomics';
|
||||
import { GuildHomeworldShipment } from './guild-homeworld-shipment';
|
||||
import { JunctionTransport } from './junction-transport';
|
||||
import { BiddingEnd } from './bidding-end';
|
||||
@@ -830,6 +831,8 @@ export function GameTable({
|
||||
: null,
|
||||
) !== p.answer,
|
||||
);
|
||||
const atomicsShipmentBlocked =
|
||||
a.type === 'ship' && a.territory === g.moritaniAtomics?.territory;
|
||||
return (
|
||||
<Button
|
||||
className="game-action"
|
||||
@@ -837,11 +840,12 @@ export function GameTable({
|
||||
[
|
||||
describedBy,
|
||||
shipmentBlocked ? 'shipment-promise-guidance' : undefined,
|
||||
atomicsShipmentBlocked ? 'atomics-shipment-guidance' : undefined,
|
||||
]
|
||||
.filter(Boolean)
|
||||
.join(' ') || undefined
|
||||
}
|
||||
disabled={busy || !!g.truthtrance || !!g.nexusTraitors?.pending || disabled || shipmentBlocked}
|
||||
disabled={busy || !!g.truthtrance || !!g.nexusTraitors?.pending || disabled || shipmentBlocked || atomicsShipmentBlocked}
|
||||
onClick={() => act(a)}
|
||||
>
|
||||
{text}
|
||||
@@ -1483,7 +1487,7 @@ export function GameTable({
|
||||
));
|
||||
})}
|
||||
<NoFieldBoardMarkers players={g.players} />
|
||||
<TerrorBoardMarkers tokens={g.moritaniTerror?.tokens ?? []} />
|
||||
<TerrorBoardMarkers tokens={g.moritaniTerror?.tokens ?? []} atomics={g.moritaniAtomics} />
|
||||
{Object.entries(g.spice)
|
||||
.filter(([, n]) => n > 0)
|
||||
.map(([k, n]) => {
|
||||
@@ -3600,24 +3604,29 @@ export function GameTable({
|
||||
<option value="">Choose a player</option>
|
||||
{g.players
|
||||
.filter((p) => p.id !== me.id)
|
||||
.map((p) => (
|
||||
<option value={p.id} key={p.id} disabled={!!g.homeworldAllianceBlocks?.[p.id]}>
|
||||
.map((p) => {
|
||||
const atomicsBlocked = atomicsAllianceChangeBlocked(g.moritaniAtomics, me.id, p.id);
|
||||
return <option value={p.id} key={p.id}
|
||||
disabled={!!g.homeworldAllianceBlocks?.[p.id] || atomicsBlocked}>
|
||||
{p.name}
|
||||
{g.homeworldAllianceBlocks?.[p.id] ? ` · ${g.homeworldAllianceBlocks[p.id]}` : ''}
|
||||
{g.allianceOffers[p.id] === me.id
|
||||
? ' · Invited you'
|
||||
: ''}
|
||||
</option>
|
||||
))}
|
||||
{atomicsBlocked ? ' · Atomics alliance ruling pending' : ''}
|
||||
{g.allianceOffers[p.id] === me.id ? ' · Invited you' : ''}
|
||||
</option>;
|
||||
})}
|
||||
</select>
|
||||
</label>
|
||||
{g.homeworldAllianceBlocks?.[target] && <p className="notice">{g.homeworldAllianceBlocks[target]}</p>}
|
||||
{!!target && atomicsAllianceChangeBlocked(g.moritaniAtomics, me.id, target) &&
|
||||
<p className="notice">Changing Moritani’s ally after Atomics awaits a hand-limit ruling.</p>}
|
||||
{actionButton('Propose / accept alliance', {
|
||||
type: 'alliance',
|
||||
target,
|
||||
}, !target || !!g.homeworldAllianceBlocks?.[target])}
|
||||
}, !target || !!g.homeworldAllianceBlocks?.[target] ||
|
||||
atomicsAllianceChangeBlocked(g.moritaniAtomics, me.id, target))}
|
||||
{me.ally &&
|
||||
actionButton('Break alliance', { type: 'alliance' })}
|
||||
actionButton('Break alliance', { type: 'alliance' },
|
||||
atomicsAllianceChangeBlocked(g.moritaniAtomics, me.id, null))}
|
||||
</>
|
||||
)}
|
||||
{g.phase === 1 && !g.nexus && !g.spiceWindow && (
|
||||
|
||||
@@ -204,12 +204,12 @@ function TokenInspector({
|
||||
),
|
||||
)}
|
||||
<p className="border-t border-[#414536] pt-4 text-sm leading-6 text-[#c9c2ae]">
|
||||
Robbery, Sabotage, Sneak Attack and ordinary native-leader
|
||||
Assassination have controls for supported entry reactions.
|
||||
Enemy of My Enemy has partial alliance controls. Atomics and
|
||||
Extortion remain under implementation, and complete Moritani
|
||||
games remain unavailable. Inspection explains the rules; it
|
||||
does not reveal a token or activate an effect.
|
||||
Supported entry reactions include Robbery, Sabotage, Sneak
|
||||
Attack, ordinary native-leader Assassination and Extortion.
|
||||
Enemy of My Enemy has partial alliance controls. Atomics can
|
||||
be revealed only on supported tables; complete Moritani games
|
||||
remain unavailable. Inspection does not reveal a token or
|
||||
activate an effect.
|
||||
</p>
|
||||
</section>
|
||||
</div>
|
||||
@@ -223,6 +223,15 @@ function TokenInspector({
|
||||
export function MoritaniTerrorSupply({ game }: { game: GameView }) {
|
||||
const titleId = useId();
|
||||
const view = game.moritaniTerror;
|
||||
const aftermath = game.moritaniAtomics;
|
||||
const affectedNames = aftermath
|
||||
? [
|
||||
game.players.find((player) => player.id === aftermath.moritaniId)?.name ?? 'Moritani',
|
||||
...(aftermath.allyAtActivation
|
||||
? [game.players.find((player) => player.id === aftermath.allyAtActivation)?.name ?? 'their ally at activation']
|
||||
: []),
|
||||
].join(' and ')
|
||||
: '';
|
||||
if (!view) return null;
|
||||
const owner =
|
||||
game.players.find((p) => p.id === game.me)?.faction === 'moritani';
|
||||
@@ -242,6 +251,22 @@ export function MoritaniTerrorSupply({ game }: { game: GameView }) {
|
||||
? 'Your supply and placed tokens. Inspect a face without revealing it to the table.'
|
||||
: 'Placed tokens stay hidden. Revealed tokens can be inspected by everyone.'}
|
||||
</p>
|
||||
{aftermath && (
|
||||
<div id="atomics-shipment-guidance" className="mb-3 rounded-lg border border-[#b99a68] bg-[#302923] p-3 text-sm leading-6 text-[#f1dfb4]" aria-label="Atomics Aftermath">
|
||||
<p className="m-0 font-semibold">
|
||||
Atomics Aftermath · {territory(aftermath.territory).name}
|
||||
</p>
|
||||
<p className="mt-1 mb-0">
|
||||
From turn {aftermath.turn}, no faction may ship forces into this
|
||||
territory, including Fremen reinforcements. Otherwise legal
|
||||
ordinary movement and Sneak Attack remain possible.
|
||||
</p>
|
||||
<p className="mt-1 mb-0">
|
||||
A lasting hand-limit reduction of one applies to {affectedNames}.
|
||||
Any excess cards were discarded at random when Atomics resolved.
|
||||
</p>
|
||||
</div>
|
||||
)}
|
||||
{tokens.length ? (
|
||||
<div className="grid min-w-0 grid-cols-2 gap-2 sm:grid-cols-3 lg:grid-cols-6">
|
||||
{tokens.map((token) => (
|
||||
@@ -563,6 +588,18 @@ export function MoritaniEntry({
|
||||
native leader. You do not choose the victim.
|
||||
</p>
|
||||
)}
|
||||
{entry.kind === 'atomics' && entry.canReveal && (
|
||||
<p className="m-0 text-sm leading-6">
|
||||
Revealing Atomics sends every faction’s forces in{' '}
|
||||
{territory(entry.territory).name} to the Tanks, including your
|
||||
own. The public Aftermath permanently blocks shipments into
|
||||
that territory, including Fremen reinforcements, but not
|
||||
otherwise legal movement or Sneak Attack. Your hand limit
|
||||
falls by one starting this turn; your ally’s at activation
|
||||
does too, if you have one. Each hand over its new limit
|
||||
discards one random card per excess card.
|
||||
</p>
|
||||
)}
|
||||
{entry.kind === 'sneakAttack' && sneak && (
|
||||
<div className="space-y-2 text-sm leading-6">
|
||||
<p className="m-0">
|
||||
@@ -625,10 +662,11 @@ export function MoritaniEntry({
|
||||
Leave token hidden
|
||||
</Button>
|
||||
<p className="m-0 text-sm leading-6">
|
||||
Robbery, Sabotage, Sneak Attack and ordinary native-leader
|
||||
Assassination support eligible arrivals. Enemy of My Enemy has
|
||||
partial alliance controls. Atomics and Extortion are not yet
|
||||
available; complete Moritani games remain unavailable.
|
||||
Robbery, Sabotage, Sneak Attack, ordinary native-leader
|
||||
Assassination and Extortion support eligible arrivals. Atomics
|
||||
is available only on supported tables. Enemy of My Enemy has
|
||||
partial alliance controls; complete Moritani games remain
|
||||
unavailable.
|
||||
</p>
|
||||
</>
|
||||
)}
|
||||
|
||||
@@ -1,26 +1,44 @@
|
||||
import { territory } from '@/game/board';
|
||||
import type { GameView } from '@/game/engine';
|
||||
|
||||
/** The map shows public stack counts without reading any hidden token face. */
|
||||
export function TerrorBoardMarkers({ tokens }: {
|
||||
/** Public map markers never inspect the face of a placed Terror token. */
|
||||
export function TerrorBoardMarkers({ tokens, atomics }: {
|
||||
tokens: readonly { status: string; location: string | null }[];
|
||||
atomics?: GameView['moritaniAtomics'];
|
||||
}) {
|
||||
const groups = new Map<string, number>();
|
||||
for (const token of tokens)
|
||||
if (token.status === 'placed' && token.location)
|
||||
groups.set(token.location, (groups.get(token.location) ?? 0) + 1);
|
||||
return [...groups].map(([id, count]) => {
|
||||
const place = territory(id);
|
||||
const label = `${count} hidden Terror token${count === 1 ? '' : 's'} in ${place.name}`;
|
||||
return (
|
||||
<g key={id} transform={`translate(${place.center[0] + 42},${place.center[1] - 22})`}
|
||||
pointerEvents="none" aria-label={label}>
|
||||
<title>{label}</title>
|
||||
{count > 1 && <circle cx="3" cy="-3" r="15" fill="#392b41" stroke="#d6b9dc" strokeWidth="2" />}
|
||||
<circle r="15" fill="#392b41" stroke="#d6b9dc" strokeWidth="2" />
|
||||
<text y="5" textAnchor="middle" fill="#f4e1f1" fontSize="15" fontWeight="700">
|
||||
{count === 1 ? 'T' : `T${count}`}
|
||||
</text>
|
||||
</g>
|
||||
);
|
||||
});
|
||||
const aftermath = atomics && territory(atomics.territory);
|
||||
const handLimitLabel = atomics?.allyAtActivation
|
||||
? 'Moritani and the ally at activation have hand limits reduced by one'
|
||||
: 'Moritani has a hand limit reduced by one';
|
||||
return (
|
||||
<>
|
||||
{[...groups].map(([id, count]) => {
|
||||
const place = territory(id);
|
||||
const label = `${count} hidden Terror token${count === 1 ? '' : 's'} in ${place.name}`;
|
||||
return (
|
||||
<g key={id} transform={`translate(${place.center[0] + 42},${place.center[1] - 22})`}
|
||||
pointerEvents="none" aria-label={label}>
|
||||
<title>{label}</title>
|
||||
{count > 1 && <circle cx="3" cy="-3" r="15" fill="#392b41" stroke="#d6b9dc" strokeWidth="2" />}
|
||||
<circle r="15" fill="#392b41" stroke="#d6b9dc" strokeWidth="2" />
|
||||
<text y="5" textAnchor="middle" fill="#f4e1f1" fontSize="15" fontWeight="700">
|
||||
{count === 1 ? 'T' : `T${count}`}
|
||||
</text>
|
||||
</g>
|
||||
);
|
||||
})}
|
||||
{aftermath && (
|
||||
<g transform={`translate(${aftermath.center[0] - 42},${aftermath.center[1] - 22})`}
|
||||
pointerEvents="none" aria-label={`Atomics Aftermath in ${aftermath.name}: permanent shipment ban; ${handLimitLabel}`}>
|
||||
<title>{`Atomics Aftermath in ${aftermath.name}: no shipments, including Fremen reinforcements; ordinary movement and Sneak Attack remain legal. ${handLimitLabel}.`}</title>
|
||||
<circle r="15" fill="#543929" stroke="#f5bc79" strokeWidth="2" />
|
||||
<text y="5" textAnchor="middle" fill="#fff1d4" fontSize="15" fontWeight="700">A</text>
|
||||
</g>
|
||||
)}
|
||||
</>
|
||||
);
|
||||
}
|
||||
Reference in new issue
Block a user