From f806ccf2a1258baa51858da58d98c835310a697a Mon Sep 17 00:00:00 2001 From: Oliver THEBAULT Date: Thu, 16 Jun 2022 21:21:38 +0200 Subject: [PATCH] Speech bubble + min/max played stats (#3) * feat: add speech bubble + stats min/max values --- stats.inc.php | 30 ++++---- velonimo.css | 92 ++++++++++++++++++++-- velonimo.game.php | 80 +++++++++++++++---- velonimo.js | 192 ++++++++++++++++++++++++++++++---------------- 4 files changed, 290 insertions(+), 104 deletions(-) diff --git a/stats.inc.php b/stats.inc.php index 2ddaae7..c5e9968 100644 --- a/stats.inc.php +++ b/stats.inc.php @@ -50,33 +50,33 @@ $stats_type = [ // Statistics global to table 'table' => [ - 'playCardsAction' => [ - 'id' => 10, - 'name' => totranslate('Played one or more cards'), + 'minValue' => [ + 'id' => 13, + 'name' => totranslate('Minimum value played'), 'type' => 'int', ], - 'passTurnAction' => [ - 'id' => 11, - 'name' => totranslate('Turns passed'), + 'maxValue' => [ + 'id' => 14, + 'name' => totranslate('Maximum value played'), 'type' => 'int', ], ], // Statistics existing for each player 'player' => [ - 'playCardsAction' => [ - 'id' => 10, - 'name' => totranslate('Played one or more cards'), + 'numberOfRoundsWon' => [ + 'id' => 12, + 'name' => totranslate('Rounds won'), 'type' => 'int', ], - 'passTurnAction' => [ - 'id' => 11, - 'name' => totranslate('Turns passed'), + 'minValue' => [ + 'id' => 13, + 'name' => totranslate('Minimum value played'), 'type' => 'int', ], - 'numberOfRoundsWon' => [ - 'id' => 12, - 'name' => totranslate('Rounds won'), + 'maxValue' => [ + 'id' => 14, + 'name' => totranslate('Maximum value played'), 'type' => 'int', ], ], diff --git a/velonimo.css b/velonimo.css index 333284e..057a16d 100644 --- a/velonimo.css +++ b/velonimo.css @@ -69,6 +69,7 @@ Board position: absolute; top: 0; left: 2px; + text-align: center; border: 1px solid black; border-radius: 8px; } @@ -79,7 +80,7 @@ Board #board-carpet { position: relative; width: 740px; - height: 450px; + height: 480px; background-color: #35714a; box-shadow: 2px 2px 5px black; border: 1px solid #3b3119; @@ -99,6 +100,7 @@ Board box-sizing: border-box; background-color: rgba(255,255,255,0.7); padding: 5px; + z-index: 2; } .player-table.active { border: 2px solid #ff0000; @@ -141,11 +143,49 @@ Board width: 1em; pointer-events: none; } -.player-table-cards { - position: absolute; /* position is dynamically computed */ - width: 130px; /* player table width */ - height: 126px; /* card height */ +.player-table-speech-bubble { + position: absolute; + top: 0; + width: 50px; + height: 40px; + border-radius: 5px; + background-color: #ffffff; + font-size: 2.2em; + text-align: center; + font-weight: bold; + opacity: 0; pointer-events: none; + transition-property: opacity; + transition-duration: 0.4s; +} +.player-table-speech-bubble::after { + content: ""; + position: absolute; + top: 30px; + width: 0; + height: 0; + border-left: 5px solid transparent; + border-right: 5px solid transparent; + border-top: 20px solid #ffffff; +} +.player-table-speech-bubble.show-bubble { + opacity: 1; +} +.speech-bubble-on-left { + left: -70px; + box-shadow: -2px 2px 5px 0 black; +} +.speech-bubble-on-left::after { + right: -10px; + transform: rotate(-60deg); +} +.speech-bubble-on-right { + right: -70px; + box-shadow: 2px 2px 5px 0 black; +} +.speech-bubble-on-right::after { + left: -10px; + transform: rotate(60deg); } .player-table.is-wearing-jersey .player-table-jersey { position: absolute; @@ -175,7 +215,7 @@ Board background: black; transform: rotate(45deg); } -.player-table.is-wearing-jersey.has-used-jersey .jersey-overlay:after { +.player-table.is-wearing-jersey.has-used-jersey .jersey-overlay::after { content: ""; position: absolute; left: -20px; @@ -189,7 +229,37 @@ END Board */ /** -Current player +Board cards + */ +#played-cards { + position: relative; + top: 166px; /* (containerHeight / 2) - (selfHeight / 2) */ + left: 235px; /* (containerWidth / 2) - (selfWidth / 2) */ + display: flex; + align-items: center; + justify-content: center; + width: 270px; /* cardWidth + (6 * (cardWidth / 3)) */ + height: 148px; /* cardHeight + (cardHeight / 6) */ + z-index: 4; +} +#last-played-cards { + position: absolute; + bottom: 0; + height: 126px; /* cardHeight */ + z-index: 10; +} +#previous-last-played-cards { + position: absolute; + top: 0; + height: 126px; /* cardHeight */ + z-index: 8; +} +/** +END Board cards + */ + +/** +Current player hand */ .spectatorMode #my-hand-wrapper { display: none; @@ -227,7 +297,7 @@ Current player border: 1px solid gray !important; } /** -END Current player +END Current player hand */ /** @@ -252,6 +322,12 @@ Cards display: flex; flex-direction: row; flex-wrap: nowrap; + opacity: 1; + transition-property: opacity; + transition-duration: 0.5s; +} +.cards-stack.previous-last-played-cards { + opacity: 0.2; } .cards-stack .velonimo-card { margin-right: -60px; /* 2/3 of card width */ diff --git a/velonimo.game.php b/velonimo.game.php index 5dbcb99..da8a9bd 100644 --- a/velonimo.game.php +++ b/velonimo.game.php @@ -42,6 +42,7 @@ class Velonimo extends Table private const CARD_LOCATION_PLAYER_HAND = 'hand'; private const CARD_LOCATION_DISCARD = 'discard'; private const CARD_LOCATION_PLAYED = 'played'; + private const CARD_LOCATION_PREVIOUS_PLAYED = 'previousPlayed'; /** @var Deck (BGA framework component to manage cards) */ private $deck; @@ -128,11 +129,11 @@ protected function setupNewGame($players, $options = []) { // Init game statistics // (note: statistics used in this file must be defined in your stats.inc.php file) // Table statistics - self::initStat('table', 'playCardsAction', 0); - self::initStat('table', 'passTurnAction', 0); + self::initStat('table', 'minValue', 0); + self::initStat('table', 'maxValue', 0); // Player statistics (init for all players) - self::initStat('player', 'playCardsAction', 0); - self::initStat('player', 'passTurnAction', 0); + self::initStat('player', 'minValue', 0); + self::initStat('player', 'maxValue', 0); self::initStat('player', 'numberOfRoundsWon', 0); // Create cards @@ -213,6 +214,9 @@ protected function getAllDatas() { ); $result['playedCardsValue'] = (int) self::getGameStateValue(self::GAME_STATE_LAST_PLAYED_CARDS_VALUE); $result['playedCardsPlayerId'] = (int) self::getGameStateValue(self::GAME_STATE_LAST_PLAYED_CARDS_PLAYER_ID); + $result['previousPlayedCards'] = $this->formatCardsForClient( + $this->fromBgaCardsToVelonimoCards($this->deck->getCardsInLocation(self::CARD_LOCATION_PREVIOUS_PLAYED)) + ); return $result; } @@ -229,7 +233,7 @@ protected function getAllDatas() { */ function getGameProgression() { $howManyRounds = (int) self::getGameStateValue(self::GAME_OPTION_HOW_MANY_ROUNDS); - $currentRound = (int) self::getGameStateValue(self::GAME_STATE_CURRENT_ROUND); + $currentRound = ((int) self::getGameStateValue(self::GAME_STATE_CURRENT_ROUND)) ?: 1; return ((int) floor(($currentRound - 1) / $howManyRounds)) * 100; } @@ -323,15 +327,14 @@ function playCards(array $playedCardIds, bool $cardsPlayedWithJersey) { } // discard table cards and play cards - $this->deck->moveAllCardsInLocation(self::CARD_LOCATION_PLAYED, self::CARD_LOCATION_DISCARD); + $this->deck->moveAllCardsInLocation(self::CARD_LOCATION_PREVIOUS_PLAYED, self::CARD_LOCATION_DISCARD); + $this->deck->moveAllCardsInLocation(self::CARD_LOCATION_PLAYED, self::CARD_LOCATION_PREVIOUS_PLAYED); $this->deck->moveCards($playedCardIds, self::CARD_LOCATION_PLAYED, $currentPlayerId); if ($cardsPlayedWithJersey) { self::setGameStateValue(self::GAME_STATE_JERSEY_HAS_BEEN_USED_IN_THE_CURRENT_ROUND, 1); } self::setGameStateValue(self::GAME_STATE_LAST_PLAYED_CARDS_PLAYER_ID, $currentPlayerId); self::setGameStateValue(self::GAME_STATE_LAST_PLAYED_CARDS_VALUE, $playedCardsValue); - self::incStat(1, 'playCardsAction'); - self::incStat(1, 'playCardsAction', $currentPlayerId); self::notifyAllPlayers('cardsPlayed', clienttranslate('${playerName} plays ${playedCardsValue}'), [ 'playedCardsPlayerId' => $currentPlayerId, 'playedCards' => $this->formatCardsForClient($playedCards), @@ -340,6 +343,18 @@ function playCards(array $playedCardIds, bool $cardsPlayedWithJersey) { 'withJersey' => $cardsPlayedWithJersey, ]); + // update player's min/max value played + $previousMinValuePlayedByPlayer = (int) $this->getStat('minValue', $currentPlayerId); + if ( + $previousMinValuePlayedByPlayer === 0 + || $playedCardsValue < $previousMinValuePlayedByPlayer + ) { + $this->setStat($playedCardsValue, 'minValue', $currentPlayerId); + } + if ($playedCardsValue > (int) $this->getStat('maxValue', $currentPlayerId)) { + $this->setStat($playedCardsValue, 'maxValue', $currentPlayerId); + } + // if the player played his last card, set its rank for this round if ((count($currentPlayerCards) - $numberOfPlayedCards) === 0) { $currentRound = (int) self::getGameStateValue(self::GAME_STATE_CURRENT_ROUND); @@ -387,9 +402,6 @@ function playCards(array $playedCardIds, bool $cardsPlayedWithJersey) { function passTurn() { self::checkAction('passTurn'); - $this->incStat(1, 'passTurnAction'); - $this->incStat(1, 'passTurnAction', (int) self::getCurrentPlayerId()); - self::notifyAllPlayers('turnPassed', clienttranslate('${playerName} passes'), [ 'playerName' => self::getCurrentPlayerName(), ]); @@ -753,9 +765,14 @@ function stEndRound() { $numberOfPlayers = count($players); $currentRound = (int) self::getGameStateValue(self::GAME_STATE_CURRENT_ROUND); $numberOfPointsForRoundByPlayerId = []; + $winnerOfCurrentRound = null; foreach ($players as $k => $player) { + $playerCurrentRoundRank = $player->getLastRoundRank(); + if ($playerCurrentRoundRank === 1) { + $winnerOfCurrentRound = $player; + } $numberOfPointsForRoundByPlayerId[$player->getId()] = $this->getNumberOfPointsAtRankForRound( - $player->getLastRoundRank(), + $playerCurrentRoundRank, $currentRound, $numberOfPlayers ); @@ -780,11 +797,23 @@ function stEndRound() { // re-allow the jersey to be used self::setGameStateValue(self::GAME_STATE_JERSEY_HAS_BEEN_USED_IN_THE_CURRENT_ROUND, 0); - self::notifyAllPlayers('roundEnded', 'Round #${currentRound} ends', [ + self::notifyAllPlayers('roundEnded', 'Round #${currentRound} has been won by ${playerName}', [ 'currentRound' => $currentRound, + 'playerName' => $winnerOfCurrentRound ? $winnerOfCurrentRound->getName() : 'N/A', 'players' => $this->formatPlayersForClient($players), ]); + // notify points earned by each player + foreach ($players as $player) { + $translatedMessage = ($numberOfPointsForRoundByPlayerId[$player->getId()] > 0) + ? clienttranslate('${playerName} does not get any point') + : clienttranslate('${playerName} wins ${points} points'); + self::notifyAllPlayers('pointsWon', $translatedMessage, [ + 'playerName' => $player->getName(), + 'points' => $numberOfPointsForRoundByPlayerId[$player->getId()], + ]); + } + $howManyRounds = (int) self::getGameStateValue(self::GAME_OPTION_HOW_MANY_ROUNDS); $isGameOver = $currentRound >= $howManyRounds; @@ -831,6 +860,24 @@ function stEndRound() { 'closing' => $isGameOver ? clienttranslate('End of game') : clienttranslate('Next round') )); + // update global min/max value played stats + if ($isGameOver) { + $minValuePlayedGlobally = 1000; + $maxValuePlayedGlobally = 0; + foreach ($players as $player) { + $minValuePlayedByPlayer = (int) $this->getStat('minValue', $player->getId()); + if ($minValuePlayedByPlayer < $minValuePlayedGlobally) { + $minValuePlayedGlobally = $minValuePlayedByPlayer; + } + $maxValuePlayedByPlayer = (int) $this->getStat('maxValue', $player->getId()); + if ($maxValuePlayedByPlayer > $maxValuePlayedGlobally) { + $maxValuePlayedGlobally = $maxValuePlayedByPlayer; + } + } + $this->setStat($minValuePlayedGlobally, 'minValue'); + $this->setStat($maxValuePlayedGlobally, 'maxValue'); + } + // go to next round or end the game $this->gamestate->nextState($isGameOver ? 'endGame' : 'nextRound'); } @@ -973,7 +1020,9 @@ private function getCardsValue(array $cards, bool $withJersey): int { * @return VelonimoPlayer[] */ private function getPlayersFromDatabase(): array { - $players = array_values(self::getCollectionFromDB('SELECT player_id, player_no, player_name, player_color, player_score, rounds_ranking, is_wearing_jersey FROM player')); + $players = array_values(self::getCollectionFromDB( + 'SELECT player_id, player_no, player_name, player_color, player_score, rounds_ranking, is_wearing_jersey FROM player' + )); return array_map( fn (array $player) => new VelonimoPlayer( @@ -983,7 +1032,7 @@ private function getPlayersFromDatabase(): array { $player['player_color'], (int) $player['player_score'], VelonimoPlayer::deserializeRoundsRanking($player['rounds_ranking']), - ((int) $player['is_wearing_jersey']) === 1, + ((int) $player['is_wearing_jersey']) === 1 ), $players ); @@ -1148,6 +1197,7 @@ private function getCurrentLoser(array $players): ?VelonimoPlayer { } private function discardLastPlayedCards(): void { + $this->deck->moveAllCardsInLocation(self::CARD_LOCATION_PREVIOUS_PLAYED, self::CARD_LOCATION_DISCARD); $this->deck->moveAllCardsInLocation(self::CARD_LOCATION_PLAYED, self::CARD_LOCATION_DISCARD); self::setGameStateValue(self::GAME_STATE_LAST_PLAYED_CARDS_VALUE, 0); self::setGameStateValue(self::GAME_STATE_LAST_PLAYED_CARDS_PLAYER_ID, 0); diff --git a/velonimo.js b/velonimo.js index 458fa78..61979ca 100644 --- a/velonimo.js +++ b/velonimo.js @@ -62,8 +62,10 @@ const JERSEY_VALUE = 10; // DOM IDs const DOM_ID_APP = 'velonimo-game'; -const DOM_ID_BOARD = 'board'; const DOM_ID_BOARD_CARPET = 'board-carpet'; +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'; const DOM_ID_PLAYER_HAND = 'my-hand'; const DOM_ID_PLAYER_HAND_TOGGLE_SORT_BUTTON = 'toggle-sort-button'; const DOM_ID_PLAYER_HAND_TOGGLE_SORT_BUTTON_LABEL = 'toggle-sort-button-label'; @@ -75,14 +77,18 @@ const DOM_ID_ACTION_BUTTON_SELECT_PLAYER = 'action-button-select-player'; const DOM_ID_ACTION_BUTTON_GIVE_CARDS = 'action-button-give-cards'; // DOM classes -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_DISABLED_ACTION_BUTTON = 'disabled' -const DOM_CLASS_ACTIVE_PLAYER = 'active' -const DOM_CLASS_SELECTABLE_PLAYER = 'selectable' -const DOM_CLASS_NON_SELECTABLE_CARD = 'non-selectable-player-card' +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_CARDS_STACK_PREVIOUS_PLAYED = 'previous-last-played-cards'; +const DOM_CLASS_DISABLED_ACTION_BUTTON = 'disabled'; +const DOM_CLASS_ACTIVE_PLAYER = 'active'; +const DOM_CLASS_SELECTABLE_PLAYER = 'selectable'; +const DOM_CLASS_NON_SELECTABLE_CARD = 'non-selectable-player-card'; +const DOM_CLASS_PLAYER_SPEECH_BUBBLE_SHOW = 'show-bubble'; +const DOM_CLASS_SPEECH_BUBBLE_LEFT = 'speech-bubble-on-left'; +const DOM_CLASS_SPEECH_BUBBLE_RIGHT = 'speech-bubble-on-right'; // Player hand sorting modes const PLAYER_HAND_SORT_BY_COLOR = 'color'; @@ -96,94 +102,87 @@ const CARD_HEIGHT = 126; const PLAYER_TABLE_WIDTH = 130; const PLAYER_TABLE_HEIGHT = 130; const PLAYER_TABLE_BORDER_SIZE = 2; -const MARGIN_BETWEEN_PLAYERS = 20; -const TABLE_STYLE_HORIZONTAL_LEFT = `left: ${MARGIN_BETWEEN_PLAYERS}px;`; -const TABLE_STYLE_HORIZONTAL_CENTER = `left: ${(BOARD_CARPET_WIDTH / 2) - (PLAYER_TABLE_WIDTH / 2) - (MARGIN_BETWEEN_PLAYERS / 2)}px;`; -const TABLE_STYLE_HORIZONTAL_RIGHT = `right: ${MARGIN_BETWEEN_PLAYERS}px;`; -const TABLE_STYLE_VERTICAL_TOP = `top: ${MARGIN_BETWEEN_PLAYERS}px;`; -const TABLE_STYLE_VERTICAL_BOTTOM = `bottom: ${MARGIN_BETWEEN_PLAYERS}px;`; -const CARDS_STYLE_ABOVE_TABLE = `top: -${CARD_HEIGHT + PLAYER_TABLE_BORDER_SIZE}px; left: -${PLAYER_TABLE_BORDER_SIZE}px;`; -const CARDS_STYLE_BELOW_TABLE = `bottom: -${CARD_HEIGHT + PLAYER_TABLE_BORDER_SIZE}px; left: -${PLAYER_TABLE_BORDER_SIZE}px;`; +const BOARD_MARGIN = 10; +const TABLE_STYLE_HORIZONTAL_LEFT = `left: ${BOARD_MARGIN}px;`; +const TABLE_STYLE_HORIZONTAL_MIDDLE_LEFT = `left: ${BOARD_MARGIN + PLAYER_TABLE_WIDTH}px;`; +const TABLE_STYLE_HORIZONTAL_CENTER = `left: ${(BOARD_CARPET_WIDTH / 2) - (PLAYER_TABLE_WIDTH / 2)}px;`; +const TABLE_STYLE_HORIZONTAL_MIDDLE_RIGHT = `right: ${BOARD_MARGIN + PLAYER_TABLE_WIDTH}px;`; +const TABLE_STYLE_HORIZONTAL_RIGHT = `right: ${BOARD_MARGIN}px;`; +const TABLE_STYLE_VERTICAL_TOP = `top: ${BOARD_MARGIN}px;`; +const TABLE_STYLE_VERTICAL_BOTTOM = `bottom: ${BOARD_MARGIN}px;`; // the current player (index 0 == current player) place is always at the bottom of the board, in a way that players always stay closed to their hand const PLAYERS_PLACES_BY_NUMBER_OF_PLAYERS = { 2: { 0: { tableStyle: `${TABLE_STYLE_VERTICAL_BOTTOM} ${TABLE_STYLE_HORIZONTAL_CENTER}`, - cardsStyle: CARDS_STYLE_ABOVE_TABLE, + bubbleClass: DOM_CLASS_SPEECH_BUBBLE_LEFT, }, 1: { tableStyle: `${TABLE_STYLE_VERTICAL_TOP} ${TABLE_STYLE_HORIZONTAL_CENTER}`, - cardsStyle: CARDS_STYLE_BELOW_TABLE, + bubbleClass: DOM_CLASS_SPEECH_BUBBLE_RIGHT, }, }, 3: { 0: { - tableStyle: `${TABLE_STYLE_VERTICAL_BOTTOM} ${TABLE_STYLE_HORIZONTAL_CENTER}`, - cardsStyle: CARDS_STYLE_ABOVE_TABLE, + tableStyle: `${TABLE_STYLE_VERTICAL_BOTTOM} ${TABLE_STYLE_HORIZONTAL_MIDDLE_LEFT}`, + bubbleClass: DOM_CLASS_SPEECH_BUBBLE_LEFT, }, 1: { - tableStyle: `${TABLE_STYLE_VERTICAL_TOP} ${TABLE_STYLE_HORIZONTAL_LEFT}`, - cardsStyle: CARDS_STYLE_BELOW_TABLE, + tableStyle: `${TABLE_STYLE_VERTICAL_TOP} ${TABLE_STYLE_HORIZONTAL_CENTER}`, + bubbleClass: DOM_CLASS_SPEECH_BUBBLE_RIGHT, }, 2: { - tableStyle: `${TABLE_STYLE_VERTICAL_TOP} ${TABLE_STYLE_HORIZONTAL_RIGHT}`, - cardsStyle: CARDS_STYLE_BELOW_TABLE, + tableStyle: `${TABLE_STYLE_VERTICAL_BOTTOM} ${TABLE_STYLE_HORIZONTAL_MIDDLE_RIGHT}`, + bubbleClass: DOM_CLASS_SPEECH_BUBBLE_RIGHT, }, }, - // @TODO: place player 1 and player 3 at the center left and the center right - // and move their played cards to the right and the left respectively 4: { 0: { - tableStyle: `${TABLE_STYLE_VERTICAL_BOTTOM} ${TABLE_STYLE_HORIZONTAL_CENTER}`, - cardsStyle: CARDS_STYLE_ABOVE_TABLE, + tableStyle: `${TABLE_STYLE_VERTICAL_BOTTOM} ${TABLE_STYLE_HORIZONTAL_MIDDLE_LEFT}`, + bubbleClass: DOM_CLASS_SPEECH_BUBBLE_LEFT, }, 1: { - tableStyle: `${TABLE_STYLE_VERTICAL_TOP} ${TABLE_STYLE_HORIZONTAL_LEFT}`, - cardsStyle: CARDS_STYLE_BELOW_TABLE, + tableStyle: `${TABLE_STYLE_VERTICAL_TOP} ${TABLE_STYLE_HORIZONTAL_MIDDLE_LEFT}`, + bubbleClass: DOM_CLASS_SPEECH_BUBBLE_LEFT, }, 2: { - tableStyle: `${TABLE_STYLE_VERTICAL_TOP} ${TABLE_STYLE_HORIZONTAL_CENTER}`, - cardsStyle: CARDS_STYLE_BELOW_TABLE, + tableStyle: `${TABLE_STYLE_VERTICAL_TOP} ${TABLE_STYLE_HORIZONTAL_MIDDLE_RIGHT}`, + bubbleClass: DOM_CLASS_SPEECH_BUBBLE_RIGHT, }, 3: { - tableStyle: `${TABLE_STYLE_VERTICAL_TOP} ${TABLE_STYLE_HORIZONTAL_RIGHT}`, - cardsStyle: CARDS_STYLE_BELOW_TABLE, + tableStyle: `${TABLE_STYLE_VERTICAL_BOTTOM} ${TABLE_STYLE_HORIZONTAL_MIDDLE_RIGHT}`, + bubbleClass: DOM_CLASS_SPEECH_BUBBLE_RIGHT, }, }, - // @TODO: place player 1 and player 4 at the center left and the center right - // and move their played cards to the right and the left respectively - // + place player 2 and 3 at the top middle-left and top middle-right respectively 5: { 0: { tableStyle: `${TABLE_STYLE_VERTICAL_BOTTOM} ${TABLE_STYLE_HORIZONTAL_LEFT}`, - cardsStyle: CARDS_STYLE_ABOVE_TABLE, + bubbleClass: DOM_CLASS_SPEECH_BUBBLE_RIGHT, }, 1: { - tableStyle: `${TABLE_STYLE_VERTICAL_TOP} ${TABLE_STYLE_HORIZONTAL_CENTER}`, - cardsStyle: CARDS_STYLE_BELOW_TABLE, + tableStyle: `${TABLE_STYLE_VERTICAL_TOP} ${TABLE_STYLE_HORIZONTAL_MIDDLE_LEFT}`, + bubbleClass: DOM_CLASS_SPEECH_BUBBLE_LEFT, }, 2: { - tableStyle: `${TABLE_STYLE_VERTICAL_TOP} ${TABLE_STYLE_HORIZONTAL_RIGHT}`, - cardsStyle: CARDS_STYLE_BELOW_TABLE, + tableStyle: `${TABLE_STYLE_VERTICAL_TOP} ${TABLE_STYLE_HORIZONTAL_MIDDLE_RIGHT}`, + bubbleClass: DOM_CLASS_SPEECH_BUBBLE_RIGHT, }, 3: { tableStyle: `${TABLE_STYLE_VERTICAL_BOTTOM} ${TABLE_STYLE_HORIZONTAL_RIGHT}`, - cardsStyle: CARDS_STYLE_ABOVE_TABLE, + bubbleClass: DOM_CLASS_SPEECH_BUBBLE_LEFT, }, 4: { tableStyle: `${TABLE_STYLE_VERTICAL_BOTTOM} ${TABLE_STYLE_HORIZONTAL_CENTER}`, - cardsStyle: CARDS_STYLE_ABOVE_TABLE, + bubbleClass: DOM_CLASS_SPEECH_BUBBLE_LEFT, }, }, }; -// @TODO: improve current round display // @TODO: show cards in logs (especially the cards picked/gave for the impacted players) // @TODO: color player names (logs, action messages) // @TODO: support 2 players game // @TODO: ? game rounds topology instead of choosing number of rounds // @TODO: ? be able to move cards individually in your hand -// @TODO: ? add the winner in the end of round log // @TODO: ? be able to click on the jersey to play it instead of having a 2nd button // @TODO: ? improve the visibility of the jersey that has been played with the combination on the table define([ @@ -196,12 +195,12 @@ function (dojo, declare) { return declare('bgagame.velonimo', ebg.core.gamegui, { constructor: function () { this.currentState = null; - this.currentRound = -1; + this.currentRound = 0; this.currentPlayerHasJersey = false; this.jerseyHasBeenUsedInTheCurrentRound = false; - this.howManyRounds = -1; - this.playedCardsValue = -1; - this.howManyCardsToGiveBack = -1; + this.howManyRounds = 0; + this.playedCardsValue = 0; + this.howManyCardsToGiveBack = 0; this.players = []; this.playerHand = null; // https://en.doc.boardgamearena.com/Stock }, @@ -213,9 +212,15 @@ function (dojo, declare) { // Setup board dojo.place( - `
-
+ `
+
+
+
+
+
+
+ ${_('Round')}
@@ -238,13 +243,14 @@ function (dojo, declare) { gamedatas.currentPlayerId ).forEach((player, index) => { const playerPosition = playersPlace[index]; + const playerColorRGB = `#${player.color}`; dojo.place( `
-
${(player.name.length > 10 ? (player.name.substr(0,10) + '...') : player.name)}
+
${(player.name.length > 10 ? (player.name.substr(0,10) + '...') : player.name)}
${player.howManyCards}
-
+
`, DOM_ID_BOARD_CARPET); }); @@ -278,6 +284,7 @@ function (dojo, declare) { // Setup cards played on table this.playedCardsValue = gamedatas.playedCardsValue; + this.setupPreviousPlayedCards(gamedatas.previousPlayedCards); this.moveCardsFromPlayerHandToTable(gamedatas.playedCardsPlayerId, gamedatas.playedCards); // Setup jersey @@ -340,7 +347,7 @@ function (dojo, declare) { }); break; case 'playerGiveCardsBackAfterPicking': - this.howManyCardsToGiveBack = -1; + this.howManyCardsToGiveBack = 0; break; } @@ -519,7 +526,7 @@ function (dojo, declare) { const animation = this.slideTemporaryObject( `
`, `player-table-${currentJerseyWearerId}-jersey`, - DOM_ID_BOARD, + `player-table-${currentJerseyWearerId}-jersey`, `player-table-${player.id}-jersey` ); dojo.connect(animation, 'onEnd', () => dojo.addClass(`player-table-${player.id}`, DOM_CLASS_PLAYER_IS_WEARING_JERSEY)); @@ -1241,11 +1248,15 @@ function (dojo, declare) { return [...players.slice(currentPlayerIndex), ...players.slice(0, currentPlayerIndex)]; }, /** - * @param {number} playerId * @param {Object[]} cards + * @param {string} placeDomId + * @returns {number} Top of stack card ID */ - moveCardsFromPlayerHandToTable: function (playerId, cards) { - if (cards.length <= 0) { + buildAndPlacePlayedStackOfCards: function (cards, placeDomId) { + if ( + cards.length <= 0 + || !placeDomId + ) { return; } @@ -1256,7 +1267,7 @@ function (dojo, declare) { const stackWith = ((stackedCards.length - 1) * (CARD_WIDTH / 3)) + CARD_WIDTH; dojo.place( `
`, - `player-table-${playerId}-cards` + placeDomId ); stackedCards.forEach((card) => { const position = this.getCardPositionInSpriteByColorAndValue(card.color, card.value); @@ -1268,18 +1279,52 @@ function (dojo, declare) { ); }); + return topOfStackCardId; + }, + /** + * @param {Object[]} cards + */ + setupPreviousPlayedCards: function (cards) { + if (cards.length <= 0) { + return; + } + + const topOfStackCardId = this.buildAndPlacePlayedStackOfCards(cards, DOM_ID_PREVIOUS_LAST_PLAYED_CARDS); + + // place cards from where the animation will start + this.placeOnObject(`cards-stack-${topOfStackCardId}`, DOM_ID_LAST_PLAYED_CARDS); + + // move cards to their destination + dojo.addClass(`cards-stack-${topOfStackCardId}`, DOM_CLASS_CARDS_STACK_PREVIOUS_PLAYED); + this.slideToObject(`cards-stack-${topOfStackCardId}`, DOM_ID_PREVIOUS_LAST_PLAYED_CARDS).play(); + }, + /** + * @param {number} playerId + * @param {Object[]} cards + */ + moveCardsFromPlayerHandToTable: function (playerId, cards) { + if (cards.length <= 0) { + return; + } + + const topOfStackCardId = this.buildAndPlacePlayedStackOfCards(cards, DOM_ID_LAST_PLAYED_CARDS); + // place cards from where the animation will start if (playerId !== this.player_id) { this.placeOnObject(`cards-stack-${topOfStackCardId}`, `player-table-${playerId}-hand`); } else if ($(`${DOM_ID_PLAYER_HAND}_item_${topOfStackCardId}`)) { this.placeOnObject(`cards-stack-${topOfStackCardId}`, `${DOM_ID_PLAYER_HAND}_item_${topOfStackCardId}`); - stackedCards.forEach((card) => { + cards.forEach((card) => { this.playerHand.removeFromStockById(card.id); }); } // move cards to their destination - this.slideToObject(`cards-stack-${topOfStackCardId}`, `player-table-${playerId}-cards`).play(); + this.slideToObject(`cards-stack-${topOfStackCardId}`, DOM_ID_LAST_PLAYED_CARDS).play(); + + // show speech bubble + $(`player-table-${playerId}-speech-bubble`).innerHTML = `${this.playedCardsValue}`; + dojo.addClass(`player-table-${playerId}-speech-bubble`, DOM_CLASS_PLAYER_SPEECH_BUBBLE_SHOW); }, /** * @param {number} senderId @@ -1341,10 +1386,24 @@ function (dojo, declare) { this.playerHand.addToStockWithId(this.getCardPositionInSpriteByColorAndValue(card.color, card.value), card.id); }); }, + movePlayedCardsToPreviousPlayedCards: function () { + dojo.query(`.${DOM_CLASS_CARDS_STACK_PREVIOUS_PLAYED}`).forEach(dojo.destroy); + dojo.query(`#${DOM_ID_LAST_PLAYED_CARDS} .${DOM_CLASS_CARDS_STACK}`).forEach((elementDomId) => { + dojo.addClass(elementDomId, DOM_CLASS_CARDS_STACK_PREVIOUS_PLAYED); + const animation = this.slideToObject(elementDomId, DOM_ID_PREVIOUS_LAST_PLAYED_CARDS); + dojo.connect(animation, 'onEnd', () => this.attachToNewParent(elementDomId, DOM_ID_PREVIOUS_LAST_PLAYED_CARDS)); + animation.play(); + }); + }, discardCards: function () { - this.playedCardsValue = -1; + this.playedCardsValue = 0; dojo.query(`.${DOM_CLASS_CARDS_STACK}`).forEach(dojo.destroy); }, + discardPlayerSpeechBubbles: function () { + dojo.query(`.${DOM_CLASS_PLAYER_SPEECH_BUBBLE_SHOW}`).forEach((elementDomId) => { + dojo.removeClass(elementDomId, DOM_CLASS_PLAYER_SPEECH_BUBBLE_SHOW); + }); + }, /////////////////////////////////////////////////// //// Player's action @@ -1462,8 +1521,8 @@ function (dojo, declare) { this.addCardsToPlayerHand(data.args.cards); }, notif_cardsPlayed: function (data) { - // remove last played cards - this.discardCards(); + this.discardPlayerSpeechBubbles(); + this.movePlayedCardsToPreviousPlayedCards(); // place new played cards this.playedCardsValue = data.args.playedCardsValue; @@ -1480,6 +1539,7 @@ function (dojo, declare) { this.setupNumberOfCardsInPlayersHand(); }, notif_cardsDiscarded: function (data) { + this.discardPlayerSpeechBubbles(); this.discardCards(); }, notif_cardsReceivedFromAnotherPlayer: function (data) {