File: KeyBindings.hs

package info (click to toggle)
haskell-lambdahack 0.11.0.1-2
  • links: PTS, VCS
  • area: main
  • in suites: forky, sid
  • size: 4,056 kB
  • sloc: haskell: 45,636; makefile: 219
file content (342 lines) | stat: -rw-r--r-- 15,976 bytes parent folder | download | duplicates (3)
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
77
78
79
80
81
82
83
84
85
86
87
88
89
90
91
92
93
94
95
96
97
98
99
100
101
102
103
104
105
106
107
108
109
110
111
112
113
114
115
116
117
118
119
120
121
122
123
124
125
126
127
128
129
130
131
132
133
134
135
136
137
138
139
140
141
142
143
144
145
146
147
148
149
150
151
152
153
154
155
156
157
158
159
160
161
162
163
164
165
166
167
168
169
170
171
172
173
174
175
176
177
178
179
180
181
182
183
184
185
186
187
188
189
190
191
192
193
194
195
196
197
198
199
200
201
202
203
204
205
206
207
208
209
210
211
212
213
214
215
216
217
218
219
220
221
222
223
224
225
226
227
228
229
230
231
232
233
234
235
236
237
238
239
240
241
242
243
244
245
246
247
248
249
250
251
252
253
254
255
256
257
258
259
260
261
262
263
264
265
266
267
268
269
270
271
272
273
274
275
276
277
278
279
280
281
282
283
284
285
286
287
288
289
290
291
292
293
294
295
296
297
298
299
300
301
302
303
304
305
306
307
308
309
310
311
312
313
314
315
316
317
318
319
320
321
322
323
324
325
326
327
328
329
330
331
332
333
334
335
336
337
338
339
340
341
342
{-# LANGUAGE RankNTypes #-}
-- | Verifying, aggregating and displaying binding of keys to commands.
module Game.LambdaHack.Client.UI.KeyBindings
  ( keyHelp, okxsN
  ) where

import Prelude ()

import Game.LambdaHack.Core.Prelude

import qualified Data.EnumMap.Strict as EM
import qualified Data.Map.Strict as M
import qualified Data.Text as T

import           Game.LambdaHack.Client.UI.Content.Input
import           Game.LambdaHack.Client.UI.Content.Screen
import           Game.LambdaHack.Client.UI.ContentClientUI
import           Game.LambdaHack.Client.UI.HumanCmd
import qualified Game.LambdaHack.Client.UI.Key as K
import           Game.LambdaHack.Client.UI.Overlay
import           Game.LambdaHack.Client.UI.PointUI
import           Game.LambdaHack.Client.UI.Slideshow
import qualified Game.LambdaHack.Definition.Color as Color

-- | Produce a set of help/menu screens from the key bindings.
--
-- When the intro screen mentions KP_5, this really is KP_Begin,
-- but since that is harder to understand we assume a different, non-default
-- state of NumLock in the help text than in the code that handles keys.
keyHelp :: CCUI -> FontSetup -> [(Text, OKX)]
keyHelp CCUI{ coinput=coinput@InputContent{..}
            , coscreen=ScreenContent{rwidth, rheight} } FontSetup{..} =
  let
    movBlurb1 =
      [ "Walk throughout a level with mouse or numeric keypad (right diagram below)"
      , "or the Vi editor keys (middle) or the left-hand movement keys (left). Run until"
      , "disturbed with Shift or Control. Go-to a position with LMB (left mouse button)."
      , "In aiming mode, the same keys (and mouse) move the aiming crosshair."
      ]
    movSchema =
      [ "     q w e     y k u     7 8 9"
      , "      \\|/       \\|/       \\|/"
      , "     a-s-d     h-.-l     4-5-6"
      , "      /|\\       /|\\       /|\\"
      , "     z x c     b j n     1 2 3"
      ]
    movBlurb2 =
      [ "Press `KP_5` (`5` on keypad) to wait, bracing for impact, which reduces any"
      , "damage taken and prevents displacement by foes. Press `S-KP_5` or `C-KP_5`"
      , "(the same key with Shift or Control) to lurk 0.1 of a turn, without bracing."
      , ""
      , "Displace enemies by running into them with Shift/Control or S-LMB. Search,"
      , "open, descend and melee by bumping into walls, doors, stairs and enemies."
      , "The best, and not on cooldown, melee weapon is automatically chosen"
      , "for attack from your equipment and from among your body parts."
      ]
    minimalBlurb =
      [ "The following few commands, joined with the movement and running keys,"
      , "let you accomplish almost anything in the game, though not necessarily"
      , "with the fewest keystrokes. You can also play the game exclusively"
      , "with a mouse, or both mouse and keyboard (e.g., mouse for go-to"
      , "and terrain inspection and keyboard for everything else). Lastly,"
      , "you can select a command with arrows or mouse directly from the help"
      , "screen or the dashboard and execute it on the spot."
      ]
    itemAllEnding =
      [ "Note how lower case item commands (stash item, equip item) place items"
      , "into a particular item store, while upper case item commands (manage Inventory,"
      , "manage Outfit) open management menu for a store. Once a store menu is opened,"
      , "you can switch stores with `<` and `>`, so the multiple commands only determine"
      , "the starting item store. Each store is accessible from the dashboard as well."
      ]
    mouseBasicsBlurb =
      [ "Screen area and UI mode (exploration/aiming) determine mouse click"
      , "effects. Here we give an overview of effects of each button over"
      , "the game map area. The list includes not only left and right buttons,"
      , "but also the optional middle mouse button (MMB) and the mouse wheel,"
      , "which is also used over menus to move selection. For mice without RMB,"
      , "one can use Control key with LMB and for mice without MMB, one can use"
      , "C-RMB or C-S-LMB."
      ]
    mouseAreasBlurb =
      [ "Next we show mouse button effects per screen area, in exploration and"
      , "(if different) aiming mode. Note that mouse is optional. Keyboard suffices,"
      , "occasionally requiring a lookup for an obscure command key in help screens."
      ]
    mouseAreasMini =
      [ "Mouse button effects per screen area, in exploration and in aiming modes"
      ]
    movTextEnd = "Press SPACE or PGDN to advance or ESC to see the map again."
    lastHelpEnd = "Use PGUP to go back and ESC to see the map again."
    seeAlso = "For more playing instructions see file PLAYING.md."
    offsetCol2 = 12
    pickLeaderDescription =
      [ fmt offsetCol2 "0, 1 ... 9"
                       "pick a particular actor as the new pointman"
      ]
    casualDescription = "Minimal cheat sheet for casual play"
    fmt0 n k h = T.justifyLeft n ' ' k <> " " <> h
    fmt n k h = " " <> fmt0 n k h
    keyCaption = fmt offsetCol2 "keys" "command"
    mouseOverviewCaption = fmt offsetCol2 "keys" "command (exploration/aiming)"
    spLen = textSize monoFont " "
    okxs cat headers footers = xytranslateOKX spLen 0 $
      okxsN coinput monoFont propFont offsetCol2 (const False)
            True cat headers footers
    mergeOKX :: OKX -> OKX -> OKX
    mergeOKX okx1 okx2 =
      let off = 1 + maxYofFontOverlayMap (fst okx1)
      in sideBySideOKX 0 off okx1 okx2
    catLength cat = length $ filter (\(_, (cats, desc, _)) ->
      cat `elem` cats && (desc /= "" || CmdInternal `elem` cats)) bcmdList
    keyM = 13
    keyB = 31
    truncatem b = if T.length b > keyB
                  then T.take (keyB - 1) b <> "$"
                  else b
    fmm a b c = fmt (keyM + 1) a $ fmt0 keyB (truncatem b) (truncatem c)
    areaCaption t = fmm t "LMB (left mouse button)" "RMB (right mouse button)"
    keySel :: (forall a. (a, a) -> a) -> K.KM
           -> [(CmdArea, KeyOrSlot, Text)]
    keySel sel key =
      let cmd = case M.lookup key bcmdMap of
            Just (_, _, cmd2) -> cmd2
            Nothing -> error $ "" `showFailure` key
          caCmds = case cmd of
            ByAimMode AimModeCmd{exploration=ByArea lexp, aiming=ByArea laim} ->
              sort $ sel (lexp, laim \\ lexp)
            _ -> error $ "" `showFailure` cmd
          caMakeChoice (ca, cmd2) =
            let (km, desc) = case M.lookup cmd2 brevMap of
                  Just ks ->
                    let descOfKM km2 = case M.lookup km2 bcmdMap of
                          Just (_, "", _) -> Nothing
                          Just (_, desc2, _) -> Just (km2, desc2)
                          Nothing -> error $ "" `showFailure` km2
                    in case mapMaybe descOfKM ks of
                      [] -> error $ "" `showFailure` (ks, cmd2)
                      kmdesc3 : _ -> kmdesc3
                  Nothing -> (key, "(not described:" <+> tshow cmd2 <> ")")
            in (ca, Left km, desc)
      in map caMakeChoice caCmds
    doubleIfSquare n | isSquareFont monoFont = 2 * n
                     | otherwise = n
    okm :: (forall a. (a, a) -> a) -> K.KM -> K.KM -> [Text] -> OKX
    okm sel key1 key2 header =
      let kst1 = keySel sel key1
          kst2 = keySel sel key2
          f (ca1, Left km1, _) (ca2, Left km2, _) y =
            assert (ca1 == ca2 `blame` (ca1, ca2, km1, km2, kst1, kst2))
              [ (Left km1, ( PointUI (doubleIfSquare $ keyM + 4) y
                           , ButtonWidth monoFont keyB ))
              , (Left km2, ( PointUI (doubleIfSquare $ keyB + keyM + 5) y
                           , ButtonWidth monoFont keyB )) ]
          f c d e = error $ "" `showFailure` (c, d, e)
          kxs = concat $ zipWith3 f kst1 kst2 [1 + length header..]
          menuLeft = map (\(ca1, _, _) -> textToAL $ areaDescription ca1) kst1
          menuMiddle = map (\(_, _, desc) -> textToAL desc) kst1
          menuRight = map (\(_, _, desc) -> textToAL desc) kst2
          y0 = 1 + length header
      in ( EM.unionsWith (++)
             [ typesetInMono $ "" : header
             , EM.singleton monoFont
               $ typesetXY (doubleIfSquare 2, y0) menuLeft
             , EM.singleton propFont
               $ typesetXY (doubleIfSquare $ keyM + 4, y0) menuMiddle
             , EM.singleton propFont
               $ typesetXY (doubleIfSquare $ keyB + keyM + 5, y0) menuRight ]
         , kxs )
    typesetInSquare :: [Text] -> FontOverlayMap
    typesetInSquare =
      EM.singleton squareFont . typesetXY (spLen, 0) . map textToAL
    typesetInMono :: [Text] -> FontOverlayMap
    typesetInMono =
      EM.singleton monoFont . typesetXY (spLen, 0) . map textToAL
    typesetInProp :: [Text] -> FontOverlayMap
    typesetInProp =
      EM.singleton propFont . typesetXY (spLen, 0) . map textToAL
    sideBySide :: [(Text, OKX)] -> [(Text, OKX)]
    sideBySide ((_t1, okx1) : (t2, okx2) : rest) | not (isSquareFont propFont) =
      (t2, sideBySideOKX rwidth 0 okx1 okx2) : sideBySide rest
    sideBySide l = l
  in sideBySide $ concat
    [ if catLength CmdMinimal
         + length movBlurb1 + length movSchema + length movBlurb2
         + length minimalBlurb
         + 6 > rheight then
        [ ( movTextEnd
          , mergeOKX
              (mergeOKX ( typesetInMono ["", casualDescription <+> "(1/2)", ""]
                        , [] )
                        (mergeOKX (typesetInProp movBlurb1, [])
                                  (typesetInSquare $ "" : movSchema, [])))
              (typesetInProp $ "" : movBlurb2, []) )
        , ( movTextEnd
          , okxs CmdMinimal
                 ( ["", casualDescription <+> "(2/2)", ""]
                 , minimalBlurb ++ [""]
                 , [keyCaption] )
                 ([], []) ) ]
      else
        [ ( movTextEnd
          , mergeOKX
              (mergeOKX ( typesetInMono ["", casualDescription, ""]
                        , [] )
                        (mergeOKX (typesetInProp movBlurb1, [])
                                  (typesetInSquare $ "" : movSchema, [])))
              (okxs CmdMinimal
                    ( []
                    , [""] ++ movBlurb2 ++ [""]
                       ++ minimalBlurb ++ [""]
                    , [keyCaption] )
                    ([], [""])) ) ]
    , if 45 > rheight then
        [ ( movTextEnd
          , let (ls, _) = okxs CmdMouse
                               ( ["", "Optional mouse commands", ""]
                               , mouseBasicsBlurb ++ [""]
                               , [mouseOverviewCaption] )
                               ([], [])
            in (ls, []) )  -- don't capture mouse wheel, etc.
        , ( movTextEnd
          , mergeOKX
              (typesetInMono $ "" : mouseAreasMini, [])
              (mergeOKX
                 (okm fst K.leftButtonReleaseKM K.rightButtonReleaseKM
                      [areaCaption "Exploration"])
                 (okm snd K.leftButtonReleaseKM K.rightButtonReleaseKM
                      [areaCaption "Aiming Mode"])) ) ]
      else
        [ ( movTextEnd
          , let (ls, _) = okxs CmdMouse
                               ( ["", "Optional mouse commands", ""]
                               , mouseBasicsBlurb ++ [""]
                               , [mouseOverviewCaption] )
                               ([], [])
                okx0 = (ls, [])  -- don't capture mouse wheel, etc.
            in mergeOKX
                 (mergeOKX
                    okx0
                    (typesetInProp $ "" : mouseAreasBlurb, []))
                 (mergeOKX
                    (okm fst K.leftButtonReleaseKM K.rightButtonReleaseKM
                         [areaCaption "Exploration"])
                    (okm snd K.leftButtonReleaseKM K.rightButtonReleaseKM
                         [areaCaption "Aiming Mode"] )) ) ]
    , if catLength CmdItem + catLength CmdMove + 9 + 9 > rheight then
        [ ( movTextEnd
          , okxs CmdItem
                 (["", categoryDescription CmdItem], [], ["", keyCaption])
                 ([], "" : itemAllEnding) )
        , ( movTextEnd
          , okxs CmdMove
                 (["", categoryDescription CmdMove], [], ["", keyCaption])
                 (pickLeaderDescription, []) ) ]
      else
        [ ( movTextEnd
          , mergeOKX
              (okxs CmdItem
                    (["", categoryDescription CmdItem], [], ["", keyCaption])
                    ([], "" : itemAllEnding))
              (okxs CmdMove
                    ( ["", "", categoryDescription CmdMove]
                    , []
                    , ["", keyCaption] )
                    (pickLeaderDescription, [""])) ) ]
    , if catLength CmdAim + catLength CmdMeta + 9 > rheight then
        [ ( movTextEnd
          , okxs CmdAim
                 (["", categoryDescription CmdAim], [], ["", keyCaption])
                 ([], []) )
        , ( lastHelpEnd
          , okxs CmdMeta
                 (["", categoryDescription CmdMeta], [], ["", keyCaption])
                 ([], ["", seeAlso]) ) ]
      else
        [ ( lastHelpEnd
          , mergeOKX
              (okxs CmdAim
                    (["", categoryDescription CmdAim], [], ["", keyCaption])
                    ([], []))
              (okxs CmdMeta
                    ( ["", "", categoryDescription CmdMeta]
                    , []
                    , ["", keyCaption] )
                    ([], ["", seeAlso, ""])) ) ]
    ]

-- | Turn the specified portion of bindings into a menu.
--
-- The length of the button may be wrong if the two supplied fonts
-- have very different widths.
okxsN :: InputContent -> DisplayFont -> DisplayFont -> Int -> (HumanCmd -> Bool)
      -> Bool -> CmdCategory -> ([Text], [Text], [Text]) -> ([Text], [Text])
      -> OKX
okxsN InputContent{..} labFont descFont offsetCol2 greyedOut
      showManyKeys cat (headerMono1, headerProp, headerMono2)
      (footerMono, footerProp) =
  let fmt k h = (T.singleton '\x00a0' <> k, h)
      coImage :: HumanCmd -> [K.KM]
      coImage cmd = M.findWithDefault (error $ "" `showFailure` cmd) cmd brevMap
      disp = T.intercalate " or " . map (T.pack . K.showKM)
      keyKnown km = case K.key km of
        K.Unknown{} -> False
        _ -> True
      keys :: [(KeyOrSlot, (Bool, (Text, Text)))]
      keys = [ (Left km, (greyedOut cmd, fmt keyNames desc))
             | (_, (cats, desc, cmd)) <- bcmdList
             , let kms = coImage cmd
                   knownKeys = filter keyKnown kms
                   keyNames =
                     disp $ (if showManyKeys then id else take 1) knownKeys
                   kmsRes = if desc == "" then knownKeys else kms
                   km = case kmsRes of
                     [] -> K.escKM
                     km1 : _ -> km1
             , cat `elem` cats
             , desc /= "" || CmdInternal `elem` cats]
      spLen = textSize labFont " "
      f (ks, (_, (_, t2))) y =
        (ks, ( PointUI spLen y
             , ButtonWidth labFont (offsetCol2 + 2 + T.length t2 - 1)))
      kxs = zipWith f keys
              [length headerMono1 + length headerProp + length headerMono2 ..]
      ts = map (\t -> (False, (t, ""))) headerMono1
           ++ map (\t -> (False, ("", t))) headerProp
           ++ map (\t -> (False, (t, ""))) headerMono2
           ++ map snd keys
           ++ map (\t -> (False, (t, ""))) footerMono
           ++ map (\t -> (False, ("", t))) footerProp
      greyToAL (b, (t1, t2)) =
        if b
        then let al1 = textFgToAL Color.BrBlack t1
             in (al1, ( if T.null t1 then 0 else spLen * (offsetCol2 + 2)
                      , textFgToAL Color.BrBlack t2 ))
        else let al1 = textToAL t1
             in (al1, ( if T.null t1 then 0 else spLen * (offsetCol2 + 2)
                      , textToAL t2 ))
      (greyLab, greyDesc) = unzip $ map greyToAL ts
  in ( EM.insertWith (++) descFont (offsetOverlayX greyDesc)
       $ EM.singleton labFont (offsetOverlay greyLab)
     , kxs )