module Perception where import qualified Data.Set as S import Data.List as L import qualified Data.IntMap as IM import Data.Maybe import Control.Monad import Geometry import State import Level import Movable import MovableState import qualified MovableKind import FOV import qualified Config import qualified Tile data Perception = Perception { preachable :: S.Set Loc, pvisible :: S.Set Loc } -- The pplayer field is void if player not on the current level, -- or if the player controls a blind monster. Right now, the field is used only -- for player-controlled monsters on the current level. data Perceptions = Perceptions { pplayer :: Maybe Perception, pheroes :: IM.IntMap Perception, ptotal :: Perception } ptreachable, ptvisible :: Perceptions -> S.Set Loc ptreachable = preachable . ptotal ptvisible = pvisible . ptotal actorPrLoc :: (Perception -> S.Set Loc) -> Actor -> Loc -> Perceptions -> Maybe Actor -> Bool actorPrLoc projection actor loc per pl = let tryHero = case actor of AMonster _ -> Nothing AHero i -> do hper <- IM.lookup i (pheroes per) return $ loc `S.member` (projection hper) tryPl = do -- the case for a monster under player control guard $ Just actor == pl pper <- pplayer per return $ loc `S.member` projection pper tryAny = tryHero `mplus` tryPl in fromMaybe False tryAny -- assume not visible, if no perception found actorSeesLoc :: Actor -> Loc -> Perceptions -> Maybe Actor -> Bool actorSeesLoc = actorPrLoc pvisible actorReachesLoc :: Actor -> Loc -> Perceptions -> Maybe Actor -> Bool actorReachesLoc = actorPrLoc preachable -- Not quite correct if FOV not symmetric (Shadow). actorReachesActor :: Actor -> Actor -> Loc -> Loc -> Perceptions -> Maybe Actor -> Bool actorReachesActor actor1 actor2 loc1 loc2 per pl = actorReachesLoc actor1 loc2 per pl || actorReachesLoc actor2 loc1 per pl perception_ :: State -> Perceptions perception_ state@(State { splayer = pl, slevel = Level { lmap = lmap, lheroes = hs }, sconfig = config, ssensory = sensory }) = let mode = Config.get config "engine" "fovMode" radius = Config.get config "engine" "fovRadius" fovMode m = if not $ MovableKind.nsight (mkind m) then Blind else -- terrible, temporary hack case sensory of Vision 3 -> Digital radius Vision 2 -> Permissive radius Vision 1 -> Shadow _ -> -- this is not a hack case mode of "permissive" -> Permissive radius "digital" -> Digital radius "shadow" -> Shadow _ -> error $ "perception_: unknown mode: " ++ show mode -- Perception for a player-controlled monster on the current level. pper = if isAMonster pl && memActor pl state then let m = getPlayerBody state in Just $ perception (fovMode m) (mloc m) lmap else Nothing pers = IM.map (\ h -> perception (fovMode h) (mloc h) lmap) hs lpers = maybeToList pper ++ IM.elems pers reachable = S.unions (L.map preachable lpers) visible = S.unions (L.map pvisible lpers) in Perceptions { pplayer = pper, pheroes = pers, ptotal = Perception reachable visible } -- | Once we compute the reachable fields using FOV, it is possible -- to compute what the hero can actually see. perception :: FovMode -> Loc -> LMap -> Perception perception fovMode ploc lmap = let -- Reachable are all fields on an unblocked path from the hero position. reachable = fullscan fovMode ploc lmap -- Everybody can see locations that have light and are reachable. uniVisible = S.filter (\ loc -> Tile.light (lmap `at` loc)) reachable -- The hero is assumed to carry a light source, too. litVisible = S.insert ploc uniVisible -- Reachable fields adjacent to lit fields are visible, too. adjVisible = S.filter (\ loc -> L.any (\ l -> S.member l litVisible) (surroundings loc)) reachable -- Visible fields are either lit or adjacent to lit. visible = S.union litVisible adjVisible in Perception reachable visible