diff --git a/gameinfos.inc.php b/gameinfos.inc.php index e5fbff7..87aa2f7 100644 --- a/gameinfos.inc.php +++ b/gameinfos.inc.php @@ -42,8 +42,8 @@ // Board game geek ID of the game 'bgg_id' => 323262, - // Players configuration that can be played (ex: 2 to 4 players) - 'players' => [3, 4, 5], + // Players configuration that can be played (2 to 5 players) + 'players' => [2, 3, 4, 5], // Suggest players to play with this number of players. Must be null if there is no such advice, or if there is only one possible player configuration. 'suggest_player_number' => null, diff --git a/modules/constants.inc.php b/modules/constants.inc.php index 20cfe1b..2e6e1e6 100644 --- a/modules/constants.inc.php +++ b/modules/constants.inc.php @@ -14,6 +14,7 @@ define('ST_ACTIVATE_NEXT_PLAYER', 22); define('ST_PLAYER_SELECT_NEXT_PLAYER', 30); define('ST_APPLY_SELECTED_NEXT_PLAYER', 31); +define('ST_PLAYER_SELECT_WHO_TAKE_ATTACK_REWARD', 38); define('ST_PLAYER_PICK_CARDS_FROM_PLAYER', 40); define('ST_PLAYER_GIVE_CARDS_BACK_TO_PLAYER_AFTER_PICKING', 41); define('ST_END_ROUND', 80); diff --git a/states.inc.php b/states.inc.php index 22e6c2d..9cfcd97 100644 --- a/states.inc.php +++ b/states.inc.php @@ -109,7 +109,12 @@ 'description' => '', 'type' => 'game', 'action' => 'stActivateNextPlayer', - 'transitions' => ['firstPlayerTurn' => ST_FIRST_PLAYER_TURN, 'playerTurn' => ST_PLAYER_TURN, 'playerSelectNextPlayer' => ST_PLAYER_SELECT_NEXT_PLAYER], + 'transitions' => [ + 'firstPlayerTurn' => ST_FIRST_PLAYER_TURN, + 'playerTurn' => ST_PLAYER_TURN, + 'playerSelectNextPlayer' => ST_PLAYER_SELECT_NEXT_PLAYER, + 'playerSelectWhoTakeAttackReward' => ST_PLAYER_SELECT_WHO_TAKE_ATTACK_REWARD, + ], ], // The "natural" next player don't have cards anymore, @@ -136,6 +141,22 @@ 'transitions' => ['firstPlayerTurn' => ST_FIRST_PLAYER_TURN], ], + // /!\ 2P mode only + // When a player wins an attack, + // he has to choose if it takes the reward or if he gives it to its opponent + ST_PLAYER_SELECT_WHO_TAKE_ATTACK_REWARD => [ + 'name' => 'playerSelectWhoTakeAttackReward', + 'description' => clienttranslate('${actplayer} must choose who will take the attacker reward'), + 'descriptionmyturn' => clienttranslate('${you} must choose who will take the attacker reward'), + 'type' => 'activeplayer', + 'args' => 'argPlayerSelectWhoTakeAttackReward', + 'possibleactions' => ['selectWhoTakeAttackReward'], + 'transitions' => [ + 'firstPlayerTurn' => ST_FIRST_PLAYER_TURN, + 'zombiePass' => ST_ACTIVATE_NEXT_PLAYER, + ], + ], + // When someone plays one or more cards of value "1", // this player has to pick one or more cards from another player of its choice ST_PLAYER_PICK_CARDS_FROM_PLAYER => [ diff --git a/velonimo.action.php b/velonimo.action.php index ad9adc9..f62dec2 100644 --- a/velonimo.action.php +++ b/velonimo.action.php @@ -71,6 +71,20 @@ public function selectNextPlayer() self::ajaxResponse(); } + /** + * /!\ 2P mode only + */ + public function selectWhoTakeAttackReward() + { + self::setAjaxMode(); + + $selectedPlayerId = trim(self::getArg('selectedPlayerId', AT_int, true)); + + $this->game->selectWhoTakeAttackReward((int) $selectedPlayerId); + + self::ajaxResponse(); + } + public function selectPlayerToPickCards() { self::setAjaxMode(); diff --git a/velonimo.css b/velonimo.css index dae2907..1625b75 100644 --- a/velonimo.css +++ b/velonimo.css @@ -135,15 +135,6 @@ Board .player-table.is-wearing-jersey .player-table-hand { margin: 5px 0 0 -6px; } -.player-table-hand .number-of-cards { - position: absolute; - left: 8px; - top: 20px; - font-size: 2em; - text-align: center; - width: 1em; - pointer-events: none; -} .player-table-speech-bubble { position: absolute; top: 0; @@ -235,6 +226,26 @@ Board cards height: 126px; /* cardHeight */ z-index: 8; } +/* /!\ 2P mode only */ +#cards-deck { + position: absolute; + display: inline-block; + top: 177px; /* (containerHeight / 2) - (selfHeight / 2) */ + left: 10px; /* boardMargin */ + width: 90px; /* cardWidth */ + height: 126px; /* cardHeight */ + background-image: url('img/remaining_cards.png'); + background-size: 90px 126px; +} +/* /!\ 2P mode only */ +#attack-reward-card { + position: absolute; + display: inline-block; + top: 177px; /* (containerHeight / 2) - (selfHeight / 2) */ + left: 110px; /* cardWidth + (2 * boardMargin) */ + width: 90px; /* cardWidth */ + height: 126px; /* cardHeight */ +} /** END Board cards */ @@ -313,6 +324,15 @@ Cards .cards-stack .velonimo-card { margin-right: -60px; /* 2/3 of card width */ } +.text-on-cards { + position: absolute; + left: 8px; + top: 20px; + font-size: 2em; + text-align: center; + width: 1em; + pointer-events: none; +} /** END Cards */ diff --git a/velonimo.game.php b/velonimo.game.php index 892cb5a..4615cc6 100644 --- a/velonimo.game.php +++ b/velonimo.game.php @@ -36,9 +36,8 @@ class Velonimo extends Table private const GAME_STATE_LAST_NUMBER_OF_CARDS_TO_PICK = 'numberOfCardsToPick'; private const GAME_STATE_LAST_NUMBER_OF_CARDS_TO_GIVE_BACK = 'numberOfCardsToGiveBack'; private const GAME_STATE_LAST_PLAYER_ID_TO_GIVE_CARDS_BACK = 'playerIdToGiveCardsBack'; - /* START 2P */ + // /!\ 2P mode only private const GAME_STATE_LAST_NUMBER_OF_CARDS_TO_PICK_FROM_DECK = 'numberOfCardsToPickFromDeck'; - /* END 2P */ private const GAME_OPTION_HOW_MANY_ROUNDS = 'howManyRounds'; @@ -47,9 +46,8 @@ class Velonimo extends Table private const CARD_LOCATION_DISCARD = 'discard'; private const CARD_LOCATION_PLAYED = 'played'; private const CARD_LOCATION_PREVIOUS_PLAYED = 'previousPlayed'; - /* START 2P */ - private const CARD_LOCATION_ATTACK_WINNER_REWARD = 'attackReward'; - /* END 2P */ + // /!\ 2P mode only + private const CARD_LOCATION_ATTACK_REWARD = 'attackReward'; /** @var Deck (BGA framework component to manage cards) */ private $deck; @@ -229,10 +227,11 @@ protected function getAllDatas() { $this->fromBgaCardsToVelonimoCards($this->deck->getCardsInLocation(self::CARD_LOCATION_PREVIOUS_PLAYED)) ); $result['previousPlayedCardsValue'] = (int) self::getGameStateValue(self::GAME_STATE_PREVIOUS_PLAYED_CARDS_VALUE); + if ($this->is2PlayersMode($players)) { - $result['numberOfCardsInDeck'] = count($this->deck->getCardsInLocation(self::CARD_LOCATION_DECK)); + $result['numberOfCardsInDeck'] = $this->getNumberOfCardsInDeck(); $result['attackRewardCards'] = $this->formatCardsForClient( - $this->fromBgaCardsToVelonimoCards($this->deck->getCardsInLocation(self::CARD_LOCATION_ATTACK_WINNER_REWARD)) + $this->fromBgaCardsToVelonimoCards($this->deck->getCardsInLocation(self::CARD_LOCATION_ATTACK_REWARD)) ); } @@ -369,8 +368,44 @@ function playCards(array $playedCardIds, bool $cardsPlayedWithJersey) { $this->setStat($playedCardsValue, 'maxValue', $currentPlayerId); } + $numberOfCurrentPlayerCards = count($currentPlayerCards); + + if ($this->is2PlayersMode($players)) { + // if the player played cards of value "2", he has to pick as many cards from deck + $numberOfPlayedCardsOfValueTwo = count( + array_filter($playedCards, fn (VelonimoCard $c) => $c->getValue() === VALUE_2) + ); + if ($numberOfPlayedCardsOfValueTwo > 0) { + $numberOfCardsInDeck = $this->getNumberOfCardsInDeck(); + $numberOfCardsToPickFromDeck = min($numberOfPlayedCardsOfValueTwo, $numberOfCardsInDeck); + + if ($numberOfCardsToPickFromDeck > 0) { + $cardsPickedFromDeck = $this->fromBgaCardsToVelonimoCards( + $this->deck->pickCards($numberOfPlayedCardsOfValueTwo, self::CARD_LOCATION_DECK, $currentPlayer->getId()) + ); + $numberOfCurrentPlayerCards = $numberOfCurrentPlayerCards + $numberOfCardsToPickFromDeck; + $translatedMessage = clienttranslate('${player_name} picks ${numberOfCards} card(s) from the top of the deck'); + foreach ($players as $player) { + if ($player->getId() === $currentPlayer->getId()) { + self::notifyPlayer($currentPlayer->getId(), 'cardsReceivedFromDeck', $translatedMessage, [ + 'cards' => $this->formatCardsForClient($cardsPickedFromDeck), + 'numberOfCards' => $numberOfCardsToPickFromDeck, + 'player_name' => $currentPlayer->getName(), + ]); + } else { + self::notifyPlayer($player->getId(), 'cardsMovedFromDeckToAnotherPlayer', $translatedMessage, [ + 'receiverPlayerId' => $currentPlayer->getId(), + 'numberOfCards' => $numberOfCardsToPickFromDeck, + 'player_name' => $currentPlayer->getName(), + ]); + } + } + } + } + } + // if the player played his last card, set its rank for this round - if ((count($currentPlayerCards) - $numberOfPlayedCards) === 0) { + if (($numberOfCurrentPlayerCards - $numberOfPlayedCards) === 0) { $currentRound = (int) self::getGameStateValue(self::GAME_STATE_CURRENT_ROUND); $nextRankForRound = $this->getNextRankForRound($players, $currentRound); $currentPlayer->addRoundRanking($currentRound, $nextRankForRound); @@ -398,35 +433,6 @@ function playCards(array $playedCardIds, bool $cardsPlayedWithJersey) { return; } - if ($this->is2PlayersMode($players)) { - // if the player played cards of value "2", he has to pick as many cards from deck - $numberOfPlayedCardsOfValueTwo = count( - array_filter($playedCards, fn (VelonimoCard $c) => $c->getValue() === VALUE_2) - ); - if ($numberOfPlayedCardsOfValueTwo > 0) { - $cardsPickedFromDeck = $this->fromBgaCardsToVelonimoCards( - $this->deck->pickCards($numberOfPlayedCardsOfValueTwo, self::CARD_LOCATION_DECK, $currentPlayer->getId()) - ); - $numberOfCardsToPickFromDeck = count($cardsPickedFromDeck); - $translatedMessage = clienttranslate('${player_name} picks ${numberOfCards} card(s) from the top of the deck'); - foreach ($players as $player) { - if ($player->getId() === $currentPlayer->getId()) { - self::notifyPlayer($currentPlayer->getId(), 'cardsReceivedFromDeck', $translatedMessage, [ - 'cards' => $this->formatCardsForClient($cardsPickedFromDeck), - 'numberOfCards' => $numberOfCardsToPickFromDeck, - 'player_name' => $currentPlayer->getName(), - ]); - } else { - self::notifyPlayer($player->getId(), 'cardsMovedFromDeckToAnotherPlayer', $translatedMessage, [ - 'receiverPlayerId' => $currentPlayer->getId(), - 'numberOfCards' => $numberOfCardsToPickFromDeck, - 'player_name' => $currentPlayer->getName(), - ]); - } - } - } - } - // if the player played cards of value "1", he has to pick as many cards from another player $numberOfPlayedCardsOfValueOne = count( array_filter($playedCards, fn (VelonimoCard $c) => $c->getValue() === VALUE_1) @@ -483,6 +489,62 @@ function selectNextPlayer(int $selectedPlayerId) { $this->gamestate->nextState('applySelectedNextPlayer'); } + /** + * /!\ 2P mode only + */ + function selectWhoTakeAttackReward(int $selectedPlayerId) { + self::checkAction('selectWhoTakeAttackReward'); + + $players = $this->getPlayersFromDatabase(); + if (!$this->is2PlayersMode($players)) { + $this->gamestate->nextState('firstPlayerTurn'); + + return; + } + + try { + $selectedPlayer = $this->getPlayerById($selectedPlayerId, $players); + $currentPlayer = $this->getPlayerById((int) self::getCurrentPlayerId(), $players); + } catch (\Throwable $e) { + throw new BgaUserException(self::_('This player does not exist.')); + } + + $rewardCards = $this->fromBgaCardsToVelonimoCards( + $this->deck->getCardsInLocation(self::CARD_LOCATION_ATTACK_REWARD) + ); + // skip if no more reward + if (count($rewardCards) <= 0) { + $this->gamestate->nextState('firstPlayerTurn'); + + return; + } + + // give reward to selected player + $this->deck->moveCards( + array_map(fn (VelonimoCard $c) => $c->getId(), $rewardCards), + self::CARD_LOCATION_PLAYER_HAND, + $selectedPlayer->getId() + ); + + // notify players + if ($currentPlayer->getId() === $selectedPlayer->getId()) { + $translatedMessage = clienttranslate('${player_name} takes the attacker reward'); + } else { + $translatedMessage = clienttranslate('${player_name} gives the attacker reward to ${player_name2}'); + } + self::notifyAllPlayers('attackRewardCardsMovedToPlayer', $translatedMessage, [ + 'cards' => $this->formatCardsForClient($rewardCards), + 'receiverPlayerId' => $selectedPlayer->getId(), + 'player_name' => $currentPlayer->getName(), + 'player_name2' => $selectedPlayer->getName(), + ]); + + // reveal new attack reward + $this->revealNewAttackRewardCardsIfEnoughCardsInDeck(); + + $this->gamestate->nextState('firstPlayerTurn'); + } + function selectPlayerToPickCards(int $selectedPlayerId) { self::checkAction('selectPlayerToPickCards'); @@ -682,6 +744,20 @@ function argPlayerSelectNextPlayer() { ]; } + /** + * /!\ 2P mode only + */ + function argPlayerSelectWhoTakeAttackReward() { + $activePlayerId = (int) self::getActivePlayerId(); + + return [ + 'activePlayerId' => $activePlayerId, + 'selectablePlayers' => $this->formatPlayersForClient( + $this->getPlayersFromDatabase(), + ), + ]; + } + function argPlayerSelectPlayerToPickCards() { $currentRound = (int) self::getGameStateValue(self::GAME_STATE_CURRENT_ROUND); $activePlayerId = (int) self::getActivePlayerId(); @@ -749,13 +825,7 @@ function stStartRound() { ]); if ($this->is2PlayersMode($players)) { - $this->deck->pickCardForLocation(self::CARD_LOCATION_DECK, self::CARD_LOCATION_ATTACK_WINNER_REWARD); - - self::notifyAllPlayers('attackRewardCardsRevealed', 'A new attack reward card is revealed', [ - 'attackRewardCards' => $this->formatCardsForClient( - $this->fromBgaCardsToVelonimoCards($this->deck->getCardsInLocation(self::CARD_LOCATION_ATTACK_WINNER_REWARD)) - ), - ]); + $this->revealNewAttackRewardCardsIfEnoughCardsInDeck(); } $this->gamestate->nextState('firstPlayerTurn'); @@ -774,15 +844,25 @@ function stActivateNextPlayer() { foreach ($players as $ignored) { $nextPlayerId = self::activeNextPlayer(); $nextPlayerCanPlay = in_array($nextPlayerId, $playersWhoCanPlayIds, true); - // if the next player is the one who played the last played cards, remove the cards from the table + // if the next player is the one who played the last played cards if ($nextPlayerId === $playerIdWhoPlayedTheLastCards) { $this->discardPlayedCards(); self::giveExtraTime($nextPlayerId); + + if ( + $this->is2PlayersMode($players) + && $this->getNumberOfCardsInAttackReward() > 0 + ) { + $this->gamestate->nextState('playerSelectWhoTakeAttackReward'); + return; + } + if ($nextPlayerCanPlay) { $this->gamestate->nextState('firstPlayerTurn'); - } else { - $this->gamestate->nextState('playerSelectNextPlayer'); + return; } + + $this->gamestate->nextState('playerSelectNextPlayer'); return; } if ($nextPlayerCanPlay) { @@ -1250,4 +1330,37 @@ private function updatePlayerRoundsRanking(VelonimoPlayer $player): void { private function isJerseyUsedInCurrentRound(): bool { return 1 === (int) self::getGameStateValue(self::GAME_STATE_JERSEY_HAS_BEEN_USED_IN_THE_CURRENT_ROUND); } + + /** + * /!\ 2P mode only + */ + private function getNumberOfCardsInDeck(): int { + return count($this->deck->getCardsInLocation(self::CARD_LOCATION_DECK)); + } + + /** + * /!\ 2P mode only + */ + private function getNumberOfCardsInAttackReward(): int { + return count($this->deck->getCardsInLocation(self::CARD_LOCATION_ATTACK_REWARD)); + } + + /** + * /!\ 2P mode only + */ + private function revealNewAttackRewardCardsIfEnoughCardsInDeck(): void { + if ($this->getNumberOfCardsInDeck() <= 0) { + return; + } + + $this->deck->pickCardForLocation(self::CARD_LOCATION_DECK, self::CARD_LOCATION_ATTACK_REWARD); + + self::notifyAllPlayers('attackRewardCardsRevealed', 'A new attack reward card is revealed', [ + 'cards' => $this->formatCardsForClient( + $this->fromBgaCardsToVelonimoCards( + $this->deck->getCardsInLocation(self::CARD_LOCATION_ATTACK_REWARD) + ) + ), + ]); + } } diff --git a/velonimo.js b/velonimo.js index 0b9b51c..152200f 100644 --- a/velonimo.js +++ b/velonimo.js @@ -65,6 +65,8 @@ const CARD_ID_JERSEY = 0; // DOM IDs const DOM_ID_APP = 'velonimo-game'; const DOM_ID_BOARD_CARPET = 'board-carpet'; +const DOM_ID_CARDS_DECK = 'cards-deck'; +const DOM_ID_ATTACK_REWARD_CARD = 'attack-reward-card'; const DOM_ID_PLAYED_CARDS_WRAPPER = 'played-cards'; const DOM_ID_LAST_PLAYED_CARDS = 'last-played-cards'; const DOM_ID_PREVIOUS_LAST_PLAYED_CARDS = 'previous-last-played-cards'; @@ -83,6 +85,7 @@ const DOM_CLASS_PLAYER_TABLE = 'player-table'; const DOM_CLASS_PLAYER_IS_WEARING_JERSEY = 'is-wearing-jersey'; const DOM_CLASS_PLAYER_HAS_USED_JERSEY = 'has-used-jersey'; const DOM_CLASS_CARDS_STACK = 'cards-stack'; +const DOM_CLASS_TEXT_ON_CARDS = 'text-on-cards'; const DOM_CLASS_CARDS_STACK_PREVIOUS_PLAYED = 'previous-last-played-cards'; const DOM_CLASS_DISABLED_ACTION_BUTTON = 'disabled'; const DOM_CLASS_ACTIVE_PLAYER = 'active'; @@ -248,7 +251,7 @@ function (dojo, declare) { dojo.place( `
${(player.name.length > 10 ? (player.name.substr(0,10) + '...') : player.name)}
-
${player.howManyCards}
+
`, @@ -256,6 +259,17 @@ function (dojo, declare) { }); this.setupNumberOfCardsInPlayersHand(); + // show 2P mode items + if (this.is2PlayersMode()) { + dojo.place( + `
DE
CK
+
`, + DOM_ID_BOARD_CARPET + ); + + this.setupAttackRewardCards(gamedatas.attackRewardCards); + } + // @TODO: support spectators (do not show "my hand" in this case) // Init playerHand "ebg.stock" component this.playerHand = new ebg.stock(); @@ -332,6 +346,9 @@ function (dojo, declare) { case 'playerSelectNextPlayer': this.setupSelectPlayerAction(data.args.activePlayerId, data.args.selectablePlayers, this.onSelectNextPlayer); break; + case 'playerSelectWhoTakeAttackReward': + this.setupSelectPlayerAction(data.args.activePlayerId, data.args.selectablePlayers, this.onSelectWhoTakeAttackReward); + break; case 'playerSelectPlayerToPickCards': this.setupSelectPlayerAction(data.args.activePlayerId, data.args.selectablePlayers, this.onSelectPlayerToPickCards); break; @@ -350,6 +367,7 @@ function (dojo, declare) { // do special things for state switch (state) { case 'playerSelectNextPlayer': + case 'playerSelectWhoTakeAttackReward': case 'playerSelectPlayerToPickCards': Object.entries(this.players).forEach((entry) => { const player = entry[1]; @@ -1262,7 +1280,7 @@ function (dojo, declare) { // create played cards const stackWith = ((stackedCards.length - 1) * (CARD_WIDTH / 3)) + CARD_WIDTH; dojo.place( - `
`, + `
`, placeDomId ); stackedCards.forEach((card) => { @@ -1277,6 +1295,110 @@ function (dojo, declare) { return topOfStackCardId; }, + /** + * @param {Object[]} cards + * @param {string} domId + */ + moveTemporaryCardsFromDomIdToPlayerHand: function (cards, domId) { + if (cards.length <= 0) { + return; + } + + let animations = []; + for (let i = 0; i < cards.length; i++) { + const position = this.getCardPositionInSpriteByColorAndValue(cards[i].color, cards[i].value); + const backgroundPositionX = this.getAbsoluteCardBackgroundPositionXFromCardPosition(position); + const backgroundPositionY = this.getAbsoluteCardBackgroundPositionYFromCardPosition(position); + animations[i] = this.slideTemporaryObject( + `
`, + domId, + domId, + `player-table-${this.player_id}-hand`, + 1000, + i * 1000 + ); + dojo.connect(animations[i], 'onEnd', () => { + this.playerHand.addToStockWithId(position, cards[i].id); + }); + animations[i].play(); + } + }, + /** + * @param {string} fromDomId + * @param {string} toDomId + * @param {number} numberOfCards + */ + moveHiddenTemporaryCardsFromDomIdToDomId: function (fromDomId, toDomId, numberOfCards) { + for (let i = 0; i < numberOfCards; i++) { + this.slideTemporaryObject( + `
`, + fromDomId, + fromDomId, + toDomId, + 1000, + i * 1000 + ).play(); + } + }, + /** + * /!\ 2P mode only + * + * @param {Object[]} cards + */ + moveCardsFromDeckToPlayerHand: function (cards) { + this.moveTemporaryCardsFromDomIdToPlayerHand(cards, DOM_ID_CARDS_DECK); + }, + /** + * /!\ 2P mode only + * + * @param {number} receiverId + * @param {number} numberOfCards + */ + moveCardsFromDeckToAnotherPlayer: function (receiverId, numberOfCards) { + this.moveHiddenTemporaryCardsFromDomIdToDomId(DOM_ID_CARDS_DECK, `player-table-${receiverId}-hand`, numberOfCards); + }, + /** + * /!\ 2P mode only + * + * @param {Object[]} cards + */ + setupAttackRewardCards: function (cards) { + if (cards.length <= 0) { + return; + } + + const topOfStackCardId = this.buildAndPlacePlayedStackOfCards(cards, DOM_ID_ATTACK_REWARD_CARD); + + // place cards from where the animation will start + this.placeOnObject(`cards-stack-${topOfStackCardId}`, DOM_ID_CARDS_DECK); + + // move cards to their destination + this.slideToObject(`cards-stack-${topOfStackCardId}`, DOM_ID_ATTACK_REWARD_CARD).play(); + }, + /** + * /!\ 2P mode only + * + * @param {number} receiverPlayerId + * @param {Object[]} cards + */ + moveAttackRewardCardsToPlayer: function (receiverPlayerId, cards) { + if (cards.length <= 0) { + return; + } + + const rewardCardDomId = `cards-stack-${cards[0].id}`; + const animation = this.slideToObject( + rewardCardDomId, + `player-table-${receiverPlayerId}-hand` + ); + dojo.connect(animation, 'onEnd', () => { + if (receiverPlayerId === this.player_id) { + this.addCardsToPlayerHand(cards); + } + this.fadeOutAndDestroy(rewardCardDomId); + }); + animation.play(); + }, /** * @param {Object[]} cards */ @@ -1327,28 +1449,7 @@ function (dojo, declare) { * @param {Object[]} cards */ receiveCardsFromAnotherPlayer: function (senderId, cards) { - if (cards.length <= 0) { - return; - } - - let animations = []; - for (let i = 0; i < cards.length; i++) { - const position = this.getCardPositionInSpriteByColorAndValue(cards[i].color, cards[i].value); - const backgroundPositionX = this.getAbsoluteCardBackgroundPositionXFromCardPosition(position); - const backgroundPositionY = this.getAbsoluteCardBackgroundPositionYFromCardPosition(position); - animations[i] = this.slideTemporaryObject( - `
`, - `player-table-${senderId}-hand`, - `player-table-${senderId}-hand`, - `player-table-${this.player_id}-hand`, - 1000, - i * 1000 - ); - dojo.connect(animations[i], 'onEnd', () => { - this.playerHand.addToStockWithId(position, cards[i].id); - }); - animations[i].play(); - } + this.moveTemporaryCardsFromDomIdToPlayerHand(cards, `player-table-${senderId}-hand`); }, /** * @param {number} receiverId @@ -1401,16 +1502,25 @@ function (dojo, declare) { * @param {number} numberOfCards */ moveCardsBetweenTwoOtherPlayers: function (senderId, receiverId, numberOfCards) { - for (let i = 0; i < numberOfCards; i++) { - this.slideTemporaryObject( - `
`, - `player-table-${senderId}-hand`, - `player-table-${senderId}-hand`, - `player-table-${receiverId}-hand`, - 1000, - i * 1000 - ).play(); - } + this.moveHiddenTemporaryCardsFromDomIdToDomId(`player-table-${senderId}-hand`, `player-table-${receiverId}-hand`, numberOfCards); + }, + /** + * @param {number} playerId + * @param {number} numberOfCards + */ + increaseNumberOfCardsOfPlayer: function (playerId, numberOfCards) { + this.players[playerId].howManyCards = this.players[playerId].howManyCards + numberOfCards; + + this.setupNumberOfCardsInPlayersHand(); + }, + /** + * @param {number} playerId + * @param {number} numberOfCards + */ + decreaseNumberOfCardsOfPlayer: function (playerId, numberOfCards) { + this.players[playerId].howManyCards = this.players[playerId].howManyCards - numberOfCards; + + this.setupNumberOfCardsInPlayersHand(); }, /** * @param {Object[]} cards @@ -1456,9 +1566,10 @@ function (dojo, declare) { animation.play(); }); }, - discardCards: function () { + discardPlayedCards: function () { this.playedCardsValue = 0; - dojo.query(`.${DOM_CLASS_CARDS_STACK}`).forEach(dojo.destroy); + // @TODO: move to discarded cards stack + dojo.query(`#${DOM_ID_PLAYED_CARDS_WRAPPER} .${DOM_CLASS_CARDS_STACK}`).forEach(dojo.destroy); }, discardPlayerSpeechBubbles: function () { dojo.query(`.${DOM_CLASS_PLAYER_SPEECH_BUBBLE_SHOW}`).forEach((elementDomId) => { @@ -1507,6 +1618,21 @@ function (dojo, declare) { selectedPlayerId: selectedPlayerId, }); }, + /** + * @param {number} selectedPlayerId + */ + onSelectWhoTakeAttackReward: function (selectedPlayerId) { + if ( + !this.is2PlayersMode() + || !this.checkAction('selectWhoTakeAttackReward') + ) { + return; + } + + this.requestAction('selectWhoTakeAttackReward', { + selectedPlayerId: selectedPlayerId, + }); + }, /** * @param {number} selectedPlayerId */ @@ -1558,8 +1684,8 @@ function (dojo, declare) { /////////////////////////////////////////////////// setupNotifications: function () { [ - ['roundStarted', 1], ['cardsDealt', 1], + ['roundStarted', 1], ['cardsPlayed', 1000], ['cardsDiscarded', 1], ['cardsReceivedFromAnotherPlayer', 1000], @@ -1567,8 +1693,8 @@ function (dojo, declare) { ['cardsMovedBetweenTwoOtherPlayers', 1000], ['roundEnded', 1], /* START 2P */ - ['attackRewardCardsRevealed', 1000], ['attackRewardCardsMovedToPlayer', 1000], + ['attackRewardCardsRevealed', 1000], ['cardsReceivedFromDeck', 1000], ['cardsMovedFromDeckToAnotherPlayer', 1000], /* END 2P */ @@ -1580,12 +1706,6 @@ function (dojo, declare) { this.notifqueue.setSynchronous(name, lockDurationInMs); }); }, - notif_roundStarted: function (data) { - this.currentRound = data.args.currentRound; - this.players = data.args.players; - this.setupRoundsInfo(); - this.setupNumberOfCardsInPlayersHand(); - }, notif_cardsDealt: function (data) { this.playerHand.removeAll(); this.addCardsToPlayerHand(data.args.cards); @@ -1596,6 +1716,12 @@ function (dojo, declare) { this.addJerseyToPlayerHand(); } }, + notif_roundStarted: function (data) { + this.currentRound = data.args.currentRound; + this.players = data.args.players; + this.setupRoundsInfo(); + this.setupNumberOfCardsInPlayersHand(); + }, notif_cardsPlayed: function (data) { this.discardPlayerSpeechBubbles(); this.movePlayedCardsToPreviousPlayedCards(); @@ -1608,45 +1734,37 @@ function (dojo, declare) { this.moveCardsFromPlayerHandToTable(data.args.playedCardsPlayerId, playedCardsWithJersey); // update number of cards in players hand - this.players[data.args.playedCardsPlayerId].howManyCards = this.players[data.args.playedCardsPlayerId].howManyCards - data.args.playedCards.length; + this.decreaseNumberOfCardsOfPlayer(data.args.playedCardsPlayerId, data.args.playedCards.length) // update jersey state if it has been used if (data.args.withJersey) { this.useJerseyForCurrentRound(); } - - this.setupNumberOfCardsInPlayersHand(); }, notif_cardsDiscarded: function (data) { this.discardPlayerSpeechBubbles(); - this.discardCards(); + this.discardPlayedCards(); }, notif_cardsReceivedFromAnotherPlayer: function (data) { - this.players[data.args.senderPlayerId].howManyCards = this.players[data.args.senderPlayerId].howManyCards - data.args.cards.length; + this.decreaseNumberOfCardsOfPlayer(data.args.senderPlayerId, data.args.cards.length); this.receiveCardsFromAnotherPlayer(data.args.senderPlayerId, data.args.cards); - this.players[this.player_id].howManyCards = this.players[this.player_id].howManyCards + data.args.cards.length; - - this.setupNumberOfCardsInPlayersHand(); + this.increaseNumberOfCardsOfPlayer(this.player_id, data.args.cards.length); }, notif_cardsSentToAnotherPlayer: function (data) { - this.players[this.player_id].howManyCards = this.players[this.player_id].howManyCards - data.args.cards.length; + this.decreaseNumberOfCardsOfPlayer(this.player_id, data.args.cards.length); this.sendCardsToAnotherPlayer(data.args.receiverPlayerId, data.args.cards); - this.players[data.args.receiverPlayerId].howManyCards = this.players[data.args.receiverPlayerId].howManyCards + data.args.cards.length; - - this.setupNumberOfCardsInPlayersHand(); + this.increaseNumberOfCardsOfPlayer(data.args.receiverPlayerId, data.args.cards.length); }, notif_cardsMovedBetweenTwoOtherPlayers: function (data) { - this.players[data.args.senderPlayerId].howManyCards = this.players[data.args.senderPlayerId].howManyCards - data.args.numberOfCards; + this.decreaseNumberOfCardsOfPlayer(data.args.senderPlayerId, data.args.numberOfCards); this.moveCardsBetweenTwoOtherPlayers(data.args.senderPlayerId, data.args.receiverPlayerId, data.args.numberOfCards); - this.players[data.args.receiverPlayerId].howManyCards = this.players[data.args.receiverPlayerId].howManyCards + data.args.numberOfCards; - - this.setupNumberOfCardsInPlayersHand(); + this.increaseNumberOfCardsOfPlayer(data.args.receiverPlayerId, data.args.numberOfCards); }, notif_roundEnded: function (data) { const currentJerseyWearerId = this.getCurrentJerseyWearerIdIfExists(); @@ -1660,28 +1778,34 @@ function (dojo, declare) { this.moveJerseyToCurrentWinner(currentJerseyWearerId); }, /** - * 2P mode + * /!\ 2P mode only */ - notif_attackRewardCardsRevealed: function (data) { - // @TODO + notif_attackRewardCardsMovedToPlayer: function (data) { + this.moveAttackRewardCardsToPlayer(data.args.receiverPlayerId, data.args.cards); + + this.increaseNumberOfCardsOfPlayer(data.args.receiverPlayerId, data.args.cards.length); }, /** - * 2P mode + * /!\ 2P mode only */ - notif_attackRewardCardsMovedToPlayer: function (data) { - // @TODO + notif_attackRewardCardsRevealed: function (data) { + this.setupAttackRewardCards(data.args.cards); }, /** - * 2P mode + * /!\ 2P mode only */ notif_cardsReceivedFromDeck: function (data) { - // @TODO + this.moveCardsFromDeckToPlayerHand(data.args.cards); + + this.increaseNumberOfCardsOfPlayer(this.player_id, data.args.numberOfCards); }, /** - * 2P mode + * /!\ 2P mode only */ notif_cardsMovedFromDeckToAnotherPlayer: function (data) { - // @TODO + this.moveCardsFromDeckToAnotherPlayer(data.args.receiverPlayerId, data.args.numberOfCards); + + this.increaseNumberOfCardsOfPlayer(data.args.receiverPlayerId, data.args.numberOfCards); }, }); });