diff --git a/client/app/index.html b/client/app/index.html index ba382887e117513d67d21e391d663789822046ee..057abb12483c06f07ab37070cb74995c1631f705 100644 --- a/client/app/index.html +++ b/client/app/index.html @@ -29,6 +29,9 @@ .card { @apply bg-gray-800 p-6 rounded-lg border-4 border-gray-600 max-w-md w-full; } + .card-playing { + @apply bg-gray-800 p-1 rounded-lg border-4 border-gray-600 w-full h-80 w-60 cursor-pointer transition-all relative overflow-hidden; + } .title { @apply text-green-400 text-2xl text-center mb-8; } diff --git a/client/app/js/socket.js b/client/app/js/socket.js index 114737d1638e4baaa3ba3f272fc7c91bf494316c..0d097f3d602d664b91b7631daf1df6ec3d0b4022 100644 --- a/client/app/js/socket.js +++ b/client/app/js/socket.js @@ -13,7 +13,8 @@ export const socketEvents = { // Room events createRoom: (username) => socket.emit("createRoom", { username }), joinRoom: (roomId, username) => socket.emit("joinRoom", { roomId, username }), - startGame: (roomId) => socket.emit("startGame", { roomId }), + startGame: (roomId, artStyle) => + socket.emit("startGame", { roomId, artStyle }), getRoomData: (roomId) => socket.emit("getRoomData", { roomId }), disconnect: () => { socket.disconnect(); diff --git a/client/app/js/store.js b/client/app/js/store.js index 1a1884708a5f96ad2cc39b2093854279e51bdd7b..f9f9cd511e9d4a0dd9093dd0a317cf11df1b6f5b 100644 --- a/client/app/js/store.js +++ b/client/app/js/store.js @@ -9,6 +9,7 @@ export const store = reactive({ //this is the room code that the user is in currentRoom: null, game: null, + loading: false, error: null, myDeck: null, @@ -43,6 +44,7 @@ export const store = reactive({ }, updateGame(game) { + this.loading = false; this.game = game; this.myState = game.users.find((user) => user.username === this.username); this.myDeck = game.gameInstance?.players.find( diff --git a/client/app/vue/Game.vue b/client/app/vue/Game.vue index d323f04269ac81ee15bfcc0261e50435eb8e130f..a0e7e87a6a523aced9196a8025fc2652551ffb77 100644 --- a/client/app/vue/Game.vue +++ b/client/app/vue/Game.vue @@ -20,8 +20,9 @@ const sendMessage = () => { message.value = ""; }; -const startGame = () => { - socketEvents.startGame(roomId); +const startGame = async () => { + store.loading = true; + socketEvents.startGame(roomId, selectedArtStyle.value); }; onMounted(() => { @@ -158,9 +159,10 @@ watch( <button @click="startGame" class="button !w-fit !px-8 !py-3" - :disabled="!canStartGame" + :disabled="!canStartGame || store.loading" > - START GAME + <span v-if="store.loading">loading...</span> + <span v-else>START GAME</span> </button> </div> diff --git a/client/app/vue/Playing.vue b/client/app/vue/Playing.vue index a609c8423e247e5539f500fe1719ed4cd96d36c6..71374e6d315d2d6b8d4a66d1845137b26897b167 100644 --- a/client/app/vue/Playing.vue +++ b/client/app/vue/Playing.vue @@ -2,6 +2,7 @@ import { ref, computed } from "vue"; import { store } from "../js/store"; import { socketEvents } from "../js/socket"; +import GameCard from "./components/GameCard.vue"; const selectedCard = ref(null); const secondSelectedCard = ref(null); @@ -239,15 +240,10 @@ const handleGiftCardSelection = (targetPlayerId) => { </div> <!-- Main Game Content --> - <div v-else> + <div v-else class="w-full"> <h1 class="title">CARD CRAWL</h1> - <div v-if="isWaitingForPlayers"> - <p class="text-gray-400 text-xl p-1"> - Waiting for other players to finish... - </p> - </div> <!-- Player info cards --> - <div class="grid grid-cols-4 gap-2 mb-4"> + <div class="flex flex-wrap w-full gap-2 mb-4 justify-center"> <div v-for="player in store.game?.users" :key="player.id" @@ -299,108 +295,55 @@ const handleGiftCardSelection = (targetPlayerId) => { <!-- Main game board --> <div class="flex flex-col items-center gap-8"> <!-- Top row: 4 cards --> - <div class="grid grid-cols-4 gap-4"> - <div + <div + class="flex flex-wrap w-full gap-2 items-center justify-evenly min-h-48" + > + <GameCard v-for="card in store.myDeck?.board" :key="card.id" - class="card w-32 h-48 cursor-pointer transition-all hover:ring-2 hover:ring-green-100" - :class="{ - 'ring-2 ring-green-500': card?.id === selectedCard?.id, - }" + :card="card" + :is-selected="card?.id === selectedCard?.id" @click="handleCardClick({ ...card, source: 'board' })" - > - <div v-if="card" class="flex flex-col h-full"> - <div class="text-center mb-2 text-green-400"> - {{ card.name }} - </div> - <div class="text-gray-400 text-xs mb-auto"> - {{ card.type }} - </div> - <div class="text-xl text-center text-white"> - {{ card.value }} - </div> - </div> - <div v-else class="flex items-center justify-center h-full"> - <div class="text-gray-600">Empty</div> - </div> + /> + <div v-if="isWaitingForPlayers"> + <p class="text-gray-400 text-xl p-1 text-center"> + Waiting for other players to finish... + </p> </div> </div> <!-- Bottom row: hands and player --> <div class="grid grid-cols-3 gap-4"> <!-- Left Hand --> - <div - class="card w-32 h-48 cursor-pointer transition-all hover:border-green-100" - :class="{ - 'ring-2 ring-green-500': secondSelectedCard === 'LEFTHAND', - }" + <GameCard + :card="store.myDeck?.leftHand" + section-type="LEFTHAND" + section-title="Left Hand" + :is-selected="secondSelectedCard === 'LEFTHAND'" @click="handleSectionClick('LEFTHAND')" - > - <div class="flex flex-col h-full"> - <div v-if="store.myDeck?.leftHand" class="text-center"> - <div class="text-green-400"> - {{ store.myDeck.leftHand.name }} - </div> - <div class="text-gray-400 text-xs"> - {{ store.myDeck.leftHand.type }} - </div> - </div> - <div v-else class="text-center"> - <div class="text-gray-600">Left Hand</div> - <div class="text-gray-500 text-xs">Empty</div> - </div> - <div class="mt-auto text-xl text-center"> - {{ store.myDeck?.leftHand?.value || 0 }} - </div> - </div> - </div> + /> <!-- Player --> - <div - class="card w-32 h-48 cursor-pointer transition-all hover:border-100" - :class="{ - 'ring-2 ring-green-500': secondSelectedCard === 'PLAYER', - }" + <GameCard + :player="store.myDeck" + section-type="PLAYER" + section-title="Player" + :is-selected="secondSelectedCard === 'PLAYER'" @click="handleSectionClick('PLAYER')" - > - <div class="flex flex-col items-center justify-center h-full"> - <div class="text-green-400">Player</div> - <div class="text-red-500 text-xl mt-2"> - HP: {{ store.myDeck?.health || 10 }} - </div> - </div> - </div> + /> <!-- Right Hand --> - <div - class="card w-32 h-48 cursor-pointer transition-all hover:ring-2 hover:ring-green-100" - :class="{ - 'ring-2 ring-green-500': secondSelectedCard === 'RIGHTHAND', - }" + <GameCard + :card="store.myDeck?.rightHand" + section-type="RIGHTHAND" + section-title="Right Hand" + :is-selected="secondSelectedCard === 'RIGHTHAND'" @click="handleSectionClick('RIGHTHAND')" - > - <div class="flex flex-col h-full"> - <div v-if="store.myDeck?.rightHand" class="text-center"> - <div class="text-green-400"> - {{ store.myDeck.rightHand.name }} - </div> - <div class="text-gray-400 text-xs"> - {{ store.myDeck.rightHand.type }} - </div> - </div> - <div v-else class="text-center"> - <div class="text-gray-600">Right Hand</div> - <div class="text-gray-500 text-xs">Empty</div> - </div> - <div class="mt-auto text-xl text-center"> - {{ store.myDeck?.rightHand?.value || 0 }} - </div> - </div> - </div> + /> </div> <!-- Inventory --> - <div class="inventory-section mt-4"> + <div class="gap-4 mt-4 w-full flex items-center flex-col relative"> <h3 class="text-green-400 cursor-pointer" @click="handleSectionClick('INVENTORY')" @@ -408,39 +351,24 @@ const handleGiftCardSelection = (targetPlayerId) => { Inventory </h3> <div class="grid grid-cols-4 gap-2 cursor-pointer min-h-48"> - <div + <GameCard v-for="card in store.myDeck?.inventory" :key="card.id" - class="card w-24 h-36 cursor-pointer transition-all hover:ring-2 hover:ring-green-100" - :class="{ - 'ring-2 ring-green-500': card?.id === selectedCard?.id, - }" + :card="card" + :is-selected="card?.id === selectedCard?.id" + size="small" @click="handleCardClick({ ...card, source: 'inventory' })" + /> + </div> + <div class="absolute bottom-5 right-5"> + <button + class="px-3 py-1 bg-green-800 text-green-400 rounded hover:bg-green-700 transition-colors" + @click="handleSectionClick('SHOP')" > - <div class="flex flex-col h-full p-2"> - <div class="text-center mb-1 text-green-400 text-sm"> - {{ card.name }} - </div> - <div class="text-gray-400 text-xs mb-auto"> - {{ card.type }} - </div> - <div class="text-lg text-center text-white"> - {{ card.value }} - </div> - </div> - </div> + Shop + </button> </div> </div> - - <!-- Shop --> - <div class="absolute bottom-5 right-5"> - <button - class="px-3 py-1 bg-green-800 text-green-400 rounded hover:bg-green-700 transition-colors" - @click="handleSectionClick('SHOP')" - > - Shop - </button> - </div> </div> </div> </div> diff --git a/client/app/vue/components/GameCard.vue b/client/app/vue/components/GameCard.vue new file mode 100644 index 0000000000000000000000000000000000000000..e53e54dfd09b6d2d0bda4c029d0b429ca14025a0 --- /dev/null +++ b/client/app/vue/components/GameCard.vue @@ -0,0 +1,121 @@ +<script setup> +import { computed } from "vue"; + +const props = defineProps({ + // Card data + card: { + type: Object, + default: null, + }, + // For section cards like PLAYER, LEFTHAND, etc. + sectionType: { + type: String, + default: "", + }, + sectionTitle: { + type: String, + default: "", + }, + // Selection states + isSelected: { + type: Boolean, + default: false, + }, + player: { + type: Object, + default: null, + }, +}); + +const emit = defineEmits(["click"]); + +const cardClasses = computed(() => ({ + "ring-2 ring-green-500": props.isSelected, + "hover:ring-2 hover:ring-green-100": true, +})); + +const handleClick = () => { + emit("click"); +}; +</script> + +<template> + <div class="card-playing" :class="cardClasses" @click="handleClick"> + <!-- Section Card (PLAYER, HANDS) --> + <div v-if="sectionType" class="flex flex-col h-full"> + <div + v-if="card" + class="flex flex-col h-full" + :style="{ + backgroundImage: card.image ? `url(${card.image})` : '', + backgroundSize: 'cover', + backgroundPosition: 'center', + }" + > + <div class="text-center bg-black/80 p-1"> + <div class="text-green-400">{{ card.name }}</div> + <div class="text-gray-200 text-xs">{{ card.type }}</div> + </div> + <div class="mt-auto text-xl text-center bg-black/80 p-1"> + {{ card?.value || 0 }} + </div> + </div> + <div + v-else-if="player" + class="flex flex-col items-center justify-center h-full bg-gradient-to-b from-gray-800/50 to-gray-900/80 p-3" + > + <div class="text-green-400 font-semibold text-lg mb-2"> + {{ player.username }} + </div> + <div class="flex gap-3 text-sm"> + <div class="flex flex-col items-center"> + <span class="text-red-400">HP</span> + <span class="text-gray-300">{{ player.health }}</span> + </div> + <div class="flex flex-col items-center"> + <span class="text-yellow-400">PTS</span> + <span class="text-gray-300">{{ player.points }}</span> + </div> + </div> + </div> + <div + v-else + class="flex flex-col items-center justify-center h-full bg-gradient-to-b from-gray-800/50 to-gray-900/80 p-3" + > + <div class="text-green-400 font-semibold mb-1">{{ sectionTitle }}</div> + <div class="text-gray-400 text-sm">Empty</div> + </div> + </div> + + <!-- Regular Card (Board/Inventory) --> + <div v-else-if="card" class="flex flex-col h-full"> + <div + class="flex flex-col h-full relative" + :style="{ + backgroundImage: `url(${card.image})`, + backgroundSize: 'cover', + backgroundPosition: 'center', + }" + > + <div + class="relative text-center bg-black/50 z-10 flex flex-col h-full p-2" + > + <div class="mb-2 text-green-400 font-semibold"> + {{ card.name }} + </div> + <div class="text-gray-200 text-xs"> + {{ card.type }} + </div> + <div class="text-center text-white mt-auto"> + {{ card.value }} + </div> + </div> + </div> + </div> + + <!-- Empty Card --> + <div v-else class="flex items-center justify-center h-full"> + <div class="text-gray-600">Empty</div> + </div> + </div> +</template> diff --git a/client/game/config.js b/client/game/config.js index c3386f80f4e12b775c3827fcd9b8e4c924f53c73..4c4155aa953964957fd9f38716758ee9d284c200 100644 --- a/client/game/config.js +++ b/client/game/config.js @@ -1,5 +1,3 @@ -const Card = require("./card"); - const GameState = { LOBBY: "LOBBY", PLAYING: "PLAYING", @@ -42,26 +40,26 @@ const CardDefinitions = { { name: "Spider", value: 2 }, { name: "Goblin", value: 3 }, { name: "Skeleton", value: 4 }, - { name: "Orc", value: 5 }, + { name: "Ogre", value: 5 }, { name: "Troll", value: 6 }, - { name: "Witch", value: 7 }, - { name: "Demon", value: 8 }, + { name: "Enchanter", value: 7 }, + { name: "Ghost Prince", value: 8 }, { name: "Dragon", value: 9 }, - { name: "Ancient Beast", value: 10 }, + { name: "Princess", value: 10 }, ], weapons: [ { name: "Dagger", value: 1 }, - { name: "Short Sword", value: 3 }, + { name: "Bow and Arrow", value: 3 }, { name: "Mace", value: 5 }, - { name: "Battle Axe", value: 7 }, - { name: "Long Sword", value: 9 }, + { name: "Wand", value: 7 }, + { name: "Excalibur", value: 9 }, ], shields: [ { name: "Wooden Shield", value: 2 }, { name: "Iron Shield", value: 4 }, { name: "Steel Shield", value: 6 }, - { name: "Knight Shield", value: 8 }, - { name: "Tower Shield", value: 10 }, + { name: "Gold Shield", value: 8 }, + { name: "Diamond Shield", value: 10 }, ], potions: [ { name: "Minor Healing Potion", value: 2 }, @@ -81,40 +79,10 @@ const PlayerConfig = { PLAYER_START_HEALTH: 20, }; -function generateDeck(playerCount) { - const deck = []; - - // For each card type - Object.entries(CardDefinitions).forEach(([typeKey, cards]) => { - const type = typeKey.slice(0, -1); // remove 's' from end - const distribution = CardDistribution[typeKey]; - - // For each card in that type - cards.forEach((card) => { - // Calculate how many of this card to add based on player count - const count = - distribution.perType + distribution.scaling * (playerCount - 1); - - // Add that many cards - for (let i = 0; i < count; i++) { - deck.push( - new Card({ - type, - name: card.name, - value: card.value, - }) - ); - } - }); - }); - - return deck; -} - module.exports = { GameState, CardType, CardDefinitions, PlayerConfig, - generateDeck, + CardDistribution, }; diff --git a/client/game/game.js b/client/game/game.js index ca6ca1521ce0b967ce2cf3f15faad48c46bfd243..a83846a693ac091a015f36c39a840106d6bb5ef9 100644 --- a/client/game/game.js +++ b/client/game/game.js @@ -1,18 +1,49 @@ -const { PlayerConfig, generateDeck } = require("./config"); -const Card = require("./card"); +const { PlayerConfig, CardDefinitions, CardDistribution } = require("./config"); const Player = require("./player"); - +const Card = require("./card"); +const { default: axios } = require("axios"); class Game { - constructor(playerNames) { - this.isActive = true; + constructor(playerNames, artStyle) { + this.artStyle = artStyle; this.currentTurn = 0; - this.sharedDeck = this.generateSharedDeck(playerNames.length); this.players = playerNames.map((name) => new Player(name, this)); this.shop = []; + this.sharedDeck = this.generateSharedDeck(); + } + + generateDeck(playerCount) { + const deck = []; + + // For each card type + Object.entries(CardDefinitions).forEach(([typeKey, cards]) => { + const type = typeKey.slice(0, -1); // remove 's' from end + const distribution = CardDistribution[typeKey]; + + // For each card in that type + cards.forEach((card) => { + // Calculate how many of this card to add based on player count + const count = + distribution.perType + distribution.scaling * (playerCount - 1); + + // Add that many cards + for (let i = 0; i < count; i++) { + deck.push( + new Card({ + type, + name: card.name, + value: card.value, + }) + ); + } + }); + }); + + return deck; } - generateSharedDeck(playerCount) { - const deck = generateDeck(playerCount); + generateSharedDeck() { + console.log(this); + const deck = this.generateDeck(this.players.length); //shuffle deck for (let i = deck.length - 1; i > 0; i--) { const j = Math.floor(Math.random() * (i + 1)); @@ -39,7 +70,6 @@ class Game { for (let i = 0; i < neededCards; i++) { if (this.sharedDeck.length === 0) { - this.endGame(); return false; } const card = this.sharedDeck.pop(); @@ -51,11 +81,6 @@ class Game { this.players.forEach((player) => this.dealCards(player.username)); } - endGame() { - this.isActive = false; - console.log("Game over!"); - } - displayShop() { console.log("\n=== Shop Items ==="); if (this.shop.length === 0) { @@ -66,6 +91,50 @@ class Game { console.log(`${card.displayCard()} - Value: ${card.value}`); }); } + + async generateCard(cardName, artStyle) { + const apiUrl = + "https://quiplash-fa-ab30g22.azurewebsites.net/api/card/generate?code=QhinlEeb4ER23RBd6PFmOvp6oNC7IBTlP-N587_wxHJGAzFuYr-Xhw%3D%3D"; + const fallbackImage = "https://placehold.co/400x600?text=Card+Not+Found"; + + try { + const response = await axios.post(apiUrl, { + art_style: artStyle, + card_name: cardName.toLowerCase(), + }); + console.log(response.data); + return response.data?.url || fallbackImage; + } catch (error) { + console.error( + "Error generating card:", + error.message, + error.response.data + ); + return fallbackImage; + } + } + + async generateImages() { + const allCards = Object.entries(CardDefinitions) + .map(([, cards]) => cards.map((card) => card.name)) + .flat(); + + //simulate generating images + // await new Promise((resolve) => setTimeout(resolve, 10000)); + // Generate all images concurrently + await Promise.all( + allCards.map(async (cardName) => { + const image = await this.generateCard(cardName, this.artStyle); + console.log(`Generated image for ${cardName}`); + this.sharedDeck.map((card) => { + if (card.name === cardName) { + card.image = image; + } + }); + }) + ); + console.log(this.sharedDeck); + } } module.exports = Game; diff --git a/client/server.js b/client/server.js index 0198eb59770f32c6f07894036d71ce178430f57b..8f5b0ecbe094968c3d71e5359a3f48a116bad342 100644 --- a/client/server.js +++ b/client/server.js @@ -201,22 +201,29 @@ io.on("connection", (socket) => { }); // Start game - socket.on("startGame", ({ roomId }) => { + socket.on("startGame", async ({ roomId, artStyle }) => { const room = rooms.get(roomId); room.gameState = GameState.PLAYING; const playerNames = Array.from(room.users.values()).map( (user) => user.username ); - room.gameInstance = new Game(playerNames); - room.gameInstance.dealCardsToAllPlayers(); + room.gameInstance = new Game(playerNames, artStyle); + try { + // Generate images for all cards + await room.gameInstance.generateImages(); - updateGame(roomId); - console.log(`Game started in room ${roomId}`); - }); + // Deal cards to all players + room.gameInstance.dealCardsToAllPlayers(); - socket.on("generateCard", async () => { - const card = await generateCard(); - io.emit("cardGenerated", { card }); + // Update game state after images are generated + updateGame(roomId); + console.log(`Game started in room ${roomId} with images generated`); + } catch (error) { + console.error("Error generating card images:", error); + // Handle error appropriately + room.gameState = GameState.LOBBY; + io.to(roomId).emit("error", "Failed to generate card images"); + } }); // Handle player actions @@ -274,13 +281,24 @@ io.on("connection", (socket) => { if (actionSuccess) { // Check if turn should end - const shouldEndTurn = room.gameInstance.players.every( - (p) => p.board.length === 0 - ); + const alivePlayers = room.gameInstance.players.filter((p) => p.isAlive); + const shouldEndTurn = alivePlayers.every((p) => p.board.length === 0); if (shouldEndTurn) { // Deal new cards to all players - room.gameInstance.dealCardsToAllPlayers(); + if (room.gameInstance.sharedDeck.length === 0) { + const highestPlayer = alivePlayers.reduce((max, p) => + p.points > max.points ? p : max + ); + // Make all players dead apart from the one with highest points + room.gameInstance.players.forEach((player) => { + if (player.username !== highestPlayer.username) { + player.isAlive = false; + } + }); + } else { + room.gameInstance.dealCardsToAllPlayers(); + } // Increment turn room.gameInstance.currentTurn++; } @@ -321,7 +339,6 @@ const updateGame = (roomId) => { const gameData = room.gameInstance ? { - isActive: room.gameInstance.isActive, currentTurn: room.gameInstance.currentTurn, players: room.gameInstance.players.map((player) => ({ username: player.username, @@ -353,19 +370,3 @@ const updateGame = (roomId) => { gameInstance: gameData, }); }; - -const generateCard = async () => { - const apiUrl = - "https://quiplash-fa-ab30g22.azurewebsites.net/api/card/generate?code=QhinlEeb4ER23RBd6PFmOvp6oNC7IBTlP-N587_wxHJGAzFuYr-Xhw%3D%3D"; - - try { - const response = await axios.post(apiUrl, { - art_style: "pixel", - card_name: "princess", - }); - return response.data; - } catch (error) { - console.error("Error generating card:", error); - return null; - } -};