From f2e4c61c87b26b9e0ded799f37d5093c2e7fb86a Mon Sep 17 00:00:00 2001 From: Oliboy50 Date: Sun, 15 May 2022 18:23:10 +0200 Subject: [PATCH] feat(jersey): support jersey --- img/jersey.png | Bin 0 -> 21827 bytes modules/constants.inc.php | 3 + velonimo.action.php | 4 +- velonimo.css | 42 ++++- velonimo.game.php | 48 +++++- velonimo.js | 312 +++++++++++++++++++++----------------- velonimo_velonimo.tpl | 1 + 7 files changed, 259 insertions(+), 151 deletions(-) create mode 100644 img/jersey.png diff --git a/img/jersey.png b/img/jersey.png new file mode 100644 index 0000000000000000000000000000000000000000..892ea5be5e94dfa063adbca31621f4832af69a28 GIT binary patch literal 21827 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 i=qdcykJhcO`q4L)ZiW^?!(K`N0000game->playCards(array_map(fn ($id) => (int) $id, $cardIds)); + $withJerseyArg = (bool) self::getArg('withJersey', AT_bool, true); + + $this->game->playCards(array_map(fn ($id) => (int) $id, $cardIds), $withJerseyArg); self::ajaxResponse(); } diff --git a/velonimo.css b/velonimo.css index a704200..91cdafa 100644 --- a/velonimo.css +++ b/velonimo.css @@ -100,23 +100,61 @@ Board border: 2px solid #ff0000; transform: scale(1.02); } +.player-table.is-wearing-jersey .player-table-jersey { + position: absolute; + bottom: 10px; + right: 0; + width: 60px; + height: 83px; + background-size: 60px 83px; + background-image: url('img/jersey.png'); + background-repeat: no-repeat; + background-position: center center; + pointer-events: none; +} +.player-table.is-wearing-jersey.has-used-jersey .jersey-overlay { + display: block; + position: relative; + top: 20px; + height: 50px; + width: 10px; + margin: 0 auto; + background: black; + transform: rotate(45deg); +} +.player-table.is-wearing-jersey.has-used-jersey .jersey-overlay:after { + content: ""; + position: absolute; + left: -20px; + top: 20px; + width: 50px; + height: 10px; + background: black; +} .player-table-name { font-weight: bold; pointer-events: none; } .player-table-hand { position: relative; - width: 90px; + width: 65px; height: 90px; + background-size: 65px 90px; margin: 5px auto 0; + border-radius: 5px; background-image: url('img/remaining_cards.png'); background-repeat: no-repeat; background-position: center center; pointer-events: none; + transition-property: all; + transition-duration: 1s; +} +.player-table.is-wearing-jersey .player-table-hand { + margin: 5px 0 0 -5px; } .player-table-hand .number-of-cards { position: absolute; - left: 20px; + left: 8px; top: 20px; font-size: 2em; text-align: center; diff --git a/velonimo.game.php b/velonimo.game.php index ab93b71..5cbac78 100644 --- a/velonimo.game.php +++ b/velonimo.game.php @@ -26,6 +26,7 @@ class Velonimo extends Table { private const GAME_STATE_CURRENT_ROUND = 'currentRound'; + private const GAME_STATE_JERSEY_HAS_BEEN_USED_IN_THE_CURRENT_ROUND = 'jerseyUsedInRound'; private const GAME_STATE_LAST_PLAYED_CARDS_VALUE = 'valueToBeat'; private const GAME_STATE_LAST_PLAYED_CARDS_PLAYER_ID = 'playerIdForValueToBeat'; private const GAME_STATE_SELECTED_NEXT_PLAYER_ID = 'selectedNextPlayerId'; @@ -54,6 +55,7 @@ function __construct() { self::GAME_STATE_LAST_PLAYED_CARDS_VALUE => 11, self::GAME_STATE_LAST_PLAYED_CARDS_PLAYER_ID => 12, self::GAME_STATE_SELECTED_NEXT_PLAYER_ID => 13, + self::GAME_STATE_JERSEY_HAS_BEEN_USED_IN_THE_CURRENT_ROUND => 14, self::GAME_OPTION_HOW_MANY_ROUNDS => 100, ]); @@ -110,6 +112,7 @@ protected function setupNewGame($players, $options = []) { self::setGameStateValue(self::GAME_STATE_LAST_PLAYED_CARDS_VALUE, 0); self::setGameStateValue(self::GAME_STATE_LAST_PLAYED_CARDS_PLAYER_ID, 0); self::setGameStateValue(self::GAME_STATE_SELECTED_NEXT_PLAYER_ID, 0); + self::setGameStateValue(self::GAME_STATE_JERSEY_HAS_BEEN_USED_IN_THE_CURRENT_ROUND, 0); // Init game statistics // (note: statistics used in this file must be defined in your stats.inc.php file) @@ -181,6 +184,7 @@ protected function getAllDatas() { // Rounds $result['currentRound'] = (int) self::getGameStateValue(self::GAME_STATE_CURRENT_ROUND); + $result['jerseyHasBeenUsedInTheCurrentRound'] = $this->isJerseyUsedInCurrentRound(); $result['howManyRounds'] = (int) self::getGameStateValue(self::GAME_OPTION_HOW_MANY_ROUNDS); // Players @@ -231,7 +235,7 @@ function getGameProgression() { /** * @param int[] $playedCardIds */ - function playCards(array $playedCardIds) { + function playCards(array $playedCardIds, bool $cardsPlayedWithJersey) { self::checkAction('playCards'); // validate $playedCardIds @@ -290,9 +294,21 @@ function playCards(array $playedCardIds) { $lastCheckedCard = $card; } + // check that player is allowed to use jersey + $players = $this->getPlayersFromDatabase(); + $currentPlayer = $this->getPlayerById($currentPlayerId, $players); + if ($cardsPlayedWithJersey) { + if ($this->isJerseyUsedInCurrentRound()) { + throw new BgaUserException(self::_('The jersey can be used only once by turn.')); + } + if (!$currentPlayer->isWearingJersey()) { + throw new BgaUserException(self::_('You cannot play the jersey if you are not wearing it.')); + } + } + // check that played cards value is higher than the previous played cards value $lastPlayedCardsValue = (int) self::getGameStateValue(self::GAME_STATE_LAST_PLAYED_CARDS_VALUE); - $playedCardsValue = $this->getCardsValue($playedCards); + $playedCardsValue = $this->getCardsValue($playedCards, $cardsPlayedWithJersey); if ($playedCardsValue <= $lastPlayedCardsValue) { throw new BgaUserException(sprintf( self::_('The value of the cards you play must be higher than %s.'), @@ -303,6 +319,9 @@ function playCards(array $playedCardIds) { // discard table cards and play cards $this->deck->moveAllCardsInLocation(self::CARD_LOCATION_PLAYED, self::CARD_LOCATION_DISCARD); $this->deck->moveCards($playedCardIds, self::CARD_LOCATION_PLAYED, $currentPlayerId); + if ($cardsPlayedWithJersey) { + self::setGameStateValue(self::GAME_STATE_JERSEY_HAS_BEEN_USED_IN_THE_CURRENT_ROUND, 1); + } self::setGameStateValue(self::GAME_STATE_LAST_PLAYED_CARDS_PLAYER_ID, $currentPlayerId); self::setGameStateValue(self::GAME_STATE_LAST_PLAYED_CARDS_VALUE, $playedCardsValue); self::incStat(1, 'playCardsAction'); @@ -313,6 +332,7 @@ function playCards(array $playedCardIds) { 'remainingNumberOfCards' => count($currentPlayerCards) - count($playedCards), 'playerName' => self::getCurrentPlayerName(), 'playedCardsValue' => $playedCardsValue, + 'withJersey' => $cardsPlayedWithJersey, ]); // if the player did not play his last card, it's next player turn @@ -322,10 +342,8 @@ function playCards(array $playedCardIds) { } // the player played his last card, set its rank for this round - $players = $this->getPlayersFromDatabase(); $currentRound = (int) self::getGameStateValue(self::GAME_STATE_CURRENT_ROUND); $nextRankForRound = $this->getNextRankForRound($players, $currentRound); - $currentPlayer = $this->getPlayerById($currentPlayerId, $players); $currentPlayer->addRoundRanking($currentRound, $nextRankForRound); $this->updatePlayerRoundsRanking($currentPlayer); @@ -519,6 +537,9 @@ function stEndRound() { )); } + // re-allow the jersey to be used + self::setGameStateValue(self::GAME_STATE_JERSEY_HAS_BEEN_USED_IN_THE_CURRENT_ROUND, 0); + self::notifyAllPlayers('roundEnded', '', [ 'players' => $this->formatPlayersForClient($players), ]); @@ -639,13 +660,21 @@ private function formatCardsForClient(array $cards): array { /** * @param VelonimoCard[] $cards */ - private function getCardsValue(array $cards): int { + private function getCardsValue(array $cards, bool $withJersey): int { if (count($cards) <= 0) { return 0; } + // the jersey cannot be played with an adventurer + if ($withJersey && in_array(COLOR_ADVENTURER, array_map(fn (VelonimoCard $c) => $c->getColor(), $cards), true)) { + return 0; + } + + $addJerseyValueIfUsed = fn (int $value) => $value + ($withJersey ? JERSEY_VALUE : 0); + + if (count($cards) === 1) { - return $cards[0]->getValue(); + return $addJerseyValueIfUsed($cards[0]->getValue()); } $minCardValue = 1000; @@ -655,7 +684,7 @@ private function getCardsValue(array $cards): int { } } - return (count($cards) * 10) + $minCardValue; + return $addJerseyValueIfUsed((count($cards) * 10) + $minCardValue); } /** @@ -727,6 +756,7 @@ private function formatPlayersForClient(array $players): array { 'name' => $player->getName(), 'color' => $player->getColor(), 'score' => $player->getScore(), + 'isWearingJersey' => $player->isWearingJersey(), 'howManyCards' => count($this->deck->getCardsInLocation(self::CARD_LOCATION_PLAYER_HAND, $player->getId())), ]; } @@ -849,4 +879,8 @@ private function updatePlayerRoundsRanking(VelonimoPlayer $player): void { $player->getId() )); } + + private function isJerseyUsedInCurrentRound(): bool { + return 1 === (int) self::getGameStateValue(self::GAME_STATE_JERSEY_HAS_BEEN_USED_IN_THE_CURRENT_ROUND); + } } diff --git a/velonimo.js b/velonimo.js index 3b5ed54..0d84935 100644 --- a/velonimo.js +++ b/velonimo.js @@ -57,22 +57,112 @@ const VALUE_40 = 40; const VALUE_45 = 45; const VALUE_50 = 50; +// Jersey value +const JERSEY_VALUE = 10; + // DOM IDs const DOM_ID_BOARD_CARPET = 'board-carpet'; const DOM_ID_PLAYER_HAND = 'my-hand'; const DOM_ID_CURRENT_ROUND = 'current-round'; const DOM_ID_ACTION_BUTTON_PLAY_CARDS = 'action-button-play-cards'; +const DOM_ID_ACTION_BUTTON_PLAY_CARDS_WITH_JERSEY = 'action-button-play-cards-with-jersey'; const DOM_ID_ACTION_BUTTON_PASS_TURN = 'action-button-pass-turn'; const DOM_ID_ACTION_BUTTON_SELECT_NEXT_PLAYER = 'action-button-select-next-player'; // DOM classes const DOM_CLASS_PLAYER_TABLE = 'player-table' +const DOM_CLASS_PLAYER_IS_WEARING_JERSEY = 'is-wearing-jersey' +const DOM_CLASS_PLAYER_HAS_USED_JERSEY = 'has-used-jersey' const DOM_CLASS_CARDS_STACK = 'cards-stack' const DOM_CLASS_DISABLED_ACTION_BUTTON = 'disabled' const DOM_CLASS_ACTIVE_PLAYER = 'active' const DOM_CLASS_SELECTABLE_PLAYER = 'selectable' const DOM_CLASS_NON_SELECTABLE_CARD = 'non-selectable-player-card' +// Style +const BOARD_CARPET_WIDTH = 740; +const BOARD_CARPET_HEIGHT = 450; +const CARD_WIDTH = 90; +const CARD_HEIGHT = 126; +const PLAYER_TABLE_WIDTH = 130; +const PLAYER_TABLE_HEIGHT = 130; +const PLAYER_TABLE_BORDER_SIZE = 2; +const MARGIN_BETWEEN_PLAYERS = 20; +const TABLE_STYLE_HORIZONTAL_LEFT = `left: ${MARGIN_BETWEEN_PLAYERS}px;`; +const TABLE_STYLE_HORIZONTAL_CENTER = `left: ${(BOARD_CARPET_WIDTH / 2) - (PLAYER_TABLE_WIDTH / 2) - (MARGIN_BETWEEN_PLAYERS / 2)}px;`; +const TABLE_STYLE_HORIZONTAL_RIGHT = `right: ${MARGIN_BETWEEN_PLAYERS}px;`; +const TABLE_STYLE_VERTICAL_TOP = `top: ${MARGIN_BETWEEN_PLAYERS}px;`; +const TABLE_STYLE_VERTICAL_BOTTOM = `bottom: ${MARGIN_BETWEEN_PLAYERS}px;`; +const CARDS_STYLE_ABOVE_TABLE = `top: -${CARD_HEIGHT + PLAYER_TABLE_BORDER_SIZE}px; left: -${PLAYER_TABLE_BORDER_SIZE}px;`; +const CARDS_STYLE_BELOW_TABLE = `bottom: -${CARD_HEIGHT + PLAYER_TABLE_BORDER_SIZE}px; left: -${PLAYER_TABLE_BORDER_SIZE}px;`; +// the current player (index 0 == current player) place is always at the bottom of the board, in a way that players always stay closed to their hand +const PLAYERS_PLACES_BY_NUMBER_OF_PLAYERS = { + 2: { + 0: { + tableStyle: `${TABLE_STYLE_VERTICAL_BOTTOM} ${TABLE_STYLE_HORIZONTAL_CENTER}`, + cardsStyle: CARDS_STYLE_ABOVE_TABLE, + }, + 1: { + tableStyle: `${TABLE_STYLE_VERTICAL_TOP} ${TABLE_STYLE_HORIZONTAL_CENTER}`, + cardsStyle: CARDS_STYLE_BELOW_TABLE, + }, + }, + 3: { + 0: { + tableStyle: `${TABLE_STYLE_VERTICAL_BOTTOM} ${TABLE_STYLE_HORIZONTAL_CENTER}`, + cardsStyle: CARDS_STYLE_ABOVE_TABLE, + }, + 1: { + tableStyle: `${TABLE_STYLE_VERTICAL_TOP} ${TABLE_STYLE_HORIZONTAL_LEFT}`, + cardsStyle: CARDS_STYLE_BELOW_TABLE, + }, + 2: { + tableStyle: `${TABLE_STYLE_VERTICAL_TOP} ${TABLE_STYLE_HORIZONTAL_RIGHT}`, + cardsStyle: CARDS_STYLE_BELOW_TABLE, + }, + }, + 4: { + 0: { + tableStyle: `${TABLE_STYLE_VERTICAL_BOTTOM} ${TABLE_STYLE_HORIZONTAL_CENTER}`, + cardsStyle: CARDS_STYLE_ABOVE_TABLE, + }, + 1: { + tableStyle: `${TABLE_STYLE_VERTICAL_TOP} ${TABLE_STYLE_HORIZONTAL_LEFT}`, + cardsStyle: CARDS_STYLE_BELOW_TABLE, + }, + 2: { + tableStyle: `${TABLE_STYLE_VERTICAL_TOP} ${TABLE_STYLE_HORIZONTAL_CENTER}`, + cardsStyle: CARDS_STYLE_BELOW_TABLE, + }, + 3: { + tableStyle: `${TABLE_STYLE_VERTICAL_TOP} ${TABLE_STYLE_HORIZONTAL_RIGHT}`, + cardsStyle: CARDS_STYLE_BELOW_TABLE, + }, + }, + 5: { + 0: { + tableStyle: `${TABLE_STYLE_VERTICAL_BOTTOM} ${TABLE_STYLE_HORIZONTAL_LEFT}`, + cardsStyle: CARDS_STYLE_ABOVE_TABLE, + }, + 1: { + tableStyle: `${TABLE_STYLE_VERTICAL_TOP} ${TABLE_STYLE_HORIZONTAL_CENTER}`, + cardsStyle: CARDS_STYLE_BELOW_TABLE, + }, + 2: { + tableStyle: `${TABLE_STYLE_VERTICAL_TOP} ${TABLE_STYLE_HORIZONTAL_RIGHT}`, + cardsStyle: CARDS_STYLE_BELOW_TABLE, + }, + 3: { + tableStyle: `${TABLE_STYLE_VERTICAL_BOTTOM} ${TABLE_STYLE_HORIZONTAL_RIGHT}`, + cardsStyle: CARDS_STYLE_ABOVE_TABLE, + }, + 4: { + tableStyle: `${TABLE_STYLE_VERTICAL_BOTTOM} ${TABLE_STYLE_HORIZONTAL_CENTER}`, + cardsStyle: CARDS_STYLE_ABOVE_TABLE, + }, + }, +}; + define([ 'dojo','dojo/_base/declare', 'ebg/core/gamegui', @@ -81,125 +171,22 @@ define([ ], function (dojo, declare) { return declare('bgagame.velonimo', ebg.core.gamegui, { - /* - Init global variables - */ constructor: function () { - // GameInfo this.currentState = null; this.currentRound = 0; + this.currentPlayerHasJersey = false; + this.jerseyHasBeenUsedInTheCurrentRound = false; this.howManyRounds = 0; - this.howManyPlayers = 0; this.playedCardsValue = 0; this.players = []; - - // Board - this.boardCarpetWidth = 740; - this.boardCarpetHeight = 450; - - // Cards this.playerHand = null; // https://en.doc.boardgamearena.com/Stock - this.cardWidth = 90; - this.cardHeight = 126; - - // Player tables (a.k.a player places) - this.playerTableWidth = 130; - this.playerTableHeight = 130; - this.playerTableBorderSize = 0; - this.marginBetweenPlayers = 20; - const TABLE_STYLE_HORIZONTAL_LEFT = `left: ${this.marginBetweenPlayers}px;`; - const TABLE_STYLE_HORIZONTAL_CENTER = `left: ${(this.boardCarpetWidth / 2) - (this.playerTableWidth / 2) - (this.marginBetweenPlayers / 2)}px;`; - const TABLE_STYLE_HORIZONTAL_RIGHT = `right: ${this.marginBetweenPlayers}px;`; - const TABLE_STYLE_VERTICAL_TOP = `top: ${this.marginBetweenPlayers}px;`; - const TABLE_STYLE_VERTICAL_BOTTOM = `bottom: ${this.marginBetweenPlayers}px;`; - const CARDS_STYLE_ABOVE_TABLE = `top: -${this.cardHeight + this.playerTableBorderSize}px; left: -${this.playerTableBorderSize}px;`; - const CARDS_STYLE_BELOW_TABLE = `bottom: -${this.cardHeight + this.playerTableBorderSize}px; left: -${this.playerTableBorderSize}px;`; - // the current player place is always at the bottom of the board, - // in a way that players always stay closed to their hand - this.playersPlacesByNumberOfPlayers = { - 2: { - 0: { - tableStyle: `${TABLE_STYLE_VERTICAL_BOTTOM} ${TABLE_STYLE_HORIZONTAL_CENTER}`, - cardsStyle: CARDS_STYLE_ABOVE_TABLE, - }, - 1: { - tableStyle: `${TABLE_STYLE_VERTICAL_TOP} ${TABLE_STYLE_HORIZONTAL_CENTER}`, - cardsStyle: CARDS_STYLE_BELOW_TABLE, - }, - }, - 3: { - 0: { - tableStyle: `${TABLE_STYLE_VERTICAL_BOTTOM} ${TABLE_STYLE_HORIZONTAL_CENTER}`, - cardsStyle: CARDS_STYLE_ABOVE_TABLE, - }, - 1: { - tableStyle: `${TABLE_STYLE_VERTICAL_TOP} ${TABLE_STYLE_HORIZONTAL_CENTER}`, - cardsStyle: CARDS_STYLE_BELOW_TABLE, - }, - 2: { - tableStyle: `${TABLE_STYLE_VERTICAL_TOP} ${TABLE_STYLE_HORIZONTAL_RIGHT}`, - cardsStyle: CARDS_STYLE_BELOW_TABLE, - }, - }, - 4: { - 0: { - tableStyle: `${TABLE_STYLE_VERTICAL_BOTTOM} ${TABLE_STYLE_HORIZONTAL_CENTER}`, - cardsStyle: CARDS_STYLE_ABOVE_TABLE, - }, - 1: { - tableStyle: `${TABLE_STYLE_VERTICAL_TOP} ${TABLE_STYLE_HORIZONTAL_CENTER}`, - cardsStyle: CARDS_STYLE_BELOW_TABLE, - }, - 2: { - tableStyle: `${TABLE_STYLE_VERTICAL_TOP} ${TABLE_STYLE_HORIZONTAL_RIGHT}`, - cardsStyle: CARDS_STYLE_BELOW_TABLE, - }, - 3: { - tableStyle: `${TABLE_STYLE_VERTICAL_BOTTOM} ${TABLE_STYLE_HORIZONTAL_RIGHT}`, - cardsStyle: CARDS_STYLE_ABOVE_TABLE, - }, - }, - 5: { - 0: { - tableStyle: `${TABLE_STYLE_VERTICAL_BOTTOM} ${TABLE_STYLE_HORIZONTAL_CENTER}`, - cardsStyle: CARDS_STYLE_ABOVE_TABLE, - }, - 1: { - tableStyle: `${TABLE_STYLE_VERTICAL_BOTTOM} ${TABLE_STYLE_HORIZONTAL_LEFT}`, - cardsStyle: CARDS_STYLE_ABOVE_TABLE, - }, - 2: { - tableStyle: `${TABLE_STYLE_VERTICAL_TOP} ${TABLE_STYLE_HORIZONTAL_CENTER}`, - cardsStyle: CARDS_STYLE_BELOW_TABLE, - }, - 3: { - tableStyle: `${TABLE_STYLE_VERTICAL_TOP} ${TABLE_STYLE_HORIZONTAL_RIGHT}`, - cardsStyle: CARDS_STYLE_BELOW_TABLE, - }, - 4: { - tableStyle: `${TABLE_STYLE_VERTICAL_BOTTOM} ${TABLE_STYLE_HORIZONTAL_RIGHT}`, - cardsStyle: CARDS_STYLE_ABOVE_TABLE, - }, - }, - }; }, - /* - setup: - - This method must set up the game user interface according to current game situation specified - in parameters. - - The method is called each time the game interface is displayed to a player, ie: - _ when the game starts - _ when a player refreshes the game page (F5) - - "gamedatas" argument contains all datas retrieved by your "getAllDatas" PHP method. - */ setup: function (gamedatas) { // @TODO: remove log console.log(gamedatas); this.currentState = gamedatas.gamestate.name; this.currentRound = gamedatas.currentRound; + this.jerseyHasBeenUsedInTheCurrentRound = gamedatas.jerseyHasBeenUsedInTheCurrentRound; this.howManyRounds = gamedatas.howManyRounds; // Setup board @@ -213,8 +200,8 @@ function (dojo, declare) { // Setup players this.players = gamedatas.players; - this.howManyPlayers = Object.keys(this.players).length; - const playersPlace = this.playersPlacesByNumberOfPlayers[this.howManyPlayers]; + const howManyPlayers = Object.keys(this.players).length; + const playersPlace = PLAYERS_PLACES_BY_NUMBER_OF_PLAYERS[howManyPlayers]; this.sortPlayersToHaveTheCurrentPlayerFirstIfPresent( this.sortPlayersByTurnOrderPosition(Object.entries(this.players).map((entry) => entry[1])), gamedatas.currentPlayerId @@ -234,7 +221,7 @@ function (dojo, declare) { // @TODO: support spectators (do not show "my hand" in this case) // Init playerHand "ebg.stock" component this.playerHand = new ebg.stock(); - this.playerHand.create(this, $(DOM_ID_PLAYER_HAND), this.cardWidth, this.cardHeight); + this.playerHand.create(this, $(DOM_ID_PLAYER_HAND), CARD_WIDTH, CARD_HEIGHT); this.playerHand.setSelectionAppearance('class'); this.playerHand.image_items_per_row = 7; const cardsImageUrl = g_gamethemeurl+'img/cards.png'; @@ -403,7 +390,6 @@ function (dojo, declare) { /////////////////////////////////////////////////// //// Utility methods /////////////////////////////////////////////////// - /** * @param {string} action * @param {object} data @@ -489,22 +475,47 @@ function (dojo, declare) { } // number of remaining cards in player hand $(`player-table-${player.id}-number-of-cards`).innerHTML = player.howManyCards; + // setup jersey + if (player.isWearingJersey) { + this.currentPlayerHasJersey = this.player_id === player.id; + } + dojo.toggleClass(`player-table-${player.id}`, DOM_CLASS_PLAYER_HAS_USED_JERSEY, player.isWearingJersey && this.jerseyHasBeenUsedInTheCurrentRound); + dojo.toggleClass(`player-table-${player.id}`, DOM_CLASS_PLAYER_IS_WEARING_JERSEY, player.isWearingJersey); + // @TODO: transition for jersey + // this.placeOnObject(`cards-stack-${topOfStackCardId}`, `player-table-${playerId}-hand`); + // this.slideToObject(`cards-stack-${topOfStackCardId}`, `player-table-${playerId}-cards`).play(); }); // display current round $(DOM_ID_CURRENT_ROUND).innerHTML = `${this.currentRound} / ${this.howManyRounds}`; }, setupPlayCardsActionButton: function () { + const selectedCards = this.getSelectedPlayerCards(); + const selectedCardsValue = this.getCardsValue(selectedCards, false); + + // setup playCards without jersey if (!$(DOM_ID_ACTION_BUTTON_PLAY_CARDS)) { - this.addActionButton(DOM_ID_ACTION_BUTTON_PLAY_CARDS, _('Play selected cards'), 'onPlayCards'); - dojo.place(``, DOM_ID_ACTION_BUTTON_PLAY_CARDS); - this.addTooltip(`${DOM_ID_ACTION_BUTTON_PLAY_CARDS}-value`, _('Total value of selected cards'), ' (0)'); + this.addActionButton(DOM_ID_ACTION_BUTTON_PLAY_CARDS, _('Play selected cards'), () => this.onPlayCards(false)); + dojo.place(` (${selectedCardsValue})`, DOM_ID_ACTION_BUTTON_PLAY_CARDS); + this.addTooltip(`${DOM_ID_ACTION_BUTTON_PLAY_CARDS}-value`, _('Total value of selected cards'), ''); } - - const selectedCards = this.getSelectedPlayerCards(); - const selectedCardsValue = this.getCardsValue(selectedCards); dojo.toggleClass(DOM_ID_ACTION_BUTTON_PLAY_CARDS, DOM_CLASS_DISABLED_ACTION_BUTTON, selectedCardsValue <= this.playedCardsValue); $(`${DOM_ID_ACTION_BUTTON_PLAY_CARDS}-value`).innerText = ` (${selectedCardsValue})`; + + // setup playCards with jersey + if ( + this.currentPlayerHasJersey + && !this.jerseyHasBeenUsedInTheCurrentRound + ) { + const selectedCardsWithJerseyValue = this.getCardsValue(selectedCards, true); + if (!$(DOM_ID_ACTION_BUTTON_PLAY_CARDS_WITH_JERSEY)) { + this.addActionButton(DOM_ID_ACTION_BUTTON_PLAY_CARDS_WITH_JERSEY, _('Play jersey with selected cards'), () => this.onPlayCards(true)); + dojo.place(` (${selectedCardsWithJerseyValue})`, DOM_ID_ACTION_BUTTON_PLAY_CARDS_WITH_JERSEY); + this.addTooltip(`${DOM_ID_ACTION_BUTTON_PLAY_CARDS_WITH_JERSEY}-value`, _(`Total value of selected cards + jersey (${JERSEY_VALUE})`), ''); + } + dojo.toggleClass(DOM_ID_ACTION_BUTTON_PLAY_CARDS_WITH_JERSEY, DOM_CLASS_DISABLED_ACTION_BUTTON, selectedCardsWithJerseyValue <= this.playedCardsValue); + $(`${DOM_ID_ACTION_BUTTON_PLAY_CARDS_WITH_JERSEY}-value`).innerText = ` (${selectedCardsWithJerseyValue})`; + } }, /** * This function gives the position of the card in the sprite "cards.png", @@ -895,7 +906,7 @@ function (dojo, declare) { // format combinations .map((cards) => ({ cards: cards, - value: this.getCardsValue(cards), + value: this.getCardsValue(cards, false), })) // sort combinations by highest value .sort((a, b) => { @@ -915,15 +926,23 @@ function (dojo, declare) { }, /** * @param {object[]} cards + * @param {boolean} withJersey * @returns {number} */ - getCardsValue: function (cards) { + getCardsValue: function (cards, withJersey) { if (!cards.length) { return 0; } + // the jersey cannot be played with an adventurer + if (withJersey && cards.map((c) => c.color).includes(COLOR_ADVENTURER)) { + return 0; + } + + const addJerseyValueIfUsed = (value) => value + (withJersey ? JERSEY_VALUE : 0); + if (cards.length === 1) { - return cards[0].value; + return addJerseyValueIfUsed(cards[0].value); } let minCardValue = 1000; @@ -933,7 +952,7 @@ function (dojo, declare) { } }); - return (cards.length * 10) + minCardValue; + return addJerseyValueIfUsed((cards.length * 10) + minCardValue); }, /** * @returns {boolean} @@ -944,7 +963,9 @@ function (dojo, declare) { return false; } - return this.playedCardsValue < playerCardsCombinations[0].value; + const playerCanPlayJersey = this.currentPlayerHasJersey && !this.jerseyHasBeenUsedInTheCurrentRound; + + return this.playedCardsValue < (playerCardsCombinations[0].value + (playerCanPlayJersey ? JERSEY_VALUE : 0)); }, /** * @param {number} cardId @@ -997,6 +1018,11 @@ function (dojo, declare) { ); }); }, + unselectAllCards: function () { + this.playerHand.unselectAll(); + this.displayCardsAsNonSelectable([]); + this.setupPlayCardsActionButton(); + }, /** * @param {object[]} cards * @return {object[]} @@ -1042,7 +1068,7 @@ function (dojo, declare) { dojo.place( this.format_block('jstpl_cards_stack', { id: topOfStackCardId, - width: ((stackedCards.length - 1) * (this.cardWidth / 3)) + this.cardWidth, + width: ((stackedCards.length - 1) * (CARD_WIDTH / 3)) + CARD_WIDTH, }), `player-table-${playerId}-cards` ); @@ -1051,8 +1077,8 @@ function (dojo, declare) { dojo.place( this.format_block('jstpl_card_in_stack', { id: card.id, - x: (position % 7) * this.cardWidth, - y: Math.floor(position / 7) * this.cardHeight, + x: (position % 7) * CARD_WIDTH, + y: Math.floor(position / 7) * CARD_HEIGHT, }), `cards-stack-${topOfStackCardId}` ); @@ -1087,8 +1113,10 @@ function (dojo, declare) { /////////////////////////////////////////////////// //// Player's action /////////////////////////////////////////////////// - - onPlayCards: function () { + /** + * @param {boolean} withJersey + */ + onPlayCards: function (withJersey) { if (!this.checkAction('playCards')) { return; } @@ -1099,13 +1127,14 @@ function (dojo, declare) { } this.requestAction('playCards', { - cards: playedCards.map(card => card.id).join(';') + cards: playedCards.map(card => card.id).join(';'), + withJersey: withJersey, }); - // reset cards selection - this.playerHand.unselectAll(); - this.displayCardsAsNonSelectable([]); - this.setupPlayCardsActionButton(); + this.unselectAllCards(); + + // @TODO: turn back jersey to show that it cannot be used anymore + // (using notification to be sure that the request is accepted on the backend side) }, onPassTurn: function () { if (!this.checkAction('passTurn')) { @@ -1114,6 +1143,9 @@ function (dojo, declare) { this.requestAction('passTurn', {}); }, + /** + * @param {number} selectedPlayerId + */ onSelectNextPlayer: function (selectedPlayerId) { if (!this.checkAction('selectNextPlayer')) { return; @@ -1127,16 +1159,6 @@ function (dojo, declare) { /////////////////////////////////////////////////// //// Reaction to cometD notifications /////////////////////////////////////////////////// - - /* - setupNotifications: - - In this method, you associate each of your game notifications with your local method to handle it. - - Note: game notification names correspond to "notifyAllPlayers" and "notifyPlayer" calls in - your velonimo.game.php file. - - */ setupNotifications: function () { [ ['roundStarted', 1], @@ -1170,6 +1192,13 @@ function (dojo, declare) { // update number of cards in players hand this.players[data.args.playedCardsPlayerId].howManyCards = data.args.remainingNumberOfCards; + + // update jersey state if it has been used + if (data.args.withJersey) { + this.jerseyHasBeenUsedInTheCurrentRound = true; + } + + // refresh remaining game info this.refreshGameInfos(); }, notif_cardsDiscarded: function (data) { @@ -1177,6 +1206,7 @@ function (dojo, declare) { }, notif_roundEnded: function (data) { this.players = data.args.players; + this.jerseyHasBeenUsedInTheCurrentRound = false; this.refreshGameInfos(); }, }); diff --git a/velonimo_velonimo.tpl b/velonimo_velonimo.tpl index a9ab3b8..0752ebd 100644 --- a/velonimo_velonimo.tpl +++ b/velonimo_velonimo.tpl @@ -12,6 +12,7 @@

\${name}
\${numberOfCardsInHand}
+
`; var jstpl_cards_stack = '
';