diff --git a/.gitignore b/.gitignore
index e9a934f..43be4f3 100644
--- a/.gitignore
+++ b/.gitignore
@@ -12,3 +12,4 @@ extra/
*.dll
stack.yaml.lock
+.vscode/settings.json
diff --git a/.vscode/tasks.json b/.vscode/tasks.json
index c27db8b..3cf0327 100644
--- a/.vscode/tasks.json
+++ b/.vscode/tasks.json
@@ -49,9 +49,10 @@
"args": [
"-s",
"-o", "verslag.pdf",
- "-f", "markdown+smart+header_attributes+yaml_metadata_block+auto_identifiers",
+ "-f", "markdown+smart+header_attributes+yaml_metadata_block+auto_identifiers+implicit_figures",
"--pdf-engine", "lualatex",
"--template", "eisvogel",
+ "--dpi=300",
"header.yaml",
"README.md"
],
diff --git a/README.md b/README.md
index 51913b2..39a363e 100644
--- a/README.md
+++ b/README.md
@@ -1,40 +1,9 @@
@@ -45,40 +14,193 @@ RPG-Engine is a game engine for playing and creating your own RPG games.
If you are interested in the development side of things, [development notes can be found here](#Development-notes).
-This README serves as both documentation and project report, so excuse the details that might not be important for the average user.
+This README serves as both documentation and project report, so excuse the details that might not be important for the average reader.
## Playing the game
-These are the keybinds *in* the game. All other keybinds in the menus should be straightforward.
+These are the keybinds while *in* game. All other keybinds in menus etc. should be straightforward.
-| Action | Primary | Secondary |
-| -------------- | ------------- | ----------- |
-| Move up | `Arrow Up` | `w` |
-| Move left | `Arrow Left` | `a` |
-| Move down | `Arrow Down` | `s` |
-| Move right | `Arrow Right` | `d` |
-| Interaction | `Space` | `f` |
-| Show inventory | `i` | `Tab` |
-| Restart level | `r` | |
-| Quit game | `Esc` | |
+| Action | Primary | Secondary |
+| -------------- | ------------- | ------------ |
+| Move up | `Arrow Up` | `w` |
+| Move left | `Arrow Left` | `a` |
+| Move down | `Arrow Down` | `s` |
+| Move right | `Arrow Right` | `d` |
+| Interaction | `Space` | `f`, `Enter` |
+| Show inventory | `i` | `Tab` |
+| Restart level | `r` | |
+| Quit game | `Esc` | |
### Example playthrough
-TODO
+{ width=600 }
+{ width=600 }
+{ width=600 }
+{ width=600 }
+{ width=600 }
+{ width=600 }
+{ width=600 }
+{ width=600 }
+{ width=600 }
+{ width=600 }
+{ width=600 }
+{ width=600 }
+{ width=600 }
+{ width=600 }
+{ width=600 }
-- An example playthrough, with pictures and explanations
+## Development notes
+
+### Engine architecture
+
+`RPGEngine` is the main module. It contains the `playRPGEngine` function which bootstraps the whole game. It is also
+ the game loop. From here, `RPGEngine` talks to its submodules.
+
+These submodules are `Config`, `Data`, `Input`, `Parse` & `Render`. They are all responsible for their own part. However,
+ each of these submodules has their own submodules to divide the work. They are conveniently named after the state of
+ the game that they work with, e.g. the main menu has a module & when the game is playing is a different module. A special
+ one is `Core`, which is kind of like a library for every piece. It contains functions that are regularly used by the other modules.
+
+- `Config`: Configuration values, should ultimately be moved into parsing from a file.
+- `Data`: Data containers and accessors of information.
+- `Input`: Anything that handles input or changes the state of the game.
+- `Parse`: Parsing
+- `Render`: Rendering of the game and everything below that.
+
+#### Monads/Monad stack
+
+Monads:
+
+- Extensive use of `Maybe` for integers or infinity and `do` in parser implementation.
+- `IO` to handle external information
+- ...
+
+Monad transformer: ??
+
+I am afraid I did not write any monad transformers in this project. I think I could (and should) have focused more on
+ writing monads and monad transformers. In hindsight, I can see where I could and should have used them. I can think of
+ plenty of ways to make the current implementation simpler. This is unfortunate. However, I want to believe that my next
+ time writing a more complex Haskell program, I will remember using monad transformers. Sadly, I forgot this time.
+
+An example of where I would use a monad transformer - in hindsight:
+
+1. Interactions in game: something along the lines of ...
+
+```haskell
+newtype StateT m a = StateT { runStateT :: m a }
+
+instance Monad m => Monad (StateT m) where
+ return = lift . return
+ x >>= f = StateT $ do
+ v <- runStateT x
+ case v of
+ Playing level -> runStateT ( f level )
+ Paused continue -> runStateT ( continue >>= f )
+ -- etc
+
+class MonadTransformer r where
+ lift :: Monad m => m a -> (r m) a
+instance MonadTransformer StateT where
+ lift = StateT
+```
+
+2. Interaction with the outside world should also be done with Monad(transformers) instead of using `unsafePerformIO`.
+
+### Tests
+
+Overall, only parsing is tested using Hspec. However, parsing is tested *thoroughly* and I am quite sure that there aren't
+ a lot of edge cases that I did not catch. This makes for a relaxing environment where you can quickly check if a change
+ you made breaks anything.
+
+`Spec` is the main module. It does not contain any tests, but functions as the 'discover' module to find the other tests
+ in its folder.
+
+`Parser.StructureSpec` tests functionality of `RPGEngine.Parse.TextToStructure`, `Parser.GameSpec` tests functionality
+ of `RPGEngine.Parse.StructureToGame`.
+
+Known issues:
+
+- [ ] Rendering is still not centered, I am sorry for those with small screens.
+- [ ] Config files cannot end with an empty line. I could not get that to work and I decided that it was more important
+ to implement other functionality first. Unfortunately, I was not able to get back to it yet.
+- [ ] The parser is unable to parse layouts with trailing whitespace.
+
+## Conclusion
+
+Parsing was way harder than I initially expected. I believe over half my time on this project was spent trying to write the
+ parser. I am still not absolutely sure that it will work with *everything*, but it gets the job done at the moment. I don't
+ know if parsing into a structure before transforming the structure into a game was a good move. It might have saved me some
+ time if I did it straight to `Game`. I want to say that I have a parser-to-structure module now, but even so, there are some
+ links between `TextToStructure` and `Game` that make it almost useless to any other project (without changing anything).
+
+Player-object interaction was easier than previous projects. I believe this is both because I am getting used to it by now
+ and because I spent a lot of time beforehand structuring everything. I also like to think that structuring the project is
+ what I did right. There is a clear hierarchy and you can find what you are looking for fairly easy, without having to search
+ for a function in file contents or having to scavenge multiple different files before finding what you want. However, I
+ absolutely wasted a lot of time restructuring the project multiple times, mostly because I was running into dependency cycles.
+
+Overall, I believe the project was a success. I am proud of the end result. Though, please note my comments on monad transformers.
+
+### Assets & dependencies
+
+The following assets were used (and modified if specified):
+
+- Kyrise's Free 16x16 RPG Icon Pack[[1]](#1)
+
+- 2D Pixel Dungeon Asset Pack by Pixel_Poem[[2]](#2)
+
+ Every needed asset was taken and put into its own `.png`, instead of in the overview.
+
+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
+- [hspec-discover](https://hackage.haskell.org/package/hspec-discover) for allowing to split test files in multiple files
+- [parsec](https://hackage.haskell.org/package/parsec) for parsing configuration files
\pagebreak
-## Writing your own stages
+## References
-A stage description file, conventionally named `.txt` is a file with a JSON-like format. It is used to describe
- everything inside a single stage of your game, including anything related to the player, the levels your game contains
+[1] [Kyrise's Free 16x16 RPG Icon Pack](https://kyrise.itch.io/kyrises-free-16x16-rpg-icon-pack) © 2018
+ by [Kyrise](https://kyrise.itch.io/) is licensed under [CC BY 4.0](http://creativecommons.org/licenses/by/4.0/?ref=chooser-v1)
+
+[2] [2D Pixel Dungeon Asset Pack](https://pixel-poem.itch.io/dungeon-assetpuck) by [Pixel_Poem](https://pixel-poem.itch.io/)
+ is not licensed
+
+\pagebreak
+
+## Appendix A: Future development ideas
+
+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
+ 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.
+
+Changes in the backend:
+
+- [ ] **Make inventory a state** At the moment, there is a boolean for inventory rendering. This should be turned into a state,
+ so it makes more sense to call it from other places as well.
+- [ ] **Direction of entities** Change the rendering based on the direction of an entity.
+- [ ] **Inventory with more details** The inventory should show more details of items, e.g. name, value, remaining use
+ times and description.
+
+\pagebreak
+
+## Appendix B: Writing your own worlds
+
+A world description file, conventionally named `.txt` is a file with a JSON-like format. It is used to describe
+ everything inside a single world of your game, including anything related to the player, the levels your game contains
and what happens in that level. It is essentially the raw representation of the initial state of a single game.
-> Note: At the moment, every game has a single stage description file. Chaining several files together is not possible yet.
-
-A stage description file consists of several elements.
+A world description file consists of several elements.
| Element | Short description |
| --------------- | --------------------------------------------------------------------------------------------------------- |
@@ -159,7 +281,7 @@ levels: [
```
-This stage description file consists of a single `Block`. A stage description file always does. This top level `Block`
+This world description file consists of a single `Block`. A world description file always does. This top level `Block`
contains two `Value`s `player` and `levels`, not separated by commas.
`player` describes a `Block` that represents the player of the game. Its `Entry`s are `hp` (a traditional value) and
@@ -252,79 +374,4 @@ If we look at the example, all the objects are
length = 1
Condition ('inventoryContains(key)')
Entry = empty ConditionList + Action ('leave()')
-```
-
-\pagebreak
-
-## Development notes
-
-### Engine architecture
-
-TODO
-
-`RPGEngine` is the main module. It contains the `playRPGEngine` function which bootstraps the whole game. It is also
- the game loop. From here, `RPGEngine` talks to its submodules.
-
-These submodules are `Config`, `Data`, `Input`, `Parse` & `Render`. They are all responsible for their own part, either
- containing the program configuration, data containers, everything needed to handle input, everything needed to parse a
- source file & everything needed to render the game. However, each of these submodules has their own submodules to
- divide the work. They are conveniently named after the state of the game that they work with, e.g. the main menu has a
- module & when the game is playing is a different module. A special one is `Core`, which is kind of like a library for
- every piece. It contains functions that are regularly used by the other modules.
-
-#### Monads/Monad stack
-
-TODO
-
-### Tests
-
-TODO
-
-### Assets & dependencies
-
-The following assets were used (and modified if specified):
-
-- Kyrise's Free 16x16 RPG Icon Pack[[1]](#1)
-
-- 2D Pixel Dungeon Asset Pack by Pixel_Poem[[2]](#2)
-
- Every needed asset was taken and put into its own `.png`, instead of in the overview.
-
-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
-- [hspec-discover](https://hackage.haskell.org/package/hspec-discover) for allowing to split test files in multiple files
-- [parsec](https://hackage.haskell.org/package/parsec) for parsing configuration files
-
-### Future development ideas
-
-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
- 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.
-
-\pagebreak
-
-## Conclusion
-
-Parsing was way harder than I initially expected. About half of my time on this project was spent writing the parser.
-
-TODO
-
-\pagebreak
-
-## References
-
-[1] [Kyrise's Free 16x16 RPG Icon Pack](https://kyrise.itch.io/kyrises-free-16x16-rpg-icon-pack) © 2018
- by [Kyrise](https://kyrise.itch.io/) is licensed under [CC BY 4.0](http://creativecommons.org/licenses/by/4.0/?ref=chooser-v1)
-
-[2] [2D Pixel Dungeon Asset Pack](https://pixel-poem.itch.io/dungeon-assetpuck) by [Pixel_Poem](https://pixel-poem.itch.io/)
- is not licensed
\ No newline at end of file
+```
\ No newline at end of file
diff --git a/assets/environment/void.png b/assets/environment/void.png
index be70f44..9eb2e3f 100644
Binary files a/assets/environment/void.png and b/assets/environment/void.png differ
diff --git a/extra/walkthrough/5-1-01.png b/extra/walkthrough/5-1-01.png
new file mode 100644
index 0000000..2b40621
Binary files /dev/null and b/extra/walkthrough/5-1-01.png differ
diff --git a/extra/walkthrough/5-2-01.png b/extra/walkthrough/5-2-01.png
new file mode 100644
index 0000000..2356231
Binary files /dev/null and b/extra/walkthrough/5-2-01.png differ
diff --git a/extra/walkthrough/5-2-02.png b/extra/walkthrough/5-2-02.png
new file mode 100644
index 0000000..001f1f2
Binary files /dev/null and b/extra/walkthrough/5-2-02.png differ
diff --git a/extra/walkthrough/5-2-03.png b/extra/walkthrough/5-2-03.png
new file mode 100644
index 0000000..63bbcf2
Binary files /dev/null and b/extra/walkthrough/5-2-03.png differ
diff --git a/extra/walkthrough/5-2-04.png b/extra/walkthrough/5-2-04.png
new file mode 100644
index 0000000..10dc2fc
Binary files /dev/null and b/extra/walkthrough/5-2-04.png differ
diff --git a/extra/walkthrough/5-2-05.png b/extra/walkthrough/5-2-05.png
new file mode 100644
index 0000000..15be37f
Binary files /dev/null and b/extra/walkthrough/5-2-05.png differ
diff --git a/extra/walkthrough/5-2-06.png b/extra/walkthrough/5-2-06.png
new file mode 100644
index 0000000..b1cfadc
Binary files /dev/null and b/extra/walkthrough/5-2-06.png differ
diff --git a/extra/walkthrough/5-3-01.png b/extra/walkthrough/5-3-01.png
new file mode 100644
index 0000000..5b44ec9
Binary files /dev/null and b/extra/walkthrough/5-3-01.png differ
diff --git a/extra/walkthrough/5-3-02.png b/extra/walkthrough/5-3-02.png
new file mode 100644
index 0000000..e590fd1
Binary files /dev/null and b/extra/walkthrough/5-3-02.png differ
diff --git a/extra/walkthrough/5-3-03.png b/extra/walkthrough/5-3-03.png
new file mode 100644
index 0000000..c84d804
Binary files /dev/null and b/extra/walkthrough/5-3-03.png differ
diff --git a/extra/walkthrough/5-3-04.png b/extra/walkthrough/5-3-04.png
new file mode 100644
index 0000000..9562083
Binary files /dev/null and b/extra/walkthrough/5-3-04.png differ
diff --git a/extra/walkthrough/5-3-05.png b/extra/walkthrough/5-3-05.png
new file mode 100644
index 0000000..d53340b
Binary files /dev/null and b/extra/walkthrough/5-3-05.png differ
diff --git a/extra/walkthrough/5-3-06.png b/extra/walkthrough/5-3-06.png
new file mode 100644
index 0000000..3c9e12f
Binary files /dev/null and b/extra/walkthrough/5-3-06.png differ
diff --git a/extra/walkthrough/5-3-07.png b/extra/walkthrough/5-3-07.png
new file mode 100644
index 0000000..ab6235c
Binary files /dev/null and b/extra/walkthrough/5-3-07.png differ
diff --git a/extra/walkthrough/5-3-08.png b/extra/walkthrough/5-3-08.png
new file mode 100644
index 0000000..fd22e1a
Binary files /dev/null and b/extra/walkthrough/5-3-08.png differ
diff --git a/extra/walkthrough/5-3-09.png b/extra/walkthrough/5-3-09.png
new file mode 100644
index 0000000..4e4f729
Binary files /dev/null and b/extra/walkthrough/5-3-09.png differ
diff --git a/extra/walkthrough/selection.png b/extra/walkthrough/selection.png
new file mode 100644
index 0000000..5c4e9cc
Binary files /dev/null and b/extra/walkthrough/selection.png differ
diff --git a/extra/walkthrough/you-win.png b/extra/walkthrough/you-win.png
new file mode 100644
index 0000000..eb4cc85
Binary files /dev/null and b/extra/walkthrough/you-win.png differ
diff --git a/levels/level5.txt b/levels/level5.txt
new file mode 100644
index 0000000..0263dac
--- /dev/null
+++ b/levels/level5.txt
@@ -0,0 +1,144 @@
+player: {
+ hp: 100,
+ inventory: [
+ {
+ id: "dagger",
+ x: 0,
+ y: 0,
+ name: "Swiss army knife",
+ description: "Your trustworthy army knife will never let you down",
+ useTimes: infinite,
+ value: 5,
+ actions: {}
+ },
+ {
+ id: "potion",
+ x: 0,
+ y: 0,
+ name: "Small healing potion",
+ description: "Will recover you from small injuries",
+ useTimes: 5,
+ value: 5,
+ actions: {}
+ }
+ ]
+}
+
+levels: [
+ {
+ layout: {
+ | * * * * * * *
+ | * s . . . e *
+ | * * * * * * *
+ },
+ items: [],
+ entities: []
+ },
+ {
+ layout: {
+ | x x * * * x x x x
+ | x x * . * x x x x
+ | * * * . * * * * *
+ | * s . . . . . e *
+ | * * * * * * * * *
+ },
+ items: [
+ {
+ id: "key",
+ x: 3,
+ y: 3,
+ name: "Secret key",
+ description: "What if this key opens a secret door?",
+ useTimes: 1,
+ value: 0,
+ actions: {
+ [not(inventoryFull())] retrieveItem(key),
+ [] leave()
+ }
+ }
+ ],
+ entities: [
+ {
+ id: "door",
+ x: 4,
+ y: 1,
+ name: "Secret door",
+ description: "This door can only be opened with a secret key",
+ direction: left,
+ actions: {
+ [inventoryContains(key)] useItem(key),
+ [] leave()
+ }
+ }
+ ]
+ },
+ {
+ layout: {
+ | * * * * * * * * * * *
+ | * . . . . . . . . . *
+ | * * * * * * * * * . *
+ | * e . . . . . . . s *
+ | * * * * * * * * * . *
+ | x x x x x x x x * . *
+ | * * * * * * * * * . *
+ | * . . . . . . . . . *
+ | * * * * * * * * * * *
+ },
+ items: [
+ {
+ id: "key",
+ x: 1,
+ y: 1,
+ name: "Key to sturdy door",
+ description: "You have proven worthy",
+ useTimes: 1,
+ value: 0,
+ actions: {
+ [not(inventoryFull())] retrieveItem(key),
+ [] leave()
+ }
+ },
+ {
+ id: "sword",
+ x: 1,
+ y: 7,
+ name: "Mighty sword",
+ description: "Slayer of evil",
+ useTimes: 3,
+ value: 100,
+ actions: {
+ [not(inventoryFull())] retrieveItem(sword),
+ [] leave()
+ }
+ }
+ ],
+ entities: [
+ {
+ id: "door",
+ x: 8,
+ y: 5,
+ name: "Sturdy door",
+ description: "I wonder what's behind it?",
+ direction: right,
+ actions: {
+ [inventoryContains(key)] useItem(key),
+ [] leave()
+ }
+ },
+ {
+ id: "devil",
+ x: 6,
+ y: 1,
+ name: "Evil powers",
+ description: "Certainly from hell",
+ hp: 55,
+ value: 10,
+ actions: {
+ [inventoryContains(dagger)] decreaseHp(devil, dagger),
+ [inventoryContains(sword)] decreaseHp(devil, sword),
+ [] leave()
+ }
+ }
+ ]
+ }
+]
\ No newline at end of file
diff --git a/lib/RPGEngine/Input/ActionSelection.hs b/lib/RPGEngine/Input/ActionSelection.hs
index d0ed414..321a796 100644
--- a/lib/RPGEngine/Input/ActionSelection.hs
+++ b/lib/RPGEngine/Input/ActionSelection.hs
@@ -16,6 +16,7 @@ import Data.Foldable (find)
handleInputActionSelection :: InputHandler Game
handleInputActionSelection = composeInputHandlers [
handleKey (SpecialKey KeySpace) Down selectAction,
+ handleKey (SpecialKey KeyEnter) Down selectAction,
handleKey (SpecialKey KeyUp) Down $ moveSelector North,
handleKey (SpecialKey KeyDown) Down $ moveSelector South
diff --git a/lib/RPGEngine/Input/Playing.hs b/lib/RPGEngine/Input/Playing.hs
index 8025611..6f1c9fa 100644
--- a/lib/RPGEngine/Input/Playing.hs
+++ b/lib/RPGEngine/Input/Playing.hs
@@ -36,6 +36,7 @@ handleInputPlaying = composeInputHandlers [
-- Interaction with entities and items
handleKey (SpecialKey KeySpace) Down checkForInteraction,
+ handleKey (SpecialKey KeyEnter) Down checkForInteraction,
handleKey (Char 'f') Down checkForInteraction,
handleKey (Char 'i') Down $ toggleInventoryShown True,
diff --git a/lib/RPGEngine/Render/Playing.hs b/lib/RPGEngine/Render/Playing.hs
index 98252c2..9a661bd 100644
--- a/lib/RPGEngine/Render/Playing.hs
+++ b/lib/RPGEngine/Render/Playing.hs
@@ -42,8 +42,7 @@ focusPlayer _ = id
renderLevel :: Renderer Level
renderLevel Level{ layout = l, items = i, entities = e } = level
where level = pictures [void, layout, items, entities]
- -- void = createVoid
- void = blank
+ void = createVoid
layout = renderLayout l
items = renderItems i
entities = renderEntities e
@@ -92,12 +91,12 @@ renderInventory :: Player -> Picture
renderInventory Player{ showInventory = False } = blank
renderInventory Player{ inventory = list } = pictures [overlay, title, items]
where title = translate 0 (offset (-1)) $ scale uizoom uizoom $ color white $ text "Inventory"
- items = pictures $ map move $ zip [0::Int ..] (map (getRender . itemId) list)
+ items = pictures $ zipWith (curry move) [0::Int ..] (map (getRender . itemId) list)
move (i, pic) = translate 0 (offset i) pic
offset i = negate (zoom * resolution * fromIntegral i)
withHealthBar :: HP -> Picture -> Picture
-withHealthBar (Nothing) renderedEntity = renderedEntity
+withHealthBar Nothing renderedEntity = renderedEntity
withHealthBar (Just hp) renderedEntity = pictures [renderedEntity, positionedBar]
where positionedBar = scale smaller smaller $ translate left up renderedBar
renderedBar = pictures [heart, counter]
diff --git a/verslag.pdf b/verslag.pdf
index d607e19..03999f6 100644
Binary files a/verslag.pdf and b/verslag.pdf differ