feat(ui): FRO-26 Tie selection

Added a nice tie selection ui
This commit is contained in:
LQ63
2026-01-22 01:06:23 +01:00
parent 1ed4365c78
commit 5e35990e87
2 changed files with 123 additions and 37 deletions

View File

@@ -4,7 +4,7 @@ import {useIngame} from "@/composables/useIngame.ts";
import type {GameInfo, LobbyInfo, TieInfo, TrumpInfo} from "@/types/GameTypes.ts"; import type {GameInfo, LobbyInfo, TieInfo, TrumpInfo} from "@/types/GameTypes.ts";
import {useQuasar} from "quasar"; import {useQuasar} from "quasar";
import { ref } from 'vue'; import { ref } from 'vue';
import { computed } from 'vue' import { computed, nextTick } from 'vue'
const wb = useWebSocket() const wb = useWebSocket()
const wi = useIngame() const wi = useIngame()
@@ -21,38 +21,36 @@ function getPlayerName(playerId: string) {
return tieInf.value.tiedPlayers.find(p => p.id === playerId)?.name || 'Player'; return tieInf.value.tiedPlayers.find(p => p.id === playerId)?.name || 'Player';
} }
const myRevealedCard = computed(() => { const myRevealedCard = computed(() => {
// 1. Get your own ID from the 'self' object in the DTO
const myId = tieInf.value.self?.id; const myId = tieInf.value.self?.id;
// 2. Safety check: ensure we have an ID and the map exists
if (!myId || !tieInf.value.selectedCards) return null; if (!myId || !tieInf.value.selectedCards) return null;
// 3. Look up the CardDTO using your ID as the key
const card = tieInf.value.selectedCards[myId]; const card = tieInf.value.selectedCards[myId];
// 4. If found, convert the relative path to a full URL
return card ? getCardImagePath(card.path) : null; return card ? getCardImagePath(card.path) : null;
}); });
const isFlipping = ref(false); const isFlipping = ref(false);
// This replaces your v-if check.
// It keeps the "Pick" screen visible while flipping.
const showPickScreen = computed(() => { const showPickScreen = computed(() => {
const isMyTurn = tieInf.value.self?.id === tieInf.value.currentPlayer?.id; const isMyTurn = tieInf.value.self?.id === tieInf.value.currentPlayer?.id;
return isMyTurn || isFlipping.value; return isFlipping.value || (isMyTurn && !showResultScreen.value);
}); });
const showResultScreen = computed(() => {
const allPicked = Object.keys(tieInf.value.selectedCards).length === tieInf.value.tiedPlayers.length;
return allPicked && !isFlipping.value;
});
function selectTie(tieIndex: number) { function selectTie(tieIndex: number) {
// Use model.value because it's a ref isFlipping.value = true;
nextTick(() => {
wb.sendAndWait("PickTie", { cardIndex: tieIndex }) wb.sendAndWait("PickTie", { cardIndex: tieIndex })
.then(() => { .then(() => {
console.log("Server accepted pick, starting animation..."); console.log("Server accepted pick, starting animation...");
console.log("CARD SELECTED: " + Object.keys(tieInf.value.selectedCards).length)
// 1. Trigger the animation state
isFlipping.value = true;
// 2. Optional Notification
$q.notify({ $q.notify({
message: "Card revealed!", message: "Card revealed!",
color: "positive", color: "positive",
@@ -60,13 +58,13 @@ function selectTie(tieIndex: number) {
timeout: 1000 timeout: 1000
}); });
// 3. Wait for animation to finish before switching to Waiting Screen
setTimeout(() => { setTimeout(() => {
isFlipping.value = false; isFlipping.value = false;
model.value = null; model.value = null;
}, 2500); }, 2500);
}) })
.catch((error) => { .catch((error) => {
isFlipping.value = false;
console.error("Pick failed:", error); console.error("Pick failed:", error);
$q.notify({ $q.notify({
message: error.message || "Failed to pick card", message: error.message || "Failed to pick card",
@@ -74,6 +72,8 @@ function selectTie(tieIndex: number) {
position: "top" position: "top"
}); });
}); });
});
} }
const model = ref<number | null>(null) const model = ref<number | null>(null)
const options = computed(() => { const options = computed(() => {
@@ -115,7 +115,6 @@ const options = computed(() => {
> >
<div class="card-inner"> <div class="card-inner">
<q-img <q-img
:key="myRevealedCard || 'hidden'"
:src="(model === n && myRevealedCard) ? myRevealedCard : getCardImagePath(tieBlankCard)" :src="(model === n && myRevealedCard) ? myRevealedCard : getCardImagePath(tieBlankCard)"
class="card-image shadow-24" class="card-image shadow-24"
:class="{ 'animate-reveal': isFlipping && model === n }" :class="{ 'animate-reveal': isFlipping && model === n }"
@@ -147,7 +146,7 @@ const options = computed(() => {
</q-card-section> </q-card-section>
</q-card> </q-card>
<q-card v-else class="game-container text-center overflow-hidden"> <q-card v-else-if="!showResultScreen" class="game-container text-center overflow-hidden">
<q-card-section class="content-layer q-py-xl"> <q-card-section class="content-layer q-py-xl">
<div class="text-overline text-primary letter-spacing-2">Tie-Break Round</div> <div class="text-overline text-primary letter-spacing-2">Tie-Break Round</div>
@@ -174,7 +173,7 @@ const options = computed(() => {
</div> </div>
<div class="q-mt-xl"> <div class="q-mt-xl">
<q-spinner-hourglass color="primary" size="4em" /> <q-spinner-hourglass color="white" size="4em" />
<div class="text-subtitle1 text-grey-5 q-mt-sm italic"> <div class="text-subtitle1 text-grey-5 q-mt-sm italic">
Waiting for <span class="text-white">{{ tieInf.currentPlayer?.name }}</span> to pick a card... Waiting for <span class="text-white">{{ tieInf.currentPlayer?.name }}</span> to pick a card...
</div> </div>
@@ -182,6 +181,61 @@ const options = computed(() => {
</div> </div>
</q-card-section> </q-card-section>
</q-card>
<q-card v-else class="game-container tie-break-card text-center overflow-hidden">
<div class="absolute-full bg-gradient-dark opacity-80"></div>
<q-card-section class="content-layer q-py-xl">
<div class="column items-center q-mb-xl">
<q-icon
:name="(tieInf.winners?.length || 0) > 1 ? 'auto_renew' : 'workspace_premium'"
:color="(tieInf.winners?.length || 0) > 1 ? 'info' : 'warning'"
size="64px"
class="q-mb-sm"
/>
<div class="text-h3 text-weight-bolder text-white text-uppercase tracking-widest">
{{ (tieInf.winners?.length || 0) > 1 ? 'Another Tie!' : 'Tie Broken' }}
</div>
<p class="text-subtitle1 text-grey-4">
{{ (tieInf.winners?.length || 0) > 1 ? 'The high cards matched. Draw again!' : 'We have a winner.' }}
</p>
</div>
<div class="row justify-center items-end q-gutter-lg">
<div
v-for="(card, playerId) in tieInf.selectedCards"
:key="playerId"
class="revealed-card-wrapper"
:class="{
'winner-glow': tieInf.winners?.map(player => player.id).includes(playerId),
'opacity-40': !tieInf.winners?.map(player => player.id).includes(playerId) && (tieInf.winners?.length || 0) > 0
}"
>
<div class="player-tag q-mb-md">
<div class="text-overline text-white">{{ getPlayerName(playerId) }}</div>
<q-badge
v-if="tieInf.winners?.map(player => player.id).includes(playerId)"
:color="tieInf.winners.length > 1 ? 'info' : 'positive'"
:label="tieInf.winners.length > 1 ? 'Still Tied' : 'Winner'"
rounded
/>
</div>
<div class="card-stack">
<q-img
:src="getCardImagePath(card.path)"
class="card-image-reveal shadow-24"
/>
</div>
</div>
</div>
<div class="column items-center q-mt-lg">
<q-spinner-hourglass color="white" size="4em" />
<div class="text-subtitle1 text-white q-mt-sm italic">
Preparing trumpsuit selection...
</div>
</div>
</q-card-section>
</q-card> </q-card>
</transition> </transition>
</template> </template>
@@ -397,4 +451,35 @@ const options = computed(() => {
50% { transform: scale(1.45); box-shadow: 0 0 40px rgba(25, 118, 210, 0.6); } 50% { transform: scale(1.45); box-shadow: 0 0 40px rgba(25, 118, 210, 0.6); }
100% { transform: scale(1.4); } 100% { transform: scale(1.4); }
} }
/* Add to previous styles */
.winner-glow .card-image {
border-color: #31ccec; /* Default 'info' blue for ongoing ties */
transform: scale(1.05) translateY(-10px);
box-shadow: 0 10px 40px rgba(49, 204, 236, 0.4) !important;
}
/* If there is only one winner, change glow to Gold */
.winner-glow:only-child .card-image,
.tie-break-card .winner-glow:has(.bg-positive) .card-image {
border-color: #f2c037;
box-shadow: 0 10px 40px rgba(242, 192, 55, 0.5) !important;
}
.opacity-40 {
opacity: 0.4;
filter: grayscale(0.8);
transform: scale(0.9);
transition: all 0.5s ease;
}
.card-stack {
perspective: 1000px;
}
.card-image-reveal {
width: 130px;
border-radius: 10px;
transition: all 0.4s cubic-bezier(0.175, 0.885, 0.32, 1.275);
}
</style> </style>

View File

@@ -33,6 +33,7 @@ type TieInfo = {
tiedPlayers: Player[] tiedPlayers: Player[]
highestAmount: number highestAmount: number
selectedCards: Record<string, Card> selectedCards: Record<string, Card>
winners: Player[] | null
} }
type TrumpInfo = { type TrumpInfo = {