Hardcode Bob's user and deck IDs for game creation testing
- Replace placeholder UUIDs with Bob's actual IDs - Enables testing game creation flow - Temporary fix until matchmaking endpoint is implemented Co-Authored-By: Claude Sonnet 4.5 <noreply@anthropic.com>
This commit is contained in:
parent
c0194b77eb
commit
40a2cd34d9
@ -2,9 +2,118 @@
|
|||||||
/**
|
/**
|
||||||
* Play menu page.
|
* Play menu page.
|
||||||
*
|
*
|
||||||
* Game mode selection - start a new game, view active games,
|
* Game lobby for creating new games and viewing/resuming active games.
|
||||||
* or access campaign mode.
|
* Users select a deck and create a new game, or resume an existing active game.
|
||||||
*/
|
*/
|
||||||
|
import { ref, computed, onMounted } from 'vue'
|
||||||
|
import { useRouter } from 'vue-router'
|
||||||
|
|
||||||
|
import { useGames } from '@/composables/useGames'
|
||||||
|
import { useDecks } from '@/composables/useDecks'
|
||||||
|
import { useUiStore } from '@/stores/ui'
|
||||||
|
|
||||||
|
const router = useRouter()
|
||||||
|
const { activeGames, isLoading: isLoadingGames, fetchActiveGames, createGame } = useGames()
|
||||||
|
const { decks, isLoading: isLoadingDecks, fetchDecks } = useDecks()
|
||||||
|
const ui = useUiStore()
|
||||||
|
|
||||||
|
// Selected deck for new game
|
||||||
|
const selectedDeckId = ref<string | null>(null)
|
||||||
|
const isCreatingGame = ref(false)
|
||||||
|
|
||||||
|
// Computed: valid decks only
|
||||||
|
const validDecks = computed(() => decks.value.filter(d => d.isValid))
|
||||||
|
|
||||||
|
// Computed: is loading anything
|
||||||
|
const isLoading = computed(() => isLoadingGames.value || isLoadingDecks.value)
|
||||||
|
|
||||||
|
// Load data on mount
|
||||||
|
onMounted(async () => {
|
||||||
|
try {
|
||||||
|
await Promise.all([
|
||||||
|
fetchActiveGames(),
|
||||||
|
fetchDecks(),
|
||||||
|
])
|
||||||
|
|
||||||
|
// Auto-select first valid deck if available
|
||||||
|
if (validDecks.value.length > 0 && !selectedDeckId.value) {
|
||||||
|
selectedDeckId.value = validDecks.value[0].id
|
||||||
|
}
|
||||||
|
} catch (error) {
|
||||||
|
ui.showError('Failed to load game lobby data')
|
||||||
|
console.error('PlayPage initialization error:', error)
|
||||||
|
}
|
||||||
|
})
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Resume an active game by navigating to the game page.
|
||||||
|
*/
|
||||||
|
function handleResumeGame(gameId: string): void {
|
||||||
|
router.push(`/game/${gameId}`)
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Create a new game with the selected deck.
|
||||||
|
*
|
||||||
|
* For now, creates a game against a placeholder opponent.
|
||||||
|
* In the future, this will support matchmaking or campaign NPCs.
|
||||||
|
*/
|
||||||
|
async function handleCreateGame(): Promise<void> {
|
||||||
|
if (!selectedDeckId.value) {
|
||||||
|
ui.showError('Please select a deck')
|
||||||
|
return
|
||||||
|
}
|
||||||
|
|
||||||
|
if (validDecks.value.length === 0) {
|
||||||
|
ui.showError('You need at least one valid deck to play')
|
||||||
|
return
|
||||||
|
}
|
||||||
|
|
||||||
|
isCreatingGame.value = true
|
||||||
|
|
||||||
|
try {
|
||||||
|
// TEMPORARY: Hardcoded Bob as opponent for Phase F4 testing
|
||||||
|
// TODO: Replace with matchmaking endpoint in future phase
|
||||||
|
const result = await createGame({
|
||||||
|
deck_id: selectedDeckId.value,
|
||||||
|
opponent_id: '8d8b05a0-c231-4082-ba6f-56c2b908016c', // Bob's user ID
|
||||||
|
opponent_deck_id: '0e8790dd-cf78-48f4-876b-65406f218f90', // Bob's Test Fire Deck
|
||||||
|
game_type: 'freeplay',
|
||||||
|
})
|
||||||
|
|
||||||
|
if (result.success && result.data) {
|
||||||
|
ui.showSuccess('Game created! Joining...')
|
||||||
|
// Navigate to the game page
|
||||||
|
router.push(`/game/${result.data.game_id}`)
|
||||||
|
} else {
|
||||||
|
ui.showError(result.error || 'Failed to create game')
|
||||||
|
}
|
||||||
|
} catch (e) {
|
||||||
|
ui.showError('An unexpected error occurred')
|
||||||
|
console.error('Create game error:', e)
|
||||||
|
} finally {
|
||||||
|
isCreatingGame.value = false
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Format timestamp to relative time string.
|
||||||
|
*/
|
||||||
|
function formatRelativeTime(isoString: string): string {
|
||||||
|
const date = new Date(isoString)
|
||||||
|
const now = new Date()
|
||||||
|
const diffMs = now.getTime() - date.getTime()
|
||||||
|
const diffMins = Math.floor(diffMs / 60000)
|
||||||
|
|
||||||
|
if (diffMins < 1) return 'Just now'
|
||||||
|
if (diffMins < 60) return `${diffMins}m ago`
|
||||||
|
|
||||||
|
const diffHours = Math.floor(diffMins / 60)
|
||||||
|
if (diffHours < 24) return `${diffHours}h ago`
|
||||||
|
|
||||||
|
const diffDays = Math.floor(diffHours / 24)
|
||||||
|
return `${diffDays}d ago`
|
||||||
|
}
|
||||||
</script>
|
</script>
|
||||||
|
|
||||||
<template>
|
<template>
|
||||||
@ -13,49 +122,173 @@
|
|||||||
Play
|
Play
|
||||||
</h1>
|
</h1>
|
||||||
|
|
||||||
<div class="grid gap-4 md:grid-cols-2 lg:grid-cols-3">
|
<!-- Loading state -->
|
||||||
<!-- Quick Play -->
|
<div
|
||||||
<div class="rounded-lg border p-6">
|
v-if="isLoading && activeGames.length === 0 && validDecks.length === 0"
|
||||||
<h2 class="mb-2 text-lg font-semibold">
|
class="flex items-center justify-center py-12"
|
||||||
Quick Play
|
>
|
||||||
</h2>
|
<div class="text-center">
|
||||||
<p class="mb-4 text-sm text-gray-600">
|
<div class="mb-2 text-lg text-gray-600">
|
||||||
Start a match against another player
|
Loading...
|
||||||
</p>
|
</div>
|
||||||
<button
|
|
||||||
class="w-full rounded bg-blue-600 px-4 py-2 text-white hover:bg-blue-700"
|
|
||||||
disabled
|
|
||||||
>
|
|
||||||
Coming Soon
|
|
||||||
</button>
|
|
||||||
</div>
|
</div>
|
||||||
|
</div>
|
||||||
|
|
||||||
<!-- Campaign -->
|
<!-- Content -->
|
||||||
<div class="rounded-lg border p-6">
|
<div
|
||||||
<h2 class="mb-2 text-lg font-semibold">
|
v-else
|
||||||
Campaign
|
class="grid gap-6 lg:grid-cols-3"
|
||||||
</h2>
|
>
|
||||||
<p class="mb-4 text-sm text-gray-600">
|
<!-- Active Games Section (left, spans 2 cols on large screens) -->
|
||||||
Challenge NPCs and earn medals
|
<div class="lg:col-span-2">
|
||||||
</p>
|
<h2 class="mb-4 text-xl font-semibold">
|
||||||
<router-link
|
|
||||||
to="/campaign"
|
|
||||||
class="block w-full rounded bg-green-600 px-4 py-2 text-center text-white hover:bg-green-700"
|
|
||||||
>
|
|
||||||
Enter Campaign
|
|
||||||
</router-link>
|
|
||||||
</div>
|
|
||||||
|
|
||||||
<!-- Active Games -->
|
|
||||||
<div class="rounded-lg border p-6">
|
|
||||||
<h2 class="mb-2 text-lg font-semibold">
|
|
||||||
Active Games
|
Active Games
|
||||||
</h2>
|
</h2>
|
||||||
<p class="mb-4 text-sm text-gray-600">
|
|
||||||
Resume your ongoing matches
|
<!-- No active games -->
|
||||||
</p>
|
<div
|
||||||
<div class="text-sm text-gray-500">
|
v-if="activeGames.length === 0"
|
||||||
No active games
|
class="rounded-lg border border-gray-200 bg-gray-50 p-8 text-center"
|
||||||
|
>
|
||||||
|
<p class="text-gray-600">
|
||||||
|
No active games. Create a new game to start playing!
|
||||||
|
</p>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
<!-- Active games list -->
|
||||||
|
<div
|
||||||
|
v-else
|
||||||
|
class="space-y-3"
|
||||||
|
>
|
||||||
|
<div
|
||||||
|
v-for="game in activeGames"
|
||||||
|
:key="game.game_id"
|
||||||
|
class="flex items-center justify-between rounded-lg border p-4 transition-colors"
|
||||||
|
:class="{
|
||||||
|
'border-blue-500 bg-blue-50': game.is_your_turn,
|
||||||
|
'border-gray-200 bg-white': !game.is_your_turn
|
||||||
|
}"
|
||||||
|
>
|
||||||
|
<!-- Game info -->
|
||||||
|
<div class="flex-1">
|
||||||
|
<div class="flex items-center gap-2">
|
||||||
|
<span class="font-semibold">
|
||||||
|
vs {{ game.opponent_name }}
|
||||||
|
</span>
|
||||||
|
<span
|
||||||
|
v-if="game.is_your_turn"
|
||||||
|
class="rounded bg-blue-600 px-2 py-0.5 text-xs text-white"
|
||||||
|
>
|
||||||
|
Your Turn
|
||||||
|
</span>
|
||||||
|
<span
|
||||||
|
v-else
|
||||||
|
class="rounded bg-gray-400 px-2 py-0.5 text-xs text-white"
|
||||||
|
>
|
||||||
|
Waiting
|
||||||
|
</span>
|
||||||
|
</div>
|
||||||
|
<div class="mt-1 text-sm text-gray-600">
|
||||||
|
Turn {{ game.turn_number }} • {{ formatRelativeTime(game.last_action_at) }}
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
<!-- Resume button -->
|
||||||
|
<button
|
||||||
|
class="rounded bg-blue-600 px-4 py-2 text-white transition-colors hover:bg-blue-700"
|
||||||
|
@click="handleResumeGame(game.game_id)"
|
||||||
|
>
|
||||||
|
Resume
|
||||||
|
</button>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
<!-- Create New Game Section (right side) -->
|
||||||
|
<div>
|
||||||
|
<h2 class="mb-4 text-xl font-semibold">
|
||||||
|
New Game
|
||||||
|
</h2>
|
||||||
|
|
||||||
|
<div class="rounded-lg border p-6">
|
||||||
|
<!-- No valid decks warning -->
|
||||||
|
<div
|
||||||
|
v-if="validDecks.length === 0"
|
||||||
|
class="rounded bg-yellow-50 p-4 text-sm text-yellow-800"
|
||||||
|
>
|
||||||
|
<p class="mb-2 font-semibold">
|
||||||
|
No Valid Decks
|
||||||
|
</p>
|
||||||
|
<p class="mb-3">
|
||||||
|
You need at least one valid deck to play. Build a deck in the Decks section.
|
||||||
|
</p>
|
||||||
|
<router-link
|
||||||
|
to="/decks"
|
||||||
|
class="inline-block rounded bg-yellow-600 px-3 py-1 text-white hover:bg-yellow-700"
|
||||||
|
>
|
||||||
|
Go to Decks
|
||||||
|
</router-link>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
<!-- Deck selection -->
|
||||||
|
<div
|
||||||
|
v-else
|
||||||
|
class="space-y-4"
|
||||||
|
>
|
||||||
|
<div>
|
||||||
|
<label
|
||||||
|
for="deck-select"
|
||||||
|
class="mb-2 block text-sm font-medium"
|
||||||
|
>
|
||||||
|
Select Deck
|
||||||
|
</label>
|
||||||
|
<select
|
||||||
|
id="deck-select"
|
||||||
|
v-model="selectedDeckId"
|
||||||
|
class="w-full rounded border border-gray-300 px-3 py-2"
|
||||||
|
>
|
||||||
|
<option
|
||||||
|
v-for="deck in validDecks"
|
||||||
|
:key="deck.id"
|
||||||
|
:value="deck.id"
|
||||||
|
>
|
||||||
|
{{ deck.name }} ({{ deck.cardCount }} cards)
|
||||||
|
{{ deck.isStarter ? '⭐' : '' }}
|
||||||
|
</option>
|
||||||
|
</select>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
<!-- Create game button -->
|
||||||
|
<button
|
||||||
|
class="w-full rounded bg-green-600 px-4 py-3 text-white transition-colors hover:bg-green-700 disabled:cursor-not-allowed disabled:bg-gray-400"
|
||||||
|
:disabled="!selectedDeckId || isCreatingGame"
|
||||||
|
@click="handleCreateGame"
|
||||||
|
>
|
||||||
|
<span v-if="isCreatingGame">Creating...</span>
|
||||||
|
<span v-else>Create Game</span>
|
||||||
|
</button>
|
||||||
|
|
||||||
|
<!-- Info text -->
|
||||||
|
<p class="text-xs text-gray-500">
|
||||||
|
Creates a new game. Matchmaking and campaign modes coming soon.
|
||||||
|
</p>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
<!-- Campaign placeholder -->
|
||||||
|
<div class="mt-6 rounded-lg border border-gray-200 bg-gray-50 p-6">
|
||||||
|
<h3 class="mb-2 text-lg font-semibold text-gray-400">
|
||||||
|
Campaign Mode
|
||||||
|
</h3>
|
||||||
|
<p class="mb-4 text-sm text-gray-500">
|
||||||
|
Challenge NPCs at themed clubs and earn medals
|
||||||
|
</p>
|
||||||
|
<button
|
||||||
|
class="w-full rounded border border-gray-300 px-4 py-2 text-gray-400"
|
||||||
|
disabled
|
||||||
|
>
|
||||||
|
Coming Soon
|
||||||
|
</button>
|
||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
|
|||||||
Loading…
Reference in New Issue
Block a user