Version 1.0 #102
@@ -18,23 +18,23 @@ class UserController extends Controller
|
||||
*/
|
||||
public function index(Request $request)
|
||||
{
|
||||
$this->authorize('viewAny', User::class);
|
||||
$this->authorize("viewAny", User::class);
|
||||
|
||||
$query = User::query();
|
||||
|
||||
if ($request->has('type')) {
|
||||
$query->where('type', $request->type);
|
||||
if ($request->has("type")) {
|
||||
$query->where("type", $request->type);
|
||||
}
|
||||
|
||||
if ($request->has('blocked')) {
|
||||
$query->where('blocked', $request->type);
|
||||
if ($request->has("blocked")) {
|
||||
$query->where("blocked", $request->type);
|
||||
}
|
||||
|
||||
if ($request->has('search')) {
|
||||
if ($request->has("search")) {
|
||||
$query->where(function ($q) use ($request) {
|
||||
$q->where('name', 'like', '%'.$request->search.'%')
|
||||
->orWhere('email', 'like', '%'.$request->search.'%')
|
||||
->orWhere('nickname', 'like', '%'.$request->search.'%');
|
||||
$q->where("name", "like", "%" . $request->search . "%")
|
||||
->orWhere("email", "like", "%" . $request->search . "%")
|
||||
->orWhere("nickname", "like", "%" . $request->search . "%");
|
||||
});
|
||||
}
|
||||
|
||||
@@ -47,16 +47,16 @@ class UserController extends Controller
|
||||
*/
|
||||
public function store(Request $request)
|
||||
{
|
||||
$this->authorize('create', User::class);
|
||||
$this->authorize("create", User::class);
|
||||
|
||||
$validated = $request->validate([
|
||||
'name' => 'required|string|max:255',
|
||||
'email' => 'required|email|unique:users,email',
|
||||
'password' => 'required|string|min:3',
|
||||
'type' => 'required|in:A,U',
|
||||
"name" => "required|string|max:255",
|
||||
"email" => "required|email|unique:users,email",
|
||||
"password" => "required|string|min:3",
|
||||
"type" => "required|in:A,U",
|
||||
]);
|
||||
|
||||
$validated['password'] = Hash::make($validated['password']);
|
||||
$validated["password"] = Hash::make($validated["password"]);
|
||||
|
||||
$user = User::create($validated);
|
||||
|
||||
@@ -82,15 +82,15 @@ class UserController extends Controller
|
||||
$currentUser = Auth::user();
|
||||
|
||||
// Admins and owners get full profile, others get limited view
|
||||
if ($currentUser->type === 'A' || $currentUser->id === $user->id) {
|
||||
if ($currentUser->type === "A" || $currentUser->id === $user->id) {
|
||||
return response()->json($user);
|
||||
} else {
|
||||
return response()->json([
|
||||
'id' => $user->id,
|
||||
'name' => $user->name,
|
||||
'nickname' => $user->nickname,
|
||||
'photo_avatar_filename' => $user->photo_avatar_filename,
|
||||
'type' => $user->type,
|
||||
"id" => $user->id,
|
||||
"name" => $user->name,
|
||||
"nickname" => $user->nickname,
|
||||
"photo_avatar_filename" => $user->photo_avatar_filename,
|
||||
"type" => $user->type,
|
||||
]);
|
||||
}
|
||||
}
|
||||
@@ -101,22 +101,22 @@ class UserController extends Controller
|
||||
*/
|
||||
public function update(Request $request, User $user)
|
||||
{
|
||||
$this->authorize('update', $user);
|
||||
|
||||
$this->authorize("update", $user);
|
||||
|
||||
$validated = $request->validate([
|
||||
'name' => 'sometimes|string|max:255',
|
||||
'nickname' => [
|
||||
'sometimes',
|
||||
'string',
|
||||
'max:20',
|
||||
Rule::unique('users')->ignore($user->id),
|
||||
"name" => "sometimes|string|max:255",
|
||||
"nickname" => [
|
||||
"sometimes",
|
||||
"string",
|
||||
"max:20",
|
||||
Rule::unique("users")->ignore($user->id),
|
||||
],
|
||||
'email' => [
|
||||
'sometimes',
|
||||
'email',
|
||||
Rule::unique('users')->ignore($user->id),
|
||||
"email" => [
|
||||
"sometimes",
|
||||
"email",
|
||||
Rule::unique("users")->ignore($user->id),
|
||||
],
|
||||
'password' => 'sometimes|string|min:3',
|
||||
"password" => "sometimes|string|min:3",
|
||||
]);
|
||||
|
||||
// Lógica de Upload de Foto (Exemplo Básico)
|
||||
@@ -130,26 +130,28 @@ class UserController extends Controller
|
||||
return response()->json($user);
|
||||
}
|
||||
|
||||
|
||||
public function updatePassword(Request $request, User $user)
|
||||
{
|
||||
$this->authorize('update', $user);
|
||||
$this->authorize("update", $user);
|
||||
|
||||
$validated = $request->validate([
|
||||
'current_password' => 'required|string',
|
||||
'new_password' => 'required|string|min:3|confirmed',
|
||||
"current_password" => "required|string",
|
||||
"new_password" => "required|string|min:3|confirmed",
|
||||
]);
|
||||
|
||||
// Verificar se a password atual está correta
|
||||
if (!Hash::check($validated['current_password'], $user->password)) {
|
||||
return response()->json(['message' => 'Current password is incorrect'], 403);
|
||||
if (!Hash::check($validated["current_password"], $user->password)) {
|
||||
return response()->json(
|
||||
["message" => "Current password is incorrect"],
|
||||
403,
|
||||
);
|
||||
}
|
||||
|
||||
// Atualizar para a nova password
|
||||
$user->password = Hash::make($validated['new_password']);
|
||||
$user->password = Hash::make($validated["new_password"]);
|
||||
$user->save();
|
||||
|
||||
return response()->json(['message' => 'Password updated successfully']);
|
||||
return response()->json(["message" => "Password updated successfully"]);
|
||||
}
|
||||
|
||||
/**
|
||||
@@ -158,24 +160,27 @@ class UserController extends Controller
|
||||
*/
|
||||
public function destroy(User $user)
|
||||
{
|
||||
$this->authorize('delete', $user);
|
||||
$this->authorize("delete", $user);
|
||||
|
||||
// Check if user has transactions or games - if so, soft delete
|
||||
$hasTransactions = $user->transactions()->exists();
|
||||
$hasGames = Game::where(function ($q) use ($user) {
|
||||
$q->where('player1_user_id', $user->id)
|
||||
->orWhere('player2_user_id', $user->id);
|
||||
$q->where("player1_user_id", $user->id)->orWhere(
|
||||
"player2_user_id",
|
||||
$user->id,
|
||||
);
|
||||
})->exists();
|
||||
|
||||
if ($hasTransactions || $hasGames) {
|
||||
$user->delete(); // Soft delete
|
||||
$message = 'User account deactivated (soft-deleted due to transaction/game history)';
|
||||
$message =
|
||||
"User account deactivated (soft-deleted due to transaction/game history)";
|
||||
} else {
|
||||
$user->forceDelete(); // Hard delete
|
||||
$message = 'User permanently deleted';
|
||||
$message = "User permanently deleted";
|
||||
}
|
||||
|
||||
return response()->json(['message' => $message]);
|
||||
return response()->json(["message" => $message]);
|
||||
}
|
||||
|
||||
/**
|
||||
@@ -184,37 +189,61 @@ class UserController extends Controller
|
||||
*/
|
||||
public function getGames(Request $request, User $user)
|
||||
{
|
||||
$this->authorize('view', $user);
|
||||
$this->authorize("view", $user);
|
||||
|
||||
// Define a query base para GAMES
|
||||
$query = \App\Models\Game::query()
|
||||
->where(function ($q) use ($user) {
|
||||
$q->where('player1_user_id', $user->id)
|
||||
->orWhere('player2_user_id', $user->id);
|
||||
$q->where("player1_user_id", $user->id)->orWhere(
|
||||
"player2_user_id",
|
||||
$user->id,
|
||||
);
|
||||
})
|
||||
->with(['winner', 'player1', 'player2']);
|
||||
->with(["winner", "player1", "player2"]);
|
||||
|
||||
if ($request->has('type')) {
|
||||
$query->where('type', $request->type);
|
||||
if ($request->has("type")) {
|
||||
$query->where("type", $request->type);
|
||||
}
|
||||
|
||||
if ($request->has('status')) {
|
||||
$query->where('status', $request->status);
|
||||
if ($request->has("status")) {
|
||||
$query->where("status", $request->status);
|
||||
}
|
||||
|
||||
$sortDirection = $request->get('sort_direction', 'desc');
|
||||
$query->orderBy('began_at', $sortDirection);
|
||||
// Date filtering
|
||||
if ($request->has("date_from")) {
|
||||
$query->whereDate("began_at", ">=", $request->date_from);
|
||||
}
|
||||
|
||||
if ($request->has("date_to")) {
|
||||
$query->whereDate("began_at", "<=", $request->date_to);
|
||||
}
|
||||
|
||||
// Flexible sorting
|
||||
$sortColumn = $request->get("sort_by", "began_at");
|
||||
$sortDirection = $request->get("sort_direction", "desc");
|
||||
|
||||
// Whitelist sortable columns for security
|
||||
$allowedColumns = [
|
||||
"began_at",
|
||||
"ended_at",
|
||||
"total_time",
|
||||
"total_points",
|
||||
];
|
||||
if (in_array($sortColumn, $allowedColumns)) {
|
||||
$query->orderBy($sortColumn, $sortDirection);
|
||||
} else {
|
||||
$query->orderBy("began_at", $sortDirection);
|
||||
}
|
||||
|
||||
|
||||
$games = $query->paginate(10);
|
||||
|
||||
$games->getCollection()->transform(function ($game) use ($user) {
|
||||
if ($game->winner_user_id == $user->id) {
|
||||
$game->outcome = 'win';
|
||||
$game->outcome = "win";
|
||||
} elseif ($game->is_draw) {
|
||||
$game->outcome = 'draw';
|
||||
$game->outcome = "draw";
|
||||
} else {
|
||||
$game->outcome = 'loss';
|
||||
$game->outcome = "loss";
|
||||
}
|
||||
return $game;
|
||||
});
|
||||
@@ -228,35 +257,58 @@ class UserController extends Controller
|
||||
*/
|
||||
public function getMatches(Request $request, User $user)
|
||||
{
|
||||
$this->authorize('view', $user);
|
||||
$this->authorize("view", $user);
|
||||
|
||||
$query = MatchGame::query()
|
||||
$query = MatchGame::query()
|
||||
->where(function ($q) use ($user) {
|
||||
$q->where('player1_user_id', $user->id)
|
||||
->orWhere('player2_user_id', $user->id);
|
||||
$q->where("player1_user_id", $user->id)->orWhere(
|
||||
"player2_user_id",
|
||||
$user->id,
|
||||
);
|
||||
})
|
||||
->with(['winner', 'player1', 'player2']);
|
||||
->with(["winner", "player1", "player2"]);
|
||||
|
||||
if ($request->has('type')) {
|
||||
$query->where('type', $request->type);
|
||||
}
|
||||
|
||||
if ($request->has('status')) {
|
||||
$query->where('status', $request->status);
|
||||
if ($request->has("type")) {
|
||||
$query->where("type", $request->type);
|
||||
}
|
||||
|
||||
$sortDirection = $request->get('sort_direction', 'desc');
|
||||
$query->orderBy('began_at', $sortDirection);
|
||||
if ($request->has("status")) {
|
||||
$query->where("status", $request->status);
|
||||
}
|
||||
|
||||
// Date filtering
|
||||
if ($request->has("date_from")) {
|
||||
$query->whereDate("began_at", ">=", $request->date_from);
|
||||
}
|
||||
|
||||
if ($request->has("date_to")) {
|
||||
$query->whereDate("began_at", "<=", $request->date_to);
|
||||
}
|
||||
|
||||
// Flexible sorting
|
||||
$sortColumn = $request->get("sort_by", "began_at");
|
||||
$sortDirection = $request->get("sort_direction", "desc");
|
||||
|
||||
// Whitelist sortable columns for security
|
||||
$allowedColumns = ["began_at", "ended_at", "stake", "total_time"];
|
||||
if (in_array($sortColumn, $allowedColumns)) {
|
||||
$query->orderBy($sortColumn, $sortDirection);
|
||||
} else {
|
||||
$query->orderBy("began_at", $sortDirection);
|
||||
}
|
||||
|
||||
$matches = $query->paginate(10);
|
||||
|
||||
$matches->getCollection()->transform(function ($match) use ($user) {
|
||||
if ($match->winner_user_id == $user->id) {
|
||||
$match->outcome = 'win';
|
||||
} elseif ($match->winner_user_id && $match->winner_user_id != $user->id) {
|
||||
$match->outcome = 'loss';
|
||||
$match->outcome = "win";
|
||||
} elseif (
|
||||
$match->winner_user_id &&
|
||||
$match->winner_user_id != $user->id
|
||||
) {
|
||||
$match->outcome = "loss";
|
||||
} else {
|
||||
$match->outcome = 'interrupted'; // Matches geralmente não empatam (alguém chega a 4 marcas)
|
||||
$match->outcome = "interrupted"; // Matches geralmente não empatam (alguém chega a 4 marcas)
|
||||
}
|
||||
return $match;
|
||||
});
|
||||
@@ -266,7 +318,7 @@ class UserController extends Controller
|
||||
|
||||
public function block(User $user)
|
||||
{
|
||||
$this->authorize('block', $user);
|
||||
$this->authorize("block", $user);
|
||||
|
||||
$user->blocked = true;
|
||||
$user->save();
|
||||
@@ -274,43 +326,51 @@ class UserController extends Controller
|
||||
// Revoke all active tokens for immediate effect
|
||||
$user->tokens()->delete();
|
||||
|
||||
return response()->json(['message' => 'User blocked successfully']);
|
||||
return response()->json(["message" => "User blocked successfully"]);
|
||||
}
|
||||
|
||||
public function unblock(User $user)
|
||||
{
|
||||
$this->authorize('unblock', $user);
|
||||
$this->authorize("unblock", $user);
|
||||
|
||||
$user->blocked = false;
|
||||
$user->save();
|
||||
|
||||
return response()->json(['message' => 'User unblocked successfully']);
|
||||
return response()->json(["message" => "User unblocked successfully"]);
|
||||
}
|
||||
|
||||
public function getTransactions(Request $request, User $user)
|
||||
{
|
||||
if ($request->user()->id !== $user->id && $request->user()->type !== 'A') {
|
||||
return response()->json(['message' => 'Forbidden'], 403);
|
||||
if (
|
||||
$request->user()->id !== $user->id &&
|
||||
$request->user()->type !== "A"
|
||||
) {
|
||||
return response()->json(["message" => "Forbidden"], 403);
|
||||
}
|
||||
|
||||
$query = $user->transactions()->with('type');
|
||||
$query = $user->transactions()->with("type");
|
||||
|
||||
if ($request->has('type')) {
|
||||
$query->whereHas('type', function ($q) use ($request) {
|
||||
$q->where('type', $request->type);
|
||||
if ($request->has("type")) {
|
||||
$query->whereHas("type", function ($q) use ($request) {
|
||||
$q->where("type", $request->type);
|
||||
});
|
||||
}
|
||||
|
||||
if ($request->has('date_from')) {
|
||||
$query->whereDate('transaction_datetime', '>=', $request->date_from);
|
||||
if ($request->has("date_from")) {
|
||||
$query->whereDate(
|
||||
"transaction_datetime",
|
||||
">=",
|
||||
$request->date_from,
|
||||
);
|
||||
}
|
||||
|
||||
if ($request->has('date_to')) {
|
||||
$query->whereDate('transaction_datetime', '<=', $request->date_to);
|
||||
if ($request->has("date_to")) {
|
||||
$query->whereDate("transaction_datetime", "<=", $request->date_to);
|
||||
}
|
||||
|
||||
$transactions = $query->orderBy('transaction_datetime', 'desc')
|
||||
->paginate(10);
|
||||
$transactions = $query
|
||||
->orderBy("transaction_datetime", "desc")
|
||||
->paginate(10);
|
||||
|
||||
return response()->json($transactions);
|
||||
}
|
||||
|
||||
@@ -1,64 +1,54 @@
|
||||
<template>
|
||||
<div>
|
||||
<NavigationMenu>
|
||||
<NavigationMenuList class="justify-around gap-20">
|
||||
<NavigationMenuItem>
|
||||
<NavigationMenuTrigger>Testing</NavigationMenuTrigger>
|
||||
<NavigationMenuContent>
|
||||
<li>
|
||||
<NavigationMenuLink as-child>
|
||||
<RouterLink to="/testing/laravel">Laravel</RouterLink>
|
||||
</NavigationMenuLink>
|
||||
<NavigationMenuLink as-child>
|
||||
<RouterLink to="/testing/websockets">Web Sockets</RouterLink>
|
||||
</NavigationMenuLink>
|
||||
</li>
|
||||
</NavigationMenuContent>
|
||||
</NavigationMenuItem>
|
||||
<div>
|
||||
<NavigationMenu>
|
||||
<NavigationMenuList class="justify-around gap-20">
|
||||
<NavigationMenuItem v-if="!userLoggedIn">
|
||||
<NavigationMenuLink>
|
||||
<RouterLink to="/login">Login</RouterLink>
|
||||
</NavigationMenuLink>
|
||||
</NavigationMenuItem>
|
||||
|
||||
<NavigationMenuItem v-if="!userLoggedIn">
|
||||
<NavigationMenuLink>
|
||||
<RouterLink to="/login">Login</RouterLink>
|
||||
</NavigationMenuLink>
|
||||
</NavigationMenuItem>
|
||||
<NavigationMenuItem v-else class="flex flex-row gap-4">
|
||||
<NavigationMenuLink
|
||||
v-if="userStore.coins > 0"
|
||||
href="/coins-purchase"
|
||||
class="flex flex-row items-center gap-1"
|
||||
>
|
||||
<div
|
||||
class="flex items-center justify-center w-5 h-5 bg-purple-400 rounded-full shadow-sm"
|
||||
>
|
||||
<span class="text-[10px] font-bold text-purple-900">$</span>
|
||||
</div>
|
||||
|
||||
<NavigationMenuItem v-else class="flex flex-row gap-4">
|
||||
<NavigationMenuLink v-if="userStore.coins > 0" href="/coins-purchase"
|
||||
class="flex flex-row items-center gap-1">
|
||||
<div class="flex items-center justify-center w-5 h-5 bg-purple-400 rounded-full shadow-sm">
|
||||
<span class="text-[10px] font-bold text-purple-900">$</span>
|
||||
</div>
|
||||
|
||||
<span class="text-sm font-medium tabular-nums">
|
||||
{{ userStore.coins }} <span class="text-muted-foreground text-xs">Coins</span>
|
||||
</span>
|
||||
</NavigationMenuLink>
|
||||
<NavigationMenuLink>
|
||||
<RouterLink to="/user">Profile</RouterLink>
|
||||
</NavigationMenuLink>
|
||||
<NavigationMenuLink>
|
||||
<a href="/home" @click.prevent="logoutClickHandler" class="cursor-pointer">Logout</a>
|
||||
</NavigationMenuLink>
|
||||
</NavigationMenuItem>
|
||||
</NavigationMenuList>
|
||||
</NavigationMenu>
|
||||
</div>
|
||||
<span class="text-sm font-medium tabular-nums">
|
||||
{{ userStore.coins }} <span class="text-muted-foreground text-xs">Coins</span>
|
||||
</span>
|
||||
</NavigationMenuLink>
|
||||
<NavigationMenuLink>
|
||||
<RouterLink to="/user">Profile</RouterLink>
|
||||
</NavigationMenuLink>
|
||||
<NavigationMenuLink>
|
||||
<a href="/home" @click.prevent="logoutClickHandler" class="cursor-pointer">Logout</a>
|
||||
</NavigationMenuLink>
|
||||
</NavigationMenuItem>
|
||||
</NavigationMenuList>
|
||||
</NavigationMenu>
|
||||
</div>
|
||||
</template>
|
||||
|
||||
<script setup>
|
||||
import { useBiscaStore } from '@/stores/bisca'
|
||||
import {
|
||||
NavigationMenu,
|
||||
NavigationMenuContent,
|
||||
NavigationMenuItem,
|
||||
NavigationMenuLink,
|
||||
NavigationMenuList,
|
||||
NavigationMenuTrigger,
|
||||
NavigationMenu,
|
||||
NavigationMenuContent,
|
||||
NavigationMenuItem,
|
||||
NavigationMenuLink,
|
||||
NavigationMenuList,
|
||||
NavigationMenuTrigger,
|
||||
} from '@/components/ui/navigation-menu'
|
||||
import router from '@/router';
|
||||
import { watch } from 'vue';
|
||||
import { useUserStore } from '@/stores/user';
|
||||
|
||||
import router from '@/router'
|
||||
import { watch } from 'vue'
|
||||
import { useUserStore } from '@/stores/user'
|
||||
|
||||
const emits = defineEmits(['logout'])
|
||||
const { userLoggedIn } = defineProps(['userLoggedIn'])
|
||||
@@ -66,30 +56,34 @@ const { userLoggedIn } = defineProps(['userLoggedIn'])
|
||||
const biscaStore = useBiscaStore()
|
||||
|
||||
const logoutClickHandler = () => {
|
||||
if (biscaStore.isGameRunning) {
|
||||
const confirmLogout = window.confirm(
|
||||
'If you leave now this game will count as "FORFEIT". Are you sure you want to logout?'
|
||||
)
|
||||
if (!confirmLogout) return
|
||||
if (biscaStore.isGameRunning) {
|
||||
const confirmLogout = window.confirm(
|
||||
'If you leave now this game will count as "FORFEIT". Are you sure you want to logout?',
|
||||
)
|
||||
if (!confirmLogout) return
|
||||
|
||||
biscaStore.isLoggingOut = true
|
||||
biscaStore.isLoggingOut = true
|
||||
|
||||
biscaStore.quitGame()
|
||||
biscaStore.quitGame()
|
||||
|
||||
return
|
||||
}
|
||||
return
|
||||
}
|
||||
|
||||
emits('logout')
|
||||
router.push('/login')
|
||||
emits('logout')
|
||||
router.push('/login')
|
||||
}
|
||||
|
||||
const userStore = useUserStore()
|
||||
|
||||
watch(() => userLoggedIn, async (isLoggedIn) => {
|
||||
watch(
|
||||
() => userLoggedIn,
|
||||
async (isLoggedIn) => {
|
||||
if (isLoggedIn) {
|
||||
await userStore.fetchCoins()
|
||||
await userStore.fetchCoins()
|
||||
} else {
|
||||
userStore.coins = 0
|
||||
userStore.coins = 0
|
||||
}
|
||||
}, { immediate: true })
|
||||
</script>
|
||||
},
|
||||
{ immediate: true },
|
||||
)
|
||||
</script>
|
||||
|
||||
@@ -1,342 +0,0 @@
|
||||
<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>
|
||||
@@ -1,252 +0,0 @@
|
||||
<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>
|
||||
@@ -1,275 +0,0 @@
|
||||
<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>
|
||||
@@ -1,444 +0,0 @@
|
||||
<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>
|
||||
@@ -603,65 +603,6 @@
|
||||
v-else-if="activeTab === 'matches'"
|
||||
class="animate-in fade-in slide-in-from-bottom-2 duration-300"
|
||||
>
|
||||
<div class="p-4 bg-gray-50 border-b border-gray-300 flex flex-wrap gap-4 items-end">
|
||||
<div class="flex flex-col gap-1">
|
||||
<label class="text-[10px] font-bold uppercase text-gray-400 tracking-widest"
|
||||
>From</label
|
||||
>
|
||||
<input
|
||||
type="date"
|
||||
v-model="filters.match_date_from"
|
||||
@change="resetAndFetch"
|
||||
class="bg-white border border-gray-300 p-2 text-xs font-bold uppercase outline-none focus:border-black"
|
||||
/>
|
||||
</div>
|
||||
<div class="flex flex-col gap-1">
|
||||
<label class="text-[10px] font-bold uppercase text-gray-400 tracking-widest"
|
||||
>To</label
|
||||
>
|
||||
<input
|
||||
type="date"
|
||||
v-model="filters.match_date_to"
|
||||
@change="resetAndFetch"
|
||||
class="bg-white border border-gray-300 p-2 text-xs font-bold uppercase outline-none focus:border-black"
|
||||
/>
|
||||
</div>
|
||||
<div class="flex flex-col gap-1">
|
||||
<label class="text-[10px] font-bold uppercase text-gray-400 tracking-widest"
|
||||
>Variant</label
|
||||
>
|
||||
<select
|
||||
v-model="filters.match_type"
|
||||
@change="resetAndFetch"
|
||||
class="bg-white border border-gray-300 p-2 text-xs font-bold uppercase outline-none focus:border-black min-w-[100px]"
|
||||
>
|
||||
<option :value="null">All</option>
|
||||
<option value="3">Bisca 3</option>
|
||||
<option value="9">Bisca 9</option>
|
||||
</select>
|
||||
</div>
|
||||
<div class="flex flex-col gap-1">
|
||||
<label class="text-[10px] font-bold uppercase text-gray-400 tracking-widest"
|
||||
>Outcome</label
|
||||
>
|
||||
<select
|
||||
v-model="filters.match_outcome"
|
||||
@change="resetAndFetch"
|
||||
class="bg-white border border-gray-300 p-2 text-xs font-bold uppercase outline-none focus:border-black min-w-[100px]"
|
||||
>
|
||||
<option :value="null">All</option>
|
||||
<option value="win">Win</option>
|
||||
<option value="draw">Draw</option>
|
||||
</select>
|
||||
</div>
|
||||
<button
|
||||
@click="clearMatchFilters"
|
||||
class="text-[10px] font-bold uppercase underline hover:text-gray-600 mb-2 ml-auto"
|
||||
>
|
||||
Clear
|
||||
</button>
|
||||
</div>
|
||||
|
||||
<div class="overflow-x-auto">
|
||||
<div
|
||||
id="table-scroll-container"
|
||||
@@ -670,20 +611,34 @@
|
||||
<table class="w-full text-left border-collapse sticky-header">
|
||||
<thead class="bg-gray-50 border-b border-gray-300">
|
||||
<tr>
|
||||
<th class="p-4 text-[10px] font-bold uppercase tracking-widest text-gray-500">
|
||||
<th
|
||||
class="p-4 text-[10px] font-bold uppercase tracking-widest text-gray-500"
|
||||
@click="handleSort('date')"
|
||||
>
|
||||
Match Date
|
||||
<span>{{ filters.match_sort_direction === 'desc' ? ' ↓' : ' ↑' }}</span>
|
||||
</th>
|
||||
<th class="p-4 text-[10px] font-bold uppercase tracking-widest text-gray-500">
|
||||
Matchup (P1 vs P2)
|
||||
</th>
|
||||
<th class="p-4 text-[10px] font-bold uppercase tracking-widest text-gray-500">
|
||||
<th
|
||||
class="p-4 text-[10px] font-bold uppercase tracking-widest text-gray-500"
|
||||
@click="handleSort('type')"
|
||||
>
|
||||
Variant
|
||||
<span v-if="filters.match_type"> (Bisca {{ filters.match_type }})</span>
|
||||
<span v-else> ↕</span>
|
||||
</th>
|
||||
<th class="p-4 text-[10px] font-bold uppercase tracking-widest text-gray-500">
|
||||
Outcome
|
||||
</th>
|
||||
<th class="p-4 text-[10px] font-bold uppercase tracking-widest text-gray-500">
|
||||
<th
|
||||
class="p-4 text-[10px] font-bold uppercase tracking-widest text-gray-500"
|
||||
@click="handleSort('pot')"
|
||||
>
|
||||
Total Pot
|
||||
<span v-if="filters.pot"> ({{ filters.pot }})</span>
|
||||
<span v-else> ↕</span>
|
||||
</th>
|
||||
</tr>
|
||||
</thead>
|
||||
@@ -882,6 +837,8 @@ const filters = reactive({
|
||||
match_outcome: null,
|
||||
match_date_from: '',
|
||||
match_date_to: '',
|
||||
match_sort_direction: null,
|
||||
pot: null,
|
||||
})
|
||||
|
||||
watch(activeTab, async () => {
|
||||
@@ -987,19 +944,6 @@ const filteredUsers = computed(() => {
|
||||
return users.value.filter((user) => user.id !== useAuthStore().currentUserID)
|
||||
})
|
||||
|
||||
const resetAndFetch = async () => {
|
||||
resetPagination()
|
||||
await fetchData()
|
||||
}
|
||||
|
||||
const clearMatchFilters = async () => {
|
||||
filters.match_type = null
|
||||
filters.match_outcome = null
|
||||
filters.match_date_from = ''
|
||||
filters.match_date_to = ''
|
||||
await resetAndFetch()
|
||||
}
|
||||
|
||||
const fetchData = async () => {
|
||||
if (isLoading.value) return
|
||||
const currentTab = activeTab.value
|
||||
@@ -1039,10 +983,9 @@ const fetchData = async () => {
|
||||
} else if (currentTab === 'matches') {
|
||||
const params = {
|
||||
page: pagination[currentTab].page,
|
||||
sort_direction: filters.match_sort_direction,
|
||||
type: filters.match_type,
|
||||
outcome: filters.match_outcome,
|
||||
date_from: filters.match_date_from || undefined,
|
||||
date_to: filters.match_date_to || undefined,
|
||||
pot: filters.pot,
|
||||
}
|
||||
response = await apiStore.getAdminMatches(params)
|
||||
matches.value.push(...response.data.data)
|
||||
@@ -1080,6 +1023,19 @@ const handleSort = async (column) => {
|
||||
else if (filters.game_type === '9') filters.game_type = '3'
|
||||
else filters.game_type = null
|
||||
}
|
||||
} else if (activeTab.value === 'matches') {
|
||||
if (column === 'type') {
|
||||
filters.match_type =
|
||||
filters.match_type === null ? '9' : filters.match_type === '9' ? '3' : null
|
||||
} else if (column === 'date') {
|
||||
filters.pot = null // Reset pot sort when sorting by date
|
||||
filters.match_sort_direction = filters.match_sort_direction === 'desc' ? 'asc' : 'desc'
|
||||
} else if (column === 'pot') {
|
||||
filters.match_sort_direction = null // Reset date sort when sorting by pot
|
||||
if (filters.pot === null) filters.pot = 'desc'
|
||||
else if (filters.pot === 'desc') filters.pot = 'asc'
|
||||
else filters.pot = 'desc'
|
||||
}
|
||||
}
|
||||
|
||||
resetPagination()
|
||||
|
||||
@@ -526,21 +526,6 @@
|
||||
</select>
|
||||
</div>
|
||||
|
||||
<div class="flex flex-col gap-1">
|
||||
<label class="text-xs font-medium text-muted-foreground">Outcome</label>
|
||||
<select
|
||||
v-model="filters.outcome"
|
||||
@change="resetAndFetch"
|
||||
class="bg-background border rounded-md px-3 py-2 text-sm outline-none"
|
||||
>
|
||||
<option value="all">All Results</option>
|
||||
<option value="win">Win</option>
|
||||
<option value="loss">Loss</option>
|
||||
<option value="draw">Draw</option>
|
||||
<option value="forfeit">Forfeit</option>
|
||||
</select>
|
||||
</div>
|
||||
|
||||
<div class="flex flex-col gap-1">
|
||||
<label class="text-xs font-medium text-muted-foreground">From</label>
|
||||
<input
|
||||
@@ -577,7 +562,6 @@
|
||||
class="bg-background border rounded-md px-3 py-2 text-sm outline-none focus:ring-2 focus:ring-purple-500"
|
||||
>
|
||||
<option value="began_at">Date</option>
|
||||
<option value="outcome">Outcome</option>
|
||||
<option value="total_time">Duration</option>
|
||||
</select>
|
||||
|
||||
@@ -666,21 +650,6 @@
|
||||
</select>
|
||||
</div>
|
||||
|
||||
<div class="flex flex-col gap-1">
|
||||
<label class="text-xs font-medium text-muted-foreground">Outcome</label>
|
||||
<select
|
||||
v-model="filters.outcome"
|
||||
@change="resetAndFetch"
|
||||
class="bg-background border rounded-md px-3 py-2 text-sm outline-none"
|
||||
>
|
||||
<option value="all">All Results</option>
|
||||
<option value="win">Win</option>
|
||||
<option value="loss">Loss</option>
|
||||
<option value="draw">Draw</option>
|
||||
<option value="forfeit">Forfeit</option>
|
||||
</select>
|
||||
</div>
|
||||
|
||||
<div class="flex flex-col gap-1">
|
||||
<label class="text-xs font-medium text-muted-foreground">From</label>
|
||||
<input
|
||||
@@ -717,7 +686,6 @@
|
||||
class="bg-background border rounded-md px-3 py-2 text-sm outline-none focus:ring-2 focus:ring-purple-500"
|
||||
>
|
||||
<option value="began_at">Date</option>
|
||||
<option value="outcome">Outcome</option>
|
||||
<option value="total_time">Duration</option>
|
||||
</select>
|
||||
|
||||
@@ -1083,7 +1051,6 @@ const pagination = ref({
|
||||
const filters = ref({
|
||||
type: 'all',
|
||||
variant: 'all',
|
||||
outcome: 'all',
|
||||
startDate: '',
|
||||
endDate: '',
|
||||
sort: {
|
||||
@@ -1164,7 +1131,6 @@ const fetchData = async () => {
|
||||
const gameMatchParams = {
|
||||
page: state.page,
|
||||
type: filters.value.variant !== 'all' ? filters.value.variant : undefined,
|
||||
outcome: filters.value.outcome !== 'all' ? filters.value.outcome : undefined,
|
||||
status: 'Ended',
|
||||
date_from: filters.value.startDate || undefined,
|
||||
date_to: filters.value.endDate || undefined,
|
||||
@@ -1324,7 +1290,6 @@ const clearFilters = () => {
|
||||
|
||||
if (isGameOrMatch) {
|
||||
filters.value.variant = 'all'
|
||||
filters.value.outcome = 'all'
|
||||
} else {
|
||||
filters.value.type = 'all'
|
||||
}
|
||||
|
||||
@@ -1,13 +1,7 @@
|
||||
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 { createRouter, createWebHistory } from 'vue-router'
|
||||
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 SinglePlayerGamePage from '@/pages/game/SinglePlayerGamePage.vue'
|
||||
import MultiplayerGamePage from '@/pages/game/MultiplayerGamePage.vue'
|
||||
import RegisterPage from '@/pages/register/RegisterPage.vue'
|
||||
@@ -22,7 +16,7 @@ const router = createRouter({
|
||||
path: '/',
|
||||
name: 'home',
|
||||
component: HomePage,
|
||||
meta: { requiresAdmin: false }
|
||||
meta: { requiresAdmin: false },
|
||||
},
|
||||
{
|
||||
path: '/login',
|
||||
@@ -52,7 +46,7 @@ const router = createRouter({
|
||||
path: '/game/multiplayer/:id',
|
||||
name: 'multiplayer-game',
|
||||
component: MultiplayerGamePage,
|
||||
meta: { requiresAuth: true }
|
||||
meta: { requiresAuth: true },
|
||||
},
|
||||
{
|
||||
path: '/user',
|
||||
@@ -70,35 +64,6 @@ const router = createRouter({
|
||||
component: AdminPage,
|
||||
meta: { requiresAuth: true, requiresAdmin: true },
|
||||
},
|
||||
{
|
||||
path: '/testing',
|
||||
children: [
|
||||
{
|
||||
path: 'laravel',
|
||||
component: LaravelPage,
|
||||
},
|
||||
{
|
||||
path: 'websockets',
|
||||
component: WebsocketsPage,
|
||||
},
|
||||
{
|
||||
path: 'animations',
|
||||
component: TestAnimations,
|
||||
},
|
||||
{
|
||||
path: 'dealing',
|
||||
component: TestDealing,
|
||||
},
|
||||
{
|
||||
path: 'all-animations',
|
||||
component: TestAllAnimations,
|
||||
},
|
||||
{
|
||||
path: 'gameboard',
|
||||
component: TestGameBoard,
|
||||
},
|
||||
],
|
||||
},
|
||||
],
|
||||
})
|
||||
|
||||
|
||||
@@ -174,19 +174,16 @@ export const useAPIStore = defineStore('api', () => {
|
||||
...(params.sort_direction == 'asc' && { sort_direction: 'asc' }),
|
||||
...(params.type && { type: params.type }),
|
||||
}).toString()
|
||||
|
||||
|
||||
return axios.get(`${API_BASE_URL}/games?${queryParams}`)
|
||||
}
|
||||
|
||||
const getAdminMatches = (params = {}) => {
|
||||
const queryParams = new URLSearchParams({
|
||||
page: params.page || 1,
|
||||
...(params.sort_direction == 'asc' && { sort_direction: 'asc' }),
|
||||
...(params.type && { type: params.type }),
|
||||
...(params.outcome && { outcome: params.outcome }),
|
||||
...(params.date_from && { date_from: params.date_from }),
|
||||
...(params.date_to && { date_to: params.date_to }),
|
||||
sort_by: params.sort_by || 'began_at',
|
||||
sort_direction: params.sort_direction || 'desc',
|
||||
...(params.pot && { pot: params.pot }),
|
||||
}).toString()
|
||||
|
||||
return axios.get(`${API_BASE_URL}/matches?${queryParams}`)
|
||||
|
||||
Reference in New Issue
Block a user