{-# LANGUAGE OverloadedStrings #-}
{-# OPTIONS_GHC -fno-warn-type-defaults #-}
-- | Semantics of 'HumanCmd' client commands that do not return
-- server commands. None of such commands takes game time.
-- TODO: document
module Game.LambdaHack.Client.HumanLocal
  ( -- * Semantics of serverl-less human commands
    moveCursor, retargetLeader
  , selectHeroHuman, memberCycleHuman, memberBackHuman
  , inventoryHuman, tgtFloorLeader, tgtEnemyLeader, tgtAscendHuman
  , epsIncrHuman, cancelHuman, displayMainMenu, acceptHuman, clearHuman
  , historyHuman, humanMarkVision, humanMarkSmell, humanMarkSuspect
  , helpHuman
    -- * Helper functions useful also elsewhere
  , endTargeting, floorItemOverlay, itemOverlay, viewedLevel, selectLeader
  , stopRunning, lookAt
  ) where

-- Cabal
import qualified Paths_LambdaHack as Self (version)

import Control.Monad
import Control.Monad.Writer.Strict (WriterT, tell)
import qualified Data.EnumMap.Strict as EM
import qualified Data.EnumSet as ES
import Data.Function
import Data.List
import qualified Data.Map.Strict as M
import Data.Maybe
import Data.Ord
import Data.Text (Text)
import qualified Data.Text as T
import Data.Version
import Game.LambdaHack.Frontend (frontendName)
import qualified NLP.Miniutter.English as MU

import Game.LambdaHack.Client.Action
import Game.LambdaHack.Client.Binding
import qualified Game.LambdaHack.Client.HumanCmd as HumanCmd
import Game.LambdaHack.Client.State
import Game.LambdaHack.Common.Action
import Game.LambdaHack.Common.Actor
import Game.LambdaHack.Common.ActorState
import qualified Game.LambdaHack.Common.Effect as Effect
import Game.LambdaHack.Common.Faction
import qualified Game.LambdaHack.Common.Feature as F
import Game.LambdaHack.Common.Item
import qualified Game.LambdaHack.Common.Key as K
import qualified Game.LambdaHack.Common.Kind as Kind
import Game.LambdaHack.Common.Level
import Game.LambdaHack.Common.Msg
import Game.LambdaHack.Common.Perception
import Game.LambdaHack.Common.Point
import Game.LambdaHack.Common.State
import qualified Game.LambdaHack.Common.Tile as Tile
import Game.LambdaHack.Common.Time
import Game.LambdaHack.Common.Vector
import Game.LambdaHack.Content.RuleKind
import Game.LambdaHack.Utils.Assert

-- * Move and Run

moveCursor :: MonadClientUI m => Vector -> Int -> WriterT Slideshow m ()
moveCursor dir n = do
  leader <- getLeaderUI
  lpos <- getsState $ bpos . getActorBody leader
  scursor <- getsClient scursor
  let cpos = fromMaybe lpos scursor
  Level{lxsize, lysize} <- cursorLevel
  let shiftB pos = shiftBounded lxsize (1, 1, lxsize - 2, lysize - 2) pos dir
  modifyClient $ \cli -> cli {scursor = Just $ iterate shiftB cpos !! n}
  doLook

cursorLevel :: MonadClient m => m Level
cursorLevel = do
  dungeon <- getsState sdungeon
  stgtMode <- getsClient stgtMode
  let tgtId =
        maybe (assert `failure` "not targetting right now") tgtLevelId stgtMode
  return $! dungeon EM.! tgtId

viewedLevel :: MonadClientUI m => m (LevelId, Level)
viewedLevel = do
  arena <- getArenaUI
  dungeon <- getsState sdungeon
  stgtMode <- getsClient stgtMode
  let tgtId = maybe arena tgtLevelId stgtMode
  return (tgtId, dungeon EM.! tgtId)

-- TODO: probably move somewhere (Level?)
-- | Produces a textual description of the terrain and items at an already
-- explored position. Mute for unknown positions.
-- The detailed variant is for use in the targeting mode.
lookAt :: MonadClientUI m
       => Bool       -- ^ detailed?
       -> Bool       -- ^ can be seen right now?
       -> Point      -- ^ position to describe
       -> ActorId    -- ^ the actor that looks
       -> Text       -- ^ an extra sentence to print
       -> m Text
lookAt detailed canSee pos aid msg = do
  Kind.COps{coitem, cotile=Kind.Ops{oname}} <- getsState scops
  (_, lvl) <- viewedLevel
  subject <- partAidLeader aid
  s <- getState
  let is = lvl `atI` pos
      verb = MU.Text $ if canSee then "see" else "remember"
  disco <- getsClient sdisco
  let nWs (iid, k) = partItemWs coitem disco k (getItemBody iid s)
      isd = case detailed of
              _ | EM.size is == 0 -> ""
              _ | EM.size is <= 2 ->
                makeSentence [ MU.SubjectVerbSg subject verb
                             , MU.WWandW $ map nWs $ EM.assocs is]
              True -> "Objects:"
              _ -> "Objects here."
  if detailed
    then let tile = lvl `at` pos
         in return $! makeSentence [MU.Text $ oname tile] <+> msg <+> isd
    else return $! msg <+> isd

-- | Perform look around in the current position of the cursor.
-- Assumes targeting mode and so assumes that a leader is selected.
doLook :: MonadClientUI m => WriterT Slideshow m ()
doLook = do
  Kind.COps{coactor} <- getsState scops
  scursor <- getsClient scursor
  (lid, lvl) <- viewedLevel
  per <- getPerFid lid
  leader <- getLeaderUI
  lpos <- getsState $ bpos . getActorBody leader
  target <- getsClient $ getTarget leader
  hms <- getsState $ actorList (const True) lid
  let p = fromMaybe lpos scursor
      canSee = ES.member p (totalVisible per)
      ihabitant | canSee = find (\m -> bpos m == p) hms
                | otherwise = Nothing
      enemyMsg =
        -- Even if it's the leader, give his proper name, not 'you'.
        maybe "" (\m -> let subject = partActor coactor m
                            verb = "be here"
                        in makeSentence [MU.SubjectVerbSg subject verb])
              ihabitant
      vis | not $ p `ES.member` totalVisible per = " (not visible)"
          | actorSeesLoc per leader p = ""
          | otherwise = " (not visible)"
      mode = case target of
               Just TEnemy{} -> "[targeting foe" <> vis <> "]"
               Just TPos{}   -> "[targeting position" <> vis <> "]"
               Nothing       -> "[targeting current" <> vis <> "]"
      -- Check if there's something lying around at current position.
      is = lvl `atI` p
  -- Show general info about current position.
  lookMsg <- lookAt True canSee p leader enemyMsg
  modifyClient (\st -> st {slastKey = Nothing})
  if EM.size is <= 2 then do
    slides <- promptToSlideshow (mode <+> lookMsg)
    tell slides
  else do
    io <- floorItemOverlay is
    slides <- overlayToSlideshow (mode <+> lookMsg) io
    tell slides

-- | Create a list of item names.
floorItemOverlay :: MonadClient m => ItemBag -> m Overlay
floorItemOverlay bag = do
  Kind.COps{coitem} <- getsState scops
  s <- getState
  disco <- getsClient sdisco
  let is = zip (EM.assocs bag) (allLetters ++ repeat (InvChar ' '))
      pr ((iid, k), l) =
         makePhrase [ letterLabel l
                    , partItemWs coitem disco k (getItemBody iid s) ]
         <> " "
  return $ map pr is

-- | Create a list of item names.
itemOverlay :: MonadClient m => ItemBag -> ItemInv -> m Overlay
itemOverlay bag inv = do
  Kind.COps{coitem} <- getsState scops
  s <- getState
  disco <- getsClient sdisco
  let checkItem (l, iid) = fmap (\k -> (l, iid, k)) $ EM.lookup iid bag
      is = mapMaybe checkItem $ EM.assocs inv
      pr (l, iid, k) =
         makePhrase [ letterLabel l
                    , partItemWs coitem disco k (getItemBody iid s) ]
         <> " "
  return $ map pr is

-- * Project

retargetLeader :: MonadClientUI m => WriterT Slideshow m ()
retargetLeader = do
  arena <- getArenaUI
  -- TODO: do not save to history:
  msgAdd "Last target invalid."
  modifyClient $ \cli -> cli {scursor = Nothing, seps = 0}
  tgtEnemyLeader $ TgtAuto arena

-- * SelectHero

selectHeroHuman :: (MonadClientAbort m, MonadClientUI m) => Int -> m ()
selectHeroHuman k = do
  side <- getsClient sside
  s <- getState
  case tryFindHeroK s side k of
    Nothing  -> abortWith "No such member of the party."
    Just (aid, _) -> void $ selectLeader aid

-- * MemberCycle

-- | Switches current member to the next on the level, if any, wrapping.
memberCycleHuman :: (MonadClientAbort m, MonadClientUI m) => m ()
memberCycleHuman = do
  leader <- getLeaderUI
  body <- getsState $ getActorBody leader
  hs <- partyAfterLeader leader
  case filter (\(_, b) -> blid b == blid body) hs of
    [] -> abortWith "Cannot select any other member on this level."
    (np, b) : _ -> do
      success <- selectLeader np
      assert (success `blame` (leader, np, b)) skip

partyAfterLeader :: MonadActionRO m
                 => ActorId
                 -> m [(ActorId, Actor)]
partyAfterLeader leader = do
  faction <- getsState $ bfid . getActorBody leader
  allA <- getsState $ EM.assocs . sactorD
  s <- getState
  let hs9 = mapMaybe (tryFindHeroK s faction) [0..9]
      factionA = filter (\(_, body) ->
        not (bproj body) && bfid body == faction) allA
      hs = hs9 ++ deleteFirstsBy ((==) `on` fst) factionA hs9
      i = fromMaybe (-1) $ findIndex ((== leader) . fst) hs
      (lt, gt) = (take i hs, drop (i + 1) hs)
  return $ gt ++ lt

-- | Select a faction leader. False, if nothing to do.
selectLeader :: MonadClientUI m => ActorId -> m Bool
selectLeader actor = do
  Kind.COps{coactor} <- getsState scops
  leader <- getLeaderUI
  stgtMode <- getsClient stgtMode
  if leader == actor
    then return False -- already selected
    else do
      pbody <- getsState $ getActorBody actor
      -- Even if it's already the leader, give his proper name, not 'you'.
      let subject = partActor coactor pbody
      msgAdd $ makeSentence [subject, "selected"]
      -- Update client state.
      s <- getState
      modifyClient $ updateLeader actor s
      assert (not (bproj pbody) `blame` (actor, pbody)) skip
      -- Move the cursor, if active, to the new level.
      when (isJust stgtMode) $ setTgtId $ blid pbody
      -- Inform about items, etc.
      lookMsg <- lookAt False True (bpos pbody) actor ""
      msgAdd lookMsg
      -- Don't continue an old run, if any.
      stopRunning
      return True

stopRunning :: MonadClient m => m ()
stopRunning = modifyClient (\ cli -> cli { srunning = Nothing })

-- * MemberBack

-- | Switches current member to the previous in the whole dungeon, wrapping.
memberBackHuman :: (MonadClientAbort m, MonadClientUI m) => m ()
memberBackHuman = do
  leader <- getLeaderUI
  hs <- partyAfterLeader leader
  case reverse hs of
    [] -> abortWith "No other member in the party."
    (np, b) : _ -> do
      success <- selectLeader np
      assert (success `blame` (leader, np, b)) skip

-- * Inventory

-- TODO: When inventory is displayed, let TAB switch the leader (without
-- announcing that) and show the inventory of the new leader.
-- | Display inventory
inventoryHuman :: (MonadClientAbort m, MonadClientUI m)
               => WriterT Slideshow m ()
inventoryHuman = do
  leader <- getLeaderUI
  subject <- partAidLeader leader
  bag <- getsState $ getActorBag leader
  inv <- getsState $ getActorInv leader
  if EM.null bag
    then abortWith $ makeSentence
      [ MU.SubjectVerbSg subject "be"
      , "not carrying anything" ]
    else do
      let blurb = makePhrase
            [MU.Capitalize $ MU.SubjectVerbSg subject "be carrying:"]
      io <- itemOverlay bag inv
      slides <- overlayToSlideshow blurb io
      tell slides

-- * TgtFloor

-- | Start floor targeting mode or reset the cursor position to the leader.
-- Note that the origin of a command (the hero that performs it) is unaffected
-- by targeting. For example, not the targeted door, but one adjacent
-- to the selected hero is closed by him.
tgtFloorLeader :: MonadClientUI m => TgtMode -> WriterT Slideshow m ()
tgtFloorLeader stgtModeNew = do
  leader <- getLeaderUI
  ppos <- getsState (bpos . getActorBody leader)
  target <- getsClient $ getTarget leader
  stgtMode <- getsClient stgtMode
  let tgt = case target of
        Just (TEnemy _ _) -> Nothing  -- forget enemy target, keep the cursor
        _ | isJust stgtMode ->
          Just (TPos ppos)  -- double key press: reset cursor
        t -> t  -- keep the target from previous targeting session
  -- Register that we want to target only positions.
  modifyClient $ updateTarget leader (const tgt)
  setCursor stgtModeNew

-- | Set, activate and display cursor information.
setCursor :: MonadClientUI m => TgtMode -> WriterT Slideshow m ()
setCursor stgtModeNew = do
  stgtModeOld <- getsClient stgtMode
  scursorOld <- getsClient scursor
  sepsOld <- getsClient seps
  scursor <- targetToPos
  let seps = if scursor == scursorOld then sepsOld else 0
      stgtMode = if isNothing stgtModeOld
                   then Just stgtModeNew
                   else stgtModeOld
  modifyClient $ \cli2 -> cli2 {scursor, seps, stgtMode}
  doLook

-- * TgtEnemy

-- | Start the enemy targeting mode. Cycle between enemy targets.
tgtEnemyLeader :: MonadClientUI m => TgtMode -> WriterT Slideshow m ()
tgtEnemyLeader stgtModeNew = do
  leader <- getLeaderUI
  ppos <- getsState (bpos . getActorBody leader)
  (lid, Level{lxsize}) <- viewedLevel
  per <- getPerFid lid
  target <- getsClient $ getTarget leader
  -- TODO: sort enemies by distance to the leader.
  stgtMode <- getsClient stgtMode
  side <- getsClient sside
  fact <- getsState $ (EM.! side) . sfactionD
  ms <- getsState $ actorNotProjAssocs (isAtWar fact) lid
  let plms = filter ((/= leader) . fst) ms  -- don't target yourself
      ordPos (_, m) = (chessDist lxsize ppos $ bpos m, bpos m)
      dms = sortBy (comparing ordPos) plms
      (lt, gt) = case target of
            Just (TEnemy n _) | isJust stgtMode ->  -- pick next enemy
              let i = fromMaybe (-1) $ findIndex ((== n) . fst) dms
              in splitAt (i + 1) dms
            Just (TEnemy n _) ->  -- try to retarget the old enemy
              let i = fromMaybe (-1) $ findIndex ((== n) . fst) dms
              in splitAt i dms
            _ -> (dms, [])  -- target first enemy (e.g., number 0)
      gtlt = gt ++ lt
      seen (_, m) =
        let mpos = bpos m            -- it is remembered by faction
        in actorSeesLoc per leader mpos  -- is it visible by actor?
      lf = filter seen gtlt
      tgt = case lf of
              [] -> target  -- no enemies in sight, stick to last target
              (na, nm) : _ -> Just (TEnemy na (bpos nm))  -- pick the next
  -- Register the chosen enemy, to pick another on next invocation.
  modifyClient $ updateTarget leader (const tgt)
  setCursor stgtModeNew

-- * TgtAscend

-- | Change the displayed level in targeting mode to (at most)
-- k levels shallower. Enters targeting mode, if not already in one.
tgtAscendHuman :: (MonadClientAbort m, MonadClientUI m)
               => Int -> WriterT Slideshow m ()
tgtAscendHuman k = do
  Kind.COps{cotile} <- getsState scops
  dungeon <- getsState sdungeon
  s <- getState
  cursor <- getsClient scursor
  (tgtId, lvl) <- viewedLevel
  let rightStairs = case cursor of
        Nothing -> False
        Just cpos ->
          let tile = lvl `at` cpos
          in k ==  1
             && Tile.hasFeature cotile (F.Cause $ Effect.Ascend 1)  tile
             || k == -1
             && Tile.hasFeature cotile (F.Cause $ Effect.Descend 1) tile
  if rightStairs  -- stairs, in the right direction
    then case whereTo s tgtId k of
      Nothing ->  -- we are at the "end" of the dungeon
        abortWith "no more levels in this direction"
      Just (nln, npos) ->
        assert (nln /= tgtId `blame` (nln, "stairs looped")) $ do
          -- Do not freely reveal the other end of the stairs.
          let scursor =
                if Tile.hasFeature cotile F.Exit (lvl `at` npos)
                then Just npos  -- already know as an exit, focus on it
                else cursor    -- unknown, do not reveal
          modifyClient $ \cli -> cli {scursor}
          setTgtId nln
    else  -- no stairs in the right direction
      case ascendInBranch dungeon tgtId k of
        [] -> abortWith "no more levels in this direction"
        nln : _ -> setTgtId nln
  doLook

setTgtId :: MonadClient m => LevelId -> m ()
setTgtId nln = do
  stgtMode <- getsClient stgtMode
  case stgtMode of
    Just (TgtAuto _) ->
      modifyClient $ \cli -> cli {stgtMode = Just (TgtAuto nln)}
    _ ->
      modifyClient $ \cli -> cli {stgtMode = Just (TgtExplicit nln)}

-- * EpsIncr

-- | Tweak the @eps@ parameter of the targeting digital line.
epsIncrHuman :: MonadClientAbort m => Bool -> m ()
epsIncrHuman b = do
  stgtMode <- getsClient stgtMode
  if isJust stgtMode
    then modifyClient $ \cli -> cli {seps = seps cli + if b then 1 else -1}
    else neverMind True  -- no visual feedback, so no sense

-- * Cancel

-- | Cancel something, e.g., targeting mode, resetting the cursor
-- to the position of the leader. Chosen target is not invalidated.
cancelHuman :: MonadClientUI m
            => WriterT Slideshow m () -> WriterT Slideshow m ()
cancelHuman h = do
  stgtMode <- getsClient stgtMode
  if isJust stgtMode
    then endTargeting False
    else h  -- nothing to cancel right now, treat this as a command invocation

-- TODO: merge with the help screens better
-- | Display the main menu.
displayMainMenu :: MonadClientUI m => WriterT Slideshow m ()
displayMainMenu = do
  Kind.COps{corule} <- getsState scops
  Binding{krevMap} <- askBinding
  let stripFrame t = case T.uncons t of
        Just ('\n', art) -> map (T.tail . T.init) $ tail . init $ T.lines art
        _ -> assert `failure` "displayMainMenu:" <+> t
      pasteVersion art =
        let pathsVersion = rpathsVersion $ Kind.stdRuleset corule
            version = " Version " ++ showVersion pathsVersion
                      ++ " (frontend: " ++ frontendName
                      ++ ", engine: LambdaHack " ++ showVersion Self.version
                      ++ ") "
            versionLen = length version
        in init art ++ [take (80 - versionLen) (last art) ++ version]
      kds =  -- key-description pairs
        let showKD cmd km = (K.showKM km, HumanCmd.cmdDescription cmd)
            revLookup cmd = maybe ("", "") (showKD cmd) $ M.lookup cmd krevMap
            cmds = [ HumanCmd.GameSave
                   , HumanCmd.GameExit
                   , HumanCmd.GameRestart "campaign"
                   , HumanCmd.GameRestart "skirmish"
                   , HumanCmd.GameRestart "PvP"
                   , HumanCmd.GameRestart "Coop"
                   , HumanCmd.GameRestart "defense"
                   ]
        in [ (fst (revLookup HumanCmd.Cancel), "back to playing")
           , (fst (revLookup HumanCmd.Accept), "see more help") ]
           ++ map revLookup cmds
      bindings =  -- key bindings to display
        let bindingLen = 25
            fmt (k, d) =
              let gapLen = (8 - T.length k) `max` 1
                  padLen = bindingLen - T.length k - gapLen - T.length d
              in k <> T.replicate gapLen " " <> d <> T.replicate padLen " "
        in map fmt kds
      overwrite =  -- overwrite the art with key bindings
        let over [] line = ([], T.pack line)
            over bs@(binding : bsRest) line =
              let (prefix, lineRest) = break (=='{') line
                  (braces, suffix)   = span  (=='{') lineRest
              in if length braces == 25
                 then (bsRest, T.pack prefix <> binding <> T.pack suffix)
                 else (bs, T.pack line)
        in snd . mapAccumL over bindings
      mainMenuArt = rmainMenuArt $ Kind.stdRuleset corule
      menuOverlay =  -- TODO: switch to Text and use T.justifyLeft
        overwrite $ pasteVersion $ map T.unpack $ stripFrame mainMenuArt
  case menuOverlay of
    [] -> assert `failure` "empty Main Menu overlay"
    hd : tl -> do
      slides <- overlayToSlideshow hd tl  -- TODO: keys don't work if tl/=[]
      tell slides

-- * Accept

-- | Accept something, e.g., targeting mode, keeping cursor where it was.
-- Or perform the default action, if nothing needs accepting.
acceptHuman :: MonadClientUI m
            => WriterT Slideshow m () -> WriterT Slideshow m ()
acceptHuman h = do
  stgtMode <- getsClient stgtMode
  if isJust stgtMode
    then endTargeting True
    else h  -- nothing to accept right now, treat this as a command invocation

-- | End targeting mode, accepting the current position or not.
endTargeting :: MonadClientUI m => Bool -> m ()
endTargeting accept = do
  when accept $ do
    leader <- getLeaderUI
    target <- getsClient $ getTarget leader
    scursor <- getsClient scursor
    (lid, _) <- viewedLevel
    side <- getsClient sside
    fact <- getsState $ (EM.! side) . sfactionD
    ms <- getsState $ actorNotProjAssocs (isAtWar fact) lid
    case target of
      Just TEnemy{} ->
        -- If in enemy targeting mode, switch to the enemy under
        -- the current cursor position, if any.
        case find (\ (_im, m) -> Just (bpos m) == scursor) ms of
          Just (im, m)  ->
            let tgt = Just $ TEnemy im (bpos m)
            in modifyClient $ updateTarget leader (const tgt)
          Nothing -> return ()
      _ -> case scursor of
        Nothing -> return ()
        Just cpos ->
          modifyClient $ updateTarget leader (const $ Just $ TPos cpos)
  if accept
    then endTargetingMsg
    -- TODO: do not save to history:
    else msgAdd "targeting canceled"
  modifyClient $ \cli -> cli { stgtMode = Nothing }

endTargetingMsg :: MonadClientUI m => m ()
endTargetingMsg = do
  Kind.COps{coactor} <- getsState scops
  leader <- getLeaderUI
  pbody <- getsState $ getActorBody leader
  target <- getsClient $ getTarget leader
  s <- getState
  Level{lxsize} <- cursorLevel
  let targetMsg = case target of
                    Just (TEnemy a _ll) ->
                      if memActor a (blid pbody) s
                      -- Never equal to leader, hence @partActor@.
                      then partActor coactor $ getActorBody a s
                      else "a fear of the past"
                    Just (TPos tpos) ->
                      MU.Text $ "position" <+> showPoint lxsize tpos
                    Nothing -> "current cursor position continuously"
  subject <- partAidLeader leader
  msgAdd $ makeSentence [MU.SubjectVerbSg subject "target", targetMsg]

-- * Clear

-- | Clear current messages, show the next screen if any.
clearHuman :: Monad m => m ()
clearHuman = return ()

-- * History

historyHuman :: MonadClientUI m => WriterT Slideshow m ()
historyHuman = do
  history <- getsClient shistory
  arena <- getArenaUI
  local <- getsState $ getLocalTime arena
  global <- getsState stime
  let  msg = makeSentence
        [ "You survived for"
        , MU.CarWs (global `timeFit` timeTurn) "half-second turn"
        , "(this level:"
        , MU.Text (showT (local `timeFit` timeTurn)) MU.:> ")" ]
        <+> "Past messages:"
  slides <- overlayToSlideshow msg $ renderHistory history
  tell slides

-- * MarkVision, MarkSmell, MarkSuspect

humanMarkVision :: MonadClientUI m => m ()
humanMarkVision = do
  modifyClient toggleMarkVision
  cur <- getsClient smarkVision
  msgAdd $ "Visible area display toggled" <+> if cur then "on." else "off."

humanMarkSmell :: MonadClientUI m => m ()
humanMarkSmell = do
  modifyClient toggleMarkSmell
  cur <- getsClient smarkSmell
  msgAdd $ "Smell display toggled" <+> if cur then "on." else "off."

humanMarkSuspect :: MonadClientUI m => m ()
humanMarkSuspect = do
  modifyClient toggleMarkSuspect
  cur <- getsClient smarkSuspect
  msgAdd $ "Suspect terrain display toggled" <+> if cur then "on." else "off."

-- * Help

-- | Display command help.
helpHuman :: MonadClientUI m => WriterT Slideshow m ()
helpHuman = do
  keyb <- askBinding
  tell $ keyHelp keyb