LambdaHack 0.2.0 → 0.2.1
raw patch · 59 files changed
+3546/−1994 lines, 59 filesdep ~LambdaHackdep ~base
Dependency ranges changed: LambdaHack, base
Files
- Game/LambdaHack/Action.hs +413/−215
- Game/LambdaHack/Actions.hs +183/−124
- Game/LambdaHack/Actor.hs +93/−58
- Game/LambdaHack/ActorState.hs +154/−82
- Game/LambdaHack/Binding.hs +19/−17
- Game/LambdaHack/BindingAction.hs +31/−19
- Game/LambdaHack/Cave.hs +21/−17
- Game/LambdaHack/Color.hs +43/−6
- Game/LambdaHack/Command.hs +26/−19
- Game/LambdaHack/Content/ActorKind.hs +5/−2
- Game/LambdaHack/Content/CaveKind.hs +6/−2
- Game/LambdaHack/Content/ItemKind.hs +2/−0
- Game/LambdaHack/Content/RuleKind.hs +2/−0
- Game/LambdaHack/Display.hs +5/−196
- Game/LambdaHack/Display/Curses.hs +39/−26
- Game/LambdaHack/Display/Gtk.hs +385/−103
- Game/LambdaHack/Display/Std.hs +30/−23
- Game/LambdaHack/Display/Vty.hs +61/−43
- Game/LambdaHack/Draw.hs +177/−0
- Game/LambdaHack/Dungeon.hs +5/−1
- Game/LambdaHack/DungeonState.hs +18/−17
- Game/LambdaHack/EffectAction.hs +357/−263
- Game/LambdaHack/Feature.hs +1/−1
- Game/LambdaHack/Grammar.hs +125/−46
- Game/LambdaHack/HighScore.hs +24/−13
- Game/LambdaHack/Item.hs +5/−3
- Game/LambdaHack/ItemAction.hs +206/−170
- Game/LambdaHack/Key.hs +48/−15
- Game/LambdaHack/Kind.hs +8/−3
- Game/LambdaHack/Level.hs +27/−35
- Game/LambdaHack/Misc.hs +1/−4
- Game/LambdaHack/Msg.hs +149/−34
- Game/LambdaHack/Perception.hs +42/−33
- Game/LambdaHack/Place.hs +10/−8
- Game/LambdaHack/Point.hs +19/−4
- Game/LambdaHack/PointXY.hs +20/−1
- Game/LambdaHack/Random.hs +1/−1
- Game/LambdaHack/Running.hs +25/−20
- Game/LambdaHack/Save.hs +27/−7
- Game/LambdaHack/Start.hs +7/−6
- Game/LambdaHack/State.hs +37/−32
- Game/LambdaHack/Strategy.hs +8/−8
- Game/LambdaHack/StrategyAction.hs +94/−63
- Game/LambdaHack/Tile.hs +8/−13
- Game/LambdaHack/Time.hs +143/−0
- Game/LambdaHack/Turn.hs +201/−123
- Game/LambdaHack/Utils/Assert.hs +1/−0
- Game/LambdaHack/Utils/Frequency.hs +35/−25
- Game/LambdaHack/Utils/LQueue.hs +35/−0
- Game/LambdaHack/Vector.hs +20/−3
- LambdaHack.cabal +20/−15
- LambdaHack/Content/ActorKind.hs +24/−10
- LambdaHack/Content/CaveKind.hs +12/−8
- LambdaHack/Content/ItemKind.hs +33/−13
- LambdaHack/Content/RuleKind.hs +2/−0
- PLAYING.md +38/−40
- README.md +11/−2
- config.default +4/−2
- scores binary
Game/LambdaHack/Action.hs view
@@ -1,46 +1,73 @@--- TODO: Add an export list, with sections, after the file is rewritten--- according to #17. Perhaps make some types abstract. -- | Game action monad and basic building blocks -- for player and monster actions. {-# LANGUAGE MultiParamTypeClasses, RankNTypes #-}-module Game.LambdaHack.Action where+module Game.LambdaHack.Action+ ( -- * Actions and basic operations+ ActionFun, Action, handlerToIO, rndToAction+ -- * Actions returning frames+ , ActionFrame, returnNoFrame, whenFrame, inFrame+ -- * Game session and its accessors+ , Session(..), getCOps, getBinding+ -- * Various ways to abort action+ , abort, abortWith, abortIfWith, neverMind+ -- * Abort exception handlers+ , tryWith, tryWithFrame, tryRepeatedlyWith, tryIgnore, tryIgnoreFrame+ -- * Diary and report+ , getDiary, msgAdd, recordHistory+ -- * Key input+ , getKeyCommand, getKeyChoice, getOverConfirm+ -- * Display each frame and confirm+ , displayMore, displayYesNo, displayOverAbort+ -- * Assorted frame operations+ , displayOverlays, displayChoiceUI, displayFramePush, drawPrompt+ -- * Clip init operations+ , startClip, remember, rememberList+ -- * Assorted operations+ , getPerception, updateAnyActor, updatePlayerBody+ -- * Assorted primitives+ , currentDate, saveGameBkp, dumpCfg, shutGame+ , debug+ ) where import Control.Monad-import Control.Monad.State hiding (State, state)+import Control.Monad.State hiding (State, state, liftIO) import qualified Data.IntMap as IM+import qualified Data.IntSet as IS import qualified Data.Map as M+import qualified Data.List as L+import System.Time import Data.Maybe+import Control.Concurrent+import Control.Exception (finally) -- import System.IO (hPutStrLn, stderr) -- just for debugging +import Game.LambdaHack.Utils.Assert import Game.LambdaHack.Perception import Game.LambdaHack.Display+import Game.LambdaHack.Draw import Game.LambdaHack.Msg import Game.LambdaHack.State import Game.LambdaHack.Level import Game.LambdaHack.Actor import Game.LambdaHack.ActorState-import Game.LambdaHack.Content.ActorKind import qualified Game.LambdaHack.Save as Save import qualified Game.LambdaHack.Kind as Kind import Game.LambdaHack.Random import qualified Game.LambdaHack.Key as K import Game.LambdaHack.Binding---- | The constant session information, not saved to the game save file.-data Session = Session- { sfs :: FrontendSession -- ^ frontend session information- , scops :: Kind.COps -- ^ game content- , skeyb :: Binding (Action ()) -- ^ binding of keys to commands- }+import qualified Game.LambdaHack.HighScore as H+import qualified Game.LambdaHack.Config as Config+import qualified Game.LambdaHack.Color as Color+import Game.LambdaHack.Point+import Game.LambdaHack.Time -- | The type of the function inside any action. -- (Separated from the @Action@ type to document each argument with haddock.) type ActionFun r a = Session -- ^ session setup data- -> (State -> Diary -> IO r) -- ^ shutdown continuation- -> Perception -- ^ cached perception+ -> DungeonPerception -- ^ cached perception -> (State -> Diary -> a -> IO r) -- ^ continuation- -> IO r -- ^ failure/reset continuation+ -> (Msg -> IO r) -- ^ failure/reset continuation -> State -- ^ current state -> Diary -- ^ current diary -> IO r@@ -60,39 +87,41 @@ (>>=) = bindAction instance Functor Action where- fmap f (Action g) = Action (\ s e p k a st ms ->+ fmap f (Action g) = Action (\ s p k a st ms -> let k' st' ms' = k st' ms' . f- in g s e p k' a st ms)+ in g s p k' a st ms) +instance MonadState State Action where+ get = Action (\ _s _p k _a st ms -> k st ms st)+ put nst = Action (\ _s _p k _a _st ms -> k nst ms ())+ -- | Invokes the action continuation on the provided argument. returnAction :: a -> Action a-returnAction x = Action (\ _s _e _p k _a st m -> k st m x)+returnAction x = Action (\ _s _p k _a st m -> k st m x) -- | Distributes the session and shutdown continuation, -- threads the state and diary. bindAction :: Action a -> (a -> Action b) -> Action b-bindAction m f = Action (\ s e p k a st ms ->+bindAction m f = Action (\ s p k a st ms -> let next nst nm x =- runAction (f x) s e p k a nst nm- in runAction m s e p next a st ms)--instance MonadIO Action where- liftIO x = Action (\ _s _e _p k _a st ms -> x >>= k st ms)+ runAction (f x) s p k a nst nm+ in runAction m s p next a st ms) -instance MonadState State Action where- get = Action (\ _s _e _p k _a st ms -> k st ms st)- put nst = Action (\ _s _e _p k _a _st ms -> k nst ms ())+-- Instance commented out and action hiden, so that outside of this module+-- nobody can subvert Action by invoking arbitrary IO.+-- instance MonadIO Action where+liftIO :: IO a -> Action a+liftIO x = Action (\ _s _p k _a st ms -> x >>= k st ms) -- | Run an action, with a given session, state and diary, in the @IO@ monad. handlerToIO :: Session -> State -> Diary -> Action () -> IO ()-handlerToIO sess@Session{sfs, scops} state diary h =+handlerToIO sess@Session{scops} state diary h = runAction h sess- (\ ns ndiary -> Save.rmBkpSaveDiary ns ndiary- >> shutdown sfs) -- get out of the game- (perception scops state) -- create and cache perception+ (dungeonPerception scops state) -- create and cache perception (\ _ _ x -> return x) -- final continuation returns result- (ioError $ userError "unhandled abort")+ (\ msg ->+ ioError $ userError $ "unhandled abort " ++ msg) -- e.g., in AI code state diary @@ -104,203 +133,321 @@ modify (\ state -> state {srandom = ng}) return a --- | Invoke a session command.-session :: (Session -> Action a) -> Action a-session f = Action (\ sess e p k a st ms ->- runAction (f sess) sess e p k a st ms)---- | Invoke a session @IO@ command.-sessionIO :: (Session -> IO a) -> Action a-sessionIO f = Action (\ sess _e _p k _a st ms -> f sess >>= k st ms)---- | Display the current level with modified current msg.-displayGeneric :: ColorMode -> (Msg -> Msg) -> Action Bool-displayGeneric dm f =- Action (\ Session{sfs, scops} _e p k _a st ms ->- displayLevel dm sfs scops p st (f (smsg ms)) Nothing- >>= k st ms)---- | Display the current level, with the current msg and color.-displayAll :: Action Bool-displayAll = displayGeneric ColorFull id---- | Display an overlay on top of the current screen.-overlay :: String -> Action Bool-overlay txt =- Action (\ Session{sfs, scops} _e p k _a st ms ->- displayLevel ColorFull sfs scops p st (smsg ms) (Just txt)- >>= k st ms)---- | Get the current diary.-currentDiary :: Action Diary-currentDiary = Action (\ _s _e _p k _a st diary -> k st diary diary)+-- | Actions and screen frames, including delays, resulting+-- from performing the actions.+type ActionFrame a = Action (a, [Maybe Color.SingleFrame]) --- | Wipe out and set a new value for the current diary.-diaryReset :: Diary -> Action ()-diaryReset ndiary = Action (\ _s _e _p k _a st _diary -> k st ndiary ())+-- | Return the value with an empty set of screen frames.+returnNoFrame :: a -> ActionFrame a+returnNoFrame a = return (a, []) --- | Get the current msg.-currentMsg :: Action Msg-currentMsg = Action (\ _s _e _p k _a st ms -> k st ms (smsg ms))+-- | As the @when@ monad operation, but on type @ActionFrame ()@.+whenFrame :: Bool -> ActionFrame () -> ActionFrame ()+whenFrame True x = x+whenFrame False _ = returnNoFrame () --- | Wipe out and set a new value for the current msg.-msgReset :: Msg -> Action ()-msgReset nm = Action (\ _s _e _p k _a st ms -> k st ms{smsg = nm} ())+-- | Inject action into actions with screen frames.+inFrame :: Action () -> ActionFrame ()+inFrame act = act >> returnNoFrame () --- | Add to the current msg.-msgAdd :: Msg -> Action ()-msgAdd nm = Action (\ _s _e _p k _a st ms ->- k st ms{smsg = addMsg (smsg ms) nm} ())+-- | The constant session information, not saved to the game save file.+data Session = Session+ { sfs :: FrontendSession -- ^ frontend session information+ , scops :: Kind.COps -- ^ game content+ , skeyb :: Binding (ActionFrame ()) -- ^ binding of keys to commands+ } --- | Clear the current msg.-msgClear :: Action ()-msgClear = Action (\ _s _e _p k _a st ms -> k st ms{smsg = ""} ())+-- | Get the frontend session.+getFrontendSession :: Action FrontendSession+getFrontendSession = Action (\ Session{sfs} _p k _a st ms -> k st ms sfs) -- | Get the content operations.-contentOps :: Action Kind.COps-contentOps = Action (\ Session{scops} _e _p k _a st ms -> k st ms scops)---- | Get the content operations modified by a function (usually a selector).-contentf :: (Kind.COps -> a) -> Action a-contentf f = Action (\ Session{scops} _e _p k _a st ms -> k st ms (f scops))+getCOps :: Action Kind.COps+getCOps = Action (\ Session{scops} _p k _a st ms -> k st ms scops) --- | End the game, i.e., invoke the shutdown continuation.-end :: Action ()-end = Action (\ _s e _p _k _a s diary -> e s diary)+-- | Get the key binding.+getBinding :: Action (Binding (ActionFrame ()))+getBinding = Action (\ Session{skeyb} _p k _a st ms -> k st ms skeyb) -- | Reset the state and resume from the last backup point, i.e., invoke -- the failure continuation. abort :: Action a-abort = Action (\ _s _e _p _k a _st _ms -> a)+abort = abortWith "" +-- | Abort with the given message.+abortWith :: Msg -> Action a+abortWith msg = Action (\ _s _p _k a _st _ms -> a msg)++-- | Abort and print the given msg if the condition is true.+abortIfWith :: Bool -> Msg -> Action a+abortIfWith True msg = abortWith msg+abortIfWith False _ = abortWith ""++-- | Abort and conditionally print the fixed message.+neverMind :: Bool -> Action a+neverMind b = abortIfWith b "never mind"+ -- | Set the current exception handler. First argument is the handler, -- second is the computation the handler scopes over.-tryWith :: Action () -> Action () -> Action ()-tryWith exc h = Action (\ s e p k a st ms ->- let runA = runAction exc s e p k a st ms- in runAction h s e p k runA st ms)+tryWith :: (Msg -> Action a) -> Action a -> Action a+tryWith exc h = Action (\ s p k a st ms ->+ let runA msg = runAction (exc msg) s p k a st ms+ in runAction h s p k runA st ms) +-- | Set the current exception handler. Apart of executing it,+-- draw and pass along a frame with the abort message, if any.+tryWithFrame :: Action a -> ActionFrame a -> ActionFrame a+tryWithFrame exc h =+ let msgToFrames "" = returnNoFrame ()+ msgToFrames msg = do+ msgReset ""+ fr <- drawPrompt ColorFull msg+ return ((), [Just fr])+ excMsg msg = do+ ((), frames) <- msgToFrames msg+ a <- exc+ return (a, frames)+ in tryWith excMsg h+ -- | Take a handler and a computation. If the computation fails, the -- handler is invoked and then the computation is retried.-tryRepeatedlyWith :: Action () -> Action () -> Action ()-tryRepeatedlyWith exc h = tryWith (exc >> tryRepeatedlyWith exc h) h+tryRepeatedlyWith :: (Msg -> Action ()) -> Action () -> Action ()+tryRepeatedlyWith exc h =+ tryWith (\ msg -> exc msg >> tryRepeatedlyWith exc h) h -- | Try the given computation and silently catch failure.-try :: Action () -> Action ()-try = tryWith (return ())+tryIgnore :: Action () -> Action ()+tryIgnore =+ tryWith (\ msg -> if null msg+ then return ()+ else assert `failure` (msg, "in tryIgnore")) --- | Try the given computation until it succeeds without failure.-tryRepeatedly :: Action () -> Action ()-tryRepeatedly = tryRepeatedlyWith (return ())+-- | Try the given computation and silently catch failure,+-- returning empty set of screen frames.+tryIgnoreFrame :: ActionFrame () -> ActionFrame ()+tryIgnoreFrame =+ tryWith (\ msg -> if null msg+ then returnNoFrame ()+ else assert `failure` (msg, "in tryIgnoreFrame")) --- | Debugging.-debug :: String -> Action ()-debug _x = return () -- liftIO $ hPutStrLn stderr _x+-- | Get the current diary.+getDiary :: Action Diary+getDiary = Action (\ _s _p k _a st diary -> k st diary diary) --- | Print the given msg, then abort.-abortWith :: Msg -> Action a-abortWith msg = do- msgReset msg- displayAll- abort+-- | Add a message to the current report.+msgAdd :: Msg -> Action ()+msgAdd nm = Action (\ _s _p k _a st ms ->+ k st ms{sreport = addMsg (sreport ms) nm} ()) --- | Abort, and print the given msg if the condition is true.-abortIfWith :: Bool -> Msg -> Action a-abortIfWith True msg = abortWith msg-abortIfWith False _ = abortWith ""+-- | Wipe out and set a new value for the history.+historyReset :: History -> Action ()+historyReset shistory = Action (\ _s _p k _a st Diary{sreport} ->+ k st Diary{..} ()) --- | Abort conditionally, with a fixed message.-neverMind :: Bool -> Action a-neverMind b = abortIfWith b "never mind"+-- | Wipe out and set a new value for the current report.+msgReset :: Msg -> Action ()+msgReset nm = Action (\ _s _p k _a st ms ->+ k st ms{sreport = singletonReport nm} ()) +-- | Store current report in the history and reset report.+recordHistory :: Action ()+recordHistory = do+ Diary{sreport, shistory} <- getDiary+ unless (nullReport sreport) $ do+ config <- gets sconfig+ let historyMax = Config.get config "ui" "historyMax"+ msgReset ""+ historyReset $ takeHistory historyMax $ addReport sreport shistory++-- | Wait for a player command.+getKeyCommand :: Maybe Bool -> Action (K.Key, K.Modifier)+getKeyCommand doPush = do+ fs <- getFrontendSession+ keyb <- getBinding+ (nc, modifier) <- liftIO $ nextEvent fs doPush+ return $ case modifier of+ K.NoModifier -> (fromMaybe nc $ M.lookup nc $ kmacro keyb, modifier)+ _ -> (nc, modifier)+ -- | Wait for a player keypress.-nextCommand :: Session -> Action K.Key-nextCommand Session{sfs, skeyb} = do- nc <- liftIO $ nextEvent sfs- return $ fromMaybe nc $ M.lookup nc $ kmacro skeyb+getKeyChoice :: [(K.Key, K.Modifier)] -> Color.SingleFrame+ -> Action (K.Key, K.Modifier)+getKeyChoice keys frame = do+ fs <- getFrontendSession+ liftIO $ promptGetKey fs keys frame +-- | Ignore unexpected kestrokes until a SPACE or ESC is pressed.+getConfirm :: Color.SingleFrame -> Action Bool+getConfirm frame = do+ fs <- getFrontendSession+ let keys = [ (K.Space, K.NoModifier), (K.Esc, K.NoModifier)]+ (k, _) <- liftIO $ promptGetKey fs keys frame+ case k of+ K.Space -> return True+ _ -> return False++-- | A series of confirmations for all overlays.+getOverConfirm :: [Color.SingleFrame] -> Action Bool+getOverConfirm [] = return True+getOverConfirm (x:xs) = do+ b <- getConfirm x+ if b+ then getOverConfirm xs+ else return False+ -- | A yes-no confirmation.-getYesNo :: Session -> Action Bool-getYesNo sess@Session{sfs} = do- e <- liftIO $ nextEvent sfs- case e of+getYesNo :: Color.SingleFrame -> Action Bool+getYesNo frame = do+ fs <- getFrontendSession+ let keys = [ (K.Char 'y', K.NoModifier)+ , (K.Char 'n', K.NoModifier)+ , (K.Esc, K.NoModifier)+ ]+ (k, _) <- liftIO $ promptGetKey fs keys frame+ case k of K.Char 'y' -> return True- K.Char 'n' -> return False- K.Esc -> return False- _ -> getYesNo sess+ _ -> return False --- | Waits for a SPACE or ESC. Passes along any other key, including RET,--- to an argument function.-getOptionalConfirm :: (Bool -> Action a)- -> (K.Key -> Action a)- -> Session- -> Action a-getOptionalConfirm h k Session{sfs} = do- e <- liftIO $ nextEvent sfs- case e of- K.Space -> h True- K.Esc -> h False- _ -> k e+-- | Display a msg with a @more@ prompt. Return value indicates if the player+-- tried to cancel/escape.+displayMore :: ColorMode -> Msg -> Action Bool+displayMore dm prompt = do+ frame <- drawPrompt dm (prompt ++ moreMsg)+ getConfirm frame --- | Ignore unexpected kestrokes until a SPACE or ESC is pressed.-getConfirm :: Session -> Action Bool-getConfirm Session{sfs} = liftIO $ getConfirmD sfs+-- | Print a yes/no question and return the player's answer. Use black+-- and white colours to turn player's attention to the choice.+displayYesNo :: Msg -> Action Bool+displayYesNo prompt = do+ frame <- drawPrompt ColorBW (prompt ++ yesnoMsg)+ getYesNo frame --- | Print msg, await confirmation. Return value indicates--- if the player tried to abort/escape.-msgMoreConfirm :: ColorMode -> Msg -> Action Bool-msgMoreConfirm dm msg = do- msgAdd (msg ++ more)- displayGeneric dm id- session getConfirm+-- | Print a msg and several overlays, one per page.+-- All frames require confirmations. Raise @abort@ if the players presses ESC.+displayOverAbort :: Msg -> [Overlay] -> Action ()+displayOverAbort prompt xs = do+ let f x = drawOverlay ColorFull prompt (x ++ [moreMsg])+ frames <- mapM f xs+ b <- getOverConfirm frames+ when (not b) abort --- | Print msg, await confirmation, ignore confirmation.-msgMore :: Msg -> Action ()-msgMore msg = msgClear >> msgMoreConfirm ColorFull msg >> return ()+-- | Print a msg and several overlays, one per page.+-- The last frame does not expect a confirmation.+displayOverlays :: Msg -> [Overlay] -> ActionFrame ()+displayOverlays _ [] = returnNoFrame ()+displayOverlays prompt [x] = do+ frame <- drawOverlay ColorFull prompt x+ return $ ((), [Just frame])+displayOverlays prompt (x:xs) = do+ frame <- drawOverlay ColorFull prompt (x ++ [moreMsg])+ b <- getConfirm frame+ if b+ then displayOverlays prompt xs+ else returnNoFrame () --- | Print a yes/no question and return the player's answer.-msgYesNo :: Msg -> Action Bool-msgYesNo msg = do- msgReset (msg ++ yesno)- displayGeneric ColorBW id -- turn player's attention to the choice- session getYesNo+-- | Print a prompt and an overlay and wait for a player keypress.+-- If many overlays, scroll screenfuls with SPACE. Do not wrap screenfuls+-- (in some menus @?@ cycles views, so the user can restart from the top).+displayChoiceUI :: Msg -> [Overlay] -> [(K.Key, K.Modifier)]+ -> Action (K.Key, K.Modifier)+displayChoiceUI prompt ovs keys = do+ let (over, rest, spc, more, keysS) = case ovs of+ [] -> ([], [], "", [], keys)+ [x] -> (x, [], "", [], keys)+ x:xs -> (x, xs, ", SPACE", [moreMsg], (K.Space, K.NoModifier) : keys)+ frame <- drawOverlay ColorFull (prompt ++ spc ++ ", ESC]") (over ++ more)+ (key, modifier) <- getKeyChoice ((K.Esc, K.NoModifier) : keysS) frame+ case key of+ K.Esc -> neverMind True+ K.Space | not (null rest) -> displayChoiceUI prompt rest keys+ _ -> return (key, modifier) --- | Clear message and overlay.-clearDisplay :: Action Bool-clearDisplay = do- msgClear- displayAll- return False+-- | Push a frame or a single frame's worth of delay to the frame queue.+displayFramePush :: Maybe Color.SingleFrame -> Action ()+displayFramePush mframe = do+ fs <- getFrontendSession+ liftIO $ displayFrame fs False mframe --- | Print a msg and several overlays, one per page, and await confirmation.--- The return value indicates if the player tried to abort/escape.-msgOverlaysConfirm :: Msg -> [String] -> Action Bool-msgOverlaysConfirm _msg [] = return True-msgOverlaysConfirm msg [x] = do- msgReset msg- b0 <- overlay (x ++ msgEnd)- if b0- then return True- else clearDisplay-msgOverlaysConfirm msg (x:xs) = do- msgReset msg- b0 <- overlay (x ++ more)- if b0- then do- b <- session getConfirm- if b- then msgOverlaysConfirm msg xs- else clearDisplay- else clearDisplay+-- | Draw the current level. The prompt is displayed, but not added+-- to history. The prompt is appended to the current message+-- and only the first screenful of the resulting overlay is displayed.+drawPrompt :: ColorMode -> Msg -> Action Color.SingleFrame+drawPrompt dm prompt = do+ cops <- getCOps+ per <- getPerception+ s <- get+ Diary{sreport} <- getDiary+ let over = splitReport $ addMsg sreport prompt+ return $ draw dm cops per s over +-- | Draw the current level. The prompt and the overlay are displayed,+-- but not added to history. The prompt is appended to the current message+-- and only the first line of the result is displayed.+-- The overlay starts on the second line.+drawOverlay :: ColorMode -> Msg -> Overlay -> Action Color.SingleFrame+drawOverlay dm prompt overlay = do+ cops <- getCOps+ per <- getPerception+ s <- get+ Diary{sreport} <- getDiary+ let xsize = lxsize $ slevel s+ msgPrompt = renderReport $ addMsg sreport prompt+ over = padMsg xsize msgPrompt : overlay+ return $ draw dm cops per s over++-- | Initialize perception, etc., display level and run the action.+startClip :: Action () -> Action ()+startClip action =+ -- Determine perception before running player command, in case monsters+ -- have opened doors, etc.+ withPerception $ do+ remember -- heroes notice their surroundings, before they get displayed+ displayPush -- draw the current surroundings+ action -- let the actor act++-- | Push the frame depicting the current level to the frame queue.+-- Only one screenful of the report is shown, the rest is ignored.+displayPush :: Action ()+displayPush = do+ fs <- getFrontendSession+ s <- get+ pl <- gets splayer+ frame <- drawPrompt ColorFull ""+ -- Visually speed up (by remving all empty frames) the show of the sequence+ -- of the move frames if the player is running.+ let (_, Actor{bdir}, _) = findActorAnyLevel pl s+ isRunning = isJust bdir+ liftIO $ displayFrame fs isRunning $ Just frame++-- | Update player memory.+remember :: Action ()+remember = do+ per <- getPerception+ let vis = IS.toList (totalVisible per)+ rememberList vis++-- | Update player at the given list of locations..+rememberList :: [Point] -> Action ()+rememberList vis = do+ lvl <- gets slevel+ let rememberTile = [(loc, lvl `at` loc) | loc <- vis]+ modify (updateLevel (updateLRMap (Kind.// rememberTile)))+ let alt Nothing = Nothing+ alt (Just ([], _)) = Nothing+ alt (Just (t, _)) = Just (t, t)+ rememberItem = IM.alter alt+ modify (updateLevel (updateIMap (\ m -> foldr rememberItem m vis)))+ -- | Update the cached perception for the given computation. withPerception :: Action () -> Action ()-withPerception h = Action (\ sess@Session{scops} e _ k a st ms ->- runAction h sess e (perception scops st) k a st ms)+withPerception h =+ Action (\ sess@Session{scops} _ k a st ms ->+ runAction h sess (dungeonPerception scops st) k a st ms) -- | Get the current perception.-currentPerception :: Action Perception-currentPerception = Action (\ _s _e p k _a st ms -> k st ms p)+getPerception :: Action Perception+getPerception = Action (\ _s per k _a s ms ->+ k s ms (fromJust $ L.lookup (slid s) per)) -- | Update actor stats. Works for actors on other levels, too. updateAnyActor :: ActorId -> (Actor -> Actor) -> Action ()@@ -312,33 +459,84 @@ pl <- gets splayer updateAnyActor pl f --- | Advance the move time for the given actor.-advanceTime :: ActorId -> Action ()-advanceTime actor = do- Kind.Ops{okind} <- contentf Kind.coactor- time <- gets stime- let upd m = m { btime = time + aspeed (okind (bkind m)) }- -- A hack to synchronize the whole party:- pl <- gets splayer- if actor == pl || isAHero actor- then do- modify (updateLevel (updateHeroes (IM.map upd)))- unless (isAHero pl) $ updatePlayerBody upd- else do- s <- get- -- If actor dead or not on current level, don't bother.- when (memActor actor s) $ updateAnyActor actor upd+-- | Obtains the current date and time.+currentDate :: Action ClockTime+currentDate = liftIO getClockTime --- | Add a turn to the player time counter.-playerAdvanceTime :: Action ()-playerAdvanceTime = do- pl <- gets splayer- advanceTime pl+-- | Save the diary and a backup of the save game file, in case of crashes.+--+-- See 'Save.saveGameBkp'.+saveGameBkp :: State -> Diary -> Action ()+saveGameBkp state diary = liftIO $ Save.saveGameBkp state diary --- | Display command help.-displayHelp :: Action ()-displayHelp = do- let disp Session{skeyb} =- msgOverlaysConfirm "Basic keys. [press SPACE or ESC]" $ keyHelp skeyb- session disp- abort+-- | Dumps the current configuration to a file.+--+-- See 'Config.dump'.+dumpCfg :: FilePath -> Config.CP -> Action ()+dumpCfg fn config = liftIO $ Config.dump fn config++-- | Handle current score and display it with the high scores.+-- False if display of the scores was void or interrupted by the user.+--+-- Warning: scores are shown during the game,+-- so we should be careful not to leak secret information through them+-- (e.g., the nature of the items through the total worth of inventory).+handleScores :: Bool -> H.Status -> Int -> Action ()+handleScores write status total =+ when (total /= 0) $ do+ config <- gets sconfig+ time <- gets stime+ curDate <- currentDate+ let points = case status of+ H.Killed _ -> (total + 1) `div` 2+ _ -> total+ let score = H.ScoreRecord points (timeNegate time) curDate status+ (placeMsg, slideshow) <- liftIO $ H.register config write score+ displayOverAbort placeMsg slideshow++-- | End the game, shutting down the frontend. The boolean argument+-- tells if ending screens should be shown, the other arguments describes+-- the cause of shutdown.+shutGame :: (Bool, H.Status) -> Action ()+shutGame (showEndingScreens, status) = do+ Kind.COps{coitem} <- getCOps+ s <- get+ diary <- getDiary+ let (_, total) = calculateTotal coitem s+ case status of+ H.Camping -> do+ -- Save an display in parallel.+ mv <- liftIO newEmptyMVar+ liftIO $ void $ forkIO (Save.saveGameFile s `finally` putMVar mv ())+ tryIgnore $ do+ handleScores False status total+ void $ displayMore ColorFull "See you soon, stronger and braver!"+ liftIO $ takeMVar mv -- wait until saved+ H.Killed _ | showEndingScreens -> do+ Diary{sreport} <- getDiary+ unless (nullReport sreport) $ do+ -- Sisplay any leftover report. Suggest it could be the cause of death.+ void $ displayMore ColorFull "Who would have thought?"+ recordHistory -- prevent repeating the report+ tryIgnore $ do+ handleScores True status total+ void $ displayMore ColorFull+ "Let's hope another party can save the day!"+ H.Victor | showEndingScreens -> do+ Diary{sreport} <- getDiary+ unless (nullReport sreport) $ do+ -- Sisplay any leftover report. Suggest it could be the master move.+ void $ displayMore ColorFull "Brilliant, wasn't it?"+ recordHistory -- prevent repeating the report+ tryIgnore $ do+ handleScores True status total+ void $ displayMore ColorFull "Can it be done better, though?"+ _ -> return ()+ fs <- getFrontendSession+ liftIO $ do+ Save.rmBkpSaveDiary s diary -- save the diary often in case of crashes+ shutdown fs++-- | Debugging.+debug :: String -> Action ()+debug _x = return () -- liftIO $ hPutStrLn stderr _x
Game/LambdaHack/Actions.hs view
@@ -24,8 +24,6 @@ import Game.LambdaHack.ActorState import Game.LambdaHack.Perception import Game.LambdaHack.State-import qualified Game.LambdaHack.Config as Config-import qualified Game.LambdaHack.Save as Save import qualified Game.LambdaHack.Effect as Effect import Game.LambdaHack.EffectAction import qualified Game.LambdaHack.Tile as Tile@@ -36,45 +34,29 @@ import Game.LambdaHack.Content.TileKind as TileKind import Game.LambdaHack.Content.ItemKind import Game.LambdaHack.Random--displayHistory :: Action ()-displayHistory = do- diary <- currentDiary- msgOverlaysConfirm "History:" [unlines $ shistory diary]- abort--dumpConfig :: Action ()-dumpConfig = do- config <- gets sconfig- let fn = "config.dump"- liftIO $ Config.dump fn config- abortWith $ "Current configuration dumped to file " ++ fn ++ "."+import Game.LambdaHack.Msg+import Game.LambdaHack.Binding+import Game.LambdaHack.Time+import qualified Game.LambdaHack.Color as Color+import Game.LambdaHack.Draw saveGame :: Action () saveGame = do- b <- msgYesNo "Really save?"+ b <- displayYesNo "Really save?" if b- then do- -- Save the game state- cops <- contentf Kind.coitem- state <- get- diary <- currentDiary- liftIO $ Save.saveGame state diary- let total = calculateTotal cops state- status = H.Camping- go <- handleScores False status total- when go $ msgMore "See you soon, stronger and braver!"- end+ then modify (\ s -> s {squit = Just (True, H.Camping)}) else abortWith "Game resumed." quitGame :: Action () quitGame = do- b <- msgYesNo "Really quit?"+ b <- displayYesNo "Really quit?" if b- then end -- no highscore display for quitters+ then let status = H.Killed $ Dungeon.levelDefault 0+ -- No highscore display for quitters.+ in modify (\ s -> s {squit = Just (False, status)}) else abortWith "Game resumed." -moveCursor :: Vector -> Int -> Action ()+moveCursor :: Vector -> Int -> ActionFrame () moveCursor dir n = do lxsize <- gets (lxsize . slevel) lysize <- gets (lysize . slevel)@@ -89,11 +71,18 @@ -- TODO: Think about doing the mode dispatch elsewhere, especially if over -- time more and more commands need to do the dispatch inside their code -- (currently only a couple do).-move :: Vector -> Action ()+move :: Vector -> ActionFrame () move dir = do pl <- gets splayer targeting <- gets (ctargeting . scursor)- if targeting /= TgtOff then moveCursor dir 1 else moveOrAttack True pl dir+ if targeting /= TgtOff+ then do+ frs <- moveCursor dir 1+ -- Mark that unexpectedly it does not take time.+ modify (\ s -> s {snoTime = True})+ return frs+ else+ inFrame $ moveOrAttack True pl dir ifRunning :: ((Vector, Int) -> Action a) -> Action a -> Action a ifRunning t e = do@@ -123,43 +112,41 @@ -- | Player tries to trigger a tile using a feature. bumpTile :: Point -> F.Feature -> Action () bumpTile dloc feat = do- cotile <- contentf Kind.cotile+ Kind.COps{cotile} <- getCOps lvl <- gets slevel let t = lvl `at` dloc if Tile.hasFeature cotile feat t then triggerTile dloc else guessBump cotile feat t- playerAdvanceTime -- | Perform the action specified for the tile in case it's triggered. triggerTile :: Point -> Action () triggerTile dloc = do- Kind.Ops{okind, opick} <- contentf Kind.cotile+ Kind.COps{cotile=Kind.Ops{okind, opick}} <- getCOps lvl <- gets slevel let f (F.Cause effect) = do pl <- gets splayer- (_b, _msg) <- effectToAction effect 0 pl pl 0+ void $ effectToAction effect 0 pl pl 0 return () f (F.ChangeTo group) = do- state <- get- let hms = levelHeroList state ++ levelMonsterList state+ Level{lactor} <- gets slevel case lvl `atI` dloc of- [] -> if unoccupied hms dloc+ [] -> if unoccupied (IM.elems lactor) dloc then do newTileId <- rndToAction $ opick group (const True) let adj = (Kind.// [(dloc, newTileId)]) modify (updateLevel (updateLMap adj))+-- TODO: take care of AI using this function (aborts, etc.). else abortWith "blocked" -- by monsters or heroes _ : _ -> abortWith "jammed" -- by items f _ = return () mapM_ f $ TileKind.tfeature $ okind $ lvl `at` dloc -- | Ask for a direction and trigger a tile, if possible.-playerTriggerDir :: F.Feature -> Action ()-playerTriggerDir feat = do- msgReset "direction?"- displayAll- e <- session nextCommand+playerTriggerDir :: F.Feature -> Verb -> Action ()+playerTriggerDir feat verb = do+ let keys = zip K.dirAllMoveKey $ repeat K.NoModifier+ e <- displayChoiceUI ("What to " ++ verb ++ "? [movement key") [] keys lxsize <- gets (lxsize . slevel) K.handleDir lxsize e (playerBumpDir feat) (neverMind True) @@ -177,13 +164,13 @@ ploc <- gets (bloc . getPlayerBody) bumpTile ploc feat --- | An actor opens a door. Player (hero or controlled monster) or enemy.+-- | An actor opens a door: player (hero or controlled monster) or enemy. actorOpenDoor :: ActorId -> Vector -> Action () actorOpenDoor actor dir = do Kind.COps{ cotile , coitem , coactor=Kind.Ops{okind}- } <- contentOps+ } <- getCOps lvl <- gets slevel pl <- gets splayer body <- gets (getActor actor)@@ -193,9 +180,9 @@ isPlayer = actor == pl isVerbose = isPlayer -- don't report, unless it's player-controlled iq = aiq $ okind $ bkind body- openPower = Tile.SecretStrength $+ openPower = timeScale timeTurn $ if isPlayer- then 1 -- player can't open hidden doors+ then 0 -- player can't open hidden doors else case strongestSearch coitem bitems of Just i -> iq + jpower i Nothing -> iq@@ -207,13 +194,12 @@ Tile.hasFeature cotile F.Hidden t) then neverMind isVerbose -- not doors at all else triggerTile dloc- advanceTime actor -- | Change the displayed level in targeting mode to (at most) -- k levels shallower. Enters targeting mode, if not already in one.-tgtAscend :: Int -> Action ()+tgtAscend :: Int -> ActionFrame () tgtAscend k = do- cotile <- contentf Kind.cotile+ Kind.COps{cotile} <- getCOps cursor <- gets scursor targeting <- gets (ctargeting . scursor) slid <- gets slid@@ -231,7 +217,11 @@ abortWith "no more levels in this direction" Just (nln, nloc) -> assert (nln /= slid `blame` (nln, "stairs looped")) $ do- modify (\ state -> state {slid = nln})+ -- We only look at the level, but we have to keep current+ -- time somewhere, e.g., for when we change the player+ -- to a hero on this level and then end targeting.+ -- If that's too slow, we could keep current time in the @Cursor@.+ switchLevel nln -- do not freely reveal the other end of the stairs lvl2 <- gets slevel let upd cur =@@ -246,7 +236,7 @@ depth = Dungeon.depth dungeon nln = Dungeon.levelDefault $ min depth $ max 1 $ n - k when (nln == slid) $ abortWith "no more levels in this direction"- modify (\ state -> state {slid = nln})+ switchLevel nln -- see comment above let upd cur = cur {clocLn = nln} modify (updateCursor upd) when (targeting == TgtOff) $ do@@ -255,36 +245,40 @@ doLook -- | Switches current hero to the next hero on the level, if any, wrapping.+-- We cycle through at most 10 heroes (\@, 0--9). cycleHero :: Action () cycleHero = do pl <- gets splayer- hs <- gets (lheroes . slevel)- let i = case pl of AHero n -> n ; _ -> -1- (lt, gt) = IM.split i hs- case IM.keys gt ++ IM.keys lt of+ s <- get+ let hs = map (tryFindHeroK s) [0..9]+ i = fromMaybe (-1) $ L.findIndex (== Just pl) hs+ (lt, gt) = (take i hs, drop (i + 1) hs)+ case L.filter (flip memActor s) $ catMaybes gt ++ catMaybes lt of [] -> abortWith "Cannot select any other hero on this level."- ni : _ -> selectPlayer (AHero ni)- >>= assert `trueM` (pl, ni, "hero duplicated")+ ni : _ -> selectPlayer ni+ >>= assert `trueM` (pl, ni, "hero duplicated") -- | Search for hidden doors. search :: Action () search = do- Kind.COps{coitem, cotile} <- contentOps+ Kind.COps{coitem, cotile} <- getCOps lvl <- gets slevel le <- gets (lsecret . slevel) lxsize <- gets (lxsize . slevel) ploc <- gets (bloc . getPlayerBody) pitems <- gets getPlayerItem- let delta = case strongestSearch coitem pitems of- Just i -> 1 + jpower i- Nothing -> 1+ let delta = timeScale timeTurn $+ case strongestSearch coitem pitems of+ Just i -> 1 + jpower i+ Nothing -> 1 searchTile sle mv = let loc = shift ploc mv t = lvl `at` loc- k = Tile.secretStrength (le IM.! loc) - delta+ -- TODO: assert or cope elsewhere with the IM.! below+ k = timeAdd (le IM.! loc) $ timeNegate delta in if Tile.hasFeature cotile F.Hidden t- then if k > 0- then IM.insert loc (Tile.SecretStrength k) sle+ then if k > timeZero+ then IM.insert loc k sle else IM.delete loc sle else sle leNew = L.foldl' searchTile le (moves lxsize)@@ -296,7 +290,6 @@ when (Tile.hasFeature cotile F.Hidden t && IM.notMember dloc leNew) $ triggerTile dloc mapM_ triggerHidden (moves lxsize)- playerAdvanceTime -- | This function performs a move (or attack) by any actor, -- i.e., it can handle monsters, heroes and both.@@ -306,7 +299,7 @@ -> Action () moveOrAttack allowAttacks actor dir = do -- We start by looking at the target position.- cops@Kind.COps{cotile = cotile@Kind.Ops{okind}} <- contentOps+ cops@Kind.COps{cotile = cotile@Kind.Ops{okind}} <- getCOps state <- get pl <- gets splayer lvl <- gets slevel@@ -321,22 +314,22 @@ actorAttackActor actor target | accessible cops lvl sloc tloc -> do -- Switching positions requires full access.- actorRunActor actor target when (actor == pl) $ msgAdd $ lookAt cops False True state lvl tloc ""- | otherwise -> abortWith ""+ actorRunActor actor target+ | otherwise -> abortWith "blocked" Nothing | accessible cops lvl sloc tloc -> do- -- perform the move+ -- Perform the move. updateAnyActor actor $ \ body -> body {bloc = tloc} when (actor == pl) $ msgAdd $ lookAt cops False True state lvl tloc ""- advanceTime actor | allowAttacks && actor == pl && Tile.canBeHidden cotile (okind $ lvl `rememberAt` tloc) -> do msgAdd "You search your surroundings." -- TODO: proper msg search- | otherwise -> actorOpenDoor actor dir -- try to open a door, TODO: playerBumpDir instead: TriggerDir { verb = "open", object = "door", feature = Openable }+ | otherwise ->+ actorOpenDoor actor dir -- try to open a door, TODO: bumpTile tloc F.Openable -- | Resolves the result of an actor moving into another. Usually this -- involves melee attack, but with two heroes it just changes focus.@@ -346,60 +339,90 @@ -- This function is analogous to projectGroupItem, but for melee -- and not using up the weapon. actorAttackActor :: ActorId -> ActorId -> Action ()-actorAttackActor source@(AHero _) target@(AHero _) =- -- Select adjacent hero by bumping into him. Takes no time.- selectPlayer target- >>= assert `trueM` (source, target, "player bumps into himself") actorAttackActor source target = do- Kind.COps{coactor, coitem=coitem@Kind.Ops{opick, okind}} <- contentOps- state <- get- sm <- gets (getActor source)- tm <- gets (getActor target)- per <- currentPerception- bitems <- gets (getActorItem source)- let h2hGroup = if isAHero source then "unarmed" else "monstrous"- h2hKind <- rndToAction $ opick h2hGroup (const True)- let sloc = bloc sm- -- The picked bodily "weapon".- h2h = Item h2hKind 0 Nothing 1- str = strongestSword coitem bitems- stack = fromMaybe h2h str- single = stack { jcount = 1 }- verb = iverbApply $ okind $ jkind single- -- The msg describes the source part of the action.- -- TODO: right now it also describes the victim and weapon;- -- perhaps, when a weapon is equipped, just say "you hit" or "you miss"- -- and then "nose dies" or "nose yells in pain".- msg = actorVerbActorExtra coactor sm verb tm $- if isJust str- then " with " ++ objectItem coitem state single- else ""- visible = sloc `IS.member` totalVisible per- when visible $ msgAdd msg- -- Msgs inside itemEffectAction describe the target part.- itemEffectAction 0 source target single- advanceTime source+ sm <- gets (getActor source)+ tm <- gets (getActor target)+ if bparty sm == heroParty && bparty tm == heroParty+ then do+ -- Select adjacent hero by bumping into him. Takes no time, so rewind.+ selectPlayer target+ >>= assert `trueM` (source, target, "player bumps into himself")+ -- Mark that unexpectedly it does not take time.+ modify (\ s -> s {snoTime = True})+ else do+ cops@Kind.COps{coactor, coitem=coitem@Kind.Ops{opick, okind}} <- getCOps+ state <- get+ per <- getPerception+ bitems <- gets (getActorItem source)+ let h2hGroup = if isAHero state source then "unarmed" else "monstrous"+ h2hKind <- rndToAction $ opick h2hGroup (const True)+ let h2hItem = Item h2hKind 0 Nothing 1+ sloc = bloc sm+ (stack, tell, verbosity, verb) =+ if bparty sm `elem` allProjectiles+ then assert (length bitems == 1) $+ (head bitems, False, 10, "hit") -- projectile+ else case strongestSword cops bitems of+ Nothing -> (h2hItem, False, 0,+ iverbApply $ okind $ h2hKind) -- hand-to-hand+ Just w -> (w, True, 0,+ iverbApply $ okind $ jkind w) -- weapon+ single = stack { jcount = 1 }+ -- The msg describes the source part of the action.+ -- TODO: right now it also describes the victim and weapon;+ -- perhaps, when a weapon is equipped, just say "you hit"+ -- or "you miss" and then "nose dies" or "nose yells in pain".+ msg = actorVerbActor coactor sm verb tm $+ if tell+ then "with " ++ objectItem coitem state single+ else ""+ visible = sloc `IS.member` totalVisible per+ when visible $ msgAdd msg+ -- Msgs inside itemEffectAction describe the target part.+ itemEffectAction verbosity source target single -- | Resolves the result of an actor running (not walking) into another. -- This involves switching positions of the two actors. actorRunActor :: ActorId -> ActorId -> Action () actorRunActor source target = do- pl <- gets splayer- sloc <- gets (bloc . getActor source) -- source location- tloc <- gets (bloc . getActor target) -- target location+ pl <- gets splayer+ sm <- gets (getActor source)+ tm <- gets (getActor target)+ let sloc = bloc sm+ tloc = bloc tm updateAnyActor source $ \ m -> m { bloc = tloc } updateAnyActor target $ \ m -> m { bloc = sloc }+ cops@Kind.COps{coactor} <- getCOps+ per <- getPerception+ let visible = sloc `IS.member` totalVisible per ||+ tloc `IS.member` totalVisible per+ msg = actorVerbActor coactor sm "displace" tm ""+ when visible $ msgAdd msg+ diary <- getDiary -- here diary possibly contains the new msg+ s <- get+ let locs = [tloc, sloc]+ anim = map (IM.fromList . zip locs)+ [ [Color.AttrChar (Color.Attr Color.BrMagenta Color.defBG) '.',+ Color.AttrChar (Color.Attr Color.Magenta Color.defBG) 'o']+ , [Color.AttrChar (Color.Attr Color.BrMagenta Color.defBG) 'd',+ Color.AttrChar (Color.Attr Color.Magenta Color.defBG) 'p']+ , [Color.AttrChar (Color.Attr Color.Magenta Color.defBG) 'p',+ Color.AttrChar (Color.Attr Color.BrMagenta Color.defBG) 'd']+ , [Color.AttrChar (Color.Attr Color.Magenta Color.defBG) 'o']+ , []+ ]+ animFrs = animate s diary cops per anim+ when visible $ mapM_ displayFramePush $ Nothing : animFrs if source == pl- then stopRunning -- do not switch positions repeatedly- else when (isAMonster source) $ focusIfAHero target- advanceTime source+ then stopRunning -- do not switch positions repeatedly+ else void $ focusIfOurs target -- | Create a new monster in the level, at a random position. rollMonster :: Kind.COps -> Perception -> State -> Rnd State rollMonster Kind.COps{cotile, coactor=Kind.Ops{opick, okind}} per state = do- let lvl = slevel state- hs = levelHeroList state- ms = levelMonsterList state+ let lvl@Level{lactor} = slevel state+ ms = hostileList state+ hs = heroList state isLit = Tile.isLit cotile rc <- monsterGenChance (Dungeon.levelNumber $ slid state) (L.length ms) if not rc@@ -417,7 +440,7 @@ , \ l _ -> not $ l `IS.member` totalVisible per , distantAtLeast 5 , \ l t -> Tile.hasFeature cotile F.Walkable t- && l `notElem` L.map bloc (hs ++ ms)+ && unoccupied (IM.elems lactor) l ] mk <- opick "monster" (const True) hp <- rollDice $ ahp $ okind mk@@ -426,9 +449,9 @@ -- | Generate a monster, possibly. generateMonster :: Action () generateMonster = do- cops <- contentOps+ cops <- getCOps state <- get- per <- currentPerception+ per <- getPerception nstate <- rndToAction $ rollMonster cops per state srandom <- gets srandom put $ nstate {srandom}@@ -438,17 +461,17 @@ regenerateLevelHP = do Kind.COps{ coitem , coactor=coactor@Kind.Ops{okind}- } <- contentOps+ } <- getCOps time <- gets stime let upd itemIM a m = let ak = okind $ bkind m bitems = fromMaybe [] $ IM.lookup a itemIM- regen = max 10 $+ regen = max 1 $ aregen ak `div` case strongestRegen coitem bitems of Just i -> 5 * jpower i Nothing -> 1- in if time `mod` regen /= 0+ in if (time `timeFit` timeTurn) `mod` regen /= 0 then m else addHp coactor 1 m -- We really want hero selection to be a purely UI distinction,@@ -456,7 +479,43 @@ -- Only the heroes on the current level regenerate (others are frozen -- in time together with their level). This prevents cheating -- via sending one hero to a safe level and waiting there.- hi <- gets (lheroItem . slevel)- modify (updateLevel (updateHeroes (IM.mapWithKey (upd hi))))- mi <- gets (lmonItem . slevel)- modify (updateLevel (updateMonsters (IM.mapWithKey (upd mi))))+ hi <- gets (linv . slevel)+ modify (updateLevel (updateActorDict (IM.mapWithKey (upd hi))))++-- | Display command help.+displayHelp :: ActionFrame ()+displayHelp = do+ keyb <- getBinding+ displayOverlays "Basic keys. [press SPACE or ESC]" $ keyHelp keyb++displayHistory :: ActionFrame ()+displayHistory = do+ Diary{shistory} <- getDiary+ time <- gets stime+ lysize <- gets (lysize . slevel)+ let turn = show $ time `timeFit` timeTurn+ msg = "Your adventuring lasts " ++ turn+ ++ " half-second turns. Past messages:"+ displayOverlays msg $ splitOverlay lysize $ renderHistory shistory++dumpConfig :: Action ()+dumpConfig = do+ config <- gets sconfig+ let fn = "config.dump"+ msg = "Current configuration dumped to file " ++ fn ++ "."+ dumpCfg fn config+ abortWith msg++redraw :: Action ()+redraw = return ()++-- | Add new smell traces to the level. Only humans leave a strong scent.+addSmell :: Action ()+addSmell = do+ s <- get+ pl <- gets splayer+ let time = stime s+ ploc = bloc (getPlayerBody s)+ upd = IM.insert ploc $ timeAdd time $ smellTimeout s+ when (isAHero s pl) $+ modify $ updateLevel $ updateSmell upd
Game/LambdaHack/Actor.hs view
@@ -2,10 +2,13 @@ -- involves the 'State' or 'Action' type. module Game.LambdaHack.Actor ( -- * Actor identifiers and related operations- ActorId(..), isAHero, isAMonster, invalidActorId- , findHeroName, monsterGenChance+ ActorId, findHeroName, monsterGenChance+ -- * Party identifiers+ , PartyId, heroParty, enemyParty, animalParty+ , heroProjectiles, enemyProjectiles, animalProjectiles, allProjectiles -- * The@ Acto@r type , Actor(..), template, addHp, unoccupied, heroKindId+ , projectileKindId, actorSpeed -- * Type of na actor target , Target(..) ) where@@ -16,14 +19,39 @@ import Data.Ratio import Game.LambdaHack.Utils.Assert-import Game.LambdaHack.Misc import Game.LambdaHack.Vector import Game.LambdaHack.Point import Game.LambdaHack.Content.ActorKind import qualified Game.LambdaHack.Kind as Kind import Game.LambdaHack.Random import qualified Game.LambdaHack.Config as Config+import Game.LambdaHack.Time+import qualified Game.LambdaHack.Color as Color +-- | The type of party identifiers.+newtype PartyId = PartyId Int+ deriving (Show, Eq, Ord)++-- | All supported party identifiers. Animals and projectiles move every turn.+-- Projectiles don't recognize friends and foes, animals turn friedly+-- or hostile, depending on various factors.+heroParty, enemyParty, animalParty,+ heroProjectiles, enemyProjectiles, animalProjectiles :: PartyId+heroParty = PartyId 0+enemyParty = PartyId 1+animalParty = PartyId 2+heroProjectiles = PartyId 3+enemyProjectiles = PartyId 4+animalProjectiles = PartyId 5++-- | The list of parties that represent projectiles.+allProjectiles :: [PartyId]+allProjectiles = [heroProjectiles, enemyProjectiles, animalProjectiles]++instance Binary PartyId where+ put (PartyId n) = put n+ get = fmap PartyId get+ -- | Actor properties that are changing throughout the game. -- If they are dublets of properties from @ActorKind@, -- they are usually modified temporarily, but tend to return@@ -32,67 +60,51 @@ { bkind :: !(Kind.Id ActorKind) -- ^ the kind of the actor , bsymbol :: !(Maybe Char) -- ^ individual map symbol , bname :: !(Maybe String) -- ^ individual name+ , bcolor :: !(Maybe Color.Color) -- ^ individual map color+ , bspeed :: !(Maybe Speed) -- ^ individual speed , bhp :: !Int -- ^ current hit points , bdir :: !(Maybe (Vector, Int)) -- ^ direction and distance of running , btarget :: Target -- ^ target for ranged attacks and AI , bloc :: !Point -- ^ current location , bletter :: !Char -- ^ next inventory letter- , btime :: !Time -- ^ time of next action+ , btime :: !Time -- ^ absolute time of next action+ , bparty :: !PartyId -- ^ to which party the actor belongs } deriving Show instance Binary Actor where- put (Actor ak an as ah ad at al ale ati) = do- put ak- put an- put as- put ah- put ad- put at- put al- put ale- put ati+ put Actor{..} = do+ put bkind+ put bsymbol+ put bname+ put bcolor+ put bspeed+ put bhp+ put bdir+ put btarget+ put bloc+ put bletter+ put btime+ put bparty get = do- ak <- get- an <- get- as <- get- ah <- get- ad <- get- at <- get- al <- get- ale <- get- ati <- get- return (Actor ak an as ah ad at al ale ati)+ bkind <- get+ bsymbol <- get+ bname <- get+ bcolor <- get+ bspeed <- get+ bhp <- get+ bdir <- get+ btarget <- get+ bloc <- get+ bletter <- get+ btime <- get+ bparty <- get+ return Actor{..} -- ActorId operations -- | A unique identifier of an actor in a dungeon.-data ActorId = AHero !Int -- ^ hero index (on the lheroes intmap)- | AMonster !Int -- ^ monster index (on the lmonsters intmap)- deriving (Show, Eq, Ord)--instance Binary ActorId where- put (AHero n) = putWord8 0 >> put n- put (AMonster n) = putWord8 1 >> put n- get = do- tag <- getWord8- case tag of- 0 -> liftM AHero get- 1 -> liftM AMonster get- _ -> fail "no parse (ActorId)"---- | Checks whether an actor identifier represents a hero.-isAHero :: ActorId -> Bool-isAHero (AHero _) = True-isAHero (AMonster _) = False---- | Checks whether an actor identifier represents a monster.-isAMonster :: ActorId -> Bool-isAMonster = not . isAHero---- | An actor that is not on any level.-invalidActorId :: ActorId-invalidActorId = AMonster (-1)+type ActorId = Int -- | Find a hero name in the config file, or create a stock name. findHeroName :: Config.CP -> Int -> String@@ -107,8 +119,8 @@ -- which monster is generated. How many and which monsters are generated -- will also depend on the cave kind used to build the level. monsterGenChance :: Int -> Int -> Rnd Bool-monsterGenChance d numMonsters =- chance $ 1%(fromIntegral (250 + 200 * (numMonsters - d)) `max` 50)+monsterGenChance depth numMonsters =+ chance $ 1%(fromIntegral (25 + 20 * (numMonsters - depth)) `max` 5) -- Actor operations @@ -119,10 +131,14 @@ -- | A template for a new actor. The initial target is invalid -- to force a reset ASAP. template :: Kind.Id ActorKind -> Maybe Char -> Maybe String -> Int -> Point- -> Actor-template mk mc ms hp loc =- let invalidTarget = TEnemy invalidActorId loc- in Actor mk mc ms hp Nothing invalidTarget loc 'a' 0+ -> Time -> PartyId -> Actor+template bkind bsymbol bname bhp bloc btime bparty =+ let bcolor = Nothing+ bspeed = Nothing+ btarget = invalidTarget+ bdir = Nothing+ bletter = 'a'+ in Actor{..} -- | Increment current hit points of an actor. addHp :: Kind.Ops ActorKind -> Int -> Actor -> Actor@@ -144,23 +160,42 @@ heroKindId :: Kind.Ops ActorKind -> Kind.Id ActorKind heroKindId Kind.Ops{ouniqGroup} = ouniqGroup "hero" +-- | The unique kind of projectiles.+projectileKindId :: Kind.Ops ActorKind -> Kind.Id ActorKind+projectileKindId Kind.Ops{ouniqGroup} = ouniqGroup "projectile"++-- | Access actor speed, individual or, otherwise, stock.+actorSpeed :: Kind.Ops ActorKind -> Actor -> Speed+actorSpeed Kind.Ops{okind} m =+ let stockSpeed = aspeed $ okind $ bkind m+ in fromMaybe stockSpeed $ bspeed m+ -- Target -- | The type of na actor target. data Target = TEnemy ActorId Point -- ^ target an actor with its last seen location | TLoc Point -- ^ target a given location+ | TPath [Vector] -- ^ target the list of locations one after another | TCursor -- ^ target current position of the cursor; default deriving (Show, Eq) +-- | An invalid target, with an actor that is not on any level.+invalidTarget :: Target+invalidTarget =+ let invalidActorId = -1+ in TEnemy invalidActorId origin+ instance Binary Target where put (TEnemy a ll) = putWord8 0 >> put a >> put ll put (TLoc loc) = putWord8 1 >> put loc- put TCursor = putWord8 2+ put (TPath ls) = putWord8 2 >> put ls+ put TCursor = putWord8 3 get = do tag <- getWord8 case tag of 0 -> liftM2 TEnemy get get 1 -> liftM TLoc get- 2 -> return TCursor+ 2 -> liftM TPath get+ 3 -> return TCursor _ -> fail "no parse (Target)"
Game/LambdaHack/ActorState.hs view
@@ -12,10 +12,12 @@ import Game.LambdaHack.Utils.Assert import Game.LambdaHack.Point+import Game.LambdaHack.Vector import Game.LambdaHack.Actor import Game.LambdaHack.Level import Game.LambdaHack.Dungeon import Game.LambdaHack.State+import Game.LambdaHack.Grammar import Game.LambdaHack.Item import Game.LambdaHack.Content.ActorKind import Game.LambdaHack.Content.TileKind@@ -24,31 +26,51 @@ import qualified Game.LambdaHack.Tile as Tile import qualified Game.LambdaHack.Kind as Kind import qualified Game.LambdaHack.Feature as F+import Game.LambdaHack.Time --- The operations with "Any", and those that use them, consider all the dungeon.+-- TODO: currently it's false for player-controlled monsters.+-- When it's no longer, rewrite the places where it matters.+-- | Checks whether an actor identifier represents a hero.+isAHero :: State -> ActorId -> Bool+isAHero s a =+ let (_, actor, _) = findActorAnyLevel a s+ in bparty actor == heroParty++-- | Checks whether an actor identifier represents a monster.+isAMonster :: State -> ActorId -> Bool+isAMonster s a =+ let (_, actor, _) = findActorAnyLevel a s+ in bparty actor == enemyParty++-- TODO: move to TileState if ever created.+-- | How long until an actor's smell vanishes from a tile.+smellTimeout :: State -> Time+smellTimeout s =+ let smellTurns = Config.get (sconfig s) "monsters" "smellTimeout"+ in timeScale timeTurn smellTurns++-- The operations with "Any", and those that use them,+-- consider all the dungeon. -- All the other actor and level operations only consider the current level. -- | Finds an actor body on any level. Fails if not found. findActorAnyLevel :: ActorId -> State -> (LevelId, Actor, [Item])-findActorAnyLevel actor state@State{slid, sdungeon} =- assert (not (absentHero actor state) `blame` actor) $+findActorAnyLevel actor State{slid, sdungeon} = let chk (ln, lvl) =- let (m, mi) = case actor of- AHero n -> (IM.lookup n (lheroes lvl),- IM.lookup n (lheroItem lvl))- AMonster n -> (IM.lookup n (lmonsters lvl),- IM.lookup n (lmonItem lvl))+ let (m, mi) = (IM.lookup actor (lactor lvl),+ IM.lookup actor (linv lvl)) in fmap (\ a -> (ln, a, fromMaybe [] mi)) m in case mapMaybe chk (currentFirst slid sdungeon) of [] -> assert `failure` actor res : _ -> res -- checking if res is unique would break laziness --- | Checks whether an actor is a hero, but not a member of the party.-absentHero :: ActorId -> State -> Bool-absentHero a State{sparty} =- case a of- AHero n -> IS.notMember n sparty- AMonster _ -> False+-- | Tries to finds an actor body satisfying a predicate on any level.+tryFindActor :: State -> (Actor -> Bool) -> Maybe (ActorId, Actor)+tryFindActor State{slid, sdungeon} p =+ let chk (_ln, lvl) = L.find (p . snd) $ IM.assocs $ lactor lvl+ in case mapMaybe chk (currentFirst slid sdungeon) of+ [] -> Nothing+ res : _ -> Just res getPlayerBody :: State -> Actor getPlayerBody s@State{splayer} =@@ -61,27 +83,22 @@ in items -- | The list of actors and their levels for all heroes in the dungeon.-allHeroesAnyLevel :: State -> [(ActorId, LevelId)]+allHeroesAnyLevel :: State -> [ActorId] allHeroesAnyLevel State{slid, sdungeon} =- let one (ln, Level{lheroes}) =- L.map (\ (i, _) -> (AHero i, ln)) (IM.assocs lheroes)+ let one (_, lvl) = L.map fst (heroAssocs lvl) in L.concatMap one (currentFirst slid sdungeon) updateAnyActorBody :: ActorId -> (Actor -> Actor) -> State -> State updateAnyActorBody actor f state = let (ln, _, _) = findActorAnyLevel actor state- in case actor of- AHero n -> updateAnyLevel (updateHeroes $ IM.adjust f n) ln state- AMonster n -> updateAnyLevel (updateMonsters $ IM.adjust f n) ln state+ in updateAnyLevel (updateActorDict $ IM.adjust f actor) ln state updateAnyActorItem :: ActorId -> ([Item] -> [Item]) -> State -> State updateAnyActorItem actor f state = let (ln, _, _) = findActorAnyLevel actor state g Nothing = Just $ f [] g (Just is) = Just $ f is- in case actor of- AHero n -> updateAnyLevel (updateHeroItem $ IM.alter g n) ln state- AMonster n -> updateAnyLevel (updateMonItem $ IM.alter g n) ln state+ in updateAnyLevel (updateInv $ IM.alter g actor) ln state updateAnyLevel :: (Level -> Level) -> LevelId -> State -> State updateAnyLevel f ln s@State{slid, sdungeon}@@ -89,11 +106,13 @@ | otherwise = updateDungeon (const $ adjust f ln sdungeon) s -- | Calculate the location of player's target.-targetToLoc :: IS.IntSet -> State -> Maybe Point-targetToLoc visible s@State{slid, scursor} =+targetToLoc :: IS.IntSet -> State -> Point -> Maybe Point+targetToLoc visible s@State{slid, scursor} aloc = case btarget (getPlayerBody s) of TLoc loc -> Just loc- TCursor ->+ TPath [] -> Nothing+ TPath (dir:_) -> Just $ shift aloc dir+ TCursor -> if slid == clocLn scursor then Just $ clocation scursor else Nothing -- cursor invalid: set at a different level@@ -106,55 +125,70 @@ -- The operations below disregard levels other than the current. -- | Checks if the actor is present on the current level.+-- The order of argument here and in other functions is set to allow+--+-- > b <- gets (memActor a) memActor :: ActorId -> State -> Bool-memActor a state =- case a of- AHero n -> IM.member n (lheroes (slevel state))- AMonster n -> IM.member n (lmonsters (slevel state))+memActor a state = IM.member a (lactor (slevel state)) -- | Gets actor body from the current level. Error if not found. getActor :: ActorId -> State -> Actor-getActor a state =- case a of- AHero n -> lheroes (slevel state) IM.! n- AMonster n -> lmonsters (slevel state) IM.! n+getActor a state = lactor (slevel state) IM.! a -- | Gets actor's items from the current level. Empty list, if not found. getActorItem :: ActorId -> State -> [Item]-getActorItem a state =- fromMaybe [] $- case a of- AHero n -> IM.lookup n (lheroItem (slevel state))- AMonster n -> IM.lookup n (lmonItem (slevel state))+getActorItem a state = fromMaybe [] $ IM.lookup a (linv (slevel state)) -- | Removes the actor, if present, from the current level. deleteActor :: ActorId -> State -> State deleteActor a =- case a of- AHero n ->- updateLevel (updateHeroes (IM.delete n) . updateHeroItem (IM.delete n))- AMonster n ->- updateLevel (updateMonsters (IM.delete n) . updateMonItem (IM.delete n))+ updateLevel (updateActorDict (IM.delete a) . updateInv (IM.delete a)) -- | Add actor to the current level. insertActor :: ActorId -> Actor -> State -> State-insertActor a m =- case a of- AHero n -> updateLevel (updateHeroes (IM.insert n m))- AMonster n -> updateLevel (updateMonsters (IM.insert n m))+insertActor a m = updateLevel (updateActorDict (IM.insert a m)) --- | Removes a player from the current level and party list.+-- | Removes a player from the current level. deletePlayer :: State -> State-deletePlayer s@State{splayer, sparty} =- let s2 = deleteActor splayer s- in case splayer of- AHero n -> s2{sparty = IS.delete n sparty}- AMonster _ -> s2+deletePlayer s@State{splayer} = deleteActor splayer s -levelHeroList, levelMonsterList :: State -> [Actor]-levelHeroList state = IM.elems $ lheroes $ slevel state-levelMonsterList state = IM.elems $ lmonsters $ slevel state+-- TODO: unify+heroAssocs, hostileAssocs, dangerousAssocs, friendlyAssocs, allButHeroesAssocs+ :: Level -> [(ActorId, Actor)]+heroAssocs lvl =+ filter (\ (_, m) -> bparty m == heroParty) $ IM.toList $ lactor lvl+hostileAssocs lvl =+ filter (\ (_, m) -> bparty m `elem` [enemyParty, animalParty]) $+ IM.toList $ lactor lvl+dangerousAssocs lvl =+ filter (\ (_, m) -> bparty m `elem`+ [enemyParty, animalParty,+ enemyProjectiles, animalProjectiles]) $+ IM.toList $ lactor lvl+friendlyAssocs lvl =+ filter (\ (_, m) -> bparty m `elem` [heroParty, heroProjectiles]) $+ IM.toList $ lactor lvl+allButHeroesAssocs lvl =+ filter (\ (_, m) -> bparty m `elem`+ [heroProjectiles, enemyParty, animalParty,+ enemyProjectiles, animalProjectiles]) $+ IM.toList $ lactor lvl +heroList, hostileList, dangerousList, friendlyList :: State -> [Actor]+heroList state =+ filter (\ m -> bparty m == heroParty) $ IM.elems $ lactor $ slevel state+hostileList state =+ filter (\ m -> bparty m `elem` [enemyParty, animalParty]) $+ IM.elems $ lactor $ slevel state+dangerousList state =+ filter (\ m -> bparty m `elem`+ [enemyParty, animalParty,+ enemyProjectiles, animalProjectiles]) $+ IM.elems $ lactor $ slevel state+friendlyList state =+ filter (\ m -> bparty m `elem` [heroParty, heroProjectiles]) $+ IM.elems $ lactor $ slevel state+ -- | Finds an actor at a location on the current level. Perception irrelevant. locToActor :: Point -> State -> Maybe ActorId locToActor loc state =@@ -164,39 +198,50 @@ locToActors :: Point -> State -> [ActorId] locToActors loc state =- getIndex (lmonsters, AMonster) ++ getIndex (lheroes, AHero)- where- getIndex (projection, injection) =- let l = IM.assocs $ projection $ slevel state+ let l = IM.assocs $ lactor $ slevel state im = L.filter (\ (_i, m) -> bloc m == loc) l- in fmap (injection . fst) im+ in fmap fst im nearbyFreeLoc :: Kind.Ops TileKind -> Point -> State -> Point nearbyFreeLoc cotile start state =- let lvl@Level{lxsize, lysize} = slevel state- hs = levelHeroList state- ms = levelMonsterList state+ let lvl@Level{lxsize, lysize, lactor} = slevel state locs = start : L.nub (concatMap (vicinity lxsize lysize) locs) good loc = Tile.hasFeature cotile F.Walkable (lvl `at` loc)- && loc `notElem` L.map bloc (hs ++ ms)+ && unoccupied (IM.elems lactor) loc in fromMaybe (assert `failure` "too crowded map") $ L.find good locs +-- | Calculate loot's worth for heroes on the current level.+calculateTotal :: Kind.Ops ItemKind -> State -> ([Item], Int)+calculateTotal coitem s =+ let ha = heroAssocs $ slevel s+ heroInv = L.concat $ catMaybes $+ L.map ( \ (k, _) -> IM.lookup k $ linv $ slevel s) ha+ in (heroInv, L.sum $ L.map (itemPrice coitem) heroInv)+ -- Adding heroes +tryFindHeroK :: State -> Int -> Maybe ActorId+tryFindHeroK s k =+ let c | k == 0 = '@'+ | k > 0 && k < 10 = Char.intToDigit k+ | otherwise = assert `failure` k+ in fmap fst $ tryFindActor s ((== Just c) . bsymbol)+ -- | Create a new hero on the current level, close to the given location. addHero :: Kind.COps -> Point -> State -> State-addHero Kind.COps{coactor, cotile} ploc state =+addHero Kind.COps{coactor, cotile} ploc state@State{scounter} = let config = sconfig state bHP = Config.get config "heroes" "baseHP" loc = nearbyFreeLoc cotile ploc state- n = fst (scounter state)- symbol = if n < 1 || n > 9 then Nothing else Just $ Char.intToDigit n+ freeHeroK = L.elemIndex Nothing $ map (tryFindHeroK state) [0..9]+ n = fromMaybe 10 freeHeroK+ symbol = if n < 1 || n > 9 then '@' else Char.intToDigit n name = findHeroName config n- startHP = bHP `div` min 10 (n + 1)- m = template (heroKindId coactor) symbol (Just name) startHP loc- state' = state { scounter = (n + 1, snd (scounter state))- , sparty = IS.insert n (sparty state) }- in updateLevel (updateHeroes (IM.insert n m)) state'+ startHP = bHP `div` min 5 (n + 1)+ m = template (heroKindId coactor) (Just symbol) (Just name)+ startHP loc (stime state) heroParty+ cstate = state { scounter = scounter + 1 }+ in updateLevel (updateActorDict (IM.insert scounter m)) cstate -- | Create a set of initial heroes on the current level, at location ploc. initialHeroes :: Kind.COps -> Point -> State -> State@@ -210,13 +255,40 @@ -- and with a given actor kind and HP. addMonster :: Kind.Ops TileKind -> Kind.Id ActorKind -> Int -> Point -> State -> State-addMonster cotile mk hp ploc state@State{scounter = (heroC, monsterC)} = do+addMonster cotile mk hp ploc state@State{scounter} = do let loc = nearbyFreeLoc cotile ploc state- m = template mk Nothing Nothing hp loc- state' = state { scounter = (heroC, monsterC + 1) }- updateLevel (updateMonsters (IM.insert monsterC m)) state'+ m = template mk Nothing Nothing hp loc (stime state) enemyParty+ cstate = state {scounter = scounter + 1}+ updateLevel (updateActorDict (IM.insert scounter m)) cstate --- | Calculate loot's worth for heroes on the current level.-calculateTotal :: Kind.Ops ItemKind -> State -> Int-calculateTotal coitem s =- L.sum $ L.map (itemPrice coitem) $ L.concat $ IM.elems $ lheroItem $ slevel s+-- Adding projectiles++-- | Create a projectile actor containing the given missile.+addProjectile :: Kind.COps -> Item -> Point -> PartyId -> [Point] -> Time+ -> State -> State+addProjectile Kind.COps{coactor, coitem=coitem@Kind.Ops{okind}}+ item loc bparty path btime state@State{scounter} =+ let ik = okind (jkind item)+ object = objectItem coitem state item+ name = "a flying " ++ unwords (tail (words object))+ speed = speedFromWeight (iweight ik) (itoThrow ik)+ range = rangeFromSpeed speed+ dirPath = take range $ displacePath path+ m = Actor+ { bkind = projectileKindId coactor+ , bsymbol = Nothing+ , bname = Just name+ , bcolor = Nothing+ , bspeed = Just speed+ , bhp = 0+ , bdir = Nothing+ , btarget = TPath dirPath+ , bloc = loc+ , bletter = 'a'+ , btime+ , bparty+ }+ cstate = state { scounter = scounter + 1 }+ upd = updateActorDict (IM.insert scounter m)+ . updateInv (IM.insert scounter [item])+ in updateLevel upd cstate
Game/LambdaHack/Binding.hs view
@@ -11,13 +11,15 @@ import Game.LambdaHack.Utils.Assert import qualified Game.LambdaHack.Key as K+import Game.LambdaHack.Msg -- | Bindings and other information about player commands. data Binding a = Binding- { kcmd :: M.Map K.Key (String, a) -- ^ binding keys to commands- , kmacro :: M.Map K.Key K.Key -- ^ macro map- , kmajor :: [K.Key] -- ^ major, most often used, commands- , ktimed :: [K.Key] -- ^ commands that take time, except movement+ { kcmd :: M.Map (K.Key, K.Modifier) (String, Bool, a)+ -- ^ binding keys to commands+ , kmacro :: M.Map K.Key K.Key -- ^ macro map+ , kmajor :: [K.Key] -- ^ major, most often used, commands+ , kdir :: [(K.Key, K.Modifier)] -- ^ direction keys for moving and running } -- | Produce the macro map from a macro association list@@ -43,8 +45,8 @@ else k : [ from | (from, to) <- M.assocs kmacro, to == k ] -- | Produce a set of help screens from the key bindings.-keyHelp :: Binding a -> [String]-keyHelp Binding{kcmd, kmacro, kmajor, ktimed} =+keyHelp :: Binding a -> [Overlay]+keyHelp Binding{kcmd, kmacro, kmajor} = let movBlurb = [ "Move throughout the level with numerical keypad or"@@ -56,14 +58,13 @@ , " /|\\ /|\\" , " 1 2 3 b j n" , ""- , "Run ahead until anything disturbs you, with SHIFT and a key."+ ,"Run ahead until anything disturbs you, with SHIFT (or CTRL) and a key." , "Press keypad '5' or '.' to skip a turn." , "In targeting mode the same keys move the targeting cursor." , "" , "Search, open and attack, by bumping into walls, doors and monsters." , "" , "Press SPACE to see the next page, with the list of major commands."- , "" ] majorBlurb = [ ""@@ -76,19 +77,20 @@ , "Press SPACE to clear the messages and go back to the game." ] fmt k h = replicate 16 ' ' ++ k ++ replicate ((15 - length k) `max` 1) ' '- ++ h ++ replicate ((40 - length h) `max` 1) ' '- fmts s = replicate 1 ' ' ++ s ++ replicate ((70 - length s) `max` 1) ' '+ ++ h ++ replicate ((41 - length h) `max` 1) ' '+ fmts s = replicate 1 ' ' ++ s ++ replicate ((71 - length s) `max` 1) ' ' blank = fmt "" "" mov = map fmts movBlurb major = map fmts majorBlurb minor = map fmts minorBlurb keyCaption = fmt "keys" "command" disp k = L.concatMap show $ coImage kmacro k- ti k = if k `elem` ktimed then "*" else ""- keys l = [ fmt (disp k) (h ++ ti k) | (k, (h, _)) <- l, h /= "" ]- (kcMajor, kcMinor) = L.partition ((`elem` kmajor) . fst) (M.toAscList kcmd)+ keys l = [ fmt (disp k) (h ++ if timed then "*" else "")+ | ((k, _), (h, timed, _)) <- l, h /= "" ]+ (kcMajor, kcMinor) =+ L.partition ((`elem` kmajor) . fst . fst) (M.toAscList kcmd) in- L.map unlines [ [blank] ++ mov- , [blank] ++ [keyCaption] ++ keys kcMajor ++ major- , [blank] ++ [keyCaption] ++ keys kcMinor ++ minor- ]+ [ [blank] ++ mov+ , [blank] ++ [keyCaption] ++ keys kcMajor ++ major+ , [blank] ++ [keyCaption] ++ keys kcMinor ++ minor+ ]
Game/LambdaHack/BindingAction.hs view
@@ -18,7 +18,7 @@ import Game.LambdaHack.EffectAction import Game.LambdaHack.Binding import qualified Game.LambdaHack.Key as K-import Game.LambdaHack.Actor+import Game.LambdaHack.ActorState import Game.LambdaHack.Command configCmd :: Config.CP -> [(K.Key, Cmd)]@@ -33,21 +33,21 @@ in L.map mkCommand section semanticsCmd :: [(K.Key, Cmd)]- -> (Cmd -> Action ())+ -> (Cmd -> ActionFrame ()) -> (Cmd -> String)- -> [(K.Key, (String, Action ()))]+ -> [((K.Key, K.Modifier), (String, Bool, ActionFrame ()))] semanticsCmd cmdList cmdS cmdD = let mkDescribed cmd = let semantics = if timedCmd cmd then checkCursor $ cmdS cmd else cmdS cmd- in (cmdD cmd, semantics)- mkCommand (key, def) = (key, mkDescribed def)+ in (cmdD cmd, timedCmd cmd, semantics)+ mkCommand (key, def) = ((key, K.NoModifier), mkDescribed def) in L.map mkCommand cmdList -- | If in targeting mode, check if the current level is the same -- as player level and refuse performing the action otherwise.-checkCursor :: Action () -> Action ()+checkCursor :: ActionFrame () -> ActionFrame () checkCursor h = do cursor <- gets scursor slid <- gets slid@@ -55,18 +55,24 @@ then h else abortWith "this command does not work on remote levels" -heroSelection :: [(K.Key, (String, Action ()))]+heroSelection :: [((K.Key, K.Modifier), (String, Bool, ActionFrame ()))] heroSelection =- let heroSelect k = (K.Char (Char.intToDigit k),- ("", void $ selectPlayer $ AHero k))+ let select k = do+ s <- get+ case tryFindHeroK s k of+ Nothing -> abortWith "No such member of the party."+ Just aid -> selectPlayer aid >> returnNoFrame ()+ heroSelect k = ( (K.Char (Char.intToDigit k), K.NoModifier)+ , ("", False, select k)+ ) in fmap heroSelect [0..9] -- | Binding of keys to movement and other standard commands, -- as well as commands defined in the config file.-stdBinding :: Config.CP -- ^ game config- -> (Cmd -> Action ()) -- ^ semantics of abstract commands- -> (Cmd -> String) -- ^ description of abstract commands- -> Binding (Action ()) -- ^ concrete binding+stdBinding :: Config.CP -- ^ game config+ -> (Cmd -> ActionFrame ()) -- ^ semantics of abstract commands+ -> (Cmd -> String) -- ^ description of abstract commands+ -> Binding (ActionFrame ()) -- ^ concrete binding stdBinding config cmdS cmdD = let section = Config.getItems config "macros" !kmacro = macroKey section@@ -78,17 +84,23 @@ runWidth f = do lxsize <- gets (lxsize . slevel) run (f lxsize, 0)+ -- Targeting cursor movement and others are wrongly marked as timed;+ -- fixed in their definitions by rewinding time.+ cmdDir = K.moveBinding moveWidth runWidth in Binding { kcmd = M.fromList $- K.moveBinding moveWidth runWidth +++ cmdDir ++ heroSelection ++ semList ++- [ -- debug commands, TODO:access them from a common menu or prefix- (K.Char 'R', ("", modify cycleMarkVision)),- (K.Char 'O', ("", modify toggleOmniscient)),- (K.Char 'I', ("", gets (lmeta . slevel) >>= abortWith))+ [ -- Debug commands.+ ((K.Char 'r', K.Control), ("", False, modify cycleMarkVision+ >> returnNoFrame ())),+ ((K.Char 'o', K.Control), ("", False, modify toggleOmniscient+ >> returnNoFrame ())),+ ((K.Char 'i', K.Control), ("", False, gets (lmeta . slevel)+ >>= abortWith)) ] , kmacro , kmajor = L.map fst $ L.filter (majorCmd . snd) cmdList- , ktimed = L.map fst $ L.filter (timedCmd . snd) cmdList+ , kdir = L.map fst cmdDir }
Game/LambdaHack/Cave.hs view
@@ -6,7 +6,9 @@ import Control.Monad import qualified Data.Map as M import qualified Data.List as L+import Data.Maybe +import Game.LambdaHack.Utils.Assert import Game.LambdaHack.PointXY import Game.LambdaHack.Area import Game.LambdaHack.AreaRnd@@ -20,15 +22,16 @@ import Game.LambdaHack.Place hiding (TileMapXY) import qualified Game.LambdaHack.Place as Place import Game.LambdaHack.Misc+import Game.LambdaHack.Time -- | The map of tile kinds in a cave. -- The map is sparse. The default tile that eventually fills the empty spaces--- is specified in the cave kind specification with @cdefTile@.+-- is specified in the cave kind specification with @cdefaultTile@. type TileMapXY = Place.TileMapXY -- | The map of starting secrecy strength of tiles in a cave. -- The map is sparse. Unspecified tiles have secrecy strength of 0.-type SecretMapXY = M.Map PointXY Tile.SecretStrength+type SecretMapXY = M.Map PointXY Tile.SecretTime -- | The map of starting items in tiles of a cave. The map is sparse. -- Unspecified tiles have no starting items.@@ -76,8 +79,8 @@ -> Rnd Cave buildCave cops@Kind.COps{ cotile=cotile@Kind.Ops{okind=tokind, opick} , cocave=Kind.Ops{okind} }- lvl depth ci = do- let CaveKind{..} = okind ci+ ln depth ci = do+ let kc@CaveKind{..} = okind ci lgrid@(gx, gy) <- rollDiceXY cgrid lminplace <- rollDiceXY cminPlaceSize let gs = grid lgrid (0, 0, cxsize - 1, cysize - 1)@@ -103,18 +106,17 @@ let r0 = places M.! p0 r1 = places M.! p1 connectPlaces r0 r1) allConnects- wallId <- opick "fillerWall" (const True)+ wallId <- opick cfillerTile (const True) let fenceBounds = (1, 1, cxsize - 2, cysize - 2) fence = buildFence wallId fenceBounds- pickedCorTile <- opick ccorTile (const True)+ pickedCorTile <- opick ccorridorTile (const True) let addPl (m, pls) (_, (x0, _, x1, _)) | x0 == x1 = return (m, pls) addPl (m, pls) (_, r) = do- (tmap, place) <-- buildPlace cops wallId pickedCorTile cdarkChance lvl depth r+ (tmap, place) <- buildPlace cops kc pickedCorTile ln depth r return (M.union tmap m, place : pls) (lplaces, dplaces) <- foldM addPl (fence, []) places0 let lcorridors = M.unions (L.map (digCorridors pickedCorTile) cs)- hiddenMap <- mapToHidden cotile+ hiddenMap <- mapToHidden cotile chiddenTile let lm = M.unionWith (mergeCorridor cotile hiddenMap) lcorridors lplaces -- Convert openings into doors, possibly. (dmap, secretMap) <-@@ -152,14 +154,14 @@ } return cave -rollSecret :: TileKind -> Rnd Tile.SecretStrength+rollSecret :: TileKind -> Rnd Tile.SecretTime rollSecret t = do let getDice (F.Secret dice) _ = dice getDice _ acc = acc defaultDice = RollDice 5 2 d = foldr getDice defaultDice (tfeature t)- secret <- rollDice d- return $ Tile.SecretStrength secret+ secretTurns <- rollDice d+ return $ timeScale timeTurn secretTurns trigger :: Kind.Ops TileKind -> Kind.Id TileKind -> Rnd (Kind.Id TileKind) trigger Kind.Ops{okind, opick} t =@@ -180,14 +182,14 @@ passable :: [F.Feature] passable = [F.Walkable, F.Openable, F.Hidden] -mapToHidden :: Kind.Ops TileKind+mapToHidden :: Kind.Ops TileKind -> String -> Rnd (M.Map (Kind.Id TileKind) (Kind.Id TileKind))-mapToHidden cotile@Kind.Ops{ofoldrWithKey, opick} =+mapToHidden cotile@Kind.Ops{ofoldrWithKey, opick} chiddenTile = let getHidden ti tk acc = if Tile.canBeHidden cotile tk then do- ti2 <- opick "hidden" $ \ k -> Tile.kindHasFeature F.Hidden k- && Tile.similar k tk+ ti2 <- opick chiddenTile $ \ k -> Tile.kindHasFeature F.Hidden k+ && Tile.similar k tk fmap (M.insert ti ti2) acc else acc in ofoldrWithKey getHidden (return M.empty)@@ -197,4 +199,6 @@ -> Kind.Id TileKind -> Kind.Id TileKind -> Kind.Id TileKind mergeCorridor cotile _ _ t | L.any (\ f -> Tile.hasFeature cotile f t) passable = t-mergeCorridor _ hiddenMap _ t = hiddenMap M.! t+mergeCorridor _ hiddenMap u t =+ fromMaybe (assert `failure` (u, hiddenMap, t)) $+ M.lookup t hiddenMap
Game/LambdaHack/Color.hs view
@@ -2,11 +2,12 @@ module Game.LambdaHack.Color ( -- * Colours Color(..), defBG, defFG, isBright, legalBG, colorToRGB- -- * Text attributes- , Attr(..), defaultAttr+ -- * Text attributes and the screen+ , Attr(..), defaultAttr, AttrChar(..), SingleFrame(..), Animation ) where -import qualified Data.Binary as Binary+import Data.Binary+import qualified Data.IntMap as IM -- TODO: since this type may be essential to speed, consider implementing -- it as an Int, with color numbered as they are on terminals, see@@ -33,9 +34,9 @@ | BrWhite deriving (Show, Eq, Ord, Enum, Bounded) -instance Binary.Binary Color where- put = Binary.putWord8 . toEnum . fromEnum- get = fmap (toEnum . fromEnum) Binary.getWord8+instance Binary Color where+ put = putWord8 . toEnum . fromEnum+ get = fmap (toEnum . fromEnum) getWord8 -- | The default colours, to optimize attribute setting. defBG, defFG :: Color@@ -49,9 +50,45 @@ } deriving (Show, Eq, Ord) +instance Binary Attr where+ put Attr{..} = do+ put fg+ put bg+ get = do+ fg <- get+ bg <- get+ return Attr{..}+ -- | The default attribute, to optimize attribute setting. defaultAttr :: Attr defaultAttr = Attr defFG defBG++data AttrChar = AttrChar+ { acAttr :: !Attr+ , acChar :: !Char+ }+ deriving (Show, Eq)++instance Binary AttrChar where+ put AttrChar{..} = do+ put acAttr+ put acChar+ get = do+ acAttr <- get+ acChar <- get+ return AttrChar{..}++-- | The data sufficent to draw a single game screen frame.+data SingleFrame = SingleFrame+ { sfLevel :: ![[AttrChar]] -- ^ content of the screen, line by line+ , sfTop :: String -- ^ an extra line to show at the top+ , sfBottom :: String -- ^ an extra line to show at the bottom+ }+ deriving Eq++-- | Animation is a list of frame modifications to play one by one,+-- where each modification if a map from locations to level map symbols.+type Animation = [IM.IntMap AttrChar] -- | A helper for the terminal frontends that display bright via bold. isBright :: Color -> Bool
Game/LambdaHack/Command.hs view
@@ -14,16 +14,20 @@ -- | Abstract syntax of player commands. The type is abstract, but the values -- are created outside this module via the Read class (from config file) . data Cmd =+ -- These take time: Apply { verb :: Verb, object :: Object, syms :: [Char] } | Project { verb :: Verb, object :: Object, syms :: [Char] } | TriggerDir { verb :: Verb, object :: Object, feature :: F.Feature } | TriggerTile { verb :: Verb, object :: Object, feature :: F.Feature } | Pickup | Drop+ | Wait+ -- These do not take time: | Inventory | TgtFloor | TgtEnemy | TgtAscend Int+ | EpsIncr Bool | GameSave | GameQuit | Cancel@@ -33,7 +37,6 @@ | HeroCycle | Version | Help- | Wait | Redraw deriving (Show, Read) @@ -52,9 +55,6 @@ Help -> True _ -> False --- TODO: Advance time automatically for these, but somehow advance--- time for monsters, too. Perhaps wait until monsters use commands, too--- (or rather the micro-commands to be added in the future). -- | Time cosuming commands are marked as such in help and cannot be -- invoked in targeting mode on a remote level (level different than -- the level of the selected hero).@@ -66,33 +66,37 @@ TriggerTile{} -> True Pickup -> True Drop -> True+ GameSave -> True+ GameQuit -> True Wait -> True _ -> False -- | The semantics of player commands in terms of the @Action@ monad.-cmdSemantics :: Cmd -> Action ()+cmdSemantics :: Cmd -> ActionFrame () cmdSemantics cmd = case cmd of- Apply{..} -> playerApplyGroupItem verb object syms+ Apply{..} -> inFrame $ playerApplyGroupItem verb object syms Project{..} -> playerProjectGroupItem verb object syms- TriggerDir{..} -> playerTriggerDir feature- TriggerTile{..} -> playerTriggerTile feature- Pickup -> pickupItem- Drop -> dropItem+ TriggerDir{..} -> inFrame $ playerTriggerDir feature verb+ TriggerTile{..} -> inFrame $ playerTriggerTile feature+ Pickup -> inFrame $ pickupItem+ Drop -> inFrame $ dropItem+ Wait -> inFrame $ return ()+ Inventory -> inventory TgtFloor -> targetFloor TgtExplicit TgtEnemy -> targetMonster TgtExplicit TgtAscend k -> tgtAscend k- GameSave -> saveGame- GameQuit -> quitGame- Cancel -> cancelCurrent+ EpsIncr b -> inFrame $ epsIncr b+ GameSave -> inFrame $ saveGame+ GameQuit -> inFrame $ quitGame+ Cancel -> inFrame $ cancelCurrent Accept -> acceptCurrent displayHelp History -> displayHistory- CfgDump -> dumpConfig- HeroCycle -> cycleHero- Version -> gameVersion+ CfgDump -> inFrame $ dumpConfig+ HeroCycle -> inFrame $ cycleHero+ Version -> inFrame $ gameVersion Help -> displayHelp- Wait -> playerAdvanceTime- Redraw -> return ()+ Redraw -> inFrame $ redraw -- | Description of player commands. cmdDescription :: Cmd -> String@@ -103,6 +107,8 @@ TriggerTile{..} -> verb ++ " " ++ addIndefinite object Pickup -> "get an object" Drop -> "drop an object"+ Wait -> ""+ Inventory -> "display inventory" TgtFloor -> "target location" TgtEnemy -> "target monster"@@ -111,6 +117,8 @@ TgtAscend k | k == -1 -> "target next deeper level" TgtAscend k | k <= -2 -> "target " ++ show (-k) ++ " levels deeper" TgtAscend _ -> error "void level change in targeting mode in config file"+ EpsIncr True -> "swerve targeting line"+ EpsIncr False -> "unswerve targeting line" GameSave -> "save and exit the game" GameQuit -> "quit without saving" Cancel -> "cancel action"@@ -120,5 +128,4 @@ HeroCycle -> "cycle among heroes on level" Version -> "display game version" Help -> "display help"- Wait -> "" Redraw -> "clear messages"
Game/LambdaHack/Content/ActorKind.hs view
@@ -9,19 +9,22 @@ import Game.LambdaHack.Color import qualified Game.LambdaHack.Random as Random import Game.LambdaHack.Misc+import Game.LambdaHack.Time +-- TODO: make all but a few fields optional in some way, so that, a.g.,+-- a game content with no regeneration does not ever need to mention aregen. -- | Actor properties that are fixed for a given kind of actors. data ActorKind = ActorKind { asymbol :: !Char -- ^ map symbol , aname :: !String -- ^ short description , afreq :: !Freqs -- ^ frequency within groups , acolor :: !Color -- ^ map color- , aspeed :: !Time -- ^ natural speed+ , aspeed :: !Speed -- ^ natural speed in m/s , ahp :: !Random.RollDice -- ^ encodes initial and maximal hp , asight :: !Bool -- ^ can it see? , asmell :: !Bool -- ^ can it smell? , aiq :: !Int -- ^ intelligence- , aregen :: !Int -- ^ regeneration interval+ , aregen :: !Int -- ^ number of turns to regenerate 1 HP } deriving Show -- No Eq and Ord to make extending it logically sound, see #53
Game/LambdaHack/Content/CaveKind.hs view
@@ -27,8 +27,12 @@ , copenChance :: Chance -- ^ if there's a door, is it open? , chiddenChance :: Chance -- ^ if not open, is it hidden? , citemNum :: RollDice -- ^ the number of items in the cave- , cdefTile :: String -- ^ the default cave tile group name- , ccorTile :: String -- ^ the cave corridor tile group name+ , cdefaultTile :: String -- ^ the default cave tile group name+ , ccorridorTile :: String -- ^ the cave corridor tile group name+ , cfillerTile :: String -- ^ the filler wall group name+ , cdarkLegendTile :: String -- ^ the dark place plan legend ground name+ , clitLegendTile :: String -- ^ the lit place plan legend ground name+ , chiddenTile :: String -- ^ the hidden tiles ground name } deriving Show -- No Eq and Ord to make extending it logically sound, see #53
Game/LambdaHack/Content/ItemKind.hs view
@@ -27,6 +27,8 @@ , ipower :: !RollDeep -- ^ created with that power , iverbApply :: !String -- ^ the verb for applying and possibly combat , iverbProject :: !String -- ^ the verb for projecting+ , iweight :: !Int -- ^ weight in grams+ , itoThrow :: !Int -- ^ percentage bonus or malus to throw speed } deriving Show -- No Eq and Ord to make extending it logically sound, see #53
Game/LambdaHack/Content/RuleKind.hs view
@@ -33,6 +33,8 @@ , rtitle :: String -- ^ the title of the game , rpathsDataFile :: FilePath -> IO FilePath -- ^ the path to data files , rpathsVersion :: Version -- ^ the version of the game+ , ritemMelee :: [Char] -- ^ symbols of melee weapons+ , ritemProject :: [Char] -- ^ symbols of items AI can project } -- | A dummy instance of the 'Show' class, to satisfy general requirments
Game/LambdaHack/Display.hs view
@@ -3,9 +3,9 @@ {-# LANGUAGE CPP #-} module Game.LambdaHack.Display ( -- * Re-exported frontend- FrontendSession, startup, shutdown, frontendName, nextEvent+ FrontendSession, startup, shutdown, frontendName, nextEvent, promptGetKey -- * Derived operations- , ColorMode(..), displayLevel, getConfirmD+ , displayFrame ) where -- Wrapper for selected Display frontend.@@ -20,199 +20,8 @@ import Game.LambdaHack.Display.Gtk as D #endif -import qualified Data.Char as Char-import qualified Data.IntSet as IS-import qualified Data.List as L-import qualified Data.Map as M-import qualified Data.IntMap as IM-import Data.Maybe--import Game.LambdaHack.Utils.Assert-import Game.LambdaHack.Misc-import Game.LambdaHack.Msg import qualified Game.LambdaHack.Color as Color-import Game.LambdaHack.State-import Game.LambdaHack.PointXY-import Game.LambdaHack.Point-import Game.LambdaHack.Level-import Game.LambdaHack.Effect-import Game.LambdaHack.Perception-import Game.LambdaHack.Tile-import Game.LambdaHack.Actor as Actor-import Game.LambdaHack.ActorState-import qualified Game.LambdaHack.Dungeon as Dungeon-import Game.LambdaHack.Content.ActorKind-import Game.LambdaHack.Content.TileKind-import Game.LambdaHack.Content.ItemKind-import qualified Game.LambdaHack.Item as Item-import qualified Game.LambdaHack.Key as K-import Game.LambdaHack.Random-import qualified Game.LambdaHack.Kind as Kind-import Game.LambdaHack.FOV --- | Waits for a SPACE or ESC.-getConfirmD :: FrontendSession -> IO Bool-getConfirmD fs = do- e <- nextEvent fs- case e of- K.Space -> return True- K.Esc -> return False- _ -> getConfirmD fs--splitOverlay :: Int -> String -> [[String]]-splitOverlay s xs = splitOverlay' (lines xs)- where- splitOverlay' ls- | length ls <= s = [ls] -- everything fits on one screen- | otherwise = let (pre, post) = splitAt (s - 1) ls- in (pre ++ [more]) : splitOverlay' post---- | Returns a function that looks up the characters in the--- string by location. Takes the height of the display plus--- the string. Returns also the number of screens required--- to display all of the string.-stringByLocation :: Y -> String -> (Int, (X, Y) -> Maybe Char)-stringByLocation lysize xs =- let ls = splitOverlay lysize xs- m = M.fromList (zip [0..] (L.map (M.fromList . zip [0..]) (concat ls)))- k = length ls- in (k, \ (x, y) -> M.lookup y m >>= \ n -> M.lookup x n)---- | Color mode for the display.-data ColorMode =- ColorFull -- ^ normal, with full colours- | ColorBW -- ^ black+white only---- TODO: split up and generally rewrite.--- | Display the whole screen: level map, messages and status area--- and multi-page overlaid information, if any.-displayLevel :: ColorMode -> FrontendSession -> Kind.COps- -> Perception -> State- -> Msg -> Maybe String -> IO Bool-displayLevel dm fs cops per- s@State{scursor, stime, sflavour, slid, splayer, sdebug}- msg moverlay =- let Kind.COps{ coactor=Kind.Ops{okind}- , coitem=coitem@Kind.Ops{okind=iokind}- , cotile=Kind.Ops{okind=tokind} } = cops- DebugMode{smarkVision, somniscient} = sdebug- lvl@Level{lxsize, lysize, lsmell, ldesc} = slevel s- (_, Actor{bkind, bhp, bloc}, bitems) = findActorAnyLevel splayer s- ActorKind{ahp, asmell} = okind bkind- reachable = debugTotalReachable per- visible = totalVisible per- (msgs, (ns, over)) =- case moverlay of- Just overlay ->- ( splitMsg (fst normalLevelBound + 1) msg (length more)- , -- ns overlay screens needed- stringByLocation lysize overlay- )- Nothing ->- case splitMsg (fst normalLevelBound + 1) msg 0 of- msgTop : mss ->- ( [msgTop]- , stringByLocation lysize $ unlines $- L.map (padMsg (fst normalLevelBound + 1)) mss- )- [] -> assert `failure` msg- (sSml, sVis) = case smarkVision of- Just Blind -> (True, True)- Just _ -> (False, True)- Nothing | asmell -> (True, False)- Nothing -> (False, False)- lAt = if somniscient then at else rememberAt- liAt = if somniscient then atI else rememberAtI- sVisBG = if sVis- then \ vis rea -> if vis- then Color.Blue- else if rea- then Color.Magenta- else Color.defBG- else \ _vis _rea -> Color.defBG- wealth = calculateTotal coitem s- damage = case Item.strongestSword coitem bitems of- Just sw -> case ieffect $ iokind $ Item.jkind sw of- Wound dice -> show dice ++ "+" ++ show (Item.jpower sw)- _ -> show (Item.jpower sw)- Nothing -> "3d1" -- TODO; use the item 'fist'- hs = levelHeroList s- ms = levelMonsterList s- dis offset p@(PointXY (x0, y0)) =- let loc0 = toPoint lxsize p- tile = lvl `lAt` loc0- items = lvl `liAt` loc0- sm = smelltime $ IM.findWithDefault (SmellTime 0) loc0 lsmell- sml = (sm - stime) `div` 100- viewActor loc Actor{bkind = bkind2, bsymbol}- | loc == bloc && slid == creturnLn scursor =- (symbol, Color.defBG) -- highlight player- | otherwise = (symbol, acolor)- where- ActorKind{asymbol, acolor} = okind bkind2- symbol = fromMaybe asymbol bsymbol- viewSmell :: Int -> Char- viewSmell k- | k > 9 = '*'- | k < 0 = '-'- | otherwise = Char.intToDigit k- rainbow loc = toEnum $ loc `rem` 14 + 1- (char, fg0) =- case L.find (\ m -> loc0 == Actor.bloc m) (hs ++ ms) of- Just m | somniscient || vis -> viewActor loc0 m- _ | sSml && sml >= 0 -> (viewSmell sml, rainbow loc0)- | otherwise ->- case items of- [] -> let u = tokind tile- in (tsymbol u, if vis then tcolor u else tcolor2 u)- i : _ -> Item.viewItem coitem (Item.jkind i) sflavour- vis = IS.member loc0 visible- rea = IS.member loc0 reachable- bg0 = if ctargeting scursor /= TgtOff && loc0 == clocation scursor- then Color.defFG -- highlight target cursor- else sVisBG vis rea -- FOV debug- reverseVideo = Color.Attr{ fg = Color.bg Color.defaultAttr- , bg = Color.fg Color.defaultAttr- }- optVisually attr@Color.Attr{fg, bg} =- if (fg == Color.defBG) || (bg == Color.defFG && fg == Color.defFG)- then reverseVideo- else attr- a = case dm of- ColorBW -> Color.defaultAttr- ColorFull -> optVisually Color.Attr{fg = fg0, bg = bg0}- in case over (x0, y0 + offset) of- Just c -> (Color.defaultAttr, c)- _ -> (a, char)- status =- take 27 (ldesc ++ repeat ' ') ++- take 7 ("L: " ++ show (Dungeon.levelNumber slid) ++ repeat ' ') ++- take 10 ("T: " ++ show (stime `div` 10) ++ repeat ' ') ++- take 9 ("$: " ++ show wealth ++ repeat ' ') ++- take 12 ("Dmg: " ++ damage ++ repeat ' ') ++- take 30 ("HP: " ++ show bhp ++- " (" ++ show (maxDice ahp) ++ ")" ++ repeat ' ')- width = fst normalLevelBound + 1- toWidth :: Int -> String -> String- toWidth n x = take n (x ++ repeat ' ')- disp n mesg = display (0, 0, lxsize-1, lysize-1) fs (dis (lysize * n))- (toWidth width mesg) (toWidth width status)- -- Perform messages slideshow.- perf [] = perfOverlay 0 ""- perf [xs] = perfOverlay 0 xs- perf (x:xs) = do- disp ns (x ++ more)- b <- getConfirmD fs- if b then perf xs else return False- -- Perform overlay pages slideshow.- perfOverlay k xs = do- disp k xs- if k < ns - 1- then do- b <- getConfirmD fs- if b- then perfOverlay (k + 1) xs- else return False- else- return True- in perf msgs+-- | Push a frame or a single frame's worth of delay to the frame queue.+displayFrame :: FrontendSession -> Bool -> Maybe Color.SingleFrame -> IO ()+displayFrame fs isRunning = display fs True isRunning
Game/LambdaHack/Display/Curses.hs view
@@ -3,7 +3,7 @@ ( -- * Session data type for the frontend FrontendSession -- * The output and input operations- , display, nextEvent+ , display, nextEvent, promptGetKey -- * Frontend administration tools , frontendName, startup, shutdown ) where@@ -14,9 +14,7 @@ import qualified Data.Map as M import Control.Monad -import Game.LambdaHack.Area-import Game.LambdaHack.PointXY-import qualified Game.LambdaHack.Key as K (Key(..))+import qualified Game.LambdaHack.Key as K (Key(..), Modifier(..)) import qualified Game.LambdaHack.Color as Color -- | Session data maintained by the frontend.@@ -34,7 +32,8 @@ startup :: String -> (FrontendSession -> IO ()) -> IO () startup _ k = do C.start- C.cursSet C.CursorInvisible+-- C.keypad C.stdScr False -- TODO: may help to fix xterm keypad on Ubuntu+ void $ C.cursSet C.CursorInvisible let s = [ (Color.Attr{fg, bg}, C.Style (toFColor fg) (toBColor bg)) | fg <- [minBound..maxBound], -- No more color combinations possible: 16*4, 64 is max.@@ -53,39 +52,50 @@ shutdown _ = C.end -- | Output to the screen via the frontend.-display :: Area -- ^ the size of the drawn area- -> FrontendSession -- ^ current session data- -> (PointXY -> (Color.Attr, Char))- -- ^ the content of the screen- -> String -- ^ an extra line to show at the top- -> String -- ^ an extra line to show at the bottom+display :: FrontendSession -- ^ frontend session data+ -> Bool+ -> Bool+ -> Maybe Color.SingleFrame -- ^ the screen frame to draw -> IO ()-display (x0, y0, x1, y1) FrontendSession{..} f msg status = do+display _ _ _ Nothing = return ()+display FrontendSession{..} _ _ (Just Color.SingleFrame{..}) = do -- let defaultStyle = C.defaultCursesStyle -- Terminals with white background require this: let defaultStyle = sstyles M.! Color.defaultAttr C.erase C.setStyle defaultStyle- C.mvWAddStr swin 0 0 msg- C.mvWAddStr swin (y1 + 2) 0 (L.init status)- -- TODO: we need to remove the last character from the status line,+ C.mvWAddStr swin 0 0 sfTop+ -- We need to remove the last character from the status line, -- because otherwise it would overflow a standard size xterm window, -- due to the curses historical limitations.- sequence_ [ C.setStyle (M.findWithDefault defaultStyle a sstyles)- >> C.mvWAddStr swin (y + 1) x [c]- | x <- [x0..x1], y <- [y0..y1],- let (a, c) = f (PointXY (x, y)) ]+ C.mvWAddStr swin (L.length sfLevel + 1) 0 (L.init sfBottom)+ let nm = L.zip [0..] $ L.map (L.zip [0..]) sfLevel+ sequence_ [ C.setStyle (M.findWithDefault defaultStyle acAttr sstyles)+ >> C.mvWAddStr swin (y + 1) x [acChar]+ | (y, line) <- nm, (x, Color.AttrChar{..}) <- line ] C.refresh -- | Input key via the frontend.-nextEvent :: FrontendSession -> IO K.Key-nextEvent _sess = do+nextEvent :: FrontendSession -> Maybe Bool -> IO (K.Key, K.Modifier)+nextEvent _sess _ = do e <- C.getKey C.refresh- return (keyTranslate e)--- case keyTranslate e of--- Unknown _ -> nextEvent sess--- k -> return k+ return (keyTranslate e, K.NoModifier) +-- | Display a prompt, wait for any of the specified keys (for any key,+-- if the list is empty). Repeat if an unexpected key received.+promptGetKey :: FrontendSession -> [(K.Key, K.Modifier)] -> Color.SingleFrame+ -> IO (K.Key, K.Modifier)+promptGetKey sess keys frame = do+ display sess True True $ Just frame+ km <- nextEvent sess Nothing+ let loop km2 =+ if null keys || km2 `elem` keys+ then return km2+ else do+ km3 <- nextEvent sess Nothing+ loop km3+ loop km+ keyTranslate :: C.Key -> K.Key keyTranslate e = case e of@@ -110,7 +120,10 @@ C.KeyB2 -> K.Begin C.KeyClear -> K.Begin -- No KP_ keys; see https://github.com/skogsbaer/hscurses/issues/10- -- Movement keys are more important than hero selection, so preferring them:+ -- TODO: try to get the Control modifier from the escape gibberish+ -- and use Control-keypad for KP_ movement.+ -- Movement keys are more important than hero selection,+ -- so disabling the latter and interpreting the keypad numbers as movement: C.KeyChar c | c `elem` ['1'..'9'] -> K.KP c | otherwise -> K.Char c
Game/LambdaHack/Display/Gtk.hs view
@@ -1,46 +1,96 @@ -- | Text frontend based on Gtk.+{-# OPTIONS_GHC -fno-warn-unused-do-bind #-} module Game.LambdaHack.Display.Gtk ( -- * Session data type for the frontend FrontendSession -- * The output and input operations- , display, nextEvent+ , display, nextEvent, promptGetKey -- * Frontend administration tools , frontendName, startup, shutdown ) where import Control.Monad+import Control.Monad.Reader import Control.Concurrent-import Graphics.UI.Gtk.Gdk.Events -- TODO: replace, deprecated+import Control.Exception (finally)+import Graphics.UI.Gtk.Gdk.EventM import Graphics.UI.Gtk hiding (Point) import qualified Data.List as L import Data.IORef+import Data.Maybe import qualified Data.Map as M import qualified Data.ByteString.Char8 as BS+import System.Time -import Game.LambdaHack.Area-import Game.LambdaHack.PointXY-import qualified Game.LambdaHack.Key as K (Key(..), keyTranslate)+import Game.LambdaHack.Utils.Assert+import Game.LambdaHack.Utils.LQueue+import qualified Game.LambdaHack.Key as K (Key(..), keyTranslate, Modifier(..)) import qualified Game.LambdaHack.Color as Color +data FrameState =+ FPushed -- frames stored in a queue, to be drawn in equal time intervals+ { fpushed :: !(LQueue (Maybe GtkFrame)) -- ^ screen output channel+ , fshown :: !GtkFrame -- ^ last full frame shown+ }+ | FSet -- a single frame stored, to be drawn when a keypress is requested+ { fsetFrame :: !(Maybe GtkFrame) -- ^ frame to draw at input key+ }+ | FNone -- no frames stored+ -- | Session data maintained by the frontend. data FrontendSession = FrontendSession- { sview :: TextView -- ^ the widget to draw to- , stags :: M.Map Color.Attr TextTag -- ^ text tags for fore/back colour pairs- , schan :: Chan String -- ^ the channel that carries input+ { sview :: !TextView -- ^ the widget to draw to+ , stags :: !(M.Map Color.Attr TextTag) -- ^ text color tags for fg/bg+ , schanKey :: !(Chan (K.Key, K.Modifier)) -- ^ channel for keyboard input+ , sframeState :: !(MVar FrameState) -- ^ state of the frame machine+ , slastFull :: !(IORef (GtkFrame, Bool))+ -- ^ most recent full (not empty, not repeated) frame received+ -- and if any empty frame followed it } +data GtkFrame = GtkFrame+ { gfChar :: !BS.ByteString+ , gfAttr :: ![[TextTag]]+ }+ deriving Eq++dummyFrame :: GtkFrame+dummyFrame = GtkFrame BS.empty []++-- | Remove all but the last element of the frame queue.+-- The kept last element ensures that slastFull is not invalidated.+trimQueue :: FrontendSession -> IO ()+trimQueue FrontendSession{sframeState} = do+ fs <- takeMVar sframeState+ case fs of+ FPushed{..} ->+ putMVar sframeState FPushed{fpushed = trimLQueue fpushed, ..}+ _ ->+ putMVar sframeState fs+ -- | The name of the frontend. frontendName :: String frontendName = "gtk" --- | Starts the main program loop using the frontend input and output.+-- | Spawns the gtk input and output thread, which spawns all the other+-- required threads. We create a separate thread for gtk to minimize+-- communication with the heavy main thread. The other threads have to be+-- spawned after gtk is initialized, because they call @postGUIAsync@,+-- and need @sview@ and @stags@. startup :: String -> (FrontendSession -> IO ()) -> IO () startup configFont k = do- -- initGUI+ mv <- newEmptyMVar+ -- Fork the gtk input and output thread.+ void $ forkIO (runGtk configFont k `finally` putMVar mv ())+ takeMVar mv++-- | Sets up and starts the main GTK loop providing input and output.+runGtk :: String -> (FrontendSession -> IO ()) -> IO ()+runGtk configFont k = do+ -- Init GUI. unsafeInitGUIForThreadedRTS- w <- windowNew+ -- Text attributes. ttt <- textTagTableNew- -- text attributes stags <- fmap M.fromList $ mapM (\ ak -> do tt <- textTagNew Nothing@@ -49,55 +99,74 @@ return (ak, tt)) [ Color.Attr{fg, bg} | fg <- [minBound..maxBound], bg <- Color.legalBG ]- -- text buffer+ -- Text buffer. tb <- textBufferNew (Just ttt)- textBufferSetText tb (unlines (replicate 25 (replicate 80 ' ')))- -- create text view, TODO: use GtkLayout or DrawingArea instead of TextView?+ -- Create text view. TODO: use GtkLayout or DrawingArea instead of TextView? sview <- textViewNewWithBuffer tb- containerAdd w sview textViewSetEditable sview False textViewSetCursorVisible sview False- -- font+ -- Set up the channel for keyboard input.+ schanKey <- newChan+ -- Set up the frame state.+ let frameState = FNone+ -- Create the session record.+ sframeState <- newMVar frameState+ slastFull <- newIORef (dummyFrame, False)+ let sess = FrontendSession{..}+ -- Fork the game logic thread.+ forkIO $ k sess+ -- Fork the thread that periodically draws a frame from a queue, if any.+ forkIO $ pollFrames sess Nothing+ -- Fill the keyboard channel.+ sview `on` keyPressEvent $ do+ n <- eventKeyName+ mods <- eventModifier+ let !key = K.keyTranslate n+ !modifier = modifierTranslate mods+ liftIO $ do+ unless (deadKey n) $ do+ -- Drop all the old frames. Some more may be arriving at the same time.+ trimQueue sess+ -- Store the key in the channel.+ writeChan schanKey (key, modifier)+ return True+ -- Set the font specified in config, if any. f <- fontDescriptionFromString configFont widgetModifyFont sview (Just f)+ -- Prepare font chooser dialog. currentfont <- newIORef f- let buttonPressHandler e = case e of- Button { Graphics.UI.Gtk.Gdk.Events.eventButton = RightButton } -> do- fsd <- fontSelectionDialogNew "Choose font"- cf <- readIORef currentfont -- TODO: "Terminus,Monospace" fails- fds <- fontDescriptionToString cf- fontSelectionDialogSetFontName fsd fds- fontSelectionDialogSetPreviewText fsd "eee...@.##+##"- resp <- dialogRun fsd- when (resp == ResponseOk) $ do- fn <- fontSelectionDialogGetFontName fsd- case fn of- Just fn' -> do- fd <- fontDescriptionFromString fn'- writeIORef currentfont fd- widgetModifyFont sview (Just fd)- Nothing -> return ()- widgetDestroy fsd- return True- _ -> return False- onButtonPress sview buttonPressHandler- -- modify default colours+ sview `on` buttonPressEvent $ do+ but <- eventButton+ liftIO $ case but of+ RightButton -> do+ fsd <- fontSelectionDialogNew "Choose font"+ cf <- readIORef currentfont -- TODO: "Terminus,Monospace" fails+ fds <- fontDescriptionToString cf+ fontSelectionDialogSetFontName fsd fds+ fontSelectionDialogSetPreviewText fsd "eee...@.##+##"+ resp <- dialogRun fsd+ when (resp == ResponseOk) $ do+ fn <- fontSelectionDialogGetFontName fsd+ case fn of+ Just fn' -> do+ fd <- fontDescriptionFromString fn'+ writeIORef currentfont fd+ widgetModifyFont sview (Just fd)+ Nothing -> return ()+ widgetDestroy fsd+ return True+ _ -> return False+ -- Modify default colours. let black = Color minBound minBound minBound -- Color.defBG == Color.Black white = Color 0xC500 0xBC00 0xB800 -- Color.defFG == Color.White widgetModifyBase sview StateNormal black widgetModifyText sview StateNormal white- -- set up the channel for communication- schan <- newChan- forkIO $ k FrontendSession{..}- -- fill the channel- onKeyPress sview- (\ e -> do- writeChan schan (Graphics.UI.Gtk.Gdk.Events.eventKeyName e)- return True)- -- set quit handler+ -- Set up the main window.+ w <- windowNew+ containerAdd w sview onDestroy w mainQuit- -- start it up widgetShowAll w+ -- Wait until the other thread draws something and show the window. yield mainGUI @@ -106,77 +175,290 @@ shutdown _ = mainQuit -- | Output to the screen via the frontend.-display :: Area -- ^ the size of the drawn area- -> FrontendSession -- ^ current session data- -> (PointXY -> (Color.Attr, Char))- -- ^ the content of the screen- -> String -- ^ an extra line to show at the top- -> String -- ^ an extra line to show at the bottom- -> IO ()-display (x0, y0, x1, y1) FrontendSession{sview, stags} f msg status =- postGUIAsync $ do- tb <- textViewGetBuffer sview- let fLine y = let (as, cs) = unzip [ f (PointXY (x, y))- | x <- [x0..x1] ]- in ((y, as), BS.pack cs)- memo = L.map fLine [y0..y1]- attrs = L.map fst memo- chars = L.map snd memo- bs = [BS.pack msg, BS.pack "\n", BS.unlines chars, BS.pack status]- textBufferSetByteString tb (BS.concat bs)- mapM_ (setTo tb stags x0) attrs+output :: FrontendSession -- ^ frontend session data+ -> GtkFrame -- ^ the screen frame to draw+ -> IO ()+output FrontendSession{sview, stags} GtkFrame{..} = do -- new frame+ tb <- textViewGetBuffer sview+ let attrs = L.zip [0..] gfAttr+ defaultAttr = stags M.! Color.defaultAttr+ textBufferSetByteString tb gfChar+ mapM_ (setTo tb defaultAttr 0) attrs -setTo :: TextBuffer -> M.Map Color.Attr TextTag -> X -> (Y, [Color.Attr])- -> IO ()+setTo :: TextBuffer -> TextTag -> Int -> (Int, [TextTag]) -> IO () setTo _ _ _ (_, []) = return ()-setTo tb tts lx (ly, attr:attrs) = do+setTo tb defaultAttr lx (ly, attr:attrs) = do ib <- textBufferGetIterAtLineOffset tb (ly + 1) lx ie <- textIterCopy ib- let setIter :: Color.Attr -> Int -> [Color.Attr] -> IO ()+ let setIter :: TextTag -> Int -> [TextTag] -> IO () setIter previous repetitions [] = do textIterForwardChars ie repetitions- when (previous /= Color.defaultAttr) $- textBufferApplyTag tb (tts M.! previous) ib ie+ when (previous /= defaultAttr) $+ textBufferApplyTag tb previous ib ie setIter previous repetitions (a:as) | a == previous = setIter a (repetitions + 1) as | otherwise = do textIterForwardChars ie repetitions- when (previous /= Color.defaultAttr) $- textBufferApplyTag tb (tts M.! previous) ib ie+ when (previous /= defaultAttr) $+ textBufferApplyTag tb previous ib ie textIterForwardChars ib repetitions setIter a 1 as setIter attr 1 attrs --- | Input key via the frontend.-nextEvent :: FrontendSession -> IO K.Key-nextEvent sess = do- e <- readUndeadChan (schan sess)- return (K.keyTranslate e)+-- TODO: configure+-- | Maximal frames per second.+-- This is better low and fixed, to avoid jerkiness and delays+-- that tell the player there are many intelligent enemies on the level.+-- That's better than scaling AI sofistication down based on the FPS setting+-- and machine speed.+maxFps :: Int+maxFps = 15 --- | Reads until a non-dead key encountered.-readUndeadChan :: Chan String -> IO String-readUndeadChan ch = do- x <- readChan ch- if dead x then readUndeadChan ch else return x- where- dead x = case x of- "Shift_R" -> True- "Shift_L" -> True- "Control_L" -> True- "Control_R" -> True- "Super_L" -> True- "Super_R" -> True- "Menu" -> True- "Alt_L" -> True- "Alt_R" -> True- "ISO_Level2_Shift" -> True- "ISO_Level3_Shift" -> True- "ISO_Level2_Latch" -> True- "ISO_Level3_Latch" -> True- "Num_Lock" -> True- "Caps_Lock" -> True- _ -> False+-- | Maximal polls per second.+maxPolls :: Int+maxPolls = let maxP = 120+ in assert (maxP >= 2 * maxFps `blame` (maxP, maxFps)) $+ maxP++-- | Add a given number of microseconds to time.+addTime :: ClockTime -> Int -> ClockTime+addTime (TOD s p) ms = TOD s (p + fromIntegral (ms * 1000000))++-- | The difference between the first and the second time, in microseconds.+diffTime :: ClockTime -> ClockTime -> Int+diffTime (TOD s1 p1) (TOD s2 p2) =+ (fromIntegral $ s1 - s2) * 1000000 ++ (fromIntegral $ p1 - p2) `div` 1000000++-- | Poll the frame queue often and draw frames at fixed intervals.+pollFrames :: FrontendSession -> Maybe ClockTime -> IO ()+pollFrames sess (Just setTime) = do+ -- Check if the time is up.+ curTime <- getClockTime+ let diffT = diffTime setTime curTime+ if diffT > 1000000 `div` maxPolls+ then do+ -- Delay half of the time difference.+ threadDelay $ diffTime curTime setTime `div` 2+ pollFrames sess $ Just setTime+ else+ -- Don't delay, because time is up!+ pollFrames sess Nothing+pollFrames sess@FrontendSession{sframeState} Nothing = do+ -- Time time is up, check if we actually wait for anyting.+ fs <- takeMVar sframeState+ case fs of+ FPushed{..} ->+ case tryReadLQueue fpushed of+ Just (Just frame, queue) -> do+ -- The frame has arrived so send it for drawing and update delay.+ putMVar sframeState FPushed{fpushed = queue, fshown = frame}+ postGUIAsync $ output sess frame+ curTime <- getClockTime+ threadDelay $ 1000000 `div` (maxFps * 2)+ pollFrames sess $ Just $ addTime curTime $ 1000000 `div` maxFps+ Just (Nothing, queue) -> do+ -- Delay requested via an empty frame.+ putMVar sframeState FPushed{fpushed = queue, ..}+ curTime <- getClockTime+ -- There is no problem if the delay is a bit delayed.+ threadDelay $ 1000000 `div` maxFps+ pollFrames sess $ Just $ addTime curTime $ 1000000 `div` maxFps+ Nothing -> do+ -- The queue is empty, the game logic thread lags.+ putMVar sframeState fs+ -- Time time is up, the game thread is going to send a frame,+ -- (otherwise it would change the state), so poll often.+ threadDelay $ 1000000 `div` maxPolls+ pollFrames sess Nothing+ _ -> do+ putMVar sframeState fs+ -- Not in the Push state, so poll lazily to catch the next state change.+ -- The slow polling also gives the game logic a head start+ -- in creating frames in case one of the further frames is slow+ -- to generate and would normally cause a jerky delay in drawing.+ threadDelay $ 1000000 `div` (maxFps * 2)+ pollFrames sess Nothing++-- | Add a frame to be drawn.+display :: FrontendSession -> Bool -> Bool -> Maybe Color.SingleFrame -> IO ()+display sess True noDelay rawFrame = pushFrame sess noDelay rawFrame+display sess False _ (Just rawFrame) = setFrame sess rawFrame+display _ _ _ _ = assert `failure` "display: empty frame to be set"++-- | Add a game screen frame to the frame drawing channel.+pushFrame :: FrontendSession -> Bool -> Maybe Color.SingleFrame -> IO ()+pushFrame sess@FrontendSession{sframeState, slastFull} noDelay rawFrame = do+ -- Full evaluation and comparison is done outside the mvar lock.+ (lastFrame, anyFollowed) <- readIORef slastFull+ let frame = maybe Nothing (Just . evalFrame sess) rawFrame+ nextFrame =+ if frame == Just lastFrame+ then Nothing -- no sense repeating+ else frame+ -- Now we take the lock.+ fs <- takeMVar sframeState+ case fs of+ FPushed{..} ->+ if (isNothing nextFrame && anyFollowed)+ then putMVar sframeState fs -- old news+ else putMVar sframeState+ FPushed{fpushed = writeLQueue fpushed nextFrame, ..}+ FSet{} -> assert `failure` "pushFrame: FSet, expecting FPushed or FNone"+ FNone ->+ -- Never start playing with an empty frame.+ let fpushed = if isJust nextFrame+ then writeLQueue newLQueue nextFrame+ else newLQueue+ fshown = dummyFrame+ in putMVar sframeState FPushed{..}+ yield -- drawing has priority+ case nextFrame of+ Nothing -> writeIORef slastFull (lastFrame, True)+ Just f -> writeIORef slastFull (f, noDelay)++evalFrame :: FrontendSession -> Color.SingleFrame -> GtkFrame+evalFrame FrontendSession{stags} Color.SingleFrame{..} =+ let levelChar = L.map (L.map Color.acChar) sfLevel+ gfChar = BS.pack $ L.intercalate "\n" $ sfTop : levelChar ++ [sfBottom]+ -- Strict version of @L.map (L.map ((stags M.!) . fst)) sfLevel@.+ gfAttr = L.reverse $ L.foldl' ff [] sfLevel+ ff ll l = (L.reverse $ L.foldl' f [] l) : ll+ f l ac = let !tag = stags M.! Color.acAttr ac in tag : l+ in GtkFrame{..}++-- | Set the frame to be drawn at the next invocation of @nextEvent@.+-- Fail if there is already a frame pushed or set.+-- Don't show the frame if it's unchanged vs the previous.+setFrame :: FrontendSession -> Color.SingleFrame -> IO ()+setFrame sess@FrontendSession{slastFull, sframeState} rawFrame = do+ -- Full evaluation and comparison is done outside the mvar lock.+ (lastFrame, _) <- readIORef slastFull+ let frame = evalFrame sess rawFrame+ fsetFrame =+ if frame == lastFrame+ then Nothing -- no sense repeating+ else Just frame+ -- Now we take the lock.+ fs <- takeMVar sframeState+ case fs of+ FPushed{} -> assert `failure` "setFrame: FPushed, expecting FNone"+ FSet{} -> assert `failure` "setFrame: FSet, expecting FNone"+ FNone -> do+ -- Update the last received frame with the processed frame.+ -- There is no race condition, because we are on the same thread+ -- as pushFrame.+ maybe (return ()) (\ fr -> writeIORef slastFull (fr, False)) fsetFrame+ -- Store the frame. Release the lock.+ putMVar sframeState FSet{..}++-- | Input key via the frontend. Fail if there is no frame to show+-- to the player as a prompt for the keypress.+nextEvent :: FrontendSession -> Maybe Bool -> IO (K.Key, K.Modifier)+nextEvent FrontendSession{schanKey, sframeState} Nothing = do+ -- Verify the state.+ -- Assumption: no other thread changes the main constructor in sframeState.+ fs <- readMVar sframeState+ case fs of+ FNone -> return () -- old frame requested, as expected+ FPushed{} -> assert `failure` "nextEvent: FPushed, expecting FNone"+ FSet{} -> assert `failure` "nextEvent: FSet, expecting FNone"+ -- Wait for a keypress.+ km <- readChan schanKey+ return km+nextEvent sess@FrontendSession{schanKey, sframeState} (Just False) = do+ -- Take the lock to display the set frame.+ fs <- takeMVar sframeState+ case fs of+ FSet{fsetFrame} -> do+ -- If the frame not repeated, draw it.+ maybe (return ()) (postGUIAsync . output sess) fsetFrame+ FPushed{} -> assert `failure` "nextEvent: FPushed, expecting FSet"+ FNone -> assert `failure` "nextEvent: FNone, expecting FSet"+ -- Clear the stored frame. Release the lock.+ putMVar sframeState FNone+ -- Wait for a keypress.+ km <- readChan schanKey+ return km+nextEvent sess@FrontendSession{schanKey, sframeState} (Just True) = do+ -- Wait for a keypress.+ km <- readChan schanKey+ -- Trim the queue.+ trimQueue sess+ -- Take the lock to wipe out the frame queue, unless it's empty already.+ fs <- takeMVar sframeState+ case fs of+ FPushed{..} -> do+ -- Draw the last frame ASAP.+ case tryReadLQueue fpushed of+ Just (Just frame, queue) -> assert (nullLQueue queue) $ do+ -- Comparison is done inside the mvar lock, this time, but it's OK.+ let lastFrame = fshown+ nextFrame =+ if frame == lastFrame+ then Nothing -- no sense repeating+ else Just frame+ maybe (return ()) (postGUIAsync . output sess) nextFrame+ Just (Nothing, _) -> assert `failure` "nextEvent: trimmed queue"+ Nothing -> return ()+ FSet{} -> assert `failure` "nextEvent: FSet, expecting FPushed"+ FNone -> assert `failure` "nextEvent: FNone, expecting FPushed"+ -- Wipe out the frame queue. No more frames will arrive, because we are+ -- on the same thread as pushFrame. Release the lock.+ putMVar sframeState FNone+ return km++-- | Display a prompt, wait for any of the specified keys (for any key,+-- if the list is empty). Repeat if an unexpected key received.+-- Starts in Push or None mode, stop in None mode.+-- Spends most time waiting for a key, so not performance critical,+-- so does not need optimization.+promptGetKey :: FrontendSession -> [(K.Key, K.Modifier)] -> Color.SingleFrame+ -> IO (K.Key, K.Modifier)+promptGetKey sess@FrontendSession{sframeState} keys frame = do+ -- Assumption: no other thread changes the main constructor in sframeState.+ fs <- readMVar sframeState+ yield -- drawing has priority+ let doPush = case fs of+ FPushed{} -> True+ FSet{} ->+ assert `failure` "promptGetKey: FSet, expecting FPushed or FNone"+ FNone -> False+ display sess doPush True $ Just frame+ km <- nextEvent sess (Just doPush)+ let loop km2 =+ if null keys || km2 `elem` keys+ then return km2+ else do+ km3 <- nextEvent sess Nothing+ loop km3+ loop km++-- | Tells a dead key.+deadKey :: String -> Bool+deadKey x = case x of+ "Shift_R" -> True+ "Shift_L" -> True+ "Control_L" -> True+ "Control_R" -> True+ "Super_L" -> True+ "Super_R" -> True+ "Menu" -> True+ "Alt_L" -> True+ "Alt_R" -> True+ "ISO_Level2_Shift" -> True+ "ISO_Level3_Shift" -> True+ "ISO_Level2_Latch" -> True+ "ISO_Level3_Latch" -> True+ "Num_Lock" -> True+ "Caps_Lock" -> True+ _ -> False++-- | Translates modifiers to our own encoding.+modifierTranslate :: [Modifier] -> K.Modifier+modifierTranslate mods =+ if Control `elem` mods then K.Control else K.NoModifier doAttr :: TextTag -> Color.Attr -> IO () doAttr tt attr@Color.Attr{fg, bg}
Game/LambdaHack/Display/Std.hs view
@@ -3,7 +3,7 @@ ( -- * Session data type for the frontend FrontendSession -- * The output and input operations- , display, nextEvent+ , display, nextEvent, promptGetKey -- * Frontend administration tools , frontendName, startup, shutdown ) where@@ -12,9 +12,7 @@ import qualified Data.ByteString.Char8 as BS import qualified System.IO as SIO -import Game.LambdaHack.Area-import Game.LambdaHack.PointXY-import qualified Game.LambdaHack.Key as K (Key(..))+import qualified Game.LambdaHack.Key as K (Key(..), Modifier(..)) import qualified Game.LambdaHack.Color as Color -- | No session data needs to be maintained by this frontend.@@ -33,31 +31,40 @@ shutdown _ = return () -- | Output to the screen via the frontend.-display :: Area -- ^ the size of the drawn area- -> FrontendSession -- ^ current session data- -> (PointXY -> (Color.Attr, Char))- -- ^ the content of the screen- -> String -- ^ an extra line to show at the top- -> String -- ^ an extra line to show at the bottom+display :: FrontendSession -- ^ frontend session data+ -> Bool+ -> Bool+ -> Maybe Color.SingleFrame -- ^ the screen frame to draw -> IO ()-display (x0, y0, x1, y1) _sess f msg status =- let xsize = x1 - x0 + 1- g y x = if x > x1- then Nothing- else Just (snd (f (PointXY (x, y))), x + 1)- fl y = fst $ BS.unfoldrN xsize (g y) x0- level = L.map fl [y0..y1]- screen = [BS.pack msg] ++ level ++ [BS.pack status, BS.empty]- in mapM_ BS.putStrLn screen+display _ _ _ Nothing = return ()+display _ _ _ (Just Color.SingleFrame{..}) =+ let chars = L.map (BS.pack . L.map Color.acChar) sfLevel+ bs = [BS.pack sfTop, BS.empty] ++ chars ++ [BS.pack sfBottom, BS.empty]+ in mapM_ BS.putStrLn bs -- | Input key via the frontend.-nextEvent :: FrontendSession -> IO K.Key-nextEvent sess = do+nextEvent :: FrontendSession -> Maybe Bool -> IO (K.Key, K.Modifier)+nextEvent sess mb = do e <- BS.hGet SIO.stdin 1 let c = BS.head e if c == '\n' -- let \n mark the end of input, for human players- then nextEvent sess- else return $ keyTranslate c+ then nextEvent sess mb+ else return (keyTranslate c, K.NoModifier)++-- | Display a prompt, wait for any of the specified keys (for any key,+-- if the list is empty). Repeat if an unexpected key received.+promptGetKey :: FrontendSession -> [(K.Key, K.Modifier)] -> Color.SingleFrame+ -> IO (K.Key, K.Modifier)+promptGetKey sess keys frame = do+ display sess True True $ Just frame+ km <- nextEvent sess Nothing+ let loop km2 =+ if null keys || km2 `elem` keys+ then return km2+ else do+ km3 <- nextEvent sess Nothing+ loop km3+ loop km -- HACK: Special translation that block commands the bots should not use -- and multiplies some other commands.
Game/LambdaHack/Display/Vty.hs view
@@ -3,7 +3,7 @@ ( -- * Session data type for the frontend FrontendSession -- * The output and input operations- , display, nextEvent+ , display, nextEvent, promptGetKey -- * Frontend administration tools , frontendName, startup, shutdown ) where@@ -13,9 +13,7 @@ import qualified Data.List as L import qualified Data.ByteString.Char8 as BS -import Game.LambdaHack.Area-import Game.LambdaHack.PointXY-import qualified Game.LambdaHack.Key as K (Key(..))+import qualified Game.LambdaHack.Key as K (Key(..), Modifier(..)) import qualified Game.LambdaHack.Color as Color -- | Session data maintained by the frontend.@@ -34,53 +32,73 @@ shutdown = Vty.shutdown -- | Output to the screen via the frontend.-display :: Area -- ^ the size of the drawn area- -> FrontendSession -- ^ current session data- -> (PointXY -> (Color.Attr, Char))- -- ^ the content of the screen- -> String -- ^ an extra line to show at the top- -> String -- ^ an extra line to show at bottom+display :: FrontendSession -- ^ frontend session data+ -> Bool+ -> Bool+ -> Maybe Color.SingleFrame -- ^ the screen frame to draw -> IO ()-display (x0, y0, x1, y1) vty f msg status =- let img = (foldr (<->) empty_image .- L.map (foldr (<|>) empty_image .- L.map (\ (x, y) -> let (a, c) = f (PointXY (x, y))- in char (setAttr a) c)))- [ [ (x, y) | x <- [x0..x1] ] | y <- [y0..y1] ]+display _ _ _ Nothing = return ()+display vty _ _ (Just Color.SingleFrame{..}) =+ let img = (foldr (<->) empty_image+ . L.map (foldr (<|>) empty_image+ . L.map (\ Color.AttrChar{..} ->+ char (setAttr acAttr) acChar)))+ sfLevel pic = pic_for_image $- utf8_bytestring (setAttr Color.defaultAttr) (BS.pack msg)+ utf8_bytestring (setAttr Color.defaultAttr) (BS.pack sfTop) <-> img <->- utf8_bytestring (setAttr Color.defaultAttr) (BS.pack status)+ utf8_bytestring (setAttr Color.defaultAttr) (BS.pack sfBottom) in update vty pic -- | Input key via the frontend.-nextEvent :: FrontendSession -> IO K.Key-nextEvent sess = do+nextEvent :: FrontendSession -> Maybe Bool -> IO (K.Key, K.Modifier)+nextEvent sess mb = do e <- next_event sess- return (keyTranslate e)--keyTranslate :: Event -> K.Key-keyTranslate e = case e of- EvKey KEsc [] -> K.Esc- EvKey KEnter [] -> K.Return- EvKey (KASCII ' ') [] -> K.Space- EvKey (KASCII '\t') [] -> K.Tab- EvKey KUp [] -> K.Up- EvKey KDown [] -> K.Down- EvKey KLeft [] -> K.Left- EvKey KRight [] -> K.Right- EvKey KHome [] -> K.Home- EvKey KPageUp [] -> K.PgUp- EvKey KEnd [] -> K.End- EvKey KPageDown [] -> K.PgDn- EvKey KBegin [] -> K.Begin- -- No KP_ keys in vty; see https://github.com/coreyoconnor/vty/issues/8- -- For now, movement keys are more important than hero selection:- EvKey (KASCII c) []- | c `elem` ['1'..'9'] -> K.KP c- | otherwise -> K.Char c- _ -> K.Unknown (show e)+ EvKey n mods -> do+ let key = keyTranslate n+ modifier = modifierTranslate mods+ return (key, modifier)+ _ -> nextEvent sess mb++-- | Display a prompt, wait for any of the specified keys (for any key,+-- if the list is empty). Repeat if an unexpected key received.+promptGetKey :: FrontendSession -> [(K.Key, K.Modifier)] -> Color.SingleFrame+ -> IO (K.Key, K.Modifier)+promptGetKey sess keys frame = do+ display sess True True $ Just frame+ km <- nextEvent sess Nothing+ let loop km2 =+ if null keys || km2 `elem` keys+ then return km2+ else do+ km3 <- nextEvent sess Nothing+ loop km3+ loop km++keyTranslate :: Key -> K.Key+keyTranslate n =+ case n of+ KEsc -> K.Esc+ KEnter -> K.Return+ (KASCII ' ') -> K.Space+ (KASCII '\t') -> K.Tab+ KUp -> K.Up+ KDown -> K.Down+ KLeft -> K.Left+ KRight -> K.Right+ KHome -> K.Home+ KPageUp -> K.PgUp+ KEnd -> K.End+ KPageDown -> K.PgDn+ KBegin -> K.Begin+ (KASCII c) -> K.Char c+ _ -> K.Unknown (show n)++-- | Translates modifiers to our own encoding.+modifierTranslate :: [Modifier] -> K.Modifier+modifierTranslate mods =+ if MCtrl `elem` mods then K.Control else K.NoModifier -- A hack to get bright colors via the bold attribute. Depending on terminal -- settings this is needed or not and the characters really get bold or not.
+ Game/LambdaHack/Draw.hs view
@@ -0,0 +1,177 @@+-- | Display game data on the screen using one of the available frontends+-- (determined at compile time with cabal flags).+{-# LANGUAGE CPP #-}+module Game.LambdaHack.Draw+ ( ColorMode(..), draw, animate+ ) where++import qualified Data.IntSet as IS+import qualified Data.List as L+import qualified Data.IntMap as IM+import Data.Maybe++import Game.LambdaHack.Msg+import qualified Game.LambdaHack.Color as Color+import Game.LambdaHack.State+import Game.LambdaHack.PointXY+import Game.LambdaHack.Point+import Game.LambdaHack.Level+import Game.LambdaHack.Effect+import Game.LambdaHack.Perception+import Game.LambdaHack.Actor as Actor+import Game.LambdaHack.ActorState+import qualified Game.LambdaHack.Dungeon as Dungeon+import Game.LambdaHack.Content.ActorKind+import Game.LambdaHack.Content.TileKind+import Game.LambdaHack.Content.ItemKind+import qualified Game.LambdaHack.Item as Item+import Game.LambdaHack.Random+import qualified Game.LambdaHack.Kind as Kind+import Game.LambdaHack.FOV+import qualified Game.LambdaHack.Feature as F+import Game.LambdaHack.Time++-- | Color mode for the display.+data ColorMode =+ ColorFull -- ^ normal, with full colours+ | ColorBW -- ^ black+white only++-- TODO: split up and generally rewrite.+-- | Draw the whole screen: level map, status area and, at most,+-- a single page overlay of text divided into lines.+draw :: ColorMode -> Kind.COps -> Perception -> State -> Overlay+ -> Color.SingleFrame+draw dm cops per s@State{ scursor=Cursor{..}+ , sflavour, slid, splayer, sdebug+ } overlay =+ let Kind.COps{ coactor=Kind.Ops{okind}+ , coitem=coitem@Kind.Ops{okind=iokind}+ , cotile=Kind.Ops{okind=tokind, ouniqGroup} } = cops+ DebugMode{smarkVision, somniscient} = sdebug+ lvl@Level{lxsize, lysize, lsmell, ldesc, lactor, ltime} = slevel s+ (_, Actor{bkind, bhp, bloc}, bitems) = findActorAnyLevel splayer s+ ActorKind{ahp, asmell} = okind bkind+ reachable = debugTotalReachable per+ visible = totalVisible per+ (msgTop, over) = stringByLocation lxsize lysize overlay+ (sSml, sVis) = case smarkVision of+ Just Blind -> (True, True)+ Just _ -> (False, True)+ Nothing | asmell -> (True, False)+ Nothing -> (False, False)+ lAt = if somniscient then at else rememberAt+ liAt = if somniscient then atI else rememberAtI+ sVisBG = if sVis+ then \ vis rea -> if vis+ then Color.Blue+ else if rea+ then Color.Magenta+ else Color.defBG+ else \ _vis _rea -> Color.defBG+ (_, wealth) = calculateTotal coitem s+ damage = case Item.strongestSword cops bitems of+ Just sw -> case ieffect $ iokind $ Item.jkind sw of+ Wound dice -> show dice ++ "+" ++ show (Item.jpower sw)+ _ -> show (Item.jpower sw)+ Nothing -> "3d1" -- TODO; use the item 'fist'+ bl = fromMaybe [] $ bla lxsize lysize ceps bloc clocation+ dis pxy =+ let loc0 = toPoint lxsize pxy+ tile = lvl `lAt` loc0+ tk = tokind tile+ items = lvl `liAt` loc0+ sm = IM.findWithDefault timeZero loc0 lsmell+ sml = sm `timeAdd` timeNegate ltime+ viewActor loc Actor{bkind = bkind2, bsymbol, bcolor}+ | loc == bloc && slid == creturnLn =+ (symbol, Color.defBG) -- highlight player+ | otherwise = (symbol, color)+ where+ ActorKind{asymbol, acolor} = okind bkind2+ color = fromMaybe acolor bcolor+ symbol = fromMaybe asymbol bsymbol+ rainbow loc = toEnum $ loc `rem` 14 + 1+ (char, fg0) =+ case L.find (\ m -> loc0 == Actor.bloc m) (IM.elems lactor) of+ _ | ctargeting /= TgtOff+ && slid == creturnLn+ && L.elem loc0 bl ->+ let unknownId = ouniqGroup "unknown space"+ in ('*', case (vis, F.Walkable `elem` tfeature tk) of+ _ | tile == unknownId -> Color.BrBlack+ (True, True) -> Color.BrGreen+ (True, False) -> Color.BrRed+ (False, True) -> Color.Green+ (False, False) -> Color.Red)+ Just m | somniscient || vis -> viewActor loc0 m+ _ | sSml && timeScale sml 10 >= smellTimeout s ->+ (timeToDigit (smellTimeout s) sml, rainbow loc0)+ | otherwise ->+ case items of+ [] -> (tsymbol tk, if vis then tcolor tk else tcolor2 tk)+ i : _ -> Item.viewItem coitem (Item.jkind i) sflavour+ vis = IS.member loc0 visible+ rea = IS.member loc0 reachable+ bg0 = if ctargeting /= TgtOff && loc0 == clocation+ then Color.defFG -- highlight target cursor+ else sVisBG vis rea -- FOV debug or standard bg+ reverseVideo = Color.Attr{ fg = Color.bg Color.defaultAttr+ , bg = Color.fg Color.defaultAttr+ }+ optVisually attr@Color.Attr{fg, bg} =+ if (fg == Color.defBG)+ || (bg == Color.defFG && fg == Color.defFG)+ then reverseVideo+ else attr+ a = case dm of+ ColorBW -> Color.defaultAttr+ ColorFull -> optVisually Color.Attr{fg = fg0, bg = bg0}+ in case over pxy of+ Just c -> Color.AttrChar Color.defaultAttr c+ _ -> Color.AttrChar a char+ status =+ take 28 (ldesc ++ repeat ' ') +++ take 9 ("L: " ++ show (Dungeon.levelNumber slid) ++ repeat ' ') +++ take 11 ("$: " ++ show wealth ++ repeat ' ') +++ take 14 ("Dmg: " ++ damage ++ repeat ' ') +++ take 32 ("HP: " ++ show bhp +++ " (" ++ show (maxDice ahp) ++ ")" ++ repeat ' ')+ toWidth :: Int -> String -> String+ toWidth n x = take n (x ++ repeat ' ')+ fLine y =+ let f l x = let !ac = dis (PointXY (x, y)) in ac : l+ in L.foldl' f [] [lxsize-1,lxsize-2..0]+ sfLevel = -- Fully evaluated.+ let f l y = let !line = fLine y in line : l+ in L.foldl' f [] [lysize-1,lysize-2..0]+ sfTop = toWidth lxsize msgTop+ sfBottom = toWidth lxsize status+ in Color.SingleFrame{..}++-- | Render animations on top of the current screen frame.+animate :: State -> Diary -> Kind.COps -> Perception -> Color.Animation+ -> [Maybe Color.SingleFrame]+animate s Diary{sreport} cops per anim =+ let xsize = lxsize $ slevel s+ over = renderReport sreport+ topLineOnly = padMsg xsize over+ basicFrame = draw ColorFull cops per s [topLineOnly]+ in rederAnim s basicFrame anim++-- | Render animations on top of a screen frame.+rederAnim :: State -> Color.SingleFrame -> Color.Animation+ -> [Maybe Color.SingleFrame]+rederAnim s basicFrame anim =+ let Level{lxsize, lysize} = slevel s+ modifyFrame Color.SingleFrame{sfLevel = levelOld, ..} am =+ let fLine y lineOld =+ let f l (x, acOld) =+ let loc = toPoint lxsize (PointXY (x, y))+ !ac = fromMaybe acOld $ IM.lookup loc am+ in ac : l+ in L.foldl' f [] (zip [lxsize-1,lxsize-2..0] (reverse lineOld))+ sfLevel = -- Fully evaluated.+ let f l (y, lineOld) = let !line = fLine y lineOld in line : l+ in L.foldl' f [] (zip [lysize-1,lysize-2..0] (reverse levelOld))+ in Just Color.SingleFrame{..}+ in map (modifyFrame basicFrame) anim
Game/LambdaHack/Dungeon.hs view
@@ -4,7 +4,7 @@ ( -- * Level identifier LevelId, levelNumber, levelDefault -- * Dungeon- , Dungeon, fromList, currentFirst, adjust, (!), lookup, depth+ , Dungeon, fromList, currentFirst, adjust, mapDungeon, (!), lookup, depth ) where import Prelude hiding (lookup)@@ -67,6 +67,10 @@ -- | Adjust the level at a given id. adjust :: (Level -> Level) -> LevelId -> Dungeon -> Dungeon adjust f lid (Dungeon m d) = Dungeon (M.adjust f lid m) d++-- | Adjust the level at a given id.+mapDungeon :: (Level -> Level) -> Dungeon -> Dungeon+mapDungeon f (Dungeon m d) = Dungeon (M.map f m) d -- | Find a level with the given id. (!) :: Dungeon -> LevelId -> Level
Game/LambdaHack/DungeonState.hs view
@@ -33,13 +33,14 @@ import Game.LambdaHack.Place import qualified Game.LambdaHack.Effect as Effect import Game.LambdaHack.Content.ItemKind+import Game.LambdaHack.Time convertTileMaps :: Rnd (Kind.Id TileKind) -> Int -> Int -> TileMapXY -> Rnd TileMap-convertTileMaps cdefTile cxsize cysize lmap = do+convertTileMaps cdefaultTile cxsize cysize lmap = do let bounds = (origin, toPoint cxsize $ PointXY (cxsize - 1, cysize - 1)) assocs = map (\ (xy, t) -> (toPoint cxsize xy, t)) (M.assocs lmap)- pickedTiles <- replicateM (cxsize * cysize) cdefTile+ pickedTiles <- replicateM (cxsize * cysize) cdefaultTile return $ Kind.listArray bounds pickedTiles Kind.// assocs unknownTileMap :: Kind.Id TileKind -> Int -> Int -> TileMap@@ -54,10 +55,10 @@ rollItems :: Kind.COps -> Int -> Int -> CaveKind -> TileMap -> Point -> Rnd [(Point, Item)] rollItems Kind.COps{cotile, coitem=coitem@Kind.Ops{okind}}- lvl depth CaveKind{cxsize, citemNum, cminStairDist} lmap ploc = do+ ln depth CaveKind{cxsize, citemNum, cminStairDist} lmap ploc = do nri <- rollDice citemNum replicateM nri $ do- item <- newItem coitem lvl depth+ item <- newItem coitem ln depth let ik = okind (jkind item) l <- case ieffect ik of Effect.Wound dice | maxDice dice > 0 -- a weapon@@ -73,9 +74,9 @@ _ -> findLoc lmap (const (Tile.hasFeature cotile F.Boring)) return (l, item) -placeStairs :: Kind.Ops TileKind -> TileMap -> X -> Int -> [Place]+placeStairs :: Kind.Ops TileKind -> TileMap -> CaveKind -> [Place] -> Rnd (Point, Kind.Id TileKind, Point, Kind.Id TileKind)-placeStairs cotile@Kind.Ops{opick} cmap cxsize cminStairDist dplaces = do+placeStairs cotile@Kind.Ops{opick} cmap CaveKind{..} dplaces = do su <- findLoc cmap (const (Tile.hasFeature cotile F.Boring)) sd <- findLocTry 1000 cmap [ \ l _ -> chessDist cxsize su l >= cminStairDist@@ -83,7 +84,8 @@ , \ l t -> l /= su && Tile.hasFeature cotile F.Boring t ] let fitArea loc = inside cxsize loc . qarea- findLegend loc = maybe "litLegend" qlegend $ L.find (fitArea loc) dplaces+ findLegend loc =+ maybe clitLegendTile qlegend $ L.find (fitArea loc) dplaces upId <- opick (findLegend su) $ Tile.kindHasFeature F.Ascendable downId <- opick (findLegend sd) $ Tile.kindHasFeature F.Descendable return (su, upId, sd, downId)@@ -92,25 +94,23 @@ buildLevel :: Kind.COps -> Cave -> Int -> Int -> Rnd Level buildLevel cops@Kind.COps{ cotile=cotile@Kind.Ops{opick, ouniqGroup} , cocave=Kind.Ops{okind} }- Cave{..} lvl depth = do- let cfg@CaveKind{..} = okind dkind- cmap <- convertTileMaps (opick cdefTile (const True)) cxsize cysize dmap+ Cave{..} ln depth = do+ let kc@CaveKind{..} = okind dkind+ cmap <- convertTileMaps (opick cdefaultTile (const True)) cxsize cysize dmap (su, upId, sd, downId) <-- placeStairs cotile cmap cxsize cminStairDist dplaces- let stairs = (su, upId) : if lvl == depth then [] else [(sd, downId)]+ placeStairs cotile cmap kc dplaces+ let stairs = (su, upId) : if ln == depth then [] else [(sd, downId)] lmap = cmap Kind.// stairs- is <- rollItems cops lvl depth cfg lmap su+ is <- rollItems cops ln depth kc lmap su -- TODO: split this into Level.defaultLevel let itemMap = mapToIMap cxsize ditem `IM.union` IM.fromList is litem = IM.map (\ i -> ([i], [])) itemMap unknownId = ouniqGroup "unknown space" level = Level- { lheroes = IM.empty- , lheroItem = IM.empty+ { lactor = IM.empty+ , linv = IM.empty , lxsize = cxsize , lysize = cysize- , lmonsters = IM.empty- , lmonItem = IM.empty , lsmell = IM.empty , lsecret = mapToIMap cxsize dsecret , litem@@ -119,6 +119,7 @@ , ldesc = cname , lmeta = dmeta , lstairs = (su, sd)+ , ltime = timeAdd timeTurn timeTurn -- just stepped into the dungeon } return level
Game/LambdaHack/EffectAction.hs view
@@ -15,9 +15,7 @@ import qualified Data.IntMap as IM import qualified Data.Set as S import qualified Data.IntSet as IS-import System.Time -import Game.LambdaHack.Misc import Game.LambdaHack.Utils.Assert import Game.LambdaHack.Action import Game.LambdaHack.Actor@@ -25,6 +23,7 @@ import Game.LambdaHack.Content.ActorKind import Game.LambdaHack.Content.RuleKind import Game.LambdaHack.Display+import Game.LambdaHack.Draw import Game.LambdaHack.Grammar import Game.LambdaHack.Point import qualified Game.LambdaHack.HighScore as H@@ -35,11 +34,13 @@ import Game.LambdaHack.Perception import Game.LambdaHack.Random import Game.LambdaHack.State+import Game.LambdaHack.Time import qualified Game.LambdaHack.Config as Config import qualified Game.LambdaHack.Effect as Effect import qualified Game.LambdaHack.Kind as Kind import Game.LambdaHack.DungeonState-import qualified Game.LambdaHack.Save as Save+import qualified Game.LambdaHack.Color as Color+import qualified Game.LambdaHack.Dungeon as Dungeon -- TODO: instead of verbosity return msg components and tailor them outside? -- TODO: separately define messages for the case when source == target@@ -49,107 +50,202 @@ -- | The source actor affects the target actor, with a given effect and power. -- The second argument is verbosity of the resulting message. -- Both actors are on the current level and can be the same actor.--- The bool result indicates if the actors identify the effect.+-- The first bool result indicates if the effect was spectacular enough+-- for the actors to identify it (and the item that caused it, if any).+-- The second bool tells if the effect was seen by or affected the party. effectToAction :: Effect.Effect -> Int -> ActorId -> ActorId -> Int- -> Action (Bool, String)-effectToAction Effect.NoEffect _ _ _ _ = nullEffect-effectToAction Effect.Heal _ _source target power = do- coactor@Kind.Ops{okind} <- contentf Kind.coactor+ -> Action (Bool, Bool)+effectToAction effect verbosity source target power = do+ oldTm <- gets (getActor target)+ let oldHP = bhp oldTm+ (b, msg) <- eff effect verbosity source target power+ s <- get+ -- If the target killed outright by the effect (e.g., in a recursive call),+ -- there's nothing left to do. TODO: hacky; aren't messages lost?+ if not (memActor target s)+ then return (b, False)+ else do+ sm <- gets (getActor source)+ tm <- gets (getActor target)+ per <- getPerception+ pl <- gets splayer+ let tloc = bloc tm+ sloc = bloc sm+ newHP = bhp $ getActor target s+ bb <-+ if isAHero s source ||+ isAHero s target ||+ pl == source ||+ pl == target ||+ -- Target part of message shown below, so target visibility checked.+ tloc `IS.member` totalVisible per+ then do+ -- Party sees the effect or is affected by it.+ msgAdd msg+ -- Try to show an animation.+ cops <- getCOps+ diary <- getDiary+ let locs = tloc : if tloc == sloc then [] else [sloc]+ twirlSplash c1 c2 = map (IM.fromList . zip locs)+ [ [Color.AttrChar (Color.Attr Color.BrWhite Color.defBG) '*']+ , [Color.AttrChar (Color.Attr c1 Color.defBG) '/',+ Color.AttrChar (Color.Attr Color.BrWhite Color.defBG) '^']+ , [Color.AttrChar (Color.Attr c1 Color.defBG) '-',+ Color.AttrChar (Color.Attr Color.BrWhite Color.defBG) '^']+ , [Color.AttrChar (Color.Attr c1 Color.defBG) '\\',+ Color.AttrChar (Color.Attr Color.BrWhite Color.defBG) '^']+ , [Color.AttrChar (Color.Attr c1 Color.defBG) '|']+ , [Color.AttrChar (Color.Attr c1 Color.defBG) '/']+ , [Color.AttrChar (Color.Attr c1 Color.defBG) '-']+ , [Color.AttrChar (Color.Attr c2 Color.defBG) '\\',+ Color.AttrChar (Color.Attr Color.BrWhite Color.defBG) '^']+ , [Color.AttrChar (Color.Attr c2 Color.defBG) '%',+ Color.AttrChar (Color.Attr Color.BrWhite Color.defBG) '^']+ , [Color.AttrChar (Color.Attr c2 Color.defBG) '%',+ Color.AttrChar (Color.Attr Color.BrWhite Color.defBG) '^']+ , []+ ]+ anim | newHP > oldHP = twirlSplash Color.BrBlue Color.Blue+ | newHP < oldHP = twirlSplash Color.BrRed Color.Red+ | otherwise = []+ animFrs = animate s diary cops per anim+ mapM_ displayFramePush $ Nothing : animFrs+ return (b, True)+ else do+ -- Hidden, but if interesting then heard.+ when b $ msgAdd "You hear some noises."+ return (b, False)+ -- Now kill the actor, if needed. For monsters, no "die" message+ -- is shown below. It should have been showsn in @eff@.+ when (newHP <= 0) $ do+ -- Place the actor's possessions on the map.+ bitems <- gets (getActorItem target)+ modify (updateLevel (dropItemsAt bitems tloc))+ -- Clean bodies up.+ if target == pl+ then -- Kill the player and check game over.+ checkPartyDeath+ else -- Kill the enemy.+ modify (deleteActor target)+ return bb++-- | The boolean part of the result says if the ation was interesting+-- and the string part describes how the target reacted+-- (not what the source did).+eff :: Effect.Effect -> Int -> ActorId -> ActorId -> Int+ -> Action (Bool, String)+eff Effect.NoEffect _ _ _ _ = nullEffect+eff Effect.Heal _ _source target power = do+ Kind.COps{coactor=coactor@Kind.Ops{okind}} <- getCOps let bhpMax m = maxDice (ahp $ okind $ bkind m) tm <- gets (getActor target) if bhp tm >= bhpMax tm || power <= 0 then nullEffect else do- focusIfAHero target- updateAnyActor target (addHp coactor power) -- TODO: duplicate code in bhpMax and addHp- return (True, actorVerbExtra coactor tm "feel" "better")-effectToAction (Effect.Wound nDm) verbosity source target power = do- coactor <- contentf Kind.coactor- pl <- gets splayer+ void $ focusIfOurs target+ updateAnyActor target (addHp coactor power)+ return (True, actorVerb coactor tm "feel" "better")+eff (Effect.Wound nDm) verbosity source target power = do+ Kind.COps{coactor} <- getCOps n <- rndToAction $ rollDice nDm if n + power <= 0 then nullEffect else do- focusIfAHero target+ void $ focusIfOurs target+ pl <- gets splayer tm <- gets (getActor target)- let newHP = bhp tm - n - power- killed = newHP <= 0+ let newHP = bhp tm - n - power msg- | killed =- if isAHero target || target == pl- then "" -- handled later on in checkPartyDeath- else actorVerb coactor tm "die"+ | newHP <= 0 =+ if target == pl+ then "" -- Handled later on in checkPartyDeath. Suspense.+ else -- Not as important, so let the player read the message+ -- about monster death while he watches the combat animation.+ if bparty tm `elem` allProjectiles+ then actorVerb coactor tm "drop" "down"+ else actorVerb coactor tm "die" "" | source == target = -- a potion of wounding, etc.- actorVerbExtra coactor tm "feel" "wounded"+ actorVerb coactor tm "feel" "wounded" | verbosity <= 0 = ""- | isAHero target || target == pl =- actorVerbExtra coactor tm "lose" $+ | target == pl =+ actorVerb coactor tm "lose" $ show (n + power) ++ "HP"- | otherwise = actorVerbExtra coactor tm "hiss" "in pain"+ | otherwise = actorVerb coactor tm "hiss" "in pain" updateAnyActor target $ \ m -> m { bhp = newHP } -- Damage the target.- when killed $ do- -- Place the actor's possessions on the map.- bitems <- gets (getActorItem target)- modify (updateLevel (dropItemsAt bitems (bloc tm)))- -- Clean bodies up.- 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- | isAMonster target = do -- Monsters have weaker will than heroes.- 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})- displayAll- return (True, "")- | source == target = do- lm <- gets (lmonsters . slevel)- lxsize <- gets (lxsize . slevel)- lysize <- gets (lysize . slevel)- let cross m = bloc m : vicinityCardinal lxsize lysize (bloc m)- vis = L.concatMap cross $ IM.elems lm- rememberList vis- return (True, "A dozen voices yells in anger.")- | otherwise = nullEffect-effectToAction Effect.SummonFriend _ source target power = do+eff Effect.Dominate _ source target _power = do+ s <- get+ if not $ isAHero s target+ then do -- Monsters have weaker will than heroes.+ -- Can't use @focusIfOurs@, because the actor is specifically not ours.+ 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 = stime s})+ -- Display status line and FOV for the newly controlled actor.+ fr <- drawPrompt ColorBW ""+ mapM_ displayFramePush [Nothing, Just fr, Nothing]+ return (True, "")+ else if source == target+ then do+ lm <- gets hostileList+ lxsize <- gets (lxsize . slevel)+ lysize <- gets (lysize . slevel)+ let cross m = bloc m : vicinityCardinal lxsize lysize (bloc m)+ vis = L.concatMap cross lm+ rememberList vis+ return (True, "A dozen voices yells in anger.")+ else nullEffect+eff Effect.SummonFriend _ source target power = do tm <- gets (getActor target)- if isAHero source+ s <- get+ if not $ isAMonster s source then summonHeroes (1 + power) (bloc tm) else summonMonsters (1 + power) (bloc tm) return (True, "")-effectToAction Effect.SummonEnemy _ source target power = do+eff Effect.SummonEnemy _ source target power = do tm <- gets (getActor target)- if not $ isAHero source -- a trick: monster player will summon a hero+ s <- get+ -- A trick: monster player summons a hero.+ if isAMonster s source then summonHeroes (1 + power) (bloc tm) else summonMonsters (1 + power) (bloc tm) return (True, "")-effectToAction Effect.ApplyPerfume _ source target _ =+eff 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.Regeneration verbosity source target power =- effectToAction Effect.Heal verbosity source target power-effectToAction Effect.Searching _ _source _target _power =+eff Effect.Regeneration verbosity source target power =+ eff Effect.Heal verbosity source target power+eff Effect.Searching _ _source _target _power = return (True, "It gets lost and you search in vain.")-effectToAction Effect.Ascend _ source target power = do- coactor <- contentf Kind.coactor+eff Effect.Ascend _ source target power = do tm <- gets (getActor target)- if isAMonster target+ s <- get+ Kind.COps{coactor} <- getCOps+ void $ focusIfOurs target+ if not $ isAHero s target -- not target /= pl: to squash friendly monster then squashActor source target else effLvlGoUp (power + 1)- -- TODO: The following message too late if a monster squashed:- return (True, actorVerbExtra coactor tm "find" "a shortcut upstrairs")-effectToAction Effect.Descend _ source target power = do- coactor <- contentf Kind.coactor+ -- TODO: The following message too late if a monster squashed by going up,+ -- unless it's ironic. ;) The same below.+ s2 <- get+ return $ if maybe H.Camping snd (squit s2) == H.Victor+ then (True, "")+ else (True, actorVerb coactor tm "find" "a way upstairs")+eff Effect.Descend _ source target power = do tm <- gets (getActor target)- if isAMonster target+ s <- get+ Kind.COps{coactor} <- getCOps+ void $ focusIfOurs target+ if not $ isAHero s target then squashActor source target else effLvlGoUp (- (power + 1))- -- TODO: The following message too late if a monster squashed:- return (True, actorVerbExtra coactor tm "find" "a shortcut downstairs")+ s2 <- get+ return $ if maybe H.Camping snd (squit s2) == H.Victor+ then (True, "")+ else (True, actorVerb coactor tm "find" "a way downstairs") nullEffect :: Action (Bool, String) nullEffect = return (False, "Nothing happens.")@@ -157,59 +253,61 @@ -- TODO: refactor with actorAttackActor. squashActor :: ActorId -> ActorId -> Action () squashActor source target = do- Kind.COps{coactor, coitem=Kind.Ops{okind, ouniqGroup}} <- contentOps+ Kind.COps{coactor, coitem=Kind.Ops{okind, ouniqGroup}} <- getCOps sm <- gets (getActor source) tm <- gets (getActor target) let h2hKind = ouniqGroup "weight" power = maxDeep $ ipower $ okind h2hKind h2h = Item h2hKind power Nothing 1 verb = iverbApply $ okind h2hKind- msg = actorVerbActorExtra coactor sm verb tm " in a staircase accident"+ msg = actorVerbActor coactor sm verb tm "in a staircase accident" msgAdd msg itemEffectAction 0 source target h2h- >>= assert `trueM` (source, target, "affected")+ s <- get+ -- The monster has to be killed first, before we step there (same turn!).+ assert (not (memActor target s) `blame` (source, target, "not killed")) $+ return () effLvlGoUp :: Int -> Action () effLvlGoUp k = do- targeting <- gets (ctargeting . scursor) pbody <- gets getPlayerBody pl <- gets splayer slid <- gets slid st <- get+ cops <- getCOps+ lvl <- gets slevel case whereTo st k of- Nothing -> do -- we are at the "end" of the dungeon- b <- msgYesNo "Really escape the dungeon?"- if b- then fleeDungeon- else abortWith "Game resumed."+ Nothing -> fleeDungeon -- we are at the "end" of the dungeon Just (nln, nloc) ->- assert (nln /= slid `blame` (nln, "stairs looped")) $- tryWith (abortWith "somebody blocks the staircase") $ do+ assert (nln /= slid `blame` (nln, "stairs looped")) $ do bitems <- gets getPlayerItem -- Remember the level (e.g., for a teleport via scroll on the floor). remember -- Remove the player from the old level. modify (deleteActor pl)- hs <- gets levelHeroList+ hs <- gets heroList -- Monsters hear that players not on the level. Cancel smell. -- Reduces memory load and savefile size. when (L.null hs) $ modify (updateLevel (updateSmell (const IM.empty))) -- At this place the invariant that the player exists fails. -- Change to the new level (invariant not needed).- modify (\ s -> s {slid = nln})- -- Add the player to the new level.+ switchLevel nln+ -- The player can now be safely added to the new level. modify (insertActor pl pbody) modify (updateAnyActorItem pl (const bitems)) -- At this place the invariant is restored again. inhabitants <- gets (locToActor nloc) case inhabitants of Nothing -> return ()- Just h | isAHero h ->- -- Bail out if a party member blocks the staircase.- abort+-- Broken if the effect happens, e.g. via a scroll and abort is not enough.+-- Just h | isAHero st h ->+-- -- Bail out if a party member blocks the staircase.+-- abortWith "somebody blocks the staircase" Just m ->- -- Somewhat of a workaround: squash monster blocking the staircase.+ -- Aquash an actor blocking the staircase.+ -- This is not a duplication with the other calls to squashActor,+ -- because here an inactive actor is squashed. squashActor pl m -- Verify the monster on the staircase died. inhabitants2 <- gets (locToActor nloc)@@ -221,65 +319,82 @@ -- The invariant "at most one actor on a tile" restored. -- Create a backup of the savegame. state <- get- diary <- currentDiary- liftIO $ Save.saveGameBkp state diary- when (targeting /= TgtOff) doLook -- TODO: lags behind perception+ diary <- getDiary+ saveGameBkp state diary+ msgAdd $ lookAt cops False True state lvl nloc "" +-- | Change level and reset it's time and update the times of all actors.+-- The player may be added to @lactor@ of the new level only after+-- this operation is executed.+switchLevel :: Dungeon.LevelId -> Action ()+switchLevel nln = do+ timeCurrent <- gets stime+ slid <- gets slid+ when (slid /= nln) $ do+ -- Switch to the new level.+ modify (\ s -> s {slid = nln})+ timeLastVisited <- gets stime+ let diff = timeAdd timeCurrent $ timeNegate timeLastVisited+ when (diff /= timeZero) $ do+ -- Reset the level time.+ modify $ updateTime $ const timeCurrent+ -- Update the times of all actors.+ let upd m@Actor{btime} = m {btime = timeAdd btime diff}+ modify (updateLevel (updateActorDict (IM.map upd)))+ -- | The player leaves the dungeon. fleeDungeon :: Action () fleeDungeon = do- coitem <- contentf Kind.coitem- state <- get- let total = calculateTotal coitem state- items = L.concat $ IM.elems $ lheroItem $ slevel state+ Kind.COps{coitem} <- getCOps+ s <- get+ go <- displayYesNo "This is the way out. Really leave now?"+ recordHistory -- Prevent repeating the ending msgs.+ when (not go) $ abortWith "Game resumed."+ let (items, total) = calculateTotal coitem s+ modify (\ st -> st {squit = Just (False, H.Victor)}) if total == 0- then do- go <- msgClear >> msgMoreConfirm ColorFull "Coward!"- when go $- msgMore "Next time try to grab some loot before escape!"- end- else do- let winMsg = "Congratulations, you won! Your loot, worth " ++- show total ++ " gold, is:" -- TODO: use the name of the '$' item instead- displayItems winMsg True items- go <- session getConfirm- when go $ do- go2 <- handleScores True H.Victor total- when go2 $ msgMore "Can it be done better, though?"- end+ then do+ -- The player can back off at each of these steps.+ go1 <- displayMore ColorFull "Coward!"+ when (not go1) $ abortWith "Brave soul!"+ go2 <- displayMore ColorFull+ "Next time try to grab some loot before escape!"+ when (not go2) $ abortWith "Here's your chance!"+ else do+ let winMsg = "Congratulations, you won! Your loot, worth " +++ show total ++ " gold, is:" -- TODO: use the name of the '$' item instead+ io <- itemOverlay True True items+ tryIgnore $ do+ displayOverAbort winMsg io+ modify (\ st -> st {squit = Just (True, H.Victor)}) -- | The source actor affects the target actor, with a given item. -- If the event is seen, the item may get identified.-itemEffectAction :: Int -> ActorId -> ActorId -> Item -> Action Bool+itemEffectAction :: Int -> ActorId -> ActorId -> Item -> Action () itemEffectAction verbosity source target item = do- Kind.Ops{okind} <- contentf Kind.coitem- sm <- gets (getActor source)- tm <- gets (getActor target)- per <- currentPerception- pl <- gets splayer+ Kind.COps{coitem=Kind.Ops{okind}} <- getCOps+ sm <- gets (getActor source)+ slidOld <- gets slid let effect = ieffect $ okind $ jkind item -- The msg describes the target part of the action.- (b, msg) <- effectToAction effect verbosity source target (jpower item)- if isAHero source || isAHero target || pl == source || pl == target ||- (bloc tm `IS.member` totalVisible per &&- bloc sm `IS.member` totalVisible per)- then do- -- Party sees or affected, so reported.- msgAdd msg- -- Party sees or affected, so if interesting, the item gets identified.- when b $ discover item- else- -- Hidden, but if interesting then heard.- when b $ msgAdd "You hear some noises."- return b+ (b1, b2) <- effectToAction effect verbosity source target (jpower item)+ -- Party sees or affected, and the effect interesting,+ -- so the item gets identified.+ when (b1 && b2) $ discover item+ -- Destroys attacking actor and its items: a hack for projectiles.+ slidNew <- gets slid+ modify (\ s -> s {slid = slidOld})+ when (bparty sm `elem` allProjectiles) $+ modify (deleteActor source)+ modify (\ s -> s {slid = slidNew}) -- | Make the item known to the player. discover :: Item -> Action () discover i = do- cops@Kind.Ops{okind} <- contentf Kind.coitem+ Kind.COps{coitem=coitem@Kind.Ops{okind}} <- getCOps state <- get let ik = jkind i- obj = unwords $ tail $ words $ objectItem cops state i+ obj = unwords $ tail $ words $ objectItem coitem state i msg = "The " ++ obj ++ " turns out to be " kind = okind ik alreadyIdentified = L.length (iflavour kind) == 1@@ -287,106 +402,99 @@ unless alreadyIdentified $ do modify (updateDiscoveries (S.insert ik)) state2 <- get- msgAdd $ msg ++ objectItem cops state2 i ++ "."+ msgAdd $ msg ++ objectItem coitem 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- coactor <- contentf Kind.coactor- pl <- gets splayer- targeting <- gets (ctargeting . scursor)+ Kind.COps{coactor} <- getCOps+ pl <- gets splayer+ cops <- getCOps+ lvl <- gets slevel 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+ -- Switch to the new level.+ switchLevel nln -- Make the new actor the player-controlled actor.- modify (\ s -> s { splayer = actor })+ modify (\ s -> s {splayer = actor}) -- Record the original level of the new player.- modify (updateCursor (\ c -> c { creturnLn = nln }))+ modify (updateCursor (\ c -> c {creturnLn = nln})) -- Don't continue an old run, if any. stopRunning- -- Switch to the level.- modify (\ s -> s{slid = nln}) -- Announce. msgAdd $ capActor coactor pbody ++ " selected."- when (targeting /= TgtOff) doLook+ msgAdd $ lookAt cops False True state lvl (bloc pbody) "" 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 $ void displayAll+focusIfOurs :: ActorId -> Action Bool+focusIfOurs target = do+ s <- get+ pl <- gets splayer+ if isAHero s target || target == pl+ then do+ -- Focus on the hero being wounded/displaced/etc.+ b <- selectPlayer target+ -- Display status line for the new hero.+ when b $ do+ -- Display status line and FOV for the new hero.+ fr <- drawPrompt ColorFull ""+ mapM_ displayFramePush [Nothing, Just fr, Nothing]+ return b+ else return False summonHeroes :: Int -> Point -> Action () summonHeroes n loc = assert (n > 0) $ do- cops <- contentOps- newHeroId <- gets (fst . scounter)+ cops <- getCOps+ newHeroId <- gets scounter modify (\ state -> iterate (addHero cops loc) state !! n)- selectPlayer (AHero newHeroId)- >>= assert `trueM` (newHeroId, "player summons himself")- -- Display status line for the new hero.- void displayAll+ b <- focusIfOurs newHeroId+ assert (b `blame` (newHeroId, "player summons himself")) $+ return () summonMonsters :: Int -> Point -> Action () summonMonsters n loc = do- Kind.COps{cotile, coactor=Kind.Ops{opick, okind}} <- contentOps+ Kind.COps{cotile, coactor=Kind.Ops{opick, okind}} <- getCOps mk <- rndToAction $ opick "summon" (const True) hp <- rndToAction $ rollDice $ ahp $ okind mk modify (\ state -> iterate (addMonster cotile mk hp loc) state !! n) --- | Update player memory.-remember :: Action ()-remember = do- per <- currentPerception- let vis = IS.toList (totalVisible per)- rememberList vis--rememberList :: [Point] -> Action ()-rememberList vis = do- lvl <- gets slevel- let rememberTile = [(loc, lvl `at` loc) | loc <- vis]- modify (updateLevel (updateLRMap (Kind.// rememberTile)))- let alt Nothing = Nothing- alt (Just ([], _)) = Nothing- alt (Just (t, _)) = Just (t, t)- rememberItem = IM.alter alt- modify (updateLevel (updateIMap (\ m -> L.foldr rememberItem m vis)))- -- | Remove dead heroes (or dead dominated monsters). Check if game is 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- cops <- contentf Kind.coactor+ Kind.COps{coactor} <- getCOps ahs <- gets allHeroesAnyLevel pl <- gets splayer pbody <- gets getPlayerBody config <- gets sconfig- when (bhp pbody <= 0) $ do -- TODO: change to guard? define mzero as abort? Why are the writes to the files performed when I call abort later? That probably breaks the laws of MonadPlus. Or is the tryWith abort handler placed after the write to files?- go <- msgMoreConfirm ColorBW $ actorVerb cops pbody "die"- history -- Prevent the msgs from being repeated.+ when (bhp pbody <= 0) $ do+ msgAdd $ actorVerb coactor pbody "die" ""+ go <- displayMore ColorBW ""+ recordHistory -- Prevent repeating the "die" msgs. let firstDeathEnds = Config.get config "heroes" "firstDeathEnds" if firstDeathEnds then gameOver go- else case L.filter (\ (actor, _) -> actor /= pl) ahs of+ else case L.filter (/= pl) ahs of [] -> gameOver go- (actor, _nln) : _ -> do+ actor : _ -> do msgAdd "The survivors carry on." -- One last look at the beautiful world. remember -- Remove the dead player. modify deletePlayer -- At this place the invariant that the player exists fails.- -- Focus on the new hero (invariant not needed).+ -- Select the new player-controlled hero (invariant not needed),+ -- but don't draw a frame for him with focusIfOurs,+ -- in case the focus changes again during the same turn.+ -- He's just a random next guy in the line. selectPlayer actor >>= assert `trueM` (pl, actor, "player resurrects") -- At this place the invariant is restored again.@@ -394,113 +502,99 @@ -- | End game, showing the ending screens, if requested. gameOver :: Bool -> Action () gameOver showEndingScreens = do+ slid <- gets slid+ modify (\ st -> st {squit = Just (False, H.Killed slid)}) when showEndingScreens $ do- cops <- contentf Kind.coitem- state <- get- slid <- gets slid- let total = calculateTotal cops state- status = H.Killed slid- handleScores True status total- msgMore "Let's hope another party can save the day!"- end---- | Handle current score and display it with the high scores.--- False if display of the scores was void or interrupted by the user.------ Warning: scores are shown during the game,--- so we should be careful not to leak secret information through them--- (e.g., the nature of the items through the total worth of inventory).-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- msgOverlaysConfirm placeMsg slideshow- session getConfirm+ Kind.COps{coitem} <- getCOps+ s <- get+ dng <- gets sdungeon+ time <- gets stime+ let (items, total) = calculateTotal coitem s+ deepest = Dungeon.levelNumber slid -- use deepest visited instead of level of death+ depth = Dungeon.depth dng+ failMsg | timeFit time timeTurn < 300 =+ "That song shall be short."+ | total < 100 =+ "Born poor, dies poor."+ | deepest < 4 && total < 500 =+ "This should end differently."+ | deepest < depth - 1 =+ "This defeat brings no dishonour."+ | deepest < depth =+ "That is your name. 'Almost'."+ | otherwise =+ "Dead heroes make better legends."+ loseMsg = failMsg ++ " Killing you nets " +++ show total ++ " gold and some junk:" -- TODO: use the name of the '$' item instead+ if null items+ then modify (\ st -> st {squit = Just (True, H.Killed slid)})+ else do+ io <- itemOverlay True True items+ tryIgnore $ do+ displayOverAbort loseMsg io+ modify (\ st -> st {squit = Just (True, H.Killed slid)}) --- effectToAction does not depend on this function right now, but it might,--- and I know no better place to put it.-displayItems :: Msg -> Bool -> [Item] -> Action Bool-displayItems msg sorted is = do- cops <- contentf Kind.coitem+-- | Create a list of item names, split into many overlays.+itemOverlay ::Bool -> Bool -> [Item] -> Action [Overlay]+itemOverlay sorted cheat is = do+ Kind.COps{coitem} <- getCOps state <- get- let inv = unlines $- L.map (\ i -> letterLabel (jletter i)- ++ objectItem cops state i ++ " ")+ lysize <- gets (lysize . slevel)+ let inv = L.map (\ i -> letterLabel (jletter i)+ ++ objectItemCheat coitem cheat state i ++ " ") ((if sorted then L.sortBy (cmpLetterMaybe `on` jletter) else id) is)- let ovl = inv ++ msgEnd- msgReset msg- overlay ovl+ return $ splitOverlay lysize inv stopRunning :: Action () stopRunning = updatePlayerBody (\ p -> p { bdir = Nothing }) --- | Store current msg in the history and reset current msg.-history :: Action ()-history = do- msg <- currentMsg- msgClear- config <- gets sconfig- let historyMax = Config.get config "ui" "historyMax"- -- TODO: not ideal, continuations of sentences are atop beginnings.- splitS = splitMsg (fst normalLevelBound + 1) msg 0- takeMax diary =- take historyMax $- L.map (padMsg (fst normalLevelBound + 1)) splitS ++ shistory diary- unless (L.null msg) $ do- diary <- currentDiary- diaryReset $ diary {shistory = takeMax diary}- -- TODO: depending on tgt, show extra info about tile or monster or both -- | Perform look around in the current location of the cursor.-doLook :: Action ()+doLook :: ActionFrame () doLook = do- cops@Kind.COps{coactor} <- contentOps+ cops@Kind.COps{coactor} <- getCOps loc <- gets (clocation . scursor) state <- get lvl <- gets slevel- per <- currentPerception+ hms <- gets (lactor . slevel)+ per <- getPerception target <- gets (btarget . getPlayerBody) pl <- gets splayer- let canSee = IS.member loc (totalVisible per)- monsterMsg =- if canSee- then case L.find (\ m -> bloc m == loc) (levelMonsterList state) of- Just m -> actorVerbExtra coactor m "be" "here" ++ " "- Nothing -> ""- else ""- vis | not $ loc `IS.member` totalVisible per =- " (not visible)" -- by party- | actorReachesLoc pl loc per (Just pl) = ""- | otherwise = " (not reachable)" -- by hero- mode = case target of- TEnemy _ _ -> "[targeting monster" ++ vis ++ "] "- TLoc _ -> "[targeting location" ++ vis ++ "] "- TCursor -> "[targeting current" ++ vis ++ "] "- -- general info about current loc- lookMsg = mode ++ lookAt cops True canSee state lvl loc monsterMsg- -- check if there's something lying around at current loc- is = lvl `rememberAtI` loc- if length is <= 2- then msgAdd lookMsg- else do- displayItems lookMsg False is- session getConfirm >> msgAdd "" -- TODO: a hack; instead keep current overlay in the state to keep it from being overwritten on the screen in Turn.hs, just as msg is kept, and reset each turn+ targeting <- gets (ctargeting . scursor)+ assert (targeting /= TgtOff) $ do+ let canSee = IS.member loc (totalVisible per)+ monsterMsg =+ if canSee+ then case L.find (\ m -> bloc m == loc) (IM.elems hms) of+ Just m -> actorVerb coactor m "be" "here" ++ " "+ Nothing -> ""+ else ""+ vis | not $ loc `IS.member` totalVisible per =+ " (not visible)" -- by party+ | actorReachesLoc pl loc per (Just pl) = ""+ | otherwise = " (not reachable)" -- by hero+ mode = case target of+ TEnemy _ _ -> "[targeting monster" ++ vis ++ "] "+ TLoc _ -> "[targeting location" ++ vis ++ "] "+ TPath _ -> "[targeting path" ++ vis ++ "] "+ TCursor -> "[targeting current" ++ vis ++ "] "+ -- general info about current loc+ lookMsg = mode ++ lookAt cops True canSee state lvl loc monsterMsg+ -- check if there's something lying around at current loc+ is = lvl `rememberAtI` loc+ io <- itemOverlay False False is+ if length is > 2+ then displayOverlays lookMsg io+ else do+ fr <- drawPrompt ColorFull lookMsg+ return ((), [Just fr]) gameVersion :: Action () gameVersion = do- Kind.COps{corule} <- contentOps- let pathsVersion = rpathsVersion $ stdRuleset corule+ Kind.COps{corule} <- getCOps+ let pathsVersion = rpathsVersion $ Kind.stdRuleset corule msg = "Version " ++ showVersion pathsVersion ++ " (frontend: " ++ frontendName ++ ", engine: LambdaHack " ++ showVersion Self.version ++ ")"
Game/LambdaHack/Feature.hs view
@@ -26,5 +26,5 @@ | Boring -- ^ items and stairs can be generated there | Exit -- ^ is a (not hidden) door, stair, etc. | Path -- ^ used for distinct paths throughout the level- | Secret !RollDice -- ^ tile is generated with this high secrecy value+ | Secret !RollDice -- ^ discovering the secret will require this many turns deriving (Show, Read, Eq, Ord)
Game/LambdaHack/Grammar.hs view
@@ -3,21 +3,22 @@ ( -- * Grammar types Verb, Object -- * General operations- , capitalize, suffixS, addIndefinite+ , capitalize, pluralise, addIndefinite -- * Objects from content- , objectItem, objectActor, capActor+ , objectItemCheat, objectItem, objectActor, capActor -- * Sentences- , actorVerb, actorVerbExtra, actorVerbItemExtra- , actorVerbActorExtra, actorVerbExtraItemExtra+ , actorVerb, actorVerbItem, actorVerbActor, actorVerbExtraItem -- * Scenery description , lookAt ) where import Data.Char import qualified Data.Set as S+import qualified Data.Map as M import qualified Data.List as L import Data.Maybe +import Game.LambdaHack.Utils.Assert import Game.LambdaHack.Point import Game.LambdaHack.Item import Game.LambdaHack.Actor@@ -35,25 +36,90 @@ -- | The grammatical object type. type Object = String +-- | Nouns with irregular plural spelling.+-- See http://en.wikipedia.org/wiki/English_plural.+irregularPlural :: M.Map String String+irregularPlural = M.fromList+ [ ("canto" , "cantos")+ , ("homo " , "homos")+ , ("photo" , "photos")+ , ("zero" , "zeros")+ , ("piano" , "pianos")+ , ("portico", "porticos")+ , ("pro" , "pros")+ , ("quarto" , "quartos")+ , ("kimono" , "kimonos")+ , ("calf" , "calves")+ , ("leaf" , "leaves")+ , ("knife" , "knives")+ , ("life" , "lives")+ , ("dwarf" , "dwarves")+ , ("hoof" , "hooves")+ , ("elf" , "elves")+ , ("staff" , "staves")+ , ("child" , "children")+ , ("foot" , "feet")+ , ("goose" , "geese")+ , ("louse" , "lice")+ , ("man" , "men")+ , ("mouse" , "mice")+ , ("tooth" , "teeth")+ , ("woman" , "women")+ ]++-- | The list of words with identical singular and plural form.+-- See http://en.wikipedia.org/wiki/English_plural.+noPlural :: S.Set String+noPlural = S.fromList+ [ "buffalo"+ , "deer"+ , "moose"+ , "sheep"+ , "bison"+ , "salmon"+ , "pike"+ , "trout"+ , "swine"+ , "aircraft"+ , "watercraft"+ , "spacecraft"+ , "hovercraft"+ , "information"+ ]+ -- | Tests if a character is a vowel (@u@ is too hard, so is @eu@). vowel :: Char -> Bool vowel l = l `elem` "aeio" -- | Adds the plural (@s@, @es@, @ies@) suffix to a word.+-- Used also for conjugation. -- See http://en.wikipedia.org/wiki/English_plural. suffixS :: String -> String suffixS word = case L.reverse word of- 'h' : 'c' : _ -> word ++ "es"- 'h' : 's' : _ -> word ++ "es"- 'i' : 's' : _ -> word ++ "es"- 's' : _ -> word ++ "es"- 'z' : _ -> word ++ "es"- 'x' : _ -> word ++ "es"- 'j' : _ -> word ++ "es"- 'o' : l : _ | not (vowel l) -> init word ++ "es"- 'y' : l : _ | not (vowel l) -> init word ++ "ies"- _ -> word ++ "s"+ 'h' : 'c' : _ -> word ++ "es"+ 'h' : 's' : _ -> word ++ "es"+ 'i' : 's' : _ -> word ++ "es"+ 's' : _ -> word ++ "es"+ 'z' : _ -> word ++ "es"+ 'x' : _ -> word ++ "es"+ 'j' : _ -> word ++ "es"+ 'o' : l : _ | not (vowel l) -> init word ++ "es"+ 'y' : l : _ | not (vowel l) -> init word ++ "ies"+ _ -> word ++ "s" +-- TODO: a suffix tree would be best, to catch ableman, seaman, etc.+pluralise :: Object -> Object+pluralise phrase =+ case reverse $ words phrase of+ [] -> assert `failure` "pluralise: no words"+ word : rest ->+ let pl = if word `S.member` noPlural+ then word+ else case M.lookup word irregularPlural of+ Just plural -> plural+ Nothing -> suffixS word+ in unwords $ reverse $ pl : rest+ conjugate :: String -> Verb -> Verb conjugate "you" "be" = "are" conjugate "You" "be" = "are"@@ -76,13 +142,15 @@ -- | Transform an object, adding a count and a plural suffix. makeObject :: Int -> (Object -> Object) -> Object -> Object makeObject 1 f obj = addIndefinite $ f obj-makeObject n f obj = show n ++ " " ++ f (suffixS obj)+makeObject n f obj = show n ++ " " ++ f (pluralise obj) -- TODO: when there's more of the above, split and move to Utils/ -- | How to refer to an item in object position of a sentence.-objectItem :: Kind.Ops ItemKind -> State -> Item -> Object-objectItem coitem@Kind.Ops{okind} state i =+-- If cheating is allowed, full identity of the object is revealed+-- together with its flavour (e.g. at game over screen).+objectItemCheat :: Kind.Ops ItemKind -> Bool -> State -> Item -> Object+objectItemCheat coitem@Kind.Ops{okind} cheat state i = let ik = jkind i kind = okind ik identified = L.length (iflavour kind) == 1 ||@@ -90,12 +158,21 @@ addSpace s = if s == "" then "" else " " ++ s eff = effectToSuffix (ieffect kind) pwr = if jpower i == 0 then "" else "(+" ++ show (jpower i) ++ ")"- adj name = if identified- then name ++ addSpace eff ++ addSpace pwr- else let flavour = getFlavour coitem (sflavour state) ik- in flavourToName flavour ++ " " ++ name+ adj name =+ let known = name ++ addSpace eff ++ addSpace pwr+ flavour = getFlavour coitem (sflavour state) ik+ obscured = flavourToName flavour ++ " " ++ name+ in if identified+ then known+ else if cheat+ then flavourToName flavour ++ " " ++ known+ else obscured in makeObject (jcount i) adj (iname kind) +-- | How to refer to an item in object position of a sentence.+objectItem :: Kind.Ops ItemKind -> State -> Item -> Object+objectItem coitem = objectItemCheat coitem False+ -- | How to refer to an actor in object position of a sentence. objectActor :: Kind.Ops ActorKind -> Actor -> Object objectActor Kind.Ops{oname} a =@@ -105,38 +182,42 @@ capActor :: Kind.Ops ActorKind -> Actor -> Object capActor coactor x = capitalize $ objectActor coactor x --- | Sentences such as \"Dog barks.\"-actorVerb :: Kind.Ops ActorKind -> Actor -> Verb -> String-actorVerb coactor a v =+-- | Sentences such as \"Dog barks loudly.\"+actorVerb :: Kind.Ops ActorKind -> Actor -> Verb -> String-> String+actorVerb coactor a v extra = let cactor = capActor coactor a verb = conjugate cactor v- in cactor ++ " " ++ verb ++ "."---- | Sentences such as \"Dog barks loudly.\"-actorVerbExtra :: Kind.Ops ActorKind -> Actor -> Verb -> String-> String-actorVerbExtra coactor a v extra =- L.init (actorVerb coactor a v) ++ " " ++ extra ++ "."+ ending | null extra = "."+ | otherwise = " " ++ extra ++ "."+ in cactor ++ " " ++ verb ++ ending -- | Sentences such as \"Dog quaffs a red potion fast.\"-actorVerbItemExtra :: Kind.COps -> State -> Actor -> Verb -> Item -> String+actorVerbItem :: Kind.COps -> State -> Actor -> Verb -> Item -> String -> String-actorVerbItemExtra Kind.COps{coactor, coitem} state a v i extra =- actorVerbExtra coactor a v $- objectItem coitem state i ++ extra+actorVerbItem Kind.COps{coactor, coitem} state a v i extra =+ let ending | null extra = ""+ | otherwise = " " ++ extra+ in actorVerb coactor a v $+ objectItem coitem state i ++ ending -- | Sentences such as \"Dog bites goblin furiously.\"-actorVerbActorExtra :: Kind.Ops ActorKind -> Actor -> Verb -> Actor -> String+actorVerbActor :: Kind.Ops ActorKind -> Actor -> Verb -> Actor -> String -> String-actorVerbActorExtra coactor a v b extra =- actorVerbExtra coactor a v $- objectActor coactor b ++ extra+actorVerbActor coactor a v b extra =+ let ending | null extra = ""+ | otherwise = " " ++ extra+ in actorVerb coactor a v $+ objectActor coactor b ++ ending -- | Sentences such as \"Dog gulps down a red potion fast.\"-actorVerbExtraItemExtra :: Kind.COps -> State -> Actor -> Verb -> String+actorVerbExtraItem :: Kind.COps -> State -> Actor -> Verb -> String -> Item -> String -> String-actorVerbExtraItemExtra Kind.COps{coactor, coitem} state a v extra1 i extra2 =- actorVerbExtra coactor a v $- extra1 ++ " " ++ objectItem coitem state i ++ extra2+actorVerbExtraItem Kind.COps{coactor, coitem} state a v extra1 i extra2 =+ assert (not $ null extra1) $+ let ending | null extra2 = ""+ | otherwise = " " ++ extra2+ in actorVerb coactor a v $+ extra1 ++ " " ++ objectItem coitem state i ++ ending -- | Produces a textual description of the terrain and items at an already -- explored location. Mute for unknown locations.@@ -158,12 +239,10 @@ where is = lvl `rememberAtI` loc prefixSee = if canSee then "You see " else "You remember "- prefixThere = if canSee- then "There are several objects here"- else "You remember several objects here" isd = case is of [] -> "" [i] -> prefixSee ++ objectItem coitem s i ++ "." [i,j] -> prefixSee ++ objectItem coitem s i ++ " and " ++ objectItem coitem s j ++ "."- _ -> prefixThere ++ if detailed then ":" else "."+ _ | detailed -> "Objects:"+ _ -> "Objects here."
Game/LambdaHack/HighScore.hs view
@@ -1,6 +1,6 @@ -- | High score table operations. module Game.LambdaHack.HighScore- ( Status(..), ScoreRecord(..), ScoreTable, restore, register, slideshow+ ( Status(..), ScoreRecord(..), register ) where import System.Directory@@ -14,6 +14,8 @@ import qualified Game.LambdaHack.Config as Config import Game.LambdaHack.Dungeon import Game.LambdaHack.Misc+import Game.LambdaHack.Time+import Game.LambdaHack.Msg -- TODO: add heroes' names, exp and level, cause of death, user number/name. -- Note: I tried using Date.Time, but got all kinds of problems,@@ -25,7 +27,7 @@ -- from the best to the worst, in lexicographic ordering wrt the fields below. data ScoreRecord = ScoreRecord { points :: !Int -- ^ the score- , negTurn :: !Int -- ^ number of turns (negated, so less better)+ , negTime :: !Time -- ^ game time spent (negated, so less better) , date :: !ClockTime -- ^ date of the last game interruption , status :: !Status -- ^ reason of the game interruption }@@ -36,7 +38,7 @@ Killed !LevelId -- ^ the player lost the game on the given level | Camping -- ^ game is supended | Victor -- ^ the player won- deriving (Eq, Ord)+ deriving (Show, Eq, Ord) instance Binary Status where put (Killed ln) = putWord8 0 >> put ln@@ -66,19 +68,28 @@ return (ScoreRecord p n (TOD cs cp) s) -- | Show a single high score, from the given ranking in the high score table.-showScore :: (Int, ScoreRecord) -> String+showScore :: (Int, ScoreRecord) -> [String] showScore (pos, score) = let died = case status score of Killed lvl -> "perished on level " ++ show (levelNumber lvl) ++ "," Camping -> "is camping somewhere," Victor -> "emerged victorious"- time = calendarTimeToString . toUTCTime . date $ score+ curDate = calendarTimeToString . toUTCTime . date $ score big = " " lil = " "- steps = negTurn score `div` (-10)- in printf- "%s\n%4d. %6d This adventuring party %s after %d steps \n%son %s. \n"- big pos (points score) died steps lil time+ turns = - (negTime score `timeFit` timeTurn)+ -- TODO: the spaces at the end are hand-crafted. Remove when display+ -- of overlays adds such spaces automatically.+ in [ printf+ "%s"+ big+ , printf+ "%4d. %6d This adventuring party %s after %d turns "+ pos (points score) died turns+ , printf+ "%son %s. "+ lil curDate+ ] -- | The list of scores, in decreasing order. type ScoreTable = [ScoreRecord]@@ -114,14 +125,14 @@ -- | Show a screenful of the high scores table. -- Parameter height is the number of (3-line) scores to be shown.-showTable :: ScoreTable -> Int -> Int -> String+showTable :: ScoreTable -> Int -> Int -> Overlay showTable h start height = let zipped = zip [1..] h screenful = take height . drop (start - 1) $ zipped- in L.concatMap showScore screenful+ in concatMap showScore screenful -- | Produce a couple of renderings of the high scores table.-slideshow :: Int -> ScoreTable -> Int -> [String]+slideshow :: Int -> ScoreTable -> Int -> [Overlay] slideshow pos h height = if pos <= height then [showTable h 1 height]@@ -130,7 +141,7 @@ -- | Take care of saving a new score to the table -- and return a list of messages to display.-register :: Config.CP -> Bool -> ScoreRecord -> IO (String, [String])+register :: Config.CP -> Bool -> ScoreRecord -> IO (String, [Overlay]) register config write s = do h <- restore config let (h', pos) = insertPos s h
Game/LambdaHack/Item.hs view
@@ -32,6 +32,7 @@ import Game.LambdaHack.Utils.Assert import Game.LambdaHack.Random import Game.LambdaHack.Content.ItemKind+import Game.LambdaHack.Content.RuleKind import qualified Game.LambdaHack.Color as Color import Game.LambdaHack.Flavour import qualified Game.LambdaHack.Kind as Kind@@ -239,9 +240,10 @@ strongestItem bitems $ \ i -> (ieffect $ okind $ jkind i) == Searching -- TODO: generalise, in particular take base damage into account-strongestSword :: Kind.Ops ItemKind -> [Item] -> Maybe Item-strongestSword Kind.Ops{osymbol} bitems =- strongestItem bitems $ \ i -> (osymbol $ jkind i) == ')'+strongestSword :: Kind.COps -> [Item] -> Maybe Item+strongestSword Kind.COps{coitem=Kind.Ops{osymbol}, corule} bitems =+ strongestItem bitems $ \ i -> (osymbol $ jkind i)+ `elem` (ritemMelee $ Kind.stdRuleset corule) strongestRegen :: Kind.Ops ItemKind -> [Item] -> Maybe Item strongestRegen Kind.Ops{okind} bitems =
Game/LambdaHack/ItemAction.hs view
@@ -9,6 +9,7 @@ import qualified Data.List as L import qualified Data.IntMap as IM import Data.Maybe+import Data.Ord import qualified Data.IntSet as IS import Game.LambdaHack.Utils.Assert@@ -25,18 +26,17 @@ import Game.LambdaHack.EffectAction import qualified Game.LambdaHack.Kind as Kind import Game.LambdaHack.Content.ItemKind-import qualified Game.LambdaHack.Feature as F-import qualified Game.LambdaHack.Tile as Tile+import Game.LambdaHack.Time -- | Display inventory-inventory :: Action ()+inventory :: ActionFrame () inventory = do items <- gets getPlayerItem if L.null items then abortWith "Not carrying anything." else do- displayItems "Carrying:" True items- abort+ io <- itemOverlay True False items+ displayOverlays "Carrying:" io -- | Let the player choose any item with a given group name. -- Note that this does not guarantee the chosen item belongs to the group,@@ -46,11 +46,11 @@ -> [Char] -- ^ accepted item symbols -> String -- ^ prompt -> String -- ^ how to refer to the collection of objects- -> Action (Maybe Item)+ -> Action Item getGroupItem is object syms prompt packName = do- Kind.Ops{osymbol} <- contentf Kind.coitem+ Kind.COps{coitem=Kind.Ops{osymbol}} <- getCOps let choice i = osymbol (jkind i) `elem` syms- header = capitalize $ suffixS object+ header = capitalize $ pluralise object getItem prompt choice header is packName applyGroupItem :: ActorId -- ^ actor applying the item (is on current level)@@ -58,124 +58,144 @@ -> Item -- ^ the item to be applied -> Action () applyGroupItem actor verb item = do- cops <- contentOps+ cops <- getCOps state <- get body <- gets (getActor actor)- per <- currentPerception+ per <- getPerception -- only one item consumed, even if several in inventory let consumed = item { jcount = 1 }- msg = actorVerbItemExtra cops state body verb consumed ""+ msg = actorVerbItem cops state body verb consumed "" loc = bloc body removeFromInventory actor consumed loc when (loc `IS.member` totalVisible per) $ msgAdd msg itemEffectAction 5 actor actor consumed- advanceTime actor playerApplyGroupItem :: Verb -> Object -> [Char] -> Action () playerApplyGroupItem verb object syms = do- Kind.Ops{okind} <- contentf Kind.coitem+ Kind.COps{coitem=Kind.Ops{okind}} <- getCOps is <- gets getPlayerItem- iOpt <- getGroupItem is object syms+ item <- getGroupItem is object syms ("What to " ++ verb ++ "?") "in inventory" pl <- gets splayer- case iOpt of- Just i -> applyGroupItem pl (iverbApply $ okind $ jkind i) i- Nothing -> neverMind True+ applyGroupItem pl (iverbApply $ okind $ jkind item) item projectGroupItem :: ActorId -- ^ actor projecting the item (is on current lvl)- -> Point -- ^ target location for the projecting+ -> Point -- ^ target location of the projectile -> Verb -- ^ how the projecting is called -> Item -- ^ the item to be projected -> Action ()-projectGroupItem source loc verb item = do- cops@Kind.COps{coactor, cotile} <- contentOps+projectGroupItem source tloc _verb item = do+ cops@Kind.COps{coactor} <- getCOps state <- get sm <- gets (getActor source)- per <- currentPerception+ per <- getPerception+ pl <- gets splayer+ Actor{btime} <- gets getPlayerBody lvl <- gets slevel- let -- TODO: refine for, e.g., wands of digging that are aimed into walls.- locWalkable = Tile.hasFeature cotile F.Walkable (lvl `at` loc)- consumed = item { jcount = 1 }+ ceps <- gets (ceps . scursor)+ lxsize <- gets (lxsize . slevel)+ lysize <- gets (lysize . slevel)+ let consumed = item { jcount = 1 } sloc = bloc sm+ sourceVis = sloc `IS.member` totalVisible per subject =- if sloc `IS.member` totalVisible per+ if sourceVis then sm else template (heroKindId coactor)- Nothing (Just "somebody") 99 sloc- msg = actorVerbItemExtra cops state subject verb consumed ""- removeFromInventory source consumed sloc- case locToActor loc state of- Just ta -> do- -- The msg describes the source part of the action.- when (sloc `IS.member` totalVisible per || isAHero ta) $ msgAdd msg- -- Msgs inside itemEffectAction describe the target part.- b <- itemEffectAction 10 source ta consumed- unless b $ modify (updateLevel (dropItemsAt [consumed] loc))- Nothing | locWalkable -> do- when (sloc `IS.member` totalVisible per) $ msgAdd msg- modify (updateLevel (dropItemsAt [consumed] loc))- _ -> abortWith "blocked"- advanceTime source+ Nothing (Just "somebody") 99 sloc timeZero animalParty+ -- When projecting, the first turn is spent aiming.+ -- The projectile is seen one tile from the actor, giving a hint+ -- about the aim and letting the target evade.+ msg = actorVerbItem cops state subject "aim" consumed ""+ -- TODO: AI should choose the best eps.+ eps = if source == pl then ceps else 0+ -- Setting monster's projectiles time to player time ensures+ -- the projectile covers the whole normal distance already the first+ -- turn that the player observes it moving. This removes+ -- the possibility of micromanagement by, e.g., waiting until+ -- the first distance is short.+ -- If and when monster all move at once, player's projectiles+ -- should be set to the time of the opposite party as well.+ -- Both parties would see their own projectiles move part of the way+ -- and the opposite party's projectiles waiting one turn.+ (party, time) =+ if bparty sm == heroParty || source == pl+ then (heroProjectiles, timeAdd btime (timeNegate timeClip))+ else (enemyProjectiles, btime)+ bl = bla lxsize lysize eps sloc tloc+ case bl of+ Nothing -> abortWith "cannot zap oneself"+ Just [] -> assert `failure` (sloc, tloc, "project from the edge of level")+ Just path@(loc:_) -> do+ let projVis = loc `IS.member` totalVisible per+ removeFromInventory source consumed sloc+ inhabitants <- gets (locToActor loc)+ if accessible cops lvl sloc loc && isNothing inhabitants+ then+ modify $ addProjectile cops consumed loc party path time+ else+ abortWith "blocked"+ when (sourceVis || projVis) $ msgAdd msg -playerProjectGroupItem :: Verb -> Object -> [Char] -> Action ()+playerProjectGroupItem :: Verb -> Object -> [Char] -> ActionFrame () playerProjectGroupItem verb object syms = do- ms <- gets (lmonsters . slevel)+ ms <- gets hostileList lxsize <- gets (lxsize . slevel) ploc <- gets (bloc . getPlayerBody)- if L.any (adjacent lxsize ploc) (L.map bloc $ IM.elems ms)+ if L.any (adjacent lxsize ploc) $ L.map bloc $+ L.filter (\ m -> bparty m `notElem` allProjectiles) ms then abortWith "You can't aim in melee." else playerProjectGI verb object syms -playerProjectGI :: Verb -> Object -> [Char] -> Action ()+playerProjectGI :: Verb -> Object -> [Char] -> ActionFrame () playerProjectGI verb object syms = do state <- get pl <- gets splayer ploc <- gets (bloc . getPlayerBody)- per <- currentPerception+ per <- getPerception let retarget msg = do msgAdd msg- updatePlayerBody (\ p -> p { btarget = TCursor })- let upd cursor = cursor {clocation=ploc}+ let upd cursor = cursor {clocation=ploc, ceps=0} modify (updateCursor upd)- targetMonster TgtAuto- -- TODO: draw digital line and see if obstacles prevent firing- -- TODO: don't tell the player if the tiles he can't see are reachable,- -- but let him throw there and let them land closer, if not reachable- -- TODO: similarly let him throw at walls and land in front (digital line)- case targetToLoc (totalVisible per) state of- Just loc | actorReachesLoc pl loc per (Just pl) -> do- Kind.Ops{okind} <- contentf Kind.coitem+ frs <- targetMonster TgtAuto+ -- Mark that unexpectedly it does not take time.+ modify (\ s -> s {snoTime = True})+ return frs+ case targetToLoc (totalVisible per) state ploc of+ Just loc -> do+ Kind.COps{coitem=Kind.Ops{okind}} <- getCOps is <- gets getPlayerItem- iOpt <- getGroupItem is object syms+ item <- getGroupItem is object syms ("What to " ++ verb ++ "?") "in inventory" targeting <- gets (ctargeting . scursor) when (targeting == TgtAuto) $ endTargeting True- case iOpt of- Just i -> projectGroupItem pl loc (iverbProject $ okind $ jkind i) i- Nothing -> neverMind True- Just _ -> retarget "Last target unreachable."+ projectGroupItem pl loc (iverbProject $ okind $ jkind item) item+ returnNoFrame () Nothing -> retarget "Last target invalid." --- TODO: also target a monster by moving the cursor, if in target monster mode.--- TODO: sort monsters by distance to the player.- -- | Start the monster targeting mode. Cycle between monster targets.-targetMonster :: TgtMode -> Action ()+targetMonster :: TgtMode -> ActionFrame () targetMonster tgtMode = do pl <- gets splayer- ms <- gets (lmonsters . slevel)- per <- currentPerception+ ploc <- gets (bloc . getPlayerBody)+ ms <- gets (hostileAssocs . slevel)+ per <- getPerception+ lxsize <- gets (lxsize . slevel) target <- gets (btarget . getPlayerBody) targeting <- gets (ctargeting . scursor)- let i = case target of- TEnemy (AMonster n) _ | targeting /= TgtOff -> n -- next monster- TEnemy (AMonster n) _ -> n - 1 -- try to retarget old monster- _ -> -1 -- try to target first monster (e.g., number 0)- dms = case pl of- AMonster n -> IM.delete n ms -- don't target yourself- AHero _ -> ms- (lt, gt) = IM.split i dms- gtlt = IM.assocs gt ++ IM.assocs lt+ -- TODO: sort monsters by distance to the player.+ let plms = L.filter ((/= pl) . fst) ms -- don't target yourself+ ordLoc (_, m) = (chessDist lxsize ploc $ bloc m, bloc m)+ dms = L.sortBy (comparing ordLoc) plms+ (lt, gt) = case target of+ TEnemy n _ | targeting /= TgtOff -> -- pick the next monster+ let i = fromMaybe (-1) $ L.findIndex ((== n) . fst) dms+ in L.splitAt (i + 1) dms+ TEnemy n _ -> -- try to retarget the old monster+ let i = fromMaybe (-1) $ L.findIndex ((== n) . fst) dms+ in L.splitAt i dms+ _ -> (dms, []) -- target first monster (e.g., number 0)+ gtlt = gt ++ lt seen (_, m) = let mloc = bloc m in mloc `IS.member` totalVisible per -- visible by any@@ -183,66 +203,84 @@ lf = L.filter seen gtlt tgt = case lf of [] -> target -- no monsters in sight, stick to last target- (na, nm) : _ -> TEnemy (AMonster na) (bloc nm) -- pick the next+ (na, nm) : _ -> TEnemy na (bloc nm) -- pick the next+ -- Register the chosen monster, to pick another on next invocation. updatePlayerBody (\ p -> p { btarget = tgt }) setCursor tgtMode -- | Start the floor targeting mode or reset the cursor location to the player.-targetFloor :: TgtMode -> Action ()+targetFloor :: TgtMode -> ActionFrame () targetFloor tgtMode = do ploc <- gets (bloc . getPlayerBody) target <- gets (btarget . getPlayerBody) targeting <- gets (ctargeting . scursor) let tgt = case target of- _ | targeting /= TgtOff -> TLoc ploc -- double key press: reset cursor TEnemy _ _ -> TCursor -- forget enemy target, keep the cursor+ _ | targeting /= TgtOff -> TLoc ploc -- double key press: reset cursor+ TPath _ -> TCursor t -> t -- keep the target from previous targeting session+ -- Register that we want to target only locations. updatePlayerBody (\ p -> p { btarget = tgt }) setCursor tgtMode -- | Set, activate and display cursor information.-setCursor :: TgtMode -> Action ()+setCursor :: TgtMode -> ActionFrame () setCursor tgtMode = assert (tgtMode /= TgtOff) $ do state <- get- per <- currentPerception+ per <- getPerception ploc <- gets (bloc . getPlayerBody) clocLn <- gets slid- let upd cursor@Cursor{ctargeting} =- let clocation = fromMaybe ploc (targetToLoc (totalVisible per) state)+ let upd cursor@Cursor{ctargeting, clocation=clocationOld, ceps=cepsOld} =+ let clocation =+ fromMaybe ploc (targetToLoc (totalVisible per) state ploc)+ ceps = if clocation == clocationOld then cepsOld else 0 newTgtMode = if ctargeting == TgtOff then tgtMode else ctargeting- in cursor { ctargeting = newTgtMode, clocation, clocLn }+ in cursor { ctargeting = newTgtMode, clocation, clocLn, ceps } modify (updateCursor upd) doLook +-- | Tweak the @eps@ parameter of the targeting digital line.+epsIncr :: Bool -> Action ()+epsIncr b = do+ targeting <- gets (ctargeting . scursor)+ if targeting /= TgtOff+ then modify $ updateCursor $+ \ c@Cursor{ceps} -> c {ceps = ceps + if b then 1 else -1}+ else neverMind True -- no visual feedback, so no sense+ -- | End targeting mode, accepting the current location or not. endTargeting :: Bool -> Action () endTargeting accept = do returnLn <- gets (creturnLn . scursor) target <- gets (btarget . getPlayerBody)- per <- currentPerception+ per <- getPerception cloc <- gets (clocation . scursor)- ms <- gets (lmonsters . slevel)- -- return to the original level of the player- modify (\ state -> state {slid = returnLn})+ ms <- gets (hostileAssocs . slevel)+ -- Return to the original level of the player. Note that this can be+ -- a different level than the one we started targeting at,+ -- if the player was changed while targeting.+ switchLevel returnLn modify (updateCursor (\ c -> c { ctargeting = TgtOff }))- case target of- TEnemy _ _ -> do- let canSee = IS.member cloc (totalVisible per)- when (accept && canSee) $- case L.find (\ (_im, m) -> bloc m == cloc) (IM.assocs ms) of- Just (im, m) ->- let tgt = TEnemy (AMonster im) (bloc m)- in updatePlayerBody (\ p -> p { btarget = tgt })- Nothing -> return ()- _ ->- if accept- then updatePlayerBody (\ p -> p { btarget = TLoc cloc })- else updatePlayerBody (\ p -> p { btarget = TCursor })- endTargetingMsg+ when accept $ do+ case target of+ TEnemy _ _ -> do+ -- If in monster targeting mode, switch to the monster under+ -- the current cursor location, if any.+ let canSee = IS.member cloc (totalVisible per)+ when (accept && canSee) $+ case L.find (\ (_im, m) -> bloc m == cloc) ms of+ Just (im, m) ->+ let tgt = TEnemy im (bloc m)+ in updatePlayerBody (\ p -> p { btarget = tgt })+ Nothing -> return ()+ _ -> updatePlayerBody (\ p -> p { btarget = TLoc cloc })+ if accept+ then endTargetingMsg+ else msgAdd "targeting canceled" endTargetingMsg :: Action () endTargetingMsg = do- cops <- contentf Kind.coactor+ Kind.COps{coactor} <- getCOps pbody <- gets getPlayerBody state <- get lxsize <- gets (lxsize . slevel)@@ -250,11 +288,12 @@ targetMsg = case btarget pbody of TEnemy a _ll -> if memActor a state- then objectActor cops $ getActor a state+ then objectActor coactor $ getActor a state else "a fear of the past" TLoc loc -> "location " ++ showPoint lxsize loc+ TPath _ -> "a path" TCursor -> "current cursor position continuously"- msgAdd $ actorVerbExtra cops pbody verb targetMsg+ msgAdd $ actorVerb coactor pbody verb targetMsg -- | Cancel something, e.g., targeting mode, resetting the cursor -- to the position of the player. Chosen target is not invalidated.@@ -267,32 +306,28 @@ -- | Accept something, e.g., targeting mode, keeping cursor where it was. -- Or perform the default action, if nothing needs accepting.-acceptCurrent :: Action () -> Action ()+acceptCurrent :: ActionFrame () -> ActionFrame () acceptCurrent h = do targeting <- gets (ctargeting . scursor) if targeting /= TgtOff- then endTargeting True+ then inFrame $ endTargeting True else h -- nothing to accept right now -- | Drop a single item. dropItem :: Action () dropItem = do -- TODO: allow dropping a given number of identical items.- cops <- contentOps+ cops <- getCOps pl <- gets splayer state <- get pbody <- gets getPlayerBody ploc <- gets (bloc . getPlayerBody)- items <- gets getPlayerItem- iOpt <- getAnyItem "What to drop?" items "inventory"- case iOpt of- Just stack -> do- let i = stack { jcount = 1 }- removeOnlyFromInventory pl i (bloc pbody)- msgAdd (actorVerbItemExtra cops state pbody "drop" i "")- modify (updateLevel (dropItemsAt [i] ploc))- Nothing -> neverMind True- playerAdvanceTime+ ims <- gets getPlayerItem+ stack <- getAnyItem "What to drop?" ims "in inventory"+ let item = stack { jcount = 1 }+ removeOnlyFromInventory pl item (bloc pbody)+ msgAdd (actorVerbItem cops state pbody "drop" item "")+ modify (updateLevel (dropItemsAt [item] ploc)) -- TODO: this is a hack for dropItem, because removeFromInventory -- makes it impossible to drop items if the floor not empty.@@ -332,10 +367,10 @@ actorPickupItem :: ActorId -> Action () actorPickupItem actor = do- cops@Kind.COps{coitem} <- contentOps+ cops@Kind.COps{coitem} <- getCOps state <- get pl <- gets splayer- per <- currentPerception+ per <- getPerception lvl <- gets slevel body <- gets (getActor actor) bitems <- gets (getActorItem actor)@@ -344,7 +379,7 @@ isPlayer = actor == pl -- check if something is here to pick up case lvl `atI` loc of- [] -> abortIfWith isPlayer "nothing here"+ [] -> abortWith "nothing here" i:is -> -- pick up first item; TODO: let pl select item; not for monsters case assignLetter (jletter i) (bletter body) bitems of Just l -> do@@ -352,18 +387,17 @@ -- msg depends on who picks up and if a hero can perceive it if isPlayer then msgAdd (letterLabel (jletter ni)- ++ objectItem coitem state ni)+ ++ objectItem coitem state ni ++ ".") else when perceived $ msgAdd $- actorVerbExtraItemExtra cops state body "pick" "up" i ""+ actorVerbExtraItem cops state body "pick" "up" i "" removeFromLoc i loc >>= assert `trueM` (i, is, loc, "item is stuck") -- add item to actor's inventory: updateAnyActor actor $ \ m -> m { bletter = maxLetter l (bletter body) } modify (updateAnyActorItem actor (const nitems))- Nothing -> abortIfWith isPlayer "cannot carry any more"- advanceTime actor+ Nothing -> abortWith "cannot carry any more" pickupItem :: Action () pickupItem = do@@ -381,18 +415,19 @@ -- known. In actor handlers we should make sure -- that messages are printed to the player only if the -- hero can perceive the action.--- Perhaps this means half of this code should be split and moved--- to ItemState, to be independent of any IO code from Action/Display. Actually, not, since the message display depends on Display. Unless we return a string to be displayed. -- TODO: you can drop an item already the floor, which works correctly, -- but is weird and useless. +allObjectsName :: String+allObjectsName = "Objects"+ -- | Let the player choose any item from a list of items. getAnyItem :: String -- ^ prompt -> [Item] -- ^ all items in question -> String -- ^ how to refer to the collection of items- -> Action (Maybe Item)-getAnyItem prompt = getItem prompt (const True) "Objects"+ -> Action Item+getAnyItem prompt = getItem prompt (const True) allObjectsName data ItemDialogState = INone | ISuitable | IAll deriving Eq @@ -403,58 +438,59 @@ -> String -- ^ how to describe suitable items -> [Item] -- ^ all items in question -> String -- ^ how to refer to the collection of items- -> Action (Maybe Item)+ -> Action Item getItem prompt p ptext is0 isn = do lvl <- gets slevel body <- gets getPlayerBody let loc = bloc body tis = lvl `atI` loc- floorMsg = if L.null tis then "" else " -,"- is = L.filter p is0+ floorFull = not $ null tis+ (floorMsg, floorKey) | floorFull = (", -", [K.Char '-'])+ | otherwise = ("", [])+ isp = L.filter p is0+ bestFull = not $ null isp+ (bestMsg, bestKey)+ | bestFull =+ let bestLetter = maybe "" (\ l -> ['(', l, ')']) $+ jletter $ L.maximumBy cmpItemLM isp+ in (", RET" ++ bestLetter, [K.Return])+ | otherwise = ("", []) cmpItemLM i1 i2 = cmpLetterMaybe (jletter i1) (jletter i2)+ keys ims =+ let mls = mapMaybe jletter ims+ ks = bestKey ++ floorKey ++ [K.Char '?'] ++ map K.Char mls+ in zip ks $ repeat K.NoModifier choice ims =- if L.null ims- then "[?," ++ floorMsg ++ " ESC]"+ if null ims+ then "[?" ++ floorMsg else let mls = mapMaybe jletter ims r = letterRange mls- ret = maybe "" (\ l -> ['(', l, ')']) $- jletter $ L.maximumBy cmpItemLM ims- in "[" ++ r ++ ", ?," ++ floorMsg ++ " RET" ++ ret ++ ", ESC]"+ in "[" ++ r ++ ", ?" ++ floorMsg ++ bestMsg ask = do when (L.null is0 && L.null tis) $ abortWith "Not carrying anything."- msgReset (prompt ++ " " ++ choice is)- displayAll- session nextCommand >>= perform ISuitable- perform itemDialogState command = do- let ims = if itemDialogState == INone then is0 else is- msgClear- case command of- K.Char '?' | itemDialogState == ISuitable -> do- -- filter for suitable items- b <- displayItems- (ptext ++ " " ++ isn ++ ". " ++ choice is) True is- if b then session (getOptionalConfirm (const ask)- (perform IAll))- else ask- K.Char '?' | itemDialogState == IAll -> do- -- show all items- b <- displayItems- ("Objects " ++ isn ++ ". " ++ choice is0) True is0- if b then session (getOptionalConfirm (const ask)- (perform INone))- else ask- K.Char '?' | itemDialogState == INone -> ask- K.Char '-' ->- case tis of- [] -> return Nothing- i:_rs -> -- use first item; TODO: let player select item- return $ Just i- K.Char l ->- return (L.find (maybe False (== l) . jletter) ims)- K.Return ->- if L.null ims- then return Nothing- else return $ Just $ L.maximumBy cmpItemLM ims- _ -> return Nothing+ perform INone+ perform itemDialogState = do+ let (ims, imsOver, msg) = case itemDialogState of+ INone -> (isp, [], prompt ++ " ")+ ISuitable -> (isp, isp, ptext ++ " " ++ isn ++ ". ")+ IAll -> (is0, is0, allObjectsName ++ " " ++ isn ++ ". ")+ io <- itemOverlay True False imsOver+ (command, modifier) <-+ displayChoiceUI (msg ++ choice ims) io (keys ims)+ assert (modifier == K.NoModifier) $+ case command of+ K.Char '?' -> case itemDialogState of+ INone -> perform ISuitable+ ISuitable | ptext /= allObjectsName -> perform IAll+ _ -> perform INone+ K.Char '-' | floorFull ->+ -- TODO: let player select item+ return $ L.maximumBy cmpItemLM tis+ K.Char l | l `elem` mapMaybe jletter ims ->+ let mitem = L.find (maybe False (== l) . jletter) ims+ in return $ fromJust mitem+ K.Return | bestFull ->+ return $ L.maximumBy cmpItemLM isp+ k -> assert `failure` "perform: unexpected key: " ++ show k ask
Game/LambdaHack/Key.hs view
@@ -1,6 +1,7 @@ -- | Frontend-independent keyboard input operations. module Game.LambdaHack.Key- ( Key(..), handleDir, moveBinding, keyTranslate+ ( Key(..), handleDir, dirAllMoveKey+ , moveBinding, keyTranslate, Modifier(..), showKM ) where import Prelude hiding (Left, Right)@@ -32,6 +33,12 @@ | Unknown !String -- ^ an unknown key, registered to warn the user deriving (Ord, Eq) +-- | Our own encoding of modifiers. Incomplete.+data Modifier =+ Control+ | NoModifier+ deriving (Ord, Eq)+ showKey :: Key -> String showKey (Char c) = [c] showKey Esc = "ESC" -- these three are common and terse abbreviations@@ -50,6 +57,11 @@ showKey (KP c) = "<KeyPad " ++ [c] ++ ">" showKey (Unknown s) = s +-- | Show a key with a modifier, if any.+showKM :: (Key, Modifier) -> String+showKM (key, Control) = "CTRL-" ++ showKey key+showKM (key, NoModifier) = showKey key+ instance Show Key where show = showKey @@ -59,32 +71,51 @@ dirViMoveKey :: [Key] dirViMoveKey = map Char dirViChar -dirViRunKey :: [Key]-dirViRunKey = map (Char . Char.toUpper) dirViChar- dirMoveKey :: [Key] dirMoveKey = [Home, Up, PgUp, Right, PgDn, Down, End, Left] +dirAllMoveKey :: [Key]+dirAllMoveKey = dirViMoveKey ++ dirMoveKey++dirViRunKey :: [Key]+dirViRunKey = map (Char . Char.toUpper) dirViChar+ dirRunKey :: [Key]-dirRunKey = map KP ['7', '8', '9', '6', '3', '2', '1', '4']+dirRunKey = map KP dirNums +_dirAllRunKey :: [Key]+_dirAllRunKey = dirViRunKey ++ dirRunKey++dirNums :: [Char]+dirNums = ['7', '8', '9', '6', '3', '2', '1', '4']++dirHeroKey :: [Key]+dirHeroKey = map Char dirNums+ -- | Configurable event handler for the direction keys. -- Used for directed commands such as close door.-handleDir :: X -> Key -> (Vector -> a) -> a -> a-handleDir lxsize e h k =+handleDir :: X -> (Key, Modifier) -> (Vector -> a) -> a -> a+handleDir lxsize (key, NoModifier) h k = let mvs = moves lxsize- assocs = zip dirViMoveKey mvs ++ zip dirMoveKey mvs- in maybe k h (L.lookup e assocs)+ assocs = zip dirAllMoveKey $ mvs ++ mvs+ in maybe k h (L.lookup key assocs)+handleDir _lxsize _ _h k = k +-- TODO: deduplicate -- | Binding of both sets of movement keys. moveBinding :: ((X -> Vector) -> a) -> ((X -> Vector) -> a)- -> [(Key, (String, a))]+ -> [((Key, Modifier), (String, Bool, a))] moveBinding move run =- let assign f (key, dir) = (key, ("", f dir))- in map (assign move) (zip dirViMoveKey movesWidth) ++- map (assign move) (zip dirMoveKey movesWidth) ++- map (assign run) (zip dirViRunKey movesWidth) ++- map (assign run) (zip dirRunKey movesWidth)+ let assign f (km, dir) = (km, ("", True, f dir))+ rNoModifier = repeat NoModifier+ rControl = repeat Control+ in map (assign move) (zip (zip dirViMoveKey rNoModifier) movesWidth) +++ map (assign move) (zip (zip dirMoveKey rNoModifier) movesWidth) +++ map (assign run) (zip (zip dirViRunKey rNoModifier) movesWidth) +++ map (assign run) (zip (zip dirRunKey rNoModifier) movesWidth) +++ map (assign run) (zip (zip dirMoveKey rControl) movesWidth) +++ map (assign run) (zip (zip dirRunKey rControl) movesWidth) +++ map (assign run) (zip (zip dirHeroKey rControl) movesWidth) -- | Translate key from a GTK string description to our internal key type. -- To be used, in particular, for the command bindings and macros@@ -104,6 +135,8 @@ keyTranslate "underscore" = Char '_' keyTranslate "minus" = Char '-' keyTranslate "KP_Subtract" = Char '-'+keyTranslate "plus" = Char '+'+keyTranslate "KP_Add" = Char '+' keyTranslate "bracketleft" = Char '[' keyTranslate "bracketright" = Char ']' keyTranslate "braceleft" = Char '{'
Game/LambdaHack/Kind.hs view
@@ -2,7 +2,7 @@ {-# LANGUAGE RankNTypes, ScopedTypeVariables, TypeFamilies #-} module Game.LambdaHack.Kind ( -- * General content types- Id, Speedup(..), Ops(..), COps(..), createOps+ Id, Speedup(..), Ops(..), COps(..), createOps, stdRuleset -- * Arrays of content identifiers , Array, (!), (//), listArray, array, bounds ) where@@ -69,13 +69,14 @@ groupFreq group k = fromMaybe 0 (L.lookup group $ getFreq k) kindFreq :: String -> Frequency (Id a, a) kindFreq group =- toFreq [ (n, (Id i, k))+ toFreq ("kindFreq ('" ++ group ++ "')")+ [ (n, (Id i, k)) | (i, k) <- kindAssocs, let n = groupFreq group k, n > 0 ] okind (Id i) = kindMap IM.! fromEnum i correct a = not (L.null (getName a)) && L.all ((> 0) . snd) (getFreq a) offenders = validate content in assert (allB correct content) $- assert (L.null offenders `blame` offenders) $+ assert (L.null offenders `blame` ("content not validated:", offenders)) $ Ops { osymbol = getSymbol . okind , oname = getName . okind@@ -104,6 +105,10 @@ , corule :: !(Ops RuleKind) , cotile :: !(Ops TileKind) }++-- | The standard ruleset used for level operations.+stdRuleset :: Ops RuleKind -> RuleKind+stdRuleset Ops{ouniqGroup, okind} = okind $ ouniqGroup "standard" instance Show COps where show _ = "Game content."
Game/LambdaHack/Level.hs view
@@ -2,13 +2,13 @@ -- as the game progresses. module Game.LambdaHack.Level ( -- * The @Level@ type and its components- Party, PartyItem, SmellMap, SecretMap, ItemMap, TileMap, Level(..)+ ActorDict, InvDict, SmellMap, SecretMap, ItemMap, TileMap, Level(..) -- * Level update- , updateHeroes, updateMonsters, updateHeroItem, updateMonItem+ , updateActorDict, updateInv , updateSmell, updateIMap, updateLMap, updateLRMap, dropItemsAt -- * Level query , at, rememberAt, atI, rememberAtI- , stdRuleset, accessible, openable, findLoc, findLocTry+ , accessible, openable, findLoc, findLocTry ) where import Data.Binary@@ -26,18 +26,19 @@ import Game.LambdaHack.Tile import qualified Game.LambdaHack.Feature as F import qualified Game.LambdaHack.Kind as Kind+import Game.LambdaHack.Time --- | All actors of a given side on the level.-type Party = IM.IntMap Actor+-- | All actors on the level, indexed by actor identifier.+type ActorDict = IM.IntMap Actor --- | Items carried by each party member.-type PartyItem = IM.IntMap [Item]+-- | Items carried by actors, indexed by actor identifier.+type InvDict = IM.IntMap [Item] -- | Current smell on map tiles. type SmellMap = IM.IntMap SmellTime -- | Current secrecy value on map tiles.-type SecretMap = IM.IntMap SecretStrength+type SecretMap = IM.IntMap SecretTime -- | Actual and remembered item lists on map tiles. type ItemMap = IM.IntMap ([Item], [Item])@@ -47,12 +48,10 @@ -- | A single, inhabited dungeon level. data Level = Level- { lheroes :: Party -- ^ all heroes on the level- , lheroItem :: PartyItem -- ^ hero items+ { lactor :: ActorDict -- ^ all actors on the level+ , linv :: InvDict -- ^ items belonging to actors , lxsize :: X -- ^ width of the level , lysize :: Y -- ^ height of the level- , lmonsters :: Party -- ^ all monsters on the level- , lmonItem :: PartyItem -- ^ monster items , lsmell :: SmellMap -- ^ smells , lsecret :: SecretMap -- ^ secrecy values , litem :: ItemMap -- ^ items on the ground@@ -61,18 +60,17 @@ , ldesc :: String -- ^ level description for the player , lmeta :: String -- ^ debug information from cave generation , lstairs :: (Point, Point) -- ^ destination of the (up, down) stairs+ , ltime :: Time -- ^ date of the last activity on the level } deriving Show -- | Update the hero and monster maps.-updateHeroes, updateMonsters :: (Party -> Party) -> Level -> Level-updateHeroes f lvl = lvl { lheroes = f (lheroes lvl) }-updateMonsters f lvl = lvl { lmonsters = f (lmonsters lvl) }+updateActorDict :: (ActorDict -> ActorDict) -> Level -> Level+updateActorDict f lvl = lvl { lactor = f (lactor lvl) } -- | Update the hero items and monster items maps.-updateHeroItem, updateMonItem :: (PartyItem -> PartyItem) -> Level -> Level-updateHeroItem f lvl = lvl { lheroItem = f (lheroItem lvl) }-updateMonItem f lvl = lvl { lmonItem = f (lmonItem lvl) }+updateInv :: (InvDict -> InvDict) -> Level -> Level+updateInv f lvl = lvl { linv = f (linv lvl) } -- | Update the smell map. updateSmell :: (SmellMap -> SmellMap) -> Level -> Level@@ -98,13 +96,11 @@ in updateIMap (IM.alter adj loc) instance Binary Level where- put (Level hs hi sx sy ms mi ls le li lm lrm ld lme lstairs) = do- put hs- put hi+ put (Level ad ia sx sy ls le li lm lrm ld lme lstairs ltime) = do+ put ad+ put ia put sx put sy- put ms- put mi put ls put le put (assert@@ -116,13 +112,12 @@ put ld put lme put lstairs+ put ltime get = do- hs <- get- hi <- get+ ad <- get+ ia <- get sx <- get sy <- get- ms <- get- mi <- get ls <- get le <- get li <- get@@ -131,7 +126,8 @@ ld <- get lme <- get lstairs <- get- return (Level hs hi sx sy ms mi ls le li lm lrm ld lme lstairs)+ ltime <- get+ return (Level ad ia sx sy ls le li lm lrm ld lme lstairs ltime) -- | Query for actual and remembered tile kinds on the map. at, rememberAt :: Level -> Point -> Kind.Id TileKind@@ -144,28 +140,24 @@ atI Level{litem} p = fst $ IM.findWithDefault ([], []) p litem rememberAtI Level{litem} p = snd $ IM.findWithDefault ([], []) p litem --- | The standard ruleset used for level operations.-stdRuleset :: Kind.Ops RuleKind -> RuleKind-stdRuleset Kind.Ops{ouniqGroup, okind} = okind $ ouniqGroup "standard"- -- | Check whether one location is accessible from another, -- using the formula from the standard ruleset. accessible :: Kind.COps -> Level -> Point -> Point -> Bool accessible Kind.COps{ cotile=Kind.Ops{okind=okind}, corule} lvl@Level{lxsize} sloc tloc =- let check = raccessible $ stdRuleset corule+ let check = raccessible $ Kind.stdRuleset corule src = okind $ lvl `at` sloc tgt = okind $ lvl `at` tloc in check lxsize sloc src tloc tgt -- | Check whether the location contains a door of secrecy lower than @k@ -- and that can be opened according to the standard ruleset.-openable :: Kind.Ops TileKind -> Level -> SecretStrength -> Point -> Bool+openable :: Kind.Ops TileKind -> Level -> SecretTime -> Point -> Bool openable cops lvl@Level{lsecret} k target = let tgt = lvl `at` target in hasFeature cops F.Openable tgt || (hasFeature cops F.Hidden tgt &&- lsecret IM.! target < k)+ lsecret IM.! target <= k) -- | Find a random location on the map satisfying a predicate. findLoc :: TileMap -> (Point -> Kind.Id TileKind -> Bool) -> Rnd Point
Game/LambdaHack/Misc.hs view
@@ -1,14 +1,11 @@ -- | Hacks that haven't found their home yet. module Game.LambdaHack.Misc- ( normalLevelBound, Time, divUp, Freqs+ ( normalLevelBound, divUp, Freqs ) where -- | Level bounds. TODO: query terminal size instead and scroll view. normalLevelBound :: (Int, Int) normalLevelBound = (79, 21)---- | Game time in turns. The time dimension.-type Time = Int -- | Integer division, rounding up. divUp :: Int -> Int -> Int
Game/LambdaHack/Msg.hs view
@@ -1,50 +1,165 @@ -- | Game messages displayed on top of the screen for the player to read. module Game.LambdaHack.Msg- ( Msg, more, msgEnd, yesno, addMsg, splitMsg, padMsg+ ( Msg, moreMsg, yesnoMsg, padMsg+ , Report, emptyReport, nullReport, singletonReport, addMsg+ , splitReport, renderReport+ , History, emptyHistory, singletonHistory, addReport, renderHistory+ , takeHistory+ , Overlay, splitOverlay, stringByLocation ) where import qualified Data.List as L import Data.Char+import Data.Binary+import qualified Data.ByteString.Char8 as BS+import qualified Data.IntMap as IM --- | The type of messages.-type Msg = String+import Game.LambdaHack.Misc+import Game.LambdaHack.PointXY --- | The \"press something to see more\" mark.-more :: Msg-more = " --more-- "+-- | The type of a single message.+type Msg = String --- | The \"the end of overlays or messages\" mark.-msgEnd :: Msg-msgEnd = " --end-- "+-- | The \"press something to see more\" mark.+moreMsg :: Msg+moreMsg = " --more-- " -- | The confirmation request message.-yesno :: Msg-yesno = " [yn]"+yesnoMsg :: Msg+yesnoMsg = " [yn]" --- | Append two messages.-addMsg :: Msg -> Msg -> Msg-addMsg [] x = x-addMsg xs [] = xs-addMsg xs x = xs ++ " " ++ x+-- | Add spaces at the message end, for display overlayed over the level map.+-- Also trims (does not wrap!) too long lines.+padMsg :: X -> String -> String+padMsg w xs =+ let len = length xs+ rev = reverse xs+ in case compare w len of+ LT -> reverse $ '$' : drop (len - w + 1) rev+ EQ -> xs+ GT -> case rev of+ [] -> xs+ ' ' : _ -> xs+ _ -> reverse $ ' ' : rev --- | Split a message into chunks that fit in one line.-splitMsg :: Int -> Msg -> Int -> [String]-splitMsg w xs m- | w <= m = [xs] -- border case, we cannot make progress- | w >= length xs = [xs] -- no problem, everything fits+-- | The type of a set of messages to show at the screen at once.+newtype Report = Report [(BS.ByteString, Int)]+ deriving Show++instance Binary Report where+ put (Report x) = put x+ get = fmap Report get++-- | Empty set of messages.+emptyReport :: Report+emptyReport = Report []++-- | Test if the set of messages is empty.+nullReport :: Report -> Bool+nullReport (Report l) = null l++-- | Construct a singleton set of messages.+singletonReport :: Msg -> Report+singletonReport m = addMsg emptyReport m++-- | Add message to the end of report.+addMsg :: Report -> Msg -> Report+addMsg r "" = r+addMsg (Report ((x, n) : xns)) y' | x == y =+ Report $ (y, n + 1) : xns+ where y = BS.pack y'+addMsg (Report xns) y = Report $ (BS.pack y, 1) : xns++-- | Split a messages into chunks that fit in one line.+-- We assume the width of the messages line is the same as of level map.+splitReport :: Report -> [String]+splitReport r =+ let w = fst normalLevelBound + 1+ in splitString w $ renderReport r++-- | Render a report as a (possibly very long) string.+renderReport ::Report -> String+renderReport (Report []) = ""+renderReport (Report [xn]) = renderRepetition xn+renderReport (Report (xn : xs)) =+ renderReport (Report xs) ++ " " ++ renderRepetition xn++renderRepetition :: (BS.ByteString, Int) -> String+renderRepetition (s, 1) = BS.unpack s+renderRepetition (s, n) = BS.unpack s ++ "<x" ++ show n ++ ">"++-- | Split a string into lines. Avoids ending the line with a character+-- other than whitespace or punctuation. Space characters are removed+-- from hte start, but never from the end of lines.+splitString :: X -> String -> [String]+splitString w xs = splitString' w $ dropWhile isSpace xs++splitString' :: X -> String -> [String]+splitString' w xs+ | w <= 0 = [xs] -- border case, we cannot make progress+ | w >= length xs = [xs] -- no problem, everything fits | otherwise =- let (pre, post) = splitAt (w - m) xs+ let (pre, post) = splitAt w xs (ppre, ppost) = break (`elem` " .,:;!?") $ reverse pre- rpost = dropWhile isSpace ppost- in if L.null rpost- then pre : splitMsg w post m- else reverse rpost : splitMsg w (reverse ppre ++ post) m+ testPost = dropWhile isSpace ppost+ in if L.null testPost+ then pre : splitString w post+ else reverse ppost : splitString w (reverse ppre ++ post) --- | Add spaces at the message end, for display overlayed over the level map.-padMsg :: Int -> String -> String-padMsg w xs =- case L.reverse xs of- [] -> xs- ' ' : _ -> xs- _ | w == length xs -> xs- reversed -> L.reverse $ ' ' : reversed+-- | The history of reports.+newtype History = History [Report]+ deriving Show++instance Binary History where+ put (History x) = put x+ get = fmap History get++-- | Empty history of reports.+emptyHistory :: History+emptyHistory = History []++-- | Construct a singleton history of reports.+singletonHistory :: Report -> History+singletonHistory r = addReport r emptyHistory++-- | Render history as many lines of text, wrapping if necessary.+renderHistory :: History -> Overlay+renderHistory (History h) = L.concatMap splitReport h++-- | Add a report to history, handling repetitions.+addReport :: Report -> History -> History+addReport (Report []) h = h+addReport m (History []) = History [m]+addReport (Report m) (History (Report h : hs)) =+ case (reverse m, h) of+ ((s1, n1) : rs, (s2, n2) : hhs) | s1 == s2 ->+ let hist = Report ((s2, n1 + n2) : hhs) : hs+ in History $ if null rs then hist else Report (reverse rs) : hist+ _ -> History $ Report m : Report h : hs++-- | Take the given prefix of reports from a history.+takeHistory :: Int -> History -> History+takeHistory k (History h) = History $ take k h++-- | A screenful of text lines. When displayed, they are trimmed, not wrapped+-- and any lines below the lower screen edge are not visible.+type Overlay = [String]++-- | Split an overlay into overlays that fit on the screen.+splitOverlay :: Y -> Overlay -> [Overlay]+splitOverlay _ [] = [] -- nothing to print over the level area+splitOverlay lysize ls | length ls <= lysize = [ls] -- all fits on one screen+splitOverlay lysize ls = let (pre, post) = splitAt (lysize - 1) ls+ in pre : splitOverlay lysize post++-- | Returns a function that looks up the characters in the+-- string by location. Takes the height of the display plus+-- the string. Returns also the message to print at the top+-- and number of screens required to display all of the string.+stringByLocation :: X -> Y -> Overlay -> (String, PointXY -> Maybe Char)+stringByLocation _ _ [] = ("", const Nothing)+stringByLocation lxsize lysize (msgTop : ls) =+ let over = map (padMsg lxsize) $ take lysize ls+ m = IM.fromDistinctAscList $+ zip [0..] (L.map (IM.fromList . zip [0..]) over)+ in (msgTop, \ (PointXY (x, y)) -> IM.lookup y m >>= \ n -> IM.lookup x n)
Game/LambdaHack/Perception.hs view
@@ -1,6 +1,7 @@ -- | Actors perceiving other actors and the dungeon level. module Game.LambdaHack.Perception- ( Perception, totalVisible, debugTotalReachable, perception+ ( DungeonPerception, Perception+ , totalVisible, debugTotalReachable, dungeonPerception , actorReachesLoc, actorReachesActor, monsterSeesHero ) where @@ -15,6 +16,7 @@ import Game.LambdaHack.Level import Game.LambdaHack.Actor import Game.LambdaHack.ActorState+import Game.LambdaHack.Dungeon import Game.LambdaHack.Content.ActorKind import Game.LambdaHack.FOV import qualified Game.LambdaHack.Config as Config@@ -30,10 +32,14 @@ { pvisible :: IS.IntSet } +-- | The type representing the perception for all levels in the dungeon.+type DungeonPerception = [(LevelId, Perception)]++-- | The type representing the perception of all actors on the level.+-- -- Note: Heroes share visibility and only have separate reachability.--- The pplayer field must be void if the player is not on the current level.+-- The pplayer field must be void on all levels except where he resides. -- Right now, the field is used only for player-controlled monsters.--- | The type representing the perception of all actors on the level. data Perception = Perception { pplayer :: Maybe PerceptionReachable , pheroes :: IM.IntMap PerceptionReachable@@ -56,15 +62,13 @@ -- Defaults to false if the actor is not player-controlled (monster or hero). actorReachesLoc :: ActorId -> Point -> Perception -> Maybe ActorId -> Bool actorReachesLoc actor loc per pl =- let tryHero = case actor of- AMonster _ -> Nothing- AHero i -> do- hper <- IM.lookup i (pheroes per)- return $ loc `IS.member` preachable hper+ let tryHero = do+ hper <- IM.lookup actor (pheroes per)+ return $ loc `IS.member` preachable hper tryPl = do -- the case for a monster under player control- guard $ Just actor == pl- pper <- pplayer per- return $ loc `IS.member` preachable pper+ guard $ Just actor == pl+ pper <- pplayer per+ return $ loc `IS.member` preachable pper tryAny = tryHero `mplus` tryPl in fromMaybe False tryAny -- assume not visible, if no perception found @@ -81,55 +85,60 @@ -- TODO: When the code for throwing, digital lines and lights is complete. -- make this a special case of ActorSeesActor.--- | Whether a monster can see a hero (@False@ if the target is not a hero).+-- | Whether a monster can see a hero (@False@ if the target has+-- no perceptions, e.g., not a hero or a hero without perception, due+-- to being spawned on the same turn by a monster and then seen by another). -- An approximation, to avoid computing FOV for the monster.--- If the target actor has no FOV pre-computed, we assume it can't be seen. monsterSeesHero :: Kind.Ops TileKind -> Perception -> Level -> ActorId -> ActorId -> Point -> Point -> Bool monsterSeesHero cotile per lvl _source target sloc tloc = let rempty = PerceptionReachable IS.empty- reachable@PerceptionReachable{preachable} = case target of- AMonster _ -> rempty- -- TODO: instead of this fromMaybe clean up how Perception- -- is regenerated when levels are switched- AHero i -> fromMaybe rempty $ IM.lookup i $ pheroes per+ reachable@PerceptionReachable{preachable} =+ fromMaybe rempty $ IM.lookup target $ pheroes per in sloc `IS.member` preachable && isVisible cotile reachable lvl IS.empty tloc -- | Calculate the perception of all actors on the level.-perception :: Kind.COps -> State -> Perception-perception cops@Kind.COps{cotile}- state@State{ splayer- , sconfig- , sdebug = DebugMode{smarkVision}- } =- let lvl@Level{lheroes = hs} = slevel state- mode = Config.get sconfig "engine" "fovMode"+dungeonPerception :: Kind.COps -> State -> DungeonPerception+dungeonPerception cops s@State{slid, sdungeon} =+ let lvlPer (ln, lvl) = (ln, levelPerception cops s lvl)+ in map lvlPer $ currentFirst slid sdungeon++-- | Calculate the perception of all actors on the level.+levelPerception :: Kind.COps -> State -> Level -> Perception+levelPerception cops@Kind.COps{cotile}+ state@State{ splayer+ , sconfig+ , sdebug = DebugMode{smarkVision}+ }+ lvl@Level{lactor} =+ let mode = Config.get sconfig "engine" "fovMode" radius = let r = Config.get sconfig "engine" "fovRadius" in if r < 1 then error $ "FOV radius is " ++ show r ++ ", should be >= 1" else r -- Perception for a player-controlled monster on the current level. mLocPer =- if isAMonster splayer && memActor splayer state+ if not (isAHero state splayer) && IM.member splayer lactor then let m = getPlayerBody state in Just (bloc m, computeReachable cops radius mode smarkVision m lvl) else Nothing (mLoc, mPer) = (fmap fst mLocPer, fmap snd mLocPer)+ hs = IM.filter (\ m -> bparty m == heroParty) lactor pers = IM.map (\ h -> computeReachable cops radius mode smarkVision h lvl) hs- locs = IM.map bloc hs+ locs = map bloc $ IM.elems hs lpers = maybeToList mPer ++ IM.elems pers reachable = PerceptionReachable $ IS.unions (map preachable lpers) -- TODO: Instead of giving the monster a light source, alter vision. playerControlledMonsterLight = maybeToList mLoc- lights = IS.fromList $ playerControlledMonsterLight ++ IM.elems locs+ lights = IS.fromList $ playerControlledMonsterLight ++ locs visible = computeVisible cotile reachable lvl lights- in Perception { pplayer = mPer- , pheroes = pers- , ptotal = visible- }+ in Perception { pplayer = mPer+ , pheroes = pers+ , ptotal = visible+ } -- | A location can be directly lit by an ambient shine or a weak, portable -- light source, e.g,, carried by a hero. (Only lights of radius 0
Game/LambdaHack/Place.hs view
@@ -17,6 +17,7 @@ import Game.LambdaHack.Misc import Game.LambdaHack.Content.TileKind import Game.LambdaHack.Random+import Game.LambdaHack.Content.CaveKind -- TODO: use more, rewrite as needed, document each field. -- | The parameters of a place. Most are immutable and set@@ -50,7 +51,7 @@ -- | The map of tile kinds in a place (and generally anywhere in a cave). -- The map is sparse. The default tile that eventually fills the empty spaces--- is specified in the cave kind specification with @cdefTile@.+-- is specified in the cave kind specification with @cdefaultTile@. type TileMapXY = M.Map PointXY (Kind.Id TileKind) -- | For @CAlternate@ tiling, require the place be comprised@@ -85,20 +86,21 @@ -- | Given a few parameters, roll and construct a 'Place' datastructure -- and fill a cave section acccording to it. buildPlace :: Kind.COps -- ^ the game content- -> Kind.Id TileKind -- ^ fence tile, if fence solid+ -> CaveKind -- ^ current cave kind -> Kind.Id TileKind -- ^ fence tile, if fence hollow- -> RollDeep -- ^ the chance of a dark place -> Int -- ^ current level depth -> Int -- ^ maximum depth -> Area -- ^ interior area of the place -> Rnd (TileMapXY, Place)-buildPlace Kind.COps{cotile, coplace=Kind.Ops{okind=pokind, opick=popick}}- qsolidFence qhollowFence cdarkChance lvl depth- r = assert (not (trivialArea r) `blame` r) $ do- dark <- chanceDeep lvl depth cdarkChance+buildPlace Kind.COps{ cotile=cotile@Kind.Ops{opick=opick}+ , coplace=Kind.Ops{okind=pokind, opick=popick} }+ CaveKind{..} qhollowFence ln depth r+ = assert (not (trivialArea r) `blame` r) $ do+ qsolidFence <- opick cfillerTile (const True)+ dark <- chanceDeep ln depth cdarkChance qkind <- popick "rogue" (placeValid r) let kr = pokind qkind- qlegend = if dark then "darkLegend" else "litLegend"+ qlegend = if dark then cdarkLegendTile else clitLegendTile qseen = False qarea = expandFence (pfence kr) r place = assert (validArea qarea `blame` qarea) $
Game/LambdaHack/Point.hs view
@@ -2,9 +2,11 @@ module Game.LambdaHack.Point ( Point, toPoint, showPoint , origin, chessDist, adjacent, vicinity, vicinityCardinal- , inside, displacement+ , inside, displacementXYZ, bla ) where +import qualified Data.List as L+ import Game.LambdaHack.PointXY import Game.LambdaHack.VectorXY import Game.LambdaHack.Area@@ -77,9 +79,22 @@ inside :: X -> Point -> Area -> Bool inside lxsize loc = insideXY $ fromPoint lxsize loc --- | Calculate the displacement vector of a location wrt another location.-displacement :: X -> Point -> Point -> VectorXY-displacement lxsize loc0 loc1+-- | Calculate the displacement vector from a location to another.+displacementXYZ :: X -> Point -> Point -> VectorXY+displacementXYZ lxsize loc0 loc1 | PointXY (x0, y0) <- fromPoint lxsize loc0 , PointXY (x1, y1) <- fromPoint lxsize loc1 = VectorXY (x1 - x0, y1 - y0)++-- | Bresenham's line algorithm generalized to arbitrary starting @eps@+-- (@eps@ value of 0 gives the standard BLA).+-- Skips the source point and goes through the second point+-- to the edge of the level. GIves @Nothing@ if the points are equal.+bla :: X -> Y -> Int -> Point -> Point -> Maybe [Point]+bla _ _ _ source target | source == target = Nothing+bla lxsize lysize eps source target = Just $+ let s = fromPoint lxsize source+ e = fromPoint lxsize target+ inBounds p@(PointXY (x, y)) =+ lxsize > x && x >= 0 && lysize > y && y >= 0 && p /= s+ in L.map (toPoint lxsize) $ L.takeWhile inBounds $ L.tail $ blaXY eps s e
Game/LambdaHack/PointXY.hs view
@@ -1,6 +1,6 @@ -- | Basic cartesian geometry operations on 2D points. module Game.LambdaHack.PointXY- ( X, Y, PointXY(..), fromTo, sortPointXY+ ( X, Y, PointXY(..), fromTo, sortPointXY, blaXY ) where import qualified Data.List as L@@ -39,3 +39,22 @@ sortPointXY :: (PointXY, PointXY) -> (PointXY, PointXY) sortPointXY (a, b) | a <= b = (a, b) | otherwise = (b, a)++-- | See <http://roguebasin.roguelikedevelopment.org/index.php/Digital_lines>.+balancedWord :: Int -> Int -> Int -> [Int]+balancedWord p q eps | eps + p < q = 0 : balancedWord p q (eps + p)+balancedWord p q eps = 1 : balancedWord p q (eps + p - q)++-- | Bresenham's line algorithm generalized to arbitrary starting @eps@+-- (@eps@ value of 0 gives the standard BLA). Includes the source point+-- and goes through the target point to infinity.+blaXY :: Int -> PointXY -> PointXY -> [PointXY]+blaXY eps (PointXY (x0, y0)) (PointXY (x1, y1)) =+ let (dx, dy) = (x1 - x0, y1 - y0)+ xyStep b (x, y) = (x + signum dx, y + signum dy * b)+ yxStep b (x, y) = (x + signum dx * b, y + signum dy)+ (p, q, step) | abs dx > abs dy = (abs dy, abs dx, xyStep)+ | otherwise = (abs dx, abs dy, yxStep)+ bw = balancedWord p q (eps `mod` (max 1 q))+ walk w xy = xy : walk (tail w) (step (head w) xy)+ in L.map PointXY $ walk bw (x0, y0)
Game/LambdaHack/Random.hs view
@@ -110,7 +110,7 @@ assert (n > 0 && n <= depth `blame` (n, depth)) $ do r1 <- rollDice d1 r2 <- rollDice d2- return $ r1 + ((n - 1) * r2) `div` (depth - 1)+ return $ r1 + ((n - 1) * r2) `div` max 1 (depth - 1) -- | Roll dice scaled with current level depth and return @True@ -- if the results if greater than 50.
Game/LambdaHack/Running.hs view
@@ -5,7 +5,6 @@ import Control.Monad.State hiding (State, state) import qualified Data.List as L-import qualified Data.IntMap as IM import qualified Data.IntSet as IS import Game.LambdaHack.Utils.Assert@@ -25,26 +24,30 @@ import qualified Game.LambdaHack.Feature as F -- | Start running in the given direction and with the given number--- of tiles already traversed (usually 0). The first step of running--- succeeds much more often than subsequent steps, because most+-- of tiles already traversed (usually 0). The first turn of running+-- succeeds much more often than subsequent turns, because most -- of the disturbances are ignored, since the player is aware of them -- and still explicitly requests a run.-run :: (Vector, Int) -> Action ()+run :: (Vector, Int) -> ActionFrame () run (dir, dist) = do- cops <- contentOps+ cops <- getCOps pl <- gets splayer locHere <- gets (bloc . getPlayerBody) lvl <- gets slevel targeting <- gets (ctargeting . scursor) if targeting /= TgtOff- then moveCursor dir 10+ then do+ frs <- moveCursor dir 10+ -- Mark that unexpectedly it does not take time.+ modify (\ s -> s {snoTime = True})+ return frs else do let accessibleDir loc d = accessible cops lvl loc (loc `shift` d) -- Do not count distance if we just open a door. distNew = if accessibleDir locHere dir then dist + 1 else dist updatePlayerBody (\ p -> p { bdir = Just (dir, distNew) })- -- attacks and opening doors disallowed while running- moveOrAttack False pl dir+ -- Attacks and opening doors disallowed when continuing to run.+ inFrame $ moveOrAttack False pl dir -- | Player running mode, determined from the nearby cave layout. data RunMode =@@ -85,19 +88,20 @@ RunCorridor (if diagonal lxsize d1 then d2 else d1, True) _ -> RunHub -- a hub of many separate corridors --- | Check for disturbances to running such newly visible items, monsters, etc.-runDisturbance :: Point -> Int -> Msg -> Party -> Party -> Perception -> Point+-- | Check for disturbances to running such as newly visible items, monsters.+runDisturbance :: Point -> Int -> Report+ -> [Actor] -> [Actor] -> Perception -> Point -> (F.Feature -> Point -> Bool) -> (Point -> Bool) -> X -> Y -> (Vector, Int) -> Maybe (Vector, Int) runDisturbance locLast distLast msg hs ms per locHere locHasFeature locHasItems lxsize lysize (dirNew, distNew) =- let msgShown = not (L.null msg)- mslocs = IS.delete locHere $ IS.fromList (L.map bloc (IM.elems ms))+ let msgShown = not $ nullReport msg+ mslocs = IS.delete locHere $ IS.fromList (L.map bloc ms) enemySeen = not (IS.null (mslocs `IS.intersection` totalVisible per)) surrLast = locLast : vicinity lxsize lysize locLast surrHere = locHere : vicinity lxsize lysize locHere locThere = locHere `shift` dirNew- heroThere = locThere `elem` L.map bloc (IM.elems hs)+ heroThere = locThere `elem` L.map bloc hs -- Stop if you touch any individual tile with these propereties -- first time, unless you enter it next move, in which case stop then. touchList = [ locHasFeature F.Exit@@ -151,12 +155,12 @@ -- and it increments the counter of traversed tiles. continueRun :: (Vector, Int) -> Action () continueRun (dirLast, distLast) = do- cops@Kind.COps{cotile} <- contentOps+ cops@Kind.COps{cotile} <- getCOps locHere <- gets (bloc . getPlayerBody)- per <- currentPerception- msg <- currentMsg- ms <- gets (lmonsters . slevel)- hs <- gets (lheroes . slevel)+ per <- getPerception+ Diary{sreport} <- getDiary+ ms <- gets dangerousList+ hs <- gets friendlyList lvl@Level{lxsize, lysize} <- gets slevel let locHasFeature f loc = Tile.hasFeature cotile f (lvl `at` loc) locHasItems loc = not $ L.null $ lvl `atI` loc@@ -164,7 +168,7 @@ tryRunDist (dir, distNew) | accessibleDir locHere dir = maybe abort run $- runDisturbance locLast distLast msg hs ms per locHere+ runDisturbance locLast distLast sreport hs ms per locHere locHasFeature locHasItems lxsize lysize (dir, distNew) | otherwise = abort -- do not open doors in the middle of a run tryRun dir = tryRunDist (dir, distLast)@@ -173,7 +177,7 @@ openableDir loc dir = Tile.hasFeature cotile F.Openable (lvl `at` (loc `shift` dir)) dirEnterable loc d = accessibleDir loc d || openableDir loc d- case runMode locHere dirLast dirEnterable lxsize of+ ((), frames) <- case runMode locHere dirLast dirEnterable lxsize of RunDeadEnd -> abort -- we don't run backwards RunOpen -> tryRun dirLast -- run forward into the open space RunHub -> abort -- stop and decide where to go@@ -185,3 +189,4 @@ RunHub | turn -> abort -- stop and decide when to turn RunOpen -> tryRunAndStop dirNext -- no turn, get closer and stop RunHub -> tryRunAndStop dirNext -- no turn, get closer and stop+ when (not $ null frames) abort -- something serious happened, stop running
Game/LambdaHack/Save.hs view
@@ -1,12 +1,14 @@ -- | Saving and restoring games and player diaries. module Game.LambdaHack.Save- ( saveGame, restoreGame, rmBkpSaveDiary, saveGameBkp+ ( saveGameFile, restoreGame, rmBkpSaveDiary, saveGameBkp ) where import System.Directory import System.FilePath import qualified Control.Exception as E hiding (handle) import Control.Monad+import Control.Concurrent+import System.IO.Unsafe (unsafePerformIO) -- horrors import Game.LambdaHack.Utils.File import Game.LambdaHack.State@@ -32,12 +34,17 @@ dfile <- diaryFile (sconfig state) encodeEOF dfile diary --- | Save a simple serialized version of the current state and diary.-saveGame :: State -> Diary -> IO ()-saveGame state diary = do+saveLock :: MVar ()+saveLock = unsafePerformIO newEmptyMVar++-- | Save a simple serialized version of the current state.+-- Protected by a lock to avoid corrupting the file.+saveGameFile :: State -> IO ()+saveGameFile state = do+ putMVar saveLock () sfile <- saveFile (sconfig state) encodeEOF sfile state- saveDiary state diary -- save the diary often in case of crashes+ takeMVar saveLock -- | Try to create a directory. Hide errors due to, -- e.g., insufficient permissions, because the game can run@@ -78,6 +85,7 @@ -- Possibly copy over data files. No problem if it fails. tryCopyDataFiles pathsDataFile appData -- If the diary file does not exist, create an empty diary.+ -- TODO: when diary gets corrupted, start a new one, too. diary <- do dfile <- diaryFile config db <- doesFileExist dfile@@ -114,19 +122,31 @@ renameFile sfile bfile -- | Save the diary and a backup of the save game file, in case of crashes.+-- This is only a backup, so no problem is the game is shut down+-- before saving finishes, so we don't wait on the mvar. However,+-- if a previous save is already in progress, we skip this save. saveGameBkp :: State -> Diary -> IO () saveGameBkp state diary = do- saveGame state diary- mvBkp (sconfig state)+ b <- tryPutMVar saveLock ()+ when b $+ void $ forkIO $ do+ saveDiary state diary -- save the diary often in case of crashes+ sfile <- saveFile (sconfig state)+ encodeEOF sfile state+ mvBkp (sconfig state)+ takeMVar saveLock -- | Remove the backup of the savegame and save the player diary. -- Should be called before any non-error exit from the game. -- Sometimes the backup file does not exist and it's OK. -- We don't bother reporting any other removal exceptions, either, -- because the backup file is relatively unimportant.+-- We wait on the mvar, because saving the diary at game shutdown is important. rmBkpSaveDiary :: State -> Diary -> IO () rmBkpSaveDiary state diary = do+ putMVar saveLock () saveDiary state diary -- save the diary often in case of crashes bfile <- bkpFile (sconfig state) bb <- doesFileExist bfile when bb $ removeFile bfile+ takeMVar saveLock
Game/LambdaHack/Start.hs view
@@ -16,8 +16,8 @@ import Game.LambdaHack.Content.TileKind import Game.LambdaHack.Content.RuleKind import Game.LambdaHack.Tile-import Game.LambdaHack.Level import qualified Game.LambdaHack.Kind as Kind+import Game.LambdaHack.Msg speedup :: Kind.Ops TileKind -> Kind.Speedup TileKind speedup Kind.Ops{ofoldrWithKey, obounds} =@@ -46,8 +46,8 @@ start :: Config.CP -> Session -> IO () start config1 slowSess = do let sess@Session{scops = cops@Kind.COps{corule}} = speedupCops slowSess- title = rtitle $ stdRuleset corule- pathsDataFile = rpathsDataFile $ stdRuleset corule+ title = rtitle $ Kind.stdRuleset corule+ pathsDataFile = rpathsDataFile $ Kind.stdRuleset corule restored <- Save.restoreGame pathsDataFile config1 title case restored of Right (msg, diary) -> do -- Starting a new game.@@ -59,8 +59,9 @@ let state = defaultState config3 sflavour freshDungeon entryLevel entryLoc g3 hstate = initialHeroes cops entryLoc state- handlerToIO sess hstate diary{smsg = msg} handle+ handlerToIO sess hstate diary{sreport = singletonReport msg} handleTurn Left (state, diary) -> -- Running a restored a game. handlerToIO sess state- diary{smsg = "Welcome back to " ++ title ++ "."} -- TODO:save old msg?- handle+ -- This overwrites the "Really save/quit?" messages.+ diary{sreport = singletonReport $ "Welcome back to " ++ title ++ "."}+ handleTurn
Game/LambdaHack/State.hs view
@@ -3,7 +3,7 @@ ( -- * Game state State(..), TgtMode(..), Cursor(..) -- * Accessor- , slevel+ , slevel, stime -- * Constructor , defaultState -- * State update@@ -15,20 +15,20 @@ ) where import qualified Data.Set as S-import qualified Data.IntSet as IS import Data.Binary import qualified Game.LambdaHack.Config as Config import qualified System.Random as R import System.Time import Game.LambdaHack.Actor-import Game.LambdaHack.Misc import Game.LambdaHack.Point import Game.LambdaHack.Level import qualified Game.LambdaHack.Dungeon as Dungeon import Game.LambdaHack.Item import Game.LambdaHack.Msg import Game.LambdaHack.FOV+import Game.LambdaHack.Time+import qualified Game.LambdaHack.HighScore as H -- | The diary contains all the player data -- that carries over from game to game.@@ -36,8 +36,8 @@ -- history of past games. This can be used for calculating player -- achievements, unlocking advanced game features and general data mining. data Diary = Diary- { smsg :: Msg- , shistory :: [Msg]+ { sreport :: Report+ , shistory :: History } -- | The state of a single game that can be save and restored.@@ -46,15 +46,15 @@ data State = State { splayer :: ActorId -- ^ represents the player-controlled actor , scursor :: Cursor -- ^ cursor location and level to return to- , stime :: Time -- ^ current in-game time , sflavour :: FlavourMap -- ^ association of flavour to items , sdisco :: Discoveries -- ^ items (kinds) that have been discovered , sdungeon :: Dungeon.Dungeon -- ^ all dungeon levels , slid :: Dungeon.LevelId -- ^ identifier of the current level- , scounter :: (Int, Int) -- ^ stores next hero index and monster index- , sparty :: IS.IntSet -- ^ heroes in the party+ , scounter :: Int -- ^ stores next actor index , srandom :: R.StdGen -- ^ current random generator , sconfig :: Config.CP -- ^ game config+ , snoTime :: Bool -- ^ last command unexpectedly took no time+ , squit :: Maybe (Bool, H.Status) -- ^ cause of game shutdown , sdebug :: DebugMode -- ^ debugging mode } deriving Show@@ -72,6 +72,7 @@ , clocLn :: Dungeon.LevelId -- ^ cursor level , clocation :: Point -- ^ cursor coordinates , creturnLn :: Dungeon.LevelId -- ^ the level current player resides on+ , ceps :: Int -- ^ a parameter of the tgt digital line } deriving Show @@ -85,15 +86,19 @@ slevel :: State -> Level slevel State{slid, sdungeon} = sdungeon Dungeon.! slid --- TODO: add date.+-- | Get current time from the dungeon data.+stime :: State -> Time+stime State{slid, sdungeon} = ltime $ sdungeon Dungeon.! slid+ -- | Initial player diary. defaultDiary :: IO Diary defaultDiary = do- curDate <- getClockTime- let time = calendarTimeToString $ toUTCTime curDate+ dateTime <- getClockTime+ let curDate = calendarTimeToString $ toUTCTime dateTime return Diary- { smsg = ""- , shistory = ["Player diary started on " ++ time ++ "."]+ { sreport = emptyReport+ , shistory = singletonHistory $ singletonReport $+ "Player diary started on " ++ curDate ++ "." } -- | Initial game state.@@ -101,17 +106,17 @@ -> Point -> R.StdGen -> State defaultState config flavour dng lid ploc g = State- (AHero 0) -- hack: the hero is not yet alive- (Cursor TgtOff lid ploc lid)- 0+ 0 -- hack: the hero is not yet alive+ (Cursor TgtOff lid ploc lid 0) flavour S.empty dng lid- (0, 0)- IS.empty+ 0 g config+ False+ Nothing defaultDebugMode defaultDebugMode :: DebugMode@@ -126,7 +131,7 @@ -- | Update time within state. updateTime :: (Time -> Time) -> State -> State-updateTime f s = s { stime = f (stime s) }+updateTime f s = updateLevel (\ lvl@Level{ltime} -> lvl {ltime = f ltime}) s -- | Update item discoveries within state. updateDiscoveries :: (Discoveries -> Discoveries) -> State -> State@@ -155,55 +160,55 @@ instance Binary Diary where put Diary{..} = do- put smsg+ put sreport put shistory get = do- smsg <- get+ sreport <- get shistory <- get return Diary{..} instance Binary State where- put (State player cursor time flav disco dng lid ct- party g config _) = do+ put (State player cursor flav disco dng lid ct+ g config snoTime _ _) = do put player put cursor- put time put flav put disco put dng put lid put ct- put party put (show g) put config+ put snoTime get = do player <- get cursor <- get- time <- get flav <- get disco <- get dng <- get lid <- get ct <- get- party <- get g <- get- config <- get+ config <- get+ snoTime <- get return- (State player cursor time flav disco dng lid ct- party (read g) config defaultDebugMode)+ (State player cursor flav disco dng lid ct+ (read g) config snoTime Nothing defaultDebugMode) instance Binary Cursor where- put (Cursor act cln loc rln) = do+ put (Cursor act cln loc rln eps) = do put act put cln put loc put rln+ put eps get = do act <- get cln <- get loc <- get rln <- get- return (Cursor act cln loc rln)+ eps <- get+ return (Cursor act cln loc rln eps) instance Binary TgtMode where put TgtOff = putWord8 0
Game/LambdaHack/Strategy.hs view
@@ -15,15 +15,15 @@ -- | Strategy is a monad. TODO: Can we write this as a monad transformer? instance Monad Strategy where- return x = Strategy $ return $ uniformFreq [x]+ return x = Strategy $ return $ uniformFreq "Strategy_return" [x] m >>= f = Strategy $- filter (not . nullFreq)- [ toFreq [ (p * q, b)- | (p, a) <- runFrequency x- , y <- runStrategy (f a)- , (q, b) <- runFrequency y- ]- | x <- runStrategy m ]+ filter (not . nullFreq)+ [ toFreq "Strategy_bind" [ (p * q, b)+ | (p, a) <- runFrequency x+ , y <- runStrategy (f a)+ , (q, b) <- runFrequency y+ ]+ | x <- runStrategy m ] instance MonadPlus Strategy where mzero = Strategy []
Game/LambdaHack/StrategyAction.hs view
@@ -7,8 +7,10 @@ import qualified Data.IntMap as IM import Data.Maybe import Control.Monad+import Control.Monad.State hiding (State, state) import Control.Arrow +import Game.LambdaHack.Utils.Assert import Game.LambdaHack.Point import Game.LambdaHack.Vector import Game.LambdaHack.Level@@ -23,11 +25,14 @@ import Game.LambdaHack.Actions import Game.LambdaHack.ItemAction import Game.LambdaHack.Content.ItemKind+import Game.LambdaHack.Content.RuleKind import Game.LambdaHack.Item import qualified Game.LambdaHack.Effect as Effect import qualified Game.LambdaHack.Tile as Tile import qualified Game.LambdaHack.Kind as Kind import qualified Game.LambdaHack.Feature as F+import Game.LambdaHack.Time+import qualified Game.LambdaHack.Color as Color {- Monster movement@@ -61,53 +66,47 @@ -- TODO: improve, split up, etc. -- | Monster AI strategy based on monster sight, smell, intelligence, etc. strategy :: Kind.COps -> ActorId -> State -> Perception -> Strategy (Action ())-strategy cops actor oldState@State{splayer = pl, stime = time} per =+strategy cops actor oldState@State{splayer = pl} per = strat where Kind.COps{ cotile- , coactor=Kind.Ops{okind}+ , coactor=coactor@Kind.Ops{okind} , coitem=coitem@Kind.Ops{okind=iokind}+ , corule } = cops- lvl@Level{lsmell = nsmap, lxsize, lysize} = slevel oldState- Actor { bkind = ak, bloc = me, bdir = ad, btarget = tgt } =+ lvl@Level{lsmell, lxsize, lysize, ltime} = slevel oldState+ actorBody@Actor{ bkind = ak, bloc = me, bdir = ad, btarget, bparty } = getActor actor oldState- items = getActorItem actor oldState+ bitems = getActorItem actor oldState mk = okind ak delState = deleteActor actor oldState enemyVisible a l =- asight mk && monsterSeesHero cotile per lvl actor a me l ||- -- Any enemy can be flet if adjacent (e. g., a monster player).+ asight mk &&+ isAHero delState a &&+ monsterSeesHero cotile per lvl actor a me l+ -- Enemy can be felt if adjacent (e. g., a player-controlled monster). -- TODO: can this be replaced by setting 'lights' to [me]?- adjacent lxsize me l- -- If no heroes on the level, monsters go at each other. TODO: let them- -- earn XP by killing each other to make this dangerous to the player.- -- TODO: with some commands blocked, this can't happen now. Find a way- -- to test it, nevertheless. Have a scroll of monster fury?- hs = L.map (AHero *** bloc) $- IM.assocs $ lheroes $ slevel delState- ms = L.map (AMonster *** bloc) $- IM.assocs $ lmonsters $ slevel delState+ || (asmell mk || asight mk)+ && adjacent lxsize me l -- Below, "foe" is the hero (or a monster, or loc) chased by the actor.- (newTgt, floc, foeVisible) =+ chase tgt = case tgt of- TEnemy a ll | focusedMonster ->- if memActor a delState- then let l = bloc $ getActor a delState- in if enemyVisible a l- then (TEnemy a l, Just l, True)- else if isJust (case closest of (_, m, _) -> m) || me == ll- then closest -- prefer visible foes- else (tgt, Just ll, False) -- last known loc of enemy- else closest -- enemy not on the level, temporarily chase others- TLoc loc -> if me == loc- then closest- else (tgt, Just loc, False) -- ignore all and go to loc+ TEnemy a ll | focusedMonster && memActor a delState ->+ let l = bloc $ getActor a delState+ in if enemyVisible a l+ then (TEnemy a l, Just l, True)+ else if isJust (case closest of (_, m, _) -> m) || me == ll+ then closest -- prefer visible foes+ else (tgt, Just ll, False) -- last known loc of enemy+ TLoc loc | me == loc -> closest+ TLoc loc -> (tgt, Just loc, False) -- ignore all and go to loc _ -> closest+ (newTgt, floc, foeVisible) = chase btarget closest =- let hsAndTraitor = if isAMonster pl && memActor pl delState- then (pl, bloc $ getPlayerBody delState) : hs- else hs- foes = if L.null hsAndTraitor then ms else hsAndTraitor+ let hs = L.map (second bloc) $ heroAssocs $ slevel delState+ foes = if not (isAHero delState pl) && memActor pl delState+ then (pl, bloc $ getPlayerBody delState) : hs+ else hs visible = L.filter (uncurry enemyVisible) foes foeDist = L.map (\ (a, l) -> (chessDist lxsize me l, l, a)) visible in case foeDist of@@ -132,51 +131,71 @@ onlyKeepsDir k = only (\ x -> maybe True (\ (d, _) -> euclidDistSq lxsize d x <= k) ad) onlyKeepsDir_9 = only (\ x -> maybe True (\ (d, _) -> neg x /= d) ad)- onlyNoMs = onlyMoves (unoccupied (levelMonsterList delState)) me+ onlyNoMs = onlyMoves (unoccupied (dangerousList delState)) me -- Monsters don't see doors more secret than that. Enforced when actually -- opening doors, too, so that monsters don't cheat. TODO: remove the code -- duplication, though.- openPower = Tile.SecretStrength $- case strongestSearch coitem items of+ openPower = timeScale timeTurn $+ case strongestSearch coitem bitems of Just i -> aiq mk + jpower i Nothing -> aiq mk openableHere = openable cotile lvl openPower onlyOpenable = onlyMoves openableHere me accessibleHere = accessible cops lvl me onlySensible = onlyMoves (\ l -> accessibleHere l || openableHere l) me- focusedMonster = aspeed mk >= 10+ focusedMonster = actorSpeed coactor actorBody <= speedNormal movesNotBack = maybe id (\ (d, _) -> L.filter (/= neg d)) ad $ moves lxsize smells = L.map fst $ L.sortBy (\ (_, s1) (_, s2) -> compare s2 s1) $- L.filter (\ (_, s) -> s > 0) $- L.map (\ x -> let sm = Tile.smelltime $ IM.findWithDefault- (Tile.SmellTime 0) (me `shift` x) nsmap- in (x, (sm - time) `max` 0)) movesNotBack+ L.filter (\ (_, s) -> s > timeZero) $+ L.map (\ x -> let sm = IM.findWithDefault timeZero (me `shift` x) lsmell+ in (x, max timeZero (sm `timeAdd` timeNegate ltime)))+ movesNotBack attackDir d = dirToAction actor newTgt True `liftM` d moveDir d = dirToAction actor newTgt False `liftM` d+ darkenActor = updateAnyActor actor $ \ m -> m {bcolor = Just Color.BrBlack} - strat =- foeVisible .=> attackDir (onlyFoe moveFreely)- .| foeVisible .=> liftFrequency (msum seenFreqs)- .| lootHere me .=> actionPickup- .| moveDir moveTowards -- go to last known foe location- .| attackDir moveAround+ strat = case btarget of+ TPath [] -> dieOrSleep+ TPath (d : _) | not $ accessible cops lvl me (shift me d) -> dieOrSleep+ -- TODO: perhaps colour differently the whole second turn of movement?+ TPath [d] -> return $ darkenActor >> dirToAction actor (TPath []) True d+ TPath (d : lv) -> return $ dirToAction actor (TPath lv) True d+ _ -> foeVisible .=> attackDir (onlyFoe moveFreely)+ .| foeVisible .=> liftFrequency (msum seenFreqs)+ .| lootHere me .=> actionPickup+ .| moveDir moveTowards -- go to last known foe location+ .| attackDir moveAround+ dieOrSleep | bparty `elem` allProjectiles = dieNow actor+ | otherwise = wait actionPickup = return $ actorPickupItem actor tis = lvl `atI` me- seenFreqs = [applyFreq items 1, applyFreq tis 2,- throwFreq items 3, throwFreq tis 6] ++ towardsFreq- applyFreq is multi = toFreq- [ (benefit * multi,- applyGroupItem actor (iverbApply ik) i)+ seenFreqs = [applyFreq bitems 1, applyFreq tis 2,+ throwFreq bitems 3, throwFreq tis 6] ++ towardsFreq+ applyFreq is multi = toFreq "applyFreq"+ [ (benefit * multi, applyGroupItem actor (iverbApply ik) i) | i <- is, let ik = iokind (jkind i), let benefit = (1 + jpower i) * Effect.effectToBenefit (ieffect ik), benefit > 0, asight mk || isymbol ik == '!']- throwFreq is multi = if adjacent lxsize me (fromJust floc) || not (asight mk)+ foeAdjacent = maybe False (adjacent lxsize me) floc+ -- TODO: also don't throw if any loc on path is visibly not accessible+ -- from previous (and tweak eps in bla to make it accessible).+ -- Also don't throw if target not in range.+ eps = 0+ bl = bla lxsize lysize eps me (fromJust floc)+ loc1 = case bl of+ Nothing -> me+ Just [] -> me+ Just (lbl:_) -> lbl+ throwFreq is multi = if foeAdjacent+ || not (asight mk)+ || not (accessible cops lvl me loc1)+ || isJust (locToActor loc1 oldState) then mzero- else toFreq+ else toFreq "throwFreq" [ (benefit * multi, projectGroupItem actor (fromJust floc) (iverbProject ik) i) | i <- is,@@ -184,8 +203,9 @@ let benefit = - (1 + jpower i) * Effect.effectToBenefit (ieffect ik), benefit > 0,- -- Wasting swords would be too cruel to the player.- isymbol ik /= ')']+ -- Wasting weapons and armour would be too cruel to the player.+ -- TODO: specify in content+ isymbol ik `elem` (ritemProject $ Kind.stdRuleset corule)] towardsFreq = map (scaleFreq 30) $ runStrategy $ moveDir moveTowards moveTowards = onlySensible $ onlyNoMs (towardsFoe moveFreely) moveAround =@@ -210,18 +230,29 @@ onlyMoves :: (Point -> Bool) -> Point -> Strategy Vector -> Strategy Vector onlyMoves p l = only (\ x -> p (l `shift` x)) moveRandomly :: Strategy Vector- moveRandomly = liftFrequency $ uniformFreq (moves lxsize)+ moveRandomly = liftFrequency $ uniformFreq "moveRandomly" (moves lxsize) dirToAction :: ActorId -> Target -> Bool -> Vector -> Action ()-dirToAction actor tgt allowAttacks dir = do+dirToAction actor btarget allowAttacks dir = do -- set new direction- updateAnyActor actor $ \ m -> m { bdir = Just (dir, 0), btarget = tgt }+ updateAnyActor actor $ \ m -> m { bdir = Just (dir, 0), btarget } -- perform action- tryWith (advanceTime actor) $- -- if the following action aborts, we just advance the time and continue+ tryWith (\ msg -> if null msg+ then return ()+ else assert `failure` (msg, "in AI")) $ do+ -- If the following action aborts, we just advance the time and continue. -- TODO: ensure time is taken for other aborted actions in this file+ -- TODO: or just fail at each abort in AI code? or use tryWithFrame? moveOrAttack allowAttacks actor dir -- | A strategy to always just wait.-wait :: ActorId -> Strategy (Action ())-wait actor = return $ advanceTime actor+wait :: Strategy (Action ())+wait = return $ return ()++-- | A strategy to always just die.+dieNow :: ActorId -> Strategy (Action ())+dieNow actor = return $ do -- TODO: explode if a potion+ bitems <- gets (getActorItem actor)+ Actor{bloc} <- gets (getActor actor)+ modify (updateLevel (dropItemsAt bitems bloc))+ modify (deleteActor actor)
Game/LambdaHack/Tile.hs view
@@ -10,33 +10,28 @@ -- to try to compress their representation and/or recompute them. -- Instead, of defining a @Tile@ type, we express various properties -- of concrete tiles by arrays or sparse IntMaps, as appropriate.+--+-- Actors at normal speed (2 m/s) take one turn to move one tile (1 m by 1 m). module Game.LambdaHack.Tile- ( SecretStrength(..), SmellTime(..)+ (SecretTime, SmellTime , kindHasFeature, kindHas, hasFeature , isClear, isLit, similar, canBeHidden ) where import qualified Data.List as L-import Data.Binary import Game.LambdaHack.Content.TileKind import qualified Game.LambdaHack.Feature as F import qualified Game.LambdaHack.Kind as Kind-import Game.LambdaHack.Misc+import Game.LambdaHack.Time --- | The type of secrecy strength of hidden terrain tiles (e.g., doors).-newtype SecretStrength = SecretStrength{secretStrength :: Time}- deriving (Show, Eq, Ord)-instance Binary SecretStrength where- put = put . secretStrength- get = fmap SecretStrength get+-- | The time interval needed to discover a given secret,+-- e.g., a hidden terrain tile, e.g., a hidden door.+type SecretTime = Time -- | The last time a hero left a smell in a given tile. To be used -- by monsters that hunt by smell.-newtype SmellTime = SmellTime{smelltime :: Time} deriving Show-instance Binary SmellTime where- put = put . smelltime- get = fmap SmellTime get+type SmellTime = Time -- | Whether a tile kind has the given feature. kindHasFeature :: F.Feature -> TileKind -> Bool
+ Game/LambdaHack/Time.hs view
@@ -0,0 +1,143 @@+-- | Game time and speed.+module Game.LambdaHack.Time+ ( Time, timeZero, timeClip, timeTurn+ , timeAdd, timeFit, timeNegate, timeScale+ , timeToDigit+ , Speed, toSpeed, speedNormal+ , speedScale, ticksPerMeter, traveled, speedFromWeight, rangeFromSpeed+ ) where++import Data.Binary+import Data.Int (Int64)+import qualified Data.Char as Char++-- | Game time in ticks. The time dimension.+-- One tick is 1 microsecond (one millionth of a second),+-- one turn is 0.5 s.+newtype Time = Time Int64+ deriving (Show, Eq, Ord)++instance Binary Time where+ put (Time n) = put n+ get = fmap Time get++-- | Start of the game time, or zero lenght time interval.+timeZero :: Time+timeZero = Time 0++-- | The smallest unit of time. Do not export, because the proportion+-- of turn to tick is an implementation detail.+-- The significance of this detail is only that it determines resolution+-- of the time dimension.+_timeTick :: Time+_timeTick = Time 1++-- TODO: don't have a fixed time, but instead set it at 1/3 or 1/4+-- of timeTurn depending on level. Clips are a UI feature+-- after all, so should depend on the user situation.+-- | At least once per clip all moves are resolved and a frame+-- or a frame delay is generated.+-- Currently one clip is 0.1 s, but it may change,+-- and the code should not depend on this fixed value.+timeClip :: Time+timeClip = Time 100000++-- | One turn is 0.5 s. The code may depend on that.+-- Actors at normal speed (2 m/s) take one turn to move one tile (1 m by 1 m).+timeTurn :: Time+timeTurn = Time 500000++-- | This many turns fit in a single second.+turnsInSecond :: Int64+turnsInSecond = 2++-- | This many ticks fits in a single second. Do not export,+_ticksInSecond :: Int64+_ticksInSecond =+ let Time ticksInTurn = timeTurn+ in ticksInTurn * turnsInSecond++-- | Time addition.+timeAdd :: Time -> Time -> Time+timeAdd (Time t1) (Time t2) = Time (t1 + t2)++-- | How many time intervals of the latter kind fits in an interval+-- of the former kind.+timeFit :: Time -> Time -> Int+timeFit (Time t1) (Time t2) = fromIntegral $ t1 `div` t2++-- | Negate a time interval. Can be used to subtract from a time+-- or to reverse the ordering on time.+timeNegate :: Time -> Time+timeNegate (Time t) = Time (-t)++-- | Scale time by an @Int@ scalar value.+timeScale :: Time -> Int -> Time+timeScale (Time t) s = Time (t * fromIntegral s)++-- | Represent the main 10 thresholds of a time range by digits,+-- given the total length of the time range.+timeToDigit :: Time -> Time -> Char+timeToDigit (Time maxT) (Time t) =+ let k = 10 * t `div` maxT+ digit | k > 9 = '*'+ | k < 0 = '-'+ | otherwise = Char.intToDigit $ fromIntegral k+ in digit++-- | Speed in meters per 1 million seconds (m/Ms).+-- Actors at normal speed (2 m/s) take one time turn (0.5 s)+-- to move one tile (1 m by 1 m).+newtype Speed = Speed Int64+ deriving (Show, Eq, Ord)++instance Binary Speed where+ put (Speed n) = put n+ get = fmap Speed get++-- | Number of seconds in a kilo-second.+sInMs :: Int64+sInMs = 1000000++-- | Constructor for content definitions.+toSpeed :: Double -> Speed+toSpeed s = Speed $ round $ s * fromIntegral sInMs++-- | Normal speed (2 m/s) that suffices to move one tile in one turn.+speedNormal :: Speed+speedNormal = Speed $ 2 * sInMs++-- | Scale speed by an @Int@ scalar value.+speedScale :: Speed -> Int -> Speed+speedScale (Speed v) s = Speed (v * fromIntegral s)++-- | The number of time ticks it takes to walk 1 meter at the given speed.+ticksPerMeter :: Speed -> Time+ticksPerMeter (Speed v) = Time $ _ticksInSecond * sInMs `div` v++-- | Distance in meters (so also in tiles, given the chess metric)+-- traveled in a given time by a body with a given speed.+traveled :: Speed -> Time -> Int+traveled (Speed v) (Time t) =+ fromIntegral $ v * t `div` (_ticksInSecond * sInMs)++-- | Calculate projectile speed from item weight in grams+-- and speed bonus in percents.+-- See <https://github.com/kosmikus/LambdaHack/wiki/Item-statistics>.+speedFromWeight :: Int -> Int -> Speed+speedFromWeight weight bonus =+ let w = fromIntegral weight+ b = fromIntegral bonus+ mpMs | w <= 500 = sInMs * 16+ | w > 500 && w <= 2000 = sInMs * 16 * 1500 `div` (w + 1000)+ | otherwise = sInMs * (10000 - w) `div` 1000+ in Speed $ max 0 $ mpMs * (100 + b) `div` 100++-- | Calculate maximum range in meters of a projectile from its speed.+-- See <https://github.com/kosmikus/LambdaHack/wiki/Item-statistics>.+-- With this formula, each projectile flies for exactly one second,+-- that is 2 turns, and then drops to the ground.+-- Dividing and multiplying by 2 ensures both turns of flight+-- cover the same distance.+rangeFromSpeed :: Speed -> Int+rangeFromSpeed (Speed v) = fromIntegral $ 2 * (v `div` (sInMs * 2))
Game/LambdaHack/Turn.hs view
@@ -1,172 +1,250 @@ -- | The main loop of the game, processing player and AI moves turn by turn.-module Game.LambdaHack.Turn ( handle ) where+module Game.LambdaHack.Turn ( handleTurn ) where import Control.Monad import Control.Monad.State hiding (State, state)+import Control.Arrow ((&&&)) import qualified Data.List as L import qualified Data.Ord as Ord-import qualified Data.IntMap as IM import qualified Data.Map as M+import qualified Data.IntMap as IM+import Data.Maybe +import Game.LambdaHack.Utils.Assert import Game.LambdaHack.Action import Game.LambdaHack.Actions-import qualified Game.LambdaHack.Config as Config import Game.LambdaHack.EffectAction import qualified Game.LambdaHack.Binding as Binding-import Game.LambdaHack.Level import Game.LambdaHack.Actor import Game.LambdaHack.ActorState+import Game.LambdaHack.Level import Game.LambdaHack.Random import Game.LambdaHack.State import Game.LambdaHack.Strategy import Game.LambdaHack.StrategyAction import Game.LambdaHack.Running-import qualified Game.LambdaHack.Tile as Tile+import qualified Game.LambdaHack.Key as K+import Game.LambdaHack.Msg+import Game.LambdaHack.Draw+import qualified Game.LambdaHack.Kind as Kind+import Game.LambdaHack.Time+import qualified Game.LambdaHack.HighScore as H --- One turn proceeds through the following functions:------ handle--- handleAI, handleMonster--- nextMove--- handle (again)------ OR:------ handle--- handlePlayer, playerCommand--- handleAI, handleMonster--- nextMove--- handle (again)+-- One clip proceeds through the following functions: --+-- handleTurn+-- handleActors+-- handleMonster or handlePlayer+-- handleActors+-- handleMonster or handlePlayer+-- ...+-- handleTurn (again)+ -- What's happening where: ----- handle: determine who moves next,--- dispatch to handleAI or handlePlayer------ handlePlayer: remember, display, get and process commmand(s),--- update smell map, update perception------ handleAI: find monsters that can move+-- handleTurn: HP regeneration, monster generation, determine who moves next,+-- dispatch to handlePlayer and handleActors, advance global game time ----- handleMonster: determine and process monster action, advance monster time+-- handleActors: find an actor that can move, advance actor time,+-- update perception, remember, push frame, repeat ----- nextMove: advance global game time, HP regeneration, monster generation+-- handlePlayer: update perception, remember, display frames,+-- get and process commmands (zero or more), update smell map ----- This is rather convoluted, and the functions aren't named very aptly, so we--- should clean this up later. TODO.+-- handleMonster: determine and process monster action --- | Decide if the hero is ready for another move.--- If yes, run a player move, if not, run an AI move.--- In either case, eventually the next turn is started or the game ends.-handle :: Action ()-handle = do- debug "handle"- state <- get- let ptime = btime (getPlayerBody state) -- time of player's next move- let time = stime state -- current game time- debug $ "handle: time check. ptime = "+-- | Start a clip (a part of a turn for which one or more frames+-- will be generated). Do whatever has to be done+-- every fixed number of time units, e.g., monster generation.+-- Run the player and other actors moves.+-- Eventually advance the time and repeat.+handleTurn :: Action ()+handleTurn = do+ debug "handleTurn"+ time <- gets stime -- the end time of this clip, inclusive+ let clipN = (time `timeFit` timeClip) `mod` (timeTurn `timeFit` timeClip)+ -- Regenerate HP and add monsters each turn, not each clip.+ when (clipN == 1) regenerateLevelHP+ when (clipN == 3) generateMonster+ ptime <- gets (btime . getPlayerBody) -- time of player's next move+ debug $ "handleTurn: time check. ptime = " ++ show ptime ++ ", time = " ++ show time- if ptime > time- then handleAI -- the hero can't make a move yet; monsters first- else handlePlayer -- it's the hero's turn!-- -- TODO: readd this, but only for the turns when anything moved- -- and only after a rendering delay is added, so that the move is visible- -- on modern computers. Use the same delay for running (not disabled now).- -- We redraw the map even between player moves so that the movements of fast- -- monsters can be traced on the map.- -- displayGeneric ColorFull (const "")+ handleActors timeZero+ modify (updateTime (timeAdd timeClip))+ squit <- gets squit+ case squit of+ Nothing -> handleTurn+ Just status@(_, H.Camping) -> do+ pl <- gets splayer+ advanceTime False pl -- rewind player time: his action was only Save+ shutGame status+ Just status -> shutGame status -- TODO: We should replace this structure using a priority search queue/tree.--- | Handle monster moves. Perform moves for individual monsters as long as--- there are monsters that have a move time which is less than or equal to--- the current time.-handleAI :: Action ()-handleAI = do- debug "handleAI"- time <- gets stime- ms <- gets (lmonsters . slevel)- pl <- gets splayer- if IM.null ms- then nextMove- else let order = Ord.comparing (btime . snd)- (i, m) = L.minimumBy order (IM.assocs ms)- actor = AMonster i- in if btime m > time || actor == pl- then nextMove -- no monster is ready for another move- else handleMonster actor+-- | Perform moves for individual actors not controlled+-- by the player, as long as there are actors+-- with the next move time less than or equal to the current time.+-- Some very fast actors may move many times a clip and then+-- we introduce subclips and produce many frames per clip to avoid+-- jerky movement. Otherwise we push exactly one frame or frame delay.+handleActors :: Time -- ^ the start time of current subclip, exclusive+ -> Action ()+handleActors subclipStart = do+ debug "handleActors"+ Kind.COps{coactor} <- getCOps+ time <- gets stime -- the end time of this clip, inclusive+ lactor <- gets (allButHeroesAssocs . slevel)+ pl <- gets splayer+ pbody <- gets getPlayerBody+ squit <- gets squit+ let as = (pl, pbody) : lactor -- older actors act first+ mnext = if null as -- wait until any actor spawned+ then Nothing+ else let -- Heroes move first then monsters, then the rest.+ order = Ord.comparing (btime . snd &&& bparty . snd)+ (actor, m) = L.minimumBy order as+ in if btime m > time+ then Nothing -- no actor is ready for another move+ else Just (actor, m)+ case mnext of+ _ | isJust squit -> return ()+ Nothing -> when (subclipStart == timeZero) $ displayFramePush Nothing+ Just (actor, m) -> do+ advanceTime True actor -- advance time while the actor still alive+ if actor == pl || bparty m == heroParty+ then+ -- Player moves always start a new subclip.+ startClip $ do+ handlePlayer+ handleActors $ btime m+ else+ let speed = actorSpeed coactor m+ delta = ticksPerMeter speed+ in if subclipStart == timeZero+ || btime m > timeAdd subclipStart delta+ then+ -- That's the first move this clip+ -- or the monster has already moved this subclip.+ -- I either case, start a new subclip.+ startClip $ do+ handleMonster actor+ handleActors $ btime m+ else do+ -- The monster didn't yet move this subclip.+ handleMonster actor+ handleActors subclipStart -- | Handle the move of a single monster. handleMonster :: ActorId -> Action () handleMonster actor = do debug "handleMonster"- cops <- contentOps+ cops <- getCOps state <- get- -- Simplification: this is the perception after the last player command- -- and does not take into account, e.g., other monsters opening doors.- per <- currentPerception+ per <- getPerception -- Run the AI: choses an action from those given by the AI strategy. join $ rndToAction $ frequency (head (runStrategy (strategy cops actor state per- .| wait actor)))- handleAI---- TODO: nextMove may not be a good name. It's part of the problem of the--- current design that all of the top-level functions directly call each--- other, rather than being called by a driver function.--- | After everything has been handled for the current game time, we can--- advance the time. Here is the place to do whatever has to be done for--- every time unit, e.g., monster generation.-nextMove :: Action ()-nextMove = do- debug "nextMove"- modify (updateTime (+1))- regenerateLevelHP- generateMonster- handle+ .| wait))) -- | Handle the move of the hero. handlePlayer :: Action () handlePlayer = do debug "handlePlayer"- -- Determine perception before running player command, in case monsters- -- have opened doors, etc.- withPerception $ do- remember -- The hero notices his surroundings, before they get displayed.- oldPlayerTime <- gets (btime . getPlayerBody)- playerCommand- -- At this point, the command was successful and possibly took some time.- newPlayerTime <- gets (btime . getPlayerBody)- if newPlayerTime == oldPlayerTime- then handlePlayer -- no time taken, repeat- else do- state <- get- pl <- gets splayer- let time = stime state- ploc = bloc (getPlayerBody state)- sTimeout = Config.get (sconfig state) "monsters" "smellTimeout"- -- Update smell. Only humans leave a strong scent.- when (isAHero pl) $- modify (updateLevel (updateSmell (IM.insert ploc- (Tile.SmellTime- (time + sTimeout)))))- -- Determine perception to let monsters target heroes.- withPerception handleAI+ -- When running, stop if aborted by a disturbance.+ -- Otherwise let the player issue commands, until any of them takes time.+ -- First time, just after pushing frames, ask for commands in Push mode.+ tryWith (\ msg -> stopRunning >> playerCommand msg) $+ ifRunning continueRun abort+ addSmell --- | Determine and process the next player command.-playerCommand :: Action ()-playerCommand = do- displayAll -- draw the current surroundings- history -- update the message history and reset current message- tryRepeatedlyWith stopRunning $ -- on abort, just ask for a new command- ifRunning continueRun $ do- k <- session nextCommand- session (\ Session{skeyb} ->- case M.lookup k (Binding.kcmd skeyb) of- Just (_, c) -> c- Nothing ->- abortWith $ "unknown command <" ++ show k ++ ">")+-- | Determine and process the next player command. The argument is the last+-- abort message due to running, if any.+playerCommand :: Msg -> Action ()+playerCommand msgRunAbort = do+ -- The frame state is now Push.+ Binding.Binding{kcmd} <- getBinding+ kmPush <- case msgRunAbort of+ "" -> getKeyCommand (Just True)+ _ -> drawPrompt ColorFull msgRunAbort >>= getKeyChoice []+ -- The frame state is now None and remains so between each pair+ -- of lines of @loop@ (but can change within called actions).+ let loop :: (K.Key, K.Modifier) -> Action ()+ loop km = do+ -- Messages shown, so update history and reset current report.+ recordHistory+ -- On abort, just reset state and call loop again below.+ (timed, frames) <- tryWithFrame (return False) $ do+ -- Look up the key.+ case M.lookup km kcmd of+ Just (_, declaredTimed, c) -> do+ ((), frs) <- c+ -- Targeting cursor movement and a few other subcommands+ -- are wrongly marked as timed. This is indicated in their+ -- definitions by setting @snoTime@ flag and used and reset here.+ snoTime <- gets snoTime+ let timed = declaredTimed && not snoTime+ modify (\ s -> s {snoTime = False})+ -- Ensure at least one frame, if the command takes no time.+ -- No frames for @abort@, so the code is here, not below.+ if not timed && null (catMaybes frs)+ then do+ fr <- drawPrompt ColorFull ""+ return (timed, [Just fr])+ else return (timed, frs)+ Nothing -> let msgKey = "unknown command <" ++ K.showKM km ++ ">"+ in abortWith msgKey+ -- The command was aborted or successful and if the latter,+ -- possibly took some time.+ if not timed+ then do+ -- If no time taken, rinse and repeat.+ -- Analyse the obtained frames.+ let (mfr, frs) = case reverse $ catMaybes frames of+ [] -> (Nothing, [])+ f : fs -> (Just f, reverse fs)+ -- Show, one by one, all but the last frame.+ -- Note: the code that generates the frames is responsible+ -- for inserting the @more@ prompt.+ b <- getOverConfirm frs+ -- Display the last frame while waiting for the next key or,+ -- if there is no next frame, just get the key.+ kmNext <- case mfr of+ Just fr | b -> getKeyChoice [] fr+ _ -> getKeyCommand Nothing+ -- Look up and perform the next command.+ loop kmNext+ else do+ -- Exit the loop and let other actors act. No next key needed+ -- and no frames could have been generated.+ assert (null frames `blame` length frames) $+ return ()+ loop kmPush +-- | Advance (or rewind) the move time for the given actor.+advanceTime :: Bool -> ActorId -> Action ()+advanceTime forward actor = do+ Kind.COps{coactor} <- getCOps+ pl <- gets splayer+ let upd m@Actor{btime} =+ let speed = actorSpeed coactor m+ ticks = ticksPerMeter speed+ delta | forward = ticks+ | otherwise = timeNegate ticks+ in m {btime = timeAdd btime delta}+ updateAnyActor actor upd+ -- A hack to synchronize the whole party:+ body <- gets (getActor actor)+ when (actor == pl) $ do+ let updParty m = if bparty m == heroParty+ then m {btime = btime body}+ else m+ modify (updateLevel (updateActorDict (IM.map updParty)))+++-- The issues below are now complicated (?) by the fact that we now generate+-- a game screen frame at least once every clip and a jointed pair+-- of frame+key input for each command that does not take time.+-- -- Design thoughts (in order to get rid or partially rid of the somewhat -- convoluted design we have): We have three kinds of commands. --
Game/LambdaHack/Utils/Assert.hs view
@@ -22,6 +22,7 @@ " " ++ show blamed in trace s False +infix 1 `failure` -- | Like 'Prelude.undefined', but shows the source location -- and also the value to blame for the failure. To be used as in: --
Game/LambdaHack/Utils/Frequency.hs view
@@ -17,55 +17,65 @@ -- TODO: do not expose runFrequency -- | The frequency distribution type.-newtype Frequency a = Frequency- { runFrequency :: [(Int, a)] -- ^ Give acces to raw frequency values.+data Frequency a = Frequency+ { _name :: String -- ^ short description for debug, etc.+ , runFrequency :: [(Int, a)] -- ^ give acces to raw frequency values } deriving Show instance Monad Frequency where- return x = Frequency [(1, x)]- m >>= f = Frequency- [(p * q, y) | (p, x) <- runFrequency m,- (q, y) <- runFrequency (f x) ]+ return x = Frequency "return" [(1, x)]+ Frequency name xs >>= f =+ Frequency ("bind (" ++ name ++ ")")+ [(p * q, y) | (p, x) <- xs,+ (q, y) <- runFrequency (f x) ] instance MonadPlus Frequency where- mplus (Frequency xs) (Frequency ys) = Frequency (xs ++ ys)- mzero = Frequency []+ mplus (Frequency xname xs) (Frequency yname ys) =+ Frequency ("mplus (" ++ xname ++ ") (" ++ yname ++ ")")+ (xs ++ ys)+ mzero = Frequency "mzero" [] instance Functor Frequency where- fmap f (Frequency xs) = Frequency (map (\ (p, x) -> (p, f x)) xs)+ fmap f (Frequency name xs) = Frequency name (map (\ (p, x) -> (p, f x)) xs) -- | Uniform discrete frequency distribution.-uniformFreq :: [a] -> Frequency a-uniformFreq = Frequency . map (\ x -> (1, x))+uniformFreq :: String -> [a] -> Frequency a+uniformFreq name = Frequency name . map (\ x -> (1, x)) --- | Takes a list of frequencies and items into the frequency distribution.-toFreq :: [(Int, a)] -> Frequency a+-- | Takes a name and a list of frequencies and items+-- into the frequency distribution.+toFreq :: String -> [(Int, a)] -> Frequency a toFreq = Frequency --- | Scale frequecy distribution, multiplying it by an integer constant.-scaleFreq :: Int -> Frequency a -> Frequency a-scaleFreq n (Frequency xs) = Frequency (map (\ (p, x) -> (n * p, x)) xs)+-- | Scale frequecy distribution, multiplying it+-- by a positive integer constant.+scaleFreq :: Show a => Int -> Frequency a -> Frequency a+scaleFreq n (Frequency name xs) =+ assert (n > 0 `blame` ("negative scale for " ++ name, n, xs)) $+ Frequency name (map (\ (p, x) -> (n * p, x)) xs) -- | Leave only items that satisfy a predicate. filterFreq :: (a -> Bool) -> Frequency a -> Frequency a-filterFreq p (Frequency l) = Frequency $ filter (p . snd) l+filterFreq p (Frequency name l) =+ Frequency ("filterFreq (" ++ name ++ ")")+ (filter (p . snd) l) -- | Randomly choose an item according to the distribution. rollFreq :: Show a => Frequency a -> R.StdGen -> (a, R.StdGen)-rollFreq (Frequency []) _ =- assert `failure` "choice from an empty frequency"-rollFreq (Frequency [(n, x)]) _ | n <= 0 =- assert `failure` ("singleton frequency with nothing to pick", n, x)-rollFreq (Frequency [(_, x)]) g = (x, g) -- speedup-rollFreq (Frequency fs) g =- assert (sumf > 0 `blame` ("frequency with nothing to pick", fs)) $+rollFreq (Frequency name []) _ =+ assert `failure` ("choice from an empty frequency: " ++ name)+rollFreq (Frequency name [(n, x)]) _ | n <= 0 =+ assert `failure` ("singleton frequency with nothing to pick: " ++ name, n, x)+rollFreq (Frequency _ [(_, x)]) g = (x, g) -- speedup+rollFreq (Frequency name fs) g =+ assert (sumf > 0 `blame` ("frequency with nothing to pick: " ++ name, fs)) $ (frec r fs, ng) where sumf = sum (map fst fs) (r, ng) = R.randomR (1, sumf) g frec :: Int -> [(Int, a)] -> a- frec m [] = assert `failure` ("impossible", fs, m)+ frec m [] = assert `failure` ("impossible", name, fs, m) frec m ((n, x) : _) | m <= n = x frec m ((n, _) : xs) = frec (m - n) xs
+ Game/LambdaHack/Utils/LQueue.hs view
@@ -0,0 +1,35 @@+-- | Queues implemented with two stacks to ensure fast writes.+module Game.LambdaHack.Utils.LQueue+ ( LQueue, newLQueue, nullLQueue, trimLQueue, tryReadLQueue, writeLQueue+ ) where++import Data.Maybe++-- | Queues implemented with two stacks.+type LQueue a = ([a], [a]) -- (read_end, write_end)++-- | Create a new empty mutable queue.+newLQueue :: LQueue a+newLQueue = ([], [])++-- | Check if the queue is empty.+nullLQueue :: LQueue a -> Bool+nullLQueue (rs, ws) = null rs && null ws++-- | Remove all but the last written non-@Nothing@ element of the queue.+trimLQueue :: LQueue (Maybe a) -> LQueue (Maybe a)+trimLQueue (rs, ws) =+ let trim (_, w:_) = ([w], [])+ trim ([], []) = ([], [])+ trim (rsj, []) = ([last rsj], [])+ in trim (filter isJust rs, filter isJust ws)++-- | Try reading a queue. Return @Nothing@ if empty.+tryReadLQueue :: LQueue a -> Maybe (a, LQueue a)+tryReadLQueue (r : rs, ws) = Just (r, (rs, ws))+tryReadLQueue ([], []) = Nothing+tryReadLQueue ([], ws) = tryReadLQueue (reverse ws, [])++-- | Write to the queue. Faster than reading.+writeLQueue :: LQueue a -> a -> LQueue a+writeLQueue (rs, ws) w = (rs, w : ws)
Game/LambdaHack/Vector.hs view
@@ -2,7 +2,7 @@ -- but not unique, way. module Game.LambdaHack.Vector ( Vector, toVector, shift, shiftBounded, moves, movesWidth- , euclidDistSq, diagonal, neg, towards+ , euclidDistSq, diagonal, neg, towards, displacement, displacePath ) where import Data.Binary@@ -94,6 +94,7 @@ neg :: Vector -> Vector neg (Vector dir) = Vector (-dir) +-- TODO: use bla for that -- | Given a vector of arbitrary non-zero length, produce a unit vector -- that points in the same direction (in the chessboard metric). -- Of several equally good directions it picks one of those that visually@@ -114,7 +115,9 @@ else neg (toDir lxsize $ VectorXY dxy) -- TODO: Perhaps produce all acceptable directions and let AI choose.--- That would also eliminate the Doubles.+-- That would also eliminate the Doubles. Or only directions from bla?+-- Smart monster could really use all dirs to be less predictable,+-- but it wouldn't look as natural as bla, so for less smart bla is better. -- | Given two distinct locations, determine the direction (a unit vector) -- in which one should move from the first in order to get closer -- to the second. Ignores obstacles. Of several equally good directions@@ -124,5 +127,19 @@ towards :: X -> Point -> Point -> Vector towards lxsize loc0 loc1 = assert (loc0 /= loc1 `blame` (loc0, loc1)) $- let v = displacement lxsize loc0 loc1+ let v = displacementXYZ lxsize loc0 loc1 in normalize lxsize v++-- | A vector from a point to another. We have+--+-- > shift loc1 (displacement loc1 loc2) == loc2+--+-- Particularly simple and fast implementation in the linear representation.+displacement :: Point -> Point -> Vector+displacement loc1 loc2 = Vector $ loc2 - loc1++-- | A vector from a point to another. W have+displacePath :: [Point] -> [Vector]+displacePath [] = []+displacePath lp1@(_ : lp2) =+ map (uncurry displacement) $ zip lp1 lp2
LambdaHack.cabal view
@@ -1,9 +1,9 @@ cabal-version: >= 1.10 name: LambdaHack-version: 0.2.0+version: 0.2.1 license: BSD3 license-file: LICENSE-tested-with: GHC == 7.2.2, GHC == 7.4+tested-with: GHC == 7.2.2, GHC == 7.4.1 data-files: LICENSE, CREDITS, PLAYING.md, README.md, config.default, config.bot, scores author: Andres Loeh, Mikolaj Konarski@@ -22,15 +22,17 @@ but the fundamental source of flexibility lies in the strict and type-safe separation of code and content. .- Upcoming new features: improved squad combat,- player action undo/redo, ranged combat animations,- completely redesigned UI. Long term goals are focused- around procedural content generation and include- the improvement of the AI monad EDSL, so that rules+ New in this release are missiles flying for three turns+ (by an old kosmikus' idea), visual feedback for targeting+ and animations of combat and individual monster moves.+ Upcoming new features: improved squad combat, player action+ undo/redo, completely redesigned UI. Long term goals+ are focused around procedural content generation and include+ in-game content creation, auto-balancing, persistent+ content modification based on player behaviour+ and the improvement of the AI monad EDSL, so that rules for synthesising monster behaviour from game content- are extensible, readable and easy to debug,- in-game content creation, auto-balancing and persistent- content modification based on player behaviour.+ are extensible, readable and easy to debug. . A larger game that depends on the LambdaHack library is Allure of the Stars, available from@@ -78,6 +80,7 @@ Game.LambdaHack.Content.RuleKind, Game.LambdaHack.Content.TileKind, Game.LambdaHack.Display,+ Game.LambdaHack.Draw, Game.LambdaHack.Dungeon, Game.LambdaHack.DungeonState, Game.LambdaHack.Effect,@@ -110,10 +113,12 @@ Game.LambdaHack.Strategy, Game.LambdaHack.StrategyAction, Game.LambdaHack.Tile,+ Game.LambdaHack.Time, Game.LambdaHack.Turn, Game.LambdaHack.Utils.Assert, Game.LambdaHack.Utils.File, Game.LambdaHack.Utils.Frequency,+ Game.LambdaHack.Utils.LQueue, Game.LambdaHack.Vector Game.LambdaHack.VectorXY other-modules: Paths_LambdaHack@@ -135,7 +140,7 @@ other-extensions: MultiParamTypeClasses, RankNTypes, ScopedTypeVariables, TypeFamilies, CPP ghc-options: -Wall -fwarn-orphans -fwarn-tabs -fwarn-incomplete-uni-patterns -fwarn-incomplete-record-updates -fwarn-monomorphism-restriction -fwarn-unrecognised-pragmas- ghc-options: -fno-warn-auto-orphans -fno-warn-implicit-prelude -fno-warn-unused-do-bind+ ghc-options: -fno-warn-auto-orphans -fno-warn-implicit-prelude ghc-options: -fno-ignore-asserts -funbox-strict-fields if flag(curses) {@@ -166,7 +171,7 @@ ConfigDefault hs-source-dirs: LambdaHack other-modules: Paths_LambdaHack- build-depends: LambdaHack >= 0.2.0 && < 0.2.1,+ build-depends: LambdaHack >= 0.2.1 && < 0.2.2, template-haskell >= 2.6 && < 3, ConfigFile >= 1.1.1 && < 2,@@ -186,9 +191,9 @@ BangPatterns, RecordWildCards, NamedFieldPuns other-extensions: CPP, QuasiQuotes ghc-options: -Wall -fwarn-orphans -fwarn-tabs -fwarn-incomplete-uni-patterns -fwarn-incomplete-record-updates -fwarn-monomorphism-restriction -fwarn-unrecognised-pragmas- ghc-options: -fno-warn-auto-orphans -fno-warn-implicit-prelude -fno-warn-unused-do-bind+ ghc-options: -fno-warn-auto-orphans -fno-warn-implicit-prelude ghc-options: -fno-ignore-asserts -funbox-strict-fields- ghc-options: -threaded+ ghc-options: -threaded -with-rtsopts=-C0.005 executable DumbBot main-is: Main.hs@@ -199,5 +204,5 @@ default-extensions: MonoLocalBinds, BangPatterns, RecordWildCards, NamedFieldPuns ghc-options: -Wall -fwarn-orphans -fwarn-tabs -fwarn-incomplete-uni-patterns -fwarn-incomplete-record-updates -fwarn-monomorphism-restriction -fwarn-unrecognised-pragmas- ghc-options: -fno-warn-auto-orphans -fno-warn-implicit-prelude -fno-warn-unused-do-bind+ ghc-options: -fno-warn-auto-orphans -fno-warn-implicit-prelude ghc-options: -fno-ignore-asserts -funbox-strict-fields
LambdaHack/Content/ActorKind.hs view
@@ -5,6 +5,7 @@ import qualified Game.LambdaHack.Content as Content import Game.LambdaHack.Content.ActorKind import Game.LambdaHack.Random+import Game.LambdaHack.Time cdefs :: Content.CDefs ActorKind cdefs = Content.CDefs@@ -13,9 +14,9 @@ , getFreq = afreq , validate = avalidate , content =- [hero, eye, fastEye, nose]+ [hero, projectile, eye, fastEye, nose] }-hero, eye, fastEye, nose :: ActorKind+hero, projectile, eye, fastEye, nose :: ActorKind hero = ActorKind { asymbol = '@'@@ -23,24 +24,37 @@ , afreq = [("hero", 1)] -- Does not appear randomly in the dungeon. , acolor = BrWhite -- Heroes white, monsters colorful. , ahp = RollDice 60 1- , aspeed = 10+ , aspeed = toSpeed 2 , asight = True , asmell = False , aiq = 13 -- Can see hidden doors, when he is under alien control.- , aregen = 5000+ , aregen = 500 } +projectile = ActorKind -- includes homing missiles+ { asymbol = '*'+ , aname = "projectile"+ , afreq = [("projectile", 1)] -- Does not appear randomly in the dungeon.+ , acolor = BrWhite+ , ahp = RollDice 0 0+ , aspeed = toSpeed 0+ , asight = False+ , asmell = False+ , aiq = 0+ , aregen = maxBound+ }+ eye = ActorKind { asymbol = 'e' , aname = "reducible eye" , afreq = [("monster", 60), ("summon", 50)] , acolor = BrRed , ahp = RollDice 3 4- , aspeed = 10+ , aspeed = toSpeed 2 , asight = True , asmell = False , aiq = 8- , aregen = 1000+ , aregen = 100 } fastEye = ActorKind { asymbol = 'e'@@ -48,11 +62,11 @@ , afreq = [("monster", 15)] , acolor = BrBlue , ahp = RollDice 1 4- , aspeed = 5+ , aspeed = toSpeed 4 , asight = True , asmell = False , aiq = 12- , aregen = 50 -- Regenerates fast (at max HP most of the time!).+ , aregen = 5 -- Regenerates fast (at max HP most of the time!). } nose = ActorKind { asymbol = 'n'@@ -60,9 +74,9 @@ , afreq = [("monster", 20), ("summon", 100)] , acolor = Green , ahp = RollDice 7 2- , aspeed = 11+ , aspeed = toSpeed 1.8 , asight = False , asmell = True , aiq = 0- , aregen = 1000+ , aregen = 100 }
LambdaHack/Content/CaveKind.hs view
@@ -36,8 +36,12 @@ , copenChance = 1%10 , chiddenChance = 1%5 , citemNum = RollDice 5 2- , cdefTile = "fillerWall"- , ccorTile = "darkCorridor"+ , cdefaultTile = "fillerWall"+ , ccorridorTile = "darkCorridor"+ , cfillerTile = "fillerWall"+ , cdarkLegendTile = "darkLegend"+ , clitLegendTile = "litLegend"+ , chiddenTile = "hidden" } arena = rogue { csymbol = 'A'@@ -49,8 +53,8 @@ , cvoidChance = 1%3 , cnonVoidMin = 2 , citemNum = RollDice 3 2 -- few rooms- , cdefTile = "floorArenaLit"- , ccorTile = "path"+ , cdefaultTile = "floorArenaLit"+ , ccorridorTile = "path" } empty = rogue { csymbol = '.'@@ -64,8 +68,8 @@ , cnonVoidMin = 1 , cminStairDist = 50 , citemNum = RollDice 6 2 -- whole floor strewn with treasure- , cdefTile = "floorRoomLit"- , ccorTile = "floorRoomLit"+ , cdefaultTile = "floorRoomLit"+ , ccorridorTile = "floorRoomLit" } noise = rogue { csymbol = '!'@@ -77,6 +81,6 @@ , cvoidChance = 0 , cnonVoidMin = 0 , citemNum = RollDice 3 2 -- few rooms- , cdefTile = "noiseSet"- , ccorTile = "path"+ , cdefaultTile = "noiseSet"+ , ccorridorTile = "path" }
LambdaHack/Content/ItemKind.hs view
@@ -15,9 +15,9 @@ , getFreq = ifreq , validate = ivalidate , content =- [amulet, dart, gem1, gem2, gem3, gold, javelin, potion1, potion2, potion3, ring, scroll1, scroll2, scroll3, sword, wand, fist, foot, tentacle, weight]+ [amulet, dart, gem1, gem2, gem3, gold, harpoon, potion1, potion2, potion3, ring, scroll1, scroll2, scroll3, sword, wand, fist, foot, tentacle, weight] }-amulet, dart, gem1, gem2, gem3, gold, javelin, potion1, potion2, potion3, ring, scroll1, scroll2, scroll3, sword, wand, fist, foot, tentacle, weight :: ItemKind+amulet, dart, gem1, gem2, gem3, gold, harpoon, potion1, potion2, potion3, ring, scroll1, scroll2, scroll3, sword, wand, fist, foot, tentacle, weight :: ItemKind gem, potion, scroll :: ItemKind -- generic templates @@ -32,7 +32,9 @@ , icount = intToDeep 1 , ipower = (RollDice 2 3, RollDice 1 10) , iverbApply = "tear down"- , iverbProject = "throw"+ , iverbProject = "cast"+ , iweight = 30+ , itoThrow = -50 -- not dense enough } dart = ItemKind { isymbol = '|'@@ -43,7 +45,9 @@ , icount = (RollDice 3 3, RollDice 0 0) , ipower = intToDeep 0 , iverbApply = "snap"- , iverbProject = "throw"+ , iverbProject = "hurl"+ , iweight = 50+ , itoThrow = 0 -- a cheap dart } gem = ItemKind { isymbol = '*'@@ -54,16 +58,18 @@ , icount = intToDeep 0 , ipower = intToDeep 0 , iverbApply = "crush"- , iverbProject = "throw"+ , iverbProject = "toss"+ , iweight = 50+ , itoThrow = 0 } gem1 = gem- { icount = (RollDice 1 1, RollDice 1 1) -- appears on lvl 1+ { icount = (RollDice 0 0, RollDice 1 1) -- appears on lvl 1 } gem2 = gem { icount = (RollDice 0 0, RollDice 1 2) -- appears halfway } gem3 = gem- { icount = (RollDice 0 0, RollDice 1 1) -- appears on max depth+ { icount = (RollDice 0 0, RollDice 1 3) -- appears on max depth } gold = ItemKind { isymbol = '$'@@ -74,18 +80,22 @@ , icount = (RollDice 0 0, RollDice 10 10) , ipower = intToDeep 0 , iverbApply = "grind"- , iverbProject = "throw"+ , iverbProject = "toss"+ , iweight = 31+ , itoThrow = 0 }-javelin = ItemKind+harpoon = ItemKind { isymbol = '|'- , iname = "javelin"+ , iname = "harpoon" , ifreq = [("dng", 30)] , iflavour = zipPlain [Brown] , ieffect = Wound (RollDice 1 2) , icount = (RollDice 0 0, RollDice 2 2) , ipower = (RollDice 1 1, RollDice 2 2) , iverbApply = "break up"- , iverbProject = "throw"+ , iverbProject = "hurl"+ , iweight = 4000+ , itoThrow = 0 -- cheap but deadly } potion = ItemKind { isymbol = '!'@@ -97,6 +107,8 @@ , ipower = intToDeep 0 , iverbApply = "gulp down" , iverbProject = "lob"+ , iweight = 200+ , itoThrow = -50 -- oily, bad grip } potion1 = potion { ifreq = [("dng", 5)]@@ -120,7 +132,9 @@ , icount = intToDeep 1 , ipower = (RollDice 1 6, RollDice 3 2) , iverbApply = "squeeze down"- , iverbProject = "throw"+ , iverbProject = "toss"+ , iweight = 15+ , itoThrow = 0 } scroll = ItemKind { isymbol = '?'@@ -131,7 +145,9 @@ , icount = intToDeep 1 , ipower = intToDeep 0 , iverbApply = "decipher"- , iverbProject = "throw"+ , iverbProject = "lob"+ , iweight = 50+ , itoThrow = -75 -- bad shape, even rolled up } scroll1 = scroll { ieffect = SummonFriend@@ -153,6 +169,8 @@ , ipower = (RollDice 1 2, RollDice 4 2) , iverbApply = "hit" , iverbProject = "heave"+ , iweight = 2000+ , itoThrow = -50 -- ensuring it hits with the tip costs speed } wand = ItemKind { isymbol = '/'@@ -164,6 +182,8 @@ , ipower = intToDeep 0 , iverbApply = "snap" , iverbProject = "zap"+ , iweight = 300+ , itoThrow = 25 -- magic } fist = sword { isymbol = '@'
LambdaHack/Content/RuleKind.hs view
@@ -37,4 +37,6 @@ , rtitle = "LambdaHack" , rpathsDataFile = Self.getDataFileName , rpathsVersion = Self.version+ , ritemMelee = ")"+ , ritemProject = "!?|/" }
PLAYING.md view
@@ -57,32 +57,29 @@ /|\ /|\ 1 2 3 b j n -Shift and a movement key make the hero run in the indicated direction,-until anything of interest is spotted. '5' and '.' skip a turn.-(Note that If you are using the curses or vty frontends,-numerical keypad may not work correctly depending on the versions-of curses, terminfo and terminal emulators. Vi keys should work regardless.)+SHIFT (or CTRL) and a movement key make the hero run in the indicated+direction, until anything of interest is spotted. '5' and '.' skip a turn. Melee, searching for secret doors and opening closed doors can be done by bumping into a monster, a wall and a door, respectively. Below are the default keys for major commands. Those of them that take hero time are marked with a *. - key command- < ascend a level*- > descend a level*- ? display help- Q quit without saving- X save and exit the game- c close a door*- d drop an object*- g get an object*- i display inventory- o open a door*- q quaff a potion*- r read a scroll*- t throw a dart*- z zap a wand*+ key command+ < ascend a level*+ > descend a level*+ ? display help+ Q quit without saving+ X save and exit the game+ c close a door*+ d drop an object*+ g get an object*+ i display inventory+ o open a door*+ q quaff a potion*+ r read a scroll*+ t throw a dart*+ z zap a wand* To make a ranged attack, you need to set your target first, using targeting mode. Note that the target, for the few commands that require any,@@ -95,28 +92,29 @@ than the selected hero). The targeting commands and all the less used commands are listed below. None of them takes hero time. - key command- ESC cancel action- RET accept choice- SPACE clear messages- TAB cycle among heroes on level- * target monster- / target location- D dump current configuration- P display previous messages- V display game version- [ target next shallower level- ] target next deeper level- { target 10 levels shallower- } target 10 levels deeper- 0--9 select a hero anywhere in the dungeon (gtk only)+ key command+ ESC cancel action+ RET accept choice+ SPACE clear messages+ TAB cycle among heroes on level+ * target monster+ / target location+ D dump current configuration+ P display previous messages+ V display game version+ [ target next shallower level+ ] target next deeper level+ { target 10 levels shallower+ } target 10 levels deeper+ 0--9 select a hero anywhere in the dungeon -There are also some debug and cheat keys. Use at your own peril!+There are also some debug and cheat keys, all entered with the CTRL+key modifier. Use at your own peril! - key command- O toggle "omniscience"- I inform about level meta-data- R rotate display modes+ key command+ CTRL-o toggle "omniscience"+ CTRL-i inform about level meta-data+ CTRL-r rotate vision modes (effective next turn) Monsters
README.md view
@@ -61,12 +61,21 @@ e.g., by helping it play longer, as in the supplied config.bot. -Compatibility note-------------------+Compatibility notes+------------------- The current code was tested with GHC 7.2.2 and several pre-release versions of GHC 7.4. A [few tweaks] [6] are needed to compile with 7.0 and some more are needed for 6.12.++If you are using the curses or vty frontends,+numerical keypad may not work correctly depending on the versions+of curses, terminfo and terminal emulators.+Selecting heroes via number keys or SHIFT-keypad keys is disabled+with curses, because CTRL-keypad for running does not work there,+so the numbers produced by the keypad have to be used. With vty on xterm,+CTRL-direction keys seem to work OK, but on rxvt they do not.+Vi keys (ykuhlbjn) should work everywhere regardless. Gtk works fine, too. Further information
config.default view
@@ -18,6 +18,8 @@ ; braceright: TgtAscend (-10) ; slash: TgtFloor ; asterisk: TgtEnemy+; plus: EpsIncr True+; minus: EpsIncr False ; Tab: HeroCycle ; ; ; ; Items.@@ -27,7 +29,7 @@ ; q: Apply { verb = "quaff", object = "potion", syms = "!" } ; r: Apply { verb = "read", object = "scroll", syms = "?" } ; z: Project { verb = "zap", object = "wand", syms = "/" }-; t: Project { verb = "throw", object = "dart", syms = "|" }+; t: Project { verb = "throw", object = "missile", syms = "|" } ; ; ; ; Saving or ending the game. ; X: GameSave@@ -84,7 +86,7 @@ ; period: KP_Begin ; [monsters]-; smellTimeout: 1000+; smellTimeout: 100 ; [ui] ; font: Terminus,Monospace normal normal normal normal 12
scores view
binary file changed (226 → 45 bytes)