Allure-0.4.2: src/EffectAction.hs
module EffectAction where
import Control.Monad
import Control.Monad.State hiding (State, state)
import Data.Function
import qualified Data.List as L
import qualified Data.IntMap as IM
import qualified Data.Set as S
import System.Time
import Utils.Assert
import Action
import Actor
import ActorState
import Content.ActorKind
import ActorAdd
import Display hiding (display)
import Grammar
import Loc
import qualified HighScores as H
import Item
import Content.ItemKind
import Level
import Message
import Perception
import Random
import State
import qualified Config
import qualified Effect
import qualified Kind
-- 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 _ _ _ _ = nullEffect
effectToAction Effect.Heal _ _source target power = do
let bhpMax m = maxDice (ahp $ Kind.getKind $ bkind m)
tm <- gets (getActor target)
if bhp tm >= bhpMax tm || power <= 0
then nullEffect
else do
focusIfAHero target
updateAnyActor target (addHp power) -- TODO: duplicates maxDice, etc.
return (True, subjectActorVerb tm "feel" ++ " better.")
effectToAction (Effect.Wound nDm) verbosity source target power = do
n <- rndToAction $ rollDice nDm
if n + power <= 0 then nullEffect else do
focusIfAHero target
tm <- gets (getActor target)
let newHP = bhp tm - n - power
killed = newHP <= 0
msg
| source == target = -- a potion of wounding, etc.
subjectActorVerb tm "feel" ++
if killed then " mortally" else "" ++ " wounded."
| killed =
if isAHero target then "" else
subjectActorVerb tm "die" ++ "."
| verbosity <= 0 = ""
| isAHero target =
subjectActorVerb tm "lose" ++
" " ++ show (n + power) ++ "HP."
| otherwise = subjectActorVerb tm "hiss" ++ " in pain."
updateAnyActor target $ \ m -> m { bhp = newHP } -- Damage the target.
when killed $ do
-- Place the actor's possessions on the map.
modify (updateLevel (dropItemsAt (bitems tm) (bloc 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
selectPlayer target
>>= assert `trueM` (source, target, "player dominates himself")
-- Prevent AI from getting a few free moves until new player ready.
updatePlayerBody (\ m -> m { btime = 0})
display
return (True, "")
else nullEffect
effectToAction Effect.SummonFriend _ source target power = do
tm <- gets (getActor target)
if isAHero source
then summonHeroes (1 + power) (bloc tm)
else summonMonsters (1 + power) (bloc 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) (bloc tm)
else summonMonsters (1 + power) (bloc 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 = IM.empty }
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.")
effectToAction Effect.Teleport _ _ _ _ = nullEffect -- TODO
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
tm <- gets (getActor target)
per <- currentPerception
let effect = ieffect $ Kind.getKind $ jkind item
-- The message describes the target part of the action.
(b, msg) <- effectToAction effect verbosity source target (jpower item)
-- Determine how the player perceives the event.
-- TODO: factor it out as a function messageActor
-- and messageActorVerb (incorporating subjectActorVerb).
if bloc tm `S.member` ptvisible per
then messageAdd msg
else unless b $
-- victim is not seen and but somethig interestng happens
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 = jkind i
obj = unwords $ tail $ words $ objectItem state i
msg = "The " ++ obj ++ " turns out to be "
kind = Kind.getKind ik
alreadyIdentified = L.length (iflavour kind) == 1 ||
ik `S.member` sdisco state
unless alreadyIdentified $ do
modify (updateDiscoveries (S.insert ik))
state2 <- get
messageAdd $ msg ++ objectItem state2 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
when (absentHero actor state) $ abortWith "No such member of the party."
let (nln, pbody) = findActorAnyLevel actor state
-- 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.
modify (\ s -> s{slid = nln})
-- Set smell display, depending on player capabilities.
-- This also resets FOV mode.
modify (\ s -> s { ssensory =
if asmell $ Kind.getKind $
bkind pbody
then Smell
else Implicit })
-- Announce.
messageAdd $ subjectActor pbody ++ " selected."
return True
focusIfAHero :: ActorId -> Action ()
focusIfAHero target =
when (isAHero target) $ do
-- Focus on the hero being wounded/displaced/etc.
b <- selectPlayer target
-- Display status line for the new hero.
when b $ display >> return ()
summonHeroes :: Int -> Loc -> Action ()
summonHeroes n loc =
assert (n > 0) $ do
newHeroId <- gets (fst . scounter)
modify (\ state -> iterate (addHero loc) state !! n)
selectPlayer (AHero newHeroId)
>>= assert `trueM` (newHeroId, "player summons himself")
-- Display status line for the new hero.
display >> return ()
summonMonsters :: Int -> Loc -> Action ()
summonMonsters n loc = do
(mk, k) <- rndToAction $ frequency Kind.frequency
hp <- rndToAction $ rollDice $ ahp k
modify (\ state ->
iterate (addMonster mk hp loc) state !! n)
-- | Remove dead heroes (or dominated monsters), check if game over.
-- For now we only check the selected hero and at current level,
-- but if poison, etc. is implemented, we'd need to check all heroes
-- on any level.
checkPartyDeath :: Action ()
checkPartyDeath =
do
ahs <- gets allHeroesAnyLevel
pl <- gets splayer
pbody <- gets getPlayerBody
config <- gets sconfig
when (bhp pbody <= 0) $ do -- TODO: change to guard? define mzero? Why are the writes to the files performed when I call abort later? That probably breaks the laws of MonadPlus.
go <- messageMoreConfirm ColorBW $
subjectActorVerb 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 deletePlayer
-- At this place the invariant that the player exists fails.
-- Focus on the new hero (invariant not needed).
selectPlayer actor
>>= assert `trueM` (pl, actor, "player resurrects")
-- 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
slid <- gets slid
let total = calculateTotal state
status = H.Killed slid
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 bitems (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
-- 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 (jletter i) ++ objectItem state i ++ " ")
((if sorted then L.sortBy (cmpLetter' `on` jletter) else id) is)
let ovl = inv ++ more
messageReset msg
overlay ovl
stopRunning :: Action ()
stopRunning = updatePlayerBody (\ p -> p { bdir = Nothing })
-- | Store current message in the history and reset current message.
history :: Action ()
history =
do
sx <- gets (lxsize . slevel)
msg <- currentMessage
messageClear
config <- gets sconfig
let historyMax = Config.get config "ui" "historyMax"
-- TODO: not ideal, continuations of sentences are atop beginnings.
splitS = splitMsg (sx + 1) (msg ++ " ")
unless (L.null msg) $
modify (updateHistory (take historyMax . (L.reverse splitS ++)))