Allow Fremen Nexus Betrayal before native worm rides

This commit is contained in:
Quintine committed 2026-10-26 07:06:15 +11:00
1 parent 78bcf22bca
commit 725672364d
19 files changed
+1251 -20

No files matched your search

+4 -2
View File
@@ -45,6 +45,7 @@ import { NexusMoritaniBetrayal } from './nexus-moritani-betrayal';
import { buildRevision, buildRevisionLabel } from '@/lib/build-revision';
import { NexusEcazBetrayal } from './nexus-ecaz-betrayal';
import { NexusFremenBetrayal } from './nexus-fremen-betrayal';
import { NexusFremenRideBetrayal } from './nexus-fremen-ride-betrayal';
import { NexusEcazInquiry } from './nexus-ecaz-inquiry';
import { NexusEcazDuke } from './nexus-ecaz-duke';
import { NexusTleilaxu } from './nexus-tleilaxu';
@@ -347,8 +348,8 @@ export function GameTable({
const harkonnenBetrayalOwnsControls = !!g.nexusHarkonnenBetrayalReaction &&
!g.response && !g.decision && !g.truthtrance && !g.phaseOpening &&
!g.automaticContinuationPending && !g.nexusCards?.waiting.length && !g.nexusTraitors?.pending;
const busy = reactionBusy || g.automaticContinuationPending || !!g.semutaReaction || !!g.kullReaction || !!g.richeseBetrayalReaction || !!g.guildBetrayalReaction || !!g.nexusIxianReplacement || ixianBetrayalOwnsControls || harkonnenBetrayalOwnsControls;
const reactionOwnsControls = !!g.kullReaction || !!g.kullCounterEvent || !!g.richeseBetrayalReaction || !!g.guildBetrayalReaction || !!g.nexusIxianReplacement || ixianBetrayalOwnsControls || harkonnenBetrayalOwnsControls;
const busy = reactionBusy || !!g.nexusFremenRideBetrayal || g.automaticContinuationPending || !!g.semutaReaction || !!g.kullReaction || !!g.richeseBetrayalReaction || !!g.guildBetrayalReaction || !!g.nexusIxianReplacement || ixianBetrayalOwnsControls || harkonnenBetrayalOwnsControls;
const reactionOwnsControls = !!g.nexusFremenRideBetrayal || !!g.kullReaction || !!g.kullCounterEvent || !!g.richeseBetrayalReaction || !!g.guildBetrayalReaction || !!g.nexusIxianReplacement || ixianBetrayalOwnsControls || harkonnenBetrayalOwnsControls;
const canSpendKarama = (card: Card, purpose: KullPurpose) =>
!kullPurposeBlock(g, purpose) && canUseAsKaramaRole(g, me, card);
const canActivateKarama = (card: Card) => canSpendKarama(card, kullSpecialPurpose(me.faction));
@@ -1974,6 +1975,7 @@ export function GameTable({
</div>
{g.battle?.revealed && <a className="battle-display-link" href="#revealed-battle-plans">Compare revealed battle plans</a>}
<PrivateBattlePlan game={g} />
<NexusFremenRideBetrayal game={g} act={act} busy={reactionBusy} />
<HomeworldEcazDuke game={g} act={act} busy={reactionBusy || !!g.roomControl?.paused || !!g.roomControl?.closed || !!me.autopilot} />
<MovementPromises game={g} act={act} busy={reactionBusy} />
{!reactionOwnsControls && g.semutaReaction && <SemutaReaction game={g} act={act} busy={reactionBusy} />}
+1 -1
View File
@@ -18,7 +18,7 @@ export function NexusFremenBetrayal({ game, act, busy }: Props) {
<p>{nexusCardReference('fremen').betrayal}</p>
{fremen && <p className="fine">
{offer.mode === 'worm'
? `Declare before the first spice blow: ${fremen.name} cannot take ordinary or Cunning worm rides this turn. Worm destruction, survival, protection and Nexus still resolve.`
? `Declare before the first spice blow, or respond at an actual before-ride opportunity: ${fremen.name} cannot take remaining ordinary or Cunning worm rides this turn. Worm destruction, survival, protection and Nexus still resolve.`
: `This use suppresses ${fremen.name}’s native two-territory movement advantage for the rest of this turn. Independent ornithopters remain available.`}
</p>}
{offer.mode === 'worm' && game.discoveries && <p className="fine">
+74
View File
@@ -0,0 +1,74 @@
'use client';
import type { Action, GameView } from '@/game/engine';
import { territory } from '@/game/board';
import { Button } from './ui/button';
export function NexusFremenRideBetrayal({ game, act, busy }: {
game: GameView;
act: (action: Action) => void;
busy: boolean;
}) {
const reaction = game.nexusFremenRideBetrayal;
if (!reaction) return null;
const source = reaction.source;
const rider = game.players.find(player => player.id === source.player)?.name ?? 'The rider';
const responder = game.players.find(player => player.id === reaction.player)?.name ?? 'The current responder';
const hasPassed = reaction.passed.includes(game.me);
const canRespond = reaction.player === game.me && !hasPassed;
const disabled = busy || game.status !== 'playing';
return (
<section className="notice min-w-0 space-y-3" aria-label="Fremen Nexus before-ride Betrayal">
<h3>Fremen Nexus Betrayal · before a ride</h3>
<p>
{source.kind === 'wormRide'
? `${rider} has a worm ride from ${territory(source.territory).name} waiting.`
: source.kind === 'nexusFremenCunningRide'
? `${rider} has a Fremen Nexus Cunning ride waiting.`
: `${rider} has a Great Maker ride waiting.`}
</p>
<p className="fine">
This before-ride response is offered without revealing anyone’s secret
Nexus identity. Continue to allow the original ride choices, or use an
eligible Fremen Nexus to prevent the remaining rides this turn.
</p>
<p className="fine">
The worm, Spice Blow and any Nexus that have already resolved stay
resolved. Prevention does not move or destroy forces. Previous payments
and effects are not repeated.
</p>
{!canRespond ? (
<p aria-live="polite">
{hasPassed
? 'You have continued. Waiting for the remaining ride responses.'
: `Waiting for ${responder} to respond before the ride.`}
</p>
) : (
<div className="flex flex-wrap gap-2">
<Button
data-testid="nexus-fremen-ride-continue"
className="game-action min-h-11 whitespace-normal"
disabled={disabled}
onClick={() => {
if (!disabled) act({ type: 'decision', event: reaction.event, use: false });
}}
>
Continue · allow rides
</Button>
{reaction.canUse && (
<Button
data-testid="nexus-fremen-ride-prevent"
className="game-action min-h-11 whitespace-normal"
disabled={disabled}
onClick={() => {
if (!disabled) act({ type: 'decision', event: reaction.event, use: true });
}}
>
Use Fremen Nexus · prevent remaining rides
</Button>
)}
</div>
)}
</section>
);
}