Integrate Harkonnen Nexus traitor retirement and Mentat replacement

This commit is contained in:
Quintine committed 2026-10-02 02:12:18 +10:00
1 parent 0c25aad62f
commit 1cf8ff72c7
25 files changed
+3049 -8

No files matched your search

+27 -2
View File
@@ -83,6 +83,7 @@ import { NexusRicheseBetrayal } from './nexus-richese-betrayal';
import { NexusGuildBetrayal } from './nexus-guild-betrayal';
import { NexusIxianReplacement } from './nexus-ixian-replacement';
import { NexusIxianBetrayal } from './nexus-ixian-betrayal';
import { NexusHarkonnenBetrayal } from './nexus-harkonnen-betrayal';
import { OrnithopterMovement } from './ornithopter-movement';
import { DiscoveryOrnithopterMovement } from './discovery-flight-movement';
import { PlanetologistMovement } from './planetologist-movement';
@@ -310,8 +311,11 @@ export function GameTable({
const ixianBetrayalOwnsControls = !!g.nexusIxianBetrayalReaction &&
!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;
const reactionOwnsControls = !!g.kullReaction || !!g.kullCounterEvent || !!g.richeseBetrayalReaction || !!g.guildBetrayalReaction || !!g.nexusIxianReplacement || ixianBetrayalOwnsControls;
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 canActivateKarama = (card: Card) =>
!g.karamaBlocked && canUseAsKaramaRole(g, me, card);
const traitorBattle = g.battle;
@@ -790,6 +794,13 @@ export function GameTable({
const act = (a: Action) => {
// Control of one's own seat is independent of every gameplay response.
if (a.type === 'setAutopilot') { void send(a); return; }
const harkonnenReaction = g.nexusHarkonnenBetrayalReaction;
if (harkonnenReaction && harkonnenBetrayalOwnsControls) {
if (reactionBusy || g.status !== 'playing' || !harkonnenReaction.canPass || harkonnenReaction.hasPassed ||
(a.type !== 'nexusHarkonnenBetrayalPass' && a.type !== 'nexusHarkonnenBetrayalUse') ||
a.event !== harkonnenReaction.event ||
(a.type === 'nexusHarkonnenBetrayalUse' && (!harkonnenReaction.canUse || harkonnenReaction.blocked))) return;
}
const ixianReaction = g.nexusIxianBetrayalReaction;
if (ixianReaction && ixianBetrayalOwnsControls) {
if (reactionBusy || g.status !== 'playing' || !ixianReaction.canPass || ixianReaction.hasPassed ||
@@ -1042,6 +1053,8 @@ export function GameTable({
faction(g.players.find((p) => p.id === id)!.faction).name,
)
.join(' + ') || 'No winner'
: harkonnenBetrayalOwnsControls
? 'Declared Harkonnen traitor acknowledgement'
: ixianBetrayalOwnsControls
? 'Native Ixian advantage acknowledgement'
: g.guildBetrayalReaction
@@ -1117,6 +1130,16 @@ export function GameTable({
<a href="/rules?topic=nexus-ixian-betrayal#nexus-ixian-betrayal">Preview rules and limits</a>
</p>
)}
{g.nexusHarkonnenBetrayalPreview && (
<p className="notice" role="status">
Harkonnen Nexus Betrayal development preview · cancel an actually declared native Harkonnen
traitor call, return that physical card to the Traitor Deck, and draw one private replacement
automatically at this turn’s Mentat Pause. Classic Basic/Advanced, native Harkonnen, base
Treachery and Nexus only. Native allied Karama counters finish first. Not complete Harkonnen
or certified Nexus rules.{' '}
<a href="/rules?topic=nexus-harkonnen-betrayal#nexus-harkonnen-betrayal">Preview rules and limits</a>
</p>
)}
{g.status !== 'lobby' && (
<nav className="phase-track" aria-label="Turn phases">
{PHASES.map((p, i) => (
@@ -1765,6 +1788,8 @@ export function GameTable({
<NexusIxianReplacement game={g} act={act} busy={reactionBusy} />
) : ixianBetrayalOwnsControls ? (
<NexusIxianBetrayal game={g} act={act} busy={reactionBusy} />
) : harkonnenBetrayalOwnsControls ? (
<NexusHarkonnenBetrayal game={g} act={act} busy={reactionBusy} />
) : g.nexusCards?.waiting.length ? (
<p className="muted">The next phase begins when the remaining Nexus card choices are finished.</p>
) : g.truthtrance ? (
+75
View File
@@ -0,0 +1,75 @@
'use client';
import type { Action, GameView } from '@/game/engine';
import { nexusCardMode } from '@/game/nexus-cards';
import { faction } from '@/game/catalog';
import { leaderStrengthLabel } from '@/game/cards';
import { LeaderInspector } from './leader-inspector';
import { NexusCardFace } from './nexus-cards';
import { Button } from './ui/button';
export function NexusHarkonnenBetrayal({ game, act, busy }: {
game: GameView;
act: (action: Action) => void;
busy: boolean;
}) {
const reaction = game.nexusHarkonnenBetrayalReaction;
if (!reaction) return null;
const own = game.players.find((player) => player.id === game.me);
const name = (id: string) => game.players.find((player) => player.id === id)?.name ?? id;
const declaredTraitor = game.allLeaders.find((leader) => leader.id === reaction.identity);
const traitor = declaredTraitor?.name ?? reaction.identity;
const declaredLeader = game.allLeaders.find((leader) => leader.id === reaction.leader)?.name ?? reaction.leader;
const canRespond = !!reaction.event && reaction.canPass && !reaction.hasPassed;
const canUse = canRespond && reaction.canUse && !reaction.blocked;
const priorityBlocked = game.automaticContinuationPending || !!game.truthtrance ||
!!game.response || !!game.decision || !!game.phaseOpening ||
!!game.nexusCards?.waiting.length || !!game.nexusTraitors?.pending;
const disabled = busy || game.status !== 'playing' || game.phase !== 6 || priorityBlocked ||
!!game.roomControl?.paused || !!game.roomControl?.closed || !!own?.autopilot;
const card = game.nexusCards?.card;
const ownMode = card && own
? nexusCardMode(card, own.faction, game.players.map((player) => player.faction))
: undefined;
return (
<section aria-label="Declared Harkonnen traitor acknowledgement" data-testid="nexus-harkonnen-betrayal" className="notice min-w-0 space-y-3 break-words">
<h3>Declared Harkonnen traitor acknowledgement</h3>
<p>{name(reaction.provider)} has declared the {traitor} Traitor Card for {name(reaction.beneficiary)}
{' '}against {name(reaction.target)}’s {declaredLeader}. The traitor battle outcome has not happened.</p>
{declaredTraitor && (
<LeaderInspector
kind="traitor"
identity={{
id: declaredTraitor.id,
name: declaredTraitor.name,
factionName: faction(declaredTraitor.faction).name,
strength: leaderStrengthLabel(declaredTraitor),
}}
/>
)}
<p className="fine">Every publicly possible unallied responder acknowledges this declaration, regardless of their secret Nexus identity.
After all passes, the original traitor call continues once. Any native Karama counter finishes first;
this acknowledgement is not another Karama counter window.</p>
{card && <NexusCardFace card={card} mode={ownMode} />}
{canUse && <p className="fine">Spend your own Harkonnen Nexus to cancel this declared traitor call.
The declared Traitor Card is shuffled into the Traitor Deck immediately; Harkonnen draws one private
replacement automatically during this turn’s Mentat Pause. No replacement selection or confirmation is needed.
{' '}<a href="/rules?topic=nexus-harkonnen-betrayal#nexus-harkonnen-betrayal">Betrayal timing and preview limits</a>
</p>}
{!canRespond ? (
<p aria-live="polite">{reaction.hasPassed ? 'You have passed. Waiting for the remaining acknowledgements.' : 'Waiting for the required acknowledgements.'}</p>
) : <>
{reaction.blocked && <output className="fine block">{reaction.blocked}</output>}
<Button data-testid="nexus-harkonnen-betrayal-pass" className="game-action min-h-11 whitespace-normal" disabled={disabled}
onClick={() => { if (!disabled) act({ type: 'nexusHarkonnenBetrayalPass', event: reaction.event }); }}>
Pass · allow the declared traitor call
</Button>
{canUse && <Button data-testid="nexus-harkonnen-betrayal-use" className="game-action min-h-11 whitespace-normal" disabled={disabled}
onClick={() => { if (!disabled) act({ type: 'nexusHarkonnenBetrayalUse', event: reaction.event }); }}>
Use Harkonnen Nexus · cancel this traitor call
</Button>}
</>}
</section>
);
}