Merge pull request 'dev' (#25) from dev into master
Reviewed-on: https://git.depeuter.tk/tdpeuter/RPG-Engine/pulls/25
19
.vscode/tasks.json
vendored
|
@ -41,6 +41,25 @@
|
||||||
"kind": "build",
|
"kind": "build",
|
||||||
"isDefault": true
|
"isDefault": true
|
||||||
}
|
}
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"label": "Create verslag.pdf",
|
||||||
|
"type": "shell",
|
||||||
|
"command": "pandoc",
|
||||||
|
"args": [
|
||||||
|
"-s",
|
||||||
|
"-o", "verslag.pdf",
|
||||||
|
"-f", "markdown+smart+header_attributes+yaml_metadata_block+auto_identifiers",
|
||||||
|
"--pdf-engine", "lualatex",
|
||||||
|
"--template", "eisvogel",
|
||||||
|
"header.yaml",
|
||||||
|
"README.md"
|
||||||
|
],
|
||||||
|
"problemMatcher": [],
|
||||||
|
"group": {
|
||||||
|
"kind": "none",
|
||||||
|
"isDefault": false
|
||||||
|
}
|
||||||
}
|
}
|
||||||
],
|
],
|
||||||
"inputs": [
|
"inputs": [
|
||||||
|
|
329
README.md
|
@ -1,3 +1,330 @@
|
||||||
|
<!--
|
||||||
|
## Functional requirements
|
||||||
|
|
||||||
|
- [ ] Parsing of engine configuration file to game object
|
||||||
|
- [ ] Rendering of all game objects (Levels, objects, entities, ...)
|
||||||
|
- [ ] A start menu with the possibility of selecting a level
|
||||||
|
- [ ] An end screen that shows wether or not a player won
|
||||||
|
- [ ] Support for built-in engine functions
|
||||||
|
|
||||||
|
- [x] Player can move around in grid-world.
|
||||||
|
- [ ] Player can pick up objects.
|
||||||
|
- [ ] Player can use objects.
|
||||||
|
- [ ] Player can loose and gain health points.
|
||||||
|
- [ ] Player can interact with other entities (fight enemies, open doors, ...).
|
||||||
|
- [ ] Player can go to the next level.
|
||||||
|
|
||||||
|
## Not-functional requirements
|
||||||
|
|
||||||
|
- [x] Use Parsing.
|
||||||
|
- [ ] Use at least one (1) monad transformer.
|
||||||
|
- [ ] Write good and plenty of documentation.:w
|
||||||
|
|
||||||
|
- [x] Write tests (for example, using HSpec).
|
||||||
|
|
||||||
|
---
|
||||||
|
|
||||||
|
Nuttige links:
|
||||||
|
|
||||||
|
- https://jakewheat.github.io/intro_to_parsing/
|
||||||
|
|
||||||
|
```
|
||||||
|
Jarne — Today at 22:44
|
||||||
|
Da kan hoor en had da eerst, me gloss eeft geen goede text dus...
|
||||||
|
ListDirectory, en er was ook een fuctie takeBaseName
|
||||||
|
```
|
||||||
|
|
||||||
|
---
|
||||||
|
|
||||||
|
<div style="page-break-after: always;"></div>
|
||||||
|
-->
|
||||||
|
|
||||||
# RPG-Engine
|
# RPG-Engine
|
||||||
|
|
||||||
Schrijf een game-engine voor een rollenspel
|
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.
|
||||||
|
|
||||||
|
## Playing the game
|
||||||
|
|
||||||
|
These are the keybinds *in* the game. All other keybinds in the menus 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` | |
|
||||||
|
|
||||||
|
### Example playthrough
|
||||||
|
|
||||||
|
TODO
|
||||||
|
|
||||||
|
- An example playthrough, with pictures and explanations
|
||||||
|
|
||||||
|
<div style="page-break-after: always; visibility: hidden">\pagebreak</div>
|
||||||
|
|
||||||
|
## Writing your own stages
|
||||||
|
|
||||||
|
A stage description file, conventionally named `<stage_name>.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
|
||||||
|
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.
|
||||||
|
|
||||||
|
| Element | Short description |
|
||||||
|
| --------------- | --------------------------------------------------------------------------------------------------------- |
|
||||||
|
| `Block` | optionally surrounded by `{ ... }`, consists of several `Entry`'s, optionally separated by commas `,` |
|
||||||
|
| `Entry` | is a `Key` - `Value` pair, optionally separated by a colon `:` |
|
||||||
|
| `Key` | is a unique, predefined `String` describing `Value` |
|
||||||
|
| `Value` | is either a `Block` or a `BlockList` or a traditional value, such as `String` or `Int` |
|
||||||
|
| `BlockList` | is a number of `Block`'s, surrounded by `[ ... ]`, separated by commas, can be empty |
|
||||||
|
|
||||||
|
<details>
|
||||||
|
<summary>We'll look at the following example to explain these concepts.</summary>
|
||||||
|
|
||||||
|
```javascript
|
||||||
|
player: {
|
||||||
|
hp: 50,
|
||||||
|
inventory: [
|
||||||
|
{
|
||||||
|
id: "dagger",
|
||||||
|
x: 0,
|
||||||
|
y: 0,
|
||||||
|
name: "Dagger",
|
||||||
|
description: "Basic dagger you found somewhere",
|
||||||
|
useTimes: infinite,
|
||||||
|
value: 10,
|
||||||
|
|
||||||
|
actions: {}
|
||||||
|
}
|
||||||
|
]
|
||||||
|
}
|
||||||
|
|
||||||
|
levels: [
|
||||||
|
{
|
||||||
|
layout: {
|
||||||
|
| * * * * * *
|
||||||
|
| * s . . e *
|
||||||
|
| * * * * * *
|
||||||
|
},
|
||||||
|
items: [],
|
||||||
|
entities: []
|
||||||
|
},
|
||||||
|
{
|
||||||
|
layout: {
|
||||||
|
| * * * * * * * *
|
||||||
|
| * s . . . . e *
|
||||||
|
| * * * * * * * *
|
||||||
|
},
|
||||||
|
items: [
|
||||||
|
{
|
||||||
|
id: "key",
|
||||||
|
x: 3,
|
||||||
|
y: 1,
|
||||||
|
name: "Door key",
|
||||||
|
description: "Unlocks 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 secret door can only be opened with a key",
|
||||||
|
direction: left,
|
||||||
|
actions: {
|
||||||
|
[inventoryContains(key)] useItem(key),
|
||||||
|
[] leave()
|
||||||
|
}
|
||||||
|
}
|
||||||
|
]
|
||||||
|
}
|
||||||
|
]
|
||||||
|
```
|
||||||
|
</details>
|
||||||
|
|
||||||
|
This stage description file consists of a single `Block`. A stage 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
|
||||||
|
`inventory` (a `BlockList` of several other `Block`s). They are both separated by commas this time. It is possible for
|
||||||
|
the inventory to be an empty list `[]`.
|
||||||
|
|
||||||
|
`levels` is a `BlockList` that contains all the information to construct your game.
|
||||||
|
|
||||||
|
### `layout` syntax
|
||||||
|
|
||||||
|
If `Key` has the value `layout`, `Value` is none of the types discussed so far. Instead `Layout` is specifically made
|
||||||
|
to describe the layout of a level. This object is surrounded by `{ ... }` and consists of multiple lines, starting with
|
||||||
|
a vertical line `|` and several characters of the following:
|
||||||
|
|
||||||
|
- `x` is an empty tile a.k.a. void.
|
||||||
|
- `.` is a tile walkable by the player.
|
||||||
|
- `*` is a tile not walkable by the player.
|
||||||
|
- `s` is the starting position of the player.
|
||||||
|
- `e` is the exit.
|
||||||
|
|
||||||
|
All characters are interspersed with spaces.
|
||||||
|
|
||||||
|
### `actions` syntax
|
||||||
|
|
||||||
|
If `Key` has the value `actions`, the following changes are important for its `Value`, which in this case is a `Block`
|
||||||
|
with zero or more `Entry`s like so:
|
||||||
|
|
||||||
|
- `Key` has type `ConditionList`.
|
||||||
|
|
||||||
|
A `ConditionList` consists of several `Condition`s, surrounded by `[ ... ]`, separated by commas. A `ConditionList`
|
||||||
|
can be empty. If so, the conditional is always fulfilled.
|
||||||
|
|
||||||
|
A `Condition` is one of the following:
|
||||||
|
|
||||||
|
- `inventoryFull()`: the players inventory is full.
|
||||||
|
- `inventoryContains(objectId)`: the players inventory contains an object with id `objectId`.
|
||||||
|
- `not(condition)`: logical negation of `condition`.
|
||||||
|
|
||||||
|
- `Value` is an `Action`.
|
||||||
|
|
||||||
|
An `Action` is one of the following:
|
||||||
|
|
||||||
|
- `leave()`
|
||||||
|
- `retrieveItem(objectId)`
|
||||||
|
- `useItem(objectId)`
|
||||||
|
- `decreaseHp(entityId, objectId)`
|
||||||
|
- `increasePlayerHp(objectId)`
|
||||||
|
|
||||||
|
### Back to the example
|
||||||
|
|
||||||
|
If we look at the example, all the objects are
|
||||||
|
|
||||||
|
```
|
||||||
|
>Block<
|
||||||
|
Entry = Key ('player') + >Block<
|
||||||
|
Entry = Key ('hp') + Value (50)
|
||||||
|
Entry = Key ('inventory') + >BlockList<
|
||||||
|
length = 1
|
||||||
|
Block
|
||||||
|
Entry = Key ('id') + Value ("dagger")
|
||||||
|
... <several traditional entries like this>
|
||||||
|
Entry = Key ('actions') + empty Block
|
||||||
|
Entry = Key ('levels') + >BlockList<
|
||||||
|
length = 2
|
||||||
|
>Block<
|
||||||
|
Entry = Key ('layout') + Layout
|
||||||
|
<multiple lines that describe the layout>
|
||||||
|
Entry = Key ('items') + empty BlockList
|
||||||
|
Entry = Key ('entities') + empty BlockList
|
||||||
|
>Block<
|
||||||
|
Entry = Key ('layout') + Layout
|
||||||
|
<multiple lines that describe the layout>
|
||||||
|
Entry = Key ('items') + >BlockList<
|
||||||
|
length = 1
|
||||||
|
>Block<
|
||||||
|
Entry = Key ('id') + Value ("key")
|
||||||
|
... <several traditional entries like this>
|
||||||
|
Entry = Key ('actions') + >Block<
|
||||||
|
Entry = >ConditionList< + Action ('retrieveItem(key)')
|
||||||
|
length = 1
|
||||||
|
Condition ('not(inventoryFull())'))
|
||||||
|
Entry = empty ConditionList + Action ('leave()')
|
||||||
|
Entry = Key ('entities') + >BlockList<
|
||||||
|
length = 1
|
||||||
|
>Block<
|
||||||
|
Entry = Key ('id') + Value ("door")
|
||||||
|
... <several traditional entries like this>
|
||||||
|
Entry = Key ('actions') + >Block<
|
||||||
|
Entry = >ConditionList< + Action ('useItem(key)')
|
||||||
|
length = 1
|
||||||
|
Condition ('inventoryContains(key)')
|
||||||
|
Entry = empty ConditionList + Action ('leave()')
|
||||||
|
```
|
||||||
|
|
||||||
|
<div style="page-break-after: always; visibility: hidden">\pagebreak</div>
|
||||||
|
|
||||||
|
## Development notes
|
||||||
|
|
||||||
|
### Engine architecture
|
||||||
|
|
||||||
|
<mark>TODO</mark>
|
||||||
|
|
||||||
|
`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
|
||||||
|
|
||||||
|
<mark>TODO</mark>
|
||||||
|
|
||||||
|
### Tests
|
||||||
|
|
||||||
|
<mark>TODO</mark>
|
||||||
|
|
||||||
|
### Assets & dependencies
|
||||||
|
|
||||||
|
The following assets were used (and modified if specified):
|
||||||
|
|
||||||
|
- Kyrise's Free 16x16 RPG Icon Pack<sup>[[1]](#1)</sup>
|
||||||
|
|
||||||
|
- 2D Pixel Dungeon Asset Pack by Pixel_Poem<sup>[[2]](#2)</sup>
|
||||||
|
|
||||||
|
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.
|
||||||
|
|
||||||
|
<div style="page-break-after: always; visibility: hidden">\pagebreak</div>
|
||||||
|
|
||||||
|
## 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
|
||||||
|
|
||||||
|
<a id="1">[1]</a> [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)
|
||||||
|
|
||||||
|
<a id="2">[2]</a> [2D Pixel Dungeon Asset Pack](https://pixel-poem.itch.io/dungeon-assetpuck) by [Pixel_Poem](https://pixel-poem.itch.io/)
|
||||||
|
is not licensed
|
BIN
assets/entities/devil.png
Normal file
After Width: | Height: | Size: 1.5 KiB |
BIN
assets/entities/door.png
Normal file
After Width: | Height: | Size: 1.5 KiB |
BIN
assets/entities/player.png
Normal file
After Width: | Height: | Size: 1.5 KiB |
BIN
assets/environment/entrance.png
Normal file
After Width: | Height: | Size: 1.5 KiB |
BIN
assets/environment/exit.png
Normal file
After Width: | Height: | Size: 1.5 KiB |
BIN
assets/environment/overlay.png
Normal file
After Width: | Height: | Size: 494 B |
BIN
assets/environment/tile.png
Normal file
After Width: | Height: | Size: 1.5 KiB |
BIN
assets/environment/void.png
Normal file
After Width: | Height: | Size: 1.5 KiB |
BIN
assets/environment/wall.png
Normal file
After Width: | Height: | Size: 1.5 KiB |
BIN
assets/gui/health.png
Normal file
After Width: | Height: | Size: 237 B |
BIN
assets/gui/main.png
Normal file
After Width: | Height: | Size: 5.3 KiB |
BIN
assets/items/dagger.png
Normal file
After Width: | Height: | Size: 734 B |
BIN
assets/items/key.png
Normal file
After Width: | Height: | Size: 1.5 KiB |
BIN
assets/items/potion.png
Normal file
After Width: | Height: | Size: 1.5 KiB |
BIN
assets/items/sword.png
Normal file
After Width: | Height: | Size: 247 B |
BIN
assets/unknown.png
Normal file
After Width: | Height: | Size: 870 B |
15
header.yaml
Normal file
|
@ -0,0 +1,15 @@
|
||||||
|
---
|
||||||
|
title: "RPG Engine"
|
||||||
|
author: "Tibo De Peuter"
|
||||||
|
date: "23 december 2022"
|
||||||
|
subtitle: "Write a game engine for an RPG game"
|
||||||
|
# geometry: "left=2.54cm,right=2.54cm,top=1.91cm,bottom=1.91cm"
|
||||||
|
geometry: "left=2.54cm,right=2.54cm,top=2.54cm,bottom=2.54cm"
|
||||||
|
titlepage: true
|
||||||
|
titlepage-rule-height: 4
|
||||||
|
toc: true
|
||||||
|
listings-disable-line-numbers: true
|
||||||
|
listings-no-page-break: false
|
||||||
|
subparagraph: true
|
||||||
|
lang: en-GB
|
||||||
|
---
|
|
@ -15,4 +15,4 @@ levels: [
|
||||||
|
|
||||||
entities: []
|
entities: []
|
||||||
}
|
}
|
||||||
]
|
]
|
|
@ -48,4 +48,4 @@ levels: [
|
||||||
}
|
}
|
||||||
]
|
]
|
||||||
}
|
}
|
||||||
]
|
]
|
|
@ -29,8 +29,8 @@ levels: [
|
||||||
items: [
|
items: [
|
||||||
{
|
{
|
||||||
id: "sword",
|
id: "sword",
|
||||||
x: 2,
|
x: 3,
|
||||||
y: 3,
|
y: 4,
|
||||||
name: "Zwaard",
|
name: "Zwaard",
|
||||||
description: "Meer schade tegen monsters",
|
description: "Meer schade tegen monsters",
|
||||||
useTimes: infinite,
|
useTimes: infinite,
|
||||||
|
@ -43,8 +43,8 @@ levels: [
|
||||||
},
|
},
|
||||||
{
|
{
|
||||||
id: "potion",
|
id: "potion",
|
||||||
x: 3,
|
x: 4,
|
||||||
y: 1,
|
y: 2,
|
||||||
name: "Levensbrouwsel",
|
name: "Levensbrouwsel",
|
||||||
description: "Geeft een aantal levenspunten terug",
|
description: "Geeft een aantal levenspunten terug",
|
||||||
useTimes: 1,
|
useTimes: 1,
|
||||||
|
@ -60,8 +60,8 @@ levels: [
|
||||||
entities: [
|
entities: [
|
||||||
{
|
{
|
||||||
id: "devil",
|
id: "devil",
|
||||||
x: 4,
|
x: 5,
|
||||||
y: 3,
|
y: 4,
|
||||||
name: "Duivel",
|
name: "Duivel",
|
||||||
description: "Een monster uit de hel",
|
description: "Een monster uit de hel",
|
||||||
hp: 50,
|
hp: 50,
|
||||||
|
@ -69,11 +69,11 @@ levels: [
|
||||||
|
|
||||||
actions: {
|
actions: {
|
||||||
[inventoryContains(potion)] increasePlayerHp(potion),
|
[inventoryContains(potion)] increasePlayerHp(potion),
|
||||||
[inventoryContains(sword)] decreaseHp(m1, sword),
|
[inventoryContains(sword)] decreaseHp(devil, sword),
|
||||||
[] decreaseHp(m1, dagger),
|
[] decreaseHp(devil, dagger),
|
||||||
[] leave()
|
[] leave()
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
]
|
]
|
||||||
}
|
}
|
||||||
]
|
]
|
134
levels/level4.txt
Normal 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: 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: 3,
|
||||||
|
y: 4,
|
||||||
|
name: "Zwaard",
|
||||||
|
description: "Meer schade tegen monsters",
|
||||||
|
useTimes: infinite,
|
||||||
|
value: 25,
|
||||||
|
|
||||||
|
actions: {
|
||||||
|
[not(inventoryFull())] retrieveItem(sword),
|
||||||
|
[] leave()
|
||||||
|
}
|
||||||
|
},
|
||||||
|
{
|
||||||
|
id: "potion",
|
||||||
|
x: 4,
|
||||||
|
y: 2,
|
||||||
|
name: "Levensbrouwsel",
|
||||||
|
description: "Geeft een aantal levenspunten terug",
|
||||||
|
useTimes: 1,
|
||||||
|
value: 50,
|
||||||
|
|
||||||
|
actions: {
|
||||||
|
[not(inventoryFull())] retrieveItem(potion),
|
||||||
|
[] leave()
|
||||||
|
}
|
||||||
|
}
|
||||||
|
],
|
||||||
|
|
||||||
|
entities: [
|
||||||
|
{
|
||||||
|
id: "devil",
|
||||||
|
x: 5,
|
||||||
|
y: 4,
|
||||||
|
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()
|
||||||
|
}
|
||||||
|
}
|
||||||
|
]
|
||||||
|
}
|
||||||
|
]
|
134
levels/level_more_levels.txt
Normal 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: 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()
|
||||||
|
}
|
||||||
|
}
|
||||||
|
]
|
||||||
|
}
|
||||||
|
]
|
102
lib/RPGEngine.hs
|
@ -5,33 +5,89 @@ module RPGEngine
|
||||||
( playRPGEngine
|
( playRPGEngine
|
||||||
) where
|
) where
|
||||||
|
|
||||||
import Game
|
import RPGEngine.Config ( bgColor, winDimensions, winOffsets )
|
||||||
import Render
|
import RPGEngine.Render ( initWindow, render )
|
||||||
import Input
|
import RPGEngine.Input ( handleAllInput )
|
||||||
|
import RPGEngine.Input.Playing ( checkPlaying, spawnPlayer )
|
||||||
|
import RPGEngine.Data (Game (..), State (..), Layout, Level (..), Physical (..), Entity(..), Direction(..), Player(..))
|
||||||
|
import RPGEngine.Data.Default (defaultLevel, defaultPlayer)
|
||||||
|
|
||||||
import Graphics.Gloss (
|
import Graphics.Gloss ( play )
|
||||||
Color(..)
|
|
||||||
, white
|
|
||||||
, play
|
|
||||||
)
|
|
||||||
|
|
||||||
----------------------------- Constants ------------------------------
|
|
||||||
|
|
||||||
-- Dimensions for main window
|
|
||||||
winDimensions :: (Int, Int)
|
|
||||||
winDimensions = (1280, 720)
|
|
||||||
|
|
||||||
-- Offsets for main window
|
|
||||||
winOffsets :: (Int, Int)
|
|
||||||
winOffsets = (0, 0)
|
|
||||||
|
|
||||||
----------------------------------------------------------------------
|
----------------------------------------------------------------------
|
||||||
|
|
||||||
-- This is the gameloop.
|
-- This is the game loop.
|
||||||
-- It can receive input and update itself. It is rendered by a renderer.
|
-- It can receive input and update itself. It is rendered by a renderer.
|
||||||
playRPGEngine :: String -> Int -> IO()
|
playRPGEngine :: String -> Int -> IO()
|
||||||
playRPGEngine title fps = do
|
playRPGEngine title fps = do
|
||||||
play window bgColor fps initGame render handleInputs step
|
play window bgColor fps initGame render handleAllInput step
|
||||||
where window = initWindow title winDimensions winOffsets
|
where window = initWindow title winDimensions winOffsets
|
||||||
step _ g = g -- TODO Do something with step?
|
step _ = checkPlaying -- TODO Do something with step? Check health etc.
|
||||||
handleInputs = handleAllInput
|
|
||||||
|
-- TODO revert this
|
||||||
|
-- Initialize the game
|
||||||
|
initGame :: Game
|
||||||
|
initGame = Game { state = Menu }
|
||||||
|
-- initGame = Game{ state = initState }
|
||||||
|
-- where initState = Playing{
|
||||||
|
-- levels = [defaultLevel, otherLevel],
|
||||||
|
-- count = 0,
|
||||||
|
-- level = defaultLevel,
|
||||||
|
-- player = spawnPlayer defaultLevel defaultPlayer,
|
||||||
|
-- restart = initState
|
||||||
|
-- }
|
||||||
|
|
||||||
|
-- TODO remove this
|
||||||
|
otherLayout :: Layout
|
||||||
|
otherLayout = [
|
||||||
|
[Blocked, Blocked, Blocked],
|
||||||
|
[Blocked, Entrance, Blocked],
|
||||||
|
[Blocked, Walkable, Blocked],
|
||||||
|
[Blocked, Walkable, Blocked],
|
||||||
|
[Blocked, Walkable, Blocked],
|
||||||
|
[Blocked, Exit, Blocked],
|
||||||
|
[Blocked, Blocked, Blocked]
|
||||||
|
]
|
||||||
|
|
||||||
|
-- TODO remove this
|
||||||
|
otherLevel :: Level
|
||||||
|
otherLevel = Level {
|
||||||
|
layout = otherLayout,
|
||||||
|
index = [
|
||||||
|
(0, 0, Blocked),
|
||||||
|
(1, 0, Blocked),
|
||||||
|
(2, 0, Blocked),
|
||||||
|
(0, 1, Blocked),
|
||||||
|
(1, 1, Entrance),
|
||||||
|
(2, 1, Blocked),
|
||||||
|
(0, 2, Blocked),
|
||||||
|
(1, 2, Walkable),
|
||||||
|
(2, 2, Blocked),
|
||||||
|
(0, 3, Blocked),
|
||||||
|
(1, 3, Walkable),
|
||||||
|
(2, 3, Blocked),
|
||||||
|
(0, 4, Blocked),
|
||||||
|
(1, 4, Walkable),
|
||||||
|
(2, 4, Blocked),
|
||||||
|
(0, 5, Blocked),
|
||||||
|
(1, 5, Exit),
|
||||||
|
(2, 5, Blocked),
|
||||||
|
(0, 6, Blocked),
|
||||||
|
(1, 6, Blocked),
|
||||||
|
(2, 6, Blocked)
|
||||||
|
],
|
||||||
|
items = [],
|
||||||
|
entities = [
|
||||||
|
Entity{
|
||||||
|
entityId = "door",
|
||||||
|
entityX = 1,
|
||||||
|
entityY = 3,
|
||||||
|
entityName = "Epic door",
|
||||||
|
entityDescription = "epic description",
|
||||||
|
entityActions = [],
|
||||||
|
entityValue = Nothing,
|
||||||
|
entityHp = Nothing,
|
||||||
|
direction = North
|
||||||
|
}
|
||||||
|
]
|
||||||
|
}
|
54
lib/RPGEngine/Config.hs
Normal file
|
@ -0,0 +1,54 @@
|
||||||
|
-- This module should ultimately be replaced by a config file parser
|
||||||
|
module RPGEngine.Config
|
||||||
|
-- All entries are exported
|
||||||
|
where
|
||||||
|
|
||||||
|
import Graphics.Gloss
|
||||||
|
|
||||||
|
----------------------- Window configuration -------------------------
|
||||||
|
|
||||||
|
-- Dimensions for main window
|
||||||
|
winDimensions :: (Int, Int)
|
||||||
|
winDimensions = (1280, 720)
|
||||||
|
|
||||||
|
-- Offsets for main window
|
||||||
|
winOffsets :: (Int, Int)
|
||||||
|
winOffsets = (0, 0)
|
||||||
|
|
||||||
|
-- Game background color
|
||||||
|
bgColor :: Color
|
||||||
|
bgColor = makeColor (37 / 256) (19 / 256) (26 / 256) 1
|
||||||
|
|
||||||
|
-- Text color
|
||||||
|
textColor :: Color
|
||||||
|
textColor = white
|
||||||
|
|
||||||
|
-- Color of selection
|
||||||
|
selectionColor :: Color
|
||||||
|
selectionColor = red
|
||||||
|
|
||||||
|
-- Default scale
|
||||||
|
zoom :: Float
|
||||||
|
zoom = 5
|
||||||
|
|
||||||
|
-- UI scale, number between 0 (small) and 1 (big)
|
||||||
|
uizoom :: Float
|
||||||
|
uizoom = 0.5
|
||||||
|
|
||||||
|
-- Resolution of the texture
|
||||||
|
resolution :: Float
|
||||||
|
resolution = 16
|
||||||
|
|
||||||
|
-- Location of the assets folder containing all images
|
||||||
|
assetsFolder :: FilePath
|
||||||
|
assetsFolder = "assets/"
|
||||||
|
|
||||||
|
-- Location of the level folder containing all levels
|
||||||
|
levelFolder :: FilePath
|
||||||
|
levelFolder = "levels/"
|
||||||
|
|
||||||
|
------------------------- Game configuration -------------------------
|
||||||
|
|
||||||
|
-- How many items can a player keep in their inventory?
|
||||||
|
inventorySize :: Int
|
||||||
|
inventorySize = 5
|
134
lib/RPGEngine/Data.hs
Normal file
|
@ -0,0 +1,134 @@
|
||||||
|
-- Contains all the data containers of the game.
|
||||||
|
-- Submodules contain accessors for these data containers.
|
||||||
|
module RPGEngine.Data
|
||||||
|
-- All data types are exported
|
||||||
|
where
|
||||||
|
|
||||||
|
import RPGEngine.Input.Core
|
||||||
|
import RPGEngine.Render.Core ( Renderer )
|
||||||
|
|
||||||
|
-------------------------------- Game --------------------------------
|
||||||
|
|
||||||
|
-- A game is the base data container.
|
||||||
|
data Game = Game {
|
||||||
|
state :: State
|
||||||
|
} deriving (Eq, Show)
|
||||||
|
|
||||||
|
------------------------------- State --------------------------------
|
||||||
|
|
||||||
|
-- Main menu
|
||||||
|
data State = Menu
|
||||||
|
-- Select the level you want to play
|
||||||
|
| LevelSelection { levelList :: [FilePath],
|
||||||
|
selector :: ListSelector }
|
||||||
|
-- Playing a level
|
||||||
|
| Playing { levels :: [Level],
|
||||||
|
count :: Int,
|
||||||
|
level :: Level,
|
||||||
|
player :: Player,
|
||||||
|
restart :: State }
|
||||||
|
-- Selecting an action
|
||||||
|
| ActionSelection { actionList :: [Action],
|
||||||
|
selector :: ListSelector,
|
||||||
|
-- The player of this state will be used to interact
|
||||||
|
continue :: State }
|
||||||
|
-- Paused while playing a level
|
||||||
|
| Paused { continue :: State }
|
||||||
|
-- Won a level
|
||||||
|
| Win
|
||||||
|
-- Lost a level
|
||||||
|
| Lose { restart :: State }
|
||||||
|
| Error Message
|
||||||
|
deriving (Eq, Show)
|
||||||
|
|
||||||
|
type Message = String
|
||||||
|
|
||||||
|
------------------------------- Level --------------------------------
|
||||||
|
|
||||||
|
data Level = Level {
|
||||||
|
layout :: Layout,
|
||||||
|
-- All Physical pieces but with their coordinates
|
||||||
|
index :: [(X, Y, Physical)],
|
||||||
|
items :: [Item],
|
||||||
|
entities :: [Entity]
|
||||||
|
} deriving (Eq, Show)
|
||||||
|
|
||||||
|
type X = Int
|
||||||
|
type Y = Int
|
||||||
|
|
||||||
|
type Layout = [Strip]
|
||||||
|
type Strip = [Physical]
|
||||||
|
|
||||||
|
-- A Physical part of the world. A single tile of the world. A block
|
||||||
|
-- with stuff on it.
|
||||||
|
data Physical = Void
|
||||||
|
| Walkable
|
||||||
|
| Blocked
|
||||||
|
| Entrance
|
||||||
|
| Exit
|
||||||
|
deriving (Eq, Show)
|
||||||
|
|
||||||
|
-------------------------------- Item --------------------------------
|
||||||
|
|
||||||
|
data Item = Item {
|
||||||
|
itemId :: ItemId,
|
||||||
|
itemX :: X,
|
||||||
|
itemY :: Y,
|
||||||
|
itemName :: String,
|
||||||
|
itemDescription :: String,
|
||||||
|
itemActions :: [([Condition], Action)],
|
||||||
|
itemValue :: Maybe Int,
|
||||||
|
useTimes :: Maybe Int
|
||||||
|
} deriving (Eq, Show)
|
||||||
|
|
||||||
|
type ItemId = String
|
||||||
|
|
||||||
|
------------------------------- Entity -------------------------------
|
||||||
|
|
||||||
|
data Entity = Entity {
|
||||||
|
entityId :: EntityId,
|
||||||
|
entityX :: X,
|
||||||
|
entityY :: Y,
|
||||||
|
entityName :: String,
|
||||||
|
entityDescription :: String,
|
||||||
|
entityActions :: [([Condition], Action)],
|
||||||
|
entityValue :: Maybe Int,
|
||||||
|
entityHp :: HP,
|
||||||
|
direction :: Direction
|
||||||
|
} deriving (Eq, Show)
|
||||||
|
|
||||||
|
type EntityId = String
|
||||||
|
type HP = Maybe Int
|
||||||
|
|
||||||
|
data Direction = North
|
||||||
|
| East
|
||||||
|
| South
|
||||||
|
| West
|
||||||
|
| Stay -- No direction
|
||||||
|
deriving (Eq, Show)
|
||||||
|
|
||||||
|
data Player = Player {
|
||||||
|
playerHp :: HP,
|
||||||
|
inventory :: [Item],
|
||||||
|
position :: (X, Y),
|
||||||
|
showHp :: Bool,
|
||||||
|
showInventory :: Bool
|
||||||
|
} deriving (Eq, Show)
|
||||||
|
|
||||||
|
------------------------------ Condition -----------------------------
|
||||||
|
|
||||||
|
data Condition = InventoryFull
|
||||||
|
| InventoryContains ItemId
|
||||||
|
| Not Condition
|
||||||
|
| AlwaysFalse
|
||||||
|
deriving (Eq, Show)
|
||||||
|
|
||||||
|
------------------------------- Action -------------------------------
|
||||||
|
|
||||||
|
data Action = Leave
|
||||||
|
| RetrieveItem ItemId
|
||||||
|
| UseItem ItemId
|
||||||
|
| DecreaseHp EntityId ItemId
|
||||||
|
| IncreasePlayerHp ItemId
|
||||||
|
| DoNothing
|
||||||
|
deriving (Eq, Show)
|
97
lib/RPGEngine/Data/Default.hs
Normal file
|
@ -0,0 +1,97 @@
|
||||||
|
module RPGEngine.Data.Default
|
||||||
|
-- Everything is exported
|
||||||
|
where
|
||||||
|
import RPGEngine.Data (Entity (..), Game (..), Item (..), Layout, Player (..), Level (..), State (..), Physical (..), Direction (..))
|
||||||
|
import RPGEngine.Input.Core (ListSelector(..))
|
||||||
|
|
||||||
|
------------------------------ Defaults ------------------------------
|
||||||
|
|
||||||
|
defaultEntity :: Entity
|
||||||
|
defaultEntity = Entity {
|
||||||
|
entityId = "",
|
||||||
|
entityX = 0,
|
||||||
|
entityY = 0,
|
||||||
|
entityName = "Default",
|
||||||
|
entityDescription = "",
|
||||||
|
entityActions = [],
|
||||||
|
entityValue = Prelude.Nothing,
|
||||||
|
entityHp = Prelude.Nothing,
|
||||||
|
direction = Stay
|
||||||
|
}
|
||||||
|
|
||||||
|
defaultItem :: Item
|
||||||
|
defaultItem = Item {
|
||||||
|
itemId = "",
|
||||||
|
itemX = 0,
|
||||||
|
itemY = 0,
|
||||||
|
itemName = "Default",
|
||||||
|
itemDescription = "",
|
||||||
|
itemActions = [],
|
||||||
|
itemValue = Prelude.Nothing,
|
||||||
|
useTimes = Prelude.Nothing
|
||||||
|
}
|
||||||
|
|
||||||
|
defaultLayout :: Layout
|
||||||
|
defaultLayout = [
|
||||||
|
[Blocked, Blocked, Blocked, Blocked, Blocked],
|
||||||
|
[Blocked, Entrance, Walkable, Exit, Blocked],
|
||||||
|
[Blocked, Blocked, Blocked, Blocked, Blocked]
|
||||||
|
]
|
||||||
|
|
||||||
|
defaultLevel :: Level
|
||||||
|
defaultLevel = Level {
|
||||||
|
layout = defaultLayout,
|
||||||
|
index = [
|
||||||
|
(0, 0, Blocked),
|
||||||
|
(0, 1, Blocked),
|
||||||
|
(0, 2, Blocked),
|
||||||
|
(1, 0, Blocked),
|
||||||
|
(1, 1, Entrance),
|
||||||
|
(1, 2, Blocked),
|
||||||
|
(2, 0, Blocked),
|
||||||
|
(2, 1, Walkable),
|
||||||
|
(2, 2, Blocked),
|
||||||
|
(3, 0, Blocked),
|
||||||
|
(3, 1, Exit),
|
||||||
|
(3, 2, Blocked),
|
||||||
|
(4, 0, Blocked),
|
||||||
|
(4, 1, Blocked),
|
||||||
|
(4, 2, Blocked)
|
||||||
|
],
|
||||||
|
items = [],
|
||||||
|
entities = []
|
||||||
|
}
|
||||||
|
|
||||||
|
defaultPlayer :: Player
|
||||||
|
defaultPlayer = Player {
|
||||||
|
-- playerHp = Prelude.Nothing, -- Compares to infinity
|
||||||
|
playerHp = Just 50,
|
||||||
|
inventory = [ Item{
|
||||||
|
itemId = "key",
|
||||||
|
itemX = 0,
|
||||||
|
itemY = 0,
|
||||||
|
itemName = "Epic key",
|
||||||
|
itemDescription = "MyKey",
|
||||||
|
itemActions = [],
|
||||||
|
itemValue = Nothing,
|
||||||
|
useTimes = Nothing
|
||||||
|
}, Item{
|
||||||
|
itemId = "dagger",
|
||||||
|
itemX = 0,
|
||||||
|
itemY = 0,
|
||||||
|
itemName = "My dagger",
|
||||||
|
itemDescription = "dagger",
|
||||||
|
itemActions = [],
|
||||||
|
itemValue = Nothing,
|
||||||
|
useTimes = Nothing
|
||||||
|
}],
|
||||||
|
position = (0, 0),
|
||||||
|
showInventory = False,
|
||||||
|
showHp = True
|
||||||
|
}
|
||||||
|
|
||||||
|
defaultSelector :: ListSelector
|
||||||
|
defaultSelector = ListSelector {
|
||||||
|
selection = 0,
|
||||||
|
selected = False
|
||||||
|
}
|
37
lib/RPGEngine/Data/Game.hs
Normal file
|
@ -0,0 +1,37 @@
|
||||||
|
module RPGEngine.Data.Game
|
||||||
|
( isLegalMove
|
||||||
|
, isPlayerAtExit
|
||||||
|
, isPlayerDead
|
||||||
|
) where
|
||||||
|
|
||||||
|
import RPGEngine.Data
|
||||||
|
( Player(..),
|
||||||
|
Direction,
|
||||||
|
Physical(Exit, Walkable, Entrance),
|
||||||
|
State(..),
|
||||||
|
Game(..) )
|
||||||
|
import RPGEngine.Data.Level (findAt, directionOffsets)
|
||||||
|
|
||||||
|
------------------------------ Exported ------------------------------
|
||||||
|
|
||||||
|
-- Check if a move is legal by checking what is located at the new position.
|
||||||
|
isLegalMove :: Direction -> Game -> Bool
|
||||||
|
isLegalMove dir g@Game{ state = Playing{ level = lvl, player = p@Player{ position = (x, y) }}} = legality
|
||||||
|
where legality = physical `elem` [Walkable, Entrance, Exit]
|
||||||
|
physical = findAt newPos lvl
|
||||||
|
newPos = (x + xD, y + yD)
|
||||||
|
(xD, yD) = directionOffsets dir
|
||||||
|
isLegalMove _ _ = False
|
||||||
|
|
||||||
|
-- Check if a player is standing on an exit
|
||||||
|
isPlayerAtExit :: Game -> Bool
|
||||||
|
isPlayerAtExit g@Game{ state = Playing{ player = player, level = level }} = atExit
|
||||||
|
where playerPos = position player
|
||||||
|
atPos = findAt playerPos level
|
||||||
|
atExit = atPos == Exit
|
||||||
|
isPlayerAtExit _ = False
|
||||||
|
|
||||||
|
-- Check if the players health is <= 0, which means the player is dead.
|
||||||
|
isPlayerDead :: Game -> Bool
|
||||||
|
isPlayerDead g@Game{ state = Playing{ player = Player{ playerHp = (Just hp)}}} = hp <= 0
|
||||||
|
isPlayerDead _ = False
|
100
lib/RPGEngine/Data/Level.hs
Normal file
|
@ -0,0 +1,100 @@
|
||||||
|
module RPGEngine.Data.Level
|
||||||
|
-- Everything is exported
|
||||||
|
where
|
||||||
|
|
||||||
|
import GHC.IO (unsafePerformIO)
|
||||||
|
import System.Directory (getDirectoryContents)
|
||||||
|
import RPGEngine.Input.Core (ListSelector(..))
|
||||||
|
import RPGEngine.Data (Action(..), Level (..), Physical (..), Direction (..), Entity (..), Game (..), Item (..), Player (..), State (..), X, Y, Layout, Condition (InventoryFull, InventoryContains, Not, AlwaysFalse), ItemId)
|
||||||
|
import RPGEngine.Config (levelFolder, inventorySize)
|
||||||
|
import Data.Foldable (find)
|
||||||
|
|
||||||
|
------------------------------ Exported ------------------------------
|
||||||
|
|
||||||
|
-- Find first position of a Physical
|
||||||
|
-- Graceful exit by giving Nothing if there is nothing found.
|
||||||
|
findFirstOf :: Level -> Physical -> Maybe (X, Y)
|
||||||
|
findFirstOf l@Level{ index = index } physical = try
|
||||||
|
where matches = filter (\(x, y, v) -> v == physical) index
|
||||||
|
try | not (null matches) = Just $ (\(x, y, _) -> (x, y)) $ head matches
|
||||||
|
| otherwise = Nothing
|
||||||
|
|
||||||
|
-- What is located at a given position in the level?
|
||||||
|
findAt :: (X, Y) -> Level -> Physical
|
||||||
|
findAt pos lvl@Level{ index = index } = try
|
||||||
|
where matches = map (\(_, _, v) -> v) $ filter (\(x, y, v) -> (x, y) == pos) index
|
||||||
|
try | not (null matches) = head matches
|
||||||
|
| otherwise = Void
|
||||||
|
|
||||||
|
hasAt :: (X, Y) -> Level -> Maybe (Either Item Entity)
|
||||||
|
hasAt pos level = match firstItem firstEntity
|
||||||
|
where match :: Maybe Item -> Maybe Entity -> Maybe (Either Item Entity)
|
||||||
|
match (Just a) _ = Just $ Left a
|
||||||
|
match _ (Just a) = Just $ Right a
|
||||||
|
match _ _ = Nothing
|
||||||
|
firstEntity = find ((== pos) . getECoord) $ entities level
|
||||||
|
getECoord e = (entityX e, entityY e)
|
||||||
|
firstItem = find ((== pos) . getICoord) $ items level
|
||||||
|
getICoord i = (itemX i, itemY i)
|
||||||
|
|
||||||
|
getWithId :: String -> Level -> Maybe (Either Item Entity)
|
||||||
|
getWithId id level = match firstItem firstEntity
|
||||||
|
where match :: Maybe Item -> Maybe Entity -> Maybe (Either Item Entity)
|
||||||
|
match (Just a) _ = Just $ Left a
|
||||||
|
match _ (Just a) = Just $ Right a
|
||||||
|
match _ _ = Nothing
|
||||||
|
firstEntity = find ((== id) . entityId) $ entities level
|
||||||
|
firstItem = find ((== id) . itemId) $ items level
|
||||||
|
|
||||||
|
directionOffsets :: Direction -> (X, Y)
|
||||||
|
directionOffsets North = ( 0, 1)
|
||||||
|
directionOffsets East = ( 1, 0)
|
||||||
|
directionOffsets South = ( 0, -1)
|
||||||
|
directionOffsets West = (-1, 0)
|
||||||
|
directionOffsets Stay = ( 0, 0)
|
||||||
|
|
||||||
|
getLevelList :: [FilePath]
|
||||||
|
getLevelList = drop 2 $ unsafePerformIO $ getDirectoryContents levelFolder
|
||||||
|
|
||||||
|
-- Get the actions of either an entity or an item
|
||||||
|
getActions :: Either Item Entity -> [([Condition], Action)]
|
||||||
|
getActions (Left item) = itemActions item
|
||||||
|
getActions (Right entity) = entityActions entity
|
||||||
|
|
||||||
|
getActionText :: Action -> String
|
||||||
|
getActionText Leave = "Leave"
|
||||||
|
getActionText (RetrieveItem _) = "Pick up"
|
||||||
|
getActionText (UseItem _) = "Use item"
|
||||||
|
getActionText (IncreasePlayerHp _) = "Take a healing potion"
|
||||||
|
getActionText (DecreaseHp _ used) = "Attack using " ++ used
|
||||||
|
getActionText _ = "ERROR"
|
||||||
|
|
||||||
|
-- Filter based on the conditions, keep only the actions of which the
|
||||||
|
-- conditions are met.
|
||||||
|
-- Should receive a Playing state
|
||||||
|
filterActions :: State -> [([Condition], Action)] -> [Action]
|
||||||
|
filterActions _ [] = []
|
||||||
|
filterActions s (entry:others) = met entry $ filterActions s others
|
||||||
|
where met (conditions, action) l | all (meetsCondition s) conditions = action:l
|
||||||
|
| otherwise = l
|
||||||
|
|
||||||
|
-- Check if a condition is met or not.
|
||||||
|
meetsCondition :: State -> Condition -> Bool
|
||||||
|
meetsCondition s InventoryFull = isInventoryFull $ player s
|
||||||
|
meetsCondition s (InventoryContains id) = inventoryContains id $ player s
|
||||||
|
meetsCondition s (Not condition) = not $ meetsCondition s condition
|
||||||
|
meetsCondition _ AlwaysFalse = False
|
||||||
|
|
||||||
|
-- Check if the inventory of the player is full.
|
||||||
|
isInventoryFull :: Player -> Bool
|
||||||
|
isInventoryFull p = inventorySize <= length (inventory p)
|
||||||
|
|
||||||
|
-- Check if the inventory of the player contains an item.
|
||||||
|
inventoryContains :: ItemId -> Player -> Bool
|
||||||
|
inventoryContains id p = any ((== id) . itemId) $ inventory p
|
||||||
|
|
||||||
|
-- Retrieve an item from inventory
|
||||||
|
itemFromInventory :: ItemId -> [Item] -> (Maybe Item, [Item])
|
||||||
|
itemFromInventory iid list = (match, filteredList)
|
||||||
|
where match = find ((== iid) . itemId) list
|
||||||
|
filteredList = filter ((/= iid) . itemId) list
|
29
lib/RPGEngine/Input.hs
Normal file
|
@ -0,0 +1,29 @@
|
||||||
|
-- Implementations for each state can be found in their respective
|
||||||
|
-- submodules.
|
||||||
|
module RPGEngine.Input
|
||||||
|
( handleAllInput
|
||||||
|
) where
|
||||||
|
|
||||||
|
import RPGEngine.Input.Core ( InputHandler, composeInputHandlers, handleAnyKey )
|
||||||
|
|
||||||
|
import RPGEngine.Data ( Game(..), State(..) )
|
||||||
|
import RPGEngine.Input.Menu ( handleInputMenu )
|
||||||
|
import RPGEngine.Input.LevelSelection (handleInputLevelSelection)
|
||||||
|
import RPGEngine.Input.Playing ( handleInputPlaying )
|
||||||
|
import RPGEngine.Input.Paused ( handleInputPaused )
|
||||||
|
import RPGEngine.Input.Win ( handleInputWin )
|
||||||
|
import RPGEngine.Input.Lose ( handleInputLose )
|
||||||
|
import RPGEngine.Input.ActionSelection (handleInputActionSelection)
|
||||||
|
|
||||||
|
------------------------------ Exported ------------------------------
|
||||||
|
|
||||||
|
-- Handle all input of all states of the game.
|
||||||
|
handleAllInput :: InputHandler Game
|
||||||
|
handleAllInput ev g@Game{ state = Menu } = handleInputMenu ev g
|
||||||
|
handleAllInput ev g@Game{ state = LevelSelection{} } = handleInputLevelSelection ev g
|
||||||
|
handleAllInput ev g@Game{ state = Playing{} } = handleInputPlaying ev g
|
||||||
|
handleAllInput ev g@Game{ state = Paused{} } = handleInputPaused ev g
|
||||||
|
handleAllInput ev g@Game{ state = Win } = handleInputWin ev g
|
||||||
|
handleAllInput ev g@Game{ state = Lose{} } = handleInputLose ev g
|
||||||
|
handleAllInput ev g@Game{ state = ActionSelection{}} = handleInputActionSelection ev g
|
||||||
|
handleAllInput ev g@Game{ state = Error _ } = handleAnyKey (\game -> game{ state = Menu}) ev g
|
141
lib/RPGEngine/Input/ActionSelection.hs
Normal file
|
@ -0,0 +1,141 @@
|
||||||
|
module RPGEngine.Input.ActionSelection
|
||||||
|
( handleInputActionSelection
|
||||||
|
) where
|
||||||
|
|
||||||
|
import RPGEngine.Input.Core (InputHandler, handleKey, composeInputHandlers, ListSelector (selection))
|
||||||
|
|
||||||
|
import RPGEngine.Data (Game (..), State (..), Direction (..), Action (..), ItemId, EntityId, Level (..), Player (inventory, playerHp, Player), Item (..), HP, Entity (..))
|
||||||
|
import Graphics.Gloss.Interface.IO.Game (Key(SpecialKey), SpecialKey (KeyUp, KeyDown))
|
||||||
|
import Graphics.Gloss.Interface.IO.Interact
|
||||||
|
( SpecialKey(..), KeyState(..) )
|
||||||
|
import RPGEngine.Data.Level (getWithId, itemFromInventory)
|
||||||
|
import Data.Foldable (find)
|
||||||
|
|
||||||
|
------------------------------ Exported ------------------------------
|
||||||
|
|
||||||
|
handleInputActionSelection :: InputHandler Game
|
||||||
|
handleInputActionSelection = composeInputHandlers [
|
||||||
|
handleKey (SpecialKey KeySpace) Down selectAction,
|
||||||
|
|
||||||
|
handleKey (SpecialKey KeyUp) Down $ moveSelector North,
|
||||||
|
handleKey (SpecialKey KeyDown) Down $ moveSelector South
|
||||||
|
]
|
||||||
|
|
||||||
|
----------------------------------------------------------------------
|
||||||
|
|
||||||
|
selectAction :: Game -> Game
|
||||||
|
selectAction game@Game{ state = ActionSelection list selector continue } = newGame
|
||||||
|
where newGame = game{ state = execute selectedAction continue }
|
||||||
|
selectedAction = list !! index
|
||||||
|
index = selection selector
|
||||||
|
selectAction g = g
|
||||||
|
|
||||||
|
-- TODO Lift this code from LevelSelection
|
||||||
|
-- Move the selector either up or down
|
||||||
|
moveSelector :: Direction -> Game -> Game
|
||||||
|
moveSelector dir game@Game{ state = state@(ActionSelection list selector _) } = newGame
|
||||||
|
where newGame = game{ state = newState }
|
||||||
|
newState = state{ selector = newSelector }
|
||||||
|
newSelector | constraint = selector{ selection = newSelection }
|
||||||
|
| otherwise = selector
|
||||||
|
constraint = 0 <= newSelection && newSelection < length list
|
||||||
|
newSelection = selection selector + diff
|
||||||
|
diff | dir == North = -1
|
||||||
|
| dir == South = 1
|
||||||
|
| otherwise = 0
|
||||||
|
moveSelector _ g = g{ state = Error "Something went wrong while moving the selector up or down"}
|
||||||
|
|
||||||
|
------------------------------ Actions -------------------------------
|
||||||
|
|
||||||
|
execute :: Action -> State -> State
|
||||||
|
execute (RetrieveItem id ) s = pickUpItem id s
|
||||||
|
execute (UseItem id ) s = useItem id s
|
||||||
|
execute (DecreaseHp eid iid) s = decreaseHp eid iid s
|
||||||
|
execute (IncreasePlayerHp iid) s = healedPlayer
|
||||||
|
where healedPlayer = s{ player = increasePlayerHp iid (player s)}
|
||||||
|
execute _ s = s
|
||||||
|
|
||||||
|
-- Pick up the item with itemId and put it in the players inventory
|
||||||
|
-- Should receive a Playing state
|
||||||
|
pickUpItem :: ItemId -> State -> State
|
||||||
|
pickUpItem id s@Playing{ level = level, player = player } = newState
|
||||||
|
where (Just (Left pickedUpItem)) = getWithId id level
|
||||||
|
newState = s{ level = newLevel, player = newPlayer }
|
||||||
|
newLevel = level{ items = filteredItems }
|
||||||
|
filteredItems = filter (/= pickedUpItem) $ items level
|
||||||
|
newPlayer = player{ inventory = newInventory }
|
||||||
|
newInventory = pickedUpItem:inventory player
|
||||||
|
pickUpItem _ _ = Error "Something went wrong while picking up an item"
|
||||||
|
|
||||||
|
-- Use an item
|
||||||
|
-- Should receive a Playing state
|
||||||
|
useItem :: ItemId -> State -> State
|
||||||
|
useItem iid s@Playing{ level = level, player = player} = newState
|
||||||
|
where newState = s{ level = newLevel, player = newPlayer }
|
||||||
|
-- Remove item from inventory if necessary
|
||||||
|
(Just usingItem) = find ((== iid) . itemId) $ inventory player
|
||||||
|
usedItem = decreaseDurability usingItem
|
||||||
|
newInventory = filter (/= usingItem) $ inventory player
|
||||||
|
newPlayer = player{ inventory = putItemBack usedItem newInventory }
|
||||||
|
putItemBack Nothing inv = inv
|
||||||
|
putItemBack (Just item) inv = item:inv
|
||||||
|
-- Remove entity if necessary
|
||||||
|
allEntities = entities level
|
||||||
|
entitiesWithUseItem = filter (any ((== UseItem iid) . snd) . entityActions) allEntities
|
||||||
|
attackedEntity = head entitiesWithUseItem
|
||||||
|
newLevel = level{ entities = filter (/= attackedEntity) $ entities level}
|
||||||
|
useItem _ _ = Error "Something went wrong while using an item"
|
||||||
|
|
||||||
|
-- Attack an entity using an item
|
||||||
|
-- Should receive a Playing state
|
||||||
|
decreaseHp :: EntityId -> ItemId -> State -> State
|
||||||
|
decreaseHp eid iid s@Playing{ level = level, player = player } = newState
|
||||||
|
where newState = s{ level = newLevel, player = newPlayer }
|
||||||
|
-- Change player
|
||||||
|
(Just usingItem) = find ((== iid) . itemId) $ inventory player
|
||||||
|
usedItem = decreaseDurability usingItem
|
||||||
|
newInventory = filter (/= usingItem) $ inventory player
|
||||||
|
newPlayer = player{ inventory = putItemBack usedItem newInventory, playerHp = newHp }
|
||||||
|
putItemBack Nothing inv = inv
|
||||||
|
putItemBack (Just item) inv = item:inv
|
||||||
|
newHp = changeHealth (playerHp player) damageGetAmount -- Damage dealt by entity
|
||||||
|
damageDealAmount = itemValue usingItem
|
||||||
|
-- Change entity
|
||||||
|
(Just (Right attackedEntity)) = getWithId eid level
|
||||||
|
newLevel = level{ entities = putEntityBack dealtWithEntity newEntities }
|
||||||
|
newEntities = filter ((/= eid) . entityId) $ entities level
|
||||||
|
dealtWithEntity = decreaseHealth attackedEntity damageDealAmount
|
||||||
|
putEntityBack Nothing list = list
|
||||||
|
putEntityBack (Just ent) list = ent:list
|
||||||
|
damageGetAmount = inverse (entityValue attackedEntity)
|
||||||
|
inverse (Just val) = Just (-val)
|
||||||
|
inverse Nothing = Nothing
|
||||||
|
decreaseHp _ _ _ = Error "something went wrong while attacking"
|
||||||
|
|
||||||
|
-- Heal a bit
|
||||||
|
-- Should receive a Player
|
||||||
|
increasePlayerHp :: ItemId -> Player -> Player
|
||||||
|
increasePlayerHp id p@Player{ playerHp = hp, inventory = inventory} = newPlayer
|
||||||
|
where newPlayer = p{ playerHp = newHp, inventory = newInventory newItem }
|
||||||
|
(Just usedItem) = find ((== id) . itemId) inventory
|
||||||
|
newItem = decreaseDurability usedItem
|
||||||
|
newInventory (Just item) = item:filteredInventory
|
||||||
|
newInventory _ = filteredInventory
|
||||||
|
filteredInventory =filter (/= usedItem) inventory
|
||||||
|
newHp = changeHealth hp (itemValue usedItem)
|
||||||
|
|
||||||
|
decreaseDurability :: Item -> Maybe Item
|
||||||
|
decreaseDurability item@Item{ useTimes = Nothing } = Just item -- Infinite uses, never breaks
|
||||||
|
decreaseDurability item@Item{ useTimes = Just val } | 0 < val - 1 = Just item{ useTimes = Just (val - 1) }
|
||||||
|
| otherwise = Nothing -- Broken
|
||||||
|
|
||||||
|
decreaseHealth :: Entity -> Maybe Int -> Maybe Entity
|
||||||
|
decreaseHealth entity@Entity{ entityHp = Nothing } _ = Just entity
|
||||||
|
decreaseHealth entity@Entity{ entityHp = Just val } (Just i) | 0 < val - i = Just entity{ entityHp = Just (val - i) }
|
||||||
|
| otherwise = Nothing
|
||||||
|
decreaseHealth entity _ = Just entity
|
||||||
|
|
||||||
|
-- Change given health by a given amount
|
||||||
|
changeHealth :: HP -> HP -> HP
|
||||||
|
changeHealth (Just health) (Just difference) = Just (health + difference)
|
||||||
|
changeHealth health _ = health
|
63
lib/RPGEngine/Input/Core.hs
Normal file
|
@ -0,0 +1,63 @@
|
||||||
|
module RPGEngine.Input.Core
|
||||||
|
( InputHandler
|
||||||
|
, ListSelector(..)
|
||||||
|
|
||||||
|
, composeInputHandlers
|
||||||
|
, handle
|
||||||
|
, handleKey
|
||||||
|
, handleAnyKey
|
||||||
|
|
||||||
|
, SpecialKey(..)
|
||||||
|
) where
|
||||||
|
|
||||||
|
import Graphics.Gloss.Interface.Pure.Game
|
||||||
|
( Event(EventKey), Key(..), KeyState(Down), SpecialKey )
|
||||||
|
|
||||||
|
----------------------------- Constants ------------------------------
|
||||||
|
|
||||||
|
type InputHandler a = Event -> (a -> a)
|
||||||
|
|
||||||
|
data ListSelector = ListSelector {
|
||||||
|
selection :: Int,
|
||||||
|
selected :: Bool
|
||||||
|
} deriving (Eq, Show)
|
||||||
|
|
||||||
|
------------------------------ Exported ------------------------------
|
||||||
|
|
||||||
|
-- Compose multiple InputHandlers into one InputHandler that handles
|
||||||
|
-- all of them.
|
||||||
|
composeInputHandlers :: [InputHandler a] -> InputHandler a
|
||||||
|
composeInputHandlers [] ev a = a
|
||||||
|
composeInputHandlers (ih:ihs) ev a = composeInputHandlers ihs ev (ih ev a)
|
||||||
|
|
||||||
|
-- Handle any event
|
||||||
|
handle :: Event -> (a -> a) -> InputHandler a
|
||||||
|
handle (EventKey key state _ _) = handleKey key state
|
||||||
|
-- handle (EventMotion _) = undefined -- TODO
|
||||||
|
-- handle (EventResize _) = undefined -- TODO
|
||||||
|
handle _ = const (const id)
|
||||||
|
|
||||||
|
-- Handle a event by pressing a key
|
||||||
|
handleKey :: Key -> KeyState -> (a -> a) -> InputHandler a
|
||||||
|
handleKey (SpecialKey sk) s = handleSpecialKey sk s
|
||||||
|
handleKey (Char c ) s = handleCharKey c s
|
||||||
|
handleKey (MouseButton _ ) _ = const (const id)
|
||||||
|
|
||||||
|
-- Handle any key, equivalent to "Press any key to start"
|
||||||
|
handleAnyKey :: (a -> a) -> InputHandler a
|
||||||
|
handleAnyKey f (EventKey _ Down _ _) = f
|
||||||
|
handleAnyKey _ _ = id
|
||||||
|
|
||||||
|
--------------------------- Help functions ---------------------------
|
||||||
|
|
||||||
|
handleCharKey :: Char -> KeyState -> (a -> a) -> InputHandler a
|
||||||
|
handleCharKey c1 s1 f (EventKey (Char c2) s2 _ _)
|
||||||
|
| c1 == c2 && s1 == s2 = f
|
||||||
|
| otherwise = id
|
||||||
|
handleCharKey _ _ _ _ = id
|
||||||
|
|
||||||
|
handleSpecialKey :: SpecialKey -> KeyState -> (a -> a) -> InputHandler a
|
||||||
|
handleSpecialKey sk1 s1 f (EventKey (SpecialKey sk2) s2 _ _)
|
||||||
|
| sk1 == sk2 && s1 == s2 = f
|
||||||
|
| otherwise = id
|
||||||
|
handleSpecialKey _ _ _ _ = id
|
45
lib/RPGEngine/Input/LevelSelection.hs
Normal file
|
@ -0,0 +1,45 @@
|
||||||
|
module RPGEngine.Input.LevelSelection
|
||||||
|
( handleInputLevelSelection
|
||||||
|
) where
|
||||||
|
|
||||||
|
import RPGEngine.Input.Core (InputHandler, composeInputHandlers, handleKey, ListSelector (..))
|
||||||
|
|
||||||
|
import RPGEngine.Data (Game (..), State (..), Direction (..))
|
||||||
|
import Graphics.Gloss.Interface.IO.Game (Key(..))
|
||||||
|
import Graphics.Gloss.Interface.IO.Interact (SpecialKey(..), KeyState(..))
|
||||||
|
import RPGEngine.Config (levelFolder)
|
||||||
|
import RPGEngine.Parse (parse)
|
||||||
|
|
||||||
|
------------------------------ Exported ------------------------------
|
||||||
|
|
||||||
|
handleInputLevelSelection :: InputHandler Game
|
||||||
|
handleInputLevelSelection = composeInputHandlers [
|
||||||
|
handleKey (SpecialKey KeySpace) Down selectLevel,
|
||||||
|
|
||||||
|
handleKey (SpecialKey KeyUp) Down $ moveSelector North,
|
||||||
|
handleKey (SpecialKey KeyDown) Down $ moveSelector South
|
||||||
|
]
|
||||||
|
|
||||||
|
----------------------------------------------------------------------
|
||||||
|
|
||||||
|
-- Select a level and load it in
|
||||||
|
selectLevel :: Game -> Game
|
||||||
|
selectLevel game@Game{ state = LevelSelection list selector } = newGame
|
||||||
|
where newGame = parse $ levelFolder ++ (list !! index)
|
||||||
|
index = selection selector
|
||||||
|
selectLevel g = g{ state = Error "Something went wrong while selecting a level"}
|
||||||
|
|
||||||
|
-- TODO Lift this code from ActionSelection
|
||||||
|
-- Move the selector either up or down
|
||||||
|
moveSelector :: Direction -> Game -> Game
|
||||||
|
moveSelector dir game@Game{ state = state@(LevelSelection list selector) } = newGame
|
||||||
|
where newGame = game{ state = newState }
|
||||||
|
newState = state{ selector = newSelector }
|
||||||
|
newSelector | constraint = selector{ selection = newSelection }
|
||||||
|
| otherwise = selector
|
||||||
|
constraint = 0 <= newSelection && newSelection < length list
|
||||||
|
newSelection = selection selector + diff
|
||||||
|
diff | dir == North = -1
|
||||||
|
| dir == South = 1
|
||||||
|
| otherwise = 0
|
||||||
|
moveSelector _ g = g{ state = Error "Something went wrong while moving the selector up or down"}
|
17
lib/RPGEngine/Input/Lose.hs
Normal file
|
@ -0,0 +1,17 @@
|
||||||
|
module RPGEngine.Input.Lose
|
||||||
|
( handleInputLose
|
||||||
|
) where
|
||||||
|
|
||||||
|
import RPGEngine.Input.Core (InputHandler, handleAnyKey)
|
||||||
|
|
||||||
|
import RPGEngine.Data (Game(..), State(..))
|
||||||
|
|
||||||
|
------------------------------ Exported ------------------------------
|
||||||
|
|
||||||
|
handleInputLose :: InputHandler Game
|
||||||
|
handleInputLose = handleAnyKey retry
|
||||||
|
|
||||||
|
----------------------------------------------------------------------
|
||||||
|
|
||||||
|
retry :: Game -> Game
|
||||||
|
retry g@Game{ state = Lose{ restart = restart }} = g{ state = restart }
|
23
lib/RPGEngine/Input/Menu.hs
Normal file
|
@ -0,0 +1,23 @@
|
||||||
|
module RPGEngine.Input.Menu
|
||||||
|
( handleInputMenu
|
||||||
|
) where
|
||||||
|
|
||||||
|
import RPGEngine.Input.Core (InputHandler, composeInputHandlers, handleAnyKey)
|
||||||
|
import RPGEngine.Data (Game (state), State (..))
|
||||||
|
import RPGEngine.Data.Default (defaultSelector)
|
||||||
|
import RPGEngine.Data.Level (getLevelList)
|
||||||
|
|
||||||
|
------------------------------ Exported ------------------------------
|
||||||
|
|
||||||
|
handleInputMenu :: InputHandler Game
|
||||||
|
handleInputMenu = composeInputHandlers [
|
||||||
|
handleAnyKey (\game -> game{ state = startLevelSelection })
|
||||||
|
]
|
||||||
|
|
||||||
|
----------------------------------------------------------------------
|
||||||
|
|
||||||
|
startLevelSelection :: State
|
||||||
|
startLevelSelection = LevelSelection {
|
||||||
|
levelList = getLevelList,
|
||||||
|
selector = defaultSelector
|
||||||
|
}
|
18
lib/RPGEngine/Input/Paused.hs
Normal file
|
@ -0,0 +1,18 @@
|
||||||
|
module RPGEngine.Input.Paused
|
||||||
|
( handleInputPaused
|
||||||
|
) where
|
||||||
|
|
||||||
|
import RPGEngine.Input.Core (InputHandler, handleAnyKey)
|
||||||
|
import RPGEngine.Data (Game (..), State (continue, Paused))
|
||||||
|
|
||||||
|
------------------------------ Exported ------------------------------
|
||||||
|
|
||||||
|
handleInputPaused :: InputHandler Game
|
||||||
|
handleInputPaused = handleAnyKey continueGame
|
||||||
|
|
||||||
|
----------------------------------------------------------------------
|
||||||
|
|
||||||
|
continueGame :: Game -> Game
|
||||||
|
continueGame g@Game{ state = Paused{ continue = state }} = newGame
|
||||||
|
where newGame = g{ state = state }
|
||||||
|
continueGame g = g
|
148
lib/RPGEngine/Input/Playing.hs
Normal file
|
@ -0,0 +1,148 @@
|
||||||
|
module RPGEngine.Input.Playing
|
||||||
|
( handleInputPlaying
|
||||||
|
, checkPlaying
|
||||||
|
, spawnPlayer
|
||||||
|
, putCoords
|
||||||
|
) where
|
||||||
|
|
||||||
|
import RPGEngine.Input.Core (InputHandler, handle, handleKey, composeInputHandlers, ListSelector (..))
|
||||||
|
|
||||||
|
import RPGEngine.Data (Game (..), Layout(..), Level(..), Physical(..), Player(..), State(..), X, Y, Direction (..), Entity (..), Item (..))
|
||||||
|
import RPGEngine.Data.Game (isLegalMove, isPlayerDead, isPlayerAtExit)
|
||||||
|
import RPGEngine.Data.Level (directionOffsets, findFirstOf, hasAt, filterActions, getActions)
|
||||||
|
|
||||||
|
import Data.Maybe (fromJust, isNothing)
|
||||||
|
import Graphics.Gloss.Interface.IO.Game (Key(..))
|
||||||
|
import Graphics.Gloss.Interface.IO.Interact (SpecialKey(..), KeyState(..), Event(..), KeyState(..))
|
||||||
|
import Prelude hiding (interact)
|
||||||
|
|
||||||
|
------------------------------ Exported ------------------------------
|
||||||
|
|
||||||
|
handleInputPlaying :: InputHandler Game
|
||||||
|
handleInputPlaying = composeInputHandlers [
|
||||||
|
-- Pause the game
|
||||||
|
handleKey (Char 'p') Down pauseGame,
|
||||||
|
|
||||||
|
-- Player movement
|
||||||
|
handleKey (SpecialKey KeyUp) Down $ movePlayer North,
|
||||||
|
handleKey (SpecialKey KeyRight) Down $ movePlayer East,
|
||||||
|
handleKey (SpecialKey KeyDown) Down $ movePlayer South,
|
||||||
|
handleKey (SpecialKey KeyLeft) Down $ movePlayer West,
|
||||||
|
|
||||||
|
handleKey (Char 'w') Down $ movePlayer North,
|
||||||
|
handleKey (Char 'd') Down $ movePlayer East,
|
||||||
|
handleKey (Char 's') Down $ movePlayer South,
|
||||||
|
handleKey (Char 'a') Down $ movePlayer West,
|
||||||
|
|
||||||
|
-- Interaction with entities and items
|
||||||
|
handleKey (SpecialKey KeySpace) Down checkForInteraction,
|
||||||
|
handleKey (Char 'f') Down checkForInteraction,
|
||||||
|
|
||||||
|
handleKey (Char 'i') Down $ toggleInventoryShown True,
|
||||||
|
handleKey (Char 'i') Up $ toggleInventoryShown False,
|
||||||
|
handleKey (SpecialKey KeyTab) Down $ toggleInventoryShown True,
|
||||||
|
handleKey (SpecialKey KeyTab) Up $ toggleInventoryShown False,
|
||||||
|
|
||||||
|
handleKey (Char 'r') Down restartGame
|
||||||
|
]
|
||||||
|
|
||||||
|
----------------------------------------------------------------------
|
||||||
|
|
||||||
|
-- Set the initial position of the player in a given level.
|
||||||
|
spawnPlayer :: Level -> Player -> Player
|
||||||
|
spawnPlayer l@Level{ layout = lay } p@Player{ position = prevPos } = p{ position = newPos }
|
||||||
|
where try = findFirstOf l Entrance
|
||||||
|
newPos | isNothing try = prevPos
|
||||||
|
| otherwise = fromJust try
|
||||||
|
|
||||||
|
checkPlaying :: Game -> Game
|
||||||
|
checkPlaying g@Game{ state = s@Playing{ restart = restart }} = newGame
|
||||||
|
where newGame | isPlayerDead g = loseGame
|
||||||
|
| isPlayerAtExit g = g{ state = goToNextLevel s }
|
||||||
|
| otherwise = g
|
||||||
|
loseGame = g{ state = Lose{ restart = restart }}
|
||||||
|
checkPlaying g = g
|
||||||
|
|
||||||
|
pauseGame :: Game -> Game
|
||||||
|
pauseGame g@Game{ state = playing@Playing{} } = pausedGame
|
||||||
|
where pausedGame = g{ state = Paused playing }
|
||||||
|
pauseGame g = g
|
||||||
|
|
||||||
|
restartGame :: Game -> Game
|
||||||
|
restartGame g@Game{ state = playing@Playing{ restart = restarted } } = g{ state = restarted }
|
||||||
|
restartGame g = g{ state = Error "something went wrong while restarting the level"}
|
||||||
|
|
||||||
|
-- Go to next level if there is a next level, otherwise, initialize win state.
|
||||||
|
goToNextLevel :: State -> State
|
||||||
|
goToNextLevel s@Playing{ levels = levels, level = current, count = count, player = player } = nextState
|
||||||
|
where nextState | (count + 1) < length levels = nextLevelState
|
||||||
|
| otherwise = Win
|
||||||
|
nextLevelState = s{ level = nextLevel, count = count + 1, player = movedPlayer, restart = nextLevelState }
|
||||||
|
nextLevel = levels !! (count + 1)
|
||||||
|
movedPlayer = spawnPlayer nextLevel player
|
||||||
|
goToNextLevel s = s
|
||||||
|
|
||||||
|
-- Move a player in a direction if possible.
|
||||||
|
movePlayer :: Direction -> Game -> Game
|
||||||
|
movePlayer dir g@Game{ state = s@Playing{ player = p@Player{ position = (x, y) }}} = tryForceInteraction newGame g
|
||||||
|
where newGame = g{ state = newState }
|
||||||
|
newState = s{ player = newPlayer }
|
||||||
|
newPlayer = p{ position = newCoord }
|
||||||
|
newCoord | isLegalMove dir g = (x + xD, y + yD)
|
||||||
|
| otherwise = (x, y)
|
||||||
|
(xD, yD) = directionOffsets dir
|
||||||
|
movePlayer _ g = g{ state = Error "something went wrong while moving the player" }
|
||||||
|
|
||||||
|
-- TODO Clean this function
|
||||||
|
-- Try to force an interaction. If there is an entity, you have to
|
||||||
|
-- interact with it. If it is an item, the user should trigger this
|
||||||
|
-- themselves. If forced, the player should not move to the new position.
|
||||||
|
tryForceInteraction :: Game -> Game -> Game
|
||||||
|
tryForceInteraction g@Game{ state = Playing { level = level, player = player }} fallBack@Game{ state = Playing{ player = firstPlayer }} = newGame triedInteraction
|
||||||
|
where newGame g@Game{ state = s@ActionSelection{ continue = c@Playing{ player = player}}} = g{ state = s{ continue = c{ player = playerWithRestorePos }}}
|
||||||
|
newGame g = g
|
||||||
|
playerWithRestorePos = (newPlayer triedInteraction){ position = position firstPlayer }
|
||||||
|
newPlayer Game{ state = ActionSelection{ continue = Playing{ player = player }}} = player
|
||||||
|
triedInteraction | hasEntity (hasAt pos level) = interact g
|
||||||
|
| otherwise = g
|
||||||
|
pos = position player
|
||||||
|
hasEntity (Just (Right entity)) = True
|
||||||
|
hasEntity _ = False
|
||||||
|
tryForceInteraction g _ = g{ state = Error "something went wrong while trying to force interaction"}
|
||||||
|
|
||||||
|
-- If there is an interaction at the current position, go to
|
||||||
|
-- actionSelection state. Otherwise just continue the game.
|
||||||
|
checkForInteraction :: Game -> Game
|
||||||
|
checkForInteraction g@Game{ state = Playing{ level = level, player = player }} = newGame
|
||||||
|
where newGame | canInteract = interact g
|
||||||
|
| otherwise = g
|
||||||
|
canInteract = not $ null $ hasAt pos level
|
||||||
|
pos = position player
|
||||||
|
checkForInteraction g = g{ state = Error "something went wrong while checking for entities to interact with" }
|
||||||
|
|
||||||
|
interact :: Game -> Game
|
||||||
|
interact g@Game{ state = s@Playing{ level = level, player = player } } = g{ state = newState }
|
||||||
|
where newState = ActionSelection actionList selector continue
|
||||||
|
actionList = filterActions s $ getActions $ fromJust $ hasAt pos level
|
||||||
|
selector = ListSelector 0 False
|
||||||
|
pos = position player
|
||||||
|
continue = s
|
||||||
|
interact g = g{ state = Error "something went wrong while interacting with object"}
|
||||||
|
|
||||||
|
toggleInventoryShown :: Bool -> Game -> Game
|
||||||
|
toggleInventoryShown shown g@Game{ state = s@Playing{ player = p }}= newGame
|
||||||
|
where newGame = g{ state = newState }
|
||||||
|
newState = s{ player = newPlayer }
|
||||||
|
newPlayer = p{ showInventory = shown }
|
||||||
|
toggleInventoryShown _ g = g{ state = Error "something went wrong while working inventory" }
|
||||||
|
|
||||||
|
-- Map all Physicals onto coordinates
|
||||||
|
putCoords :: Level -> [(X, Y, Physical)]
|
||||||
|
putCoords l@Level{ layout = lay } = concatMap (\(a, bs) -> map (\(b, c) -> (b, a, c)) bs) numberedList
|
||||||
|
where numberedStrips = reverse $ zip [0::Int .. ] $ reverse lay
|
||||||
|
numberedList = map (\(x, strip) -> (x, zip [0::Int ..] strip)) numberedStrips
|
||||||
|
|
||||||
|
-- putCoords l = concatMap numberColumns intermediate
|
||||||
|
-- where numberColumns (rowIndex, numberedRow) = map (\(colIndex, cell) -> (colIndex, rowIndex, cell)) numberedRow
|
||||||
|
-- intermediate = zip [0 .. ] numberedRows
|
||||||
|
-- numberedRows = zip [0::X .. ] $ layout l
|
16
lib/RPGEngine/Input/Win.hs
Normal file
|
@ -0,0 +1,16 @@
|
||||||
|
module RPGEngine.Input.Win
|
||||||
|
( handleInputWin
|
||||||
|
) where
|
||||||
|
|
||||||
|
import RPGEngine.Input.Core (InputHandler, handleAnyKey)
|
||||||
|
import RPGEngine.Data (Game (..), State (Menu))
|
||||||
|
|
||||||
|
------------------------------ Exported ------------------------------
|
||||||
|
|
||||||
|
handleInputWin :: InputHandler Game
|
||||||
|
handleInputWin = handleAnyKey goToMenu
|
||||||
|
|
||||||
|
----------------------------------------------------------------------
|
||||||
|
|
||||||
|
goToMenu :: Game -> Game
|
||||||
|
goToMenu g = g{ state = Menu }
|
16
lib/RPGEngine/Parse.hs
Normal file
|
@ -0,0 +1,16 @@
|
||||||
|
module RPGEngine.Parse
|
||||||
|
( parse
|
||||||
|
) where
|
||||||
|
|
||||||
|
import RPGEngine.Data ( Game )
|
||||||
|
import RPGEngine.Parse.StructureToGame ( structureToGame )
|
||||||
|
import GHC.IO (unsafePerformIO)
|
||||||
|
import Text.Parsec.String (parseFromFile)
|
||||||
|
import RPGEngine.Parse.TextToStructure ( gameFile )
|
||||||
|
|
||||||
|
------------------------------ Exported ------------------------------
|
||||||
|
|
||||||
|
parse :: FilePath -> Game
|
||||||
|
parse filename = structureToGame struct
|
||||||
|
where (Right struct) = unsafePerformIO io
|
||||||
|
io = parseFromFile gameFile filename
|
36
lib/RPGEngine/Parse/Core.hs
Normal file
|
@ -0,0 +1,36 @@
|
||||||
|
module RPGEngine.Parse.Core
|
||||||
|
( parseWith
|
||||||
|
, parseWithRest
|
||||||
|
, ignoreWS
|
||||||
|
) where
|
||||||
|
|
||||||
|
import Text.Parsec
|
||||||
|
( ParseError,
|
||||||
|
anyChar,
|
||||||
|
endOfLine,
|
||||||
|
spaces,
|
||||||
|
string,
|
||||||
|
anyToken,
|
||||||
|
choice,
|
||||||
|
eof,
|
||||||
|
manyTill,
|
||||||
|
parse )
|
||||||
|
import Text.Parsec.String ( Parser )
|
||||||
|
|
||||||
|
------------------------------ Exported ------------------------------
|
||||||
|
|
||||||
|
-- A wrapper, which takes a parser and some input and returns a
|
||||||
|
-- parsed output.
|
||||||
|
parseWith :: Parser a -> String -> Either ParseError a
|
||||||
|
parseWith parser = parse parser ""
|
||||||
|
|
||||||
|
-- Also return anything that has not yet been parsed
|
||||||
|
parseWithRest :: Parser a -> String -> Either ParseError (a, String)
|
||||||
|
-- fmap (,) over Parser monad and apply to rest
|
||||||
|
parseWithRest parser = parse ((,) <$> parser <*> rest) ""
|
||||||
|
where rest = manyTill anyToken eof
|
||||||
|
|
||||||
|
-- Ignore all kinds of whitespace
|
||||||
|
ignoreWS :: Parser a -> Parser a
|
||||||
|
ignoreWS parser = choice [skipComment, spaces] >> parser
|
||||||
|
where skipComment = do{ string "#"; manyTill anyChar endOfLine; return ()}
|
121
lib/RPGEngine/Parse/StructureToGame.hs
Normal file
|
@ -0,0 +1,121 @@
|
||||||
|
module RPGEngine.Parse.StructureToGame
|
||||||
|
-- Everything is exported for testing
|
||||||
|
where
|
||||||
|
|
||||||
|
import RPGEngine.Data
|
||||||
|
( Action,
|
||||||
|
Condition,
|
||||||
|
Player(playerHp, inventory),
|
||||||
|
Entity(entityId, entityX, entityY, entityName, entityDescription,
|
||||||
|
entityActions, entityValue, entityHp, direction),
|
||||||
|
Item(itemId, itemX, itemY, itemName, itemDescription, itemValue,
|
||||||
|
itemActions, useTimes),
|
||||||
|
Level(layout, items, entities, index),
|
||||||
|
Game (..), State (..) )
|
||||||
|
import RPGEngine.Parse.TextToStructure
|
||||||
|
( Value(Infinite, Action, Layout, String, Direction, Integer),
|
||||||
|
Key(Tag, ConditionList),
|
||||||
|
Structure(..) )
|
||||||
|
import RPGEngine.Data.Default (defaultPlayer, defaultLevel, defaultItem, defaultEntity)
|
||||||
|
import RPGEngine.Input.Playing (putCoords, spawnPlayer)
|
||||||
|
|
||||||
|
------------------------------ Exported ------------------------------
|
||||||
|
|
||||||
|
structureToGame :: [Structure] -> Game
|
||||||
|
structureToGame [Entry (Tag "player") playerBlock, Entry (Tag "levels") levelsBlock] = game
|
||||||
|
where game = Game newState
|
||||||
|
newState = Playing newLevels 0 currentLevel newPlayer newState
|
||||||
|
newLevels = structureToLevels levelsBlock
|
||||||
|
currentLevel = head newLevels
|
||||||
|
newPlayer = spawnPlayer currentLevel $ structureToPlayer playerBlock
|
||||||
|
structureToGame _ = Game Menu
|
||||||
|
|
||||||
|
------------------------------- Player -------------------------------
|
||||||
|
|
||||||
|
structureToPlayer :: Structure -> Player
|
||||||
|
structureToPlayer (Block block) = structureToPlayer' block defaultPlayer
|
||||||
|
structureToPlayer _ = defaultPlayer
|
||||||
|
|
||||||
|
structureToPlayer' :: [Structure] -> Player -> Player
|
||||||
|
structureToPlayer' [] p = p
|
||||||
|
structureToPlayer' ((Entry(Tag "hp") val ):es) p = (structureToPlayer' es p){ playerHp = structureToMaybeInt val }
|
||||||
|
structureToPlayer' ((Entry(Tag "inventory") (Block inv)):es) p = (structureToPlayer' es p){ inventory = structureToItems inv }
|
||||||
|
structureToPlayer' _ _ = defaultPlayer
|
||||||
|
|
||||||
|
structureToActions :: Structure -> [([Condition], Action)]
|
||||||
|
structureToActions (Block []) = []
|
||||||
|
structureToActions (Block block) = structureToActions' block []
|
||||||
|
structureToActions _ = []
|
||||||
|
|
||||||
|
structureToActions' :: [Structure] -> [([Condition], Action)] -> [([Condition], Action)]
|
||||||
|
structureToActions' [] list = list
|
||||||
|
structureToActions' ((Entry(ConditionList cs) (Regular (Action a))):as) list = structureToActions' as ((cs, a):list)
|
||||||
|
structureToActions' _ list = list
|
||||||
|
|
||||||
|
------------------------------- Levels -------------------------------
|
||||||
|
|
||||||
|
structureToLevels :: Structure -> [Level]
|
||||||
|
structureToLevels (Block struct) = structureToLevel <$> struct
|
||||||
|
structureToLevels _ = [defaultLevel]
|
||||||
|
|
||||||
|
structureToLevel :: Structure -> Level
|
||||||
|
structureToLevel (Block entries) = indexIsSet
|
||||||
|
where indexIsSet = level{ index = putCoords level }
|
||||||
|
level = structureToLevel' entries defaultLevel
|
||||||
|
structureToLevel _ = defaultLevel
|
||||||
|
|
||||||
|
structureToLevel' :: [Structure] -> Level -> Level
|
||||||
|
structureToLevel' ((Entry(Tag "layout") (Regular (Layout layout))):ls) l = (structureToLevel' ls l){ RPGEngine.Data.layout = layout }
|
||||||
|
structureToLevel' ((Entry(Tag "items") (Block items) ):ls) l = (structureToLevel' ls l){ items = structureToItems items }
|
||||||
|
structureToLevel' ((Entry(Tag "entities") (Block entities) ):ls) l = (structureToLevel' ls l){ entities = structureToEntities entities }
|
||||||
|
structureToLevel' _ _ = defaultLevel
|
||||||
|
|
||||||
|
------------------------------- Items --------------------------------
|
||||||
|
|
||||||
|
structureToItems :: [Structure] -> [Item]
|
||||||
|
structureToItems items = structureToItem <$> items
|
||||||
|
|
||||||
|
structureToItem :: Structure -> Item
|
||||||
|
structureToItem (Block block) = structureToItem' block defaultItem
|
||||||
|
structureToItem _ = defaultItem
|
||||||
|
|
||||||
|
structureToItem' :: [Structure] -> Item -> Item
|
||||||
|
structureToItem' [] i = i
|
||||||
|
structureToItem' ((Entry(Tag "id") (Regular(String id ))):is) i = (structureToItem' is i){ itemId = id }
|
||||||
|
structureToItem' ((Entry(Tag "x") (Regular(Integer x ))):is) i = (structureToItem' is i){ itemX = x }
|
||||||
|
structureToItem' ((Entry(Tag "y") (Regular(Integer y ))):is) i = (structureToItem' is i){ itemY = y }
|
||||||
|
structureToItem' ((Entry(Tag "name") (Regular(String name))):is) i = (structureToItem' is i){ itemName = name }
|
||||||
|
structureToItem' ((Entry(Tag "description") (Regular(String desc))):is) i = (structureToItem' is i){ itemDescription = desc }
|
||||||
|
structureToItem' ((Entry(Tag "value") val ):is) i = (structureToItem' is i){ itemValue = structureToMaybeInt val }
|
||||||
|
structureToItem' ((Entry(Tag "actions") actions ):is) i = (structureToItem' is i){ itemActions = structureToActions actions }
|
||||||
|
structureToItem' ((Entry (Tag "useTimes") useTimes ):is) i = (structureToItem' is i){ useTimes = structureToMaybeInt useTimes }
|
||||||
|
structureToItem' _ _ = defaultItem
|
||||||
|
|
||||||
|
------------------------------ Entities ------------------------------
|
||||||
|
|
||||||
|
structureToEntities :: [Structure] -> [Entity]
|
||||||
|
structureToEntities entities = structureToEntity <$> entities
|
||||||
|
|
||||||
|
structureToEntity :: Structure -> Entity
|
||||||
|
structureToEntity (Block block) = structureToEntity' block defaultEntity
|
||||||
|
structureToEntity _ = defaultEntity
|
||||||
|
|
||||||
|
structureToEntity' :: [Structure] -> Entity -> Entity
|
||||||
|
structureToEntity' [] e = e
|
||||||
|
structureToEntity' ((Entry(Tag "id") (Regular(String id )) ):es) e = (structureToEntity' es e){ entityId = id }
|
||||||
|
structureToEntity' ((Entry(Tag "x") (Regular(Integer x )) ):es) e = (structureToEntity' es e){ entityX = x }
|
||||||
|
structureToEntity' ((Entry(Tag "y") (Regular(Integer y )) ):es) e = (structureToEntity' es e){ entityY = y }
|
||||||
|
structureToEntity' ((Entry(Tag "name") (Regular(String name)) ):es) e = (structureToEntity' es e){ entityName = name }
|
||||||
|
structureToEntity' ((Entry(Tag "description") (Regular(String desc)) ):es) e = (structureToEntity' es e){ entityDescription = desc }
|
||||||
|
structureToEntity' ((Entry(Tag "actions") actions ):es) e = (structureToEntity' es e){ entityActions = structureToActions actions }
|
||||||
|
structureToEntity' ((Entry(Tag "value") val ):es) e = (structureToEntity' es e){ entityValue = structureToMaybeInt val }
|
||||||
|
structureToEntity' ((Entry(Tag "hp") val ):es) e = (structureToEntity' es e){ entityHp = structureToMaybeInt val }
|
||||||
|
structureToEntity' ((Entry(Tag "direction") (Regular(Direction dir))):es) e = (structureToEntity' es e){ RPGEngine.Data.direction = dir }
|
||||||
|
structureToEntity' _ _ = defaultEntity
|
||||||
|
|
||||||
|
----------------------------------------------------------------------
|
||||||
|
|
||||||
|
structureToMaybeInt :: Structure -> Maybe Int
|
||||||
|
structureToMaybeInt (Regular (Integer val)) = Just val
|
||||||
|
structureToMaybeInt (Regular Infinite) = Prelude.Nothing
|
||||||
|
structureToMaybeInt _ = Prelude.Nothing -- TODO
|
206
lib/RPGEngine/Parse/TextToStructure.hs
Normal file
|
@ -0,0 +1,206 @@
|
||||||
|
module RPGEngine.Parse.TextToStructure
|
||||||
|
-- Everything is exported for testing
|
||||||
|
where
|
||||||
|
|
||||||
|
import RPGEngine.Parse.Core ( ignoreWS )
|
||||||
|
|
||||||
|
import RPGEngine.Data (Action (..), Condition (..), Direction (..), Layout, Strip, Physical (..))
|
||||||
|
|
||||||
|
import Text.Parsec
|
||||||
|
( alphaNum,
|
||||||
|
char,
|
||||||
|
digit,
|
||||||
|
noneOf,
|
||||||
|
oneOf,
|
||||||
|
between,
|
||||||
|
choice,
|
||||||
|
many1,
|
||||||
|
notFollowedBy,
|
||||||
|
sepBy,
|
||||||
|
many,
|
||||||
|
try, spaces, endOfLine )
|
||||||
|
import qualified Text.Parsec as P ( string )
|
||||||
|
import Text.Parsec.String ( Parser )
|
||||||
|
import Text.Parsec.Combinator (lookAhead)
|
||||||
|
|
||||||
|
gameFile :: Parser [Structure]
|
||||||
|
gameFile = try $ do many1 $ ignoreWS structure
|
||||||
|
|
||||||
|
-------------------------- StructureElement --------------------------
|
||||||
|
|
||||||
|
-- See documentation for more details, only a short description is
|
||||||
|
-- provided here.
|
||||||
|
data Structure = Block [Structure]
|
||||||
|
| Entry Key Structure -- Key + Value
|
||||||
|
| Regular Value -- Regular value, Integer or String or Infinite
|
||||||
|
deriving (Eq, Show)
|
||||||
|
|
||||||
|
----------------------------------------------------------------------
|
||||||
|
|
||||||
|
structure :: Parser Structure
|
||||||
|
structure = try $ choice [block, entry, regular]
|
||||||
|
|
||||||
|
-- A list of entries
|
||||||
|
block :: Parser Structure
|
||||||
|
block = try $ do
|
||||||
|
open <- ignoreWS $ oneOf openingBrackets
|
||||||
|
middle <- ignoreWS $ choice [entry, block] `sepBy` char ','
|
||||||
|
let closingBracket = getMatchingClosingBracket open
|
||||||
|
ignoreWS $ char closingBracket
|
||||||
|
return $ Block middle
|
||||||
|
|
||||||
|
entry :: Parser Structure
|
||||||
|
entry = try $ do
|
||||||
|
key <- ignoreWS key
|
||||||
|
-- TODO Fix this
|
||||||
|
oneOf ": " -- Can be left out
|
||||||
|
value <- ignoreWS structure
|
||||||
|
return $ Entry key value
|
||||||
|
|
||||||
|
regular :: Parser Structure
|
||||||
|
regular = try $ Regular <$> value
|
||||||
|
|
||||||
|
--------------------------------- Key --------------------------------
|
||||||
|
|
||||||
|
data Key = Tag String
|
||||||
|
| ConditionList [Condition]
|
||||||
|
deriving (Eq, Show)
|
||||||
|
|
||||||
|
data ConditionArgument = ArgString String
|
||||||
|
| Condition Condition
|
||||||
|
deriving (Eq, Show)
|
||||||
|
|
||||||
|
----------------------------------------------------------------------
|
||||||
|
|
||||||
|
key :: Parser Key
|
||||||
|
key = try $ choice [conditionList, tag]
|
||||||
|
|
||||||
|
tag :: Parser Key
|
||||||
|
tag = try $ Tag <$> many1 alphaNum
|
||||||
|
|
||||||
|
conditionList :: Parser Key
|
||||||
|
conditionList = try $ do
|
||||||
|
open <- ignoreWS $ oneOf openingBrackets
|
||||||
|
list <- ignoreWS condition `sepBy` char ','
|
||||||
|
let closingBracket = getMatchingClosingBracket open
|
||||||
|
ignoreWS $ char closingBracket
|
||||||
|
return $ ConditionList $ extract list
|
||||||
|
where extract ((Condition cond):list) = cond:extract list
|
||||||
|
extract _ = []
|
||||||
|
|
||||||
|
condition :: Parser ConditionArgument
|
||||||
|
condition = try $ do
|
||||||
|
text <- ignoreWS $ many1 $ noneOf illegalCharacters
|
||||||
|
open <- ignoreWS $ oneOf openingBrackets
|
||||||
|
cond <- ignoreWS $ choice [condition, argString]
|
||||||
|
let closingBracket = getMatchingClosingBracket open
|
||||||
|
ignoreWS $ char closingBracket
|
||||||
|
return $ Condition $ make text cond
|
||||||
|
where make "inventoryFull" _ = InventoryFull
|
||||||
|
make "inventoryContains" (ArgString arg) = InventoryContains arg
|
||||||
|
make "not" (Condition cond) = Not cond
|
||||||
|
make _ _ = AlwaysFalse
|
||||||
|
argString = try $ ArgString <$> many (noneOf illegalCharacters)
|
||||||
|
|
||||||
|
-------------------------------- Value -------------------------------
|
||||||
|
|
||||||
|
data Value = String String
|
||||||
|
| Integer Int
|
||||||
|
| Infinite
|
||||||
|
| Action Action
|
||||||
|
| Direction Direction
|
||||||
|
| Layout Layout
|
||||||
|
deriving (Eq, Show)
|
||||||
|
|
||||||
|
----------------------------------------------------------------------
|
||||||
|
|
||||||
|
value :: Parser Value
|
||||||
|
value = choice [layout, string, integer, infinite, direction, action]
|
||||||
|
|
||||||
|
string :: Parser Value
|
||||||
|
string = try $ String <$> between (char '\"') (char '\"') reading
|
||||||
|
where reading = ignoreWS $ many1 $ noneOf illegalCharacters
|
||||||
|
|
||||||
|
integer :: Parser Value
|
||||||
|
integer = try $ do
|
||||||
|
value <- ignoreWS $ many1 digit
|
||||||
|
return $ Integer (read value :: Int)
|
||||||
|
|
||||||
|
infinite :: Parser Value
|
||||||
|
infinite = try $ do
|
||||||
|
ignoreWS $ P.string "infinite"
|
||||||
|
notFollowedBy alphaNum
|
||||||
|
return Infinite
|
||||||
|
|
||||||
|
action :: Parser Value
|
||||||
|
action = try $ do
|
||||||
|
script <- ignoreWS $ many1 $ noneOf "("
|
||||||
|
arg <- ignoreWS $ between (char '(') (char ')') $ many $ noneOf ")"
|
||||||
|
let answer | script == "leave" = Leave
|
||||||
|
| script == "retrieveItem" = RetrieveItem arg
|
||||||
|
| script == "useItem" = UseItem arg
|
||||||
|
| script == "decreaseHp" = DecreaseHp first (filter (/= ' ') second) -- TODO Work this hack away
|
||||||
|
| script == "increasePlayerHp" = IncreasePlayerHp arg
|
||||||
|
| otherwise = DoNothing
|
||||||
|
(first, ',':second) = break (== ',') arg
|
||||||
|
return $ Action answer
|
||||||
|
|
||||||
|
direction :: Parser Value
|
||||||
|
direction = try $ do
|
||||||
|
value <- choice [
|
||||||
|
ignoreWS $ P.string "up",
|
||||||
|
ignoreWS $ P.string "down",
|
||||||
|
ignoreWS $ P.string "left",
|
||||||
|
ignoreWS $ P.string "right"
|
||||||
|
]
|
||||||
|
-- lookAhead $ char ','
|
||||||
|
return $ Direction $ make value
|
||||||
|
where make "up" = North
|
||||||
|
make "right" = East
|
||||||
|
make "down" = South
|
||||||
|
make "left" = West
|
||||||
|
make _ = Stay
|
||||||
|
|
||||||
|
layout :: Parser Value
|
||||||
|
layout = try $ do
|
||||||
|
open <- ignoreWS $ oneOf openingBrackets
|
||||||
|
let closing = getMatchingClosingBracket open
|
||||||
|
value <- many1 strip <* ignoreWS (char closing)
|
||||||
|
return $ Layout value
|
||||||
|
|
||||||
|
strip :: Parser Strip
|
||||||
|
strip = try $ do ignoreWS (char '|') *> ignoreWS (physical `sepBy` char ' ')
|
||||||
|
|
||||||
|
physical :: Parser Physical
|
||||||
|
physical = try $ do
|
||||||
|
value <- choice [
|
||||||
|
char 'x',
|
||||||
|
char '.',
|
||||||
|
char '*',
|
||||||
|
char 's',
|
||||||
|
char 'e'
|
||||||
|
]
|
||||||
|
return $ make value
|
||||||
|
where make '.' = Walkable
|
||||||
|
make '*' = Blocked
|
||||||
|
make 's' = Entrance
|
||||||
|
make 'e' = Exit
|
||||||
|
make _ = Void
|
||||||
|
|
||||||
|
------------------------------ Brackets ------------------------------
|
||||||
|
|
||||||
|
openingBrackets :: [Char]
|
||||||
|
openingBrackets = "<({["
|
||||||
|
|
||||||
|
closingBrackets :: [Char]
|
||||||
|
closingBrackets = ">)}]"
|
||||||
|
|
||||||
|
illegalCharacters :: [Char]
|
||||||
|
illegalCharacters = ",:\"" ++ openingBrackets ++ closingBrackets
|
||||||
|
|
||||||
|
----------------------------------------------------------------------
|
||||||
|
|
||||||
|
getMatchingClosingBracket :: Char -> Char
|
||||||
|
getMatchingClosingBracket opening = closingBrackets !! index
|
||||||
|
where combo = zip openingBrackets [0 ..]
|
||||||
|
index = head $ [y | (x, y) <- combo, x == opening]
|
39
lib/RPGEngine/Render.hs
Normal file
|
@ -0,0 +1,39 @@
|
||||||
|
-- Implementation for each state can be found in their respective
|
||||||
|
-- submodules.
|
||||||
|
module RPGEngine.Render
|
||||||
|
( initWindow
|
||||||
|
, render
|
||||||
|
) where
|
||||||
|
|
||||||
|
import RPGEngine.Render.Core ( Renderer(..) )
|
||||||
|
|
||||||
|
import RPGEngine.Data (Game(..), State (..))
|
||||||
|
import RPGEngine.Render.Menu( renderMenu )
|
||||||
|
import RPGEngine.Render.LevelSelection ( renderLevelSelection )
|
||||||
|
import RPGEngine.Render.Playing ( renderPlaying )
|
||||||
|
import RPGEngine.Render.Paused ( renderPaused )
|
||||||
|
import RPGEngine.Render.Win ( renderWin )
|
||||||
|
import RPGEngine.Render.Lose ( renderLose )
|
||||||
|
|
||||||
|
import Graphics.Gloss ( Display, text, color )
|
||||||
|
import Graphics.Gloss.Data.Picture (Picture, blank)
|
||||||
|
import Graphics.Gloss.Data.Display (Display(..))
|
||||||
|
import RPGEngine.Render.ActionSelection (renderActionSelection)
|
||||||
|
import RPGEngine.Config (textColor)
|
||||||
|
|
||||||
|
----------------------------------------------------------------------
|
||||||
|
|
||||||
|
-- Initialize a window to play in
|
||||||
|
initWindow :: String -> (Int, Int) -> (Int, Int) -> Display
|
||||||
|
initWindow = InWindow
|
||||||
|
|
||||||
|
-- Render all different states
|
||||||
|
render :: Game -> Picture
|
||||||
|
render Game{ state = s@Menu } = renderMenu s
|
||||||
|
render Game{ state = s@LevelSelection{} } = renderLevelSelection s
|
||||||
|
render Game{ state = s@Playing{} } = renderPlaying s
|
||||||
|
render Game{ state = s@Paused{} } = renderPaused s
|
||||||
|
render Game{ state = s@Win } = renderWin s
|
||||||
|
render Game{ state = s@Lose{} } = renderLose s
|
||||||
|
render Game{ state = s@ActionSelection{}} = renderActionSelection s
|
||||||
|
render Game{ state = Error message } = color textColor $ text message
|
26
lib/RPGEngine/Render/ActionSelection.hs
Normal file
|
@ -0,0 +1,26 @@
|
||||||
|
module RPGEngine.Render.ActionSelection
|
||||||
|
( renderActionSelection
|
||||||
|
) where
|
||||||
|
|
||||||
|
import RPGEngine.Data (State (..), Action (..))
|
||||||
|
import Graphics.Gloss
|
||||||
|
( Picture, text, pictures, translate, scale, color )
|
||||||
|
import Graphics.Gloss.Data.Picture (blank)
|
||||||
|
import RPGEngine.Data.Level (getActionText)
|
||||||
|
import RPGEngine.Config (uizoom, selectionColor, textColor)
|
||||||
|
import RPGEngine.Input.Core (ListSelector(selection))
|
||||||
|
import RPGEngine.Render.Playing (renderPlaying)
|
||||||
|
import RPGEngine.Render.Core (overlay)
|
||||||
|
|
||||||
|
------------------------------ Exported ------------------------------
|
||||||
|
|
||||||
|
renderActionSelection :: State -> Picture
|
||||||
|
renderActionSelection (ActionSelection list selector continue) = everything
|
||||||
|
where numberedTexts = zip [0::Int ..] $ map getActionText list
|
||||||
|
sel = selection selector
|
||||||
|
everything = pictures $ [renderPlaying continue, overlay] ++ map render numberedTexts
|
||||||
|
render (i, t) | i == sel = color selectionColor $ make (i, t)
|
||||||
|
| otherwise = color textColor $ make (i, t)
|
||||||
|
make (i, t) = scale uizoom uizoom $ translate 0 (offset i) $ text t
|
||||||
|
offset i = negate (250 * uizoom * fromIntegral i)
|
||||||
|
renderActionSelection _ = blank
|
93
lib/RPGEngine/Render/Core.hs
Normal file
|
@ -0,0 +1,93 @@
|
||||||
|
module RPGEngine.Render.Core
|
||||||
|
( Renderer
|
||||||
|
|
||||||
|
, getRender
|
||||||
|
, setRenderPos
|
||||||
|
, overlay
|
||||||
|
) where
|
||||||
|
|
||||||
|
import RPGEngine.Config
|
||||||
|
|
||||||
|
import Data.Maybe
|
||||||
|
import Graphics.Gloss
|
||||||
|
import GHC.IO
|
||||||
|
import Graphics.Gloss.Juicy
|
||||||
|
|
||||||
|
----------------------------- Constants ------------------------------
|
||||||
|
|
||||||
|
type Renderer a = a -> Picture
|
||||||
|
|
||||||
|
unknownImage :: FilePath
|
||||||
|
unknownImage = "unknown.png"
|
||||||
|
|
||||||
|
allEntities :: [(String, FilePath)]
|
||||||
|
allEntities = [
|
||||||
|
("player", "player.png"),
|
||||||
|
("devil", "devil.png" ),
|
||||||
|
("door", "door.png")
|
||||||
|
]
|
||||||
|
|
||||||
|
allEnvironment :: [(String, FilePath)]
|
||||||
|
allEnvironment = [
|
||||||
|
("void", "void.png"),
|
||||||
|
("overlay", "overlay.png"),
|
||||||
|
("tile", "tile.png"),
|
||||||
|
("wall", "wall.png"),
|
||||||
|
("entrance", "entrance.png"),
|
||||||
|
("exit", "exit.png")
|
||||||
|
]
|
||||||
|
|
||||||
|
allItems :: [(String, FilePath)]
|
||||||
|
allItems = [
|
||||||
|
("dagger", "dagger.png"),
|
||||||
|
("key", "key.png" ),
|
||||||
|
("potion", "potion.png"),
|
||||||
|
("sword", "sword.png" )
|
||||||
|
]
|
||||||
|
|
||||||
|
allGui :: [(String, FilePath)]
|
||||||
|
allGui = [
|
||||||
|
("main", "main.png"),
|
||||||
|
("health", "health.png")
|
||||||
|
]
|
||||||
|
|
||||||
|
-- Map of all renders
|
||||||
|
library :: [(String, Picture)]
|
||||||
|
library = unknown:entities ++ environment ++ gui ++ items
|
||||||
|
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 = map (\(f, s) -> (f, renderPNG (assetsFolder ++ "gui/" ++ s))) allGui
|
||||||
|
items = map (\(f, s) -> (f, renderPNG (assetsFolder ++ "items/" ++ s))) allItems
|
||||||
|
|
||||||
|
------------------------------ Exported ------------------------------
|
||||||
|
|
||||||
|
-- Retrieve an image from the library. If the library does not contain
|
||||||
|
-- the requested image, a default is returned.
|
||||||
|
getRender :: String -> Picture
|
||||||
|
getRender id = get filtered
|
||||||
|
where filtered = filter ((== id) . fst) library
|
||||||
|
get [] = snd $ head library
|
||||||
|
get ((_, res):_) = res
|
||||||
|
|
||||||
|
-- Move a picture by game coordinates
|
||||||
|
setRenderPos :: Int -> Int -> Picture -> Picture
|
||||||
|
setRenderPos x y = translate floatX floatY
|
||||||
|
where floatX = fromIntegral x * 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)
|
||||||
|
|
||||||
|
----------------------------------------------------------------------
|
||||||
|
|
||||||
|
-- Turn a path to a .png file into a Picture.
|
||||||
|
renderPNG :: FilePath -> Picture
|
||||||
|
renderPNG path = scale zoom zoom $ fromJust $ unsafePerformIO $ loadJuicyPNG path
|
32
lib/RPGEngine/Render/LevelSelection.hs
Normal file
|
@ -0,0 +1,32 @@
|
||||||
|
module RPGEngine.Render.LevelSelection
|
||||||
|
( renderLevelSelection
|
||||||
|
) where
|
||||||
|
|
||||||
|
import RPGEngine.Render.Core (Renderer)
|
||||||
|
|
||||||
|
import RPGEngine.Config (resolution, zoom, uizoom, textColor, selectionColor )
|
||||||
|
import RPGEngine.Data (State (..))
|
||||||
|
|
||||||
|
import Graphics.Gloss ( pictures, color, text, translate, blank )
|
||||||
|
import Graphics.Gloss.Data.Color (red)
|
||||||
|
import Graphics.Gloss.Data.Picture (scale)
|
||||||
|
import RPGEngine.Input.Core (ListSelector(..))
|
||||||
|
|
||||||
|
------------------------------ Exported ------------------------------
|
||||||
|
|
||||||
|
renderLevelSelection :: Renderer State
|
||||||
|
renderLevelSelection state = result
|
||||||
|
where result = renderLevelList state
|
||||||
|
|
||||||
|
----------------------------------------------------------------------
|
||||||
|
|
||||||
|
renderLevelList :: Renderer State
|
||||||
|
renderLevelList (LevelSelection list selector) = everything
|
||||||
|
where everything = pictures $ map render entries
|
||||||
|
sel = selection selector
|
||||||
|
entries = zip [0::Int .. ] list
|
||||||
|
render (i, path) | i == sel = color selectionColor $ make (i, path)
|
||||||
|
| otherwise = color textColor $ make (i, path)
|
||||||
|
make (i, path) = scale uizoom uizoom $ translate 0 (offset i) $ text path
|
||||||
|
offset i = negate (250 * uizoom * fromIntegral i)
|
||||||
|
renderLevelList _ = blank
|
21
lib/RPGEngine/Render/Lose.hs
Normal file
|
@ -0,0 +1,21 @@
|
||||||
|
module RPGEngine.Render.Lose
|
||||||
|
( renderLose
|
||||||
|
) where
|
||||||
|
import RPGEngine.Render.Core (Renderer)
|
||||||
|
|
||||||
|
import RPGEngine.Config (uizoom, textColor)
|
||||||
|
import RPGEngine.Data (State)
|
||||||
|
|
||||||
|
import Graphics.Gloss (text, scale, color, translate)
|
||||||
|
|
||||||
|
------------------------------ Constants -----------------------------
|
||||||
|
|
||||||
|
message :: String
|
||||||
|
message = "You lose! Press any key to retry."
|
||||||
|
|
||||||
|
------------------------------ Exported ------------------------------
|
||||||
|
|
||||||
|
renderLose :: Renderer State
|
||||||
|
renderLose _ = scaled $ center $ color textColor $ text message
|
||||||
|
where scaled = scale uizoom uizoom
|
||||||
|
center = translate (-1200) 0
|
24
lib/RPGEngine/Render/Menu.hs
Normal file
|
@ -0,0 +1,24 @@
|
||||||
|
module RPGEngine.Render.Menu
|
||||||
|
( renderMenu
|
||||||
|
) where
|
||||||
|
|
||||||
|
import RPGEngine.Render.Core (Renderer, getRender)
|
||||||
|
|
||||||
|
import RPGEngine.Config ( uizoom, textColor )
|
||||||
|
import RPGEngine.Data (State)
|
||||||
|
|
||||||
|
import Graphics.Gloss (text, scale, color, translate, pictures)
|
||||||
|
|
||||||
|
------------------------------ Constants -----------------------------
|
||||||
|
|
||||||
|
message :: String
|
||||||
|
message = "[Press any key to start]"
|
||||||
|
|
||||||
|
------------------------------ Exported ------------------------------
|
||||||
|
|
||||||
|
renderMenu :: Renderer State
|
||||||
|
renderMenu _ = pictures [main, pressAny]
|
||||||
|
where pressAny = scaled $ center $ color textColor $ text message
|
||||||
|
scaled = scale uizoom uizoom
|
||||||
|
center = translate (-800) (-320)
|
||||||
|
main = getRender "main"
|
20
lib/RPGEngine/Render/Paused.hs
Normal file
|
@ -0,0 +1,20 @@
|
||||||
|
module RPGEngine.Render.Paused
|
||||||
|
( renderPaused
|
||||||
|
) where
|
||||||
|
|
||||||
|
import RPGEngine.Render.Core (Renderer, overlay)
|
||||||
|
|
||||||
|
import RPGEngine.Data (State(..))
|
||||||
|
import RPGEngine.Render.Playing (renderPlaying)
|
||||||
|
|
||||||
|
import Graphics.Gloss (pictures, white, color, Color(..), text, scale)
|
||||||
|
|
||||||
|
------------------------------ Exported ------------------------------
|
||||||
|
|
||||||
|
renderPaused :: Renderer State
|
||||||
|
renderPaused state = pictures [playing, pause]
|
||||||
|
where playing = renderPlaying $ continue state
|
||||||
|
pause = pictures [
|
||||||
|
overlay,
|
||||||
|
color white $ scale 0.5 0.5 $ text "[Press any key to continue]"
|
||||||
|
]
|
112
lib/RPGEngine/Render/Playing.hs
Normal file
|
@ -0,0 +1,112 @@
|
||||||
|
module RPGEngine.Render.Playing
|
||||||
|
( renderPlaying
|
||||||
|
) where
|
||||||
|
|
||||||
|
import RPGEngine.Render.Core (Renderer, getRender, setRenderPos, overlay)
|
||||||
|
|
||||||
|
import RPGEngine.Config (resolution, zoom, uizoom)
|
||||||
|
import RPGEngine.Data (State(..), Player (..), Game (..), Level (..), Layout, Physical (..), Item (..), Entity (..), HP)
|
||||||
|
|
||||||
|
import Data.Maybe ( fromJust )
|
||||||
|
import Graphics.Gloss ( pictures, Picture, translate, white )
|
||||||
|
import Graphics.Gloss.Data.Picture ( blank, text, color, scale )
|
||||||
|
|
||||||
|
------------------------------ Exported ------------------------------
|
||||||
|
|
||||||
|
renderPlaying :: Renderer State
|
||||||
|
renderPlaying Playing { level = lvl, player = player } = pictures [
|
||||||
|
renderLevel lvl,
|
||||||
|
renderPlayer player,
|
||||||
|
renderInventory player
|
||||||
|
]
|
||||||
|
renderPlaying _ = blank
|
||||||
|
|
||||||
|
------------------------------- Player -------------------------------
|
||||||
|
|
||||||
|
renderPlayer :: Renderer Player
|
||||||
|
renderPlayer Player{ position = (x, y), playerHp = playerHp } = move picture
|
||||||
|
where move = setRenderPos x y
|
||||||
|
picture = withHealthBar playerHp $ getRender "player"
|
||||||
|
|
||||||
|
-- Center the player in the middle of the screen.
|
||||||
|
-- Not in use at the moment, might be useful later.
|
||||||
|
focusPlayer :: Game -> Picture -> Picture
|
||||||
|
focusPlayer Game{ state = Playing{ player = Player{ position = (x, y) }}} = move
|
||||||
|
where move = translate centerX centerY
|
||||||
|
centerX = resolution * zoom * fromIntegral (negate x)
|
||||||
|
centerY = resolution * zoom * fromIntegral (negate y)
|
||||||
|
focusPlayer _ = id
|
||||||
|
|
||||||
|
------------------------------- Level --------------------------------
|
||||||
|
|
||||||
|
renderLevel :: Renderer Level
|
||||||
|
renderLevel Level{ layout = l, items = i, entities = e } = level
|
||||||
|
where level = pictures [void, layout, items, entities]
|
||||||
|
-- void = createVoid
|
||||||
|
void = blank
|
||||||
|
layout = renderLayout l
|
||||||
|
items = renderItems i
|
||||||
|
entities = renderEntities e
|
||||||
|
|
||||||
|
renderLayout :: Layout -> Picture
|
||||||
|
renderLayout strips = pictures [setRenderPos 0 (count - y) (renderStrip (strips !! y)) | y <- [0 .. count]]
|
||||||
|
where count = length strips - 1
|
||||||
|
|
||||||
|
renderStrip :: [Physical] -> Picture
|
||||||
|
renderStrip list = pictures physicals
|
||||||
|
where physicals = [setRenderPos x 0 (image (list !! x)) | x <- [0 .. count]]
|
||||||
|
image Void = getRender "void"
|
||||||
|
image Walkable = getRender "tile"
|
||||||
|
image Blocked = getRender "wall"
|
||||||
|
image Entrance = pictures [getRender "tile", getRender "entrance"]
|
||||||
|
image Exit = pictures [getRender "tile", getRender "exit"]
|
||||||
|
count = length list - 1
|
||||||
|
|
||||||
|
createVoid :: Picture
|
||||||
|
createVoid = setRenderPos offX offY $ pictures voids
|
||||||
|
where voids = [setRenderPos x y void | x <- [0 .. width], y <- [0 .. height]]
|
||||||
|
void = getRender "void"
|
||||||
|
intZoom = round zoom :: Int
|
||||||
|
height = round $ 4320 / resolution / zoom
|
||||||
|
width = round $ 7680 / resolution / zoom
|
||||||
|
offX = negate (width `div` 2)
|
||||||
|
offY = negate (height `div` 2)
|
||||||
|
|
||||||
|
-------------------------- Items & Entities --------------------------
|
||||||
|
|
||||||
|
renderItems :: [Item] -> Picture
|
||||||
|
renderItems list = pictures $ map renderItem list
|
||||||
|
|
||||||
|
renderItem :: Item -> Picture
|
||||||
|
renderItem Item{ itemId = id, itemX = x, itemY = y} = setRenderPos x y image
|
||||||
|
where image = getRender id
|
||||||
|
|
||||||
|
renderEntities :: [Entity] -> Picture
|
||||||
|
renderEntities list = pictures $ map renderEntity list
|
||||||
|
|
||||||
|
renderEntity :: Entity -> Picture
|
||||||
|
renderEntity Entity{ entityId = id, entityX = x, entityY = y, entityHp = hp} = setRenderPos x y image
|
||||||
|
where image = withHealthBar hp $ getRender id
|
||||||
|
|
||||||
|
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)
|
||||||
|
move (i, pic) = translate 0 (offset i) pic
|
||||||
|
offset i = negate (zoom * resolution * fromIntegral i)
|
||||||
|
|
||||||
|
withHealthBar :: HP -> Picture -> Picture
|
||||||
|
withHealthBar (Nothing) renderedEntity = renderedEntity
|
||||||
|
withHealthBar (Just hp) renderedEntity = pictures [renderedEntity, positionedBar]
|
||||||
|
where positionedBar = scale smaller smaller $ translate left up renderedBar
|
||||||
|
renderedBar = pictures [heart, counter]
|
||||||
|
heart = scale by by $ getRender "health"
|
||||||
|
counter = translate right down $ scale scaler scaler $ color white $ text $ show hp
|
||||||
|
left = negate $ uizoom * resolution * scaler
|
||||||
|
right = uizoom * resolution * 0.05
|
||||||
|
up = uizoom * resolution
|
||||||
|
down = negate $ resolution * uizoom * 0.15
|
||||||
|
smaller = resolution * uizoom
|
||||||
|
by = uizoom * 0.1
|
||||||
|
scaler = by * 0.5
|
22
lib/RPGEngine/Render/Win.hs
Normal file
|
@ -0,0 +1,22 @@
|
||||||
|
module RPGEngine.Render.Win
|
||||||
|
( renderWin
|
||||||
|
) where
|
||||||
|
|
||||||
|
import RPGEngine.Render.Core (Renderer)
|
||||||
|
|
||||||
|
import RPGEngine.Config (uizoom, textColor)
|
||||||
|
import RPGEngine.Data (State)
|
||||||
|
|
||||||
|
import Graphics.Gloss (text, scale, color, translate)
|
||||||
|
|
||||||
|
------------------------------ Constants -----------------------------
|
||||||
|
|
||||||
|
message :: String
|
||||||
|
message = "You win! Press any key to return to the menu."
|
||||||
|
|
||||||
|
------------------------------ Exported ------------------------------
|
||||||
|
|
||||||
|
renderWin :: Renderer State
|
||||||
|
renderWin _ = scaled $ center $ color textColor $ text message
|
||||||
|
where scaled = scale uizoom uizoom
|
||||||
|
center = translate (-1500) 0
|
|
@ -1,23 +0,0 @@
|
||||||
module Input
|
|
||||||
(
|
|
||||||
-- Handle all input for RPG-Engine
|
|
||||||
handleAllInput
|
|
||||||
) where
|
|
||||||
|
|
||||||
import Game
|
|
||||||
import State
|
|
||||||
import InputHandling
|
|
||||||
|
|
||||||
import Graphics.Gloss.Interface.IO.Game
|
|
||||||
|
|
||||||
----------------------------------------------------------------------
|
|
||||||
|
|
||||||
handleAllInput :: InputHandler Game
|
|
||||||
handleAllInput = composeInputHandlers [
|
|
||||||
handleSpecialKey KeySpace setNextState
|
|
||||||
]
|
|
||||||
|
|
||||||
-- Go to the next stage of the Game
|
|
||||||
setNextState :: Game -> Game
|
|
||||||
setNextState game = game{ state = newState }
|
|
||||||
where newState = nextState $ state game
|
|
|
@ -1,41 +0,0 @@
|
||||||
-- Allows to create a massive inputHandler that can handle anything
|
|
||||||
-- after you specify what you want it to do.
|
|
||||||
|
|
||||||
module InputHandling
|
|
||||||
( InputHandler(..),
|
|
||||||
-- Compose multiple InputHandlers into one InputHandler that handles
|
|
||||||
-- all of them.
|
|
||||||
composeInputHandlers,
|
|
||||||
|
|
||||||
handle,
|
|
||||||
handleSpecialKey
|
|
||||||
) where
|
|
||||||
|
|
||||||
import Graphics.Gloss.Interface.IO.Game
|
|
||||||
|
|
||||||
----------------------------- Constants ------------------------------
|
|
||||||
|
|
||||||
type InputHandler a = Event -> (a -> a)
|
|
||||||
|
|
||||||
----------------------------------------------------------------------
|
|
||||||
|
|
||||||
composeInputHandlers :: [InputHandler a] -> InputHandler a
|
|
||||||
composeInputHandlers [] ev a = a
|
|
||||||
composeInputHandlers (ih:ihs) ev a = composeInputHandlers ihs ev (ih ev a)
|
|
||||||
|
|
||||||
handle :: Event -> (a -> a) -> Event -> (a -> a)
|
|
||||||
handle (EventKey key _ _ _) = handleKey key
|
|
||||||
-- handle (EventMotion _) = undefined
|
|
||||||
-- handle (EventResize _) = undefined
|
|
||||||
handle _ = (\_ -> (\_ -> id))
|
|
||||||
|
|
||||||
handleKey :: Key -> (a -> a) -> Event -> (a -> a)
|
|
||||||
handleKey (SpecialKey key) = handleSpecialKey key
|
|
||||||
handleKey (Char _ ) = (\_ -> (\_ -> id))
|
|
||||||
handleKey (MouseButton _ ) = (\_ -> (\_ -> id))
|
|
||||||
|
|
||||||
handleSpecialKey :: SpecialKey -> (a -> a) -> Event -> (a -> a)
|
|
||||||
handleSpecialKey sk1 f (EventKey (SpecialKey sk2) Down _ _)
|
|
||||||
| sk1 == sk2 = f
|
|
||||||
| otherwise = id
|
|
||||||
handleSpecialKey _ _ _ = id
|
|
|
@ -1,25 +0,0 @@
|
||||||
-- Representation of all the game's data
|
|
||||||
|
|
||||||
module Game
|
|
||||||
( Game(..)
|
|
||||||
|
|
||||||
-- Initialize the game
|
|
||||||
, initGame
|
|
||||||
) where
|
|
||||||
|
|
||||||
import State
|
|
||||||
|
|
||||||
----------------------------- Constants ------------------------------
|
|
||||||
|
|
||||||
-- TODO Add more
|
|
||||||
data Game = Game {
|
|
||||||
-- Current state of the game
|
|
||||||
state :: State
|
|
||||||
}
|
|
||||||
|
|
||||||
----------------------------------------------------------------------
|
|
||||||
|
|
||||||
initGame :: Game
|
|
||||||
initGame = Game {
|
|
||||||
state = defaultState
|
|
||||||
}
|
|
|
@ -1,32 +0,0 @@
|
||||||
-- Describes the current state of the game,
|
|
||||||
-- e.g. Main menu, game, pause, win or lose
|
|
||||||
-- Allows to easily go to a next state and change rendering accordingly
|
|
||||||
|
|
||||||
module State
|
|
||||||
( State(..)
|
|
||||||
-- Default state of the game, Menu
|
|
||||||
, defaultState
|
|
||||||
|
|
||||||
-- Get the next state based on the current state
|
|
||||||
, nextState
|
|
||||||
) where
|
|
||||||
|
|
||||||
----------------------------- Constants ------------------------------
|
|
||||||
|
|
||||||
-- Current state of the game.
|
|
||||||
data State = Menu
|
|
||||||
| Playing
|
|
||||||
| Pause
|
|
||||||
| Win
|
|
||||||
| Lose
|
|
||||||
|
|
||||||
----------------------------------------------------------------------
|
|
||||||
|
|
||||||
defaultState :: State
|
|
||||||
defaultState = Menu
|
|
||||||
|
|
||||||
nextState :: State -> State
|
|
||||||
nextState Menu = Playing
|
|
||||||
nextState Playing = Pause
|
|
||||||
nextState Pause = Playing
|
|
||||||
nextState _ = Menu
|
|
|
@ -1,47 +0,0 @@
|
||||||
-- Allows to render the played game
|
|
||||||
|
|
||||||
module Render
|
|
||||||
(
|
|
||||||
-- Initialize a window to play in
|
|
||||||
initWindow
|
|
||||||
|
|
||||||
-- Render the game
|
|
||||||
, render
|
|
||||||
) where
|
|
||||||
|
|
||||||
import Game(Game(..))
|
|
||||||
import State(State(..))
|
|
||||||
import Graphics.Gloss
|
|
||||||
|
|
||||||
----------------------------------------------------------------------
|
|
||||||
|
|
||||||
initWindow :: String -> (Int, Int) -> (Int, Int) -> Display
|
|
||||||
initWindow title dims offs = InWindow title dims offs
|
|
||||||
|
|
||||||
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
|
|
||||||
|
|
||||||
|
|
||||||
-- TODO
|
|
||||||
renderMenu :: Game -> Picture
|
|
||||||
renderMenu _ = text "Menu"
|
|
||||||
|
|
||||||
-- TODO
|
|
||||||
renderPlaying :: Game -> Picture
|
|
||||||
renderPlaying _ = text "Playing"
|
|
||||||
|
|
||||||
-- TODO
|
|
||||||
renderPause :: Game -> Picture
|
|
||||||
renderPause _ = text "Pause"
|
|
||||||
|
|
||||||
-- TODO
|
|
||||||
renderWin :: Game -> Picture
|
|
||||||
renderWin _ = text "Win"
|
|
||||||
|
|
||||||
-- TODO
|
|
||||||
renderLose :: Game -> Picture
|
|
||||||
renderLose _ = text "Lose"
|
|
|
@ -5,15 +5,46 @@ cabal-version: 1.12
|
||||||
build-type: Simple
|
build-type: Simple
|
||||||
|
|
||||||
library
|
library
|
||||||
hs-source-dirs: lib, lib/control, lib/data, lib/render
|
hs-source-dirs: lib
|
||||||
build-depends:
|
build-depends:
|
||||||
base >= 4.7 && <5,
|
base >= 4.7 && <5,
|
||||||
gloss >= 1.11 && < 1.14, gloss-juicy >= 0.2.3
|
directory >= 1.3.6.0,
|
||||||
|
gloss >= 1.11 && < 1.14, gloss-juicy >= 0.2.3,
|
||||||
|
parsec >= 3.1.15.1
|
||||||
exposed-modules:
|
exposed-modules:
|
||||||
RPGEngine,
|
RPGEngine
|
||||||
Input, InputHandling,
|
|
||||||
Game, State,
|
RPGEngine.Config
|
||||||
Render
|
|
||||||
|
RPGEngine.Data
|
||||||
|
RPGEngine.Data.Default
|
||||||
|
RPGEngine.Data.Game
|
||||||
|
RPGEngine.Data.Level
|
||||||
|
|
||||||
|
RPGEngine.Input
|
||||||
|
RPGEngine.Input.Core
|
||||||
|
RPGEngine.Input.ActionSelection
|
||||||
|
RPGEngine.Input.Menu
|
||||||
|
RPGEngine.Input.LevelSelection
|
||||||
|
RPGEngine.Input.Playing
|
||||||
|
RPGEngine.Input.Paused
|
||||||
|
RPGEngine.Input.Win
|
||||||
|
RPGEngine.Input.Lose
|
||||||
|
|
||||||
|
RPGEngine.Parse
|
||||||
|
RPGEngine.Parse.Core
|
||||||
|
RPGEngine.Parse.TextToStructure
|
||||||
|
RPGEngine.Parse.StructureToGame
|
||||||
|
|
||||||
|
RPGEngine.Render
|
||||||
|
RPGEngine.Render.Core
|
||||||
|
RPGEngine.Render.ActionSelection
|
||||||
|
RPGEngine.Render.Menu
|
||||||
|
RPGEngine.Render.LevelSelection
|
||||||
|
RPGEngine.Render.Playing
|
||||||
|
RPGEngine.Render.Paused
|
||||||
|
RPGEngine.Render.Win
|
||||||
|
RPGEngine.Render.Lose
|
||||||
|
|
||||||
executable rpg-engine
|
executable rpg-engine
|
||||||
main-is: Main.hs
|
main-is: Main.hs
|
||||||
|
@ -23,7 +54,14 @@ executable rpg-engine
|
||||||
|
|
||||||
test-suite rpg-engine-test
|
test-suite rpg-engine-test
|
||||||
type: exitcode-stdio-1.0
|
type: exitcode-stdio-1.0
|
||||||
main-is: RPG-Engine-Test.hs
|
main-is: Spec.hs
|
||||||
hs-source-dirs: test
|
hs-source-dirs: test
|
||||||
default-language: Haskell2010
|
default-language: Haskell2010
|
||||||
build-depends: base >=4.7 && <5, hspec <= 2.10.6, rpg-engine
|
build-depends:
|
||||||
|
base >=4.7 && <5,
|
||||||
|
rpg-engine,
|
||||||
|
hspec <= 2.10.6, hspec-discover,
|
||||||
|
parsec >= 3.1.15.1
|
||||||
|
other-modules:
|
||||||
|
Parser.GameSpec
|
||||||
|
Parser.StructureSpec
|
|
@ -1,4 +1,4 @@
|
||||||
import RPGEngine
|
import RPGEngine ( playRPGEngine )
|
||||||
|
|
||||||
----------------------------- Constants ------------------------------
|
----------------------------- Constants ------------------------------
|
||||||
|
|
||||||
|
|
|
@ -42,6 +42,7 @@ extra-deps:
|
||||||
#
|
#
|
||||||
# extra-deps: []
|
# extra-deps: []
|
||||||
- gloss-juicy-0.2.3@sha256:0c3bca95237cbf91f8b3b1936a0661f1e0457acd80502276d54d6c5210f88b25,1618
|
- gloss-juicy-0.2.3@sha256:0c3bca95237cbf91f8b3b1936a0661f1e0457acd80502276d54d6c5210f88b25,1618
|
||||||
|
- parsec-3.1.15.1@sha256:8c7a36aaadff12a38817fc3c4ff6c87e3352cffd1a58df640de7ed7a97ad8fa3,4601
|
||||||
|
|
||||||
# Override default flag values for local packages and extra-deps
|
# Override default flag values for local packages and extra-deps
|
||||||
# flags: {}
|
# flags: {}
|
||||||
|
@ -66,3 +67,5 @@ extra-deps:
|
||||||
#
|
#
|
||||||
# Allow a newer minor version of GHC than the snapshot specifies
|
# Allow a newer minor version of GHC than the snapshot specifies
|
||||||
# compiler-check: newer-minor
|
# compiler-check: newer-minor
|
||||||
|
|
||||||
|
custom-preprocessor-extensions: []
|
187
test/Parser/GameSpec.hs
Normal file
|
@ -0,0 +1,187 @@
|
||||||
|
module Parser.GameSpec where
|
||||||
|
|
||||||
|
import Test.Hspec
|
||||||
|
|
||||||
|
import RPGEngine.Data
|
||||||
|
import RPGEngine.Parse.Core
|
||||||
|
import RPGEngine.Parse.TextToStructure
|
||||||
|
import RPGEngine.Parse.StructureToGame
|
||||||
|
|
||||||
|
spec :: Spec
|
||||||
|
spec = do
|
||||||
|
describe "Game" $ do
|
||||||
|
it "Simple game" $ do
|
||||||
|
pendingWith "There is a weird bug that caused this to go in an infinite loop. Fix later."
|
||||||
|
let input = "player: {\n hp: 50,\n inventory: []\n}\n\nlevels: [\n {\n layout: {\n | * * * * * *\n | * s . . e *\n | * * * * * *\n },\n \n items: [],\n\n entities: []\n\n\n }\n]"
|
||||||
|
correct = Game {
|
||||||
|
state = Playing {
|
||||||
|
levels = [],
|
||||||
|
count = 0,
|
||||||
|
level = Level {
|
||||||
|
RPGEngine.Data.layout = [],
|
||||||
|
index = [],
|
||||||
|
items = [],
|
||||||
|
entities = []
|
||||||
|
},
|
||||||
|
player = Player {
|
||||||
|
playerHp = Just 50,
|
||||||
|
inventory = [],
|
||||||
|
position = (0, 0),
|
||||||
|
showHp = True,
|
||||||
|
showInventory = False
|
||||||
|
},
|
||||||
|
restart = Menu
|
||||||
|
}
|
||||||
|
}
|
||||||
|
(Right struct) = parseWith gameFile input
|
||||||
|
structureToGame struct `shouldBe` correct
|
||||||
|
it "More complex game" $ do
|
||||||
|
pendingWith "Still need to write this"
|
||||||
|
it "Game with multiple levels" $ do
|
||||||
|
pendingWith "Still need to write this"
|
||||||
|
|
||||||
|
describe "Player" $ do
|
||||||
|
it "cannot die" $ do
|
||||||
|
let input = "player: { hp: infinite, inventory: [] }"
|
||||||
|
correct = Player {
|
||||||
|
playerHp = Prelude.Nothing,
|
||||||
|
inventory = [],
|
||||||
|
position = (0, 0),
|
||||||
|
showHp = True,
|
||||||
|
showInventory = False
|
||||||
|
}
|
||||||
|
Right (Entry (Tag "player") struct) = parseWith structure input
|
||||||
|
structureToPlayer struct `shouldBe` correct
|
||||||
|
|
||||||
|
it "without inventory" $ do
|
||||||
|
let input = "player: { hp: 50, inventory: [] }"
|
||||||
|
correct = Player {
|
||||||
|
playerHp = Just 50,
|
||||||
|
inventory = [],
|
||||||
|
position = (0, 0),
|
||||||
|
showHp = True,
|
||||||
|
showInventory = False
|
||||||
|
}
|
||||||
|
Right (Entry (Tag "player") struct) = parseWith structure input
|
||||||
|
structureToPlayer struct `shouldBe` correct
|
||||||
|
|
||||||
|
it "with inventory" $ do
|
||||||
|
let input = "player: { hp: 50, inventory: [ { id: \"dagger\", x: 0, y: 0, name: \"Dolk\", description: \"Basis schade tegen monsters\", useTimes: infinite, value: 10, actions: {} } ] }"
|
||||||
|
correct = Player {
|
||||||
|
playerHp = Just 50,
|
||||||
|
inventory = [
|
||||||
|
Item {
|
||||||
|
itemId = "dagger",
|
||||||
|
itemX = 0,
|
||||||
|
itemY = 0,
|
||||||
|
itemName = "Dolk",
|
||||||
|
itemDescription = "Basis schade tegen monsters",
|
||||||
|
itemActions = [],
|
||||||
|
itemValue = Just 10,
|
||||||
|
useTimes = Prelude.Nothing
|
||||||
|
}
|
||||||
|
],
|
||||||
|
position = (0, 0),
|
||||||
|
showHp = True,
|
||||||
|
showInventory = False
|
||||||
|
}
|
||||||
|
Right (Entry (Tag "player") struct) = parseWith structure input
|
||||||
|
structureToPlayer struct `shouldBe` correct
|
||||||
|
|
||||||
|
describe "Items" $ do
|
||||||
|
it "simple" $ do
|
||||||
|
let input = "{ id: \"dagger\", x: 0, y: 0, name: \"Dagger\", description: \"Basic dagger you found somewhere\", useTimes: infinite, value: 10, actions: {} }"
|
||||||
|
correct = Item {
|
||||||
|
itemId = "dagger",
|
||||||
|
itemX = 0,
|
||||||
|
itemY = 0,
|
||||||
|
itemName = "Dagger",
|
||||||
|
itemDescription = "Basic dagger you found somewhere",
|
||||||
|
itemValue = Just 10,
|
||||||
|
itemActions = [],
|
||||||
|
useTimes = Prelude.Nothing
|
||||||
|
}
|
||||||
|
Right struct = parseWith structure input
|
||||||
|
structureToItem struct `shouldBe` correct
|
||||||
|
|
||||||
|
it "with actions" $ do
|
||||||
|
let input = "{ id: \"key\", x: 3, y: 1, name: \"Doorkey\", description: \"Unlocks a secret door\", useTimes: 1, value: 0, actions: { [not(inventoryFull())] retrieveItem(key), [] leave() } }"
|
||||||
|
correct = Item {
|
||||||
|
itemId = "key",
|
||||||
|
itemX = 3,
|
||||||
|
itemY = 1,
|
||||||
|
itemName = "Doorkey",
|
||||||
|
itemDescription = "Unlocks a secret door",
|
||||||
|
itemActions = [
|
||||||
|
([], Leave),
|
||||||
|
([Not InventoryFull], RetrieveItem "key")
|
||||||
|
],
|
||||||
|
itemValue = Just 0,
|
||||||
|
useTimes = Just 1
|
||||||
|
}
|
||||||
|
Right struct = parseWith structure input
|
||||||
|
structureToItem struct `shouldBe` correct
|
||||||
|
|
||||||
|
describe "Actions" $ do
|
||||||
|
it "no conditions" $ do
|
||||||
|
let input = "{[] leave()}"
|
||||||
|
correct = [([], Leave)]
|
||||||
|
Right struct = parseWith structure input
|
||||||
|
structureToActions struct `shouldBe` correct
|
||||||
|
|
||||||
|
it "single condition" $ do
|
||||||
|
let input = "{ [inventoryFull()] useItem(itemId)}"
|
||||||
|
correct = [([InventoryFull], UseItem "itemId")]
|
||||||
|
Right struct = parseWith structure input
|
||||||
|
structureToActions struct `shouldBe` correct
|
||||||
|
|
||||||
|
it "multiple conditions" $ do
|
||||||
|
let input = "{ [not(inventoryFull()), inventoryContains(itemId)] increasePlayerHp(itemId)}"
|
||||||
|
correct = [([Not InventoryFull, InventoryContains "itemId"], IncreasePlayerHp "itemId")]
|
||||||
|
Right struct = parseWith structure input
|
||||||
|
structureToActions struct `shouldBe` correct
|
||||||
|
|
||||||
|
it "DecreaseHp(entityid, itemid)" $ do
|
||||||
|
let input = "{ [] decreaseHp(devil, sword) }"
|
||||||
|
correct = [([], DecreaseHp "devil" "sword")]
|
||||||
|
Right struct = parseWith structure input
|
||||||
|
structureToActions struct `shouldBe` correct
|
||||||
|
|
||||||
|
describe "Entities" $ do
|
||||||
|
it "Simple entity" $ do
|
||||||
|
pendingWith "still need to write this"
|
||||||
|
|
||||||
|
describe "Level" $ do
|
||||||
|
it "Simple layout" $ do
|
||||||
|
let input = "{ layout: { | * * * * * *\n| * s . . e *\n| * * * * * *\n }, items: [], entities: [] }"
|
||||||
|
correct = Level {
|
||||||
|
RPGEngine.Data.layout = [
|
||||||
|
[Blocked, Blocked, Blocked, Blocked, Blocked, Blocked],
|
||||||
|
[Blocked, Entrance, Walkable, Walkable, Exit, Blocked],
|
||||||
|
[Blocked, Blocked, Blocked, Blocked, Blocked, Blocked]
|
||||||
|
],
|
||||||
|
index = [
|
||||||
|
(0, 0, Blocked),
|
||||||
|
(1, 0, Blocked),
|
||||||
|
(2, 0, Blocked),
|
||||||
|
(3, 0, Blocked),
|
||||||
|
(4, 0, Blocked),
|
||||||
|
(5, 0, Blocked),
|
||||||
|
(0, 1, Blocked),
|
||||||
|
(1, 1, Entrance),
|
||||||
|
(2, 1, Walkable),
|
||||||
|
(3, 1, Walkable),
|
||||||
|
(4, 1, Exit),
|
||||||
|
(5, 1, Blocked),
|
||||||
|
(0, 2, Blocked),
|
||||||
|
(1, 2, Blocked),
|
||||||
|
(2, 2, Blocked),
|
||||||
|
(3, 2, Blocked),
|
||||||
|
(4, 2, Blocked),
|
||||||
|
(5, 2, Blocked)
|
||||||
|
],
|
||||||
|
items = [],
|
||||||
|
entities = []
|
||||||
|
}
|
||||||
|
Right struct = parseWith structure input
|
||||||
|
structureToLevel struct `shouldBe` correct
|
379
test/Parser/StructureSpec.hs
Normal file
|
@ -0,0 +1,379 @@
|
||||||
|
module Parser.StructureSpec where
|
||||||
|
|
||||||
|
import Test.Hspec
|
||||||
|
|
||||||
|
import RPGEngine.Data
|
||||||
|
import RPGEngine.Parse.Core
|
||||||
|
import RPGEngine.Parse.TextToStructure
|
||||||
|
import Text.Parsec.String (parseFromFile)
|
||||||
|
import GHC.IO (unsafePerformIO)
|
||||||
|
|
||||||
|
spec :: Spec
|
||||||
|
spec = do
|
||||||
|
describe "StructureElement" $ do
|
||||||
|
it "can parse blocks" $ do
|
||||||
|
let input = "{}"
|
||||||
|
correct = Right $ Block []
|
||||||
|
parseWith structure input `shouldBe` correct
|
||||||
|
|
||||||
|
let input = "{{}}"
|
||||||
|
correct = Right $ Block [Block []]
|
||||||
|
parseWith structure input `shouldBe` correct
|
||||||
|
|
||||||
|
let input = "{{}, {}}"
|
||||||
|
correct = Right $ Block [Block [], Block []]
|
||||||
|
parseWith structure input `shouldBe` correct
|
||||||
|
|
||||||
|
let input = "{ id: 1 }"
|
||||||
|
correct = Right (Block [
|
||||||
|
Entry (Tag "id") $ Regular $ Integer 1
|
||||||
|
], "")
|
||||||
|
parseWithRest structure input `shouldBe` correct
|
||||||
|
|
||||||
|
let input = "{ id: \"key\", x: 3, y: 1}"
|
||||||
|
correct = Right $ Block [
|
||||||
|
Entry (Tag "id") $ Regular $ String "key",
|
||||||
|
Entry (Tag "x") $ Regular $ Integer 3,
|
||||||
|
Entry (Tag "y") $ Regular $ Integer 1
|
||||||
|
]
|
||||||
|
parseWith structure input `shouldBe` correct
|
||||||
|
|
||||||
|
let input = "actions: { [not(inventoryFull())] retrieveItem(key), [] leave()}"
|
||||||
|
correct = Right (Entry (Tag "actions") $ Block [
|
||||||
|
Entry (ConditionList [Not InventoryFull]) $ Regular $ Action $ RetrieveItem "key",
|
||||||
|
Entry (ConditionList []) $ Regular $ Action Leave
|
||||||
|
], "")
|
||||||
|
parseWithRest structure input `shouldBe` correct
|
||||||
|
|
||||||
|
let input = "entities: [ { id: \"door\", x: 4, name:\"Secret door\", description: \"This secret door can only be opened with a key\", direction: left, y: 1}]"
|
||||||
|
correct = Right (Entry (Tag "entities") $ Block [ Block [
|
||||||
|
Entry (Tag "id") $ Regular $ String "door",
|
||||||
|
Entry (Tag "x") $ Regular $ Integer 4,
|
||||||
|
Entry (Tag "name") $ Regular $ String "Secret door",
|
||||||
|
Entry (Tag "description") $ Regular $ String "This secret door can only be opened with a key",
|
||||||
|
Entry (Tag "direction") $ Regular $ Direction West,
|
||||||
|
Entry (Tag "y") $ Regular $ Integer 1
|
||||||
|
]], "")
|
||||||
|
parseWithRest structure input `shouldBe` correct
|
||||||
|
|
||||||
|
let input = "entities: [ { id: \"door\", x: 4, y: 1, name:\"Secret door\", description: \"This secret door can only be opened with a key\", actions: { [inventoryContains(key)] useItem(key), [] leave() } } ]"
|
||||||
|
correct = Right (Entry (Tag "entities") $ Block [ Block [
|
||||||
|
Entry (Tag "id") $ Regular $ String "door",
|
||||||
|
Entry (Tag "x") $ Regular $ Integer 4,
|
||||||
|
Entry (Tag "y") $ Regular $ Integer 1,
|
||||||
|
Entry (Tag "name") $ Regular $ String "Secret door",
|
||||||
|
Entry (Tag "description") $ Regular $ String "This secret door can only be opened with a key",
|
||||||
|
Entry (Tag "actions") $ Block [
|
||||||
|
Entry (ConditionList [InventoryContains "key"]) $ Regular $ Action $ UseItem "key",
|
||||||
|
Entry (ConditionList []) $ Regular $ Action Leave
|
||||||
|
]
|
||||||
|
]], "")
|
||||||
|
parseWithRest structure input `shouldBe` correct
|
||||||
|
|
||||||
|
let input = "entities: [ { id: \"door\", x: 4, y: 1, name:\"Secret door\", description: \"This secret door can only be opened with a key\", direction: left, actions: { [inventoryContains(key)] useItem(key), [] leave() } } ]"
|
||||||
|
correct = Right (Entry (Tag "entities") $ Block [ Block [
|
||||||
|
Entry (Tag "id") $ Regular $ String "door",
|
||||||
|
Entry (Tag "x") $ Regular $ Integer 4,
|
||||||
|
Entry (Tag "y") $ Regular $ Integer 1,
|
||||||
|
Entry (Tag "name") $ Regular $ String "Secret door",
|
||||||
|
Entry (Tag "description") $ Regular $ String "This secret door can only be opened with a key",
|
||||||
|
Entry (Tag "direction") $ Regular $ Direction West,
|
||||||
|
Entry (Tag "actions") $ Block [
|
||||||
|
Entry (ConditionList [InventoryContains "key"]) $ Regular $ Action $ UseItem "key",
|
||||||
|
Entry (ConditionList []) $ Regular $ Action Leave
|
||||||
|
]
|
||||||
|
]], "")
|
||||||
|
parseWithRest structure input `shouldBe` correct
|
||||||
|
|
||||||
|
it "combines actions and direction" $ do
|
||||||
|
let input = "entities: [ { direction: left, actions: { [inventoryContains(key)] useItem(key), [] leave() } } ]"
|
||||||
|
correct = Right (Entry (Tag "entities") $ Block [ Block [
|
||||||
|
Entry (Tag "direction") $ Regular $ Direction West,
|
||||||
|
Entry (Tag "actions") $ Block [
|
||||||
|
Entry (ConditionList [InventoryContains "key"]) $ Regular $ Action $ UseItem "key",
|
||||||
|
Entry (ConditionList []) $ Regular $ Action Leave
|
||||||
|
]
|
||||||
|
]], "")
|
||||||
|
parseWithRest structure input `shouldBe` correct
|
||||||
|
|
||||||
|
it "can parse entries" $ do
|
||||||
|
let input = "id: \"dagger\""
|
||||||
|
correct = Right $ Entry (Tag "id") $ Regular $ String "dagger"
|
||||||
|
parseWith entry input `shouldBe` correct
|
||||||
|
|
||||||
|
let input = "x: 0"
|
||||||
|
correct = Right $ Entry (Tag "x") $ Regular $ Integer 0
|
||||||
|
parseWith entry input `shouldBe` correct
|
||||||
|
|
||||||
|
let input = "useTimes: infinite"
|
||||||
|
correct = Right $ Entry (Tag "useTimes") $ Regular Infinite
|
||||||
|
parseWith entry input `shouldBe` correct
|
||||||
|
|
||||||
|
let input = "direction: up"
|
||||||
|
correct = Right $ Entry (Tag "direction") $ Regular $ Direction North
|
||||||
|
parseWith entry input `shouldBe` correct
|
||||||
|
|
||||||
|
let input = "actions: { [not(inventoryFull())] retrieveItem(key), [] leave()}"
|
||||||
|
correct = Right (Entry (Tag "actions") $ Block [
|
||||||
|
Entry (ConditionList [Not InventoryFull]) $ Regular $ Action $ RetrieveItem "key",
|
||||||
|
Entry (ConditionList []) $ Regular $ Action Leave
|
||||||
|
], "")
|
||||||
|
parseWithRest structure input `shouldBe` correct
|
||||||
|
|
||||||
|
it "can parse regulars" $ do
|
||||||
|
let input = "this is a string"
|
||||||
|
correct = Right $ Regular $ String input
|
||||||
|
parseWith regular ("\"" ++ input ++ "\"") `shouldBe` correct
|
||||||
|
|
||||||
|
let correct = Right $ Regular $ Integer 1
|
||||||
|
parseWith regular "1" `shouldBe` correct
|
||||||
|
|
||||||
|
let correct = Right $ Regular Infinite
|
||||||
|
parseWith regular "infinite" `shouldBe` correct
|
||||||
|
|
||||||
|
let wrong = Right $ Regular Infinite
|
||||||
|
parseWith regular "infinitee" `shouldNotBe` wrong
|
||||||
|
|
||||||
|
let input = "leave()"
|
||||||
|
correct = Right $ Regular $ Action Leave
|
||||||
|
parseWith regular input `shouldBe` correct
|
||||||
|
|
||||||
|
let input = "retrieveItem(firstId)"
|
||||||
|
correct = Right $ Regular $ Action $ RetrieveItem "firstId"
|
||||||
|
parseWith regular input `shouldBe` correct
|
||||||
|
|
||||||
|
let input = "useItem(secondId)"
|
||||||
|
correct = Right $ Regular $ Action $ UseItem "secondId"
|
||||||
|
parseWith regular input `shouldBe` correct
|
||||||
|
|
||||||
|
let input = "decreaseHp(entityId, objectId)"
|
||||||
|
correct = Right $ Regular $ Action $ DecreaseHp "entityId" "objectId"
|
||||||
|
parseWith regular input `shouldBe` correct
|
||||||
|
|
||||||
|
let input = "decreaseHp(entityId,objectId)"
|
||||||
|
correct = Right $ Regular $ Action $ DecreaseHp "entityId" "objectId"
|
||||||
|
parseWith regular input `shouldBe` correct
|
||||||
|
|
||||||
|
let input = "increasePlayerHp(objectId)"
|
||||||
|
correct = Right $ Regular $ Action $ IncreasePlayerHp "objectId"
|
||||||
|
parseWith regular input `shouldBe` correct
|
||||||
|
|
||||||
|
let input = "up"
|
||||||
|
correct = Right $ Regular $ Direction North
|
||||||
|
parseWith regular input `shouldBe` correct
|
||||||
|
|
||||||
|
let input = "right"
|
||||||
|
correct = Right $ Regular $ Direction East
|
||||||
|
parseWith regular input `shouldBe` correct
|
||||||
|
|
||||||
|
let input = "down"
|
||||||
|
correct = Right $ Regular $ Direction South
|
||||||
|
parseWith regular input `shouldBe` correct
|
||||||
|
|
||||||
|
let input = "left"
|
||||||
|
correct = Right $ Regular $ Direction West
|
||||||
|
parseWith regular input `shouldBe` correct
|
||||||
|
|
||||||
|
describe "Key" $ do
|
||||||
|
it "can parse tags" $ do
|
||||||
|
let input = "simpletag"
|
||||||
|
correct = Right $ Tag "simpletag"
|
||||||
|
parseWith tag input `shouldBe` correct
|
||||||
|
|
||||||
|
it "can parse conditionlists" $ do
|
||||||
|
let input = "[not(inventoryFull())]"
|
||||||
|
correct = Right (ConditionList [Not InventoryFull], "")
|
||||||
|
parseWithRest conditionList input `shouldBe` correct
|
||||||
|
|
||||||
|
let input = "[inventoryFull(), inventoryContains(itemId)]"
|
||||||
|
correct = Right (ConditionList [
|
||||||
|
InventoryFull,
|
||||||
|
InventoryContains "itemId"
|
||||||
|
], "")
|
||||||
|
parseWithRest conditionList input `shouldBe` correct
|
||||||
|
|
||||||
|
let input = "[]"
|
||||||
|
correct = Right $ ConditionList []
|
||||||
|
parseWith conditionList input `shouldBe` correct
|
||||||
|
|
||||||
|
it "can parse conditions" $ do
|
||||||
|
let input = "inventoryFull()"
|
||||||
|
correct = Right (Condition InventoryFull, "")
|
||||||
|
parseWithRest condition input `shouldBe` correct
|
||||||
|
|
||||||
|
let input = "inventoryContains(itemId)"
|
||||||
|
correct = Right (Condition $ InventoryContains "itemId", "")
|
||||||
|
parseWithRest condition input `shouldBe` correct
|
||||||
|
|
||||||
|
let input = "not(inventoryFull())"
|
||||||
|
correct = Right (Condition $ Not InventoryFull, "")
|
||||||
|
parseWithRest condition input `shouldBe` correct
|
||||||
|
|
||||||
|
let input = "not(inventoryContains(itemId))"
|
||||||
|
correct = Right (Condition $ Not $ InventoryContains "itemId", "")
|
||||||
|
parseWithRest condition input `shouldBe` correct
|
||||||
|
|
||||||
|
describe "Value" $ do
|
||||||
|
it "can parse strings" $ do
|
||||||
|
let input = "dit is een string"
|
||||||
|
correct = Right $ String input
|
||||||
|
parseWith string ("\"" ++ input ++ "\"") `shouldBe` correct
|
||||||
|
|
||||||
|
it "can parse integers" $ do
|
||||||
|
let correct = Right $ Integer 1
|
||||||
|
parseWith integer "1" `shouldBe` correct
|
||||||
|
|
||||||
|
it "can parse infinite" $ do
|
||||||
|
let correct = Right Infinite
|
||||||
|
parseWith infinite "infinite" `shouldBe` correct
|
||||||
|
|
||||||
|
let wrong = Right Infinite
|
||||||
|
parseWith infinite "infinitee" `shouldNotBe` wrong
|
||||||
|
|
||||||
|
it "can parse actions" $ do
|
||||||
|
let input = "leave()"
|
||||||
|
correct = Right $ Action Leave
|
||||||
|
parseWith action input `shouldBe` correct
|
||||||
|
|
||||||
|
let input = "retrieveItem(firstId)"
|
||||||
|
correct = Right $ Action $ RetrieveItem "firstId"
|
||||||
|
parseWith action input `shouldBe` correct
|
||||||
|
|
||||||
|
let input = "useItem(secondId)"
|
||||||
|
correct = Right $ Action $ UseItem "secondId"
|
||||||
|
parseWith action input `shouldBe` correct
|
||||||
|
|
||||||
|
let input = "decreaseHp(entityId,objectId)"
|
||||||
|
correct = Right $ Action $ DecreaseHp "entityId" "objectId"
|
||||||
|
parseWith action input `shouldBe` correct
|
||||||
|
|
||||||
|
let input = "increasePlayerHp(objectId)"
|
||||||
|
correct = Right $ Action $ IncreasePlayerHp "objectId"
|
||||||
|
parseWith action input `shouldBe` correct
|
||||||
|
|
||||||
|
it "can parse directions" $ do
|
||||||
|
let input = "up"
|
||||||
|
correct = Right $ Direction North
|
||||||
|
parseWith RPGEngine.Parse.TextToStructure.direction input `shouldBe` correct
|
||||||
|
|
||||||
|
let input = "right"
|
||||||
|
correct = Right $ Direction East
|
||||||
|
parseWith RPGEngine.Parse.TextToStructure.direction input `shouldBe` correct
|
||||||
|
|
||||||
|
let input = "down"
|
||||||
|
correct = Right $ Direction South
|
||||||
|
parseWith RPGEngine.Parse.TextToStructure.direction input `shouldBe` correct
|
||||||
|
|
||||||
|
let input = "left"
|
||||||
|
correct = Right $ Direction West
|
||||||
|
parseWith RPGEngine.Parse.TextToStructure.direction input `shouldBe` correct
|
||||||
|
|
||||||
|
it "can parse layouts" $ do
|
||||||
|
let input = "{ | * * * * * * * *\n | * s . . . . e *\n | * * * * * * * *\n }"
|
||||||
|
correct = Right $ Layout [
|
||||||
|
[Blocked, Blocked, Blocked, Blocked, Blocked, Blocked, Blocked, Blocked],
|
||||||
|
[Blocked, Entrance, Walkable, Walkable, Walkable, Walkable, Exit, Blocked],
|
||||||
|
[Blocked, Blocked, Blocked, Blocked, Blocked, Blocked, Blocked, Blocked]
|
||||||
|
]
|
||||||
|
parseWith value input `shouldBe` correct
|
||||||
|
|
||||||
|
let input = "layout: { | * * * * * * * *\n | * s . . . . e *\n | * * * * * * * *\n }"
|
||||||
|
correct = Right $ Entry (Tag "layout") $ Regular $ Layout [
|
||||||
|
[Blocked, Blocked, Blocked, Blocked, Blocked, Blocked, Blocked, Blocked],
|
||||||
|
[Blocked, Entrance, Walkable, Walkable, Walkable, Walkable, Exit, Blocked],
|
||||||
|
[Blocked, Blocked, Blocked, Blocked, Blocked, Blocked, Blocked, Blocked]
|
||||||
|
]
|
||||||
|
parseWith structure input `shouldBe` correct
|
||||||
|
|
||||||
|
describe "Brackets" $ do
|
||||||
|
it "matches closing <" $ do
|
||||||
|
let input = '<'
|
||||||
|
correct = '>'
|
||||||
|
getMatchingClosingBracket input `shouldBe` correct
|
||||||
|
|
||||||
|
it "matches closing (" $ do
|
||||||
|
let input = '('
|
||||||
|
correct = ')'
|
||||||
|
getMatchingClosingBracket input `shouldBe` correct
|
||||||
|
|
||||||
|
it "matches closing {" $ do
|
||||||
|
let input = '{'
|
||||||
|
correct = '}'
|
||||||
|
getMatchingClosingBracket input `shouldBe` correct
|
||||||
|
|
||||||
|
it "matches closing [" $ do
|
||||||
|
let input = '['
|
||||||
|
correct = ']'
|
||||||
|
getMatchingClosingBracket input `shouldBe` correct
|
||||||
|
|
||||||
|
describe "Full game file" $ do
|
||||||
|
it "single level" $ do
|
||||||
|
let input = "player: {\n hp: 50,\n inventory: []\n}\n\nlevels: [\n {\n layout: {\n | * * * * * *\n | * s . . e *\n | * * * * * *\n },\n \n items: [],\n\n entities: []\n\n\n }\n]"
|
||||||
|
correct = Right [
|
||||||
|
Entry (Tag "player") $ Block [
|
||||||
|
Entry (Tag "hp") $ Regular $ Integer 50,
|
||||||
|
Entry (Tag "inventory") $ Block []
|
||||||
|
],
|
||||||
|
Entry (Tag "levels") $ Block [ Block [
|
||||||
|
Entry (Tag "layout") $ Regular $ Layout [
|
||||||
|
[Blocked, Blocked, Blocked, Blocked, Blocked, Blocked],
|
||||||
|
[Blocked, Entrance, Walkable, Walkable, Exit, Blocked],
|
||||||
|
[Blocked, Blocked, Blocked, Blocked, Blocked, Blocked]
|
||||||
|
],
|
||||||
|
Entry (Tag "items") $ Block [],
|
||||||
|
Entry (Tag "entities") $ Block []
|
||||||
|
]]
|
||||||
|
]
|
||||||
|
parseWith gameFile input `shouldBe` correct
|
||||||
|
|
||||||
|
it "two levels" $ do
|
||||||
|
let input = "player: {\n hp: 50,\n inventory: []\n}\n\nlevels: [\n {\n layout: {\n | * * * * * *\n | * s . . e *\n | * * * * * *\n },\n \n items: [],\n\n entities: []\n },\n {\n layout: {\n | * * *\n | * e *\n | * . *\n | * . *\n | * . *\n | * . *\n | * s *\n | * * *\n },\n\n items: [],\n\n entities: []\n }\n]"
|
||||||
|
correct = Right [
|
||||||
|
Entry (Tag "player") $ Block [
|
||||||
|
Entry (Tag "hp") $ Regular $ Integer 50,
|
||||||
|
Entry (Tag "inventory") $ Block []
|
||||||
|
],
|
||||||
|
Entry (Tag "levels") $ Block [
|
||||||
|
Block [
|
||||||
|
Entry (Tag "layout") $ Regular $ Layout [
|
||||||
|
[Blocked, Blocked, Blocked, Blocked, Blocked, Blocked],
|
||||||
|
[Blocked, Entrance, Walkable, Walkable, Exit, Blocked],
|
||||||
|
[Blocked, Blocked, Blocked, Blocked, Blocked, Blocked]
|
||||||
|
],
|
||||||
|
Entry (Tag "items") $ Block [],
|
||||||
|
Entry (Tag "entities") $ Block []
|
||||||
|
], Block [
|
||||||
|
Entry (Tag "layout") $ Regular $ Layout [
|
||||||
|
[Blocked,Blocked,Blocked],
|
||||||
|
[Blocked,Exit,Blocked],
|
||||||
|
[Blocked,Walkable,Blocked],
|
||||||
|
[Blocked,Walkable,Blocked],
|
||||||
|
[Blocked,Walkable,Blocked],
|
||||||
|
[Blocked,Walkable,Blocked],
|
||||||
|
[Blocked,Entrance,Blocked],
|
||||||
|
[Blocked,Blocked,Blocked]
|
||||||
|
],
|
||||||
|
Entry (Tag "items") $ Block [],
|
||||||
|
Entry (Tag "entities") $ Block []
|
||||||
|
]
|
||||||
|
]
|
||||||
|
]
|
||||||
|
parseWith gameFile input `shouldBe` correct
|
||||||
|
|
||||||
|
it "from file" $ do
|
||||||
|
let correct = Right [
|
||||||
|
Entry (Tag "player") $ Block [
|
||||||
|
Entry (Tag "hp") $ Regular $ Integer 50,
|
||||||
|
Entry (Tag "inventory") $ Block []
|
||||||
|
],
|
||||||
|
Entry (Tag "levels") $ Block [ Block [
|
||||||
|
Entry (Tag "layout") $ Regular $ Layout [
|
||||||
|
[Blocked, Blocked, Blocked, Blocked, Blocked, Blocked],
|
||||||
|
[Blocked, Entrance, Walkable, Walkable, Exit, Blocked],
|
||||||
|
[Blocked, Blocked, Blocked, Blocked, Blocked, Blocked]
|
||||||
|
],
|
||||||
|
Entry (Tag "items") $ Block [],
|
||||||
|
Entry (Tag "entities") $ Block []
|
||||||
|
]]
|
||||||
|
]
|
||||||
|
unsafePerformIO (parseFromFile gameFile "levels/level1.txt") `shouldBe` correct
|
|
@ -1,7 +0,0 @@
|
||||||
import Test.Hspec
|
|
||||||
|
|
||||||
main :: IO()
|
|
||||||
main = hspec $ do
|
|
||||||
describe "Dummy category" $ do
|
|
||||||
it "Dummy test" $ do
|
|
||||||
0 `shouldBe` 0
|
|
18
test/Spec.hs
Normal file
|
@ -0,0 +1,18 @@
|
||||||
|
{-# OPTIONS_GHC -F -pgmF hspec-discover #-}
|
||||||
|
|
||||||
|
-------------------------- How to use Hspec --------------------------
|
||||||
|
|
||||||
|
-- If a test has not yet been written:
|
||||||
|
-- Use `pending` or `pendingWith`.
|
||||||
|
-- it "Description" $ do
|
||||||
|
-- pendingWith "Reason"
|
||||||
|
|
||||||
|
-- Temporarily disable running a test:
|
||||||
|
-- Replace `it` with `xit`
|
||||||
|
-- xit "Description" $ do ...
|
||||||
|
|
||||||
|
-- Temporarily only run a specific test:
|
||||||
|
-- Put `focus` in front.
|
||||||
|
-- it "Description" $ do ...
|
||||||
|
-- becomes
|
||||||
|
-- focus $ it "Description" $ do ...
|