You cannot select more than 25 topics Topics must start with a letter or number, can include dashes ('-') and can be up to 35 characters long.
cardGame/gameMod.js

629 lines
21 KiB
JavaScript

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
};