feat(ui): Tie selection #28

Closed
lq64 wants to merge 2 commits from feat/FRO-26 into main
2 changed files with 405 additions and 58 deletions
Showing only changes of commit dbffce8818 - Show all commits

View File

@@ -1,10 +1,9 @@
<script setup lang="ts">
import {useWebSocket} from "@/composables/useWebsocket.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 { ref } from 'vue';
import { computed } from 'vue'
import { computed, nextTick, ref } from 'vue'
const wb = useWebSocket()
const wi = useIngame()
@@ -35,46 +34,51 @@ const myRevealedCard = computed(() => {
});
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.
// It keeps the "Pick" screen visible while flipping.
const showPickScreen = computed(() => {
const isMyTurn = tieInf.value.self?.id === tieInf.value.currentPlayer?.id;
return isMyTurn || isFlipping.value;
});
// This replaces your v-if check.
// It keeps the "Pick" screen visible while flipping.
const showPickScreen = computed(() => {
const isMyTurn = tieInf.value.self?.id === tieInf.value.currentPlayer?.id;
return isMyTurn || isFlipping.value;
});
function selectTie(tieIndex: number) {
// Use model.value because it's a ref
wb.sendAndWait("PickTie", { cardIndex: tieIndex })
.then(() => {
console.log("Server accepted pick, starting animation...");
function selectTie(tieIndex: number) {
// Use model.value because it's a ref
wb.sendAndWait("PickTie", { cardIndex: tieIndex })
.then(async () => {
console.log("Server accepted pick, starting animation...");
// 1. Trigger the animation state
isFlipping.value = true;
// 1. Trigger the animation state
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
$q.notify({
message: "Card revealed!",
color: "positive",
position: "top",
timeout: 1000
});
// 3. Optional Notification
$q.notify({
message: "Card revealed!",
color: "positive",
position: "top",
timeout: 1000
});
// 3. Wait for animation to finish before switching to Waiting Screen
setTimeout(() => {
isFlipping.value = false;
model.value = null;
}, 2500);
})
.catch((error) => {
console.error("Pick failed:", error);
$q.notify({
message: error.message || "Failed to pick card",
color: "negative",
position: "top"
});
});
}
// 4. Wait for animation to finish before switching to Waiting Screen
setTimeout(() => {
isFlipping.value = false;
model.value = null;
}, 500);
})
.catch((error) => {
console.error("Pick failed:", error);
$q.notify({
message: error.message || "Failed to pick card",
color: "negative",
position: "top"
});
});
}
const model = ref<number | null>(null)
const options = computed(() => {
const list = []
@@ -114,19 +118,41 @@ const options = computed(() => {
:class="{ 'selected-card': model === n, 'is-flipping': isFlipping && model === n }"
>
<div class="card-inner">
<q-img
:key="myRevealedCard || 'hidden'"
:src="(model === n && myRevealedCard) ? myRevealedCard : getCardImagePath(tieBlankCard)"
class="card-image shadow-24"
:class="{ 'animate-reveal': isFlipping && model === n }"
/>
<!-- 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>
<div v-if="!isFlipping" class="card-number">
{{ n }}
<!-- BACK: revealed card (your pick result) -->
<div class="flip-card__face flip-card__back">
<q-img
:src="(model === n && myRevealedCard) ? myRevealedCard : getCardImagePath(tieBlankCard)"
class="card-image shadow-24"
no-spinner
no-transition
/>
</div>
</div>
</div>
</div>
</div>
</div>
<div v-if="!isFlipping" class="card-number">
{{ n }}
</div>
</div>
</div>
</div>
<div class="action-area" :class="{ 'visible': model !== null && !isFlipping }">
<div class="row q-gutter-md justify-center items-center">
@@ -358,38 +384,55 @@ const options = computed(() => {
}
.card-inner {
perspective: 1000px;
transform-style: preserve-3d;
}
/* The Reveal Animation */
.animate-reveal {
animation: dramaticFlip 1.2s cubic-bezier(0.4, 0, 0.2, 1) forwards;
/* --- Flip card (reliable 3D flip) --- */
.flip-card {
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;
z-index: 1000;
}
@keyframes dramaticFlip {
0% {
transform: rotateY(0deg) scale(1);
}
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 q-img root stretches to fill the face */
.flip-card__face :deep(.q-img) {
width: 100%;
}
/* Ensure the image isn't mirrored after the rotation logic */
.animate-reveal :deep(img) {
backface-visibility: hidden;
.flip-card__front {
transform: rotateY(0deg);
}
.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 */
.selected-card:not(.is-flipping) {
animation: pulse 2s infinite;
animation: pulse 0.5s infinite;
}
@keyframes pulse {