{-# LANGUAGE RankNTypes, FlexibleContexts #-} {- This module is part of Antisplice. Copyleft (c) 2014 Marvin Cohrs All wrongs reversed. Sharing is an act of love, not crime. Please share Antisplice with everyone you like. Antisplice is free software: you can redistribute it and/or modify it under the terms of the GNU Affero General Public License as published by the Free Software Foundation, either version 3 of the License, or (at your option) any later version. Antisplice is distributed in the hope that it will be useful, but WITHOUT ANY WARRANTY; without even the implied warranty of MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the GNU Affero General Public License for more details. You should have received a copy of the GNU Affero General Public License along with Antisplice. If not, see . -} -- | A huge pile of utility functions for building our dungeon. module Game.Antisplice.Rooms ( -- * Room modification modifyRoomState, getRoomDesc, getRoomTitle, setRoomTitle, markRoom, -- * Moving around enterRoom, reenterCurrentRoom, enterAndAnnounce, changeRoom, -- * Room construction constructRoom, establishWay, addRoomObject, removeRoomObject, insertRoomObject, -- * Object construction modifyObjectState, setObjectDesc, setObjectTitle, addObjectName, addObjectAttr, setObjectIsMob, setObjectIsAcquirable, addFeature, addDescSeg, addEquipSlot, -- * Object forms registerForm, instanciateForm, -- * Object investigation getObjectTitle, getObjectDesc, getObjectNames, matchObjectName, getObjectIsMob, getObjectIsAcquirable, roomOfObject, -- * Object actions setMobRoute, continueMobRoute, -- * Scheduling schedule, -- * Players subscribePlayer, setPlayerRoom, acquireObject, dropObject, equipObject, equipObjectAt, getEquipment, getCooldown, setCooldown, getCurrency, modifyCurrency, -- * Fight damage, focusOpponent, dealDamage, -- * Masquerades withRoom, withPlayer, withObject, guardVisible ) where import Text.Chatty.Printer import Text.Chatty.Scanner import Text.Chatty.Expansion import System.Chatty.Misc import Game.Antisplice.Monad.Dungeon import Game.Antisplice.Monad import Game.Antisplice.Utils.Graph import Game.Antisplice.Utils.Counter import Game.Antisplice.Utils.BST import Game.Antisplice.Utils.AVL import Game.Antisplice.Utils.Atoms import Game.Antisplice.Utils.None import Game.Antisplice.Errors import Control.Arrow import Control.Monad import Control.Monad.Error import Control.Monad.Trans.Class import Data.Text (pack,unpack) import Data.Maybe import Data.List import Data.Time.Clock import Text.Printf -- | Modify the room state. modifyRoomState :: MonadRoom m => (RoomState -> RoomState) -> m () modifyRoomState f = do s <- getRoomState putRoomState (f s) -- | Get the current room's description getRoomDesc :: (MonadRoom m,MonadAtoms m) => m String getRoomDesc = do s <- getRoomState --return (fromText $ roomDescOf s) let descseg (Described a) = getAtom a descseg _ = return none is <- liftM (filter $ not.null) $ mapM descseg $ concatMap (avlInorder.objectFeaturesOf) $ avlInorder $ roomObjectsOf s return $ concat $ intersperse " " is -- | Get the current room's title getRoomTitle :: (MonadRoom m,IsText t) => m t getRoomTitle = do s <- getRoomState return (fromText $ roomTitleOf s) {- | Set the current room's description setRoomDesc :: (MonadRoom m,IsText t) => t -> m () setRoomDesc t = modifyRoomState $ \s -> s{roomDescOf=toText t}-} -- | Set the current room's title setRoomTitle :: (MonadRoom m,IsText t) => t -> m () setRoomTitle t = modifyRoomState $ \s -> s{roomTitleOf=toText t} -- | Mark the current room as visited markRoom :: MonadDungeon m => m () markRoom = do s <- getDungeonState case currentRoomOf s of Just r -> putDungeonState s{roomsOf=markNode r $ roomsOf s} -- | Enter the given room and trigger most events, but don't announce it. Result tells whether this room is visited the first time. enterRoom :: NodeId -> ChattyDungeonM Bool enterRoom n = do rs0 <- getRoomState roomTriggerOnLeaveOf rs0 s <- getDungeonState modifyPlayerState $ \p -> p{playerRoomOf=n,playerOpponentOf=FalseObject} let marked = nodeMarked $ getNode' n $ roomsOf s rs <- getRoomState unless marked $ do markRoom roomTriggerOnFirstEnterOf rs roomTriggerOnEachEnterOf rs sequence_ $ avlInorder $ flip fmap (roomObjectsOf rs) $ \os -> do unless (objectOnceSeenOf os) $ objectTriggerOnFirstSightOf os objectTriggerOnEachSightOf os putRoomState rs{roomObjectsOf=fmap (\os -> os{objectOnceSeenOf=True}) $ roomObjectsOf rs} return $ not marked -- | Reenter the current room and trigger all events (but don't announce it). reenterCurrentRoom :: ChattyDungeonM () reenterCurrentRoom = do s <- getDungeonState case currentRoomOf s of Just r -> void $ enterRoom r -- | Enter the given room, trigger all events and announce it. On the first visit, look around. enterAndAnnounce :: NodeId -> ChattyDungeonM () enterAndAnnounce n = do r <- enterRoom n rs <- getRoomState roomTriggerOnAnnounceOf rs when r $ roomTriggerOnLookOf rs -- | Construct a new room using the room monad. constructRoom :: (MonadDungeon m) => RoomT m a -> m NodeId constructRoom m = do (_,rs) <- runRoomT m $ RoomState none none noneM noneM noneM noneM noneM s <- getDungeonState let (nid,g) = addNode' rs $ roomsOf s putDungeonState s{roomsOf=g} when (isNothing $ currentRoomOf s) $ modifyPlayerState $ \p -> p{playerRoomOf=nid} return nid -- | Run a function in the context of the given room. withRoom :: MonadDungeon m => NodeId -> RoomT m a -> m a withRoom n m = do s <- getDungeonState let rs = getNode n $ roomsOf s (a,rs') <- runRoomT m rs putDungeonState s{roomsOf=setNode n rs' $ roomsOf s} return a -- | Establish a path from one room to another one (one-way only). establishWay :: MonadDungeon m => NodeId -> NodeId -> Direction -> PathState -> m () establishWay f t d c = do s <- getDungeonState let g = addEdge f t 0 d c $ roomsOf s putDungeonState s{roomsOf=g} -- | Enter a neighbouring room by its direction. changeRoom :: Direction -> ChattyDungeonM () changeRoom d = do s <- getDungeonState case currentRoomOf s of Just r -> case queryEdge r d (roomsOf s) of Just c -> do b <- pathPrerequisiteOf c unless b $ throwError CantWalkThereError case followEdge r d (roomsOf s) of Just n -> enterAndAnnounce n Nothing -> throwError CantWalkThereError -- | Add a new object to the current room. It is contructed using the object monad. addRoomObject :: (MonadCounter m,MonadRoom m) => ObjectT m a -> m ObjectId addRoomObject m = do i <- liftM ObjectId countOn o <- constructObject m $ Just i insertRoomObject o return i -- | Construct a room object (but don't add it) constructObject :: Monad m => ObjectT m a -> Maybe ObjectId -> m ObjectState constructObject m j = do (_,o) <- runObjectT m $ ObjectState (if isJust j then (\(Just j) -> j) j else none) -- id (pack "Something") -- title (pack "I don't know what this is.") -- desc none none False False False False -- names, attr, 1seen?, 1acq?, 1insp?, 1eq? 100 100 -- maxhp, curhp none none -- route, feat none -- faction noneM -- on 1 sight noneM -- on sight noneM -- on 1 acq noneM -- on acq noneM -- on 1 insp noneM -- on insp (mprintLn "There is nothing special about this.") -- on look at (mprintLn "There is nothing inside this.") -- on look in (mprintLn "There is nothing written on this.") -- on read (mprintLn "You cannot enter this.") -- on enter noneM -- on enter room noneM -- on leave room noneM -- on announce noneM -- on drop noneM -- on 1 eq noneM -- on eq noneM -- on uneq noneM -- on die noneM -- on take damage return o -- | Remove an object from the current room and return its state. removeRoomObject :: MonadRoom m => ObjectId -> m ObjectState removeRoomObject i = do s <- getRoomState let Just o = avlLookup i $ roomObjectsOf s putRoomState s{roomObjectsOf=avlRemove i $ roomObjectsOf s} return o -- | Insert an already constructed object to the current room. insertRoomObject :: MonadRoom m => ObjectState -> m () insertRoomObject o = modifyRoomState $ \s -> s{roomObjectsOf=avlInsert o $ roomObjectsOf s} -- | Modify the object state modifyObjectState :: MonadObject m => (ObjectState -> ObjectState) -> m () modifyObjectState f = do s <- getObjectState putObjectState $ f s -- | Get the object's title getObjectTitle :: (MonadObject m,IsText t) => m t getObjectTitle = do s <- getObjectState return (fromText $ objectTitleOf s) -- | Set the object's title setObjectTitle :: (MonadObject m,IsText t) => t -> m () setObjectTitle t = modifyObjectState $ \s -> s{objectTitleOf=toText t} -- | Get the object's description getObjectDesc :: (MonadObject m,IsText t) => m t getObjectDesc = do s <- getObjectState return (fromText $ objectDescOf s) -- | Set the object's description setObjectDesc :: (MonadObject m,IsText t) => t -> m () setObjectDesc t = modifyObjectState $ \s -> s{objectDescOf=toText t} -- | Get the object's names getObjectNames :: MonadObject m => m [String] getObjectNames = do s <- getObjectState return $ objectNamesOf s -- | Add a name for the current object addObjectName :: MonadObject m => String -> m () addObjectName t = modifyObjectState $ \s -> s{objectNamesOf=t:objectNamesOf s} -- | Check if the given name matches our current object matchObjectName :: MonadObject m => String -> m Bool matchObjectName t = do s <- getObjectState return $ elem t $ objectNamesOf s -- | Add an attribute for the current object addObjectAttr :: MonadObject m => String -> m () addObjectAttr t = modifyObjectState $ \s -> s{objectAttributesOf=t:objectAttributesOf s} -- | Create a new player using the player monad subscribePlayer :: (MonadDungeon m,MonadCounter m) => PlayerT m a -> m () subscribePlayer m = do s <- getDungeonState i <- liftM PlayerId countOn let r = rootNode $ roomsOf s (_,a) <- runPlayerT m $ PlayerState i r 100 none none none none none (avlInsert (Health,100) none) none none none putDungeonState s{playersOf=anyBstInsert a $ playersOf s} -- | Move the current player to the given room, but don't trigger anything. setPlayerRoom :: MonadPlayer m => NodeId -> m () setPlayerRoom r = modifyPlayerState $ \p -> p{playerRoomOf=r} -- | Check if the current object is a mob. getObjectIsMob :: (MonadObject m,Functor m) => m Bool getObjectIsMob = fmap (isJust . avlLookup Mobile . objectFeaturesOf) getObjectState -- | Set whether the current object is a mob. setObjectIsMob :: MonadObject m => Bool -> m () setObjectIsMob b = modifyObjectState $ \o -> o{objectFeaturesOf=a b $ objectFeaturesOf o} where a True = avlInsert Mobile a False = avlRemove Mobile -- | Check if the current object is acquirable. getObjectIsAcquirable :: (MonadObject m,Functor m) => m Bool getObjectIsAcquirable = fmap (isJust . avlLookup Acquirable . objectFeaturesOf) getObjectState -- | Set whether the current object is acquirable. setObjectIsAcquirable :: MonadObject m => Bool -> m () setObjectIsAcquirable b = modifyObjectState $ \o -> o{objectFeaturesOf=a b $ objectFeaturesOf o} where a True = avlInsert Acquirable a False = avlRemove Acquirable -- | Schedule an event for a given time offset (in milliseconds). schedule :: (MonadDungeon m,MonadClock m) => Integer -> Handler -> m () schedule ms t = do now <- mgetstamp let t' = now + (realToFrac ms / 1000) s <- getDungeonState putDungeonState s{timeTriggersOf=avlInsert (t',Handler t) $ timeTriggersOf s} -- | Set the current mob's route setMobRoute :: MonadObject m => [NodeId] -> m () setMobRoute rs = modifyObjectState $ \s -> s{objectRouteOf=rs} -- | The given object may continue its route continueMobRoute :: ObjectId -> Handler continueMobRoute i = do rs <- roomOfObject i case rs of [r] -> do o <- withRoom r $ removeRoomObject i guardVisible r $ objectTriggerOnRoomLeaveOf o let (n:rs) = objectRouteOf o withRoom n $ insertRoomObject o{objectRouteOf=rs++[n]} guardVisible n $ objectTriggerOnRoomEnterOf o modifyPlayerState $ \s -> if playerOpponentOf s == i then s{playerOpponentOf=FalseObject} else s return () -- | Only run the given function if the player is inside the also given room. guardVisible :: MonadDungeon m => NodeId -> m () -> m () guardVisible n m = do s <- getDungeonState when (currentRoomOf s == Just n) m -- | Determine which rooms contain the given object (won't be more than one, but we're careful) roomOfObject :: MonadDungeon m => ObjectId -> m [NodeId] roomOfObject o = (return . map nodeId . filter (isJust . avlLookup o . roomObjectsOf . nodeContent) . allNodes . roomsOf) =<< getDungeonState -- | Acquire the given object and put it in the player's inventory acquireObject :: ObjectId -> ChattyDungeonM () acquireObject i = do rs <- roomOfObject i case rs of [r] -> do s <- withObject i getObjectState if isJust $ avlLookup Acquirable $ objectFeaturesOf s then do o <- withRoom r $ removeRoomObject i modifyPlayerState $ \s -> s{playerInventoryOf=avlInsert o $ playerInventoryOf s} else throwError CantAcquireThatError [] -> throwError CantSeeOneError _ -> throwError WhichOneError -- | Drop the given object and remove it from the player's inventory dropObject :: ObjectId -> ChattyDungeonM () dropObject i = do Just r <- (return . currentRoomOf) =<< getDungeonState ps <- getPlayerState let Just o = avlLookup i $ playerInventoryOf ps putPlayerState ps{playerInventoryOf=avlRemove i $ playerInventoryOf ps} insertRoomObject o -- | Add an object feature to the current object addFeature :: MonadObject m => Feature -> m () addFeature f = modifyObjectState $ \s -> s{objectFeaturesOf=avlInsert f $ objectFeaturesOf s} -- | Add a room description segment to the current object addDescSeg :: (MonadObject m,MonadAtoms m) => String -> m () addDescSeg s = do a <- newAtom putAtom a s addFeature $ Described a -- | Register an object form and return its atom registerForm :: (MonadAtoms m) => ObjectT m () -> m (Atom ObjectState) registerForm m = do a <- newAtom o <- constructObject m Nothing putAtom a o return a -- | Instanciate a registered form instanciateForm :: (MonadAtoms m,MonadRoom m,MonadCounter m) => Atom ObjectState -> m ObjectId instanciateForm a = do i <- countOn o <- getAtom a insertRoomObject o{objectIdOf=ObjectId i} return $ ObjectId i -- | Equip the given object at a given key equipObjectAt :: (MonadPlayer m,MonadError SplErr m) => EquipKey -> ObjectState -> m (Maybe ObjectState) equipObjectAt k o | isJust (avlLookup (Equipable k) $ objectFeaturesOf o) = do p <- getPlayerState let o1 = avlLookup k $ playerEquipOf p putPlayerState p{playerEquipOf=avlInsert (k,o) $ playerEquipOf p, playerInventoryOf=avlRemove (indexOf o) $ playerInventoryOf p} return o1 | otherwise = throwError CantEquipThatThereError -- | Equip the given object somewhere equipObject :: (MonadPlayer m,MonadError SplErr m) => ObjectState -> m (Maybe ObjectState) equipObject o = let equ (Equipable k) = [k] equ _ = none in case concatMap equ $ avlInorder $ objectFeaturesOf o of [] -> throwError CantEquipThatError [k] -> equipObjectAt k o ks -> do p <- getPlayerState case filter (isJust.flip avlLookup (playerEquipOf p)) ks of [] -> throwError WhereToEquipError [k] -> equipObjectAt k o _ -> throwError WhereToEquipError -- | Get equipped object getEquipment :: MonadPlayer m => EquipKey -> m (Maybe ObjectState) getEquipment k = liftM (avlLookup k.playerEquipOf) getPlayerState -- | Add equippable slot addEquipSlot :: MonadObject m => EquipKey -> m () addEquipSlot = addFeature . Equipable -- | Set/unset cooldown setCooldown :: MonadPlayer m => CooldownId -> Bool -> m () setCooldown c b = modifyPlayerState $ \p -> p{playerCooldownsOf=(if b then avlInsert else avlRemove) c $ playerCooldownsOf p} -- | Get cooldown state getCooldown :: MonadPlayer m => CooldownId -> m Bool getCooldown c = liftM (isJust . avlLookup c . playerCooldownsOf) getPlayerState -- | Get currency state getCurrency :: MonadPlayer m => CurrencyId -> m Int getCurrency c = liftM (joinMaybe . avlLookup c . playerCurrenciesOf) getPlayerState -- | Modify currency state modifyCurrency :: MonadPlayer m => CurrencyId -> (Int -> Int) -> m () modifyCurrency c f = modifyPlayerState $ \p -> let c1 = joinMaybe $ avlLookup c $ playerCurrenciesOf p in p{playerCurrenciesOf=avlInsert (c,f c1) $ playerCurrenciesOf p} -- | Run a function in the context of a given player withPlayer :: MonadDungeon m => PlayerId -> PlayerT (RoomT m) a -> m a withPlayer i m = do d <- getDungeonState case anyBstLookup i $ playersOf d of Just p -> do (a,p') <- withRoom (playerRoomOf p) $ runPlayerT m p d <- getDungeonState putDungeonState d{playersOf=anyBstInsert p' $ playersOf d} return a -- | Run a function in the context of a given object withObject :: MonadDungeon m => ObjectId -> ObjectT (RoomT m) a -> m a withObject i m = do rs <- roomOfObject i case rs of [r] -> withRoom r $ do rs <- getRoomState case anyBstLookup i $ roomObjectsOf rs of Just o -> do (a,o') <- runObjectT m o modifyRoomState $ \r -> r{roomObjectsOf=anyBstInsert o' $ roomObjectsOf r} return a -- | Damage a target (no matter whether player or mob) without setting focus damage :: DamageTarget -> Int -> ChattyDungeonM () damage (TargetPlayer p) v = withPlayer p $ modifyCurrency Health $ subtract v damage (TargetObject o) v = do d <- withObject o $ do modifyObjectState $ \o -> o{objectCurHealthOf=objectCurHealthOf o - v} liftM ((<=0).objectCurHealthOf) getObjectState when d $ do objectTriggerOnDieOf =<< withObject o getObjectState s <- getPlayerState when (playerOpponentOf s == o) $ putPlayerState s{playerOpponentOf=FalseObject} void $ removeRoomObject o -- | Focus an opponent focusOpponent :: ObjectId -> ChattyDungeonM () focusOpponent o = do os <- withObject o getObjectState if (isJust $ avlLookup Damagable $ objectFeaturesOf os) then modifyPlayerState $ \p -> p{playerOpponentOf=o} else throwError WontHitThatError -- | Deal damage to an opponent. Real damage is influenced by random. dealDamage :: Int -> ChattyDungeonM () dealDamage d = do let dmin = truncate (fromIntegral d * 0.8) dmax = truncate (fromIntegral d * 1.2) r <- mrandomR (dmin,dmax) o <- liftM playerOpponentOf getPlayerState damage (TargetObject o) r instance (Functor m,MonadExpand m) => MonadExpand (DungeonT m) where expand = lift . expand <=< liftM (replay.snd) . runRecorderT . expandDun expandDun :: (MonadPrinter m,MonadDungeon m) => String -> m () expandDun [] = return () expandDun ('#':'?':'{':ss) = let nm = takeBrace 0 ss rm = drop (length nm + 1) ss takeBrace 0 ('}':ss) = "" takeBrace n ('}':ss) = '}' : takeBrace (n-1) ss takeBrace n ('{':ss) = '{' : takeBrace (n+1) ss takeBrace n (s:ss) = s : takeBrace n ss in do o <- liftM playerOpponentOf getPlayerState case o of FalseObject -> expandDun rm _ -> do expandDun nm expandDun rm expandDun ('#':'{':ss) = let (nm,rm) = (takeWhile (/='}') &&& tail.dropWhile (/='}')) ss replace "health" = liftM (show . joinMaybe . avlLookup Health . playerCurrenciesOf) getPlayerState >>= mprint replace "ohealth" = do i <- liftM playerOpponentOf getPlayerState o <- withObject i getObjectState mprint $ show $ objectCurHealthOf o replace "otitle" = do i <- liftM playerOpponentOf getPlayerState o <- withObject i getObjectState mprint $ unpack $ objectTitleOf o replace s = do cs <- liftM (avlInorder . currenciesOf) getDungeonState case filter ((==s).currencyNameOf) cs of [] -> return () [c] -> liftM (show . joinMaybe . avlLookup (currencyIdOf c) . playerCurrenciesOf) getPlayerState >>= mprint in do replace nm expandDun rm expandDun ('#':ss) = let (nm,rm) = (takeWhile isAnum &&& dropWhile isAnum) ss isAnum = flip elem (['A'..'Z']++['a'..'z']++['0'..'9']) in expandDun ("#{"++nm++"}"++rm) expandDun (s:ss) = do mprint [s] expandDun ss