const gameHelper = require('./gameHelper'); // For anything related to the actual game itself (kinda) // this will be split into different bits, but should be what manages a rooms // game states, and alladat // Basically here to prevent circular dependencies (where I can) // PlayerId is using array 0,1,2 for now, not the actual id // actual Id would be better, but the player should be passed correctly // from the client. They can edit data, but server-side validation SHOULD prevent // in the future function gameStart(roomId){ // Each player shuffles for(const player of roomData[roomId].playerData){ // TODO: Make sure this only does for players, not spectators (in fut.) shuffleDeck(roomId, player.playerDataId); } // Each player plays X shield for(const player of roomData[roomId].playerData){ // TODO: Make sure this only does for players, not spectators (in fut.) // If shieldCount is less than the 'most' shield at start of game for(let shieldCount = 0; shieldCount < 4; shieldCount++){ playShield(roomId, player.playerDataId); } } // Each player draws X cards to hand for(const player of roomData[roomId].playerData){ // TODO: Make sure this only does for players, not spectators (in fut.) // for(let handCount = 0; handCount < 1; handCount++){ drawACard(roomId, player.playerDataId); } } } function passTurn(roomId, playerId){ // TODO:Check playerId and roomId before doing the stuff, to verify the user // IS the user, and in the room let playerTurn = global.roomData[roomId].itemData.component.playerTurn; //global.socketAlert(roomData[roomId].playerData[playerId].socketId, playerTurn, 'log'); if(playerTurn != playerId){ global.socketAlert(global.getPlayerSocketFromRoom(playerId, roomId), 'Not your turn', 'alert'); return false; }; // Turns are 0,1,0,1 at the mo, no coinflip or re-order, etc. so this JANK is ok for now // %2 as 2 players and Ids are 0 and 1 so it works let newPlayerTurn = (playerTurn + 1)%maxPlayersPerRoom; global.roomData[roomId].itemData.component.playerTurn = newPlayerTurn; // If it's back to the player that went first, the turn count increased too if(playerTurn == 0){ global.roomData[roomId].itemData.component.turn++; } // Send turn data to each player global.socketResponsePassTurn(roomId); // Let the player know it's their turn via alert too (in case tabbed out) // TODO: This could probably be done front-end from the newPlayerTurn in socketResponsePassTurn global.socketAlert(roomData[roomId].playerData[newPlayerTurn].socketId, 'Your turn', 'alert'); // Start of the new players turn, draw a card drawACard(roomId, newPlayerTurn); } function playShield(roomId, playerId){ if(global.roomData[roomId].itemData.component.shield[playerId] >= 2){ global.socketAlert(roomData[roomId].playerData[playerId].socketId, 'Shield full; cannot play shield', 'alert'); return false; } // Change position to last position available in shield zone let fromPosition = global.roomData[roomId].itemData.component.cardCount.deck[playerId]; // 'top' of deck let toPosition = global.roomData[roomId].itemData.component.cardCount.shield[playerId]+1; // newest shield pos. // TODO: This is essential the same as in drawACard() so should be normalised into a function // Get each card from the deck for (const [key, value] of Object.entries(global.roomData[roomId].itemData.component.inDeck)) { // Key is the entity here // If the card inDeck does not belongs to the player, skip over it if(global.roomData[roomId].itemData.component.player[key] != playerId){ continue; } // If the card isn't the last (bottom) card of deck, skip over it // TODO: -1 is jank, sort so listPositions all start from 1..x if(global.roomData[roomId].itemData.component.listPosition[key] != fromPosition){ continue; } // The main man // Move positions in hand/deck, and put the item from the deck into the shield gameHelper.setCardPosition(roomId, playerId, key, toPosition, global.roomData[roomId].itemData.component.shield, fromPosition, global.roomData[roomId].itemData.component.inDeck); } // Reduce deckSize by 1 for the player that drew global.roomData[roomId].itemData.component.cardCount.deck[playerId]--; // And increase the shield size by 1 global.roomData[roomId].itemData.component.cardCount.shield[playerId]++; // Then emit the deckSize and hand size to all the player's sockets global.socketResponsePlayedShield(roomId, playerId); } function drawACard(roomId, playerId){ if(global.roomData[roomId].itemData.component.cardCount.hand[playerId] >= 2){ global.socketAlert(roomData[roomId].playerData[playerId].socketId, 'Hand full; cannot draw card', 'alert'); return false; } if(global.roomData[roomId].itemData.component.cardCount.deck[playerId] <= 0){ global.socketAlert(roomData[roomId].playerData[playerId].socketId, 'Deck empty; cannot draw card', 'alert'); return false; } // TODO: Check no card event/trigger occured that prevents/change draw card // Change position to last position available in hand let fromPosition = global.roomData[roomId].itemData.component.cardCount.deck[playerId]; // 'top' of deck let toPosition = global.roomData[roomId].itemData.component.cardCount.hand[playerId] + 1; // Rightmost hand pos (starting at 1) // ECSey att2, there's surely a better way of getting playerX top card within inDeck? // Tried unions but it messes up the object data. Maybe need to have no data in each object // and have it literally just be keys? // Get each card from the deck for (const [key, value] of Object.entries(global.roomData[roomId].itemData.component.inDeck)) { // Key is the entity here // If the card inDeck does not belongs to the player, skip over it if(global.roomData[roomId].itemData.component.player[key] != playerId){ continue; } // If the card isn't the last (bottom) card of deck, skip over it // TODO: -1 is jank, sort so listPositions all start from 1..x if(global.roomData[roomId].itemData.component.listPosition[key] != fromPosition){ continue; } // The main man // Move positions in hand/deck, and put the item from the deck into the hand gameHelper.setCardPosition(roomId, playerId, key, toPosition, global.roomData[roomId].itemData.component.hand, fromPosition, global.roomData[roomId].itemData.component.inDeck); } // Reduce deckSize by 1 for the player that drew global.roomData[roomId].itemData.component.cardCount.deck[playerId]--; // And increase the hand size by 1 global.roomData[roomId].itemData.component.cardCount.hand[playerId]++; // Then emit the deckSize and hand size to all the player's sockets global.socketResponseDrawCard(roomId, playerId); // Emit the 'hand' and related cardData for cards in the players hand // Could merge this with the top? global.socketResponsePlayerDrewCard(roomId, playerId); } function playManaFromHand(roomId, playerId, position){ playFromHand(roomId, playerId, position, true); } // TODO: Rename and rejig the 3 play from hand functions function playFromHand(roomId, playerId, position, mana = false){ let cardId = null; // Get the cardId of the card from position within players hand for (const [key, value] of Object.entries(global.roomData[roomId].itemData.component.hand)) { // Key is the entity here // If the card in hand's position does not match the position passed, skip it if(global.roomData[roomId].itemData.component.listPosition[key] != position){ continue; } cardId = key; break; } // Then play the card if(cardId == null){ // TODO: Respond to player (who triggered play) that this // is an 'illegal/errored' move via socket return false; } // Attempt to play the card from hand if(!playACardFromHand(roomId, playerId, cardId, mana)){ return false; } } function playACardFromHand(roomId, playerId, cardId, mana = false){ // Add the card to field (and its effect to 'stack') or spell to the 'stack' if(playACard(roomId, playerId, cardId, 'hand', mana) !== true){ // TODO: Return socket to player about 'illegal move' and why return false; } if(mana){ global.socketResponsePlayManaFromHand(roomId, playerId, cardId); return true; } // TODO: Maybe update with 'location played to' so animations, draws, etc. are correct global.socketResponsePlayFromHand(roomId, playerId, cardId); // TODO: Above can probably be the same/similar to what is added to the roomData on // client-end (when it's fully done) return true; } // 'Play' a card is activation with cost (triggering play events) // 'Summon' puts it onto the board without play events // yada yada to think about in future function playACard(roomId, playerId, cardId, playedFrom, mana = false){ // Play a mana (a card that was played as mana, loses it's normal card data) if(mana){ // Play to board. If there's a 'onPlay' effect, add that to the 'stack' return playMana(roomId, playerId, cardId, playedFrom); } // Play a unit if(cardId in global.roomData[roomId].itemData.component.type.unit){ // Play to board. If there's a 'onPlay' effect, add that to the 'stack' return playAUnit(roomId, playerId, cardId, playedFrom); } // Cast a spell if(cardId in global.roomData[roomId].itemData.component.type.spell){ // Add the card/effect onto the 'stack'. When complete/cancelled send to grave return playASpell(roomId, playerId, cardId, playedFrom); } // Add a token if(cardId in global.roomData[roomId].itemData.component.type.token){ // ???? Tokens maybe just onto other cards as 'equips' or as standalones? IDK return playAToken(roomId, playerId, cardId, playedFrom); } } function hasSpaceOnBoard(roomId, playerId){ // TODO: return true; } function removeFromHand(roomId, playerId, cardId){ console.log('remove from hand'); if(cardId in global.roomData[roomId].itemData.component.hand){ delete(global.roomData[roomId].itemData.component.hand[cardId]); } global.roomData[roomId].itemData.component.cardCount.hand[playerId]--; } function playAUnit(roomId, playerId, cardId, playedFrom){ console.log('playAUnit'); // TODO: Make work, AND allow to play to opponent board (in future) if(hasSpaceOnBoard(roomId, playerId) !== true){ return false } // TODO: Costs if(playedFrom == 'hand'){ // Remove from hand removeFromHand(roomId, playerId, cardId); // Add unit to board global.roomData[roomId].itemData.component.board[cardId] = cardId; global.roomData[roomId].itemData.component.cardCount.board[playerId]++; // Change list positions of hand and board // Next position on board (using cardCount to determine here) // From current position in hand (the listPosition of the entity at this current point) gameHelper.setCardPosition(roomId, playerId, cardId, global.roomData[roomId].itemData.component.cardCount.board[playerId], global.roomData[roomId].itemData.component.board, global.roomData[roomId].itemData.component.listPosition[cardId], global.roomData[roomId].itemData.component.hand); // TODO: unit onPlay effects to the stack } // Add to board return true; } function playASpell(roomId, playerId, cardId, playedFrom){ console.log('playASpell'); // TODO: Pay costs (Need to play mana first...) // TODO: If spell has different effects, select which one/ensure // correct one is used based on criteria if(playedFrom == 'hand'){ // Remove from hand console.log(cardId); console.log(global.roomData[roomId].itemData.component.hand); // Remove from ahnd removeFromHand(roomId, playerId, cardId); // if Spell when it's final effect finished (or removed) from stack, it should get // added to grave then } // If card was played, add its effect(s) to the stack // If it should be added to stack (spell, or onPlay effect) do so // Add to stack (each part of the spells effect) // TODO: Use actual effects, for now just adding a 'drawCard' for testing addToStack(roomId, playerId, cardId, null); // Spell can/'has' been played return true; } function playAToken(roomId, playerId, cardId, playedFrom){ return false; return true; } function playMana(roomId, playerId, cardId, playedFrom){ console.log('playMana'); // TODO: Check if mana can be played (default 10 max) // TODO: Check if a mana has already been played this turn (default 1 per turn per player) if(playedFrom == 'hand'){ // Remove from hand removeFromHand(roomId, playerId, cardId); // Add card to mana zone global.roomData[roomId].itemData.component.mana[cardId] = cardId; global.roomData[roomId].itemData.component.cardCount.mana[playerId]++; console.log(global.roomData[roomId].itemData.component.mana); console.log(global.roomData[roomId].itemData.component.cardCount.mana[playerId]); // Change list positions of hand and mana gameHelper.setCardPosition(roomId, playerId, cardId , global.roomData[roomId].itemData.component.cardCount.mana[playerId] , global.roomData[roomId].itemData.component.mana , global.roomData[roomId].itemData.component.listPosition[cardId] , global.roomData[roomId].itemData.component.hand ); // Mana has been played return true; } return false; } // Not 100% sure how to implement the stack // TODO: Make it better // TODO: Make it do actual things, currently just adds 'drawCard' effect function addToStack(roomId, playerId, cardId, effectId){ // Stack does its own effect, or 'counter' etc. the card prior to it on the stack // etc. etc. // TODO: Add card effect to stack in reverse order OR have the stack work from x..0 // prolly the latter, makes sense to me let stack = global.roomData[roomId].itemData.component.stack; let stackLength = Object.keys(stack).length; // TODO: First ensure the cardEffects are added in their step order 1..x // Add as next event in the stack 1..x // TODO: Use actual effect, not just 'draw' as that's just for testing global.roomData[roomId].itemData.component.stack[stackLength + 1] = { 'cardId': cardId ,'effect': null ,'effectStep': null ,'targetCard': null ,'targetPlayer': playerId }; console.log(global.roomData[roomId].itemData.component.stack); // Send addToStack response to trigger any animations, etc. global.socketResponseAddToStack(roomId); // TODO: TEMP, this will need to wait for a 'resolve' accept from both players before the stack // would trigger. //resolveStack(roomId); // TODO: Improve this, potentially drop out of function, and have a while stack > 0 // do the stack stuff. If it's <= 0 then other functionality is as normal // Need to write a game loop for this rather than a // nested function calling the getStackResponse. getStackResponse(roomId); } function getStackResponse(roomId){ // If there's something in the stack both/all players must accept to resolve // and/or have a chance to play a counter/chain card/effect atop of the current // top of the stack // TODO: Opponent gets chance to chain first, then player // if opponent chained, player gets to chain that chain first before stopping // opponent double chaining global.socketGetStackResponse(roomId); } function acceptResolveStack(roomId, playerId){ // TODO: Make so each player needs to accept // with whoever is to counter getting to ability to chain first // Once the player has resolved, the next player gets the option to chain/resolve // If all players have resolved, then resolve the top of the stack resolveStack(roomId); } function resolveStack(roomId){ // Resolve the stack if all players have requested // to resolve the stack, and not chain anything atop of it // Does the next effect in the stack, if something // is to chain onto the stack that would instead trigger // 'addToStack' after paying any costs // If there is anything in the stack let stackLength = Object.keys(global.roomData[roomId].itemData.component.stack).length; if(stackLength > 0){ // Send the 'resolve' response to room to trigger any animations, etc. global.socketResponseResolveStack(roomId); // Trigger the last (most recently added to) the stack effect // THIS WILL NOW ACTUALLY CAST THE EFFECT STEP WITHOUT INTERRUPT // While the stack is being resolved their are no counters/chains until // the next stack action which players will get option to chain or not again let stackTrigger = global.roomData[roomId].itemData.component.stack[stackLength]; // TODO: actually trigger the correct effect, etc. // check if targets, check validity, etc. then trigger // TODO: Remove drawACard and use actual triggers/effects drawACard(roomId, stackTrigger.targetPlayer); // Once the effect atop the stack has triggered, remove it from the stack delete(global.roomData[roomId].itemData.component.stack[stackLength]); } } // Shuffle the deck 'randomly' for a certain player function shuffleDeck(roomId, playerId){ // Create a tempDeck array of same length of the player deck let deckLength = global.roomData[roomId].itemData.component.cardCount.deck[playerId]; let tempDeck = Array.from(Array(deckLength).keys()); // Loop the tempDeck and shuffle // https://stackoverflow.com/a/73603221 for(let i = 0; i < deckLength; i++){ // picks the random number between 0 and length of the deck (-1 so 0..34) let shuffle = Math.floor(Math.random() * (tempDeck.length - 1)); // swap the current listPosition with a random one with the deck count [ tempDeck[i], tempDeck[shuffle] ] = [ tempDeck[shuffle], tempDeck[i] ]; } let tempDeckItem = 0; // Now change the related inDeck entities listPositions to match the random number from tempDeck for (const [key, value] of Object.entries(global.roomData[roomId].itemData.component.inDeck)) { if(global.roomData[roomId].itemData.component.player[key] != playerId){ continue; } // Add the tempDeckItems number as the listPosition for the inDeck item // + 1 as listPositions start at 1 global.roomData[roomId].itemData.component.listPosition[key] = tempDeck[tempDeckItem] + 1; // Move to next tempDeckItem tempDeckItem++; } global.socketResponseShuffleDeck(roomId, playerId, true); } function tapCard(roomId, playerId, cardId){ global.roomData[roomId].itemData.component.cardStatus.tapped[cardId] = cardId; global.socketResponseTapped(roomId, cardId); } function untapCard(roomId, playerId, cardId){ delete(global.roomData[roomId].itemData.component.cardStatus.tapped[cardId]); global.socketResponseUntapped(roomId, cardId); } function startAttack(roomId, playerId, cardId){ console.log('start attack'); if(!gameHelper.canAttack){ return false; } // Set the card to 'attacking' global.roomData[roomId].itemData.component.cardStatus.attacking[cardId] = cardId; // TODO: Maybe set targetable as component and return that global.roomData[roomId].itemData.component.cardStatus.targetable = gameHelper.getTargetableCards(roomId, playerId, cardId); // Return the available targets (give them a border in UI) global.socketResponseUpdateTargetable(roomId, playerId, global.roomData[roomId].itemData.component.cardStatus.targetable ); } // DATA RETURNER DUDES // TODO: Where to put this? Kind of a helper, kind of functionality. Hmmmmm // maybe do a dataHelper? then anything to return data can be included there? // TODO: May get all the data from hand, board, grave, etc. in functions // like this, then union all the data and return that in one swoomp // Probably better to just get all the keys from the boardlemenets and do // the loop once though... function getPlayerHandData(roomId, playerId){ let handEntities = {}; let handPositions = {}; let handCardData = {}; for (const [key, value] of Object.entries(global.roomData[roomId].itemData.component.hand)) { // Key is entity ID here // If the entity in hand belongs to the player, then they are allowed its data if(global.roomData[roomId].itemData.component.player[key] == playerId){ // Get entity of items in the hand handEntities[key] = global.roomData[roomId].itemData.component.hand[key]; // Get listPosition of just items in the hand handPositions[key] = global.roomData[roomId].itemData.component.listPosition[key]; // Same for cardData handCardData[key] = global.roomData[roomId].itemData.component.cardData[key]; // TODO: Nothing on client side? // Leaving other bits for now } } // TODO: This is here to prevent overwriting with less content when a draw happens. // Will want reverting at some point (or other functions for returning only certain bits // everywhere else should be written) //handEntities = global.roomData[roomId].itemData.component.hand; handPositions = global.roomData[roomId].itemData.component.listPosition; handCardData = global.roomData[roomId].itemData.component.cardData; return { 'handEntities': handEntities, 'handPositions': handPositions, 'handCardData': handCardData }; } module.exports = { passTurn ,getPlayerHandData ,drawACard ,shuffleDeck ,playFromHand ,playManaFromHand ,acceptResolveStack ,gameStart ,startAttack // TEMP ,tapCard ,untapCard };