From 77fcd7e9a685e9e00a894370914ea5cdac0ac34d Mon Sep 17 00:00:00 2001 From: Gareth Latty Date: Wed, 30 Dec 2020 23:28:10 +0000 Subject: [PATCH] Progress on #154: Add custom call support for Czar Choices house rule. --- client/assets/images/pencil.svg | 1 + client/src/elm/MassiveDecks/Card/Call.elm | 20 ++ .../src/elm/MassiveDecks/Card/Call/Editor.elm | 336 ++++++++++++++++++ .../MassiveDecks/Card/Call/Editor/Model.elm | 29 ++ client/src/elm/MassiveDecks/Card/Parts.elm | 96 ++--- .../src/elm/MassiveDecks/Card/Parts/Part.elm | 68 ++++ client/src/elm/MassiveDecks/Error.elm | 2 +- client/src/elm/MassiveDecks/Game.elm | 63 +++- client/src/elm/MassiveDecks/Game/Action.elm | 5 +- .../elm/MassiveDecks/Game/Action/Model.elm | 1 + client/src/elm/MassiveDecks/Game/Messages.elm | 3 + client/src/elm/MassiveDecks/Game/Round.elm | 2 + .../elm/MassiveDecks/Game/Round/Playing.elm | 6 +- .../elm/MassiveDecks/Game/Round/Starting.elm | 57 ++- .../src/elm/MassiveDecks/Models/Decoders.elm | 30 +- .../src/elm/MassiveDecks/Models/Encoders.elm | 68 ++++ .../elm/MassiveDecks/Pages/Lobby/Actions.elm | 15 +- client/src/elm/MassiveDecks/Pages/Start.elm | 11 +- .../elm/MassiveDecks/Requests/HttpData.elm | 4 +- client/src/elm/MassiveDecks/Settings.elm | 19 +- client/src/elm/MassiveDecks/Strings.elm | 15 + .../elm/MassiveDecks/Strings/Languages.elm | 14 +- .../elm/MassiveDecks/Strings/Languages/De.elm | 57 +++ .../Strings/Languages/DeXInformal.elm | 57 +++ .../Strings/Languages/En/Internal.elm | 50 +++ .../elm/MassiveDecks/Strings/Languages/Id.elm | 57 +++ .../elm/MassiveDecks/Strings/Languages/It.elm | 57 +++ .../elm/MassiveDecks/Strings/Languages/Pl.elm | 57 +++ .../MassiveDecks/Strings/Languages/PtBR.elm | 57 +++ .../src/elm/MassiveDecks/Strings/Render.elm | 35 +- .../elm/MassiveDecks/Strings/Translation.elm | 8 +- .../Strings/Translation/Model.elm | 2 +- client/src/elm/Material/IconButton.elm | 59 +-- client/src/scss/_cards.scss | 20 +- client/src/scss/_game.scss | 1 + client/src/scss/cards/_call-editor.scss | 80 +++++ client/src/scss/cards/_colors.scss | 5 + client/src/scss/game/_starting.scss | 25 ++ client/src/scss/pages/_lobby.scss | 9 +- .../ts/action/game-action/czar/pick-call.ts | 4 +- .../src/ts/action/game-action/player/fill.ts | 2 +- server/src/ts/action/validation.validator.ts | 57 +++ server/src/ts/caches/postgres.ts | 2 +- server/src/ts/games/cards/card.ts | 31 +- server/src/ts/games/cards/decks.ts | 2 +- server/src/ts/games/game.ts | 25 +- server/src/ts/games/game/round.ts | 38 +- server/src/ts/games/rules/czar-choices.ts | 11 + 48 files changed, 1447 insertions(+), 226 deletions(-) create mode 100644 client/assets/images/pencil.svg create mode 100644 client/src/elm/MassiveDecks/Card/Call/Editor.elm create mode 100644 client/src/elm/MassiveDecks/Card/Call/Editor/Model.elm create mode 100644 client/src/elm/MassiveDecks/Card/Parts/Part.elm create mode 100644 client/src/scss/cards/_call-editor.scss create mode 100644 client/src/scss/cards/_colors.scss create mode 100644 client/src/scss/game/_starting.scss diff --git a/client/assets/images/pencil.svg b/client/assets/images/pencil.svg new file mode 100644 index 00000000..cb4dc56d --- /dev/null +++ b/client/assets/images/pencil.svg @@ -0,0 +1 @@ + \ No newline at end of file diff --git a/client/src/elm/MassiveDecks/Card/Call.elm b/client/src/elm/MassiveDecks/Card/Call.elm index f397f946..b4b6ff74 100644 --- a/client/src/elm/MassiveDecks/Card/Call.elm +++ b/client/src/elm/MassiveDecks/Card/Call.elm @@ -11,6 +11,7 @@ import Html.Attributes as HtmlA import MassiveDecks.Card as Card import MassiveDecks.Card.Model exposing (..) import MassiveDecks.Card.Parts as Parts exposing (Parts) +import MassiveDecks.Card.Source.Model as Source import MassiveDecks.Game.Rules exposing (Rules) import MassiveDecks.Model exposing (Shared) import MassiveDecks.Pages.Lobby.Configure.Decks as Decks @@ -41,6 +42,18 @@ viewFilled shared config side attributes slotAttrs fillWith call = viewInternal shared config side attributes (Parts.viewFilled slotAttrs fillWith) call +{-| Render a potentially blank card to HTML. +-} +viewPotentiallyCustom : Shared -> Config -> Side -> (String -> msg) -> (String -> msg) -> List (Html.Attribute msg) -> Call -> Html msg +viewPotentiallyCustom shared config side update canonicalize attributes call = + case call.details.source of + Source.Custom -> + viewCustom shared config side update canonicalize attributes call + + _ -> + view shared config side attributes call + + {-| Render an unknown response to HTML, face-down. -} viewUnknown : Shared -> List (Html.Attribute msg) -> Html msg @@ -48,6 +61,13 @@ viewUnknown shared attributes = Card.viewUnknown shared "call" attributes +{-| Render a blank card to HTML. +-} +viewCustom : Shared -> Config -> Side -> (String -> msg) -> (String -> msg) -> List (Html.Attribute msg) -> Call -> Html msg +viewCustom shared config side update canonicalize attributes call = + Html.div [] [] + + {- Private -} diff --git a/client/src/elm/MassiveDecks/Card/Call/Editor.elm b/client/src/elm/MassiveDecks/Card/Call/Editor.elm new file mode 100644 index 00000000..339b74ae --- /dev/null +++ b/client/src/elm/MassiveDecks/Card/Call/Editor.elm @@ -0,0 +1,336 @@ +module MassiveDecks.Card.Call.Editor exposing + ( init + , toParts + , update + , view + ) + +import FontAwesome.Icon as Icon +import FontAwesome.Layering as Icon +import FontAwesome.Solid as Icon +import Html exposing (Html) +import Html.Attributes as HtmlA +import Html.Events as HtmlE +import Html5.DragDrop as DragDrop +import List.Extra as List +import MassiveDecks.Card.Call.Editor.Model exposing (..) +import MassiveDecks.Card.Model as Card +import MassiveDecks.Card.Parts as Parts exposing (Parts) +import MassiveDecks.Card.Parts.Part as Part +import MassiveDecks.Components.Form as Form +import MassiveDecks.Components.Form.Message as Message +import MassiveDecks.Model exposing (Shared) +import MassiveDecks.Strings as Strings exposing (MdString) +import MassiveDecks.Strings.Languages as Lang +import MassiveDecks.Util.Html as Html +import MassiveDecks.Util.Maybe as Maybe +import MassiveDecks.Util.NeList as NeList +import Material.Button as Button +import Material.IconButton as IconButton +import Material.TextArea as TextArea +import Material.TextField as TextField + + +init : Card.Call -> Model +init source = + { source = source + , parts = source.body |> Parts.toList |> List.intersperse [ Parts.Text "\n" Part.NoStyle ] |> List.concat + , selected = Nothing + , error = Nothing + , dragDrop = DragDrop.init + } + + +update : Msg -> Model -> ( Model, Cmd msg ) +update msg model = + case msg of + Select index -> + ( { model | selected = index }, Cmd.none ) + + Add index part -> + ( model |> changeParts (Just index) (model.parts |> insertAt index part), Cmd.none ) + + Set index part -> + case part of + Parts.Text "" _ -> + ( model |> changeParts Nothing (model.parts |> List.removeAt index), Cmd.none ) + + _ -> + ( model |> changeParts model.selected (model.parts |> List.setAt index part), Cmd.none ) + + Move index by -> + let + from = + index + + to = + index + by + in + ( model |> changeParts (Just to) (model.parts |> List.swapAt from to), Cmd.none ) + + Remove index -> + ( model |> changeParts Nothing (model.parts |> List.removeAt index), Cmd.none ) + + NoOp -> + ( model, Cmd.none ) + + DragDropMsg dragDropMsg -> + let + ( newDragDrop, drag ) = + DragDrop.update dragDropMsg model.dragDrop + + newModel = + case drag of + Just ( from, to, _ ) -> + model |> changeParts (Just to) (model.parts |> List.swapAt from to) + + Nothing -> + model + in + ( { newModel | dragDrop = newDragDrop }, Cmd.none ) + + +view : (Msg -> msg) -> Shared -> Model -> Html msg +view wrap shared { parts, selected, error } = + let + partFor index = + parts |> List.getAt index |> Maybe.map (\p -> ( index, p )) + + interactions index = + List.concat + [ [ HtmlE.onClick (index |> Just |> Select |> wrap) ] + , DragDrop.draggable (DragDropMsg >> wrap) index + , DragDrop.droppable (DragDropMsg >> wrap) index + ] + + renderPart index part = + case part of + Parts.Text text style -> + Part.styledElement + style + (HtmlA.classList [ ( "text", True ), ( "selected", Just index == selected ) ] :: interactions index) + [ Html.text text ] + + Parts.Slot slot transform style -> + Part.transformedStyledElement + transform + style + (HtmlA.classList [ ( "slot", True ), ( "selected", Just index == selected ) ] :: interactions index) + [ Html.span [] [ Strings.Blank |> Lang.string shared |> String.toLower |> Html.text ] + , Html.span [ HtmlA.class "index" ] [ slot + 1 |> String.fromInt |> Html.text ] + ] + + renderedParts = + parts |> List.indexedMap renderPart + + addAction part = + Add (parts |> List.length) part |> wrap + + nextSlotIndex = + parts |> Parts.nextSlotIndex + + addSlot = + addAction (Parts.Slot nextSlotIndex Part.NoTransform Part.NoStyle) + + inlineControls = + Html.p [] + [ Button.view shared Button.Outlined Strings.AddText Strings.AddText (Icon.plus |> Icon.viewIcon) [ addAction (Parts.Text "..." Part.NoStyle) |> HtmlE.onClick ] + , Button.view shared Button.Outlined Strings.AddSlot Strings.AddSlot (Icon.plus |> Icon.viewIcon) [ addSlot |> HtmlE.onClick ] + ] + + selectedPart = + selected |> Maybe.andThen partFor + + editor = + case selectedPart of + Just ( index, Parts.Text text style ) -> + Form.section shared + "part-editor" + (TextArea.view + [ (\t -> Set index (Parts.Text t style) |> wrap) |> HtmlE.onInput + , HtmlA.class "text part-editor" + , HtmlA.value text + ] + [] + ) + [] + + Just ( index, Parts.Slot slotIndex transform style ) -> + let + setSlotIndex str = + str + |> String.toInt + |> Maybe.map (\i -> Set index (Parts.Slot (i - 1) transform style)) + |> Maybe.withDefault NoOp + |> wrap + in + Form.section shared + "part-editor" + (TextField.view shared + Strings.Blank + TextField.Number + (slotIndex + 1 |> String.fromInt) + [ HtmlA.min "1" + , HtmlE.onInput setSlotIndex + ] + ) + [ Message.info Strings.SlotIndexExplanation ] + + Nothing -> + Html.nothing + + viewError e = + Message.errorWithFix e [ { description = Strings.AddSlot, icon = Icon.plus, action = addSlot } ] + |> Message.view shared + in + Html.div [ HtmlA.class "call-editor" ] + [ Html.div [ HtmlA.class "parts" ] [ Html.p [] renderedParts, inlineControls ] + , controls wrap shared (List.length parts - 1) selectedPart + , editor + , error |> Maybe.andThen viewError |> Maybe.withDefault Html.nothing + ] + + +toParts : List Parts.Part -> Result MdString Parts +toParts parts = + let + splitOnNewLines part = + case part of + Parts.Slot _ _ _ -> + Just part + + Parts.Text text _ -> + if text == "\n" then + Nothing + + else + Just part + in + parts |> List.concatMap separateNewLines |> splitMap splitOnNewLines |> Parts.fromList + + + +{- Private -} + + +changeParts : Maybe Index -> List Parts.Part -> Model -> Model +changeParts selected newParts model = + let + source = + model.source + + ( newSource, error ) = + case newParts |> toParts of + Ok body -> + ( { source | body = body }, Nothing ) + + Err e -> + ( source, Just e ) + in + { model | parts = newParts, selected = selected, source = newSource, error = error } + + +controls : (Msg -> msg) -> Shared -> Int -> Maybe ( Index, Parts.Part ) -> Html msg +controls wrap shared max selected = + let + sep = + Html.div [ HtmlA.class "separator" ] [] + + index = + selected |> Maybe.map Tuple.first + + move by test = + index |> Maybe.andThen (\i -> Move i by |> wrap |> Maybe.justIf (test i)) + + generalControls = + [ IconButton.view shared Strings.Remove (Icon.minus |> Icon.present |> NeList.just) (index |> Maybe.map (Remove >> wrap)) + , IconButton.view shared Strings.MoveLeft (Icon.arrowLeft |> Icon.present |> NeList.just) (move -1 ((<) 0)) + , IconButton.view shared Strings.MoveRight (Icon.arrowRight |> Icon.present |> NeList.just) (move 1 ((>) max)) + ] + + setIfDifferent old updated new = + index |> Maybe.andThen (\i -> Set i (updated new) |> wrap |> Maybe.justIf (old /= new)) + + styleControls setStyle = + [ IconButton.view shared Strings.Normal (Icon.font |> Icon.present |> NeList.just) (setStyle Part.NoStyle) + , IconButton.view shared Strings.Emphasise (Icon.italic |> Icon.present |> NeList.just) (setStyle Part.Em) + ] + + transformControls setTransform = + let + textIcon text = + Icon.layers [] [ Icon.text [] text ] + in + [ IconButton.viewCustomIcon shared Strings.Normal (textIcon "aa") (setTransform Part.NoTransform) + , IconButton.viewCustomIcon shared Strings.Capitalise (textIcon "Aa") (setTransform Part.Capitalize) + , IconButton.viewCustomIcon shared Strings.UpperCase (textIcon "AA") (setTransform Part.UpperCase) + ] + + ( replaceStyle, replaceTransform ) = + case selected of + Just ( _, Parts.Slot slot transform style ) -> + ( setIfDifferent style (Parts.Slot slot transform) + , setIfDifferent transform (\t -> Parts.Slot slot t style) + ) + + Just ( _, Parts.Text text style ) -> + ( setIfDifferent style (Parts.Text text), always Nothing ) + + _ -> + ( always Nothing, always Nothing ) + + collected = + List.concat + [ generalControls + , [ sep ] + , styleControls replaceStyle + , [ sep ] + , transformControls replaceTransform + ] + in + Html.div [ HtmlA.class "controls" ] collected + + +splitMap : (x -> Maybe y) -> List x -> List (List y) +splitMap map values = + let + internal vs = + case vs of + first :: rest -> + let + ( current, lines ) = + internal rest + in + case map first of + Just value -> + ( value :: current, lines ) + + Nothing -> + ( [], current :: lines ) + + [] -> + ( [], [] ) + + ( a, b ) = + values |> internal + in + a :: b + + +separateNewLines : Parts.Part -> List Parts.Part +separateNewLines part = + case part of + Parts.Text string style -> + String.split "\n" string |> List.map (\t -> Parts.Text t style) |> List.intersperse (Parts.Text "\n" style) + + Parts.Slot _ _ _ -> + [ part ] + + +insertAt : Int -> a -> List a -> List a +insertAt index item items = + let + ( start, end ) = + List.splitAt index items + in + start ++ item :: end diff --git a/client/src/elm/MassiveDecks/Card/Call/Editor/Model.elm b/client/src/elm/MassiveDecks/Card/Call/Editor/Model.elm new file mode 100644 index 00000000..4aba9e81 --- /dev/null +++ b/client/src/elm/MassiveDecks/Card/Call/Editor/Model.elm @@ -0,0 +1,29 @@ +module MassiveDecks.Card.Call.Editor.Model exposing (Index, Model, Msg(..)) + +import Html5.DragDrop as DragDrop +import MassiveDecks.Card.Model as Card +import MassiveDecks.Card.Parts as Parts +import MassiveDecks.Strings exposing (MdString) + + +type Msg + = Select (Maybe Index) + | Add Index Parts.Part + | Set Index Parts.Part + | Move Index Int + | Remove Index + | NoOp + | DragDropMsg (DragDrop.Msg Index Index) + + +type alias Model = + { source : Card.Call + , selected : Maybe Index + , parts : List Parts.Part + , error : Maybe MdString + , dragDrop : DragDrop.Model Index Index + } + + +type alias Index = + Int diff --git a/client/src/elm/MassiveDecks/Card/Parts.elm b/client/src/elm/MassiveDecks/Card/Parts.elm index b36a1bb1..18b1af25 100644 --- a/client/src/elm/MassiveDecks/Card/Parts.elm +++ b/client/src/elm/MassiveDecks/Card/Parts.elm @@ -3,13 +3,13 @@ module MassiveDecks.Card.Parts exposing , Part(..) , Parts , SlotAttrs - , Style(..) - , Transform(..) , fillsFromPlay , fromList , missingSlotIndices + , nextSlotIndex , nonObviousSlotIndices , slotCount + , toList , unsafeFromList , view , viewFilled @@ -21,24 +21,11 @@ import Dict exposing (Dict) import Html exposing (Html) import Html.Attributes as HtmlA import List.Extra as List +import MassiveDecks.Card.Parts.Part as Part exposing (Style(..), Transform(..)) +import MassiveDecks.Strings as Strings exposing (MdString) import Set exposing (Set) -{-| A transform to apply to the value in a slot. --} -type Transform - = NoTransform - | UpperCase - | Capitalize - - -{-| A style to be applied to some text. --} -type Style - = NoStyle - | Em - - {-| A part of a call's text. This is either just text or a position for a call to be inserted in-game. -} type Part @@ -72,38 +59,29 @@ type Parts {-| Construct a `Parts` from a `List` of `Line`s. This will fail if there is not at least one `Slot`. -} -fromList : List Line -> Result String Parts +fromList : List Line -> Result MdString Parts fromList lines = let - indicesList = - lines |> List.concat |> List.filterMap slotIndex - indices = - indicesList |> Set.fromList + lines |> List.concat |> List.filterMap slotIndex |> Set.fromList in if Set.isEmpty indices then - Err "Must contain at least one slot." + Err Strings.MustContainAtLeastOneSlot else let - max = - indicesList |> List.maximum |> Maybe.withDefault 0 - - expect = - List.range 0 max |> Set.fromList - in - if expect == indices then - Ok (Parts lines) + remapping = + indices |> Set.toList |> List.sort |> List.indexedMap (\new old -> ( old, new )) |> Dict.fromList - else - let - missing = - Set.diff expect indices + rewrite part = + case part of + Slot index transform style -> + Slot (Dict.get index remapping |> Maybe.withDefault 0) transform style - missingStr = - missing |> Set.toList |> List.map String.fromInt |> String.join ", " - in - "Gap in given slot indexes, missing: " ++ missingStr |> Err + _ -> + part + in + lines |> List.map (List.map rewrite) |> Parts |> Ok {-| Construct without checking for at least one `Slot`. This is designed for use with fake cards or the editor where @@ -114,6 +92,13 @@ unsafeFromList lines = Parts lines +{-| Get a list of lines. +-} +toList : Parts -> List (List Part) +toList (Parts lines) = + lines + + {-| The number of `Slot`s with distinct indexes in the `Parts`. This will be one or more. -} slotCount : Parts -> Int @@ -193,6 +178,13 @@ nonObviousSlotIndices (Parts lines) = (indices |> List.length) /= (indices |> Set.fromList |> Set.size) +{-| Get the next slot index not in use. +-} +nextSlotIndex : List Part -> Int +nextSlotIndex parts = + parts |> List.filterMap slotIndex |> List.maximum |> Maybe.map ((+) 1) |> Maybe.withDefault 0 + + {- Private -} @@ -291,29 +283,9 @@ cluster parts = viewPart : SlotAttrs msg -> Fills -> Part -> Html msg viewPart slotAttrs fills part = - let - styleToElement style = - case style of - NoStyle -> - Html.span - - Em -> - Html.em - - transformToAttrs transform = - case transform of - NoTransform -> - [] - - UpperCase -> - [ HtmlA.class "upper-case" ] - - Capitalize -> - [ HtmlA.class "capitalize" ] - in case part of Text text style -> - styleToElement style [ HtmlA.class "text" ] [ Html.text text ] + Part.styledElement style [ HtmlA.class "text" ] [ Html.text text ] Slot index transform style -> let @@ -334,10 +306,10 @@ viewPart slotAttrs fills part = , HtmlA.attribute "data-slot-index" (index + 1 |> String.fromInt) ] , slotAttrs index - , transformToAttrs transform + , Part.transformAttrs transform ] in - styleToElement style attrs fill + Part.styledElement style attrs fill viewCluster : SlotAttrs msg -> Fills -> List Part -> List (Html msg) diff --git a/client/src/elm/MassiveDecks/Card/Parts/Part.elm b/client/src/elm/MassiveDecks/Card/Parts/Part.elm new file mode 100644 index 00000000..53d1e20c --- /dev/null +++ b/client/src/elm/MassiveDecks/Card/Parts/Part.elm @@ -0,0 +1,68 @@ +module MassiveDecks.Card.Parts.Part exposing + ( Style(..) + , Transform(..) + , styledElement + , transformAttrs + , transformClass + , transformedStyledElement + ) + +import Html +import Html.Attributes as HtmlA +import MassiveDecks.Util.Maybe as Maybe + + +{-| A transform to apply to the value in a slot. +-} +type Transform + = NoTransform + | UpperCase + | Capitalize + + +{-| A style to be applied to some text. +-} +type Style + = NoStyle + | Em + + +{-| Get an element function that applies the given style and transform. +-} +transformedStyledElement : Transform -> Style -> List (Html.Attribute msg) -> List (Html.Html msg) -> Html.Html msg +transformedStyledElement transform style attrs = + styledElement style (transformAttrs transform ++ attrs) + + +{-| Get a element function that applies the given style. +-} +styledElement : Style -> List (Html.Attribute msg) -> List (Html.Html msg) -> Html.Html msg +styledElement style = + case style of + NoStyle -> + Html.span + + Em -> + Html.em + + +{-| Get a class name that applies the given transform. +-} +transformClass : Transform -> Maybe String +transformClass transform = + case transform of + NoTransform -> + Nothing + + UpperCase -> + Just "upper-case" + + Capitalize -> + Just "capitalize" + + +{-| Get an attribute list that applies the given transform. +-} +transformAttrs : Transform -> List (Html.Attribute msg) +transformAttrs = + transformClass >> Maybe.map HtmlA.class >> Maybe.toList diff --git a/client/src/elm/MassiveDecks/Error.elm b/client/src/elm/MassiveDecks/Error.elm index 2aa8d1b1..013254a8 100644 --- a/client/src/elm/MassiveDecks/Error.elm +++ b/client/src/elm/MassiveDecks/Error.elm @@ -93,7 +93,7 @@ body shared route description details = ++ "\n\tPage: " ++ Route.externalUrl shared.origin route ++ "\n\tEnglish Error: " - ++ Lang.givenLanguageString shared Lang.En description + ++ Lang.givenLanguageString Lang.En description ++ "\n\tDetails: " ++ details in diff --git a/client/src/elm/MassiveDecks/Game.elm b/client/src/elm/MassiveDecks/Game.elm index 9669f632..ab7e1481 100644 --- a/client/src/elm/MassiveDecks/Game.elm +++ b/client/src/elm/MassiveDecks/Game.elm @@ -19,6 +19,7 @@ import Html.Attributes as HtmlA import Html.Events as HtmlE import Html5.DragDrop as DragDrop import MassiveDecks.Card.Call as Call +import MassiveDecks.Card.Call.Editor as CallEditor import MassiveDecks.Card.Model as Card exposing (Call) import MassiveDecks.Card.Parts as Parts import MassiveDecks.Card.Play as Play exposing (Play) @@ -26,7 +27,6 @@ import MassiveDecks.Card.Response as Response import MassiveDecks.Card.Source.Model as Source import MassiveDecks.Components as Components import MassiveDecks.Game.Action as Action -import MassiveDecks.Game.Action.Model exposing (Action) import MassiveDecks.Game.History as History import MassiveDecks.Game.Messages exposing (..) import MassiveDecks.Game.Model exposing (..) @@ -231,6 +231,46 @@ update wrap shared msg model = else ( model, Cmd.none ) + WriteCall -> + let + newStage = + case round.stage of + Round.S stage -> + let + fromId id = + stage.calls |> Maybe.andThen (List.filter (.details >> .id >> (==) id) >> List.head) + + card = + stage.pick |> Maybe.andThen fromId + in + Round.S { stage | editor = card |> Maybe.map CallEditor.init } + + _ -> + round.stage + in + ( { model | game = { game | round = { round | stage = newStage } } }, Cmd.none ) + + CallEditorMsg callEditorMsg -> + let + ( newStage, cmd ) = + case round.stage of + Round.S stage -> + let + ( newEditor, editorCmd ) = + case stage.editor of + Just editor -> + CallEditor.update callEditorMsg editor |> Tuple.mapFirst Just + + Nothing -> + ( Nothing, Cmd.none ) + in + ( Round.S { stage | editor = newEditor }, editorCmd ) + + _ -> + ( round.stage, Cmd.none ) + in + ( { model | game = { game | round = { round | stage = newStage } } }, cmd ) + PickPlay id -> let makePick stage wrapRound = @@ -263,7 +303,22 @@ update wrap shared msg model = Submit -> case round.stage of Round.S stage -> - ( model, stage.pick |> Maybe.map Actions.pickCall |> Maybe.withDefault Cmd.none ) + let + call = + stage.calls + |> Maybe.andThen (List.filter (.details >> .id >> Just >> (==) stage.pick) >> List.head) + + fill c = + if c.details.source == Source.Custom then + stage.editor |> Maybe.map (.source >> .body) + + else + Nothing + + action = + Maybe.map2 Actions.pickCall stage.pick (call |> Maybe.map fill) |> Maybe.withDefault Cmd.none + in + ( model, action ) Round.P stage -> let @@ -779,7 +834,7 @@ applyGameEvent wrap wrapEvent auth shared gameEvent model = , czar = czar , players = players , startedAt = time - , stage = Round.S (Round.Starting Nothing (Just calls) False) + , stage = Round.S (Round.Starting Nothing Nothing (Just calls) False) } , if auth.claims.uid == czar then Notifications.notify shared @@ -796,7 +851,7 @@ applyGameEvent wrap wrapEvent auth shared gameEvent model = , czar = czar , players = players , startedAt = time - , stage = Round.S (Round.Starting Nothing Nothing False) + , stage = Round.S (Round.Starting Nothing Nothing Nothing False) } , Cmd.none ) diff --git a/client/src/elm/MassiveDecks/Game/Action.elm b/client/src/elm/MassiveDecks/Game/Action.elm index fd8ce0fb..be16a02b 100644 --- a/client/src/elm/MassiveDecks/Game/Action.elm +++ b/client/src/elm/MassiveDecks/Game/Action.elm @@ -13,7 +13,7 @@ import Material.Fab as Fab actions : List Action actions = - [ PickCall, Submit, TakeBack, Judge, Like, Advance ] + [ PickCall, EditCall, Submit, TakeBack, Judge, Like, Advance ] {-| Style for actions that are blocking the game from continuing until they are performed. @@ -43,6 +43,9 @@ viewSingle wrap shared visible action = PickCall -> IconView Icon.check blocking Fab.Normal Strings.PickCall Game.Submit + EditCall -> + IconView Icon.pencilAlt blocking Fab.Normal Strings.WriteCall Game.WriteCall + Submit -> IconView Icon.check blocking Fab.Normal Strings.SubmitPlay Game.Submit diff --git a/client/src/elm/MassiveDecks/Game/Action/Model.elm b/client/src/elm/MassiveDecks/Game/Action/Model.elm index 77299a43..3ff66d69 100644 --- a/client/src/elm/MassiveDecks/Game/Action/Model.elm +++ b/client/src/elm/MassiveDecks/Game/Action/Model.elm @@ -3,6 +3,7 @@ module MassiveDecks.Game.Action.Model exposing (Action(..)) type Action = PickCall + | EditCall | Submit | TakeBack | Judge diff --git a/client/src/elm/MassiveDecks/Game/Messages.elm b/client/src/elm/MassiveDecks/Game/Messages.elm index 70e2ecef..dfbce37b 100644 --- a/client/src/elm/MassiveDecks/Game/Messages.elm +++ b/client/src/elm/MassiveDecks/Game/Messages.elm @@ -1,6 +1,7 @@ module MassiveDecks.Game.Messages exposing (Msg(..)) import Html5.DragDrop as DragDrop +import MassiveDecks.Card.Call.Editor.Model as CallEditor import MassiveDecks.Card.Model as Card import MassiveDecks.Card.Play as Play import MassiveDecks.Game.Model exposing (..) @@ -16,6 +17,8 @@ type Msg | EditBlank Card.Id String | Fill Card.Id String | Submit + | WriteCall + | CallEditorMsg CallEditor.Msg | TakeBack | PickPlay Play.Id | Reveal Play.Id diff --git a/client/src/elm/MassiveDecks/Game/Round.elm b/client/src/elm/MassiveDecks/Game/Round.elm index 676f31df..b7462e7e 100644 --- a/client/src/elm/MassiveDecks/Game/Round.elm +++ b/client/src/elm/MassiveDecks/Game/Round.elm @@ -26,6 +26,7 @@ module MassiveDecks.Game.Round exposing import Dict exposing (Dict) import Json.Decode as Json +import MassiveDecks.Card.Call.Editor.Model as CallEditor import MassiveDecks.Card.Model as Card import MassiveDecks.Card.Play as Play exposing (Play) import MassiveDecks.Game.Time exposing (Time) @@ -199,6 +200,7 @@ type PickState -} type alias Starting = { pick : Maybe Card.Id + , editor : Maybe CallEditor.Model , calls : Maybe (List Card.Call) , timedOut : Bool } diff --git a/client/src/elm/MassiveDecks/Game/Round/Playing.elm b/client/src/elm/MassiveDecks/Game/Round/Playing.elm index e8415798..1eb99456 100644 --- a/client/src/elm/MassiveDecks/Game/Round/Playing.elm +++ b/client/src/elm/MassiveDecks/Game/Round/Playing.elm @@ -15,6 +15,7 @@ import MassiveDecks.Card.Call as Call import MassiveDecks.Card.Model as Card import MassiveDecks.Card.Parts as Parts import MassiveDecks.Card.Response as Response +import MassiveDecks.Card.Source.Model as Source import MassiveDecks.Game.Action.Model as Action import MassiveDecks.Game.Messages as Game exposing (Msg) import MassiveDecks.Game.Model exposing (..) @@ -154,7 +155,10 @@ viewHandCard wrap shared config filled picked response = attrs = List.concat - [ [ HtmlA.classList [ ( "picked", pick /= Nothing ) ] + [ [ HtmlA.classList + [ ( "picked", pick /= Nothing ) + , ( "custom", response.details.source == Source.Custom ) + ] , pick |> Maybe.map pickedForSlot |> Maybe.withDefault HtmlA.nothing , details.id |> Game.Pick Nothing |> wrap |> HtmlE.onClick ] diff --git a/client/src/elm/MassiveDecks/Game/Round/Starting.elm b/client/src/elm/MassiveDecks/Game/Round/Starting.elm index d68722d2..9a3816c7 100644 --- a/client/src/elm/MassiveDecks/Game/Round/Starting.elm +++ b/client/src/elm/MassiveDecks/Game/Round/Starting.elm @@ -6,10 +6,12 @@ import Html.Attributes as HtmlA import Html.Events as HtmlE import Html.Keyed as HtmlK import MassiveDecks.Card.Call as Call +import MassiveDecks.Card.Call.Editor as CallEditor import MassiveDecks.Card.Model as Card import MassiveDecks.Card.Response as Response +import MassiveDecks.Card.Source.Model as Source import MassiveDecks.Game.Action.Model as Action exposing (Action) -import MassiveDecks.Game.Messages as Game exposing (Msg) +import MassiveDecks.Game.Messages as Game exposing (Msg(..)) import MassiveDecks.Game.Model exposing (..) import MassiveDecks.Game.Round as Round exposing (Round) import MassiveDecks.Model exposing (Shared) @@ -17,7 +19,6 @@ import MassiveDecks.Pages.Lobby.Configure.Model exposing (Config) import MassiveDecks.Pages.Lobby.Model as Lobby import MassiveDecks.Strings as Strings exposing (MdString) import MassiveDecks.User as User exposing (User) -import MassiveDecks.Util.Maybe as Maybe view : (Msg -> msg) -> Lobby.Auth -> Shared -> Config -> Dict User.Id User -> Model -> Round.Specific Round.Starting -> RoundView msg @@ -27,24 +28,44 @@ view wrap _ shared config _ model round = round.stage { call, instruction, action, content } = - case stage.calls of - Just calls -> - let - findCallById id = - calls |> List.filter (.details >> .id >> (==) id) |> List.head - in + case stage.editor of + Just editor -> ViewSubset - (stage.pick |> Maybe.andThen findCallById) + (Just editor.source) Strings.PickCallInstruction - (Action.PickCall |> Maybe.justIf (stage.pick /= Nothing)) - (calls |> viewOptions wrap shared config stage.pick) + (Just Action.PickCall) + (CallEditor.view (CallEditorMsg >> wrap) shared editor) Nothing -> - ViewSubset - Nothing - Strings.WaitForCallInstruction - Nothing - (model.hand |> viewHand wrap shared config model.filledCards) + case stage.calls of + Just calls -> + let + findCallById id = + calls |> List.filter (.details >> .id >> (==) id) |> List.head + + picked = + stage.pick |> Maybe.andThen findCallById + + toAction c = + case c.details.source of + Source.Custom -> + Action.EditCall + + _ -> + Action.PickCall + in + ViewSubset + picked + Strings.PickCallInstruction + (picked |> Maybe.map toAction) + (calls |> viewOptions wrap shared config stage.pick) + + Nothing -> + ViewSubset + Nothing + Strings.WaitForCallInstruction + Nothing + (model.hand |> viewHand wrap shared config model.filledCards) in { call = call , instruction = Just instruction @@ -52,7 +73,7 @@ view wrap _ shared config _ model round = , content = content , slotAttrs = always [] , fillCallWith = Dict.empty - , roundAttrs = [] + , roundAttrs = [ HtmlA.classList [ ( "starting", True ), ( "show-slot-indices", stage.editor /= Nothing ) ] ] } @@ -81,7 +102,7 @@ viewOptions wrap shared config pick calls = shared config Card.Front - [ HtmlA.classList [ ( "picked", pick == Just id ) ] + [ HtmlA.classList [ ( "picked", pick == Just id ), ( "custom", call.details.source == Source.Custom ) ] , id |> Game.Pick Nothing |> wrap |> HtmlE.onClick ] call diff --git a/client/src/elm/MassiveDecks/Models/Decoders.elm b/client/src/elm/MassiveDecks/Models/Decoders.elm index c52febcd..09a3b9ca 100644 --- a/client/src/elm/MassiveDecks/Models/Decoders.elm +++ b/client/src/elm/MassiveDecks/Models/Decoders.elm @@ -30,6 +30,7 @@ import Json.Decode.Pipeline as Json import Json.Patch import MassiveDecks.Card.Model as Card exposing (Call, Response) import MassiveDecks.Card.Parts as Parts exposing (Part, Parts) +import MassiveDecks.Card.Parts.Part as Part import MassiveDecks.Card.Play as Play exposing (Play) import MassiveDecks.Card.Source.BuiltIn.Model as BuiltIn import MassiveDecks.Card.Source.Generated.Model as Generated @@ -1079,7 +1080,7 @@ parts = Json.succeed s Err e -> - "Not a valid call: " ++ e |> Json.fail + "Not a valid call: " ++ (e |> Lang.givenLanguageString Lang.En) |> Json.fail in Json.list (Json.list part) |> Json.map enrich @@ -1088,8 +1089,8 @@ parts = type PartData - = Text String Parts.Style - | Slot (Maybe Int) Parts.Transform Parts.Style + = Text String Part.Style + | Slot (Maybe Int) Part.Transform Part.Style enrich : List (List PartData) -> List (List Parts.Part) @@ -1124,7 +1125,7 @@ enrich partData = part : Json.Decoder PartData part = Json.oneOf - [ Json.string |> Json.map (\t -> Text t Parts.NoStyle) + [ Json.string |> Json.map (\t -> Text t Part.NoStyle) , styled , slot ] @@ -1134,45 +1135,45 @@ slot : Json.Decoder PartData slot = Json.succeed Slot |> Json.optional "index" (Json.int |> Json.map Just) Nothing - |> Json.optional "transform" transform Parts.NoTransform - |> Json.optional "style" style Parts.NoStyle + |> Json.optional "transform" transform Part.NoTransform + |> Json.optional "style" style Part.NoStyle styled : Json.Decoder PartData styled = Json.succeed Text |> Json.required "text" Json.string - |> Json.optional "style" style Parts.NoStyle + |> Json.optional "style" style Part.NoStyle -transform : Json.Decoder Parts.Transform +transform : Json.Decoder Part.Transform transform = Json.string |> Json.andThen transformByName -transformByName : String -> Json.Decoder Parts.Transform +transformByName : String -> Json.Decoder Part.Transform transformByName name = case name of "UpperCase" -> - Json.succeed Parts.UpperCase + Json.succeed Part.UpperCase "Capitalize" -> - Json.succeed Parts.Capitalize + Json.succeed Part.Capitalize _ -> unknownValue "transform" name -style : Json.Decoder Parts.Style +style : Json.Decoder Part.Style style = Json.string |> Json.andThen styleByName -styleByName : String -> Json.Decoder Parts.Style +styleByName : String -> Json.Decoder Part.Style styleByName name = case name of "Em" -> - Json.succeed Parts.Em + Json.succeed Part.Em _ -> unknownValue "style" name @@ -1242,6 +1243,7 @@ playerSet = startingRound : Maybe (List Card.Call) -> Json.Decoder Round.Starting startingRound calls = Json.succeed Round.Starting + |> Json.hardcoded Nothing |> Json.hardcoded Nothing |> Json.hardcoded calls |> Json.optional "timedOut" Json.bool False diff --git a/client/src/elm/MassiveDecks/Models/Encoders.elm b/client/src/elm/MassiveDecks/Models/Encoders.elm index c6ae30b7..b80b5e3c 100644 --- a/client/src/elm/MassiveDecks/Models/Encoders.elm +++ b/client/src/elm/MassiveDecks/Models/Encoders.elm @@ -7,6 +7,7 @@ module MassiveDecks.Models.Encoders exposing , lobbyCreation , lobbyToken , packingHeat + , parts , playerPresence , privilege , rando @@ -23,6 +24,8 @@ module MassiveDecks.Models.Encoders exposing import Dict import Json.Encode as Json +import MassiveDecks.Card.Parts as Parts exposing (Parts) +import MassiveDecks.Card.Parts.Part as Part exposing (Style) import MassiveDecks.Card.Source.BuiltIn.Model as BuiltIn import MassiveDecks.Card.Source.JsonAgainstHumanity.Model as JsonAgainstHumanity import MassiveDecks.Card.Source.ManyDecks.Model as ManyDecks @@ -389,3 +392,68 @@ userRegistration r = (( "name", r.name |> Json.string ) :: (r.password |> Maybe.map (\p -> [ ( "password", p |> Json.string ) ]) |> Maybe.withDefault []) ) + + +transform : Part.Transform -> Maybe Json.Value +transform t = + let + name = + case t of + Part.NoTransform -> + Nothing + + Part.Capitalize -> + Just "Capitalize" + + Part.UpperCase -> + Just "UpperCase" + in + name |> Maybe.map Json.string + + +style : Style -> Maybe Json.Value +style s = + let + name = + case s of + Part.NoStyle -> + Nothing + + Part.Em -> + Just "Em" + in + name |> Maybe.map Json.string + + +part : Parts.Part -> Json.Value +part p = + let + maybeField field = + case field of + ( n, Just v ) -> + Just ( n, v ) + + ( _, Nothing ) -> + Nothing + + maybeObject = + List.filterMap maybeField >> Json.object + in + case p of + Parts.Text text Part.NoStyle -> + text |> Json.string + + Parts.Text text s -> + maybeObject [ ( "text", text |> Json.string |> Just ), ( "style", s |> style ) ] + + Parts.Slot index t s -> + maybeObject [ ( "index", index |> Json.int |> Just ), ( "transform", t |> transform ), ( "style", s |> style ) ] + + +parts : Parts -> Json.Value +parts ps = + let + line = + Json.list part + in + ps |> Parts.toList |> Json.list line diff --git a/client/src/elm/MassiveDecks/Pages/Lobby/Actions.elm b/client/src/elm/MassiveDecks/Pages/Lobby/Actions.elm index 3cb69f2a..07611036 100644 --- a/client/src/elm/MassiveDecks/Pages/Lobby/Actions.elm +++ b/client/src/elm/MassiveDecks/Pages/Lobby/Actions.elm @@ -23,12 +23,14 @@ module MassiveDecks.Pages.Lobby.Actions exposing import Json.Encode as Json import Json.Patch exposing (Patch) import MassiveDecks.Card.Model as Card +import MassiveDecks.Card.Parts exposing (Parts) import MassiveDecks.Card.Play as Play import MassiveDecks.Game.Player as Player import MassiveDecks.Game.Round as Round import MassiveDecks.Models.Encoders as Encoders import MassiveDecks.ServerConnection as ServerConnection import MassiveDecks.User as User +import MassiveDecks.Util.Maybe as Maybe configure : Json.Patch.Patch -> Cmd msg @@ -56,9 +58,16 @@ submit play = action "Submit" [ ( "play", play |> Json.list Json.string ) ] -pickCall : Card.Id -> Cmd msg -pickCall call = - action "PickCall" [ ( "call", call |> Json.string ) ] +pickCall : Card.Id -> Maybe Parts -> Cmd msg +pickCall call parts = + let + fillWith ps = + ( "fill", ps |> Encoders.parts ) + + fillIfGiven = + parts |> Maybe.map fillWith |> Maybe.toList + in + action "PickCall" (( "call", call |> Json.string ) :: fillIfGiven) takeBack : Cmd msg diff --git a/client/src/elm/MassiveDecks/Pages/Start.elm b/client/src/elm/MassiveDecks/Pages/Start.elm index fad0f11b..c6bf1bcd 100644 --- a/client/src/elm/MassiveDecks/Pages/Start.elm +++ b/client/src/elm/MassiveDecks/Pages/Start.elm @@ -19,6 +19,7 @@ import Http import MassiveDecks.Card.Call as Call import MassiveDecks.Card.Model as Card import MassiveDecks.Card.Parts as Parts +import MassiveDecks.Card.Parts.Part as Part import MassiveDecks.Card.Response as Response import MassiveDecks.Card.Source.Model as Source import MassiveDecks.Components.Form as Form @@ -599,12 +600,12 @@ examplePick2 : Card.Call examplePick2 = Card.call (Parts.unsafeFromList - [ [ Parts.Slot 0 Parts.NoTransform Parts.NoStyle - , Parts.Text " + " Parts.NoStyle - , Parts.Slot 1 Parts.NoTransform Parts.NoStyle + [ [ Parts.Slot 0 Part.NoTransform Part.NoStyle + , Parts.Text " + " Part.NoStyle + , Parts.Slot 1 Part.NoTransform Part.NoStyle ] - , [ Parts.Text " = " Parts.NoStyle - , Parts.Slot 2 Parts.NoTransform Parts.NoStyle + , [ Parts.Text " = " Part.NoStyle + , Parts.Slot 2 Part.NoTransform Part.NoStyle ] ] ) diff --git a/client/src/elm/MassiveDecks/Requests/HttpData.elm b/client/src/elm/MassiveDecks/Requests/HttpData.elm index a9c1892e..e8d23dfd 100644 --- a/client/src/elm/MassiveDecks/Requests/HttpData.elm +++ b/client/src/elm/MassiveDecks/Requests/HttpData.elm @@ -21,7 +21,7 @@ import MassiveDecks.Requests.HttpData.Model exposing (..) import MassiveDecks.Requests.Request as Request exposing (Request) import MassiveDecks.Strings as Strings exposing (MdString) import MassiveDecks.Util.Maybe as Maybe -import MassiveDecks.Util.NeList exposing (NeList(..)) +import MassiveDecks.Util.NeList as NeList exposing (NeList(..)) import Material.IconButton as IconButton import Time @@ -121,5 +121,5 @@ refreshButton shared { loading, data } = in IconButton.view shared Strings.Refresh - (NeList (Icon.sync |> Icon.present |> applyStyle) []) + (Icon.sync |> Icon.present |> applyStyle |> NeList.just) (Pull |> Maybe.justIf (not loading)) diff --git a/client/src/elm/MassiveDecks/Settings.elm b/client/src/elm/MassiveDecks/Settings.elm index b781edb8..a401a845 100644 --- a/client/src/elm/MassiveDecks/Settings.elm +++ b/client/src/elm/MassiveDecks/Settings.elm @@ -40,7 +40,8 @@ import MassiveDecks.Speech as Speech import MassiveDecks.Strings as Strings import MassiveDecks.Strings.Languages as Lang import MassiveDecks.Strings.Languages.Model as Lang exposing (Language) -import MassiveDecks.Util.NeList exposing (NeList(..)) +import MassiveDecks.Util.Maybe as Maybe +import MassiveDecks.Util.NeList as NeList exposing (NeList(..)) import MassiveDecks.Util.Order as Order import Material.IconButton as IconButton import Material.Select as Select @@ -167,7 +168,10 @@ view wrap shared = Icon.cog button = - IconButton.view shared Strings.SettingsTitle (NeList (icon |> Icon.present |> Icon.styled [ Icon.lg ]) []) (ToggleOpen |> wrap |> Just) + IconButton.view shared + Strings.SettingsTitle + (icon |> Icon.present |> Icon.styled [ Icon.lg ] |> NeList.just) + (ToggleOpen |> wrap |> Just) panel = Html.div [ HtmlA.classList [ ( "settings-panel", True ), ( "mdc-card", True ), ( "open", model.open ) ] ] @@ -555,7 +559,7 @@ languageOption shared currentLanguage language = nameInCurrentLanguage = language |> Lang.languageName - |> Lang.givenLanguageString shared currentLanguage + |> Lang.givenLanguageString currentLanguage viewAutonym n = [ Html.span [ language |> Lang.langAttr ] @@ -563,14 +567,7 @@ languageOption shared currentLanguage language = ] autonym = - if language /= currentLanguage then - language - |> Lang.autonym shared - |> viewAutonym - |> Just - - else - Nothing + language |> Lang.autonym |> viewAutonym |> Maybe.justIf (language /= currentLanguage) in { id = language , icon = Nothing diff --git a/client/src/elm/MassiveDecks/Strings.elm b/client/src/elm/MassiveDecks/Strings.elm index 36f70029..6dffe8ad 100644 --- a/client/src/elm/MassiveDecks/Strings.elm +++ b/client/src/elm/MassiveDecks/Strings.elm @@ -318,6 +318,7 @@ type MdString | ConfigureNextGame -- A description of the action of configuring the next game for the lobby after the current one finished. -- Game | PickCall -- A description of the action of picking a call for the players to play into. + | WriteCall -- A description of the action of writing a custom call for the players to play into. | SubmitPlay -- A description of the action of submitting the play for the czar to judge. | 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. @@ -355,6 +356,20 @@ type MdString -- Actions | Refresh -- The action to refresh the page with newer information. | Accept -- A term for accepting something. + -- Editor + | AddSlot -- Add a slot to the card. + | AddText -- Add text to the card. + | EditText -- Edit text on the card. + | EditSlotIndex -- Edit the slot index for a card, allowing multiple slots to be filled with the same response. + | MoveLeft -- The action of moving an item to the left. + | MoveRight -- The action of moving an item to the right. + | Remove -- Remove something from a card. + | Normal -- A term for the absence of any style or transform. + | Capitalise -- Make the selected slot capitalise any response played into it (teSt -> TeSt). + | UpperCase -- Make the selected slot upper case any response played into it (teSt -> TEST). + | Emphasise -- Make the selected text emphasised on the card (i.e: shown in italics, but don't specify that). + | MustContainAtLeastOneSlot -- An error message telling the user a call needs at least one slot. + | SlotIndexExplanation -- An explanation of what a slot index is. -- Errors | Error -- A title for a generic error (something having gone wrong). | ErrorHelp -- A message telling the user that an error has occurred and what to do. diff --git a/client/src/elm/MassiveDecks/Strings/Languages.elm b/client/src/elm/MassiveDecks/Strings/Languages.elm index 3a00defd..c022791d 100644 --- a/client/src/elm/MassiveDecks/Strings/Languages.elm +++ b/client/src/elm/MassiveDecks/Strings/Languages.elm @@ -86,9 +86,9 @@ languageNameOrCode shared givenCode = {-| The given language's name for itself. -} -autonym : Shared -> Language -> String -autonym shared language = - languageName language |> givenLanguageString shared language +autonym : Language -> String +autonym language = + languageName language |> givenLanguageString language {-| A sort that gives the closest matches first. @@ -135,14 +135,14 @@ findBestMatch codes = -} string : Shared -> MdString -> String string shared mdString = - mdString |> givenLanguageString shared (currentLanguage shared) + mdString |> givenLanguageString (currentLanguage shared) {-| Build an actual string in the given language. -} -givenLanguageString : Shared -> Language -> MdString -> String -givenLanguageString shared lang mdString = - mdString |> (pack lang).string shared +givenLanguageString : Language -> MdString -> String +givenLanguageString lang mdString = + mdString |> (pack lang).string {-| An HTML text node from the given `MdString`. Note this is more than just convenience - we enhance some strings diff --git a/client/src/elm/MassiveDecks/Strings/Languages/De.elm b/client/src/elm/MassiveDecks/Strings/Languages/De.elm index 3d1cf9b2..08d27bc1 100644 --- a/client/src/elm/MassiveDecks/Strings/Languages/De.elm +++ b/client/src/elm/MassiveDecks/Strings/Languages/De.elm @@ -1034,6 +1034,10 @@ translate _ mdString = PickCall -> [ Missing ] + -- TODO: Translate + WriteCall -> + [ Missing ] + SubmitPlay -> [ Text "Geben Sie diese Karten dem ", ref Czar, Text " zum Abschluss dieser Runde." ] @@ -1156,6 +1160,59 @@ translate _ mdString = Accept -> [ Text "OK" ] + -- Editor + -- TODO: Translate + AddSlot -> + [ Missing ] + + -- TODO: Translate + AddText -> + [ Missing ] + + -- TODO: Translate + EditText -> + [ Missing ] + + -- TODO: Translate + EditSlotIndex -> + [ Missing ] + + -- TODO: Translate + MoveLeft -> + [ Missing ] + + -- TODO: Translate + Remove -> + [ Missing ] + + -- TODO: Translate + MoveRight -> + [ Missing ] + + -- TODO: Translate + Normal -> + [ Missing ] + + -- TODO: Translate + Capitalise -> + [ Missing ] + + -- TODO: Translate + UpperCase -> + [ Missing ] + + -- TODO: Translate + Emphasise -> + [ Missing ] + + -- TODO: Translate + MustContainAtLeastOneSlot -> + [ Missing ] + + -- TODO: Translate + SlotIndexExplanation -> + [ Missing ] + -- Errors Error -> [ Text "Fehler" ] diff --git a/client/src/elm/MassiveDecks/Strings/Languages/DeXInformal.elm b/client/src/elm/MassiveDecks/Strings/Languages/DeXInformal.elm index 4e2e2130..afb3bfc3 100644 --- a/client/src/elm/MassiveDecks/Strings/Languages/DeXInformal.elm +++ b/client/src/elm/MassiveDecks/Strings/Languages/DeXInformal.elm @@ -1033,6 +1033,10 @@ translate _ mdString = PickCall -> [ Missing ] + -- TODO: Translate + WriteCall -> + [ Missing ] + SubmitPlay -> [ Text "Gib diese Karten dem ", ref Czar, Text " zum Abschluss dieser Runde." ] @@ -1155,6 +1159,59 @@ translate _ mdString = Accept -> [ Text "OK" ] + -- Editor + -- TODO: Translate + AddSlot -> + [ Missing ] + + -- TODO: Translate + AddText -> + [ Missing ] + + -- TODO: Translate + EditText -> + [ Missing ] + + -- TODO: Translate + EditSlotIndex -> + [ Missing ] + + -- TODO: Translate + MoveLeft -> + [ Missing ] + + -- TODO: Translate + Remove -> + [ Missing ] + + -- TODO: Translate + MoveRight -> + [ Missing ] + + -- TODO: Translate + Normal -> + [ Missing ] + + -- TODO: Translate + Capitalise -> + [ Missing ] + + -- TODO: Translate + UpperCase -> + [ Missing ] + + -- TODO: Translate + Emphasise -> + [ Missing ] + + -- TODO: Translate + MustContainAtLeastOneSlot -> + [ Missing ] + + -- TODO: Translate + SlotIndexExplanation -> + [ Missing ] + -- Errors Error -> [ Text "Fehler" ] diff --git a/client/src/elm/MassiveDecks/Strings/Languages/En/Internal.elm b/client/src/elm/MassiveDecks/Strings/Languages/En/Internal.elm index b8954012..6e667015 100644 --- a/client/src/elm/MassiveDecks/Strings/Languages/En/Internal.elm +++ b/client/src/elm/MassiveDecks/Strings/Languages/En/Internal.elm @@ -989,6 +989,9 @@ translate _ mdString = PickCall -> [ Text "Pick this ", ref (noun Call 1), Text " for the others to play into for the round." ] + WriteCall -> + [ Text "Write a custom ", ref (noun Call 1), Text " for the others to play into for the round." ] + SubmitPlay -> [ Text "Give these cards to the ", ref Czar, Text " as your play for the round." ] @@ -1113,6 +1116,53 @@ translate _ mdString = Accept -> [ Text "OK" ] + -- Editor + AddSlot -> + [ Text "Add ", ref Blank ] + + AddText -> + [ Text "Add Text" ] + + EditText -> + [ Text "Edit" ] + + EditSlotIndex -> + [ Text "Edit" ] + + MoveLeft -> + [ Text "Move Earlier" ] + + Remove -> + [ Text "Remove" ] + + MoveRight -> + [ Text "Move Later" ] + + Normal -> + [ Text "Normal" ] + + Capitalise -> + [ Text "Capitalise" ] + + UpperCase -> + [ Text "Upper Case" ] + + Emphasise -> + [ Text "Emphasise" ] + + MustContainAtLeastOneSlot -> + [ Text "You must have at least one ", ref Blank, Text " for people to play into." ] + + SlotIndexExplanation -> + [ Text "What number " + , ref (noun Response 1) + , Text " played will be used for this " + , ref Blank + , Text ". This lets you repeat a " + , ref (noun Response 1) + , Text "." + ] + -- Errors Error -> [ Text "Error" ] diff --git a/client/src/elm/MassiveDecks/Strings/Languages/Id.elm b/client/src/elm/MassiveDecks/Strings/Languages/Id.elm index 585c0315..e3b3978d 100644 --- a/client/src/elm/MassiveDecks/Strings/Languages/Id.elm +++ b/client/src/elm/MassiveDecks/Strings/Languages/Id.elm @@ -1024,6 +1024,10 @@ translate _ mdString = PickCall -> [ Missing ] + -- TODO: Translate + WriteCall -> + [ Missing ] + SubmitPlay -> [ Text "Berikan kartu ini ke ", ref Czar, Text " sebagai permainan Anda untuk putaran tersebut." ] @@ -1146,6 +1150,59 @@ translate _ mdString = Accept -> [ Text "OK" ] + -- Editor + -- TODO: Translate + AddSlot -> + [ Missing ] + + -- TODO: Translate + AddText -> + [ Missing ] + + -- TODO: Translate + EditText -> + [ Missing ] + + -- TODO: Translate + EditSlotIndex -> + [ Missing ] + + -- TODO: Translate + MoveLeft -> + [ Missing ] + + -- TODO: Translate + Remove -> + [ Missing ] + + -- TODO: Translate + MoveRight -> + [ Missing ] + + -- TODO: Translate + Normal -> + [ Missing ] + + -- TODO: Translate + Capitalise -> + [ Missing ] + + -- TODO: Translate + UpperCase -> + [ Missing ] + + -- TODO: Translate + Emphasise -> + [ Missing ] + + -- TODO: Translate + MustContainAtLeastOneSlot -> + [ Missing ] + + -- TODO: Translate + SlotIndexExplanation -> + [ Missing ] + -- Errors Error -> [ Text "Error" ] diff --git a/client/src/elm/MassiveDecks/Strings/Languages/It.elm b/client/src/elm/MassiveDecks/Strings/Languages/It.elm index cbfb6879..c3a201e4 100644 --- a/client/src/elm/MassiveDecks/Strings/Languages/It.elm +++ b/client/src/elm/MassiveDecks/Strings/Languages/It.elm @@ -1054,6 +1054,10 @@ translate _ mdString = PickCall -> [ Missing ] + -- TODO: Translate + WriteCall -> + [ Missing ] + SubmitPlay -> [ Text "Dai queste carte al ", ref Czar, Text " come giocata per il turno." ] @@ -1177,6 +1181,59 @@ translate _ mdString = Accept -> [ Missing ] + -- Editor + -- TODO: Translate + AddSlot -> + [ Missing ] + + -- TODO: Translate + AddText -> + [ Missing ] + + -- TODO: Translate + EditText -> + [ Missing ] + + -- TODO: Translate + EditSlotIndex -> + [ Missing ] + + -- TODO: Translate + MoveLeft -> + [ Missing ] + + -- TODO: Translate + Remove -> + [ Missing ] + + -- TODO: Translate + MoveRight -> + [ Missing ] + + -- TODO: Translate + Normal -> + [ Missing ] + + -- TODO: Translate + Capitalise -> + [ Missing ] + + -- TODO: Translate + UpperCase -> + [ Missing ] + + -- TODO: Translate + Emphasise -> + [ Missing ] + + -- TODO: Translate + MustContainAtLeastOneSlot -> + [ Missing ] + + -- TODO: Translate + SlotIndexExplanation -> + [ Missing ] + -- Errors Error -> [ Text "Errore" ] diff --git a/client/src/elm/MassiveDecks/Strings/Languages/Pl.elm b/client/src/elm/MassiveDecks/Strings/Languages/Pl.elm index a3851b2c..d96fb065 100644 --- a/client/src/elm/MassiveDecks/Strings/Languages/Pl.elm +++ b/client/src/elm/MassiveDecks/Strings/Languages/Pl.elm @@ -1131,6 +1131,10 @@ translate maybeDeclCase mdString = PickCall -> [ Missing ] + -- TODO: Translate + WriteCall -> + [ Missing ] + SubmitPlay -> [ Text "Daj te karty ", refDecl Dative Czar, Text " jako odpowiedzi w tej rundzie." ] @@ -1252,6 +1256,59 @@ translate maybeDeclCase mdString = Accept -> [ Text "OK" ] + -- Editor + -- TODO: Translate + AddSlot -> + [ Missing ] + + -- TODO: Translate + AddText -> + [ Missing ] + + -- TODO: Translate + EditText -> + [ Missing ] + + -- TODO: Translate + EditSlotIndex -> + [ Missing ] + + -- TODO: Translate + MoveLeft -> + [ Missing ] + + -- TODO: Translate + Remove -> + [ Missing ] + + -- TODO: Translate + MoveRight -> + [ Missing ] + + -- TODO: Translate + Normal -> + [ Missing ] + + -- TODO: Translate + Capitalise -> + [ Missing ] + + -- TODO: Translate + UpperCase -> + [ Missing ] + + -- TODO: Translate + Emphasise -> + [ Missing ] + + -- TODO: Translate + MustContainAtLeastOneSlot -> + [ Missing ] + + -- TODO: Translate + SlotIndexExplanation -> + [ Missing ] + -- Errors Error -> [ Text "Błąd" ] diff --git a/client/src/elm/MassiveDecks/Strings/Languages/PtBR.elm b/client/src/elm/MassiveDecks/Strings/Languages/PtBR.elm index 9689dcd5..c45d7114 100644 --- a/client/src/elm/MassiveDecks/Strings/Languages/PtBR.elm +++ b/client/src/elm/MassiveDecks/Strings/Languages/PtBR.elm @@ -1030,6 +1030,10 @@ translate _ mdString = PickCall -> [ Missing ] + -- TODO: Translate + WriteCall -> + [ Missing ] + SubmitPlay -> [ Text "Dar essas cartas ao ", ref Czar, Text " como sua jogada da partida." ] @@ -1151,6 +1155,59 @@ translate _ mdString = Accept -> [ Text "OK" ] + -- Editor + -- TODO: Translate + AddSlot -> + [ Missing ] + + -- TODO: Translate + AddText -> + [ Missing ] + + -- TODO: Translate + EditText -> + [ Missing ] + + -- TODO: Translate + EditSlotIndex -> + [ Missing ] + + -- TODO: Translate + MoveLeft -> + [ Missing ] + + -- TODO: Translate + Remove -> + [ Missing ] + + -- TODO: Translate + MoveRight -> + [ Missing ] + + -- TODO: Translate + Normal -> + [ Missing ] + + -- TODO: Translate + Capitalise -> + [ Missing ] + + -- TODO: Translate + UpperCase -> + [ Missing ] + + -- TODO: Translate + Emphasise -> + [ Missing ] + + -- TODO: Translate + MustContainAtLeastOneSlot -> + [ Missing ] + + -- TODO: Translate + SlotIndexExplanation -> + [ Missing ] + -- Errors Error -> [ Text "Erro" ] diff --git a/client/src/elm/MassiveDecks/Strings/Render.elm b/client/src/elm/MassiveDecks/Strings/Render.elm index 0435088b..7c945e48 100644 --- a/client/src/elm/MassiveDecks/Strings/Render.elm +++ b/client/src/elm/MassiveDecks/Strings/Render.elm @@ -18,7 +18,6 @@ type alias Context langContext = { lang : Language , translate : Maybe langContext -> MdString -> List (Translation.Result langContext) , parent : MdString - , shared : Shared } @@ -32,9 +31,9 @@ asString context mdString = {-| An HTML text node from the given `MdString`. Note this is more than just convenience - we enhance some strings with rich HTML content (e.g: links, icons, etc...) when rendered as HTML. -} -asHtml : Context langContext -> MdString -> Html Never -asHtml context mdString = - [ Translation.Ref Nothing mdString ] |> resultsToHtml context |> Html.span [] +asHtml : Shared -> Context langContext -> MdString -> Html Never +asHtml shared context mdString = + [ Translation.Ref Nothing mdString ] |> resultsToHtml shared context |> Html.span [] @@ -78,13 +77,13 @@ resultToString context result = EnInternal.translate Nothing parent |> partsToString -resultsToHtml : Context langContext -> List (Translation.Result langContext) -> List (Html msg) -resultsToHtml context results = - results |> List.concatMap (resultToHtml context) +resultsToHtml : Shared -> Context langContext -> List (Translation.Result langContext) -> List (Html msg) +resultsToHtml shared context results = + results |> List.concatMap (resultToHtml shared context) -resultToHtml : Context langContext -> Translation.Result langContext -> List (Html msg) -resultToHtml context result = +resultToHtml : Shared -> Context langContext -> Translation.Result langContext -> List (Html msg) +resultToHtml shared context result = let { translate, parent } = context @@ -97,8 +96,8 @@ resultToHtml context result = in mdString |> translate langContext - |> resultsToHtml childContext - |> enhanceHtml childContext mdString + |> resultsToHtml shared childContext + |> enhanceHtml shared childContext mdString Translation.Text text -> [ Html.text text ] @@ -107,16 +106,16 @@ resultToHtml context result = [ mdString |> translate langContext |> resultsToString { context | parent = mdString } |> Html.text ] Translation.Em emphasised -> - [ Html.strong [] (emphasised |> resultsToHtml context) ] + [ Html.strong [] (emphasised |> resultsToHtml shared context) ] Translation.Segment cluster -> - [ Html.span [ HtmlA.class "segment" ] (cluster |> resultsToHtml context) ] + [ Html.span [ HtmlA.class "segment" ] (cluster |> resultsToHtml shared context) ] Translation.Missing -> let english = Html.span [ HtmlA.class "string", HtmlA.lang "en" ] - (EnInternal.translate Nothing parent |> resultsToHtml context) + (EnInternal.translate Nothing parent |> resultsToHtml shared context) translationBeg = Html.blankA @@ -128,8 +127,8 @@ resultToHtml context result = [ Html.span [ HtmlA.class "not-translated" ] [ english, Html.text " ", translationBeg ] ] -enhanceHtml : Context langContext -> MdString -> List (Html msg) -> List (Html msg) -enhanceHtml context mdString unenhanced = +enhanceHtml : Shared -> Context langContext -> MdString -> List (Html msg) -> List (Html msg) +enhanceHtml shared context mdString unenhanced = case mdString of Noun { noun } -> case noun of @@ -251,7 +250,7 @@ enhanceHtml context mdString unenhanced = term context PlayedDescription Icon.check unenhanced ManyDecks -> - case context.shared.sources.manyDecks of + case shared.sources.manyDecks of Just { baseUrl } -> [ Html.blankA [ HtmlA.href baseUrl ] unenhanced ] @@ -259,7 +258,7 @@ enhanceHtml context mdString unenhanced = unenhanced JsonAgainstHumanity -> - case context.shared.sources.jsonAgainstHumanity of + case shared.sources.jsonAgainstHumanity of Just { aboutUrl } -> [ Html.blankA [ HtmlA.href aboutUrl ] unenhanced ] diff --git a/client/src/elm/MassiveDecks/Strings/Translation.elm b/client/src/elm/MassiveDecks/Strings/Translation.elm index b7f88f76..9845b73d 100644 --- a/client/src/elm/MassiveDecks/Strings/Translation.elm +++ b/client/src/elm/MassiveDecks/Strings/Translation.elm @@ -9,12 +9,12 @@ import MassiveDecks.Strings.Translation.Model exposing (..) pack : PackDefinition langContext -> Pack pack def = let - provideContext render shared mdString = - render { lang = def.lang, translate = def.translate, parent = mdString, shared = shared } mdString + context mdString = + { lang = def.lang, translate = def.translate, parent = mdString } in { code = def.code , name = def.name , recommended = def.recommended - , html = provideContext Render.asHtml - , string = provideContext Render.asString + , html = \shared str -> Render.asHtml shared (context str) str + , string = \str -> Render.asString (context str) str } diff --git a/client/src/elm/MassiveDecks/Strings/Translation/Model.elm b/client/src/elm/MassiveDecks/Strings/Translation/Model.elm index fb447fe2..1bfcbbc4 100644 --- a/client/src/elm/MassiveDecks/Strings/Translation/Model.elm +++ b/client/src/elm/MassiveDecks/Strings/Translation/Model.elm @@ -54,5 +54,5 @@ type alias Pack = , html : Shared -> MdString -> Html Never -- Translate the given string to a raw string. - , string : Shared -> MdString -> String + , string : MdString -> String } diff --git a/client/src/elm/Material/IconButton.elm b/client/src/elm/Material/IconButton.elm index 4572e4f0..8fa6e888 100644 --- a/client/src/elm/Material/IconButton.elm +++ b/client/src/elm/Material/IconButton.elm @@ -1,5 +1,6 @@ module Material.IconButton exposing ( view + , viewCustomIcon , viewNoPropagation ) @@ -19,49 +20,49 @@ import MassiveDecks.Util.NeList exposing (NeList(..)) -} view : Shared -> MdString -> NeList (Icon.Presentation id msg) -> Maybe msg -> Html msg view shared title icon action = - let - actionAttr = - case action of - Just msg -> - msg |> HtmlE.onClick - - Nothing -> - HtmlA.disabled True - in - viewInternal [ actionAttr ] shared title icon + viewRenderedIcon [ actionAttrFromMaybe HtmlE.onClick action ] shared title (renderIcon icon) {-| View a button that displays as a simple icon, and blocks clicks propagating to other elements. -} viewNoPropagation : Shared -> MdString -> NeList (Icon.Presentation id msg) -> Maybe msg -> Html msg viewNoPropagation shared title icon action = - let - actionAttr = - case action of - Just msg -> - msg |> HtmlE.onClickNoPropagation + viewRenderedIcon [ actionAttrFromMaybe HtmlE.onClickNoPropagation action ] shared title (renderIcon icon) + - Nothing -> - HtmlA.disabled True - in - viewInternal [ actionAttr ] shared title icon +{-| View a button that displays as a custom rendered icon. +-} +viewCustomIcon : Shared -> MdString -> Html msg -> Maybe msg -> Html msg +viewCustomIcon shared title icon action = + viewRenderedIcon [ actionAttrFromMaybe HtmlE.onClick action ] shared title icon {- Private -} -viewInternal : List (Html.Attribute msg) -> Shared -> MdString -> NeList (Icon.Presentation id msg) -> Html msg -viewInternal actionAttr shared title (NeList first rest) = - let - renderedIcon = - case rest of - [] -> - first |> Icon.view +actionAttrFromMaybe : (a -> Html.Attribute msg) -> Maybe a -> Html.Attribute msg +actionAttrFromMaybe onClick action = + case action of + Just msg -> + msg |> onClick + + Nothing -> + HtmlA.disabled True + + +renderIcon : NeList (Icon.Presentation id msg) -> Html msg +renderIcon (NeList first rest) = + case rest of + [] -> + first |> Icon.view + + _ -> + (first :: rest) |> List.map Icon.view |> Icon.layers [] + - _ -> - (first :: rest) |> List.map Icon.view |> Icon.layers [] - in +viewRenderedIcon : List (Html.Attribute msg) -> Shared -> MdString -> Html msg -> Html msg +viewRenderedIcon actionAttr shared title renderedIcon = Html.node "mwc-icon-button" ((title |> Lang.title shared) :: actionAttr) [ renderedIcon ] diff --git a/client/src/scss/_cards.scss b/client/src/scss/_cards.scss index d50d46de..c5db735e 100644 --- a/client/src/scss/_cards.scss +++ b/client/src/scss/_cards.scss @@ -1,12 +1,8 @@ @use "_fonts"; @use "cards/_size"; @use "cards/_sources"; - -$call-color: #ffffff; -$call-background: #000000; - -$response-color: $call-background; -$response-background: $call-color; +@use "cards/_call-editor"; +@use "cards/_colors"; @mixin shadow($color: #000000) { .side { @@ -44,13 +40,13 @@ $response-background: $call-color; } &.response { - --bg: #{$response-background}; - --fg: #{$response-color}; + --bg: #{colors.$response-background}; + --fg: #{colors.$response-color}; } &.call { - --bg: #{$call-background}; - --fg: #{$call-color}; + --bg: #{colors.$call-background}; + --fg: #{colors.$call-color}; p { display: flex; @@ -260,8 +256,8 @@ $response-background: $call-color; height: 1.2em; margin-left: 0.3em; border-radius: 50%; - background: var(--fg, $call-background); - color: var(--bg, $call-color); + background: var(--fg, colors.$call-background); + color: var(--bg, colors.$call-color); } .show-slot-indices .game-card { diff --git a/client/src/scss/_game.scss b/client/src/scss/_game.scss index 987ca323..b074e922 100644 --- a/client/src/scss/_game.scss +++ b/client/src/scss/_game.scss @@ -2,6 +2,7 @@ @use "./cards"; @use "cards/_size"; @use "./game/_judging"; +@use "./game/_starting"; @use "./game/_playing"; @use "./game/_revealing"; @use "./game/_plays"; diff --git a/client/src/scss/cards/_call-editor.scss b/client/src/scss/cards/_call-editor.scss new file mode 100644 index 00000000..fd3a0d62 --- /dev/null +++ b/client/src/scss/cards/_call-editor.scss @@ -0,0 +1,80 @@ +@use "_colors"; +@use "../_fonts"; + +.call-editor { + display: flex; + flex-direction: column; + align-items: center; + margin: 1em; + padding: 1em; + + --bg: #{colors.$call-background}; + --fg: #{colors.$call-color}; + + text-rendering: optimizeLegibility; + font-family: fonts.$card; + font-weight: bold; + + .controls { + display: flex; + flex-wrap: wrap; + + .sep { + width: 2em; + } + } + + .parts { + color: var(--fg); + background-color: var(--bg); + font-size: 1.5em; + width: 100%; + padding: 1em; + + line-height: 1.6em; + + user-select: none; + + .text { + white-space: pre-wrap; + + border: 1px solid rgba(255, 255, 255, 0.5); + } + + .slot { + text-decoration: underline; + } + } + + #part-editor { + width: 100%; + } + + .selected { + background-color: rgba(255, 255, 255, 0.5); + } + + .index { + display: inline-flex; + justify-content: center; + align-items: center; + content: attr(data-slot-index); + color: var(--bg); + background-color: var(--fg); + border-radius: 1em; + width: 1em; + height: 1em; + margin-left: 0.25em; + } + + .capitalize > :first-child { + text-transform: capitalize; + } + + .upper-case { + text-transform: uppercase; + } + + .custom { + } +} diff --git a/client/src/scss/cards/_colors.scss b/client/src/scss/cards/_colors.scss new file mode 100644 index 00000000..24c47a61 --- /dev/null +++ b/client/src/scss/cards/_colors.scss @@ -0,0 +1,5 @@ +$call-color: #ffffff; +$call-background: #000000; + +$response-color: $call-background; +$response-background: $call-color; diff --git a/client/src/scss/game/_starting.scss b/client/src/scss/game/_starting.scss new file mode 100644 index 00000000..c5eb9659 --- /dev/null +++ b/client/src/scss/game/_starting.scss @@ -0,0 +1,25 @@ +@use "../cards/_size"; +@use "../_colors"; +@use "../_cards"; + +.options.cards { + .picked.game-card .side { + border-color: colors.$secondary; + } +} + +.custom { + .side::after { + position: absolute; + bottom: 1em; + right: 1em; + width: 8em; + height: 8em; + + display: block; + content: ""; + opacity: 0.3; + background: colors.$primary; + mask-image: url("../../../assets/images/pencil.svg"); + } +} diff --git a/client/src/scss/pages/_lobby.scss b/client/src/scss/pages/_lobby.scss index 0bd3244b..a56b57bc 100644 --- a/client/src/scss/pages/_lobby.scss +++ b/client/src/scss/pages/_lobby.scss @@ -3,6 +3,7 @@ @use "../_game"; @use "lobby/_configure"; @use "lobby/_invite"; +@use "../cards/_colors" as card-colors; $top-bar-height: 4rem; $users-width: 18rem; @@ -33,8 +34,8 @@ $users-width: 18rem; justify-content: space-between; align-items: center; - color: cards.$call-color; - background-color: cards.$call-background; + color: card-colors.$call-color; + background-color: card-colors.$call-background; .left { display: flex; @@ -217,8 +218,8 @@ $users-width: 18rem; padding: 0.5em; transform: translateY(100%); - background-color: #{cards.$call-background}; - color: #{cards.$call-color}; + background-color: #{card-colors.$call-background}; + color: #{card-colors.$call-color}; &.error { background-color: #{colors.$error}; diff --git a/server/src/ts/action/game-action/czar/pick-call.ts b/server/src/ts/action/game-action/czar/pick-call.ts index d7bc9e0c..1d6bdc81 100644 --- a/server/src/ts/action/game-action/czar/pick-call.ts +++ b/server/src/ts/action/game-action/czar/pick-call.ts @@ -11,6 +11,7 @@ import { Czar } from "../czar"; export interface PickCall { action: "PickCall"; call: Card.Id; + fill?: Card.Part[][]; } class PickCallActions extends Actions.Implementation< @@ -33,7 +34,8 @@ class PickCallActions extends Actions.Implementation< const { round, events, timeouts } = lobbyRound.advance( server, lobby.game, - action.call + action.call, + action.fill ); lobby.game.round = round; return { lobby, events, timeouts }; diff --git a/server/src/ts/action/game-action/player/fill.ts b/server/src/ts/action/game-action/player/fill.ts index 756e57fe..ce4cc375 100644 --- a/server/src/ts/action/game-action/player/fill.ts +++ b/server/src/ts/action/game-action/player/fill.ts @@ -42,7 +42,7 @@ class FillActions extends Actions.Implementation< "The given card doesn't exist or isn't in the player's hand." ); } - if (Card.isCustomResponse(filled)) { + if (Card.isCustom(filled)) { filled.text = action.text; return { lobby }; } else { diff --git a/server/src/ts/action/validation.validator.ts b/server/src/ts/action/validation.validator.ts index 24f0bc9e..02065a32 100644 --- a/server/src/ts/action/validation.validator.ts +++ b/server/src/ts/action/validation.validator.ts @@ -596,6 +596,25 @@ export const Schema = { call: { $ref: "#/definitions/Id", }, + fill: { + items: { + items: { + anyOf: [ + { + $ref: "#/definitions/Slot", + }, + { + $ref: "#/definitions/Styled", + }, + { + type: "string", + }, + ], + }, + type: "array", + }, + type: "array", + }, }, required: ["action", "call"], type: "object", @@ -909,6 +928,26 @@ export const Schema = { required: ["action", "role"], type: "object", }, + Slot: { + additionalProperties: false, + defaultProperties: [], + description: "An empty slot for responses to be played into.", + properties: { + index: { + type: "number", + }, + style: { + $ref: "#/definitions/Style", + }, + transform: { + description: + "Defines a transformation over the content the slot is filled with.", + enum: ["Capitalize", "UpperCase"], + type: "string", + }, + }, + type: "object", + }, Stage: { enum: ["Complete", "Judging", "Playing", "Revealing", "Starting"], type: "string", @@ -981,6 +1020,24 @@ export const Schema = { required: ["action"], type: "object", }, + Style: { + enum: ["Em", "Strong"], + type: "string", + }, + Styled: { + additionalProperties: false, + defaultProperties: [], + properties: { + style: { + $ref: "#/definitions/Style", + }, + text: { + type: "string", + }, + }, + required: ["text"], + type: "object", + }, Submit: { additionalProperties: false, defaultProperties: [], diff --git a/server/src/ts/caches/postgres.ts b/server/src/ts/caches/postgres.ts index ed4876b1..045f1fa8 100644 --- a/server/src/ts/caches/postgres.ts +++ b/server/src/ts/caches/postgres.ts @@ -179,7 +179,7 @@ export class PostgresCache extends Cache.Cache { } for (const response of templates.responses) { - if (Card.isCustomResponse(response)) { + if (Card.isCustom(response)) { throw Error("Can't have blank cards in a cached deck."); } await client.query( diff --git a/server/src/ts/games/cards/card.ts b/server/src/ts/games/cards/card.ts index 7827f908..991fecac 100644 --- a/server/src/ts/games/cards/card.ts +++ b/server/src/ts/games/cards/card.ts @@ -8,6 +8,14 @@ import { Custom } from "./sources/custom"; */ export type Card = Call | Response; +/** Values shared by all cards.*/ +export interface BaseCard { + /** A unique id for a card.*/ + id: Id; + /** Where the card came from.*/ + source: Source; +} + /** * A call for plays. Some text with blank slots to be filled with responses. */ @@ -20,36 +28,25 @@ export interface Call extends BaseCard { * A response (some text) played into slots. */ export interface Response extends BaseCard { - /** The text on the response. If this is undefined, the card is a blank - * card. */ + /** The text on the response. */ text: string; } /** - * A custom response is special in that it is mutable by the player holding it. + * A custom card is special in that it is mutable by the player holding it. */ -export interface CustomResponse extends Response { - source: Custom; -} +export type CustomCard = TCard & { source: Custom }; /** * If the response is a custom one, and therefore mutable. */ -export const isCustomResponse = ( - response: Response -): response is CustomResponse => response.source.source == "Custom"; +export const isCustom = ( + card: TCard +): card is CustomCard => card.source.source == "Custom"; /** A unique id for an instance of a card.*/ export type Id = string; -/** Values shared by all cards.*/ -export interface BaseCard { - /** A unique id for a card.*/ - id: Id; - /** Where the card came from.*/ - source: Source; -} - export type Style = "Em" | "Strong"; /** An empty slot for responses to be played into.*/ diff --git a/server/src/ts/games/cards/decks.ts b/server/src/ts/games/cards/decks.ts index 95813a84..16c44119 100644 --- a/server/src/ts/games/cards/decks.ts +++ b/server/src/ts/games/cards/decks.ts @@ -100,7 +100,7 @@ export class Responses extends Deck { this.discarded.add({ ...card, id: Card.id(), - ...(Card.isCustomResponse(card) ? { text: "" } : {}), + ...(Card.isCustom(card) ? { text: "" } : {}), }); } diff --git a/server/src/ts/games/game.ts b/server/src/ts/games/game.ts index c6fd49c1..093a1db9 100644 --- a/server/src/ts/games/game.ts +++ b/server/src/ts/games/game.ts @@ -22,6 +22,7 @@ import { StoredPlay } from "./game/round/stored-play"; import * as Player from "./player"; import * as Rules from "./rules"; import * as HappyEnding from "./rules/happy-ending"; +import * as CzarChoices from "./rules/czar-choices"; export interface Public { round: PublicRound.Public; @@ -207,10 +208,13 @@ export class Game { const [call] = gameDecks.calls.draw(1); round = new Round.Playing(0, czar, playersInRound, call); } else { - const calls = gameDecks.calls.draw( - rules.houseRules.czarChoices.numberOfChoices + round = Round.Starting.forGivenChoices( + 0, + czar, + playersInRound, + rules.houseRules.czarChoices, + gameDecks ); - round = new Round.Starting(0, czar, playersInRound, calls); } return new Game(round, playerOrder, playerMap, gameDecks, rules) as Game & { round: Round.Playing; @@ -263,9 +267,15 @@ export class Game { // Discard what is left over from the last round. const round = this.round; if (round.stage === "Starting") { - this.decks.calls.discard(round.calls); + this.decks.calls.discard( + // We destroy custom calls by not discarding them because we don't want them back in rotation. + round.calls.filter((card) => !Card.isCustom(card)) + ); } else { - this.decks.calls.discard([round.call]); + // We destroy custom calls by not discarding them because we don't want them back in rotation. + if (!Card.isCustom(round.call)) { + this.decks.calls.discard([round.call]); + } const plays: StoredPlay[] = round.plays; this.decks.responses.discard(plays.flatMap((play) => play.play)); } @@ -290,11 +300,12 @@ export class Game { const [call] = this.decks.calls.draw(1); this.round = new Round.Playing(roundId, czar, playersInRound, call); } else { - this.round = new Round.Starting( + this.round = Round.Starting.forGivenChoices( roundId, czar, playersInRound, - this.decks.calls.draw(czarChoices.numberOfChoices) + czarChoices, + this.decks ); } } diff --git a/server/src/ts/games/game/round.ts b/server/src/ts/games/game/round.ts index 66ada445..41c2daef 100644 --- a/server/src/ts/games/game/round.ts +++ b/server/src/ts/games/game/round.ts @@ -16,6 +16,9 @@ import * as Rules from "../rules"; import * as Game from "../game"; import { InvalidActionError } from "../../errors/validation"; import { ServerState } from "../../server-state"; +import { Part } from "../cards/card"; +import * as CzarChoices from "../rules/czar-choices"; +import { Decks } from "../cards/decks"; export type Round = Starting | Playing | Revealing | Judging | Complete; @@ -538,6 +541,21 @@ export class Starting extends Base<"Starting"> implements Timed { this.timedOut = timedOut; } + public static forGivenChoices( + id: Id, + czar: User.Id, + players: Set, + czarChoices: CzarChoices.CzarChoices, + decks: Decks + ) { + const first = czarChoices.custom ? [CzarChoices.customCall()] : []; + const cards = [ + ...first, + ...decks.calls.draw(czarChoices.numberOfChoices - first.length), + ]; + return new this(id, czar, players, cards); + } + public waitingFor(): Set | null { return new Set(this.czar); } @@ -545,7 +563,8 @@ export class Starting extends Base<"Starting"> implements Timed { public advance( server: ServerState, game: Game.Game, - chosen: Card.Id + chosen: Card.Id, + fill: Part[][] | undefined ): { round: Playing; timeouts?: Iterable; @@ -557,6 +576,23 @@ export class Starting extends Base<"Starting"> implements Timed { "The given call doesn't exist or wasn't in the given options." ); } + if (Card.isCustom(call)) { + if (fill === undefined) { + throw new InvalidActionError("Custom calls must be filled to be used."); + } + if (Card.slotCount(fill) < 1) { + throw new InvalidActionError("Must have at least one slot."); + } + call.parts = fill; + } else { + if (fill !== undefined) { + throw new InvalidActionError("Only custom calls can be filled."); + } + } + game.decks.calls.discard( + // We destroy custom calls by not discarding them because we don't want them back in rotation. + this.calls.filter((card) => !Card.isCustom(card) && card.id !== chosen) + ); const round = new Playing(this.id, this.czar, this.players, call); const eventsAndTimeouts = game.startPlaying(server, false, round, true); return { round, ...eventsAndTimeouts }; diff --git a/server/src/ts/games/rules/czar-choices.ts b/server/src/ts/games/rules/czar-choices.ts index 0e61ded6..40ca5fba 100644 --- a/server/src/ts/games/rules/czar-choices.ts +++ b/server/src/ts/games/rules/czar-choices.ts @@ -1,3 +1,5 @@ +import * as Card from "../cards/card"; + /** * Configuration for the "Czar Choices" house rule. * At the beginning of the round, the Czar draws multiple calls and chooses one of them. @@ -17,3 +19,12 @@ export interface CzarChoices { */ custom?: boolean; } + +/** + * Generate a blank custom call to use. + */ +export const customCall = (): Card.CustomCard => ({ + id: Card.id(), + source: { source: "Custom" }, + parts: [[{}]], +});