diff --git a/components/homeworld-cards.tsx b/components/homeworld-cards.tsx index 440570d5..c8bb1411 100644 --- a/components/homeworld-cards.tsx +++ b/components/homeworld-cards.tsx @@ -215,7 +215,8 @@ export function HomeworldCardGallery() {

13 worlds · Inspect either face to read its complete gameplay effects. - Homeworld games are not yet available. + Supported Homeworld selections are available as unfinished previews; + some combinations remain unavailable.

{HOMEWORLD_CARDS.map((card) => ( diff --git a/components/turn-action-fields.tsx b/components/turn-action-fields.tsx index a2da9d49..55a26cd4 100644 --- a/components/turn-action-fields.tsx +++ b/components/turn-action-fields.tsx @@ -9,7 +9,7 @@ import { parseBattleActionClaim } from '@/game/mixed-battle-question'; import { battlePowerOf, nativeBattlePowerOpportunity, type BattlePowerClaim } from '@/game/battle-power-question'; import { battleCardLabel, voiceChoices } from '@/game/battle-cards'; import { nativeStormDialOpportunity } from '@/game/storm-dial-question'; -import { karamaBattleCancelOpportunity, WINDOW_CARD_EFFECTS, windowCardOpportunity, type WindowCardEffect } from '@/game/window-card-play'; +import { karamaBattleCancelOpportunity, parseKaramaHarkonnenSpecialSource, WINDOW_CARD_EFFECTS, windowCardOpportunity, type WindowCardEffect } from '@/game/window-card-play'; import { parseTurnActionLeaf, TURN_ACTION_MAX_DEPTH, TURN_ACTION_MAX_LEAVES, type TurnActionLeaf } from '@/game/turn-action-promises'; import { BattleClaimFields } from './battle-promises'; import { MixedActionFields, mixedActionInputError, type MixedActionConfig } from './mixed-action-fields'; @@ -55,8 +55,17 @@ const windowRoleNames: Record = { weather: 'Weather Co karamaAuction: 'Spend a printed Karama for this current ordinary lot', karamaShipment: 'Spend a printed Karama for the current shipment-price opportunity', karamaBattleCancel: 'Spend a printed Karama to cancel this Voice or Prescience', + karamaHarkonnenSpecial: 'Use Harkonnen’s special Karama during Bidding', thumper: 'Play Thumper before this turn’s first spice reveal', amal: 'Play Amal in this original phase opening' }; const battleCancelOpportunity = karamaBattleCancelOpportunity; +/** Harkonnen is public; do not inspect that player's hand or target's cards. */ +const windowOpportunity = (game: GameView, target: string, effect: WindowCardEffect) => { + if (effect === 'karamaBattleCancel') return battleCancelOpportunity(game, target); + const opportunity = windowCardOpportunity(game, effect); + if (effect === 'karamaHarkonnenSpecial' && + (!opportunity || parseKaramaHarkonnenSpecialSource(opportunity.source)?.owner !== target)) return null; + return opportunity; +}; const config: MixedActionConfig = { prefix: 'turn-action', legend: 'Current battle and this-turn action conditions, with optional current facts', @@ -113,6 +122,8 @@ const config: MixedActionConfig = { Only spending a physical printed Karama to cancel that declaration after this answer counts. Passing, another Karama role, Worthless conversion and a different battle or response do not count. The condition closes when the original response ends; another player's cancellation can end the opportunity. + Harkonnen's special Karama condition counts its own printed special power during Bidding, not another Karama role. + The condition does not disclose the respondent's hand or the exchange target. Thumper conditions count an actual accepted future printed play before this turn's original first spice reveal. Earlier plays, disposal, other roles and recovery alone do not count. A later reveal is a different opportunity. This condition closes when the actual first spice draw begins. @@ -157,8 +168,7 @@ const config: MixedActionConfig = { throw new Error('Choose this player’s original native Voice or Prescience opportunity in the current battle.'); } if (action.kind === 'windowCardPlay') { - const opportunity = action.effect === 'karamaBattleCancel' ? battleCancelOpportunity(game, target) - : windowCardOpportunity(game, action.effect); + const opportunity = windowOpportunity(game, target, action.effect); if (!opportunity || opportunity.turn !== game.turn || action.source !== opportunity.source) throw new Error('Choose an original current native window for this printed role, including an eligible battle response.'); } @@ -168,8 +178,7 @@ const config: MixedActionConfig = { const choices = recipients(game, target), leaderChoices = leaders(game, target), lot = currentLot(game), partnerChoices = partners(game, target); const currentPower = value.kind === 'battlePower' ? powerOpportunity(game, target, value.claim) : null; const currentWindow = value.kind === 'windowCardPlay' - ? value.effect === 'karamaBattleCancel' ? battleCancelOpportunity(game, target) : windowCardOpportunity(game, value.effect) - : null; + ? windowOpportunity(game, target, value.effect) : null; return <> { const effect = event.currentTarget.value; - if (effect === 'weather' || effect === 'atomics' || effect === 'harvester' || effect === 'karamaAuction' || effect === 'karamaShipment' || effect === 'karamaBattleCancel' || effect === 'thumper' || effect === 'amal') - onChange({ kind: 'windowCardPlay', effect, source: (effect === 'karamaBattleCancel' - ? battleCancelOpportunity(game, target) : windowCardOpportunity(game, effect))?.source ?? '' }); + if (effect === 'weather' || effect === 'atomics' || effect === 'harvester' || effect === 'karamaAuction' || effect === 'karamaShipment' || effect === 'karamaBattleCancel' || effect === 'karamaHarkonnenSpecial' || effect === 'thumper' || effect === 'amal') + onChange({ kind: 'windowCardPlay', effect, source: windowOpportunity(game, target, effect)?.source ?? '' }); }}> - {WINDOW_CARD_EFFECTS.filter(effect => effect !== 'karamaBattleCancel' || !!battleCancelOpportunity(game, target) || value.effect === effect) + {WINDOW_CARD_EFFECTS.filter(effect => (effect !== 'karamaBattleCancel' && effect !== 'karamaHarkonnenSpecial') || !!windowOpportunity(game, target, effect) || value.effect === effect) .map(effect => )}