packages feed

Allure-0.4.2: src/Perception.hs

module Perception where

import qualified Data.Set as S
import qualified Data.List as L
import qualified Data.IntMap as IM
import Data.Maybe
import Control.Monad

import Loc
import State
import Level
import Actor
import ActorState
import Content.ActorKind
import FOV
import qualified Config
import qualified Tile
import qualified Kind

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) ->
              ActorId -> Loc -> Perceptions -> Maybe ActorId -> 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    :: ActorId -> Loc -> Perceptions -> Maybe ActorId -> Bool
actorSeesLoc    = actorPrLoc pvisible

actorReachesLoc :: ActorId -> Loc -> Perceptions -> Maybe ActorId -> Bool
actorReachesLoc = actorPrLoc preachable

-- Not quite correct if FOV not symmetric (Shadow).
actorReachesActor :: ActorId -> ActorId -> Loc -> Loc
                     -> Perceptions -> Maybe ActorId
                     -> 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,
                           sconfig  = config,
                           ssensory = sensory }) =
  let lvl@Level{lheroes = hs} = slevel state
      mode   = Config.get config "engine" "fovMode"
      radius = let r = Config.get config "engine" "fovRadius"
               in if r < 1
                  then error $ "FOV radius is " ++ show r ++ ", should be >= 1"
                  else r
      fovMode m = if not $ asight $ Kind.getKind $ bkind 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 $ "Unknown FOV 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) (bloc m) lvl
             else Nothing
      pers = IM.map (\ h -> perception (fovMode h) (bloc h) lvl) 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 -> Level -> Perception
perception fovMode ploc lvl@Level{lxsize, lysize} =
  let
    -- Reachable are all fields on an unblocked path from the hero position.
    -- The player position is visible, but not reachable (e.g. for targeting).
    reachable  = fullscan fovMode ploc lvl
    -- Everybody can see locations that are lit and are reachable.
    uniVisible = S.filter (\ loc -> Tile.isLit (lvl `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
        (L.any (`S.member` litVisible) . surroundings lxsize lysize)
        reachable
    -- Visible fields are either lit or adjacent to lit.
    visible = S.union litVisible adjVisible
  in Perception reachable visible