From aaf3f2bd093b19d9e10959418695d1ac1dda9228 Mon Sep 17 00:00:00 2001 From: Gareth Latty Date: Sun, 14 Jul 2019 13:47:51 +0100 Subject: [PATCH] Restructure styles, add finished round advance button. --- client/src/elm/MassiveDecks.elm | 13 +- .../src/elm/MassiveDecks/Components/Menu.elm | 13 +- client/src/elm/MassiveDecks/Game.elm | 118 ++++--- client/src/elm/MassiveDecks/Game/Action.elm | 28 +- .../elm/MassiveDecks/Game/Action/Model.elm | 1 + client/src/elm/MassiveDecks/Game/Messages.elm | 1 + client/src/elm/MassiveDecks/Game/Model.elm | 2 + .../elm/MassiveDecks/Game/Round/Complete.elm | 15 +- .../elm/MassiveDecks/Game/Round/Playing.elm | 9 +- .../src/elm/MassiveDecks/Game/Round/Plays.elm | 19 +- client/src/elm/MassiveDecks/Icon.elm | 43 +++ client/src/elm/MassiveDecks/Messages.elm | 2 + .../src/elm/MassiveDecks/Models/Decoders.elm | 28 +- .../src/elm/MassiveDecks/Models/Encoders.elm | 7 +- client/src/elm/MassiveDecks/Pages/Lobby.elm | 144 +++++--- .../elm/MassiveDecks/Pages/Lobby/Actions.elm | 6 + .../elm/MassiveDecks/Pages/Lobby/Events.elm | 9 +- client/src/elm/MassiveDecks/Pages/Start.elm | 6 +- .../Pages/Start/LobbyBrowser/Model.elm | 1 - client/src/elm/MassiveDecks/Requests/Api.elm | 2 +- client/src/elm/MassiveDecks/Settings.elm | 87 +++-- .../elm/MassiveDecks/Settings/Messages.elm | 3 +- .../src/elm/MassiveDecks/Settings/Model.elm | 51 ++- client/src/elm/MassiveDecks/Strings.elm | 9 +- .../elm/MassiveDecks/Strings/Languages/En.elm | 23 +- .../src/elm/MassiveDecks/Strings/Render.elm | 2 +- client/src/elm/MassiveDecks/Util.elm | 27 +- client/src/elm/Reread.elm | 13 - client/src/elm/Weightless.elm | 62 ++-- client/src/elm/Weightless/Attributes.elm | 6 + client/src/elm/Weightless/Internal.elm | 42 +++ client/src/elm/Weightless/Slider.elm | 54 +++ client/src/scss/card/size.scss | 98 ++++++ client/src/scss/cards.scss | 24 +- client/src/scss/game.scss | 308 +++++------------- client/src/scss/game/judging.scss | 25 ++ client/src/scss/game/playing.scss | 71 ++++ client/src/scss/game/plays.scss | 16 + client/src/scss/pages/lobby.scss | 129 +++----- client/src/scss/pages/lobby/configure.scss | 1 - client/src/scss/pages/lobby/variables.scss | 2 + client/src/ts/weightless.ts | 1 + server/package-lock.json | 90 +++-- server/package.json | 12 +- server/src/ts/action/validation.validator.ts | 38 ++- 45 files changed, 1078 insertions(+), 583 deletions(-) create mode 100644 client/src/elm/MassiveDecks/Icon.elm delete mode 100644 client/src/elm/Reread.elm create mode 100644 client/src/elm/Weightless/Internal.elm create mode 100644 client/src/elm/Weightless/Slider.elm create mode 100644 client/src/scss/card/size.scss create mode 100644 client/src/scss/game/judging.scss create mode 100644 client/src/scss/game/playing.scss create mode 100644 client/src/scss/game/plays.scss create mode 100644 client/src/scss/pages/lobby/variables.scss diff --git a/client/src/elm/MassiveDecks.elm b/client/src/elm/MassiveDecks.elm index 197cedd..f773ded 100644 --- a/client/src/elm/MassiveDecks.elm +++ b/client/src/elm/MassiveDecks.elm @@ -163,6 +163,14 @@ update msg model = , Cmd.batch [ lobbyCmd, settingsCmd ] ) + UpdateToken auth -> + let + shared = + model.shared + in + Util.modelLift (\settings -> { model | shared = { shared | settings = settings } }) + (Settings.onTokenUpdate auth shared.settings) + StartMsg startMsg -> case model.page of Pages.Start startModel -> @@ -215,7 +223,7 @@ update msg model = Refresh -> ( model, Navigation.reload ) - -- TODO: Maybe popup something here? + -- TODO: Error? This would be an application error. It's pretty harmless though, probably not worth interrupting. BlockedExternalUrl -> ( model, Cmd.none ) @@ -225,6 +233,9 @@ update msg model = NoOp -> ( model, Cmd.none ) + Batch messages -> + Util.batchUpdate messages model update + view : Model -> Browser.Document Msg view model = diff --git a/client/src/elm/MassiveDecks/Components/Menu.elm b/client/src/elm/MassiveDecks/Components/Menu.elm index 3a861de..4d42531 100644 --- a/client/src/elm/MassiveDecks/Components/Menu.elm +++ b/client/src/elm/MassiveDecks/Components/Menu.elm @@ -62,8 +62,14 @@ link icon text description href = {-| Render a menu to Html. -} -view : Shared -> String -> WlA.XOrigin -> WlA.YOrigin -> Menu msg -> Html msg -view shared anchorId xOrigin yOrigin menu = +view : + Shared + -> String + -> ( WlA.XOrigin, WlA.YOrigin ) + -> ( WlA.XOrigin, WlA.YOrigin ) + -> Menu msg + -> Html msg +view shared anchorId ( xAnchor, yAnchor ) ( xTransform, yTransform ) menu = Wl.popover (List.concat [ [ WlA.anchor anchorId @@ -73,7 +79,8 @@ view shared anchorId xOrigin yOrigin menu = , HtmlA.class "menu" , WlA.disableFocusTrap ] - , WlA.anchorOrigin xOrigin yOrigin + , WlA.anchorOrigin xAnchor yAnchor + , WlA.transformOrigin xTransform yTransform ] ) [ Wl.popoverCard [] [ Html.ul [] (menu |> List.map (menuItem shared)) ] ] diff --git a/client/src/elm/MassiveDecks/Game.elm b/client/src/elm/MassiveDecks/Game.elm index 507bb97..beb963a 100644 --- a/client/src/elm/MassiveDecks/Game.elm +++ b/client/src/elm/MassiveDecks/Game.elm @@ -17,6 +17,7 @@ import Html.Events as HtmlE import MassiveDecks.Card as Card import MassiveDecks.Card.Model as Card import MassiveDecks.Card.Play as Play exposing (Play) +import MassiveDecks.Components as Components import MassiveDecks.Game.Action as Action import MassiveDecks.Game.Action.Model as Action exposing (Action) import MassiveDecks.Game.Messages exposing (..) @@ -39,6 +40,7 @@ import MassiveDecks.Strings.Languages as Lang import MassiveDecks.User as User exposing (User) import MassiveDecks.Util as Util import MassiveDecks.Util.Html as Html +import MassiveDecks.Util.Maybe as Maybe import Set import Task import Weightless as Wl @@ -176,6 +178,16 @@ update msg model = SetPlayStyles playStyles -> ( { model | playStyles = playStyles }, Cmd.none ) + AdvanceRound -> + case model.nextRound of + Just nextRound -> + ( { model | game = { game | round = Round.P nextRound }, nextRound = Nothing } + , Cmd.none + ) + + Nothing -> + ( model, Cmd.none ) + subscriptions : Model -> Sub Global.Msg subscriptions model = @@ -197,7 +209,7 @@ view shared auth decks users model = Judging.view shared auth decks round Round.C round -> - Complete.view shared decks users round + Complete.view shared (Maybe.isJust model.nextRound) decks users round game = model.game @@ -212,33 +224,49 @@ view shared auth decks users model = call |> Card.viewFilled shared decks Card.Front [] parts help = - case instruction of - Just i -> - Html.div [ HtmlA.class "context-help" ] [ Icon.view Icon.questionCircle, i |> Lang.html shared ] + let + id = + "context-help-button" - Nothing -> - Html.nothing + helpContent = + case instruction of + Just i -> + [ Components.iconButton + [ HtmlA.id id, Strings.WhatToDo |> Lang.title shared ] + Icon.question + , Wl.popover + (List.concat + [ WlA.anchorOrigin WlA.XCenter WlA.YBottom + , WlA.transformOrigin WlA.XRight WlA.YTop + , [ WlA.anchor id, WlA.fixed, WlA.anchorOpenEvents [ "click" ] ] + ] + ) + [ Wl.popoverCard [] [ i |> Lang.html shared ] ] + ] + + Nothing -> + [] + in + Html.div [ HtmlA.id "context-help" ] helpContent in Html.div [ HtmlA.id "game" ] [ help - , Html.div [ HtmlA.class "card-area" ] - [ Html.div [ HtmlA.class "play-area" ] - [ Html.div [ HtmlA.class "cards-in-play" ] [ renderedCall ] - , viewAction shared action - ] - , content - , Html.div [ HtmlA.class "scroll-top-spacer" ] [] + , Html.div [ HtmlA.class "round" ] + [ renderedCall + , viewAction shared action + ] + , content + , Html.div [ HtmlA.class "scroll-top-spacer" ] [] - -- TODO: Hide this when at top. Waiting on native elm scroll events, as currently we'd have to ping. - , Html.div [ HtmlA.class "scroll-top" ] - [ Wl.button - [ WlA.flat - , WlA.fab - , WlA.inverted - , ScrollToTop |> lift |> HtmlE.onClick - ] - [ Icon.view Icon.arrowUp ] + -- TODO: Hide this when at top. Waiting on native elm scroll events, as currently we'd have to ping constantly. + , Html.div [ HtmlA.class "scroll-top" ] + [ Wl.button + [ WlA.flat + , WlA.fab + , WlA.inverted + , ScrollToTop |> lift |> HtmlE.onClick ] + [ Icon.view Icon.arrowUp ] ] ] @@ -248,25 +276,29 @@ viewAction shared visible = Html.div [] (Action.actions |> List.map (Action.view shared visible)) -applyGameEvent : Events.GameEvent -> Model -> ( Model, Cmd Global.Msg ) -applyGameEvent gameEvent model = +applyGameEvent : Lobby.Auth -> Events.GameEvent -> Model -> ( Model, Cmd Global.Msg ) +applyGameEvent auth gameEvent model = case gameEvent of - Events.HandRedrawn redraw -> - case redraw of - Events.Player { hand } -> - -- TODO: Impl - ( model, Cmd.none ) + Events.HandRedrawn { player, hand } -> + let + game = + model.game - Events.Other { player } -> - --let - -- playerRecord = - -- Dict.get player model.game.players - -- - -- newPlayerRecord = - -- Dict.insert player { playerRecord | score = playerRecord.score - lobby.config.rules.houseRules } - --in - -- TODO: Impl - ( model, Cmd.none ) + -- TODO: Error, if the rule isn't enabled, we are out of sync. + cost = + game.rules.houseRules.reboot |> Maybe.map .cost |> Maybe.withDefault 0 + + updatePlayer = + \id -> \p -> { p | score = p.score - cost } |> Maybe.justIf (player == id) |> Maybe.withDefault p + + -- TODO: Error, if we get a hand but the event claims another player, we are out of sync. + newHand = + hand |> Maybe.andThen (Maybe.justIf (player == auth.claims.uid)) |> Maybe.withDefault model.hand + + players = + game.players |> Dict.map updatePlayer + in + ( { model | game = { game | players = players }, hand = newHand }, Cmd.none ) Events.PlayRevealed { id, play } -> let @@ -350,19 +382,13 @@ applyGameEvent gameEvent model = Events.RoundStarted { id, czar, players, call, drawn } -> let - oldGame = - model.game - drawnAsList = drawn |> Maybe.withDefault [] ( newRound, cmd ) = Playing.init (Round.playing id czar players call Set.empty) Round.noPick - - game = - { oldGame | round = Round.P newRound } in - ( { model | game = game, hand = model.hand ++ drawnAsList }, cmd ) + ( { model | nextRound = Just newRound, hand = model.hand ++ drawnAsList }, cmd ) Events.StartRevealing { plays, drawn } -> case model.game.round of diff --git a/client/src/elm/MassiveDecks/Game/Action.elm b/client/src/elm/MassiveDecks/Game/Action.elm index d521ec6..dd19570 100644 --- a/client/src/elm/MassiveDecks/Game/Action.elm +++ b/client/src/elm/MassiveDecks/Game/Action.elm @@ -13,13 +13,26 @@ import MassiveDecks.Model exposing (..) import MassiveDecks.Pages.Lobby.Messages as Lobby import MassiveDecks.Strings as Strings exposing (MdString) import MassiveDecks.Strings.Languages as Lang -import MassiveDecks.Util.Html.Attributes as HtmlA import Weightless.Attributes as WlA actions : List Action actions = - [ Submit, TakeBack, Judge, Like ] + [ Submit, TakeBack, Judge, Like, Advance ] + + +{-| Style for actions that are blocking the game from continuing until they are performed. +-} +blocking : List (Html.Attribute msg) +blocking = + [ HtmlA.class "important" ] + + +{-| Style for an action that doesn't block the game. +-} +normal : List (Html.Attribute msg) +normal = + [ WlA.inverted, WlA.outlined ] view : Shared -> Maybe Action -> Action -> Html Global.Msg @@ -28,16 +41,19 @@ view shared visible action = { icon, attrs, title, onClick } = case action of Submit -> - IconView Icon.check [ HtmlA.class "important" ] Strings.SubmitPlay Game.Submit + IconView Icon.check blocking Strings.SubmitPlay Game.Submit TakeBack -> - IconView Icon.undo [ WlA.inverted, WlA.outlined ] Strings.TakeBackPlay Game.TakeBack + IconView Icon.undo normal Strings.TakeBackPlay Game.TakeBack Judge -> - IconView Icon.trophy [ HtmlA.class "important" ] Strings.JudgePlay Game.Judge + IconView Icon.trophy blocking Strings.JudgePlay Game.Judge Like -> - IconView Icon.thumbsUp [ WlA.inverted, WlA.outlined ] Strings.LikePlay Game.Like + IconView Icon.thumbsUp normal Strings.LikePlay Game.Like + + Advance -> + IconView Icon.forward normal Strings.AdvanceRound Game.AdvanceRound in Components.floatingActionButton (List.concat diff --git a/client/src/elm/MassiveDecks/Game/Action/Model.elm b/client/src/elm/MassiveDecks/Game/Action/Model.elm index d4f54f9..b0a4893 100644 --- a/client/src/elm/MassiveDecks/Game/Action/Model.elm +++ b/client/src/elm/MassiveDecks/Game/Action/Model.elm @@ -6,3 +6,4 @@ type Action | TakeBack | Judge | Like + | Advance diff --git a/client/src/elm/MassiveDecks/Game/Messages.elm b/client/src/elm/MassiveDecks/Game/Messages.elm index 5ad19a4..ed55277 100644 --- a/client/src/elm/MassiveDecks/Game/Messages.elm +++ b/client/src/elm/MassiveDecks/Game/Messages.elm @@ -15,3 +15,4 @@ type Msg | Like | ScrollToTop | SetPlayStyles PlayStyles + | AdvanceRound diff --git a/client/src/elm/MassiveDecks/Game/Model.elm b/client/src/elm/MassiveDecks/Game/Model.elm index 454e421..a1ed88a 100644 --- a/client/src/elm/MassiveDecks/Game/Model.elm +++ b/client/src/elm/MassiveDecks/Game/Model.elm @@ -25,6 +25,7 @@ type alias Model = { game : Game , hand : List Card.Response , playStyles : PlayStyles + , nextRound : Maybe Round.Playing } @@ -35,6 +36,7 @@ emptyModel game = { game = game , hand = [] , playStyles = Dict.empty + , nextRound = Nothing } diff --git a/client/src/elm/MassiveDecks/Game/Round/Complete.elm b/client/src/elm/MassiveDecks/Game/Round/Complete.elm index d4a30f8..cbc817b 100644 --- a/client/src/elm/MassiveDecks/Game/Round/Complete.elm +++ b/client/src/elm/MassiveDecks/Game/Round/Complete.elm @@ -8,6 +8,7 @@ import Html.Attributes as HtmlA import Html.Keyed as HtmlK import MassiveDecks.Card as Card import MassiveDecks.Card.Model as Card +import MassiveDecks.Game.Action.Model as Action import MassiveDecks.Game.Model exposing (..) import MassiveDecks.Game.Round as Round import MassiveDecks.Messages as Global @@ -20,16 +21,16 @@ import MassiveDecks.Util.Html as Html import MassiveDecks.Util.Maybe as Maybe -view : Shared -> List Configure.Deck -> Dict User.Id User -> Round.Complete -> RoundView Global.Msg -view shared decks users round = +view : Shared -> Bool -> List Configure.Deck -> Dict User.Id User -> Round.Complete -> RoundView Global.Msg +view shared nextRoundReady decks users round = let winning = round.plays |> Dict.get round.winner in - { instruction = Nothing - , action = Nothing + { instruction = Strings.AdvanceRoundInstruction |> Maybe.justIf nextRoundReady + , action = Action.Advance |> Maybe.justIf nextRoundReady , content = - HtmlK.ul [ HtmlA.class "judging plays" ] + HtmlK.ul [ HtmlA.class "complete plays cards" ] (round.plays |> Dict.toList |> List.map (viewPlay shared decks users round.winner)) , fillCallWith = winning |> Maybe.withDefault [] } @@ -45,7 +46,7 @@ viewPlay : viewPlay shared decks users winner ( id, responses ) = let cards = - responses |> List.map (\r -> Card.view shared decks Card.Front [] (Card.R r)) + responses |> List.map (\r -> ( r.details.id, Card.view shared decks Card.Front [] (Card.R r) )) playedBy = users |> Dict.get id |> Maybe.map .name |> Maybe.withDefault (Strings.UnknownUser |> Lang.string shared) @@ -58,6 +59,6 @@ viewPlay shared decks users winner ( id, responses ) = ( id , Html.li [ HtmlA.class "play-with-byline" ] [ Html.span [ HtmlA.class "byline" ] byline - , Html.div [ HtmlA.class "play" ] cards + , HtmlK.ol [ HtmlA.class "play card-set" ] cards ] ) diff --git a/client/src/elm/MassiveDecks/Game/Round/Playing.elm b/client/src/elm/MassiveDecks/Game/Round/Playing.elm index ae6217f..cec8c30 100644 --- a/client/src/elm/MassiveDecks/Game/Round/Playing.elm +++ b/client/src/elm/MassiveDecks/Game/Round/Playing.elm @@ -7,6 +7,7 @@ import Dict exposing (Dict) import Html exposing (Html) import Html.Attributes as HtmlA import Html.Events as HtmlE +import Html.Keyed as HtmlK import MassiveDecks.Card as Card import MassiveDecks.Card.Model as Card import MassiveDecks.Game.Action.Model as Action @@ -66,7 +67,7 @@ view shared auth decks model round = ( Just Action.TakeBack, Strings.WaitingForPlaysInstruction, True ) hand = - Html.div [ HtmlA.classList [ ( "hand", True ), ( "not-playing", notPlaying ) ] ] + HtmlK.ul [ HtmlA.classList [ ( "hand", True ), ( "cards", True ), ( "not-playing", notPlaying ) ] ] (model.hand |> List.map (round.pick.cards |> viewHandCard shared decks)) backgroundPlays = @@ -93,9 +94,10 @@ view shared auth decks model round = {- Private -} -viewHandCard : Shared -> List Configure.Deck -> List Card.Id -> Card.Response -> Html Global.Msg +viewHandCard : Shared -> List Configure.Deck -> List Card.Id -> Card.Response -> ( String, Html Global.Msg ) viewHandCard shared decks picked response = - Card.view + ( response.details.id + , Card.view shared decks Card.Front @@ -103,6 +105,7 @@ viewHandCard shared decks picked response = , response.details.id |> Game.Pick |> lift |> HtmlE.onClick ] (Card.R response) + ) viewBackgroundPlay : PlayStyles -> Int -> Set User.Id -> User.Id -> Html msg diff --git a/client/src/elm/MassiveDecks/Game/Round/Plays.elm b/client/src/elm/MassiveDecks/Game/Round/Plays.elm index 74cae4f..579db47 100644 --- a/client/src/elm/MassiveDecks/Game/Round/Plays.elm +++ b/client/src/elm/MassiveDecks/Game/Round/Plays.elm @@ -22,7 +22,7 @@ type alias Details msg = -} view : String -> Maybe Play.Id -> List (Details msg) -> Html msg view class picked details = - HtmlK.ul [ HtmlA.classList [ ( class, True ), ( "plays", True ) ] ] (details |> List.map (viewPlay picked)) + HtmlK.ul [ HtmlA.classList [ ( class, True ), ( "cards", True ), ( "plays", True ) ] ] (details |> List.map (viewPlay picked)) @@ -32,13 +32,12 @@ view class picked details = viewPlay : Maybe Play.Id -> Details msg -> ( String, Html msg ) viewPlay picked { id, responses, action, attrs } = ( id - , Html.li - (List.concat - [ [ HtmlA.classList [ ( "play", True ), ( "picked", picked == Just id ) ] - , action |> Maybe.map HtmlE.onClick |> Maybe.withDefault HtmlA.nothing - ] - , attrs - ] - ) - (responses |> List.map (\card -> Html.div [ HtmlA.class "overlap" ] [ card ])) + , Html.li [] + [ Html.ol + (HtmlA.classList [ ( "play", True ), ( "card-set", True ), ( "picked", picked == Just id ) ] + :: (action |> Maybe.map HtmlE.onClick |> Maybe.withDefault HtmlA.nothing) + :: attrs + ) + (responses |> List.map (\card -> Html.li [] [ card ])) + ] ) diff --git a/client/src/elm/MassiveDecks/Icon.elm b/client/src/elm/MassiveDecks/Icon.elm new file mode 100644 index 0000000..60eda42 --- /dev/null +++ b/client/src/elm/MassiveDecks/Icon.elm @@ -0,0 +1,43 @@ +module MassiveDecks.Icon exposing + ( callCard + , massiveDecks + , minimalCardSize + , rereadGames + , responseCard + , squareCardSize + ) + +{-| Icons that aren't in FontAwesome that we need. +-} + +import FontAwesome.Icon as Icon exposing (Icon) + + +rereadGames : Icon +rereadGames = + Icon "fab" "reread-games" 512 512 "M258 16c-62 0-124 24-171 71-95 94-94 246 0 339 95 93 250 93 345 0l12-12-15-14-12 12-10-10c-82 80-213 80-295 0-82-81-82-210-1-291 81-80 212-81 294-3l-38 35 129 25-1-3-26-117-39 36c-48-46-110-68-172-68zm-79 159c-46 1-70 10-70 10v133s24-9 70-9c50 0 70 20 70 20V196s-20-21-70-21zm160 0c-50 0-70 21-70 21v133s20-20 70-20c46 0 70 9 70 9V185s-24-9-70-10zM179 323c-46 1-70 10-70 10v16s24-9 70-9c50 0 81 31 81 31s30-31 80-31c46 0 70 9 70 9v-16s-24-9-70-10c-50 0-80 32-80 32s-31-32-81-32z" + + +massiveDecks : Icon +massiveDecks = + Icon "fab" "massive-decks" 512 512 "M273 20c-11 0-21 9-23 21l-9 88h-8L39 163c-12 2-21 15-19 27l50 283c2 12 15 21 27 19l194-34c12-3 21-15 18-27l-13-73 140 14c13 2 25-8 26-20l30-286c1-13-8-24-21-25L276 20h-3zm0 16h1l196 21c4 0 6 3 6 7l-30 286c0 4-4 7-8 6l-144-15-35-193-3-9 10-97c1-3 4-6 7-6zm47 106l-10 99 39 4c15 2 25 0 32-8 9-8 15-21 16-37 2-15-1-29-8-39-6-9-15-13-30-15zm19 20l18 1c16 2 22 14 20 35-3 22-11 32-27 31l-18-2zm-135 91l17 98-20 4-13-77-4 80-20 3-32-73 14 76-20 4-18-98 31-6 31 75 4-81z" + + +minimalCardSize : Icon +minimalCardSize = + Icon "fas" "minimal-card-size" 384 512 "M45 395c-21 0-39 17-39 39v39c0 21 18 39 39 39h294c21 0 39-18 39-39v-39c0-22-18-39-39-39z" + + +squareCardSize : Icon +squareCardSize = + Icon "fas" "square-card-size" 384 512 "M45 141c-21 0-39 17-39 39v293c0 21 18 39 39 39h294c21 0 39-18 39-39V180c0-22-18-39-39-39z" + + +callCard : Icon +callCard = + Icon "fas" "call-card" 384 512 "M45 10h294c16 0 29 13 29 29v434c0 16-13 29-29 29H45c-16 0-29-13-29-29V39c0-16 13-29 29-29z" + + +responseCard : Icon +responseCard = + Icon "fas" "response-card" 384 512 "M45 0C24 0 6 18 6 39v434c0 21 18 39 39 39h294c21 0 39-18 39-39V39c0-21-18-39-39-39zm0 20h294c11 0 19 8 19 19v434c0 11-8 19-19 19H45c-11 0-19-8-19-19V39c0-11 8-19 19-19z" diff --git a/client/src/elm/MassiveDecks/Messages.elm b/client/src/elm/MassiveDecks/Messages.elm index fe15bf8..1f27f10 100644 --- a/client/src/elm/MassiveDecks/Messages.elm +++ b/client/src/elm/MassiveDecks/Messages.elm @@ -22,9 +22,11 @@ type Msg | LobbyMsg Lobby.Msg | SettingsMsg Settings.Msg | ErrorMsg Error.Msg + | UpdateToken Lobby.Auth | CastStatusUpdate Cast.Status | TryCast Lobby.Auth | Refresh | BlockedExternalUrl | Copy String | NoOp + | Batch (List Msg) diff --git a/client/src/elm/MassiveDecks/Models/Decoders.elm b/client/src/elm/MassiveDecks/Models/Decoders.elm index 069f548..dfb56af 100644 --- a/client/src/elm/MassiveDecks/Models/Decoders.elm +++ b/client/src/elm/MassiveDecks/Models/Decoders.elm @@ -97,7 +97,12 @@ settings = (Json.maybe (Json.field "lastUsedName" Json.string)) (Json.field "recentDecks" (Json.list externalSource)) (Json.maybe (Json.field "chosenLanguage" language)) - (Json.field "compactCards" Json.bool) + (Json.field "compactCards" cardSize) + + +cardSize : Json.Decoder Settings.CardSize +cardSize = + Json.int |> Json.andThen (\i -> i |> Settings.cardSizeFromValue |> Maybe.map Json.succeed |> Maybe.withDefault (unknownValue "card size" (String.fromInt i))) gameCode : Json.Decoder GameCode @@ -463,10 +468,31 @@ eventByName name = "RoundFinished" -> gameEvent roundFinished + "HandRedrawn" -> + gameEvent handRedrawn + + "PrivilegeSet" -> + privilegeSet + _ -> unknownValue "event" name +privilegeSet : Json.Decoder Events.Event +privilegeSet = + Json.map3 (\u -> \p -> \t -> Events.PrivilegeSet { user = u, privilege = p, token = t }) + (Json.field "user" userId) + (Json.field "privilege" privilege) + (Json.maybe (Json.field "token" lobbyToken)) + + +handRedrawn : Json.Decoder Events.GameEvent +handRedrawn = + Json.map2 (\pl -> \ha -> Events.HandRedrawn { player = pl, hand = ha }) + (Json.field "player" userId) + (Json.maybe (Json.field "hand" (Json.list response))) + + roundFinished : Json.Decoder Events.GameEvent roundFinished = Json.map2 (\wi -> \pb -> Events.RoundFinished { winner = wi, playedBy = pb }) diff --git a/client/src/elm/MassiveDecks/Models/Encoders.elm b/client/src/elm/MassiveDecks/Models/Encoders.elm index 4de53dc..168701d 100644 --- a/client/src/elm/MassiveDecks/Models/Encoders.elm +++ b/client/src/elm/MassiveDecks/Models/Encoders.elm @@ -43,7 +43,7 @@ settings s = [ [ ( "tokens", s.tokens |> Dict.toList |> List.map (\( gc, t ) -> ( gc, Json.string t )) |> Json.object ) , ( "openUserList", Json.bool s.openUserList ) , ( "recentDecks", Json.list source s.recentDecks ) - , ( "compactCards", Json.bool s.compactCards ) + , ( "compactCards", s.cardSize |> cardSize ) ] , lun , cl @@ -52,6 +52,11 @@ settings s = Json.object fields +cardSize : Settings.CardSize -> Json.Value +cardSize = + Settings.cardSizeToValue >> Json.int + + lobbyCreation : Start.LobbyCreation -> Json.Value lobbyCreation c = Json.object diff --git a/client/src/elm/MassiveDecks/Pages/Lobby.elm b/client/src/elm/MassiveDecks/Pages/Lobby.elm index fa1a1c0..f8953e6 100644 --- a/client/src/elm/MassiveDecks/Pages/Lobby.elm +++ b/client/src/elm/MassiveDecks/Pages/Lobby.elm @@ -40,17 +40,21 @@ import MassiveDecks.Pages.Lobby.Invite as Invite import MassiveDecks.Pages.Lobby.Messages exposing (..) import MassiveDecks.Pages.Lobby.Model exposing (..) import MassiveDecks.Pages.Lobby.Route exposing (..) +import MassiveDecks.Pages.Lobby.Token as Token import MassiveDecks.Pages.Route as Route import MassiveDecks.Pages.Start.Route as Start import MassiveDecks.ServerConnection as ServerConnection import MassiveDecks.Settings as Settings import MassiveDecks.Settings.Messages as Settings +import MassiveDecks.Settings.Model as Settings import MassiveDecks.Strings as Strings exposing (MdString) import MassiveDecks.Strings.Languages as Lang import MassiveDecks.User as User exposing (User) import MassiveDecks.Util as Util import MassiveDecks.Util.Html as Html +import MassiveDecks.Util.Html.Attributes as HtmlA import MassiveDecks.Util.Maybe as Maybe +import MassiveDecks.Util.Result as Result import Weightless as Wl import Weightless.Attributes as WlA @@ -99,7 +103,7 @@ subscriptions model = [ model.lobby |> Maybe.andThen .game |> Maybe.map Game.subscriptions |> Maybe.withDefault Sub.none , model.notifications |> Animated.subscriptions |> Sub.map (NotificationMsg >> Global.LobbyMsg) , ServerConnection.notifications - (EventReceived >> Global.LobbyMsg) + eventPreprocess (ErrorReceived >> Global.LobbyMsg) (Error.Json >> Error.Add >> Global.ErrorMsg) ] @@ -149,7 +153,7 @@ update msg model = (\game -> Util.modelLift (\g -> { model | lobby = Just { lobby | game = Just g } }) - (Game.applyGameEvent gameEvent game) + (Game.applyGameEvent model.auth gameEvent game) ) |> Maybe.withDefault ( model, Cmd.none ) @@ -193,6 +197,16 @@ update msg model = in addNotification message { model | lobby = Just { lobby | users = newUsers } } + Events.PrivilegeSet { user, privilege } -> + let + updateUser = + \id -> \u -> { u | privilege = privilege } |> Maybe.justIf (user == id) |> Maybe.withDefault u + + users = + lobby.users |> Dict.map updateUser + in + ( { model | lobby = Just { lobby | users = users } }, Cmd.none ) + Nothing -> case event of Events.Sync { state, hand, play } -> @@ -260,21 +274,15 @@ view shared model = else Icon.users - title = - model.lobby - |> Maybe.andThen .game - |> Maybe.map (.game >> .round >> Round.data >> .call >> .body >> Parts.viewSingleLine) - notifications = model.notifications |> List.map (keyedViewNotification shared model.lobby) in [ Html.div - [ HtmlA.classList - [ ( "lobby", True ) - , ( "compact-cards", shared.settings.settings.compactCards ) - ] + [ HtmlA.id "lobby" + , HtmlA.classList [ ( "collapsed-users", not usersShown ) ] + , shared.settings.settings.cardSize |> cardSizeToAttr ] - [ Html.div [ HtmlA.id "top-bar" ] + (Html.div [ HtmlA.id "top-bar" ] [ Html.div [ HtmlA.class "left" ] (List.concat [ [ Components.iconButtonStyled @@ -282,22 +290,22 @@ view shared model = , Strings.ToggleUserList |> Lang.title shared ] ( [ Icon.lg ], usersIcon ) - , gameMenu shared + , lobbyMenu shared model.auth ] , castButton ] ) - , title |> Maybe.map (Html.div [ HtmlA.class "title-call" ]) |> Maybe.withDefault Html.nothing , Html.div [] [ Settings.view shared ] ] - , model.lobby - |> Maybe.map (viewLobby shared model.configure usersShown model.auth model.selectedUser) - |> Maybe.withDefault - (Html.div [ HtmlA.class "loading" ] - [ Icon.viewStyled [ Icon.spin, Icon.fa3x ] Icon.circleNotch ] - ) - , HtmlK.ol [ HtmlA.class "notifications" ] notifications - ] + :: HtmlK.ol [ HtmlA.class "notifications" ] notifications + :: (model.lobby + |> Maybe.map (viewLobby shared model.configure model.auth model.selectedUser) + |> Maybe.withDefault + [ Html.div [ HtmlA.class "loading" ] + [ Icon.viewStyled [ Icon.spin, Icon.fa3x ] Icon.circleNotch ] + ] + ) + ) , Invite.dialog shared model.auth.claims.gc (model.lobby |> Maybe.andThen (.config >> .password)) @@ -309,26 +317,71 @@ view shared model = {- Private -} -gameMenu : Shared -> Html Global.Msg -gameMenu shared = +cardSizeToAttr : Settings.CardSize -> Html.Attribute msg +cardSizeToAttr cardSize = + case cardSize of + Settings.Minimal -> + HtmlA.class "minimal-card-size" + + Settings.Square -> + HtmlA.class "square-card-size" + + Settings.Full -> + HtmlA.nothing + + +eventPreprocess : Events.Event -> Global.Msg +eventPreprocess event = + case event of + Events.PrivilegeSet { token } -> + Global.Batch + [ token + |> Maybe.map (Token.decode >> Result.unifiedMap (Error.Token >> Error.Add >> Global.ErrorMsg) Global.UpdateToken) + |> Maybe.withDefault Global.NoOp + , event |> EventReceived |> Global.LobbyMsg + ] + + _ -> + event |> EventReceived |> Global.LobbyMsg + + +lobbyMenu : Shared -> Auth -> Html Global.Msg +lobbyMenu shared auth = let id = - "game-menu-button" + "lobby-menu-button" - menuItems = - [ Menu.button Icon.bullhorn Strings.InvitePlayers Strings.InvitePlayers (ToggleInviteDialog |> Global.LobbyMsg |> Just) - , Menu.Separator - , Menu.button Icon.userClock Strings.SetAway Strings.SetAway Nothing + lobbyMenuItems = + [ Menu.button Icon.bullhorn Strings.InvitePlayers Strings.InvitePlayers (ToggleInviteDialog |> Global.LobbyMsg |> Just) ] + + userLobbyMenuItems = + [ Menu.button Icon.userClock Strings.SetAway Strings.SetAway Nothing , Menu.button Icon.signOutAlt Strings.LeaveGame Strings.LeaveGame Nothing - , Menu.Separator - , Menu.link Icon.info Strings.AboutTheGame Strings.AboutTheGame (Just "https://github.com/lattyware/massivedecks") + ] + + privilegedLobbyMenuItems = + [ Menu.button Icon.stopCircle Strings.EndGame Strings.EndGameDescription Nothing + ] + + mdMenuItems = + [ Menu.link Icon.info Strings.AboutTheGame Strings.AboutTheGame (Just "https://github.com/lattyware/massivedecks") , Menu.link Icon.bug Strings.ReportError Strings.ReportError (Just "https://github.com/Lattyware/massivedecks/issues/new") ] + + menuItems = + [ lobbyMenuItems |> Just + , userLobbyMenuItems |> Just + , privilegedLobbyMenuItems |> Maybe.justIf (auth.claims.pvg == User.Privileged) + , mdMenuItems |> Just + ] + + separatedMenuItems = + menuItems |> List.filterMap identity |> List.intersperse [ Menu.Separator ] |> List.concat in Html.div [] [ Components.iconButtonStyled [ HtmlA.id id, Strings.GameMenu |> Lang.title shared ] ( [ Icon.lg ], Icon.bars ) - , Menu.view shared id WlA.XCenter WlA.YBottom menuItems + , Menu.view shared id ( WlA.XCenter, WlA.YBottom ) ( WlA.XLeft, WlA.YTop ) separatedMenuItems ] @@ -447,30 +500,29 @@ username shared users id = |> Maybe.withDefault (Strings.UnknownUser |> Lang.string shared) -viewLobby : Shared -> Configure.Model -> Bool -> Auth -> Maybe User.Id -> Lobby -> Html Global.Msg -viewLobby shared configure usersShown auth selectedUser lobby = +viewLobby : Shared -> Configure.Model -> Auth -> Maybe User.Id -> Lobby -> List (Html Global.Msg) +viewLobby shared configure auth selectedUser lobby = let game = lobby.game |> Maybe.map .game privileged = auth.claims.pvg == User.Privileged - - content = - [ viewUsers shared auth.claims.uid selectedUser lobby.users game - , Html.div [ HtmlA.id "scroll-frame" ] - [ lobby.game - |> Maybe.map (Game.view shared auth lobby.config.decks lobby.users) - |> Maybe.withDefault (Configure.view shared privileged configure auth.claims.gc lobby lobby.config) - ] - ] in - Html.div [ HtmlA.classList [ ( "content-wrapper", True ), ( "collapsed-users", not usersShown ) ] ] content + [ Html.div [ HtmlA.id "lobby-content" ] + [ viewUsers shared auth.claims.uid selectedUser lobby.users game + , Html.div [ HtmlA.id "scroll-frame" ] + [ lobby.game + |> Maybe.map (Game.view shared auth lobby.config.decks lobby.users) + |> Maybe.withDefault (Configure.view shared privileged configure auth.claims.gc lobby lobby.config) + ] + ] + ] viewUsers : Shared -> User.Id -> Maybe User.Id -> Dict User.Id User -> Maybe Game -> Html Global.Msg viewUsers shared player selectedUser users game = - Wl.card [ HtmlA.class "users" ] + Wl.card [ HtmlA.id "users" ] [ Html.div [ HtmlA.class "collapsible" ] [ users |> Dict.toList @@ -533,7 +585,7 @@ viewUser shared player selectedUser game ( userId, user ) = ) , Html.span [ WlA.listItemSlot WlA.AfterItem ] score ] - , Menu.view shared id WlA.XRight WlA.YTop menuItems + , Menu.view shared id ( WlA.XRight, WlA.YCenter ) ( WlA.XLeft, WlA.YCenter ) menuItems ] ) diff --git a/client/src/elm/MassiveDecks/Pages/Lobby/Actions.elm b/client/src/elm/MassiveDecks/Pages/Lobby/Actions.elm index 2947cd9..f76976b 100644 --- a/client/src/elm/MassiveDecks/Pages/Lobby/Actions.elm +++ b/client/src/elm/MassiveDecks/Pages/Lobby/Actions.elm @@ -2,6 +2,7 @@ module MassiveDecks.Pages.Lobby.Actions exposing ( addDeck , changeHouseRule , judge + , redraw , removeDeck , reveal , setHandSize @@ -82,6 +83,11 @@ judge play = action "Judge" [ ( "winner", play |> Json.string ) ] +redraw : Cmd msg +redraw = + action "Redraw" [] + + {- Private -} diff --git a/client/src/elm/MassiveDecks/Pages/Lobby/Events.elm b/client/src/elm/MassiveDecks/Pages/Lobby/Events.elm index 4cef3cd..461f8f5 100644 --- a/client/src/elm/MassiveDecks/Pages/Lobby/Events.elm +++ b/client/src/elm/MassiveDecks/Pages/Lobby/Events.elm @@ -4,7 +4,6 @@ module MassiveDecks.Pages.Lobby.Events exposing , Event(..) , GameEvent(..) , PresenceState(..) - , Redraw(..) ) import Dict exposing (Dict) @@ -28,6 +27,7 @@ type Event -- Not a game event because we don't need to be in a game | GameStarted { round : Round.Playing, hand : List Card.Response } | Game GameEvent + | PrivilegeSet { user : User.Id, privilege : User.Privilege, token : Maybe Lobby.Token } {-| The user's intentional presence in the lobby. @@ -53,13 +53,8 @@ type DeckChange | Fail { reason : Source.LoadFailureReason } -type Redraw - = Player { hand : List Card.Response } - | Other { player : User.Id } - - type GameEvent - = HandRedrawn Redraw + = HandRedrawn { player : User.Id, hand : Maybe (List Card.Response) } | PlayRevealed { id : Play.Id, play : List Card.Response } | PlaySubmitted { by : User.Id } | PlayTakenBack { by : User.Id } diff --git a/client/src/elm/MassiveDecks/Pages/Start.elm b/client/src/elm/MassiveDecks/Pages/Start.elm index d4b0f82..8284359 100644 --- a/client/src/elm/MassiveDecks/Pages/Start.elm +++ b/client/src/elm/MassiveDecks/Pages/Start.elm @@ -21,6 +21,7 @@ import MassiveDecks.Card.Source.Model as Source import MassiveDecks.Components.Form as Form import MassiveDecks.Components.Form.Message as Message import MassiveDecks.Error as Error +import MassiveDecks.Icon as Icon import MassiveDecks.Messages as Global import MassiveDecks.Model exposing (..) import MassiveDecks.Models.MdError as MdError exposing (MdError) @@ -43,7 +44,6 @@ import MassiveDecks.Util as Util import MassiveDecks.Util.Html as Html import MassiveDecks.Util.Maybe as Maybe import MassiveDecks.Version as Version -import Reread import Svg.Attributes as SvgA import Weightless as Wl import Weightless.Attributes as WlA @@ -169,13 +169,13 @@ view shared model = , Strings.MDProject |> Lang.title shared , HtmlA.href "https://github.com/Lattyware/massivedecks" ] - [ Icon.viewStyled [ Strings.MDLogoDescription |> Lang.alt shared ] Reread.mdIcon ] + [ Icon.viewStyled [ Strings.MDLogoDescription |> Lang.alt shared ] Icon.massiveDecks ] , Html.blankA [ HtmlA.class "logo" , Strings.DevelopedByReread |> Lang.title shared , HtmlA.href "https://www.rereadgames.com/" ] - [ Icon.viewStyled [ Strings.RereadLogoDescription |> Lang.alt shared ] Reread.icon ] + [ Icon.viewStyled [ Strings.RereadLogoDescription |> Lang.alt shared ] Icon.rereadGames ] ] , Html.p [ HtmlA.class "version" ] [ Html.text "\"" diff --git a/client/src/elm/MassiveDecks/Pages/Start/LobbyBrowser/Model.elm b/client/src/elm/MassiveDecks/Pages/Start/LobbyBrowser/Model.elm index 173a854..b5d73cc 100644 --- a/client/src/elm/MassiveDecks/Pages/Start/LobbyBrowser/Model.elm +++ b/client/src/elm/MassiveDecks/Pages/Start/LobbyBrowser/Model.elm @@ -4,7 +4,6 @@ module MassiveDecks.Pages.Start.LobbyBrowser.Model exposing , UserSummary ) -import MassiveDecks.Error.Model exposing (Error) import MassiveDecks.Pages.Lobby.GameCode as GameCode exposing (GameCode) import MassiveDecks.Pages.Lobby.Model as Lobby import MassiveDecks.Requests.HttpData.Model exposing (HttpData) diff --git a/client/src/elm/MassiveDecks/Requests/Api.elm b/client/src/elm/MassiveDecks/Requests/Api.elm index 4b9675a..6aedb4b 100644 --- a/client/src/elm/MassiveDecks/Requests/Api.elm +++ b/client/src/elm/MassiveDecks/Requests/Api.elm @@ -67,7 +67,7 @@ checkAlive : (Request.Response () (Dict Lobby.Token Bool) -> msg) -> List Lobby. checkAlive msg tokens = { method = "POST" , headers = [] - , url = url [ "games", "alive" ] + , url = url [ "alive" ] , body = tokens |> Encoders.checkAlive |> Http.jsonBody , expect = Request.expectResponse msg noError Decoders.tokenValidity , timeout = Nothing diff --git a/client/src/elm/MassiveDecks/Settings.elm b/client/src/elm/MassiveDecks/Settings.elm index cea73ce..f7247b9 100644 --- a/client/src/elm/MassiveDecks/Settings.elm +++ b/client/src/elm/MassiveDecks/Settings.elm @@ -3,6 +3,7 @@ module MassiveDecks.Settings exposing , defaults , init , onJoinLobby + , onTokenUpdate , update , view ) @@ -19,6 +20,7 @@ import Http import MassiveDecks.Components as Components import MassiveDecks.Components.Form as Form import MassiveDecks.Components.Form.Message as Message +import MassiveDecks.Icon as Icon import MassiveDecks.LocalStorage as LocalStorage import MassiveDecks.Messages as Global import MassiveDecks.Model exposing (..) @@ -34,6 +36,7 @@ import MassiveDecks.Strings.Languages as Lang import MassiveDecks.Strings.Languages.Model as Lang exposing (Language) import Weightless as Wl import Weightless.Attributes as WlA +import Weightless.Slider as Slider init : Settings -> ( Model, Cmd Global.Msg ) @@ -63,7 +66,7 @@ defaults = , openUserList = False , recentDecks = [] , chosenLanguage = Nothing - , compactCards = False + , cardSize = Full } @@ -76,8 +79,8 @@ update msg model = ChangeLang language -> changeSettings (\s -> { s | chosenLanguage = language }) model - ChangeCompactCards enabled -> - changeSettings (\s -> { s | compactCards = enabled }) model + ChangeCardSize size -> + changeSettings (\s -> { s | cardSize = size }) model ChangeOpenUserList open -> changeSettings (\s -> { s | openUserList = open }) model @@ -117,7 +120,7 @@ view shared = [ Html.h3 [] [ Strings.SettingsTitle |> Lang.html shared ] , Html.div [ HtmlA.class "body" ] [ languageSelector shared - , compactSwitch shared + , cardSize shared , speechSwitch shared , notificationsSwitch shared ] @@ -144,6 +147,22 @@ onJoinLobby auth name model = ( { model | settings = updatedSettings }, LocalStorage.store updatedSettings ) +{-| Replace a token in our storage because it has been updated. +-} +onTokenUpdate : Lobby.Auth -> Model -> ( Model, Cmd msg ) +onTokenUpdate auth model = + let + settings = + model.settings + + updatedSettings = + { settings + | tokens = Dict.insert (auth.claims.gc |> GameCode.toString) auth.token settings.tokens + } + in + ( { model | settings = updatedSettings }, LocalStorage.store updatedSettings ) + + {-| Get all the tokens in settings as `Auth`s by game code. -} auths : Settings -> Dict String Lobby.Auth @@ -171,8 +190,8 @@ changeSettings f model = ( { model | settings = settings }, LocalStorage.store settings ) -compactSwitch : Shared -> Html Global.Msg -compactSwitch shared = +cardSize : Shared -> Html Global.Msg +cardSize shared = let model = shared.settings @@ -181,29 +200,60 @@ compactSwitch shared = model.settings in Form.section shared - "compact-cards" + "card-size" (Html.div [ HtmlA.class "multipart" ] - [ Wl.switch - [ HtmlA.checked settings.compactCards - , HtmlE.onCheck (ChangeCompactCards >> Global.SettingsMsg) + [ Wl.slider + [ HtmlA.class "primary" + , Slider.step 1 + , Slider.min 1 + , Slider.max 3 + , WlA.label "Card Size" + , WlA.outlined + , Slider.thumbLabel True + , String.toInt + >> Maybe.andThen cardSizeFromValue + >> Maybe.withDefault Full + >> ChangeCardSize + >> Global.SettingsMsg + |> HtmlE.onInput + , model.settings.cardSize |> cardSizeToValue |> String.fromInt |> WlA.value ] - , Html.label [] - [ Icon.view Icon.searchMinus - , Html.text " " - , Strings.CompactCardsSetting |> Lang.html shared + [ Icon.viewStyled [ Slider.slot Slider.Before ] Icon.searchMinus + , [ model.settings.cardSize |> cardSizeThumb ] |> Html.span [ Slider.slot Slider.ThumbLabel ] + , Icon.viewStyled [ Slider.slot Slider.After ] Icon.searchPlus ] ] + -- [ Wl.switch + -- [ HtmlA.checked settings.compactCards + -- , HtmlE.onCheck (ChangeCompactCards >> Global.SettingsMsg) + -- ] + -- , Html.label [] + -- [ Icon.view Icon.searchMinus + -- , Html.text " " + -- , Strings.CompactCardsSetting |> Lang.html shared + -- ] + -- ] ) - [ Message.info Strings.CompactCardsExplanation ] + [ Message.info Strings.CardSizeExplanation ] +cardSizeThumb : CardSize -> Html msg +cardSizeThumb size = + case size of + Minimal -> + Icon.view Icon.minimalCardSize --- TODO: Impl + Square -> + Icon.view Icon.squareCardSize + + Full -> + Icon.view Icon.callCard speechSwitch : Shared -> Html Global.Msg speechSwitch shared = + -- TODO: Impl Form.section shared "speech" (Html.div @@ -219,12 +269,9 @@ speechSwitch shared = [ Message.info Strings.SpeechExplanation ] - --- TODO: Impl - - notificationsSwitch : Shared -> Html Global.Msg notificationsSwitch shared = + -- TODO: Impl Form.section shared "notifications" (Html.div diff --git a/client/src/elm/MassiveDecks/Settings/Messages.elm b/client/src/elm/MassiveDecks/Settings/Messages.elm index 184db73..847155e 100644 --- a/client/src/elm/MassiveDecks/Settings/Messages.elm +++ b/client/src/elm/MassiveDecks/Settings/Messages.elm @@ -2,12 +2,13 @@ module MassiveDecks.Settings.Messages exposing (Msg(..)) import Dict exposing (Dict) import MassiveDecks.Pages.Lobby.Model as Lobby +import MassiveDecks.Settings.Model exposing (..) import MassiveDecks.Strings.Languages.Model exposing (Language) type Msg = ChangeLang (Maybe Language) - | ChangeCompactCards Bool + | ChangeCardSize CardSize | ChangeOpenUserList Bool | ToggleOpen | RemoveInvalid (Dict Lobby.Token Bool) diff --git a/client/src/elm/MassiveDecks/Settings/Model.elm b/client/src/elm/MassiveDecks/Settings/Model.elm index 5159590..d4aa159 100644 --- a/client/src/elm/MassiveDecks/Settings/Model.elm +++ b/client/src/elm/MassiveDecks/Settings/Model.elm @@ -1,4 +1,11 @@ -module MassiveDecks.Settings.Model exposing (Model, Settings) +module MassiveDecks.Settings.Model exposing + ( CardSize(..) + , Model + , Settings + , cardSizeFromValue + , cardSizeToValue + , cardSizes + ) import Dict exposing (Dict) import MassiveDecks.Card.Source.Model as Source exposing (Source) @@ -23,5 +30,45 @@ type alias Settings = , lastUsedName : Maybe String , recentDecks : List Source.External , chosenLanguage : Maybe Language - , compactCards : Bool + , cardSize : CardSize } + + +type CardSize + = Minimal + | Square + | Full + + +cardSizes : List CardSize +cardSizes = + [ Minimal, Square, Full ] + + +cardSizeToValue : CardSize -> Int +cardSizeToValue size = + case size of + Minimal -> + 1 + + Square -> + 2 + + Full -> + 3 + + +cardSizeFromValue : Int -> Maybe CardSize +cardSizeFromValue size = + case size of + 1 -> + Just Minimal + + 2 -> + Just Square + + 3 -> + Just Full + + _ -> + Nothing diff --git a/client/src/elm/MassiveDecks/Strings.elm b/client/src/elm/MassiveDecks/Strings.elm index 07b05fe..003d9bc 100644 --- a/client/src/elm/MassiveDecks/Strings.elm +++ b/client/src/elm/MassiveDecks/Strings.elm @@ -68,8 +68,8 @@ type MdString | LanguageSetting -- The label for the "Language" setting. | MissingLanguage -- A question asking if the user doesn't see the language they want. | TranslationBeg -- A request for help translating the game. - | CompactCardsSetting -- The label for the "Compact Cards" setting. - | CompactCardsExplanation -- An explanation of what compact cards does (makes cards square). + | CardSizeSetting -- The label for the "Card Size" setting. + | CardSizeExplanation -- An explanation of what the card size does (changes the size of the card). | SpeechSetting -- The label for the speech setting. | SpeechExplanation -- An explanation of what the speech setting does (enables TTS on cards). | NotificationsSetting -- The label for the notifications setting. @@ -133,6 +133,8 @@ type MdString | InvitePlayers -- A short term for inviting players to the game. | SetAway -- A short term for leaving the game temporarily. | LeaveGame -- A short term for the action of leaving the game permanently. + | EndGame -- A short term for the action of ending the game early. + | EndGameDescription -- A description of the action of ending the game early. | KickUser -- A short term for the action of forcing a user to leave the game permanently. | Promote -- A short term for the action of allowing a user to edit the game configuration. -- Notifications @@ -173,17 +175,20 @@ type MdString | TakeBackPlay -- A description of the action of taking back a previously submitted play. | JudgePlay -- A description of the action of choosing a play to win the round. | LikePlay -- A description of the action of liking a play. + | AdvanceRound -- A description of the action of finishing looking at the winner and advancing to the next round. | Playing -- A description of the stage of the round where players are playing responses into the round. | Revealing -- A description of the stage of the round where the czar is revealing the plays. | Judging -- A description of the stage of the round where the czar is picking a winner. | Complete -- A description of the stage of the round where it is finished. -- Instructions + | WhatToDo -- A description of the action of asking for help on what to do in the game at this time. | PlayInstruction { numberOfCards : Int } -- Instruction to the player on how to play cards. | SubmitInstruction -- Instruction to the player on how to submit their play. | WaitingForPlaysInstruction -- Instruction to the player that they need to wait for other players to play. | CzarsDontPlayInstruction -- Instruction to the player that as Czar they don't play into the round. | RevealPlaysInstruction -- Instruction to reveal plays for the round. | WaitingForCzarInstruction -- Instruction to wait for the czar to reveal plays and pick a winner. + | AdvanceRoundInstruction -- Instruction that the next round is ready and they can advance. -- 404 Unknown | UnknownPageTitle -- A title explaining the page the user tried to go to doesn't exist. | GoBackHome -- The action to go back to the main page of the application. diff --git a/client/src/elm/MassiveDecks/Strings/Languages/En.elm b/client/src/elm/MassiveDecks/Strings/Languages/En.elm index ae38fbf..1b09292 100644 --- a/client/src/elm/MassiveDecks/Strings/Languages/En.elm +++ b/client/src/elm/MassiveDecks/Strings/Languages/En.elm @@ -250,11 +250,11 @@ translate mdString = , Text "!" ] - CompactCardsSetting -> + CardSizeSetting -> [ Text "Compact Cards" ] - CompactCardsExplanation -> - [ Text "Show cards as square—this is useful on small screens to fit more cards on at once." ] + CardSizeExplanation -> + [ Text "Adjust how big cards are—this can be useful on small screens to scroll less." ] SpeechSetting -> [ Text "Text To Speech" ] @@ -475,6 +475,12 @@ translate mdString = LeaveGame -> [ Text "Leave Game" ] + EndGame -> + [ Text "End Game" ] + + EndGameDescription -> + [ Text "End the game early." ] + KickUser -> [ Text "Kick" ] @@ -609,6 +615,9 @@ translate mdString = LikePlay -> [ Text "Add a like to this play." ] + AdvanceRound -> + [ Text "Next round." ] + Playing -> [ Text "Playing" ] @@ -622,6 +631,9 @@ translate mdString = [ Text "Finished" ] -- Instructions + WhatToDo -> + [ Text "What should I be doing?" ] + PlayInstruction { numberOfCards } -> [ Text "You need to choose " , Text (asWord numberOfCards) @@ -648,7 +660,10 @@ translate mdString = [ Text "Click on the plays to flip them, then pick the one you think is best." ] WaitingForCzarInstruction -> - [ Text "You ware waiting for the ", Ref Czar, Text " to reveal the plays and pick a winner for the round." ] + [ Text "You can like plays while you wait for the ", Ref Czar, Text " to reveal the plays and pick a winner for the round." ] + + AdvanceRoundInstruction -> + [ Text "The next round has started, you can advance." ] -- 404 Unknown UnknownPageTitle -> diff --git a/client/src/elm/MassiveDecks/Strings/Render.elm b/client/src/elm/MassiveDecks/Strings/Render.elm index ec919a0..49e2aa4 100644 --- a/client/src/elm/MassiveDecks/Strings/Render.elm +++ b/client/src/elm/MassiveDecks/Strings/Render.elm @@ -154,7 +154,7 @@ enhanceHtml context mdString unenhanced = prefixed unenhanced Icon.parachuteBox HouseRuleReboot -> - prefixed unenhanced Icon.redo + prefixed unenhanced Icon.random HouseRuleRandoCardrissian -> prefixed unenhanced Icon.robot diff --git a/client/src/elm/MassiveDecks/Util.elm b/client/src/elm/MassiveDecks/Util.elm index fa34e25..fc8095d 100644 --- a/client/src/elm/MassiveDecks/Util.elm +++ b/client/src/elm/MassiveDecks/Util.elm @@ -1,5 +1,6 @@ module MassiveDecks.Util exposing - ( lift + ( batchUpdate + , lift , messageLift , modelLift ) @@ -27,3 +28,27 @@ messageLift liftMsg ( model, subCmd ) = modelLift : (subModel -> model) -> ( subModel, Cmd msg ) -> ( model, Cmd msg ) modelLift liftModel ( subModel, cmd ) = ( liftModel subModel, cmd ) + + +{-| Perform the given update task for each of the messages in a row. +-} +batchUpdate : List msg -> model -> (msg -> model -> ( model, Cmd cmdMsg )) -> ( model, Cmd cmdMsg ) +batchUpdate messages model update = + let + ( newModel, cmdList ) = + messages |> List.foldl (batchUpdateStep update) ( model, [] ) + in + ( newModel, cmdList |> List.reverse |> Cmd.batch ) + + + +{- Private -} + + +batchUpdateStep : (msg -> model -> ( model, Cmd cmdMsg )) -> msg -> ( model, List (Cmd cmdMsg) ) -> ( model, List (Cmd cmdMsg) ) +batchUpdateStep update msg ( model, cmdList ) = + let + ( newModel, newCmd ) = + update msg model + in + ( newModel, newCmd :: cmdList ) diff --git a/client/src/elm/Reread.elm b/client/src/elm/Reread.elm deleted file mode 100644 index 88ac08c..0000000 --- a/client/src/elm/Reread.elm +++ /dev/null @@ -1,13 +0,0 @@ -module Reread exposing (icon, mdIcon) - -import FontAwesome.Icon as Icon exposing (Icon) - - -icon : Icon -icon = - Icon "fab" "reread" 512 512 "M258 16c-62 0-124 24-171 71-95 94-94 246 0 339 95 93 250 93 345 0l12-12-15-14-12 12-10-10c-82 80-213 80-295 0-82-81-82-210-1-291 81-80 212-81 294-3l-38 35 129 25-1-3-26-117-39 36c-48-46-110-68-172-68zm-79 159c-46 1-70 10-70 10v133s24-9 70-9c50 0 70 20 70 20V196s-20-21-70-21zm160 0c-50 0-70 21-70 21v133s20-20 70-20c46 0 70 9 70 9V185s-24-9-70-10zM179 323c-46 1-70 10-70 10v16s24-9 70-9c50 0 81 31 81 31s30-31 80-31c46 0 70 9 70 9v-16s-24-9-70-10c-50 0-80 32-80 32s-31-32-81-32z" - - -mdIcon : Icon -mdIcon = - Icon "fab" "massivedecks" 512 512 "M273 20c-11 0-21 9-23 21l-9 88h-8L39 163c-12 2-21 15-19 27l50 283c2 12 15 21 27 19l194-34c12-3 21-15 18-27l-13-73 140 14c13 2 25-8 26-20l30-286c1-13-8-24-21-25L276 20h-3zm0 16h1l196 21c4 0 6 3 6 7l-30 286c0 4-4 7-8 6l-144-15-35-193-3-9 10-97c1-3 4-6 7-6zm47 106l-10 99 39 4c15 2 25 0 32-8 9-8 15-21 16-37 2-15-1-29-8-39-6-9-15-13-30-15zm19 20l18 1c16 2 22 14 20 35-3 22-11 32-27 31l-18-2zm-135 91l17 98-20 4-13-77-4 80-20 3-32-73 14 76-20 4-18-98 31-6 31 75 4-81z" diff --git a/client/src/elm/Weightless.elm b/client/src/elm/Weightless.elm index a35f329..789212a 100644 --- a/client/src/elm/Weightless.elm +++ b/client/src/elm/Weightless.elm @@ -7,6 +7,7 @@ module Weightless exposing , popover , popoverCard , select + , slider , switch , tab , tabGroup @@ -39,29 +40,9 @@ listItem = Html.node "wl-list-item" -tabGroup : List (Html.Attribute msg) -> List (Html msg) -> Html msg -tabGroup = - Html.node "wl-tab-group" - - -tab : List (Html.Attribute msg) -> List (Html msg) -> Html msg -tab = - Html.node "wl-tab" - - -switch : List (Html.Attribute msg) -> Html msg -switch attrs = - Html.node "wl-switch" attrs [] - - -textField : List (Html.Attribute msg) -> List (Html msg) -> Html msg -textField = - Html.node "wl-textfield" - - -textArea : List (Html.Attribute msg) -> List (Html msg) -> Html msg -textArea = - Html.node "wl-textarea" +navigationBar : List (Html.Attribute msg) -> List (Html msg) -> Html msg +navigationBar = + Html.node "wl-nav" popover : List (Html.Attribute msg) -> List (Html msg) -> Html msg @@ -74,16 +55,41 @@ popoverCard = Html.node "wl-popover-card" -navigationBar : List (Html.Attribute msg) -> List (Html msg) -> Html msg -navigationBar = - Html.node "wl-nav" - - select : List (Html.Attribute msg) -> List (Html msg) -> Html msg select = Html.node "wl-select" +slider : List (Html.Attribute msg) -> List (Html msg) -> Html msg +slider = + Html.node "wl-slider" + + +switch : List (Html.Attribute msg) -> Html msg +switch attrs = + Html.node "wl-switch" attrs [] + + +tab : List (Html.Attribute msg) -> List (Html msg) -> Html msg +tab = + Html.node "wl-tab" + + +tabGroup : List (Html.Attribute msg) -> List (Html msg) -> Html msg +tabGroup = + Html.node "wl-tab-group" + + +textArea : List (Html.Attribute msg) -> List (Html msg) -> Html msg +textArea = + Html.node "wl-textarea" + + +textField : List (Html.Attribute msg) -> List (Html msg) -> Html msg +textField = + Html.node "wl-textfield" + + tooltip : List (Html.Attribute msg) -> List (Html msg) -> Html msg tooltip = Html.node "wl-tooltip" diff --git a/client/src/elm/Weightless/Attributes.elm b/client/src/elm/Weightless/Attributes.elm index ca52ff2..ad97cc1 100644 --- a/client/src/elm/Weightless/Attributes.elm +++ b/client/src/elm/Weightless/Attributes.elm @@ -37,6 +37,7 @@ module Weightless.Attributes exposing , outlined , readonly , selected + , step , textFieldSlot , transformOrigin , type_ @@ -49,6 +50,11 @@ import Html.Attributes as HtmlA import Json.Encode as Json +step : Int -> Html.Attribute msg +step = + numberAttr "step" + + min : Int -> Html.Attribute msg min = numberAttr "min" diff --git a/client/src/elm/Weightless/Internal.elm b/client/src/elm/Weightless/Internal.elm new file mode 100644 index 0000000..ac90105 --- /dev/null +++ b/client/src/elm/Weightless/Internal.elm @@ -0,0 +1,42 @@ +module Weightless.Internal exposing + ( boolProp + , numberAttr + , numberProp + , presentAttribute + , stringArrayProperty + , stringAttr + ) + +import Html +import Html.Attributes as HtmlA +import Json.Encode as Json + + +presentAttribute : String -> Html.Attribute msg +presentAttribute attribute = + HtmlA.attribute attribute attribute + + +stringArrayProperty : String -> List String -> Html.Attribute msg +stringArrayProperty property values = + HtmlA.property property (values |> Json.list Json.string) + + +stringAttr : String -> String -> Html.Attribute msg +stringAttr attribute stringValue = + HtmlA.attribute attribute stringValue + + +numberAttr : String -> Int -> Html.Attribute msg +numberAttr attribute intValue = + intValue |> String.fromInt |> HtmlA.attribute attribute + + +numberProp : String -> Int -> Html.Attribute msg +numberProp property intValue = + intValue |> Json.int |> HtmlA.property property + + +boolProp : String -> Bool -> Html.Attribute msg +boolProp property boolValue = + boolValue |> Json.bool |> HtmlA.property property diff --git a/client/src/elm/Weightless/Slider.elm b/client/src/elm/Weightless/Slider.elm new file mode 100644 index 0000000..23d6dce --- /dev/null +++ b/client/src/elm/Weightless/Slider.elm @@ -0,0 +1,54 @@ +module Weightless.Slider exposing + ( Slot(..) + , max + , min + , slot + , step + , thumbLabel + ) + +import Html +import Weightless.Internal as Internal + + +type Slot + = Before + | After + | ThumbLabel + + +slot : Slot -> Html.Attribute msg +slot s = + let + textSlot = + case s of + Before -> + "before" + + After -> + "after" + + ThumbLabel -> + "thumb-label" + in + Internal.stringAttr "slot" textSlot + + +step : Int -> Html.Attribute msg +step = + Internal.numberProp "step" + + +min : Int -> Html.Attribute msg +min = + Internal.numberProp "min" + + +max : Int -> Html.Attribute msg +max = + Internal.numberProp "max" + + +thumbLabel : Bool -> Html.Attribute msg +thumbLabel = + Internal.boolProp "thumbLabel" diff --git a/client/src/scss/card/size.scss b/client/src/scss/card/size.scss new file mode 100644 index 0000000..e27466a --- /dev/null +++ b/client/src/scss/card/size.scss @@ -0,0 +1,98 @@ +@import "./variables"; + +.minimal-card-size #game { + --card-aspect-ratio: 0; + + ul.cards { + flex-direction: column; + align-items: stretch; + } + + ul.card-set { + flex-direction: row; + align-items: stretch; + + li { + flex-grow: 1; + } + } + + :not(.background-plays) .game-card { + flex-grow: 1; + flex-shrink: 1; + width: auto; + + .content { + p { + justify-content: center; + text-align: center; + } + } + + .info { + display: none; + } + + .aspect { + position: static; + height: auto; + width: auto; + padding: 0; + + transition: none; + + & > * { + position: static; + width: auto; + height: auto; + } + } + + .side.back { + display: none; + + .content { + text-align: center; + + p { + display: none; + } + + &::after { + content: "?"; + } + } + } + + &.face-down { + .side { + &.back { + display: block; + } + + &.front { + display: none; + } + } + } + } + + .round { + justify-content: stretch; + align-items: stretch; + } + + .liked { + .side::after { + right: 1em; + top: 50%; + margin-top: -1.5em; + width: 3em; + height: 3em; + } + } +} + +.square-card-size #game { + --card-aspect-ratio: #{$card-compact-ratio}; +} diff --git a/client/src/scss/cards.scss b/client/src/scss/cards.scss index ba8a6a1..3aed914 100644 --- a/client/src/scss/cards.scss +++ b/client/src/scss/cards.scss @@ -2,12 +2,9 @@ @import "./colors"; @import "./fonts"; @import "./card/sources"; +@import "./card/size"; -.compact-cards .card-area { - --card-aspect-ratio: #{$card-compact-ratio}; -} - -.card-area { +#game { --card-aspect-ratio: #{$card-ratio}; @include fluid-type(20rem, 64rem, 0.5rem, 0.8rem); } @@ -59,12 +56,14 @@ height: 1em; flex-grow: 1; flex-basis: 2em; + max-width: 14em; border-bottom: 0.1em solid var(--fg); } } } } + // Creates a fixed size from an aspect ratio. .aspect { transition: transform 0.75s; transform-style: preserve-3d; @@ -73,7 +72,7 @@ width: 100%; @include pad-to-aspect-ratio(var(--card-aspect-ratio)); - & > * { + & .side { position: absolute; top: 0; left: 0; @@ -129,6 +128,7 @@ line-height: 1.4em; } + // For custom cards. textarea { display: block; vertical-align: top; @@ -179,16 +179,4 @@ } } } - - &.error .side { - border-color: $error; - } -} - -.picked { - position: relative; - - .side { - border-color: $primary; - } } diff --git a/client/src/scss/game.scss b/client/src/scss/game.scss index 8d10dac..f9b15ae 100644 --- a/client/src/scss/game.scss +++ b/client/src/scss/game.scss @@ -1,81 +1,23 @@ -@import "card/variables"; -@import "colors"; +@import "./card/variables"; +@import "./colors"; @import "~weightless/style/base/elevation"; +@import "./game/judging"; +@import "./game/playing"; +@import "./game/plays"; +@import "./pages/lobby/variables"; +// The main game area. #game { - height: 100%; position: relative; display: flex; flex-direction: column; align-items: stretch; - .card-area { - position: relative; - } - - .context-help { - position: fixed; - top: 4em; - right: 0; - z-index: 1; - - background-color: $secondary-background; - padding: 0.5em; - border-bottom-left-radius: 1em; - - display: flex; - align-items: center; - align-self: stretch; - - transition: transform 200ms ease-out; - transform: translate(calc(100% - 2em), 0); - - &:hover, - &:active { - transform: translate(0, 0); - transition: transform 200ms ease-in; - } - - & > svg { - margin-right: 0.5em; - } - } - - .scroll-top { - pointer-events: none; - display: flex; - flex-direction: column; - justify-content: flex-end; - - position: absolute; - left: 0; - top: 0; - bottom: 0; - - wl-button { - pointer-events: all; - position: sticky; - bottom: 0.5em; - margin: 0.5em; - z-index: 1; - - opacity: 0.5; - - &:hover { - opacity: 1; - } - } - } - - // This pushes the scroll-top button down below the page content. - .scroll-top-spacer { - height: 6em; - } - + // A button that allows the user to perform the main action in the game for them at this moment. .action { position: fixed; - // Hack because the debugger covers it up. + // TODO: Remove Hack (here because the debugger covers it up). bottom: 5rem; //bottom: 1em; right: 1rem; @@ -84,179 +26,105 @@ --button-fab-size: 4rem; font-size: 1.5rem; - } - // A style for actions that the game is waiting on - high contrast to grab attention. - .important { - --button-bg: #{$secondary}; - --button-color: #{$on-secondary}; - --button-bg-hover: #{$secondary-light}; - --button-color-hover: #{$on-secondary-light}; - --button-bg-active: #{$secondary-dark}; - --button-color-acitve: #{$on-secondary-dark}; + // A style for actions that the game is waiting on - high contrast to grab attention. + &.important { + --button-bg: #{$secondary}; + --button-color: #{$on-secondary}; + --button-bg-hover: #{$secondary-light}; + --button-color-hover: #{$on-secondary-light}; + --button-bg-active: #{$secondary-dark}; + --button-color-acitve: #{$on-secondary-dark}; + } } } -.play-area { - overflow: hidden; +// Contextual help for the player telling them what they need to do at this point in the game. +#context-help { + font-size: 1.2rem; + position: fixed; + top: $top-bar-height; + right: 0; + z-index: 1; + --card-padding: 0.5em; + + #context-help-button { + font-size: 1.4rem; + } + + wl-popover-card { + min-width: 10em; + } +} + +// The button to scroll to the top of the game area. +.scroll-top { + pointer-events: none; + display: flex; + flex-direction: column; + justify-content: flex-end; + + position: absolute; + left: 0; + top: 0; + bottom: 0; + + wl-button { + pointer-events: all; + position: sticky; + bottom: 0.5em; + margin: 0.5em; + z-index: 1; + + opacity: 0.5; + + &:hover { + opacity: 1; + } + } +} + +// This pushes the scroll-top button down below the page content. +.scroll-top-spacer { + height: 6em; +} + +// Where the call for the round is displayed. +.round { display: flex; flex-direction: column; justify-content: center; - align-items: stretch; - flex-basis: $card-width; + align-items: center; min-width: $card-width; - padding: 2em 0; + padding: 1em 0; +} - .cards-in-play { - display: flex; - justify-content: center; +// A play or card that has been picked by the player as the target of some action. +.game-card, +ol.card-set { + &.picked { + .side { + border-color: $primary; + } } } -.hand { - margin-top: 0.5em; - +// Display some number of cards or card sets together. +ul.cards { display: flex; flex-direction: row; flex-wrap: wrap; justify-content: center; - position: relative; - - &.not-playing { - pointer-events: none; - - .game-card .aspect::after { - content: ""; - display: block; - position: absolute; - top: 0; - left: 0; - right: 0; - bottom: 0; - border-radius: 0.75em; - background-color: #000000; - opacity: 0.1; - } - } -} - -.background-plays { - position: absolute; - top: 0; - left: 0; - right: 0; - z-index: -10; - - display: flex; - flex-direction: row; - flex-wrap: nowrap; - justify-content: space-evenly; - - .play { - position: relative; - transform: translateY(-100%); - transition: transform 500ms ease-in; - width: $card-width + $card-margin * 2; - @include pad-to-aspect-ratio(var(--card-aspect-ratio)); - - .game-card { - transform: rotate(0); - transition: transform 500ms ease-in; - transform-origin: center; - - position: absolute; - top: 0; - right: 0; - left: 0; - } - - &.played { - transform: translateY(0); - transition: transform 500ms ease-out; - - .game-card { - transform: rotate(var(--rotation)); - transition: transform 500ms ease-out; - } - } - } -} - -.plays { list-style: none; padding: 0; - margin-top: 0.5em; +} +// Multiple cards that are grouped together. +ol.card-set { + margin: 0.5em; display: flex; - flex-direction: row; - flex-wrap: wrap; - justify-content: center; - - position: relative; - - .play-with-byline { - display: flex; - flex-direction: column; - align-items: center; - } - - .byline { - font-size: 2em; - text-align: center; - - svg { - margin-right: 0.5em; - } - } - - .play { - display: flex; - margin: 1em; - - .overlap { - flex-shrink: 1; - flex-grow: 0; - flex-basis: $card-width; - overflow: visible; - z-index: 0; - - &:last-of-type { - width: $card-width; - } - - &:hover { - z-index: 1; - } - - .game-card { - margin: 0 $card-margin; - } - } - } -} - -.liked { - .side::after { - position: absolute; - bottom: 1em; - right: 1em; - width: 8em; - height: 8em; - transform: rotate(-0.125turn); - - display: block; - content: ""; - opacity: 0.3; - //noinspection CssInvalidFunction - background: inline-svg( - "./assets/images/certificate-thumbs-up.svg", - ( - path: ( - fill: $primary - ) - ) - ); - } + list-style: none; + padding: 0; } diff --git a/client/src/scss/game/judging.scss b/client/src/scss/game/judging.scss new file mode 100644 index 0000000..7023bec --- /dev/null +++ b/client/src/scss/game/judging.scss @@ -0,0 +1,25 @@ +@import "../colors"; + +.liked { + .side::after { + position: absolute; + bottom: 1em; + right: 1em; + width: 8em; + height: 8em; + transform: rotate(-0.125turn); + + display: block; + content: ""; + opacity: 0.3; + //noinspection CssInvalidFunction + background: inline-svg( + "./assets/images/certificate-thumbs-up.svg", + ( + path: ( + fill: $primary + ) + ) + ); + } +} diff --git a/client/src/scss/game/playing.scss b/client/src/scss/game/playing.scss new file mode 100644 index 0000000..10f143f --- /dev/null +++ b/client/src/scss/game/playing.scss @@ -0,0 +1,71 @@ +@import "../card/variables"; + +// A hand of cards. +.hand { + margin-top: 0.5em; + + position: relative; + + &.not-playing { + pointer-events: none; + + .game-card .aspect::after { + content: ""; + display: block; + position: absolute; + top: 0; + left: 0; + right: 0; + bottom: 0; + border-radius: 0.75em; + background-color: #000000; + opacity: 0.1; + } + } +} + +// Unknown plays for the round animated in at the top of the screen during the playing stage. +.background-plays { + --card-aspect-ratio: #{$card-ratio}; + + position: fixed; + top: 0; + left: 0; + right: 0; + bottom: 0; + z-index: -10; + + display: flex; + flex-direction: row; + flex-wrap: nowrap; + justify-content: space-evenly; + + .play { + position: relative; + transform: translateY(-100%); + transition: transform 500ms ease-in; + width: $card-width + $card-margin * 2; + @include pad-to-aspect-ratio(var(--card-aspect-ratio)); + + .game-card { + transform: rotate(0); + transition: transform 500ms ease-in; + transform-origin: center; + + position: absolute; + top: 0; + right: 0; + left: 0; + } + + &.played { + transform: translateY(0); + transition: transform 500ms ease-out; + + .game-card { + transform: rotate(var(--rotation)); + transition: transform 500ms ease-out; + } + } + } +} diff --git a/client/src/scss/game/plays.scss b/client/src/scss/game/plays.scss new file mode 100644 index 0000000..1f40b72 --- /dev/null +++ b/client/src/scss/game/plays.scss @@ -0,0 +1,16 @@ +// A play with the user's name. +.play-with-byline { + display: flex; + flex-direction: column; + align-items: center; +} + +// The user's name. +.byline { + font-size: 2em; + text-align: center; + + svg { + margin-right: 0.5em; + } +} diff --git a/client/src/scss/pages/lobby.scss b/client/src/scss/pages/lobby.scss index 6341c89..b5f0624 100644 --- a/client/src/scss/pages/lobby.scss +++ b/client/src/scss/pages/lobby.scss @@ -3,42 +3,27 @@ @import "../game"; @import "./lobby/configure"; @import "./lobby/invite"; +@import "./lobby/variables"; @import "~weightless/style/base/elevation"; -$users-width: 15em; - @keyframes fadeIn { from { opacity: 0.1; } } -.lobby { - box-sizing: border-box; - height: 100vh; - padding-top: 4em; - overflow: hidden; - - .content-wrapper { - display: flex; - flex-direction: row; - flex-wrap: wrap; - min-width: fit-content; - overflow: hidden; - height: 100%; - width: 100%; - } +#lobby { } #top-bar { - position: fixed; + position: absolute; top: 0; left: 0; right: 0; z-index: 10; - height: 4em; + height: $top-bar-height; padding: 0 1em; display: flex; @@ -55,57 +40,30 @@ $users-width: 15em; flex-direction: row; flex-wrap: nowrap; } +} - .title-call { - flex-grow: 1; - flex-shrink: 1; - overflow: hidden; - - margin: 0 1em; - - white-space: pre; - font-size: 1em; - text-rendering: optimizeLegibility; - font-family: "Helvetica Neue", "Nimbus Sans L", sans-serif; - font-weight: 700; - - display: flex; - justify-content: center; - - .slot { - display: inline-block; - flex-shrink: 0; - flex-basis: 3em; - border-bottom: 0.1em solid $call-color; - } - - &::before { - content: "“"; - } - - &::after { - content: "”"; - } - } +#lobby-content { + position: absolute; + left: 0; + top: $top-bar-height; + right: 0; + bottom: 0; + box-sizing: border-box; + min-width: fit-content; } #scroll-frame { - flex-grow: 100; - flex-shrink: 1; - flex-basis: $card-width; - max-height: 100%; - max-width: 100%; - min-width: $card-width; - + position: absolute; + top: 0; + left: $users-width; + right: 0; + bottom: 0; overflow: auto; + transition: left 0.3s; } -.outlined-solid-icon { - path { - fill: none; - stroke: black; - stroke-width: 32px; - } +.collapsed-users #scroll-frame { + left: 0; } .loading { @@ -119,23 +77,9 @@ $users-width: 15em; justify-content: center; } -.collapsed-users .users { - flex-grow: 0; - flex-basis: 0; -} - -.context-help { - left: $users-width; -} - -.collapsed-users .context-help { - left: 0; -} - -.users { +#users { font-size: 1rem; - overflow-y: auto; - overflow-x: hidden; + overflow: auto; z-index: 2; @@ -143,16 +87,15 @@ $users-width: 15em; padding: 0; - flex-grow: 1; - flex-shrink: 1; - min-width: 0; - min-height: 100%; - flex-basis: $users-width; + position: absolute; + left: 0; + top: 0; + bottom: 0; + width: $users-width; - transition: flex-basis 0.3s, flex-grow 0.3s, padding-left 0.3s, - padding-right 0.3s; + transition: width 0.3s; --list-item-padding: 0.5em; @@ -180,6 +123,20 @@ $users-width: 15em; } } +@media screen and (max-width: $users-width * 2) { + #scroll-frame { + left: 100vw; + } + + #users { + width: 100vw; + } +} + +.collapsed-users #users { + width: 0; +} + .cast-button { &.connecting { animation: fadeIn 1s infinite alternate; diff --git a/client/src/scss/pages/lobby/configure.scss b/client/src/scss/pages/lobby/configure.scss index 72bd39f..baf5db1 100644 --- a/client/src/scss/pages/lobby/configure.scss +++ b/client/src/scss/pages/lobby/configure.scss @@ -4,7 +4,6 @@ .configure { padding: 1em; height: 100%; - overflow: auto; wl-card { max-width: 30em; diff --git a/client/src/scss/pages/lobby/variables.scss b/client/src/scss/pages/lobby/variables.scss new file mode 100644 index 0000000..ed472e9 --- /dev/null +++ b/client/src/scss/pages/lobby/variables.scss @@ -0,0 +1,2 @@ +$top-bar-height: 4rem; +$users-width: 15rem; diff --git a/client/src/ts/weightless.ts b/client/src/ts/weightless.ts index 9b7fe98..c32df5a 100644 --- a/client/src/ts/weightless.ts +++ b/client/src/ts/weightless.ts @@ -7,6 +7,7 @@ import "weightless/nav"; import "weightless/popover"; import "weightless/popover-card"; import "weightless/select"; +import "weightless/slider"; import "weightless/switch"; import "weightless/tab"; import "weightless/tab-group"; diff --git a/server/package-lock.json b/server/package-lock.json index 0757e2a..83a5ea6 100644 --- a/server/package-lock.json +++ b/server/package-lock.json @@ -262,12 +262,12 @@ "dev": true }, "@typescript-eslint/eslint-plugin": { - "version": "1.10.2", - "resolved": "https://registry.npmjs.org/@typescript-eslint/eslint-plugin/-/eslint-plugin-1.10.2.tgz", - "integrity": "sha512-7449RhjE1oLFIy5E/5rT4wG5+KsfPzakJuhvpzXJ3C46lq7xywY0/Rjo9ZBcwrfbk0nRZ5xmUHkk7DZ67tSBKw==", + "version": "1.11.0", + "resolved": "https://registry.npmjs.org/@typescript-eslint/eslint-plugin/-/eslint-plugin-1.11.0.tgz", + "integrity": "sha512-mXv9ccCou89C8/4avKHuPB2WkSZyY/XcTQUXd5LFZAcLw1I3mWYVjUu6eS9Ja0QkP/ClolbcW9tb3Ov/pMdcqw==", "dev": true, "requires": { - "@typescript-eslint/experimental-utils": "1.10.2", + "@typescript-eslint/experimental-utils": "1.11.0", "eslint-utils": "^1.3.1", "functional-red-black-tree": "^1.0.1", "regexpp": "^2.0.1", @@ -275,31 +275,31 @@ } }, "@typescript-eslint/experimental-utils": { - "version": "1.10.2", - "resolved": "https://registry.npmjs.org/@typescript-eslint/experimental-utils/-/experimental-utils-1.10.2.tgz", - "integrity": "sha512-Hf5lYcrnTH5Oc67SRrQUA7KuHErMvCf5RlZsyxXPIT6AXa8fKTyfFO6vaEnUmlz48RpbxO4f0fY3QtWkuHZNjg==", + "version": "1.11.0", + "resolved": "https://registry.npmjs.org/@typescript-eslint/experimental-utils/-/experimental-utils-1.11.0.tgz", + "integrity": "sha512-7LbfaqF6B8oa8cp/315zxKk8FFzosRzzhF8Kn/ZRsRsnpm7Qcu25cR/9RnAQo5utZ2KIWVgaALr+ZmcbG47ruw==", "dev": true, "requires": { - "@typescript-eslint/typescript-estree": "1.10.2", + "@typescript-eslint/typescript-estree": "1.11.0", "eslint-scope": "^4.0.0" } }, "@typescript-eslint/parser": { - "version": "1.10.2", - "resolved": "https://registry.npmjs.org/@typescript-eslint/parser/-/parser-1.10.2.tgz", - "integrity": "sha512-xWDWPfZfV0ENU17ermIUVEVSseBBJxKfqBcRCMZ8nAjJbfA5R7NWMZmFFHYnars5MjK4fPjhu4gwQv526oZIPQ==", + "version": "1.11.0", + "resolved": "https://registry.npmjs.org/@typescript-eslint/parser/-/parser-1.11.0.tgz", + "integrity": "sha512-5xBExyXaxVyczrZvbRKEXvaTUFFq7gIM9BynXukXZE0zF3IQP/FxF4mPmmh3gJ9egafZFqByCpPTFm3dk4SY7Q==", "dev": true, "requires": { "@types/eslint-visitor-keys": "^1.0.0", - "@typescript-eslint/experimental-utils": "1.10.2", - "@typescript-eslint/typescript-estree": "1.10.2", + "@typescript-eslint/experimental-utils": "1.11.0", + "@typescript-eslint/typescript-estree": "1.11.0", "eslint-visitor-keys": "^1.0.0" } }, "@typescript-eslint/typescript-estree": { - "version": "1.10.2", - "resolved": "https://registry.npmjs.org/@typescript-eslint/typescript-estree/-/typescript-estree-1.10.2.tgz", - "integrity": "sha512-Kutjz0i69qraOsWeI8ETqYJ07tRLvD9URmdrMoF10bG8y8ucLmPtSxROvVejWvlJUGl2et/plnMiKRDW+rhEhw==", + "version": "1.11.0", + "resolved": "https://registry.npmjs.org/@typescript-eslint/typescript-estree/-/typescript-estree-1.11.0.tgz", + "integrity": "sha512-fquUHF5tAx1sM2OeRCC7wVxFd1iMELWMGCzOSmJ3pLzArj9+kRixdlC4d5MncuzXpjEqc6045p3KwM0o/3FuUA==", "dev": true, "requires": { "lodash.unescape": "4.0.1", @@ -871,6 +871,12 @@ "text-hex": "1.0.x" } }, + "commander": { + "version": "2.20.0", + "resolved": "https://registry.npmjs.org/commander/-/commander-2.20.0.tgz", + "integrity": "sha512-7j2y+40w61zy6YC2iRNpUe/NwhNyoXrYpHMrSunaMG64nRnaf96zO/KMQR4OyN/UnE5KLyEBnKHd4aG3rskjpQ==", + "dev": true + }, "component-emitter": { "version": "1.3.0", "resolved": "https://registry.npmjs.org/component-emitter/-/component-emitter-1.3.0.tgz", @@ -1151,13 +1157,13 @@ "integrity": "sha1-G2HAViGQqN/2rjuyzwIAyhMLhtQ=" }, "eslint": { - "version": "5.16.0", - "resolved": "https://registry.npmjs.org/eslint/-/eslint-5.16.0.tgz", - "integrity": "sha512-S3Rz11i7c8AA5JPv7xAH+dOyq/Cu/VXHiHXBPOU1k/JAM5dXqQPt3qcrhpHSorXmrpu2g0gkIBVXAqCpzfoZIg==", + "version": "6.0.1", + "resolved": "https://registry.npmjs.org/eslint/-/eslint-6.0.1.tgz", + "integrity": "sha512-DyQRaMmORQ+JsWShYsSg4OPTjY56u1nCjAmICrE8vLWqyLKxhFXOthwMj1SA8xwfrv0CofLNVnqbfyhwCkaO0w==", "dev": true, "requires": { "@babel/code-frame": "^7.0.0", - "ajv": "^6.9.1", + "ajv": "^6.10.0", "chalk": "^2.1.0", "cross-spawn": "^6.0.5", "debug": "^4.0.1", @@ -1165,18 +1171,19 @@ "eslint-scope": "^4.0.3", "eslint-utils": "^1.3.1", "eslint-visitor-keys": "^1.0.0", - "espree": "^5.0.1", + "espree": "^6.0.0", "esquery": "^1.0.1", "esutils": "^2.0.2", "file-entry-cache": "^5.0.1", "functional-red-black-tree": "^1.0.1", - "glob": "^7.1.2", + "glob-parent": "^3.1.0", "globals": "^11.7.0", "ignore": "^4.0.6", "import-fresh": "^3.0.0", "imurmurhash": "^0.1.4", "inquirer": "^6.2.2", - "js-yaml": "^3.13.0", + "is-glob": "^4.0.0", + "js-yaml": "^3.13.1", "json-stable-stringify-without-jsonify": "^1.0.1", "levn": "^0.3.0", "lodash": "^4.17.11", @@ -1184,7 +1191,6 @@ "mkdirp": "^0.5.1", "natural-compare": "^1.4.0", "optionator": "^0.8.2", - "path-is-inside": "^1.0.2", "progress": "^2.0.0", "regexpp": "^2.0.1", "semver": "^5.5.1", @@ -1225,9 +1231,9 @@ } }, "eslint-config-prettier": { - "version": "5.0.0", - "resolved": "https://registry.npmjs.org/eslint-config-prettier/-/eslint-config-prettier-5.0.0.tgz", - "integrity": "sha512-c17Aqiz5e8LEqoc/QPmYnaxQFAHTx2KlCZBPxXXjEMmNchOLnV/7j0HoPZuC+rL/tDC9bazUYOKJW9bOhftI/w==", + "version": "6.0.0", + "resolved": "https://registry.npmjs.org/eslint-config-prettier/-/eslint-config-prettier-6.0.0.tgz", + "integrity": "sha512-vDrcCFE3+2ixNT5H83g28bO/uYAwibJxerXPj+E7op4qzBCsAV36QfvdAyVOoNxKAH2Os/e01T/2x++V0LPukA==", "dev": true, "requires": { "get-stdin": "^6.0.0" @@ -1265,9 +1271,9 @@ "dev": true }, "espree": { - "version": "5.0.1", - "resolved": "https://registry.npmjs.org/espree/-/espree-5.0.1.tgz", - "integrity": "sha512-qWAZcWh4XE/RwzLJejfcofscgMc9CamR6Tn1+XRXNzrvUSSbiAjGOI/fggztjIi7y9VLPqnICMIPiGyr8JaZ0A==", + "version": "6.0.0", + "resolved": "https://registry.npmjs.org/espree/-/espree-6.0.0.tgz", + "integrity": "sha512-lJvCS6YbCn3ImT3yKkPe0+tJ+mH6ljhGNjHQH9mRtiO6gjhVAOhVXW1yjnwqGwTkK3bGbye+hb00nFNmu0l/1Q==", "dev": true, "requires": { "acorn": "^6.0.7", @@ -1640,9 +1646,9 @@ } }, "flatted": { - "version": "2.0.0", - "resolved": "https://registry.npmjs.org/flatted/-/flatted-2.0.0.tgz", - "integrity": "sha512-R+H8IZclI8AAkSBRQJLVOsxwAoHd6WC40b4QTNWIjzAa6BXOBfQcM587MXDTVPeYaopFNWHUFLx7eNmHDSxMWg==", + "version": "2.0.1", + "resolved": "https://registry.npmjs.org/flatted/-/flatted-2.0.1.tgz", + "integrity": "sha512-a1hQMktqW9Nmqr5aktAux3JMNqaucxGcjtjWnZLHX7yyPCmlSV3M54nGYbqT8K+0GhF3NBgmJCc3ma+WOgX8Jg==", "dev": true }, "follow-redirects": { @@ -4404,10 +4410,22 @@ "resolved": "https://registry.npmjs.org/triple-beam/-/triple-beam-1.3.0.tgz", "integrity": "sha512-XrHUvV5HpdLmIj4uVMxHggLbFSZYIn7HEWsqePZcI50pco+MPqJ50wMGY794X7AOOhxOBAjbkqfAbEe/QMp2Lw==" }, + "ts-json-schema-generator": { + "version": "0.50.2", + "resolved": "https://registry.npmjs.org/ts-json-schema-generator/-/ts-json-schema-generator-0.50.2.tgz", + "integrity": "sha512-S6n8kHpU2y7IJvr07rA2Xn9wbKRWTs31jvgpFQnkWn4n7jWXOj/yGEzp1UN/qQOBB0r3qSZjsOMr2KzuaSWTGA==", + "dev": true, + "requires": { + "commander": "~2.20.0", + "glob": "~7.1.4", + "json-stable-stringify": "^1.0.1", + "typescript": "~3.5.2" + } + }, "ts-postgres": { - "version": "1.0.2", - "resolved": "https://registry.npmjs.org/ts-postgres/-/ts-postgres-1.0.2.tgz", - "integrity": "sha512-WdCth7Q00KrcL2FwxiQq5FM2aUFa2fXqrcOssd537zb8RPa1omhtIZ6Jpi2oL2GjqXEADqar3YPQ4OUbIzjESA==", + "version": "1.1.0", + "resolved": "https://registry.npmjs.org/ts-postgres/-/ts-postgres-1.1.0.tgz", + "integrity": "sha512-POEX+ovAPWyvGqXwBV/xFBJ0Km7sOyfCHjxDMkFIEN3XyzLmEEwPeJM+84SH9kkcDY4Use3senRa7+kkAuDaNQ==", "requires": { "ts-typed-events": "^1.1.1" } diff --git a/server/package.json b/server/package.json index edd2f86..60ffe32 100644 --- a/server/package.json +++ b/server/package.json @@ -17,7 +17,7 @@ "start": "node dist/index.js", "dev": "nodemon .", "debug": "nodemon --inspect .", - "generate-json-validators": "npx typescript-json-validator src/ts/action/validation.ts --collection --noExtraProps--format=full --aliasRefs --ignoreErrors && sed -i '1s;^;/* eslint-disable */\\n;' src/ts/action/validation.validator.ts", + "generate-json-validators": "npx typescript-json-validator src/ts/action/validation.ts --collection --noExtraProps --format=full --aliasRefs --ignoreErrors && sed -i -e 's/allErrors: true/allErrors: false/g' -e 's/ChangeBase<\"\\(.*\\)\",\\(.*\\)>/ChangeBase\\1/g' -e 's/\\/\\* tslint:disable \\*\\//\\/\\* eslint-disable \\*\\//g' src/ts/action/validation.validator.ts", "generate-secret": "npm run prestart && node dist/secret.js" }, "files": [ @@ -40,7 +40,7 @@ "jsonwebtoken": "^8.5.1", "moment": "^2.24.0", "source-map-support": "^0.5.12", - "ts-postgres": "^1.0.2", + "ts-postgres": "^1.1.0", "uuid": "^3.3.2", "winston": "^3.2.1", "wu": "^2.1.0" @@ -57,10 +57,10 @@ "@types/source-map-support": "^0.5.0", "@types/uuid": "^3.4.4", "@types/wu": "^2.1.39", - "@typescript-eslint/eslint-plugin": "^1.10.2", - "@typescript-eslint/parser": "^1.10.2", - "eslint": "^5.12.1", - "eslint-config-prettier": "^5.0.0", + "@typescript-eslint/eslint-plugin": "^1.11.0", + "@typescript-eslint/parser": "^1.11.0", + "eslint": "^6.0.1", + "eslint-config-prettier": "^6.0.0", "eslint-plugin-prettier": "^3.1.0", "nodemon": "^1.19.1", "prettier": "1.18.2", diff --git a/server/src/ts/action/validation.validator.ts b/server/src/ts/action/validation.validator.ts index 1f622a4..c75bb6c 100644 --- a/server/src/ts/action/validation.validator.ts +++ b/server/src/ts/action/validation.validator.ts @@ -1,12 +1,11 @@ /* eslint-disable */ -/* tslint:disable */ // generated by typescript-json-validator import Ajv = require("ajv"); import { RegisterUser, CreateLobby, Action, CheckAlive } from "./validation"; export const ajv = new Ajv({ allErrors: false, coerceTypes: false, - format: "fast", + format: "full", nullable: true, unicode: true, uniqueItems: true, @@ -63,6 +62,7 @@ export const Schema = { ] }, Authenticate: { + additionalProperties: false, defaultProperties: [], description: "Authenticate with the game.", properties: { @@ -77,6 +77,7 @@ export const Schema = { type: "object" }, Cardcast: { + additionalProperties: false, defaultProperties: [], description: "A source for Cardcast.", properties: { @@ -94,13 +95,13 @@ export const Schema = { Change: { anyOf: [ { - $ref: '#/definitions/ChangeBase<"PackingHeat",PackingHeat>' + $ref: "#/definitions/ChangeBasePackingHeat" }, { - $ref: '#/definitions/ChangeBase<"Rando",Rando>' + $ref: "#/definitions/ChangeBaseRando" }, { - $ref: '#/definitions/ChangeBase<"Reboot",Reboot>' + $ref: "#/definitions/ChangeBaseReboot" } ] }, @@ -125,7 +126,8 @@ export const Schema = { enum: ["Reboot"], type: "string" }, - 'ChangeBase<"PackingHeat",PackingHeat>': { + ChangeBasePackingHeat: { + additionalProperties: false, defaultProperties: [], properties: { houseRule: { @@ -138,7 +140,8 @@ export const Schema = { required: ["houseRule"], type: "object" }, - 'ChangeBase<"Rando",Rando>': { + ChangeBaseRando: { + additionalProperties: false, defaultProperties: [], properties: { houseRule: { @@ -151,7 +154,8 @@ export const Schema = { required: ["houseRule"], type: "object" }, - 'ChangeBase<"Reboot",Reboot>': { + ChangeBaseReboot: { + additionalProperties: false, defaultProperties: [], properties: { houseRule: { @@ -165,6 +169,7 @@ export const Schema = { type: "object" }, ChangeDecks: { + additionalProperties: false, defaultProperties: [], description: "Make a change to the configuration of decks for the lobby.", properties: { @@ -187,6 +192,7 @@ export const Schema = { type: "object" }, ChangeHouseRule: { + additionalProperties: false, defaultProperties: [], description: "Set the hand size for the lobby.", properties: { @@ -206,6 +212,7 @@ export const Schema = { type: "object" }, CheckAlive: { + additionalProperties: false, defaultProperties: [], description: "Previously obtained tokens to check the validity of.", properties: { @@ -220,6 +227,7 @@ export const Schema = { type: "object" }, CreateLobby: { + additionalProperties: false, defaultProperties: [], description: "The details needed to create a new lobby.", properties: { @@ -246,6 +254,7 @@ export const Schema = { type: "string" }, Judge: { + additionalProperties: false, defaultProperties: [], description: "A user declares the winning play for a round.", properties: { @@ -306,6 +315,7 @@ export const Schema = { type: "string" }, PackingHeat: { + additionalProperties: false, defaultProperties: [], description: 'Configuration for the "Packing Heat" house rule.', type: "object" @@ -319,6 +329,7 @@ export const Schema = { type: "string" }, Rando: { + additionalProperties: false, defaultProperties: [], properties: { number: { @@ -332,6 +343,7 @@ export const Schema = { type: "object" }, Reboot: { + additionalProperties: false, defaultProperties: [], description: 'Configuration for the "Reboot the Universe" house rule.\nThis rule allows players to draw a new hand by sacrificing a given number\nof points.', @@ -347,6 +359,7 @@ export const Schema = { type: "object" }, Redraw: { + additionalProperties: false, defaultProperties: [], description: "A player plays a white card into a round.", properties: { @@ -359,6 +372,7 @@ export const Schema = { type: "object" }, RegisterUser: { + additionalProperties: false, defaultProperties: [], description: "The details to register a new user for a lobby.", properties: { @@ -376,6 +390,7 @@ export const Schema = { type: "object" }, Reveal: { + additionalProperties: false, defaultProperties: [], description: "A user judges the winning play for a round.", properties: { @@ -390,6 +405,7 @@ export const Schema = { type: "object" }, SetHandSize: { + additionalProperties: false, defaultProperties: [], description: "Set the hand size for the lobby.", properties: { @@ -411,6 +427,7 @@ export const Schema = { type: "object" }, SetPassword: { + additionalProperties: false, defaultProperties: [], description: "Set (or unset) the password for the lobby.", properties: { @@ -432,6 +449,7 @@ export const Schema = { type: "object" }, SetPublic: { + additionalProperties: false, defaultProperties: [], description: "Set (or unset) the password for the lobby.", properties: { @@ -451,6 +469,7 @@ export const Schema = { type: "object" }, SetScoreLimit: { + additionalProperties: false, defaultProperties: [], description: "(Un)Set the score limit for the lobby.", properties: { @@ -473,6 +492,7 @@ export const Schema = { type: "object" }, StartGame: { + additionalProperties: false, defaultProperties: [], description: "Start a game in the lobby if possible.", properties: { @@ -484,6 +504,7 @@ export const Schema = { type: "object" }, Submit: { + additionalProperties: false, defaultProperties: [], description: "A player plays a white card into a round.", properties: { @@ -502,6 +523,7 @@ export const Schema = { type: "object" }, TakeBack: { + additionalProperties: false, defaultProperties: [], description: "A player plays a white card into a round.", properties: {