-- | Field Of View scanning with a variety of algorithms. -- See -- for discussion. module Game.LambdaHack.Server.Fov ( dungeonPerception, levelPerception , fullscan, FovMode(..) ) where import Control.Arrow (second) import Data.Binary import qualified Data.EnumMap.Strict as EM import qualified Data.EnumSet as ES import qualified Data.List as L import Game.LambdaHack.Common.Actor import Game.LambdaHack.Common.ActorState import Game.LambdaHack.Common.Faction import qualified Game.LambdaHack.Common.Kind as Kind import Game.LambdaHack.Common.Level import Game.LambdaHack.Common.Perception import Game.LambdaHack.Common.Point import Game.LambdaHack.Common.State import qualified Game.LambdaHack.Common.Tile as Tile import Game.LambdaHack.Common.Vector import Game.LambdaHack.Common.VectorXY import Game.LambdaHack.Content.ActorKind import Game.LambdaHack.Content.TileKind import Game.LambdaHack.Server.Fov.Common import qualified Game.LambdaHack.Server.Fov.Digital as Digital import qualified Game.LambdaHack.Server.Fov.Permissive as Permissive import qualified Game.LambdaHack.Server.Fov.Shadow as Shadow newtype PerceptionReachable = PerceptionReachable { preachable :: ES.EnumSet Point } deriving Show -- | Calculate perception of the level. levelPerception :: Kind.COps -> State -> FovMode -> FactionId -> LevelId -> Level -> Perception levelPerception cops@Kind.COps{cotile} s configFov fid lid lvl = let hs = actorAssocs (== fid) lid s reas = map (second $ computeReachable cops configFov lvl) hs lreas = map (preachable . snd) reas totalRea = PerceptionReachable $ ES.unions lreas -- TODO: give actors light sources explicitly or alter vision. lights = ES.fromList $ map (bpos . snd) hs ptotal = computeVisible cotile totalRea lvl lights g = PerceptionVisible . ES.intersection (pvisible ptotal) . preachable perActor = EM.map g $ EM.fromList reas -- reas is not sorted psmell = smellFromActors cops s perActor in Perception {..} -- | Calculate perception of a faction. factionPerception :: Kind.COps -> FovMode -> State -> FactionId -> FactionPers factionPerception cops configFov s fid = EM.mapWithKey (levelPerception cops s configFov fid) $ sdungeon s -- | Calculate the perception of the whole dungeon. dungeonPerception :: Kind.COps -> FovMode -> State -> Pers dungeonPerception cops configFov s = let f fid _ = factionPerception cops configFov s fid in EM.mapWithKey f $ sfactionD s -- | A position can be directly lit by an ambient shine or a weak, portable -- light source, e.g,, carried by a hero. (Only lights of radius 0 -- are considered for now and it's assumed they do not reveal hero's position. -- TODO: change this to be radius 1 noctovision and introduce stronger -- light sources that show more but make the hero visible.) -- A position is visible if it's reachable and either directly lit -- or adjacent to one that is at once directly lit and reachable. -- The last condition approximates being -- on the same side of obstacles as the light source. -- The approximation is not exact for multiple heroes, but the discrepancy -- can be attributed to deduction based on combined vague visual hints, -- e.g., if I don't see the reachable light seen by another hero, -- there must be a wall in-between. Stray rays indicate doors, -- moving shadows indicate monsters, etc. computeVisible :: Kind.Ops TileKind -> PerceptionReachable -> Level -> ES.EnumSet Point -> PerceptionVisible computeVisible cotile reachable@PerceptionReachable{preachable} lvl lights = let isV = isVisible cotile reachable lvl lights in PerceptionVisible $ ES.filter isV preachable -- TODO: this is calculated per-faction, not per-actor, but still optimize, -- e.g., by partitioning preachable wrt litDirectly and then running -- isVisible only over one of the parts, depending on which is smaller. isVisible :: Kind.Ops TileKind -> PerceptionReachable -> Level -> ES.EnumSet Point -> Point -> Bool isVisible cotile PerceptionReachable{preachable} lvl@Level{lxsize, lysize} lights pos0 = let litDirectly pos = Tile.isLit cotile (lvl `at` pos) || pos `ES.member` lights l_and_R pos = litDirectly pos && pos `ES.member` preachable in litDirectly pos0 || L.any l_and_R (vicinity lxsize lysize pos0) -- | Reachable are all fields on an unblocked path from the hero position. computeReachable :: Kind.COps -> FovMode -> Level -> Actor -> PerceptionReachable computeReachable Kind.COps{cotile, coactor=Kind.Ops{okind}} configFov lvl body = let sight = asight $ okind $ bkind body fovMode = if sight then configFov else Blind ppos = bpos body scan = fullscan cotile fovMode ppos lvl in PerceptionReachable $ ES.fromList scan -- | Perform a full scan for a given position. Returns the positions -- that are currently in the field of view. The Field of View -- algorithm to use, passed in the second argument, is set in the config file. -- The actor's own position is considred reachable by him. fullscan :: Kind.Ops TileKind -- ^ tile content, determines clear tiles -> FovMode -- ^ scanning mode -> Point -- ^ position of the spectator -> Level -- ^ the map that is scanned -> [Point] fullscan cotile fovMode spectatorPos Level{lxsize, ltile} = spectatorPos : case fovMode of Shadow -> L.concatMap (\ tr -> map tr (Shadow.scan (isCl . tr) 1 (0, 1))) tr8 Permissive -> L.concatMap (\ tr -> map tr (Permissive.scan (isCl . tr))) tr4 Digital r -> L.concatMap (\ tr -> map tr (Digital.scan r (isCl . tr))) tr4 Blind -> -- all actors feel adjacent positions (for easy exploration) let radiusOne = 1 in L.concatMap (\ tr -> map tr (Digital.scan radiusOne (isCl . tr))) tr4 where isCl :: Point -> Bool isCl = Tile.isClear cotile . (ltile Kind.!) trV xy = shift spectatorPos $ toVector lxsize $ VectorXY xy -- | The translation, rotation and symmetry functions for octants. tr8 :: [(Distance, Progress) -> Point] tr8 = [ \ (p, d) -> trV ( p, d) , \ (p, d) -> trV (- p, d) , \ (p, d) -> trV ( p, - d) , \ (p, d) -> trV (- p, - d) , \ (p, d) -> trV ( d, p) , \ (p, d) -> trV (- d, p) , \ (p, d) -> trV ( d, - p) , \ (p, d) -> trV (- d, - p) ] -- | The translation and rotation functions for quadrants. tr4 :: [Bump -> Point] tr4 = [ \ (B(x, y)) -> trV ( x, - y) -- quadrant I , \ (B(x, y)) -> trV ( y, x) -- II (we rotate counter-clockwise) , \ (B(x, y)) -> trV (- x, y) -- III , \ (B(x, y)) -> trV (- y, - x) -- IV ] -- TODO: should Blind really be a FovMode, or a modifier? Let's decide -- when other similar modifiers are added. -- | Field Of View scanning mode. data FovMode = Shadow -- ^ restrictive shadow casting | Permissive -- ^ permissive FOV | Digital !Int -- ^ digital FOV with the given radius | Blind -- ^ only feeling out adjacent tiles by touch deriving (Show, Read) instance Binary FovMode where put Shadow = putWord8 0 put Permissive = putWord8 1 put (Digital r) = putWord8 2 >> put r put Blind = putWord8 3 get = do tag <- getWord8 case tag of 0 -> return Shadow 1 -> return Permissive 2 -> fmap Digital get 3 -> return Blind _ -> fail "no parse (FovMode)"