diff --git a/README.md b/README.md index 2bcfea2..881e519 100644 --- a/README.md +++ b/README.md @@ -8,7 +8,11 @@ - Create sprite from 180px wide PNG card images: ```shell - magick montage $(ls blue-*.png) $(ls brown-*.png) $(ls gray-*.png) $(ls green-*.png) $(ls pink-*.png) $(ls red-*.png) $(ls yellow-*.png) jersey.png $(ls adventurer-*.png) -geometry +0+0 -tile 7x8 -mode concatenate -background none cards.png + magick montage $(ls blue-*.png) $(ls brown-*.png) $(ls gray-*.png) $(ls green-*.png) $(ls pink-*.png) $(ls red-*.png) $(ls yellow-*.png) $(ls special-*.png) back-ext_legendes.png $(ls adventurer-*.png) -geometry +0+0 -tile 7x9 -mode concatenate -background none cards.png + ``` +- Create finish.png sprite from 150x150 finish images: + ```shell + magick montage $(ls finish_*.png) -geometry +0+0 -tile 4x1 -mode concatenate -background none finish.png ``` - Reduce generated sprite size by ~90% (using https://tinypng.com or https://compresspng.com if the file is too large) diff --git a/gameoptions.inc.php b/gameoptions.inc.php index 59e9669..de562fd 100644 --- a/gameoptions.inc.php +++ b/gameoptions.inc.php @@ -58,14 +58,16 @@ ], 'default' => 5, ], -// 110 => [ -// 'name' => totranslate('Extension "Legends"'), -// 'values' => [ -// 0 => ['name' => totranslate('No')], -// 1 => ['name' => totranslate('Yes')], -// ], -// 'default' => 0, -// 'description' => totranslate('Add 6 special cards'), -// 'nobeginner' => true, -// ], + 110 => [ + 'name' => totranslate('With "Legend" cards'), + 'values' => [ + 0 => ['name' => totranslate('No')], + 1 => [ + 'name' => totranslate('Yes'), + 'description' => totranslate('Permanently give a coach to each player. Also give the broom wagon to the loser of the previous round.'), + ], + ], + 'default' => 0, + 'nobeginner' => true, + ], ]; diff --git a/img/cards.png b/img/cards.png index 31d39ce..c3f001d 100644 Binary files a/img/cards.png and b/img/cards.png differ diff --git a/img/finish.png b/img/finish.png index 73c6fa7..d7f3df1 100644 Binary files a/img/finish.png and b/img/finish.png differ diff --git a/img/jersey.png b/img/jersey.png deleted file mode 100644 index 892ea5b..0000000 Binary files a/img/jersey.png and /dev/null differ diff --git a/img/jersey_player_panel.png b/img/jersey_player_panel.png deleted file mode 100644 index 78c046c..0000000 Binary files a/img/jersey_player_panel.png and /dev/null differ diff --git a/material.inc.php b/material.inc.php index a65632a..d9fd7f0 100644 --- a/material.inc.php +++ b/material.inc.php @@ -20,3 +20,21 @@ */ require_once('modules/constants.inc.php'); + +$this->legends_coaches = [ + CARD_ID_LEGENDS_EAGLE_ADD_ONE_OTHER_NUMBER => [ + 'name' => clienttranslate('Eagle'), + ], + CARD_ID_LEGENDS_PANDA_ADD_ONE_OTHER_COLOR => [ + 'name' => clienttranslate('Panda'), + ], + CARD_ID_LEGENDS_SHARK_ONE_RED_MULTIPLY_TEN => [ + 'name' => clienttranslate('Shark'), + ], + CARD_ID_LEGENDS_BADGER_ANY_NUMBER_OF_EACH_COLOR => [ + 'name' => clienttranslate('Badger'), + ], + CARD_ID_LEGENDS_ELEPHANT_STOP => [ + 'name' => clienttranslate('Elephant'), + ], +]; diff --git a/modules/VelonimoPlayer.php b/modules/VelonimoPlayer.php index a833e94..280dd28 100644 --- a/modules/VelonimoPlayer.php +++ b/modules/VelonimoPlayer.php @@ -169,6 +169,26 @@ public function isLastRoundWinner(): bool { return $this->getLastRoundRank() === 1; } + public function isLastRoundLoser(int $numberOfPlayers): bool + { + return $this->getLastRoundRank() === $numberOfPlayers; + } + public function getCoachCardId(): ?int + { + if ($this->hasCardLegendsEagle) { + return CARD_ID_LEGENDS_EAGLE_ADD_ONE_OTHER_NUMBER; + } elseif ($this->hasCardLegendsPanda) { + return CARD_ID_LEGENDS_PANDA_ADD_ONE_OTHER_COLOR; + } elseif ($this->hasCardLegendsShark) { + return CARD_ID_LEGENDS_SHARK_ONE_RED_MULTIPLY_TEN; + } elseif ($this->hasCardLegendsBadger) { + return CARD_ID_LEGENDS_BADGER_ANY_NUMBER_OF_EACH_COLOR; + } elseif ($this->hasCardLegendsElephant) { + return CARD_ID_LEGENDS_ELEPHANT_STOP; + } else { + return null; + } + } /* * SETTERS @@ -191,6 +211,42 @@ public function setIsWearingJersey(bool $isWearingJersey): self return $this; } + public function setHasCardLegendsBroomWagon(bool $hasCardLegendsBroomWagon): self + { + $this->hasCardLegendsBroomWagon = $hasCardLegendsBroomWagon; + + return $this; + } + public function setHasCardLegendsEagle(bool $hasCardLegendsEagle): self + { + $this->hasCardLegendsEagle = $hasCardLegendsEagle; + + return $this; + } + public function setHasCardLegendsPanda(bool $hasCardLegendsPanda): self + { + $this->hasCardLegendsPanda = $hasCardLegendsPanda; + + return $this; + } + public function setHasCardLegendsShark(bool $hasCardLegendsShark): self + { + $this->hasCardLegendsShark = $hasCardLegendsShark; + + return $this; + } + public function setHasCardLegendsBadger(bool $hasCardLegendsBadger): self + { + $this->hasCardLegendsBadger = $hasCardLegendsBadger; + + return $this; + } + public function setHasCardLegendsElephant(bool $hasCardLegendsElephant): self + { + $this->hasCardLegendsElephant = $hasCardLegendsElephant; + + return $this; + } public function addRoundRanking(int $round, int $rank): self { if (isset($this->roundsRanking[$round])) { diff --git a/modules/constants.inc.php b/modules/constants.inc.php index bf84721..1479571 100644 --- a/modules/constants.inc.php +++ b/modules/constants.inc.php @@ -29,7 +29,7 @@ define('COLOR_RED', 60); define('COLOR_YELLOW', 70); define('COLOR_ADVENTURER', 80); -define('COLOR_JERSEY', 90); +define('COLOR_SPECIAL', 90); // Cards value define('VALUE_1', 1); @@ -45,8 +45,13 @@ define('VALUE_40', 40); define('VALUE_45', 45); define('VALUE_50', 50); -define('VALUE_JERSEY', 10); -define('VALUE_LEGENDS_BROOM_WAGON', 5); +define('VALUE_JERSEY_PLUS_TEN', 10); +define('VALUE_LEGENDS_BROOM_WAGON_PLUS_FIVE', 5); +define('VALUE_LEGENDS_EAGLE_ADD_ONE_OTHER_NUMBER', -4); +define('VALUE_LEGENDS_PANDA_ADD_ONE_OTHER_COLOR', -5); +define('VALUE_LEGENDS_SHARK_ONE_RED_MULTIPLY_TEN', -6); +define('VALUE_LEGENDS_BADGER_ANY_NUMBER_OF_EACH_COLOR', -7); +define('VALUE_LEGENDS_ELEPHANT_STOP', -8); // Special cards ID define('CARD_ID_JERSEY_PLUS_TEN', -2); diff --git a/stats.inc.php b/stats.inc.php index c5e9968..835b909 100644 --- a/stats.inc.php +++ b/stats.inc.php @@ -47,17 +47,63 @@ */ +$numberOfRoundsWonName = totranslate('Rounds won'); +$minValueName = totranslate('Minimum value played'); +$maxValueName = totranslate('Maximum value played'); +$numberOfJerseyPlayedName = totranslate('Number of Jersey played'); +$numberOfLegendsBroomWagonPlayedName = totranslate('Number of Broom Wagon played'); +$numberOfLegendsEaglePlayedName = totranslate('Number of coach Eagle played'); +$numberOfLegendsPandaPlayedName = totranslate('Number of coach Panda played'); +$numberOfLegendsSharkPlayedName = totranslate('Number of coach Shark played'); +$numberOfLegendsBadgerPlayedName = totranslate('Number of coach Badger played'); +$numberOfLegendsElephantPlayedName = totranslate('Number of coach Elephant played'); + $stats_type = [ // Statistics global to table 'table' => [ 'minValue' => [ 'id' => 13, - 'name' => totranslate('Minimum value played'), + 'name' => $minValueName, 'type' => 'int', ], 'maxValue' => [ 'id' => 14, - 'name' => totranslate('Maximum value played'), + 'name' => $maxValueName, + 'type' => 'int', + ], + 'numberOfJerseyPlayed' => [ + 'id' => 15, + 'name' => $numberOfJerseyPlayedName, + 'type' => 'int', + ], + 'numberOfLegendsBroomWagonPlayed' => [ + 'id' => 16, + 'name' => $numberOfLegendsBroomWagonPlayedName, + 'type' => 'int', + ], + 'numberOfLegendsEaglePlayed' => [ + 'id' => 17, + 'name' => $numberOfLegendsEaglePlayedName, + 'type' => 'int', + ], + 'numberOfLegendsPandaPlayed' => [ + 'id' => 18, + 'name' => $numberOfLegendsPandaPlayedName, + 'type' => 'int', + ], + 'numberOfLegendsSharkPlayed' => [ + 'id' => 19, + 'name' => $numberOfLegendsSharkPlayedName, + 'type' => 'int', + ], + 'numberOfLegendsBadgerPlayed' => [ + 'id' => 20, + 'name' => $numberOfLegendsBadgerPlayedName, + 'type' => 'int', + ], + 'numberOfLegendsElephantPlayed' => [ + 'id' => 21, + 'name' => $numberOfLegendsElephantPlayedName, 'type' => 'int', ], ], @@ -66,17 +112,52 @@ 'player' => [ 'numberOfRoundsWon' => [ 'id' => 12, - 'name' => totranslate('Rounds won'), + 'name' => $numberOfRoundsWonName, 'type' => 'int', ], 'minValue' => [ 'id' => 13, - 'name' => totranslate('Minimum value played'), + 'name' => $minValueName, 'type' => 'int', ], 'maxValue' => [ 'id' => 14, - 'name' => totranslate('Maximum value played'), + 'name' => $maxValueName, + 'type' => 'int', + ], + 'numberOfJerseyPlayed' => [ + 'id' => 15, + 'name' => $numberOfJerseyPlayedName, + 'type' => 'int', + ], + 'numberOfLegendsBroomWagonPlayed' => [ + 'id' => 16, + 'name' => $numberOfLegendsBroomWagonPlayedName, + 'type' => 'int', + ], + 'numberOfLegendsEaglePlayed' => [ + 'id' => 17, + 'name' => $numberOfLegendsEaglePlayedName, + 'type' => 'int', + ], + 'numberOfLegendsPandaPlayed' => [ + 'id' => 18, + 'name' => $numberOfLegendsPandaPlayedName, + 'type' => 'int', + ], + 'numberOfLegendsSharkPlayed' => [ + 'id' => 19, + 'name' => $numberOfLegendsSharkPlayedName, + 'type' => 'int', + ], + 'numberOfLegendsBadgerPlayed' => [ + 'id' => 20, + 'name' => $numberOfLegendsBadgerPlayedName, + 'type' => 'int', + ], + 'numberOfLegendsElephantPlayed' => [ + 'id' => 21, + 'name' => $numberOfLegendsElephantPlayedName, 'type' => 'int', ], ], diff --git a/velonimo.action.php b/velonimo.action.php index 53c97a2..2a08bba 100644 --- a/velonimo.action.php +++ b/velonimo.action.php @@ -52,7 +52,6 @@ public function playCards() $withLegendsBadgerArg = (bool) self::getArg('withLegendsBadger', AT_bool, true); $withLegendsElephantArg = (bool) self::getArg('withLegendsElephant', AT_bool, true); - // @TODO: support extension legends $this->game->playCards( array_map(fn ($id) => (int) $id, $cardIds), $withJerseyArg, diff --git a/velonimo.css b/velonimo.css index 62bd259..0540341 100644 --- a/velonimo.css +++ b/velonimo.css @@ -47,18 +47,22 @@ /** Override BGA elements style */ -#page-title { - z-index: 97; -} -#log_history_status { - z-index: 97; -} +#page-title, +#log_history_status, #maingameview_menuheader { z-index: 97; } .tableWindow table { width: 100%; } +.tableWindow table th { + text-align: center; +} +.tableWindow table tr:last-child td { + border-bottom: none; + text-transform: uppercase; + font-weight: bold; +} /** END Override BGA elements style */ @@ -119,7 +123,7 @@ Board font-weight: bold; border: 2px solid transparent; box-sizing: border-box; - z-index: 5; + z-index: 6; } .player-table.selectable .player-table-name, .player-table.selectable-for-card-picking .player-table-name { @@ -270,46 +274,65 @@ Board .player-table-speech-bubble.show-bubble { opacity: 1; } -.player-table.is-wearing-jersey .player-table-jersey { +.player-table .player-special-cards { position: absolute; - width: 65px; - height: 90px; - background-size: 65px 90px; - background-image: url('img/jersey.png'); - background-repeat: no-repeat; - background-position: center center; - opacity: 1; - transition-property: opacity; - transition-duration: 0.5s; + width: 100%; + display: flex; + flex-direction: row; + flex-wrap: wrap; + justify-content: center; + align-content: center; + align-items: center; } -.player-table.selectable .player-table-jersey, -.player-table.selectable-for-card-picking .player-table-jersey { - pointer-events: none; +.player-table.player-position-top .player-special-cards { + top: 50px; } -.player-table.player-position-bottom .player-table-jersey { - bottom: 0; +.player-table.player-position-bottom .player-special-cards { + bottom: 50px; } -.player-table.player-position-top .player-table-jersey { - top: 0; +.player-special-cards div { + display: block; + width: 30px; + height: 30px; + background-image: url('img/cards.png'); /* background-position is dynamically computed */ + background-size: 630px 1134px; /* width = cardWidth * number of columns in sprite ; height = cardHeight * number of rows in sprite */ + border-radius: 50%; + opacity: 0; + transition-property: opacity; + transition-duration: 0.5s; + z-index: 5; } -.player-table.player-position-jersey-left .player-table-jersey { - left: -40px; +.player-table.has-jersey .player-table-jersey, +.player-table.has-legends-broom-wagon .player-table-legends-broom-wagon, +.player-table.has-legends-coach-eagle .player-table-legends-coach, +.player-table.has-legends-coach-panda .player-table-legends-coach, +.player-table.has-legends-coach-shark .player-table-legends-coach, +.player-table.has-legends-coach-badger .player-table-legends-coach, +.player-table.has-legends-coach-elephant .player-table-legends-coach { + opacity: 1; } -.player-table.player-position-jersey-right .player-table-jersey { - right: -40px; +.player-table.has-jersey.has-used-jersey .player-table-jersey, +.player-table.has-legends-broom-wagon.has-used-legends-broom-wagon .player-table-legends-broom-wagon, +.player-table.has-legends-coach-eagle.has-used-legends-coach-eagle .player-table-legends-coach, +.player-table.has-legends-coach-panda.has-used-legends-coach-panda .player-table-legends-coach, +.player-table.has-legends-coach-shark.has-used-legends-coach-shark .player-table-legends-coach, +.player-table.has-legends-coach-badger.has-used-legends-coach-badger .player-table-legends-coach, +.player-table.has-legends-coach-elephant.has-used-legends-coach-elephant .player-table-legends-coach { + display: none; } -.player-table.is-wearing-jersey.has-used-jersey .player-table-jersey { - opacity: 0.5; +.player-table.selectable .player-special-cards div, +.player-table.selectable-for-card-picking .player-special-cards div { + pointer-events: none; } .player-table .player-table-finish-position { display: none; position: absolute; - left: 75px; - width: 50px; - height: 90px; + left: 25px; + width: 150px; + height: 150px; background-repeat: no-repeat; background-image: url('img/finish.png'); - background-size: 150px 90px; + background-size: 600px 150px; background-position-y: center; } .player-table.selectable .player-table-finish-position, @@ -317,24 +340,34 @@ Board pointer-events: none; } .player-table.player-position-top .player-table-finish-position { - bottom: 0; + bottom: -20px; } .player-table.player-position-bottom .player-table-finish-position { - top: 0; + top: -20px; } .player-table.has-finished-1 .player-table-finish-position, .player-table.has-finished-2 .player-table-finish-position, -.player-table.has-finished-3 .player-table-finish-position { +.player-table.has-finished-3 .player-table-finish-position, +.player-table.has-finished-4 .player-table-finish-position { display: block; } +.player-table.has-finished-1 .player-special-cards div, +.player-table.has-finished-2 .player-special-cards div, +.player-table.has-finished-3 .player-special-cards div, +.player-table.has-finished-4 .player-special-cards div { + display: none; +} .player-table.has-finished-1 .player-table-finish-position { - background-position-x: left; + background-position-x: 0; } .player-table.has-finished-2 .player-table-finish-position { - background-position-x: center; + background-position-x: 150px; } .player-table.has-finished-3 .player-table-finish-position { - background-position-x: right; + background-position-x: 300px; +} +.player-table.has-finished-4 .player-table-finish-position { + background-position-x: 450px; } /** END Board @@ -496,7 +529,7 @@ Cards } .velonimo-card.front-side { background-image: url('img/cards.png'); /* background-position is dynamically computed */ - background-size: 630px 1008px; + background-size: 630px 1134px; /* width = cardWidth * number of columns in sprite ; height = cardHeight * number of rows in sprite */ } .velonimo-card.back-side { background-image: url('img/remaining_cards.png'); @@ -543,35 +576,47 @@ Cards bottom: -2px; right: -3px; } +#my-hand .stockitem_selected.cards-group-card.cards-group-1, +#my-hand .stockitem.cards-group-card.cards-group-1:hover, .cards-group-card.cards-group-1::after, .cards-group-card-left.cards-group-1::before, .cards-group-card-right.cards-group-1::before { - border-color: green; + border-color: green !important; } +#my-hand .stockitem_selected.cards-group-card.cards-group-2, +#my-hand .stockitem.cards-group-card.cards-group-2:hover, .cards-group-card.cards-group-2::after, .cards-group-card-left.cards-group-2::before, .cards-group-card-right.cards-group-2::before { - border-color: red; + border-color: red !important; } +#my-hand .stockitem_selected.cards-group-card.cards-group-3, +#my-hand .stockitem.cards-group-card.cards-group-3:hover, .cards-group-card.cards-group-3::after, .cards-group-card-left.cards-group-3::before, .cards-group-card-right.cards-group-3::before { - border-color: blue; + border-color: blue !important; } +#my-hand .stockitem_selected.cards-group-card.cards-group-4, +#my-hand .stockitem.cards-group-card.cards-group-4:hover, .cards-group-card.cards-group-4::after, .cards-group-card-left.cards-group-4::before, .cards-group-card-right.cards-group-4::before { - border-color: orange; + border-color: orange !important; } +#my-hand .stockitem_selected.cards-group-card.cards-group-5, +#my-hand .stockitem.cards-group-card.cards-group-5:hover, .cards-group-card.cards-group-5::after, .cards-group-card-left.cards-group-5::before, .cards-group-card-right.cards-group-5::before { - border-color: hotpink; + border-color: hotpink !important; } +#my-hand .stockitem_selected.cards-group-card.cards-group-6, +#my-hand .stockitem.cards-group-card.cards-group-6:hover, .cards-group-card.cards-group-6::after, .cards-group-card-left.cards-group-6::before, .cards-group-card-right.cards-group-6::before { - border-color: royalblue; + border-color: royalblue !important; } /** END Cards @@ -583,13 +628,10 @@ Animations .moving-card { z-index: 100; } -.moving-jersey { - position: absolute; - width: 65px; - height: 90px; - background-size: 65px 90px; - background-image: url('img/jersey.png'); - background-repeat: no-repeat; +.moving-special-card.player-table-jersey, +.moving-special-card.player-table-legends-broom-wagon, +.moving-special-card.player-table-legends-coach { + opacity: 1; z-index: 100; } /** @@ -633,7 +675,7 @@ Player panel HTML flex-wrap: wrap; justify-content: flex-start; align-content: center; - align-items: center; + align-items: flex-end; } .player-panel-number-of-remaining-cards { width: 40px; @@ -641,12 +683,24 @@ Player panel HTML margin-right: 10px; text-align: center; } -.player-panel-jersey { - width: 40px; - height: 40px; - background-size: 40px 40px; - background-image: url('img/jersey_player_panel.png'); - background-repeat: no-repeat; +.player-panel-jersey, +.player-panel-legends-broom-wagon, +.player-panel-legends-coach-eagle.velonimo-card, +.player-panel-legends-coach-panda.velonimo-card, +.player-panel-legends-coach-shark.velonimo-card, +.player-panel-legends-coach-badger.velonimo-card, +.player-panel-legends-coach-elephant.velonimo-card { + border-radius: 50%; +} +.has-used-jersey .player-panel-jersey, +.has-used-legends-broom-wagon .player-panel-legends-broom-wagon, +.has-used-legends-coach-eagle .player-panel-legends-coach-eagle, +.has-used-legends-coach-panda .player-panel-legends-coach-panda, +.has-used-legends-coach-shark .player-panel-legends-coach-shark, +.has-used-legends-coach-badger .player-panel-legends-coach-badger, +.has-used-legends-coach-elephant .player-panel-legends-coach-elephant { + filter: grayscale(1); + opacity: 0.5; } /** END Player panel HTML diff --git a/velonimo.game.php b/velonimo.game.php index c558dba..ec34b4c 100644 --- a/velonimo.game.php +++ b/velonimo.game.php @@ -36,7 +36,21 @@ class Velonimo extends Table private const GAME_STATE_LEGENDS_BADGER_IS_NOT_PLAYABLE = 'legendsBadgerIsNotPlayable'; private const GAME_STATE_LEGENDS_ELEPHANT_IS_NOT_PLAYABLE = 'legendsElephantIsNotPlayable'; private const GAME_STATE_PREVIOUS_PLAYED_CARDS_VALUE = 'previousValueToBeat'; + private const GAME_STATE_PREVIOUS_PLAYED_CARDS_CONTAIN_JERSEY = 'previousPlayedCardsContainJersey'; + private const GAME_STATE_PREVIOUS_PLAYED_CARDS_CONTAIN_LEGENDS_BROOM_WAGON = 'previousPlayedCardsContainLegendsBroomWagon'; + private const GAME_STATE_PREVIOUS_PLAYED_CARDS_CONTAIN_LEGENDS_EAGLE = 'previousPlayedCardsContainLegendsEagle'; + private const GAME_STATE_PREVIOUS_PLAYED_CARDS_CONTAIN_LEGENDS_PANDA = 'previousPlayedCardsContainLegendsPanda'; + private const GAME_STATE_PREVIOUS_PLAYED_CARDS_CONTAIN_LEGENDS_SHARK = 'previousPlayedCardsContainLegendsShark'; + private const GAME_STATE_PREVIOUS_PLAYED_CARDS_CONTAIN_LEGENDS_BADGER = 'previousPlayedCardsContainLegendsBadger'; + private const GAME_STATE_PREVIOUS_PLAYED_CARDS_CONTAIN_LEGENDS_ELEPHANT = 'previousPlayedCardsContainLegendsElephant'; private const GAME_STATE_LAST_PLAYED_CARDS_VALUE = 'valueToBeat'; + private const GAME_STATE_LAST_PLAYED_CARDS_CONTAIN_JERSEY = 'lastPlayedCardsContainJersey'; + private const GAME_STATE_LAST_PLAYED_CARDS_CONTAIN_LEGENDS_BROOM_WAGON = 'lastPlayedCardsContainLegendsBroomWagon'; + private const GAME_STATE_LAST_PLAYED_CARDS_CONTAIN_LEGENDS_EAGLE = 'lastPlayedCardsContainLegendsEagle'; + private const GAME_STATE_LAST_PLAYED_CARDS_CONTAIN_LEGENDS_PANDA = 'lastPlayedCardsContainLegendsPanda'; + private const GAME_STATE_LAST_PLAYED_CARDS_CONTAIN_LEGENDS_SHARK = 'lastPlayedCardsContainLegendsShark'; + private const GAME_STATE_LAST_PLAYED_CARDS_CONTAIN_LEGENDS_BADGER = 'lastPlayedCardsContainLegendsBadger'; + private const GAME_STATE_LAST_PLAYED_CARDS_CONTAIN_LEGENDS_ELEPHANT = 'lastPlayedCardsContainLegendsElephant'; private const GAME_STATE_LAST_PLAYED_CARDS_PLAYER_ID = 'playerIdForValueToBeat'; private const GAME_STATE_LAST_SELECTED_NEXT_PLAYER_ID = 'selectedNextPlayerId'; private const GAME_STATE_LAST_NUMBER_OF_CARDS_TO_PICK = 'numberOfCardsToPick'; @@ -84,8 +98,22 @@ function __construct() { self::GAME_STATE_LEGENDS_SHARK_IS_NOT_PLAYABLE => 23, self::GAME_STATE_LEGENDS_BADGER_IS_NOT_PLAYABLE => 24, self::GAME_STATE_LEGENDS_ELEPHANT_IS_NOT_PLAYABLE => 25, + self::GAME_STATE_LAST_PLAYED_CARDS_CONTAIN_JERSEY => 26, + self::GAME_STATE_LAST_PLAYED_CARDS_CONTAIN_LEGENDS_BROOM_WAGON => 27, + self::GAME_STATE_LAST_PLAYED_CARDS_CONTAIN_LEGENDS_EAGLE => 28, + self::GAME_STATE_LAST_PLAYED_CARDS_CONTAIN_LEGENDS_PANDA => 29, + self::GAME_STATE_LAST_PLAYED_CARDS_CONTAIN_LEGENDS_SHARK => 30, + self::GAME_STATE_LAST_PLAYED_CARDS_CONTAIN_LEGENDS_BADGER => 31, + self::GAME_STATE_LAST_PLAYED_CARDS_CONTAIN_LEGENDS_ELEPHANT => 32, + self::GAME_STATE_PREVIOUS_PLAYED_CARDS_CONTAIN_JERSEY => 33, + self::GAME_STATE_PREVIOUS_PLAYED_CARDS_CONTAIN_LEGENDS_BROOM_WAGON => 34, + self::GAME_STATE_PREVIOUS_PLAYED_CARDS_CONTAIN_LEGENDS_EAGLE => 35, + self::GAME_STATE_PREVIOUS_PLAYED_CARDS_CONTAIN_LEGENDS_PANDA => 36, + self::GAME_STATE_PREVIOUS_PLAYED_CARDS_CONTAIN_LEGENDS_SHARK => 37, + self::GAME_STATE_PREVIOUS_PLAYED_CARDS_CONTAIN_LEGENDS_BADGER => 38, + self::GAME_STATE_PREVIOUS_PLAYED_CARDS_CONTAIN_LEGENDS_ELEPHANT => 39, self::GAME_OPTION_HOW_MANY_ROUNDS => 100, -// self::GAME_OPTION_ENABLE_EXTENSION_LEGENDS => 110, + self::GAME_OPTION_ENABLE_EXTENSION_LEGENDS => 110, ]); $this->deck = self::getNew('module.common.deck'); @@ -139,7 +167,21 @@ protected function setupNewGame($players, $options = []) { // Init global values with their initial values self::setGameStateValue(self::GAME_STATE_CURRENT_ROUND, 0); self::setGameStateValue(self::GAME_STATE_PREVIOUS_PLAYED_CARDS_VALUE, 0); + self::setGameStateValue(self::GAME_STATE_PREVIOUS_PLAYED_CARDS_CONTAIN_JERSEY, 0); + self::setGameStateValue(self::GAME_STATE_PREVIOUS_PLAYED_CARDS_CONTAIN_LEGENDS_BROOM_WAGON, 0); + self::setGameStateValue(self::GAME_STATE_PREVIOUS_PLAYED_CARDS_CONTAIN_LEGENDS_EAGLE, 0); + self::setGameStateValue(self::GAME_STATE_PREVIOUS_PLAYED_CARDS_CONTAIN_LEGENDS_PANDA, 0); + self::setGameStateValue(self::GAME_STATE_PREVIOUS_PLAYED_CARDS_CONTAIN_LEGENDS_SHARK, 0); + self::setGameStateValue(self::GAME_STATE_PREVIOUS_PLAYED_CARDS_CONTAIN_LEGENDS_BADGER, 0); + self::setGameStateValue(self::GAME_STATE_PREVIOUS_PLAYED_CARDS_CONTAIN_LEGENDS_ELEPHANT, 0); self::setGameStateValue(self::GAME_STATE_LAST_PLAYED_CARDS_VALUE, 0); + self::setGameStateValue(self::GAME_STATE_LAST_PLAYED_CARDS_CONTAIN_JERSEY, 0); + self::setGameStateValue(self::GAME_STATE_LAST_PLAYED_CARDS_CONTAIN_LEGENDS_BROOM_WAGON, 0); + self::setGameStateValue(self::GAME_STATE_LAST_PLAYED_CARDS_CONTAIN_LEGENDS_EAGLE, 0); + self::setGameStateValue(self::GAME_STATE_LAST_PLAYED_CARDS_CONTAIN_LEGENDS_PANDA, 0); + self::setGameStateValue(self::GAME_STATE_LAST_PLAYED_CARDS_CONTAIN_LEGENDS_SHARK, 0); + self::setGameStateValue(self::GAME_STATE_LAST_PLAYED_CARDS_CONTAIN_LEGENDS_BADGER, 0); + self::setGameStateValue(self::GAME_STATE_LAST_PLAYED_CARDS_CONTAIN_LEGENDS_ELEPHANT, 0); self::setGameStateValue(self::GAME_STATE_LAST_PLAYED_CARDS_PLAYER_ID, 0); self::setGameStateValue(self::GAME_STATE_LAST_SELECTED_NEXT_PLAYER_ID, 0); self::setGameStateValue(self::GAME_STATE_JERSEY_IS_NOT_PLAYABLE, 0); @@ -159,10 +201,24 @@ protected function setupNewGame($players, $options = []) { // Table statistics self::initStat('table', 'minValue', 0); self::initStat('table', 'maxValue', 0); + self::initStat('table', 'numberOfJerseyPlayed', 0); + self::initStat('table', 'numberOfLegendsBroomWagonPlayed', 0); + self::initStat('table', 'numberOfLegendsEaglePlayed', 0); + self::initStat('table', 'numberOfLegendsPandaPlayed', 0); + self::initStat('table', 'numberOfLegendsSharkPlayed', 0); + self::initStat('table', 'numberOfLegendsBadgerPlayed', 0); + self::initStat('table', 'numberOfLegendsElephantPlayed', 0); // Player statistics (init for all players) + self::initStat('player', 'numberOfRoundsWon', 0); self::initStat('player', 'minValue', 0); self::initStat('player', 'maxValue', 0); - self::initStat('player', 'numberOfRoundsWon', 0); + self::initStat('player', 'numberOfJerseyPlayed', 0); + self::initStat('player', 'numberOfLegendsBroomWagonPlayed', 0); + self::initStat('player', 'numberOfLegendsEaglePlayed', 0); + self::initStat('player', 'numberOfLegendsPandaPlayed', 0); + self::initStat('player', 'numberOfLegendsSharkPlayed', 0); + self::initStat('player', 'numberOfLegendsBadgerPlayed', 0); + self::initStat('player', 'numberOfLegendsElephantPlayed', 0); // Create cards $cards = []; @@ -242,11 +298,17 @@ protected function getAllDatas() { $result['playedCards'] = $this->formatCardsForClient( $this->fromBgaCardsToVelonimoCards($this->deck->getCardsInLocation(self::CARD_LOCATION_PLAYED)) ); + $result['playedSpecialCards'] = $this->formatCardsForClient( + $this->fromSpecialPlayedCardsToVelonimoCards('LAST') + ); $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)) ); + $result['previousPlayedSpecialCards'] = $this->formatCardsForClient( + $this->fromSpecialPlayedCardsToVelonimoCards('PREVIOUS') + ); $result['previousPlayedCardsValue'] = (int) self::getGameStateValue(self::GAME_STATE_PREVIOUS_PLAYED_CARDS_VALUE); if ($this->is2PlayersMode($players)) { @@ -257,6 +319,7 @@ protected function getAllDatas() { } if ($this->isExtensionLegendsEnabled()) { + $result['isExtensionLegendsEnabled'] = true; $result['legendsBroomWagonIsNotPlayable'] = $this->isLegendsBroomWagonNotPlayable(); $result['legendsEagleIsNotPlayable'] = $this->isLegendsEagleNotPlayable(); $result['legendsPandaIsNotPlayable'] = $this->isLegendsPandaNotPlayable(); @@ -315,9 +378,8 @@ function playCards( $currentPlayerCards, ] = $this->hydratePlayedCards($playedCardIds); - $legendsExtensionIsEnabled = $this->isExtensionLegendsEnabled(); if ( - !$legendsExtensionIsEnabled + !$this->isExtensionLegendsEnabled() && ( $cardsPlayedWithLegendsBroomWagon || $cardsPlayedWithLegendsEagle @@ -333,62 +395,76 @@ function playCards( // check that player is allowed to use special cards $players = $this->getPlayersFromDatabase(); $currentPlayer = $this->getPlayerById($currentPlayerId, $players); + $currentPlayerIsWearingJersey = $currentPlayer->isWearingJersey(); if ($cardsPlayedWithJersey) { if ($this->isJerseyNotPlayable()) { $this->throwPlayedCardNotPlayable(); } - if (!$currentPlayer->isWearingJersey()) { + if (!$currentPlayerIsWearingJersey) { $this->throwPlayedCardNotInPlayerHand(); } } - if ($legendsExtensionIsEnabled) { - if ($cardsPlayedWithLegendsBroomWagon) { - if ($this->isLegendsBroomWagonNotPlayable()) { - $this->throwPlayedCardNotPlayable(); - } - if (!$currentPlayer->hasCardLegendsBroomWagon()) { - $this->throwPlayedCardNotInPlayerHand(); - } + if ($cardsPlayedWithLegendsBroomWagon) { + if ($this->isLegendsBroomWagonNotPlayable()) { + $this->throwPlayedCardNotPlayable(); } - if ($cardsPlayedWithLegendsEagle) { - if ($this->isLegendsEagleNotPlayable()) { - $this->throwPlayedCardNotPlayable(); - } - if (!$currentPlayer->hasCardLegendsEagle()) { - $this->throwPlayedCardNotInPlayerHand(); - } + if (!$currentPlayer->hasCardLegendsBroomWagon()) { + $this->throwPlayedCardNotInPlayerHand(); } - if ($cardsPlayedWithLegendsPanda) { - if ($this->isLegendsPandaNotPlayable()) { - $this->throwPlayedCardNotPlayable(); - } - if (!$currentPlayer->hasCardLegendsPanda()) { - $this->throwPlayedCardNotInPlayerHand(); - } + } + if ($cardsPlayedWithLegendsEagle) { + if ( + $currentPlayerIsWearingJersey + || $this->isLegendsEagleNotPlayable() + ) { + $this->throwPlayedCardNotPlayable(); } - if ($cardsPlayedWithLegendsShark) { - if ($this->isLegendsSharkNotPlayable()) { - $this->throwPlayedCardNotPlayable(); - } - if (!$currentPlayer->hasCardLegendsShark()) { - $this->throwPlayedCardNotInPlayerHand(); - } + if (!$currentPlayer->hasCardLegendsEagle()) { + $this->throwPlayedCardNotInPlayerHand(); } - if ($cardsPlayedWithLegendsBadger) { - if ($this->isLegendsBadgerNotPlayable()) { - $this->throwPlayedCardNotPlayable(); - } - if (!$currentPlayer->hasCardLegendsBadger()) { - $this->throwPlayedCardNotInPlayerHand(); - } + } + if ($cardsPlayedWithLegendsPanda) { + if ( + $currentPlayerIsWearingJersey + || $this->isLegendsPandaNotPlayable() + ) { + $this->throwPlayedCardNotPlayable(); } - if ($cardsPlayedWithLegendsElephant) { - if ($this->isLegendsElephantNotPlayable()) { - $this->throwPlayedCardNotPlayable(); - } - if (!$currentPlayer->hasCardLegendsElephant()) { - $this->throwPlayedCardNotInPlayerHand(); - } + if (!$currentPlayer->hasCardLegendsPanda()) { + $this->throwPlayedCardNotInPlayerHand(); + } + } + if ($cardsPlayedWithLegendsShark) { + if ( + $currentPlayerIsWearingJersey + || $this->isLegendsSharkNotPlayable() + ) { + $this->throwPlayedCardNotPlayable(); + } + if (!$currentPlayer->hasCardLegendsShark()) { + $this->throwPlayedCardNotInPlayerHand(); + } + } + if ($cardsPlayedWithLegendsBadger) { + if ( + $currentPlayerIsWearingJersey + || $this->isLegendsBadgerNotPlayable() + ) { + $this->throwPlayedCardNotPlayable(); + } + if (!$currentPlayer->hasCardLegendsBadger()) { + $this->throwPlayedCardNotInPlayerHand(); + } + } + if ($cardsPlayedWithLegendsElephant) { + if ( + $currentPlayerIsWearingJersey + || $this->isLegendsElephantNotPlayable() + ) { + $this->throwPlayedCardNotPlayable(); + } + if (!$currentPlayer->hasCardLegendsElephant()) { + $this->throwPlayedCardNotInPlayerHand(); } } @@ -409,11 +485,7 @@ function playCards( $playedCards, $cardsPlayedWithJersey, $cardsPlayedWithLegendsBroomWagon, - $cardsPlayedWithLegendsEagle, - $cardsPlayedWithLegendsPanda, - $cardsPlayedWithLegendsShark, - $cardsPlayedWithLegendsBadger, - $cardsPlayedWithLegendsElephant + $cardsPlayedWithLegendsShark ); if ($playedCardsValue <= $lastPlayedCardsValue) { throw new BgaUserException(sprintf( @@ -428,40 +500,79 @@ function playCards( $this->deck->moveCards($playedCardIds, self::CARD_LOCATION_PLAYED, $currentPlayerId); if ($cardsPlayedWithJersey) { self::setGameStateValue(self::GAME_STATE_JERSEY_IS_NOT_PLAYABLE, 1); + $this->incStat(1, 'numberOfJerseyPlayed'); + $this->incStat(1, 'numberOfJerseyPlayed', $currentPlayerId); } - if ($legendsExtensionIsEnabled) { - if ($cardsPlayedWithLegendsBroomWagon) { - self::setGameStateValue(self::GAME_STATE_LEGENDS_BROOM_WAGON_IS_NOT_PLAYABLE, 1); - } - if ($cardsPlayedWithLegendsEagle) { - self::setGameStateValue(self::GAME_STATE_LEGENDS_EAGLE_IS_NOT_PLAYABLE, 1); - } - if ($cardsPlayedWithLegendsPanda) { - self::setGameStateValue(self::GAME_STATE_LEGENDS_PANDA_IS_NOT_PLAYABLE, 1); - } - if ($cardsPlayedWithLegendsShark) { - self::setGameStateValue(self::GAME_STATE_LEGENDS_SHARK_IS_NOT_PLAYABLE, 1); - } - if ($cardsPlayedWithLegendsBadger) { - self::setGameStateValue(self::GAME_STATE_LEGENDS_BADGER_IS_NOT_PLAYABLE, 1); - } - if ($cardsPlayedWithLegendsElephant) { - self::setGameStateValue(self::GAME_STATE_LEGENDS_ELEPHANT_IS_NOT_PLAYABLE, 1); - } + if ($cardsPlayedWithLegendsBroomWagon) { + self::setGameStateValue(self::GAME_STATE_LEGENDS_BROOM_WAGON_IS_NOT_PLAYABLE, 1); + $this->incStat(1, 'numberOfLegendsBroomWagonPlayed'); + $this->incStat(1, 'numberOfLegendsBroomWagonPlayed', $currentPlayerId); + } + if ($cardsPlayedWithLegendsEagle) { + self::setGameStateValue(self::GAME_STATE_LEGENDS_EAGLE_IS_NOT_PLAYABLE, 1); + $this->incStat(1, 'numberOfLegendsEaglePlayed'); + $this->incStat(1, 'numberOfLegendsEaglePlayed', $currentPlayerId); + } + if ($cardsPlayedWithLegendsPanda) { + self::setGameStateValue(self::GAME_STATE_LEGENDS_PANDA_IS_NOT_PLAYABLE, 1); + $this->incStat(1, 'numberOfLegendsPandaPlayed'); + $this->incStat(1, 'numberOfLegendsPandaPlayed', $currentPlayerId); + } + if ($cardsPlayedWithLegendsShark) { + self::setGameStateValue(self::GAME_STATE_LEGENDS_SHARK_IS_NOT_PLAYABLE, 1); + $this->incStat(1, 'numberOfLegendsSharkPlayed'); + $this->incStat(1, 'numberOfLegendsSharkPlayed', $currentPlayerId); + } + if ($cardsPlayedWithLegendsBadger) { + self::setGameStateValue(self::GAME_STATE_LEGENDS_BADGER_IS_NOT_PLAYABLE, 1); + $this->incStat(1, 'numberOfLegendsBadgerPlayed'); + $this->incStat(1, 'numberOfLegendsBadgerPlayed', $currentPlayerId); + } + if ($cardsPlayedWithLegendsElephant) { + self::setGameStateValue(self::GAME_STATE_LEGENDS_ELEPHANT_IS_NOT_PLAYABLE, 1); + $this->incStat(1, 'numberOfLegendsElephantPlayed'); + $this->incStat(1, 'numberOfLegendsElephantPlayed', $currentPlayerId); } self::setGameStateValue(self::GAME_STATE_LAST_PLAYED_CARDS_PLAYER_ID, $currentPlayerId); self::setGameStateValue(self::GAME_STATE_PREVIOUS_PLAYED_CARDS_VALUE, $lastPlayedCardsValue); + self::setGameStateValue(self::GAME_STATE_PREVIOUS_PLAYED_CARDS_CONTAIN_JERSEY, (int) self::getGameStateValue(self::GAME_STATE_LAST_PLAYED_CARDS_CONTAIN_JERSEY)); + self::setGameStateValue(self::GAME_STATE_PREVIOUS_PLAYED_CARDS_CONTAIN_LEGENDS_BROOM_WAGON, (int) self::getGameStateValue(self::GAME_STATE_LAST_PLAYED_CARDS_CONTAIN_LEGENDS_BROOM_WAGON)); + self::setGameStateValue(self::GAME_STATE_PREVIOUS_PLAYED_CARDS_CONTAIN_LEGENDS_EAGLE, (int) self::getGameStateValue(self::GAME_STATE_LAST_PLAYED_CARDS_CONTAIN_LEGENDS_EAGLE)); + self::setGameStateValue(self::GAME_STATE_PREVIOUS_PLAYED_CARDS_CONTAIN_LEGENDS_PANDA, (int) self::getGameStateValue(self::GAME_STATE_LAST_PLAYED_CARDS_CONTAIN_LEGENDS_PANDA)); + self::setGameStateValue(self::GAME_STATE_PREVIOUS_PLAYED_CARDS_CONTAIN_LEGENDS_SHARK, (int) self::getGameStateValue(self::GAME_STATE_LAST_PLAYED_CARDS_CONTAIN_LEGENDS_SHARK)); + self::setGameStateValue(self::GAME_STATE_PREVIOUS_PLAYED_CARDS_CONTAIN_LEGENDS_BADGER, (int) self::getGameStateValue(self::GAME_STATE_LAST_PLAYED_CARDS_CONTAIN_LEGENDS_BADGER)); + self::setGameStateValue(self::GAME_STATE_PREVIOUS_PLAYED_CARDS_CONTAIN_LEGENDS_ELEPHANT, (int) self::getGameStateValue(self::GAME_STATE_LAST_PLAYED_CARDS_CONTAIN_LEGENDS_ELEPHANT)); self::setGameStateValue(self::GAME_STATE_LAST_PLAYED_CARDS_VALUE, $playedCardsValue); + self::setGameStateValue(self::GAME_STATE_LAST_PLAYED_CARDS_CONTAIN_JERSEY, $cardsPlayedWithJersey ? CARD_ID_JERSEY_PLUS_TEN : 0); + self::setGameStateValue(self::GAME_STATE_LAST_PLAYED_CARDS_CONTAIN_LEGENDS_BROOM_WAGON, $cardsPlayedWithLegendsBroomWagon ? CARD_ID_LEGENDS_BROOM_WAGON_PLUS_FIVE : 0); + self::setGameStateValue(self::GAME_STATE_LAST_PLAYED_CARDS_CONTAIN_LEGENDS_EAGLE, $cardsPlayedWithLegendsEagle ? CARD_ID_LEGENDS_EAGLE_ADD_ONE_OTHER_NUMBER : 0); + self::setGameStateValue(self::GAME_STATE_LAST_PLAYED_CARDS_CONTAIN_LEGENDS_PANDA, $cardsPlayedWithLegendsPanda ? CARD_ID_LEGENDS_PANDA_ADD_ONE_OTHER_COLOR : 0); + self::setGameStateValue(self::GAME_STATE_LAST_PLAYED_CARDS_CONTAIN_LEGENDS_SHARK, $cardsPlayedWithLegendsShark ? CARD_ID_LEGENDS_SHARK_ONE_RED_MULTIPLY_TEN : 0); + self::setGameStateValue(self::GAME_STATE_LAST_PLAYED_CARDS_CONTAIN_LEGENDS_BADGER, $cardsPlayedWithLegendsBadger ? CARD_ID_LEGENDS_BADGER_ANY_NUMBER_OF_EACH_COLOR : 0); + self::setGameStateValue(self::GAME_STATE_LAST_PLAYED_CARDS_CONTAIN_LEGENDS_ELEPHANT, $cardsPlayedWithLegendsElephant ? CARD_ID_LEGENDS_ELEPHANT_STOP : 0); self::notifyAllPlayers('cardsPlayed', clienttranslate('${player_name} attacks with ${cardsImage}'), [ 'players' => $this->formatPlayersForClient($players), 'playedCardsPlayerId' => $currentPlayerId, 'playedCards' => $formattedCards = $this->formatCardsForClient($playedCards), - 'cardsImage' => $cardsPlayedWithJersey - ? array_merge([$this->formatJerseyForClient()], $formattedCards) - : $formattedCards, + 'cardsImage' => [ + ...$formattedCards, + ...($cardsPlayedWithJersey ? $this->formatCardsForClient($this->fromSpecialCardIdsToVelonimoCards([CARD_ID_JERSEY_PLUS_TEN])) : []), + ...($cardsPlayedWithLegendsBroomWagon ? $this->formatCardsForClient($this->fromSpecialCardIdsToVelonimoCards([CARD_ID_LEGENDS_BROOM_WAGON_PLUS_FIVE])) : []), + ...($cardsPlayedWithLegendsEagle ? $this->formatCardsForClient($this->fromSpecialCardIdsToVelonimoCards([CARD_ID_LEGENDS_EAGLE_ADD_ONE_OTHER_NUMBER])) : []), + ...($cardsPlayedWithLegendsPanda ? $this->formatCardsForClient($this->fromSpecialCardIdsToVelonimoCards([CARD_ID_LEGENDS_PANDA_ADD_ONE_OTHER_COLOR])) : []), + ...($cardsPlayedWithLegendsShark ? $this->formatCardsForClient($this->fromSpecialCardIdsToVelonimoCards([CARD_ID_LEGENDS_SHARK_ONE_RED_MULTIPLY_TEN])) : []), + ...($cardsPlayedWithLegendsBadger ? $this->formatCardsForClient($this->fromSpecialCardIdsToVelonimoCards([CARD_ID_LEGENDS_BADGER_ANY_NUMBER_OF_EACH_COLOR])) : []), + ...($cardsPlayedWithLegendsElephant ? $this->formatCardsForClient($this->fromSpecialCardIdsToVelonimoCards([CARD_ID_LEGENDS_ELEPHANT_STOP])) : []), + ], 'player_name' => $currentPlayerName = self::getCurrentPlayerName(), 'playedCardsValue' => $playedCardsValue, 'withJersey' => $cardsPlayedWithJersey, + 'withLegendsBroomWagon' => $cardsPlayedWithLegendsBroomWagon, + 'withLegendsEagle' => $cardsPlayedWithLegendsEagle, + 'withLegendsPanda' => $cardsPlayedWithLegendsPanda, + 'withLegendsShark' => $cardsPlayedWithLegendsShark, + 'withLegendsBadger' => $cardsPlayedWithLegendsBadger, + 'withLegendsElephant' => $cardsPlayedWithLegendsElephant, ]); // update player's min/max value played @@ -759,6 +870,19 @@ function selectCardsToGiveBack(array $selectedCardIds) { if (count($selectedCardIds) !== $numberOfCardsToGiveBack) { throw new BgaUserException(sprintf(self::_('You must select exactly %s cards.'), $numberOfCardsToGiveBack)); } + foreach ($selectedCardIds as $id) { + if (in_array($id, [ + CARD_ID_JERSEY_PLUS_TEN, + CARD_ID_LEGENDS_BROOM_WAGON_PLUS_FIVE, + CARD_ID_LEGENDS_EAGLE_ADD_ONE_OTHER_NUMBER, + CARD_ID_LEGENDS_PANDA_ADD_ONE_OTHER_COLOR, + CARD_ID_LEGENDS_SHARK_ONE_RED_MULTIPLY_TEN, + CARD_ID_LEGENDS_BADGER_ANY_NUMBER_OF_EACH_COLOR, + CARD_ID_LEGENDS_ELEPHANT_STOP, + ], true)) { + throw new BgaUserException(self::_('This card cannot be given.')); + } + } [ $selectedCards, @@ -924,6 +1048,29 @@ function stStartRound() { ]); } + // if first round, + // deal a coach of legends extension to each player + if ( + $this->isExtensionLegendsEnabled() + && $newRound === 1 + ) { + $availableCoachCards = $this->fromSpecialCardIdsToVelonimoCards([ + CARD_ID_LEGENDS_EAGLE_ADD_ONE_OTHER_NUMBER, + CARD_ID_LEGENDS_PANDA_ADD_ONE_OTHER_COLOR, + CARD_ID_LEGENDS_SHARK_ONE_RED_MULTIPLY_TEN, + CARD_ID_LEGENDS_BADGER_ANY_NUMBER_OF_EACH_COLOR, + CARD_ID_LEGENDS_ELEPHANT_STOP, + ]); + foreach ($players as $player) + { + $pickedCoachKey = array_rand($availableCoachCards, 1); + $pickedCoachCard = $availableCoachCards[$pickedCoachKey]; + unset($availableCoachCards[$pickedCoachKey]); + + $this->dealCoachCardToPlayer($player, $pickedCoachCard); + } + } + // if there is a loser, he plays first during this round if ($currentLoser = $this->getCurrentLoser($players)) { $this->gamestate->changeActivePlayer($currentLoser->getId()); @@ -931,16 +1078,16 @@ function stStartRound() { self::activeNextPlayer(); } + if ($this->is2PlayersMode($players)) { + $this->revealNewAttackRewardCardsIfEnoughCardsInDeck(); + } + self::notifyAllPlayers('roundStarted', clienttranslate('Round #${currentRound} starts'), [ 'currentRound' => $newRound, 'players' => $this->formatPlayersForClient($players), 'numberOfCardsInDeck' => $this->getNumberOfCardsInDeck(), ]); - if ($this->is2PlayersMode($players)) { - $this->revealNewAttackRewardCardsIfEnoughCardsInDeck(); - } - $this->gamestate->nextState('firstPlayerTurn'); } @@ -954,8 +1101,12 @@ function stActivateNextPlayer() { $playerIdWhoPlayedTheLastCards = (int) self::getGameStateValue(self::GAME_STATE_LAST_PLAYED_CARDS_PLAYER_ID); // activate next players until we find one who can play - foreach ($players as $ignored) { - $nextPlayerId = self::activeNextPlayer(); + for ($i = 0; $i < count($players); $i++) { + // if the last player played the coach Elephant, then he is the next player, + // otherwise the next player is the next one in the "natural" order + $nextPlayerId = $this->lastPlayedCardsContainLegendsElephant() + ? $playerIdWhoPlayedTheLastCards + : self::activeNextPlayer(); $nextPlayerCanPlay = in_array($nextPlayerId, $playersWhoCanPlayIds, true); // if the next player is the one who played the last played cards if ($nextPlayerId === $playerIdWhoPlayedTheLastCards) { @@ -998,30 +1149,68 @@ function stApplySelectedNextPlayer() { function stEndRound() { // update players score and jersey $players = $this->getPlayersFromDatabase(); + $numberOfPlayers = count($players); $currentRound = (int) self::getGameStateValue(self::GAME_STATE_CURRENT_ROUND); foreach ($players as $k => $player) { $players[$k] = $player->addPoints($player->getLastNumberOfPointsEarned()); } $newWinner = $this->getCurrentWinner($players); + $newLoserIds = array_map( + fn (VelonimoPlayer $p) => $p->getId(), + $this->getCurrentLosers($players) + ); + + // check if all coaches have been used + $allCoachesAreNotPlayable = true; + foreach ($players as $player) { + if ( + ($coachCardId = $player->getCoachCardId()) + && 0 === (int) self::getGameStateValue($this->fromSpecialCardIdToIsNotPlayableGameStateKey($coachCardId)) + ) { + $allCoachesAreNotPlayable = false; + break; + } + } + + $restoredCoachCardIds = []; foreach ($players as $k => $player) { + // update player state $players[$k] = $player->setIsWearingJersey( $newWinner && $newWinner->getId() === $player->getId() + )->setHasCardLegendsBroomWagon( + $player->isLastRoundLoser($numberOfPlayers) ); - self::DbQuery(sprintf( - 'UPDATE player SET player_score=%s, has_card_jersey=%s WHERE player_id=%s', + 'UPDATE player SET player_score=%s, %s=%s, %s=%s WHERE player_id=%s', $player->getScore(), + $this->fromSpecialCardIdToPlayerTableKey(CARD_ID_JERSEY_PLUS_TEN), $player->isWearingJersey() ? 1 : 0, + $this->fromSpecialCardIdToPlayerTableKey(CARD_ID_LEGENDS_BROOM_WAGON_PLUS_FIVE), + $player->hasCardLegendsBroomWagon() ? 1 : 0, $player->getId() )); + + // mark player coach card as reusable if needed + if ( + ($coachCardId = $player->getCoachCardId()) + && ( + $allCoachesAreNotPlayable + || in_array($player->getId(), $newLoserIds, true) + ) + ) { + $restoredCoachCardIds[] = $coachCardId; + self::setGameStateValue($this->fromSpecialCardIdToIsNotPlayableGameStateKey($coachCardId), 0); + } } - // re-allow the jersey to be used + // re-allow the jersey/broom_wagon to be used self::setGameStateValue(self::GAME_STATE_JERSEY_IS_NOT_PLAYABLE, 0); + self::setGameStateValue(self::GAME_STATE_LEGENDS_BROOM_WAGON_IS_NOT_PLAYABLE, 0); self::notifyAllPlayers('roundEnded', clienttranslate('Round #${currentRound} ends'), [ 'currentRound' => $currentRound, 'players' => $this->formatPlayersForClient($players), + 'specialCardIdsToRestore' => [CARD_ID_JERSEY_PLUS_TEN, CARD_ID_LEGENDS_BROOM_WAGON_PLUS_FIVE, ...$restoredCoachCardIds], ]); // notify points earned by each player @@ -1042,6 +1231,12 @@ function stEndRound() { $headers = [ '', // the first column of headers is empty ]; + $previousPoints = [ + [ + 'str' => clienttranslate('Previous score'), + 'args' => [], + ], + ]; $roundPoints = [ [ 'str' => clienttranslate('Round points'), @@ -1050,7 +1245,7 @@ function stEndRound() { ]; $totalPoints = [ [ - 'str' => clienttranslate('Total points'), + 'str' => clienttranslate('New score'), 'args' => [], ], ]; @@ -1062,23 +1257,25 @@ function stEndRound() { ], 'type' => 'header' ]; - $roundPoints[] = $player->getLastNumberOfPointsEarned(); + $previousPoints[] = $player->getScore() - $player->getLastNumberOfPointsEarned(); + $roundPoints[] = $player->getLastNumberOfPointsEarned() === 0 ? '0' : sprintf('+%s', $player->getLastNumberOfPointsEarned()); $totalPoints[] = $player->getScore(); } - $this->notifyAllPlayers( 'tableWindow', '', array( + $this->notifyAllPlayers('tableWindow', '', [ 'id' => 'finalScoring', - 'title' => sprintf( + 'title' => sprintf( clienttranslate('Results of round %s/%s'), $currentRound, $howManyRounds ), 'table' => [ $headers, + $previousPoints, $roundPoints, $totalPoints ], 'closing' => $isGameOver ? clienttranslate('End of game') : clienttranslate('Next round') - )); + ]); // update global min/max value played stats if ($isGameOver) { @@ -1173,6 +1370,78 @@ private function fromBgaCardsToVelonimoCards(array $bgaCards): array { ); } + /** + * @return VelonimoCard[] + */ + private function fromSpecialPlayedCardsToVelonimoCards(string $playedCardsType): array { + if (!in_array($playedCardsType, [ + 'LAST', + 'PREVIOUS', + ], true)) { + throw new BgaVisibleSystemException('Invalid argument for ' . __FUNCTION__); + } + + $cardIds = array_values(array_filter( + array_map( + fn (string $cardKey) => (int) self::getGameStateValue(constant('self::GAME_STATE_'.$playedCardsType.'_PLAYED_CARDS_CONTAIN_'.$cardKey)), + [ + 'JERSEY', + 'LEGENDS_BROOM_WAGON', + 'LEGENDS_EAGLE', + 'LEGENDS_PANDA', + 'LEGENDS_SHARK', + 'LEGENDS_BADGER', + 'LEGENDS_ELEPHANT', + ] + ), + fn (int $id) => $id !== 0 + )); + + return $this->fromSpecialCardIdsToVelonimoCards($cardIds); + } + + /** + * @param int[] $cardIds + * + * @return VelonimoCard[] + */ + private function fromSpecialCardIdsToVelonimoCards(array $cardIds): array { + return array_map( + fn (int $id) => new VelonimoCard( + $id, + COLOR_SPECIAL, + $this->fromSpecialCardIdToValue($id) + ), + $cardIds + ); + } + + private function fromSpecialCardIdToValue(int $cardId): int { + if ($cardId === CARD_ID_JERSEY_PLUS_TEN) { + return VALUE_JERSEY_PLUS_TEN; + } + if ($cardId === CARD_ID_LEGENDS_BROOM_WAGON_PLUS_FIVE) { + return VALUE_LEGENDS_BROOM_WAGON_PLUS_FIVE; + } + if ($cardId === CARD_ID_LEGENDS_EAGLE_ADD_ONE_OTHER_NUMBER) { + return VALUE_LEGENDS_EAGLE_ADD_ONE_OTHER_NUMBER; + } + if ($cardId === CARD_ID_LEGENDS_PANDA_ADD_ONE_OTHER_COLOR) { + return VALUE_LEGENDS_PANDA_ADD_ONE_OTHER_COLOR; + } + if ($cardId === CARD_ID_LEGENDS_SHARK_ONE_RED_MULTIPLY_TEN) { + return VALUE_LEGENDS_SHARK_ONE_RED_MULTIPLY_TEN; + } + if ($cardId === CARD_ID_LEGENDS_BADGER_ANY_NUMBER_OF_EACH_COLOR) { + return VALUE_LEGENDS_BADGER_ANY_NUMBER_OF_EACH_COLOR; + } + if ($cardId === CARD_ID_LEGENDS_ELEPHANT_STOP) { + return VALUE_LEGENDS_ELEPHANT_STOP; + } + + throw new BgaVisibleSystemException('Unsupported special card.'); + } + /** * @param VelonimoCard[] $cards */ @@ -1187,14 +1456,6 @@ private function formatCardsForClient(array $cards): array { ); } - private function formatJerseyForClient(): array { - return [ - 'id' => CARD_ID_JERSEY_PLUS_TEN, - 'color' => COLOR_JERSEY, - 'value' => VALUE_JERSEY, - ]; - } - /** * @param VelonimoCard[] $cards */ @@ -1202,29 +1463,27 @@ private function getCardsValue( array $cards, bool $cardsPlayedWithJersey, bool $cardsPlayedWithLegendsBroomWagon, - bool $cardsPlayedWithLegendsEagle, - bool $cardsPlayedWithLegendsPanda, - bool $cardsPlayedWithLegendsShark, - bool $cardsPlayedWithLegendsBadger, - bool $cardsPlayedWithLegendsElephant + bool $cardsPlayedWithLegendsShark ): int { if (count($cards) <= 0) { return 0; } - $addJerseyOrBroomWagonValueIfUsed = fn (int $value) => $value + ( - $cardsPlayedWithJersey ? VALUE_JERSEY : ( - $cardsPlayedWithLegendsBroomWagon ? VALUE_LEGENDS_BROOM_WAGON : 0 - ) + $addJerseyValueIfUsed = fn (int $value) => $value + ( + $cardsPlayedWithJersey ? VALUE_JERSEY_PLUS_TEN : 0 ); + $addBroomWagonValueIfUsed = fn (int $value) => $value + ( + $cardsPlayedWithLegendsBroomWagon ? VALUE_LEGENDS_BROOM_WAGON_PLUS_FIVE : 0 + ); + $withJerseyAndBroomWagon = fn (int $value) => $addJerseyValueIfUsed($addBroomWagonValueIfUsed($value)); if (count($cards) === 1) { $uniqueCardValue = current($cards)->getValue(); if ($cardsPlayedWithLegendsShark) { - return $addJerseyOrBroomWagonValueIfUsed($uniqueCardValue * 10); + return $withJerseyAndBroomWagon($uniqueCardValue * 10); } - return $addJerseyOrBroomWagonValueIfUsed($uniqueCardValue); + return $withJerseyAndBroomWagon($uniqueCardValue); } $minCardValue = 1000; @@ -1234,7 +1493,7 @@ private function getCardsValue( } } - return $addJerseyOrBroomWagonValueIfUsed((count($cards) * 10) + $minCardValue); + return $withJerseyAndBroomWagon((count($cards) * 10) + $minCardValue); } /** @@ -1254,13 +1513,13 @@ private function getPlayersFromDatabase(): array { (int) $player['player_score'], (int) $player['player_score_aux'], VelonimoPlayer::deserializeRoundsRanking($player['rounds_ranking']), - ((int) $player['has_card_jersey']) === 1, - ((int) $player['has_card_legends_broom_wagon']) === 1, - ((int) $player['has_card_legends_eagle']) === 1, - ((int) $player['has_card_legends_panda']) === 1, - ((int) $player['has_card_legends_shark']) === 1, - ((int) $player['has_card_legends_badger']) === 1, - ((int) $player['has_card_legends_elephant']) === 1 + ((int) $player[$this->fromSpecialCardIdToPlayerTableKey(CARD_ID_JERSEY_PLUS_TEN)]) === 1, + ((int) $player[$this->fromSpecialCardIdToPlayerTableKey(CARD_ID_LEGENDS_BROOM_WAGON_PLUS_FIVE)]) === 1, + ((int) $player[$this->fromSpecialCardIdToPlayerTableKey(CARD_ID_LEGENDS_EAGLE_ADD_ONE_OTHER_NUMBER)]) === 1, + ((int) $player[$this->fromSpecialCardIdToPlayerTableKey(CARD_ID_LEGENDS_PANDA_ADD_ONE_OTHER_COLOR)]) === 1, + ((int) $player[$this->fromSpecialCardIdToPlayerTableKey(CARD_ID_LEGENDS_SHARK_ONE_RED_MULTIPLY_TEN)]) === 1, + ((int) $player[$this->fromSpecialCardIdToPlayerTableKey(CARD_ID_LEGENDS_BADGER_ANY_NUMBER_OF_EACH_COLOR)]) === 1, + ((int) $player[$this->fromSpecialCardIdToPlayerTableKey(CARD_ID_LEGENDS_ELEPHANT_STOP)]) === 1 ), $players ); @@ -1355,7 +1614,7 @@ private function getPlayersWhoCanPlayDuringRound(int $round, array $players = nu } /** - * @param array $players Indexed by player ID + * @param VelonimoPlayer[] $players Indexed by player ID */ private function formatPlayersForClient(array $players): array { $result = []; @@ -1369,6 +1628,12 @@ private function formatPlayersForClient(array $players): array { 'lastNumberOfPointsEarned' => $player->getLastNumberOfPointsEarned(), 'roundsRanking' => $player->getRoundsRanking(), 'isWearingJersey' => $player->isWearingJersey(), + 'hasCardLegendsBroomWagon' => $player->hasCardLegendsBroomWagon(), + 'hasCardLegendsEagle' => $player->hasCardLegendsEagle(), + 'hasCardLegendsPanda' => $player->hasCardLegendsPanda(), + 'hasCardLegendsShark' => $player->hasCardLegendsShark(), + 'hasCardLegendsBadger' => $player->hasCardLegendsBadger(), + 'hasCardLegendsElephant' => $player->hasCardLegendsElephant(), 'howManyCards' => count($this->deck->getCardsInLocation(self::CARD_LOCATION_PLAYER_HAND, $player->getId())), ]; } @@ -1421,6 +1686,31 @@ private function getCurrentWinner(array $players): ?VelonimoPlayer { return null; } + /** + * The current losers have the smallest number of points. + * + * @param VelonimoPlayer[] $players + * + * @return VelonimoPlayer[] + */ + private function getCurrentLosers(array $players): array { + $lowestScore = 1000; + foreach ($players as $player) { + if ($player->getScore() < $lowestScore) { + $lowestScore = $player->getScore(); + } + } + + $playersWhoHaveLowestScore = []; + foreach ($players as $player) { + if ($player->getScore() === $lowestScore) { + $playersWhoHaveLowestScore[] = $player; + } + } + + return $playersWhoHaveLowestScore; + } + private function getNumberOfPointsAtRankForRound( int $rank, int $round, @@ -1481,7 +1771,21 @@ private function discardPlayedCards(): 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_PREVIOUS_PLAYED_CARDS_VALUE, 0); + self::setGameStateValue(self::GAME_STATE_PREVIOUS_PLAYED_CARDS_CONTAIN_JERSEY, 0); + self::setGameStateValue(self::GAME_STATE_PREVIOUS_PLAYED_CARDS_CONTAIN_LEGENDS_BROOM_WAGON, 0); + self::setGameStateValue(self::GAME_STATE_PREVIOUS_PLAYED_CARDS_CONTAIN_LEGENDS_EAGLE, 0); + self::setGameStateValue(self::GAME_STATE_PREVIOUS_PLAYED_CARDS_CONTAIN_LEGENDS_PANDA, 0); + self::setGameStateValue(self::GAME_STATE_PREVIOUS_PLAYED_CARDS_CONTAIN_LEGENDS_SHARK, 0); + self::setGameStateValue(self::GAME_STATE_PREVIOUS_PLAYED_CARDS_CONTAIN_LEGENDS_BADGER, 0); + self::setGameStateValue(self::GAME_STATE_PREVIOUS_PLAYED_CARDS_CONTAIN_LEGENDS_ELEPHANT, 0); self::setGameStateValue(self::GAME_STATE_LAST_PLAYED_CARDS_VALUE, 0); + self::setGameStateValue(self::GAME_STATE_LAST_PLAYED_CARDS_CONTAIN_JERSEY, 0); + self::setGameStateValue(self::GAME_STATE_LAST_PLAYED_CARDS_CONTAIN_LEGENDS_BROOM_WAGON, 0); + self::setGameStateValue(self::GAME_STATE_LAST_PLAYED_CARDS_CONTAIN_LEGENDS_EAGLE, 0); + self::setGameStateValue(self::GAME_STATE_LAST_PLAYED_CARDS_CONTAIN_LEGENDS_PANDA, 0); + self::setGameStateValue(self::GAME_STATE_LAST_PLAYED_CARDS_CONTAIN_LEGENDS_SHARK, 0); + self::setGameStateValue(self::GAME_STATE_LAST_PLAYED_CARDS_CONTAIN_LEGENDS_BADGER, 0); + self::setGameStateValue(self::GAME_STATE_LAST_PLAYED_CARDS_CONTAIN_LEGENDS_ELEPHANT, 0); self::setGameStateValue(self::GAME_STATE_LAST_PLAYED_CARDS_PLAYER_ID, 0); self::notifyAllPlayers('playedCardsDiscarded', '', []); } @@ -1515,6 +1819,67 @@ private function updatePlayerRoundsRanking( )); } + private function dealCoachCardToPlayer( + VelonimoPlayer $player, + VelonimoCard $coachCard + ): void { + self::DbQuery(sprintf( + 'UPDATE player SET %s=1 WHERE player_id=%s', + $this->fromSpecialCardIdToPlayerTableKey($coachCard->getId()), + $player->getId() + )); + + self::notifyAllPlayers('legendsCoachCardDealt', clienttranslate('${player_name} receives coach ${coachName} ${cardsImage}'), [ + 'cards' => $formattedCards = $this->formatCardsForClient([$coachCard]), + 'cardsImage' => $formattedCards, + 'coachName' => $this->legends_coaches[$coachCard->getId()]['name'], + 'receiverPlayerId' => $player->getId(), + 'player_name' => $player->getName(), + ]); + } + + private function fromSpecialCardIdToPlayerTableKey(int $cardId): string + { + if ($cardId === CARD_ID_JERSEY_PLUS_TEN) { + return 'has_card_jersey'; + } elseif ($cardId === CARD_ID_LEGENDS_BROOM_WAGON_PLUS_FIVE) { + return 'has_card_legends_broom_wagon'; + } elseif ($cardId === CARD_ID_LEGENDS_EAGLE_ADD_ONE_OTHER_NUMBER) { + return 'has_card_legends_eagle'; + } elseif ($cardId === CARD_ID_LEGENDS_PANDA_ADD_ONE_OTHER_COLOR) { + return 'has_card_legends_panda'; + } elseif ($cardId === CARD_ID_LEGENDS_SHARK_ONE_RED_MULTIPLY_TEN) { + return 'has_card_legends_shark'; + } elseif ($cardId === CARD_ID_LEGENDS_BADGER_ANY_NUMBER_OF_EACH_COLOR) { + return 'has_card_legends_badger'; + } elseif ($cardId === CARD_ID_LEGENDS_ELEPHANT_STOP) { + return 'has_card_legends_elephant'; + } else { + throw new BgaVisibleSystemException('Unsupported special card ID'); + } + } + + private function fromSpecialCardIdToIsNotPlayableGameStateKey(int $cardId): string + { + if ($cardId === CARD_ID_JERSEY_PLUS_TEN) { + return self::GAME_STATE_JERSEY_IS_NOT_PLAYABLE; + } elseif ($cardId === CARD_ID_LEGENDS_BROOM_WAGON_PLUS_FIVE) { + return self::GAME_STATE_LEGENDS_BROOM_WAGON_IS_NOT_PLAYABLE; + } elseif ($cardId === CARD_ID_LEGENDS_EAGLE_ADD_ONE_OTHER_NUMBER) { + return self::GAME_STATE_LEGENDS_EAGLE_IS_NOT_PLAYABLE; + } elseif ($cardId === CARD_ID_LEGENDS_PANDA_ADD_ONE_OTHER_COLOR) { + return self::GAME_STATE_LEGENDS_PANDA_IS_NOT_PLAYABLE; + } elseif ($cardId === CARD_ID_LEGENDS_SHARK_ONE_RED_MULTIPLY_TEN) { + return self::GAME_STATE_LEGENDS_SHARK_IS_NOT_PLAYABLE; + } elseif ($cardId === CARD_ID_LEGENDS_BADGER_ANY_NUMBER_OF_EACH_COLOR) { + return self::GAME_STATE_LEGENDS_BADGER_IS_NOT_PLAYABLE; + } elseif ($cardId === CARD_ID_LEGENDS_ELEPHANT_STOP) { + return self::GAME_STATE_LEGENDS_ELEPHANT_IS_NOT_PLAYABLE; + } else { + throw new BgaVisibleSystemException('Unsupported special card ID'); + } + } + private function isJerseyNotPlayable(): bool { return 1 === (int) self::getGameStateValue(self::GAME_STATE_JERSEY_IS_NOT_PLAYABLE); } @@ -1548,9 +1913,7 @@ private function getHowManyRounds(): int { } private function isExtensionLegendsEnabled(): bool { - // @TODO: support extension legends - return false; -// return 1 === (int) self::getGameStateValue(self::GAME_OPTION_ENABLE_EXTENSION_LEGENDS); + return 1 === (int) self::getGameStateValue(self::GAME_OPTION_ENABLE_EXTENSION_LEGENDS); } /** @@ -1568,10 +1931,16 @@ private function assertCardsCanBePlayedTogether( ): void { $cardsCannotBePlayedTogetherErrorMessage = self::_('These cards cannot be played together.'); - // validate jersey and broom wagon are not played together + // validate jersey is not played with coaches if ( $cardsPlayedWithJersey - && $cardsPlayedWithLegendsBroomWagon + && ( + $cardsPlayedWithLegendsEagle + || $cardsPlayedWithLegendsPanda + || $cardsPlayedWithLegendsShark + || $cardsPlayedWithLegendsBadger + || $cardsPlayedWithLegendsElephant + ) ) { throw new BgaUserException($cardsCannotBePlayedTogetherErrorMessage); } @@ -1593,7 +1962,7 @@ private function assertCardsCanBePlayedTogether( throw new BgaUserException($cardsCannotBePlayedTogetherErrorMessage); } - // validate adventurer is played alone + // validate adventurer is played alone and without jersey or broom wagon $cardsContainAnAdventurer = in_array(COLOR_ADVENTURER, array_map(fn (VelonimoCard $c) => $c->getColor(), $playedCards), true); $numberOfCards = count($playedCards); if ( @@ -1602,10 +1971,6 @@ private function assertCardsCanBePlayedTogether( $numberOfCards > 1 || $cardsPlayedWithJersey || $cardsPlayedWithLegendsBroomWagon - || $cardsPlayedWithLegendsEagle - || $cardsPlayedWithLegendsPanda - || $cardsPlayedWithLegendsShark - || $cardsPlayedWithLegendsBadger ) ) { throw new BgaUserException($cardsCannotBePlayedTogetherErrorMessage); @@ -1661,6 +2026,13 @@ private function assertCardsCanBePlayedTogether( } } + /** + * /!\ Extension Legends only + */ + private function lastPlayedCardsContainLegendsElephant(): bool { + return (0 !== (int) self::getGameStateValue(self::GAME_STATE_LAST_PLAYED_CARDS_CONTAIN_LEGENDS_ELEPHANT)); + } + /** * /!\ 2P mode only */ diff --git a/velonimo.js b/velonimo.js index 8c8e272..0ddb9e6 100644 --- a/velonimo.js +++ b/velonimo.js @@ -41,7 +41,9 @@ const COLOR_PINK = 50; const COLOR_RED = 60; const COLOR_YELLOW = 70; const COLOR_ADVENTURER = 80; -const COLOR_JERSEY = 90; +const COLOR_SPECIAL = 90; + +const SIMPLE_COLORS = [COLOR_BLUE, COLOR_BROWN, COLOR_GRAY, COLOR_GREEN, COLOR_PINK, COLOR_RED, COLOR_YELLOW]; // Cards value const VALUE_1 = 1; @@ -57,7 +59,13 @@ const VALUE_35 = 35; const VALUE_40 = 40; const VALUE_45 = 45; const VALUE_50 = 50; -const VALUE_JERSEY = 10; +const VALUE_JERSEY_PLUS_TEN = 10; +const VALUE_LEGENDS_BROOM_WAGON_PLUS_FIVE = 5; +const VALUE_LEGENDS_EAGLE_ADD_ONE_OTHER_NUMBER = -4; +const VALUE_LEGENDS_PANDA_ADD_ONE_OTHER_COLOR = -5; +const VALUE_LEGENDS_SHARK_ONE_RED_MULTIPLY_TEN = -6; +const VALUE_LEGENDS_BADGER_ANY_NUMBER_OF_EACH_COLOR = -7; +const VALUE_LEGENDS_ELEPHANT_STOP = -8; // Special cards ID const CARD_ID_JERSEY_PLUS_TEN = -2; @@ -68,6 +76,10 @@ const CARD_ID_LEGENDS_SHARK_ONE_RED_MULTIPLY_TEN = -6; const CARD_ID_LEGENDS_BADGER_ANY_NUMBER_OF_EACH_COLOR = -7; const CARD_ID_LEGENDS_ELEPHANT_STOP = -8; +// Sprites +const NUMBER_OF_COLUMNS_IN_CARDS_SPRITE = 7; +const NUMBER_OF_ROWS_IN_CARDS_SPRITE = 9; + // DOM IDs const DOM_ID_APP = 'velonimo-game'; const DOM_ID_BOARD_CARPET = 'board-carpet'; @@ -95,12 +107,33 @@ 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_JERSEY = 'player-table-jersey'; +const DOM_CLASS_PLAYER_LEGENDS_BROOM_WAGON = 'player-table-legends-broom-wagon'; +const DOM_CLASS_PLAYER_LEGENDS_COACH = 'player-table-legends-coach'; +const DOM_CLASS_PLAYER_HAS_JERSEY = 'has-jersey'; +const DOM_CLASS_PLAYER_HAS_LEGENDS_BROOM_WAGON = 'has-legends-broom-wagon'; +const DOM_CLASS_PLAYER_HAS_LEGENDS_EAGLE = 'has-legends-coach-eagle'; +const DOM_CLASS_PLAYER_HAS_LEGENDS_PANDA = 'has-legends-coach-panda'; +const DOM_CLASS_PLAYER_HAS_LEGENDS_SHARK = 'has-legends-coach-shark'; +const DOM_CLASS_PLAYER_HAS_LEGENDS_BADGER = 'has-legends-coach-badger'; +const DOM_CLASS_PLAYER_HAS_LEGENDS_ELEPHANT = 'has-legends-coach-elephant'; const DOM_CLASS_PLAYER_HAS_USED_JERSEY = 'has-used-jersey'; +const DOM_CLASS_PLAYER_HAS_USED_LEGENDS_BROOM_WAGON = 'has-used-legends-broom-wagon'; +const DOM_CLASS_PLAYER_HAS_USED_LEGENDS_EAGLE = 'has-used-legends-coach-eagle'; +const DOM_CLASS_PLAYER_HAS_USED_LEGENDS_PANDA = 'has-used-legends-coach-panda'; +const DOM_CLASS_PLAYER_HAS_USED_LEGENDS_SHARK = 'has-used-legends-coach-shark'; +const DOM_CLASS_PLAYER_HAS_USED_LEGENDS_BADGER = 'has-used-legends-coach-badger'; +const DOM_CLASS_PLAYER_HAS_USED_LEGENDS_ELEPHANT = 'has-used-legends-coach-elephant'; const DOM_CLASS_PLAYER_PANEL_CONTAINER = 'player-panel-velonimo-wrapper'; const DOM_CLASS_PLAYER_PANEL_LEFT = 'player-panel-velonimo-left'; const DOM_CLASS_PLAYER_PANEL_RIGHT = 'player-panel-velonimo-right'; const DOM_CLASS_JERSEY_IN_PLAYER_PANEL = 'player-panel-jersey'; +const DOM_CLASS_LEGENDS_BROOM_WAGON_IN_PLAYER_PANEL = 'player-panel-legends-broom-wagon'; +const DOM_CLASS_LEGENDS_EAGLE_IN_PLAYER_PANEL = 'player-panel-legends-coach-eagle'; +const DOM_CLASS_LEGENDS_PANDA_IN_PLAYER_PANEL = 'player-panel-legends-coach-panda'; +const DOM_CLASS_LEGENDS_SHARK_IN_PLAYER_PANEL = 'player-panel-legends-coach-shark'; +const DOM_CLASS_LEGENDS_BADGER_IN_PLAYER_PANEL = 'player-panel-legends-coach-badger'; +const DOM_CLASS_LEGENDS_ELEPHANT_IN_PLAYER_PANEL = 'player-panel-legends-coach-elephant'; const DOM_CLASS_NUMBER_OF_REMAINING_CARDS_IN_PLAYER_PANEL = 'player-panel-number-of-remaining-cards'; const DOM_CLASS_CARDS_STACK = 'cards-stack'; const DOM_CLASS_CARDS_STACK_PREVIOUS_PLAYED = 'previous-last-played-cards'; @@ -115,6 +148,11 @@ const DOM_CLASS_SPEECH_BUBBLE_RIGHT = 'speech-bubble-on-right'; const DOM_CLASS_CARDS_GROUP_CARD = 'cards-group-card'; const DOM_CLASS_CARDS_GROUP_CARD_LEFT = 'cards-group-card-left'; const DOM_CLASS_CARDS_GROUP_CARD_RIGHT = 'cards-group-card-right'; +const DOM_CLASS_VELONIMO_CARD = 'velonimo-card'; +const DOM_CLASS_CARD_FRONT_SIDE = 'front-side'; +const DOM_CLASS_CARD_BACK_SIDE = 'back-side'; +const DOM_CLASS_MOVING_CARD = 'moving-card'; +const DOM_CLASS_MOVING_SPECIAL_CARD = 'moving-special-card'; // Player hand sorting modes const PLAYER_HAND_SORT_BY_COLOR = 'color'; @@ -214,7 +252,20 @@ function (dojo, declare) { this.resetCurrentState(); this.currentRound = 0; this.currentPlayerHasJersey = false; + this.currentPlayerHasLegendsBroomWagon = false; + this.currentPlayerHasLegendsEagle = false; + this.currentPlayerHasLegendsPanda = false; + this.currentPlayerHasLegendsShark = false; + this.currentPlayerHasLegendsBadger = false; + this.currentPlayerHasLegendsElephant = false; this.jerseyIsNotPlayable = false; + this.legendsBroomWagonIsNotPlayable = false; + this.legendsEagleIsNotPlayable = false; + this.legendsPandaIsNotPlayable = false; + this.legendsSharkIsNotPlayable = false; + this.legendsBadgerIsNotPlayable = false; + this.legendsElephantIsNotPlayable = false; + this.isExtensionLegendsEnabled = false; this.howManyRounds = 0; this.playedCardsValue = 0; this.howManyCardsToGiveBack = 0; @@ -230,7 +281,14 @@ function (dojo, declare) { this.currentState = gamedatas.gamestate.name; this.currentRound = gamedatas.currentRound; this.jerseyIsNotPlayable = gamedatas.jerseyIsNotPlayable; + this.legendsBroomWagonIsNotPlayable = gamedatas.legendsBroomWagonIsNotPlayable; + this.legendsEagleIsNotPlayable = gamedatas.legendsEagleIsNotPlayable; + this.legendsPandaIsNotPlayable = gamedatas.legendsPandaIsNotPlayable; + this.legendsSharkIsNotPlayable = gamedatas.legendsSharkIsNotPlayable; + this.legendsBadgerIsNotPlayable = gamedatas.legendsBadgerIsNotPlayable; + this.legendsElephantIsNotPlayable = gamedatas.legendsElephantIsNotPlayable; this.howManyRounds = gamedatas.howManyRounds; + this.isExtensionLegendsEnabled = gamedatas.isExtensionLegendsEnabled === true; // setup board dojo.place( @@ -271,14 +329,13 @@ function (dojo, declare) { const playerPosition = playersPlace[index]; const playerColorRGB = `#${player.color}`; const isPositionTop = playerPosition.tableStyle.indexOf('top') !== -1; - const hasJerseyOnLeft = playerPosition.bubbleClass.indexOf('left') !== -1; // setup player on board dojo.place( - `
-
${(player.name.length > 10 ? (player.name.substr(0,10) + '...') : player.name)}
+ `
+
${(player.name.length > 10 ? (player.name.substring(0,10) + '...') : player.name)}
-
+
`, @@ -301,11 +358,49 @@ function (dojo, declare) { }); this.setupPlayersFinishPosition(); + if (this.isExtensionLegendsEnabled) { + // setup legends coaches + if (gamedatas.legendsEagleIsNotPlayable) { + this.useSpecialCardForCurrentRound(CARD_ID_LEGENDS_EAGLE_ADD_ONE_OTHER_NUMBER); + } else { + this.restoreSpecialCardForCurrentRound(CARD_ID_LEGENDS_EAGLE_ADD_ONE_OTHER_NUMBER); + } + if (gamedatas.legendsPandaIsNotPlayable) { + this.useSpecialCardForCurrentRound(CARD_ID_LEGENDS_PANDA_ADD_ONE_OTHER_COLOR); + } else { + this.restoreSpecialCardForCurrentRound(CARD_ID_LEGENDS_PANDA_ADD_ONE_OTHER_COLOR); + } + if (gamedatas.legendsSharkIsNotPlayable) { + this.useSpecialCardForCurrentRound(CARD_ID_LEGENDS_SHARK_ONE_RED_MULTIPLY_TEN); + } else { + this.restoreSpecialCardForCurrentRound(CARD_ID_LEGENDS_SHARK_ONE_RED_MULTIPLY_TEN); + } + if (gamedatas.legendsBadgerIsNotPlayable) { + this.useSpecialCardForCurrentRound(CARD_ID_LEGENDS_BADGER_ANY_NUMBER_OF_EACH_COLOR); + } else { + this.restoreSpecialCardForCurrentRound(CARD_ID_LEGENDS_BADGER_ANY_NUMBER_OF_EACH_COLOR); + } + if (gamedatas.legendsElephantIsNotPlayable) { + this.useSpecialCardForCurrentRound(CARD_ID_LEGENDS_ELEPHANT_STOP); + } else { + this.restoreSpecialCardForCurrentRound(CARD_ID_LEGENDS_ELEPHANT_STOP); + } + this.moveLegendsCoachToPlayers(); + + // setup legends broom wagon + if (gamedatas.legendsBroomWagonIsNotPlayable) { + this.useSpecialCardForCurrentRound(CARD_ID_LEGENDS_BROOM_WAGON_PLUS_FIVE); + } else { + this.restoreSpecialCardForCurrentRound(CARD_ID_LEGENDS_BROOM_WAGON_PLUS_FIVE); + } + this.moveLegendsBroomWagonToLastLoser(); + } + // setup jersey if (gamedatas.jerseyIsNotPlayable) { - this.useJerseyForCurrentRound(); + this.useSpecialCardForCurrentRound(CARD_ID_JERSEY_PLUS_TEN); } else { - this.restoreJerseyForCurrentRound(); + this.restoreSpecialCardForCurrentRound(CARD_ID_JERSEY_PLUS_TEN); } this.moveJerseyToCurrentWinner(); @@ -325,12 +420,12 @@ function (dojo, declare) { // init playerHand "ebg.stock" component this.playerHand = new ebg.stock(); this.playerHand.create(this, $(DOM_ID_PLAYER_HAND), CARD_WIDTH, CARD_HEIGHT); - this.playerHand.resizeItems(CARD_WIDTH, CARD_HEIGHT, CARD_WIDTH * 7, CARD_HEIGHT * 8); + this.playerHand.resizeItems(CARD_WIDTH, CARD_HEIGHT, CARD_WIDTH * NUMBER_OF_COLUMNS_IN_CARDS_SPRITE, CARD_HEIGHT * NUMBER_OF_ROWS_IN_CARDS_SPRITE); this.playerHand.setSelectionAppearance('class'); - this.playerHand.image_items_per_row = 7; + this.playerHand.image_items_per_row = NUMBER_OF_COLUMNS_IN_CARDS_SPRITE; // create cards const cardsImageUrl = g_gamethemeurl+'img/cards.png'; - this.execFnForEachCardsInGame((color, value) => { + this.execFnForEachCardInGame((color, value) => { const cardPositionInSprite = this.getCardPositionInSpriteByColorAndValue(color, value); this.playerHand.addItemType( cardPositionInSprite, // stock item ID @@ -364,24 +459,18 @@ function (dojo, declare) { dojo.connect($(DOM_ID_PLAYER_HAND_TOGGLE_SORT_BUTTON), 'onclick', this, 'onClickOnTogglePlayerHandSortButton'); // setup currentPlayer cards this.addCardsToPlayerHand( - (this.currentPlayerHasJersey && !this.jerseyIsNotPlayable) - ? this.addJerseyToCards(gamedatas.currentPlayerCards) - : gamedatas.currentPlayerCards, + this.addSpecialCardsToCards(this.getSpecialPlayerCardIds(), gamedatas.currentPlayerCards), false ); // setup cards played on table this.playedCardsValue = gamedatas.playedCardsValue; this.setupPreviousPlayedCards( - (gamedatas.previousPlayedCardsValue === (this.getCardsValue(gamedatas.previousPlayedCards) + VALUE_JERSEY)) - ? this.addJerseyToCards(gamedatas.previousPlayedCards) - : gamedatas.previousPlayedCards + gamedatas.previousPlayedCards.concat(gamedatas.previousPlayedSpecialCards) ); this.moveCardsFromPlayerHandToTable( gamedatas.playedCardsPlayerId, - (this.playedCardsValue === (this.getCardsValue(gamedatas.playedCards) + VALUE_JERSEY)) - ? this.addJerseyToCards(gamedatas.playedCards) - : gamedatas.playedCards + gamedatas.playedCards.concat(gamedatas.playedSpecialCards) ); this.resetDisplayedNumberOfCardsByPlayerId(); this.setupPlayersHiddenCards(); @@ -444,6 +533,10 @@ function (dojo, declare) { break; case 'playerGiveCardsBackAfterPicking': this.howManyCardsToGiveBack = 0; + if (this.isCurrentPlayerActive()) { + this.unselectAllCards(); + this.displayCardsAsNonSelectable([]); + } break; } @@ -469,9 +562,7 @@ function (dojo, declare) { } break; case 'playerGiveCardsBackAfterPicking': - if (this.isCurrentPlayerActive()) { - this.unselectAllCards(); - } + this.unselectAllCards(); this.setupGiveCardsBackAfterPickingActionButton(); break; } @@ -507,48 +598,92 @@ function (dojo, declare) { * @returns {string} */ getLogHtmlForCards: function (cards) { - const getWidthForCard = (card) => { - if (card.color === COLOR_ADVENTURER) { - return 32; - } - if (card.color === COLOR_JERSEY) { - return 25; - } - return 17; - }; - const getBackgroundOffsetXForCard = (card) => { - if (card.color === COLOR_ADVENTURER) { - return 6; - } - if (card.value === VALUE_1) { - return 12; - } - if (card.value === VALUE_2) { - return 63; - } - if (card.value === VALUE_7) { - return 10; - } - if (card.color === COLOR_JERSEY) { - return 6; - } - return 11; - }; - const getBackgroundOffsetYForCard = (card) => { - if (card.color === COLOR_ADVENTURER) { - return 8; - } - return 7; - }; - return this.sortPlayedCards(cards).map((card) => { const position = this.getCardPositionInSpriteByColorAndValue(card.color, card.value); - const backgroundX = this.getAbsoluteCardBackgroundPositionXFromCardPosition(position) + getBackgroundOffsetXForCard(card); - const backgroundY = this.getAbsoluteCardBackgroundPositionYFromCardPosition(position) + getBackgroundOffsetYForCard(card); + const backgroundX = this.getAbsoluteCardBackgroundPositionXFromCardPosition(position) + this.getLogHtmlBackgroundOffsetXForCard(card); + const backgroundY = this.getAbsoluteCardBackgroundPositionYFromCardPosition(position) + this.getLogHtmlBackgroundOffsetYForCard(card); - return `
`; + return `
`; }).join(' '); }, + getLogHtmlWidthForCard: function (card) { + if (card.color === COLOR_ADVENTURER) { + return 32; + } + if (card.color === COLOR_SPECIAL) { + return 30; + } + return 17; + }, + getLogHtmlBackgroundOffsetXForCard: function (card) { + if (card.color === COLOR_ADVENTURER) { + return 6; + } + if (card.value === VALUE_1) { + return 11; + } + if (card.value === VALUE_2) { + return 63; + } + if (card.value === VALUE_7) { + return 10; + } + if ( + card.color === COLOR_SPECIAL + && card.value === VALUE_JERSEY_PLUS_TEN + ) { + return 4; + } + if ( + card.color === COLOR_SPECIAL + && card.value === VALUE_LEGENDS_BROOM_WAGON_PLUS_FIVE + ) { + return 1; + } + if ( + card.color === COLOR_SPECIAL + && [ + VALUE_LEGENDS_EAGLE_ADD_ONE_OTHER_NUMBER, + VALUE_LEGENDS_PANDA_ADD_ONE_OTHER_COLOR, + VALUE_LEGENDS_SHARK_ONE_RED_MULTIPLY_TEN, + VALUE_LEGENDS_BADGER_ANY_NUMBER_OF_EACH_COLOR, + VALUE_LEGENDS_ELEPHANT_STOP, + ].includes(card.value) + ) { + return 30; + } + return 11; + }, + getLogHtmlBackgroundOffsetYForCard: function (card) { + if (card.color === COLOR_ADVENTURER) { + return 8; + } + if ( + card.color === COLOR_SPECIAL + && card.value === VALUE_JERSEY_PLUS_TEN + ) { + return 5; + } + if ( + card.color === COLOR_SPECIAL + && card.value === VALUE_LEGENDS_BROOM_WAGON_PLUS_FIVE + ) { + return 1; + } + if ( + card.color === COLOR_SPECIAL + && [ + VALUE_LEGENDS_EAGLE_ADD_ONE_OTHER_NUMBER, + VALUE_LEGENDS_PANDA_ADD_ONE_OTHER_COLOR, + VALUE_LEGENDS_SHARK_ONE_RED_MULTIPLY_TEN, + VALUE_LEGENDS_BADGER_ANY_NUMBER_OF_EACH_COLOR, + VALUE_LEGENDS_ELEPHANT_STOP, + ].includes(card.value) + ) { + return 91; + } + return 7; + }, /** * @param {Object} card * @returns {[string, string]|null} @@ -584,11 +719,46 @@ function (dojo, declare) { dojo.string.substitute(_('Adventurer - Value: ${v} - This card cannot be played with others, because the adventurers does not belong to a team, they always play solo.'), { v: card.value }), '', ]; - case COLOR_JERSEY: - return [ - _('Carrot polka dot Jersey - Value: +10 - It adds 10 points to any valid card combinations (one or more colored cards). It cannot be played with adventurers.'), - '' - ]; + case COLOR_SPECIAL: + switch (card.value) { + case VALUE_JERSEY_PLUS_TEN: + return [ + dojo.string.substitute(_('Carrot polka dot Jersey - Given to the current winner of the game - It adds ${v} points to any valid card combinations (one or more colored cards). It cannot be played with adventurers.'), { v: VALUE_JERSEY_PLUS_TEN }), + '' + ]; + case VALUE_LEGENDS_BROOM_WAGON_PLUS_FIVE: + return [ + dojo.string.substitute(_('Broom Wagon - Given to the loser of the previous round - It adds ${v} points to any valid card combinations (one or more colored cards). It cannot be played with adventurers.'), { v: VALUE_LEGENDS_BROOM_WAGON_PLUS_FIVE }), + '' + ]; + case VALUE_LEGENDS_EAGLE_ADD_ONE_OTHER_NUMBER: + return [ + _('Coach Eagle - Allow to add a card of a different value to card(s) that share the same value.'), + '' + ]; + case VALUE_LEGENDS_PANDA_ADD_ONE_OTHER_COLOR: + return [ + _('Coach Panda - Allow to add a card of a different color to card(s) that share the same color.'), + '' + ]; + case VALUE_LEGENDS_SHARK_ONE_RED_MULTIPLY_TEN: + return [ + _('Coach Shark - Multiply the value of a single red card by 10 (e.g. a "5 red" played with this coach is equal to "50").'), + '' + ]; + case VALUE_LEGENDS_BADGER_ANY_NUMBER_OF_EACH_COLOR: + return [ + _('Coach Badger - Allow to combine a single card of each color, even if they do not share the same value.'), + '' + ]; + case VALUE_LEGENDS_ELEPHANT_STOP: + return [ + _('Coach Elephant - The card(s) played with this coach immediately end the current turn (i.e. like if no other players wanted to play on top of it).'), + '' + ]; + default: + return null; + } default: return null; } @@ -667,7 +837,7 @@ function (dojo, declare) { const playerCardsHtml = []; for (let i = 0; i < howManyCards; i++) { - playerCardsHtml.push(`
`); + playerCardsHtml.push(`
`); } $(`player-table-${playerId}-hand-cards`).innerHTML = playerCardsHtml.join(''); $(`player-panel-${playerId}-remaining-cards-number`).innerHTML = howManyCards; @@ -690,6 +860,7 @@ function (dojo, declare) { dojo.removeClass(`player-table-${player.id}`, `has-finished-1`); dojo.removeClass(`player-table-${player.id}`, `has-finished-2`); dojo.removeClass(`player-table-${player.id}`, `has-finished-3`); + dojo.removeClass(`player-table-${player.id}`, `has-finished-4`); if (playerCurrentRoundRank) { dojo.addClass(`player-table-${player.id}`, `has-finished-${playerCurrentRoundRank}`); } @@ -779,53 +950,176 @@ function (dojo, declare) { dojo.toggleClass(DOM_ID_ACTION_BUTTON_GIVE_CARDS, DOM_CLASS_DISABLED_ACTION_BUTTON, (selectedCards.length === 0) || (selectedCards.length !== this.howManyCardsToGiveBack)); }, /** - * @param {number|undefined} [previousJerseyWearerId] + * @param {number=} previousJerseyWearerId */ moveJerseyToCurrentWinner: function (previousJerseyWearerId) { - const wearJersey = (playerId) => { - dojo.addClass(`player-table-${playerId}`, DOM_CLASS_PLAYER_IS_WEARING_JERSEY); - dojo.place(`
`, `player-panel-${playerId}-velonimo-right`); - this.addTooltip(`player-panel-${playerId}-jersey`, _('Current leader of the game'), ''); + const card = this.addSpecialCardsToCards([CARD_ID_JERSEY_PLUS_TEN], [])[0]; + const tooltipTexts = this.getTooltipTextsForCard(card); + const position = this.getCardPositionInSpriteByColorAndValue(card.color, card.value); + const backgroundX = this.getAbsoluteCardBackgroundPositionXFromCardPosition(position) + this.getLogHtmlBackgroundOffsetXForCard(card); + const backgroundY = this.getAbsoluteCardBackgroundPositionYFromCardPosition(position) + this.getLogHtmlBackgroundOffsetYForCard(card); + + const applyJersey = (playerId) => { + dojo.place(`
`, `player-table-${playerId}-special-cards`); + this.addTooltip(`player-table-${playerId}-jersey`, tooltipTexts[0], tooltipTexts[1]); + dojo.addClass(`player-table-${playerId}`, DOM_CLASS_PLAYER_HAS_JERSEY); + dojo.place(`
`, `player-panel-${playerId}-velonimo-right`); + this.addTooltip(`player-panel-${playerId}-jersey`, tooltipTexts[0], tooltipTexts[1]); }; const removeJersey = (playerId) => { - dojo.removeClass(`player-table-${playerId}`, DOM_CLASS_PLAYER_IS_WEARING_JERSEY); + dojo.removeClass(`player-table-${playerId}`, DOM_CLASS_PLAYER_HAS_JERSEY); this.fadeOutAndDestroy(`player-panel-${playerId}-jersey`); }; Object.entries(this.players).forEach((entry) => { const player = entry[1]; - const isCurrentPlayer = this.player_id === player.id; if (player.isWearingJersey) { - this.currentPlayerHasJersey = isCurrentPlayer; - - if (!dojo.hasClass(`player-table-${player.id}`, DOM_CLASS_PLAYER_IS_WEARING_JERSEY)) { - if (previousJerseyWearerId) { - // move jersey from player A to player B, - // then add isWearingJersey class to player B at the end of the animation - const animation = this.slideTemporaryObject( - `
`, - `player-table-${previousJerseyWearerId}-jersey`, - `player-table-${previousJerseyWearerId}-jersey`, - `player-table-${player.id}-jersey` - ); - dojo.connect(animation, 'onEnd', () => wearJersey(player.id)); - animation.play(); - } else { - wearJersey(player.id); - } + this.currentPlayerHasJersey = this.player_id === player.id; + + if (!dojo.hasClass(`player-table-${player.id}`, DOM_CLASS_PLAYER_HAS_JERSEY)) { + applyJersey(player.id); } } else { - if (dojo.hasClass(`player-table-${player.id}`, DOM_CLASS_PLAYER_IS_WEARING_JERSEY)) { + if (dojo.hasClass(`player-table-${player.id}`, DOM_CLASS_PLAYER_HAS_JERSEY)) { removeJersey(player.id); } } }); }, + /** + * @param {number=} previousLegendsBroomWagonHolderId + */ + moveLegendsBroomWagonToLastLoser: function (previousLegendsBroomWagonHolderId) { + if (!this.isExtensionLegendsEnabled) { + return; + } + + const card = this.addSpecialCardsToCards([CARD_ID_LEGENDS_BROOM_WAGON_PLUS_FIVE], [])[0]; + const tooltipTexts = this.getTooltipTextsForCard(card); + const position = this.getCardPositionInSpriteByColorAndValue(card.color, card.value); + const backgroundX = this.getAbsoluteCardBackgroundPositionXFromCardPosition(position) + this.getLogHtmlBackgroundOffsetXForCard(card); + const backgroundY = this.getAbsoluteCardBackgroundPositionYFromCardPosition(position) + this.getLogHtmlBackgroundOffsetYForCard(card); + + const applyLegendsBroomWagon = (playerId) => { + dojo.place(`
`, `player-table-${playerId}-special-cards`); + this.addTooltip(`player-table-${playerId}-legends-broom-wagon`, tooltipTexts[0], tooltipTexts[1]); + dojo.addClass(`player-table-${playerId}`, DOM_CLASS_PLAYER_HAS_LEGENDS_BROOM_WAGON); + dojo.place(`
`, `player-panel-${playerId}-velonimo-right`); + this.addTooltip(`player-panel-${playerId}-legends-broom-wagon`, tooltipTexts[0], tooltipTexts[1]); + }; + const removeLegendsBroomWagon = (playerId) => { + dojo.removeClass(`player-table-${playerId}`, DOM_CLASS_PLAYER_HAS_LEGENDS_BROOM_WAGON); + this.fadeOutAndDestroy(`player-panel-${playerId}-legends-broom-wagon`); + }; + + Object.entries(this.players).forEach((entry) => { + const player = entry[1]; + + if (player.hasCardLegendsBroomWagon) { + this.currentPlayerHasLegendsBroomWagon = this.player_id === player.id; + + if (!dojo.hasClass(`player-table-${player.id}`, DOM_CLASS_PLAYER_HAS_LEGENDS_BROOM_WAGON)) { + applyLegendsBroomWagon(player.id); + } + } else { + if (dojo.hasClass(`player-table-${player.id}`, DOM_CLASS_PLAYER_HAS_LEGENDS_BROOM_WAGON)) { + removeLegendsBroomWagon(player.id); + } + } + }); + }, + moveLegendsCoachToPlayers: function () { + if (!this.isExtensionLegendsEnabled) { + return; + } + + Object.entries(this.players).forEach((entry) => { + const player = entry[1]; + const cardId = this.getLegendsCoachCardIdFromPlayerIfExists(player); + if (typeof cardId === 'undefined') { + return; + } + + const card = this.addSpecialCardsToCards([cardId], [])[0]; + const specialCardDescription = this.getTooltipTextsForCard(card); + let playerHasSpecialCardClass; + let specialCardInPanelClass; + let specialCardDomSuffix; + let currentPlayerHasSpecialCardProperty; + switch (cardId) { + case CARD_ID_LEGENDS_EAGLE_ADD_ONE_OTHER_NUMBER: + playerHasSpecialCardClass = DOM_CLASS_PLAYER_HAS_LEGENDS_EAGLE; + specialCardInPanelClass = DOM_CLASS_LEGENDS_EAGLE_IN_PLAYER_PANEL; + specialCardDomSuffix = 'legends-coach-eagle'; + currentPlayerHasSpecialCardProperty = 'currentPlayerHasLegendsEagle'; + break; + case CARD_ID_LEGENDS_PANDA_ADD_ONE_OTHER_COLOR: + playerHasSpecialCardClass = DOM_CLASS_PLAYER_HAS_LEGENDS_PANDA; + specialCardInPanelClass = DOM_CLASS_LEGENDS_PANDA_IN_PLAYER_PANEL; + specialCardDomSuffix = 'legends-coach-panda'; + currentPlayerHasSpecialCardProperty = 'currentPlayerHasLegendsPanda'; + break; + case CARD_ID_LEGENDS_SHARK_ONE_RED_MULTIPLY_TEN: + playerHasSpecialCardClass = DOM_CLASS_PLAYER_HAS_LEGENDS_SHARK; + specialCardInPanelClass = DOM_CLASS_LEGENDS_SHARK_IN_PLAYER_PANEL; + specialCardDomSuffix = 'legends-coach-shark'; + currentPlayerHasSpecialCardProperty = 'currentPlayerHasLegendsShark'; + break; + case CARD_ID_LEGENDS_BADGER_ANY_NUMBER_OF_EACH_COLOR: + playerHasSpecialCardClass = DOM_CLASS_PLAYER_HAS_LEGENDS_BADGER; + specialCardInPanelClass = DOM_CLASS_LEGENDS_BADGER_IN_PLAYER_PANEL; + specialCardDomSuffix = 'legends-coach-badger'; + currentPlayerHasSpecialCardProperty = 'currentPlayerHasLegendsBadger'; + break; + case CARD_ID_LEGENDS_ELEPHANT_STOP: + playerHasSpecialCardClass = DOM_CLASS_PLAYER_HAS_LEGENDS_ELEPHANT; + specialCardInPanelClass = DOM_CLASS_LEGENDS_ELEPHANT_IN_PLAYER_PANEL; + specialCardDomSuffix = 'legends-coach-elephant'; + currentPlayerHasSpecialCardProperty = 'currentPlayerHasLegendsElephant'; + break; + default: + throw new Error('Unsupported cardId for moveLegendsCoachToPlayers'); + } + + const position = this.getCardPositionInSpriteByColorAndValue(card.color, card.value); + const backgroundX = this.getAbsoluteCardBackgroundPositionXFromCardPosition(position) + this.getLogHtmlBackgroundOffsetXForCard(card); + const backgroundY = this.getAbsoluteCardBackgroundPositionYFromCardPosition(position) + this.getLogHtmlBackgroundOffsetYForCard(card); + + this[currentPlayerHasSpecialCardProperty] = this.player_id === player.id; + dojo.place(`
`, `player-table-${player.id}-special-cards`); + dojo.addClass(`player-table-${player.id}`, playerHasSpecialCardClass); + this.addTooltip(`player-table-${player.id}-legends-coach`, specialCardDescription[0], specialCardDescription[1]); + dojo.place(`
`, `player-panel-${player.id}-velonimo-right`); + this.addTooltip(`player-panel-${player.id}-${specialCardDomSuffix}`, specialCardDescription[0], specialCardDescription[1]); + }); + }, + /** + * @param {Object} player + * @returns {number|undefined} + */ + getLegendsCoachCardIdFromPlayerIfExists: function (player) { + if (player.hasCardLegendsEagle) { + return CARD_ID_LEGENDS_EAGLE_ADD_ONE_OTHER_NUMBER; + } + if (player.hasCardLegendsPanda) { + return CARD_ID_LEGENDS_PANDA_ADD_ONE_OTHER_COLOR; + } + if (player.hasCardLegendsShark) { + return CARD_ID_LEGENDS_SHARK_ONE_RED_MULTIPLY_TEN; + } + if (player.hasCardLegendsBadger) { + return CARD_ID_LEGENDS_BADGER_ANY_NUMBER_OF_EACH_COLOR; + } + if (player.hasCardLegendsElephant) { + return CARD_ID_LEGENDS_ELEPHANT_STOP; + } + return undefined; + }, /** * @returns {number|undefined} */ - getCurrentJerseyWearerIdIfExists: function () { + getCurrentJerseyOwnerIdIfExists: function () { const players = Object.entries(this.players); for (let i = 0; i < players.length; i++) { const player = players[i][1]; @@ -836,26 +1130,124 @@ function (dojo, declare) { return undefined; }, - useJerseyForCurrentRound: function () { - this.jerseyIsNotPlayable = true; + /** + * @returns {number|undefined} + */ + getCurrentLegendsBroomWagonOwnerIdIfExists: function () { + const players = Object.entries(this.players); + for (let i = 0; i < players.length; i++) { + const player = players[i][1]; + if (player.hasCardLegendsBroomWagon) { + return player.id; + } + } + return undefined; + }, + /** + * @param {number} cardId + */ + useSpecialCardForCurrentRound: function (cardId) { Object.entries(this.players).forEach((entry) => { const player = entry[1]; - if ( - player.isWearingJersey - && !dojo.hasClass(`player-table-${player.id}`, DOM_CLASS_PLAYER_HAS_USED_JERSEY) - ) { - dojo.addClass(`player-table-${player.id}`, DOM_CLASS_PLAYER_HAS_USED_JERSEY); + let playerHasSpecialCard; + let playerHasUsedSpecialCardClass; + switch (cardId) { + case CARD_ID_JERSEY_PLUS_TEN: + this.jerseyIsNotPlayable = true; + playerHasSpecialCard = player.isWearingJersey; + playerHasUsedSpecialCardClass = DOM_CLASS_PLAYER_HAS_USED_JERSEY; + break; + case CARD_ID_LEGENDS_BROOM_WAGON_PLUS_FIVE: + this.legendsBroomWagonIsNotPlayable = true; + playerHasSpecialCard = player.hasCardLegendsBroomWagon; + playerHasUsedSpecialCardClass = DOM_CLASS_PLAYER_HAS_USED_LEGENDS_BROOM_WAGON; + break; + case CARD_ID_LEGENDS_EAGLE_ADD_ONE_OTHER_NUMBER: + this.legendsEagleIsNotPlayable = true; + playerHasSpecialCard = player.hasCardLegendsEagle; + playerHasUsedSpecialCardClass = DOM_CLASS_PLAYER_HAS_USED_LEGENDS_EAGLE; + break; + case CARD_ID_LEGENDS_PANDA_ADD_ONE_OTHER_COLOR: + this.legendsPandaIsNotPlayable = true; + playerHasSpecialCard = player.hasCardLegendsPanda; + playerHasUsedSpecialCardClass = DOM_CLASS_PLAYER_HAS_USED_LEGENDS_PANDA; + break; + case CARD_ID_LEGENDS_SHARK_ONE_RED_MULTIPLY_TEN: + this.legendsSharkIsNotPlayable = true; + playerHasSpecialCard = player.hasCardLegendsShark; + playerHasUsedSpecialCardClass = DOM_CLASS_PLAYER_HAS_USED_LEGENDS_SHARK; + break; + case CARD_ID_LEGENDS_BADGER_ANY_NUMBER_OF_EACH_COLOR: + this.legendsBadgerIsNotPlayable = true; + playerHasSpecialCard = player.hasCardLegendsBadger; + playerHasUsedSpecialCardClass = DOM_CLASS_PLAYER_HAS_USED_LEGENDS_BADGER; + break; + case CARD_ID_LEGENDS_ELEPHANT_STOP: + this.legendsElephantIsNotPlayable = true; + playerHasSpecialCard = player.hasCardLegendsElephant; + playerHasUsedSpecialCardClass = DOM_CLASS_PLAYER_HAS_USED_LEGENDS_ELEPHANT; + break; + default: + throw new Error('Unsupported cardId for useSpecialCardForCurrentRound'); + } + + if (playerHasSpecialCard) { + if (!dojo.hasClass(`player-table-${player.id}`, playerHasUsedSpecialCardClass)) { + dojo.addClass(`player-table-${player.id}`, playerHasUsedSpecialCardClass); + } + if (!dojo.hasClass(`player-panel-${player.id}-velonimo-right`, playerHasUsedSpecialCardClass)) { + dojo.addClass(`player-panel-${player.id}-velonimo-right`, playerHasUsedSpecialCardClass); + } } }); }, - restoreJerseyForCurrentRound: function () { - this.jerseyIsNotPlayable = false; + /** + * @param {number} cardId + */ + restoreSpecialCardForCurrentRound: function (cardId) { + let playerHasUsedSpecialCardClass; + switch (cardId) { + case CARD_ID_JERSEY_PLUS_TEN: + this.jerseyIsNotPlayable = false; + playerHasUsedSpecialCardClass = DOM_CLASS_PLAYER_HAS_USED_JERSEY; + break; + case CARD_ID_LEGENDS_BROOM_WAGON_PLUS_FIVE: + this.legendsBroomWagonIsNotPlayable = false; + playerHasUsedSpecialCardClass = DOM_CLASS_PLAYER_HAS_USED_LEGENDS_BROOM_WAGON; + break; + case CARD_ID_LEGENDS_EAGLE_ADD_ONE_OTHER_NUMBER: + this.legendsEagleIsNotPlayable = false; + playerHasUsedSpecialCardClass = DOM_CLASS_PLAYER_HAS_USED_LEGENDS_EAGLE; + break; + case CARD_ID_LEGENDS_PANDA_ADD_ONE_OTHER_COLOR: + this.legendsPandaIsNotPlayable = false; + playerHasUsedSpecialCardClass = DOM_CLASS_PLAYER_HAS_USED_LEGENDS_PANDA; + break; + case CARD_ID_LEGENDS_SHARK_ONE_RED_MULTIPLY_TEN: + this.legendsSharkIsNotPlayable = false; + playerHasUsedSpecialCardClass = DOM_CLASS_PLAYER_HAS_USED_LEGENDS_SHARK; + break; + case CARD_ID_LEGENDS_BADGER_ANY_NUMBER_OF_EACH_COLOR: + this.legendsBadgerIsNotPlayable = false; + playerHasUsedSpecialCardClass = DOM_CLASS_PLAYER_HAS_USED_LEGENDS_BADGER; + break; + case CARD_ID_LEGENDS_ELEPHANT_STOP: + this.legendsElephantIsNotPlayable = false; + playerHasUsedSpecialCardClass = DOM_CLASS_PLAYER_HAS_USED_LEGENDS_ELEPHANT; + break; + default: + throw new Error('Unsupported cardId for useSpecialCardForCurrentRound'); + } Object.entries(this.players).forEach((entry) => { const player = entry[1]; - if (dojo.hasClass(`player-table-${player.id}`, DOM_CLASS_PLAYER_HAS_USED_JERSEY)) { - dojo.removeClass(`player-table-${player.id}`, DOM_CLASS_PLAYER_HAS_USED_JERSEY); + + if (dojo.hasClass(`player-table-${player.id}`, playerHasUsedSpecialCardClass)) { + dojo.removeClass(`player-table-${player.id}`, playerHasUsedSpecialCardClass); + } + if (dojo.hasClass(`player-panel-${player.id}-velonimo-right`, playerHasUsedSpecialCardClass)) { + dojo.removeClass(`player-panel-${player.id}-velonimo-right`, playerHasUsedSpecialCardClass); } }); }, @@ -873,17 +1265,9 @@ function (dojo, declare) { /** * @param {function (number, number)} fn such as (color, value) => {...} */ - execFnForEachCardsInGame: function (fn) { + execFnForEachCardInGame: function (fn) { // colored cards - [ - COLOR_BLUE, - COLOR_BROWN, - COLOR_GRAY, - COLOR_GREEN, - COLOR_PINK, - COLOR_RED, - COLOR_YELLOW, - ].forEach((color) => { + SIMPLE_COLORS.forEach((color) => { [ VALUE_1, VALUE_2, @@ -905,14 +1289,24 @@ function (dojo, declare) { VALUE_50, ].forEach((value) => fn.bind(this)(COLOR_ADVENTURER, value)); - // jersey card - fn.bind(this)(COLOR_JERSEY, VALUE_JERSEY); + // special cards + fn.bind(this)(COLOR_SPECIAL, VALUE_JERSEY_PLUS_TEN); + if (this.isExtensionLegendsEnabled) { + [ + VALUE_LEGENDS_BROOM_WAGON_PLUS_FIVE, + VALUE_LEGENDS_EAGLE_ADD_ONE_OTHER_NUMBER, + VALUE_LEGENDS_PANDA_ADD_ONE_OTHER_COLOR, + VALUE_LEGENDS_SHARK_ONE_RED_MULTIPLY_TEN, + VALUE_LEGENDS_BADGER_ANY_NUMBER_OF_EACH_COLOR, + VALUE_LEGENDS_ELEPHANT_STOP, + ].forEach((value) => fn.bind(this)(COLOR_SPECIAL, value)); + } }, /** - * This function gives the position of the card in the sprite "cards.png", - * it also gives weight to cards to sort them by color (blue-1, blue-2, ...) just like in the sprite, - * it also gives the type ID for the stock component. - * + * This function gives: + * - the position of the card in the sprite "cards.png" + * - weight to cards to sort them by color (blue-1, blue-2, ...) just like in the sprite + * - the type ID for the stock component * * @param {number} color * @param {number} value @@ -934,26 +1328,45 @@ function (dojo, declare) { return 35 + value - 1; case COLOR_YELLOW: return 42 + value - 1; + case COLOR_SPECIAL: + switch (value) { + case VALUE_JERSEY_PLUS_TEN: + return 49; + case VALUE_LEGENDS_BROOM_WAGON_PLUS_FIVE: + return 50; + case VALUE_LEGENDS_EAGLE_ADD_ONE_OTHER_NUMBER: + return 51; + case VALUE_LEGENDS_PANDA_ADD_ONE_OTHER_COLOR: + return 52; + case VALUE_LEGENDS_SHARK_ONE_RED_MULTIPLY_TEN: + return 53; + case VALUE_LEGENDS_BADGER_ANY_NUMBER_OF_EACH_COLOR: + return 54; + case VALUE_LEGENDS_ELEPHANT_STOP: + return 55; + default: + throw new Error('Unsupported'); + } case COLOR_ADVENTURER: switch (value) { case VALUE_25: - return 50; + return 57; case VALUE_30: - return 51; + return 58; case VALUE_35: - return 52; + return 59; case VALUE_40: - return 53; + return 60; case VALUE_45: - return 54; + return 61; case VALUE_50: - return 55; + return 62; default: throw new Error('Unsupported'); } default: - // Jersey - return 49; + // Card back + throw new Error('Unsupported'); } }, getSortingWeightForCardsGroups: function () { @@ -978,7 +1391,7 @@ function (dojo, declare) { }, sortPlayerCardsByColor: function () { const cardsWeightByPosition = {}; - this.execFnForEachCardsInGame((color, value) => { + this.execFnForEachCardInGame((color, value) => { const cardPositionAndWeight = this.getCardPositionInSpriteByColorAndValue(color, value); cardsWeightByPosition[cardPositionAndWeight] = cardPositionAndWeight; }); @@ -991,11 +1404,15 @@ function (dojo, declare) { * @returns {number} */ getCardWeightForColorAndValueToSortThemByValue: function (color, value) { + if (color === COLOR_SPECIAL) { + return value + 1000; + } + return (value * 100) + color; }, sortPlayerCardsByValue: function () { const cardsWeightByPosition = {}; - this.execFnForEachCardsInGame((color, value) => { + this.execFnForEachCardInGame((color, value) => { cardsWeightByPosition[this.getCardPositionInSpriteByColorAndValue(color, value)] = this.getCardWeightForColorAndValueToSortThemByValue(color, value); }); @@ -1006,14 +1423,14 @@ function (dojo, declare) { * @returns {number} */ getAbsoluteCardBackgroundPositionXFromCardPosition: function (position) { - return (position % 7) * CARD_WIDTH; + return (position % NUMBER_OF_COLUMNS_IN_CARDS_SPRITE) * CARD_WIDTH; }, /** * @param {number} position * @returns {number} */ getAbsoluteCardBackgroundPositionYFromCardPosition: function (position) { - return Math.floor(position / 7) * CARD_HEIGHT; + return Math.floor(position / NUMBER_OF_COLUMNS_IN_CARDS_SPRITE) * CARD_HEIGHT; }, /** * @@ -1223,30 +1640,54 @@ function (dojo, declare) { value = VALUE_7; break; case 49: - color = COLOR_JERSEY; - value = VALUE_JERSEY; + color = COLOR_SPECIAL; + value = VALUE_JERSEY_PLUS_TEN; break; case 50: + color = COLOR_SPECIAL; + value = VALUE_LEGENDS_BROOM_WAGON_PLUS_FIVE; + break; + case 51: + color = COLOR_SPECIAL; + value = VALUE_LEGENDS_EAGLE_ADD_ONE_OTHER_NUMBER; + break; + case 52: + color = COLOR_SPECIAL; + value = VALUE_LEGENDS_PANDA_ADD_ONE_OTHER_COLOR; + break; + case 53: + color = COLOR_SPECIAL; + value = VALUE_LEGENDS_SHARK_ONE_RED_MULTIPLY_TEN; + break; + case 54: + color = COLOR_SPECIAL; + value = VALUE_LEGENDS_BADGER_ANY_NUMBER_OF_EACH_COLOR; + break; + case 55: + color = COLOR_SPECIAL; + value = VALUE_LEGENDS_ELEPHANT_STOP; + break; + case 57: color = COLOR_ADVENTURER; value = VALUE_25; break; - case 51: + case 58: color = COLOR_ADVENTURER; value = VALUE_30; break; - case 52: + case 59: color = COLOR_ADVENTURER; value = VALUE_35; break; - case 53: + case 60: color = COLOR_ADVENTURER; value = VALUE_40; break; - case 54: + case 61: color = COLOR_ADVENTURER; value = VALUE_45; break; - case 55: + case 62: color = COLOR_ADVENTURER; value = VALUE_50; break; @@ -1268,66 +1709,119 @@ function (dojo, declare) { */ getCardsThatCanBePlayedWithCard: function (color, value, cards) { return cards.filter((card) => { - if (color === COLOR_JERSEY) { + // legends coach elephant + if ( + (color === COLOR_SPECIAL && value === VALUE_LEGENDS_ELEPHANT_STOP) + || card.id === CARD_ID_LEGENDS_ELEPHANT_STOP + ) { + return true; + } + + // adventurer + if ( + (color === COLOR_ADVENTURER || card.color === COLOR_ADVENTURER) + && value !== card.value + ) { + return false; + } + + // legends broom wagon + if (color === COLOR_SPECIAL && value === VALUE_LEGENDS_BROOM_WAGON_PLUS_FIVE) { return card.color !== COLOR_ADVENTURER; } - if (card.color === COLOR_JERSEY) { + if (card.id === CARD_ID_LEGENDS_BROOM_WAGON_PLUS_FIVE) { return color !== COLOR_ADVENTURER; } - if (color === COLOR_ADVENTURER && value !== card.value) { - return false; + // jersey + if (color === COLOR_SPECIAL && value === VALUE_JERSEY_PLUS_TEN) { + return ( + card.color !== COLOR_ADVENTURER + && ![ + CARD_ID_LEGENDS_EAGLE_ADD_ONE_OTHER_NUMBER, + CARD_ID_LEGENDS_PANDA_ADD_ONE_OTHER_COLOR, + CARD_ID_LEGENDS_SHARK_ONE_RED_MULTIPLY_TEN, + CARD_ID_LEGENDS_BADGER_ANY_NUMBER_OF_EACH_COLOR, + CARD_ID_LEGENDS_ELEPHANT_STOP, + ].includes(card.id) + ); + } + if (card.id === CARD_ID_JERSEY_PLUS_TEN) { + return ( + color !== COLOR_ADVENTURER + && !( + color === COLOR_SPECIAL + && [ + VALUE_LEGENDS_EAGLE_ADD_ONE_OTHER_NUMBER, + VALUE_LEGENDS_PANDA_ADD_ONE_OTHER_COLOR, + VALUE_LEGENDS_SHARK_ONE_RED_MULTIPLY_TEN, + VALUE_LEGENDS_BADGER_ANY_NUMBER_OF_EACH_COLOR, + VALUE_LEGENDS_ELEPHANT_STOP, + ].includes(value) + ) + ); } - if (color === card.color) { - return true; + // legends coach shark + if (color === COLOR_SPECIAL && value === VALUE_LEGENDS_SHARK_ONE_RED_MULTIPLY_TEN) { + return (card.color === COLOR_RED || card.id === CARD_ID_LEGENDS_SHARK_ONE_RED_MULTIPLY_TEN); + } + if (card.id === CARD_ID_LEGENDS_SHARK_ONE_RED_MULTIPLY_TEN) { + return (color === COLOR_RED || (color === COLOR_SPECIAL && value === VALUE_LEGENDS_SHARK_ONE_RED_MULTIPLY_TEN)); } - return value === card.value; - }); - }, - /** - * @param {Object[]} cards - * @returns {Object[]} - */ - getPlayerCardsThatCanBePlayedWithCards: function (cards) { - const playerCards = this.getAllPlayerCards(); - if (cards.length === 0) { - return playerCards; - } + // legends coach eagle + if (color === COLOR_SPECIAL && value === VALUE_LEGENDS_EAGLE_ADD_ONE_OTHER_NUMBER) { + return ( + card.id === CARD_ID_LEGENDS_EAGLE_ADD_ONE_OTHER_NUMBER + || SIMPLE_COLORS.includes(card.color) + ); + } + if (card.id === CARD_ID_LEGENDS_EAGLE_ADD_ONE_OTHER_NUMBER) { + return ( + (color === COLOR_SPECIAL && value === VALUE_LEGENDS_EAGLE_ADD_ONE_OTHER_NUMBER) + || SIMPLE_COLORS.includes(color) + ); + } - return cards.reduce( - (acc, card, i, cards) => acc.concat( - this.getCardsThatCanBePlayedWithCard(card.color, card.value, playerCards) - .filter((c) => acc.every((accCard) => c.id !== accCard.id)) - ).filter((c) => cards.every((cardsCard) => c.id !== cardsCard.id)), - [] - ); - }, - /** - * @param {number} color - * @param {number} value - * @param {Object[]} cards - * @returns {Object[]} - */ - getCardsThatCannotBePlayedWithCard: function (color, value, cards) { - return cards.filter((card) => { - if (color === COLOR_JERSEY) { - return card.color === COLOR_ADVENTURER; + // legends coach panda + if (color === COLOR_SPECIAL && value === VALUE_LEGENDS_PANDA_ADD_ONE_OTHER_COLOR) { + return ( + card.id === CARD_ID_LEGENDS_PANDA_ADD_ONE_OTHER_COLOR + || SIMPLE_COLORS.includes(card.color) + ); } - if (card.color === COLOR_JERSEY) { - return color === COLOR_ADVENTURER; + if (card.id === CARD_ID_LEGENDS_PANDA_ADD_ONE_OTHER_COLOR) { + return ( + (color === COLOR_SPECIAL && value === VALUE_LEGENDS_PANDA_ADD_ONE_OTHER_COLOR) + || SIMPLE_COLORS.includes(color) + ); } - if (color === COLOR_ADVENTURER && value !== card.value) { - return true; + // legends coach badger + if (color === COLOR_SPECIAL && value === VALUE_LEGENDS_BADGER_ANY_NUMBER_OF_EACH_COLOR) { + return ( + card.id === CARD_ID_LEGENDS_BADGER_ANY_NUMBER_OF_EACH_COLOR + || SIMPLE_COLORS.includes(card.color) + ); + } + if (card.id === CARD_ID_LEGENDS_BADGER_ANY_NUMBER_OF_EACH_COLOR) { + return ( + (color === COLOR_SPECIAL && value === VALUE_LEGENDS_BADGER_ANY_NUMBER_OF_EACH_COLOR) + || SIMPLE_COLORS.includes(color) + ); } + // simple cards if (color === card.color) { - return false; + return true; + } + if (value === card.value) { + return true; } - return value !== card.value; + // everything else + return false; }); }, /** @@ -1339,15 +1833,144 @@ function (dojo, declare) { return []; } + const sortEntriesByLength = ([_k1, cards1], [_k2, cards2]) => cards2.length - cards1.length; + const playerCards = this.getAllPlayerCards(); + const playerCardIds = playerCards.map((c) => c.id); + const cardIds = cards.map((c) => c.id); - return cards.reduce( + /** @var {Object[]} nonPlayableCards */ + let nonPlayableCards = cards.reduce( (acc, card) => acc.concat( - this.getCardsThatCannotBePlayedWithCard(card.color, card.value, playerCards) - .filter((c) => acc.every((accCard) => c.id !== accCard.id)) + playerCards.filter((c) => !this.getCardsThatCanBePlayedWithCard(card.color, card.value, playerCards).some((playableCard) => c.id === playableCard.id)) ), [] ); + const nonPlayableCardIds = nonPlayableCards.map((c) => c.id); + + if (playerCardIds.includes(CARD_ID_LEGENDS_SHARK_ONE_RED_MULTIPLY_TEN)) { + const selectedRedCards = cards.filter((c) => c.color === COLOR_RED); + const numberOfSelectedRedCards = selectedRedCards.length; + if (numberOfSelectedRedCards > 1) { + if (cardIds.includes(CARD_ID_LEGENDS_SHARK_ONE_RED_MULTIPLY_TEN)) { + nonPlayableCards = [...selectedRedCards, ...nonPlayableCards]; + } else if (!nonPlayableCardIds.includes(CARD_ID_LEGENDS_SHARK_ONE_RED_MULTIPLY_TEN)) { + nonPlayableCards = this.addSpecialCardsToCards([CARD_ID_LEGENDS_SHARK_ONE_RED_MULTIPLY_TEN], nonPlayableCards); + } + } + } else if (playerCardIds.includes(CARD_ID_LEGENDS_EAGLE_ADD_ONE_OTHER_NUMBER)) { + const selectedSimpleCards = cards.filter((c) => SIMPLE_COLORS.includes(c.color)); + const selectedSimpleCardValues = selectedSimpleCards.map((c) => c.value); + const numberOfDifferentValues = [...new Set(selectedSimpleCardValues)].length; + const selectedSimpleCardsGroupedBySameValue = Object.entries(selectedSimpleCards.reduce((acc, c) => { + return { + ...acc, + [c.value]: [...(acc[c.value] || []), c], + }; + }, {})).sort(sortEntriesByLength); + if ( + numberOfDifferentValues > 2 + || ( + numberOfDifferentValues === 2 + && selectedSimpleCardsGroupedBySameValue[0][1].length > 1 + && selectedSimpleCardsGroupedBySameValue[1][1].length > 1 + ) + ) { + if (cardIds.includes(CARD_ID_LEGENDS_EAGLE_ADD_ONE_OTHER_NUMBER)) { + nonPlayableCards = [...selectedSimpleCards, ...nonPlayableCards]; + } else if (!nonPlayableCardIds.includes(CARD_ID_LEGENDS_EAGLE_ADD_ONE_OTHER_NUMBER)) { + nonPlayableCards = this.addSpecialCardsToCards([CARD_ID_LEGENDS_EAGLE_ADD_ONE_OTHER_NUMBER], nonPlayableCards); + } + } else if (cardIds.includes(CARD_ID_LEGENDS_EAGLE_ADD_ONE_OTHER_NUMBER)) { + const selectedSimpleCardIds = selectedSimpleCards.map((c) => c.id); + const playerSimpleCardIds = playerCards.filter((c) => SIMPLE_COLORS.includes(c.color)).map((c) => c.id); + const nonPlayablePlayerSimpleCardIds = playerCards + .filter((c) => SIMPLE_COLORS.includes(c.color)) + .filter((c) => + !selectedSimpleCardIds.includes(c.id) + && numberOfDifferentValues > 1 + && ( + ( + selectedSimpleCardsGroupedBySameValue[0][1].length === 1 + && !selectedSimpleCardValues.includes(c.value) + ) || ( + selectedSimpleCardsGroupedBySameValue[0][1].length > 1 + && c.value !== parseInt(selectedSimpleCardsGroupedBySameValue[0][0], 10) + ) + ) + ) + .map((c) => c.id); + nonPlayableCards = playerCards + .filter((c) => nonPlayablePlayerSimpleCardIds.includes(c.id) || (nonPlayableCardIds.includes(c.id) && !playerSimpleCardIds.includes(c.id))); + } + } else if (playerCardIds.includes(CARD_ID_LEGENDS_PANDA_ADD_ONE_OTHER_COLOR)) { + const selectedSimpleCards = cards.filter((c) => SIMPLE_COLORS.includes(c.color)); + const selectedSimpleCardColors = selectedSimpleCards.map((c) => c.color); + const numberOfDifferentColors = [...new Set(selectedSimpleCardColors)].length; + const selectedSimpleCardsGroupedBySameColor = Object.entries(selectedSimpleCards.reduce((acc, c) => { + return { + ...acc, + [c.color]: [...(acc[c.color] || []), c], + }; + }, {})).sort(sortEntriesByLength); + if ( + numberOfDifferentColors > 2 + || ( + numberOfDifferentColors === 2 + && selectedSimpleCardsGroupedBySameColor[0][1].length > 1 + && selectedSimpleCardsGroupedBySameColor[1][1].length > 1 + ) + ) { + if (cardIds.includes(CARD_ID_LEGENDS_PANDA_ADD_ONE_OTHER_COLOR)) { + nonPlayableCards = [...selectedSimpleCards, ...nonPlayableCards]; + } else if (!nonPlayableCardIds.includes(CARD_ID_LEGENDS_PANDA_ADD_ONE_OTHER_COLOR)) { + nonPlayableCards = this.addSpecialCardsToCards([CARD_ID_LEGENDS_PANDA_ADD_ONE_OTHER_COLOR], nonPlayableCards); + } + } else if (cardIds.includes(CARD_ID_LEGENDS_PANDA_ADD_ONE_OTHER_COLOR)) { + const selectedSimpleCardIds = selectedSimpleCards.map((c) => c.id); + const playerSimpleCardIds = playerCards.filter((c) => SIMPLE_COLORS.includes(c.color)).map((c) => c.id); + const nonPlayablePlayerSimpleCardIds = playerCards + .filter((c) => SIMPLE_COLORS.includes(c.color)) + .filter((c) => + !selectedSimpleCardIds.includes(c.id) + && numberOfDifferentColors > 1 + && ( + ( + selectedSimpleCardsGroupedBySameColor[0][1].length === 1 + && !selectedSimpleCardColors.includes(c.color) + ) || ( + selectedSimpleCardsGroupedBySameColor[0][1].length > 1 + && c.color !== parseInt(selectedSimpleCardsGroupedBySameColor[0][0], 10) + ) + ) + ) + .map((c) => c.id); + nonPlayableCards = playerCards + .filter((c) => nonPlayablePlayerSimpleCardIds.includes(c.id) || (nonPlayableCardIds.includes(c.id) && !playerSimpleCardIds.includes(c.id))); + } + } else if (playerCardIds.includes(CARD_ID_LEGENDS_BADGER_ANY_NUMBER_OF_EACH_COLOR)) { + const selectedSimpleCards = cards.filter((c) => SIMPLE_COLORS.includes(c.color)); + const selectedSimpleCardColors = selectedSimpleCards.map((c) => c.color); + const numberOfDifferentColors = [...new Set(selectedSimpleCardColors)].length; + if (numberOfDifferentColors !== selectedSimpleCardColors.length) { + if (cardIds.includes(CARD_ID_LEGENDS_BADGER_ANY_NUMBER_OF_EACH_COLOR)) { + nonPlayableCards = [...selectedSimpleCards, ...nonPlayableCards]; + } else if (!nonPlayableCardIds.includes(CARD_ID_LEGENDS_BADGER_ANY_NUMBER_OF_EACH_COLOR)) { + nonPlayableCards = this.addSpecialCardsToCards([CARD_ID_LEGENDS_BADGER_ANY_NUMBER_OF_EACH_COLOR], nonPlayableCards); + } + } else if (cardIds.includes(CARD_ID_LEGENDS_BADGER_ANY_NUMBER_OF_EACH_COLOR)) { + const selectedSimpleCardIds = selectedSimpleCards.map((c) => c.id); + const playerSimpleCardIds = playerCards.filter((c) => SIMPLE_COLORS.includes(c.color)).map((c) => c.id); + const nonPlayablePlayerSimpleCardIds = playerCards + .filter((c) => SIMPLE_COLORS.includes(c.color)) + .filter((c) => !selectedSimpleCardIds.includes(c.id) && selectedSimpleCardColors.includes(c.color)) + .map((c) => c.id); + nonPlayableCards = playerCards + .filter((c) => nonPlayablePlayerSimpleCardIds.includes(c.id) || (nonPlayableCardIds.includes(c.id) && !playerSimpleCardIds.includes(c.id))); + } + } + + return nonPlayableCards; }, /** * @returns {Object[]} @@ -1363,28 +1986,166 @@ function (dojo, declare) { return this.playerHand.getSelectedItems() .map((item) => this.getCardObjectFromPositionInSpriteAndId(item.type, item.id)); }, + /** + * @returns {number[]} + */ + getSpecialPlayerCardIds: function () { + return [ + [CARD_ID_JERSEY_PLUS_TEN, () => this.currentPlayerHasJersey && !this.jerseyIsNotPlayable], + [CARD_ID_LEGENDS_BROOM_WAGON_PLUS_FIVE, () => this.currentPlayerHasLegendsBroomWagon && !this.legendsBroomWagonIsNotPlayable], + [CARD_ID_LEGENDS_EAGLE_ADD_ONE_OTHER_NUMBER, () => this.currentPlayerHasLegendsEagle && !this.legendsEagleIsNotPlayable && !this.currentPlayerHasJersey], + [CARD_ID_LEGENDS_PANDA_ADD_ONE_OTHER_COLOR, () => this.currentPlayerHasLegendsPanda && !this.legendsPandaIsNotPlayable && !this.currentPlayerHasJersey], + [CARD_ID_LEGENDS_SHARK_ONE_RED_MULTIPLY_TEN, () => this.currentPlayerHasLegendsShark && !this.legendsSharkIsNotPlayable && !this.currentPlayerHasJersey], + [CARD_ID_LEGENDS_BADGER_ANY_NUMBER_OF_EACH_COLOR, () => this.currentPlayerHasLegendsBadger && !this.legendsBadgerIsNotPlayable && !this.currentPlayerHasJersey], + [CARD_ID_LEGENDS_ELEPHANT_STOP, () => this.currentPlayerHasLegendsElephant && !this.legendsElephantIsNotPlayable && !this.currentPlayerHasJersey], + ].reduce((acc, cardIdAndPredicate) => { + if (cardIdAndPredicate[1]()) { + return acc.concat(cardIdAndPredicate[0]); + } + return acc; + }, []); + }, /** * @returns {Object[]} */ getPlayerCardsCombinationsSortedByHighestValue: function () { - const playerCards = this.getAllPlayerCards(); const sortCardsById = (a, b) => a.id - b.id; - const highestPlayableGroupOfCards = playerCards.reduce((acc, card) => { - const cardsThatCanBePlayedWithCard = this.getPlayerCardsThatCanBePlayedWithCards([card]); + const sortCardsByHighestValue = (a, b) => b.value - a.value; + const sortEntriesByLengthThenHighestValues = ([_k1, cards1], [_k2, cards2]) => { + if (!cards1.length) { + return 1; + } + if (!cards2.length) { + return -1; + } + if (cards1.length !== cards2.length) { + return cards2.length - cards1.length; + } - if (!cardsThatCanBePlayedWithCard.length) { - // add single card combination - return [...acc, [card]]; + const sortedCards1 = [...cards1].sort(sortCardsByHighestValue); + const sortedCards2 = [...cards2].sort(sortCardsByHighestValue); + for (let i = 0; i < sortedCards1.length; i++) { + if (sortedCards1[i].value !== sortedCards2[i].value) { + return sortedCards2[i].value - sortedCards1[i].value; + } } + return 0; + }; + + const playerCards = this.getAllPlayerCards(); + const simplePlayerCardsSortedByHighestValue = playerCards.filter((c) => SIMPLE_COLORS.includes(c.color)).sort(sortCardsByHighestValue); + const highestPlayableGroupOfCards = playerCards.reduce((acc, card) => { + if (card.id === CARD_ID_LEGENDS_SHARK_ONE_RED_MULTIPLY_TEN) { + const redCardsSortedByHighestValue = simplePlayerCardsSortedByHighestValue.filter((c) => c.color === COLOR_RED); + if (!redCardsSortedByHighestValue.length) { + return acc; + } + + return [ + ...acc, + [card, ...playerCards.filter((c) => [ + redCardsSortedByHighestValue[0].id, + CARD_ID_LEGENDS_BROOM_WAGON_PLUS_FIVE, + ].includes(c.id))].sort(sortCardsById), + ]; + } else if (card.id === CARD_ID_LEGENDS_EAGLE_ADD_ONE_OTHER_NUMBER) { + const simpleCardColors = simplePlayerCardsSortedByHighestValue.map((c) => c.color); + const differentColors = [...new Set(simpleCardColors)]; + const simpleCardValues = simplePlayerCardsSortedByHighestValue.map((c) => c.value); + const differentValues = [...new Set(simpleCardValues)]; + if (differentColors.length < 2 || differentValues.length < 2) { + return acc; + } + + const simpleCardIdsWithHighestNumberOfCardsOfSameValue = Object.entries( + simplePlayerCardsSortedByHighestValue.reduce((acc, c) => { + return { + ...acc, + [c.value]: [...(acc[c.value] || []), c], + }; + }, {}) + ).sort(sortEntriesByLengthThenHighestValues)[0][1].map((c) => c.id); + const otherSimpleCardIdWithHighestValue = simplePlayerCardsSortedByHighestValue.filter((c) => !simpleCardIdsWithHighestNumberOfCardsOfSameValue.includes(c.id))[0].id; + + return [ + ...acc, + [card, ...playerCards.filter((c) => [ + ...simpleCardIdsWithHighestNumberOfCardsOfSameValue, + otherSimpleCardIdWithHighestValue, + CARD_ID_LEGENDS_BROOM_WAGON_PLUS_FIVE, + ].includes(c.id))].sort(sortCardsById), + ]; + } else if (card.id === CARD_ID_LEGENDS_PANDA_ADD_ONE_OTHER_COLOR) { + const simpleCardColors = simplePlayerCardsSortedByHighestValue.map((c) => c.color); + const differentColors = [...new Set(simpleCardColors)]; + const simpleCardValues = simplePlayerCardsSortedByHighestValue.map((c) => c.value); + const differentValues = [...new Set(simpleCardValues)]; + if (differentColors.length < 2 || differentValues.length < 2) { + return acc; + } + + const simpleCardIdsWithHighestNumberOfCardsOfSameColor = Object.entries( + simplePlayerCardsSortedByHighestValue.reduce((acc, c) => { + return { + ...acc, + [c.color]: [...(acc[c.color] || []), c], + }; + }, {}) + ).sort(sortEntriesByLengthThenHighestValues)[0][1].map((c) => c.id); + const otherSimpleCardIdWithHighestValue = simplePlayerCardsSortedByHighestValue.filter((c) => !simpleCardIdsWithHighestNumberOfCardsOfSameColor.includes(c.id))[0].id; + + return [ + ...acc, + [card, ...playerCards.filter((c) => [ + ...simpleCardIdsWithHighestNumberOfCardsOfSameColor, + otherSimpleCardIdWithHighestValue, + CARD_ID_LEGENDS_BROOM_WAGON_PLUS_FIVE, + ].includes(c.id))].sort(sortCardsById), + ]; + } else if (card.id === CARD_ID_LEGENDS_BADGER_ANY_NUMBER_OF_EACH_COLOR) { + const simpleCardColors = simplePlayerCardsSortedByHighestValue.map((c) => c.color); + const differentColors = [...new Set(simpleCardColors)]; + const simpleCardValues = simplePlayerCardsSortedByHighestValue.map((c) => c.value); + const differentValues = [...new Set(simpleCardValues)]; + if (differentColors.length < 2 || differentValues.length < 2) { + return acc; + } - return [ - ...acc, - // add the highest color combination - [card, ...cardsThatCanBePlayedWithCard].filter((c) => c.color === card.color || c.id === CARD_ID_JERSEY_PLUS_TEN).sort(sortCardsById), - // add the highest value combination - [card, ...cardsThatCanBePlayedWithCard].filter((c) => c.value === card.value || c.id === CARD_ID_JERSEY_PLUS_TEN).sort(sortCardsById), - ]; + const simpleCardIdsOfHighestValueOfEachColor = Object.entries( + simplePlayerCardsSortedByHighestValue.reduce((acc, c) => { + if ((acc[c.color] || []).length) { + return acc; + } + + return { + ...acc, + [c.color]: [c], + }; + }, {}) + ).map(([_color, cards]) => cards[0].id); + + return [ + ...acc, + [card, ...playerCards.filter((c) => [ + ...simpleCardIdsOfHighestValueOfEachColor, + CARD_ID_LEGENDS_BROOM_WAGON_PLUS_FIVE, + ].includes(c.id))].sort(sortCardsById), + ]; + } else { + let cardsThatCanBePlayedWithCard = this.getCardsThatCanBePlayedWithCard(card.color, card.value, playerCards).filter((c) => card.id !== c.id); + if (!cardsThatCanBePlayedWithCard.length) { + // add single card combination + return [...acc, [card]]; + } + return [ + ...acc, + // add the highest color combination + [card, ...cardsThatCanBePlayedWithCard].filter((c) => c.color === card.color || c.id === CARD_ID_JERSEY_PLUS_TEN || c.id === CARD_ID_LEGENDS_BROOM_WAGON_PLUS_FIVE).sort(sortCardsById), + // add the highest value combination + [card, ...cardsThatCanBePlayedWithCard].filter((c) => c.value === card.value || c.id === CARD_ID_JERSEY_PLUS_TEN || c.id === CARD_ID_LEGENDS_BROOM_WAGON_PLUS_FIVE).sort(sortCardsById), + ]; + } }, []); return highestPlayableGroupOfCards @@ -1402,6 +2163,7 @@ function (dojo, declare) { if ( i <= 0 || combinations[i - 1].value !== combination.value + || combinations[i - 1].cards.length > combination.cards.length ) { return true; } @@ -1414,32 +2176,56 @@ function (dojo, declare) { * @returns {number} */ getCardsValue: function (cards) { - const withJersey = cards.map((c) => c.id).includes(CARD_ID_JERSEY_PLUS_TEN); - const cardsWithoutJersey = cards.filter((c) => c.id !== CARD_ID_JERSEY_PLUS_TEN); - if (!cardsWithoutJersey.length) { + // detect special cards + const cardIds = cards.map((c) => c.id); + const withJersey = cardIds.includes(CARD_ID_JERSEY_PLUS_TEN); + const withLegendsBroomWagon = cardIds.includes(CARD_ID_LEGENDS_BROOM_WAGON_PLUS_FIVE); + const withLegendsShark = cardIds.includes(CARD_ID_LEGENDS_SHARK_ONE_RED_MULTIPLY_TEN); + + // remove special cards + const cardsWithoutSpecialCards = cards.filter((c) => ![ + CARD_ID_JERSEY_PLUS_TEN, + CARD_ID_LEGENDS_BROOM_WAGON_PLUS_FIVE, + CARD_ID_LEGENDS_EAGLE_ADD_ONE_OTHER_NUMBER, + CARD_ID_LEGENDS_PANDA_ADD_ONE_OTHER_COLOR, + CARD_ID_LEGENDS_SHARK_ONE_RED_MULTIPLY_TEN, + CARD_ID_LEGENDS_BADGER_ANY_NUMBER_OF_EACH_COLOR, + CARD_ID_LEGENDS_ELEPHANT_STOP, + ].includes(c.id)); + if (!cardsWithoutSpecialCards.length) { return 0; } // an adventurer is only playable solo (and without jersey) - const adventurer = cardsWithoutJersey.find((c) => c.color === COLOR_ADVENTURER); + const adventurer = cardsWithoutSpecialCards.find((c) => c.color === COLOR_ADVENTURER); if (adventurer) { return adventurer.value; } - const addJerseyValueIfUsed = (value) => value + (withJersey ? VALUE_JERSEY : 0); + // special cards rules + const addJerseyValueIfUsed = (value) => value + (withJersey ? VALUE_JERSEY_PLUS_TEN : 0); + const addLegendsBroomWagonValueIfUsed = (value) => value + (withLegendsBroomWagon ? VALUE_LEGENDS_BROOM_WAGON_PLUS_FIVE : 0); + const addLegendsSharkValueIfUsed = (value) => (withLegendsShark ? value * 10 : value); - if (cardsWithoutJersey.length === 1) { - return addJerseyValueIfUsed(cardsWithoutJersey[0].value); + // if single non-special card + if (cardsWithoutSpecialCards.length === 1) { + return addLegendsBroomWagonValueIfUsed( + addJerseyValueIfUsed( + addLegendsSharkValueIfUsed(cardsWithoutSpecialCards[0].value) + ) + ); } + // if multiple non-special cards let minCardValue = 1000; - cardsWithoutJersey.forEach((card) => { + cardsWithoutSpecialCards.forEach((card) => { if (card.value < minCardValue) { minCardValue = card.value; } }); - - return addJerseyValueIfUsed((cardsWithoutJersey.length * 10) + minCardValue); + return addLegendsBroomWagonValueIfUsed( + addJerseyValueIfUsed((cardsWithoutSpecialCards.length * 10) + minCardValue) + ); }, /** * @returns {boolean} @@ -1453,17 +2239,30 @@ function (dojo, declare) { return this.playedCardsValue < playerCardsCombinations[0].value; }, setupPlayerHandSelectableCards: function () { - const selectedCards = this.getSelectedPlayerCards(); - if ( this.isCurrentPlayerActive() && this.currentState === 'playerGiveCardsBackAfterPicking' ) { - if (this.playerHand.isSelected(CARD_ID_JERSEY_PLUS_TEN)) { - this.playerHand.unselectItem(CARD_ID_JERSEY_PLUS_TEN); + let specialCardIdsThatCannotBeGiven = [CARD_ID_JERSEY_PLUS_TEN]; + if (this.isExtensionLegendsEnabled) { + specialCardIdsThatCannotBeGiven = [ + ...specialCardIdsThatCannotBeGiven, + CARD_ID_LEGENDS_BROOM_WAGON_PLUS_FIVE, + CARD_ID_LEGENDS_EAGLE_ADD_ONE_OTHER_NUMBER, + CARD_ID_LEGENDS_PANDA_ADD_ONE_OTHER_COLOR, + CARD_ID_LEGENDS_SHARK_ONE_RED_MULTIPLY_TEN, + CARD_ID_LEGENDS_BADGER_ANY_NUMBER_OF_EACH_COLOR, + CARD_ID_LEGENDS_ELEPHANT_STOP, + ]; } - this.displayCardsAsNonSelectable(this.addJerseyToCards([])); + specialCardIdsThatCannotBeGiven.forEach((cardId) => { + if (this.playerHand.isSelected(cardId)) { + this.playerHand.unselectItem(cardId); + } + }); + this.displayCardsAsNonSelectable(this.addSpecialCardsToCards(specialCardIdsThatCannotBeGiven, [])); } else { + const selectedCards = this.getSelectedPlayerCards(); this.displayCardsAsNonSelectable(this.getPlayerCardsThatCannotBePlayedWithCards(selectedCards)); } }, @@ -1471,11 +2270,11 @@ function (dojo, declare) { * @param {Object[]} cards */ displayCardsAsNonSelectable: function (cards) { - this.getAllPlayerCards().forEach((playedCard) => { + this.getAllPlayerCards().forEach((playerCard) => { dojo.toggleClass( - `${DOM_ID_PLAYER_HAND}_item_${playedCard.id}`, + `${DOM_ID_PLAYER_HAND}_item_${playerCard.id}`, DOM_CLASS_NON_SELECTABLE_CARD, - cards.map((card) => card.id).includes(playedCard.id) + cards.map((c) => c.id).includes(playerCard.id) ); }); }, @@ -1525,7 +2324,46 @@ function (dojo, declare) { * @returns {Object[]} */ sortPlayedCards: function (cards) { - return [...cards].sort((a, b) => b.value - a.value); + return [...cards].sort((a, b) => { + // coach at the end + if ([ + CARD_ID_LEGENDS_EAGLE_ADD_ONE_OTHER_NUMBER, + CARD_ID_LEGENDS_PANDA_ADD_ONE_OTHER_COLOR, + CARD_ID_LEGENDS_SHARK_ONE_RED_MULTIPLY_TEN, + CARD_ID_LEGENDS_BADGER_ANY_NUMBER_OF_EACH_COLOR, + CARD_ID_LEGENDS_ELEPHANT_STOP, + ].includes(b.id)) { + return -1; + } + if ([ + CARD_ID_LEGENDS_EAGLE_ADD_ONE_OTHER_NUMBER, + CARD_ID_LEGENDS_PANDA_ADD_ONE_OTHER_COLOR, + CARD_ID_LEGENDS_SHARK_ONE_RED_MULTIPLY_TEN, + CARD_ID_LEGENDS_BADGER_ANY_NUMBER_OF_EACH_COLOR, + CARD_ID_LEGENDS_ELEPHANT_STOP, + ].includes(a.id)) { + return 1; + } + + // then jersey + if (b.id === CARD_ID_JERSEY_PLUS_TEN) { + return -1; + } + if (a.id === CARD_ID_JERSEY_PLUS_TEN) { + return 1; + } + + // then broom wagon + if (b.id === CARD_ID_LEGENDS_BROOM_WAGON_PLUS_FIVE) { + return -1; + } + if (a.id === CARD_ID_LEGENDS_BROOM_WAGON_PLUS_FIVE) { + return 1; + } + + // then smallest values + return b.value - a.value; + }); }, /** * @param {Object[]} players @@ -1575,7 +2413,7 @@ function (dojo, declare) { const x = this.getAbsoluteCardBackgroundPositionXFromCardPosition(position); const y = this.getAbsoluteCardBackgroundPositionYFromCardPosition(position); dojo.place( - `
`, + `
`, `cards-stack-${topOfStackCardId}` ); }); @@ -1633,7 +2471,7 @@ function (dojo, declare) { let animations = []; for (let i = 0; i < numberOfCards; i++) { animations[i] = this.slideTemporaryObject( - `
`, + `
`, fromDomId, fromDomId, toDomId, @@ -1827,7 +2665,15 @@ function (dojo, declare) { if (playerId !== this.player_id) { this.placeOnObject(`cards-stack-${topOfStackCardId}`, `player-table-${playerId}-hand`); cards.forEach((card) => { - if (card.id !== CARD_ID_JERSEY_PLUS_TEN) { + if (![ + CARD_ID_JERSEY_PLUS_TEN, + CARD_ID_LEGENDS_BROOM_WAGON_PLUS_FIVE, + CARD_ID_LEGENDS_EAGLE_ADD_ONE_OTHER_NUMBER, + CARD_ID_LEGENDS_PANDA_ADD_ONE_OTHER_COLOR, + CARD_ID_LEGENDS_SHARK_ONE_RED_MULTIPLY_TEN, + CARD_ID_LEGENDS_BADGER_ANY_NUMBER_OF_EACH_COLOR, + CARD_ID_LEGENDS_ELEPHANT_STOP, + ].includes(card.id)) { this.decreaseNumberOfCardsOfPlayer(playerId, 1); } }); @@ -1872,29 +2718,21 @@ function (dojo, declare) { return; } - // sort cards to always send them from right to left (to avoid a visual bug in player hand) - const currentSortingMode = this.getCurrentPlayerCardsSortingMode(); - const sortedCards = [...cards].sort((a, b) => { - switch (currentSortingMode) { - case PLAYER_HAND_SORT_BY_COLOR: - return this.getCardPositionInSpriteByColorAndValue(b.color, b.value) - this.getCardPositionInSpriteByColorAndValue(a.color, a.value); - case PLAYER_HAND_SORT_BY_VALUE: - return this.getCardWeightForColorAndValueToSortThemByValue(b.color, b.value) - this.getCardWeightForColorAndValueToSortThemByValue(a.color, a.value); - default: - return 0; - } - }); - - this.removeCardFromPlayerHand(sortedCards[0].id); + // sort cards like the ones in player hand but reversed, + // to always send them from right to left, + // in order to avoid a visual bug in player hand when cards leave the hand one by one + const playerCardIds = this.getAllPlayerCards().map((c) => c.id); + const sortedCards = [...cards].sort((a, b) => playerCardIds.indexOf(b.id) - playerCardIds.indexOf(a.id)); let animations = []; + this.removeCardFromPlayerHand(sortedCards[0].id); for (let i = 0; i < sortedCards.length; i++) { const position = this.getCardPositionInSpriteByColorAndValue(sortedCards[i].color, sortedCards[i].value); const backgroundPositionX = this.getAbsoluteCardBackgroundPositionXFromCardPosition(position); const backgroundPositionY = this.getAbsoluteCardBackgroundPositionYFromCardPosition(position); const animationStartDomId = $(`${DOM_ID_PLAYER_HAND}_item_${sortedCards[i].id}`) ? `${DOM_ID_PLAYER_HAND}_item_${sortedCards[i].id}` : `player-table-${this.player_id}-hand`; animations[i] = this.slideTemporaryObject( - `
`, + `
`, `player-table-${this.player_id}-hand`, animationStartDomId, `player-table-${receiverId}-hand`, @@ -1976,18 +2814,22 @@ function (dojo, declare) { }, /** * @param {Object[]} cards - * @param {boolean=true} updateNumberOfCards + * @param {boolean} updateNumberOfCards * @param {string=} fromDomId */ addCardsToPlayerHand: function (cards, updateNumberOfCards, fromDomId) { - if (typeof updateNumberOfCards === 'undefined') { - updateNumberOfCards = true; - } - cards.forEach((card) => { if ( updateNumberOfCards - && card.id !== CARD_ID_JERSEY_PLUS_TEN + && ![ + CARD_ID_JERSEY_PLUS_TEN, + CARD_ID_LEGENDS_BROOM_WAGON_PLUS_FIVE, + CARD_ID_LEGENDS_EAGLE_ADD_ONE_OTHER_NUMBER, + CARD_ID_LEGENDS_PANDA_ADD_ONE_OTHER_COLOR, + CARD_ID_LEGENDS_SHARK_ONE_RED_MULTIPLY_TEN, + CARD_ID_LEGENDS_BADGER_ANY_NUMBER_OF_EACH_COLOR, + CARD_ID_LEGENDS_ELEPHANT_STOP, + ].includes(card.id) ) { if (fromDomId) { setTimeout( @@ -2022,7 +2864,15 @@ function (dojo, declare) { } }); this.playerHand.removeFromStockById(cardId); - if (cardId !== CARD_ID_JERSEY_PLUS_TEN) { + if (![ + CARD_ID_JERSEY_PLUS_TEN, + CARD_ID_LEGENDS_BROOM_WAGON_PLUS_FIVE, + CARD_ID_LEGENDS_EAGLE_ADD_ONE_OTHER_NUMBER, + CARD_ID_LEGENDS_PANDA_ADD_ONE_OTHER_COLOR, + CARD_ID_LEGENDS_SHARK_ONE_RED_MULTIPLY_TEN, + CARD_ID_LEGENDS_BADGER_ANY_NUMBER_OF_EACH_COLOR, + CARD_ID_LEGENDS_ELEPHANT_STOP, + ].includes(cardId)) { this.decreaseNumberOfCardsOfPlayer(this.player_id, 1); } this.setupPlayerHandSelectableCards(); @@ -2030,15 +2880,45 @@ function (dojo, declare) { this.setupSelectedCardsValueInPlayerHand(); }, /** + * @param {number[]} specialCardIds * @param {Object[]} cards */ - addJerseyToCards: function (cards) { - return cards.concat( - this.getCardObjectFromPositionInSpriteAndId( - this.getCardPositionInSpriteByColorAndValue(COLOR_JERSEY, VALUE_JERSEY), - CARD_ID_JERSEY_PLUS_TEN - ) - ); + addSpecialCardsToCards: function (specialCardIds, cards) { + const specialCards = specialCardIds.map((cardId) => { + let value; + switch (cardId) { + case CARD_ID_JERSEY_PLUS_TEN: + value = VALUE_JERSEY_PLUS_TEN; + break; + case CARD_ID_LEGENDS_BROOM_WAGON_PLUS_FIVE: + value = VALUE_LEGENDS_BROOM_WAGON_PLUS_FIVE; + break; + case CARD_ID_LEGENDS_EAGLE_ADD_ONE_OTHER_NUMBER: + value = VALUE_LEGENDS_EAGLE_ADD_ONE_OTHER_NUMBER; + break; + case CARD_ID_LEGENDS_PANDA_ADD_ONE_OTHER_COLOR: + value = VALUE_LEGENDS_PANDA_ADD_ONE_OTHER_COLOR; + break; + case CARD_ID_LEGENDS_SHARK_ONE_RED_MULTIPLY_TEN: + value = VALUE_LEGENDS_SHARK_ONE_RED_MULTIPLY_TEN; + break; + case CARD_ID_LEGENDS_BADGER_ANY_NUMBER_OF_EACH_COLOR: + value = VALUE_LEGENDS_BADGER_ANY_NUMBER_OF_EACH_COLOR; + break; + case CARD_ID_LEGENDS_ELEPHANT_STOP: + value = VALUE_LEGENDS_ELEPHANT_STOP; + break; + default: + throw new Error('Unsupported cardId for addSpecialCardsToCards'); + } + + return this.getCardObjectFromPositionInSpriteAndId( + this.getCardPositionInSpriteByColorAndValue(COLOR_SPECIAL, value), + cardId + ); + }); + + return cards.concat(specialCards); }, movePlayedCardsToPreviousPlayedCards: function () { dojo.query(`.${DOM_CLASS_CARDS_STACK_PREVIOUS_PLAYED}`).forEach(this.fadeOutAndDestroy); @@ -2096,7 +2976,7 @@ function (dojo, declare) { this.addTooltip(DOM_ID_PLAYER_HAND_UNGROUP_CARDS_BUTTON, '', _('Click this button to stop grouping selected cards.')); this.connect($(DOM_ID_PLAYER_HAND_UNGROUP_CARDS_BUTTON), 'onclick', 'onClickOnUngroupCardsButton'); } - } else if (selectedCards.length > 1) { + } else if (selectedCards.length > 1 && this.getCardsValue(selectedCards) > 0) { if (!$(DOM_ID_PLAYER_HAND_GROUP_CARDS_BUTTON)) { dojo.place( `${_('Group cards')}`, @@ -2228,11 +3108,8 @@ function (dojo, declare) { if (selectedCardsWithoutLastSelectedCardGroups.length > 0) { this.unselectCards(selectedCardsWithoutLastSelectedCard); } else { - const playerCardsThatCannotBePlayedWithSelectedCards = this.getPlayerCardsThatCannotBePlayedWithCards(selectedCards); - this.unselectCards(selectedCards.filter( - (card) => playerCardsThatCannotBePlayedWithSelectedCards.map((c) => c.id).includes(card.id) - && cardId !== card.id - )); + const playerCardIdsThatCannotBePlayedWithSelectedCards = this.getPlayerCardsThatCannotBePlayedWithCards(selectedCards).map((c) => c.id); + this.unselectCards(selectedCards.filter((c) => playerCardIdsThatCannotBePlayedWithSelectedCards.includes(c.id) && cardId !== c.id)); } } }, @@ -2240,7 +3117,15 @@ function (dojo, declare) { * @param {number} cardId */ onPlayerCardUnselected: function (cardId) { - this.unselectCards(this.getAllPlayerCards().filter((card) => card.id === cardId)); + const unselectedCardGroup = this.getCardsGroupOfCard(cardId); + if (unselectedCardGroup) { + this.unselectCards(unselectedCardGroup.cards); + } else { + const selectedCards = this.getSelectedPlayerCards(); + const selectedCardsWithoutUnselectedCard = selectedCards.filter((card) => card.id !== cardId); + const playerCardIdsThatCannotBePlayedWithRemainingSelectedCards = this.getPlayerCardsThatCannotBePlayedWithCards(selectedCardsWithoutUnselectedCard).map((c) => c.id); + this.unselectCards(selectedCards.filter((c) => playerCardIdsThatCannotBePlayedWithRemainingSelectedCards.includes(c.id) || cardId === c.id)); + } }, onClickOnTogglePlayerHandSortButton: function () { const currentSortingMode = this.getCurrentPlayerCardsSortingMode(); @@ -2386,8 +3271,6 @@ function (dojo, declare) { this.requestAction('selectCardsToGiveBack', { cards: selectedCards.map(card => card.id).join(';'), }); - - this.unselectAllCards(); }, /////////////////////////////////////////////////// @@ -2399,13 +3282,14 @@ function (dojo, declare) { ['roundStarted', 1], ['roundEnded', 1], ['cardsDealt', 1], + ['legendsCoachCardDealt', 1], ['cardsPlayed', 1000], ['turnPassed', isReadOnly ? 1000 : 1], ['playerHasFinishedRound', 1], ['playedCardsDiscarded', 1], - ['cardsReceivedFromAnotherPlayer', isReadOnly ? 3000 : 1500], - ['cardsSentToAnotherPlayer', isReadOnly ? 3000 : 1500], - ['cardsMovedBetweenTwoOtherPlayers', isReadOnly ? 2000 : 1500, (notif) => (notif.args.receiverPlayerId === this.player_id || notif.args.senderPlayerId === this.player_id)], + ['cardsReceivedFromAnotherPlayer', 3000], + ['cardsSentToAnotherPlayer', 3000], + ['cardsMovedBetweenTwoOtherPlayers', 3000, (notif) => (notif.args.receiverPlayerId === this.player_id || notif.args.senderPlayerId === this.player_id)], // /!\ 2P mode only ['attackRewardCardsDiscarded', 1], // /!\ 2P mode only @@ -2415,7 +3299,7 @@ function (dojo, declare) { // /!\ 2P mode only ['cardsReceivedFromDeck', isReadOnly ? 2000 : 1000], // /!\ 2P mode only - ['cardsMovedFromDeckToAnotherPlayer', isReadOnly ? 2000 : 1500, (notif) => notif.args.receiverPlayerId === this.player_id], + ['cardsMovedFromDeckToAnotherPlayer', isReadOnly ? 2000 : 1000, (notif) => notif.args.receiverPlayerId === this.player_id], ].forEach((notif) => { const name = notif[0]; const lockDurationInMs = notif[1]; @@ -2432,13 +3316,17 @@ function (dojo, declare) { notif_cardsDealt: function (data) { this.playerHand.removeAll(); this.resetCardsGroups(); - this.addCardsToPlayerHand((this.currentPlayerHasJersey && !this.jerseyIsNotPlayable) - ? this.addJerseyToCards(data.args.cards) - : data.args.cards, + this.addCardsToPlayerHand( + this.addSpecialCardsToCards(this.getSpecialPlayerCardIds(), data.args.cards), false ); this.sortPlayerCardsByCurrentSortingMode(); }, + notif_legendsCoachCardDealt: function (data) { + // this notif function is never called, + // because it is only triggered once before the game starts, + // before setupNotifications() is called + }, notif_roundStarted: function (data) { this.currentRound = data.args.currentRound; this.setupRoundsInfo(); @@ -2460,14 +3348,48 @@ function (dojo, declare) { // place new played cards this.playedCardsValue = data.args.playedCardsValue; - const playedCardsWithJersey = data.args.withJersey - ? this.addJerseyToCards(data.args.playedCards) - : data.args.playedCards; - this.moveCardsFromPlayerHandToTable(data.args.playedCardsPlayerId, playedCardsWithJersey); + this.moveCardsFromPlayerHandToTable( + data.args.playedCardsPlayerId, + this.addSpecialCardsToCards( + [ + [CARD_ID_JERSEY_PLUS_TEN, () => data.args.withJersey], + [CARD_ID_LEGENDS_BROOM_WAGON_PLUS_FIVE, () => data.args.withLegendsBroomWagon], + [CARD_ID_LEGENDS_EAGLE_ADD_ONE_OTHER_NUMBER, () => data.args.withLegendsEagle], + [CARD_ID_LEGENDS_PANDA_ADD_ONE_OTHER_COLOR, () => data.args.withLegendsPanda], + [CARD_ID_LEGENDS_SHARK_ONE_RED_MULTIPLY_TEN, () => data.args.withLegendsShark], + [CARD_ID_LEGENDS_BADGER_ANY_NUMBER_OF_EACH_COLOR, () => data.args.withLegendsBadger], + [CARD_ID_LEGENDS_ELEPHANT_STOP, () => data.args.withLegendsElephant], + ].reduce((acc, cardIdAndPredicate) => { + if (cardIdAndPredicate[1]()) { + return acc.concat(cardIdAndPredicate[0]); + } + return acc; + }, []), + data.args.playedCards, + ) + ); - // update jersey state if it has been used + // update special cards state if used if (data.args.withJersey) { - this.useJerseyForCurrentRound(); + this.useSpecialCardForCurrentRound(CARD_ID_JERSEY_PLUS_TEN); + } + if (data.args.withLegendsBroomWagon) { + this.useSpecialCardForCurrentRound(CARD_ID_LEGENDS_BROOM_WAGON_PLUS_FIVE); + } + if (data.args.withLegendsEagle) { + this.useSpecialCardForCurrentRound(CARD_ID_LEGENDS_EAGLE_ADD_ONE_OTHER_NUMBER); + } + if (data.args.withLegendsPanda) { + this.useSpecialCardForCurrentRound(CARD_ID_LEGENDS_PANDA_ADD_ONE_OTHER_COLOR); + } + if (data.args.withLegendsShark) { + this.useSpecialCardForCurrentRound(CARD_ID_LEGENDS_SHARK_ONE_RED_MULTIPLY_TEN); + } + if (data.args.withLegendsBadger) { + this.useSpecialCardForCurrentRound(CARD_ID_LEGENDS_BADGER_ANY_NUMBER_OF_EACH_COLOR); + } + if (data.args.withLegendsElephant) { + this.useSpecialCardForCurrentRound(CARD_ID_LEGENDS_ELEPHANT_STOP); } }, notif_turnPassed: function (data) { @@ -2493,20 +3415,28 @@ function (dojo, declare) { this.players = data.args.players; this.moveCardsBetweenTwoOtherPlayers(data.args.senderPlayerId, data.args.receiverPlayerId, data.args.numberOfCards); }, + /** + * Note about special cards: the special cards are restored and moved to their next owner before the next round begins, + * so, when the game ends, the winner will visibly own the jersey, and the last loser will visibly own the legends broom wagon + */ notif_roundEnded: function (data) { - // restore and move jersey before the next round begin, - // in order to have a beautiful jersey for the winner of the game (at the very end) - if ( - this.currentPlayerHasJersey - && !this.jerseyIsNotPlayable - ) { + // remove special cards (that can move) from current player hand + if (this.currentPlayerHasJersey && !this.jerseyIsNotPlayable) { this.removeCardFromPlayerHand(CARD_ID_JERSEY_PLUS_TEN); } - const previousJerseyWearerId = this.getCurrentJerseyWearerIdIfExists(); + if (this.currentPlayerHasLegendsBroomWagon && !this.legendsBroomWagonIsNotPlayable) { + this.removeCardFromPlayerHand(CARD_ID_LEGENDS_BROOM_WAGON_PLUS_FIVE); + } + + const previousJerseyWearerId = this.getCurrentJerseyOwnerIdIfExists(); + const previousLegendsBroomWagonOwnerId = this.getCurrentLegendsBroomWagonOwnerIdIfExists(); this.players = data.args.players; this.resetDisplayedNumberOfCardsByPlayerId(); this.setupPlayersHiddenCards(); - this.restoreJerseyForCurrentRound(); + data.args.specialCardIdsToRestore.forEach((cardId) => { + this.restoreSpecialCardForCurrentRound(cardId); + }); + this.moveLegendsBroomWagonToLastLoser(previousLegendsBroomWagonOwnerId); this.moveJerseyToCurrentWinner(previousJerseyWearerId); this.setupPlayersScore();