diff --git a/env.d.ts b/env.d.ts index 70b745d..6b5d34f 100644 --- a/env.d.ts +++ b/env.d.ts @@ -1,7 +1,7 @@ /// declare global { - interface Window { __RUNTIME_CONFIG__?: { API_URL?: string } } + interface Window { __RUNTIME_CONFIG__?: { API_URL?: string; WEBSOCKET_URL?: string } } } export {}; diff --git a/package-lock.json b/package-lock.json index e69b7d2..270da99 100644 --- a/package-lock.json +++ b/package-lock.json @@ -74,7 +74,6 @@ "integrity": "sha512-e7jT4DxYvIDLk1ZHmU/m/mB19rex9sv0c2ftBtjSBv+kVM/902eh0fINUzD7UwLLNR+jU585GxUJ8/EBfAM5fw==", "dev": true, "license": "MIT", - "peer": true, "dependencies": { "@babel/code-frame": "^7.27.1", "@babel/generator": "^7.28.5", @@ -2575,7 +2574,6 @@ "integrity": "sha512-GNWcUTRBgIRJD5zj+Tq0fKOJ5XZajIiBroOF0yvj2bSU1WvNdYS/dn9UxwsujGW4JX06dnHyjV2y9rRaybH0iQ==", "dev": true, "license": "MIT", - "peer": true, "dependencies": { "undici-types": "~7.16.0" } @@ -2626,7 +2624,6 @@ "integrity": "sha512-PC0PDZfJg8sP7cmKe6L3QIL8GZwU5aRvUFedqSIpw3B+QjRSUZeeITC2M5XKeMXEzL6wccN196iy3JLwKNvDVA==", "dev": true, "license": "MIT", - "peer": true, "dependencies": { "@typescript-eslint/scope-manager": "8.48.1", "@typescript-eslint/types": "8.48.1", @@ -2844,7 +2841,6 @@ "integrity": "sha512-iHmwV3QcVGGvSC1BG5bZ4z6iwa1SOpAPWmnjOErd4Ske+lZua5K9TtAVdx0gMBClJ28DViCbSmZitjWZsWO3LA==", "dev": true, "license": "MIT", - "peer": true, "dependencies": { "@rolldown/pluginutils": "1.0.0-beta.50" }, @@ -3231,7 +3227,6 @@ "integrity": "sha512-NZyJarBfL7nWwIq+FDL6Zp/yHEhePMNnnJ0y3qfieCrmNvYct8uvtiV41UvlSe6apAfk0fY1FbWx+NwfmpvtTg==", "dev": true, "license": "MIT", - "peer": true, "bin": { "acorn": "bin/acorn" }, @@ -3323,7 +3318,6 @@ "resolved": "https://registry.npmjs.org/axios/-/axios-1.13.2.tgz", "integrity": "sha512-VPk9ebNqPcy5lRGuSlKx752IlDatOjT9paPlm8A7yOuW2Fbvp4X3JznJtT4f0GzGLLiWE9W8onz51SqLYwzGaA==", "license": "MIT", - "peer": true, "dependencies": { "follow-redirects": "^1.15.6", "form-data": "^4.0.4", @@ -3406,7 +3400,6 @@ } ], "license": "MIT", - "peer": true, "dependencies": { "baseline-browser-mapping": "^2.9.0", "caniuse-lite": "^1.0.30001759", @@ -3874,7 +3867,6 @@ "integrity": "sha512-BhHmn2yNOFA9H9JmmIVKJmd288g9hrVRDkdoIgRCRuSySRUHH7r/DI6aAXW9T1WwUuY3DFgrcaqB+deURBLR5g==", "dev": true, "license": "MIT", - "peer": true, "dependencies": { "@eslint-community/eslint-utils": "^4.8.0", "@eslint-community/regexpp": "^4.12.1", @@ -3935,7 +3927,6 @@ "integrity": "sha512-82GZUjRS0p/jganf6q1rEO25VSoHH0hKPCTrgillPjdI/3bgBhAE1QzHrHTizjpRvy6pGAvKjDJtk2pF9NDq8w==", "dev": true, "license": "MIT", - "peer": true, "bin": { "eslint-config-prettier": "bin/cli.js" }, @@ -3983,7 +3974,6 @@ "integrity": "sha512-SbR9ZBUFKgvWAbq3RrdCtWaW0IKm6wwUiApxf3BVTNfqUIo4IQQmreMg2iHFJJ6C/0wss3LXURBJ1OwS/MhFcQ==", "dev": true, "license": "MIT", - "peer": true, "dependencies": { "@eslint-community/eslint-utils": "^4.4.0", "natural-compare": "^1.4.0", @@ -4665,7 +4655,6 @@ "integrity": "sha512-ekilCSN1jwRvIbgeg/57YFh8qQDNbwDb9xT/qu2DAHbFFZUicIl4ygVaAvzveMhMVr3LnpSKTNnwt8PoOfmKhQ==", "dev": true, "license": "MIT", - "peer": true, "bin": { "jiti": "lib/jiti-cli.mjs" } @@ -5351,7 +5340,6 @@ "integrity": "sha512-I7AIg5boAr5R0FFtJ6rCfD+LFsWHp81dolrFD8S79U9tb8Az2nGrJncnMSnys+bpQJfRUzqs9hnA81OAA3hCuQ==", "dev": true, "license": "MIT", - "peer": true, "bin": { "prettier": "bin/prettier.cjs" }, @@ -5396,7 +5384,6 @@ "resolved": "https://registry.npmjs.org/quasar/-/quasar-2.18.6.tgz", "integrity": "sha512-ZlK+vJXOBPSFDCNQDBDNwSI+AHoqaFPxK8ve6mhsYLhMKWI5b8zsGY9VU1xYjngO2aBvU4fvGWXy4tTbzrBk8Q==", "license": "MIT", - "peer": true, "engines": { "node": ">= 10.18.1", "npm": ">= 6.13.4", @@ -6157,7 +6144,6 @@ "integrity": "sha512-5gTmgEY/sqK6gFXLIsQNH19lWb4ebPDLA4SdLP7dsWkIXHWlG66oPuVvXSGFPppYZz8ZDZq0dYYrbHfBCVUb1Q==", "dev": true, "license": "MIT", - "peer": true, "engines": { "node": ">=12" }, @@ -6262,7 +6248,6 @@ "integrity": "sha512-jl1vZzPDinLr9eUt3J/t7V6FgNEw9QjvBPdysz9KfQDD41fQrC2Y4vKQdiaUpFT4bXlb1RHhLpp8wtm6M5TgSw==", "devOptional": true, "license": "Apache-2.0", - "peer": true, "bin": { "tsc": "bin/tsc", "tsserver": "bin/tsserver" @@ -6393,7 +6378,6 @@ "integrity": "sha512-tI2l/nFHC5rLh7+5+o7QjKjSR04ivXDF4jcgV0f/bTQ+OJiITy5S6gaynVsEM+7RqzufMnVbIon6Sr5x1SDYaQ==", "dev": true, "license": "MIT", - "peer": true, "dependencies": { "esbuild": "^0.25.0", "fdir": "^6.5.0", @@ -6644,7 +6628,6 @@ "integrity": "sha512-5gTmgEY/sqK6gFXLIsQNH19lWb4ebPDLA4SdLP7dsWkIXHWlG66oPuVvXSGFPppYZz8ZDZq0dYYrbHfBCVUb1Q==", "dev": true, "license": "MIT", - "peer": true, "engines": { "node": ">=12" }, @@ -6664,7 +6647,6 @@ "resolved": "https://registry.npmjs.org/vue/-/vue-3.5.25.tgz", "integrity": "sha512-YLVdgv2K13WJ6n+kD5owehKtEXwdwXuj2TTyJMsO7pSeKw2bfRNZGjhB7YzrpbMYj5b5QsUebHpOqR3R3ziy/g==", "license": "MIT", - "peer": true, "dependencies": { "@vue/compiler-dom": "3.5.25", "@vue/compiler-sfc": "3.5.25", diff --git a/public/env.js b/public/env.js index fc45354..430b3cc 100644 --- a/public/env.js +++ b/public/env.js @@ -1,4 +1,5 @@ window.__RUNTIME_CONFIG__ = { - API_URL: "http://localhost:9000" + API_URL: "http://localhost:9000", + WEBSOCKET_API_URL: "ws://localhost:9000/websocket" }; diff --git a/public/env.template.js b/public/env.template.js index cba0fc7..b9d921c 100644 --- a/public/env.template.js +++ b/public/env.template.js @@ -1,4 +1,5 @@ window.__RUNTIME_CONFIG__ = { - API_URL: "${API_URL}" + API_URL: "${API_URL}", + WEBSOCKET_API_URL: "${WEBSOCKET_API_URL}" }; diff --git a/src/components/Ingame.vue b/src/components/Ingame.vue index 5b139b2..ad6eedf 100644 --- a/src/components/Ingame.vue +++ b/src/components/Ingame.vue @@ -13,38 +13,45 @@ const ig = useIngame() - - + + + + - - - - + + + + - - + + + - - + + + + - + + + + + + + + + + + + - - - - - - - - - - - - - + + + + diff --git a/src/components/ingame/GameInfoC.vue b/src/components/ingame/GameInfoC.vue index 4d89704..4f1d96e 100644 --- a/src/components/ingame/GameInfoC.vue +++ b/src/components/ingame/GameInfoC.vue @@ -11,7 +11,7 @@ const {trumpsuit, firstCard} = toRefs(props) const trumpName = computed(() => { - switch (trumpsuit.value.path.charAt(trumpsuit.value.path.length - 1)) { + switch (trumpsuit.value.identifier.charAt(1) as string) { case 'S': return 'Spades' case 'H': @@ -36,14 +36,12 @@ const trumpName = computed(() => { First Card - - - No image - + diff --git a/src/components/ingame/HandC.vue b/src/components/ingame/HandC.vue index 1d13ce0..9a79b2d 100644 --- a/src/components/ingame/HandC.vue +++ b/src/components/ingame/HandC.vue @@ -1,42 +1,40 @@ - - + + - - - + + + - + @@ -119,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 { diff --git a/src/components/ingame/PlayedCardsC.vue b/src/components/ingame/PlayedCardsC.vue index 027ca69..eb418ad 100644 --- a/src/components/ingame/PlayedCardsC.vue +++ b/src/components/ingame/PlayedCardsC.vue @@ -6,12 +6,15 @@ const props = defineProps<{ trick: Trick }>() const {trick } = toRefs(props) const playedCards = computed(() => { - return [...trick.value.cards].map(card => { - return { - cardId: card[1].path, - player: card[0].name - } - }) + if (!trick.value) return [] + let result: { cardId: string, player: string }[] = [] + for (const key in trick.value.cards) { + result.push({ + cardId: trick.value.cards[key]?.path ?? '', + player: key + }) + } + return result; }) function getCardImagePath(cardPath: string) { diff --git a/src/components/ingame/ScoreboardC.vue b/src/components/ingame/ScoreboardC.vue index 1bff232..b7d15cd 100644 --- a/src/components/ingame/ScoreboardC.vue +++ b/src/components/ingame/ScoreboardC.vue @@ -1,15 +1,18 @@ diff --git a/src/components/ingame/TurnC.vue b/src/components/ingame/TurnC.vue index 12f4478..d4259fa 100644 --- a/src/components/ingame/TurnC.vue +++ b/src/components/ingame/TurnC.vue @@ -1,26 +1,26 @@ - + Current Player {{ - props.queue.currentPlayer?.name + currentPlayer?.name }} - + Next Players - + {{ player.name }} @@ -35,4 +35,7 @@ const safeNextPlayers = computed(() => props.queue.players ?? []) .turn-tracker-container { max-width: 320px; } +.no-background { + background: none !important; +} diff --git a/src/components/lobby/LobbyComponent.vue b/src/components/lobby/LobbyComponent.vue new file mode 100644 index 0000000..da96572 --- /dev/null +++ b/src/components/lobby/LobbyComponent.vue @@ -0,0 +1,150 @@ + + + + + + + Lobby Name: {{ lobbyName }} + ig.data).self)" + class="q-ml-auto" + /> + + + + + Players: {{ players.length }} / {{ maxPlayers }} + + + + + + + + + + + + + + {{ player.username }} + (You) + + + (Host) + + + + + + + + + + + + + + + + + + + + + + + Waiting for the host to start the game... + + + + + + + + diff --git a/src/composables/useIngame.ts b/src/composables/useIngame.ts index 459d505..d843415 100644 --- a/src/composables/useIngame.ts +++ b/src/composables/useIngame.ts @@ -2,6 +2,7 @@ import { defineStore } from 'pinia' import {ref, type Ref} from 'vue' import type {GameInfo, LobbyInfo, TieInfo, TrumpInfo, WonInfo} from "@/types/GameTypes.ts"; import axios from "axios"; +import {initWebSocket} from "@/services/ws.ts"; const api = window?.__RUNTIME_CONFIG__?.API_URL; @@ -14,8 +15,8 @@ export const useIngame = defineStore('ingame', () => { data.value = newData; } - function requestGame(gameId: string) { - axios.get(`${api}/status/${gameId}`, {withCredentials: true}).then((response) => { + async function requestGame(gameId: string) { + await axios.get(`${api}/status/${gameId}`, {withCredentials: true}).then((response) => { setIngame(response.data.state, response.data.data); }); } diff --git a/src/composables/useUserInfo.ts b/src/composables/useUserInfo.ts index 781ba8a..77418df 100644 --- a/src/composables/useUserInfo.ts +++ b/src/composables/useUserInfo.ts @@ -18,13 +18,15 @@ export const useUserInfo = defineStore('userInfo', () => { gameId.value = id; } - function requestState() { - axios.get(`${api}/status`, {withCredentials: true}).then((response) => { + async function requestState() { + await axios.get(`${api}/status`, {withCredentials: true}).then((response) => { + console.log("STATUS DATA:" + response.data.status + response.data.inGame) username.value = response.data.username; - if (response.data.ingame) { + if (response.data.gameId) { + console.log("GAMEID:" + response.data.gameId) gameId.value = response.data.gameId; } - }); + }) } function clearUserInfo() { diff --git a/src/composables/useWebsocket.ts b/src/composables/useWebsocket.ts index 12d721a..e66860c 100644 --- a/src/composables/useWebsocket.ts +++ b/src/composables/useWebsocket.ts @@ -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, () => {}); }); } diff --git a/src/main.ts b/src/main.ts index 03eb752..33dc571 100644 --- a/src/main.ts +++ b/src/main.ts @@ -13,10 +13,15 @@ import axios from 'axios' import VueAxios from 'vue-axios' import {useUserInfo} from "@/composables/useUserInfo.ts"; import 'animate.css/animate.min.css'; +import {useIngame} from "@/composables/useIngame.ts"; +import {initWebSocket} from "@/services/ws.ts"; + const app = createApp(App) const pinia = createPinia() app.use(pinia) +const ui = useIngame(); +initWebSocket(ui); app.use(router) app.use(Quasar, { plugins: { diff --git a/src/router/index.ts b/src/router/index.ts index 908614e..35cf7a6 100644 --- a/src/router/index.ts +++ b/src/router/index.ts @@ -7,14 +7,14 @@ import defaultMenu from "../components/DefaultMenu.vue" import axios from "axios"; import { useUserInfo } from "@/composables/useUserInfo"; import rulesView from "../components/Rules.vue"; - +import Game from "@/views/Game.vue"; const api = window?.__RUNTIME_CONFIG__?.API_URL; const router = createRouter({ history: createWebHistory(import.meta.env.BASE_URL), routes: [ { - path: '/mainmenu/', + path: '/', component: MainMenuView, meta: { requiresAuth: true }, children: [ @@ -50,6 +50,12 @@ const router = createRouter({ component: LoginView, meta: { requiresAuth: false } }, + { + path: '/game', + name: 'game', + component: Game, + meta: {requiresAuth: true } + } ], }) diff --git a/src/runtimeConfig.ts b/src/runtimeConfig.ts index ce5dca0..5a0e74b 100644 --- a/src/runtimeConfig.ts +++ b/src/runtimeConfig.ts @@ -2,13 +2,18 @@ // Reads runtime configuration injected into window.__RUNTIME_CONFIG__ (created by env.js at container start) declare global { - interface Window { __RUNTIME_CONFIG__?: { API_URL?: string } } + interface Window { __RUNTIME_CONFIG__?: { API_URL?: string; WEBSOCKET_URL?: string } } } const runtime = (globalThis as any).__RUNTIME_CONFIG__ || {}; export const API_URL = runtime.API_URL || (import.meta.env.VITE_API_URL as string) || 'http://localhost:9000'; +export const WEBSOCKET_URL = runtime.API_URL || (import.meta.env.VITE_WEBSOCKET_URL as string) || 'http://localhost:9000'; export function getApiUrl(): string { return API_URL; } + +export function getWebsocketUrl(): string { + return WEBSOCKET_URL; +} diff --git a/src/services/ws.ts b/src/services/ws.ts index 7b59a09..5d2d0b6 100644 --- a/src/services/ws.ts +++ b/src/services/ws.ts @@ -1,7 +1,8 @@ import {useIngame} from "@/composables/useIngame.ts"; import type {GameInfo, LobbyInfo, TieInfo, TrumpInfo, WonInfo} from "@/types/GameTypes.ts"; +import router from "@/router"; -const api = window.__RUNTIME_CONFIG__?.API_URL; +const api = "localhost:9000" // ---------- Types --------------------------------------------------------- @@ -33,10 +34,13 @@ let ws: WebSocket | null = null; const pending = new Map(); const handlers = new Map(); -const uState = useIngame(); +let uState: ReturnType | null = null; let heartbeatTimer: ReturnType | null = null; +export function initWebSocket(ingameStore: ReturnType) { + uState = ingameStore; +} let defaultHandler: HandlerFn | null = null; function uuid(): string { @@ -70,6 +74,10 @@ function stopHeartbeat() { } function setupSocketHandlers(socket: WebSocket) { + if (!uState) { + console.error("[WS] WebSocket module not initialized with Pinia store!"); + return; + } socket.onmessage = async (raw) => { console.debug("[WS] MESSAGE:", raw.data); @@ -100,7 +108,11 @@ function setupSocketHandlers(socket: WebSocket) { } if (state && stateData) { - uState.setIngame(state, stateData); + console.debug("[WS] State change:", state, stateData); + if(uState) { + console.debug("[WS] State change cascade:", state, stateData); + uState.setIngame(state, stateData); + } } // Server event → handler branch @@ -155,7 +167,7 @@ function setupSocketHandlers(socket: WebSocket) { } // You redirect here — if you don’t want auto reconnect, keep as is. - location.href = "/mainmenu"; + router.replace("/"); }; } diff --git a/src/types/GameSubTypes.ts b/src/types/GameSubTypes.ts index 378da40..27a663c 100644 --- a/src/types/GameSubTypes.ts +++ b/src/types/GameSubTypes.ts @@ -16,7 +16,7 @@ type Player = { type PlayerQueue = { currentPlayer: Player | null - players: Player[] + queue: Player[] } type PodiumPlayer = { @@ -34,7 +34,7 @@ type Round = { } type Trick = { - cards: Map + cards: { [key: string]: Card} | null firstCard: Card | null winner: Player | null } diff --git a/src/views/CreateGame.vue b/src/views/CreateGame.vue index 01a2e73..c9a3b0d 100644 --- a/src/views/CreateGame.vue +++ b/src/views/CreateGame.vue @@ -11,7 +11,6 @@ const playerAmount = ref(2); const $q = useQuasar(); const isLoading = ref(false); const router = useRouter(); -const delay = (ms: number) => new Promise(resolve => setTimeout(resolve, ms)); const createGameQuasar = async () => { if (!lobbyName.value) { $q.notify({ message: 'Lobby-Name wird benötigt', color: 'red', position: 'top', icon: 'cancel' }); @@ -27,7 +26,7 @@ const createGameQuasar = async () => { icon: 'check_circle', position: 'top' }); - router.push("/lobby") + router.replace("/game") }).catch((err) => { console.log("ERROR:" + err) }).finally(() => diff --git a/src/views/Game.vue b/src/views/Game.vue index ccc2934..3798bcc 100644 --- a/src/views/Game.vue +++ b/src/views/Game.vue @@ -1,26 +1,40 @@ - - + + @@ -28,6 +42,6 @@ if (ui.gameId) { .lobby-background { background-color: var(--body-background-color); width: 100%; - min-height:100vh; + min-height: 100vh; } diff --git a/src/views/JoinGameView.vue b/src/views/JoinGameView.vue index aab8b5a..2a57f38 100644 --- a/src/views/JoinGameView.vue +++ b/src/views/JoinGameView.vue @@ -22,7 +22,7 @@ const startGameQuasar = async() => { icon: 'check_circle', position: 'top' }); - router.push("/lobby") + router.replace("/game") }).catch(() => { $q.notify({ message: `Lobby "${lobbyCode.value}" nicht gefunden`, diff --git a/src/views/LobbyView.vue b/src/views/LobbyView.vue new file mode 100644 index 0000000..1637d86 --- /dev/null +++ b/src/views/LobbyView.vue @@ -0,0 +1,53 @@ + + + + + + + + + + + + + diff --git a/src/views/LoginView.vue b/src/views/LoginView.vue index 9a460c9..50997d6 100644 --- a/src/views/LoginView.vue +++ b/src/views/LoginView.vue @@ -80,7 +80,7 @@ const onSubmit = () => { loginError.value = '' axios.post(`${api}/login`, {username: username.value, password: password.value}, {withCredentials: true}).then((response) => { uInfo.setUserInfo(response.data.user.username, response.data.user.id) - router.push("/mainmenu") + router.push("/") }).catch(() => { loginError.value = 'Invalid username or password' }).finally(() =>