feat: Enhance game and lobby components with improved state management and user notifications

This commit is contained in:
2025-12-14 15:08:13 +01:00
parent 76258b3ca8
commit 30798ce723
6 changed files with 72 additions and 78 deletions

View File

@@ -3,35 +3,38 @@ import {useWebSocket} from "@/composables/useWebsocket.ts";
import {useIngame} from "@/composables/useIngame.ts";
import type {GameInfo} from "@/types/GameTypes.ts";
import {useQuasar} from "quasar";
import { ref } from 'vue';
const wb = useWebSocket()
const wi = useIngame()
const $q = useQuasar();
const wiggleIdx = ref<number | null>(null)
let wiggleTimer: ReturnType<typeof setTimeout> | null = null
function triggerWiggle(index: number) {
// clear previous timer if any
if (wiggleTimer) clearTimeout(wiggleTimer)
wiggleIdx.value = index
wiggleTimer = setTimeout(() => {
wiggleIdx.value = null
wiggleTimer = null
}, 700)
}
function handlePlayCard(index: number | null) {
if (index === null) return
const wiggleKeyframes = [
{ transform: 'translateX(0)' },
{ transform: 'translateX(-5px)' },
{ transform: 'translateX(5px)' },
{ transform: 'translateX(-5px)' },
{ transform: 'translateX(0)' }
];
const wiggleTiming = {
duration: 400,
iterations: 1,
easing: 'ease-in-out',
fill: 'forwards'
};
wb.sendAndWait("PlayCard", { cardindex: index })
.catch((error) => {
triggerWiggle(index)
$q.notify({
message: "Error playing card: " + error.message,
color: "negative"
message: error.message,
color: "negative",
position: "top"
})
})
}
@@ -50,7 +53,7 @@ function getCardImagePath(cardPath: string) {
<div class="hand-container">
<div id="card-slide" class="ingame-cards-slide">
<div class="cards-row">
<div v-for="card in (<GameInfo>wi.data)?.hand?.cards" :key="card.identifier" class="handcard">
<div v-for="card in (<GameInfo>wi.data)?.hand?.cards" :key="card.identifier" :class="['handcard', { wiggle: wiggleIdx === card.idx }]">
<div class="card-btn" aria-label="Play card">
<q-img :src="getCardImagePath(card.path)" v-on:click="handlePlayCard(card.idx)" :alt="card.identifier" class="card" />
</div>
@@ -114,6 +117,18 @@ function getCardImagePath(cardPath: string) {
width:120px;
border-radius:6px
}
.wiggle {
animation: wiggle 700ms ease-in-out;
}
@keyframes wiggle {
0% { transform: translateY(0) rotate(0deg); }
15% { transform: translateY(-8px) rotate(-6deg); }
35% { transform: translateY(0) rotate(6deg); }
55% { transform: translateY(-4px) rotate(-3deg); }
75% { transform: translateY(0) rotate(2deg); }
100% { transform: translateY(0) rotate(0deg); }
}
@media (max-height: 500px) {
.card {

View File

@@ -4,31 +4,60 @@ import {useIngame} from "@/composables/useIngame.ts";
import type {LobbyInfo} from "@/types/GameTypes.ts";
import {useWebSocket} from "@/composables/useWebsocket.ts";
import {computed} from "vue";
import router from "@/router";
import {useQuasar} from "quasar";
const wb = useWebSocket()
const ig = useIngame();
const $q = useQuasar();
const maxPlayers = computed(() => (<LobbyInfo>ig.data).maxPlayers);
const isHost = computed(() => (<LobbyInfo>ig.data).self.host);
const players = computed(() => {
return (<LobbyInfo>ig.data).users;
})
const lobbyName = `${ig.data?.gameId}`;
const lobbyName = computed(() => {
return `${ig.data?.gameId}`
});
const handleKickPlayer = (user: User) => {
if (isHost) {
emit('kick-player', user);
wb.send("KickPlayer", {playerId: user.id})
}
};
const handleStartGame = () => {
if (isHost) {
wb.send("StartGame", {})
wb.sendAndWait("StartGame", {})
}
};
const handleLeaveGame = (user: User) => {
emit('leave-game', user);
wb.sendAndWait("LeaveGame", {user: user})
};
wb.useEvent("SessionClosed", () => {
$q.notify({
message: `You left the lobby.`,
color: "positive"
})
router.replace("/")
})
wb.useEvent("LeftEvent", () => {
$q.notify({
message: `You left the lobby.`,
color: "positive"
})
router.replace("/")
})
wb.useEvent("KickEvent", () => {
$q.notify({
message: `You were kicked from the lobby!`,
color: "amber"
})
router.replace("/")
})
const profileIcon = 'person';
</script>
@@ -75,7 +104,7 @@ const profileIcon = 'person';
v-if="player.id !== (<LobbyInfo>ig.data).self.id"
color="negative"
label="Remove"
@click="handleKickPlayer((<LobbyInfo>ig.data).self)"
@click="handleKickPlayer(player)"
class="full-width"
/>
<q-btn

View File

@@ -37,10 +37,12 @@ export function useWebSocket() {
};
onMounted(() => {
console.log("Registering event handler for " + event);
onEvent(event, wrapped);
});
onBeforeUnmount(() => {
console.log("Unregistering event handler for " + event);
onEvent(event, () => {});
});
}

View File

@@ -7,8 +7,6 @@ import defaultMenu from "../components/DefaultMenu.vue"
import axios from "axios";
import { useUserInfo } from "@/composables/useUserInfo";
import rulesView from "../components/Rules.vue";
import LobbyView from "@/views/LobbyView.vue";
import GameView from "@/views/Game.vue"
import Game from "@/views/Game.vue";
const api = window?.__RUNTIME_CONFIG__?.API_URL;

View File

@@ -6,6 +6,7 @@ import {useUserInfo} from "@/composables/useUserInfo.ts";
import LobbyComponent from "@/components/lobby/LobbyComponent.vue";
import {storeToRefs} from "pinia";
import {useQuasar} from "quasar";
import router from "@/router";
const wb = useWebSocket()
const ig = useIngame()
@@ -13,6 +14,7 @@ const { state } = storeToRefs(ig)
const ui = useUserInfo()
const $q = useQuasar();
ui.requestState().then(() => {
if (ui.gameId == null) {
$q.notify({
@@ -20,6 +22,7 @@ ui.requestState().then(() => {
color: "negative"
})
router.replace("/")
} else {
ig.requestGame(ui.gameId).then(() => {
wb.connect()

View File

@@ -1,53 +0,0 @@
<script setup lang="ts">
import {computed, ref} from 'vue';
import LobbyComponent from '../components/lobby/LobbyComponent.vue';
import type {LobbyInfo} from "@/types/GameTypes.ts";
import type {User} from "@/types/GameSubTypes.ts";
import {useIngame} from "@/composables/useIngame.ts";
import {sendEvent} from "@/services/ws.ts";
const ig = useIngame()
const lobbyInfo = computed<LobbyInfo | null>(() => {
if (ig.state === 'Lobby' && ig.data) {
return ig.data as LobbyInfo;
}
return null;
});
const handleKickPlayer = (user: User) => {
sendEvent("KickEvent", {
user: user
})
};
const handleStartGame = () => {
//TODO: Implement start game
};
const handleLeaveGame = (user: User) => {
sendEvent("LeftEvent",{
user: user
})
};
</script>
<template>
<q-layout>
<q-page-container>
<q-page class="vh-100 column">
<lobby-component
v-if="lobbyInfo"
:lobbyInfo="lobbyInfo"
@kick-player="handleKickPlayer"
@start-game="handleStartGame"
@leave-game="handleLeaveGame"
/>
</q-page>
</q-page-container>
</q-layout>
</template>
<style scoped>
</style>