Version 1.0 #102
@@ -80,6 +80,7 @@ frontend/.env.*.local
|
||||
# Websockets: NodeJs (in websockets)
|
||||
# -------------------------
|
||||
/websockets/node_modules/
|
||||
/websockets/.env
|
||||
# -------------------------
|
||||
# Other useful ignores
|
||||
# -------------------------
|
||||
|
||||
|
After Width: | Height: | Size: 43 KiB |
|
After Width: | Height: | Size: 255 KiB |
|
After Width: | Height: | Size: 238 KiB |
|
After Width: | Height: | Size: 273 KiB |
|
After Width: | Height: | Size: 20 KiB |
|
After Width: | Height: | Size: 24 KiB |
|
After Width: | Height: | Size: 24 KiB |
|
After Width: | Height: | Size: 29 KiB |
|
After Width: | Height: | Size: 33 KiB |
|
After Width: | Height: | Size: 34 KiB |
|
After Width: | Height: | Size: 42 KiB |
|
After Width: | Height: | Size: 242 KiB |
|
After Width: | Height: | Size: 201 KiB |
|
After Width: | Height: | Size: 197 KiB |
|
After Width: | Height: | Size: 21 KiB |
|
After Width: | Height: | Size: 26 KiB |
|
After Width: | Height: | Size: 26 KiB |
|
After Width: | Height: | Size: 32 KiB |
|
After Width: | Height: | Size: 36 KiB |
|
After Width: | Height: | Size: 36 KiB |
|
After Width: | Height: | Size: 36 KiB |
|
After Width: | Height: | Size: 210 KiB |
|
After Width: | Height: | Size: 191 KiB |
|
After Width: | Height: | Size: 260 KiB |
|
After Width: | Height: | Size: 19 KiB |
|
After Width: | Height: | Size: 24 KiB |
|
After Width: | Height: | Size: 24 KiB |
|
After Width: | Height: | Size: 29 KiB |
|
After Width: | Height: | Size: 33 KiB |
|
After Width: | Height: | Size: 33 KiB |
|
After Width: | Height: | Size: 42 KiB |
|
After Width: | Height: | Size: 203 KiB |
|
After Width: | Height: | Size: 279 KiB |
|
After Width: | Height: | Size: 257 KiB |
|
After Width: | Height: | Size: 23 KiB |
|
After Width: | Height: | Size: 28 KiB |
|
After Width: | Height: | Size: 28 KiB |
|
After Width: | Height: | Size: 34 KiB |
|
After Width: | Height: | Size: 39 KiB |
|
After Width: | Height: | Size: 40 KiB |
|
After Width: | Height: | Size: 312 KiB |
@@ -3,8 +3,8 @@
|
||||
<nav class="max-w-full p-5 flex flex-row justify-between align-middle">
|
||||
<div class="align-middle text-xl">
|
||||
<RouterLink to="/"> {{ pageTitle }} </RouterLink>
|
||||
<span class="text-xs" v-if="authStore.currentUser">
|
||||
({{ authStore.currentUser?.name }})
|
||||
<span class="text-xs" v-if="authStore.currentUser"
|
||||
> ({{ authStore.currentUser?.name }})
|
||||
</span>
|
||||
</div>
|
||||
<NavBar @logout="logout" :userLoggedIn="authStore.isLoggedIn" />
|
||||
@@ -17,26 +17,22 @@
|
||||
</template>
|
||||
|
||||
<script setup>
|
||||
import { RouterLink, RouterView } from 'vue-router';
|
||||
import { toast } from 'vue-sonner';
|
||||
import { RouterLink, RouterView } from 'vue-router'
|
||||
import { toast } from 'vue-sonner'
|
||||
import 'vue-sonner/style.css'
|
||||
import { ref, onMounted } from 'vue';
|
||||
import { ref, onMounted } from 'vue'
|
||||
import { Toaster } from '@/components/ui/sonner'
|
||||
import NavBar from './components/layout/NavBar.vue';
|
||||
import { useAuthStore } from './stores/auth';
|
||||
import { useSocketStore } from './stores/socket';
|
||||
import NavBar from './components/layout/NavBar.vue'
|
||||
import { useAuthStore } from './stores/auth'
|
||||
import { useSocketStore } from './stores/socket'
|
||||
|
||||
const authStore = useAuthStore()
|
||||
const socketStore = useSocketStore()
|
||||
|
||||
|
||||
const year = new Date().getFullYear()
|
||||
const pageTitle = ref(`DAD ${year}/${String(year + 1).slice(-2)}`)
|
||||
|
||||
|
||||
|
||||
const logout = () => {
|
||||
|
||||
toast.promise(authStore.logout(), {
|
||||
loading: 'Calling API',
|
||||
success: () => {
|
||||
@@ -44,15 +40,13 @@ const logout = () => {
|
||||
},
|
||||
error: (data) => `[API] Error - ${data?.response?.data?.message}`,
|
||||
})
|
||||
|
||||
}
|
||||
|
||||
onMounted(async () => {
|
||||
await authStore.restoreSession()
|
||||
socketStore.handleConnection()
|
||||
})
|
||||
|
||||
|
||||
</script>
|
||||
|
||||
<style></style>`
|
||||
<style></style>
|
||||
`
|
||||
|
||||
@@ -0,0 +1,113 @@
|
||||
<template>
|
||||
<div class="relative flex items-center justify-center w-64 h-48">
|
||||
<div
|
||||
class="absolute rotate-90 origin-center transition-all duration-700"
|
||||
:class="[trumpReveal && 'scale-110']"
|
||||
>
|
||||
<div
|
||||
class="ring-2 ring-amber-400/60 ring-offset-2 rounded-lg transition-all duration-700"
|
||||
:class="[
|
||||
trumpReveal &&
|
||||
'ring-4 ring-amber-400 shadow-[0_0_30px_rgba(251,191,36,0.8)] animate-pulse',
|
||||
]"
|
||||
>
|
||||
<GameCard :suit="trumpCard.suit" :rank="trumpCard.rank" />
|
||||
</div>
|
||||
<div
|
||||
v-if="trumpReveal"
|
||||
class="absolute -bottom-8 left-1/2 -translate-x-1/2 bg-amber-500/90 text-white text-[10px] font-bold px-3 py-1 rounded-full whitespace-nowrap animate-pulse"
|
||||
>
|
||||
TRUMP
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<div
|
||||
v-if="!isEmpty && cardsRemaining > 0"
|
||||
class="absolute -translate-x-8 -translate-y-4 transition-all duration-300"
|
||||
:class="[cardsRemaining === 0 && 'opacity-0 scale-95', deckPulse && 'scale-105']"
|
||||
>
|
||||
<div class="relative">
|
||||
<div
|
||||
class="absolute inset-0 bg-gray-800 rounded-lg opacity-20 translate-x-1 translate-y-1"
|
||||
></div>
|
||||
<div
|
||||
class="absolute inset-0 bg-gray-800 rounded-lg opacity-15 translate-x-2 translate-y-2"
|
||||
></div>
|
||||
<div
|
||||
class="absolute inset-0 bg-gray-800 rounded-lg opacity-10 translate-x-3 translate-y-3"
|
||||
></div>
|
||||
|
||||
<div class="relative">
|
||||
<GameCard suit="c" :rank="1" :face-down="true" />
|
||||
|
||||
<div
|
||||
class="absolute -top-2 -right-2 bg-blue-600 text-white text-xs font-bold rounded-full w-8 h-8 flex items-center justify-center shadow-lg border-2 border-white transition-all duration-300"
|
||||
>
|
||||
<Transition
|
||||
enter-active-class="transition-all duration-200"
|
||||
enter-from-class="scale-150 opacity-0"
|
||||
enter-to-class="scale-100 opacity-100"
|
||||
leave-active-class="transition-all duration-200"
|
||||
leave-from-class="scale-100 opacity-100"
|
||||
leave-to-class="scale-50 opacity-0"
|
||||
mode="out-in"
|
||||
>
|
||||
<span :key="cardsRemaining">{{ cardsRemaining }}</span>
|
||||
</Transition>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</template>
|
||||
|
||||
<script setup>
|
||||
import { ref, watch } from 'vue'
|
||||
import GameCard from './GameCard.vue'
|
||||
|
||||
const props = defineProps({
|
||||
trumpCard: {
|
||||
type: Object,
|
||||
required: true,
|
||||
},
|
||||
cardsRemaining: {
|
||||
type: Number,
|
||||
required: true,
|
||||
},
|
||||
isEmpty: {
|
||||
type: Boolean,
|
||||
required: true,
|
||||
},
|
||||
revealTrump: {
|
||||
type: Boolean,
|
||||
default: false,
|
||||
},
|
||||
})
|
||||
|
||||
const deckPulse = ref(false)
|
||||
const trumpReveal = ref(false)
|
||||
|
||||
// Pulse animation when cards remaining changes
|
||||
watch(
|
||||
() => props.cardsRemaining,
|
||||
() => {
|
||||
deckPulse.value = true
|
||||
setTimeout(() => {
|
||||
deckPulse.value = false
|
||||
}, 300)
|
||||
},
|
||||
)
|
||||
|
||||
// Trump reveal animation
|
||||
watch(
|
||||
() => props.revealTrump,
|
||||
(newValue) => {
|
||||
if (newValue) {
|
||||
trumpReveal.value = true
|
||||
setTimeout(() => {
|
||||
trumpReveal.value = false
|
||||
}, 3000) // Show for 3 seconds
|
||||
}
|
||||
},
|
||||
)
|
||||
</script>
|
||||
@@ -0,0 +1,76 @@
|
||||
<template>
|
||||
<Teleport to="body">
|
||||
<div
|
||||
v-if="isVisible"
|
||||
class="fixed pointer-events-none z-[9999]"
|
||||
:style="{
|
||||
left: currentPosition.x + 'px',
|
||||
top: currentPosition.y + 'px',
|
||||
transition: isAnimating ? `all ${duration}ms ease-out` : 'none',
|
||||
}"
|
||||
>
|
||||
<GameCard :suit="card.suit" :rank="card.rank" :face-down="faceDown" />
|
||||
</div>
|
||||
</Teleport>
|
||||
</template>
|
||||
|
||||
<script setup>
|
||||
import { ref, onMounted, watch } from 'vue'
|
||||
import GameCard from './GameCard.vue'
|
||||
|
||||
const props = defineProps({
|
||||
card: {
|
||||
type: Object,
|
||||
required: true,
|
||||
},
|
||||
startPosition: {
|
||||
type: Object,
|
||||
required: true,
|
||||
validator: (value) => 'x' in value && 'y' in value,
|
||||
},
|
||||
endPosition: {
|
||||
type: Object,
|
||||
required: true,
|
||||
validator: (value) => 'x' in value && 'y' in value,
|
||||
},
|
||||
duration: {
|
||||
type: Number,
|
||||
default: 500,
|
||||
},
|
||||
faceDown: {
|
||||
type: Boolean,
|
||||
default: true,
|
||||
},
|
||||
delay: {
|
||||
type: Number,
|
||||
default: 0,
|
||||
},
|
||||
})
|
||||
|
||||
const emit = defineEmits(['complete'])
|
||||
|
||||
const isVisible = ref(false)
|
||||
const isAnimating = ref(false)
|
||||
const currentPosition = ref({ x: 0, y: 0 })
|
||||
|
||||
onMounted(() => {
|
||||
// Start at the deck position
|
||||
currentPosition.value = { ...props.startPosition }
|
||||
isVisible.value = true
|
||||
|
||||
// Wait for delay, then start animation
|
||||
setTimeout(() => {
|
||||
// Force a reflow to ensure starting position is set
|
||||
requestAnimationFrame(() => {
|
||||
isAnimating.value = true
|
||||
currentPosition.value = { ...props.endPosition }
|
||||
|
||||
// After animation completes, emit event and hide
|
||||
setTimeout(() => {
|
||||
emit('complete')
|
||||
isVisible.value = false
|
||||
}, props.duration)
|
||||
})
|
||||
}, props.delay)
|
||||
})
|
||||
</script>
|
||||
@@ -0,0 +1,116 @@
|
||||
<template>
|
||||
<div
|
||||
class="min-h-screen bg-linear-to-br from-green-700 via-green-800 to-green-900 grid grid-rows-[auto_auto_1fr_auto] grid-cols-[1fr_3fr_1fr] gap-6 p-8"
|
||||
>
|
||||
<!-- Score Display (Top Left) -->
|
||||
<div class="col-start-1 col-end-4 row-start-1 flex justify-start items-start">
|
||||
<ScoreDisplay
|
||||
:player-score="playerScore"
|
||||
:opponent-score="opponentScore"
|
||||
:current-turn="currentTurn"
|
||||
:round-number="1"
|
||||
/>
|
||||
</div>
|
||||
|
||||
<!-- Opponent Hand -->
|
||||
<div class="col-start-2 col-end-3 row-start-2 flex justify-center">
|
||||
<PlayerHand
|
||||
:cards="opponentHand"
|
||||
:face-down="true"
|
||||
:max-cards="maxCards"
|
||||
:playable-cards="[]"
|
||||
/>
|
||||
</div>
|
||||
|
||||
<!-- Play Area -->
|
||||
<div class="col-start-2 col-end-3 row-start-3 flex items-center justify-center">
|
||||
<PlayArea
|
||||
:player-card="currentTrick.playerCard"
|
||||
:opponent-card="currentTrick.opponentCard"
|
||||
:winner="currentTrick.winner"
|
||||
/>
|
||||
</div>
|
||||
|
||||
<!-- Deck Area -->
|
||||
<div class="col-start-3 col-end-4 row-start-3 flex items-center justify-center">
|
||||
<DeckArea :trump-card="trumpCard" :cards-remaining="cardsRemaining" :is-empty="deckIsEmpty" />
|
||||
</div>
|
||||
|
||||
<!-- Player Hand -->
|
||||
<div class="col-start-2 col-end-3 row-start-4 flex justify-center">
|
||||
<PlayerHand
|
||||
:cards="playerHand"
|
||||
:face-down="false"
|
||||
:max-cards="maxCards"
|
||||
:playable-cards="playableCardIndices"
|
||||
@card-clicked="handleCardClick"
|
||||
/>
|
||||
</div>
|
||||
</div>
|
||||
</template>
|
||||
|
||||
<script setup>
|
||||
import PlayerHand from './PlayerHand.vue'
|
||||
import PlayArea from './PlayArea.vue'
|
||||
import DeckArea from './DeckArea.vue'
|
||||
import ScoreDisplay from './ScoreDisplay.vue'
|
||||
|
||||
// Props for testing - in real implementation, these come from Pinia store
|
||||
const props = defineProps({
|
||||
playerHand: {
|
||||
type: Array,
|
||||
default: () => [],
|
||||
},
|
||||
opponentHand: {
|
||||
type: Array,
|
||||
default: () => [],
|
||||
},
|
||||
currentTrick: {
|
||||
type: Object,
|
||||
default: () => ({
|
||||
playerCard: null,
|
||||
opponentCard: null,
|
||||
winner: null,
|
||||
}),
|
||||
},
|
||||
trumpCard: {
|
||||
type: Object,
|
||||
required: true,
|
||||
},
|
||||
cardsRemaining: {
|
||||
type: Number,
|
||||
default: 0,
|
||||
},
|
||||
deckIsEmpty: {
|
||||
type: Boolean,
|
||||
default: false,
|
||||
},
|
||||
maxCards: {
|
||||
type: Number,
|
||||
default: 3,
|
||||
},
|
||||
playableCardIndices: {
|
||||
type: Array,
|
||||
default: () => [],
|
||||
},
|
||||
playerScore: {
|
||||
type: Number,
|
||||
default: 45,
|
||||
},
|
||||
opponentScore: {
|
||||
type: Number,
|
||||
default: 23,
|
||||
},
|
||||
currentTurn: {
|
||||
type: String,
|
||||
default: 'player',
|
||||
validator: (value) => ['player', 'opponent'].includes(value),
|
||||
},
|
||||
})
|
||||
|
||||
const emit = defineEmits(['card-clicked'])
|
||||
|
||||
const handleCardClick = (payload) => {
|
||||
emit('card-clicked', payload)
|
||||
}
|
||||
</script>
|
||||
@@ -0,0 +1,39 @@
|
||||
<template>
|
||||
<div
|
||||
class="relative w-32 aspect-[2.5/3.5] cursor-pointer transition-all duration-300 ease-in-out hover:scale-105 hover:-translate-y-2 hover:shadow-2xl"
|
||||
>
|
||||
<img
|
||||
:src="cardImage"
|
||||
:alt="faceDown ? 'Card back' : `${suit} ${rank}`"
|
||||
class="w-full h-full object-cover rounded-lg shadow-lg select-none"
|
||||
draggable="false"
|
||||
/>
|
||||
</div>
|
||||
</template>
|
||||
|
||||
<script setup>
|
||||
import { computed } from 'vue'
|
||||
|
||||
const props = defineProps({
|
||||
suit: {
|
||||
type: String,
|
||||
required: true,
|
||||
},
|
||||
rank: {
|
||||
type: Number,
|
||||
required: true,
|
||||
},
|
||||
faceDown: {
|
||||
type: Boolean,
|
||||
default: false,
|
||||
},
|
||||
})
|
||||
|
||||
const cardImage = computed(() => {
|
||||
if (props.faceDown) {
|
||||
return '/cards/semFace.png'
|
||||
} else {
|
||||
return `/cards/${props.suit}${props.rank}.png`
|
||||
}
|
||||
})
|
||||
</script>
|
||||
@@ -0,0 +1,295 @@
|
||||
<template>
|
||||
<Teleport to="body">
|
||||
<Transition
|
||||
enter-active-class="transition-all duration-500 ease-out"
|
||||
enter-from-class="opacity-0"
|
||||
enter-to-class="opacity-100"
|
||||
leave-active-class="transition-all duration-300 ease-in"
|
||||
leave-from-class="opacity-100"
|
||||
leave-to-class="opacity-0"
|
||||
>
|
||||
<div
|
||||
v-if="isVisible"
|
||||
class="fixed inset-0 bg-black/70 backdrop-blur-sm z-[9998] flex items-center justify-center p-4"
|
||||
@click.self="handleClose"
|
||||
>
|
||||
<Transition
|
||||
enter-active-class="transition-all duration-500 ease-out delay-100"
|
||||
enter-from-class="opacity-0 scale-75 -translate-y-10"
|
||||
enter-to-class="opacity-100 scale-100 translate-y-0"
|
||||
leave-active-class="transition-all duration-300 ease-in"
|
||||
leave-from-class="opacity-100 scale-100"
|
||||
leave-to-class="opacity-0 scale-90"
|
||||
>
|
||||
<div
|
||||
v-if="isVisible"
|
||||
class="bg-gradient-to-br from-gray-900 via-gray-800 to-gray-900 rounded-2xl shadow-2xl border-2 max-w-md w-full p-8 relative overflow-hidden"
|
||||
:class="[
|
||||
winner === 'player'
|
||||
? 'border-emerald-500/50'
|
||||
: winner === 'opponent'
|
||||
? 'border-rose-500/50'
|
||||
: 'border-gray-600/50',
|
||||
]"
|
||||
>
|
||||
<!-- Confetti Effect (if player wins) -->
|
||||
<div v-if="winner === 'player' && showConfetti" class="absolute inset-0 pointer-events-none">
|
||||
<div
|
||||
v-for="i in 30"
|
||||
:key="i"
|
||||
class="absolute w-2 h-2 animate-confetti"
|
||||
:style="{
|
||||
left: Math.random() * 100 + '%',
|
||||
top: '-10px',
|
||||
backgroundColor: ['#fbbf24', '#34d399', '#60a5fa', '#f472b6', '#a78bfa'][
|
||||
Math.floor(Math.random() * 5)
|
||||
],
|
||||
animationDelay: Math.random() * 2 + 's',
|
||||
animationDuration: 3 + Math.random() * 2 + 's',
|
||||
}"
|
||||
></div>
|
||||
</div>
|
||||
|
||||
<!-- Winner Icon/Badge -->
|
||||
<div class="flex justify-center mb-6">
|
||||
<div
|
||||
class="relative"
|
||||
:class="[winner === 'player' ? 'animate-bounce' : 'animate-pulse']"
|
||||
>
|
||||
<div
|
||||
v-if="winner === 'player'"
|
||||
class="w-24 h-24 rounded-full bg-gradient-to-br from-emerald-400 to-emerald-600 flex items-center justify-center shadow-lg shadow-emerald-500/50"
|
||||
>
|
||||
<span class="text-5xl">🏆</span>
|
||||
</div>
|
||||
<div
|
||||
v-else-if="winner === 'opponent'"
|
||||
class="w-24 h-24 rounded-full bg-gradient-to-br from-rose-400 to-rose-600 flex items-center justify-center shadow-lg shadow-rose-500/50"
|
||||
>
|
||||
<span class="text-5xl">😔</span>
|
||||
</div>
|
||||
<div
|
||||
v-else
|
||||
class="w-24 h-24 rounded-full bg-gradient-to-br from-gray-400 to-gray-600 flex items-center justify-center shadow-lg"
|
||||
>
|
||||
<span class="text-5xl">🤝</span>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<!-- Title -->
|
||||
<h2
|
||||
class="text-4xl font-black text-center mb-2 bg-clip-text text-transparent bg-gradient-to-r"
|
||||
:class="[
|
||||
winner === 'player'
|
||||
? 'from-emerald-300 to-emerald-500'
|
||||
: winner === 'opponent'
|
||||
? 'from-rose-300 to-rose-500'
|
||||
: 'from-gray-300 to-gray-500',
|
||||
]"
|
||||
>
|
||||
{{ title }}
|
||||
</h2>
|
||||
|
||||
<!-- Subtitle -->
|
||||
<p class="text-center text-gray-400 mb-8">{{ subtitle }}</p>
|
||||
|
||||
<!-- Scores -->
|
||||
<div class="bg-black/30 rounded-xl p-6 mb-8 space-y-4">
|
||||
<div class="flex items-center justify-between">
|
||||
<span class="text-gray-300 font-medium">{{ playerName }}</span>
|
||||
<span
|
||||
class="text-3xl font-black tabular-nums transition-all duration-500"
|
||||
:class="[
|
||||
playerScore > opponentScore
|
||||
? 'text-emerald-400 drop-shadow-[0_0_12px_rgba(52,211,153,0.6)]'
|
||||
: 'text-white',
|
||||
]"
|
||||
>
|
||||
{{ displayPlayerScore }}
|
||||
</span>
|
||||
</div>
|
||||
<div class="h-px bg-gray-700"></div>
|
||||
<div class="flex items-center justify-between">
|
||||
<span class="text-gray-300 font-medium">{{ opponentName }}</span>
|
||||
<span
|
||||
class="text-3xl font-black tabular-nums transition-all duration-500"
|
||||
:class="[
|
||||
opponentScore > playerScore
|
||||
? 'text-rose-400 drop-shadow-[0_0_12px_rgba(251,113,133,0.6)]'
|
||||
: 'text-white',
|
||||
]"
|
||||
>
|
||||
{{ displayOpponentScore }}
|
||||
</span>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<!-- Stats (optional) -->
|
||||
<div v-if="stats" class="grid grid-cols-2 gap-4 mb-8">
|
||||
<div class="bg-black/20 rounded-lg p-3 text-center">
|
||||
<p class="text-gray-400 text-xs mb-1">Your Tricks</p>
|
||||
<p class="text-2xl font-bold text-white">{{ stats.playerTricks }}</p>
|
||||
</div>
|
||||
<div class="bg-black/20 rounded-lg p-3 text-center">
|
||||
<p class="text-gray-400 text-xs mb-1">Opponent Tricks</p>
|
||||
<p class="text-2xl font-bold text-white">{{ stats.opponentTricks }}</p>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<!-- Actions -->
|
||||
<div class="flex gap-3">
|
||||
<button
|
||||
@click="$emit('play-again')"
|
||||
class="flex-1 bg-gradient-to-r from-emerald-500 to-emerald-600 hover:from-emerald-600 hover:to-emerald-700 text-white font-bold py-3 px-6 rounded-lg transition-all duration-200 hover:scale-105 active:scale-95 shadow-lg"
|
||||
>
|
||||
Play Again
|
||||
</button>
|
||||
<button
|
||||
v-if="!hideClose"
|
||||
@click="handleClose"
|
||||
class="flex-1 bg-gray-700 hover:bg-gray-600 text-white font-bold py-3 px-6 rounded-lg transition-all duration-200 hover:scale-105 active:scale-95"
|
||||
>
|
||||
Close
|
||||
</button>
|
||||
</div>
|
||||
</div>
|
||||
</Transition>
|
||||
</div>
|
||||
</Transition>
|
||||
</Teleport>
|
||||
</template>
|
||||
|
||||
<script setup>
|
||||
import { ref, computed, watch, onMounted } from 'vue'
|
||||
|
||||
const props = defineProps({
|
||||
isVisible: {
|
||||
type: Boolean,
|
||||
required: true,
|
||||
},
|
||||
winner: {
|
||||
type: String,
|
||||
required: true,
|
||||
validator: (value) => ['player', 'opponent', 'draw'].includes(value),
|
||||
},
|
||||
playerScore: {
|
||||
type: Number,
|
||||
required: true,
|
||||
},
|
||||
opponentScore: {
|
||||
type: Number,
|
||||
required: true,
|
||||
},
|
||||
playerName: {
|
||||
type: String,
|
||||
default: 'You',
|
||||
},
|
||||
opponentName: {
|
||||
type: String,
|
||||
default: 'Bot',
|
||||
},
|
||||
stats: {
|
||||
type: Object,
|
||||
default: null,
|
||||
},
|
||||
hideClose: {
|
||||
type: Boolean,
|
||||
default: false,
|
||||
},
|
||||
})
|
||||
|
||||
const emit = defineEmits(['close', 'play-again'])
|
||||
|
||||
const showConfetti = ref(false)
|
||||
const displayPlayerScore = ref(0)
|
||||
const displayOpponentScore = ref(0)
|
||||
|
||||
// Computed title based on winner
|
||||
const title = computed(() => {
|
||||
if (props.winner === 'player') return 'Victory!'
|
||||
if (props.winner === 'opponent') return 'Defeat'
|
||||
return 'Draw!'
|
||||
})
|
||||
|
||||
// Computed subtitle
|
||||
const subtitle = computed(() => {
|
||||
if (props.winner === 'player') return 'Congratulations! You won the game!'
|
||||
if (props.winner === 'opponent') return 'Better luck next time!'
|
||||
return 'The game ended in a draw'
|
||||
})
|
||||
|
||||
// Animate score count-up
|
||||
const animateScore = (fromValue, toValue, callback) => {
|
||||
const duration = 1000
|
||||
const steps = 30
|
||||
const stepDuration = duration / steps
|
||||
const increment = (toValue - fromValue) / steps
|
||||
|
||||
let current = fromValue
|
||||
let step = 0
|
||||
|
||||
const interval = setInterval(() => {
|
||||
step++
|
||||
current += increment
|
||||
|
||||
if (step >= steps) {
|
||||
current = toValue
|
||||
clearInterval(interval)
|
||||
}
|
||||
|
||||
callback(Math.round(current))
|
||||
}, stepDuration)
|
||||
}
|
||||
|
||||
// Watch for visibility and trigger animations
|
||||
watch(
|
||||
() => props.isVisible,
|
||||
(newValue) => {
|
||||
if (newValue) {
|
||||
// Start confetti if player wins
|
||||
if (props.winner === 'player') {
|
||||
setTimeout(() => {
|
||||
showConfetti.value = true
|
||||
}, 300)
|
||||
}
|
||||
|
||||
// Animate scores counting up
|
||||
setTimeout(() => {
|
||||
animateScore(0, props.playerScore, (value) => {
|
||||
displayPlayerScore.value = value
|
||||
})
|
||||
animateScore(0, props.opponentScore, (value) => {
|
||||
displayOpponentScore.value = value
|
||||
})
|
||||
}, 400)
|
||||
} else {
|
||||
showConfetti.value = false
|
||||
displayPlayerScore.value = 0
|
||||
displayOpponentScore.value = 0
|
||||
}
|
||||
},
|
||||
{ immediate: true },
|
||||
)
|
||||
|
||||
const handleClose = () => {
|
||||
emit('close')
|
||||
}
|
||||
</script>
|
||||
|
||||
<style scoped>
|
||||
@keyframes confetti {
|
||||
0% {
|
||||
transform: translateY(0) rotate(0deg);
|
||||
opacity: 1;
|
||||
}
|
||||
100% {
|
||||
transform: translateY(100vh) rotate(720deg);
|
||||
opacity: 0;
|
||||
}
|
||||
}
|
||||
|
||||
.animate-confetti {
|
||||
animation: confetti linear forwards;
|
||||
}
|
||||
</style>
|
||||
@@ -0,0 +1,100 @@
|
||||
<template>
|
||||
<div class="flex items-center justify-center h-64 relative">
|
||||
<div class="relative w-96 h-full flex items-center justify-center">
|
||||
<!-- Opponent Card Slot -->
|
||||
<div
|
||||
class="absolute top-4"
|
||||
:class="[
|
||||
winner === 'opponent' && 'ring-4 ring-green-400 ring-offset-4 rounded-lg animate-pulse',
|
||||
]"
|
||||
:style="{ zIndex: opponentCard ? (firstPlayer === 'opponent' ? 1 : 2) : 0 }"
|
||||
>
|
||||
<div
|
||||
v-if="!opponentCard"
|
||||
class="w-32 aspect-[2.5/3.5] border-4 border-dashed border-gray-400/40 rounded-lg bg-gray-100/20 flex items-center justify-center"
|
||||
>
|
||||
<span class="text-gray-400 text-xs font-medium">Opponent</span>
|
||||
</div>
|
||||
<Transition
|
||||
enter-active-class="transition-all duration-500 ease-out"
|
||||
enter-from-class="opacity-0 -translate-y-[200px] scale-75"
|
||||
enter-to-class="opacity-100 translate-y-0 scale-100 rotate-6"
|
||||
leave-active-class="transition-all duration-500 ease-in"
|
||||
:leave-from-class="`opacity-100 translate-y-0 scale-100 rotate-6`"
|
||||
:leave-to-class="
|
||||
winner === 'opponent'
|
||||
? 'opacity-0 -translate-y-[200px] translate-x-[300px] scale-50 rotate-0'
|
||||
: 'opacity-0 translate-y-[200px] translate-x-[300px] scale-50 rotate-0'
|
||||
"
|
||||
mode="out-in"
|
||||
>
|
||||
<div
|
||||
v-if="opponentCard"
|
||||
:key="`${opponentCard.suit}-${opponentCard.rank}`"
|
||||
class="rotate-6"
|
||||
>
|
||||
<GameCard :suit="opponentCard.suit" :rank="opponentCard.rank" />
|
||||
</div>
|
||||
</Transition>
|
||||
</div>
|
||||
|
||||
<!-- Player Card Slot -->
|
||||
<div
|
||||
class="absolute bottom-4"
|
||||
:class="[
|
||||
winner === 'player' && 'ring-4 ring-green-400 ring-offset-4 rounded-lg animate-pulse',
|
||||
]"
|
||||
:style="{ zIndex: playerCard ? (firstPlayer === 'player' ? 1 : 2) : 0 }"
|
||||
>
|
||||
<div
|
||||
v-if="!playerCard"
|
||||
class="w-32 aspect-[2.5/3.5] border-4 border-dashed border-gray-400/40 rounded-lg bg-gray-100/20 flex items-center justify-center"
|
||||
>
|
||||
<span class="text-gray-400 text-xs font-medium">You</span>
|
||||
</div>
|
||||
<Transition
|
||||
enter-active-class="transition-all duration-500 ease-out"
|
||||
enter-from-class="opacity-0 translate-y-[200px] scale-75"
|
||||
enter-to-class="opacity-100 translate-y-0 scale-100 -rotate-6"
|
||||
leave-active-class="transition-all duration-500 ease-in"
|
||||
:leave-from-class="`opacity-100 translate-y-0 scale-100 -rotate-6`"
|
||||
:leave-to-class="
|
||||
winner === 'opponent'
|
||||
? 'opacity-0 -translate-y-[200px] translate-x-[300px] scale-50 rotate-0'
|
||||
: 'opacity-0 translate-y-[200px] translate-x-[300px] scale-50 rotate-0'
|
||||
"
|
||||
mode="out-in"
|
||||
>
|
||||
<div v-if="playerCard" :key="`${playerCard.suit}-${playerCard.rank}`" class="-rotate-6">
|
||||
<GameCard :suit="playerCard.suit" :rank="playerCard.rank" />
|
||||
</div>
|
||||
</Transition>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</template>
|
||||
|
||||
<script setup>
|
||||
import GameCard from './GameCard.vue'
|
||||
|
||||
const props = defineProps({
|
||||
playerCard: {
|
||||
type: Object,
|
||||
default: null,
|
||||
},
|
||||
opponentCard: {
|
||||
type: Object,
|
||||
default: null,
|
||||
},
|
||||
winner: {
|
||||
type: String,
|
||||
default: null,
|
||||
validator: (value) => [null, 'player', 'opponent'].includes(value),
|
||||
},
|
||||
firstPlayer: {
|
||||
type: String,
|
||||
default: null,
|
||||
validator: (value) => [null, 'player', 'opponent'].includes(value),
|
||||
},
|
||||
})
|
||||
</script>
|
||||
@@ -0,0 +1,98 @@
|
||||
<template>
|
||||
<div class="flex justify-center items-end relative h-40">
|
||||
<TransitionGroup
|
||||
enter-active-class="transition-all duration-500 ease-out"
|
||||
enter-from-class="opacity-0 -translate-y-8 scale-75"
|
||||
enter-to-class="opacity-100 translate-y-0 scale-100"
|
||||
>
|
||||
<div
|
||||
v-for="(card, index) in cards"
|
||||
:key="`${card.suit}-${card.rank}-${index}`"
|
||||
class="absolute transition-all duration-300 ease-out"
|
||||
:style="getCardStyle(index)"
|
||||
@mouseenter="hoveredIndex = index"
|
||||
@mouseleave="hoveredIndex = null"
|
||||
@click="$emit('card-clicked', { card, index })"
|
||||
>
|
||||
<div
|
||||
:class="[
|
||||
'transition-all duration-300',
|
||||
isPlayable(index) && 'ring-2 ring-yellow-400 ring-offset-2 rounded-lg',
|
||||
hoveredIndex === index && 'scale-110 -translate-y-4 z-50',
|
||||
]"
|
||||
>
|
||||
<GameCard :suit="card.suit" :rank="card.rank" :face-down="faceDown" />
|
||||
</div>
|
||||
</div>
|
||||
</TransitionGroup>
|
||||
</div>
|
||||
</template>
|
||||
|
||||
<script setup>
|
||||
import { ref, computed } from 'vue'
|
||||
import GameCard from './GameCard.vue'
|
||||
|
||||
const props = defineProps({
|
||||
cards: {
|
||||
type: Array,
|
||||
required: true,
|
||||
default: () => [],
|
||||
},
|
||||
faceDown: {
|
||||
type: Boolean,
|
||||
default: false,
|
||||
},
|
||||
maxCards: {
|
||||
type: Number,
|
||||
default: 3,
|
||||
validator: (value) => [3, 9].includes(value),
|
||||
},
|
||||
playableCards: {
|
||||
type: Array,
|
||||
default: () => [],
|
||||
},
|
||||
})
|
||||
|
||||
defineEmits(['card-clicked'])
|
||||
|
||||
const hoveredIndex = ref(null)
|
||||
|
||||
// Calculate overlap percentage based on max cards
|
||||
// 3 cards: 70% overlap, 9 cards: 85% overlap
|
||||
const overlapPercentage = computed(() => {
|
||||
return props.maxCards === 3 ? 0.7 : 0.85
|
||||
})
|
||||
|
||||
// Calculate card spacing dynamically
|
||||
const cardWidth = 128 // w-32 from GameCard component
|
||||
const getCardStyle = (index) => {
|
||||
const totalCards = props.cards.length
|
||||
const overlapOffset = cardWidth * (1 - overlapPercentage.value)
|
||||
|
||||
// Calculate total width needed
|
||||
const totalWidth = cardWidth + (totalCards - 1) * overlapOffset
|
||||
const startOffset = -totalWidth / 2 + cardWidth / 2
|
||||
|
||||
// Base position
|
||||
let left = startOffset + index * overlapOffset
|
||||
|
||||
// If a card is hovered, shift neighbors
|
||||
if (hoveredIndex.value !== null && hoveredIndex.value !== index) {
|
||||
if (index < hoveredIndex.value) {
|
||||
left -= 10 // Push left cards more to the left
|
||||
} else if (index > hoveredIndex.value) {
|
||||
left += 10 // Push right cards more to the right
|
||||
}
|
||||
}
|
||||
|
||||
return {
|
||||
left: `calc(50% + ${left}px)`,
|
||||
zIndex: hoveredIndex.value === index ? 50 : index + 1,
|
||||
}
|
||||
}
|
||||
|
||||
// Check if a card is playable
|
||||
const isPlayable = (index) => {
|
||||
return props.playableCards.includes(index)
|
||||
}
|
||||
</script>
|
||||
@@ -0,0 +1,176 @@
|
||||
<template>
|
||||
<div
|
||||
class="inline-flex bg-gray-900/80 backdrop-blur-sm border border-gray-700/50 shadow-xl rounded-md px-4 py-1.5 items-center gap-4"
|
||||
>
|
||||
<!-- Opponent Score (Left) -->
|
||||
<div class="flex items-center gap-2">
|
||||
<div class="flex flex-col items-start">
|
||||
<span class="text-gray-400 text-[10px] uppercase tracking-wide mb-0.5">{{
|
||||
opponentName
|
||||
}}</span>
|
||||
<span
|
||||
class="text-2xl font-black tabular-nums transition-all duration-300"
|
||||
:class="[
|
||||
opponentScore > playerScore
|
||||
? 'text-amber-400 drop-shadow-[0_0_12px_rgba(251,191,36,0.6)]'
|
||||
: 'text-white',
|
||||
scoreChanged === 'opponent' && 'scale-110',
|
||||
]"
|
||||
>
|
||||
{{ displayOpponentScore }}
|
||||
</span>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<!-- Center Turn Indicator -->
|
||||
<div class="flex flex-col items-center gap-1">
|
||||
<div
|
||||
class="px-3 py-0.5 rounded-full text-[9px] font-bold uppercase tracking-wider transition-all duration-300"
|
||||
:class="[
|
||||
currentTurn === 'player'
|
||||
? 'bg-emerald-500/20 text-emerald-400 border border-emerald-500/40 shadow-[0_0_10px_rgba(52,211,153,0.4)]'
|
||||
: 'bg-rose-500/20 text-rose-400 border border-rose-500/40 shadow-[0_0_10px_rgba(251,113,133,0.4)]',
|
||||
turnChanged && 'animate-pulse',
|
||||
]"
|
||||
>
|
||||
{{ currentTurn === 'player' ? 'Your Turn' : "Bot's Turn" }}
|
||||
</div>
|
||||
<span v-if="roundNumber" class="text-gray-500 text-[10px] font-medium">
|
||||
Round {{ roundNumber }}
|
||||
</span>
|
||||
</div>
|
||||
|
||||
<!-- Player Score (Right) -->
|
||||
<div class="flex items-center gap-2">
|
||||
<div class="flex flex-col items-end">
|
||||
<span class="text-gray-400 text-[10px] uppercase tracking-wide mb-0.5">{{
|
||||
playerName
|
||||
}}</span>
|
||||
<span
|
||||
class="text-2xl font-black tabular-nums transition-all duration-300"
|
||||
:class="[
|
||||
playerScore > opponentScore
|
||||
? 'text-amber-400 drop-shadow-[0_0_12px_rgba(251,191,36,0.6)]'
|
||||
: 'text-white',
|
||||
scoreChanged === 'player' && 'scale-110',
|
||||
]"
|
||||
>
|
||||
{{ displayPlayerScore }}
|
||||
</span>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</template>
|
||||
|
||||
<script setup>
|
||||
import { ref, watch, computed } from 'vue'
|
||||
|
||||
const props = defineProps({
|
||||
playerScore: {
|
||||
type: Number,
|
||||
required: true,
|
||||
},
|
||||
opponentScore: {
|
||||
type: Number,
|
||||
required: true,
|
||||
},
|
||||
playerName: {
|
||||
type: String,
|
||||
default: 'You',
|
||||
},
|
||||
opponentName: {
|
||||
type: String,
|
||||
default: 'Bot',
|
||||
},
|
||||
currentTurn: {
|
||||
type: String,
|
||||
default: 'player',
|
||||
validator: (value) => ['player', 'opponent'].includes(value),
|
||||
},
|
||||
roundNumber: {
|
||||
type: Number,
|
||||
default: null,
|
||||
},
|
||||
})
|
||||
|
||||
const scoreChanged = ref(null)
|
||||
const turnChanged = ref(false)
|
||||
const displayPlayerScore = ref(props.playerScore)
|
||||
const displayOpponentScore = ref(props.opponentScore)
|
||||
|
||||
// Animate score count-up
|
||||
const animateScore = (fromValue, toValue, callback) => {
|
||||
const duration = 500 // Total animation duration in ms
|
||||
const steps = 20 // Number of increments
|
||||
const stepDuration = duration / steps
|
||||
const increment = (toValue - fromValue) / steps
|
||||
|
||||
let current = fromValue
|
||||
let step = 0
|
||||
|
||||
const interval = setInterval(() => {
|
||||
step++
|
||||
current += increment
|
||||
|
||||
if (step >= steps) {
|
||||
current = toValue
|
||||
clearInterval(interval)
|
||||
}
|
||||
|
||||
callback(Math.round(current))
|
||||
}, stepDuration)
|
||||
}
|
||||
|
||||
// Watch for player score changes and trigger count-up animation
|
||||
watch(
|
||||
() => props.playerScore,
|
||||
(newScore, oldScore) => {
|
||||
scoreChanged.value = 'player'
|
||||
|
||||
if (oldScore !== undefined && newScore !== oldScore) {
|
||||
animateScore(oldScore, newScore, (value) => {
|
||||
displayPlayerScore.value = value
|
||||
})
|
||||
} else {
|
||||
displayPlayerScore.value = newScore
|
||||
}
|
||||
|
||||
setTimeout(() => {
|
||||
scoreChanged.value = null
|
||||
}, 300)
|
||||
},
|
||||
{ immediate: true },
|
||||
)
|
||||
|
||||
// Watch for opponent score changes and trigger count-up animation
|
||||
watch(
|
||||
() => props.opponentScore,
|
||||
(newScore, oldScore) => {
|
||||
scoreChanged.value = 'opponent'
|
||||
|
||||
if (oldScore !== undefined && newScore !== oldScore) {
|
||||
animateScore(oldScore, newScore, (value) => {
|
||||
displayOpponentScore.value = value
|
||||
})
|
||||
} else {
|
||||
displayOpponentScore.value = newScore
|
||||
}
|
||||
|
||||
setTimeout(() => {
|
||||
scoreChanged.value = null
|
||||
}, 300)
|
||||
},
|
||||
{ immediate: true },
|
||||
)
|
||||
|
||||
// Watch for turn changes and trigger pulse animation
|
||||
watch(
|
||||
() => props.currentTurn,
|
||||
() => {
|
||||
turnChanged.value = true
|
||||
setTimeout(() => {
|
||||
turnChanged.value = false
|
||||
}, 1000)
|
||||
},
|
||||
)
|
||||
</script>
|
||||
@@ -0,0 +1,342 @@
|
||||
<template>
|
||||
<div class="min-h-screen bg-gradient-to-br from-green-700 via-green-800 to-green-900 p-8">
|
||||
<div class="max-w-6xl mx-auto">
|
||||
<!-- Title -->
|
||||
<h1 class="text-white text-3xl font-bold mb-8 text-center">
|
||||
All Animations Test Page
|
||||
</h1>
|
||||
|
||||
<!-- Control Panel -->
|
||||
<div class="bg-gray-900/80 rounded-lg p-6 mb-8">
|
||||
<h2 class="text-white text-xl font-semibold mb-4">Controls</h2>
|
||||
|
||||
<!-- Score Animation Tests -->
|
||||
<div class="mb-6">
|
||||
<h3 class="text-emerald-400 font-semibold mb-3">Score Count-Up Animation</h3>
|
||||
<div class="flex flex-wrap gap-3">
|
||||
<button
|
||||
@click="increasePlayerScore"
|
||||
class="px-4 py-2 bg-green-600 hover:bg-green-700 text-white rounded-lg font-medium transition-colors"
|
||||
>
|
||||
+10 Player Score
|
||||
</button>
|
||||
<button
|
||||
@click="increaseOpponentScore"
|
||||
class="px-4 py-2 bg-red-600 hover:bg-red-700 text-white rounded-lg font-medium transition-colors"
|
||||
>
|
||||
+10 Opponent Score
|
||||
</button>
|
||||
<button
|
||||
@click="resetScores"
|
||||
class="px-4 py-2 bg-gray-600 hover:bg-gray-700 text-white rounded-lg font-medium transition-colors"
|
||||
>
|
||||
Reset Scores
|
||||
</button>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<!-- Turn Transition Tests -->
|
||||
<div class="mb-6">
|
||||
<h3 class="text-blue-400 font-semibold mb-3">Turn Transition Animation</h3>
|
||||
<div class="flex flex-wrap gap-3">
|
||||
<button
|
||||
@click="toggleTurn"
|
||||
class="px-4 py-2 bg-blue-600 hover:bg-blue-700 text-white rounded-lg font-medium transition-colors"
|
||||
>
|
||||
Switch Turn
|
||||
</button>
|
||||
<button
|
||||
@click="currentTurn = 'player'"
|
||||
class="px-4 py-2 bg-emerald-600 hover:bg-emerald-700 text-white rounded-lg font-medium transition-colors"
|
||||
>
|
||||
Your Turn
|
||||
</button>
|
||||
<button
|
||||
@click="currentTurn = 'opponent'"
|
||||
class="px-4 py-2 bg-rose-600 hover:bg-rose-700 text-white rounded-lg font-medium transition-colors"
|
||||
>
|
||||
Bot's Turn
|
||||
</button>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<!-- Trump Reveal Tests -->
|
||||
<div class="mb-6">
|
||||
<h3 class="text-amber-400 font-semibold mb-3">Trump Reveal Animation</h3>
|
||||
<div class="flex flex-wrap gap-3">
|
||||
<button
|
||||
@click="triggerTrumpReveal"
|
||||
class="px-4 py-2 bg-amber-600 hover:bg-amber-700 text-white rounded-lg font-medium transition-colors"
|
||||
>
|
||||
Reveal Trump
|
||||
</button>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<!-- Game Over Tests -->
|
||||
<div class="mb-6">
|
||||
<h3 class="text-purple-400 font-semibold mb-3">Game Over Screen</h3>
|
||||
<div class="flex flex-wrap gap-3">
|
||||
<button
|
||||
@click="showGameOverPlayerWin"
|
||||
class="px-4 py-2 bg-emerald-600 hover:bg-emerald-700 text-white rounded-lg font-medium transition-colors"
|
||||
>
|
||||
Player Wins
|
||||
</button>
|
||||
<button
|
||||
@click="showGameOverOpponentWin"
|
||||
class="px-4 py-2 bg-rose-600 hover:bg-rose-700 text-white rounded-lg font-medium transition-colors"
|
||||
>
|
||||
Opponent Wins
|
||||
</button>
|
||||
<button
|
||||
@click="showGameOverDraw"
|
||||
class="px-4 py-2 bg-gray-600 hover:bg-gray-700 text-white rounded-lg font-medium transition-colors"
|
||||
>
|
||||
Draw
|
||||
</button>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<!-- Full Sequence -->
|
||||
<div>
|
||||
<h3 class="text-pink-400 font-semibold mb-3">Complete Sequence</h3>
|
||||
<div class="flex flex-wrap gap-3">
|
||||
<button
|
||||
@click="runFullSequence"
|
||||
:disabled="isRunningSequence"
|
||||
class="px-4 py-2 bg-pink-600 hover:bg-pink-700 disabled:bg-gray-600 disabled:cursor-not-allowed text-white rounded-lg font-medium transition-colors"
|
||||
>
|
||||
Run Full Animation Sequence
|
||||
</button>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<!-- Game Board Preview -->
|
||||
<div class="grid grid-rows-[auto_1fr] gap-8">
|
||||
<!-- Score Display -->
|
||||
<div class="flex justify-center">
|
||||
<ScoreDisplay
|
||||
:player-score="playerScore"
|
||||
:opponent-score="opponentScore"
|
||||
:current-turn="currentTurn"
|
||||
:round-number="1"
|
||||
/>
|
||||
</div>
|
||||
|
||||
<!-- Deck Area (with trump) -->
|
||||
<div class="flex justify-center">
|
||||
<div class="bg-gray-800/30 rounded-lg p-6">
|
||||
<h3 class="text-white text-sm font-semibold mb-4 text-center">Trump Card</h3>
|
||||
<DeckArea
|
||||
:trump-card="trumpCard"
|
||||
:cards-remaining="cardsRemaining"
|
||||
:is-empty="false"
|
||||
:reveal-trump="revealTrump"
|
||||
/>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<!-- Instructions -->
|
||||
<div class="bg-gray-900/80 rounded-lg p-6 mt-8 text-gray-300">
|
||||
<h3 class="text-white font-semibold mb-2">Instructions:</h3>
|
||||
<ul class="list-disc list-inside space-y-1 text-sm">
|
||||
<li>
|
||||
<strong>Score Count-Up:</strong> Click "+10" buttons to see scores animate smoothly
|
||||
from old to new value
|
||||
</li>
|
||||
<li>
|
||||
<strong>Turn Transition:</strong> Switch turns to see the indicator pulse and glow with
|
||||
color change
|
||||
</li>
|
||||
<li>
|
||||
<strong>Trump Reveal:</strong> Triggers a 3-second pulse/glow animation on the trump
|
||||
card with label
|
||||
</li>
|
||||
<li>
|
||||
<strong>Game Over:</strong> Shows victory/defeat modal with score count-up and confetti
|
||||
(on win)
|
||||
</li>
|
||||
<li>
|
||||
<strong>Full Sequence:</strong> Runs all animations in order: trump reveal → turn
|
||||
changes → score updates → game over
|
||||
</li>
|
||||
</ul>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<!-- Game Over Modal -->
|
||||
<GameOver
|
||||
:is-visible="gameOverVisible"
|
||||
:winner="gameOverWinner"
|
||||
:player-score="playerScore"
|
||||
:opponent-score="opponentScore"
|
||||
:stats="gameOverStats"
|
||||
@close="gameOverVisible = false"
|
||||
@play-again="handlePlayAgain"
|
||||
/>
|
||||
</div>
|
||||
</template>
|
||||
|
||||
<script setup>
|
||||
import { ref } from 'vue'
|
||||
import ScoreDisplay from '@/components/game/ScoreDisplay.vue'
|
||||
import DeckArea from '@/components/game/DeckArea.vue'
|
||||
import GameOver from '@/components/game/GameOver.vue'
|
||||
|
||||
// Game state
|
||||
const playerScore = ref(23)
|
||||
const opponentScore = ref(15)
|
||||
const currentTurn = ref('player')
|
||||
const cardsRemaining = ref(30)
|
||||
const revealTrump = ref(false)
|
||||
const isRunningSequence = ref(false)
|
||||
|
||||
// Trump card
|
||||
const trumpCard = ref({ suit: 'o', rank: 7 })
|
||||
|
||||
// Game Over state
|
||||
const gameOverVisible = ref(false)
|
||||
const gameOverWinner = ref('player')
|
||||
const gameOverStats = ref({
|
||||
playerTricks: 8,
|
||||
opponentTricks: 6,
|
||||
})
|
||||
|
||||
// Score manipulation
|
||||
const increasePlayerScore = () => {
|
||||
playerScore.value += 10
|
||||
}
|
||||
|
||||
const increaseOpponentScore = () => {
|
||||
opponentScore.value += 10
|
||||
}
|
||||
|
||||
const resetScores = () => {
|
||||
playerScore.value = 0
|
||||
opponentScore.value = 0
|
||||
}
|
||||
|
||||
// Turn control
|
||||
const toggleTurn = () => {
|
||||
currentTurn.value = currentTurn.value === 'player' ? 'opponent' : 'player'
|
||||
}
|
||||
|
||||
// Trump reveal
|
||||
const triggerTrumpReveal = () => {
|
||||
revealTrump.value = true
|
||||
setTimeout(() => {
|
||||
revealTrump.value = false
|
||||
}, 100)
|
||||
}
|
||||
|
||||
// Game Over screens
|
||||
const showGameOverPlayerWin = () => {
|
||||
playerScore.value = 67
|
||||
opponentScore.value = 53
|
||||
gameOverWinner.value = 'player'
|
||||
gameOverStats.value = {
|
||||
playerTricks: 11,
|
||||
opponentTricks: 9,
|
||||
}
|
||||
gameOverVisible.value = true
|
||||
}
|
||||
|
||||
const showGameOverOpponentWin = () => {
|
||||
playerScore.value = 45
|
||||
opponentScore.value = 75
|
||||
gameOverWinner.value = 'opponent'
|
||||
gameOverStats.value = {
|
||||
playerTricks: 7,
|
||||
opponentTricks: 13,
|
||||
}
|
||||
gameOverVisible.value = true
|
||||
}
|
||||
|
||||
const showGameOverDraw = () => {
|
||||
playerScore.value = 60
|
||||
opponentScore.value = 60
|
||||
gameOverWinner.value = 'draw'
|
||||
gameOverStats.value = {
|
||||
playerTricks: 10,
|
||||
opponentTricks: 10,
|
||||
}
|
||||
gameOverVisible.value = true
|
||||
}
|
||||
|
||||
const handlePlayAgain = () => {
|
||||
gameOverVisible.value = false
|
||||
resetScores()
|
||||
currentTurn.value = 'player'
|
||||
cardsRemaining.value = 40
|
||||
}
|
||||
|
||||
// Full animation sequence
|
||||
const runFullSequence = async () => {
|
||||
if (isRunningSequence.value) return
|
||||
isRunningSequence.value = true
|
||||
|
||||
// Reset everything
|
||||
resetScores()
|
||||
currentTurn.value = 'player'
|
||||
gameOverVisible.value = false
|
||||
await new Promise((resolve) => setTimeout(resolve, 500))
|
||||
|
||||
// 1. Trump Reveal
|
||||
console.log('1. Trump Reveal')
|
||||
triggerTrumpReveal()
|
||||
await new Promise((resolve) => setTimeout(resolve, 3500))
|
||||
|
||||
// 2. First turn
|
||||
console.log('2. Player turn')
|
||||
currentTurn.value = 'player'
|
||||
await new Promise((resolve) => setTimeout(resolve, 1500))
|
||||
|
||||
// 3. Player scores
|
||||
console.log('3. Player scores')
|
||||
playerScore.value = 11
|
||||
await new Promise((resolve) => setTimeout(resolve, 2000))
|
||||
|
||||
// 4. Switch turn
|
||||
console.log('4. Switch to opponent turn')
|
||||
currentTurn.value = 'opponent'
|
||||
await new Promise((resolve) => setTimeout(resolve, 1500))
|
||||
|
||||
// 5. Opponent scores
|
||||
console.log('5. Opponent scores')
|
||||
opponentScore.value = 10
|
||||
await new Promise((resolve) => setTimeout(resolve, 2000))
|
||||
|
||||
// 6. Back to player
|
||||
console.log('6. Back to player')
|
||||
currentTurn.value = 'player'
|
||||
await new Promise((resolve) => setTimeout(resolve, 1000))
|
||||
|
||||
// 7. More scoring
|
||||
console.log('7. More scoring')
|
||||
playerScore.value = 23
|
||||
await new Promise((resolve) => setTimeout(resolve, 1500))
|
||||
opponentScore.value = 19
|
||||
await new Promise((resolve) => setTimeout(resolve, 1500))
|
||||
playerScore.value = 34
|
||||
await new Promise((resolve) => setTimeout(resolve, 1500))
|
||||
|
||||
// 8. Final scores and game over
|
||||
console.log('8. Game over')
|
||||
playerScore.value = 67
|
||||
opponentScore.value = 53
|
||||
await new Promise((resolve) => setTimeout(resolve, 2000))
|
||||
|
||||
// Show game over
|
||||
gameOverWinner.value = 'player'
|
||||
gameOverStats.value = {
|
||||
playerTricks: 11,
|
||||
opponentTricks: 9,
|
||||
}
|
||||
gameOverVisible.value = true
|
||||
|
||||
isRunningSequence.value = false
|
||||
}
|
||||
</script>
|
||||
@@ -0,0 +1,252 @@
|
||||
<template>
|
||||
<div class="min-h-screen bg-gradient-to-br from-green-700 via-green-800 to-green-900 p-8">
|
||||
<div class="max-w-4xl mx-auto">
|
||||
<!-- Title -->
|
||||
<h1 class="text-white text-3xl font-bold mb-8 text-center">Animation Test Page</h1>
|
||||
|
||||
<!-- Control Panel -->
|
||||
<div class="bg-gray-900/80 rounded-lg p-6 mb-8">
|
||||
<h2 class="text-white text-xl font-semibold mb-4">Controls</h2>
|
||||
<div class="flex flex-wrap gap-3">
|
||||
<button
|
||||
@click="playPlayerCard"
|
||||
class="px-4 py-2 bg-green-600 hover:bg-green-700 text-white rounded-lg font-medium transition-colors"
|
||||
>
|
||||
Play Player Card
|
||||
</button>
|
||||
<button
|
||||
@click="playOpponentCard"
|
||||
class="px-4 py-2 bg-red-600 hover:bg-red-700 text-white rounded-lg font-medium transition-colors"
|
||||
>
|
||||
Play Opponent Card
|
||||
</button>
|
||||
<button
|
||||
@click="playBothCards"
|
||||
class="px-4 py-2 bg-blue-600 hover:bg-blue-700 text-white rounded-lg font-medium transition-colors"
|
||||
>
|
||||
Play Both Cards
|
||||
</button>
|
||||
<button
|
||||
@click="clearCards"
|
||||
class="px-4 py-2 bg-gray-600 hover:bg-gray-700 text-white rounded-lg font-medium transition-colors"
|
||||
>
|
||||
Clear Cards
|
||||
</button>
|
||||
<button
|
||||
@click="setPlayerWinner"
|
||||
class="px-4 py-2 bg-emerald-600 hover:bg-emerald-700 text-white rounded-lg font-medium transition-colors"
|
||||
>
|
||||
Player Wins
|
||||
</button>
|
||||
<button
|
||||
@click="setOpponentWinner"
|
||||
class="px-4 py-2 bg-rose-600 hover:bg-rose-700 text-white rounded-lg font-medium transition-colors"
|
||||
>
|
||||
Opponent Wins
|
||||
</button>
|
||||
<button
|
||||
@click="fullSequence"
|
||||
class="px-4 py-2 bg-purple-600 hover:bg-purple-700 text-white rounded-lg font-medium transition-colors"
|
||||
>
|
||||
Full Trick Sequence
|
||||
</button>
|
||||
<button
|
||||
@click="collectTrickPlayerWins"
|
||||
:disabled="!playerCard || !opponentCard"
|
||||
class="px-4 py-2 bg-cyan-600 hover:bg-cyan-700 disabled:bg-gray-600 disabled:cursor-not-allowed text-white rounded-lg font-medium transition-colors"
|
||||
>
|
||||
Collect Trick (Player Wins)
|
||||
</button>
|
||||
<button
|
||||
@click="collectTrickOpponentWins"
|
||||
:disabled="!playerCard || !opponentCard"
|
||||
class="px-4 py-2 bg-orange-600 hover:bg-orange-700 disabled:bg-gray-600 disabled:cursor-not-allowed text-white rounded-lg font-medium transition-colors"
|
||||
>
|
||||
Collect Trick (Opponent Wins)
|
||||
</button>
|
||||
</div>
|
||||
|
||||
<!-- Current State Display -->
|
||||
<div class="mt-4 text-gray-300 text-sm">
|
||||
<p>
|
||||
<strong>Player Card:</strong>
|
||||
{{ playerCard ? `${playerCard.suit}${playerCard.rank}` : 'None' }}
|
||||
</p>
|
||||
<p>
|
||||
<strong>Opponent Card:</strong>
|
||||
{{ opponentCard ? `${opponentCard.suit}${opponentCard.rank}` : 'None' }}
|
||||
</p>
|
||||
<p><strong>Winner:</strong> {{ winner || 'None' }}</p>
|
||||
<p><strong>First Player:</strong> {{ firstPlayer || 'None' }}</p>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<!-- PlayArea Component -->
|
||||
<div class="bg-gray-800/50 rounded-lg p-8">
|
||||
<h2 class="text-white text-xl font-semibold mb-4 text-center">Play Area</h2>
|
||||
<PlayArea
|
||||
:player-card="playerCard"
|
||||
:opponent-card="opponentCard"
|
||||
:winner="winner"
|
||||
:first-player="firstPlayer"
|
||||
/>
|
||||
</div>
|
||||
|
||||
<!-- Instructions -->
|
||||
<div class="bg-gray-900/80 rounded-lg p-6 mt-8 text-gray-300">
|
||||
<h3 class="text-white font-semibold mb-2">Instructions:</h3>
|
||||
<ul class="list-disc list-inside space-y-1 text-sm">
|
||||
<li><strong>Play Player Card:</strong> Animates a card from bottom (player's hand)</li>
|
||||
<li><strong>Play Opponent Card:</strong> Animates a card from top (opponent's hand)</li>
|
||||
<li><strong>Play Both Cards:</strong> Both cards animate in simultaneously</li>
|
||||
<li><strong>Clear Cards:</strong> Triggers exit animations (cards slide out)</li>
|
||||
<li>
|
||||
<strong>Winner Buttons:</strong> Add glowing ring effect to winning card (play cards
|
||||
first)
|
||||
</li>
|
||||
<li>
|
||||
<strong>Full Trick Sequence:</strong> Plays both cards → shows winner → clears (2 second
|
||||
delay)
|
||||
</li>
|
||||
<li>
|
||||
<strong>Collect Trick (Player Wins):</strong> Sets player as winner, waits 1.5s, then
|
||||
cards slide DOWN toward player area
|
||||
</li>
|
||||
<li>
|
||||
<strong>Collect Trick (Opponent Wins):</strong> Sets opponent as winner, waits 1.5s,
|
||||
then cards slide UP toward opponent area
|
||||
</li>
|
||||
</ul>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</template>
|
||||
|
||||
<script setup>
|
||||
import { ref } from 'vue'
|
||||
import PlayArea from '@/components/game/PlayArea.vue'
|
||||
|
||||
// Reactive state
|
||||
const playerCard = ref(null)
|
||||
const opponentCard = ref(null)
|
||||
const winner = ref(null)
|
||||
const firstPlayer = ref(null)
|
||||
|
||||
// Sample cards to test with
|
||||
const sampleCards = [
|
||||
{ suit: 'c', rank: 1 },
|
||||
{ suit: 'c', rank: 7 },
|
||||
{ suit: 'c', rank: 13 },
|
||||
{ suit: 'e', rank: 11 },
|
||||
{ suit: 'e', rank: 12 },
|
||||
{ suit: 'o', rank: 2 },
|
||||
{ suit: 'o', rank: 3 },
|
||||
{ suit: 'p', rank: 4 },
|
||||
{ suit: 'p', rank: 7 },
|
||||
]
|
||||
|
||||
// Get random card
|
||||
const getRandomCard = () => {
|
||||
return sampleCards[Math.floor(Math.random() * sampleCards.length)]
|
||||
}
|
||||
|
||||
// Control functions
|
||||
const playPlayerCard = () => {
|
||||
if (!playerCard.value && !opponentCard.value) {
|
||||
firstPlayer.value = 'player'
|
||||
}
|
||||
playerCard.value = getRandomCard()
|
||||
winner.value = null
|
||||
}
|
||||
|
||||
const playOpponentCard = () => {
|
||||
if (!playerCard.value && !opponentCard.value) {
|
||||
firstPlayer.value = 'opponent'
|
||||
}
|
||||
opponentCard.value = getRandomCard()
|
||||
winner.value = null
|
||||
}
|
||||
|
||||
const playBothCards = () => {
|
||||
firstPlayer.value = 'player' // Player plays first in this scenario
|
||||
playerCard.value = getRandomCard()
|
||||
opponentCard.value = getRandomCard()
|
||||
winner.value = null
|
||||
}
|
||||
|
||||
const clearCards = () => {
|
||||
playerCard.value = null
|
||||
opponentCard.value = null
|
||||
winner.value = null
|
||||
firstPlayer.value = null
|
||||
}
|
||||
|
||||
const setPlayerWinner = () => {
|
||||
if (playerCard.value) {
|
||||
winner.value = 'player'
|
||||
}
|
||||
}
|
||||
|
||||
const setOpponentWinner = () => {
|
||||
if (opponentCard.value) {
|
||||
winner.value = 'opponent'
|
||||
}
|
||||
}
|
||||
|
||||
const fullSequence = async () => {
|
||||
// Clear first
|
||||
clearCards()
|
||||
await new Promise((resolve) => setTimeout(resolve, 500))
|
||||
|
||||
// Play both cards
|
||||
playBothCards()
|
||||
await new Promise((resolve) => setTimeout(resolve, 1000))
|
||||
|
||||
// Show winner (randomly)
|
||||
winner.value = Math.random() > 0.5 ? 'player' : 'opponent'
|
||||
await new Promise((resolve) => setTimeout(resolve, 2000))
|
||||
|
||||
// Clear cards
|
||||
clearCards()
|
||||
}
|
||||
|
||||
// Collect trick with player winning
|
||||
const collectTrickPlayerWins = async () => {
|
||||
if (!playerCard.value || !opponentCard.value) return
|
||||
|
||||
// Set player as winner
|
||||
winner.value = 'player'
|
||||
|
||||
// Wait 1.5 seconds to show winner glow
|
||||
await new Promise((resolve) => setTimeout(resolve, 1500))
|
||||
|
||||
// Clear cards (triggers exit animation toward player)
|
||||
playerCard.value = null
|
||||
opponentCard.value = null
|
||||
|
||||
// Reset after animation completes
|
||||
await new Promise((resolve) => setTimeout(resolve, 600))
|
||||
winner.value = null
|
||||
firstPlayer.value = null
|
||||
}
|
||||
|
||||
// Collect trick with opponent winning
|
||||
const collectTrickOpponentWins = async () => {
|
||||
if (!playerCard.value || !opponentCard.value) return
|
||||
|
||||
// Set opponent as winner
|
||||
winner.value = 'opponent'
|
||||
|
||||
// Wait 1.5 seconds to show winner glow
|
||||
await new Promise((resolve) => setTimeout(resolve, 1500))
|
||||
|
||||
// Clear cards (triggers exit animation toward opponent)
|
||||
playerCard.value = null
|
||||
opponentCard.value = null
|
||||
|
||||
// Reset after animation completes
|
||||
await new Promise((resolve) => setTimeout(resolve, 600))
|
||||
winner.value = null
|
||||
firstPlayer.value = null
|
||||
}
|
||||
</script>
|
||||
@@ -0,0 +1,275 @@
|
||||
<template>
|
||||
<div class="min-h-screen bg-gradient-to-br from-green-700 via-green-800 to-green-900 p-8">
|
||||
<div class="max-w-6xl mx-auto">
|
||||
<!-- Title -->
|
||||
<h1 class="text-white text-3xl font-bold mb-8 text-center">Card Dealing Test Page</h1>
|
||||
|
||||
<!-- Control Panel -->
|
||||
<div class="bg-gray-900/80 rounded-lg p-6 mb-8">
|
||||
<h2 class="text-white text-xl font-semibold mb-4">Controls</h2>
|
||||
<div class="flex flex-wrap gap-3 mb-4">
|
||||
<button
|
||||
@click="startDealing"
|
||||
:disabled="isDealing"
|
||||
class="px-4 py-2 bg-green-600 hover:bg-green-700 disabled:bg-gray-600 disabled:cursor-not-allowed text-white rounded-lg font-medium transition-colors"
|
||||
>
|
||||
Start Dealing
|
||||
</button>
|
||||
<button
|
||||
@click="dealSingleCard"
|
||||
:disabled="isDealing || cardsRemaining === 0"
|
||||
class="px-4 py-2 bg-blue-600 hover:bg-blue-700 disabled:bg-gray-600 disabled:cursor-not-allowed text-white rounded-lg font-medium transition-colors"
|
||||
>
|
||||
Deal One Card
|
||||
</button>
|
||||
<button
|
||||
@click="reset"
|
||||
class="px-4 py-2 bg-red-600 hover:bg-red-700 text-white rounded-lg font-medium transition-colors"
|
||||
>
|
||||
Reset
|
||||
</button>
|
||||
</div>
|
||||
|
||||
<!-- Current State Display -->
|
||||
<div class="grid grid-cols-3 gap-4 text-gray-300 text-sm">
|
||||
<div>
|
||||
<p><strong>Player Hand:</strong> {{ playerHand.length }} cards</p>
|
||||
<p><strong>Opponent Hand:</strong> {{ opponentHand.length }} cards</p>
|
||||
</div>
|
||||
<div>
|
||||
<p><strong>Cards Remaining:</strong> {{ cardsRemaining }}</p>
|
||||
<p><strong>Dealing:</strong> {{ isDealing ? 'Yes' : 'No' }}</p>
|
||||
</div>
|
||||
<div>
|
||||
<p><strong>Next Recipient:</strong> {{ nextRecipient }}</p>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<!-- Game Board Layout -->
|
||||
<div class="grid grid-rows-[auto_1fr_auto] gap-8">
|
||||
<!-- Opponent Hand (Top) -->
|
||||
<div class="flex justify-center">
|
||||
<div class="bg-gray-800/30 rounded-lg p-4">
|
||||
<h3 class="text-white text-sm font-semibold mb-2 text-center">Opponent Hand</h3>
|
||||
<PlayerHand
|
||||
ref="opponentHandRef"
|
||||
:cards="opponentHand"
|
||||
:face-down="true"
|
||||
:max-cards="3"
|
||||
:playable-cards="[]"
|
||||
/>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<!-- Center Area (Deck) -->
|
||||
<div class="flex items-center justify-center">
|
||||
<div class="bg-gray-800/30 rounded-lg p-6">
|
||||
<h3 class="text-white text-sm font-semibold mb-4 text-center">Deck</h3>
|
||||
<DeckArea
|
||||
ref="deckRef"
|
||||
:trump-card="trumpCard"
|
||||
:cards-remaining="cardsRemaining"
|
||||
:is-empty="cardsRemaining === 0"
|
||||
/>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<!-- Player Hand (Bottom) -->
|
||||
<div class="flex justify-center">
|
||||
<div class="bg-gray-800/30 rounded-lg p-4">
|
||||
<h3 class="text-white text-sm font-semibold mb-2 text-center">Your Hand</h3>
|
||||
<PlayerHand
|
||||
ref="playerHandRef"
|
||||
:cards="playerHand"
|
||||
:face-down="false"
|
||||
:max-cards="3"
|
||||
:playable-cards="[]"
|
||||
/>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<!-- Instructions -->
|
||||
<div class="bg-gray-900/80 rounded-lg p-6 mt-8 text-gray-300">
|
||||
<h3 class="text-white font-semibold mb-2">Instructions:</h3>
|
||||
<ul class="list-disc list-inside space-y-1 text-sm">
|
||||
<li><strong>Start Dealing:</strong> Automatically deals 6 cards (3 to each player) with 200ms delay</li>
|
||||
<li><strong>Deal One Card:</strong> Manually deal a single card to the next recipient</li>
|
||||
<li><strong>Reset:</strong> Clear all hands and reset the deck to 40 cards</li>
|
||||
<li>Watch the FlyingCard animation as cards move from deck to hands</li>
|
||||
<li>Notice the deck counter decreasing and the pulse effect</li>
|
||||
<li>Cards appear in hands with a fade-in animation</li>
|
||||
</ul>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<!-- Flying Cards Container -->
|
||||
<FlyingCard
|
||||
v-for="flyingCard in flyingCards"
|
||||
:key="flyingCard.id"
|
||||
:card="flyingCard.card"
|
||||
:start-position="flyingCard.startPosition"
|
||||
:end-position="flyingCard.endPosition"
|
||||
:duration="500"
|
||||
:face-down="flyingCard.faceDown"
|
||||
:delay="flyingCard.delay"
|
||||
@complete="onFlyingCardComplete(flyingCard)"
|
||||
/>
|
||||
</div>
|
||||
</template>
|
||||
|
||||
<script setup>
|
||||
import { ref, computed, nextTick } from 'vue'
|
||||
import PlayerHand from '@/components/game/PlayerHand.vue'
|
||||
import DeckArea from '@/components/game/DeckArea.vue'
|
||||
import FlyingCard from '@/components/game/FlyingCard.vue'
|
||||
|
||||
// Refs for components
|
||||
const playerHandRef = ref(null)
|
||||
const opponentHandRef = ref(null)
|
||||
const deckRef = ref(null)
|
||||
|
||||
// Game state
|
||||
const playerHand = ref([])
|
||||
const opponentHand = ref([])
|
||||
const cardsRemaining = ref(40)
|
||||
const isDealing = ref(false)
|
||||
const flyingCards = ref([])
|
||||
let flyingCardIdCounter = 0
|
||||
|
||||
// Trump card (fixed for testing)
|
||||
const trumpCard = ref({ suit: 'o', rank: 7 })
|
||||
|
||||
// Sample deck
|
||||
const sampleCards = [
|
||||
{ suit: 'c', rank: 1 },
|
||||
{ suit: 'c', rank: 2 },
|
||||
{ suit: 'c', rank: 3 },
|
||||
{ suit: 'c', rank: 4 },
|
||||
{ suit: 'c', rank: 5 },
|
||||
{ suit: 'c', rank: 6 },
|
||||
{ suit: 'c', rank: 7 },
|
||||
{ suit: 'c', rank: 11 },
|
||||
{ suit: 'c', rank: 12 },
|
||||
{ suit: 'c', rank: 13 },
|
||||
{ suit: 'e', rank: 1 },
|
||||
{ suit: 'e', rank: 2 },
|
||||
{ suit: 'e', rank: 3 },
|
||||
{ suit: 'e', rank: 4 },
|
||||
{ suit: 'e', rank: 5 },
|
||||
{ suit: 'e', rank: 6 },
|
||||
{ suit: 'e', rank: 7 },
|
||||
{ suit: 'e', rank: 11 },
|
||||
{ suit: 'e', rank: 12 },
|
||||
{ suit: 'e', rank: 13 },
|
||||
{ suit: 'o', rank: 1 },
|
||||
{ suit: 'o', rank: 2 },
|
||||
{ suit: 'o', rank: 3 },
|
||||
{ suit: 'o', rank: 4 },
|
||||
{ suit: 'o', rank: 5 },
|
||||
{ suit: 'o', rank: 6 },
|
||||
{ suit: 'p', rank: 1 },
|
||||
{ suit: 'p', rank: 2 },
|
||||
{ suit: 'p', rank: 3 },
|
||||
{ suit: 'p', rank: 4 },
|
||||
{ suit: 'p', rank: 5 },
|
||||
{ suit: 'p', rank: 6 },
|
||||
{ suit: 'p', rank: 7 },
|
||||
{ suit: 'p', rank: 11 },
|
||||
{ suit: 'p', rank: 12 },
|
||||
{ suit: 'p', rank: 13 },
|
||||
]
|
||||
|
||||
// Computed
|
||||
const nextRecipient = computed(() => {
|
||||
const totalDealt = playerHand.value.length + opponentHand.value.length
|
||||
return totalDealt % 2 === 0 ? 'Player' : 'Opponent'
|
||||
})
|
||||
|
||||
// Get random card
|
||||
const getRandomCard = () => {
|
||||
return sampleCards[Math.floor(Math.random() * sampleCards.length)]
|
||||
}
|
||||
|
||||
// Get position of an element
|
||||
const getElementPosition = (el) => {
|
||||
if (!el) return { x: 0, y: 0 }
|
||||
const rect = el.$el?.getBoundingClientRect() || el.getBoundingClientRect()
|
||||
return {
|
||||
x: rect.left + rect.width / 2 - 64, // Center, minus half card width
|
||||
y: rect.top + rect.height / 2 - 89, // Center, minus half card height
|
||||
}
|
||||
}
|
||||
|
||||
// Deal a single card
|
||||
const dealSingleCard = async () => {
|
||||
if (cardsRemaining.value === 0) return
|
||||
|
||||
await nextTick()
|
||||
|
||||
// Determine recipient
|
||||
const totalDealt = playerHand.value.length + opponentHand.value.length
|
||||
const isPlayer = totalDealt % 2 === 0
|
||||
|
||||
// Get positions
|
||||
const startPos = getElementPosition(deckRef.value)
|
||||
const endPos = getElementPosition(isPlayer ? playerHandRef.value : opponentHandRef.value)
|
||||
|
||||
// Create flying card
|
||||
const card = getRandomCard()
|
||||
const flyingCard = {
|
||||
id: flyingCardIdCounter++,
|
||||
card,
|
||||
startPosition: startPos,
|
||||
endPosition: endPos,
|
||||
faceDown: !isPlayer, // Face down for opponent
|
||||
delay: 0,
|
||||
recipient: isPlayer ? 'player' : 'opponent',
|
||||
}
|
||||
|
||||
flyingCards.value.push(flyingCard)
|
||||
cardsRemaining.value--
|
||||
}
|
||||
|
||||
// Flying card animation complete
|
||||
const onFlyingCardComplete = (flyingCard) => {
|
||||
// Add card to appropriate hand
|
||||
if (flyingCard.recipient === 'player') {
|
||||
playerHand.value.push(flyingCard.card)
|
||||
} else {
|
||||
opponentHand.value.push(flyingCard.card)
|
||||
}
|
||||
|
||||
// Remove flying card
|
||||
flyingCards.value = flyingCards.value.filter((fc) => fc.id !== flyingCard.id)
|
||||
}
|
||||
|
||||
// Start automatic dealing sequence
|
||||
const startDealing = async () => {
|
||||
if (isDealing.value) return
|
||||
|
||||
isDealing.value = true
|
||||
|
||||
// Deal 6 cards (3 to each player)
|
||||
for (let i = 0; i < 6; i++) {
|
||||
if (cardsRemaining.value === 0) break
|
||||
|
||||
await dealSingleCard()
|
||||
|
||||
// Wait 200ms before next card
|
||||
await new Promise((resolve) => setTimeout(resolve, 700))
|
||||
}
|
||||
|
||||
isDealing.value = false
|
||||
}
|
||||
|
||||
// Reset everything
|
||||
const reset = () => {
|
||||
playerHand.value = []
|
||||
opponentHand.value = []
|
||||
cardsRemaining.value = 40
|
||||
flyingCards.value = []
|
||||
isDealing.value = false
|
||||
}
|
||||
</script>
|
||||
@@ -0,0 +1,444 @@
|
||||
<template>
|
||||
<div class="min-h-screen bg-gradient-to-br from-green-700 via-green-800 to-green-900">
|
||||
<!-- Control Panel (Floating) -->
|
||||
<div class="fixed top-4 left-4 bg-gray-900/95 rounded-lg p-4 max-w-xs z-50 shadow-2xl">
|
||||
<h3 class="text-white font-bold mb-3 text-sm">Test Controls</h3>
|
||||
<div class="space-y-2">
|
||||
<button
|
||||
@click="dealInitialCards"
|
||||
:disabled="isDealing"
|
||||
class="w-full px-3 py-2 bg-blue-600 hover:bg-blue-700 disabled:bg-gray-600 text-white rounded text-xs font-medium transition-colors"
|
||||
>
|
||||
Deal Cards
|
||||
</button>
|
||||
<button
|
||||
@click="playRandomCards"
|
||||
:disabled="playerHand.length === 0"
|
||||
class="w-full px-3 py-2 bg-green-600 hover:bg-green-700 disabled:bg-gray-600 text-white rounded text-xs font-medium transition-colors"
|
||||
>
|
||||
Play Both Cards
|
||||
</button>
|
||||
<button
|
||||
@click="collectTrick"
|
||||
:disabled="!currentTrick.playerCard || !currentTrick.opponentCard"
|
||||
class="w-full px-3 py-2 bg-purple-600 hover:bg-purple-700 disabled:bg-gray-600 text-white rounded text-xs font-medium transition-colors"
|
||||
>
|
||||
Collect Trick
|
||||
</button>
|
||||
<button
|
||||
@click="revealTrump = !revealTrump"
|
||||
class="w-full px-3 py-2 bg-amber-600 hover:bg-amber-700 text-white rounded text-xs font-medium transition-colors"
|
||||
>
|
||||
{{ revealTrump ? 'Hide' : 'Reveal' }} Trump
|
||||
</button>
|
||||
<button
|
||||
@click="showGameOver"
|
||||
class="w-full px-3 py-2 bg-rose-600 hover:bg-rose-700 text-white rounded text-xs font-medium transition-colors"
|
||||
>
|
||||
Show Game Over
|
||||
</button>
|
||||
<button
|
||||
@click="resetGame"
|
||||
class="w-full px-3 py-2 bg-gray-600 hover:bg-gray-700 text-white rounded text-xs font-medium transition-colors"
|
||||
>
|
||||
Reset Game
|
||||
</button>
|
||||
<div class="pt-2 border-t border-gray-700 mt-2 text-gray-400 text-xs">
|
||||
<p><strong>Turn:</strong> {{ currentTurn === 'player' ? 'You' : 'Bot' }}</p>
|
||||
<p><strong>Deck:</strong> {{ cardsRemaining }} cards</p>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<!-- Main Game Board -->
|
||||
<div
|
||||
class="grid grid-rows-[auto_auto_1fr_auto] grid-cols-[1fr_3fr_1fr] gap-6 p-8 min-h-screen"
|
||||
>
|
||||
<!-- Score Display (Top Left) -->
|
||||
<div class="col-start-1 col-end-4 row-start-1 flex justify-start items-start">
|
||||
<ScoreDisplay
|
||||
:player-score="playerScore"
|
||||
:opponent-score="opponentScore"
|
||||
:current-turn="currentTurn"
|
||||
:round-number="1"
|
||||
/>
|
||||
</div>
|
||||
|
||||
<!-- Opponent Hand -->
|
||||
<div class="col-start-2 col-end-3 row-start-2 flex justify-center">
|
||||
<PlayerHand
|
||||
ref="opponentHandRef"
|
||||
:cards="opponentHand"
|
||||
:face-down="true"
|
||||
:max-cards="3"
|
||||
:playable-cards="[]"
|
||||
/>
|
||||
</div>
|
||||
|
||||
<!-- Play Area -->
|
||||
<div class="col-start-2 col-end-3 row-start-3 flex items-center justify-center">
|
||||
<PlayArea
|
||||
:player-card="currentTrick.playerCard"
|
||||
:opponent-card="currentTrick.opponentCard"
|
||||
:winner="currentTrick.winner"
|
||||
:first-player="currentTrick.firstPlayer"
|
||||
/>
|
||||
</div>
|
||||
|
||||
<!-- Deck Area -->
|
||||
<div class="col-start-3 col-end-4 row-start-3 flex items-center justify-center">
|
||||
<DeckArea
|
||||
ref="deckRef"
|
||||
:trump-card="trumpCard"
|
||||
:cards-remaining="cardsRemaining"
|
||||
:is-empty="cardsRemaining === 0"
|
||||
:reveal-trump="revealTrump"
|
||||
/>
|
||||
</div>
|
||||
|
||||
<!-- Player Hand -->
|
||||
<div class="col-start-2 col-end-3 row-start-4 flex justify-center">
|
||||
<PlayerHand
|
||||
ref="playerHandRef"
|
||||
:cards="playerHand"
|
||||
:face-down="false"
|
||||
:max-cards="3"
|
||||
:playable-cards="playableCards"
|
||||
@card-clicked="handleCardClick"
|
||||
/>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<!-- Flying Cards Container -->
|
||||
<FlyingCard
|
||||
v-for="flyingCard in flyingCards"
|
||||
:key="flyingCard.id"
|
||||
:card="flyingCard.card"
|
||||
:start-position="flyingCard.startPosition"
|
||||
:end-position="flyingCard.endPosition"
|
||||
:duration="500"
|
||||
:face-down="flyingCard.faceDown"
|
||||
:delay="flyingCard.delay"
|
||||
@complete="onFlyingCardComplete(flyingCard)"
|
||||
/>
|
||||
|
||||
<!-- Game Over Modal -->
|
||||
<GameOver
|
||||
:is-visible="gameOverVisible"
|
||||
:winner="gameOverWinner"
|
||||
:player-score="playerScore"
|
||||
:opponent-score="opponentScore"
|
||||
:stats="gameOverStats"
|
||||
@close="gameOverVisible = false"
|
||||
@play-again="resetGame"
|
||||
/>
|
||||
</div>
|
||||
</template>
|
||||
|
||||
<script setup>
|
||||
import { ref, computed, nextTick } from 'vue'
|
||||
import GameBoard from '@/components/game/GameBoard.vue'
|
||||
import ScoreDisplay from '@/components/game/ScoreDisplay.vue'
|
||||
import PlayerHand from '@/components/game/PlayerHand.vue'
|
||||
import PlayArea from '@/components/game/PlayArea.vue'
|
||||
import DeckArea from '@/components/game/DeckArea.vue'
|
||||
import FlyingCard from '@/components/game/FlyingCard.vue'
|
||||
import GameOver from '@/components/game/GameOver.vue'
|
||||
|
||||
// Component refs
|
||||
const playerHandRef = ref(null)
|
||||
const opponentHandRef = ref(null)
|
||||
const deckRef = ref(null)
|
||||
|
||||
// Game state
|
||||
const playerHand = ref([])
|
||||
const opponentHand = ref([])
|
||||
const playerScore = ref(0)
|
||||
const opponentScore = ref(0)
|
||||
const currentTurn = ref('player')
|
||||
const cardsRemaining = ref(40)
|
||||
const isDealing = ref(false)
|
||||
const revealTrump = ref(false)
|
||||
|
||||
// Trump card
|
||||
const trumpCard = ref({ suit: 'o', rank: 7 })
|
||||
|
||||
// Current trick
|
||||
const currentTrick = ref({
|
||||
playerCard: null,
|
||||
opponentCard: null,
|
||||
winner: null,
|
||||
firstPlayer: null,
|
||||
})
|
||||
|
||||
// Flying cards for dealing animation
|
||||
const flyingCards = ref([])
|
||||
let flyingCardIdCounter = 0
|
||||
|
||||
// Game Over
|
||||
const gameOverVisible = ref(false)
|
||||
const gameOverWinner = ref('player')
|
||||
const gameOverStats = ref({
|
||||
playerTricks: 0,
|
||||
opponentTricks: 0,
|
||||
})
|
||||
|
||||
// Sample deck
|
||||
const sampleCards = [
|
||||
{ suit: 'c', rank: 1 },
|
||||
{ suit: 'c', rank: 2 },
|
||||
{ suit: 'c', rank: 3 },
|
||||
{ suit: 'c', rank: 4 },
|
||||
{ suit: 'c', rank: 5 },
|
||||
{ suit: 'c', rank: 6 },
|
||||
{ suit: 'c', rank: 7 },
|
||||
{ suit: 'c', rank: 11 },
|
||||
{ suit: 'c', rank: 12 },
|
||||
{ suit: 'c', rank: 13 },
|
||||
{ suit: 'e', rank: 1 },
|
||||
{ suit: 'e', rank: 2 },
|
||||
{ suit: 'e', rank: 3 },
|
||||
{ suit: 'e', rank: 4 },
|
||||
{ suit: 'e', rank: 5 },
|
||||
{ suit: 'e', rank: 6 },
|
||||
{ suit: 'e', rank: 7 },
|
||||
{ suit: 'e', rank: 11 },
|
||||
{ suit: 'e', rank: 12 },
|
||||
{ suit: 'e', rank: 13 },
|
||||
{ suit: 'o', rank: 1 },
|
||||
{ suit: 'o', rank: 2 },
|
||||
{ suit: 'o', rank: 3 },
|
||||
{ suit: 'o', rank: 4 },
|
||||
{ suit: 'o', rank: 5 },
|
||||
{ suit: 'o', rank: 6 },
|
||||
{ suit: 'p', rank: 1 },
|
||||
{ suit: 'p', rank: 2 },
|
||||
{ suit: 'p', rank: 3 },
|
||||
{ suit: 'p', rank: 4 },
|
||||
{ suit: 'p', rank: 5 },
|
||||
{ suit: 'p', rank: 6 },
|
||||
{ suit: 'p', rank: 7 },
|
||||
{ suit: 'p', rank: 11 },
|
||||
{ suit: 'p', rank: 12 },
|
||||
{ suit: 'p', rank: 13 },
|
||||
]
|
||||
|
||||
// Playable cards (all cards in hand for testing)
|
||||
const playableCards = computed(() => {
|
||||
return playerHand.value.map((_, index) => index)
|
||||
})
|
||||
|
||||
// Get random card
|
||||
const getRandomCard = () => {
|
||||
return sampleCards[Math.floor(Math.random() * sampleCards.length)]
|
||||
}
|
||||
|
||||
// Get element position
|
||||
const getElementPosition = (el) => {
|
||||
if (!el) return { x: 0, y: 0 }
|
||||
const rect = el.$el?.getBoundingClientRect() || el.getBoundingClientRect()
|
||||
return {
|
||||
x: rect.left + rect.width / 2 - 64,
|
||||
y: rect.top + rect.height / 2 - 89,
|
||||
}
|
||||
}
|
||||
|
||||
// Deal a single card
|
||||
const dealSingleCard = async (isPlayer) => {
|
||||
if (cardsRemaining.value === 0) return
|
||||
|
||||
await nextTick()
|
||||
|
||||
const startPos = getElementPosition(deckRef.value)
|
||||
const endPos = getElementPosition(isPlayer ? playerHandRef.value : opponentHandRef.value)
|
||||
|
||||
const card = getRandomCard()
|
||||
const flyingCard = {
|
||||
id: flyingCardIdCounter++,
|
||||
card,
|
||||
startPosition: startPos,
|
||||
endPosition: endPos,
|
||||
faceDown: !isPlayer,
|
||||
delay: 0,
|
||||
recipient: isPlayer ? 'player' : 'opponent',
|
||||
}
|
||||
|
||||
flyingCards.value.push(flyingCard)
|
||||
cardsRemaining.value--
|
||||
}
|
||||
|
||||
// Flying card complete
|
||||
const onFlyingCardComplete = (flyingCard) => {
|
||||
if (flyingCard.recipient === 'player') {
|
||||
playerHand.value.push(flyingCard.card)
|
||||
} else {
|
||||
opponentHand.value.push(flyingCard.card)
|
||||
}
|
||||
flyingCards.value = flyingCards.value.filter((fc) => fc.id !== flyingCard.id)
|
||||
}
|
||||
|
||||
// Deal initial cards
|
||||
const dealInitialCards = async () => {
|
||||
if (isDealing.value) return
|
||||
isDealing.value = true
|
||||
|
||||
// Reveal trump at start
|
||||
revealTrump.value = true
|
||||
await new Promise((resolve) => setTimeout(resolve, 1000))
|
||||
revealTrump.value = false
|
||||
|
||||
// Deal 6 cards (3 to each player)
|
||||
for (let i = 0; i < 6; i++) {
|
||||
if (cardsRemaining.value === 0) break
|
||||
const isPlayer = i % 2 === 0
|
||||
await dealSingleCard(isPlayer)
|
||||
await new Promise((resolve) => setTimeout(resolve, 700))
|
||||
}
|
||||
|
||||
isDealing.value = false
|
||||
}
|
||||
|
||||
// Handle card click from player hand
|
||||
const handleCardClick = ({ card, index }) => {
|
||||
if (currentTurn.value !== 'player') return
|
||||
if (currentTrick.value.playerCard) return
|
||||
|
||||
// Set first player if no cards played yet
|
||||
if (!currentTrick.value.opponentCard) {
|
||||
currentTrick.value.firstPlayer = 'player'
|
||||
}
|
||||
|
||||
// Play the card
|
||||
currentTrick.value.playerCard = card
|
||||
playerHand.value.splice(index, 1)
|
||||
|
||||
// Switch turn
|
||||
currentTurn.value = 'opponent'
|
||||
|
||||
// Auto-play opponent card after delay
|
||||
setTimeout(() => {
|
||||
if (opponentHand.value.length > 0 && !currentTrick.value.opponentCard) {
|
||||
playOpponentCard()
|
||||
}
|
||||
}, 1000)
|
||||
}
|
||||
|
||||
// Play opponent card
|
||||
const playOpponentCard = () => {
|
||||
if (opponentHand.value.length === 0) return
|
||||
|
||||
// Set first player if no cards played yet
|
||||
if (!currentTrick.value.playerCard) {
|
||||
currentTrick.value.firstPlayer = 'opponent'
|
||||
}
|
||||
|
||||
// Play random card
|
||||
const randomIndex = Math.floor(Math.random() * opponentHand.value.length)
|
||||
const card = opponentHand.value[randomIndex]
|
||||
currentTrick.value.opponentCard = card
|
||||
opponentHand.value.splice(randomIndex, 1)
|
||||
|
||||
// Switch turn
|
||||
currentTurn.value = 'player'
|
||||
}
|
||||
|
||||
// Play random cards from both players
|
||||
const playRandomCards = () => {
|
||||
if (playerHand.value.length === 0) return
|
||||
|
||||
// Player plays first
|
||||
currentTrick.value.firstPlayer = 'player'
|
||||
const playerCard = playerHand.value[0]
|
||||
currentTrick.value.playerCard = playerCard
|
||||
playerHand.value.shift()
|
||||
|
||||
// Opponent plays after delay
|
||||
setTimeout(() => {
|
||||
if (opponentHand.value.length > 0) {
|
||||
const opponentCard = opponentHand.value[0]
|
||||
currentTrick.value.opponentCard = opponentCard
|
||||
opponentHand.value.shift()
|
||||
}
|
||||
}, 600)
|
||||
}
|
||||
|
||||
// Collect trick
|
||||
const collectTrick = async () => {
|
||||
if (!currentTrick.value.playerCard || !currentTrick.value.opponentCard) return
|
||||
|
||||
// Randomly determine winner
|
||||
const winner = Math.random() > 0.5 ? 'player' : 'opponent'
|
||||
currentTrick.value.winner = winner
|
||||
|
||||
// Wait to show winner glow
|
||||
await new Promise((resolve) => setTimeout(resolve, 1500))
|
||||
|
||||
// Update score (random points)
|
||||
const points = Math.floor(Math.random() * 15) + 5
|
||||
if (winner === 'player') {
|
||||
playerScore.value += points
|
||||
gameOverStats.value.playerTricks++
|
||||
} else {
|
||||
opponentScore.value += points
|
||||
gameOverStats.value.opponentTricks++
|
||||
}
|
||||
|
||||
// Clear cards (triggers collection animation)
|
||||
currentTrick.value.playerCard = null
|
||||
currentTrick.value.opponentCard = null
|
||||
|
||||
// Reset after animation
|
||||
await new Promise((resolve) => setTimeout(resolve, 600))
|
||||
currentTrick.value.winner = null
|
||||
currentTrick.value.firstPlayer = null
|
||||
|
||||
// Deal new cards if deck has cards
|
||||
if (cardsRemaining.value >= 2 && playerHand.value.length < 3) {
|
||||
await new Promise((resolve) => setTimeout(resolve, 500))
|
||||
await dealSingleCard(winner === 'player')
|
||||
await new Promise((resolve) => setTimeout(resolve, 700))
|
||||
await dealSingleCard(winner === 'opponent')
|
||||
}
|
||||
|
||||
// Switch turn to winner
|
||||
currentTurn.value = winner
|
||||
}
|
||||
|
||||
// Show game over
|
||||
const showGameOver = () => {
|
||||
// Make player win for demo
|
||||
if (playerScore.value <= opponentScore.value) {
|
||||
playerScore.value = opponentScore.value + 10
|
||||
}
|
||||
gameOverWinner.value =
|
||||
playerScore.value > opponentScore.value
|
||||
? 'player'
|
||||
: opponentScore.value > playerScore.value
|
||||
? 'opponent'
|
||||
: 'draw'
|
||||
gameOverVisible.value = true
|
||||
}
|
||||
|
||||
// Reset game
|
||||
const resetGame = () => {
|
||||
playerHand.value = []
|
||||
opponentHand.value = []
|
||||
playerScore.value = 0
|
||||
opponentScore.value = 0
|
||||
currentTurn.value = 'player'
|
||||
cardsRemaining.value = 40
|
||||
currentTrick.value = {
|
||||
playerCard: null,
|
||||
opponentCard: null,
|
||||
winner: null,
|
||||
firstPlayer: null,
|
||||
}
|
||||
flyingCards.value = []
|
||||
gameOverVisible.value = false
|
||||
gameOverStats.value = {
|
||||
playerTricks: 0,
|
||||
opponentTricks: 0,
|
||||
}
|
||||
revealTrump.value = false
|
||||
}
|
||||
</script>
|
||||
@@ -2,7 +2,10 @@ import HomePage from '@/pages/home/HomePage.vue'
|
||||
import LoginPage from '@/pages/login/LoginPage.vue'
|
||||
import LaravelPage from '@/pages/testing/LaravelPage.vue'
|
||||
import WebsocketsPage from '@/pages/testing/WebsocketsPage.vue'
|
||||
import UserPage from '@/pages/user/UserPage.vue'
|
||||
import TestAnimations from '@/pages/TestAnimations.vue'
|
||||
import TestDealing from '@/pages/TestDealing.vue'
|
||||
import TestAllAnimations from '@/pages/TestAllAnimations.vue'
|
||||
import TestGameBoard from '@/pages/TestGameBoard.vue'
|
||||
import { createRouter, createWebHistory } from 'vue-router'
|
||||
|
||||
const router = createRouter({
|
||||
@@ -31,6 +34,22 @@ const router = createRouter({
|
||||
path: 'websockets',
|
||||
component: WebsocketsPage,
|
||||
},
|
||||
{
|
||||
path: 'animations',
|
||||
component: TestAnimations,
|
||||
},
|
||||
{
|
||||
path: 'dealing',
|
||||
component: TestDealing,
|
||||
},
|
||||
{
|
||||
path: 'all-animations',
|
||||
component: TestAllAnimations,
|
||||
},
|
||||
{
|
||||
path: 'gameboard',
|
||||
component: TestGameBoard,
|
||||
},
|
||||
],
|
||||
},
|
||||
],
|
||||
|
||||
@@ -1 +1,2 @@
|
||||
PORT=
|
||||
API_URL=
|
||||
@@ -0,0 +1,19 @@
|
||||
import axios from "axios";
|
||||
import { getUser } from "../state/connection.js";
|
||||
import { createGame, joinGame, playCard, getGame } from "../state/game.js";
|
||||
|
||||
const API_URL = process.env.API_URL || "http://localhost:8000/api";
|
||||
|
||||
export const handleGameEvents = (io, socket) => {
|
||||
socket.on("create-game", async (data) => {
|
||||
try {
|
||||
const user = getUser(socket.id);
|
||||
const game = createGame(data.type, user);
|
||||
socket.join(`game_${game.match_id}`);
|
||||
|
||||
} catch (error) {
|
||||
console.error("Error creating game:", error);
|
||||
socket.emit("error", { message: "Failed to create game." });
|
||||
}
|
||||
});
|
||||
};
|
||||
@@ -0,0 +1,186 @@
|
||||
const games = new Map();
|
||||
let currentGameID = 0;
|
||||
|
||||
// Bisca deck configuration
|
||||
const suits = ["hearts", "diamonds", "clubs", "spades"];
|
||||
const cards = [
|
||||
{ face: "A", value: 11, points: 11 },
|
||||
{ face: "7", value: 10, points: 10 },
|
||||
{ face: "K", value: 4, points: 4 },
|
||||
{ face: "J", value: 3, points: 3 },
|
||||
{ face: "Q", value: 2, points: 2 },
|
||||
{ face: "6", value: 0, points: 0 },
|
||||
{ face: "5", value: 0, points: 0 },
|
||||
{ face: "4", value: 0, points: 0 },
|
||||
{ face: "3", value: 0, points: 0 },
|
||||
{ face: "2", value: 0, points: 0 },
|
||||
];
|
||||
|
||||
const createDeck = () => {
|
||||
const deck = [];
|
||||
suits.forEach((suit) => {
|
||||
cards.forEach((card) => {
|
||||
deck.push({ ...card, suit });
|
||||
});
|
||||
});
|
||||
return deck;
|
||||
};
|
||||
|
||||
const shuffleDeck = (deck) => {
|
||||
const shuffled = [...deck];
|
||||
for (let i = shuffled.length - 1; i > 0; i--) {
|
||||
const j = Math.floor(Math.random() * (i + 1));
|
||||
[shuffled[i], shuffled[j]] = [shuffled[j], shuffled[i]];
|
||||
}
|
||||
return shuffled;
|
||||
};
|
||||
|
||||
const dealCards = (deck, cardsPerPlayer = 3) => {
|
||||
const player1Hand = deck.slice(0, cardsPerPlayer);
|
||||
const player2Hand = deck.slice(cardsPerPlayer, cardsPerPlayer * 2);
|
||||
const trumpCard = deck.pop();
|
||||
return {
|
||||
player1Hand,
|
||||
player2Hand,
|
||||
trumpCard,
|
||||
remainingDeck: deck,
|
||||
};
|
||||
};
|
||||
|
||||
export const createGame = (type, host) => {
|
||||
const gameID = currentGameID++;
|
||||
const game = {
|
||||
match_id: gameID,
|
||||
type: type,
|
||||
player1_user_id: host.id,
|
||||
player2_user_id: null,
|
||||
is_draw: false,
|
||||
winner_user_id: null,
|
||||
loser_user_id: null,
|
||||
status: "pending",
|
||||
began_at: null,
|
||||
ended_at: null,
|
||||
player1_points: 0,
|
||||
player2_points: 0,
|
||||
// Game-specific state
|
||||
player1_hand: [],
|
||||
player2_hand: [],
|
||||
trump_card: null,
|
||||
trump_suit: null,
|
||||
current_trick: [],
|
||||
current_player: null,
|
||||
round_number: 0,
|
||||
};
|
||||
games.set(gameID, game);
|
||||
return game;
|
||||
};
|
||||
|
||||
export const joinGame = (gameID, player2) => {
|
||||
const game = games.get(gameID);
|
||||
game.player2_user_id = player2.id;
|
||||
game.status = "playing";
|
||||
game.began_at = new Date();
|
||||
|
||||
const cardCount = parseInt(game.type, 10);
|
||||
if (![3, 9].includes(cardCount)) {
|
||||
throw new Error("Unsupported game type");
|
||||
}
|
||||
|
||||
// Initialize game state
|
||||
const deck = shuffleDeck(createDeck());
|
||||
const { player1Hand, player2Hand, trumpCard, remainingDeck } = dealCards(
|
||||
deck,
|
||||
cardCount
|
||||
);
|
||||
|
||||
game.player1_hand = player1Hand;
|
||||
game.player2_hand = player2Hand;
|
||||
game.trump_card = trumpCard;
|
||||
game.trump_suit = trumpCard.suit;
|
||||
game.remaining_deck = remainingDeck;
|
||||
game.current_player = game.player1_user_id; // Player 1 starts
|
||||
|
||||
return game;
|
||||
};
|
||||
|
||||
export const playCard = (gameID, playerID, cardIndex) => {
|
||||
const game = games.get(gameID);
|
||||
if (game.current_player !== playerID) {
|
||||
throw new Error("Not this player's turn");
|
||||
}
|
||||
|
||||
const isPlayer1 = playerID === game.player1_user_id;
|
||||
const playerHand = isPlayer1 ? game.player1_hand : game.player2_hand;
|
||||
const playedCard = playerHand.splice(cardIndex, 1)[0];
|
||||
|
||||
game.current_trick.push({ playerID, card: playedCard });
|
||||
|
||||
if (game.current_trick.length === 2) {
|
||||
resolveTrick(game);
|
||||
} else {
|
||||
game.current_player = isPlayer1
|
||||
? game.player2_user_id
|
||||
: game.player1_user_id;
|
||||
}
|
||||
return game;
|
||||
};
|
||||
|
||||
const resolveTrick = (game) => {
|
||||
const [first, second] = game.current_trick;
|
||||
let winner;
|
||||
|
||||
if (first.card.suit === second.card.suit) {
|
||||
winner = first.card.value > second.card.value ? first : second;
|
||||
} else if (second.card.suit === game.trump_suit) {
|
||||
winner = second;
|
||||
} else {
|
||||
winner = first; // First card wins if no trump and different suits
|
||||
}
|
||||
|
||||
const points = first.card.points + second.card.points;
|
||||
winner.playerID === game.player1_user_id
|
||||
? (game.player1_points += points)
|
||||
: (game.player2_points += points);
|
||||
|
||||
if (deck.length > 0) {
|
||||
const winnerHand =
|
||||
winner.playerID === game.player1_user_id
|
||||
? game.player1_hand
|
||||
: game.player2_hand;
|
||||
loserHand =
|
||||
winner.playerID === game.player1_user_id
|
||||
? game.player2_hand
|
||||
: game.player1_hand;
|
||||
|
||||
winnerHand.push(game.deck.shift());
|
||||
if (game.deck.length > 0) {
|
||||
loserHand.push(game.deck.shift());
|
||||
}
|
||||
}
|
||||
|
||||
game.current_trick = [];
|
||||
game.current_player = winner.playerID;
|
||||
game.round_number += 1;
|
||||
|
||||
if (game.player1_hand.length === 0 && game.player2_hand.length === 0) {
|
||||
endGame(game);
|
||||
}
|
||||
};
|
||||
|
||||
const endGame = (game) => {
|
||||
game.status = "ended";
|
||||
game.ended_at = new Date();
|
||||
|
||||
if (game.player1_points > game.player2_points) {
|
||||
game.winner_user_id = game.player1_user_id;
|
||||
game.loser_user_id = game.player2_user_id;
|
||||
} else if (game.player2_points > game.player1_points) {
|
||||
game.winner_user_id = game.player2_user_id;
|
||||
game.loser_user_id = game.player1_user_id;
|
||||
} else {
|
||||
game.is_draw = true;
|
||||
}
|
||||
};
|
||||
|
||||
export const getGame = (gameID) => games.get(gameID);
|
||||
export const getAllGames = () => Array.from(games.values());
|
||||