{-# LANGUAGE CPP #-} module Display where -- wrapper for selected Display frontend #ifdef CURSES import qualified Display.Curses as D #elif VTY import qualified Display.Vty as D #elif STD import qualified Display.Std as D #else import qualified Display.Gtk as D #endif -- Display routines that are independent of the selected display frontend. import qualified Data.Char as Char import qualified Data.Set as S import qualified Data.List as L import qualified Data.Map as M import qualified Data.IntMap as IM import Control.Monad.IO.Class import Data.Maybe import Message import qualified Color import State import Geometry import Loc import Area import Level import Perception import Actor import ActorState import Content.ActorKind import Content.TileKind import Item import qualified Keys as K import WorldLoc import Random import qualified Kind -- Re-exported from the display frontend, with an extra slot for function -- for translating keys to a canonical form. type InternalSession = D.Session type Session = (InternalSession, M.Map K.Key K.Key) display :: Area -> Session -> (Loc -> (Color.Attr, Char)) -> String -> String -> IO () display area = D.display area . fst startup :: (InternalSession -> IO ()) -> IO () startup = D.startup shutdown :: Session -> IO () shutdown = D.shutdown . fst displayId :: String displayId = D.displayId -- | Next event translated to a canonical form. nextCommand :: MonadIO m => Session -> m K.Key nextCommand session = do e <- liftIO $ D.nextEvent (fst session) return $ fromMaybe (K.canonMoveKey e) (M.lookup e (snd session)) -- | Displays a message on a blank screen. Waits for confirmation. displayBlankConfirm :: Session -> String -> IO Bool displayBlankConfirm session txt = let x = txt ++ more doBlank = const (Color.defaultAttr, ' ') (lx, ly) = normalLevelBound -- TODO: query terminal size instead in do display (0, 0, lx, ly) session doBlank x "" getConfirm session -- | Waits for a space or return or '?' or '*'. The last two act this way, -- to let keys that request information toggle display the information off. getConfirm :: MonadIO m => Session -> m Bool getConfirm session = getOptionalConfirm return (const $ getConfirm session) session getOptionalConfirm :: MonadIO m => (Bool -> m a) -> (K.Key -> m a) -> Session -> m a getOptionalConfirm h k session = do e <- liftIO $ nextCommand session case e of K.Char ' ' -> h True K.Char '?' -> h True K.Char '*' -> h True K.Return -> h True K.Esc -> h False _ -> k e -- | A yes-no confirmation. getYesNo :: MonadIO m => Session -> m Bool getYesNo session = do e <- liftIO $ nextCommand session case e of K.Char 'y' -> return True K.Char 'n' -> return False K.Esc -> return False _ -> getYesNo session splitOverlay :: Int -> String -> [[String]] splitOverlay s xs = splitOverlay' (lines xs) where splitOverlay' ls | length ls <= s = [ls] -- everything fits on one screen | otherwise = let (pre,post) = splitAt (s - 1) ls in (pre ++ [more]) : splitOverlay' post -- | Returns a function that looks up the characters in the -- string by location. Takes the height of the display plus -- the string. Returns also the number of screens required -- to display all of the string. stringByLocation :: Y -> String -> (Int, (X, Y) -> Maybe Char) stringByLocation sy xs = let ls = splitOverlay sy xs m = M.fromList (zip [0..] (L.map (M.fromList . zip [0..]) (concat ls))) k = length ls in (k, \ (x, y) -> M.lookup y m >>= \ n -> M.lookup x n) data ColorMode = ColorFull | ColorBW displayLevel :: ColorMode -> Session -> Perceptions -> State -> Message -> Maybe String -> IO Bool displayLevel dm session per state@State{scursor, stime, sflavour, slid} msg moverlay = let lvl@Level{lxsize = sx, lysize = sy, lsmell = smap} = slevel state Actor{bkind, bhp, bloc, bitems} = getPlayerBody state ActorKind{ahp} = Kind.getKind bkind reachable = ptreachable per visible = ptvisible per overlay = fromMaybe "" moverlay (ns, over) = stringByLocation sy overlay -- n overlay screens needed sSml = ssensory state == Smell sVis = case ssensory state of Vision _ -> True; _ -> False sOmn = sdisplay state == Omniscient lAt = if sOmn then at else rememberAt liAt = if sOmn then iat else irememberAt sVisBG = if sVis then \ vis rea -> if vis then Color.Blue else if rea then Color.Magenta else Color.defBG else \ _vis _rea -> Color.defBG wealth = L.sum $ L.map itemPrice bitems damage = case strongestItem bitems "sword" of Just sw -> 3 + jpower sw Nothing -> 3 hs = levelHeroList state ms = levelMonsterList state dis n loc0 = let tile = lvl `lAt` loc0 items = lvl `liAt` loc0 sm = smelltime $ IM.findWithDefault (SmellTime 0) loc0 smap sml = (sm - stime) `div` 100 viewActor loc Actor{bkind = bkind2, bsymbol} | loc == bloc && slid == creturnLn scursor = (symbol, Color.defBG) -- highlight player | otherwise = (symbol, acolor) where ActorKind{asymbol, acolor} = Kind.getKind bkind2 symbol = fromMaybe asymbol bsymbol viewSmell :: Int -> Char viewSmell k | k > 9 = '*' | k < 0 = '-' | otherwise = Char.intToDigit k rainbow loc = toEnum $ loc `rem` 14 + 1 (char, fg0) = case L.find (\ m -> loc0 == Actor.bloc m) (hs ++ ms) of Just m | sOmn || vis -> viewActor loc0 m _ | sSml && sml >= 0 -> (viewSmell sml, rainbow loc0) | otherwise -> case items of [] -> let u = Kind.getKind tile in (tsymbol u, if vis then tcolor u else tcolor2 u) i : _ -> Item.viewItem (jkind i) sflavour vis = S.member loc0 visible rea = S.member loc0 reachable bg0 = if ctargeting scursor && loc0 == clocation scursor then Color.defFG -- highlight targeting cursor else sVisBG vis rea -- FOV debug reverseVideo = (snd Color.defaultAttr, fst Color.defaultAttr) optVisually (fg, bg) = if (fg == Color.defBG) || (bg == Color.defFG && fg == Color.defFG) then reverseVideo else (fg, bg) a = case dm of ColorBW -> Color.defaultAttr ColorFull -> optVisually (fg0, bg0) in case over (fromLoc sx loc0 `shiftXY` (0, sy * n)) of Just c -> (Color.defaultAttr, c) _ -> (a, char) status = take 30 (levelName slid ++ repeat ' ') ++ take 10 ("T: " ++ show (stime `div` 10) ++ repeat ' ') ++ take 10 ("$: " ++ show wealth ++ repeat ' ') ++ take 10 ("Dmg: " ++ show damage ++ repeat ' ') ++ take 20 ("HP: " ++ show bhp ++ " (" ++ show (maxDice ahp) ++ ")" ++ repeat ' ') disp n mesg = display (0, 0, sx - 1, sy - 1) session (dis n) mesg status msgs = splitMsg sx msg perf k [] = perfo k "" perf k [xs] = perfo k xs perf k (x:xs) = disp ns (x ++ more) >> getConfirm session >>= \ b -> if b then perf k xs else return False perfo k xs | k < ns - 1 = disp k xs >> getConfirm session >>= \ b -> if b then perfo (k+1) xs else return False | otherwise = disp k xs >> return True in perf 0 msgs