packages feed

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)