const ctx = canvas.getContext('2d'); const canvasLeft = canvas.offsetLeft + canvas.clientLeft; const canvasTop = canvas.offsetTop + canvas.clientTop; const cardWidth = 80; const cardHeight = 120; const cardArt = new Image(); const cardBackArt = new Image(); // Colours const COLOUR = { 'white':{'id': 1, 'name':'White','colour':'#EEE'}, 'blue':{'id':2, 'name':'Blue','colour':'#0033EE'}, 'red':{'id':3, 'name':'Red','colour':'#ED344A'}, }; // Counters to keep track of players, and boardElements, may be changed in future // But once game starts, will be const anyway, so shouldn't need passing let players = 2; // Player, Opponent for now, but will be up to 6 players for 5v1 boss raids? let elements = ['realDeck', 'deck','board','hand','mana','shield', 'grave']; let elementsSizes = {}; // TODO: May need to have the base XY WH of board, hand, etc. stored for loop draw // Array of items, the 'Entity Manager' as such let item = []; let itemCount = 0; // Component 1: Where on the board, hand, deck, mana, shield, etc. // send back item, itemCount and individual comp. from server? let boardElement = {}; let cardData = {}; let position = {}; let size = {}; //let shape = {}; let cardStatus = {}; // tapped, attacking, inspected, untargettable (TODO:maybe used this instead of inEvent later) let player = {}; let listPosition = {}; let cardFace = {}; let cardSprite = {}; let deckIn = {}; // NEW, may be used, for now player substitute let deckData = {}; let cardAttack = {}; // TODO: add to the logic let cardColours = {}; let cardManaColour = {}; let inEvent = null; // To disable drawing each time something changes let drawEachEvent = true; // For disabling draw each time and only occuring where I want to test let yourPlayerId = 0; // To compare click events of your/opponents cards let viewingPlayerId = 0; // To show the board from your/opponent/teammates perspective, etc. without play permission const maxHandSize = 4; const maxBoardSize = 3; const maxShield = 2; // Gonna need lots of refactoring, and sorting class Board{ constructor(){ console.log('initBoard'); ctx.font = "12px Arial"; canvas.style.backgroundColor = 'rgb(143 153 150)'; cardArt.src = 'images/cardArt.jpg'; cardBackArt.src = 'images/cardBack.jpg'; ctx.fillStyle = '#000'; } drawBoard(force = false){ if(drawEachEvent == false && force == false){ return 0; } // Reset board ctx.clearRect(0, 0, canvas.width, canvas.height); // Room Name ctx.fillText(name, 0, 10); this.drawPlayerNames('Nathan', 'Evil Nathan'); this.drawItems(); // Atop most everything atm for testing if(this.checkGameWin() == true){ this.drawWin(); } } checkGameWin(){ // If opponent shield is 0 then you win TODO: Otherwincons, check w/l for each player if(this.remainingShieldCount(1) <= 0){return true;} return false; } drawWin(){ var winBoard = new Shape({ name: name, x: 0, y: canvas.height/2 - (canvas.height/4)/2, width: canvas.width, height: canvas.height/4, fillStyle: '#CCC', strokeStyle: '#00EEAA' }); winBoard.draw(); ctx.fillStyle = '#000'; ctx.font = "bold 50pt Arial"; ctx.fillText('WINNER', 200, 300); ctx.font = "10pt Arial"; } drawPlayerNames(playerName, opponentName = false){ // Player Name ctx.fillText(playerName, 50, canvas.height - 50); // Opponent's Name if(opponentName){ ctx.fillText(opponentName, canvas.width - (ctx.measureText(opponentName).width + 50), 50); } } drawItems(){ // Loop all items for(let itemKey = 0; itemKey < item.length; itemKey++){ // Loop each element to draw (each have distinct locations, and draw logic) for(let elementCount in elements){ // Don't draw deck TODO:/gy/void // TODO: Unless inspecting let element = elements[elementCount]; if(element == 'deck' || element == 'grave'){ continue; } // Draw Elements // Loop each item remaining, draw them if(itemKey in boardElement && boardElement[itemKey] == element){ // Get the player the item belongs to let itemPlayer = player[itemKey]; //console.log('Element: '+element+', Player: '+itemPlayer); calculateItemSizePosition(itemKey); this.printCardToCanvas(itemKey); } } } } setCardFill(itemKey){ let fill = '#B0B0B0'; let coloursOfCard = cardColours[itemKey]; if(coloursOfCard.length > 1){ // Create a gradient for the card colours // x-start,y-start,x-end,y-end const grad=ctx.createLinearGradient( position[itemKey][0], 0, //position[itemKey][1], position[itemKey][0] + size[itemKey][0], 0, //position[itemKey][1] + size[itemKey][1] ); for(let i = 0; i < coloursOfCard.length; i++){ let gradientPos = 0; if(coloursOfCard.length == i){ gradientPos = 1; }else{ // Colour stops from 0..1 (0 to 100% along) // If 4, first is 0, second is 100/3 * 1 = 33.33% // 0,33,66,100. Need last to always be 100 (1) gradientPos = (coloursOfCard.length - 1)*i; } // TODO: Make new array ensuring colours are ordered by highest // cost. This will do as it adds the colours, but in future grad.addColorStop(gradientPos, this.setFillByManaColour(cardColours[itemKey][i][0])); } fill = grad; }else{ // Set to a normal fill of the first (only) colour fill = this.setFillByManaColour(cardColours[itemKey][0][0]); } return fill; } printCardToCanvas(itemKey){ // If the itemKey has cardData, position, size, and boardElement we can draw it // TODO: Add a check for error handling // Check status, and change border colour for display (for tapped atm) let border = null; if(this.isTapped(itemKey)){border = '#E0BC00';} if(this.isAttacking(itemKey)){border = '#C92D22';} let name = itemKey; // Not needed really anymore, but keeping for now let positionX = position[itemKey][0]; let positionY = position[itemKey][1]; let width = size[itemKey][0]; let height = size[itemKey][1]; // Set the card 'cardboard' colour based on the card colour type let fill = '#B0B0B0'; if(boardElement[itemKey] != 'realDeck'){ // TODO: Change these to isset checks instead... fill = this.setCardFill(itemKey); } // Draw the card shape itself let shape = new Shape({ name: name, x: positionX, y: positionY, width: width, height: height, fillStyle: fill, strokeStyle: border }); shape.draw(); // Draw the card face-up if(this.isFaceUp(itemKey)){ this.addCardImage(itemKey); this.printCardDetails(itemKey); this.printColourRequirements(itemKey); } if(!this.isFaceUp(itemKey)){ this.addCardBack(itemKey); } // If it's the deck, draw the circle surrounding it if(boardElement[itemKey] == 'realDeck'){ // TODO: For realDeck only atm, also janked in. Seperate this... let counterx= positionX; let countery= positionY; let counterwidth= cardWidth*.375; let counterheight= cardHeight*.375; // TODO: Center in the circle let deckLength = getCurrentPositionAndLength('deck', player[itemKey])[1]; let textx=positionX - (ctx.measureText(deckLength).width/2); let texty=positionY + (ctx.measureText(deckLength).width/2); // Draw circle for deck count to sit in let deckCounterSprite = new Shape({ shape: 'circle', name: 'deckCountererer', x: counterx, y: countery, width: counterwidth, height: counterheight, fillStyle: '#DCDCDC' }); deckCounterSprite.draw(); // Draw deck count text ctx.fillStyle = '#000'; ctx.fillText(deckLength, textx, texty); } // If the item is a mana, draw the mana colour within it // Temp solution, but works for UI and testing if(boardElement[itemKey] == 'mana'){ let manaFill = this.setFillByManaColour(cardManaColour[itemKey][0]); // TODO: Seperate this into a 'drawMana' or something? let manaColour = new Shape({ shape: 'circle', //name: 'deckCountererer', x: positionX + width/2, y: positionY + height/2, width: width*.75, height: height*.75, fillStyle: manaFill // Fill should be the card's main colour }); manaColour.draw(); } } isFaceUp(itemKey){ if(cardFace[itemKey] == 1){ return true; } return false; } flipCard(itemKey, direction = null){ // TODO: Add a third param for 'triggerEffects', for instance // flipping up from deck to hand, or shield to hand shouldn't trigger // MOST TIMES if(cardFace[itemKey] == 1 && (direction == null || direction == 0)){ cardFace[itemKey] = 0; return true; } if(cardFace[itemKey] == 0 && (direction == null || direction == 1)){ cardFace[itemKey] = 1; return true; } // TODO:Activate any flip effects, etc. } printCardImage(itemKey){ let positionX = position[itemKey][0]; let positionY = position[itemKey][1]; let width = size[itemKey][0]; let height = size[itemKey][1]; // Draw the image into the clipping mask // image, dx,dy,dw,dh // image, sx,sy, sw,sh,dx,dy,dw,dh let spriteSheetX = 80*cardSprite[itemKey][0]; let spriteSheetY = 120*cardSprite[itemKey][1]; ctx.drawImage(cardArt, spriteSheetX,spriteSheetY, 80,120,positionX,positionY,width,height); } addCardImage(itemKey){ let positionX = position[itemKey][0]; let positionY = position[itemKey][1]; let width = size[itemKey][0]; let height = size[itemKey][1]; // Create the clipping shape let cardImageContainer = new Shape({ shape: 'unit', name: 'cardImageContainer_'+name, x: positionX+height/3, y: positionY+width/2, width: width*.9, height: height*.9 }); // Save canvas drawing, start the clip cardImageContainer.startClip(); // Print the image to canvas, within the clipping mask this.printCardImage(itemKey); // Restore the canvas draw post clip applied, to get everything else back too cardImageContainer.endClip(); } addCardBack(itemKey){ let positionX = position[itemKey][0]; let positionY = position[itemKey][1]; let width = size[itemKey][0]; let height = size[itemKey][1]; // Print the sleeve image to cardPosition this.printCardBack(itemKey); } printCardBack(itemKey){ let positionX = position[itemKey][0]; let positionY = position[itemKey][1]; let width = size[itemKey][0]; let height = size[itemKey][1]; // TODO: CardBack/Sleeves as spritesheet like cardArt ctx.drawImage(cardBackArt, 0,0, 80,120,positionX,positionY,width,height); } printColourRequirements(itemKey){ // Set the size(s) let width = size[itemKey][0]*.1; let height = size[itemKey][1]*.1; let positionX = position[itemKey][0] + (size[itemKey][0]*.1); let positionY = position[itemKey][1] + (size[itemKey][0]*.2); // Get all the colour reqs needed. let colourReqs = cardColours[itemKey]; // Loop them all let totalColours = 1; // How many colours the card has, for positioning colourReqs.forEach((colour) => { // Add each to the card (colour BG, and required count within it) // TODO: Maybe building a class for each draw is bad, eh. // TODO: Should probably change the shapes.js class into functions // Will look into once animations, etc are in, incase classes are the way // TODO: change colours to have id(done), name, and hexcode as const/enum let colourId = colour[0]; let fill = this.setFillByManaColour(colourId); // Draw the mana shape/icon let manaColour = new Shape({ shape: 'circle', //name: 'deckCountererer', x: positionX, y: positionY + height*totalColours, width: width, height: height, fillStyle: fill // Fill should be the card's main colour }); manaColour.draw(); // Draw the requirement within the shape/icon // Make sure the text scales with card size let fontSize = (width*10/cardWidth)*10; // 10 = baseFontSize of 10pt // ^ calced with the width of the card overall context.font = fontSize+"pt Arial"; // Get a constrating B/W to the background and set text to that context.fillStyle = invertColour(fill, true); // Center the text (cost) within the shape this.printCenterText(colour[1], positionX, positionY + height*totalColours); // Reset context stylings context.font = "10pt Arial"; // Reset to default context.fillStyle = "#000"; // Inc totalColours in case there's more totalColours++; }); } setFillByManaColour(colourId){ for (const colour in COLOUR) { if(colourId == COLOUR[colour]['id']){ return COLOUR[colour]['colour']; // Lmao } } return '#B0B0B0'; } printCenterText(text, positionX, positionY){ context.textAlign = 'center'; context.textBaseline = 'middle'; context.fillText(text, positionX, positionY); // Now reset the context aligns to the defaults context.textAlign = 'left'; context.textBaseline = 'alphabetic'; } printCardDetails(itemKey){ let name = itemKey; // Not needed really anymore, but keeping for now let positionX = position[itemKey][0]; let positionY = position[itemKey][1]; let width = size[itemKey][0]; let height = size[itemKey][1]; // Add card name let fontSize = width/cardWidth*10; // 10 = baseFontSize of 10pt ctx.font = "bold "+fontSize+"pt Arial"; ctx.fillStyle = '#000'; ctx.fillText( cardData[itemKey]['name'] , positionX + (ctx.measureText(cardData[itemKey]['name']/2).width) - width/4 , positionY+height*.25 ); // Add card type ctx.fillText( cardData[itemKey]['type'] , positionX + (ctx.measureText(cardData[itemKey]['type']/2).width) - width/4 , positionY+height*.7 ); // Add text/effect area if(cardData[itemKey]['effect'] !== null){ ctx.fillText( cardData[itemKey]['effect'] , positionX + (ctx.measureText(cardData[itemKey]['effect']/2).width) - width/4 , positionY+height*.8 ); } // Attack ctx.fillText( cardData[itemKey]['atk'] , positionX + (ctx.measureText(cardData[itemKey]['atk']).width) , positionY+height*.95 ); // Add cost ctx.fillText( cardData[itemKey]['cost'] , positionX + (ctx.measureText(cardData[itemKey]['cost']).width) , positionY+height*.1 ); // Unbold font for other draws ctx.font = "10pt Arial"; } // boardElement, cardData?, position?, size?, cardStatus, player, listPosition getItems(boardElementId = null, playerId = null, cardStatusId = null, listPositionId = null){ // Loop each item, building a new (smaller) loop each time an ECSey element // matches what was passed. This is the core loop for practically every function. let newItems = []; let tempArray = []; // Set to all items in itemArray to start newItems = item; // Check if each item shares the PLAYERID passed if(playerId !== null){ for(let newItem = 0; newItem < newItems.length; newItem++){ // newItems[newItem] gets the original itemKey (needed), newItem would be // the the array index, which would be wrong in most cases in this function. let itemKey = newItems[newItem]; // If the item shares the playerId, add it to the tempArray if(playerId == player[itemKey]){ tempArray.push(newItems[newItem]); } } // Set newItems to tempArray so it can be looped again with only what matched newItems = tempArray; } // Reset tempArray so it can be reused tempArray = []; // Next check if each remaining item shares the CARDSTATUS passed if(cardStatusId !== null){ for(let newItem = 0; newItem < newItems.length; newItem++){ let itemKey = newItems[newItem]; // If the item shares the cardStatusId, add it to the tempArray if(cardStatusId == cardStatus[itemKey]){ tempArray.push(newItems[newItem]); } } // Set newItems to tempArray so it can be looped again with only what matched newItems = tempArray; } // Reset tempArray so it can be reused tempArray = []; // Next check if each remaining item shares the LISTPOSITION passed if(listPositionId !== null){ for(let newItem = 0; newItem < newItems.length; newItem++){ let itemKey = newItems[newItem]; // If the item shares the cardStatusId, add it to the tempArray if(listPositionId == listPosition[itemKey]){ tempArray.push(newItems[newItem]); } } // Set newItems to tempArray so it can be looped again with only what matched newItems = tempArray; } // Reset tempArray so it can be reused tempArray = []; // Next check if each remaining item shares the BOARDELEMENT passed if(boardElementId !== null){ for(let newItem = 0; newItem < newItems.length; newItem++){ let itemKey = newItems[newItem]; // If the item shares the boardElement, add it to the tempArray if(boardElementId == boardElement[itemKey]){ tempArray.push(newItems[newItem]); } } // Set newItems to tempArray so it can be looped again with only what matched newItems = tempArray; } // Reset tempArray so it can be reused tempArray = []; // Return the new specified itemList return newItems; } //TODO: Change vars to newPosition, newElement, newPlayer, oldPosition, oldElement, oldPlayer addFromBoardElement(playerFrom, fromPosition, elementFrom, elementTo, toPosition=null, playerTo=null){ // Move itemKey fromPosition in elementFrom from playerFrom // to toPosition in elementTo for playerTo // If no playerTo provided (typical behavior), pass between elements for same player if(playerTo == null){ playerTo = playerFrom; } // Check there is only 1 item that exists with the from info let items = this.getItems(elementFrom, playerFrom, null, fromPosition); if(items.length > 1){ alert('ERROR: There is more than 1 card being added'); } // The first (and only) item returned let itemKey = items[0]; // Check if there is a specific position the item needs to go to if(toPosition == null){ // If not get the next available position of the elementTo toPosition = getCurrentPositionAndLength(elementTo, playerTo)[0]+1 } //console.log('itemKey: '+itemKey+' fromPosition: '+fromPosition+' elementFrom: '+elementFrom+' elementTo: '+elementTo+' toPosition: '+toPosition+' playerFrom: '+playerFrom+' playerTo: '+playerTo); // TODO: rename function this lives in to vars accepted by setCardPosition function, more sense this.setCardPosition(itemKey, toPosition, elementTo, playerTo, fromPosition, elementFrom, playerFrom); this.removeItemStatus(itemKey); this.drawBoard(); return 1; // Got a loop that calls a loop, and checks the new values atm, so this keeps counting down } // This took me a while to think through. It makes sense, don't change. // Get a pen and paper and just work through an array of 0..5 moving some number about setCardPosition(card, newPosition, newElement, newPlayer, oldPosition, oldElement, oldPlayer){ // Move anything in the old boardElement after the old listPosition down by one // Coming from position 3, moves existing 4..10 down one to 3..9 // Moving from pos 5 = 6,7,8 go down to 5,6,7 this.moveElementPositions(0, oldElement, oldPosition, oldPlayer); // Move anything in the new boardElement after (including) the new listPosition up by one // Going to position 3, moves existing 3..10 up one to 4..11 to make space // Moving back to pos 5 = current 5,6,7 go up to 6,7,8 this.moveElementPositions(1, newElement, newPosition, newPlayer); // Then fit the card into the new gap that's opened up listPosition[card] = newPosition; boardElement[card] = newElement; } moveElementPositions(direction, elementFrom, fromPosition, playerFrom){ // Loop the elementFrom, and move positions up/down by one let items = this.getItems(elementFrom, playerFrom, null, null); for(let item = 0; item < items.length; item++){ let itemKey = items[item]; // Move everything after the old position down // Moving from pos 5 = 6,7,8 go down to 5,6,7 if(direction == 0 && listPosition[itemKey] > fromPosition){ listPosition[itemKey]--; } // Move everything from the new position up // Moving back to pos 5 = current 5,6,7 go up to 6,7,8 if(direction == 1 && listPosition[itemKey] >= fromPosition){ listPosition[itemKey]++; } } } // Draw a card, traditional TCG drawACard(playerId, cardsToDraw = 1){ for(let draw = 0; draw < cardsToDraw; draw++){ // Check there's space in hand let elementLength = getCurrentPositionAndLength('hand', playerId)[1]; if(elementLength >= maxHandSize){ alert('Hand full '+elementLength+'/'+maxHandSize); return false; } let card = this.getItemKey('deck', 1, playerId); // Move from players deck to hand, from position 1 (bottom deck) this.addFromBoardElement(playerId, 1, 'deck', 'hand', null, null); this.flipCard(card); } } // Currently only functionality in hand playCardToBoard(positionFrom, fromElement, toElement, fromPlayer, toPlayer = null, cardsToPlay = 1){ // TODO: if from hand, use mana according to the card cost + mana types if(toPlayer === null){ toPlayer = fromPlayer; } // Loop probably not needed, but may be for eg. 'play X cards from top of deck' for(let play = 0; play < cardsToPlay; play++){ // Check there's space on the board/mana zone/shield/etc if(!this.hasSpaceInBoardElement(toElement, toPlayer)){ alert('No space in element'); return false; } let itemKey = this.getItemKey(fromElement, positionFrom, fromPlayer); console.log(itemKey); console.log(positionFrom); switch(fromElement){ case 'hand': // Mana cost required and mana tapping for playing a card from hand, etc // The player casting/summoning should pay, ofc let canPayMana = this.canPayMana(itemKey, fromPlayer); if(canPayMana !== true){ alert(canPayMana); return false; }else{ // TODO: May be best to tap in canPayMana while looping reqs. and if the reqs aren't hit untap the mana that were tapped in that call? // This atm will loop manaReq and tap. let tapped = this.tapManaRequired(itemKey, fromPlayer); alert('tapManaRequired: '+tapped); } } // Move from player0, position 0 (top) of deck, to hand, to pos(null/auto) for toPlayer this.addFromBoardElement(fromPlayer, positionFrom, fromElement, toElement, null, toPlayer); } this.drawBoard(); } tapManaRequired(itemToPayCost, playerId){ // TODO: Look at combining or normalising this and canPayMana() let manaRequired = this.getManaTotalOf(itemToPayCost); let noManaReq = {1:0, 2:0, 3:0, 4:0}; // Loop all the mana let items = this.getItems('mana', playerId, null, null); for(let item = 0; item < items.length; item++){ let mana = items[item]; if(this.isTapped(mana)){ continue; } let colourId = cardManaColour[mana][0]; console.log(JSON.stringify(manaRequired)); // Loop the requirements of the cost to pay for (const manaColour in manaRequired) { //console.log(manaColour+' '+manaRequired[manaColour]+', '+colourId); // If the colour of the mana is in the requirements // of the cost to pay, reduce the cost for that colour by // 1 and tap the mana if(manaColour == colourId && manaRequired[manaColour] > 0){ // Reduce the required mana manaRequired[colourId]--; this.tapCard(mana); } } // End loop once mana Req are fulfilled if(JSON.stringify(manaRequired) == JSON.stringify(noManaReq)){ return true; } } return('tapMana could not tap correct mana'); } getItemKey(boardElementId, listPositionId, playerFrom){ let itemKey = this.getItems(boardElementId, playerFrom, null, listPositionId); if(itemKey.length < 1){ alert('Could not find key'); return false; } if(itemKey.length > 1){ alert('Found more than one key'); return false } // Return first index of array, aka the itemKey return itemKey[0]; } // TODO: Make this work for each element hasSpaceInBoardElement(toElement, toPlayer){ let elementLength = getCurrentPositionAndLength(toElement, toPlayer)[1]; if(elementLength >= maxHandSize){ alert('Board full '+elementLength+'/'+maxHandSize); return false; } return true; } canPayMana(itemToPlay, playerId){ // Check player has enough mana if(this.getItems('mana', playerId, null, null).length < cardData[itemToPlay].cost){ return 'Not enough mana'; } // Check if the player has X amount of each mana colour required // may not be needed to tap them all (if cost reduced), but they need // to have those colours available. // Setting them to 0 to start so they can be added to. Could be nicer let manaAvailable = {1:0, 2:0, 3:0, 4:0}; // TODO: Thought, maybe instead of tapping colour used, it's just if you // have the colour? May be a silly thought, but // Loop the card to be played, and total its mana requirements let manaRequired = this.getManaTotalOf(itemToPlay); // player check for this may need changing for "play from opp hand" etc? let items = this.getItems('mana', playerId, null, null); for(let item = 0; item < items.length; item++){ // Looping the mana to check player has equal mana/colours to req. let itemKey = items[item]; // Check if the mana is tapped. Ignore if it is (for now) if(this.isTapped(itemKey)){ continue; } let colourId = cardManaColour[itemKey][0]; manaAvailable[colourId] += 1; } // If the manaReq is satiated by the manaAvailable then return true for (const manaColour in manaRequired) { //console.log('req'); //console.log(manaRequired[manaColour]); //console.log('av'); //console.log(manaAvailable[manaColour]); if(manaRequired[manaColour] > manaAvailable[manaColour]){ return 'Do not have enough: '+manaColour+' mana'; } } //console.log('manaAvailable: '); //console.log(manaAvailable); return true; } tempGetPrimaryManaOfCard(targetCard){ // Loop the mana card's colours // can be multi-coloured, and can be used as diff costs // TODO: This currently takes a White/Red mana as 2 available manas // when it should be 1 available of either type... // TODO: FOR NOW, the colour of the mana is the primary colour // will set 'manaColour' in DB at some point. So multi-colour // cards aren't all those when in mana (ie. 6 colour decks can't play // everything all the time) let manaType = null; // TODO: Will use the highest mana req as it's colour for now let manaColours = cardColours[targetCard]; for(let i = 0; i < manaColours.length; i++){ // Set mana colour for card if there isn't one if(manaType == null){ manaType = manaColours[i]; } // Check each other colour, use the highest cost colour // as the colour type for this specific mana // TODO: Do better, and nicer, manaType in DB with the colour? if(manaColours[i][1] > manaType[1]){ manaType = manaColours[i]; } } let colourId = manaType[0]; return colourId; } getManaTotalOf(itemKey){ let manaTotal = {1:0, 2:0, 3:0, 4:0}; let manaColours = cardColours[itemKey]; console.log(manaColours.length); for(let i = 0; i < manaColours.length; i++){ // Add one to the available mana let colourId = manaColours[i][0]; manaTotal[colourId] += manaColours[i][1]; // Add the cost } //console.log('manaTotal: '); //console.log(manaTotal); return manaTotal; } inspectCard(cardToInspect){ // Set inspectedCard (for now) to the card itemKey inEvent = ['inspect', cardToInspect]; this.drawBoard(); } isInEvent(eventToCheck = null){ if(eventToCheck == null && inEvent){ // If in any event return true; } if(inEvent && inEvent[0] == eventToCheck){ // If in specified event return true; } return false; } cancelInspect(){ if(this.isInEvent('inspect')){ this.clearEvent(); this.drawBoard(); } } startAttack(itemAttacking){ // Selects the card that will be attacking if(this.isTapped(itemAttacking)){ alert('Cannot attack, as tapped'); return false; } this.setEvent('attack', itemAttacking); // TODO:In future this MAY be what's used for check this.setCardStatus(itemAttacking, 'attacking'); this.drawBoard(); } setEvent(eventName, itemInEvent){ // TODO: Think this may need a better name inEvent = [eventName, itemInEvent]; } clearEvent(){ inEvent = null; } setCardStatus(itemToUpdate, newStatus = null){ // TODO: Check if valid status change, if not ignore cardStatus[itemToUpdate] = newStatus; } // Do the attack makeAttack(itemDefending, itemAttacking = null){ // TODO: Check if mana owner has any 'block', etc. first, then prevent if so // If itemAttacking not defined, use the item from inEvent if(itemAttacking == null){ itemAttacking = inEvent[1]; } if(this.isTapped(itemAttacking)){ alert('Cannot attack, as tapped'); return false; } switch (boardElement[itemDefending]) { // If card on 'board' attacked // Compare attackingCard and defendingCard case 'board': let atkAttacker = this.attackOf(itemAttacking); let atkDefender = this.attackOf(itemDefending); // Does Attacker kill Defender if(atkDefender <= atkAttacker){ this.sendToGrave(itemDefending); } // Does Defender kill Attacker if(atkAttacker <= atkDefender){ this.sendToGrave(itemAttacking); this.endAttackFor(itemAttacking); }else{ // If not, end the attacker cards attack 'phase' this.endAttackFor(itemAttacking); } break; case 'shield': // If the shield is tapped 'destroy' it if(this.isTapped(itemDefending)){ this.destroyShield(itemDefending); } // Otherwise tap the shield, so it can be destroyed in future else{ this.tapCard(itemDefending); } // End the attacker card attack 'phase' this.endAttackFor(itemAttacking); break; } this.drawBoard(); } attackOf(itemKey){ // TODO: Change this to ECSey element when added return cardData[itemKey]; } endAttackFor(itemAttacking){ // Tap the card (this would check if after attack cards taps or not. this.tapCard(itemAttacking); // If the item attacking was from the 'attack' event, remove the event if(itemAttacking == inEvent[1]){ inEvent = null; } } cancelAttackFor(itemAttacking){ if(itemAttacking != inEvent[1]){ return false; } // If the card to attack is from inEvent of 'attack', remove it and its cardStatus cardStatus[itemAttacking] = null; inEvent = null; this.drawBoard(); } playShield(fromPosition, fromElement, playerId, cardsToPlay = 1){ for(let shieldNo = 0; shieldNo < cardsToPlay; shieldNo++){ let shieldLength = this.shieldCount(playerId); // Check shield zone isn't already full if(shieldLength >= maxShield){ alert('Shield full '+shieldLength+'/'+maxShield); return false; } // Move from player, position 0 (top) of deck, to hand (next available position) this.addFromBoardElement(playerId, fromPosition, fromElement, 'shield', null, null); } } shieldCount(playerId){ return getCurrentPositionAndLength('shield', playerId)[1]; } playMana(fromPosition, fromElement, fromPlayer){ // TODO: getItemKey may be the best way to normalise other function calls instead of searching for item key within certain other funcs. (such as addFromBoardElement) let itemKey = this.getItemKey(fromElement, fromPosition, fromPlayer); console.log(itemKey); // Now add to mana zone this.addFromBoardElement(fromPlayer, fromPosition, fromElement, 'mana', null, null); // Flip the card face down (for now) TODO: just add colour atop this.flipCard(itemKey, 0); // Face down (TODO: Change to have spec. style) } // HELPER METHODS, to simplify code-base for me in the future destroyShield(itemKey){ // Adds from 'shield' to 'hand' boardElement for player shield belongs to // TODO:Check if shield can be destroyed (instantly, i.e. poisontouch, or some mechanic/spell akin) // Check the shield is not tapped, if not can't be destroyed if(!this.isTapped(itemKey)){ alert('Shield not tapped, cannot be destroyed'); return false; } // Get all shields, that belong to the player whose shield was targetted. // Check if they're all tapped before destroying the target let items = this.getItems('shield', player[itemKey], null, null); for(let item = 0; item < items.length; item++){ let itemKey = items[item]; // If ANY of their shields are untapped, you can't destroy target if(!board.isTapped(itemKey)){ alert('There is an untapped shield, cannot destroy target'); return false; } } // Shield is now destroyed, move it from shield to owners hand (for the catchup mechanic) this.addShieldToHand(player[itemKey], listPosition[itemKey]); return true; } addShieldToHand(playerFrom = null, listPositionFrom = null){ this.addFromBoardElement(playerFrom, listPositionFrom, 'shield', 'hand', null, null); } sendToGrave(itemKey){ console.log('SendToGrave: '+itemKey); //addFromBoardElement(playerFrom, fromPosition, elementFrom, elementTo, toPosition=null, playerTo=null) this.addFromBoardElement(player[itemKey], listPosition[itemKey], boardElement[itemKey], 'grave', null, null); } tapCard(itemKey){ // Set the cardStatus to tapped cardStatus[itemKey] = 'tapped'; // Do any other 'on tap' effects, etc. in the future } tap(itemKey){ tapCard(itemKey); } untapCard(itemKey){ cardStatus[itemKey] = null; // Do any other 'on tap' effects, etc. in the future } untap(itemKey){ untapCard(itemKey); } remainingShieldCount(playerId){ return getCurrentPositionAndLength('shield', playerId)[1]; } removeItemStatus(itemKey){ // TODO: Remove status of card, and do/don't do effect depending on locationTo/locationFrom? cardStatus[itemKey] = null; } isTapped(itemKey){ if(cardStatus[itemKey] == 'tapped'){ return true; } return false; } isAttacking(itemKey){ if(cardStatus[itemKey] == 'attacking'){ return true; } return false; } } // Run board commands here for testing let board = new Board; // Board is now async (initialisation) // Everything beyond initialisation shouldn't be required so no need to wait // At least I think, for now. May need more stuff in future // Request the deck(s) from server/db // When the page loads (for now) start the game requestStartGame(); // This will trigger an emit to and on from the server will trigger loadBoard(); // public/main.js // socket.on('responseStartGame', function (data) { function loadBoard(data) { console.log(data); // Built the original boardstate using the data passed from server // This is only for the first time recieve (TODO: ALL) game data at match start // Then ideally only what's needed to be passed to/from client/server sent after item = data.item; itemCount = data.itemCount; boardElement = data.boardElement; cardData = data.cardData; // position; // Not to do, as calculated per client (and will be per screen-size) // size; // Like position, calculated per client. cardStatus = data.cardStatus; player = data.player; // Set each item to correct player listPosition = data.listPosition; cardFace = data.cardFace; // Like status, for DCs and reloads cardSprite = {}; // TODO: ? Maybe, or this could be done clientside based // ^ on card id? deckIn = data.deckIn; // NEW, may be used, for now player substitute deckData = data.deckData; cardAttack = data.cardAttack; // TODO: add to the logic cardColours = data.cardColours; cardManaColour = data.cardManaColour; // TODO: JANK IN, CHANGE CODE TO USE NEW ARRAY!! // Temp jank, set colour to first colour req. for(let i = 0; i < itemCount; i++){ //console.log(itemCount); //console.log(cardColours[itemCount]); // after && to check if there is a colourReq record in cardColours if(cardData[i] !== undefined && i in cardColours){ // i may not have carddata, as realDeck //console.log(i); cardData[i].colour = board.tempGetPrimaryManaOfCard(i); //cardData[i].colour = cardColours[i][0][0]; // Colour Id of first req // Set the artwork (this would be done front-end I think) cardSprite[i] = [0,0]; // Temp sprite set based on colour TODO: Change to set correct sprite from DB switch (cardData[i].colour){ case COLOUR.white.id: // White cardSprite[i] = [0,0]; break; case COLOUR.blue.id: // Blue cardSprite[i] = [0,1]; break; case COLOUR.red.id: // Red cardSprite[i] = [1,0]; break; case COLOUR.green.id: // Green cardSprite[i] = [1,1]; break; default: break; } } } // Stuff not currently in the getDecks TODO: To be stripped down and // rewritten/made into generateItems or something, for each match // Decks can be as-is for getting deckLists for deckselection, but // TODO: for matches need something to generate and return every item, and attribute // TODO: Loop these for the amount of players in players (and use the correct itemKey) shuffleDeck(0); // Shuffle player 0 deck shuffleDeck(1); // Shuffle player 1 deck // Play shield from top of each players deck to the maximum allowed (4 typically) for(let currentPlayer = 0; currentPlayer <= players-1; currentPlayer++){ board.playShield(1, 'deck', currentPlayer, maxShield); } // Draw the graphics of the board/game // Wait for the cardArt to load on first load // Otherwise it'll be boxes, and art will flash in on first click event triggered cardArt.onload = function(){ board.drawBoard(true); }; } // Right Click, Rightclick, rightclick, right click canvas.addEventListener('contextmenu', function(event) { event.preventDefault(); var x = event.pageX - canvasLeft, y = event.pageY - canvasTop; console.log('RIGHT CLICK X: '+x+' Y: '+y); for(let itemKey = 0; itemKey < item.length; itemKey++){ if(elements[itemKey] == 'deck'){ continue; } // TODO: Normalise this too, may need to change how size+position work? // Check the item has a size and position if(itemKey in size && itemKey in position){ // Compare the event XY position to the item if(clickableCheck(x,y,itemKey)){ // Only want to happen once (for now) // Maybe in future add to hand would trigger another event if there's an effect? // Player the item belongs to, not who's doing the action let playerId = player[itemKey]; // Check the location of element switch(boardElement[itemKey]){ // Check item location, and trigger events based on it case 'board': // TODO: If the current event is attack, right clicking same item in event // will end the attack event // If there's an attack event, target other cards // If there is an event, the event is attack event, and the item currently loop // is the same one as currently in the event if(board.isInEvent('attack') && itemKey == inEvent[1]){ board.cancelAttackFor(itemKey); } break; case 'hand': // Can be played as mana (right click for now) // Play item from boardElement hand. To boardElement mana (explanitory) if(!inEvent){ board.playMana(listPosition[itemKey], 'hand', playerId); } break; default: break; } board.drawBoard(); return true; } } } // Return false to prevent context opening: // https://stackoverflow.com/a/4236294 return false; }, false); // Left Click, Leftclick, leftclick, left click canvas.addEventListener('click', function(event) { var x = event.pageX - canvasLeft, y = event.pageY - canvasTop; // Check clicks against itemKey position+size // Will be the new way // TODO:Maybe write this into a function? If XY WH is hit return true, and the itemKey // then it can be re-used in contextclick, hover, etc without rewrite console.log('LEFT CLICK X: '+x+' Y: '+y); // TODO: Normalise this too for(let itemKey = 0; itemKey < item.length; itemKey++){ if(boardElement[itemKey] == 'deck'){ continue; }else{ // Will print everything that's clickable (or should be clickable) // any undefined attributes and such should display with this too //printECSData([itemKey]); } // Check the item has a size and position if(itemKey in size && itemKey in position){ // Compare the event XY position to the item if(clickableCheck(x,y,itemKey)){ // Only want to happen once (for now) // Maybe in future add to hand would trigger another event if there's an effect? // Get the playerId of who the item belongs to // TODO: Some other playerId may be needed to for 'mind control' cards let playerId = player[itemKey]; // Check the location of element switch(boardElement[itemKey]){ // Check item location, and trigger events based on it // TODO: Change inEvent locations, and checks elsewhere? // TODO: Make existing mechanics ECSey case 'realDeck': if(!inEvent){ board.drawACard(playerId, 1); } break; case 'board': // player/opponentBoard not seperated, as want to have // each player able to have effects, etc. to make others attack // etc. // yourPlayerId in for now to prevent using opponents card // TODO:the cards may be used to attack, there will be another // check like 'canUseOpponentsBoard' or something if(!inEvent && !board.isTapped(itemKey) && playerId == yourPlayerId){ board.startAttack(itemKey); break; } // opponentBoard // If there's an attack event, target other cards // Again yourPlayerId check for now, this time so your can't hit your own if(inEvent && inEvent[0] == 'attack' && inEvent[1] != itemKey && playerId != yourPlayerId){ // Make attack on the card clicked, with card in inEvent[1] board.makeAttack(itemKey); break; } // If no event, and clicked an TODO:OPPONENT CARD (for now) // Wants to be on option on r/l click maybe // left click inspects then you choose play from there? // inspect the card (zoom in on it) if(!inEvent && playerId != yourPlayerId){ board.inspectCard(itemKey); break; } else if(inEvent && inEvent[0] == 'inspect' && inEvent[1] == itemKey){ board.cancelInspect(); break; } break; case 'hand': // TODO: Ensure it can be played if(!inEvent){ board.playCardToBoard(listPosition[itemKey], 'hand', 'board', playerId, playerId, 1); } break; case 'shield': // If you have an attack, and click opponent shield, // destroy (unless they can block, etc.) if(inEvent && inEvent[0] == 'attack' && inEvent[1] != itemKey){ board.makeAttack(itemKey); } break; default: break; } board.drawBoard(); return true; } } } }, false); function clickableCheck(cursorX,cursorY,itemKey){ // Collision detection between clicked offset and clickableItems // https://stackoverflow.com/a/9880302 let itemX = position[itemKey][0]; let itemY = position[itemKey][1]; let itemWidth = size[itemKey][0]; let itemHeight = size[itemKey][1]; // Debug Stuff let debug = false if(debug){ console.log(itemY+' <'); console.log(cursorY+ '<') console.log(itemY+itemHeight); } if( cursorY > itemY && cursorY < itemY + itemHeight && cursorX > itemX && cursorX < itemX + itemWidth) { return true; } return false; } function shuffleDeck(playerId){ // Create a tempDeck array of same length of the player deck let deckLength = getCurrentPositionAndLength('deck', playerId)[1]; 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 let shuffle = Math.floor(Math.random() * (tempDeck.length)); // swap the current listPosition with a random one with the deck count [ tempDeck[i], tempDeck[shuffle] ] = [ tempDeck[shuffle], tempDeck[i] ]; } // For each item in the actual deck, set the listPosition to the random number from tempDeck let items = board.getItems('deck', playerId, null, null); for(let item = 0; item < items.length; item++){ let itemKey = items[item]; listPosition[itemKey] = tempDeck[item]; } } function untapZone(elementFrom, playerFrom){ let items = board.getItems(elementFrom, playerFrom, null, null); for(let item = 0; item < items.length; item++){ let itemKey = items[item]; if(board.isTapped(itemKey)){ board.untapCard(itemKey); } } board.drawBoard(); } function untapAllZones(currentPlayer = null){ if(currentPlayer === null){ currentPlayer = 0; } for(let currentPlayer = 0; currentPlayer <= players-1; currentPlayer++){ // Loop all the elements, and utap each card in the zone for(let element = 0; element < elements.length; element++){ untapZone(elements[element], currentPlayer); } } } function getCurrentPositionAndLength(elementName, playerId){ let highestListPosition = 0; let length = 0; let items = board.getItems(elementName, playerId, null, null); for(let item = 0; item < items.length; item++){ let itemKey = items[item]; if(listPosition[itemKey] >= highestListPosition){ highestListPosition = listPosition[itemKey]; } length++; } return [highestListPosition, length]; } function calculateItemSizePosition(itemKey){ // Calc position and size of the item // Used for drawing and interacting // TODO: Make better calculations and positions for here (also rescaling in future) // TODO: Maybe instead of checking each player/element combo in if, do some fancy loop? let itemPlayer = player[itemKey]; let itemElement = boardElement[itemKey]; let itemListPositionLength = getCurrentPositionAndLength(itemElement, itemPlayer); let itemListPositionNext = itemListPositionLength[0]; let itemListLength = itemListPositionLength[1]; let cardMargin = 10; let positionX = 0; let positionY = 0; let width = 0; let height = 0; let i = listPosition[itemKey]; //console.log('cardName: '+cardData[itemKey].name+' listPosition/i: '+i); if(itemPlayer == 1 && itemElement == 'realDeck'){ positionX = 40; positionY = 60; width = cardWidth*1.5; height = cardHeight*1.5; } if(itemPlayer == 0 && itemElement == 'realDeck'){ positionX = canvas.width-cardWidth*1.5-40; positionY = canvas.height-cardHeight*1.5-60; width = cardWidth*1.5; height = cardHeight*1.5; } if(itemPlayer == 1 && itemElement == 'board'){ positionX = canvas.width/2 - (cardWidth * (itemListLength - i) - (cardMargin * i)); positionY = cardHeight + 30; width = cardWidth; height = cardHeight; } if(itemPlayer == 1 && itemElement == 'hand'){ positionX = canvas.width/2 - (cardWidth * (itemListLength - (i+1)) - (cardMargin * (i+1))); positionY = 20; width = cardWidth; height = cardHeight; } if(itemPlayer == 1 && itemElement == 'mana'){ // TODO: Opponent Mana } if(itemPlayer == 1 && itemElement == 'shield'){ let fromX = canvas.width-60; let fromY = 300; let cWidth = cardWidth*.8; let cHeight = cardHeight*.8; let split = 0; // i-1 here as it's based on 0 being start, like array. // If there's 2 in a row, split onto next row // TODO: Not sure if I want to start elements at 1 (for clienty) or 0 (for programmy) if(i-1>=2){ split = 1; } // i%2 0 = 0, 1 = 1, 2 = 0, 3 = 1 to prevent margin from X/Y axis, and just between cards positionX = (fromX+((i%2)*cardMargin))+(i%2*cWidth)-(cWidth*2); positionY = fromY+(split*cHeight+(cardMargin*split))-(cHeight*2 + cardMargin); width = cWidth; height = cHeight; } if(itemPlayer == 0 && itemElement == 'board'){ positionX = canvas.width/2 - (cardWidth * (itemListLength - (i+1)) - (cardMargin * (i+1))); positionY = canvas.height - cardHeight-30-(cardHeight); width = cardWidth; height = cardHeight; } if(itemPlayer == 0 && itemElement == 'hand'){ positionX = canvas.width/2 - (cardWidth * (itemListLength - (i+1)) - (cardMargin * (i+1))); positionY = canvas.height-cardWidth*1.5-20; width = cardWidth; height = cardHeight; } if(itemPlayer == 0 && itemElement == 'mana'){ let fromX = 60; let fromY = 60; let cWidth = cardWidth*.5; let cHeight = cardHeight*.5; positionX = (fromX+cardMargin)+(i*cWidth-cardMargin); positionY = canvas.height-fromY; width = cWidth; height = cHeight; } if(itemPlayer == 0 && itemElement == 'shield'){ let fromX = 60; let fromY = 300; let cWidth = cardWidth*.8; let cHeight = cardHeight*.8; let split = 0; // i-1 here as it's based on 0 being start, like array. // TODO: Not sure if I want to start elements at 1 (for clienty) or 0 (for programmy) if(i-1>=2){ split = 1; } positionX = (fromX+((i%2)*cardMargin)) +(i%2*cWidth); positionY = canvas.height-fromY+(split*cHeight+(cardMargin*split)); width = cWidth; height = cHeight; } // Inspected Card // TODO: May need to make a new itemKey for inspectedCard to loop through if(inEvent && inEvent[0] == 'inspect' && inEvent[1] == itemKey){ positionX = canvas.width/2 - cardWidth; positionY = canvas.height/2 - cardHeight; width = cardWidth*2; height = cardHeight*2; } //console.log('cardName: '+cardData[itemKey].name+', i/listPosition: '+i+', listPosition Length: '+itemListLength); // Set the size/position of the item size[itemKey] = [width, height]; position[itemKey] = [positionX,positionY]; } // For checking data without needing to change the codebase with logs function getItemsAndPrintFrontEnd(){ let boardElementId = document.getElementById("boardElementId").value; if(boardElementId == ""){ boardElementId = null; } let playerId = document.getElementById("playerId").value; if(playerId == ""){ playerId = null; } let cardStatusId = document.getElementById("cardStatusId").value; if(cardStatusId == ""){ cardStatusId = null; } let listPositionId = document.getElementById("listPositionId").value; if(listPositionId == ""){ listPositionId = null; } let itemOrCard = document.getElementById("itemOrCardData").value; getItemsAndPrint(boardElementId, playerId, cardStatusId, listPositionId, itemOrCard); } function getItemsAndPrint(boardElementId = null, playerId = null, cardStatusId = null, listPositionId = null, itemOrCard = 'item'){ let items = board.getItems(boardElementId, playerId, cardStatusId, listPositionId); if(itemOrCard == 'card'){ console.log('----- CardData -----'); printCardData(items); }else{ console.log('----- ItemData -----'); printECSData(items); } console.log('Items array length: '+items.length); } function printECSData(items){ for(let item = 0; item < items.length; item++){ let itemKey = items[item]; console.log( 'itemId: '+itemKey+"\n"+ 'boardElement: '+boardElement[itemKey]+"\n"+ 'cardData: '+cardData[itemKey]+"\n"+ 'position: '+position[itemKey]+"\n"+ 'size: '+size[itemKey]+"\n"+ 'cardStatus: '+cardStatus[itemKey]+"\n"+ 'player: '+player[itemKey]+"\n"+ 'listPosition: '+listPosition[itemKey]+"\n"+ 'cardFace: '+cardFace[itemKey] ); } } function printCardData(items){ let cardArray = []; for(let item = 0; item < items.length; item++){ let itemKey = items[item]; cardArray.push(cardData[itemKey]); } console.log(cardArray); } function echoCards(){ console.log(cardArray); } function echoCardsInDeck(playerId){ // Get all the cards that belong to player let cardsInDeck = board.getItems(null, playerId); let deckList = []; for(let i = 0; i < cardsInDeck.length; i++){ // If it's a card TODO: Change this to check 'card' from some ECS element if(cardData[i] !== null && cardData[i] !== undefined){ deckList.push(cardData[i]); } } console.log(deckList); } // https://stackoverflow.com/a/35970186 function invertColour(hex, bw = true) { // Split the hex code into individual alphanumerics if (hex.indexOf('#') === 0) { hex = hex.slice(1); } // convert 3-digit hex to 6-digits. if (hex.length === 3) { hex = hex[0] + hex[0] + hex[1] + hex[1] + hex[2] + hex[2]; } if (hex.length !== 6) { throw new Error('Invalid HEX colour.'); } let r = parseInt(hex.slice(0, 2), 16), g = parseInt(hex.slice(2, 4), 16), b = parseInt(hex.slice(4, 6), 16); // Make it black/white for contrast (typical behaviour here) if (bw) { // https://stackoverflow.com/a/3943023/112731 return (r * 0.299 + g * 0.587 + b * 0.114) > 186 ? '#000000' : '#FFFFFF'; } // Invert colour components r = (255 - r).toString(16); g = (255 - g).toString(16); b = (255 - b).toString(16); // Pad each with zeros and return return "#" + padZero(r) + padZero(g) + padZero(b); }