Version 1.0 #102

Merged
FernandoJVideira merged 163 commits from develop into master 2026-01-03 14:49:00 +00:00
4 changed files with 112 additions and 111 deletions
Showing only changes of commit ceb3d735ad - Show all commits
+12 -55
View File
@@ -2,7 +2,6 @@
<div <div
class="min-h-screen bg-linear-to-br from-green-700 via-green-800 to-green-900 grid grid-rows-[auto_auto_1fr_auto] grid-cols-[1fr_3fr_1fr] gap-6 p-8" class="min-h-screen bg-linear-to-br from-green-700 via-green-800 to-green-900 grid grid-rows-[auto_auto_1fr_auto] grid-cols-[1fr_3fr_1fr] gap-6 p-8"
> >
<!-- Score Display (Top Left) -->
<div class="col-start-1 col-end-4 row-start-1 flex justify-start items-start"> <div class="col-start-1 col-end-4 row-start-1 flex justify-start items-start">
<ScoreDisplay <ScoreDisplay
:player-score="playerScore" :player-score="playerScore"
@@ -12,17 +11,14 @@
/> />
</div> </div>
<!-- Opponent Hand -->
<div class="col-start-2 col-end-3 row-start-2 flex justify-center"> <div class="col-start-2 col-end-3 row-start-2 flex justify-center">
<PlayerHand <PlayerHand
:cards="opponentHand" :cards="opponentHand"
:face-down="true" :face-down="true"
:max-cards="maxCards" :max-cards="maxCards"
:playable-cards="[]"
/> />
</div> </div>
<!-- Play Area -->
<div class="col-start-2 col-end-3 row-start-3 flex items-center justify-center"> <div class="col-start-2 col-end-3 row-start-3 flex items-center justify-center">
<PlayArea <PlayArea
:player-card="currentTrick.playerCard" :player-card="currentTrick.playerCard"
@@ -31,18 +27,15 @@
/> />
</div> </div>
<!-- Deck Area -->
<div class="col-start-3 col-end-4 row-start-3 flex items-center justify-center"> <div class="col-start-3 col-end-4 row-start-3 flex items-center justify-center">
<DeckArea :trump-card="trumpCard" :cards-remaining="cardsRemaining" :is-empty="deckIsEmpty" /> <DeckArea :trump-card="trumpCard" :cards-remaining="cardsRemaining" :is-empty="deckIsEmpty" />
</div> </div>
<!-- Player Hand -->
<div class="col-start-2 col-end-3 row-start-4 flex justify-center"> <div class="col-start-2 col-end-3 row-start-4 flex justify-center">
<PlayerHand <PlayerHand
:cards="playerHand" :cards="playerHand"
:face-down="false" :face-down="false"
:max-cards="maxCards" :max-cards="maxCards"
:playable-cards="playableCardIndices"
@card-clicked="handleCardClick" @card-clicked="handleCardClick"
/> />
</div> </div>
@@ -55,52 +48,20 @@ import PlayArea from './PlayArea.vue'
import DeckArea from './DeckArea.vue' import DeckArea from './DeckArea.vue'
import ScoreDisplay from './ScoreDisplay.vue' import ScoreDisplay from './ScoreDisplay.vue'
// Props for testing - in real implementation, these come from Pinia store
const props = defineProps({ const props = defineProps({
playerHand: { playerHand: { type: Array, default: () => [] },
type: Array, opponentHand: { type: Array, default: () => [] },
default: () => [],
},
opponentHand: {
type: Array,
default: () => [],
},
currentTrick: { currentTrick: {
type: Object, type: Object,
default: () => ({ default: () => ({ playerCard: null, opponentCard: null, winner: null }),
playerCard: null,
opponentCard: null,
winner: null,
}),
},
trumpCard: {
type: Object,
required: true,
},
cardsRemaining: {
type: Number,
default: 0,
},
deckIsEmpty: {
type: Boolean,
default: false,
},
maxCards: {
type: Number,
default: 3,
},
playableCardIndices: {
type: Array,
default: () => [],
},
playerScore: {
type: Number,
default: 0,
},
opponentScore: {
type: Number,
default: 0,
}, },
trumpCard: { type: Object, required: true },
cardsRemaining: { type: Number, default: 0 },
deckIsEmpty: { type: Boolean, default: false },
maxCards: { type: Number, default: 3 },
// REMOVIDO: playableCardIndices (já não precisamos dele)
playerScore: { type: Number, default: 0 },
opponentScore: { type: Number, default: 0 },
currentTurn: { currentTurn: {
type: String, type: String,
default: 'player', default: 'player',
@@ -108,13 +69,9 @@ const props = defineProps({
}, },
}) })
const emit = defineEmits(['card-clicked']) const emit = defineEmits(['play-card'])
const handleCardClick = (payload) => { const handleCardClick = (payload) => {
emit('card-clicked', payload) emit('play-card', payload.card)
}
const handlePlayCard = (card) => {
store.playerPlayCard(card)
} }
</script> </script>
+34 -24
View File
@@ -12,13 +12,15 @@
:style="getCardStyle(index)" :style="getCardStyle(index)"
@mouseenter="hoveredIndex = index" @mouseenter="hoveredIndex = index"
@mouseleave="hoveredIndex = null" @mouseleave="hoveredIndex = null"
@click="$emit('card-clicked', { card, index })" @click="handleCardClick(card, index)"
> >
<div <div
:class="[ :class="[
'transition-all duration-300', 'transition-all duration-300 rounded-lg',
isPlayable(index) && 'ring-2 ring-yellow-400 ring-offset-2 rounded-lg', isValid(card)
hoveredIndex === index && 'scale-110 -translate-y-4 z-50', ? 'cursor-pointer hover:shadow-xl ring-2 ring-transparent hover:ring-yellow-400/50'
: 'cursor-not-allowed opacity-60 grayscale brightness-75',
hoveredIndex === index && isValid(card) && 'scale-110 -translate-y-4 z-50',
]" ]"
> >
<GameCard :suit="card.suit" :rank="card.rank" :face-down="faceDown" /> <GameCard :suit="card.suit" :rank="card.rank" :face-down="faceDown" />
@@ -31,6 +33,8 @@
<script setup> <script setup>
import { ref, computed } from 'vue' import { ref, computed } from 'vue'
import GameCard from './GameCard.vue' import GameCard from './GameCard.vue'
import { toast } from 'vue-sonner'
import { useBiscaStore } from '@/stores/bisca'
const props = defineProps({ const props = defineProps({
cards: { cards: {
@@ -47,41 +51,52 @@ const props = defineProps({
default: 3, default: 3,
validator: (value) => [3, 9].includes(value), validator: (value) => [3, 9].includes(value),
}, },
playableCards: {
type: Array,
default: () => [],
},
}) })
defineEmits(['card-clicked']) const emit = defineEmits(['card-clicked'])
const store = useBiscaStore()
const hoveredIndex = ref(null) const hoveredIndex = ref(null)
// Calculate overlap percentage based on max cards const isValid = (card) => {
// 3 cards: 70% overlap, 9 cards: 85% overlap if (props.faceDown) return false
return store.canPlayCard(card)
}
const handleCardClick = (card, index) => {
if (props.faceDown) return
if (store.canPlayCard(card)) {
emit('card-clicked', { card, index })
} else {
toast.warning('Jogada Inválida', {
description: 'Tens de assistir ao naipe jogado!',
duration: 2000,
})
}
}
const overlapPercentage = computed(() => { const overlapPercentage = computed(() => {
return props.maxCards === 3 ? 0.7 : 0.85 return props.maxCards === 3 ? 0.7 : 0.85
}) })
// Calculate card spacing dynamically const cardWidth = 128
const cardWidth = 128 // w-32 from GameCard component
const getCardStyle = (index) => { const getCardStyle = (index) => {
const totalCards = props.cards.length const totalCards = props.cards.length
const overlapOffset = cardWidth * (1 - overlapPercentage.value) const overlapOffset = cardWidth * (1 - overlapPercentage.value)
// Calculate total width needed
const totalWidth = cardWidth + (totalCards - 1) * overlapOffset const totalWidth = cardWidth + (totalCards - 1) * overlapOffset
const startOffset = -totalWidth / 2 + cardWidth / 2 const startOffset = -totalWidth / 2 + cardWidth / 2
// Base position
let left = startOffset + index * overlapOffset let left = startOffset + index * overlapOffset
// If a card is hovered, shift neighbors
if (hoveredIndex.value !== null && hoveredIndex.value !== index) { if (hoveredIndex.value !== null && hoveredIndex.value !== index) {
if (isValid(props.cards[hoveredIndex.value])) {
if (index < hoveredIndex.value) { if (index < hoveredIndex.value) {
left -= 10 // Push left cards more to the left left -= 10
} else if (index > hoveredIndex.value) { } else if (index > hoveredIndex.value) {
left += 10 // Push right cards more to the right left += 10
}
} }
} }
@@ -90,9 +105,4 @@ const getCardStyle = (index) => {
zIndex: hoveredIndex.value === index ? 50 : index + 1, zIndex: hoveredIndex.value === index ? 50 : index + 1,
} }
} }
// Check if a card is playable
const isPlayable = (index) => {
return props.playableCards.includes(index)
}
</script> </script>
@@ -51,7 +51,6 @@ import router from '@/router';
const emits = defineEmits(['logout']) const emits = defineEmits(['logout'])
const { userLoggedIn } = defineProps(['userLoggedIn']) const { userLoggedIn } = defineProps(['userLoggedIn'])
const router = useRouter()
const biscaStore = useBiscaStore() const biscaStore = useBiscaStore()
const logoutClickHandler = () => { const logoutClickHandler = () => {
+60 -25
View File
@@ -74,6 +74,10 @@ export const useBiscaStore = defineStore('bisca', () => {
if (!isGameRunning.value || currentTurn.value !== 'player') return if (!isGameRunning.value || currentTurn.value !== 'player') return
if (table.value.playerCard) return if (table.value.playerCard) return
if (!canPlayCard(card)) {
console.warn('Jogada Inválida! Tens de assistir ao naipe.')
return
}
playerHand.value = playerHand.value.filter((c) => c.id !== card.id) playerHand.value = playerHand.value.filter((c) => c.id !== card.id)
table.value.playerCard = card table.value.playerCard = card
@@ -137,55 +141,65 @@ export const useBiscaStore = defineStore('bisca', () => {
} }
} }
const resolveTrick = () => { const resolveTrick = async () => {
const pCard = table.value.playerCard const pCard = table.value.playerCard
const oCard = table.value.opponentCard const oCard = table.value.opponentCard
const trumpSuit = trumpCard.value.suit const trump = trumpCard.value.suit
let trickWinner = ''
if (pCard.suit === trumpSuit && oCard.suit !== trumpSuit) trickWinner = 'player' let weWon = false
else if (oCard.suit === trumpSuit && pCard.suit !== trumpSuit) trickWinner = 'opponent'
else if (pCard.suit === oCard.suit) { if (table.value.firstToPlay === 'player') {
trickWinner = getStrength(pCard.rank) > getStrength(oCard.rank) ? 'player' : 'opponent' if (doesCardWin(oCard, pCard, trump)) weWon = false
else weWon = true
} else { } else {
trickWinner = table.value.firstToPlay if (doesCardWin(pCard, oCard, trump)) weWon = true
else weWon = false
} }
const points = pCard.points + oCard.points const points = pCard.points + oCard.points
if (weWon) {
if (trickWinner === 'player') {
playerScore.value += points playerScore.value += points
playerTricks.value++ playerTricks.value++
currentTurn.value = 'player'
} else { } else {
opponentScore.value += points opponentScore.value += points
opponentTricks.value++ opponentTricks.value++
currentTurn.value = 'opponent'
} }
table.value = { playerCard: null, opponentCard: null, firstToPlay: null } table.value = { playerCard: null, opponentCard: null, firstToPlay: null }
if ( if (deck.value.length > 0) {
deck.value.length === 0 && if (weWon) {
playerHand.value.length === 0 && await drawOneCard('player')
opponentHand.value.length === 0 await drawOneCard('opponent')
) { } else {
await drawOneCard('opponent')
await drawOneCard('player')
}
} else if (playerHand.value.length === 0 && opponentHand.value.length === 0) {
endGame() endGame()
return return
} }
if (deck.value.length > 0) { if (currentTurn.value === 'opponent') {
if (trickWinner === 'player') { setTimeout(botPlayCard, 1000)
playerHand.value.push(deck.value.pop())
opponentHand.value.push(deck.value.pop())
} else {
opponentHand.value.push(deck.value.pop())
playerHand.value.push(deck.value.pop())
} }
} }
currentTurn.value = trickWinner const drawOneCard = async (who) => {
if (trickWinner === 'opponent') { if (deck.value.length === 0) return
setTimeout(botPlayCard, 1000)
let card
if (deck.value.length === 1) {
card = deck.value.pop()
trumpCard.value = null
} else {
card = deck.value.pop()
} }
if (who === 'player') playerHand.value.push(card)
else opponentHand.value.push(card)
} }
const doesCardWin = (attacker, defender, trumpSuit) => { const doesCardWin = (attacker, defender, trumpSuit) => {
@@ -272,6 +286,26 @@ export const useBiscaStore = defineStore('bisca', () => {
isGameOver.value = true isGameOver.value = true
} }
const canPlayCard = (card) => {
// 1. Se não for a minha vez, não posso jogar nada
if (currentTurn.value !== 'player') return false
if (!table.value.opponentCard) return true
if (deck.value.length > 0) return true
const suitToFollow = table.value.opponentCard.suit
const hasSuit = playerHand.value.some((c) => c.suit === suitToFollow)
if (hasSuit && card.suit !== suitToFollow) {
return false
}
return true
}
const wait = (ms) => new Promise((resolve) => setTimeout(resolve, ms)) const wait = (ms) => new Promise((resolve) => setTimeout(resolve, ms))
return { return {
@@ -295,5 +329,6 @@ export const useBiscaStore = defineStore('bisca', () => {
quitGame, quitGame,
resolveTrick, resolveTrick,
playerPlayCard, playerPlayCard,
canPlayCard,
} }
}) })