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
396 lines
10 KiB
TypeScript
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'],
|
|
},
|
|
})
|