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
|
-- | A set of Frame monad operations.
module Game.LambdaHack.Client.UI.FrameM
( drawOverlay, promptGetKey, addToMacro, dropEmptyMacroFrames
, lastMacroFrame, stopPlayBack, renderAnimFrames, animate
#ifdef EXPOSE_INTERNAL
-- * Internal operations
, resetPlayBack, restoreLeaderFromRun, basicFrameForAnimation
#endif
) where
import Prelude ()
import Game.LambdaHack.Core.Prelude
import qualified Data.Bifunctor as B
import qualified Data.EnumMap.Strict as EM
import qualified Data.Map.Strict as M
import qualified Data.Vector.Unboxed as U
import Game.LambdaHack.Client.MonadClient
import Game.LambdaHack.Client.State
import Game.LambdaHack.Client.UI.Animation
import Game.LambdaHack.Client.UI.Content.Input
import Game.LambdaHack.Client.UI.Content.Screen
import Game.LambdaHack.Client.UI.ContentClientUI
import Game.LambdaHack.Client.UI.DrawM
import Game.LambdaHack.Client.UI.Frame
import qualified Game.LambdaHack.Client.UI.HumanCmd as HumanCmd
import qualified Game.LambdaHack.Client.UI.Key as K
import Game.LambdaHack.Client.UI.MonadClientUI
import Game.LambdaHack.Client.UI.Msg
import Game.LambdaHack.Client.UI.MsgM
import Game.LambdaHack.Client.UI.Overlay
import Game.LambdaHack.Client.UI.PointUI
import Game.LambdaHack.Client.UI.SessionUI
import Game.LambdaHack.Client.UI.Slideshow
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.Common.Types
import qualified Game.LambdaHack.Definition.Color as Color
-- | Draw the current level with the overlay on top.
drawOverlay :: MonadClientUI m
=> ColorMode -> Bool -> FontOverlayMap -> LevelId
-> m PreFrame3
drawOverlay dm onBlank ovs lid = do
CCUI{coscreen=ScreenContent{rwidth, rheight}} <- getsSession sccui
basicFrame <- if onBlank
then do
let m = U.replicate (rwidth * rheight)
(Color.attrCharW32 Color.spaceAttrW32)
return (m, FrameForall $ \_v -> return ())
else drawHudFrame dm lid
FontSetup{..} <- getFontSetup
let propWidth = if isMonoFont propFont then 2 * rwidth else 4 * rwidth
ovProp | not (isSquareFont propFont)
= truncateOverlay False propWidth rheight False 0 onBlank
$ EM.findWithDefault [] propFont ovs
| otherwise = []
ovMono = if not (isSquareFont monoFont)
then truncateOverlay False (2 * rwidth) rheight False 0 onBlank
$ EM.findWithDefault [] monoFont ovs
else []
ovSquare | not (isSquareFont propFont)
= truncateOverlay False (2 * rwidth) rheight False 0 onBlank
$ EM.findWithDefault [] squareFont ovs
| otherwise = []
ovOther | not (isSquareFont propFont) = []
| otherwise
= truncateOverlay True rwidth rheight True 20 onBlank
$ concat $ EM.elems ovs
-- 20 needed not to leave gaps in skill menu
-- in the absence of backdrop
ovBackdrop =
if not (isSquareFont propFont) && not onBlank
then let propOutline =
truncateOverlay False propWidth rheight True 0 onBlank
$ EM.findWithDefault [] propFont ovs
monoOutline =
truncateOverlay False (2 * rwidth) rheight True 0 onBlank
$ EM.findWithDefault [] monoFont ovs
squareOutline =
truncateOverlay False (2 * rwidth) rheight True 0 onBlank
$ EM.findWithDefault [] squareFont ovs
g x al Nothing = Just (x, x + length al - 1)
g x al (Just (xmin, xmax)) =
Just (min xmin x, max xmax (x + length al - 1))
f em (PointUI x y, al) = EM.alter (g x al) y em
extentMap = foldl' f EM.empty
$ propOutline ++ monoOutline ++ squareOutline
listBackdrop (y, (xmin, xmax)) =
( PointUI (2 * (xmin `div` 2)) y
, blankAttrString
$ min (rwidth - 2 * (xmin `div` 2))
(1 + xmax `divUp` 2 - xmin `div` 2) )
in map listBackdrop $ EM.assocs extentMap
else []
overlayedFrame = overlayFrame rwidth ovOther
$ overlayFrame rwidth ovBackdrop basicFrame
return (overlayedFrame, (ovProp, ovSquare, ovMono))
promptGetKey :: MonadClientUI m
=> ColorMode -> FontOverlayMap -> Bool -> [K.KM]
-> m K.KM
promptGetKey dm ovs onBlank frontKeyKeys = do
lidV <- viewedLevelUI
report <- getsSession $ newReport . shistory
sreqQueried <- getsSession sreqQueried
macroFrame <- getsSession smacroFrame
let interrupted =
-- If server is not querying for request, then the key is needed due to
-- a special event, not ordinary querying the player for command,
-- so interrupt.
not sreqQueried
-- Any alarming message interupts macros, except when the macro
-- displays help and ends, which is a helpful thing to do.
|| (anyInReport disturbsResting report
&& keyPending macroFrame /= KeyMacro [K.mkKM "F1"])
km <- case keyPending macroFrame of
KeyMacro (km : kms) | not interrupted
-- A faulty key in a macro is a good reason
-- to interrupt it, as well.
&& (null frontKeyKeys || km `elem` frontKeyKeys) -> do
-- No need to display the frame, because a frame was displayed
-- when the player chose to play a macro and each turn or more often
-- a frame is displayed elsewhere.
-- The only excepton is when navigating menus through macros,
-- but there the speed is particularly welcome.
modifySession $ \sess ->
sess {smacroFrame = (smacroFrame sess) {keyPending = KeyMacro kms}}
msgAdd MsgMacroOperation $ "Voicing '" <> tshow km <> "'."
return km
KeyMacro kms -> do
if null kms then do
-- There was no macro. Not important if there was a reason
-- for interrupt or not.
when (dm /= ColorFull) $ do
-- This marks a special event, regardless of @sreqQueried@.
side <- getsClient sside
fact <- getsState $ (EM.! side) . sfactionD
unless (gunderAI fact) -- don't forget special autoplay keypresses
-- Forget the furious keypresses just before a special event.
resetPressedKeys
-- Running, if any, must have ended naturally, because no macro.
-- Therefore no need to restore leader back to initial run leader,
-- but running itself is cancelled below.
else do
-- The macro was not empty, but not played, so it must have been
-- interrupted, so we can't continue playback, so wipe out the macro.
resetPlayBack
-- This might have been an unexpected end of a run, too.
restoreLeaderFromRun
-- Macro was killed, so emergency, so reset input, too.
resetPressedKeys
frontKeyFrame <- drawOverlay dm onBlank ovs lidV
recordHistory
modifySession $ \sess ->
sess { srunning = Nothing
, sxhairGoTo = Nothing
, sdisplayNeeded = False
, sturnDisplayed = True }
connFrontendFrontKey frontKeyKeys frontKeyFrame
-- In-game macros need to be recorded here, not in @UI.humanCommand@,
-- to also capture choice of items from menus, etc.
-- Notice that keys coming from macros (from content, in-game, config)
-- are recorded as well and this is well defined and essential.
--
-- Only keys pressed when player is queried for a command are recorded.
when sreqQueried $ do
CCUI{coinput=InputContent{bcmdMap}} <- getsSession sccui
modifySession $ \sess ->
sess {smacroFrame = addToMacro bcmdMap km $ smacroFrame sess}
return km
addToMacro :: M.Map K.KM HumanCmd.CmdTriple -> K.KM -> KeyMacroFrame
-> KeyMacroFrame
addToMacro bcmdMap km macroFrame =
case (\(_, _, cmd) -> cmd) <$> M.lookup km bcmdMap of
Nothing -> macroFrame
Just HumanCmd.Record -> macroFrame
Just HumanCmd.RepeatLast{} -> macroFrame
_ -> macroFrame { keyMacroBuffer =
(km :) `B.first` keyMacroBuffer macroFrame }
-- This is noop when not recording a macro,
-- which is exactly the required semantics.
dropEmptyMacroFrames :: KeyMacroFrame -> [KeyMacroFrame]
-> (KeyMacroFrame, [KeyMacroFrame])
dropEmptyMacroFrames mf [] = (mf, [])
dropEmptyMacroFrames (KeyMacroFrame _ (KeyMacro []) _)
(mf : mfs) = dropEmptyMacroFrames mf mfs
dropEmptyMacroFrames mf mfs = (mf, mfs)
lastMacroFrame :: KeyMacroFrame -> [KeyMacroFrame] -> KeyMacroFrame
lastMacroFrame mf [] = mf
lastMacroFrame _ (mf : mfs) = lastMacroFrame mf mfs
stopPlayBack :: MonadClientUI m => m ()
stopPlayBack = msgAdd MsgStopPlayback "!"
-- | We wipe any actions in progress, but keep the data needed to repeat
-- the last global macros and the last command.
resetPlayBack :: MonadClientUI m => m ()
resetPlayBack =
modifySession $ \sess ->
let lastFrame = lastMacroFrame (smacroFrame sess) (smacroStack sess)
in sess { smacroFrame = lastFrame {keyPending = mempty}
, smacroStack = [] }
restoreLeaderFromRun :: MonadClientUI m => m ()
restoreLeaderFromRun = do
srunning <- getsSession srunning
case srunning of
Nothing -> return ()
Just RunParams{runLeader} -> do
-- Switch to the original leader, from before the run start,
-- unless dead or unless the faction never runs with multiple
-- (but could have the leader changed automatically meanwhile).
side <- getsClient sside
fact <- getsState $ (EM.! side) . sfactionD
arena <- getArenaUI
memA <- getsState $ memActor runLeader arena
when (memA && not (noRunWithMulti fact)) $
updateClientLeader runLeader
-- This is not our turn, so we can't obstruct screen with messages
-- and message reformatting causes distraction, so there's no point
-- trying to squeeze the report into the single available line,
-- except when it's not our turn permanently, because AI runs UI.
basicFrameForAnimation :: MonadClientUI m
=> LevelId -> Maybe Bool -> m PreFrame3
basicFrameForAnimation arena forceReport = do
FontSetup{propFont} <- getFontSetup
sbenchMessages <- getsClient $ sbenchMessages . soptions
side <- getsClient sside
fact <- getsState $ (EM.! side) . sfactionD
report <- getReportUI False
let par1 = firstParagraph $ foldr (<+:>) [] $ renderReport True report
-- If messages are benchmarked, they can't be displayed under AI,
-- because this is not realistic when player is in control.
truncRep | not sbenchMessages && fromMaybe (gunderAI fact) forceReport =
EM.fromList [(propFont, [(PointUI 0 0, par1)])]
| otherwise = EM.empty
drawOverlay ColorFull False truncRep arena
-- | Render animations on top of the current screen frame.
renderAnimFrames :: MonadClientUI m
=> LevelId -> Animation -> Maybe Bool -> m PreFrames3
renderAnimFrames arena anim forceReport = do
CCUI{coscreen=ScreenContent{rwidth}} <- getsSession sccui
snoAnim <- getsClient $ snoAnim . soptions
basicFrame <- basicFrameForAnimation arena forceReport
smuteMessages <- getsSession smuteMessages
return $! if | smuteMessages -> []
| fromMaybe False snoAnim -> [Just basicFrame]
| otherwise -> map (fmap (\fr -> (fr, snd basicFrame)))
$ renderAnim rwidth (fst basicFrame) anim
-- | Render and display animations on top of the current screen frame.
animate :: MonadClientUI m => LevelId -> Animation -> m ()
animate arena anim = do
-- The delay before reaction to keypress was too long in case of many
-- projectiles hitting actors, so frames need to be skipped.
keyPressed <- anyKeyPressed
unless keyPressed $ do
frames <- renderAnimFrames arena anim Nothing
displayFrames arena frames
|