module EffectAction where import Control.Monad import Control.Monad.State hiding (State) import Data.Function import Data.List as L import Data.Map as M import qualified Data.IntMap as IM import Data.Maybe import Data.Set as S import System.Time import Control.Exception (assert) import Action import Display hiding (display) import Dungeon import Geometry import Grammar import qualified HighScores as H import Item import qualified ItemKind import qualified Keys as K import Level import LevelState import Message import Actor import ActorState import ActorKind import ActorAdd import Perception import Random import State import qualified Config import qualified Save import Terrain import qualified Effect -- The effectToAction function and all it depends on. -- This file should not depend on Action.hs nor ItemAction.hs. -- | The source actor affects the target actor, with a given effect and power. -- The second argument is verbosity of the resulting message. -- TODO: instead of verbosity return msg components and tailor them outside? -- Both actors are on the current level and can be the same actor. -- The bool result indicates if the actors identify the effect. -- TODO: separately define messages for the case when source == target -- and for the other case; then use the messages outside of effectToAction, -- depending on the returned bool, perception and identity of the actors. effectToAction :: Effect.Effect -> Int -> ActorId -> ActorId -> Int -> Action (Bool, String) effectToAction Effect.NoEffect _ source target power = nullEffect effectToAction Effect.Heal _ _source target power = do tm <- gets (getActor target) if ahp tm >= bhpMax (akind tm) || power <= 0 then nullEffect else do focusIfAHero target let upd m = m { ahp = min (bhpMax (akind m)) (ahp m + power) } updateAnyActor target upd return (True, subjectActorVerb (akind tm) "feel" ++ " better.") effectToAction (Effect.Wound nDm) verbosity source target power = do n <- liftIO $ rndToIO $ rollDice nDm if (n + power <= 0) then nullEffect else do focusIfAHero target tm <- gets (getActor target) let newHP = ahp tm - n - power killed = newHP <= 0 msg = if source == target -- a potion of wounding, etc. then subjectActorVerb (akind tm) "feel" ++ if killed then " mortally" else "" ++ " wounded." else if killed then if isAHero target then "" else subjectActorVerb (akind tm) "die" ++ "." else if verbosity <= 0 then "" else if isAHero target then subjectActorVerb (akind tm) "lose" ++ " " ++ show (n + power) ++ "HP." else subjectActorVerb (akind tm) "hiss" ++ " in pain." updateAnyActor target $ \ m -> m { ahp = newHP } -- Damage the target. when killed $ do -- Place the actor's possessions on the map. modify (updateLevel (dropItemsAt (aitems tm) (aloc tm))) -- Clean bodies up. pl <- gets splayer if target == pl then checkPartyDeath -- kills the player and checks game over else modify (deleteActor target) -- kills the enemy return (True, msg) effectToAction Effect.Dominate _ source target power = if isAMonster target -- Monsters have weaker will than heroes. then do assertTrue $ selectPlayer target -- Prevent AI from getting a few free moves until new player ready. updatePlayerBody (\ m -> m { atime = 0}) display return (True, "") else nullEffect effectToAction Effect.SummonFriend _ source target power = do tm <- gets (getActor target) if isAHero source then summonHeroes (1 + power) (aloc tm) else summonMonsters (1 + power) (aloc tm) return (True, "") effectToAction Effect.SummonEnemy _ source target power = do tm <- gets (getActor target) if not $ isAHero source -- a trick: monster player will summon a hero then summonHeroes (1 + power) (aloc tm) else summonMonsters (1 + power) (aloc tm) return (True, "") effectToAction Effect.ApplyPerfume _ source target _ = if source == target then return (True, "Tastes like water, but with a strong rose scent.") else do let upd lvl = lvl { lsmell = M.map (const (-100)) (lsmell lvl) } modify (updateLevel upd) return (True, "The fragrance quells all scents in the vicinity.") effectToAction Effect.Regneration verbosity source target power = effectToAction Effect.Heal verbosity source target power effectToAction Effect.Searching _ source target power = return (True, "It gets lost and you search in vain.") nullEffect :: Action (Bool, String) nullEffect = return (False, "Nothing happens.") -- | The source actor affects the target actor, with a given item. -- If either actor is a hero, the item may get identified. itemEffectAction :: Int -> ActorId -> ActorId -> Item -> Action Bool itemEffectAction verbosity source target item = do state <- get pl <- gets splayer tm <- gets (getActor target) per <- currentPerception let effect = ItemKind.jeffect $ ItemKind.getIK $ ikind item -- The message describes the target part of the action. (b, msg) <- effectToAction effect verbosity source target (ipower item) -- Determine how the player perceives the event. -- TODO: factor it out as a function messageActor -- and messageActorVerb (incorporating subjectActorVerb). if aloc tm `S.member` ptvisible per then messageAdd msg else if not b then return () -- victim is not seen and nothing interestng happens else messageAdd "You hear some noises." -- If something happens, the item gets identified. when (b && (isAHero source || isAHero target)) $ discover item return b -- | Given item is now known to the player. discover :: Item -> Action () discover i = do state <- get let ik = ikind i obj = unwords $ tail $ words $ objectItem state i msg = "The " ++ obj ++ " turns out to be " kind = ItemKind.getIK ik alreadyIdentified = L.length (ItemKind.jflavour kind) == 1 || ik `S.member` sdiscoveries state if alreadyIdentified then return () else do modify (updateDiscoveries (S.insert ik)) state <- get messageAdd $ msg ++ objectItem state i ++ "." -- | Make the actor controlled by the player. -- Focus on the actor if level changes. False, if nothing to do. selectPlayer :: ActorId -> Action Bool selectPlayer actor = do pl <- gets splayer if (actor == pl) then return False -- already selected else do state <- get case findActorAnyLevel actor state of Nothing -> abortWith $ "No such member of the party." Just (nln, pbody) -> do -- Make the new actor the player-controlled actor. modify (\ s -> s { splayer = actor }) -- Record the original level of the new player. modify (updateCursor (\ c -> c { creturnLn = nln })) -- Don't continue an old run, if any. stopRunning -- Switch to the level. lvlSwitch nln -- Set smell display, depending on player capabilities. -- This also resets FOV mode. modify (\ s -> s { ssensory = if ActorKind.bsmell (akind pbody) then Smell else Implicit }) -- Announce. messageAdd $ subjectActor (akind pbody) ++ " selected." return True focusIfAHero :: ActorId -> Action () focusIfAHero target = if isAHero target then do -- Focus on the hero being wounded/displaced/etc. b <- selectPlayer target -- Display status line for the new hero. when b $ display >> return () else return () summonHeroes :: Int -> Loc -> Action () summonHeroes n loc = assert (n > 0) $ do newHeroId <- gets (fst . scounter) modify (\ state -> iterate (addHero loc) state !! n) assertTrue $ selectPlayer (AHero newHeroId) -- Display status line for the new hero. display >> return () summonMonsters :: Int -> Loc -> Action () summonMonsters n loc = do let fmk = Frequency $ L.zip (L.map bfreq dungeonMonsters) dungeonMonsters mk <- liftIO $ rndToIO $ frequency fmk modify (\ state -> iterate (addMonster mk (bhpMax mk) loc) state !! n) -- | Remove dead heroes, check if game over. -- For now we only check the selected hero, but if poison, etc. -- is implemented, we'd need to check all heroes on the level. checkPartyDeath :: Action () checkPartyDeath = do ahs <- gets allHeroesAnyLevel pl <- gets splayer pbody <- gets getPlayerBody config <- gets sconfig when (ahp pbody <= 0) $ do -- TODO: change to guard? define mzero? Why are the writes to to files performed when I call abort later? That probably breaks the laws of MonadPlus. go <- messageMoreConfirm ColorBW $ subjectActorVerb (akind pbody) "die" ++ "." history -- Prevent the messages from being repeated. let firstDeathEnds = Config.get config "heroes" "firstDeathEnds" if firstDeathEnds then gameOver go else case L.filter (\ (actor, _) -> actor /= pl) ahs of [] -> gameOver go (actor, _nln) : _ -> do messageAdd "The survivors carry on." -- Remove the dead player. modify (deleteActor pl) -- At this place the invariant that the player exists fails. -- Focus on the new hero (invariant not needed). assertTrue $ selectPlayer actor -- At this place the invariant is restored again. -- | End game, showing the ending screens, if requested. gameOver :: Bool -> Action () gameOver showEndingScreens = do when showEndingScreens $ do state <- get ln <- gets (lname . slevel) let total = calculateTotal state status = H.Killed ln handleScores True status total messageMore "Let's hope another party can save the day!" end -- | Calculate loot's worth for heroes on the current level. calculateTotal :: State -> Int calculateTotal s = L.sum $ L.map itemPrice $ L.concatMap aitems (levelHeroList s) -- | Handle current score and display it with the high scores. Scores -- should not be shown during the game, because ultimately the worth of items might give -- information about the nature of the items. -- False if display of the scores was void or interrupted by the user handleScores :: Bool -> H.Status -> Int -> Action Bool handleScores write status total = if (total == 0) then return False else do config <- gets sconfig time <- gets stime curDate <- liftIO getClockTime let points = case status of H.Killed _ -> (total + 1) `div` 2 _ -> total let score = H.ScoreRecord points (-time) curDate status (placeMsg, slideshow) <- liftIO $ H.register config write score messageOverlaysConfirm placeMsg slideshow -- | Perform a level switch to a given level. False, if nothing to do. lvlSwitch :: LevelName -> Action Bool lvlSwitch nln = do ln <- gets (lname . slevel) if (nln == ln) then return False else do level <- gets slevel dungeon <- gets sdungeon -- put back current level -- (first put back, then get, in case we change to the same level!) let full = putDungeonLevel level dungeon -- get new level let (new, ndng) = getDungeonLevel nln full modify (\ s -> s { sdungeon = ndng, slevel = new }) return True -- effectToAction does not depend on this function right now, but it might, -- and I know no better place to put it. displayItems :: Message -> Bool -> [Item] -> Action Bool displayItems msg sorted is = do state <- get let inv = unlines $ L.map (\ i -> letterLabel (iletter i) ++ objectItem state i ++ " ") ((if sorted then sortBy (cmpLetter' `on` iletter) else id) is) let ovl = inv ++ more messageReset msg overlay ovl stopRunning :: Action () stopRunning = updatePlayerBody (\ p -> p { adir = Nothing }) -- | Store current message in the history and reset current message. history :: Action () history = do (_, sx) <- gets (lsize . slevel) msg <- currentMessage messageClear config <- gets sconfig let historyMax = Config.get config "ui" "historyMax" -- TODO: not ideal, continuations of sentences are atop beginnings. split = splitMsg sx (msg ++ " ") unless (L.null msg) $ modify (updateHistory (take historyMax . (L.reverse split ++)))