File: DrawM.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 (851 lines) | stat: -rw-r--r-- 40,645 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
784
785
786
787
788
789
790
791
792
793
794
795
796
797
798
799
800
801
802
803
804
805
806
807
808
809
810
811
812
813
814
815
816
817
818
819
820
821
822
823
824
825
826
827
828
829
830
831
832
833
834
835
836
837
838
839
840
841
842
843
844
845
846
847
848
849
850
851
-- {-# OPTIONS_GHC -fprof-auto #-}
-- | Display game data on the screen using one of the available frontends
-- (determined at compile time with cabal flags).
module Game.LambdaHack.Client.UI.DrawM
  ( targetDesc, targetDescXhair, drawHudFrame
  , checkWarningHP, checkWarningCalm
#ifdef EXPOSE_INTERNAL
    -- * Internal operations
  , drawFrameTerrain, drawFrameContent
  , drawFramePath, drawFrameActor, drawFrameExtra, drawFrameStatus
  , drawArenaStatus, drawLeaderStatus, drawLeaderDamage, drawSelected
  , checkWarnings
#endif
  ) where

import Prelude ()

import Game.LambdaHack.Core.Prelude

import           Control.Monad.ST.Strict
import qualified Data.Char as Char
import qualified Data.EnumMap.Strict as EM
import qualified Data.EnumSet as ES
import           Data.Int (Int64)
import qualified Data.IntMap.Strict as IM
import qualified Data.IntSet as IS
import qualified Data.Text as T
import qualified Data.Vector.Unboxed as U
import qualified Data.Vector.Unboxed.Mutable as VM
import           Data.Word (Word16, Word32)
import           GHC.Exts (inline)
import qualified NLP.Miniutter.English as MU

import           Game.LambdaHack.Client.Bfs
import           Game.LambdaHack.Client.CommonM
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.Frame
import           Game.LambdaHack.Client.UI.Frontend (frontendName)
import           Game.LambdaHack.Client.UI.ItemDescription
import           Game.LambdaHack.Client.UI.MonadClientUI
import           Game.LambdaHack.Client.UI.Overlay
import           Game.LambdaHack.Client.UI.SessionUI
import           Game.LambdaHack.Client.UI.UIOptions
import           Game.LambdaHack.Common.Actor
import           Game.LambdaHack.Common.ActorState
import           Game.LambdaHack.Common.Faction
import           Game.LambdaHack.Common.Item
import qualified Game.LambdaHack.Common.ItemAspect as IA
import           Game.LambdaHack.Common.Kind
import           Game.LambdaHack.Common.Level
import           Game.LambdaHack.Common.Misc
import           Game.LambdaHack.Common.MonadStateRead
import           Game.LambdaHack.Common.Perception
import           Game.LambdaHack.Common.Point
import qualified Game.LambdaHack.Common.PointArray as PointArray
import           Game.LambdaHack.Common.State
import qualified Game.LambdaHack.Common.Tile as Tile
import           Game.LambdaHack.Common.Time
import           Game.LambdaHack.Common.Types
import           Game.LambdaHack.Common.Vector
import           Game.LambdaHack.Content.CaveKind (cname)
import qualified Game.LambdaHack.Content.FactionKind as FK
import qualified Game.LambdaHack.Content.ItemKind as IK
import           Game.LambdaHack.Content.RuleKind
import           Game.LambdaHack.Content.TileKind (TileKind, isUknownSpace)
import qualified Game.LambdaHack.Content.TileKind as TK
import qualified Game.LambdaHack.Core.Dice as Dice
import qualified Game.LambdaHack.Definition.Ability as Ability
import qualified Game.LambdaHack.Definition.Color as Color
import           Game.LambdaHack.Definition.Defs
import qualified Game.LambdaHack.Definition.DefsInternal as DefsInternal

targetDesc :: MonadClientUI m => Maybe Target -> m (Maybe Text, Maybe Text)
targetDesc mtarget = do
  arena <- getArenaUI
  lidV <- viewedLevelUI
  mleader <- getsClient sleader
  let describeActorTarget aid = do
        side <- getsClient sside
        b <- getsState $ getActorBody aid
        bUI <- getsSession $ getActorUI aid
        actorMaxSk <- getsState $ getActorMaxSkills aid
        let percentage =
             100 * bhp b
              `div` xM (max 5 $ Ability.getSk Ability.SkMaxHP actorMaxSk)
            chs n = "[" <> T.replicate (4 - n) "_"
                        <> T.replicate n "*" <> "]"
            stars = chs $ fromEnum $ max 0 $ min 4 $ percentage `div` 20
            hpIndicator = if bfid b == side then Nothing else Just stars
        return (Just $ bname bUI, hpIndicator)
  case mtarget of
    Just (TEnemy aid) -> describeActorTarget aid
    Just (TNonEnemy aid) -> describeActorTarget aid
    Just (TPoint tgoal lid p) -> case tgoal of
      TEnemyPos{} -> do
        let hotText = if lid == lidV && arena == lidV
                      then "hot spot" <+> tshow p
                      else "a hot spot on level" <+> tshow (abs $ fromEnum lid)
        return (Just hotText, Nothing)
      _ -> do  -- the other goals can be invalidated by now anyway and it's
               -- better to say what there is rather than what there isn't
        pointedText <-
          if lid == lidV && arena == lidV
          then do
            bag <- getsState $ getFloorBag lid p
            case EM.assocs bag of
              [] -> return $! "spot" <+> tshow p
              [(iid, kit@(k, _))] -> do
                localTime <- getsState $ getLocalTime lid
                itemFull <- getsState $ itemToFull iid
                side <- getsClient sside
                factionD <- getsState sfactionD
                CCUI{coscreen=ScreenContent{rwidth}} <- getsSession sccui
                let (name, powers) =
                      partItem rwidth side factionD localTime itemFull kit
                return $! makePhrase [MU.Car1Ws k name, powers]
              _ -> return $! "many items at" <+> tshow p
          else return $! "an exact spot on level" <+> tshow (abs $ fromEnum lid)
        return (Just pointedText, Nothing)
    Just TVector{} -> do
      mtgtPos <- getsState $ aidTgtToPos mleader lidV mtarget
      let invalidMsg = "a relative shift"
          validMsg p = "shift to" <+> tshow p
      return (Just $ maybe invalidMsg validMsg mtgtPos, Nothing)
    Nothing -> return (Nothing, Nothing)

targetDescXhair :: MonadClientUI m
                => m (Maybe Text, Maybe Text, Maybe Watchfulness)
targetDescXhair = do
  sxhair <- getsSession sxhair
  (mhairDesc, mxhairHP) <- targetDesc sxhair
  let maid = case sxhair of
        Just (TEnemy a) -> Just a
        Just (TNonEnemy a) -> Just a
        _ -> Nothing
  case maid of
    Nothing -> return (mhairDesc, mxhairHP, Nothing)
    Just aid -> do
      watchfulness <- bwatch <$> getsState (getActorBody aid)
      return (mhairDesc, mxhairHP, Just watchfulness)

drawFrameTerrain :: forall m. MonadClientUI m => LevelId -> m (U.Vector Word32)
drawFrameTerrain drawnLevelId = do
  COps{corule=RuleContent{rWidthMax}, cotile, coTileSpeedup} <- getsState scops
  StateClient{smarkSuspect} <- getClient
  -- Not @ScreenContent@, because indexing in level's data.
  Level{ltile=PointArray.Array{avector}, lembed} <- getLevel drawnLevelId
  totVisible <- totalVisible <$> getPerFid drawnLevelId
  frameStatus <- drawFrameStatus drawnLevelId
  let dis :: PointI -> ContentId TileKind -> Color.AttrCharW32
      {-# INLINE dis #-}
      dis pI tile =
        let TK.TileKind{tsymbol, tcolor, tcolor2} = okind cotile tile
            -- @smarkSuspect@ can be turned off easily, so let's overlay it
            -- over both visible and remembered tiles.
            fg :: Color.Color
            fg | smarkSuspect > 0
                 && Tile.isSuspect coTileSpeedup tile = Color.BrMagenta
               | smarkSuspect > 1
                 && Tile.isHideAs coTileSpeedup tile = Color.Magenta
               | -- Converting maps is cheaper than converting points
                 -- and this function is a bottleneck, so we hack a bit.
                 pI `IS.member` ES.enumSetToIntSet totVisible
                 -- If all embeds spent, mark it with darker colour.
                 && not (Tile.isEmbed coTileSpeedup tile
                         && pI `IM.notMember`
                              EM.enumMapToIntMap lembed) = tcolor
               | otherwise = tcolor2
        in Color.attrChar2ToW32 fg tsymbol
      g :: PointI -> Word16 -> Word32
      g !pI !tile = Color.attrCharW32 $ dis pI (DefsInternal.toContentId tile)
      caveVector :: U.Vector Word32
      caveVector = U.imap g avector
      messageVector =
        U.replicate rWidthMax (Color.attrCharW32 Color.spaceAttrW32)
      statusVector = U.fromListN (2 * rWidthMax) $ map Color.attrCharW32 frameStatus
  -- The vector package is so smart that the 3 vectors are not allocated
  -- separately at all, but written to the big vector at once.
  -- But even with double allocation it would be faster than writing
  -- to a mutable vector via @FrameForall@.
  return $ U.concat [messageVector, caveVector, statusVector]

drawFrameContent :: forall m. MonadClientUI m => LevelId -> m FrameForall
drawFrameContent drawnLevelId = do
  COps{corule=RuleContent{rWidthMax}} <- getsState scops
  SessionUI{smarkSmell} <- getSession
  -- Not @ScreenContent@, because indexing in level's data.
  Level{lsmell, ltime, lfloor} <- getLevel drawnLevelId
  itemToF <- getsState $ flip itemToFull
  let {-# INLINE viewItemBag #-}
      viewItemBag _ floorBag = case EM.toDescList floorBag of
        (iid, _kit) : _ -> viewItem $ itemToF iid
        [] -> error $ "lfloor not sparse" `showFailure` ()
      viewSmell :: PointI -> Time -> Color.AttrCharW32
      {-# INLINE viewSmell #-}
      viewSmell pI sml =
        let fg = toEnum $ pI `rem` 13 + 2
            smlt = smellTimeout `timeDeltaSubtract`
                     (sml `timeDeltaToFrom` ltime)
        in Color.attrChar2ToW32 fg (timeDeltaToDigit smellTimeout smlt)
      mapVAL :: forall a s. (PointI -> a -> Color.AttrCharW32) -> [(PointI, a)]
             -> FrameST s
      {-# INLINE mapVAL #-}
      mapVAL f l v = do
        let g :: (PointI, a) -> ST s ()
            g (!pI, !a0) = do
              let w = Color.attrCharW32 $ f pI a0
              VM.write v (pI + rWidthMax) w
        mapM_ g l
      -- We don't usually show embedded items, because normally we don't
      -- want them to clutter the display. If they are really important,
      -- the tile they reside on has special colours and changes as soon
      -- as the item disappears. In the remaining cases, the main menu
      -- UI setting for suspect terrain highlights most tiles with embeds.
      upd :: FrameForall
      upd = FrameForall $ \v -> do
        mapVAL viewItemBag (IM.assocs $ EM.enumMapToIntMap lfloor) v
        when smarkSmell $
          mapVAL viewSmell (filter ((> ltime) . snd)
                            $ IM.assocs $ EM.enumMapToIntMap lsmell) v
  return upd

drawFramePath :: forall m. MonadClientUI m
              => LevelId -> m (FrameForall, FrameForall)
drawFramePath drawnLevelId = do
 SessionUI{saimMode} <- getSession
 sreportNull <- getsSession sreportNull
 let frameForallId = FrameForall $ const $ return ()
 case saimMode of
   Just AimMode{detailLevel} | not sreportNull
                               && detailLevel /= DetailHigh
                               && detailLevel /= DetailLow -> do
     COps{corule=RuleContent{rWidthMax, rHeightMax}, coTileSpeedup}
       <- getsState scops
     StateClient{seps} <- getClient
     -- Not @ScreenContent@, because pathing in level's map.
     Level{ltile=PointArray.Array{avector}} <- getLevel drawnLevelId
     totVisible <- totalVisible <$> getPerFid drawnLevelId
     mleader <- getsClient sleader
     xhairPos <- xhairToPos
     bline <- case mleader of
       Just leader -> do
         Actor{bpos, blid} <- getsState $ getActorBody leader
         return $! if blid /= drawnLevelId
                   then []
                   else fromMaybe []
                        $ bresenhamsLineAlgorithm seps bpos xhairPos
       _ -> return []
     mpath <- maybe (return Nothing) (\aid -> do
       mtgtMPath <- getsClient $ EM.lookup aid . stargetD
       case mtgtMPath of
         Just TgtAndPath{tapPath=tapPath@(Just AndPath{pathGoal})}
           | pathGoal == xhairPos -> return tapPath
         _ -> getCachePath aid xhairPos) mleader
     assocsAtxhair <- getsState $ posToAidAssocs xhairPos drawnLevelId
     let shiftedBTrajectory = case assocsAtxhair of
           (_, Actor{btrajectory = Just p, bpos = prPos}) : _
             | detailLevel == defaultDetailLevel ->
               trajectoryToPath prPos (fst p)
           _ -> []
         shiftedLine =
           delete xhairPos
           $ takeWhile (insideP (0, 0, rWidthMax - 1, rHeightMax - 1))
           $ if null shiftedBTrajectory
             then bline
             else shiftedBTrajectory
         lpath = if not (null bline) && null shiftedBTrajectory
                 then delete xhairPos $ maybe [] pathList mpath
                 else []
         acOnPathOrLine :: Char -> Point -> ContentId TileKind
                        -> Color.AttrCharW32
         acOnPathOrLine !ch !p0 !tile =
           let fgOnPathOrLine =
                 case ( ES.member p0 totVisible
                      , Tile.isWalkable coTileSpeedup tile ) of
                   _ | isUknownSpace tile -> Color.BrBlack
                   _ | Tile.isSuspect coTileSpeedup tile -> Color.BrMagenta
                   (True, True)   -> Color.BrGreen
                   (True, False)  -> Color.BrRed
                   (False, True)  -> Color.Green
                   (False, False) -> Color.Red
           in Color.attrChar2ToW32 fgOnPathOrLine ch
         mapVTL :: forall s. (Point -> ContentId TileKind -> Color.AttrCharW32)
                -> [Point]
                -> FrameST s
         mapVTL f l v = do
           let g :: Point -> ST s ()
               g !p0 = do
                 let pI = fromEnum p0
                     tile = avector U.! pI
                     w = Color.attrCharW32
                         $ f p0 (DefsInternal.toContentId tile)
                 VM.write v (pI + rWidthMax) w
           mapM_ g l
         upd :: FrameForall
         upd = FrameForall $ \v -> do
           mapVTL (acOnPathOrLine ';') lpath v
           mapVTL (acOnPathOrLine '*') shiftedLine v  -- overwrites path
     return (upd, if null shiftedBTrajectory then frameForallId else upd)
   _ -> return (frameForallId, frameForallId)

drawFrameActor :: forall m. MonadClientUI m => LevelId -> m FrameForall
drawFrameActor drawnLevelId = do
  COps{corule=RuleContent{rWidthMax}} <- getsState scops
  SessionUI{sactorUI, sselected, sUIOptions} <- getSession
  -- Not @ScreenContent@, because indexing in level's data.
  Level{lbig, lproj} <- getLevel drawnLevelId
  side <- getsClient sside
  s <- getState
  let {-# INLINE viewBig #-}
      viewBig aid =
          let Actor{bhp, bfid, btrunk, bwatch} = getActorBody aid s
              ActorUI{bsymbol, bcolor} = sactorUI EM.! aid
              Item{jfid} = getItemBody btrunk s
              symbol | bhp > 0 = bsymbol
                     | otherwise = '%'
              dominated = maybe False (/= bfid) jfid
              bg = if | bwatch == WSleep -> Color.HighlightBlue
                      | dominated -> if bfid == side  -- dominated by us
                                     then Color.HighlightCyan
                                     else Color.HighlightBrown
                      | ES.member aid sselected -> Color.HighlightGreen
                      | otherwise -> Color.HighlightNone
              fg | bfid /= side || bhp <= 0 = bcolor
                 | otherwise =
                let (hpCheckWarning, calmCheckWarning) =
                      checkWarnings sUIOptions aid s
                in if hpCheckWarning || calmCheckWarning
                   then Color.Red
                   else bcolor
         in Color.attrCharToW32 $ Color.AttrChar Color.Attr{..} symbol
      {-# INLINE viewProj #-}
      viewProj as = case as of
        aid : _ ->
          let ActorUI{bsymbol, bcolor} = sactorUI EM.! aid
              bg = Color.HighlightNone
              fg = bcolor
         in Color.attrCharToW32 $ Color.AttrChar Color.Attr{..} bsymbol
        [] -> error $ "lproj not sparse" `showFailure` ()
      mapVAL :: forall a s. (a -> Color.AttrCharW32) -> [(PointI, a)]
             -> FrameST s
      {-# INLINE mapVAL #-}
      mapVAL f l v = do
        let g :: (PointI, a) -> ST s ()
            g (!pI, !a0) = do
              let w = Color.attrCharW32 $ f a0
              VM.write v (pI + rWidthMax) w
        mapM_ g l
      upd :: FrameForall
      upd = FrameForall $ \v -> do
        mapVAL viewProj (IM.assocs $ EM.enumMapToIntMap lproj) v
        mapVAL viewBig (IM.assocs $ EM.enumMapToIntMap lbig) v
          -- big actor overlay projectiles
  return upd

drawFrameExtra :: forall m. MonadClientUI m
               => ColorMode -> LevelId -> m FrameForall
drawFrameExtra dm drawnLevelId = do
  -- Not @ScreenContent@, because indexing in level's data.
  COps{corule=RuleContent{rWidthMax, rHeightMax}} <- getsState scops
  SessionUI{saimMode, smarkVision} <- getSession
  mleader <- getsClient sleader
  mbody <- getsState $ \s -> flip getActorBody s <$> mleader
  totVisible <- totalVisible <$> getPerFid drawnLevelId
  mxhairPos <- mxhairToPos
  mtgtPos <- do
    mtgt <- getsClient $ maybe (const Nothing) getTarget mleader
    getsState $ aidTgtToPos mleader drawnLevelId mtgt
  side <- getsClient sside
  factionD <- getsState sfactionD
  let visionMarks = IS.toList $ ES.enumSetToIntSet totVisible
      backlightVision :: Color.AttrChar -> Color.AttrChar
      backlightVision ac = case ac of
        Color.AttrChar (Color.Attr fg Color.HighlightNone) ch ->
          Color.AttrChar (Color.Attr fg Color.HighlightBackground) ch
        _ -> ac
      writeSquare !hi (Color.AttrChar (Color.Attr fg bg) ch) =
        let hiUnlessLeader | bg == Color.HighlightYellow = bg
                           | otherwise = hi
        in Color.AttrChar (Color.Attr fg hiUnlessLeader) ch
      turnBW (Color.AttrChar _ ch) = Color.AttrChar Color.defAttr ch
      mapVL :: forall s. (Color.AttrChar -> Color.AttrChar) -> [PointI]
            -> FrameST s
      mapVL f l v = do
        let g :: PointI -> ST s ()
            g !pI = do
              w0 <- VM.read v (pI + rWidthMax)
              let w = Color.attrCharW32 . Color.attrCharToW32
                      . f . Color.attrCharFromW32 . Color.AttrCharW32 $ w0
              VM.write v (pI + rWidthMax) w
        mapM_ g l
      -- Here @rWidthMax@ and @rHeightMax@ are correct, because we are not
      -- turning the whole screen into black&white, but only the level map.
      lDungeon = [0..rWidthMax * rHeightMax - 1]
      leaderColor = if isJust saimMode
                    then Color.HighlightYellowAim
                    else Color.HighlightYellow
      xhairColor = if isJust saimMode
                   then Color.HighlightRedAim
                   else Color.HighlightRed
      locateStash (fid, fact) = case gstash fact of
        Just (lid, pos) | lid == drawnLevelId ->
          let stashColor = if fid == side
                           then Color.HighlightWhite
                           else Color.HighlightMagenta
          in Just (pos, stashColor)
        _ -> Nothing
      stashesToDisplay = mapMaybe locateStash $ EM.assocs factionD
      upd :: FrameForall
      upd = FrameForall $ \v -> do
        when (isJust saimMode && smarkVision >= 1 || smarkVision == 2) $
          mapVL backlightVision visionMarks v
        case mtgtPos of
          Nothing -> return ()
          Just p -> mapVL (writeSquare Color.HighlightGrey) [fromEnum p] v
        mapM_ (\(pos, color) -> mapVL (writeSquare color) [fromEnum pos] v)
              stashesToDisplay
        case mbody of  -- overwrites target
          Just body | drawnLevelId == blid body ->
            mapVL (writeSquare leaderColor) [fromEnum $ bpos body] v
          _ -> return ()
        case mxhairPos of  -- overwrites target and non-aim leader box
          Nothing -> return ()
          Just p -> mapVL (writeSquare xhairColor) [fromEnum p] v
        when (dm == ColorBW) $ mapVL turnBW lDungeon v
  return upd

drawFrameStatus :: MonadClientUI m => LevelId -> m AttrString
drawFrameStatus drawnLevelId = do
  cops@COps{corule=RuleContent{rWidthMax=_rWidthMax}} <- getsState scops
  SessionUI{sselected, saimMode, swaitTimes, sitemSel} <- getSession
  mleader <- getsClient sleader
  mxhairPos <- mxhairToPos
  mbfs <- maybe (return Nothing) (fmap Just . getCacheBfs) mleader
  (mhairDesc, mxhairHP, mxhairWatchfulness) <- targetDescXhair
  lvl <- getLevel drawnLevelId
  side <- getsClient sside
  fact <- getsState $ (EM.! side) . sfactionD
  (mblid, mbpos, mbodyUI) <- case mleader of
    Just leader -> do
      Actor{bpos, blid} <- getsState $ getActorBody leader
      bodyUI <- getsSession $ getActorUI leader
      return (Just blid, Just bpos, Just bodyUI)
    Nothing -> return (Nothing, Nothing, Nothing)
  let widthX = 80
      widthTgt = 39
      widthStatus = widthX - widthTgt - 1
      arenaStatus = drawArenaStatus cops lvl widthStatus
      leaderStatusWidth = 23
  leaderStatus <- drawLeaderStatus swaitTimes
  (selectedStatusWidth, selectedStatus)
    <- drawSelected drawnLevelId (widthStatus - leaderStatusWidth) sselected
  let speedStatusWidth = widthStatus - leaderStatusWidth - selectedStatusWidth
  speedDisplay <- case mleader of
    Nothing -> return []
    Just leader -> do
      actorCurAndMaxSk <- getsState $ getActorMaxSkills leader
      kitAssRaw <- getsState $ kitAssocs leader [CEqp, COrgan]
      let speed = Ability.getSk Ability.SkSpeed actorCurAndMaxSk
          unknownBonus = unknownSpeedBonus $ map (fst . snd) kitAssRaw
          speedString = displaySpeed speed ++ if unknownBonus then "?" else ""
          conditionBonus = conditionSpeedBonus $ map snd kitAssRaw
          cspeed = case compare conditionBonus 0 of
            LT -> Color.Red
            EQ -> Color.White
            GT -> Color.Green
      return $! map (Color.attrChar2ToW32 cspeed) speedString
  let speedStatus = if length speedDisplay >= speedStatusWidth
                    then []
                    else speedDisplay ++ [Color.spaceAttrW32]
      displayPathText mp mt =
        let (plen, llen) | Just target <- mp
                         , Just bfs <- mbfs
                         , Just bpos <- mbpos
                         , mblid == Just drawnLevelId
                         = ( fromMaybe 0 (accessBfs bfs target)
                           , chessDist bpos target )
                         | otherwise = (0, 0)
            pText | plen == 0 = ""
                  | otherwise = "p" <> tshow plen
            lText | llen == 0 = ""
                  | otherwise = "l" <> tshow llen
            text = fromMaybe (pText <+> lText) mt
        in if T.null text then "" else " " <> text
      -- The indicators must fit, they are the actual information.
      pathCsr = displayPathText mxhairPos mxhairHP
      trimTgtDesc n t = assert (not (T.null t) && n > 2 `blame` (t, n)) $
        if T.length t <= n then t else T.take (n - 3) t <> "..."
      -- The indicators must fit, they are the actual information.
      widthXhairOrItem = widthTgt - T.length pathCsr
      nMember = MU.Ord $ 1 + sum (EM.elems $ gvictims fact)
      fallback = if FK.fhasPointman (gkind fact)
                 then makePhrase
                        ["Waiting for", nMember, "team member to spawn"]
                 else "This faction never picks a pointman"
      leaderName bUI = trimTgtDesc (widthTgt - 10) (bname bUI)
      leaderBlurbLong = maybe fallback (\bUI ->
        "Pointman:" <+> leaderName bUI) mbodyUI
      leaderBlurbShort = maybe fallback leaderName mbodyUI
  ours <- getsState $ fidActorNotProjGlobalAssocs side
  ns <- getsState $ EM.size . getFactionStashBag side
  let na = length ours
      nl = ES.size $ ES.fromList $ map (blid . snd) ours
      -- To be replaced by something more useful.
      teamBlurb = textToAS $ trimTgtDesc widthTgt $
        makePhrase [ "Team:"
                   , MU.CarWs na "actor", "on"
                   , MU.CarWs nl "level" <> ","
                   , "stash", MU.Car ns ]
      markSleepTgtDesc
        | mxhairWatchfulness /= Just WSleep = textToAS
        | otherwise = textFgToAS Color.cSleep
      xdetail AimMode{detailLevel} =
        "x" <> tshow (1 + fromEnum detailLevel)
      xhairName aimMode = "Crosshair" <+> xdetail aimMode
      xhairBlurb =
        maybe
          teamBlurb
          (\t -> case saimMode of
             Just aimMode ->
               textToAS (xhairName aimMode <> ":")
               <+:> markSleepTgtDesc (trimTgtDesc (widthXhairOrItem - 14) t)
             Nothing -> markSleepTgtDesc (trimTgtDesc widthXhairOrItem t))
          mhairDesc
      tgtOrItem
        | Just (iid, fromCStore, _) <- sitemSel
        , Just leader <- mleader
        = do
            b <- getsState $ getActorBody leader
            bag <- getsState $ getBodyStoreBag b fromCStore
            case iid `EM.lookup` bag of
              Nothing -> return (xhairBlurb, pathCsr)
              Just kit@(k, _) -> do
                localTime <- getsState $ getLocalTime (blid b)
                itemFull <- getsState $ itemToFull iid
                factionD <- getsState sfactionD
                CCUI{coscreen=ScreenContent{rwidth}} <- getsSession sccui
                let (name, powers) =
                      partItem rwidth (bfid b) factionD localTime itemFull kit
                    t = makePhrase [MU.Car1Ws k name, powers]
                    xhairHP = maybe "" (" " <>) mxhairHP
                    (xItemWidth, xItemText) = case saimMode of
                      Just aimMode -> (9, "Item" <+> xdetail aimMode)
                      Nothing -> (6, "Item")
                    trimTD =
                      trimTgtDesc (widthTgt - T.length xhairHP - xItemWidth) t
                return (textToAS $ xItemText <> ":" <+> trimTD, xhairHP)
        | otherwise =
            return (xhairBlurb, pathCsr)
  (xhairLine, pathXhairOrNull) <- tgtOrItem
  damageStatus <- maybe (return []) (drawLeaderDamage widthTgt) mleader
  let damageStatusWidth = length damageStatus
      withForLeader = widthTgt - damageStatusWidth - 1
      leaderBottom =
        if | T.length leaderBlurbShort > withForLeader -> ""
           | T.length leaderBlurbLong > withForLeader -> leaderBlurbShort
           | otherwise -> leaderBlurbLong
      damageGap = blankAttrString
                  $ widthTgt - damageStatusWidth - T.length leaderBottom
      xhairGap = blankAttrString (widthTgt - T.length pathXhairOrNull
                                         - length xhairLine)
      xhairStatus = xhairLine ++ xhairGap ++ textToAS pathXhairOrNull
      selectedGap = blankAttrString (widthStatus - leaderStatusWidth
                                               - selectedStatusWidth
                                               - length speedStatus)
      status = arenaStatus
               <> [Color.spaceAttrW32]
               <> xhairStatus
               <> selectedStatus ++ selectedGap ++ speedStatus ++ leaderStatus
               <> [Color.spaceAttrW32]
               <> (textToAS leaderBottom ++ damageGap ++ damageStatus)
  -- Keep it at least partially lazy, to avoid allocating the whole list:
  return
#ifdef WITH_EXPENSIVE_ASSERTIONS
    $ assert (length status == 2 * _rWidthMax
              `blame` attrStringToString status)
#endif
        status

-- | Draw the whole screen: level map and status area.
drawHudFrame :: MonadClientUI m => ColorMode -> LevelId -> m PreFrame
drawHudFrame dm drawnLevelId = do
  baseTerrain <- drawFrameTerrain drawnLevelId
  updContent <- drawFrameContent drawnLevelId
  (updPath, updTrajectory) <- drawFramePath drawnLevelId
  updActor <- drawFrameActor drawnLevelId
  updExtra <- drawFrameExtra dm drawnLevelId
  soptions <- getsClient soptions
  let upd = FrameForall $ \v -> do
        unFrameForall updContent v
        -- ANSI frontend is screen-reader friendly, so avoid visual fluff
        unless (frontendName soptions == "ANSI") $ unFrameForall updPath v
        unFrameForall updActor v
        unFrameForall updTrajectory v
        unFrameForall updExtra v
  return (baseTerrain, upd)

-- Comfortably accomodates 3-digit level numbers and 25-character
-- level descriptions (currently enforced max).
--
-- Sometimes the level seems fully explored, but the display shows
-- 99% or even goes from 100% to 99% at some moment.
-- This is due to monsters, e.g., clearning rubble or burning bush,
-- and so creating a new explorable terrain.
drawArenaStatus :: COps -> Level -> Int -> AttrString
drawArenaStatus COps{cocave}
                Level{lkind, ldepth=Dice.AbsDepth ld, lseen, lexpl}
                width =
  let ck = okind cocave lkind
      seenN = 100 * lseen `div` max 1 lexpl
      seenTxt | seenN >= 100 = "all"
              | otherwise = tshow seenN <> "%"
      lvlN = T.justifyLeft 2 ' ' (tshow ld)
      seenStatus = "[" <> seenTxt <+> "seen]"
  in textToAS $ T.take (width - 10)
                       (T.justifyLeft (width - 10) ' ' (lvlN <+> cname ck))
                <> T.justifyRight 10 ' ' seenStatus

drawLeaderStatus :: MonadClientUI m => Int -> m AttrString
drawLeaderStatus waitT = do
  time <- getsState stime
  let calmHeaderText = "Calm"
      hpHeaderText = "HP"
      slashes = ["/", "|", "\\", "|"]
      waitGlobal = timeFit time timeTurn
  sUIOptions <- getsSession sUIOptions
  mleader <- getsClient sleader
  case mleader of
    Just leader -> do
      b <- getsState $ getActorBody leader
      actorCurAndMaxSk <- getsState $ getActorMaxSkills leader
      (hpCheckWarning, calmCheckWarning)
        <- getsState $ checkWarnings sUIOptions leader
      bdark <- getsState $ not . actorInAmbient b
      let showTrunc x = let t = show x
                        in if length t > 3
                           then if x > 0 then "***" else "---"
                           else t
          waitSlash | bwatch b == WSleep = waitGlobal
                    | otherwise = abs waitT
          -- This is a valuable feedback for the otherwise hard to observe
          -- 'wait' command or for passing of time when sole leader sleeps.
          slashPick = slashes !! (max 0 waitSlash `mod` length slashes)
          addColor c = map (Color.attrChar2ToW32 c)
          checkDelta ResDelta{..}
            | fst resCurrentTurn < 0 || fst resPreviousTurn < 0
              = addColor Color.BrRed  -- alarming news have priority
            | snd resCurrentTurn > 0 || snd resPreviousTurn > 0
              = addColor Color.BrGreen
            | otherwise = stringToAS  -- only if nothing at all noteworthy
          checkSleep body resDelta
            | bwatch body == WSleep = addColor Color.cSleep
            | otherwise = checkDelta resDelta
          calmAddAttr = checkSleep b $ bcalmDelta b
          -- We only show ambient light, because in fact client can't tell
          -- if a tile is lit, because it it's seen it may be due to ambient
          -- or dynamic light or due to infravision.
          darkPick | bdark = "."
                   | otherwise = ":"
          calmHeader = calmAddAttr $ calmHeaderText <> darkPick
          maxCalm = max 0 $ Ability.getSk Ability.SkMaxCalm actorCurAndMaxSk
          calmText = showTrunc (bcalm b `divUp` oneM)
                     <> (if bdark then slashPick else "/")
                     <> showTrunc maxCalm
          bracePick | actorWaits b = "}"
                    | otherwise = ":"
          hpAddAttr = checkDelta $ bhpDelta b
          hpHeader = hpAddAttr $ hpHeaderText <> bracePick
          maxHP = max 0 $ Ability.getSk Ability.SkMaxHP actorCurAndMaxSk
          hpText = showTrunc (bhp b `divUp` oneM)
                   <> (if not bdark then slashPick else "/")
                   <> showTrunc maxHP
          justifyRight n t = replicate (n - length t) ' ' ++ t
          colorWarning w enough full | w = addColor Color.Red
                                     | not enough = addColor Color.Brown
                                     | full = addColor Color.Magenta
                                     | otherwise = stringToAS
      return $! calmHeader
                <> colorWarning calmCheckWarning
                                (calmEnough b actorCurAndMaxSk)
                                (bcalm b > xM maxCalm)
                                (justifyRight 7 calmText)
                <+:> hpHeader
                <> colorWarning hpCheckWarning
                                True
                                (bhp b > xM maxHP)
                                (justifyRight 7 hpText)
    Nothing -> do
      -- This is a valuable feedback for passing of time while faction
      -- leaderless and especially while temporarily actor-less..
      let slashPick = slashes !! (max 0 waitGlobal `mod` length slashes)
      return $! stringToAS (calmHeaderText ++ ":  --" ++ slashPick ++ "--")
                <+:> stringToAS (hpHeaderText <> ":  --/--")

drawLeaderDamage :: MonadClientUI m => Int -> ActorId -> m AttrString
drawLeaderDamage width leader = do
  kitAssRaw <- getsState $ kitAssocs leader [CEqp, COrgan]
  actorCurAndMaxSk <- getsState $ getActorMaxSkills leader
  let unBurn (IK.Burn d) = Just d
      unBurn _ = Nothing
      unRefillHP (IK.RefillHP n) = Just n
      unRefillHP _ = Nothing
      hasNonDamagesEffect itemFull =
        any (\eff -> IK.forApplyEffect eff && not (IK.forDamageEffect eff))
            (IK.ieffects $ itemKind itemFull)
      ppDice :: Bool -> (Bool, Int, Int, ItemFullKit)
             -> [(Bool, (AttrString, AttrString))]
      ppDice showInBrief (hasEffect, timeout, ncha, (itemFull, (k, _))) =
        let dice = IK.idamage $ itemKind itemFull
            tdice = case Dice.reduceDice dice of
              Just d | showInBrief -> show d
              _ -> show dice
            -- We ignore nested effects because they are, in general, avoidable.
            -- We also ignore repeated effect kinds for HUD simplicity.
            tBurn = maybe "" (('+' :) . show)  $ listToMaybe $ mapMaybe unBurn
                                               $ IK.ieffects $ itemKind itemFull
            nRefillHP = maybe 0 (min 0) $ listToMaybe $ mapMaybe unRefillHP
                                        $ IK.ieffects $ itemKind itemFull
            tRefillHP | nRefillHP < 0 = '+' : show (- nRefillHP)
                      | otherwise = ""
            tdiceEffect = if hasEffect && hasNonDamagesEffect itemFull
                          then map Char.toUpper tdice
                          else tdice
            ldice color = map (Color.attrChar2ToW32 color) tdiceEffect
            lBurnHP charged =
              let cburn = if charged then Color.BrRed else Color.Red
                  chp = if charged then Color.BrMagenta else Color.Magenta
              in map (Color.attrChar2ToW32 cburn) tBurn
                 ++ map (Color.attrChar2ToW32 chp) tRefillHP
            possiblyHasTimeout = timeout > 0 || itemSuspect itemFull
        in if possiblyHasTimeout
           then replicate (k - ncha)
                          (False, (ldice Color.Cyan, lBurnHP False))
                ++ replicate ncha (True, (ldice Color.BrCyan, lBurnHP True))
           else [(True, (ldice Color.BrBlue, lBurnHP True))]
      lbonus :: AttrString
      lbonus =
        let bonusRaw = Ability.getSk Ability.SkHurtMelee actorCurAndMaxSk
            bonus = min 200 $ max (-200) bonusRaw
            unknownBonus = unknownMeleeBonus $ map (fst . snd) kitAssRaw
            tbonus = if bonus == 0
                     then if unknownBonus then "+?" else ""
                     else (if bonus > 0 then "+" else "")
                          <> show bonus
                          <> (if bonus /= bonusRaw then "$" else "")
                          <> if unknownBonus then "%?" else "%"
            conditionBonus = conditionMeleeBonus $ map snd kitAssRaw
            cbonus = case compare conditionBonus 0 of
              LT -> Color.Red
              EQ -> Color.White
              GT -> Color.Green
        in map (Color.attrChar2ToW32 cbonus) tbonus
  let kitAssOnlyWeapons =
        filter (IA.checkFlag Ability.Meleeable
                . aspectRecordFull . fst . snd) kitAssRaw
  discoBenefit <- getsClient sdiscoBenefit
  strongest <-
    map (\(_, hasEffect, timeout, ncha, _, itemFullKit) ->
          (hasEffect, timeout, ncha, itemFullKit))
    <$> pickWeaponM True (Just discoBenefit) kitAssOnlyWeapons
                    actorCurAndMaxSk leader
  let possiblyHasTimeout (_, timeout, _, (itemFull, _)) =
        timeout > 0 || itemSuspect itemFull
      (lT, lTrest) = span possiblyHasTimeout strongest
      strongestToDisplay = lT ++ case lTrest of
        [] -> []
        noTimeout : lTrest2 -> noTimeout : filter possiblyHasTimeout lTrest2
          -- the second portion of timeout weapons won't ever be used
          -- but often it's the player's mistake, so show them anyway
      showStrongest showInBrief l =
        let lToDisplay = concatMap (ppDice showInBrief) l
            (ldischarged, lrest) = break fst lToDisplay
            lWithBonus = case map snd lrest of
              [] -> []  -- no timeout-free organ, e.g., rattlesnake or hornet
              (ldmg, lextra) : rest -> (ldmg ++ lbonus, lextra) : rest
            displayDmgAndExtra (ldmg, lextra) =
              if attrStringToString ldmg == "0"
              then case lextra of
                [] -> ldmg
                _plus : lextraRest -> lextraRest
              else ldmg ++ lextra
        in intercalate [Color.spaceAttrW32]
           $ map displayDmgAndExtra $ map snd ldischarged ++ lWithBonus
      lFull = showStrongest False strongestToDisplay
      lBrief = showStrongest True strongestToDisplay
      lFits | length lFull <= width = lFull
                -- the prevailing case, so optimized for this case only
            | length lBrief <= width = lBrief
            | otherwise = take (width - 3) lBrief ++ stringToAS "..."
  return $! lFits

drawSelected :: MonadClientUI m
             => LevelId -> Int -> ES.EnumSet ActorId -> m (Int, AttrString)
drawSelected drawnLevelId width selected = do
  mleader <- getsClient sleader
  side <- getsClient sside
  sactorUI <- getsSession sactorUI
  ours <- getsState $ filter (not . bproj . snd)
                      . inline actorAssocs (== side) drawnLevelId
  let oursUI = map (\(aid, b) -> (aid, b, sactorUI EM.! aid)) ours
      viewOurs (aid, Actor{bhp, bwatch}, ActorUI{bsymbol, bcolor}) =
        -- Sleep considered before being selected, because sleeping
        -- actors can't move, so selection is mostly irrelevant.
        -- Domination not considered at all, because map already shows it
        -- and so here is the only place where selection is conveyed.
        let bg = if | mleader == Just aid -> Color.HighlightYellow
                    | bwatch == WSleep -> Color.HighlightBlue
                    | ES.member aid selected -> Color.HighlightGreen
                    | otherwise -> Color.HighlightNone
            sattr = Color.Attr {Color.fg = bcolor, bg}
        in Color.attrCharToW32 $ Color.AttrChar sattr
           $ if bhp > 0 then bsymbol else '%'
      maxViewed = width - 2
      len = length oursUI
      star = let fg = case ES.size selected of
                   0 -> Color.BrBlack
                   n | n == len -> Color.BrWhite
                   _ -> Color.defFG
                 char = if len > maxViewed then '$' else '*'
             in Color.attrChar2ToW32 fg char
      viewed = map viewOurs $ take maxViewed
               $ sortOn keySelected oursUI
  return (min width (len + 2), [star] ++ viewed ++ [Color.spaceAttrW32])

checkWarningHP :: UIOptions -> ActorId -> Int64 -> State -> Bool
checkWarningHP UIOptions{uhpWarningPercent} leader hp s =
  let actorCurAndMaxSk = getActorMaxSkills leader s
      maxHp = Ability.getSk Ability.SkMaxHP actorCurAndMaxSk
  in hp <= xM (uhpWarningPercent * maxHp `div` 100)

checkWarningCalm :: UIOptions -> ActorId -> Int64 -> State -> Bool
checkWarningCalm UIOptions{uhpWarningPercent} leader calm s =
  let b = getActorBody leader s
      actorCurAndMaxSk = getActorMaxSkills leader s
      isImpression iid =
        maybe False (> 0) $ lookup IK.S_IMPRESSED $ IK.ifreq $ getIidKind iid s
      isImpressed = any isImpression $ EM.keys $ borgan b
      maxCalm = Ability.getSk Ability.SkMaxCalm actorCurAndMaxSk
  in calm <= xM (uhpWarningPercent * maxCalm `div` 100)
     && isImpressed

checkWarnings :: UIOptions -> ActorId -> State -> (Bool, Bool)
checkWarnings uiOptions leader s =
  let b = getActorBody leader s
  in ( checkWarningHP uiOptions leader (bhp b) s
     , checkWarningCalm uiOptions leader (bcalm b) s )