Keep Atreides special inspection available until plan reveal
Allow native Advanced use after declining the first offer, including a sealed target. Reuse projected card/target permission in persistent controls and minimal bots; retain sealed plans and finish target-last public reveal once. Source: authorized supplied physical27 F. Ordinary preparation priority and target-first ordering remain provisional; no new reading acknowledgement. Verified: 11 affected offline files126/126, types/lint, production build, five actual control/gameplay/GameTable SSR journeys and eight legal profile paths. No saved-room, D1, reset, restart or deployment operation.
This commit is contained in:
1 parent
660b0a0c25
commit
61828150b9
15 files changed
+917
-70
No files matched your search
@@ -0,0 +1,50 @@
|
||||
import type { FactionId } from './catalog';
|
||||
import type { Action, GameView } from './engine';
|
||||
|
||||
/** Timing needs only public commitment status, never the hidden plan contents. */
|
||||
export type AtreidesFullPlanContext = {
|
||||
status: string;
|
||||
phase: number;
|
||||
advanced: boolean;
|
||||
owner: { id: string; faction: FactionId; specialKaramaUsed?: boolean };
|
||||
blocked?: string | null;
|
||||
battle: null | {
|
||||
attacker: string;
|
||||
defender: string;
|
||||
revealed: boolean;
|
||||
preparing: boolean;
|
||||
fullPlan: boolean;
|
||||
};
|
||||
};
|
||||
|
||||
/** Supplied revised rules, physical p27 F: before Battle Plans are revealed.
|
||||
* The original offer is a convenience, not a deadline. A sealed plan stays fixed.
|
||||
* Voice/ordinary Prescience keep their existing preparation priority.
|
||||
*/
|
||||
export function quoteAtreidesFullPlan(context: AtreidesFullPlanContext) {
|
||||
const { owner, battle } = context;
|
||||
let blocked: string | null = null;
|
||||
if (!context.advanced) blocked = 'Special Karama powers require the Advanced game.';
|
||||
else if (owner.faction !== 'atreides') blocked = 'Only native Atreides may use this special Karama.';
|
||||
else if (context.status !== 'playing' || context.phase !== 6 || !battle)
|
||||
blocked = 'Use special prescience during a battle before plans are revealed.';
|
||||
else if (owner.specialKaramaUsed || battle.fullPlan)
|
||||
blocked = 'Your special Karama power has already been used this game.';
|
||||
else if (battle.revealed) blocked = 'The Battle Plans have already been revealed.';
|
||||
else if (battle.preparing)
|
||||
blocked = 'Finish battle preparation before using special prescience.';
|
||||
else if (context.blocked) blocked = context.blocked;
|
||||
return { blocked, targets: battle ? [battle.attacker, battle.defender] : [] };
|
||||
}
|
||||
|
||||
/** Reuse the existing own/allied inspection preference, without strategy tuning. */
|
||||
export function atreidesFullPlanAction(game: GameView): Action | null {
|
||||
const offer = game.atreidesFullPlan;
|
||||
if (!offer || offer.blocked || !offer.cards.length) return null;
|
||||
const me = game.players.find(player => player.id === game.me);
|
||||
if (!me) return null;
|
||||
const beneficiary = offer.targets.includes(me.id) ? me.id : me.ally;
|
||||
if (!beneficiary || !offer.targets.includes(beneficiary)) return null;
|
||||
const target = offer.targets.find(player => player !== beneficiary);
|
||||
return target ? { type: 'card', mode: 'special', card: offer.cards[0], target } : null;
|
||||
}
|
||||
+4
-18
@@ -1,4 +1,5 @@
|
||||
import { discoveryBotActions } from './discovery-options';
|
||||
import { atreidesFullPlanAction } from './atreides-full-plan';
|
||||
import { liveMovementPromises } from './movement-promises';
|
||||
import { ecazOccupyDialChoices, ecazOccupyOwnProfile, ecazOccupyPolicyActions } from './ecaz-occupy-options';
|
||||
import { quoteEcazOccupyDial } from './ecaz-occupy-battle';
|
||||
@@ -2581,24 +2582,7 @@ function policyActions(g: GameView): Action[] {
|
||||
];
|
||||
}
|
||||
if (d.kind === 'fullPlanOffer') {
|
||||
const b = g.battle!;
|
||||
const beneficiary = [b.attacker, b.defender].includes(me.id)
|
||||
? me.id
|
||||
: me.ally;
|
||||
if (
|
||||
specialCard &&
|
||||
beneficiary &&
|
||||
[b.attacker, b.defender].includes(beneficiary)
|
||||
)
|
||||
return [
|
||||
{
|
||||
type: 'card',
|
||||
mode: 'special',
|
||||
card: specialCard.id,
|
||||
target: b.attacker === beneficiary ? b.defender : b.attacker,
|
||||
},
|
||||
];
|
||||
return [{ type: 'decision', decline: true }];
|
||||
return [atreidesFullPlanAction(g) ?? { type: 'decision', decline: true }];
|
||||
}
|
||||
// A legacy inspection gate is cleared by authoritative normalization.
|
||||
if (d.kind === 'fullPlanRead') return [];
|
||||
@@ -3801,6 +3785,8 @@ function policyActions(g: GameView): Action[] {
|
||||
}
|
||||
if (g.phase === 6) {
|
||||
const b = g.battle;
|
||||
const inspection = atreidesFullPlanAction(g);
|
||||
if (inspection) return [inspection];
|
||||
if (b?.outcomeWindow && !b.outcomeWindow.closed) {
|
||||
if (![b.attacker, b.defender].includes(me.id) || b.outcomeWindow.ready.includes(me.id)) return [];
|
||||
return [{type: 'battleOutcomeReady', event: b.outcomeWindow.event}];
|
||||
|
||||
+37
-15
@@ -10,6 +10,7 @@ import { independentDiscoveryProfile, independentNexusProfile, nexusCoreProfile,
|
||||
import { classicDiscoveryLeaderSkillsProfile, classicDiscoveryNexusProfile, homeworldDiscoveryProfile, discoveryModeSupported, discoveryModuleProfile, mixedE1E2DiscoveryNexusProfile, mixedE3DiscoveryNexusProfile, pairedDiscoveryNexusProfile, pairedE3DiscoveryNexusProfile, singleE1E2DiscoveryNexusProfile, standaloneE3DiscoveryNexusProfile } from './discovery-module-profile';
|
||||
import { advancedMoritaniLeaderSkillsProfile, selectedDeckLeaderSkillsProfile, homeworldLeaderSkillsProfile, independentTreacheryLeaderSkillsProfile, classicNexusLeaderSkillsProfile, classicTechLeaderSkillsProfile, guildBetrayalNexusLeaderSkillsProfile, mixedE1E2NexusLeaderSkillsProfile, mixedE3NexusLeaderSkillsProfile, nativeDiscoveryLeaderSkillsProfile, nativeEcazLeaderSkillsProfile, nativeExpansionLeaderSkillsProfile, nativeTechLeaderSkillsProfile, ordinaryLeaderSkillModeSupported, pairedE3LeaderSkillsProfile, pairedE3NexusLeaderSkillsProfile, pairedNexusLeaderSkillsProfile, singleE1E2NexusLeaderSkillsProfile, standaloneE3NexusLeaderSkillsProfile, strongholdLeaderSkillsProfile } from './leader-skill-profile';
|
||||
import { bribeTimingBlock, maximumBribe, type BribeOptions } from './bribe-options';
|
||||
import { quoteAtreidesFullPlan } from './atreides-full-plan';
|
||||
import { quoteSpicePlacement, stormExposesTerritory, stormSectorAfter, wormConsumesForces } from './disaster-rules';
|
||||
import { isStormCardDistance, type StormCardComponent } from './storm-cards';
|
||||
import { createLeaderSkills, validateLeaderSkills, dealLeaderSkills, chooseLeaderSkill, returnDeadLeaderSkills, offerRevivedLeaderSkill, drawRevivedLeaderSkills, declineRevivedLeaderSkill, LeaderSkillError, type LeaderSkillsState, type LeaderSkillsView } from './leader-skills';
|
||||
@@ -15347,9 +15348,13 @@ function auctionSpicePaymentFunded(g: Game, p: Player) {
|
||||
}
|
||||
function recordFullPlanInspection(g: Game, owner: string) {
|
||||
const p = getPlayer(g, owner);
|
||||
const b = g.battle;
|
||||
const bothSealed = b && b.plans[b.attacker] && b.plans[b.defender];
|
||||
log(
|
||||
g,
|
||||
`${p.name} can now inspect the committed battle plan privately. The plan remains available until both plans are revealed.`,
|
||||
bothSealed
|
||||
? `${p.name}'s inspection target has committed. Both plans reveal immediately; existing sealed plans cannot change.`
|
||||
: `${p.name} can now inspect the committed battle plan privately. The plan remains available until both plans are revealed.`,
|
||||
{ faction: p.faction, name: 'Full plan inspection' },
|
||||
);
|
||||
}
|
||||
@@ -30774,6 +30779,31 @@ function actualSpecialKaramaCards(g: Game, p: Player) {
|
||||
return p.hand.filter(card => card.kind === 'special' && card.effect === 'karama' &&
|
||||
!karamaSpendingBlock(g, p, card, kullSpecialPurpose(p.faction)));
|
||||
}
|
||||
function atreidesFullPlanTiming(g: Game, p: Player) {
|
||||
const b = g.battle;
|
||||
const ownOffer = g.decision?.kind === 'fullPlanOffer' && g.decision.player === p.id;
|
||||
return quoteAtreidesFullPlan({
|
||||
status: g.status, phase: g.phase, advanced: g.advanced, owner: p,
|
||||
blocked: g.truthtrance || g.response || (g.decision && !ownOffer) ||
|
||||
g.phaseOpening || g.pendingKarama || g.pendingRevival || g.choamMarket ||
|
||||
g.pendingTreacheryDiscard || g.pendingNullentropy
|
||||
? 'Resolve the current response, decision or card effect before special prescience.' : null,
|
||||
battle: b ? { attacker: b.attacker, defender: b.defender, revealed: b.revealed,
|
||||
// Preserve the existing special offer before shared pre-leader readiness;
|
||||
// that separate opportunity still guards every actual plan commitment.
|
||||
preparing: !!b.preparation,
|
||||
fullPlan: !!b.fullPlan } : null,
|
||||
});
|
||||
}
|
||||
function atreidesFullPlanView(g: Game, p: Player) {
|
||||
if (g.status !== 'playing' || !g.advanced || p.faction !== 'atreides' ||
|
||||
g.phase !== 6 || !g.battle) return null;
|
||||
const quote = atreidesFullPlanTiming(g, p);
|
||||
const cards = p.hand.filter(card => canUseAsKaramaRole(g, p, card) &&
|
||||
!karamaSpendingBlock(g, p, card, kullSpecialPurpose(p.faction))).map(card => card.id);
|
||||
return { ...quote, cards,
|
||||
blocked: quote.blocked ?? (cards.length ? null : 'A spendable Karama card is required.') };
|
||||
}
|
||||
function ecazSpecialKaramaContext(g: Game, p: Player) {
|
||||
const b = g.battle;
|
||||
return { status: g.status, phase: g.phase, advanced: g.advanced, owner: p,
|
||||
@@ -31036,21 +31066,12 @@ export function prepareSpecialKaramaIntent(
|
||||
);
|
||||
return { ...base, kind: 'fremen', territory: t };
|
||||
} else if (p.faction === 'atreides') {
|
||||
const b = g.battle;
|
||||
requireRule(
|
||||
g.phase === 6 &&
|
||||
!g.response &&
|
||||
g.decision?.kind === 'fullPlanOffer' &&
|
||||
g.decision.player === p.id &&
|
||||
b &&
|
||||
!b.revealed &&
|
||||
!b.preparation &&
|
||||
!Object.keys(b.plans).length,
|
||||
'Choose special prescience after battle preparation and before plans are sealed.',
|
||||
);
|
||||
const quote = atreidesFullPlanTiming(g, p);
|
||||
requireRule(!quote.blocked, quote.blocked ?? 'Special prescience is unavailable.');
|
||||
const b = g.battle!;
|
||||
const target = stringField(action.target);
|
||||
requireRule(
|
||||
[b.attacker, b.defender].includes(target),
|
||||
quote.targets.includes(target),
|
||||
'Choose a player in this battle.',
|
||||
);
|
||||
// Distinct special Karama inspects the sealed plan, not the token inventory.
|
||||
@@ -31425,6 +31446,7 @@ function resolveSpecialKaramaEffect(g: Game, p: Player, card: Card, intent: Spec
|
||||
g,
|
||||
`${p.name} used special Karama to inspect ${getPlayer(g, target).name}’s entire battle plan.`,
|
||||
);
|
||||
if (b.plans[target]) recordFullPlanInspection(g, p.id);
|
||||
} else if (intent.kind === 'guildHomeworld') {
|
||||
const shipper = getPlayer(g, intent.target);
|
||||
p.specialKaramaUsed = true;
|
||||
@@ -35831,7 +35853,6 @@ function applyActionInner(
|
||||
log(g, `${p.name} announced a Battle Plan without a leader or Cheap Hero; no battle cards can be played.`);
|
||||
if (b.fullPlan?.target === id) {
|
||||
recordFullPlanInspection(g, b.fullPlan.owner);
|
||||
return g;
|
||||
}
|
||||
if (b.plans[b.attacker] && b.plans[b.defender]) {
|
||||
for (const playerId of b.noFieldPlayers ?? []) {
|
||||
@@ -37147,6 +37168,7 @@ export function viewGame(state: Game, id: string) {
|
||||
}
|
||||
: null,
|
||||
richeseSpecialKarama: richeseSpecialKaramaView(g, me),
|
||||
atreidesFullPlan: atreidesFullPlanView(g, me),
|
||||
...(me.faction === 'ecaz' && g.advanced && g.battle
|
||||
? { ecazSpecialKarama: ecazSpecialKaramaView(g, me) } : {}),
|
||||
...(g.moritaniSpecialKarama?.source.owner === me.id && g.moritaniSpecialKarama.stage === 'offer'
|
||||
|
||||
+23
-1
@@ -76,7 +76,7 @@ export const HELP = {
|
||||
choamCombat:
|
||||
'CHOAM may reserve spice for an ally’s advanced battle support. The ally chooses the payment split when sealing. A traitor victory costs the winner no support; any revealed traitor prevents CHOAM force-payment income.',
|
||||
fullPlan:
|
||||
'Atreides may spend its Advanced special Karama once per game to inspect the full committed plan. Only Atreides receives dial, leader, actual played faces, support and KH while the other combatant plans; no inspection confirmation. The provisional No-Field distinction permits this separate special power while ordinary dial Prescience stays barred. A private read does not reveal or materialize the token before both plans seal.',
|
||||
'Atreides may spend its Advanced special Karama once per game after preparation and before public reveal, even after declining the first offer or one plan sealing. A sealed target is read immediately; an unsealed target commits first. Existing sealed plans cannot change. Only Atreides receives the private inspection; no confirmation. Both seals reveal immediately. The provisional No-Field distinction permits this separate special power while ordinary dial Prescience stays barred; private reading does not materialize its token.',
|
||||
guildShipment:
|
||||
'The Guild may spend its special Karama once per game to stop a declared off-planet shipment before payment or arrival. The shipper keeps its forces and spice and may still move. Fremen’s southern reinforcements are on-planet.',
|
||||
specialKarama:
|
||||
@@ -2716,6 +2716,28 @@ export const RULE_TOPICS: RuleTopic[] = [
|
||||
],
|
||||
related: ['revival'],
|
||||
},
|
||||
{
|
||||
id: 'atreides-full-plan',
|
||||
title: 'Atreides: special full Battle Plan inspection',
|
||||
category: 'Factions',
|
||||
coverage: 'Partial',
|
||||
developmentStage: 'Prototyped',
|
||||
summary: 'Spend the native Advanced special Karama before public revelation, including after one plan seals.',
|
||||
steps: [
|
||||
'After battle preparation, spend a projected available Karama and select either combatant. Keeping the power at the initial offer preserves later use before revelation, without opening another mandatory offer.',
|
||||
'If the target has sealed, inspect the original full plan immediately. Otherwise that target commits next. Only Atreides receives the plan and selected card faces; an ally gains no automatic access.',
|
||||
'Already sealed plans stay fixed. If you have already sealed, the other combatant’s last commitment reveals both plans immediately. The special power grants no replanning, payment refund or separate reading pause.',
|
||||
'Preparation order and target-first sequencing remain provisional. The distinct No-Field permission retains ordinary dial Prescience’s prohibition and does not materialize the marker during private inspection.',
|
||||
],
|
||||
checklist: [
|
||||
{ area: 'Implementation', status: 'Partial', detail: 'Shared before-reveal timing replaces the initial-offer deadline; sealed target reading and target-last public reveal preserve original commitments.', evidence: ['game/atreides-full-plan.ts', 'game/engine.ts'] },
|
||||
{ area: 'Player controls', status: 'Partial', detail: 'Initial and persistent later controls share projected spendable physical cards and combatant targets, with unavailable and own-sealed guidance.', evidence: ['components/atreides-full-plan.tsx', 'components/game-table.tsx'] },
|
||||
{ area: 'AI', status: 'Partial', detail: 'The existing own/allied target preference shares the legal projection at initial and later windows. Strategy and calibration remain deferred.', evidence: ['game/atreides-full-plan.ts', 'game/bots.ts'] },
|
||||
{ area: 'Documentation', status: 'Partial', detail: 'Supplied source deadline, retained provisional sequencing and same-action last-seal limitation are explicit.', evidence: ['docs/ATREIDES_FULL_PLAN_TIMING.md', 'docs/RULE_DECISIONS.md'] },
|
||||
{ area: 'Verification', status: 'Partial', detail: 'Genuine49/50-action public setup, paid acquisition, shipments and movement create the native battle. Frozen11-file126/126, types/lint/build, five actual control/gameplay/table-SSR journeys and eight existing-profile paths pass. Full modes, broader combinations and comprehensive/browser/deployed assurance remain open.', evidence: ['tests/atreides-full-plan-window.test.ts', 'tests/atreides-full-plan-ui.test.tsx', 'tests/fixture-atreides-full-plan-window.ts', 'tests/full-plan.test.ts', 'tests/no-field-full-plan.test.ts'] },
|
||||
],
|
||||
related: ['card-karama', 'advanced-combat', 'battle-cards'],
|
||||
},
|
||||
{
|
||||
id: 'card-karama',
|
||||
title: 'Karama',
|
||||
|
||||
Reference in new issue
Block a user