File: InventoryM.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 (783 lines) | stat: -rw-r--r-- 35,792 bytes parent folder | download | duplicates (2)
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
343
344
345
346
347
348
349
350
351
352
353
354
355
356
357
358
359
360
361
362
363
364
365
366
367
368
369
370
371
372
373
374
375
376
377
378
379
380
381
382
383
384
385
386
387
388
389
390
391
392
393
394
395
396
397
398
399
400
401
402
403
404
405
406
407
408
409
410
411
412
413
414
415
416
417
418
419
420
421
422
423
424
425
426
427
428
429
430
431
432
433
434
435
436
437
438
439
440
441
442
443
444
445
446
447
448
449
450
451
452
453
454
455
456
457
458
459
460
461
462
463
464
465
466
467
468
469
470
471
472
473
474
475
476
477
478
479
480
481
482
483
484
485
486
487
488
489
490
491
492
493
494
495
496
497
498
499
500
501
502
503
504
505
506
507
508
509
510
511
512
513
514
515
516
517
518
519
520
521
522
523
524
525
526
527
528
529
530
531
532
533
534
535
536
537
538
539
540
541
542
543
544
545
546
547
548
549
550
551
552
553
554
555
556
557
558
559
560
561
562
563
564
565
566
567
568
569
570
571
572
573
574
575
576
577
578
579
580
581
582
583
584
585
586
587
588
589
590
591
592
593
594
595
596
597
598
599
600
601
602
603
604
605
606
607
608
609
610
611
612
613
614
615
616
617
618
619
620
621
622
623
624
625
626
627
628
629
630
631
632
633
634
635
636
637
638
639
640
641
642
643
644
645
646
647
648
649
650
651
652
653
654
655
656
657
658
659
660
661
662
663
664
665
666
667
668
669
670
671
672
673
674
675
676
677
678
679
680
681
682
683
684
685
686
687
688
689
690
691
692
693
694
695
696
697
698
699
700
701
702
703
704
705
706
707
708
709
710
711
712
713
714
715
716
717
718
719
720
721
722
723
724
725
726
727
728
729
730
731
732
733
734
735
736
737
738
739
740
741
742
743
744
745
746
747
748
749
750
751
752
753
754
755
756
757
758
759
760
761
762
763
764
765
766
767
768
769
770
771
772
773
774
775
776
777
778
779
780
781
782
783
-- | UI of inventory management.
module Game.LambdaHack.Client.UI.InventoryM
  ( Suitability(..), ResultItemDialogMode(..)
  , getFull, getGroupItem, getStoreItem
  , skillCloseUp, placeCloseUp, factionCloseUp
#ifdef EXPOSE_INTERNAL
    -- * Internal operations
  , ItemDialogState(..), accessModeBag, storeItemPrompt, getItem
  , DefItemKey(..), transition
  , runDefMessage, runDefAction, runDefSkills, skillsInRightPane
  , runDefPlaces, placesInRightPane
  , runDefFactions, factionsInRightPane
  , runDefModes, runDefInventory
#endif
  ) where

import Prelude ()

import Game.LambdaHack.Core.Prelude

import           Data.Either
import qualified Data.EnumMap.Strict as EM
import qualified Data.EnumSet as ES
import           Data.Function
import qualified Data.Text as T
import qualified NLP.Miniutter.English as MU

import           Game.LambdaHack.Client.MonadClient
import           Game.LambdaHack.Client.State
import           Game.LambdaHack.Client.UI.ActorUI
import           Game.LambdaHack.Client.UI.Content.Screen
import           Game.LambdaHack.Client.UI.ContentClientUI
import           Game.LambdaHack.Client.UI.EffectDescription
import           Game.LambdaHack.Client.UI.HandleHelperM
import           Game.LambdaHack.Client.UI.HumanCmd
import qualified Game.LambdaHack.Client.UI.Key as K
import           Game.LambdaHack.Client.UI.MonadClientUI
import           Game.LambdaHack.Client.UI.Msg
import           Game.LambdaHack.Client.UI.MsgM
import           Game.LambdaHack.Client.UI.Overlay
import           Game.LambdaHack.Client.UI.SessionUI
import           Game.LambdaHack.Client.UI.Slideshow
import           Game.LambdaHack.Client.UI.SlideshowM
import           Game.LambdaHack.Common.Actor
import           Game.LambdaHack.Common.ActorState
import           Game.LambdaHack.Common.ClientOptions
import           Game.LambdaHack.Common.Faction
import qualified Game.LambdaHack.Common.Faction as Faction
import           Game.LambdaHack.Common.Item
import           Game.LambdaHack.Common.Kind
import           Game.LambdaHack.Common.Misc
import           Game.LambdaHack.Common.MonadStateRead
import           Game.LambdaHack.Common.State
import           Game.LambdaHack.Common.Types
import qualified Game.LambdaHack.Content.FactionKind as FK
import qualified Game.LambdaHack.Content.ItemKind as IK
import qualified Game.LambdaHack.Content.PlaceKind as PK
import qualified Game.LambdaHack.Definition.Ability as Ability
import qualified Game.LambdaHack.Definition.Color as Color
import           Game.LambdaHack.Definition.Defs

data ItemDialogState = ISuitable | IAll
  deriving (Show, Eq)

data ResultItemDialogMode =
    RStore CStore [ItemId]
  | ROwned ItemId
  | RLore SLore MenuSlot [(ItemId, ItemQuant)]
  | RSkills MenuSlot
  | RPlaces MenuSlot
  | RFactions MenuSlot
  | RModes MenuSlot
  deriving Show

accessModeBag :: ActorId -> State -> ItemDialogMode -> ItemBag
accessModeBag leader s (MStore cstore) = let b = getActorBody leader s
                                         in getBodyStoreBag b cstore s
accessModeBag leader s MOwned = let fid = bfid $ getActorBody leader s
                                in combinedItems fid s
accessModeBag _ _ MSkills = EM.empty
accessModeBag leader s (MLore SBody) = let b = getActorBody leader s
                                       in getBodyStoreBag b COrgan s
accessModeBag _ s MLore{} = EM.map (const quantSingle) $ sitemD s
accessModeBag _ _ MPlaces = EM.empty
accessModeBag _ _ MFactions = EM.empty
accessModeBag _ _ MModes = EM.empty

-- | Let a human player choose any item from a given group.
-- Note that this does not guarantee the chosen item belongs to the group,
-- as the player can override the choice.
-- Used e.g., for applying and projecting.
getGroupItem :: MonadClientUI m
             => ActorId
             -> m Suitability
                          -- ^ which items to consider suitable
             -> Text      -- ^ specific prompt for only suitable items
             -> Text      -- ^ generic prompt
             -> Text      -- ^ the verb to use
             -> Text      -- ^ the generic verb to use
             -> [CStore]  -- ^ stores to cycle through
             -> m (Either Text (CStore, ItemId))
getGroupItem leader psuit prompt promptGeneric verb verbGeneric stores = do
  side <- getsClient sside
  mstash <- getsState $ \s -> gstash $ sfactionD s EM.! side
  let ppItemDialogBody v body actorSk cCur = case cCur of
        MStore CEqp | not $ calmEnough body actorSk ->
          "distractedly attempt to" <+> v <+> ppItemDialogModeIn cCur
        MStore CGround | mstash == Just (blid body, bpos body) ->
          "greedily attempt to" <+> v <+> ppItemDialogModeIn cCur
        _ -> v <+> ppItemDialogModeFrom cCur
  soc <- getFull leader psuit
                 (\body _ actorSk cCur _ ->
                    prompt <+> ppItemDialogBody verb body actorSk cCur)
                 (\body _ actorSk cCur _ ->
                    promptGeneric
                    <+> ppItemDialogBody verbGeneric body actorSk cCur)
                 stores True False
  case soc of
    Left err -> return $ Left err
    Right (rstore, [(iid, _)]) -> return $ Right (rstore, iid)
    Right _ -> error $ "" `showFailure` soc

-- | Display all items from a store and let the human player choose any
-- or switch to any other store.
-- Used, e.g., for viewing inventory and item descriptions.
getStoreItem :: MonadClientUI m
             => ActorId         -- ^ the pointman
             -> ItemDialogMode  -- ^ initial mode
             -> m (Either Text ResultItemDialogMode)
getStoreItem leader cInitial = do
  side <- getsClient sside
  let -- No @COrgan@, because triggerable organs are rare and,
      -- if really needed, accessible directly from the trigger menu.
      itemCs = map MStore [CStash, CEqp, CGround]
      -- This should match, including order, the items in standardKeysAndMouse
      -- marked with CmdDashboard up to @MSkills@.
      leaderCs = itemCs ++ [MOwned, MLore SBody, MSkills]
      -- No @SBody@, because repeated in other lores and included elsewhere.
      itemLoreCs = map MLore [minBound..SEmbed]
      -- This should match, including order, the items in standardKeysAndMouse
      -- marked with CmdDashboard past @MSkills@ and up to @MModes@.
      loreCs = itemLoreCs ++ [MPlaces, MFactions, MModes]
  let !_A1 = assert (null (leaderCs `intersect` loreCs)) ()
      !_A2 = assert (sort (leaderCs ++ loreCs ++ [MStore COrgan])
                     == map MStore [minBound..maxBound]
                        ++ [MOwned, MSkills]
                        ++ map MLore [minBound..maxBound]
                        ++ [MPlaces, MFactions, MModes]) ()
      allCs | cInitial `elem` leaderCs = leaderCs
            | cInitial `elem` loreCs = loreCs
            | otherwise = assert (cInitial == MStore COrgan) leaderCs
                            -- werrd content, but let it be
      (pre, rest) = break (== cInitial) allCs
      post = dropWhile (== cInitial) rest
      remCs = post ++ pre
      prompt = storeItemPrompt side
  getItem leader (return SuitsEverything) prompt prompt cInitial remCs
          True False

storeItemPrompt :: FactionId
                -> Actor -> ActorUI -> Ability.Skills -> ItemDialogMode -> State
                -> Text
storeItemPrompt side body bodyUI actorCurAndMaxSk c2 s =
  let COps{coitem} = scops s
      fact = sfactionD s EM.! side
      (tIn, t) = ppItemDialogMode c2
      subject = partActor bodyUI
      f (k, _) acc = k + acc
      countItems store = EM.foldr' f 0 $ getBodyStoreBag body store s
  in case c2 of
    MStore CGround ->
      let n = countItems CGround
          nItems = MU.CarAWs n "item"
          verbGround = if gstash fact == Just (blid body, bpos body)
                       then "fondle greedily"
                       else "notice"
      in makePhrase
           [ MU.Capitalize $ MU.SubjectVerbSg subject verbGround
           , nItems, "at"
           , MU.WownW (MU.Text $ bpronoun bodyUI) $ MU.Text "feet" ]
    MStore CEqp ->
      let n = countItems CEqp
          (verbEqp, nItems) =
            if | n == 0 -> ("find nothing", "")
               | calmEnough body actorCurAndMaxSk ->
                   ("find", MU.CarAWs n "item")
               | otherwise -> ("paw distractedly at", MU.CarAWs n "item")
      in makePhrase
           [ MU.Capitalize $ MU.SubjectVerbSg subject verbEqp
           , nItems, MU.Text tIn
           , MU.WownW (MU.Text $ bpronoun bodyUI) $ MU.Text t ]
    MStore cstore ->
      let n = countItems cstore
          nItems = MU.CarAWs n "item"
          (verb, onLevel) = case cstore of
            COrgan -> ("feel", [])
            CStash ->
              ( "notice"
              , case gstash fact of
                  Just (lid, _) ->
                    map MU.Text ["on level", tshow $ abs $ fromEnum lid]
                  Nothing -> [] )
          ownObject = case cstore of
            CStash -> ["our", MU.Text t]
            _ -> [MU.WownW (MU.Text $ bpronoun bodyUI) $ MU.Text t]
      in makePhrase $
           [ MU.Capitalize $ MU.SubjectVerbSg subject verb
           , nItems, MU.Text tIn ] ++ ownObject ++ onLevel
    MOwned ->
      -- We assume "gold grain", not "grain" with label "of gold":
      let currencyName = IK.iname $ okind coitem
                         $ ouniqGroup coitem IK.S_CURRENCY
          dungeonTotal = sgold s
          (_, total) = calculateTotal side s
      in T.init $ spoilsBlurb currencyName total dungeonTotal
        -- no space for more, e.g., the pointman, but it can't be changed anyway
    MSkills ->
      makePhrase
        [ MU.Capitalize $ MU.SubjectVerbSg subject "estimate"
        , MU.WownW (MU.Text $ bpronoun bodyUI) $ MU.Text t ]
    MLore SBody ->
      makePhrase
        [ MU.Capitalize $ MU.SubjectVerbSg subject "feel"
        , MU.Text tIn
        , MU.WownW (MU.Text $ bpronoun bodyUI) $ MU.Text t ]
    MLore slore ->
      makePhrase
        [ MU.Capitalize $ MU.Text $
            if slore == SEmbed
            then "terrain (including crafting recipes)"
            else t ]
    MPlaces ->
      makePhrase
        [ MU.Capitalize $ MU.Text t ]
    MFactions ->
      makePhrase
        [ MU.Capitalize $ MU.Text t ]
    MModes ->
      makePhrase
        [ MU.Capitalize $ MU.Text t ]

-- | Let the human player choose a single, preferably suitable,
-- item from a list of items. Don't display stores empty for all actors.
-- Start with a non-empty store.
getFull :: MonadClientUI m
        => ActorId
        -> m Suitability    -- ^ which items to consider suitable
        -> (Actor -> ActorUI -> Ability.Skills -> ItemDialogMode -> State
            -> Text)        -- ^ specific prompt for only suitable items
        -> (Actor -> ActorUI -> Ability.Skills -> ItemDialogMode -> State
            -> Text)        -- ^ generic prompt
        -> [CStore]         -- ^ stores to cycle through
        -> Bool             -- ^ whether to ask, when the only item
                            --   in the starting mode is suitable
        -> Bool             -- ^ whether to permit multiple items as a result
        -> m (Either Text (CStore, [(ItemId, ItemQuant)]))
getFull leader psuit prompt promptGeneric stores askWhenLone permitMulitple = do
  mpsuit <- psuit
  let psuitFun = case mpsuit of
        SuitsEverything -> \_ _ _ -> True
        SuitsSomething f -> f
  -- Move the first store that is non-empty for suitable items for this actor
  -- to the front, if any.
  b <- getsState $ getActorBody leader
  getCStoreBag <- getsState $ \s cstore -> getBodyStoreBag b cstore s
  let hasThisActor = not . EM.null . getCStoreBag
  case filter hasThisActor stores of
    [] -> do
      let dialogModes = map MStore stores
          ts = map (MU.Text . ppItemDialogModeIn) dialogModes
      return $ Left $ "no items" <+> makePhrase [MU.WWxW "nor" ts]
    haveThis@(headThisActor : _) -> do
      itemToF <- getsState $ flip itemToFull
      let suitsThisActor store =
            let bag = getCStoreBag store
            in any (\(iid, kit) -> psuitFun (Just store) (itemToF iid) kit)
                   (EM.assocs bag)
          firstStore = fromMaybe headThisActor $ find suitsThisActor haveThis
          -- Don't display stores totally empty for all actors.
          breakStores cInit =
            let (pre, rest) = break (== cInit) stores
                post = dropWhile (== cInit) rest
            in (MStore cInit, map MStore $ post ++ pre)
          (modeFirst, modeRest) = breakStores firstStore
      res <- getItem leader psuit prompt promptGeneric modeFirst modeRest
                     askWhenLone permitMulitple
      case res of
        Left t -> return $ Left t
        Right (RStore fromCStore iids) -> do
          let bagAll = getCStoreBag fromCStore
              f iid = (iid, bagAll EM.! iid)
          return $ Right (fromCStore, map f iids)
        Right _ -> error $ "" `showFailure` res

-- | Let the human player choose a single, preferably suitable,
-- item from a list of items.
getItem :: MonadClientUI m
        => ActorId
        -> m Suitability    -- ^ which items to consider suitable
        -> (Actor -> ActorUI -> Ability.Skills -> ItemDialogMode -> State
            -> Text)        -- ^ specific prompt for only suitable items
        -> (Actor -> ActorUI -> Ability.Skills -> ItemDialogMode -> State
            -> Text)        -- ^ generic prompt
        -> ItemDialogMode   -- ^ first mode to display
        -> [ItemDialogMode] -- ^ the (rest of) modes
        -> Bool             -- ^ whether to ask, when the only item
                            --   in the starting mode is suitable
        -> Bool             -- ^ whether to permit multiple items as a result
        -> m (Either Text ResultItemDialogMode)
getItem leader psuit prompt promptGeneric cCur cRest askWhenLone
        permitMulitple = do
  accessCBag <- getsState $ accessModeBag leader
  let storeAssocs = EM.assocs . accessCBag
      allAssocs = concatMap storeAssocs (cCur : cRest)
  case (allAssocs, cCur) of
    ([(iid, _)], MStore rstore) | null cRest && not askWhenLone ->
      return $ Right $ RStore rstore [iid]
    _ -> transition leader psuit prompt promptGeneric permitMulitple
                    cCur cRest ISuitable

data DefItemKey m = DefItemKey
  { defLabel  :: Either Text K.KM
  , defCond   :: Bool
  , defAction :: ~(m (Either Text ResultItemDialogMode))
      -- this field may be expensive or undefined when @defCond@ is false
  }

data Suitability =
    SuitsEverything
  | SuitsSomething (Maybe CStore -> ItemFull -> ItemQuant -> Bool)

transition :: forall m. MonadClientUI m
           => ActorId
           -> m Suitability
           -> (Actor -> ActorUI -> Ability.Skills -> ItemDialogMode -> State
               -> Text)
           -> (Actor -> ActorUI -> Ability.Skills -> ItemDialogMode -> State
               -> Text)
           -> Bool
           -> ItemDialogMode
           -> [ItemDialogMode]
           -> ItemDialogState
           -> m (Either Text ResultItemDialogMode)
transition leader psuit prompt promptGeneric permitMulitple
           cCur cRest itemDialogState = do
  let recCall cCur2 cRest2 itemDialogState2 = do
        -- Pointman could have been changed by keypresses near the end of
        -- the current recursive call, so refresh it for the next call.
        mleader <- getsClient sleader
        -- When run inside a test, without mleader, assume leader not changed.
        let leader2 = fromMaybe leader mleader
        transition leader2 psuit prompt promptGeneric permitMulitple
                   cCur2 cRest2 itemDialogState2
  actorCurAndMaxSk <- getsState $ getActorMaxSkills leader
  body <- getsState $ getActorBody leader
  bodyUI <- getsSession $ getActorUI leader
  fact <- getsState $ (EM.! bfid body) . sfactionD
  hs <- partyAfterLeader leader
  revCmd <- revCmdMap
  promptChosen <- getsState $ \s -> case itemDialogState of
    ISuitable -> prompt body bodyUI actorCurAndMaxSk cCur s <> ":"
    IAll -> promptGeneric body bodyUI actorCurAndMaxSk cCur s <> ":"
  let keyDefsCommon :: [(K.KM, DefItemKey m)]
      keyDefsCommon = filter (defCond . snd)
        [ let km = K.mkChar '<'
          in (km, changeContainerDef Backward $ Right km)
        , let km = K.mkChar '>'
          in (km, changeContainerDef Forward $ Right km)
        , cycleKeyDef Forward
        , cycleKeyDef Backward
        , cycleLevelKeyDef Forward
        , cycleLevelKeyDef Backward
        , (K.KM K.NoModifier K.LeftButtonRelease, DefItemKey
           { defLabel = Left ""
           , defCond = maySwitchLeader cCur && not (null hs)
           , defAction = do
               -- This is verbose even in aiming mode, displaying
               -- terrain description, but it's fine, mouse may do that.
               merror <- pickLeaderWithPointer leader
               case merror of
                 Nothing -> recCall cCur cRest itemDialogState
                 Just{} -> return $ Left "not a menu item nor teammate position"
                             -- don't inspect the error, it's expected
           })
        , (K.escKM, DefItemKey
           { defLabel = Right K.escKM
           , defCond = True
           , defAction = return $ Left "never mind"
           })
        ]
      cycleLevelKeyDef direction =
        let km = revCmd $ PointmanCycleLevel direction
        in (km, DefItemKey
                { defLabel = Left ""
                , defCond = maySwitchLeader cCur
                            && any (\(_, b, _) -> blid b == blid body) hs
                , defAction = do
                    err <- pointmanCycleLevel leader False direction
                    let !_A = assert (isNothing err `blame` err) ()
                    recCall cCur cRest itemDialogState
                })
      changeContainerDef direction defLabel =
        let (cCurAfterCalm, cRestAfterCalm) = nextContainers direction
        in DefItemKey
          { defLabel
          , defCond = cCurAfterCalm /= cCur
          , defAction = recCall cCurAfterCalm cRestAfterCalm itemDialogState
          }
      nextContainers direction = case direction of
        Forward -> case cRest ++ [cCur] of
          c1 : rest -> (c1, rest)
          [] -> error $ "" `showFailure` cRest
        Backward -> case reverse $ cCur : cRest of
          c1 : rest -> (c1, reverse rest)
          [] -> error $ "" `showFailure` cRest
      banned = bannedPointmanSwitchBetweenLevels fact
      maySwitchLeader MStore{} = True
      maySwitchLeader MOwned = False
      maySwitchLeader MSkills = True
      maySwitchLeader (MLore SBody) = True
      maySwitchLeader MLore{} = False
      maySwitchLeader MPlaces = False
      maySwitchLeader MFactions = False
      maySwitchLeader MModes = False
      cycleKeyDef direction =
        let km = revCmd $ PointmanCycle direction
        in (km, DefItemKey
               { defLabel = if direction == Forward then Right km else Left ""
               , defCond = maySwitchLeader cCur && not (banned || null hs)
               , defAction = do
                   err <- pointmanCycle leader False direction
                   let !_A = assert (isNothing err `blame` err) ()
                   recCall cCur cRest itemDialogState
               })
  case cCur of
    MSkills -> runDefSkills keyDefsCommon promptChosen leader
    MPlaces -> runDefPlaces keyDefsCommon promptChosen
    MFactions -> runDefFactions keyDefsCommon promptChosen
    MModes -> runDefModes keyDefsCommon promptChosen
    _ -> do
      bagHuge <- getsState $ \s -> accessModeBag leader s cCur
      itemToF <- getsState $ flip itemToFull
      mpsuit <- psuit  -- when throwing, this sets eps and checks xhair validity
      psuitFun <- case mpsuit of
        SuitsEverything -> return $ \_ _ _ -> True
        SuitsSomething f -> return f  -- When throwing, this function takes
                                      -- missile range into accout.
      ItemRoles itemRoles <- getsSession sroles
      let slore = loreFromMode cCur
          itemRole = itemRoles EM.! slore
          bagAll = EM.filterWithKey (\iid _ -> iid `ES.member` itemRole) bagHuge
          mstore = case cCur of
            MStore store -> Just store
            _ -> Nothing
          filterP = psuitFun mstore . itemToF
          bagSuit = EM.filterWithKey filterP bagAll
          bagFiltered = case itemDialogState of
            ISuitable -> bagSuit
            IAll -> bagAll
          iids = sortIids itemToF $ EM.assocs bagFiltered
          keyDefsExtra =
            [ let km = K.mkChar '+'
              in (km, DefItemKey
               { defLabel = Right km
               , defCond = bagAll /= bagSuit
               , defAction = recCall cCur cRest $ case itemDialogState of
                                                    ISuitable -> IAll
                                                    IAll -> ISuitable
               })
            , let km = K.mkChar '*'
              in (km, useMultipleDef $ Right km)
            , let km = K.mkChar '!'
              in (km, useMultipleDef $ Left "")  -- alias close to 'g'
            ]
          useMultipleDef defLabel = DefItemKey
            { defLabel
            , defCond = permitMulitple && not (null iids)
            , defAction = case cCur of
                MStore rstore -> return $! Right $ RStore rstore $ map fst iids
                _ -> error "transition: multiple items not for MStore"
            }
          keyDefs = keyDefsCommon ++ filter (defCond . snd) keyDefsExtra
      runDefInventory keyDefs promptChosen leader cCur iids

runDefMessage :: MonadClientUI m
              => [(K.KM, DefItemKey m)]
              -> Text
              -> m ()
runDefMessage keyDefs prompt = do
  let wrapB s = "[" <> s <> "]"
      keyLabelsRaw = lefts $ map (defLabel . snd) keyDefs
      keyLabels = filter (not . T.null) keyLabelsRaw
      choice = T.intercalate " " $ map wrapB $ nub keyLabels
        -- switch to Data.Containers.ListUtils.nubOrd when we drop GHC 8.4.4
  msgAdd MsgPromptGeneric $ prompt <+> choice

runDefAction :: MonadClientUI m
             => [(K.KM, DefItemKey m)]
             -> (MenuSlot -> Either Text ResultItemDialogMode)
             -> KeyOrSlot
             -> m (Either Text ResultItemDialogMode)
runDefAction keyDefs slotDef ekm = case ekm of
  Left km -> case km `lookup` keyDefs of
    Just keyDef -> defAction keyDef
    Nothing -> error $ "unexpected key:" `showFailure` K.showKM km
  Right slot -> return $! slotDef slot

runDefSkills :: MonadClientUI m
             => [(K.KM, DefItemKey m)] -> Text -> ActorId
             -> m (Either Text ResultItemDialogMode)
runDefSkills keyDefsCommon promptChosen leader = do
  CCUI{coscreen=ScreenContent{rheight}} <- getsSession sccui
  runDefMessage keyDefsCommon promptChosen
  let itemKeys = map fst keyDefsCommon
      keys = rights $ map (defLabel . snd) keyDefsCommon
  okx <- skillsOverlay leader
  sli <- overlayToSlideshow (rheight - 2) keys okx
  ekm <- displayChoiceScreenWithDefItemKey
           (skillsInRightPane leader) sli itemKeys (show MSkills)
  runDefAction keyDefsCommon (Right . RSkills) ekm

skillsInRightPane :: MonadClientUI m => ActorId -> Int -> MenuSlot -> m OKX
skillsInRightPane leader width slot = do
  FontSetup{propFont} <- getFontSetup
  (prompt, attrString) <- skillCloseUp leader slot
  let promptAS | T.null prompt = []
               | otherwise = textFgToAS Color.Brown $ prompt <> "\n\n"
      ov = EM.singleton propFont $ offsetOverlay
                                 $ splitAttrString width width
                                 $ promptAS ++ attrString
  return (ov, [])

runDefPlaces :: MonadClientUI m
             => [(K.KM, DefItemKey m)] -> Text
             -> m (Either Text ResultItemDialogMode)
runDefPlaces keyDefsCommon promptChosen = do
  COps{coplace} <- getsState scops
  CCUI{coscreen=ScreenContent{rheight}} <- getsSession sccui
  soptions <- getsClient soptions
  places <- getsState $ EM.assocs
                      . placesFromState coplace (sexposePlaces soptions)
  runDefMessage keyDefsCommon promptChosen
  let itemKeys = map fst keyDefsCommon
      keys = rights $ map (defLabel . snd) keyDefsCommon
  okx <- placesOverlay
  sli <- overlayToSlideshow (rheight - 2) keys okx
  ekm <- displayChoiceScreenWithDefItemKey
           (placesInRightPane places) sli itemKeys (show MPlaces)
  runDefAction keyDefsCommon (Right . RPlaces) ekm

placesInRightPane :: MonadClientUI m
                  => [( ContentId PK.PlaceKind
                      , (ES.EnumSet LevelId, Int, Int, Int) )]
                  -> Int -> MenuSlot
                  -> m OKX
placesInRightPane places width slot = do
  FontSetup{propFont} <- getFontSetup
  soptions <- getsClient soptions
  (prompt, blurbs) <- placeCloseUp places (sexposePlaces soptions) slot
  let promptAS | T.null prompt = []
               | otherwise = textFgToAS Color.Brown $ prompt <> "\n\n"
      splitText = splitAttrString width width
      ov = attrLinesToFontMap
           $ map (second $ concatMap splitText)
           $ (propFont, [promptAS]) : blurbs
  return (ov, [])

runDefFactions :: MonadClientUI m
               => [(K.KM, DefItemKey m)] -> Text
               -> m (Either Text ResultItemDialogMode)
runDefFactions keyDefsCommon promptChosen = do
  CCUI{coscreen=ScreenContent{rheight}} <- getsSession sccui
  sroles <- getsSession sroles
  factions <- getsState $ factionsFromState sroles
  runDefMessage keyDefsCommon promptChosen
  let itemKeys = map fst keyDefsCommon
      keys = rights $ map (defLabel . snd) keyDefsCommon
  okx <- factionsOverlay
  sli <- overlayToSlideshow (rheight - 2) keys okx
  ekm <- displayChoiceScreenWithDefItemKey
           (factionsInRightPane factions)
           sli itemKeys (show MFactions)
  runDefAction keyDefsCommon (Right . RFactions) ekm

factionsInRightPane :: MonadClientUI m
                    => [(FactionId, Faction)]
                    -> Int -> MenuSlot
                    -> m OKX
factionsInRightPane factions width slot = do
  FontSetup{propFont} <- getFontSetup
  (prompt, blurbs) <- factionCloseUp factions slot
  let promptAS | T.null prompt = []
               | otherwise = textFgToAS Color.Brown $ prompt <> "\n\n"
      splitText = splitAttrString width width
      ov = attrLinesToFontMap
           $ map (second $ concatMap splitText)
           $ (propFont, [promptAS]) : blurbs
  return (ov, [])

runDefModes :: MonadClientUI m
            => [(K.KM, DefItemKey m)] -> Text
            -> m (Either Text ResultItemDialogMode)
runDefModes keyDefsCommon promptChosen = do
  CCUI{coscreen=ScreenContent{rheight}} <- getsSession sccui
  runDefMessage keyDefsCommon promptChosen
  let itemKeys = map fst keyDefsCommon
      keys = rights $ map (defLabel . snd) keyDefsCommon
  okx <- modesOverlay
  sli <- overlayToSlideshow (rheight - 2) keys okx
  -- Modes would cover the whole screen, so we don't display in right pane.
  -- But we display and highlight menu bullets.
  ekm <- displayChoiceScreenWithDefItemKey
           (\_ _ -> return emptyOKX) sli itemKeys (show MModes)
  runDefAction keyDefsCommon (Right . RModes) ekm

runDefInventory :: MonadClientUI m
                => [(K.KM, DefItemKey m)]
                -> Text
                -> ActorId
                -> ItemDialogMode
                -> [(ItemId, ItemQuant)]
                -> m (Either Text ResultItemDialogMode)
runDefInventory keyDefs promptChosen leader dmode iids = do
  CCUI{coscreen=ScreenContent{rheight}} <- getsSession sccui
  actorCurAndMaxSk <- getsState $ getActorMaxSkills leader
  let meleeSkill = Ability.getSk Ability.SkHurtMelee actorCurAndMaxSk
      slotDef :: MenuSlot -> Either Text ResultItemDialogMode
      slotDef slot =
        let iid = fst $ iids !! fromEnum slot
        in Right $ case dmode of
          MStore rstore -> RStore rstore [iid]
          MOwned -> ROwned iid
          MLore rlore -> RLore rlore slot iids
          _ -> error $ "" `showFailure` dmode
      promptFun _iid _itemFull _k = ""
        -- TODO, e.g., if the party still owns any copies, if the actor
        -- was ever killed by us or killed ours, etc.
        -- This can be the same prompt or longer than what entering
        -- the item screen shows.
  runDefMessage keyDefs promptChosen
  let itemKeys = map fst keyDefs
      keys = rights $ map (defLabel . snd) keyDefs
  okx <- itemOverlay iids dmode
  sli <- overlayToSlideshow (rheight - 2) keys okx
  ekm <- displayChoiceScreenWithDefItemKey
           (okxItemLoreInline promptFun meleeSkill dmode iids)
           sli itemKeys (show dmode)
  runDefAction keyDefs slotDef ekm

skillCloseUp :: MonadClientUI m => ActorId -> MenuSlot -> m (Text, AttrString)
skillCloseUp leader slot = do
  b <- getsState $ getActorBody leader
  bUI <- getsSession $ getActorUI leader
  actorCurAndMaxSk <- getsState $ getActorMaxSkills leader
  let skill = skillsInDisplayOrder !! fromEnum slot
      valueText = skillToDecorator skill b
                  $ Ability.getSk skill actorCurAndMaxSk
      prompt = makeSentence
        [ MU.WownW (partActor bUI) (MU.Text $ skillName skill)
        , "is", MU.Text valueText ]
      attrString = textToAS $ skillDesc skill
  return (prompt, attrString)

placeCloseUp :: MonadClientUI m
             => [(ContentId PK.PlaceKind, (ES.EnumSet LevelId, Int, Int, Int))]
             -> Bool
             -> MenuSlot
             -> m (Text, [(DisplayFont, [AttrString])])
placeCloseUp places sexposePlaces slot = do
  COps{coplace} <- getsState scops
  FontSetup{..} <- getFontSetup
  let (pk, (es, ne, na, _)) = places !! fromEnum slot
      pkind = okind coplace pk
      prompt = makeSentence ["you remember", MU.Text $ PK.pname pkind]
      freqsText = "Frequencies:" <+> T.intercalate " "
        (map (\(grp, n) -> "(" <> displayGroupName grp
                           <> ", " <> tshow n <> ")")
         $ PK.pfreq pkind)
      onLevels | ES.null es = []
               | otherwise = [makeSentence
                               [ "Appears on"
                               , MU.CarWs (ES.size es) "level" <> ":"
                               , MU.WWandW $ map MU.Car $ sort
                                 $ map (abs . fromEnum) $ ES.elems es ]]
      placeParts = ["it has" | ne > 0 || na > 0]
                   ++ [MU.CarWs ne "entrance" | ne > 0]
                   ++ ["and" | ne > 0 && na > 0]
                   ++ [MU.CarWs na "surrounding" | na > 0]
      partsSentence | null placeParts = []
                    | otherwise = [makeSentence placeParts, "\n"]
      blurbs = [(propFont, partsSentence)]
               ++ [(monoFont, [freqsText, "\n"]) | sexposePlaces]
               ++ [(squareFont, PK.ptopLeft pkind ++ ["\n"]) | sexposePlaces]
               ++ [(propFont, onLevels)]
  return (prompt, map (second $ map textToAS) blurbs)

factionCloseUp :: MonadClientUI m
               => [(FactionId, Faction)]
               -> MenuSlot
               -> m (Text, [(DisplayFont, [AttrString])])
factionCloseUp factions slot = do
  side <- getsClient sside
  FontSetup{propFont} <- getFontSetup
  factionD <- getsState sfactionD
  let (fid, fact@Faction{gkind=FK.FactionKind{..}, ..}) =
        factions !! fromEnum slot
      (name, person) = if fhasGender  -- but we ignore "Controlled", etc.
                       then (makePhrase [MU.Ws $ MU.Text fname], MU.PlEtc)
                       else (fname, MU.Sg3rd)
      (youThey, prompt) =
        if fid == side
        then ("You", makeSentence  ["you are the", MU.Text name])
        else ("They", makeSentence ["you are wary of the", MU.Text name])
               -- wary even if the faction is allied
      ts1 =
        -- Display only the main groups, not to spam.
        case map fst $ filter ((>= 100) . snd) fgroups of
          [] -> []  -- only initial actors in the faction?
          [fgroup] ->
            [makeSentence [ "the faction consists of"
                          , MU.Ws $ MU.Text $ displayGroupName fgroup ]]
          grps -> [makeSentence
                    [ "the faction attracts members such as:"
                    ,  MU.WWandW $ map (MU.Text . displayGroupName) grps ]]
        ++ [if fskillsOther == Ability.zeroSkills  -- simplified
            then youThey <+> "don't care about each other and crowd and stampede all at once, sometimes brutally colliding by accident."
            else youThey <+> "pay attention to each other and take care to move one at a time."]
        ++ [ if fcanEscape
             then "The faction is able to take part in races to an area exit."
             else "The faction doesn't escape areas of conflict and attempts to block exits instead."]
        ++ [ "When all members are incapacitated, the faction dissolves."
           | fneverEmpty ]
        ++ [if fhasGender
            then "Its members are known to have sexual dimorphism and use gender pronouns."
            else "Its members seem to prefer naked ground for sleeping."]
        ++ [ "Its ranks swell with time."
           | fspawnsFast ]
        ++ [ "The faction is able to maintain activity on a level on its own, with a pointman coordinating each tactical maneuver."
           | fhasPointman ]
      -- Changes to all of these have visibility @PosAll@, so the player
      -- knows them fully, except for @gvictims@, which is coupled to tracking
      -- other factions' actors and so only incremented when we've seen
      -- their actor killed (mostly likely killed by us).
      ts2 =  -- reporting regardless of whether any of the factions are dead
        let renderDiplGroup [] = error "renderDiplGroup: null"
            renderDiplGroup ((fid2, diplomacy) : rest) = MU.Phrase
              [ MU.Text $ tshowDiplomacy diplomacy
              , "with"
              , MU.WWandW $ map renderFact2 $ fid2 : map fst rest ]
            renderFact2 fid2 = MU.Text $ Faction.gname (factionD EM.! fid2)
            valid (fid2, diplomacy) = isJust (lookup fid2 factions)
                                      && diplomacy /= Unknown
            knownAssocsGroups = groupBy ((==) `on` snd) $ sortOn snd
                                $ filter valid $ EM.assocs gdipl
        in [ makeSentence [ MU.SubjectVerb person MU.Yes (MU.Text name) "be"
                          , MU.WWandW $ map renderDiplGroup knownAssocsGroups ]
           | not (null knownAssocsGroups) ]
      ts3 =
        case gquit of
          Just Status{..} | not $ isHorrorFact fact ->
            ["The faction has already" <+> FK.nameOutcomePast stOutcome
             <+> "around level" <+> tshow (abs stDepth) <> "."]
          _ -> []
        ++ let nkilled = sum $ EM.elems gvictims
               personKilled = if nkilled == 1 then MU.Sg3rd else MU.PlEtc
           in [ makeSentence $
                  [ "so far," | isNothing gquit ]
                  ++ [ "at least"
                     , MU.CardinalWs nkilled "member"
                     , MU.SubjectVerb personKilled
                                      MU.Yes
                                      "of this faction"
                                      "have been incapacitated" ]
              | nkilled > 0 ]
        ++ let adjective = if isNothing gquit then "current" else "last"
               verb = if isNothing gquit then "is" else "was"
           in ["Its" <+> adjective <+> "doctrine" <+> verb
               <+> "'" <> Ability.nameDoctrine gdoctrine
               <> "' (" <> Ability.describeDoctrine gdoctrine <> ")."]
      -- Description of the score polynomial would go into a separate section,
      -- but it's hard to make it sound non-technical enough.
      blurbs = intersperse ["\n"] $ filter (not . null) [ts1, ts2, ts3]
  return (prompt, map (\t -> (propFont, map textToAS t)) blurbs)