LambdaHack-0.1.20110918: src/EffectAction.hs
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 ++)))