diff --git a/README.md b/README.md index d173f3f..cc195df 100644 --- a/README.md +++ b/README.md @@ -64,7 +64,7 @@ TODO - An example playthrough, with pictures and explanations -\pagebreak +
## Writing your own stages @@ -250,10 +250,22 @@ If we look at the example, all the objects are Entry = empty ConditionList + Action ('leave()') ``` -\pagebreak + ## Development notes +### Engine architecture + +TODO + +#### Monads/Monad stack + +TODO + +### Tests + +TODO + ### Assets & dependencies The following assets were used (and modified if specified): @@ -268,6 +280,7 @@ The following assets were used (and modified if specified): RPG-Engine makes use of the following libraries: +- [directory](https://hackage.haskell.org/package/directory) for listing levels in a directory - [gloss](https://hackage.haskell.org/package/gloss) for game rendering - [gloss-juicy](https://hackage.haskell.org/package/gloss-juicy) for rendering images - [hspec](https://hackage.haskell.org/package/hspec) for testing @@ -278,12 +291,23 @@ RPG-Engine makes use of the following libraries: The following ideas could (or should) be implemented in the future of this project. -- [ ] Entity system: With en ES, you can implement moving entities and repeated input. It also resembles the typical +- [ ] **Entity system:** With en ES, you can implement moving entities and repeated input. It also resembles the typical game loop more closely which can make it easier to implement other ideas in the future. +- [ ] **Game music:** Ambient game music and sound effects can improve the gaming experience I think. +- [ ] **Expand configuration file:** Implement the same methods for parsing stage description files to a configuration file, + containing keybinds, dimension sizes, even window titles, making this a truly customizable engine. +- [ ] **Camera follows player:** The camera should follow the player, making it always center. This allows for larger levels + increases the immersion of the game. -- [ ] Game music: Ambient game music and sound effects can improve the gaming experience I think. + -\pagebreak +## Conclusion + +Parsing was way harder than I initially expected. About half of my time on this project was spent writing the parser. + +TODO + + ## References diff --git a/assets/environment/overlay.png b/assets/environment/overlay.png new file mode 100644 index 0000000..b5d500d Binary files /dev/null and b/assets/environment/overlay.png differ diff --git a/assets/unkown.png b/assets/unknown.png similarity index 100% rename from assets/unkown.png rename to assets/unknown.png diff --git a/levels/level2.txt b/levels/level2.txt index bb589d6..ca3a220 100644 --- a/levels/level2.txt +++ b/levels/level2.txt @@ -19,8 +19,8 @@ levels: [ items: [ { id: "key", - x: 1, - y: 2, + x: 0, + y: 1, name: "Sleutel", description: "Deze sleutel kan een deur openen", useTimes: 1, @@ -35,8 +35,8 @@ levels: [ entities: [ { id: "door", - x: 1, - y: 4, + x: 0, + y: 3, name: "Deur", description: "Deze deur kan geopend worden met een sleutel", direction: up, diff --git a/levels/level3.txt b/levels/level3.txt index f7e1e5d..f943fe7 100644 --- a/levels/level3.txt +++ b/levels/level3.txt @@ -69,8 +69,8 @@ levels: [ actions: { [inventoryContains(potion)] increasePlayerHp(potion), - [inventoryContains(sword)] decreaseHp(m1, sword), - [] decreaseHp(m1, dagger), + [inventoryContains(sword)] decreaseHp(devil, sword), + [] decreaseHp(devil, dagger), [] leave() } } diff --git a/levels/level4.txt b/levels/level4.txt new file mode 100644 index 0000000..f7ec761 --- /dev/null +++ b/levels/level4.txt @@ -0,0 +1,134 @@ +player: { + hp: 50, + inventory: [ + { + id: "dagger", + x: 0, + y: 0, + name: "Dolk", + description: "Basis schade tegen monsters", + useTimes: infinite, + value: 10, + + actions: {} + } + ] +} + +levels: [ + { + layout: { + | * * * * * * + | * s . . e * + | * * * * * * + }, + + items: [], + + entities: [] + }, + { + layout: { + | * * * + | * e * + | * . * + | * . * + | * . * + | * . * + | * s * + | * * * + }, + + items: [ + { + id: "key", + x: 0, + y: 1, + name: "Sleutel", + description: "Deze sleutel kan een deur openen", + useTimes: 1, + value: 0, + actions: { + [not(inventoryFull())] retrieveItem(key), + [] leave() + } + } + ], + + entities: [ + { + id: "door", + x: 0, + y: 3, + name: "Deur", + description: "Deze deur kan geopend worden met een sleutel", + direction: up, + + actions: { + [inventoryContains(key)] useItem(key), + [] leave() + } + } + ] + }, + { + layout: { + | * * * * * * * * + | * . . . . . . * + | * s . . . . . * + | * . . . . . e * + | * . . . . . . * + | * * * * * * * * + }, + + items: [ + { + id: "sword", + x: 2, + y: 3, + name: "Zwaard", + description: "Meer schade tegen monsters", + useTimes: infinite, + value: 25, + + actions: { + [not(inventoryFull())] retrieveItem(sword), + [] leave() + } + }, + { + id: "potion", + x: 3, + y: 1, + name: "Levensbrouwsel", + description: "Geeft een aantal levenspunten terug", + useTimes: 1, + value: 50, + + actions: { + [not(inventoryFull())] retrieveItem(potion), + [] leave() + } + } + ], + + entities: [ + { + id: "devil", + x: 4, + y: 3, + name: "Duivel", + description: "Een monster uit de hel", + hp: 50, + value: 5, + + actions: { + [inventoryContains(potion)] increasePlayerHp(potion), + [inventoryContains(sword)] decreaseHp(devil, sword), + [] decreaseHp(devil, dagger), + [] leave() + } + } + ] + } +] diff --git a/levels/level_more_levels.txt b/levels/level_more_levels.txt new file mode 100644 index 0000000..879dc6b --- /dev/null +++ b/levels/level_more_levels.txt @@ -0,0 +1,138 @@ +# Dit gehele bestand is een Block + +# Dit is een entry: key + value +player: { # Value is hier een block + hp: 50, + inventory: [ # BlockList + { + id: "dagger", + x: 0, + y: 0, + name: "Dolk", + description: "Basis schade tegen monsters", + useTimes: infinite, + value: 10, + + actions: {} + } + ] +} + +# Dit is een entry +levels: [ + { + layout: { + | * * * * * * + | * s . . e * + | * * * * * * + }, + + items: [], + + entities: [] + }, + { + layout: { + | * * * + | * e * + | * . * + | * . * + | * . * + | * . * + | * s * + | * * * + }, + + items: [ + { + id: "key", + x: 1, + y: 2, + name: "Sleutel", + description: "Deze sleutel kan een deur openen", + useTimes: 1, + value: 0, + actions: { + [not(inventoryFull())] retrieveItem(key), + [] leave() + } + } + ], + + entities: [ + { + id: "door", + x: 1, + y: 4, + name: "Deur", + description: "Deze deur kan geopend worden met een sleutel", + direction: up, + + actions: { + [inventoryContains(key)] useItem(key), + [] leave() + } + } + ] + }, + { + layout: { + | * * * * * * * * + | * . . . . . . * + | * s . . . . . * + | * . . . . . e * + | * . . . . . . * + | * * * * * * * * + }, + + items: [ + { + id: "sword", + x: 2, + y: 3, + name: "Zwaard", + description: "Meer schade tegen monsters", + useTimes: infinite, + value: 25, + + actions: { + [not(inventoryFull())] retrieveItem(sword), + [] leave() + } + }, + { + id: "potion", + x: 3, + y: 1, + name: "Levensbrouwsel", + description: "Geeft een aantal levenspunten terug", + useTimes: 1, + value: 50, + + actions: { + [not(inventoryFull())] retrieveItem(potion), + [] leave() + } + } + ], + + entities: [ + { + id: "devil", + x: 4, + y: 3, + name: "Duivel", + description: "Een monster uit de hel", + hp: 50, + value: 5, + + actions: { + [inventoryContains(potion)] increasePlayerHp(potion), + [inventoryContains(sword)] decreaseHp(m1, sword), + [] decreaseHp(m1, dagger), + [] leave() + } + } + ] + } +] diff --git a/lib/RPGEngine/Data.hs b/lib/RPGEngine/Data.hs index 3a701a2..2ebe39b 100644 --- a/lib/RPGEngine/Data.hs +++ b/lib/RPGEngine/Data.hs @@ -48,6 +48,7 @@ instance Living Player where -- Current state of the game. data State = Menu + | LvlSelect | Playing | Pause | Win diff --git a/lib/RPGEngine/Data/State.hs b/lib/RPGEngine/Data/State.hs index 057bc9e..0cc5347 100644 --- a/lib/RPGEngine/Data/State.hs +++ b/lib/RPGEngine/Data/State.hs @@ -14,7 +14,7 @@ import RPGEngine.Data -- Get the next state based on the current state nextState :: State -> State -nextState Menu = Playing +nextState Menu = LvlSelect nextState Playing = Pause nextState Pause = Playing nextState _ = Menu diff --git a/lib/RPGEngine/Input.hs b/lib/RPGEngine/Input.hs index a737d05..2fae6bb 100644 --- a/lib/RPGEngine/Input.hs +++ b/lib/RPGEngine/Input.hs @@ -15,8 +15,9 @@ import Graphics.Gloss.Interface.IO.Game -- Handle all input for RPG-Engine handleAllInput :: InputHandler Game -handleAllInput ev g@Game{ state = Playing } = handlePlayInputs ev g -handleAllInput ev g = handleAnyKey setNextState ev g +handleAllInput ev g@Game{ state = Playing } = handlePlayInputs ev g +handleAllInput ev g@Game{ state = LvlSelect } = handleLvlSelectInput ev g +handleAllInput ev g = handleAnyKey setNextState ev g ---------------------------------------------------------------------- @@ -38,6 +39,10 @@ handlePlayInputs = composeInputHandlers [ handleKey (Char 'a') $ movePlayer West ] +-- Input for selection a level to load +handleLvlSelectInput :: InputHandler Game +handleLvlSelectInput = composeInputHandlers [] + -- Go to the next stage of the Game setNextState :: Game -> Game setNextState game = game{ state = newState } diff --git a/lib/RPGEngine/Input/LvlSelect.hs b/lib/RPGEngine/Input/LvlSelect.hs new file mode 100644 index 0000000..5b1bf35 --- /dev/null +++ b/lib/RPGEngine/Input/LvlSelect.hs @@ -0,0 +1,12 @@ +module RPGEngine.Input.LvlSelect +( getLvlList +) where + +import GHC.IO (unsafePerformIO) +import System.Directory (getDirectoryContents) + +lvlFolder :: FilePath +lvlFolder = "levels" + +getLvlList :: [FilePath] +getLvlList = unsafePerformIO $ getDirectoryContents lvlFolder \ No newline at end of file diff --git a/lib/RPGEngine/Render.hs b/lib/RPGEngine/Render.hs index 1468f77..09b9b66 100644 --- a/lib/RPGEngine/Render.hs +++ b/lib/RPGEngine/Render.hs @@ -9,14 +9,24 @@ module RPGEngine.Render import RPGEngine.Data ( State(..), - Game(..) ) -import RPGEngine.Render.Level + Game(..), Player (..) ) +import RPGEngine.Render.Level ( renderLevel ) - import Graphics.Gloss - ( white, pictures, text, Display(InWindow), Color, Picture ) -import RPGEngine.Render.Player (renderPlayer) + ( white, + pictures, + text, + Display(InWindow), + Color, + Picture, + scale, + translate ) +import RPGEngine.Render.Player (renderPlayer, focusPlayer) import RPGEngine.Render.GUI (renderGUI) +import Graphics.Gloss.Data.Picture (color) +import RPGEngine.Render.Core (overlay) +import RPGEngine.Input.LvlSelect (getLvlList) +import RPGEngine.Render.LvlSelect (renderLvlList) ----------------------------- Constants ------------------------------ @@ -32,11 +42,12 @@ initWindow = InWindow -- Render the game render :: Game -> Picture -render g@Game{ state = Menu } = renderMenu g -render g@Game{ state = Playing } = renderPlaying g -render g@Game{ state = Pause } = renderPause g -render g@Game{ state = Win } = renderWin g -render g@Game{ state = Lose } = renderLose g +render g@Game{ state = Menu } = renderMenu g +render g@Game{ state = LvlSelect } = renderLevelSelection g +render g@Game{ state = Playing } = renderPlaying g +render g@Game{ state = Pause } = renderPause g +render g@Game{ state = Win } = renderWin g +render g@Game{ state = Lose } = renderLose g ---------------------------------------------------------------------- @@ -44,6 +55,10 @@ render g@Game{ state = Lose } = renderLose g renderMenu :: Game -> Picture renderMenu _ = text "[Press any key to start]" +-- TODO +renderLevelSelection :: Game -> Picture +renderLevelSelection _ = renderLvlList getLvlList + renderPlaying :: Game -> Picture renderPlaying g@Game{ playing = lvl, player = player } = pictures [ renderLevel lvl, @@ -51,9 +66,12 @@ renderPlaying g@Game{ playing = lvl, player = player } = pictures [ renderGUI g ] --- TODO renderPause :: Game -> Picture -renderPause _ = text "[Press any key to continue]" +renderPause g = pictures [renderPlaying g, pause] + where pause = pictures [ + overlay, + color white $ scale 0.5 0.5 $ text "[Press any key to continue]" + ] -- TODO renderWin :: Game -> Picture diff --git a/lib/RPGEngine/Render/Core.hs b/lib/RPGEngine/Render/Core.hs index 0e51063..e5155f4 100644 --- a/lib/RPGEngine/Render/Core.hs +++ b/lib/RPGEngine/Render/Core.hs @@ -1,6 +1,6 @@ module RPGEngine.Render.Core where -import Graphics.Gloss ( Picture, translate ) +import Graphics.Gloss ( Picture, translate, pictures ) import GHC.IO (unsafePerformIO) import Graphics.Gloss.Juicy (loadJuicyPNG) import Data.Maybe (fromJust) @@ -21,7 +21,7 @@ assetsFolder :: FilePath assetsFolder = "assets/" unknownImage :: FilePath -unknownImage = "unkown.png" +unknownImage = "unknown.png" allEntities :: [(String, FilePath)] allEntities = [ @@ -32,6 +32,7 @@ allEntities = [ allEnvironment :: [(String, FilePath)] allEnvironment = [ ("void", "void.png"), + ("overlay", "overlay.png"), ("tile", "tile.png"), ("wall", "wall.png"), ("entrance", "entrance.png"), @@ -47,7 +48,7 @@ allItems = [ -- Map of all renders library :: [(String, Picture)] library = unknown:entities ++ environment ++ gui ++ items - where unknown = ("unkown", renderPNG (assetsFolder ++ unknownImage)) + where unknown = ("unknown", renderPNG (assetsFolder ++ unknownImage)) entities = map (\(f, s) -> (f, renderPNG (assetsFolder ++ "entities/" ++ s))) allEntities environment = map (\(f, s) -> (f, renderPNG (assetsFolder ++ "environment/" ++ s))) allEnvironment gui = [] @@ -71,4 +72,14 @@ getRender id = get filtered setRenderPos :: Int -> Int -> Picture -> Picture setRenderPos x y = translate floatX floatY where floatX = fromIntegral x * zoom * resolution - floatY = fromIntegral y * zoom * resolution \ No newline at end of file + floatY = fromIntegral y * zoom * resolution + +overlay :: Picture +overlay = setRenderPos offX offY $ pictures voids + where voids = [setRenderPos x y void | x <- [0 .. width], y <- [0 .. height]] + void = getRender "overlay" + intZoom = round zoom :: Int + height = round $ 4320 / resolution / zoom + width = round $ 7680 / resolution / zoom + offX = negate (width `div` 2) + offY = negate (height `div` 2) \ No newline at end of file diff --git a/lib/RPGEngine/Render/LvlSelect.hs b/lib/RPGEngine/Render/LvlSelect.hs new file mode 100644 index 0000000..b395e9d --- /dev/null +++ b/lib/RPGEngine/Render/LvlSelect.hs @@ -0,0 +1,15 @@ +module RPGEngine.Render.LvlSelect +( renderLvlList +) where + +import Graphics.Gloss ( Picture, pictures, translate, scale ) +import Graphics.Gloss.Data.Picture (blank, text) +import RPGEngine.Render.Core (resolution, zoom) + +-- Render all level names, under each other. +renderLvlList :: [FilePath] -> Picture +renderLvlList list = pictures $ map render entries + where entries = zip [0::Int .. ] list + render (i, path) = scale zoomed zoomed $ translate 0 (offset i) $ text path + zoomed = 0.1 * zoom + offset i = negate (2 * resolution * zoom * fromIntegral i) \ No newline at end of file diff --git a/lib/RPGEngine/Render/Player.hs b/lib/RPGEngine/Render/Player.hs index 7adb8b0..0b6a124 100644 --- a/lib/RPGEngine/Render/Player.hs +++ b/lib/RPGEngine/Render/Player.hs @@ -1,11 +1,17 @@ module RPGEngine.Render.Player ( renderPlayer +, focusPlayer ) where -import RPGEngine.Data (Player(..)) - +import RPGEngine.Data (Player(..), Game(..)) import Graphics.Gloss (Picture, text) -import RPGEngine.Render.Core (getRender, setRenderPos) +import RPGEngine.Render.Core (getRender, setRenderPos, zoom, resolution) +import Graphics.Gloss.Data.Picture (translate) renderPlayer :: Player -> Picture -renderPlayer Player{ position = (x, y) } = setRenderPos x y $ getRender "player" \ No newline at end of file +renderPlayer Player{ position = (x, y) } = setRenderPos x y $ getRender "player" + +focusPlayer :: Game -> Picture -> Picture +focusPlayer Game{ player = Player{ position = (x, y)}} = translate centerX centerY + where centerX = resolution * zoom * fromIntegral (negate x) + centerY = resolution * zoom * fromIntegral (negate y) \ No newline at end of file diff --git a/rpg-engine.cabal b/rpg-engine.cabal index d7c3090..9e11d89 100644 --- a/rpg-engine.cabal +++ b/rpg-engine.cabal @@ -8,6 +8,7 @@ library hs-source-dirs: lib build-depends: base >= 4.7 && <5, + directory >= 1.3.6.0, gloss >= 1.11 && < 1.14, gloss-juicy >= 0.2.3, parsec >= 3.1.15.1 exposed-modules: @@ -20,6 +21,7 @@ library RPGEngine.Input RPGEngine.Input.Core RPGEngine.Input.Level + RPGEngine.Input.LvlSelect RPGEngine.Input.Player RPGEngine.Parse @@ -31,6 +33,7 @@ library RPGEngine.Render.Core RPGEngine.Render.GUI RPGEngine.Render.Level + RPGEngine.Render.LvlSelect RPGEngine.Render.Player executable rpg-engine diff --git a/verslag.pdf b/verslag.pdf index f2dba5d..24a6e81 100644 Binary files a/verslag.pdf and b/verslag.pdf differ