SyntaxSquad/DADProject#14 feature/frontend-gameboard-singleplayer #75
@@ -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)
|
||||||
}
|
}
|
||||||
|
</script>
|
||||||
const handlePlayCard = (card) => {
|
|
||||||
store.playerPlayCard(card)
|
|
||||||
}
|
|
||||||
</script>
|
|
||||||
@@ -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 (index < hoveredIndex.value) {
|
if (isValid(props.cards[hoveredIndex.value])) {
|
||||||
left -= 10 // Push left cards more to the left
|
if (index < hoveredIndex.value) {
|
||||||
} else if (index > hoveredIndex.value) {
|
left -= 10
|
||||||
left += 10 // Push right cards more to the right
|
} else if (index > hoveredIndex.value) {
|
||||||
|
left += 10
|
||||||
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -90,9 +105,4 @@ const getCardStyle = (index) => {
|
|||||||
zIndex: hoveredIndex.value === index ? 50 : index + 1,
|
zIndex: hoveredIndex.value === index ? 50 : index + 1,
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
</script>
|
||||||
// Check if a card is playable
|
|
||||||
const isPlayable = (index) => {
|
|
||||||
return props.playableCards.includes(index)
|
|
||||||
}
|
|
||||||
</script>
|
|
||||||
@@ -49,7 +49,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 = () => {
|
||||||
|
|||||||
@@ -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,57 +141,67 @@ 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') {
|
|
||||||
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
|
|
||||||
if (trickWinner === 'opponent') {
|
|
||||||
setTimeout(botPlayCard, 1000)
|
setTimeout(botPlayCard, 1000)
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
const drawOneCard = async (who) => {
|
||||||
|
if (deck.value.length === 0) return
|
||||||
|
|
||||||
|
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) => {
|
||||||
if (attacker.suit === trumpSuit && defender.suit !== trumpSuit) return true
|
if (attacker.suit === trumpSuit && defender.suit !== trumpSuit) return true
|
||||||
|
|
||||||
@@ -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,
|
||||||
}
|
}
|
||||||
})
|
})
|
||||||
|
|||||||
Reference in New Issue
Block a user