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
@@ -29,9 +29,9 @@ export function ChoamCashIn({
|
||||
<label>
|
||||
Karama to spend{' '}
|
||||
<select value={card.id} onChange={(e) => setActivation(e.target.value)}>
|
||||
{cash.karamas.map((c, i) => (
|
||||
{cash.karamas.map((c) => (
|
||||
<option key={c.id} value={c.id}>
|
||||
Karama {i + 1}
|
||||
{c.name}
|
||||
</option>
|
||||
))}
|
||||
</select>
|
||||
|
||||
@@ -92,7 +92,7 @@ export function DiscoveryPanel({ game, act, busy }: Props) {
|
||||
);
|
||||
})}
|
||||
</div>
|
||||
<p className="fine">Inspection and reveal are optional. Next-turn free entry, sole-occupant Cistern income and later-turn Ornithopter movement are available in the development prototype. Jacurutu battle income, Ecological Testing Station, Shrine and Orgiz abilities remain in development.</p>
|
||||
<p className="fine">Inspection and reveal are optional. Next-turn free entry, sole-occupant Cistern income and later-turn Ornithopter movement are available in the development prototype. Jacurutu income, Testing Station storm choices and Shrine card conversion are prototyped. Orgiz, mixed-force Jacurutu rewards and contested Cistern/Testing Station benefits remain pending.</p>
|
||||
</section>
|
||||
);
|
||||
}
|
||||
|
||||
@@ -0,0 +1,53 @@
|
||||
'use client';
|
||||
|
||||
import type { Action, GameView } from '@/game/engine';
|
||||
import { discoveryStormActions } from '@/game/discovery-storm-options';
|
||||
import { Button } from './ui/button';
|
||||
|
||||
export function DiscoveryStormDecision({
|
||||
game,
|
||||
act,
|
||||
busy,
|
||||
}: {
|
||||
game: GameView;
|
||||
act: (action: Action) => void;
|
||||
busy: boolean;
|
||||
}) {
|
||||
const actions = discoveryStormActions(game),
|
||||
offer = game.ecologicalStorm;
|
||||
if (!offer || !actions.length) return null;
|
||||
const me = game.players.find((player) => player.id === game.me)!;
|
||||
return (
|
||||
<section aria-label="Ecological Testing Station" className="space-y-3">
|
||||
<p>
|
||||
Your forces occupy Ecological Testing Station. The storm will move{' '}
|
||||
{offer.base} sectors. You may decrease or increase this by one, or keep
|
||||
the ordinary movement.
|
||||
</p>
|
||||
<p className="fine">This choice cannot change Weather Control.</p>
|
||||
<div className="flex flex-wrap gap-2">
|
||||
{offer.options.map((option) => {
|
||||
const action = actions.find(
|
||||
(candidate) => candidate.delta === option.delta,
|
||||
)!;
|
||||
return (
|
||||
<Button
|
||||
key={option.delta}
|
||||
className="game-action"
|
||||
variant={option.delta === 0 ? 'outline' : 'default'}
|
||||
disabled={busy || !!me.autopilot}
|
||||
onClick={() => act(action)}
|
||||
>
|
||||
{option.delta === -1
|
||||
? 'Decrease'
|
||||
: option.delta === 1
|
||||
? 'Increase'
|
||||
: 'Keep'}
|
||||
: {option.distance} {option.distance === 1 ? 'sector' : 'sectors'}
|
||||
</Button>
|
||||
);
|
||||
})}
|
||||
</div>
|
||||
</section>
|
||||
);
|
||||
}
|
||||
+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'
|
||||
|
||||
+27
-18
@@ -16,6 +16,7 @@ import {
|
||||
type TruthQuestion,
|
||||
} from '@/game/truthtrance';
|
||||
import type { Action, GameView } from '@/game/engine';
|
||||
import { canUseAsTruthtranceRole } from '@/game/shrine';
|
||||
|
||||
export function Truthtrance({
|
||||
game: g,
|
||||
@@ -29,6 +30,20 @@ export function Truthtrance({
|
||||
const w = g.truthtrance!;
|
||||
const me = g.players.find((p) => p.id === g.me)!;
|
||||
const current = w.queue[0];
|
||||
const truthCards =
|
||||
me.hand?.filter((card) => canUseAsTruthtranceRole(g, me, card)) ?? [];
|
||||
const declaration = (cards: typeof truthCards): Action => ({
|
||||
type: 'card',
|
||||
card: cards[0].id,
|
||||
...(cards.length > 1 ? { cards: cards.slice(1).map((card) => card.id) } : {}),
|
||||
...(cards.some((card) => card.effect === 'karama')
|
||||
? {
|
||||
shrineTruthtrance: cards
|
||||
.filter((card) => card.effect === 'karama')
|
||||
.map((card) => card.id),
|
||||
}
|
||||
: {}),
|
||||
});
|
||||
const [target, setTarget] = useState(
|
||||
g.players.find((p) => p.id !== me.id)!.id,
|
||||
);
|
||||
@@ -135,27 +150,21 @@ export function Truthtrance({
|
||||
</ul>
|
||||
{!w.passed.includes(me.id) && (
|
||||
<>
|
||||
{me.hand
|
||||
?.filter((c) => c.effect === 'truthtrance')
|
||||
.slice(0, 1)
|
||||
.map((c) => (
|
||||
{truthCards.map((c) => (
|
||||
<div key={c.id}>
|
||||
{button('Declare my Truthtrance', {
|
||||
type: 'card',
|
||||
card: c.id,
|
||||
})}
|
||||
{button(
|
||||
c.effect === 'karama'
|
||||
? 'Use my Karama as Truthtrance'
|
||||
: 'Declare my Truthtrance',
|
||||
declaration([c]),
|
||||
)}
|
||||
</div>
|
||||
))}
|
||||
{(me.hand?.filter((c) => c.effect === 'truthtrance').length ??
|
||||
0) > 1 &&
|
||||
button('Declare both Truthtrances', {
|
||||
type: 'card',
|
||||
card: me.hand!.find((c) => c.effect === 'truthtrance')!.id,
|
||||
cards: me
|
||||
.hand!.filter((c) => c.effect === 'truthtrance')
|
||||
.slice(1)
|
||||
.map((c) => c.id),
|
||||
})}
|
||||
{truthCards.length > 1 &&
|
||||
button(
|
||||
`Declare all ${truthCards.length} as Truthtrance`,
|
||||
declaration(truthCards),
|
||||
)}
|
||||
{button('Pass Truthtrance priority', { type: 'truthPass' })}
|
||||
</>
|
||||
)}
|
||||
|
||||
Reference in new issue
Block a user