content: -- | Semantics of "Game.LambdaHack.Client.UI.HumanCmd"
  -- client commands that do not return server requests,,
  -- but only change internal client state.
  -- None of such commands takes game time.
  module Game.LambdaHack.Client.UI.HandleHumanLocalM
    ( -- * Meta commands
      macroHuman, macroHumanTransition
      -- * Local commands
    , chooseItemHuman, chooseItemDialogMode
    , chooseItemProjectHuman, chooseItemApplyHuman
    , psuitReq, triggerSymbols, pickLeaderHuman, pickLeaderWithPointerHuman
    , pointmanCycleHuman, pointmanCycleLevelHuman
    , selectActorHuman, selectNoneHuman, selectWithPointerHuman
    , repeatHuman, repeatHumanTransition
    , repeatLastHuman, repeatLastHumanTransition
    , recordHuman, recordHumanTransition, allHistoryHuman
    , markVisionHuman, markSmellHuman, markSuspectHuman, markAnimHuman
    , overrideTutHuman
    , printScreenHuman
      -- * Commands specific to aiming
    , cancelHuman, acceptHuman, detailCycleHuman
    , clearTargetIfItemClearHuman, itemClearHuman
    , moveXhairHuman, aimTgtHuman, aimFloorHuman, aimEnemyHuman, aimItemHuman
    , aimAscendHuman, epsIncrHuman
    , xhairUnknownHuman, xhairItemHuman, xhairStairHuman
    , xhairPointerFloorHuman, xhairPointerMuteHuman, xhairPointerEnemyHuman
    , aimPointerFloorHuman, aimPointerEnemyHuman
  #ifdef EXPOSE_INTERNAL
      -- * Internal operations
    , chooseItemDialogModeLore, projectCheck
    , posFromXhair, permittedApplyClient, endAiming, endAimingMsg
    , flashAiming
  #endif
      -- * Operations both internal and used in unit tests
    , permittedProjectClient, xhairLegalEps
    ) 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 qualified Data.Map.Strict as M
  import qualified Data.Text as T
  import qualified NLP.Miniutter.English as MU
  
  import           Game.LambdaHack.Client.BfsM
  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.Animation
  import           Game.LambdaHack.Client.UI.Content.Screen
  import           Game.LambdaHack.Client.UI.ContentClientUI
  import           Game.LambdaHack.Client.UI.DrawM
  import           Game.LambdaHack.Client.UI.EffectDescription
  import           Game.LambdaHack.Client.UI.Frame
  import           Game.LambdaHack.Client.UI.FrameM
  import           Game.LambdaHack.Client.UI.HandleHelperM
  import qualified Game.LambdaHack.Client.UI.HumanCmd as HumanCmd
  import           Game.LambdaHack.Client.UI.InventoryM
  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.PointUI
  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           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.Point
  import           Game.LambdaHack.Common.ReqFailure
  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 qualified Game.LambdaHack.Content.FactionKind as FK
  import qualified Game.LambdaHack.Content.ItemKind as IK
  import qualified Game.LambdaHack.Content.ModeKind as MK
  import           Game.LambdaHack.Content.RuleKind
  import qualified Game.LambdaHack.Definition.Ability as Ability
  import qualified Game.LambdaHack.Definition.Color as Color
  import           Game.LambdaHack.Definition.Defs
  
  -- * Macro
  
  macroHuman :: MonadClientUI m => [String] -> m ()
  macroHuman ks = do
    modifySession $ \sess ->
      let kms = K.mkKM <$> ks
          (smacroFrameNew, smacroStackMew) =
             macroHumanTransition kms (smacroFrame sess) (smacroStack sess)
      in sess { smacroFrame = smacroFrameNew
              , smacroStack = smacroStackMew }
    msgAdd MsgMacroOperation $ "Macro activated:" <+> T.pack (unwords ks)
  
  -- | Push a new macro frame to the stack whenever repeating a macro.
  macroHumanTransition :: [K.KM] -> KeyMacroFrame -> [KeyMacroFrame]
                       -> (KeyMacroFrame, [KeyMacroFrame])
  macroHumanTransition kms macroFrame macroFrames =
    let smacroFrameNew = emptyMacroFrame {keyPending = KeyMacro kms}
    in (smacroFrameNew, macroFrame : macroFrames)
  
  -- * ChooseItem
  
  -- | Display items from a given container store and possibly let the user
  -- chose one.
  chooseItemHuman :: MonadClientUI m => ActorId -> ItemDialogMode -> m MError
  chooseItemHuman leader c =
    either Just (const Nothing) <$> chooseItemDialogMode leader False c
  
  chooseItemDialogModeLore :: forall m . MonadClientUI m
                           => m (Maybe ResultItemDialogMode)
  chooseItemDialogModeLore = do
    schosenLoreOld <- getsSession schosenLore
    (inhabitants, embeds) <- case schosenLoreOld of
      ChosenLore inh emb -> return (inh, emb)
      ChosenNothing -> computeChosenLore
    bagHuge <- getsState $ EM.map (const quantSingle) . sitemD
    itemToF <- getsState $ flip itemToFull
    ItemRoles itemRoles <- getsSession sroles
    let rlore :: ItemId -> SLore -> ChosenLore -> m (Maybe ResultItemDialogMode)
        rlore iid slore schosenLore = do
          let itemRole = itemRoles EM.! slore
              bagAll = EM.filterWithKey (\iid2 _ -> iid2 `ES.member` itemRole)
                                        bagHuge
          modifySession $ \sess -> sess {schosenLore}
          let iids = sortIids itemToF $ EM.assocs bagAll
              slot = toEnum $ fromMaybe (error $ "" `showFailure` (iid, iids))
                            $ elemIndex iid $ map fst iids
          return $ Just $ RLore slore slot iids
    case inhabitants of
      (_, b) : rest -> do
        let iid = btrunk b
        arItem <- getsState $ aspectRecordFromIid iid
        let slore | not $ bproj b = STrunk
                  | IA.checkFlag Ability.Blast arItem = SBlast
                  | otherwise = SItem
        rlore iid slore (ChosenLore rest embeds)
      [] ->
        case embeds of
          (iid, _) : rest -> do
            let slore = SEmbed
            rlore iid slore (ChosenLore inhabitants rest)
          [] -> do
            modifySession $ \sess -> sess {schosenLore = ChosenNothing}
            return Nothing
  
  chooseItemDialogMode :: forall m. MonadClientUI m
                       => ActorId -> Bool -> ItemDialogMode
                       -> m (FailOrCmd ActorId)
  chooseItemDialogMode leader0 permitLoreCycle c = do
    CCUI{coscreen=ScreenContent{rwidth, rheight}} <- getsSession sccui
    FontSetup{propFont} <- getFontSetup
    side <- getsClient sside
    fact <- getsState $ (EM.! side) . sfactionD
    (ggi, loreFound) <- do
      mggiLore <- if permitLoreCycle && c == MLore SItem
                  then chooseItemDialogModeLore
                  else return Nothing
      case mggiLore of
        Just rlore -> return (Right rlore, True)
        Nothing -> do
          ggi <- getStoreItem leader0 c
          return (ggi, False)
    -- Pointman could have been changed in @getStoreItem@ above.
    mleader <- getsClient sleader
    -- When run inside a test, without mleader, assume leader not changed.
    let leader = fromMaybe leader0 mleader
    recordHistory  -- item chosen, wipe out already shown msgs
    actorCurAndMaxSk <- getsState $ getActorMaxSkills leader
    let meleeSkill = Ability.getSk Ability.SkHurtMelee actorCurAndMaxSk
    bUI <- getsSession $ getActorUI leader
    case ggi of
      Right result -> case result of
        RStore fromCStore [iid] -> do
          modifySession $ \sess ->
            sess {sitemSel = Just (iid, fromCStore, False)}
          return $ Right leader
        RStore{} -> error $ "" `showFailure` result
        ROwned iid -> do
          found <- getsState $ findIid leader side iid
          let (newAid, bestStore) = case leader `lookup` found of
                Just (_, store) -> (leader, store)
                Nothing -> case found of
                  (aid, (_, store)) : _ -> (aid, store)
                  [] -> error $ "" `showFailure` result
          modifySession $ \sess ->
            sess {sitemSel = Just (iid, bestStore, False)}
          arena <- getArenaUI
          b2 <- getsState $ getActorBody newAid
          let banned = bannedPointmanSwitchBetweenLevels fact
          if | newAid == leader -> return $ Right leader
             | blid b2 /= arena && banned ->
               failSer NoChangeDunLeader
             | otherwise -> do
               -- We switch leader only here, not when processing results
               -- of lore screens, because lore is only about inspecting items.
               --
               -- This is a bit too verbose in aiming mode, but verbosity
               -- here is good to turn player's attention to the switch.
               void $ pickLeader True newAid
               return $ Right newAid
        RLore slore slot iids -> do
          let promptFun _ itemFull _ = case slore of
                SBody ->
                  let blurb = if IA.checkFlag Ability.Condition
                                 $ aspectRecordFull itemFull
                              then "condition"
                              else "organ"
                  in makeSentence [partActor bUI, "is aware of" ,MU.AW blurb]
                _ ->
                  makeSentence [ MU.SubjectVerbSg (partActor bUI) "remember"
                               , MU.AW $ MU.Text (headingSLore slore) ]
          schosenLore <- getsSession schosenLore
          let lorePending = loreFound && case schosenLore of
                ChosenLore [] [] -> False
                _ -> True
              renderOneItem =
                okxItemLoreMsg promptFun meleeSkill (MLore slore) iids
              extraKeys = [K.mkChar '~' | lorePending]
              slotBound = length iids - 1
          km <- displayOneMenuItem renderOneItem extraKeys slotBound slot
          case K.key km of
            K.Space -> do
              modifySession $ \sess -> sess {schosenLore = ChosenNothing}
              chooseItemDialogMode leader False (MLore slore)
            K.Char '~' -> chooseItemDialogMode leader True c
            K.Esc -> do
              modifySession $ \sess -> sess {schosenLore = ChosenNothing}
              failWith "never mind"
            _ -> error $ "" `showFailure` km
        RSkills slot0 -> do
          -- This can be used in the future, e.g., to increase stats from
          -- level-up stat points, so let's keep it even if it shows
          -- no extra info compared to right pane display in menu.
          let renderOneItem slot = do
                (prompt2, attrString) <- skillCloseUp leader slot
                let ov0 = EM.singleton propFont
                          $ offsetOverlay
                          $ splitAttrString rwidth rwidth attrString
                msgAdd MsgPromptGeneric prompt2
                return (ov0, [])
              extraKeys = []
              slotBound = length skillsInDisplayOrder - 1
          km <- displayOneMenuItem renderOneItem extraKeys slotBound slot0
          case K.key km of
            K.Space -> chooseItemDialogMode leader False MSkills
            K.Esc -> failWith "never mind"
            _ -> error $ "" `showFailure` km
        RPlaces slot0 -> do
          COps{coplace} <- getsState scops
          soptions <- getsClient soptions
          -- This is computed just once for the whole series of up and down arrow
          -- navigations, avoid quadratic blowup.
          places <- getsState $ EM.assocs
                                . placesFromState coplace (sexposePlaces soptions)
          let renderOneItem slot = do
                (prompt2, blurbs) <-
                  placeCloseUp places (sexposePlaces soptions) slot
                let splitText = splitAttrString rwidth rwidth
                    ov0 = attrLinesToFontMap
                          $ map (second $ concatMap splitText) blurbs
                msgAdd MsgPromptGeneric prompt2
                return (ov0, [])
              extraKeys = []
              slotBound = length places - 1
          km <- displayOneMenuItem renderOneItem extraKeys slotBound slot0
          case K.key km of
            K.Space -> chooseItemDialogMode leader False MPlaces
            K.Esc -> failWith "never mind"
            _ -> error $ "" `showFailure` km
        RFactions slot0 -> do
          sroles <- getsSession sroles
          factions <- getsState $ factionsFromState sroles
          let renderOneItem slot = do
                (prompt2, blurbs) <- factionCloseUp factions slot
                let splitText = splitAttrString rwidth rwidth
                    ov0 = attrLinesToFontMap
                          $ map (second $ concatMap splitText) blurbs
                msgAdd MsgPromptGeneric prompt2
                return (ov0, [])
              extraKeys = []
              slotBound = length factions - 1
          km <- displayOneMenuItem renderOneItem extraKeys slotBound slot0
          case K.key km of
            K.Space -> chooseItemDialogMode leader False MFactions
            K.Esc -> failWith "never mind"
            _ -> error $ "" `showFailure` km
        RModes slot0 -> do
          let displayOneMenuItemBig :: (MenuSlot -> m OKX)
                                    -> [K.KM] -> Int -> MenuSlot
                                    -> m K.KM
              displayOneMenuItemBig renderOneItem extraKeys slotBound slot = do
                let keys = [K.spaceKM, K.escKM]
                           ++ [K.upKM | fromEnum slot > 0]
                           ++ [K.downKM | fromEnum slot < slotBound]
                           ++ extraKeys
                okx <- renderOneItem slot
                -- Here it differs from @displayOneMenuItem@,
                slides <- overlayToSlideshow rheight keys okx
                ekm2 <- displayChoiceScreen "" ColorFull True slides keys
                let km = either id (error $ "" `showFailure` ekm2) ekm2
                -- Here it stops differing.
                case K.key km of
                  K.Up -> displayOneMenuItemBig renderOneItem extraKeys
                                                slotBound $ pred slot
                  K.Down -> displayOneMenuItemBig renderOneItem extraKeys
                                                  slotBound $ succ slot
                  _ -> return km
          COps{comode} <- getsState scops
          svictories <- getsSession svictories
          nxtChal <- getsClient snxtChal
            -- mark victories only for current difficulty
          let f !acc _p !i !a = (i, a) : acc
              campaignModes = ofoldlGroup' comode MK.CAMPAIGN_SCENARIO f []
              renderOneItem slot = do
                let (gameModeId, gameMode) = campaignModes !! fromEnum slot
                ov0 <- describeMode False gameModeId
                let victories = case EM.lookup gameModeId svictories of
                      Nothing -> 0
                      Just cm -> fromMaybe 0 (M.lookup nxtChal cm)
                    verb = if victories > 0 then "remember" else "forsee"
                    prompt2 = makeSentence
                      [ MU.SubjectVerbSg "you" verb
                      , MU.Text $ "the '" <> MK.mname gameMode <> "' adventure" ]
                msgAdd MsgPromptGeneric prompt2
                return (ov0, [])
              extraKeys = []
              slotBound = length campaignModes - 1
          km <- displayOneMenuItemBig renderOneItem extraKeys slotBound slot0
          case K.key km of
            K.Space -> chooseItemDialogMode leader False MModes
            K.Esc -> failWith "never mind"
            _ -> error $ "" `showFailure` km
      Left err -> failWith err
  
  -- * ChooseItemProject
  
  chooseItemProjectHuman :: forall m. (MonadClient m, MonadClientUI m)
                         => ActorId -> [HumanCmd.TriggerItem] -> m MError
  chooseItemProjectHuman leader ts = do
    b <- getsState $ getActorBody leader
    mstash <- getsState $ \s -> gstash $ sfactionD s EM.! bfid b
    let overStash = mstash == Just (blid b, bpos b)
        storesBase = [CStash, CEqp]
        stores | overStash = storesBase ++ [CGround]
               | otherwise = CGround : storesBase
        (verb1, object1) = case ts of
          [] -> ("aim", "item")
          tr : _ -> (HumanCmd.tiverb tr, HumanCmd.tiobject tr)
        verb = makePhrase [verb1]
        triggerSyms = triggerSymbols ts
    mpsuitReq <- psuitReq leader
    case mpsuitReq of
      -- If xhair aim invalid, no item is considered a (suitable) missile.
      Left err -> failMsg err
      Right psuitReqFun -> do
        itemSel <- getsSession sitemSel
        case itemSel of
          Just (_, _, True) -> return Nothing
          Just (iid, fromCStore, False) -> do
            -- We don't validate vs @ts@ here, because player has selected
            -- this item, so he knows what he's doing (unless really absurd).
            itemFull <- getsState $ itemToFull iid
            bag <- getsState $ getBodyStoreBag b fromCStore
            case iid `EM.lookup` bag of
              Just _ | isRight (psuitReqFun itemFull) ->
                -- The player knows what he's doing. We warn him about range
                -- and experimenting with unknown precious items is fine.
                return Nothing
              _ -> do
                modifySession $ \sess -> sess {sitemSel = Nothing}
                chooseItemProjectHuman leader ts
          Nothing -> do
            let psuit =
                  return $ SuitsSomething $ \_ itemFull _kit ->
                    -- Here the player does not explicitly pick an item,
                    -- so we may exclude precious unknown items, etc.
                    either (const False) snd (psuitReqFun itemFull)
                    && (null triggerSyms
                        || IK.isymbol (itemKind itemFull) `elem` triggerSyms)
                prompt = makePhrase ["What", object1, "to"]
                promptGeneric = "What to"
            ggi <- getGroupItem leader psuit prompt promptGeneric verb "fling"
                                stores
            case ggi of
              Right (fromCStore, iid) -> do
                modifySession $ \sess ->
                  sess {sitemSel = Just (iid, fromCStore, False)}
                return Nothing
              Left err -> failMsg err
  
  permittedProjectClient :: MonadClientUI m
                         => ActorId -> m (ItemFull -> Either ReqFailure Bool)
  permittedProjectClient leader = do
    actorCurAndMaxSk <- getsState $ getActorMaxSkills leader
    b <- getsState $ getActorBody leader
    let skill = Ability.getSk Ability.SkProject actorCurAndMaxSk
        calmE = calmEnough b actorCurAndMaxSk
    return $ permittedProject False skill calmE
  
  projectCheck :: MonadClientUI m => ActorId -> Point -> m (Maybe ReqFailure)
  projectCheck leader tpos = do
    COps{coTileSpeedup} <- getsState scops
    eps <- getsClient seps
    sb <- getsState $ getActorBody leader
    let lid = blid sb
        spos = bpos sb
    -- Not @ScreenContent@, because not drawing here.
    case bresenhamsLineAlgorithm eps spos tpos of
      Nothing -> return $ Just ProjectAimOnself
      Just [] -> error $ "project from the edge of level"
                         `showFailure` (spos, tpos, sb)
      Just (pos : _) -> do
        lvl <- getLevel lid
        let t = lvl `at` pos
        if not $ Tile.isWalkable coTileSpeedup t
          then return $ Just ProjectBlockTerrain
          else if occupiedBigLvl pos lvl
               then return $ Just ProjectBlockActor
               else return Nothing
  
  -- | Check whether one is permitted to aim (for projecting) at a target.
  -- The check is stricter for actor targets, assuming the player simply wants
  -- to hit a single actor. In order to fine tune trick-shots, e.g., piercing
  -- many actors, other aiming modes should be used.
  -- Returns a different @seps@ if needed to reach the target.
  --
  -- Note: Simple Perception check is not enough for the check,
  -- e.g., because the target actor can be obscured by a glass wall.
  xhairLegalEps :: MonadClientUI m => ActorId -> m (Either Text Int)
  xhairLegalEps leader = do
    cops@COps{corule=RuleContent{rWidthMax, rHeightMax}} <- getsState scops
    b <- getsState $ getActorBody leader
    lidV <- viewedLevelUI
    let !_A = assert (lidV == blid b) ()
        findNewEps onlyFirst pos = do
          lvl <- getLevel (blid b)
          oldEps <- getsClient seps
          return $! case makeLine onlyFirst b pos oldEps cops lvl of
            Just newEps -> Right newEps
            Nothing -> Left $ if onlyFirst
                              then "aiming blocked at the first step"
                              else "aiming line blocked somewhere"
    xhair <- getsSession sxhair
    case xhair of
      Nothing -> return $ Left "no aim designated"
      Just (TEnemy a) -> do
        body <- getsState $ getActorBody a
        let pos = bpos body
        if blid body == lidV
        then findNewEps False pos
        else return $ Left "can't fling at an enemy on remote level"
      Just (TNonEnemy a) -> do
        body <- getsState $ getActorBody a
        let pos = bpos body
        if blid body == lidV
        then findNewEps False pos
        else return $ Left "can't fling at a non-enemy on remote level"
      Just (TPoint TEnemyPos{} _ _) ->
        return $ Left "selected opponent not visible"
      Just (TPoint _ lid pos) ->
        if lid == lidV
        then findNewEps True pos  -- @True@ to help pierce many foes, etc.
        else return $ Left "can't fling at a target on remote level"
      Just (TVector v) -> do
        -- Not @ScreenContent@, because not drawing here.
        let shifted = shiftBounded rWidthMax rHeightMax (bpos b) v
        if shifted == bpos b && v /= Vector 0 0
        then return $ Left "selected translation is void"
        else findNewEps True shifted  -- @True@, because the goal is vague anyway
  
  posFromXhair :: (MonadClient m, MonadClientUI m)
               => ActorId -> m (Either Text Point)
  posFromXhair leader = do
    canAim <- xhairLegalEps leader
    case canAim of
      Right newEps -> do
        -- Modify @seps@, permanently.
        modifyClient $ \cli -> cli {seps = newEps}
        mxhairPos <- mxhairToPos
        case mxhairPos of
          Nothing -> error $ "" `showFailure` mxhairPos
          Just pos -> do
            munit <- projectCheck leader pos
            case munit of
              Nothing -> return $ Right pos
              Just reqFail -> return $ Left $ showReqFailure reqFail
      Left cause -> return $ Left cause
  
  -- | On top of `permittedProjectClient`, it also checks legality
  -- of aiming at the target and projection range. It also modifies @eps@.
  psuitReq :: (MonadClient m, MonadClientUI m)
           => ActorId
           -> m (Either Text (ItemFull -> Either ReqFailure (Point, Bool)))
  psuitReq leader = do
    b <- getsState $ getActorBody leader
    lidV <- viewedLevelUI
    if lidV /= blid b
    then return $ Left "can't fling on remote level"
    else do
      mpos <- posFromXhair leader
      p <- permittedProjectClient leader
      case mpos of
        Left err -> return $ Left err
        Right pos -> return $ Right $ \itemFull ->
          case p itemFull of
            Left err -> Left err
            Right False -> Right (pos, False)
            Right True ->
              let arItem = aspectRecordFull itemFull
              in Right (pos, 1 + IA.totalRange arItem (itemKind itemFull)
                             >= chessDist (bpos b) pos)
  
  -- $setup
  -- >>> import Game.LambdaHack.Definition.DefsInternal
  
  -- |
  -- >>> let trigger1 = HumanCmd.TriggerItem{tiverb="verb", tiobject="object", tisymbols=[toContentSymbol 'a', toContentSymbol 'b']}
  -- >>> let trigger2 = HumanCmd.TriggerItem{tiverb="verb2", tiobject="object2", tisymbols=[toContentSymbol 'c']}
  -- >>> triggerSymbols [trigger1, trigger2]
  -- "abc"
  --
  -- >>> triggerSymbols []
  -- ""
  triggerSymbols :: [HumanCmd.TriggerItem] -> [ContentSymbol IK.ItemKind]
  triggerSymbols [] = []
  triggerSymbols (HumanCmd.TriggerItem{tisymbols} : ts) =
    tisymbols ++ triggerSymbols ts
  
  -- * ChooseItemApply
  
  chooseItemApplyHuman :: forall m. MonadClientUI m
                       => ActorId -> [HumanCmd.TriggerItem] -> m MError
  chooseItemApplyHuman leader ts = do
    b <- getsState $ getActorBody leader
    mstash <- getsState $ \s -> gstash $ sfactionD s EM.! bfid b
    let overStash = mstash == Just (blid b, bpos b)
        storesBase = [CStash, CEqp, COrgan]
        stores | overStash = storesBase ++ [CGround]
               | otherwise = CGround : storesBase
        (verb1, object1) = case ts of
          [] -> ("trigger", "item")
          tr : _ -> (HumanCmd.tiverb tr, HumanCmd.tiobject tr)
        verb = makePhrase [verb1]
        triggerSyms = triggerSymbols ts
        prompt = makePhrase ["What", object1, "to"]
        promptGeneric = "What to"
    itemSel <- getsSession sitemSel
    case itemSel of
      Just (_, _, True) -> return Nothing
      Just (iid, fromCStore, False) -> do
        -- We don't validate vs @ts@ here, because player has selected
        -- this item, so he knows what he's doing (unless really absurd).
        itemFull <- getsState $ itemToFull iid
        bag <- getsState $ getBodyStoreBag b fromCStore
        mp <- permittedApplyClient leader
        case iid `EM.lookup` bag of
          Just kit | fromRight False (mp (Just fromCStore) itemFull kit) ->
            return Nothing
          _ -> do
            modifySession $ \sess -> sess {sitemSel = Nothing}
            chooseItemApplyHuman leader ts
      Nothing -> do
        let psuit :: m Suitability
            psuit = do
              mp <- permittedApplyClient leader
              return $ SuitsSomething $ \cstore itemFull kit ->
                fromRight False (mp cstore itemFull kit)
                && (null triggerSyms
                    || IK.isymbol (itemKind itemFull) `elem` triggerSyms)
        ggi <- getGroupItem leader psuit prompt promptGeneric verb "trigger"
                            stores
        case ggi of
          Right (fromCStore, iid) -> do
            modifySession $ \sess ->
              sess {sitemSel = Just (iid, fromCStore, False)}
            return Nothing
          Left err -> failMsg err
  
  permittedApplyClient :: MonadClientUI m
                       => ActorId
                       -> m (Maybe CStore -> ItemFull -> ItemQuant
                             -> Either ReqFailure Bool)
  permittedApplyClient leader = do
    COps{corule} <- getsState scops
    actorCurAndMaxSk <- getsState $ getActorMaxSkills leader
    b <- getsState $ getActorBody leader
    let skill = Ability.getSk Ability.SkApply actorCurAndMaxSk
        calmE = calmEnough b actorCurAndMaxSk
    localTime <- getsState $ getLocalTime (blid b)
    return $ permittedApply corule localTime skill calmE
  
  -- * PickLeader
  
  pickLeaderHuman :: MonadClientUI m => Int -> m MError
  pickLeaderHuman k = do
    side <- getsClient sside
    fact <- getsState $ (EM.! side) . sfactionD
    arena <- getArenaUI
    sactorUI <- getsSession sactorUI
    mhero <- getsState $ tryFindHeroK sactorUI side k
    allOurs <- getsState $ fidActorNotProjGlobalAssocs side -- not only on level
    let allOursUI = map (\(aid, b) -> (aid, b, sactorUI EM.! aid)) allOurs
        hs = sortOn keySelected allOursUI
        mactor = case drop k hs of
                   [] -> Nothing
                   (aid, b, _) : _ -> Just (aid, b)
        mchoice = if FK.fhasGender (gkind fact) then mhero else mactor
        banned = bannedPointmanSwitchBetweenLevels fact
    case mchoice of
      Nothing -> failMsg "no such member of the party"
      Just (aid, b)
        | blid b /= arena && banned ->
            failMsg $ showReqFailure NoChangeDunLeader
        | otherwise -> do
            void $ pickLeader True aid
            return Nothing
  
  -- * PickLeaderWithPointer
  
  pickLeaderWithPointerHuman :: MonadClientUI m => ActorId -> m MError
  pickLeaderWithPointerHuman = pickLeaderWithPointer
  
  -- * PointmanCycle
  
  -- | Switch current pointman to the next on the viewed level, if any, wrapping.
  pointmanCycleLevelHuman :: MonadClientUI m => ActorId -> Direction -> m MError
  pointmanCycleLevelHuman leader = pointmanCycleLevel leader True
  
  -- * PointmanBack
  
  -- | Switch current pointman to the previous in the whole dungeon, wrapping.
  pointmanCycleHuman :: MonadClientUI m => ActorId -> Direction -> m MError
  pointmanCycleHuman leader = pointmanCycle leader True
  
  -- * SelectActor
  
  selectActorHuman :: MonadClientUI m => ActorId -> m ()
  selectActorHuman leader = do
    bodyUI <- getsSession $ getActorUI leader
    wasMemeber <- getsSession $ ES.member leader . sselected
    let upd = if wasMemeber
              then ES.delete leader  -- already selected, deselect instead
              else ES.insert leader
    modifySession $ \sess -> sess {sselected = upd $ sselected sess}
    let subject = partActor bodyUI
    msgAdd MsgActionAlert $ makeSentence [subject, if wasMemeber
                                                   then "deselected"
                                                   else "selected"]
  
  -- * SelectNone
  
  selectNoneHuman :: MonadClientUI m => m ()
  selectNoneHuman = do
    side <- getsClient sside
    lidV <- viewedLevelUI
    oursIds <- getsState $ fidActorRegularIds side lidV
    let ours = ES.fromDistinctAscList oursIds
    oldSel <- getsSession sselected
    let wasNone = ES.null $ ES.intersection ours oldSel
        upd = if wasNone
              then ES.union  -- already all deselected; select all instead
              else ES.difference
    modifySession $ \sess -> sess {sselected = upd (sselected sess) ours}
    let subject = "all party members on the level"
    msgAdd MsgActionAlert $ makeSentence [subject, if wasNone
                                                   then "selected"
                                                   else "deselected"]
  
  -- * SelectWithPointer
  
  selectWithPointerHuman :: MonadClientUI m => m MError
  selectWithPointerHuman = do
    COps{corule=RuleContent{rHeightMax}} <- getsState scops
    lidV <- viewedLevelUI
    -- Not @ScreenContent@, because not drawing here.
    side <- getsClient sside
    ours <- getsState $ filter (not . bproj . snd)
                        . actorAssocs (== side) lidV
    sactorUI <- getsSession sactorUI
    let oursUI = map (\(aid, b) -> (aid, b, sactorUI EM.! aid)) ours
        viewed = sortOn keySelected oursUI
    pUI <- getsSession spointer
    let p@(Point px py) = squareToMap $ uiToSquare pUI
    -- Select even if no space in status line for the actor's symbol.
    if | py == rHeightMax + 1 && px == 0 -> selectNoneHuman >> return Nothing
       | py == rHeightMax + 1 ->
           case drop (px - 1) viewed of
             [] -> failMsg "not pointing at an actor"
             (aid, _, _) : _ -> selectActorHuman aid >> return Nothing
       | otherwise ->
           case find (\(_, b) -> bpos b == p) ours of
             Nothing -> failMsg "not pointing at an actor"
             Just (aid, _) -> selectActorHuman aid >> return Nothing
  
  -- * Repeat
  
  -- Note that walk followed by repeat should not be equivalent to run,
  -- because the player can really use a command that does not stop
  -- at terrain change or when walking over items.
  repeatHuman :: MonadClientUI m => Int -> m ()
  repeatHuman n =
    modifySession $ \sess ->
      let (smacroFrameNew, smacroStackMew) =
             repeatHumanTransition n (smacroFrame sess) (smacroStack sess)
      in sess { smacroFrame = smacroFrameNew
              , smacroStack = smacroStackMew }
  
  repeatHumanTransition :: Int -> KeyMacroFrame -> [KeyMacroFrame]
                        -> (KeyMacroFrame, [KeyMacroFrame])
  repeatHumanTransition n macroFrame macroFrames =
    let kms = concat . replicate n . unKeyMacro . fromRight mempty
              $ keyMacroBuffer macroFrame
    in macroHumanTransition kms macroFrame macroFrames
  
  -- * RepeatLast
  
  -- Note that walk followed by repeat should not be equivalent to run,
  -- because the player can really use a command that does not stop
  -- at terrain change or when walking over items.
  repeatLastHuman :: MonadClientUI m => Int -> m ()
  repeatLastHuman n = modifySession $ \sess ->
    sess {smacroFrame = repeatLastHumanTransition n (smacroFrame sess) }
  
  repeatLastHumanTransition :: Int -> KeyMacroFrame -> KeyMacroFrame
  repeatLastHumanTransition n macroFrame =
    let macro = KeyMacro . concat . replicate n . maybeToList $ keyLast macroFrame
    in macroFrame { keyPending = macro <> keyPending macroFrame }
  
  -- * Record
  
  -- | Starts and stops recording of macros.
  recordHuman :: MonadClientUI m => m ()
  recordHuman = do
    smacroFrameOld <- getsSession smacroFrame
    let (smacroFrameNew, msg) = recordHumanTransition smacroFrameOld
    modifySession $ \sess -> sess {smacroFrame = smacroFrameNew}
    macroStack <- getsSession smacroStack
    unless (T.null msg || not (null macroStack)) $ msgAdd MsgPromptGeneric msg
  
  recordHumanTransition :: KeyMacroFrame -> (KeyMacroFrame, Text)
  recordHumanTransition macroFrame =
    let (buffer, msg) = case keyMacroBuffer macroFrame of
          Right _ ->
            -- Start recording in-game macro.
            (Left [], "Recording a macro. Stop recording with the same key.")
          Left xs ->
            -- Stop recording in-game macro.
            (Right . KeyMacro . reverse $ xs, "Macro recording stopped.")
        smacroFrameNew = macroFrame {keyMacroBuffer = buffer}
    in (smacroFrameNew, msg)
  
  -- * AllHistory
  
  allHistoryHuman :: forall m. MonadClientUI m => m ()
  allHistoryHuman = do
    CCUI{coscreen=ScreenContent{rwidth, rheight}} <- getsSession sccui
    history <- getsSession shistory
    arena <- getArenaUI
    localTime <- getsState $ getLocalTime arena
    global <- getsState stime
    FontSetup{..} <- getFontSetup
    let renderedHistoryRaw = renderHistory history
        histLenRaw = length renderedHistoryRaw
        placeholderLine = textFgToAS Color.BrBlack
          "Newest_messages_are_at_the_bottom._Press_END_to_get_there."
        placeholderCount =
          (- histLenRaw `mod` (rheight - 4)) `mod` (rheight - 4)
        renderedHistory = replicate placeholderCount placeholderLine
                          ++ renderedHistoryRaw
        histLen = placeholderCount + histLenRaw
        splitRow as =
          let (tLab, tDesc) = span (/= Color.spaceAttrW32) as
              labLen = textSize monoFont tLab
              par1 = case filter (/= emptyAttrLine) $ linesAttr tDesc of
                [] -> emptyAttrLine
                [l] -> l
                ls -> attrStringToAL $ intercalate [Color.spaceAttrW32]
                                     $ map attrLine ls
          in (attrStringToAL tLab, (labLen, par1))
        (tsLab, tsDesc) = unzip $ map splitRow renderedHistory
        ovs = EM.insertWith (++) monoFont (offsetOverlay tsLab)
              $ EM.singleton propFont $ offsetOverlayX tsDesc
        turnsGlobal = global `timeFitUp` timeTurn
        turnsLocal = localTime `timeFitUp` timeTurn
        msg = makeSentence
          [ "You survived for"
          , MU.CarWs turnsGlobal "half-second turn"
          , "(this level:"
          , MU.Car turnsLocal <> ")" ]
        kxs = [ (Right sn, ( PointUI 0 (fromEnum sn)
                           , ButtonWidth propFont 1000 ))
              | sn <- take histLen natSlots ]
    msgAdd MsgPromptGeneric msg
    let keysAllHistory =
          K.returnKM
  #ifndef USE_JSFILE
          : K.mkChar '.'
  #endif
          : [K.spaceKM, K.escKM]
    slides <- overlayToSlideshow (rheight - 2) keysAllHistory (ovs, kxs)
    let historyLines = case reverse $ concatMap snd $ slideshow slides of
          (Left{}, _) : rest -> rest  -- don't count the @--more--@ line
          l -> l
        maxIx = length historyLines - 1 - length keysAllHistory
        menuName = "history"
    modifySession $ \sess ->
      sess {smenuIxMap = M.insert menuName maxIx $ smenuIxMap sess}
    let displayAllHistory = do
          ekm <- displayChoiceScreen menuName ColorFull False slides
                                     keysAllHistory
          case ekm of
            Left km | km == K.mkChar '.' -> do
              let t = T.unlines $ map (T.pack . map Color.charFromW32)
                                      renderedHistoryRaw
              path <- dumpTextFile t "history.txt"
              msgAdd MsgPromptGeneric $ "All of history dumped to file" <+> T.pack path <> "."
            Left km | km == K.escKM ->
              msgAdd MsgPromptGeneric "Try to survive a few seconds more, if you can."
            Left km | km == K.spaceKM ->
              msgAdd MsgPromptGeneric "Steady on."
            Left km | km == K.returnKM ->
              msgAdd MsgPromptGeneric "Press RET when history message selected to see it in full."
            Right slot ->
              displayOneReport $ toEnum $ max 0 $ fromEnum slot - placeholderCount
            _ -> error $ "" `showFailure` ekm
        displayOneReport :: MenuSlot -> m ()
        displayOneReport slot0 = do
          let renderOneItem slot = do
                let timeReport = case drop (fromEnum slot)
                                           renderedHistoryRaw of
                      [] -> error $ "" `showFailure` slot
                      tR : _ -> tR
                    markParagraph c | Color.charFromW32 c == '\n' = [c, c]
                    markParagraph c = [c]
                    reportWithParagraphs = concatMap markParagraph timeReport
                    (ovLab, ovDesc) =
                      labDescOverlay monoFont rwidth reportWithParagraphs
                    ov0 = EM.insertWith (++) monoFont ovLab
                          $ EM.singleton propFont ovDesc
                    prompt = makeSentence
                      [ "the", MU.Ordinal $ fromEnum slot + 1
                      , "most recent record follows" ]
                msgAdd MsgPromptGeneric prompt
                return (ov0, [])
              extraKeys = []
              slotBound = histLenRaw - 1
          km <- displayOneMenuItem renderOneItem extraKeys slotBound slot0
          case K.key km of
            K.Space -> displayAllHistory
            K.Esc -> msgAdd MsgPromptGeneric
                            "Try to learn from your previous mistakes."
            _ -> error $ "" `showFailure` km
    displayAllHistory
  
  -- * MarkVision
  
  markVisionHuman :: MonadClientUI m => Int -> m ()
  markVisionHuman delta = modifySession $ cycleMarkVision delta
  
  -- * MarkSmell
  
  markSmellHuman :: MonadClientUI m => m ()
  markSmellHuman = modifySession toggleMarkSmell
  
  -- * MarkSuspect
  
  markSuspectHuman :: MonadClient m => Int -> m ()
  markSuspectHuman delta = do
    -- @condBFS@ depends on the setting we change here.
    invalidateBfsAll
    modifyClient (cycleMarkSuspect delta)
  
  -- * MarkAnim
  
  markAnimHuman :: MonadClient m => m ()
  markAnimHuman = do
    noAnim <- getsClient $ fromMaybe False . snoAnim . soptions
    modifyClient $ \cli ->
      cli {soptions = (soptions cli) {snoAnim = Just $ not noAnim}}
  
  -- * OverrideTut
  
  overrideTutHuman :: MonadClientUI m => Int -> m ()
  overrideTutHuman delta = modifySession $ cycleOverrideTut delta
  
  -- * PrintScreen
  
  printScreenHuman :: MonadClientUI m => m ()
  printScreenHuman = do
    msgAdd MsgActionAlert "Screenshot printed."
    printScreen
  
  -- * Cancel
  
  -- | End aiming mode, rejecting the current position, unless when on
  -- remote level, in which case, return to our level.
  cancelHuman :: MonadClientUI m => m ()
  cancelHuman = do
    maimMode <- getsSession saimMode
    case maimMode of
      Just aimMode -> do
        let lidV = aimLevelId aimMode
        lidOur <- getArenaUI
        if lidV == lidOur
        then clearAimMode
        else do
          xhairPos <- xhairToPos
          let sxhair = Just $ TPoint TKnown lidOur xhairPos
          modifySession $ \sess ->
            sess {saimMode = Just aimMode {aimLevelId = lidOur}}
          setXHairFromGUI sxhair
          doLook
      Nothing -> return ()
  
  -- * Accept
  
  -- | Accept the current crosshair position as target, ending
  -- aiming mode, if active.
  acceptHuman :: (MonadClient m, MonadClientUI m) => ActorId -> m ()
  acceptHuman leader = do
    endAiming leader
    endAimingMsg leader
    clearAimMode
  
  -- | End aiming mode, accepting the current position.
  endAiming :: (MonadClient m, MonadClientUI m) => ActorId -> m ()
  endAiming leader = do
    sxhair <- getsSession sxhair
    modifyClient $ updateTarget leader $ const sxhair
  
  endAimingMsg :: MonadClientUI m => ActorId -> m ()
  endAimingMsg leader = do
    subject <- partActorLeader leader
    tgt <- getsClient $ getTarget leader
    (mtargetMsg, _) <- targetDesc tgt
    msgAdd MsgActionAlert $ case mtargetMsg of
      Nothing ->
        makeSentence [MU.SubjectVerbSg subject "clear target"]
      Just targetMsg ->
        makeSentence [MU.SubjectVerbSg subject "target", MU.Text targetMsg]
  
  -- * DetailCycle
  
  -- | Cycle detail level of aiming mode descriptions, starting up.
  detailCycleHuman :: MonadClientUI m => m ()
  detailCycleHuman = do
    modifySession $ \sess -> sess {saimMode =
      (\aimMode -> aimMode {detailLevel = detailCycle $ detailLevel aimMode})
                   <$> saimMode sess}
    doLook
  
  detailCycle :: DetailLevel -> DetailLevel
  detailCycle detail = if detail == minBound then maxBound else pred detail
  
  -- * ClearTargetIfItemClear
  
  clearTargetIfItemClearHuman :: (MonadClient m, MonadClientUI m)
                              => ActorId -> m ()
  clearTargetIfItemClearHuman leader = do
    itemSel <- getsSession sitemSel
    when (isNothing itemSel) $ do
      setXHairFromGUI Nothing
      modifyClient $ updateTarget leader (const Nothing)
      doLook
  
  -- * ItemClear
  
  itemClearHuman :: MonadClientUI m => m ()
  itemClearHuman = modifySession $ \sess -> sess {sitemSel = Nothing}
  
  -- * MoveXhair
  
  -- | Move the xhair. Assumes aiming mode.
  moveXhairHuman :: MonadClientUI m => Vector -> Int -> m MError
  moveXhairHuman dir n = do
    -- Not @ScreenContent@, because not drawing here.
    COps{corule=RuleContent{rWidthMax, rHeightMax}} <- getsState scops
    saimMode <- getsSession saimMode
    let lidV = maybe (error $ "" `showFailure` (dir, n)) aimLevelId saimMode
    xhair <- getsSession sxhair
    xhairPos <- xhairToPos
    let shiftB pos = shiftBounded rWidthMax rHeightMax pos dir
        newPos = iterate shiftB xhairPos !! n
    if newPos == xhairPos then failMsg "never mind"
    else do
      mleader <- getsClient sleader
      sxhair <- case (xhair, mleader) of
       (Just TVector{}, Just leader) -> do
         lpos <- getsState $ bpos . getActorBody leader
         return $ Just $ TVector $ newPos `vectorToFrom` lpos
       _ -> return $ Just $ TPoint TKnown lidV newPos
      setXHairFromGUI sxhair
      doLook
      return Nothing
  
  -- * AimTgt
  
  -- | Start aiming.
  aimTgtHuman :: MonadClientUI m => m ()
  aimTgtHuman = do
    -- (Re)start aiming at the current level.
    lidV <- viewedLevelUI
    modifySession $ \sess -> sess {saimMode =
      let newDetail = maybe defaultDetailLevel detailLevel (saimMode sess)
      in Just $ AimMode lidV newDetail}
    doLook
    msgAdd MsgPromptAction "*flinging started; press again to project*"
  
  -- * AimFloor
  
  -- | Cycle aiming mode. Do not change position of the xhair,
  -- switch target to point at different things at that position.
  aimFloorHuman :: MonadClientUI m => m ()
  aimFloorHuman = do
    lidV <- viewedLevelUI
    mleader <- getsClient sleader
    mlpos <- case mleader of
      Nothing -> return Nothing
      Just leader -> getsState $ Just . bpos . getActorBody leader
    xhairPos <- xhairToPos
    xhair <- getsSession sxhair
    saimMode <- getsSession saimMode
    bsAll <- getsState $ actorAssocs (const True) lidV
    side <- getsClient sside
    fact <- getsState $ (EM.! side) . sfactionD
    let sxhair = case xhair of
          _ | isNothing saimMode ->  -- first key press: keep target
            xhair
          Just TEnemy{} -> Just $ TPoint TKnown lidV xhairPos
          Just TNonEnemy{} -> Just $ TPoint TKnown lidV xhairPos
          Just TPoint{} | Just lpos <- mlpos, xhairPos /= lpos ->
            Just $ TVector $ xhairPos `vectorToFrom` lpos
          Just TVector{} ->
            -- If many actors, we pick here the first that would be picked
            -- by '*', so that all other projectiles on the tile come next,
            -- when pressing '*', without any intervening actors from other tiles.
            -- This is why we use @actorAssocs@ above instead of @posToAidAssocs@.
            case find (\(_, b) -> bpos b == xhairPos) bsAll of
              Just (aid, b) -> Just $ if isFoe side fact (bfid b)
                                      then TEnemy aid
                                      else TNonEnemy aid
              Nothing -> Just $ TPoint TUnknown lidV xhairPos
          _ -> xhair
    modifySession $ \sess -> sess {saimMode =
      let newDetail = maybe defaultDetailLevel detailLevel saimMode
      in Just $ AimMode lidV newDetail}
    setXHairFromGUI sxhair
    doLook
  
  -- * AimEnemy
  
  aimEnemyHuman :: MonadClientUI m => m ()
  aimEnemyHuman = do
    lidV <- viewedLevelUI
    mleader <- getsClient sleader
    mlpos <- case mleader of
      Nothing -> return Nothing
      Just leader -> getsState $ Just . bpos . getActorBody leader
    mxhairPos <- mxhairToPos
    xhair <- getsSession sxhair
    saimMode <- getsSession saimMode
    side <- getsClient sside
    fact <- getsState $ (EM.! side) . sfactionD
    bsAll <- getsState $ actorAssocs (const True) lidV
    let -- On the same position, big actors come before projectiles.
        ordPos lpos (_, b) = (chessDist lpos $ bpos b, bpos b, bproj b)
        dbs = case mlpos of
          Nothing -> bsAll
          Just lpos -> sortOn (ordPos lpos) bsAll
        pickUnderXhair =  -- switch to the actor under xhair, if any
          fromMaybe (-1) $ findIndex ((== mxhairPos) . Just . bpos . snd) dbs
        (pickEnemies, i) = case xhair of
          Just (TEnemy a) | isJust saimMode ->  -- pick next enemy
            (True, 1 + fromMaybe (-1) (findIndex ((== a) . fst) dbs))
          Just (TEnemy a) ->  -- first key press, retarget old enemy
            (True, fromMaybe (-1) $ findIndex ((== a) . fst) dbs)
          Just (TNonEnemy a) | isJust saimMode ->  -- pick next non-enemy
            (False, 1 + fromMaybe (-1) (findIndex ((== a) . fst) dbs))
          Just (TNonEnemy a) ->  -- first key press, retarget old non-enemy
            (False, fromMaybe (-1) $ findIndex ((== a) . fst) dbs)
          _ -> (True, pickUnderXhair)
        (lt, gt) = splitAt i dbs
        isEnemy b = isFoe side fact (bfid b)
                    && not (bproj b)
                    && bhp b > 0
        cond = if pickEnemies then isEnemy else not . isEnemy
        lf = filter (cond . snd) $ gt ++ lt
        sxhair = case lf of
          (a, _) : _ -> Just $ if pickEnemies then TEnemy a else TNonEnemy a
          [] -> xhair  -- no seen foes in sight, stick to last target
    -- Register the chosen enemy, to pick another on next invocation.
    modifySession $ \sess -> sess {saimMode =
      let newDetail = maybe defaultDetailLevel detailLevel saimMode
      in Just $ AimMode lidV newDetail}
    setXHairFromGUI sxhair
    doLook
  
  -- * AimItem
  
  aimItemHuman :: MonadClientUI m => m ()
  aimItemHuman = do
    side <- getsClient sside
    lidV <- viewedLevelUI
    mleader <- getsClient sleader
    mlpos <- case mleader of
      Nothing -> return Nothing
      Just leader -> getsState $ Just . bpos . getActorBody leader
    mxhairPos <- mxhairToPos
    xhair <- getsSession sxhair
    saimMode <- getsSession saimMode
    Level{lfloor} <- getLevel lidV
    mstash <- getsState $ \s -> gstash $ sfactionD s EM.! side
    -- Don't consider own stash an ordinary pile of items.
    let lfloorBarStash = case mstash of
          Just (lid, pos) | lid == lidV -> EM.delete pos lfloor
          _ -> lfloor
        bsAll = EM.keys lfloorBarStash
        ordPos lpos p = (chessDist lpos p, p)
        dbs = case mlpos of
          Nothing -> bsAll
          Just lpos -> sortOn (ordPos lpos) bsAll
        pickUnderXhair =  -- switch to the item under xhair, if any
          let i = fromMaybe (-1)
                  $ findIndex ((== mxhairPos) . Just) dbs
          in splitAt i dbs
        (lt, gt) = case xhair of
          Just (TPoint _ lid pos)
            | isJust saimMode && lid == lidV ->  -- pick next item
              let i = fromMaybe (-1) $ elemIndex pos dbs
              in splitAt (i + 1) dbs
          Just (TPoint _ lid pos)
            | lid == lidV ->  -- first key press, retarget old item
              let i = fromMaybe (-1) $ elemIndex pos dbs
              in splitAt i dbs
          _ -> pickUnderXhair
        gtlt = gt ++ lt
        sxhair = case gtlt of
          p : _ -> Just $ TPoint TKnown lidV p  -- don't force AI to collect it
          [] -> xhair  -- no items remembered, stick to last target
    -- Register the chosen enemy, to pick another on next invocation.
    modifySession $ \sess -> sess {saimMode =
      let newDetail = maybe defaultDetailLevel detailLevel saimMode
      in Just $ AimMode lidV newDetail}
    setXHairFromGUI sxhair
    doLook
  
  -- * AimAscend
  
  -- | Change the displayed level in aiming mode to (at most)
  -- k levels shallower. Enters aiming mode, if not already in one.
  aimAscendHuman :: MonadClientUI m => Int -> m MError
  aimAscendHuman k = do
    dungeon <- getsState sdungeon
    lidV <- viewedLevelUI
    let up = k > 0
    case ascendInBranch dungeon up lidV of
      [] -> failMsg "no more levels in this direction"
      _ : _ -> do
        let ascendOne lid = case ascendInBranch dungeon up lid of
              [] -> lid
              nlid : _ -> nlid
            lidK = iterate ascendOne lidV !! abs k
        xhairPos <- xhairToPos
        let sxhair = Just $ TPoint TKnown lidK xhairPos
        modifySession $ \sess -> sess {saimMode =
          let newDetail = maybe defaultDetailLevel detailLevel (saimMode sess)
          in Just $ AimMode lidK newDetail}
        setXHairFromGUI sxhair
        doLook
        return Nothing
  
  -- * EpsIncr
  
  -- | Tweak the @eps@ parameter of the aiming digital line.
  epsIncrHuman :: (MonadClient m, MonadClientUI m) => Direction -> m ()
  epsIncrHuman d = do
    -- Perform the change:
    let sepsDelta = case d of
          Forward -> 1
          Backward -> -1
    modifyClient $ \cli -> cli {seps = seps cli + sepsDelta}
    invalidateBfsPathAll
    -- Provide UI feedback:
    -- Hack @sreportNull@ to display the new line even if no earlier messages.
    modifySession $ \sess -> sess {sreportNull = False}
    saimMode <- getsSession saimMode
    lidV <- viewedLevelUI
    modifySession $ \sess -> sess {saimMode =
      let newDetail = maybe DetailLow detailLevel saimMode
      in Just $ AimMode lidV newDetail}
    flashAiming
    modifySession $ \sess -> sess {saimMode}
    -- The change may not affect the line shape, hence 'possibly'.
    msgAdd MsgPromptAction "Aiming line (possibly) modified."
  
  -- Flash the aiming line and path.
  flashAiming :: MonadClientUI m => m ()
  flashAiming = do
    lidV <- viewedLevelUI
    animate lidV pushAndDelay
  
  -- * XhairUnknown
  
  xhairUnknownHuman :: (MonadClient m, MonadClientUI m) => ActorId -> m MError
  xhairUnknownHuman leader = do
    b <- getsState $ getActorBody leader
    mpos <- closestUnknown leader
    case mpos of
      Nothing -> failMsg "no more unknown spots left"
      Just p -> do
        let sxhair = Just $ TPoint TUnknown (blid b) p
        setXHairFromGUI sxhair
        doLook
        return Nothing
  
  -- * XhairItem
  
  xhairItemHuman :: (MonadClient m, MonadClientUI m) => ActorId -> m MError
  xhairItemHuman leader = do
    b <- getsState $ getActorBody leader
    items <- closestItems leader
    case items of
      [] -> failMsg "no more reachable items remembered or visible"
      _ -> do
        let (_, (p, bag)) = maximumBy (comparing fst) items
            sxhair = Just $ TPoint (TItem bag) (blid b) p
        setXHairFromGUI sxhair
        doLook
        return Nothing
  
  -- * XhairStair
  
  xhairStairHuman :: (MonadClient m, MonadClientUI m)
                  => ActorId -> Bool -> m MError
  xhairStairHuman leader up = do
    b <- getsState $ getActorBody leader
    stairs <- closestTriggers (if up then ViaStairsUp else ViaStairsDown) leader
    case stairs of
      [] -> failMsg $ "no reachable stairs" <+> if up then "up" else "down"
      _ -> do
        let (_, (p, (p0, bag))) = maximumBy (comparing fst) stairs
            sxhair = Just $ TPoint (TEmbed bag p0) (blid b) p
        setXHairFromGUI sxhair
        doLook
        return Nothing
  
  -- * XhairPointerFloor
  
  xhairPointerFloorHuman :: MonadClientUI m => m ()
  xhairPointerFloorHuman = do
    saimMode <- getsSession saimMode
    aimPointerFloorHuman
    when (isNothing saimMode) $
      modifySession $ \sess -> sess {saimMode}
  
  -- * XhairPointerMute
  
  xhairPointerMuteHuman :: MonadClientUI m => m ()
  xhairPointerMuteHuman = do
    saimMode <- getsSession saimMode
    aimPointerFloorLoud False
    when (isNothing saimMode) $
      modifySession $ \sess -> sess {saimMode}
  
  -- * XhairPointerEnemy
  
  xhairPointerEnemyHuman :: MonadClientUI m => m ()
  xhairPointerEnemyHuman = do
    saimMode <- getsSession saimMode
    aimPointerEnemyHuman
    when (isNothing saimMode) $
      modifySession $ \sess -> sess {saimMode}
  
  -- * AimPointerFloor
  
  aimPointerFloorHuman :: MonadClientUI m => m ()
  aimPointerFloorHuman = aimPointerFloorLoud True
  
  aimPointerFloorLoud :: MonadClientUI m => Bool -> m ()
  aimPointerFloorLoud loud = do
    COps{corule=RuleContent{rWidthMax, rHeightMax}} <- getsState scops
    lidV <- viewedLevelUI
    -- Not @ScreenContent@, because not drawing here.
    pUI <- getsSession spointer
    let p = squareToMap $ uiToSquare pUI
    if insideP (0, 0, rWidthMax - 1, rHeightMax - 1) p
    then do
      oldXhair <- getsSession sxhair
      let sxhair = Just $ TPoint TUnknown lidV p
          sxhairMoused = sxhair /= oldXhair
          detailSucc = if sxhairMoused
                       then detailLevel
                       else detailCycle . detailLevel
      modifySession $ \sess ->
        sess { saimMode =
                 let newDetail = maybe defaultDetailLevel detailSucc
                                       (saimMode sess)
                 in Just $ AimMode lidV newDetail
             , sxhairMoused }
      setXHairFromGUI sxhair
      when loud doLook
    else stopPlayBack
  
  -- * AimPointerEnemy
  
  aimPointerEnemyHuman :: MonadClientUI m => m ()
  aimPointerEnemyHuman = do
    COps{corule=RuleContent{rWidthMax, rHeightMax}} <- getsState scops
    lidV <- viewedLevelUI
    -- Not @ScreenContent@, because not drawing here.
    pUI <- getsSession spointer
    let p = squareToMap $ uiToSquare pUI
    if insideP (0, 0, rWidthMax - 1, rHeightMax - 1) p
    then do
      bsAll <- getsState $ actorAssocs (const True) lidV
      oldXhair <- getsSession sxhair
      side <- getsClient sside
      fact <- getsState $ (EM.! side) . sfactionD
      let sxhair =
            -- If many actors, we pick here the first that would be picked
            -- by '*', so that all other projectiles on the tile come next,
            -- when pressing '*', without any intervening actors from other tiles.
            -- This is why we use @actorAssocs@ above instead of @posToAidAssocs@.
            case find (\(_, b) -> bpos b == p) bsAll of
              Just (aid, b) -> Just $ if isFoe side fact (bfid b)
                                      then TEnemy aid
                                      else TNonEnemy aid
              Nothing -> Just $ TPoint TUnknown lidV p
          sxhairMoused = sxhair /= oldXhair
          detailSucc = if sxhairMoused
                       then detailLevel
                       else detailCycle . detailLevel
      modifySession $ \sess ->
        sess { saimMode =
                 let newDetail = maybe defaultDetailLevel detailSucc
                                       (saimMode sess)
                 in Just $ AimMode lidV newDetail
             , sxhairMoused }
      setXHairFromGUI sxhair
      doLook
    else stopPlayBack
  