MazesOfMonad-1.0: src/MoresmauJP/Rpg/RPG.hs
-- | Handling of main actions (character management, game management, in game actions)
-- (c) JP Moresmau 2009
module MoresmauJP.Rpg.RPG where
import Control.Monad.State
import Data.Char
import Data.List
import Data.Maybe
import qualified Data.Set as DataSet
import MoresmauJP.Core.Screen
import MoresmauJP.Maze1.Maze
import qualified MoresmauJP.Maze1.MazeGame as MG
import MoresmauJP.Rpg.Actions
import MoresmauJP.Rpg.Arena
import MoresmauJP.Rpg.Character
import MoresmauJP.Rpg.Items
import MoresmauJP.Rpg.NPC
import MoresmauJP.Rpg.Profile
import MoresmauJP.Rpg.TextOutput
import MoresmauJP.Rpg.Inventory as Inv
import MoresmauJP.Rpg.Magic
import MoresmauJP.Rpg.MazeObjects as Objs
import MoresmauJP.Rpg.Save
import MoresmauJP.Util.Lists
import MoresmauJP.Util.Random
import System.Directory
import Text.Printf
type LocationCode=String
initialGameStateInApp :: IO(GameState RPGState)
initialGameStateInApp = do
dir<-getAppUserDataDirectory "RPG"
--createDirectoryIfMissing True dir
return $ initialGameState dir
initialGameState :: FilePath -> GameState RPGState
initialGameState fp=GameState (RPGState Nothing Nothing fp) (Just initialScreen)
initialScreen :: Screen RPGState
initialScreen = Screen [Action "games" "Games: new game, load , delete" games,
Action "characters" "Characters: new character, delete" chars
]
games:: ActionFunction RPGState
games _=do
sc <- gets (fromJust . screen)
a <- gets gsData
put (GameState a (Just (getGameScreen sc)))
w2 <- help False []
return (combineWidget (WText "Games") w2)
--combineActionAfter (WText "Games",) (help False) (Message [])
chars:: ActionFunction RPGState
chars _ = do
sc <- gets (fromJust . screen)
a <- gets gsData
put (GameState a (Just (getCharacterScreen sc)))
w2 <- help False []
return (combineWidget (WText "Characters") w2)
--combineActionAfter (WText "Characters",(GameState a (Just (getCharacterScreen sc)))) (help False) (Message [])
getCharacterScreen :: Screen RPGState -> Screen RPGState
getCharacterScreen sc= Screen [Action "new" "Create a new character" newCharacter,
Action "delete" "Delete an existing character" deleteCharacters,
Action "view" "View an existing character" viewCharacters,
backAction sc
]
newCharacter :: ActionFunction RPGState
newCharacter _ =return (WInput ["Enter name:"] nameF)
nameF :: String -> GSWScreenT RPGState
nameF "" =return (WText "Creation canceled")
nameF name =do
gs <- get
exists<-liftIO $ doesCharacterExists (gsData gs) name
if exists
then
return (WInput [name++" already exists!","Enter name:"] nameF)
else
return ((WCombo ["Choose gender:"] (map show [Male,Female]) (genderF name)))
genderF :: String -> String -> GSWScreenT RPGState
genderF _ ""=return (WText "Creation canceled")
genderF name genderName= do
let gender=read (genderName)
return ((getMappedCombo fst ["Choose profile:"] profiles (profile (name,gender))))
profile :: (String,Gender) -> ComboResult Profile -> GSWScreenT RPGState
profile _ Empty=return (WText "Creation canceled")
profile (name,gender) (Unknown _)= do
w2<- (genderF name) (show gender)
return (combineWidget (WText "Unknown profile") w2)
profile (name,gender) (Exact prof)= do
mt<-generateTraits prof
let h=getDefaultHealth mt
let c=Character name gender h (makeEmptyInventory 10 20) [] []
extraGoldFromTrade<-diffResult' c trade (subsequentDifficulty Standard) (\x->if x<0 then 0 else x ^ 2)
extraGoldFromStealing<-diffResult' c steal (subsequentDifficulty Standard) (\x->if x<0 then 0 else x ^ 2)
let extraGold=max extraGoldFromTrade extraGoldFromStealing
nbSpellsLearned<-diffResult' c spelllearning (toIntLevel Neutral) (\x->if x<0 then 0 else (round $ sqrt $ fromIntegral x))
spells<-randomPickpn allSpells nbSpellsLearned
let c'=c{inventory=addGold (inventory c) extraGold,spells=spells}
return (WCheck [ppCharacterAndGold' c'] "Accept?" True (profileAccept c'))
profileAccept :: Character -> Bool -> GSWScreenT RPGState
profileAccept c False = (genderF (name c) (show $ gender c))
profileAccept c True = do
gs <- get
s <- liftIO $ saveCharacter (gsData gs) c
return $ either WText WText s
deleteCharacters :: ActionFunction RPGState
deleteCharacters _ = listCharactersW $ deleteFileW deleteCharacter
viewCharacters :: ActionFunction RPGState
viewCharacters _=listCharactersW viewCharacter
listCharactersW :: (String -> GSWScreenT RPGState) -> GSWScreenT RPGState
listCharactersW af= do
rs<-gets gsData
cs<-liftIO $ listCharacters rs
if null cs
then return (WText "No character defined")
else return (WCombo ["Choose character:"] cs (af))
--listFilesW (characterExtension,"Choose character:","No character defined")
{--
listFilesW:: (String,String,String) -> (String -> GSWScreenT RPGState) -> GSWScreenT RPGState
listFilesW (ext,title,msg) af = do
rs<-gets gsData
files <- liftIO $ listFiles rs ext
return
(if null files
then
(WText msg)
else
(WCombo [title] files (af))
)
--}
viewCharacter :: String -> GSWScreenT RPGState
viewCharacter ""=return (WText "View canceled")
viewCharacter shortName=do
rs<-gets gsData
w<-withCharacter rs shortName
(\c -> return $ WText $ ppCharacterAndInventory' c)
onError w
{--withFileContents :: String -> (String -> IO(Either String a)) -> (a->GSWScreenT RPGState) -> GSWScreenT RPGState
withFileContents shortName act act2= do
rs<-gets gsData
r<-readF rs shortName act act2
onError r--}
onError :: Either String (Widget a) -> GSWScreenT a
onError (Right w)=return w
onError (Left s)=return $ WText s
getGameScreen :: Screen RPGState -> Screen RPGState
getGameScreen sc= Screen [Action "new" "Start a new game" newGame,
Action "delete" "Delete an existing game" deleteGames,
Action "play" "Continue playing a game" playGames,
backAction sc
]
newGame :: ActionFunction RPGState
newGame _=listCharactersW gameNameF
gameNameF :: String -> GSWScreenT RPGState
gameNameF ""=return (WText "Creation canceled")
gameNameF shortName=do
gs <- get
let rs = gsData gs
exists<-liftIO $ doesCurrentGameExists rs shortName
if exists
then
return (WText (printf "%s already plays!" shortName))
else
do
r <- withCharacter rs shortName createNewMazeState
onError r
mazeOptions :: MazeOptions
mazeOptions = MazeOptions {
itemProportion=4,
npcProportion=3
}
createNewMazeState :: Character -> GSWScreenT RPGState
createNewMazeState c = do
(GameState gw _) <- MG.initialGameState
mo<-generateObjects (maze $ gw) mazeOptions (characterLevel c)
let mgs2=RPGGameState gw mo 0
createMazeState' c mgs2
--(printf "Game saved to %s " fileName)
createMazeState' :: Character -> RPGGameState -> GSWScreenT RPGState
createMazeState' c rgs = do
(RPGState {fp=fp}) <- gets gsData
s<-getSplit
let rpg2=RPGState (Just c) (Just rgs) fp
(rpg3,wGold)<-liftIO $ processFight rpg2 s
let ms=getMazeScreen rpg3
let gs2=GameState rpg3 ms
put gs2
w<-if isJust ms
then mapAction []
else do
return (WText "Game over")
saveGameAndCharacter (combineWidget wGold w)
getMazeScreen :: RPGState -> Maybe (Screen RPGState)
getMazeScreen RPGState{rpgCharacter=Nothing} = Nothing
getMazeScreen RPGState{mgs=Nothing} = Nothing
getMazeScreen rs | isOutOfService $ fromJust $ rpgCharacter rs = Nothing
getMazeScreen RPGState{rpgCharacter=Just c1,mgs=Just mgs} =
let
gw=mazegameworld mgs
dirs=MG.getDirections gw
f dir = Action (map toLower (show dir)) ("Move " ++ (show dir)) (move dir)
actions=map f dirs
mapA= Action "map" "See the map" mapAction
invA= Action "inventory" "See what you're carrying" inventoryAction
statusA=Action "character" "See your current character characteristics" statusAction
acts=statusA:mapA:invA:(backAction $ getGameScreen initialScreen):actions
items=Objs.listItems (mazegameworld mgs) (objects mgs)
acts2=if null items
then acts
else (Action "pickup" "Pick up an item" (pickupAction items)):acts
itemsCarried=Inv.listCarriedItems (inventory $ c1)
acts3=if null itemsCarried
then acts2
else (Action "drop" "Drop an item" (dropAction itemsCarried)):acts2
usableItems=filter (canUseItem . snd) itemsCarried
acts4=if null usableItems
then acts3
else (Action "use" "Use an item (drink a potion, etc.)" (useItem usableItems)):acts3
usableSpells=spellsToMyself c1
acts5=if null usableSpells
then acts4
else (Action "cast" "Cast a spell" (castSpell usableSpells)):acts4
in
Just (Screen acts5)
statusAction :: ActionFunction RPGState
statusAction _ = do
c<-gets (fromJust . rpgCharacter . gsData)
return (WText $ ppCharacter' c)
getItems :: RPGState -> Widget RPGState
getItems (RPGState {mgs=Nothing})= WNothing
getItems (RPGState {mgs=Just mgs})=
let
items=Objs.listItems (mazegameworld mgs) (objects mgs)
in WList ("You see:":
if null items
then ["Nothing"]
else zipWith (\x y->(show x) ++ ": "++(itName y)) [1..] items)
getNPCW :: RPGState -> Widget RPGState
getNPCW (RPGState {mgs=Nothing})= WNothing
getNPCW (RPGState{mgs=Just mgs})=
let
npc=Objs.getNPC (mazegameworld mgs) (objects mgs)
in WText ("You meet: "++
(if isNothing npc
then "nobody"
else ("a " ++ (name $ npcCharacter $ fromJust npc))))
move :: MG.Direction -> ActionFunction RPGState
move dir _ =do
rs@(RPGState {rpgCharacter=Just c1,mgs=(Just rmgs),fp=fp}) <- gets gsData
let gw=mazegameworld rmgs
sg <- getSplit
(WList s,(GameState gw2 sc))<- liftIO $ runStateT (evalRandT (MG.move dir []) sg) (GameState gw Nothing)
let nameC=name $ c1
if (isJust sc)
then
do
rs2<- tick 10 rs{mgs=Just rmgs{mazegameworld=gw2}}
srw<-getSplit
(rs3,wGold)<-liftIO $ processFight rs2 srw
let ms=getMazeScreen rs3
let gs2=GameState rs3 ms
let (WList itemNames)=getItems rs3
put gs2
w<-if isJust ms
-- we have escaped, probably
then if (position $ mazegameworld $ fromJust $ mgs rs3)== (position $ mazegameworld $ fromJust $ mgs rs2)
then return (WList (s++itemNames))
else mapAction []
else do
return (WText "Game over")
saveGameAndCharacter (combineWidget wGold w)
else
do
liftIO $ deleteGame rs nameC
put (GameState (RPGState Nothing Nothing fp) (Just (getGameScreen initialScreen)))
return (WList s)
processFight :: RPGState -> RandomWrapper -> IO (RPGState,Widget RPGState)
processFight rs@(RPGState{rpgCharacter=Nothing}) _=return (rs,WNothing)
processFight rs@(RPGState{mgs=Nothing}) _=return (rs,WNothing)
processFight rs@(RPGState{rpgCharacter=Just c,mgs=Just mgs}) rw= do
let npc=Objs.getNPC (mazegameworld mgs) (objects mgs)
if (isJust npc)
then do
let npc'=fromJust npc
let (rw1,rw2)=splitWrapper rw
es <- screenInteract c npc' rw1 (tickCount mgs)
let (mo,c2,extraGold) =case (exitStatus es) of
Victory -> killNPC (mazegameworld mgs) (objects mgs) (exitCharacter es)
_ -> (updateNPC (mazegameworld mgs) (objects mgs) (exitOpponent es),exitCharacter es,0)
let mo2=foldl (Objs.dropItem (mazegameworld mgs)) mo (newItems es)
gw<-case (exitStatus es) of
Flight -> do
let gw=mazegameworld mgs
pos <- evalRandT (randomPickp (DataSet.elems (DataSet.delete (position gw) (explored gw)))) rw2
return (gw{position=pos})
_ -> return (mazegameworld mgs)
let mgs2=mgs{mazegameworld=gw,objects=mo2,tickCount=exitTickCount es}
let rs2=rs{rpgCharacter=Just c2,mgs=Just mgs2}
let w=if extraGold>0
then WText (printf "You pick up %d gold coins" extraGold)
else WNothing
return (rs2,w)
else
return (rs,WNothing)
mapAction :: ActionFunction RPGState
mapAction _ = do
rs@(RPGState {mgs=(Just mgs)}) <- gets gsData
let gw=mazegameworld mgs
sg <- getSplit
(WList l,_)<-liftIO $ runStateT (evalRandT (MG.mapAction []) sg) (GameState gw Nothing)
let itemNames=getItems rs
return (combineWidget (WList l) itemNames)
inventoryAction :: ActionFunction RPGState
inventoryAction _ =do
(RPGState {rpgCharacter=(Just c)}) <- gets gsData
return (WText (ppInventory' $ inventory c))
pickupAction :: [ItemType] -> ActionFunction RPGState
pickupAction items cmds = do
if null items
then return (WText "Nothing to pick up")
else getPretypedWidget (getMappedCombo itName ["Which item?"] items pickupItem) (if null cmds then [] else (tail cmds))
pickupItem :: ComboResult ItemType -> GSWScreenT RPGState
pickupItem Empty=return (WText "Pick up canceled")
pickupItem (Unknown _)=return (WText "That's not there to pick")
pickupItem (Exact item) =do
(RPGState {rpgCharacter=(Just c1)}) <- gets gsData
let positions=Inv.listAllowedPositions (inventory $ c1) item
return (WCombo ["Put it where?"] (map ppInventoryPosition' positions) (doPickup item positions))
doPickup :: ItemType -> [(Position,Maybe ItemType)] -> String -> GSWScreenT RPGState
doPickup _ _ ""=return (WText "Pick up canceled")
doPickup item positions pos =do
(GameState {gsData=rs@(RPGState {rpgCharacter=Just c1,mgs=(Just mgs)})}) <- get
let matching=filter (\x->(ppInventoryPosition' x)==pos) positions
if null matching
then return (WText "You can't put that in there")
else do
let match=head matching
let Right (inv,it)=Inv.takeItem (inventory $ c1) item (fst match)
let c2=c1{inventory=inv}
let objs2=Objs.takeItem (mazegameworld mgs) (objects mgs) item
let objs3= foldl (\x y->Objs.dropItem (mazegameworld mgs) x y) objs2 it
let rs2=rs{rpgCharacter=Just c2,mgs=Just mgs{objects=objs3}}
saveNewGameState rs2
let itemNames=getItems rs2
let w =(combineWidget (WText "Item picked up") itemNames)
saveGameAndCharacter w
saveNewGameState :: RPGState -> ScreenT (GameState RPGState) ()
saveNewGameState rs2=do
put (GameState rs2 (getMazeScreen rs2))
tick :: Int -> RPGState -> ScreenT (GameState RPGState) (RPGState)
tick _ rs@(RPGState{mgs=Nothing})=return rs
tick tc rs@(RPGState{mgs=Just mgs})=do
let rs2=rs{mgs=(Just mgs{tickCount=(tickCount mgs)+tc})}
saveNewGameState rs2
return rs2
dropAction :: [(Position,ItemType)] -> ActionFunction RPGState
dropAction items _ =do
return (if null items
then (WText "You're not carrying anything")
else (getMappedCombo ppItemPosition' ["What do you want to drop?"] items dropItemAction)
)
dropItemAction :: ComboResult (Position,ItemType) -> GSWScreenT RPGState
dropItemAction Empty=return (WText "Drop canceled")
dropItemAction (Unknown _)=return (WText "You can't drop that")
dropItemAction (Exact item) =do
rs@(RPGState {rpgCharacter=(Just c1),mgs=(Just mgs)}) <- gets gsData
let Right (inv,it)=Inv.dropItem (inventory $ c1) (fst item)
let c2=c1{inventory=inv}
let objs=if isJust it
then Objs.dropItem (mazegameworld mgs) (objects mgs) (fromJust it)
else (objects mgs)
let rs2=rs{rpgCharacter=Just c2,mgs=Just mgs{objects=objs}}
saveNewGameState rs2
let itemNames=getItems rs2
saveGameAndCharacter (combineWidget (WText "Item dropped") itemNames)
saveGameAndCharacter :: Widget RPGState -> GSWScreenT RPGState
saveGameAndCharacter w=do
gs@(GameState {gsData=rs@(RPGState {rpgCharacter=(Just c1)})}) <- get
when (isOutOfService c1) (put (gs{screen=Just (getGameScreen initialScreen)}))
saveResult<- liftIO $ saveCurrent rs
let ws=case saveResult of
Right _ -> Nothing
Left t -> Just $ WText t
return (combineMaybeWidget w ws)
listGamesW :: (String -> GSWScreenT RPGState) -> String -> GSWScreenT RPGState
listGamesW af name= do
rs<-gets gsData
cs<-liftIO $ listGames rs name
if null cs
then return (WText "No game created")
else do
let (l,r)=partition (not . isRight) cs
let l'=(map (\(Left a)->a) l)
let r'=map (\(Right (n,_))->n) r
return (WCombo ("Choose game:":l') r' (af))
deleteGames :: ActionFunction RPGState
deleteGames _ =listCharactersW (listGamesW $ deleteFileW deleteGame)
deleteFileW :: (RPGState -> String -> IO (Either String String)) -> String -> GSWScreenT RPGState
deleteFileW _ ""=return (WText "Deletion canceled")
deleteFileW f name=return (WCheck ["Delete "++name] "Are you sure?" False (reallyDeleteFileW f name))
reallyDeleteFileW :: (RPGState -> String -> IO (Either String String)) -> Name -> Bool -> GSWScreenT RPGState
reallyDeleteFileW _ _ False = return (WText "Deletion canceled")
reallyDeleteFileW f name True = do
rs<-gets gsData
liftIO $ f rs name
return (WText (name++" deleted"))
playGames :: ActionFunction RPGState
playGames _ =listCharactersW (listGamesW playGame)
playGame :: String -> GSWScreenT RPGState
playGame ""= return (WText "No game chosen")
playGame shortName= do
rs <- gets gsData
r <- withBackup rs shortName
(\BackupState{bckCharacter=c,bckGame=(Just g)}->do
createMazeState' c g)
onError r
useItem :: [(Position,ItemType)] -> ActionFunction RPGState
useItem items _=do
return (if null items
then (WText "You're not carrying anything to use")
else (getMappedCombo ppItemPosition' ["What do you want to use?"] items useItemAction)
)
useItemAction :: ComboResult (Position,ItemType) -> GSWScreenT RPGState
useItemAction Empty=return (WText "Use canceled")
useItemAction (Unknown _)=return (WText "You can't use that")
useItemAction (Exact item) =do
rs@(RPGState {rpgCharacter=Just c1}) <- gets gsData
maybeSC1<- useItemEffect (snd item) c1
let (s,c2)= case maybeSC1 of
Nothing -> ("You can't use that",c1)
Just (s,c',remove) ->
let
c2=if remove
then
let Right (inv,_)=Inv.dropItem (inventory $ c1) (fst item)
in c'{inventory=inv}
else c'
in (s,c2)
let rs2=rs{rpgCharacter=Just c2}
saveNewGameState rs2
saveGameAndCharacter (WText s)
castSpell :: [Spell] -> ActionFunction RPGState
castSpell spells _=do
return (if null spells
then (WText "You don't know any useful spell")
else (getMappedCombo spellName ["Which spell do you want to cast?"] spells castSpellAction)
)
castSpellAction :: ComboResult Spell -> GSWScreenT RPGState
castSpellAction Empty=return (WText "Cast canceled")
castSpellAction (Unknown _)=return (WText "You can't cast that")
castSpellAction (Exact spell) =do
rs@(RPGState {rpgCharacter=Just c1,mgs=Just mgs}) <- gets gsData
(c2,s)<-spellToMyself c1 spell (tickCount mgs)
tick 5 (rs{rpgCharacter=Just c2})
saveGameAndCharacter (WText $ show s)