mantimon-tcg/frontend/src/stores/game.ts
Claude 42e0116aec
Add conditional energy deck zone based on RulesConfig
Extends RulesConfig support in the frontend game board to conditionally
render energy deck zones based on deck.energy_deck_enabled setting.
When disabled (classic mode), energy zones are null and omitted from
the layout, saving screen space.

Changes:
- Add energyDeckEnabled option to LayoutOptions interface
- Update landscape/portrait layouts to conditionally generate energy zones
- Make myEnergyZone/oppEnergyZone nullable in BoardLayout type
- Update StateRenderer to conditionally create and update energy zones
- Add energyDeckEnabled computed property to game store
- Add 7 tests for conditional energy deck rendering

https://claude.ai/code/session_01AAxKmpq2AGde327eX1nzUC
2026-02-02 09:41:25 +00:00

396 lines
10 KiB
TypeScript

/**
* Game store for managing active game state.
*
* Receives VisibleGameState from WebSocket and provides computed
* helpers for accessing player data, cards, and game status.
*/
import { ref, computed } from 'vue'
import { defineStore } from 'pinia'
import type {
VisibleGameState,
VisiblePlayerState,
CardInstance,
CardDefinition,
TurnPhase,
Action,
RulesConfig,
} from '@/types'
import { getMyPlayerState, getOpponentState, getCardDefinition, ConnectionStatus } from '@/types'
export const useGameStore = defineStore('game', () => {
// ---------------------------------------------------------------------------
// State
// ---------------------------------------------------------------------------
/** Current game state from server */
const gameState = ref<VisibleGameState | null>(null)
/** Current game ID */
const currentGameId = ref<string | null>(null)
/** Whether we're currently in a game */
const isInGame = ref(false)
/** WebSocket connection status */
const connectionStatus = ref<ConnectionStatus>(ConnectionStatus.DISCONNECTED)
/** Last event ID received from server (for reconnection replay) */
const lastEventId = ref<string | null>(null)
/** Actions queued while offline (for retry on reconnect) */
const pendingActions = ref<Action[]>([])
/** Loading state */
const isLoading = ref(false)
/** Error message */
const error = ref<string | null>(null)
// ---------------------------------------------------------------------------
// Computed - Player States
// ---------------------------------------------------------------------------
/** My player state */
const myPlayer = computed<VisiblePlayerState | null>(() => {
if (!gameState.value) return null
return getMyPlayerState(gameState.value)
})
/** Opponent's player state */
const opponent = computed<VisiblePlayerState | null>(() => {
if (!gameState.value) return null
return getOpponentState(gameState.value)
})
// ---------------------------------------------------------------------------
// Computed - My Zones
// ---------------------------------------------------------------------------
/** Cards in my hand */
const myHand = computed<CardInstance[]>(() =>
myPlayer.value?.hand.cards ?? []
)
/** My active Pokemon */
const myActive = computed<CardInstance | null>(() =>
myPlayer.value?.active.cards[0] ?? null
)
/** My benched Pokemon */
const myBench = computed<CardInstance[]>(() =>
myPlayer.value?.bench.cards ?? []
)
/** My discard pile */
const myDiscard = computed<CardInstance[]>(() =>
myPlayer.value?.discard.cards ?? []
)
/** My available energy */
const myEnergy = computed<CardInstance[]>(() =>
myPlayer.value?.energy_zone.cards ?? []
)
/** My deck count */
const myDeckCount = computed<number>(() =>
myPlayer.value?.deck_count ?? 0
)
/** My prize count */
const myPrizeCount = computed<number>(() =>
myPlayer.value?.prizes_count ?? 0
)
/** My score */
const myScore = computed<number>(() =>
myPlayer.value?.score ?? 0
)
// ---------------------------------------------------------------------------
// Computed - Opponent Zones
// ---------------------------------------------------------------------------
/** Opponent's active Pokemon */
const oppActive = computed<CardInstance | null>(() =>
opponent.value?.active.cards[0] ?? null
)
/** Opponent's benched Pokemon */
const oppBench = computed<CardInstance[]>(() =>
opponent.value?.bench.cards ?? []
)
/** Opponent's hand count (not contents) */
const oppHandCount = computed<number>(() =>
opponent.value?.hand.count ?? 0
)
/** Opponent's deck count */
const oppDeckCount = computed<number>(() =>
opponent.value?.deck_count ?? 0
)
/** Opponent's prize count */
const oppPrizeCount = computed<number>(() =>
opponent.value?.prizes_count ?? 0
)
/** Opponent's score */
const oppScore = computed<number>(() =>
opponent.value?.score ?? 0
)
// ---------------------------------------------------------------------------
// Computed - Game State
// ---------------------------------------------------------------------------
/** Current game ID from state (prefer currentGameId ref for setting) */
const gameId = computed<string | null>(() =>
gameState.value?.game_id ?? currentGameId.value
)
/** Current turn phase */
const currentPhase = computed<TurnPhase | null>(() =>
gameState.value?.phase ?? null
)
/** Computed connection state helpers */
const isConnected = computed<boolean>(() =>
connectionStatus.value === ConnectionStatus.CONNECTED
)
/** My player state */
const myPlayerState = computed<VisiblePlayerState | null>(() => {
if (!gameState.value) return null
return getMyPlayerState(gameState.value)
})
/** Opponent's player state */
const opponentPlayerState = computed<VisiblePlayerState | null>(() => {
if (!gameState.value) return null
return getOpponentState(gameState.value)
})
/** Whether it's my turn */
const isMyTurn = computed<boolean>(() =>
gameState.value?.is_my_turn ?? false
)
/** Current turn phase */
const phase = computed<TurnPhase | null>(() =>
gameState.value?.phase ?? null
)
/** Current turn number */
const turnNumber = computed<number>(() =>
gameState.value?.turn_number ?? 0
)
/** Whether game is over */
const isGameOver = computed<boolean>(() =>
gameState.value?.winner_id !== null
)
/** Winner ID if game is over */
const winnerId = computed<string | null>(() =>
gameState.value?.winner_id ?? null
)
/** Whether I won */
const didIWin = computed<boolean>(() =>
winnerId.value === gameState.value?.viewer_id
)
/** Forced action info */
const forcedAction = computed(() => {
if (!gameState.value?.forced_action_type) return null
return {
player: gameState.value.forced_action_player,
type: gameState.value.forced_action_type,
reason: gameState.value.forced_action_reason,
}
})
/** Whether I need to respond to a forced action */
const hasForcedAction = computed<boolean>(() =>
forcedAction.value?.player === gameState.value?.viewer_id
)
// ---------------------------------------------------------------------------
// Computed - Rules Config
// ---------------------------------------------------------------------------
/** Current game rules configuration */
const rulesConfig = computed<RulesConfig | null>(() =>
gameState.value?.rules_config ?? null
)
/** Whether this game uses classic prize cards (vs points system) */
const usePrizeCards = computed<boolean>(() =>
rulesConfig.value?.prizes.use_prize_cards ?? false
)
/** Number of prizes/points needed to win */
const prizeCount = computed<number>(() =>
rulesConfig.value?.prizes.count ?? 4
)
/** Maximum number of Pokemon on the bench */
const benchSize = computed<number>(() =>
rulesConfig.value?.bench.max_size ?? 5
)
/** Whether this game uses an energy deck zone (Pokemon Pocket style) */
const energyDeckEnabled = computed<boolean>(() =>
rulesConfig.value?.deck.energy_deck_enabled ?? true
)
// ---------------------------------------------------------------------------
// Computed - Card Lookup
// ---------------------------------------------------------------------------
/** Look up a card definition by ID */
function lookupCard(definitionId: string): CardDefinition | null {
if (!gameState.value) return null
return getCardDefinition(gameState.value, definitionId)
}
// ---------------------------------------------------------------------------
// Actions
// ---------------------------------------------------------------------------
/** Set the game state (called from WebSocket handler) */
function setGameState(state: VisibleGameState, eventId?: string): void {
gameState.value = state
currentGameId.value = state.game_id
isInGame.value = true
error.value = null
// Track last event ID for reconnection support
if (eventId) {
lastEventId.value = eventId
}
}
/** Set WebSocket connection status */
function setConnectionStatus(status: ConnectionStatus): void {
connectionStatus.value = status
}
/** Queue an action for retry when connection is restored */
function queueAction(action: Action): void {
pendingActions.value.push(action)
}
/** Clear pending actions (after successful retry or on game exit) */
function clearPendingActions(): void {
pendingActions.value = []
}
/** Get and clear pending actions (for retry logic) */
function takePendingActions(): Action[] {
const actions = [...pendingActions.value]
pendingActions.value = []
return actions
}
/** Clear the game state (when leaving game) */
function clearGame(): void {
gameState.value = null
currentGameId.value = null
isInGame.value = false
pendingActions.value = []
error.value = null
// Keep lastEventId for potential reconnection
}
/** Set loading state */
function setLoading(loading: boolean): void {
isLoading.value = loading
}
/** Set error message */
function setError(message: string | null): void {
error.value = message
}
// ---------------------------------------------------------------------------
// Return
// ---------------------------------------------------------------------------
return {
// State
gameState,
currentGameId,
isInGame,
connectionStatus,
lastEventId,
pendingActions,
isLoading,
error,
// Player states
myPlayer,
opponent,
myPlayerState,
opponentPlayerState,
// My zones
myHand,
myActive,
myBench,
myDiscard,
myEnergy,
myDeckCount,
myPrizeCount,
myScore,
// Opponent zones
oppActive,
oppBench,
oppHandCount,
oppDeckCount,
oppPrizeCount,
oppScore,
// Game state
gameId,
isMyTurn,
phase,
currentPhase,
turnNumber,
isGameOver,
winnerId,
didIWin,
forcedAction,
hasForcedAction,
isConnected,
// Rules config
rulesConfig,
usePrizeCards,
prizeCount,
benchSize,
energyDeckEnabled,
// Card lookup
lookupCard,
// Actions
setGameState,
setConnectionStatus,
queueAction,
clearPendingActions,
takePendingActions,
clearGame,
setLoading,
setError,
}
}, {
persist: {
// Persist only lastEventId for reconnection support
paths: ['lastEventId'],
},
})