diff --git a/app/assets/stylesheets/lobby.less b/app/assets/stylesheets/lobby.less index c7afd17..fb16477 100644 --- a/app/assets/stylesheets/lobby.less +++ b/app/assets/stylesheets/lobby.less @@ -32,6 +32,16 @@ align-items: center; position: relative; + .action-menu { + position: absolute; + top: 0px; + right: 5px; + + .disabled { + opacity: 0.5; + } + } + .round-area { display: flex; flex-direction: row; diff --git a/app/assets/stylesheets/start.less b/app/assets/stylesheets/start.less index 34cbc45..af21bdf 100644 --- a/app/assets/stylesheets/start.less +++ b/app/assets/stylesheets/start.less @@ -16,7 +16,7 @@ text-align: center; font-weight: bold; padding: 0px 0px 10px 0px; - margin: 0px 0px 20px 0px; + margin: 0px 0px 10px 0px; } button { @@ -24,6 +24,11 @@ margin: 15px auto 0px auto; } + .info-message { + text-align: center; + padding: 0px 0px 10px 0px; + } + .mui-textfield { width: 90%; margin: 10px auto 0px auto; diff --git a/app/controllers/massivedecks/game/Actions.scala b/app/controllers/massivedecks/game/Actions.scala index b85915e..f29f28e 100644 --- a/app/controllers/massivedecks/game/Actions.scala +++ b/app/controllers/massivedecks/game/Actions.scala @@ -30,6 +30,9 @@ object Actions { case class GetLobbyAndHand(secret: Secret) extends Command case class Skip(secret: Secret, players: List[Id]) extends Command case class Back(secret: Secret) extends Command + case class EnableRule(secret: Secret, rule: String) extends Command + case class DisableRule(secret: Secret, rule: String) extends Command + case class Redraw(secret: Secret) extends Command object Action { def apply(json: JsValue): Option[Command] = @@ -44,6 +47,9 @@ object Actions { case "getLobbyAndHand" => Json.fromJson[GetLobbyAndHand](command).asOpt case "skip" => Json.fromJson[Skip](command).asOpt case "back" => Json.fromJson[Back](command).asOpt + case "enableRule" => Json.fromJson[EnableRule](command).asOpt + case "disableRule" => Json.fromJson[DisableRule](command).asOpt + case "redraw" => Json.fromJson[Redraw](command).asOpt case _ => None } }) @@ -60,6 +66,9 @@ object Actions { implicit val getLobbyAndHandFormat: Format[GetLobbyAndHand] = Json.format[GetLobbyAndHand] implicit val skipFormat: Format[Skip] = Json.format[Skip] implicit val backFormat: Format[Back] = Json.format[Back] + implicit val redrawFormat: Format[Redraw] = Json.format[Redraw] + implicit val enableRuleFormat: Format[EnableRule] = Json.format[EnableRule] + implicit val disableRuleFormat: Format[DisableRule] = Json.format[DisableRule] } } diff --git a/app/controllers/massivedecks/game/Game.scala b/app/controllers/massivedecks/game/Game.scala index b327e35..a4a2913 100644 --- a/app/controllers/massivedecks/game/Game.scala +++ b/app/controllers/massivedecks/game/Game.scala @@ -118,6 +118,24 @@ class Game @Inject()(private val state: State, @Assisted private val id: String) Json.toJson(state.lobbyAndHand(secret)) } + case Redraw(secret) => + sender() ! Try { + state.redraw(secret) + Json.toJson(state.lobbyAndHand(secret)) + } + + case EnableRule(secret, rule) => + sender() ! Try { + state.enableRule(secret, rule) + Json.toJson(state.lobbyAndHand(secret)) + } + + case DisableRule(secret, rule) => + sender() ! Try { + state.disableRule(secret, rule) + Json.toJson(state.lobbyAndHand(secret)) + } + case _ => sender() ! Try { throw new Exception("Unknown message: " + message) diff --git a/app/controllers/massivedecks/game/State.scala b/app/controllers/massivedecks/game/State.scala index 883aaf3..2338e19 100644 --- a/app/controllers/massivedecks/game/State.scala +++ b/app/controllers/massivedecks/game/State.scala @@ -22,6 +22,7 @@ import play.api.Play.current class State @Inject()(private val cardCast: CardcastAPI, @Assisted val gameCode: String)(implicit ec: ExecutionContext) { private var decks: Set[CardcastDeck] = Set() + private var rules: Set[String] = Set() private var players: List[Player] = List() private var lastPlayerId: Int = -1 private var secrets: Map[Id, Secret] = Map() @@ -34,7 +35,7 @@ class State @Inject()(private val cardCast: CardcastAPI, @Assisted val gameCode: private var ais: Set[Secret] = Set() private var connected: Set[Id] = Set() - def config = Config(decks.map(deck => deck.info).toList) + def config = Config(decks.map(deck => deck.info).toList, rules) def lobby = Lobby(gameCode, config, players, game.map(game => game.round)) def newAi(): Unit = { @@ -251,6 +252,31 @@ class State @Inject()(private val cardCast: CardcastAPI, @Assisted val gameCode: sendNotifications() } + def redraw(secret: Secret): Unit = { + val id = validateSecretAndGetId(secret) + val state = validateInGameAndGetState() + verify(rules.contains("reboot"), "rule-not-enabled") + players = players.map(player => if (player.id == id) { + verify(player.score > 0, "not-enough-points-to-redraw") + player.copy(score = player.score - 1) + } else { + player + }) + var hands = state.hands + hands += (id -> Hand(state.deck.drawResponses(Hand.size))) + game = Some(state.copy(hands = hands)) + } + + def enableRule(secret: Secret, rule: String): Unit = { + val id = validateSecretAndGetId(secret) + rules += rule + } + + def disableRule(secret: Secret, rule: String): Unit = { + val id = validateSecretAndGetId(secret) + rules -= rule + } + def register(secret: Secret, socket: ActorRef): Unit = { val id = validateSecretAndGetId(secret) val player = playerForId(id) diff --git a/app/models/massivedecks/Game.scala b/app/models/massivedecks/Game.scala index 655e665..459c352 100644 --- a/app/models/massivedecks/Game.scala +++ b/app/models/massivedecks/Game.scala @@ -22,7 +22,7 @@ object Game { case class DeckInfo(id: String, name: String, calls: Int, responses: Int) - case class Config(decks: List[DeckInfo]) + case class Config(decks: List[DeckInfo], houseRules: Set[String]) case class Round(czar: Player.Id, call: Call, responses: Responses) { require(responses.revealed.isEmpty || diff --git a/client/src/MassiveDecks/API.elm b/client/src/MassiveDecks/API.elm index 2432068..1794207 100644 --- a/client/src/MassiveDecks/API.elm +++ b/client/src/MassiveDecks/API.elm @@ -4,6 +4,7 @@ import Json.Decode as Decode exposing ((:=)) import Json.Encode as Encode import MassiveDecks.API.Request exposing (..) +import MassiveDecks.Scenes.Playing.HouseRule.Id as HouseRule import MassiveDecks.Models.Game as Game import MassiveDecks.Models.Player as Player exposing (Player) import MassiveDecks.Models.JSON.Decode exposing (..) @@ -22,7 +23,7 @@ createLobby = request "POST" "/lobbies" Nothing [] lobbyDecoder -} type NewPlayerError = NameInUse - | LobbyNotFound + | NewPlayerLobbyNotFound {-| Makes a request to add a new player to the given lobby. On success, returns a `Secret` for that player. -} @@ -33,15 +34,22 @@ newPlayer gameCode name = ("/lobbies/" ++ gameCode ++ "/players") (Just (encodeName name)) [ ((400, "name-in-use"), Decode.succeed NameInUse) - , ((404, "lobby-not-found"), Decode.succeed LobbyNotFound) + , ((404, "lobby-not-found"), Decode.succeed NewPlayerLobbyNotFound) ] playerSecretDecoder +{-| Errors specific to getting the lobby and hand. +* `LobbyNotFound` - The given lobby does not exist. +-} +type GetLobbyAndHandError + = LobbyNotFound + + {-| Get the lobby and the hand for the player with the given secret (using it to authenticate). -} -getLobbyAndHand : String -> Player.Secret -> Request Never Game.LobbyAndHand -getLobbyAndHand = commandRequest "getLobbyAndHand" [] [] +getLobbyAndHand : String -> Player.Secret -> Request GetLobbyAndHandError Game.LobbyAndHand +getLobbyAndHand = commandRequest "getLobbyAndHand" [] [ ((404, "lobby-not-found"), Decode.succeed LobbyNotFound) ] {-| Errors specific to add deck requests. @@ -198,6 +206,18 @@ redraw = ] +{-| Make a request to enable a house rule. +-} +enableRule : HouseRule.Id -> String -> Player.Secret -> Request Never Game.LobbyAndHand +enableRule rule gameCode secret = commandRequest "enableRule" [ ("rule", rule |> HouseRule.toString |> Encode.string) ] [] gameCode secret + + +{-| Make a request to disable a house rule. +-} +disableRule : HouseRule.Id -> String -> Player.Secret -> Request Never Game.LobbyAndHand +disableRule rule gameCode secret = commandRequest "disableRule" [ ("rule", rule |> HouseRule.toString |> Encode.string) ] [] gameCode secret + + {- Private -} diff --git a/client/src/MassiveDecks/API/Request.elm b/client/src/MassiveDecks/API/Request.elm index ac26dcf..3528d27 100644 --- a/client/src/MassiveDecks/API/Request.elm +++ b/client/src/MassiveDecks/API/Request.elm @@ -8,6 +8,7 @@ import Dict exposing (Dict) import Http exposing (Value(..)) import MassiveDecks.Components.Errors as Errors +import MassiveDecks.Util as Util {-| Send a request to the server. @@ -32,13 +33,7 @@ send request knownHandler errorMessageWrapper resultToMessage = {-| Same as 'send', but for requests with no known errors. -} send' : Request Never result -> (Errors.Message -> message) -> (result -> message) -> Cmd message -send' request errorMessageWrapper resultToMessage = send request noKnownErrors errorMessageWrapper resultToMessage - - -{-| It's impossible to get an instance of Never, this will never happen, so just crash. --} -noKnownErrors : Never -> message -noKnownErrors noPossibleValue = Debug.crash "Got an impossible response." +send' request errorMessageWrapper resultToMessage = send request Util.impossible errorMessageWrapper resultToMessage {-| A request to the API. diff --git a/client/src/MassiveDecks/Models/Game.elm b/client/src/MassiveDecks/Models/Game.elm index ecdf7dc..780ad0d 100644 --- a/client/src/MassiveDecks/Models/Game.elm +++ b/client/src/MassiveDecks/Models/Game.elm @@ -2,6 +2,7 @@ module MassiveDecks.Models.Game exposing (..) import MassiveDecks.Models.Card as Card import MassiveDecks.Models.Player as Player exposing (Player) +import MassiveDecks.Scenes.Playing.HouseRule.Id as HouseRule {-| The required information to rejoin a lobby - the ID and the secret. @@ -22,6 +23,7 @@ type alias GameCode = String -} type alias Config = { decks : List DeckInfo + , houseRules : List HouseRule.Id } diff --git a/client/src/MassiveDecks/Models/JSON/Decode.elm b/client/src/MassiveDecks/Models/JSON/Decode.elm index faf4c03..03a4fc6 100644 --- a/client/src/MassiveDecks/Models/JSON/Decode.elm +++ b/client/src/MassiveDecks/Models/JSON/Decode.elm @@ -5,6 +5,7 @@ import Json.Decode exposing (..) import MassiveDecks.Models.Game exposing (..) import MassiveDecks.Models.Player exposing (..) import MassiveDecks.Models.Card exposing (..) +import MassiveDecks.Scenes.Playing.HouseRule.Id as HouseRule lobbyAndHandDecoder : Decoder LobbyAndHand @@ -30,8 +31,9 @@ deckInfoDecoder = object4 DeckInfo configDecoder : Decoder Config -configDecoder = object1 Config +configDecoder = object2 Config ("decks" := (list deckInfoDecoder)) + ("houseRules" := (list houseRuleDecoder)) handDecoder : Decoder Hand @@ -50,7 +52,7 @@ playerDecoder = object6 Player playerStatusDecoder : Decoder Status -playerStatusDecoder = customDecoder (string) (\name -> Result.Ok (Maybe.withDefault NotPlayed (nameToStatus name))) +playerStatusDecoder = customDecoder (string) (\name -> nameToStatus name |> Result.fromMaybe ("Unknown player status '" ++ name ++ "'.")) roundDecoder : Decoder Round @@ -104,6 +106,16 @@ playerSecretDecoder = object2 Secret ("secret" := string) +houseRuleDecoder : Decoder HouseRule.Id +houseRuleDecoder = customDecoder (string) (\name -> ruleNameToId name |> Result.fromMaybe ("Unknown house rule '" ++ name ++ "'.")) + +ruleNameToId : String -> Maybe HouseRule.Id +ruleNameToId name = + case name of + "reboot" -> Just HouseRule.Reboot + _ -> Nothing + + responsesTransportDecoder : Decoder ResponsesTransport responsesTransportDecoder = object2 ResponsesTransport (maybe ("hidden" := int)) diff --git a/client/src/MassiveDecks/Scenes/Config.elm b/client/src/MassiveDecks/Scenes/Config.elm index eb6b1b1..e044caf 100644 --- a/client/src/MassiveDecks/Scenes/Config.elm +++ b/client/src/MassiveDecks/Scenes/Config.elm @@ -78,6 +78,12 @@ update message lobbyModel = GameStarted lobbyAndHand -> (model, Util.cmd (LobbyUpdate lobbyAndHand)) + EnableRule rule -> + (model, Request.send' (API.enableRule rule gameCode secret) ErrorMessage LobbyUpdate) + + DisableRule rule -> + (model, Request.send' (API.disableRule rule gameCode secret) ErrorMessage LobbyUpdate) + NoOp -> (model, Cmd.none) diff --git a/client/src/MassiveDecks/Scenes/Config/Messages.elm b/client/src/MassiveDecks/Scenes/Config/Messages.elm index dc71886..ad5efd6 100644 --- a/client/src/MassiveDecks/Scenes/Config/Messages.elm +++ b/client/src/MassiveDecks/Scenes/Config/Messages.elm @@ -3,6 +3,7 @@ module MassiveDecks.Scenes.Config.Messages exposing (..) import MassiveDecks.Components.Input as Input import MassiveDecks.Components.Errors as Errors import MassiveDecks.Models.Game as Game +import MassiveDecks.Scenes.Playing.HouseRule.Id as HouseRule {-| This type is used for all sending of messages, allowing us to send messages handled outside this scene. @@ -21,6 +22,8 @@ type Message | AddAi | StartGame | GameStarted Game.LobbyAndHand + | EnableRule HouseRule.Id + | DisableRule HouseRule.Id | NoOp diff --git a/client/src/MassiveDecks/Scenes/Config/UI.elm b/client/src/MassiveDecks/Scenes/Config/UI.elm index 581d9c5..957a58d 100644 --- a/client/src/MassiveDecks/Scenes/Config/UI.elm +++ b/client/src/MassiveDecks/Scenes/Config/UI.elm @@ -10,6 +10,9 @@ import MassiveDecks.Components.Icon as Icon import MassiveDecks.Components.Input as Input import MassiveDecks.Models.Game as Game import MassiveDecks.Scenes.Lobby.Models as Lobby +import MassiveDecks.Scenes.Playing.HouseRule as HouseRule exposing (HouseRule) +import MassiveDecks.Scenes.Playing.HouseRule.Id as HouseRule +import MassiveDecks.Scenes.Playing.HouseRule.Available exposing (houseRules) import MassiveDecks.Scenes.Config.Messages exposing (Message(..), InputId(..), Deck(..)) import MassiveDecks.Util as Util @@ -38,13 +41,14 @@ view lobbyModel = ] , div [ id "decks", class "mui-tabs__pane mui--is-active" ] [ deckList decks model.loadingDecks model.deckIdInput ] - , div [ id "house-rules", class "mui-tabs__pane" ] [ rando ] + , div [ id "house-rules", class "mui-tabs__pane" ] + ([ rando ] ++ (List.map (\rule -> houseRule (List.member rule.id lobbyModel.lobby.config.houseRules) rule) houseRules)) , div [ class "mui-divider" ] [] , startGameButton enoughPlayers enoughCards ] ] -invite : String -> String -> Html Message +invite : String -> String -> Html msg invite appUrl lobbyId = let url = Util.lobbyUrl appUrl lobbyId @@ -99,7 +103,7 @@ deckList decks loadingDecks deckId = ] -deckLink : String -> Html Message +deckLink : String -> Html msg deckLink id = a [ href ("https://www.cardcastgame.com/browse/deck/" ++ id), target "_blank" ] [ text id ] @@ -125,8 +129,20 @@ emptyDeckListInfo display = [] -houseRule : String -> String -> String -> String -> String -> Message -> Html Message -houseRule id' title icon description buttonText message = +houseRule : Bool -> HouseRule -> Html Message +houseRule enabled rule = + let + (buttonText, command) = + if enabled then + ("Disable", DisableRule rule.id) + else + ("Enable", EnableRule rule.id) + in + houseRuleTemplate (HouseRule.toString rule.id) rule.name rule.icon rule.description buttonText command + + +houseRuleTemplate : String -> String -> String -> String -> String -> msg -> Html msg +houseRuleTemplate id' title icon description buttonText message = div [ id id', class "house-rule" ] [ div [] [ h3 [] [ Icon.icon icon, text " ", text title ] , button [ class "mui-btn mui-btn--small mui-btn--primary", onClick message ] @@ -137,12 +153,12 @@ houseRule id' title icon description buttonText message = rando : Html Message -rando = houseRule "rando" "Rando Cardrissian" "cogs" +rando = houseRuleTemplate "rando" "Rando Cardrissian" "cogs" "Every round, one random card will be played for an imaginary player named Rando Cardrissian, if he wins, all players go home in a state of everlasting shame." "Add an AI player" AddAi -startGameWarning : Bool -> Html Message +startGameWarning : Bool -> Html msg startGameWarning canStart = if canStart then text "" else span [] [ Icon.icon "info-circle", text " You will need at least two players to start the game." ] diff --git a/client/src/MassiveDecks/Scenes/Playing.elm b/client/src/MassiveDecks/Scenes/Playing.elm index 44c89da..c6e2fd4 100644 --- a/client/src/MassiveDecks/Scenes/Playing.elm +++ b/client/src/MassiveDecks/Scenes/Playing.elm @@ -19,6 +19,9 @@ import MassiveDecks.Scenes.Lobby.Models as Lobby import MassiveDecks.Scenes.Playing.UI as UI import MassiveDecks.Scenes.Playing.Models exposing (Model, ShownPlayedCards) import MassiveDecks.Scenes.Playing.Messages exposing (ConsumerMessage(..), Message(..)) +import MassiveDecks.Scenes.Playing.HouseRule as HouseRule exposing (HouseRule) +import MassiveDecks.Scenes.Playing.HouseRule.Id as HouseRule +import MassiveDecks.Scenes.Playing.HouseRule.Reboot as Reboot import MassiveDecks.Util as Util exposing ((:>)) @@ -34,6 +37,12 @@ init init = } +houseRule : HouseRule.Id -> HouseRule +houseRule id = + case id of + HouseRule.Reboot -> Reboot.rule + + {-| We shouldn't need to do this! int flags blow up at the moment. For now, we pass a string, but we should take an int from JS in the future. -} @@ -129,13 +138,16 @@ update message lobbyModel = (model, Request.send' (API.back gameCode secret) ErrorMessage (UpdateLobbyAndHand >> LocalMessage)) UpdateLobbyAndHand lobbyAndHand -> - lobbyModel |> updateLobbyAndHand lobbyAndHand + updateLobbyAndHand lobbyAndHand lobbyModel + + Redraw -> + (model, Request.send (API.redraw lobbyModel.lobby.gameCode lobbyModel.secret) redrawErrorHandler ErrorMessage (UpdateLobbyAndHand >> LocalMessage)) + + NoOp -> + (model, Cmd.none) -type alias Update = Lobby.Model -> (Model, Cmd ConsumerMessage) - - -updateLobbyAndHand : Game.LobbyAndHand -> Update +updateLobbyAndHand : Game.LobbyAndHand -> Lobby.Model -> (Model, Cmd ConsumerMessage) updateLobbyAndHand lobbyAndHand lobbyModel = let lobby = lobbyAndHand.lobby @@ -162,6 +174,12 @@ updateLobbyAndHand lobbyAndHand lobbyModel = (newModel, Util.cmd (LobbyUpdate lobbyAndHand)) +redrawErrorHandler : API.RedrawError -> ConsumerMessage +redrawErrorHandler error = + case error of + API.NotEnoughPoints -> ErrorMessage <| Errors.New "You do not have enough points to redraw your hand." False + + chooseErrorHandler : API.ChooseError -> ConsumerMessage chooseErrorHandler error = case error of diff --git a/client/src/MassiveDecks/Scenes/Playing/HouseRule.elm b/client/src/MassiveDecks/Scenes/Playing/HouseRule.elm new file mode 100644 index 0000000..6e50396 --- /dev/null +++ b/client/src/MassiveDecks/Scenes/Playing/HouseRule.elm @@ -0,0 +1,23 @@ +module MassiveDecks.Scenes.Playing.HouseRule exposing (HouseRule, Action) + +import MassiveDecks.Scenes.Playing.HouseRule.Id exposing (Id) +import MassiveDecks.Scenes.Playing.Messages as Playing +import MassiveDecks.Scenes.Lobby.Models as Lobby + + +type alias HouseRule = + { id : Id + , icon : String + , name : String + , description : String + , actions : List Action + } + + +type alias Action = + { icon : String + , text : String + , description : String + , onClick : Playing.Message + , enabled : Lobby.Model -> Bool + } diff --git a/client/src/MassiveDecks/Scenes/Playing/HouseRule/Available.elm b/client/src/MassiveDecks/Scenes/Playing/HouseRule/Available.elm new file mode 100644 index 0000000..a37755b --- /dev/null +++ b/client/src/MassiveDecks/Scenes/Playing/HouseRule/Available.elm @@ -0,0 +1,8 @@ +module MassiveDecks.Scenes.Playing.HouseRule.Available exposing (houseRules) + +import MassiveDecks.Scenes.Playing.HouseRule as HouseRule exposing (HouseRule) +import MassiveDecks.Scenes.Playing.HouseRule.Reboot as Reboot + + +houseRules : List HouseRule +houseRules = [ Reboot.rule ] diff --git a/client/src/MassiveDecks/Scenes/Playing/HouseRule/Id.elm b/client/src/MassiveDecks/Scenes/Playing/HouseRule/Id.elm new file mode 100644 index 0000000..775d940 --- /dev/null +++ b/client/src/MassiveDecks/Scenes/Playing/HouseRule/Id.elm @@ -0,0 +1,11 @@ +module MassiveDecks.Scenes.Playing.HouseRule.Id exposing (Id(..), toString) + + +type Id + = Reboot + + +toString : Id -> String +toString id = + case id of + Reboot -> "reboot" diff --git a/client/src/MassiveDecks/Scenes/Playing/HouseRule/Reboot.elm b/client/src/MassiveDecks/Scenes/Playing/HouseRule/Reboot.elm new file mode 100644 index 0000000..e9ebc35 --- /dev/null +++ b/client/src/MassiveDecks/Scenes/Playing/HouseRule/Reboot.elm @@ -0,0 +1,34 @@ +module MassiveDecks.Scenes.Playing.HouseRule.Reboot exposing (rule) + +import MassiveDecks.Models.Player as Player +import MassiveDecks.Scenes.Playing.HouseRule as HouseRule exposing (HouseRule) +import MassiveDecks.Scenes.Playing.HouseRule.Id as HouseRule +import MassiveDecks.Scenes.Playing.Messages as Playing +import MassiveDecks.Scenes.Lobby.Models as Lobby + + +rule : HouseRule +rule = + { id = HouseRule.Reboot + , icon = "recycle" + , name = "Rebooting the Universe" + , description = "At any time, players may trade in a point to discard their hand and redraw." + , actions = [ rebootAction ] + } + + +rebootAction : HouseRule.Action +rebootAction = + { icon = "recycle" + , text = "Redraw" + , description = "Lose one point to discard your hand and draw a new one." + , onClick = Playing.Redraw + , enabled = checkEnabled + } + + +checkEnabled : Lobby.Model -> Bool +checkEnabled lobbyModel = + Player.byId lobbyModel.secret.id lobbyModel.lobby.players + |> Maybe.map (.score >> ((>) 0)) + |> Maybe.withDefault False diff --git a/client/src/MassiveDecks/Scenes/Playing/Messages.elm b/client/src/MassiveDecks/Scenes/Playing/Messages.elm index 184ca1b..ebd7d62 100644 --- a/client/src/MassiveDecks/Scenes/Playing/Messages.elm +++ b/client/src/MassiveDecks/Scenes/Playing/Messages.elm @@ -26,4 +26,6 @@ type Message | CheckForPlayedCardsToAnimate | Skip (List Player.Id) | Back + | Redraw | UpdateLobbyAndHand Game.LobbyAndHand + | NoOp diff --git a/client/src/MassiveDecks/Scenes/Playing/UI.elm b/client/src/MassiveDecks/Scenes/Playing/UI.elm index f5e26af..e108411 100644 --- a/client/src/MassiveDecks/Scenes/Playing/UI.elm +++ b/client/src/MassiveDecks/Scenes/Playing/UI.elm @@ -10,6 +10,8 @@ import MassiveDecks.Models.Player as Player exposing (Player) import MassiveDecks.Models.Card as Card import MassiveDecks.Scenes.Lobby.Models as Lobby import MassiveDecks.Scenes.Playing.Messages exposing (Message(..)) +import MassiveDecks.Scenes.Playing.HouseRule as HouseRule exposing (HouseRule) +import MassiveDecks.Scenes.Playing.HouseRule.Available exposing (houseRules) import MassiveDecks.Util as Util @@ -32,6 +34,45 @@ view lobbyModel = ([], []) +houseRuleMenu : Lobby.Model -> List (Html Message) +houseRuleMenu lobbyModel = + let + enabled = List.filter (\rule -> List.member rule.id lobbyModel.lobby.config.houseRules) houseRules + in + if List.isEmpty enabled then + [] + else + [ div [ class "action-menu mui-dropdown"] + [ button [ class "mui-btn mui-btn--small mui-btn--fab" + , title "Game actions." + , attribute "data-mui-toggle" "dropdown" + ] + [ Icon.icon "bars" ] + , ul [ class "mui-dropdown__menu mui-dropdown__menu--right" ] + (List.concatMap (houseRuleMenuItems lobbyModel) enabled) + ] + ] + +houseRuleMenuItems : Lobby.Model -> HouseRule -> List (Html Message) +houseRuleMenuItems lobbyModel rule = List.map (houseRuleMenuItem lobbyModel rule) rule.actions + + +houseRuleMenuItem : Lobby.Model -> HouseRule -> HouseRule.Action -> Html Message +houseRuleMenuItem lobbyModel rule action = + let + enabled = action.enabled lobbyModel + message = if enabled then action.onClick else NoOp + in + li [] [ a [ classList [ ("link", True), ("disabled", not enabled) ] + , title action.description + , attribute "tabindex" "0" + , attribute "role" "button" + , onClick message + ] + [ Icon.fwIcon action.icon, text " ", text action.text ] + ] + + roundContents : Lobby.Model -> Game.Round -> List (Html Message) roundContents lobbyModel round = let @@ -63,9 +104,9 @@ roundContents lobbyModel round = Card.Hidden _ -> handView model.picked (not canPlay) hand in [ playArea - [ div [ class "round-area" ] (List.concat [ [ call round.call callFill ], pickedOrChosen ]) - , playedOrHand - ] + ([ div [ class "round-area" ] (List.concat [ [ call round.call callFill ], pickedOrChosen ]) + , playedOrHand + ] ++ houseRuleMenu lobbyModel) ] diff --git a/client/src/MassiveDecks/Scenes/Start.elm b/client/src/MassiveDecks/Scenes/Start.elm index a264cd0..c9a34de 100644 --- a/client/src/MassiveDecks/Scenes/Start.elm +++ b/client/src/MassiveDecks/Scenes/Start.elm @@ -34,6 +34,7 @@ init init = , init = init , nameInput = Input.init Name "name-input" [ text "Your name in the game." ] "" "Nickname" InputMessage , gameCodeInput = Input.init GameCode "game-code-input" [ text "The code for the game to join." ] (init.gameCode |> Maybe.withDefault "") "" InputMessage + , info = Nothing , errors = Errors.init } , command) @@ -80,6 +81,9 @@ update message model = in ({ model | errors = newErrors }, Cmd.map ErrorMessage cmd) + ShowInfoMessage message -> + ({ model | info = Just message }, Cmd.none) + CreateLobby -> (model, Request.send' API.createLobby ErrorMessage (\lobby -> JoinLobbyAsNewPlayer lobby.gameCode)) @@ -90,7 +94,7 @@ update message model = {- TODO: We send the secret to the websocket here for crazy reasons - remove - Bug workaround! -} model ! [ Lobby.sendSecretToWebSocket model.init.url gameCode secret - , Request.send' (API.getLobbyAndHand gameCode secret) ErrorMessage (JoinLobby secret) + , Request.send (API.getLobbyAndHand gameCode secret) getLobbyAndHandErrorHandler ErrorMessage (JoinLobby secret) , Storage.storeInGame (Game.GameCodeAndSecret gameCode secret) ] @@ -133,4 +137,10 @@ newPlayerErrorHandler : API.NewPlayerError -> Message newPlayerErrorHandler error = case error of API.NameInUse -> (Name, Just "This name is already in use in this game, try something else." |> Input.Error) |> InputMessage - API.LobbyNotFound -> (GameCode, Just "This game doesn't exist - check you have the right code." |> Input.Error) |> InputMessage + API.NewPlayerLobbyNotFound -> (GameCode, Just "This game doesn't exist - check you have the right code." |> Input.Error) |> InputMessage + + +getLobbyAndHandErrorHandler : API.GetLobbyAndHandError -> Message +getLobbyAndHandErrorHandler error = + case error of + API.LobbyNotFound -> "The game you were in has ended." |> ShowInfoMessage diff --git a/client/src/MassiveDecks/Scenes/Start/Messages.elm b/client/src/MassiveDecks/Scenes/Start/Messages.elm index 8a1ad6f..8dc15d0 100644 --- a/client/src/MassiveDecks/Scenes/Start/Messages.elm +++ b/client/src/MassiveDecks/Scenes/Start/Messages.elm @@ -11,6 +11,7 @@ import MassiveDecks.Models.Player as Player -} type Message = CreateLobby + | ShowInfoMessage String | JoinLobbyAsNewPlayer String | JoinLobbyAsExistingPlayer Player.Secret String | JoinLobby Player.Secret Game.LobbyAndHand diff --git a/client/src/MassiveDecks/Scenes/Start/Models.elm b/client/src/MassiveDecks/Scenes/Start/Models.elm index 2530db5..a1960cd 100644 --- a/client/src/MassiveDecks/Scenes/Start/Models.elm +++ b/client/src/MassiveDecks/Scenes/Start/Models.elm @@ -14,5 +14,6 @@ type alias Model = , init : Init , nameInput : Input.Model InputId Message , gameCodeInput : Input.Model InputId Message + , info : Maybe String , errors : Errors.Model } diff --git a/client/src/MassiveDecks/Scenes/Start/UI.elm b/client/src/MassiveDecks/Scenes/Start/UI.elm index 2c78afa..e002503 100644 --- a/client/src/MassiveDecks/Scenes/Start/UI.elm +++ b/client/src/MassiveDecks/Scenes/Start/UI.elm @@ -6,8 +6,8 @@ import Html exposing (..) import Html.Attributes exposing (..) import Html.Events exposing (..) -import MassiveDecks.Components.Icon exposing (..) -import MassiveDecks.Components.About exposing (..) +import MassiveDecks.Components.Icon as Icon +import MassiveDecks.Components.About as About import MassiveDecks.Components.Input as Input import MassiveDecks.Scenes.Start.Models exposing (Model) import MassiveDecks.Scenes.Start.Messages exposing (Message(..)) @@ -27,27 +27,28 @@ view model = in div [ id "start-screen" ] [ div [ id "start-screen-content", class "mui-panel" ] - [ h1 [ class "mui--divider-bottom" ] [ text "Massive Decks" ] - , Input.view model.nameInput - , ul [ class "mui-tabs__bar mui-tabs__bar--justified" ] - [ li createLiClass [ a [ attribute "data-mui-toggle" "tab", attribute "data-mui-controls" "pane-new" ] [ text "Create" ] ] - , li joinLiClass [ a [ attribute "data-mui-toggle" "tab", attribute "data-mui-controls" "pane-existing" ] [ text "Join" ] ] + ([ h1 [ class "mui--divider-bottom" ] [ text "Massive Decks" ] + ] ++ (model.info |> Maybe.map (\message -> [ div [ class "info-message mui--divider-bottom" ] [ Icon.icon "info-circle", text " ", text message ] ]) |> Maybe.withDefault []) ++ + [ Input.view model.nameInput + , ul [ class "mui-tabs__bar mui-tabs__bar--justified" ] + [ li createLiClass [ a [ attribute "data-mui-toggle" "tab", attribute "data-mui-controls" "pane-new" ] [ text "Create" ] ] + , li joinLiClass [ a [ attribute "data-mui-toggle" "tab", attribute "data-mui-controls" "pane-existing" ] [ text "Join" ] ] ] - , div [ class ("mui-tabs__pane" ++ createDivClass), id "pane-new" ] [ createLobbyButton nameEntered ] - , div [ class ("mui-tabs__pane" ++ joinDivClass), id "pane-existing" ] - [ Input.view model.gameCodeInput - , joinLobbyButton model.gameCodeInput.value (nameEntered && gameCodeEntered) - ] - , a [ class "about-link mui--divider-top link" - , attribute "tabindex" "0" - , attribute "role" "button" - , attribute "onClick" "aboutOverlay()" - ] - [ icon "question-circle" , text " About" ] - , aboutOverlay - , div [ id "forkongithub" ] [ div [] [ a [ href "https://github.com/lattyware/massivedecks", target "_blank" ] - [ icon "github", text " Fork me on GitHub" ] ] ] - ] + , div [ class ("mui-tabs__pane" ++ createDivClass), id "pane-new" ] [ createLobbyButton nameEntered ] + , div [ class ("mui-tabs__pane" ++ joinDivClass), id "pane-existing" ] + [ Input.view model.gameCodeInput + , joinLobbyButton model.gameCodeInput.value (nameEntered && gameCodeEntered) + ] + , a [ class "about-link mui--divider-top link" + , attribute "tabindex" "0" + , attribute "role" "button" + , attribute "onClick" "aboutOverlay()" + ] + [ Icon.icon "question-circle" , text " About" ] + , About.aboutOverlay + , div [ id "forkongithub" ] [ div [] [ a [ href "https://github.com/lattyware/massivedecks", target "_blank" ] + [ Icon.icon "github", text " Fork me on GitHub" ] ] ] + ]) ] diff --git a/client/src/MassiveDecks/Util.elm b/client/src/MassiveDecks/Util.elm index 7a38db5..bb2a0a9 100644 --- a/client/src/MassiveDecks/Util.elm +++ b/client/src/MassiveDecks/Util.elm @@ -4,6 +4,13 @@ import Task import String +{-| Since a value of `Never` can never exist, this can't ever actually happen. Used to fill gaps where the type system +requires a method, but it'll never get called. +-} +impossible : Never -> a +impossible n = impossible n + + {-| Add an item to a list if the value is not nothing. -} andMaybe : List a -> Maybe a -> List a