-- | Ways for the client to use player input via UI to produce server -- requests, based on the client's view (visualized for the player) -- of the game state. module Game.LambdaHack.Client.UI ( -- * Querying the human player queryUI, queryUIunderAI -- * UI monad and session type , MonadClientUI(..), SessionUI(..) -- * Updating UI state wrt game state changes , watchRespUpdAtomicUI, watchRespSfxAtomicUI -- * Startup and initialization , CCUI(..) , UIOptions, applyUIOptions, uOverrideCmdline, mkUIOptions -- * Operations exposed for "Game.LambdaHack.Client.LoopM" , ChanFrontend, chanFrontend, tryRestore, clientPrintUI #ifdef EXPOSE_INTERNAL -- * Internal operations , stepQueryUIwithLeader, stepQueryUI #endif ) where import Prelude () import Game.LambdaHack.Core.Prelude 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 Game.LambdaHack.Client.MonadClient import Game.LambdaHack.Client.Request import Game.LambdaHack.Client.State import Game.LambdaHack.Client.UI.Content.Input import Game.LambdaHack.Client.UI.ContentClientUI import Game.LambdaHack.Client.UI.Frame import Game.LambdaHack.Client.UI.FrameM import Game.LambdaHack.Client.UI.Frontend import Game.LambdaHack.Client.UI.HandleHelperM import Game.LambdaHack.Client.UI.HandleHumanM import qualified Game.LambdaHack.Client.UI.Key as K import Game.LambdaHack.Client.UI.MonadClientUI import Game.LambdaHack.Client.UI.Msg import Game.LambdaHack.Client.UI.MsgM import Game.LambdaHack.Client.UI.Overlay import Game.LambdaHack.Client.UI.SessionUI import Game.LambdaHack.Client.UI.Slideshow import Game.LambdaHack.Client.UI.SlideshowM import Game.LambdaHack.Client.UI.UIOptions import Game.LambdaHack.Client.UI.UIOptionsParse import Game.LambdaHack.Client.UI.Watch import Game.LambdaHack.Common.Actor import Game.LambdaHack.Common.ActorState import Game.LambdaHack.Common.ClientOptions import Game.LambdaHack.Common.Faction import Game.LambdaHack.Common.MonadStateRead import Game.LambdaHack.Common.State import Game.LambdaHack.Content.ModeKind -- | Handle the move of a human player. queryUI :: (MonadClient m, MonadClientUI m) => m (Maybe RequestUI) queryUI = do sreqQueried <- getsSession sreqQueried let !_A = assert (not sreqQueried) () -- querying not nested modifySession $ \sess -> sess {sreqQueried = True} let loop = do mres <- stepQueryUIwithLeader saimMode <- getsSession saimMode case mres of Nothing | isJust saimMode -> loop -- loop until aiming finished _ -> return mres mres <- loop modifySession $ \sess -> sess {sreqQueried = False} return mres queryUIunderAI :: (MonadClient m, MonadClientUI m) => m RequestUI queryUIunderAI = do -- Record history so the player can browse it later on. recordHistory -- As long as UI faction is under AI control, check, once per move, -- for immediate control regain or benchmark game stop. sregainControl <- getsSession sregainControl if sregainControl then do modifySession $ \sess -> sess { sregainControl = False , sreqDelay = ReqDelayNot , sreqPending = Nothing } -- just in case -- The keys mashed to gain control are not considered a command. resetPressedKeys -- Menu is entered in @displayRespUpdAtomicUI@ at @UpdAutoFaction@ -- and @stopAfter@ is canceled in @cmdAtomicSemCli@ -- when handling the results of the request below. return (ReqUIAutomate, Nothing) else do stopAfterFrames <- getsClient $ sstopAfterFrames . soptions bench <- getsClient $ sbenchmark . soptions let exitCmd = if bench then ReqUIGameDropAndExit else ReqUIGameSaveAndExit case stopAfterFrames of Nothing -> do stopAfterSeconds <- getsClient $ sstopAfterSeconds . soptions case stopAfterSeconds of Nothing -> return (ReqUINop, Nothing) Just stopS -> do sstartPOSIX <- getsSession sstart exit <- elapsedSessionTimeGT sstartPOSIX stopS if exit then do tellAllClipPS return (exitCmd, Nothing) -- ask server to exit else return (ReqUINop, Nothing) Just stopF -> do allNframes <- getsSession sallNframes gnframes <- getsSession snframes if allNframes + gnframes >= stopF then do tellAllClipPS return (exitCmd, Nothing) -- ask server to exit else return (ReqUINop, Nothing) stepQueryUIwithLeader :: (MonadClient m, MonadClientUI m) => m (Maybe RequestUI) stepQueryUIwithLeader = do side <- getsClient sside mleader <- getsState $ gleader . (EM.! side) . sfactionD mreq <- stepQueryUI case mreq of Nothing -> return Nothing Just req -> do mleader2 <- getsClient sleader -- Don't send the leader switch to the server with these commands, -- to avoid leader death at resume if his HP <= 0. That would violate -- the principle that save and reload doesn't change game state. let saveCmd cmd = case cmd of ReqUIGameDropAndExit -> True ReqUIGameSaveAndExit -> True ReqUIGameSave -> True _ -> False return $ Just (req, if mleader /= mleader2 && not (saveCmd req) then mleader2 else Nothing) -- | Let the human player issue commands until any command takes time. stepQueryUI :: (MonadClient m, MonadClientUI m) => m (Maybe ReqUI) stepQueryUI = do FontSetup{propFont} <- getFontSetup keyPressed <- anyKeyPressed macroFrame <- getsSession smacroFrame -- This message, in particular, disturbs. when (keyPressed && not (null (unKeyMacro (keyPending macroFrame)))) $ msgAdd MsgActionWarning "*interrupted*" report <- getsSession $ newReport . shistory modifySession $ \sess -> sess {sreportNull = nullVisibleReport report} slides <- reportToSlideshowKeepHalt False [] ovs <- case unsnoc slides of Nothing -> return EM.empty Just (allButLast, (ov, _)) -> if allButLast == emptySlideshow then do -- Display the only generated slide while waiting for next key. -- Strip the "--end-" prompt from it, by ignoring @MonoFont@. let ovProp = ov EM.! propFont return $! EM.singleton propFont $ if EM.size ov > 1 then ovProp else init ovProp else do -- Show, one by one, all slides, awaiting confirmation for each. void $ getConfirms ColorFull [K.spaceKM, K.escKM] slides -- Indicate that report wiped out. modifySession $ \sess -> sess {sreportNull = True} -- Display base frame at the end. return EM.empty mleader <- getsClient sleader case mleader of Nothing -> return () Just leader -> do body <- getsState $ getActorBody leader lastLost <- getsSession slastLost if bhp body <= 0 then do side <- getsClient sside fact <- getsState $ (EM.! side) . sfactionD let gameOver = maybe False ((/= Camping) . stOutcome) (gquit fact) when (not gameOver && leader `ES.notMember` lastLost) $ do -- Hacky reuse of @slastLost@ for near-death spam prevention. modifySession $ \sess -> sess {slastLost = ES.insert leader lastLost} displayMore ColorBW "If you move, the exertion will kill you. Consider asking for first aid instead." else modifySession $ \sess -> sess {slastLost = ES.empty} km <- promptGetKey ColorFull ovs False [] abortOrCmd <- do -- Look up the key. CCUI{coinput=InputContent{bcmdMap}} <- getsSession sccui case km `M.lookup` bcmdMap of Just (_, _, cmd) -> do modifySession $ \sess -> sess {swaitTimes = if swaitTimes sess > 0 then - swaitTimes sess else 0} cmdSemInCxtOfKM km cmd _ -> let msgKey = "unknown command '" <> K.showKM km <> "'" in weaveJust <$> failWith (T.pack msgKey) -- GC macro stack if there are no actions left to handle, -- removing all unnecessary macro frames at once, -- but leaving the last one for user's in-game macros. modifySession $ \sess -> let (smacroFrameNew, smacroStackMew) = dropEmptyMacroFrames (smacroFrame sess) (smacroStack sess) in sess { smacroFrame = smacroFrameNew , smacroStack = smacroStackMew } -- The command was failed or successful and if the latter, -- possibly took some time. case abortOrCmd of Right cmdS -> -- Exit the loop and let other actors act. No next key needed -- and no report could have been generated. return $ Just cmdS Left Nothing -> return Nothing Left (Just err) -> do msgAdd MsgActionAlert $ showFailError err return Nothing