Prototype Jacurutu income, Testing Station and Shrine effects
Connect automatic unambiguous Jacurutu physical-force income, a sole-occupant Testing Station storm choice and Shrine reciprocal physical card roles to engine actions, player controls, legal AI and saved continuation. Preserve Weather Control exclusion, exact card/force custody, private hand boundaries and committed effects after restart. Shared role checks cover existing ordinary and special Karama paths; reserved cards cannot enter another pending use. Keep Orgiz, mixed-force Jacurutu allocation and contested Cistern/Testing Station benefits explicitly pending. These are bounded prototypes; complete Discovery/Advanced/expansion and publication gates remain closed. Validation: types, lint and 4,345 offline tests passed (32 added), production build passed, and 40 HTTP tests passed. Independent cross-reviews repaired reserved-card deadlock, physical-card selection, alternate-role permission, storm source/token binding and public-entitlement bot ordering. The first broad run found five failures; focused repairs preceded the stable repeated check. Browser A5EJQ2W4 used genuine Discovery setup/reveal/free entry with targeted QA positioning. A human increased storm movement from two to three sectors, moving sector 6 to 9. Refresh restored station forces and the private Snooper. This was targeted control/recovery evidence, not an unmodified full game. All 187 opening saved rooms remained unchanged; 31 QA/HTTP rooms were added (218 total). Reused the running server; no restart or save reset. Source-bound report: /home/quintine/.local/share/dune/checkpoints/20260913-discovery-location-effects/verification-final/report.json Source fingerprint: beecb85ab7ec39319115389c115434d161591e8549802c542a6343a0fc19bea6 Durations: check 140540ms; build 6741ms; integration 24956ms. Aggregate goal counter 63,116,444 to 64,295,091 at final verification: 1,178,647 tokens before final Git/push overhead for three bounded functions. Standing authorization: Push all from now on.
This commit is contained in:
1 parent
ab0ab52849
commit
f0d3d9c464
31 files changed
+2524
-138
No files matched your search
+52
-23
@@ -15,6 +15,7 @@ import { NexusTraitors } from './nexus-traitors';
|
||||
import { GreatMakerDecision } from './great-maker';
|
||||
import { DiscoveryPanel, DiscoveryDiscardDecision } from './discoveries';
|
||||
import { DiscoveryEntryDecision } from './discovery-entry';
|
||||
import { DiscoveryStormDecision } from './discovery-storm';
|
||||
import { NexusChoamTrade } from './nexus-choam-trade';
|
||||
import { NexusTleilaxu } from './nexus-tleilaxu';
|
||||
import { NexusSuboids } from './nexus-suboids';
|
||||
@@ -111,7 +112,10 @@ import {
|
||||
import { stoneBurnerPlanBlock } from '@/game/stone-burner';
|
||||
import { TECH_TOKENS, ownedTech } from '@/game/tech-tokens';
|
||||
import Link from 'next/link';
|
||||
import { canUseAsKarama } from '@/game/karama';
|
||||
import {
|
||||
canUseAsKaramaRole,
|
||||
canUseAsTruthtranceRole,
|
||||
} from '@/game/shrine';
|
||||
import { LeaderPortrait } from './leader-portrait';
|
||||
import { ShipmentQuote } from './shipment-quote';
|
||||
import { reserveShipmentCost } from '@/game/shipment-price';
|
||||
@@ -295,7 +299,7 @@ export function GameTable({
|
||||
bidDraft.auction === auctionKey ? bidDraft.value : minimumBid,
|
||||
);
|
||||
const maximumBid = me.hand?.some((c) =>
|
||||
canUseAsKarama(g.advanced, me.faction, c),
|
||||
canUseAsKaramaRole(g, me, c),
|
||||
)
|
||||
? Number.MAX_SAFE_INTEGER
|
||||
: (me.spice ?? 0) + g.aid.available;
|
||||
@@ -2213,6 +2217,8 @@ export function GameTable({
|
||||
.kind ===
|
||||
'discoveryEntry'
|
||||
? 'Enter a Discovery location'
|
||||
: g.decision.kind === 'ecologicalStorm'
|
||||
? 'Ecological Testing Station'
|
||||
: g
|
||||
.decision
|
||||
.kind ===
|
||||
@@ -2408,7 +2414,7 @@ export function GameTable({
|
||||
<p role="status">{g.revival.specialKaramaBlock}</p>
|
||||
)}
|
||||
{me.hand
|
||||
?.filter((c) => c.effect === 'karama')
|
||||
?.filter((c) => canUseAsKaramaRole(g, me, c))
|
||||
.map((c) => (
|
||||
<div key={c.id}>
|
||||
{actionButton(
|
||||
@@ -2507,7 +2513,7 @@ export function GameTable({
|
||||
</p>
|
||||
<HelpTip topic="fullPlan" />
|
||||
{me.hand
|
||||
?.filter((c) => c.effect === 'karama')
|
||||
?.filter((c) => canUseAsKaramaRole(g, me, c))
|
||||
.flatMap((c) =>
|
||||
[g.battle!.attacker, g.battle!.defender].map((id) => (
|
||||
<div key={`${c.id}-${id}`}>
|
||||
@@ -2540,7 +2546,7 @@ export function GameTable({
|
||||
</p>
|
||||
<HelpTip topic="guildShipment" />
|
||||
{actionButton('Allow Homeworld shipment', { type: 'decision', event: g.decision.event, allow: true })}
|
||||
{me.hand?.filter((c) => c.effect === 'karama').map((c) => (
|
||||
{me.hand?.filter((c) => canUseAsKaramaRole(g, me, c)).map((c) => (
|
||||
<div key={c.id}>{actionButton('Spend Karama · stop shipment', { type: 'card', mode: 'special', card: c.id })}</div>
|
||||
))}
|
||||
</>
|
||||
@@ -2567,7 +2573,7 @@ export function GameTable({
|
||||
allow: true,
|
||||
})}
|
||||
{me.hand
|
||||
?.filter((c) => c.effect === 'karama')
|
||||
?.filter((c) => canUseAsKaramaRole(g, me, c))
|
||||
.map((c) => (
|
||||
<div key={c.id}>
|
||||
{actionButton('Spend Karama · stop shipment', {
|
||||
@@ -2872,7 +2878,7 @@ export function GameTable({
|
||||
(g.auction?.bid ?? 0) > (me.spice ?? 0) + g.aid.available,
|
||||
)}
|
||||
{me.hand
|
||||
?.filter((c) => canUseAsKarama(g.advanced, me.faction, c))
|
||||
?.filter((c) => canUseAsKaramaRole(g, me, c))
|
||||
.map((c) => (
|
||||
<div key={c.id}>
|
||||
{actionButton(
|
||||
@@ -2950,6 +2956,8 @@ export function GameTable({
|
||||
</>
|
||||
) : g.decision.kind === 'discoveryEntry' ? (
|
||||
<DiscoveryEntryDecision game={g} act={act} busy={transportBusy || !!me.autopilot} />
|
||||
) : g.decision.kind === 'ecologicalStorm' ? (
|
||||
<DiscoveryStormDecision game={g} act={act} busy={transportBusy || !!me.autopilot} />
|
||||
) : g.decision.kind === 'discoveryDiscard' ? (
|
||||
<DiscoveryDiscardDecision game={g} act={act} busy={transportBusy || !!me.autopilot} />
|
||||
) : g.decision.kind === 'greatMakerVote' || g.decision.kind === 'greatMakerRide' ? (
|
||||
@@ -3694,7 +3702,7 @@ export function GameTable({
|
||||
g.mobileStronghold?.location &&
|
||||
(me.forces['hidden_mobile_stronghold:0'] ?? 0) > 0 &&
|
||||
me.hand
|
||||
?.filter((c) => c.effect === 'karama')
|
||||
?.filter((c) => canUseAsKaramaRole(g, me, c))
|
||||
.slice(0, 1)
|
||||
.map((c) => (
|
||||
<details key={c.id}>
|
||||
@@ -4607,7 +4615,7 @@ export function GameTable({
|
||||
: richese?.card.effect === 'nullentropyBox'
|
||||
? 'Use the Nullentropy Box panel above to begin the paid search.'
|
||||
: richeseCardActionBlock(c)) ??
|
||||
(canUseAsKarama(g.advanced, me.faction, c)
|
||||
(canUseAsKaramaRole(g, me, c)
|
||||
? g.response
|
||||
? !g.responseControls?.cancelCards.includes(c.id)
|
||||
? 'This card cannot cancel the current power.'
|
||||
@@ -4705,9 +4713,12 @@ export function GameTable({
|
||||
? '◇'
|
||||
: '†'}
|
||||
</span>
|
||||
{c.effect === 'truthtrance' && (
|
||||
{canUseAsTruthtranceRole(g, me, c) && (
|
||||
<>
|
||||
<p className="fine">
|
||||
{c.effect === 'karama'
|
||||
? 'Shrine lets you use this physical Karama as Truthtrance while you occupy it. '
|
||||
: ''}
|
||||
Ask another player a public yes/no question, including
|
||||
during another player’s decision. Competing plays
|
||||
follow storm order.
|
||||
@@ -4719,12 +4730,22 @@ export function GameTable({
|
||||
!!g.truthtrance ||
|
||||
!['setup', 'playing'].includes(g.status)
|
||||
}
|
||||
onClick={() => act({ type: 'card', card: c.id })}
|
||||
onClick={() =>
|
||||
act({
|
||||
type: 'card',
|
||||
card: c.id,
|
||||
...(c.effect === 'karama'
|
||||
? { shrineTruthtrance: [c.id] }
|
||||
: {}),
|
||||
})
|
||||
}
|
||||
>
|
||||
Declare Truthtrance
|
||||
{c.effect === 'karama'
|
||||
? 'Use Karama as Truthtrance'
|
||||
: 'Declare Truthtrance'}
|
||||
</Button>
|
||||
{(me.hand?.filter(
|
||||
(other) => other.effect === 'truthtrance',
|
||||
(other) => canUseAsTruthtranceRole(g, me, other),
|
||||
).length ?? 0) > 1 && (
|
||||
<Button
|
||||
variant="outline"
|
||||
@@ -4740,10 +4761,17 @@ export function GameTable({
|
||||
cards: me
|
||||
.hand!.filter(
|
||||
(other) =>
|
||||
other.effect === 'truthtrance' &&
|
||||
canUseAsTruthtranceRole(g, me, other) &&
|
||||
other.id !== c.id,
|
||||
)
|
||||
.map((other) => other.id),
|
||||
shrineTruthtrance: me
|
||||
.hand!.filter(
|
||||
(other) =>
|
||||
canUseAsTruthtranceRole(g, me, other) &&
|
||||
other.effect === 'karama',
|
||||
)
|
||||
.map((other) => other.id),
|
||||
})
|
||||
}
|
||||
>
|
||||
@@ -4819,7 +4847,7 @@ export function GameTable({
|
||||
</>
|
||||
)}
|
||||
{g.advanced &&
|
||||
c.effect === 'karama' &&
|
||||
canUseAsKaramaRole(g, me, c) &&
|
||||
!me.specialKaramaUsed &&
|
||||
me.faction === 'harkonnen' &&
|
||||
g.phase === 3 && (
|
||||
@@ -4875,7 +4903,7 @@ export function GameTable({
|
||||
</>
|
||||
)}
|
||||
{g.advanced &&
|
||||
c.effect === 'karama' &&
|
||||
canUseAsKaramaRole(g, me, c) &&
|
||||
!me.specialKaramaUsed &&
|
||||
me.faction === 'fremen' &&
|
||||
g.phase === 1 && (
|
||||
@@ -4926,7 +4954,7 @@ export function GameTable({
|
||||
</>
|
||||
)}
|
||||
{g.advanced &&
|
||||
c.effect === 'karama' &&
|
||||
canUseAsKaramaRole(g, me, c) &&
|
||||
!me.specialKaramaUsed &&
|
||||
me.faction === 'emperor' &&
|
||||
g.phase === 4 && (
|
||||
@@ -5007,7 +5035,7 @@ export function GameTable({
|
||||
/>
|
||||
</label>
|
||||
)}
|
||||
{canUseAsKarama(g.advanced, me.faction, c) &&
|
||||
{canUseAsKaramaRole(g, me, c) &&
|
||||
g.phase === 5 &&
|
||||
g.active &&
|
||||
(g.active === me.id ? shipmentAvailable : !g.players.find((p) => p.id === g.active)?.shipped) && (
|
||||
@@ -5033,9 +5061,10 @@ export function GameTable({
|
||||
{g.players.find((p) => p.id === g.active)?.name}
|
||||
</Button>
|
||||
)}
|
||||
{c.effect !== 'truthtrance' &&
|
||||
{(c.effect !== 'truthtrance' ||
|
||||
canUseAsKaramaRole(g, me, c)) &&
|
||||
(c.kind === 'special' ||
|
||||
canUseAsKarama(g.advanced, me.faction, c)) && (
|
||||
canUseAsKaramaRole(g, me, c)) && (
|
||||
<Button
|
||||
variant="outline"
|
||||
aria-describedby={
|
||||
@@ -5052,7 +5081,7 @@ export function GameTable({
|
||||
!(
|
||||
g.status === 'setup' &&
|
||||
g.response &&
|
||||
canUseAsKarama(g.advanced, me.faction, c)
|
||||
canUseAsKaramaRole(g, me, c)
|
||||
)) ||
|
||||
(!!g.phaseOpening &&
|
||||
(c.effect !== 'amal' ||
|
||||
@@ -5068,7 +5097,7 @@ export function GameTable({
|
||||
type: 'card',
|
||||
elite: eliteAmount,
|
||||
card: c.id,
|
||||
mode: canUseAsKarama(g.advanced, me.faction, c)
|
||||
mode: canUseAsKaramaRole(g, me, c)
|
||||
? g.response
|
||||
? 'cancel'
|
||||
: 'purchase'
|
||||
@@ -5083,7 +5112,7 @@ export function GameTable({
|
||||
})
|
||||
}
|
||||
>
|
||||
{canUseAsKarama(g.advanced, me.faction, c)
|
||||
{canUseAsKaramaRole(g, me, c)
|
||||
? g.response
|
||||
? 'Cancel this power'
|
||||
: 'Take current auction card'
|
||||
|
||||
Reference in new issue
Block a user