feat: FRO-26 Create Tie selection component

This commit is contained in:
2026-01-14 10:35:27 +01:00
parent f29ed9d338
commit dbffce8818

View File

@@ -1,10 +1,9 @@
<script setup lang="ts"> <script setup lang="ts">
import {useWebSocket} from "@/composables/useWebsocket.ts"; import {useWebSocket} from "@/composables/useWebsocket.ts";
import {useIngame} from "@/composables/useIngame.ts"; import {useIngame} from "@/composables/useIngame.ts";
import type {GameInfo, LobbyInfo, TieInfo, TrumpInfo} from "@/types/GameTypes.ts"; import type { TieInfo } from "@/types/GameTypes.ts";
import {useQuasar} from "quasar"; import {useQuasar} from "quasar";
import { ref } from 'vue'; import { computed, nextTick, ref } from 'vue'
import { computed } from 'vue'
const wb = useWebSocket() const wb = useWebSocket()
const wi = useIngame() const wi = useIngame()
@@ -35,6 +34,8 @@ const myRevealedCard = computed(() => {
}); });
const isFlipping = ref(false); const isFlipping = ref(false);
// Used to force-remount the flipping subtree so the CSS transition/animation restarts reliably
const revealKey = ref(0);
// This replaces your v-if check. // This replaces your v-if check.
// It keeps the "Pick" screen visible while flipping. // It keeps the "Pick" screen visible while flipping.
@@ -46,13 +47,16 @@ const showPickScreen = computed(() => {
function selectTie(tieIndex: number) { function selectTie(tieIndex: number) {
// Use model.value because it's a ref // Use model.value because it's a ref
wb.sendAndWait("PickTie", { cardIndex: tieIndex }) wb.sendAndWait("PickTie", { cardIndex: tieIndex })
.then(() => { .then(async () => {
console.log("Server accepted pick, starting animation..."); console.log("Server accepted pick, starting animation...");
// 1. Trigger the animation state // 1. Trigger the animation state
isFlipping.value = true; isFlipping.value = true;
// 2. Force a remount of the flip-card for the selected card so the flip restarts even if src doesn't change
revealKey.value += 1;
await nextTick();
// 2. Optional Notification // 3. Optional Notification
$q.notify({ $q.notify({
message: "Card revealed!", message: "Card revealed!",
color: "positive", color: "positive",
@@ -60,11 +64,11 @@ function selectTie(tieIndex: number) {
timeout: 1000 timeout: 1000
}); });
// 3. Wait for animation to finish before switching to Waiting Screen // 4. 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); }, 500);
}) })
.catch((error) => { .catch((error) => {
console.error("Pick failed:", error); console.error("Pick failed:", error);
@@ -114,12 +118,34 @@ const options = computed(() => {
:class="{ 'selected-card': model === n, 'is-flipping': isFlipping && model === n }" :class="{ 'selected-card': model === n, 'is-flipping': isFlipping && model === n }"
> >
<div class="card-inner"> <div class="card-inner">
<!-- Flip card: keep both faces mounted; flip the inner wrapper. -->
<div
class="flip-card"
:class="{ 'flip-card--flipped': isFlipping && model === n && !!myRevealedCard }"
:key="(isFlipping && model === n) ? `${revealKey}-${n}` : `static-${n}`"
>
<div class="flip-card__inner">
<!-- FRONT: blank card (deck back) -->
<div class="flip-card__face flip-card__front">
<q-img
:src="getCardImagePath(tieBlankCard)"
class="card-image shadow-24"
no-spinner
no-transition
/>
</div>
<!-- BACK: revealed card (your pick result) -->
<div class="flip-card__face flip-card__back">
<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 }" no-spinner
no-transition
/> />
</div>
</div>
</div>
<div v-if="!isFlipping" class="card-number"> <div v-if="!isFlipping" class="card-number">
{{ n }} {{ n }}
@@ -358,38 +384,55 @@ const options = computed(() => {
} }
.card-inner { .card-inner {
perspective: 1000px; perspective: 1000px;
transform-style: preserve-3d;
} }
/* The Reveal Animation */ /* --- Flip card (reliable 3D flip) --- */
.animate-reveal { .flip-card {
animation: dramaticFlip 1.2s cubic-bezier(0.4, 0, 0.2, 1) forwards; width: 60px;
/* height follows image; q-img sets its own box, but we want a stable 3D container */
perspective: 1000px;
}
.flip-card__inner {
position: relative;
width: 100%;
transform-style: preserve-3d;
transition: transform 1.2s cubic-bezier(0.4, 0, 0.2, 1);
}
.flip-card__face {
position: absolute;
inset: 0;
backface-visibility: hidden; backface-visibility: hidden;
z-index: 1000;
} }
@keyframes dramaticFlip { /* Ensure the q-img root stretches to fill the face */
0% { .flip-card__face :deep(.q-img) {
transform: rotateY(0deg) scale(1); width: 100%;
}
40% {
/* Halfway: Card is vertical and lifted up */
transform: rotateY(90deg) scale(1.5) translateY(-30px);
filter: brightness(1.3);
}
100% {
/* Final: Card is flat again, showing the face */
transform: rotateY(0deg) scale(1.3) translateY(0);
}
} }
/* Ensure the image isn't mirrored after the rotation logic */ .flip-card__front {
.animate-reveal :deep(img) { transform: rotateY(0deg);
backface-visibility: hidden; }
.flip-card__back {
transform: rotateY(180deg);
}
.flip-card--flipped .flip-card__inner {
/* Lift/scale a bit like your original dramatic flip */
transform: rotateY(180deg) scale(1.3) translateY(-10px);
}
/* While flipping, don't let hover/selected transforms fight the 3D transform */
.tie-card-wrapper.is-flipping:hover {
transform: none;
} }
/* Optional: Slight pulse while the button is waiting to be clicked */ /* Optional: Slight pulse while the button is waiting to be clicked */
.selected-card:not(.is-flipping) { .selected-card:not(.is-flipping) {
animation: pulse 2s infinite; animation: pulse 0.5s infinite;
} }
@keyframes pulse { @keyframes pulse {