feat(ui): FRO-34 Lobby (#21)
Started with Lobby Component Co-authored-by: LQ63 <lkhermann@web.de> Co-authored-by: Janis <janis-e@gmx.de> Reviewed-on: #21 Reviewed-by: Janis <janis-e@gmx.de> Co-authored-by: lq64 <lq@blackhole.local> Co-committed-by: lq64 <lq@blackhole.local>
This commit is contained in:
@@ -13,38 +13,45 @@ const ig = useIngame()
|
||||
</script>
|
||||
|
||||
<template>
|
||||
<q-page class="game-field-background vh-100 ingame-side-shadow">
|
||||
<div class="py-5 container-xxl">
|
||||
<q-layout>
|
||||
<q-page-container>
|
||||
<q-page class="game-field-background vh-100 ingame-side-shadow">
|
||||
<div class="py-5 container-xxl">
|
||||
|
||||
<q-row class="q-gutter-md q-mx-md">
|
||||
<q-col cols="4" class="mt-5 text-start">
|
||||
<TurnC v-if="(ig.data as GameInfo)?.playerQueue" :queue="(ig.data as GameInfo).playerQueue!"/>
|
||||
</q-col>
|
||||
<div class="fit row wrap justify-center items-center content-start">
|
||||
<div class="mt-5 ml-4 self-start col-2">
|
||||
<TurnC v-if="(ig.data as GameInfo)?.playerQueue" :queue="(ig.data as GameInfo).playerQueue!"/>
|
||||
</div>
|
||||
|
||||
<q-col cols="4" class="text-center">
|
||||
<ScoreboardC v-if="(ig.data as GameInfo)?.currentRound" :current-round="(ig.data as GameInfo).currentRound!" />
|
||||
<div class="text-center col-6">
|
||||
<ScoreboardC v-if="(ig.data as GameInfo)?.currentRound" :current-round="(ig.data as GameInfo).currentRound!" />
|
||||
</div>
|
||||
|
||||
<div class="d-flex justify-content-center g-3 mb-5">
|
||||
<PlayedCardsC v-if="(ig.data as GameInfo)?.currentTrick" :trick="(ig.data as GameInfo).currentTrick!" />
|
||||
|
||||
<div class="mt-5 ml-4 self-end col-2" style="margin-left: 6em">
|
||||
<GameInfoC v-if="(ig.data as GameInfo)?.currentRound"
|
||||
:first-card="(ig.data as GameInfo).currentTrick?.firstCard ?? null"
|
||||
:trumpsuit="(ig.data as GameInfo).currentRound!.trumpSuit"/>
|
||||
</div>
|
||||
</div>
|
||||
</q-col>
|
||||
|
||||
<div class="fit row wrap justify-center items-center content-start">
|
||||
<div class="mt-4 ml-4 col-3 justify-content-center">
|
||||
<div class="d-flex justify-content-center g-3 mb-5">
|
||||
<PlayedCardsC v-if="(ig.data as GameInfo)?.currentTrick" :trick="(ig.data as GameInfo).currentTrick!" />
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
<div class="q-gutter-sm mt-4 bottom-div justify-content-center row" style="backdrop-filter: blur(4px); margin-left: 0; margin-right: 0;">
|
||||
<div class="flex justify-center col-12">
|
||||
<HandC v-if="(ig.data as GameInfo)?.hand && (ig.data as GameInfo)?.self"/>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<q-col cols="4" class="mt-5 text-end">
|
||||
<GameInfoC v-if="(ig.data as GameInfo)?.currentRound"
|
||||
:first-card="(ig.data as GameInfo).currentTrick?.firstCard ?? null"
|
||||
:trumpsuit="(ig.data as GameInfo).currentRound!.trumpSuit"/>
|
||||
</q-col>
|
||||
</q-row>
|
||||
|
||||
<q-row justify="center" class="q-gutter-sm mt-4 bottom-div" style="backdrop-filter: blur(4px); margin-left: 0; margin-right: 0;">
|
||||
<q-col cols="12" class="flex justify-center">
|
||||
<HandC v-if="(ig.data as GameInfo)?.hand && (ig.data as GameInfo)?.self" :hand="(ig.data as GameInfo).hand!" :self="(ig.data as GameInfo).self"/>
|
||||
</q-col>
|
||||
</q-row>
|
||||
|
||||
</div>
|
||||
</q-page>
|
||||
</div>
|
||||
</q-page>
|
||||
</q-page-container>
|
||||
</q-layout>
|
||||
</template>
|
||||
|
||||
<style scoped>
|
||||
@@ -59,4 +66,15 @@ const ig = useIngame()
|
||||
.ingame-side-shadow {
|
||||
box-shadow: 0 1px 15px 0 #000000
|
||||
}
|
||||
.bottom-div {
|
||||
position: fixed;
|
||||
bottom: 0;
|
||||
left: 50%;
|
||||
transform: translateX(-50%);
|
||||
max-width: 1400px;
|
||||
width: 100%;
|
||||
margin: 0;
|
||||
text-align: center;
|
||||
padding: 10px;
|
||||
}
|
||||
</style>
|
||||
|
||||
@@ -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(() => {
|
||||
|
||||
<div class="q-mt-md">
|
||||
<div class="text-subtitle1 q-mb-xs q-font-medium">First Card</div>
|
||||
<div id="first-card-container" class="q-pa-sm bg-grey-2 rounded shadow-2"
|
||||
<div id="first-card-container" class="q-pa-sm rounded shadow-2"
|
||||
style="display:inline-block;">
|
||||
<q-img v-if="firstCard" :src="firstCard.path" alt="First Card" class="firstbox"
|
||||
style="width: 80px; border-radius: 6px;"/>
|
||||
<div v-else class="q-pa-sm"
|
||||
style="width: 80px; height: 120px; display:flex; align-items:center; justify-content:center; border-radius:6px; background: #ffffff; color: #666;">
|
||||
No image
|
||||
</div>
|
||||
<q-img v-else src="/images/cards/1B.png" alt="First Card" class="firstbox"
|
||||
style="width: 80px; border-radius: 6px;"/>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
@@ -1,42 +1,40 @@
|
||||
<script lang="ts" setup>
|
||||
import {toRefs} from 'vue'
|
||||
import type {Hand, Player} from "@/types/GameSubTypes.ts";
|
||||
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 props = defineProps<{
|
||||
hand: Hand
|
||||
isHandInactive?: boolean
|
||||
self: Player | null
|
||||
}>()
|
||||
const wiggleIdx = ref<number | null>(null)
|
||||
let wiggleTimer: ReturnType<typeof setTimeout> | null = null
|
||||
|
||||
const { hand, isHandInactive, self } = toRefs(props)
|
||||
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(element: any, index: number | null) {
|
||||
function handlePlayCard(index: number | null) {
|
||||
if (index === null) return
|
||||
if (isHandInactive?.value) return
|
||||
isHandInactive.value = true
|
||||
|
||||
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(() => {
|
||||
element.animate(wiggleKeyframes, wiggleTiming);
|
||||
isHandInactive.value = false;
|
||||
.catch((error) => {
|
||||
triggerWiggle(index)
|
||||
|
||||
$q.notify({
|
||||
message: error.message,
|
||||
color: "negative",
|
||||
position: "top"
|
||||
})
|
||||
|
||||
})
|
||||
}
|
||||
|
||||
@@ -52,16 +50,16 @@ function getCardImagePath(cardPath: string) {
|
||||
</script>
|
||||
|
||||
<template>
|
||||
<div class="bottom-div hand-container">
|
||||
<div id="card-slide" :class="'ingame-cards-slide ' + (isHandInactive ? 'inactive' : '' )">
|
||||
<div class="hand-container">
|
||||
<div id="card-slide" class="ingame-cards-slide">
|
||||
<div class="cards-row">
|
||||
<div v-for="card in hand.cards" :key="card.identifier" class="handcard">
|
||||
<div class="card-btn" @click="handlePlayCard(this, card.idx)" aria-label="Play card">
|
||||
<q-img :src="getCardImagePath(card.path)" :alt="card.identifier" class="card" />
|
||||
<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>
|
||||
</div>
|
||||
</div>
|
||||
<div v-if="self?.dogLife" class="dog-actions">
|
||||
<div v-if="(<GameInfo>wi.data)?.self?.dogLife" class="dog-actions">
|
||||
<q-btn color="negative" label="Skip Turn" @click="handleSkipDogLife" />
|
||||
</div>
|
||||
</div>
|
||||
@@ -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 {
|
||||
|
||||
@@ -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) {
|
||||
|
||||
@@ -1,15 +1,18 @@
|
||||
<script setup lang="ts">
|
||||
import type {Round} from "@/types/GameSubTypes.ts";
|
||||
import {computed, type ComputedRef} from "vue";
|
||||
|
||||
interface PlayerScore { name: string; tricks: number }
|
||||
const props = defineProps<{ currentRound: Round }>()
|
||||
const playerScores: PlayerScore[] = props.currentRound.playersIn.map(player => {
|
||||
return {
|
||||
name: player.name,
|
||||
tricks: props.currentRound.trickList.filter(trick => {
|
||||
return trick.winner?.id === player.id
|
||||
}).length
|
||||
}
|
||||
const playerScores: ComputedRef<PlayerScore[]> = computed(() => {
|
||||
return props.currentRound.playersIn.map(player => {
|
||||
return {
|
||||
name: player.name,
|
||||
tricks: props.currentRound.trickList.filter(trick => {
|
||||
return trick.winner?.id === player.id
|
||||
}).length
|
||||
}
|
||||
})
|
||||
})
|
||||
</script>
|
||||
|
||||
|
||||
@@ -1,26 +1,26 @@
|
||||
<script setup lang="ts">
|
||||
import { computed } from 'vue'
|
||||
import {computed, toRefs} from 'vue'
|
||||
import type {PlayerQueue} from "@/types/GameSubTypes.ts";
|
||||
import {useIngame} from "@/composables/useIngame.ts";
|
||||
import type {GameInfo} from "@/types/GameTypes.ts";
|
||||
|
||||
const props = defineProps<{
|
||||
queue: PlayerQueue
|
||||
}>()
|
||||
|
||||
const safeNextPlayers = computed(() => props.queue.players ?? [])
|
||||
const ig = useIngame()
|
||||
const currentPlayer = computed(() => (<GameInfo>ig.data).playerQueue.currentPlayer)
|
||||
const queue = computed(() => { return (<GameInfo>ig.data).playerQueue.queue ?? []})
|
||||
</script>
|
||||
|
||||
<template>
|
||||
<q-card flat class="turn-tracker-container q-pa-md">
|
||||
<q-card flat class="turn-tracker-container q-pa-md no-background">
|
||||
<q-card-section>
|
||||
<div class="text-subtitle2 q-mb-xs">Current Player</div>
|
||||
<div id="current-player-name" class="text-h6 text-weight-bold text-positive">{{
|
||||
props.queue.currentPlayer?.name
|
||||
currentPlayer?.name
|
||||
}}</div>
|
||||
|
||||
<div v-if="safeNextPlayers.length > 0" class="q-mt-md">
|
||||
<div v-if="queue.length > 0" class="q-mt-md">
|
||||
<div id="next-players-text" class="text-subtitle2 q-mb-xs">Next Players</div>
|
||||
<q-list id="next-players-container" dense>
|
||||
<q-item v-for="player in safeNextPlayers" :key="player.id">
|
||||
<q-item v-for="player in queue" :key="player.id">
|
||||
<q-item-section>
|
||||
<div class="text-body1 text-primary">{{ player.name }}</div>
|
||||
</q-item-section>
|
||||
@@ -35,4 +35,7 @@ const safeNextPlayers = computed(() => props.queue.players ?? [])
|
||||
.turn-tracker-container {
|
||||
max-width: 320px;
|
||||
}
|
||||
.no-background {
|
||||
background: none !important;
|
||||
}
|
||||
</style>
|
||||
|
||||
150
src/components/lobby/LobbyComponent.vue
Normal file
150
src/components/lobby/LobbyComponent.vue
Normal file
@@ -0,0 +1,150 @@
|
||||
<script setup lang="ts">
|
||||
import type {User} from "@/types/GameSubTypes.ts";
|
||||
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 = computed(() => {
|
||||
return `${ig.data?.gameId}`
|
||||
});
|
||||
|
||||
const handleKickPlayer = (user: User) => {
|
||||
if (isHost) {
|
||||
wb.send("KickPlayer", {playerId: user.id})
|
||||
}
|
||||
};
|
||||
|
||||
const handleStartGame = () => {
|
||||
if (isHost) {
|
||||
wb.sendAndWait("StartGame", {})
|
||||
}
|
||||
};
|
||||
|
||||
const handleLeaveGame = (user: 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>
|
||||
|
||||
<template>
|
||||
|
||||
<div class="row q-pa-md items-center">
|
||||
<div class="col text-center text-h4 text-weight-medium">
|
||||
Lobby Name: {{ lobbyName }} </div>
|
||||
<q-btn
|
||||
color="negative"
|
||||
label="Exit"
|
||||
@click="handleLeaveGame((<LobbyInfo>ig.data).self)"
|
||||
class="q-ml-auto"
|
||||
/>
|
||||
</div>
|
||||
|
||||
<div class="row q-pb-md">
|
||||
<div class="col text-center text-subtitle1">
|
||||
Players: {{ players.length }} / {{ maxPlayers }}
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<div class="row justify-center items-center flex-grow-1 q-px-md">
|
||||
<div class="col-12">
|
||||
<div class="row justify-center q-gutter-md">
|
||||
<div v-for="player in players" :key="player.id" class="col-auto">
|
||||
<q-card class="bg-dark q-pa-md text-center" style="width: 250px;">
|
||||
|
||||
<q-avatar size="80px" color="primary" text-color="white" :icon="profileIcon" class="q-mb-sm" />
|
||||
|
||||
<q-card-section>
|
||||
<div class="text-h6">
|
||||
{{ player.username }} <q-badge v-if="player.id === (<LobbyInfo>ig.data).self.id" color="orange" align="middle" class="q-ml-xs">
|
||||
(You)
|
||||
</q-badge>
|
||||
<q-badge v-else-if="player.host" color="blue" align="middle" class="q-ml-xs">
|
||||
(Host)
|
||||
</q-badge>
|
||||
</div>
|
||||
</q-card-section>
|
||||
|
||||
<q-card-actions align="center" v-if="isHost">
|
||||
<q-btn
|
||||
v-if="player.id !== (<LobbyInfo>ig.data).self.id"
|
||||
color="negative"
|
||||
label="Remove"
|
||||
@click="handleKickPlayer(player)"
|
||||
class="full-width"
|
||||
/>
|
||||
<q-btn
|
||||
v-else
|
||||
color="negative"
|
||||
label="Remove (Cannot Kick Self)"
|
||||
disable
|
||||
class="full-width"
|
||||
/>
|
||||
</q-card-actions>
|
||||
|
||||
</q-card>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<div class="row q-py-lg text-center">
|
||||
<div class="col-12">
|
||||
<template v-if="isHost">
|
||||
<q-btn
|
||||
color="positive"
|
||||
label="Start Game"
|
||||
size="lg"
|
||||
@click="handleStartGame"
|
||||
/>
|
||||
</template>
|
||||
<template v-else>
|
||||
<div class="text-h6 q-mb-sm">
|
||||
Waiting for the host to start the game...
|
||||
</div>
|
||||
<q-spinner
|
||||
color="primary"
|
||||
size="3em"
|
||||
:thickness="2"
|
||||
/>
|
||||
</template>
|
||||
</div>
|
||||
</div>
|
||||
</template>
|
||||
|
||||
<style scoped>
|
||||
</style>
|
||||
Reference in New Issue
Block a user