module Level where import Control.Monad import Data.Binary import Data.Map as M import Data.List as L import qualified Data.IntMap as IM import Geometry import GeometryRnd import Actor import Item import Random import qualified Terrain -- | Names of the dungeon levels are represented using a -- custom data structure. data LevelName = LambdaCave Int | Exit deriving (Show, Eq, Ord) instance Binary LevelName where put (LambdaCave n) = put n get = liftM LambdaCave get -- | Provide a textual description of a level name. levelName :: LevelName -> String levelName (LambdaCave n) = "The Lambda Cave " ++ show n -- | Gives the numeric representation of the level's depth. levelNumber :: LevelName -> Int levelNumber (LambdaCave n) = n -- | A dungeon location is a level together with a location on that level. type DungeonLoc = (LevelName, Loc) type Party = IM.IntMap Actor data Level = Level { lname :: LevelName, lheroes :: Party, -- ^ all heroes on the level lsize :: (Y,X), lmonsters :: Party, -- ^ all monsters on the level lsmell :: SMap, lmap :: LMap, lmeta :: String } deriving Show updateLMap :: (LMap -> LMap) -> Level -> Level updateLMap f lvl = lvl { lmap = f (lmap lvl) } updateSMap :: (SMap -> SMap) -> Level -> Level updateSMap f lvl = lvl { lsmell = f (lsmell lvl) } updateMonsters :: (Party -> Party) -> Level -> Level updateMonsters f lvl = lvl { lmonsters = f (lmonsters lvl) } updateHeroes :: (Party -> Party) -> Level -> Level updateHeroes f lvl = lvl { lheroes = f (lheroes lvl) } emptyParty :: Party emptyParty = IM.empty instance Binary Level where put (Level nm hs sz@(sy,sx) ms lsmell lmap lmeta) = do put nm put hs put sz put ms put [ lsmell ! (y,x) | y <- [0..sy], x <- [0..sx] ] put [ lmap ! (y,x) | y <- [0..sy], x <- [0..sx] ] put lmeta get = do nm <- get hs <- get sz@(sy,sx) <- get ms <- get xs <- get let lsmell = M.fromList (zip [ (y,x) | y <- [0..sy], x <- [0..sx] ] xs) xs <- get let lmap = M.fromList (zip [ (y,x) | y <- [0..sy], x <- [0..sx] ] xs) lmeta <- get return (Level nm hs sz ms lsmell lmap lmeta) type LMap = Map (Y,X) (Tile,Tile) type SMap = Map (Y,X) Time data Tile = Tile { tterrain :: Terrain.Terrain DungeonLoc, titems :: [Item] } deriving Show instance Binary Tile where put (Tile t is) = put t >> put is get = liftM2 Tile get get at l p = fst (findWithDefault (unknown, unknown) p l) rememberAt l p = snd (findWithDefault (unknown, unknown) p l) unknown :: Tile unknown = Tile Terrain.Unknown [] -- | blocks moves and vision closed :: Tile -> Bool closed = not . open floor :: Tile -> Bool floor = Terrain.isFloor . tterrain canBeDoor :: Tile -> Bool canBeDoor t = case t of Tile d@(Terrain.Door hv o) _ | secret o -> True _ -> Terrain.isWall (tterrain t) || Terrain.isRock (tterrain t) || Terrain.isUnknown (tterrain t) secret :: Maybe Int -> Bool secret (Just n) | n /= 0 = True secret _ = False isUnknown :: Tile -> Bool isUnknown = Terrain.isUnknown . tterrain toOpen :: Bool -> Maybe Int toOpen True = Nothing toOpen False = Just 0 -- | allows moves and vision open :: Tile -> Bool open = Terrain.isOpen . tterrain -- | is lighted on its own light :: Tile -> Bool light = Terrain.isAlight . tterrain -- | can be lighted by sourrounding tiles reflects :: Tile -> Bool reflects = Terrain.reflects . tterrain -- | Passive tiles reflect light from some other (usually adjacent) -- positions. This function returns the offsets from which light is -- reflected. Not all passively lighted tiles reflect from all directions. -- Walls, for instance, cannot usually be seen from the outside. passive :: Tile -> [Dir] passive = Terrain.passive . tterrain -- | Perceptible is similar to passive, but describes which tiles can -- be seen from which adjacent fields in the dark. perceptible :: Tile -> [Dir] perceptible = Terrain.perceptible . tterrain -- Checks for the presence of actors. Does *not* check if the tile is open. unoccupied :: [Actor] -> Loc -> Bool unoccupied actors loc = all (\ body -> aloc body /= loc) actors -- check whether one location is accessible from the other -- precondition: the two locations are next to each other -- currently only implements that doors aren't accessible diagonally, -- and that the target location has to be open accessible :: LMap -> Loc -> Loc -> Bool accessible lmap source target = let dir = shift source (neg target) src = lmap `at` source tgt = lmap `at` target in open tgt && (not (diagonal dir) || case (tterrain src, tterrain tgt) of (Terrain.Door {}, _) -> False (_, Terrain.Door {}) -> False _ -> True) -- check whether the location contains a door of secrecy level lower than k openable :: Int -> LMap -> Loc -> Bool openable k lmap target = let tgt = lmap `at` target in case tterrain tgt of Terrain.Door _ (Just n) -> n < k _ -> False findLoc :: Level -> (Loc -> Tile -> Bool) -> Rnd Loc findLoc l@(Level { lsize = sz, lmap = lm }) p = do loc <- locInArea ((0,0),sz) let tile = lm `at` loc if p loc tile then return loc else findLoc l p findLocTry :: Int -> -- try k times Level -> (Loc -> Tile -> Bool) -> -- loop until satisfied (Loc -> Tile -> Bool) -> -- only try to satisfy k times Rnd Loc findLocTry k l@(Level { lsize = sz, lmap = lm }) p pTry = do loc <- locInArea ((0,0),sz) let tile = lm `at` loc if p loc tile && pTry loc tile then return loc else if k > 1 then findLocTry (k - 1) l p pTry else findLoc l p -- Actually, do not scatter items around, it's too much work for the player. dropItemsAt :: [Item] -> Loc -> Level -> Level dropItemsAt items loc lvl@(Level { lmap = lmap }) = let joinItems items = L.foldl' (\ acc i -> snd (joinItem i acc)) items t = lmap `at` loc nt = t { titems = joinItems items (titems t) } ntRemember = lmap `rememberAt` loc in updateLMap (M.insert loc (nt, ntRemember)) lvl