#9 Added level selection render

This commit is contained in:
Tibo De Peuter 2022-12-21 16:07:05 +01:00
parent 0786a41006
commit 2055ef234e
17 changed files with 401 additions and 34 deletions

View file

@ -64,7 +64,7 @@ TODO
- An example playthrough, with pictures and explanations
\pagebreak
<div style="page-break-after: always; visibility: hidden">\pagebreak</div>
## Writing your own stages
@ -250,10 +250,22 @@ If we look at the example, all the objects are
Entry = empty ConditionList + Action ('leave()')
```
\pagebreak
<div style="page-break-after: always; visibility: hidden">\pagebreak</div>
## Development notes
### Engine architecture
<mark>TODO</mark>
#### Monads/Monad stack
<mark>TODO</mark>
### Tests
<mark>TODO</mark>
### 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.
<div style="page-break-after: always; visibility: hidden">\pagebreak</div>
\pagebreak
## Conclusion
Parsing was way harder than I initially expected. About half of my time on this project was spent writing the parser.
<mark>TODO</mark>
<div style="page-break-after: always; visibility: hidden">\pagebreak</div>
## References

Binary file not shown.

After

Width:  |  Height:  |  Size: 494 B

View file

Before

Width:  |  Height:  |  Size: 870 B

After

Width:  |  Height:  |  Size: 870 B

Before After
Before After

View file

@ -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,

View file

@ -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()
}
}

134
levels/level4.txt Normal file
View file

@ -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()
}
}
]
}
]

View file

@ -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()
}
}
]
}
]

View file

@ -48,6 +48,7 @@ instance Living Player where
-- Current state of the game.
data State = Menu
| LvlSelect
| Playing
| Pause
| Win

View file

@ -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

View file

@ -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 }

View file

@ -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

View file

@ -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

View file

@ -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
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)

View file

@ -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)

View file

@ -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"
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)

View file

@ -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

Binary file not shown.