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: [[{}]],
+});