From 0257bb82205db7273a5ba69c41eec0ae3589e9af Mon Sep 17 00:00:00 2001 From: Tibo De Peuter Date: Thu, 15 Dec 2022 10:22:30 +0100 Subject: [PATCH 01/27] #15 Input handling made modular --- lib/control/Input.hs | 8 ++++++-- lib/control/InputHandling.hs | 30 ++++++++++++++++++++++-------- 2 files changed, 28 insertions(+), 10 deletions(-) diff --git a/lib/control/Input.hs b/lib/control/Input.hs index e13b523..30c6a6b 100644 --- a/lib/control/Input.hs +++ b/lib/control/Input.hs @@ -13,8 +13,12 @@ import Graphics.Gloss.Interface.IO.Game ---------------------------------------------------------------------- handleAllInput :: InputHandler Game -handleAllInput = composeInputHandlers [ - handleSpecialKey KeySpace setNextState +handleAllInput ev g@Game{ state = Playing } = handlePlayInputs ev g +handleAllInput ev g = handleAnyKey setNextState ev g + +handlePlayInputs :: InputHandler Game +handlePlayInputs = composeInputHandlers [ + handleKey (Char 'p') (\game -> game{ state = Pause }) ] -- Go to the next stage of the Game diff --git a/lib/control/InputHandling.hs b/lib/control/InputHandling.hs index 86704e4..1b4db4a 100644 --- a/lib/control/InputHandling.hs +++ b/lib/control/InputHandling.hs @@ -7,8 +7,12 @@ module InputHandling -- all of them. composeInputHandlers, -handle, -handleSpecialKey +-- Handle any event +handle, +-- Handle a event by pressing a key +handleKey, +-- Handle any key, equivalent to "Press any key to start" +handleAnyKey ) where import Graphics.Gloss.Interface.IO.Game @@ -23,19 +27,29 @@ 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 :: Event -> (a -> a) -> InputHandler 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)) +handleKey :: Key -> (a -> a) -> InputHandler a +handleKey (SpecialKey sk) = handleSpecialKey sk +handleKey (Char c ) = handleCharKey c +handleKey (MouseButton _ ) = (\_ -> (\_ -> id)) -handleSpecialKey :: SpecialKey -> (a -> a) -> Event -> (a -> a) +handleCharKey :: Char -> (a -> a) -> InputHandler a +handleCharKey c1 f (EventKey (Char c2) Down _ _) + | c1 == c2 = f + | otherwise = id +handleCharKey _ _ _ = id + +handleSpecialKey :: SpecialKey -> (a -> a) -> InputHandler a handleSpecialKey sk1 f (EventKey (SpecialKey sk2) Down _ _) | sk1 == sk2 = f | otherwise = id handleSpecialKey _ _ _ = id + +handleAnyKey :: (a -> a) -> InputHandler a +handleAnyKey f (EventKey _ Down _ _) = f +handleAnyKey _ _ = id From 4c1f25e49db049f6db15c9879baa4cae8278ea73 Mon Sep 17 00:00:00 2001 From: Tibo De Peuter Date: Thu, 15 Dec 2022 18:08:33 +0100 Subject: [PATCH 02/27] #4 Added player and object data types --- lib/data/Internals.hs | 61 +++++++++++++++++++++++++++++++++++++++++++ lib/data/Player.hs | 15 +++++++++++ rpg-engine.cabal | 5 +++- 3 files changed, 80 insertions(+), 1 deletion(-) create mode 100644 lib/data/Internals.hs create mode 100644 lib/data/Player.hs diff --git a/lib/data/Internals.hs b/lib/data/Internals.hs new file mode 100644 index 0000000..68f1a8d --- /dev/null +++ b/lib/data/Internals.hs @@ -0,0 +1,61 @@ +-- Represents an item in the game. + +module Internals +( Action(..) +, Object(..) +) where + +----------------------------- Constants ------------------------------ + +data Object = + Item { -- All fields are required + -- Easy way to identify items + id :: String, + -- Horizontal coördinate in the level + x :: Int, + -- Vertical coördinate in the level + y :: Int, + name :: String, + -- Short description of the object + description :: String, + -- Counts how often the object can be used by the player. Either + -- infinite or a natural number + useTimes :: Maybe Int, + -- List of conditional actions when the player is standing on this object + actions :: [Action], + -- Interpretation depends on action with this object. + value :: Maybe Int + } + | Entity { + -- Required fields + -- Easy way to identify items + id :: String, + -- Horizontal coördinate in the level + x :: Int, + -- Vertical coördinate in the level + y :: Int, + name :: String, + -- Short description of the object + description :: String, + -- List of conditional actions when the player is standing on this object + actions :: [Action], + -- Optional fields + -- The direction of the item. e.g. a door has a direction. + direction :: Maybe Direction, + -- Some entities have health points. + hp :: Maybe Int, + -- Interpretation depends on action with this object. + value :: Maybe Int + } + +data Direction = North + | East + | South + | West + deriving (Show) + +type Action = ([Condition], Event) + +type Condition = Bool + +type Event = * \ No newline at end of file diff --git a/lib/data/Player.hs b/lib/data/Player.hs new file mode 100644 index 0000000..642a6c3 --- /dev/null +++ b/lib/data/Player.hs @@ -0,0 +1,15 @@ +-- Represents a player in the game. This player can move around, pick +-- up items and interact with the world. + +module Player +( Player(..) +) where + +import Internals + +----------------------------- Constants ------------------------------ + +data Player = Player { + hp :: Int, + inventory :: [Object] +} \ No newline at end of file diff --git a/rpg-engine.cabal b/rpg-engine.cabal index 28aea68..cf4a6f8 100644 --- a/rpg-engine.cabal +++ b/rpg-engine.cabal @@ -11,8 +11,11 @@ library gloss >= 1.11 && < 1.14, gloss-juicy >= 0.2.3 exposed-modules: RPGEngine, + -- Control Input, InputHandling, - Game, State, + -- Data + Game, Internals, Player, State, + -- Render Render executable rpg-engine From 83659e69b45b84ed19e038356f5d4330200d5459 Mon Sep 17 00:00:00 2001 From: Tibo De Peuter Date: Sat, 17 Dec 2022 23:14:04 +0100 Subject: [PATCH 03/27] #18 #14 Inital parser commit Added basic parser functionality & tests for these functionalites. Split tests in several files --- README.md | 249 ++++++++++++++++++++++++++++++++++++++- lib/control/Parse.hs | 132 +++++++++++++++++++++ rpg-engine.cabal | 13 +- stack.yaml | 1 + test/InteractionSpec.hs | 9 ++ test/ParsedToGameSpec.hs | 52 ++++++++ test/ParserSpec.hs | 52 ++++++++ test/RPG-Engine-Test.hs | 7 -- test/RPGEngineSpec.hs | 1 + 9 files changed, 504 insertions(+), 12 deletions(-) create mode 100644 lib/control/Parse.hs create mode 100644 test/InteractionSpec.hs create mode 100644 test/ParsedToGameSpec.hs create mode 100644 test/ParserSpec.hs delete mode 100644 test/RPG-Engine-Test.hs create mode 100644 test/RPGEngineSpec.hs diff --git a/README.md b/README.md index 5f92037..49869ed 100644 --- a/README.md +++ b/README.md @@ -1,3 +1,250 @@ # RPG-Engine -Schrijf een game-engine voor een rollenspel \ No newline at end of file +Schrijf een game-engine voor een rollenspel + +https://pixel-poem.itch.io/dungeon-assetpuck +https://kyrise.itch.io/kyrises-free-16x16-rpg-icon-pack + +# RPG Engine requirements + +## 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 + +- [ ] 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 enimies, open doors, ...). +- [ ] Player can go to the next level. + +## Not-functional requirements + +- [ ] Use Parsing. +- [ ] Use at least one (1) monad transformer. +- [ ] Write good and plenty of documentation.:w + +- [ ] Write tests (for example, using HSpec). + +--- + +# Plaats om dingen neer te jotten + +``` +Play <--- HandleInput + | + | + v +Level <--- LoadLevel <--- Parse + | + | + v +RenderLevel +``` + +- [ ] State paradigma gebruiken om van startscherm naar playscherm naar pause scherm naar endscherm te gaan + +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 +``` + +# RPG-Engine Documentation + +## Playing the game + +TODO + +- Input commands etc +- An example playthrough + +## Writing your own stages + +A stage description file, conventionally named `.txt` is a file with a JSON-like format. It is used to describe + everything inside a single stage of your game, including anything related to the player, the levels your game contains + 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 | + +We'll look at the following example to explain these concepts. + +```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: "Doorkey", + 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() + } + } + ] + } +] +``` + +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") + ... + Entry = Key ('actions') + empty Block + Entry = Key ('levels') + >BlockList< + length = 2 + >Block< + Entry = Key ('layout') + Layout + + Entry = Key ('items') + empty BlockList + Entry = Key ('entities') + empty BlockList + >Block< + Entry = Key ('layout') + Layout + + Entry = Key ('items') + >BlockList< + length = 1 + >Block< + Entry = Key ('id') + Value ("key") + ... + 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") + ... + Entry = Key ('actions') + >Block< + Entry = >ConditionList< + Action ('useItem(key)') + length = 1 + Condition ('inventoryContains(key)') + Entry = empty ConditionList + Action ('leave()') +``` \ No newline at end of file diff --git a/lib/control/Parse.hs b/lib/control/Parse.hs new file mode 100644 index 0000000..376f29b --- /dev/null +++ b/lib/control/Parse.hs @@ -0,0 +1,132 @@ +module Parse where + +-- TODO Maak wrapper module +-- TODO This module should not be used by anything except for wrapper module and tests + +import Game +import Player +import Text.Parsec +import Text.Parsec.Char +import Text.Parsec.String +import Data.List +import Data.Maybe +import Text.Parsec.Error (Message(UnExpect)) + +-- TODO parseFromFile gebruiken + +-- Parser type +-- type Parser = Parsec String () + +-- 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 "" + +ignoreWS :: Parser a -> Parser a +ignoreWS parser = spaces >> 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 + +parseToGame :: Game +parseToGame = undefined + +-- Info in between brackets, '(..)', '[..]', '{..}' or '<..>' +data Brackets a = Brackets a + deriving (Eq, Show) + +parseToPlayer :: Player +parseToPlayer = undefined + +-- any words separated by whitespace +parseWord :: Parser String +parseWord = do many alphaNum + +-- TODO Expand to allow different kinds of brackets, also see Brackets data type. +-- TODO Check if brackets match order. +-- TODO Allow nested brackets. +brackets :: Parser (Brackets String) +brackets = do + ignoreWS $ char '(' + e <- ignoreWS $ many1 alphaNum + ignoreWS $ char ')' + return $ Brackets e + +------------------------ + +data Value = String String + | Integer Int + | Infinite + deriving (Show, Eq) + +-- See documentation for more details, only a short description is +--provided here. +data StructureElement = Block [StructureElement] + | Entry String StructureElement-- Key + Value + | Regular Value -- Regular value, Integer or String or Infinite + | ConditionList [StructureElement] + -- TODO + | Condition -- inventoryFull() etc. + -- TODO + | Action -- leave(), useItem(objectId) etc. + deriving (Show, Eq) + +-- TODO Add ConditionList and Action +structureElement :: Parser StructureElement +structureElement = choice [block, regular] + +-- A Block is a list of Entry s +block :: Parser StructureElement +block = do + ignoreWS $ char '{' + list <- ignoreWS $ many1 entry + ignoreWS $ char '}' + return $ Block list + +entry :: Parser StructureElement +entry = do + key <- ignoreWS $ many1 alphaNum + ignoreWS $ char ':' + value <- ignoreWS structureElement -- TODO Is this the correct one to use? + return $ Entry key value + +regular :: Parser StructureElement +regular = do + value <- ignoreWS $ choice [integer, valueString, infinite] + return $ Regular value + +integer :: Parser Value +integer = do + value <- ignoreWS $ many1 digit + return $ Integer (read value :: Int) + +valueString :: Parser Value +valueString = do + ignoreWS $ char '"' + value <- ignoreWS $ many1 (noneOf ['"']) + ignoreWS $ char '"' + return $ String value + +infinite :: Parser Value +infinite = do + ignoreWS $ string "infinite" + notFollowedBy alphaNum + return Infinite + +conditionList :: Parser StructureElement +conditionList = do + ignoreWS $ char '[' + list <- ignoreWS $ many1 condition + ignoreWS $ char ']' + return $ ConditionList list + +-- TODO +condition :: Parser StructureElement +condition = undefined + +-- TODO YOU ARE HERE +action :: Parser StructureElement +action = undefined diff --git a/rpg-engine.cabal b/rpg-engine.cabal index cf4a6f8..3775de7 100644 --- a/rpg-engine.cabal +++ b/rpg-engine.cabal @@ -8,11 +8,12 @@ library hs-source-dirs: lib, lib/control, lib/data, lib/render build-depends: base >= 4.7 && <5, - gloss >= 1.11 && < 1.14, gloss-juicy >= 0.2.3 + gloss >= 1.11 && < 1.14, gloss-juicy >= 0.2.3, + parsec >= 3.1.15.1 exposed-modules: RPGEngine, -- Control - Input, InputHandling, + Input, InputHandling, Parse, -- Data Game, Internals, Player, State, -- Render @@ -26,7 +27,11 @@ executable rpg-engine test-suite rpg-engine-test type: exitcode-stdio-1.0 - main-is: RPG-Engine-Test.hs + main-is: RPGEngineSpec.hs hs-source-dirs: test default-language: Haskell2010 - build-depends: base >=4.7 && <5, hspec <= 2.10.6, rpg-engine + build-depends: base >=4.7 && <5, hspec <= 2.10.6, hspec-discover, rpg-engine + other-modules: + InteractionSpec, + -- Parsing + ParsedToGameSpec, ParserSpec diff --git a/stack.yaml b/stack.yaml index 2539f5a..344fbba 100644 --- a/stack.yaml +++ b/stack.yaml @@ -42,6 +42,7 @@ extra-deps: # # extra-deps: [] - 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 # flags: {} diff --git a/test/InteractionSpec.hs b/test/InteractionSpec.hs new file mode 100644 index 0000000..63801af --- /dev/null +++ b/test/InteractionSpec.hs @@ -0,0 +1,9 @@ +module InteractionSpec where + +import Test.Hspec + +spec :: Spec +spec = do + describe "Player with Inventory" $ do + it "TODO: Simple test" $ do + pending \ No newline at end of file diff --git a/test/ParsedToGameSpec.hs b/test/ParsedToGameSpec.hs new file mode 100644 index 0000000..b1d3930 --- /dev/null +++ b/test/ParsedToGameSpec.hs @@ -0,0 +1,52 @@ +module ParsedToGameSpec where + +import Test.Hspec +import Parse + +spec :: Spec +spec = do + describe "Game" $ do + it "TODO: Simple game" $ do + pending + it "TODO: More complex game" $ do + pending + it "TODO: Game with multiple levels" $ do + pending + + describe "Player" $ do + it "TODO: Simple player" $ do + pending + + describe "Inventory" $ do + it "TODO: Empty inventory" $ do + pending + it "TODO: Singleton inventory" $ do + pending + it "TODO: Filled inventory" $ do + pending + + describe "Items" $ do + it "TODO: Simple item" $ do + pending + -- Check id + -- Check x + -- Check y + -- Check name + -- Check description + -- Check useTimes + -- Check value + -- Check actions + + describe "Actions" $ do + it "TODO: Simple action" $ do + pending + + describe "Entities" $ do + it "TODO: Simple entity" $ do + pending + + describe "Level" $ do + it "TODO: Simple layout" $ do + pending + it "TODO: Complex layout" $ do + pending \ No newline at end of file diff --git a/test/ParserSpec.hs b/test/ParserSpec.hs new file mode 100644 index 0000000..b5db0cc --- /dev/null +++ b/test/ParserSpec.hs @@ -0,0 +1,52 @@ +module ParserSpec where + +import Test.Hspec +import Parse +import Data.Either + +spec :: Spec +spec = do + describe "Basics of entries" $ do + it "can parse integers" $ do + let correct = Right $ Regular $ Integer 1 + correct `shouldBe` parseWith regular "1" + it "can parse string" $ do + let input = "dit is een string" + correct = Right $ Regular $ String input + correct `shouldBe` parseWith regular ("\"" ++ input ++ "\"") + it "can parse infinite" $ do + let correct = Right $ Regular Infinite + correct `shouldBe` parseWith regular "infinite" + + let wrong = Right $ Regular Infinite + wrong `shouldNotBe` parseWith regular "infinitee" + + it "can parse entries" $ do + let input = "id : \"dagger\"" + correct = Right $ Entry "id" $ Regular $ String "dagger" + correct `shouldBe` parseWith entry input + + let input = "x: 0" + correct = Right $ Entry "x" $ Regular $ Integer 0 + correct `shouldBe` parseWith entry input + + let input = "useTimes: infinite" + correct = Right $ Entry "useTimes" $ Regular Infinite + correct `shouldBe` parseWith entry input + + describe "Special kinds" $ do + it "can parse actions" $ do + let input = "actions: {}" + correct = Right $ Entry "actions" $ Regular Infinite -- TODO Change this + correct `shouldBe` parseWith action input + + it "can parse conditions" $ do + pending + + it "can parse layouts" $ do + pending + + describe "Lists and blocks" $ do + it "can parse entities" $ do + pending + diff --git a/test/RPG-Engine-Test.hs b/test/RPG-Engine-Test.hs deleted file mode 100644 index 39131fd..0000000 --- a/test/RPG-Engine-Test.hs +++ /dev/null @@ -1,7 +0,0 @@ -import Test.Hspec - -main :: IO() -main = hspec $ do - describe "Dummy category" $ do - it "Dummy test" $ do - 0 `shouldBe` 0 \ No newline at end of file diff --git a/test/RPGEngineSpec.hs b/test/RPGEngineSpec.hs new file mode 100644 index 0000000..52ef578 --- /dev/null +++ b/test/RPGEngineSpec.hs @@ -0,0 +1 @@ +{-# OPTIONS_GHC -F -pgmF hspec-discover #-} \ No newline at end of file From 3b0de65de145dddd5ea13f76b73ab76107545b45 Mon Sep 17 00:00:00 2001 From: Tibo De Peuter Date: Mon, 19 Dec 2022 22:54:42 +0100 Subject: [PATCH 04/27] #18 & massive structure overhaul --- lib/RPGEngine.hs | 8 +- lib/{control => RPGEngine}/Input.hs | 19 ++- .../Internals/Data}/Game.hs | 10 +- .../Internals/Data}/Internals.hs | 32 +++- .../Internals/Data}/Player.hs | 4 +- .../Internals/Data}/State.hs | 14 +- .../Internals/Input.hs} | 37 ++-- lib/RPGEngine/Internals/Parse.hs | 20 +++ .../Internals/Parse/StructureElement.hs | 161 ++++++++++++++++++ lib/RPGEngine/Parse.hs | 8 + lib/{render => RPGEngine}/Render.hs | 27 +-- lib/control/Parse.hs | 132 -------------- rpg-engine.cabal | 22 ++- stack.yaml | 2 + test/ParsedToGameSpec.hs | 2 +- test/ParserSpec.hs | 120 ++++++++++--- 16 files changed, 397 insertions(+), 221 deletions(-) rename lib/{control => RPGEngine}/Input.hs (69%) rename lib/{data => RPGEngine/Internals/Data}/Game.hs (79%) rename lib/{data => RPGEngine/Internals/Data}/Internals.hs (69%) rename lib/{data => RPGEngine/Internals/Data}/Player.hs (77%) rename lib/{data => RPGEngine/Internals/Data}/State.hs (84%) rename lib/{control/InputHandling.hs => RPGEngine/Internals/Input.hs} (84%) create mode 100644 lib/RPGEngine/Internals/Parse.hs create mode 100644 lib/RPGEngine/Internals/Parse/StructureElement.hs create mode 100644 lib/RPGEngine/Parse.hs rename lib/{render => RPGEngine}/Render.hs (63%) delete mode 100644 lib/control/Parse.hs diff --git a/lib/RPGEngine.hs b/lib/RPGEngine.hs index d5ea2e7..1ab1fa5 100644 --- a/lib/RPGEngine.hs +++ b/lib/RPGEngine.hs @@ -5,9 +5,9 @@ module RPGEngine ( playRPGEngine ) where -import Game -import Render -import Input +import RPGEngine.Internals.Data.Game +import RPGEngine.Render +import RPGEngine.Input import Graphics.Gloss ( Color(..) @@ -33,5 +33,5 @@ playRPGEngine :: String -> Int -> IO() playRPGEngine title fps = do play window bgColor fps initGame render handleInputs step where window = initWindow title winDimensions winOffsets - step _ g = g -- TODO Do something with step? + step _ g = g -- TODO Do something with step? Check health etc. handleInputs = handleAllInput diff --git a/lib/control/Input.hs b/lib/RPGEngine/Input.hs similarity index 69% rename from lib/control/Input.hs rename to lib/RPGEngine/Input.hs index 30c6a6b..337a3bc 100644 --- a/lib/control/Input.hs +++ b/lib/RPGEngine/Input.hs @@ -1,21 +1,25 @@ -module Input -( --- Handle all input for RPG-Engine -handleAllInput +-- Input for RPG-Engine + +module RPGEngine.Input +( handleAllInput ) where -import Game -import State -import InputHandling +import RPGEngine.Internals.Data.Game +import RPGEngine.Internals.Data.State +import RPGEngine.Internals.Input import Graphics.Gloss.Interface.IO.Game ---------------------------------------------------------------------- +-- Handle all input for RPG-Engine handleAllInput :: InputHandler Game handleAllInput ev g@Game{ state = Playing } = handlePlayInputs ev g handleAllInput ev g = handleAnyKey setNextState ev g +---------------------------------------------------------------------- + +-- Input for 'Playing' state handlePlayInputs :: InputHandler Game handlePlayInputs = composeInputHandlers [ handleKey (Char 'p') (\game -> game{ state = Pause }) @@ -25,3 +29,4 @@ handlePlayInputs = composeInputHandlers [ setNextState :: Game -> Game setNextState game = game{ state = newState } where newState = nextState $ state game + diff --git a/lib/data/Game.hs b/lib/RPGEngine/Internals/Data/Game.hs similarity index 79% rename from lib/data/Game.hs rename to lib/RPGEngine/Internals/Data/Game.hs index 3a07903..7aa8ef4 100644 --- a/lib/data/Game.hs +++ b/lib/RPGEngine/Internals/Data/Game.hs @@ -1,13 +1,12 @@ -- Representation of all the game's data -module Game -( Game(..) +module RPGEngine.Internals.Data.Game +( Game(..), --- Initialize the game -, initGame +initGame ) where -import State +import RPGEngine.Internals.Data.State ----------------------------- Constants ------------------------------ @@ -19,6 +18,7 @@ data Game = Game { ---------------------------------------------------------------------- +-- Initialize the game initGame :: Game initGame = Game { state = defaultState diff --git a/lib/data/Internals.hs b/lib/RPGEngine/Internals/Data/Internals.hs similarity index 69% rename from lib/data/Internals.hs rename to lib/RPGEngine/Internals/Data/Internals.hs index 68f1a8d..476772b 100644 --- a/lib/data/Internals.hs +++ b/lib/RPGEngine/Internals/Data/Internals.hs @@ -1,16 +1,22 @@ -- Represents an item in the game. -module Internals +module RPGEngine.Internals.Data.Internals ( Action(..) +, Condition(..) , Object(..) +, EntityId +, ItemId ) where ----------------------------- Constants ------------------------------ +type EntityId = String +type ItemId = String + data Object = Item { -- All fields are required -- Easy way to identify items - id :: String, + id :: ItemId, -- Horizontal coördinate in the level x :: Int, -- Vertical coördinate in the level @@ -22,14 +28,14 @@ data Object = -- infinite or a natural number useTimes :: Maybe Int, -- List of conditional actions when the player is standing on this object - actions :: [Action], + actions :: [([Condition], Action)], -- Interpretation depends on action with this object. value :: Maybe Int } | Entity { -- Required fields -- Easy way to identify items - id :: String, + id :: EntityId, -- Horizontal coördinate in the level x :: Int, -- Vertical coördinate in the level @@ -38,7 +44,7 @@ data Object = -- Short description of the object description :: String, -- List of conditional actions when the player is standing on this object - actions :: [Action], + actions :: [([Condition], Action)], -- Optional fields -- The direction of the item. e.g. a door has a direction. direction :: Maybe Direction, @@ -54,8 +60,18 @@ data Direction = North | West deriving (Show) -type Action = ([Condition], Event) +data Action = Leave + | RetrieveItem ItemId + | UseItem ItemId + | DecreaseHp EntityId ItemId + | IncreasePlayerHp ItemId + | Nothing + deriving (Show, Eq) -type Condition = Bool +data Condition = InventoryFull + | InventoryContains ItemId + | Not Condition + | AlwaysFalse + deriving (Show, Eq) -type Event = * \ No newline at end of file +---------------------------------------------------------------------- diff --git a/lib/data/Player.hs b/lib/RPGEngine/Internals/Data/Player.hs similarity index 77% rename from lib/data/Player.hs rename to lib/RPGEngine/Internals/Data/Player.hs index 642a6c3..c325df0 100644 --- a/lib/data/Player.hs +++ b/lib/RPGEngine/Internals/Data/Player.hs @@ -1,11 +1,11 @@ -- Represents a player in the game. This player can move around, pick -- up items and interact with the world. -module Player +module RPGEngine.Internals.Data.Player ( Player(..) ) where -import Internals +import RPGEngine.Internals.Data.Internals ----------------------------- Constants ------------------------------ diff --git a/lib/data/State.hs b/lib/RPGEngine/Internals/Data/State.hs similarity index 84% rename from lib/data/State.hs rename to lib/RPGEngine/Internals/Data/State.hs index 1ae7a29..b567517 100644 --- a/lib/data/State.hs +++ b/lib/RPGEngine/Internals/Data/State.hs @@ -2,12 +2,10 @@ -- e.g. Main menu, game, pause, win or lose -- Allows to easily go to a next state and change rendering accordingly -module State +module RPGEngine.Internals.Data.State ( State(..) --- Default state of the game, Menu , defaultState --- Get the next state based on the current state , nextState ) where @@ -20,13 +18,17 @@ data State = Menu | Win | Lose ----------------------------------------------------------------------- - +-- Default state of the game, Menu defaultState :: State defaultState = Menu +---------------------------------------------------------------------- + +-- Get the next state based on the current state nextState :: State -> State nextState Menu = Playing nextState Playing = Pause nextState Pause = Playing -nextState _ = Menu \ No newline at end of file +nextState _ = Menu + +---------------------------------------------------------------------- diff --git a/lib/control/InputHandling.hs b/lib/RPGEngine/Internals/Input.hs similarity index 84% rename from lib/control/InputHandling.hs rename to lib/RPGEngine/Internals/Input.hs index 1b4db4a..d74c6d6 100644 --- a/lib/control/InputHandling.hs +++ b/lib/RPGEngine/Internals/Input.hs @@ -1,18 +1,12 @@ -- 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 any event -handle, --- Handle a event by pressing a key -handleKey, --- Handle any key, equivalent to "Press any key to start" -handleAnyKey +module RPGEngine.Internals.Input +( InputHandler(..) +, composeInputHandlers +, handle +, handleKey +, handleAnyKey ) where import Graphics.Gloss.Interface.IO.Game @@ -23,20 +17,31 @@ type InputHandler a = Event -> (a -> a) ---------------------------------------------------------------------- +-- 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 _ _ _) = handleKey key -- handle (EventMotion _) = undefined -- handle (EventResize _) = undefined -handle _ = (\_ -> (\_ -> id)) +handle _ = const (const id) +-- Handle a event by pressing a key handleKey :: Key -> (a -> a) -> InputHandler a handleKey (SpecialKey sk) = handleSpecialKey sk handleKey (Char c ) = handleCharKey c -handleKey (MouseButton _ ) = (\_ -> (\_ -> id)) +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 + +---------------------------------------------------------------------- handleCharKey :: Char -> (a -> a) -> InputHandler a handleCharKey c1 f (EventKey (Char c2) Down _ _) @@ -49,7 +54,3 @@ handleSpecialKey sk1 f (EventKey (SpecialKey sk2) Down _ _) | sk1 == sk2 = f | otherwise = id handleSpecialKey _ _ _ = id - -handleAnyKey :: (a -> a) -> InputHandler a -handleAnyKey f (EventKey _ Down _ _) = f -handleAnyKey _ _ = id diff --git a/lib/RPGEngine/Internals/Parse.hs b/lib/RPGEngine/Internals/Parse.hs new file mode 100644 index 0000000..1118d85 --- /dev/null +++ b/lib/RPGEngine/Internals/Parse.hs @@ -0,0 +1,20 @@ +module RPGEngine.Internals.Parse where + +import Text.Parsec +import Text.Parsec.String + +-- 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 whitespaces +ignoreWS :: Parser a -> Parser a +ignoreWS parser = choice [skipComment, spaces] >> parser + where skipComment = do{ string "#"; manyTill anyChar endOfLine; return ()} \ No newline at end of file diff --git a/lib/RPGEngine/Internals/Parse/StructureElement.hs b/lib/RPGEngine/Internals/Parse/StructureElement.hs new file mode 100644 index 0000000..f9955fd --- /dev/null +++ b/lib/RPGEngine/Internals/Parse/StructureElement.hs @@ -0,0 +1,161 @@ +module RPGEngine.Internals.Parse.StructureElement where + +import RPGEngine.Internals.Data.Internals (Action(..), Condition(..)) +import RPGEngine.Internals.Parse ( ignoreWS ) + +import Text.Parsec + ( char, + many, + try, + alphaNum, + digit, + noneOf, + oneOf, + between, + choice, + many1, + notFollowedBy, + sepBy ) +import qualified Text.Parsec as P ( string ) +import Text.Parsec.String ( Parser ) +import GHC.IO.Device (RawIO(readNonBlocking)) + +-------------------------- StructureElement -------------------------- + +-- See documentation for more details, only a short description is +-- provided here. +data StructureElement = Block [StructureElement] + | Entry Key StructureElement -- Key + Value + | Regular Value -- Regular value, Integer or String or Infinite + deriving (Show, Eq) + +---------------------------------------------------------------------- + +structureElement :: Parser StructureElement +structureElement = try $ choice [block, entry, regular] + +-- A list of entries +block :: Parser StructureElement +block = try $ do + open <- ignoreWS $ oneOf openingBrackets + middle <- ignoreWS entry `sepBy` ignoreWS (char ',') + let closingBracket = getMatchingClosingBracket open + ignoreWS $ char closingBracket + return $ Block middle + +entry :: Parser StructureElement +entry = try $ do + key <- ignoreWS key + -- TODO Fix this + oneOf ": " -- Can be left out + value <- ignoreWS structureElement + return $ Entry key value + +regular :: Parser StructureElement +regular = try $ Regular <$> value + +--------------------------------- Key -------------------------------- + +data Key = Tag String + | ConditionList [Condition] + deriving (Show, Eq) + +data ConditionArgument = ArgString String + | Condition Condition + deriving (Show, Eq) + +---------------------------------------------------------------------- + +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 <- try $ ignoreWS condition `sepBy` ignoreWS (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 + | Layout -- TODO Add element + deriving (Show, Eq) + +---------------------------------------------------------------------- + +value :: Parser Value +value = choice [string, integer, infinite, 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 second + | script == "increasePlayerHp" = IncreasePlayerHp arg + | otherwise = RPGEngine.Internals.Data.Internals.Nothing + (first, ',':second) = break (== ',') arg + return $ Action answer + +-- TODO +layout :: Parser Value +layout = undefined + +------------------------------ 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] \ No newline at end of file diff --git a/lib/RPGEngine/Parse.hs b/lib/RPGEngine/Parse.hs new file mode 100644 index 0000000..15d3458 --- /dev/null +++ b/lib/RPGEngine/Parse.hs @@ -0,0 +1,8 @@ +module RPGEngine.Parse where + +import RPGEngine.Internals.Data.Game + +-- TODO parseFromFile gebruiken + +parseToGame :: Game +parseToGame = undefined \ No newline at end of file diff --git a/lib/render/Render.hs b/lib/RPGEngine/Render.hs similarity index 63% rename from lib/render/Render.hs rename to lib/RPGEngine/Render.hs index a94fed5..e65d589 100644 --- a/lib/render/Render.hs +++ b/lib/RPGEngine/Render.hs @@ -1,23 +1,29 @@ -- Allows to render the played game -module Render -( --- Initialize a window to play in -initWindow +module RPGEngine.Render +( initWindow +, bgColor --- Render the game , render ) where -import Game(Game(..)) -import State(State(..)) +import RPGEngine.Internals.Data.Game(Game(..)) +import RPGEngine.Internals.Data.State(State(..)) import Graphics.Gloss +----------------------------- Constants ------------------------------ + +-- Game background color +bgColor :: Color +bgColor = white + ---------------------------------------------------------------------- +-- Initialize a window to play in initWindow :: String -> (Int, Int) -> (Int, Int) -> Display -initWindow title dims offs = InWindow title dims offs +initWindow = InWindow +-- Render the game render :: Game -> Picture render g@Game{ state = Menu } = renderMenu g render g@Game{ state = Playing } = renderPlaying g @@ -25,10 +31,11 @@ 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" +renderMenu _ = text "[Press any key to start]" -- TODO renderPlaying :: Game -> Picture @@ -36,7 +43,7 @@ renderPlaying _ = text "Playing" -- TODO renderPause :: Game -> Picture -renderPause _ = text "Pause" +renderPause _ = text "[Press any key to continue]" -- TODO renderWin :: Game -> Picture diff --git a/lib/control/Parse.hs b/lib/control/Parse.hs deleted file mode 100644 index 376f29b..0000000 --- a/lib/control/Parse.hs +++ /dev/null @@ -1,132 +0,0 @@ -module Parse where - --- TODO Maak wrapper module --- TODO This module should not be used by anything except for wrapper module and tests - -import Game -import Player -import Text.Parsec -import Text.Parsec.Char -import Text.Parsec.String -import Data.List -import Data.Maybe -import Text.Parsec.Error (Message(UnExpect)) - --- TODO parseFromFile gebruiken - --- Parser type --- type Parser = Parsec String () - --- 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 "" - -ignoreWS :: Parser a -> Parser a -ignoreWS parser = spaces >> 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 - -parseToGame :: Game -parseToGame = undefined - --- Info in between brackets, '(..)', '[..]', '{..}' or '<..>' -data Brackets a = Brackets a - deriving (Eq, Show) - -parseToPlayer :: Player -parseToPlayer = undefined - --- any words separated by whitespace -parseWord :: Parser String -parseWord = do many alphaNum - --- TODO Expand to allow different kinds of brackets, also see Brackets data type. --- TODO Check if brackets match order. --- TODO Allow nested brackets. -brackets :: Parser (Brackets String) -brackets = do - ignoreWS $ char '(' - e <- ignoreWS $ many1 alphaNum - ignoreWS $ char ')' - return $ Brackets e - ------------------------- - -data Value = String String - | Integer Int - | Infinite - deriving (Show, Eq) - --- See documentation for more details, only a short description is ---provided here. -data StructureElement = Block [StructureElement] - | Entry String StructureElement-- Key + Value - | Regular Value -- Regular value, Integer or String or Infinite - | ConditionList [StructureElement] - -- TODO - | Condition -- inventoryFull() etc. - -- TODO - | Action -- leave(), useItem(objectId) etc. - deriving (Show, Eq) - --- TODO Add ConditionList and Action -structureElement :: Parser StructureElement -structureElement = choice [block, regular] - --- A Block is a list of Entry s -block :: Parser StructureElement -block = do - ignoreWS $ char '{' - list <- ignoreWS $ many1 entry - ignoreWS $ char '}' - return $ Block list - -entry :: Parser StructureElement -entry = do - key <- ignoreWS $ many1 alphaNum - ignoreWS $ char ':' - value <- ignoreWS structureElement -- TODO Is this the correct one to use? - return $ Entry key value - -regular :: Parser StructureElement -regular = do - value <- ignoreWS $ choice [integer, valueString, infinite] - return $ Regular value - -integer :: Parser Value -integer = do - value <- ignoreWS $ many1 digit - return $ Integer (read value :: Int) - -valueString :: Parser Value -valueString = do - ignoreWS $ char '"' - value <- ignoreWS $ many1 (noneOf ['"']) - ignoreWS $ char '"' - return $ String value - -infinite :: Parser Value -infinite = do - ignoreWS $ string "infinite" - notFollowedBy alphaNum - return Infinite - -conditionList :: Parser StructureElement -conditionList = do - ignoreWS $ char '[' - list <- ignoreWS $ many1 condition - ignoreWS $ char ']' - return $ ConditionList list - --- TODO -condition :: Parser StructureElement -condition = undefined - --- TODO YOU ARE HERE -action :: Parser StructureElement -action = undefined diff --git a/rpg-engine.cabal b/rpg-engine.cabal index 3775de7..bc85730 100644 --- a/rpg-engine.cabal +++ b/rpg-engine.cabal @@ -5,19 +5,25 @@ cabal-version: 1.12 build-type: Simple library - hs-source-dirs: lib, lib/control, lib/data, lib/render + hs-source-dirs: lib build-depends: base >= 4.7 && <5, gloss >= 1.11 && < 1.14, gloss-juicy >= 0.2.3, parsec >= 3.1.15.1 exposed-modules: - RPGEngine, - -- Control - Input, InputHandling, Parse, - -- Data - Game, Internals, Player, State, - -- Render - Render + RPGEngine + + RPGEngine.Input + RPGEngine.Parse + RPGEngine.Render + + RPGEngine.Internals.Data.Game + RPGEngine.Internals.Data.Internals + RPGEngine.Internals.Data.Player + RPGEngine.Internals.Data.State + RPGEngine.Internals.Input + RPGEngine.Internals.Parse + RPGEngine.Internals.Parse.StructureElement executable rpg-engine main-is: Main.hs diff --git a/stack.yaml b/stack.yaml index 344fbba..2f59104 100644 --- a/stack.yaml +++ b/stack.yaml @@ -67,3 +67,5 @@ extra-deps: # # Allow a newer minor version of GHC than the snapshot specifies # compiler-check: newer-minor + +custom-preprocessor-extensions: [] \ No newline at end of file diff --git a/test/ParsedToGameSpec.hs b/test/ParsedToGameSpec.hs index b1d3930..9a6aec6 100644 --- a/test/ParsedToGameSpec.hs +++ b/test/ParsedToGameSpec.hs @@ -1,7 +1,7 @@ module ParsedToGameSpec where import Test.Hspec -import Parse +import RPGEngine.Internals.Parse.StructureElement spec :: Spec spec = do diff --git a/test/ParserSpec.hs b/test/ParserSpec.hs index b5db0cc..301b282 100644 --- a/test/ParserSpec.hs +++ b/test/ParserSpec.hs @@ -1,7 +1,9 @@ module ParserSpec where import Test.Hspec -import Parse +import RPGEngine.Internals.Parse +import RPGEngine.Internals.Parse.StructureElement +import RPGEngine.Internals.Data.Internals import Data.Either spec :: Spec @@ -9,40 +11,118 @@ spec = do describe "Basics of entries" $ do it "can parse integers" $ do let correct = Right $ Regular $ Integer 1 - correct `shouldBe` parseWith regular "1" + parseWith regular "1" `shouldBe` correct it "can parse string" $ do let input = "dit is een string" correct = Right $ Regular $ String input - correct `shouldBe` parseWith regular ("\"" ++ input ++ "\"") + parseWith regular ("\"" ++ input ++ "\"") `shouldBe` correct it "can parse infinite" $ do let correct = Right $ Regular Infinite - correct `shouldBe` parseWith regular "infinite" + parseWith regular "infinite" `shouldBe` correct let wrong = Right $ Regular Infinite - wrong `shouldNotBe` parseWith regular "infinitee" + parseWith regular "infinitee" `shouldNotBe` wrong it "can parse entries" $ do - let input = "id : \"dagger\"" - correct = Right $ Entry "id" $ Regular $ String "dagger" - correct `shouldBe` parseWith entry input + let input = "id: \"dagger\"" + correct = Right $ Entry (Tag "id") $ Regular $ String "dagger" + parseWith entry input `shouldBe` correct let input = "x: 0" - correct = Right $ Entry "x" $ Regular $ Integer 0 - correct `shouldBe` parseWith entry input + correct = Right $ Entry (Tag "x") $ Regular $ Integer 0 + parseWith entry input `shouldBe` correct let input = "useTimes: infinite" - correct = Right $ Entry "useTimes" $ Regular Infinite - correct `shouldBe` parseWith entry input + correct = Right $ Entry (Tag "useTimes") $ Regular Infinite + parseWith entry input `shouldBe` correct - describe "Special kinds" $ do + describe "block: {...}" $ do + it "can parse a block with a single entry" $ do + let input = "{ id: 1}" + correct = Right (Block [ + Entry (Tag "id") $ Regular $ Integer 1 + ], "") + parseWithRest structureElement input `shouldBe` correct + + it "can parse a block with entries" $ do + 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 structureElement input `shouldBe` correct + + describe "Basics" $ do + it "can parse leave()" $ do + let input = "leave()" + correct = Right $ Action Leave + parseWith action input `shouldBe` correct + + it "can parse retrieveItem()" $ do + let input = "retrieveItem(firstId)" + correct = Right $ Action $ RetrieveItem "firstId" + parseWith action input `shouldBe` correct + + it "can parse useItem()" $ do + let input = "useItem(secondId)" + correct = Right $ Action $ UseItem "secondId" + parseWith action input `shouldBe` correct + + it "can parse decreaseHp()" $ do + let input = "decreaseHp(entityId,objectId)" + correct = Right $ Action $ DecreaseHp "entityId" "objectId" + parseWith action input `shouldBe` correct + + it "can parse increasePlayerHp()" $ do + let input = "increasePlayerHp(objectId)" + correct = Right $ Action $ IncreasePlayerHp "objectId" + parseWith action input `shouldBe` correct + + it "can parse inventoryFull()" $ do + let input = "inventoryFull()" + correct = Right (Condition InventoryFull, "") + parseWithRest condition input `shouldBe` correct + + it "can parse inventoryContains()" $ do + let input = "inventoryContains(itemId)" + correct = Right (Condition $ InventoryContains "itemId", "") + parseWithRest condition input `shouldBe` correct + + it "can parse not()" $ do + 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 + + 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 actions" $ do - let input = "actions: {}" - correct = Right $ Entry "actions" $ Regular Infinite -- TODO Change this - correct `shouldBe` parseWith action input - - it "can parse conditions" $ do - pending - + 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 structureElement input `shouldBe` correct + + describe "Layouts" $ do it "can parse layouts" $ do pending From 0720f3b71954a86868b3917c73097c5c60dcc5f9 Mon Sep 17 00:00:00 2001 From: Tibo De Peuter Date: Tue, 20 Dec 2022 11:13:14 +0100 Subject: [PATCH 05/27] Another structure overhaul --- lib/RPGEngine.hs | 2 +- lib/RPGEngine/Data/Game.hs | 17 +++ lib/RPGEngine/{Internals => }/Data/State.hs | 11 +- lib/RPGEngine/Data/Types.hs | 115 ++++++++++++++++++ lib/RPGEngine/Input.hs | 6 +- .../{Internals/Input.hs => Input/Core.hs} | 2 +- lib/RPGEngine/Internals/Data/Game.hs | 25 ---- lib/RPGEngine/Internals/Data/Internals.hs | 77 ------------ lib/RPGEngine/Internals/Data/Player.hs | 15 --- lib/RPGEngine/Parse.hs | 2 +- .../{Internals/Parse.hs => Parse/Core.hs} | 2 +- .../{Internals => }/Parse/StructureElement.hs | 8 +- lib/RPGEngine/Render.hs | 3 +- rpg-engine.cabal | 21 ++-- 14 files changed, 158 insertions(+), 148 deletions(-) create mode 100644 lib/RPGEngine/Data/Game.hs rename lib/RPGEngine/{Internals => }/Data/State.hs (80%) create mode 100644 lib/RPGEngine/Data/Types.hs rename lib/RPGEngine/{Internals/Input.hs => Input/Core.hs} (98%) delete mode 100644 lib/RPGEngine/Internals/Data/Game.hs delete mode 100644 lib/RPGEngine/Internals/Data/Internals.hs delete mode 100644 lib/RPGEngine/Internals/Data/Player.hs rename lib/RPGEngine/{Internals/Parse.hs => Parse/Core.hs} (94%) rename lib/RPGEngine/{Internals => }/Parse/StructureElement.hs (94%) diff --git a/lib/RPGEngine.hs b/lib/RPGEngine.hs index 1ab1fa5..ee52fc9 100644 --- a/lib/RPGEngine.hs +++ b/lib/RPGEngine.hs @@ -5,7 +5,7 @@ module RPGEngine ( playRPGEngine ) where -import RPGEngine.Internals.Data.Game +import RPGEngine.Data.Game import RPGEngine.Render import RPGEngine.Input diff --git a/lib/RPGEngine/Data/Game.hs b/lib/RPGEngine/Data/Game.hs new file mode 100644 index 0000000..9f5bd8a --- /dev/null +++ b/lib/RPGEngine/Data/Game.hs @@ -0,0 +1,17 @@ +-- Representation of all the game's data + +module RPGEngine.Data.Game +( Game(..) + ,initGame +) where + +import RPGEngine.Data.Types +import RPGEngine.Data.State + +---------------------------------------------------------------------- + +-- Initialize the game +initGame :: Game +initGame = Game { + state = defaultState +} diff --git a/lib/RPGEngine/Internals/Data/State.hs b/lib/RPGEngine/Data/State.hs similarity index 80% rename from lib/RPGEngine/Internals/Data/State.hs rename to lib/RPGEngine/Data/State.hs index b567517..1953d73 100644 --- a/lib/RPGEngine/Internals/Data/State.hs +++ b/lib/RPGEngine/Data/State.hs @@ -2,21 +2,16 @@ -- e.g. Main menu, game, pause, win or lose -- Allows to easily go to a next state and change rendering accordingly -module RPGEngine.Internals.Data.State +module RPGEngine.Data.State ( State(..) , defaultState , nextState ) where ------------------------------ Constants ------------------------------ +import RPGEngine.Data.Types --- Current state of the game. -data State = Menu - | Playing - | Pause - | Win - | Lose +----------------------------- Constants ------------------------------ -- Default state of the game, Menu defaultState :: State diff --git a/lib/RPGEngine/Data/Types.hs b/lib/RPGEngine/Data/Types.hs new file mode 100644 index 0000000..af812bd --- /dev/null +++ b/lib/RPGEngine/Data/Types.hs @@ -0,0 +1,115 @@ +module RPGEngine.Data.Types where + +-------------------------------- Game -------------------------------- + +-- TODO Add more +data Game = Game { + -- Current state of the game + state :: State +} + +------------------------------- Player ------------------------------- + +data Player = Player { + playerHp :: Maybe Int, + inventory :: [Item] +} + +instance Living Player where + hp = playerHp + +------------------------------- State -------------------------------- + +-- Current state of the game. +data State = Menu + | Playing + | Pause + | Win + | Lose + +------------------------------- Object ------------------------------- + +class Object a where + id :: a -> String + x :: a -> Int + y :: a -> Int + name :: a -> String + description :: a -> String + actions :: a -> [([Condition], Action)] + value :: a -> Maybe Int + +class Living a where + hp :: a -> Maybe Int + +data Item = Item { + itemId :: ItemId, + itemX :: Int, + itemY :: Int, + itemName :: String, + itemDescription :: String, + itemActions :: [([Condition], Action)], + itemValue :: Maybe Int, + useTimes :: Maybe Int +} + +instance Object Item where + id = itemId + x = itemX + y = itemY + name = itemName + description = itemDescription + actions = itemActions + value = itemValue + +data Entity = Entity { + entityId :: EntityId, + entityX :: Int, + entityY :: Int, + entityName :: String, + entityDescription :: String, + entityActions :: [([Condition], Action)], + entityValue :: Maybe Int, + entityHp :: Maybe Int, + direction :: Maybe Direction +} + +instance Object Entity where + id = entityId + x = entityX + y = entityY + name = entityName + description = entityDescription + actions = entityActions + value = entityValue + +instance Living Entity where + hp = entityHp + +type EntityId = String +type ItemId = String + +------------------------------ Condition ----------------------------- + +data Condition = InventoryFull + | InventoryContains ItemId + | Not Condition + | AlwaysFalse + deriving (Show, Eq) + +------------------------------- Action ------------------------------- + +data Action = Leave + | RetrieveItem ItemId + | UseItem ItemId + | DecreaseHp EntityId ItemId + | IncreasePlayerHp ItemId + | Nothing + deriving (Show, Eq) + +------------------------------ Direction ----------------------------- + +data Direction = North + | East + | South + | West + deriving (Show) \ No newline at end of file diff --git a/lib/RPGEngine/Input.hs b/lib/RPGEngine/Input.hs index 337a3bc..767a8d4 100644 --- a/lib/RPGEngine/Input.hs +++ b/lib/RPGEngine/Input.hs @@ -4,9 +4,9 @@ module RPGEngine.Input ( handleAllInput ) where -import RPGEngine.Internals.Data.Game -import RPGEngine.Internals.Data.State -import RPGEngine.Internals.Input +import RPGEngine.Data.Types +import RPGEngine.Data.State +import RPGEngine.Input.Core import Graphics.Gloss.Interface.IO.Game diff --git a/lib/RPGEngine/Internals/Input.hs b/lib/RPGEngine/Input/Core.hs similarity index 98% rename from lib/RPGEngine/Internals/Input.hs rename to lib/RPGEngine/Input/Core.hs index d74c6d6..e2e81b9 100644 --- a/lib/RPGEngine/Internals/Input.hs +++ b/lib/RPGEngine/Input/Core.hs @@ -1,7 +1,7 @@ -- Allows to create a massive inputHandler that can handle anything -- after you specify what you want it to do. -module RPGEngine.Internals.Input +module RPGEngine.Input.Core ( InputHandler(..) , composeInputHandlers , handle diff --git a/lib/RPGEngine/Internals/Data/Game.hs b/lib/RPGEngine/Internals/Data/Game.hs deleted file mode 100644 index 7aa8ef4..0000000 --- a/lib/RPGEngine/Internals/Data/Game.hs +++ /dev/null @@ -1,25 +0,0 @@ --- Representation of all the game's data - -module RPGEngine.Internals.Data.Game -( Game(..), - -initGame -) where - -import RPGEngine.Internals.Data.State - ------------------------------ Constants ------------------------------ - --- TODO Add more -data Game = Game { - -- Current state of the game - state :: State -} - ----------------------------------------------------------------------- - --- Initialize the game -initGame :: Game -initGame = Game { - state = defaultState -} diff --git a/lib/RPGEngine/Internals/Data/Internals.hs b/lib/RPGEngine/Internals/Data/Internals.hs deleted file mode 100644 index 476772b..0000000 --- a/lib/RPGEngine/Internals/Data/Internals.hs +++ /dev/null @@ -1,77 +0,0 @@ --- Represents an item in the game. - -module RPGEngine.Internals.Data.Internals -( Action(..) -, Condition(..) -, Object(..) -, EntityId -, ItemId -) where - ------------------------------ Constants ------------------------------ - -type EntityId = String -type ItemId = String - -data Object = - Item { -- All fields are required - -- Easy way to identify items - id :: ItemId, - -- Horizontal coördinate in the level - x :: Int, - -- Vertical coördinate in the level - y :: Int, - name :: String, - -- Short description of the object - description :: String, - -- Counts how often the object can be used by the player. Either - -- infinite or a natural number - useTimes :: Maybe Int, - -- List of conditional actions when the player is standing on this object - actions :: [([Condition], Action)], - -- Interpretation depends on action with this object. - value :: Maybe Int - } - | Entity { - -- Required fields - -- Easy way to identify items - id :: EntityId, - -- Horizontal coördinate in the level - x :: Int, - -- Vertical coördinate in the level - y :: Int, - name :: String, - -- Short description of the object - description :: String, - -- List of conditional actions when the player is standing on this object - actions :: [([Condition], Action)], - -- Optional fields - -- The direction of the item. e.g. a door has a direction. - direction :: Maybe Direction, - -- Some entities have health points. - hp :: Maybe Int, - -- Interpretation depends on action with this object. - value :: Maybe Int - } - -data Direction = North - | East - | South - | West - deriving (Show) - -data Action = Leave - | RetrieveItem ItemId - | UseItem ItemId - | DecreaseHp EntityId ItemId - | IncreasePlayerHp ItemId - | Nothing - deriving (Show, Eq) - -data Condition = InventoryFull - | InventoryContains ItemId - | Not Condition - | AlwaysFalse - deriving (Show, Eq) - ----------------------------------------------------------------------- diff --git a/lib/RPGEngine/Internals/Data/Player.hs b/lib/RPGEngine/Internals/Data/Player.hs deleted file mode 100644 index c325df0..0000000 --- a/lib/RPGEngine/Internals/Data/Player.hs +++ /dev/null @@ -1,15 +0,0 @@ --- Represents a player in the game. This player can move around, pick --- up items and interact with the world. - -module RPGEngine.Internals.Data.Player -( Player(..) -) where - -import RPGEngine.Internals.Data.Internals - ------------------------------ Constants ------------------------------ - -data Player = Player { - hp :: Int, - inventory :: [Object] -} \ No newline at end of file diff --git a/lib/RPGEngine/Parse.hs b/lib/RPGEngine/Parse.hs index 15d3458..477b150 100644 --- a/lib/RPGEngine/Parse.hs +++ b/lib/RPGEngine/Parse.hs @@ -1,6 +1,6 @@ module RPGEngine.Parse where -import RPGEngine.Internals.Data.Game +import RPGEngine.Data.Types -- TODO parseFromFile gebruiken diff --git a/lib/RPGEngine/Internals/Parse.hs b/lib/RPGEngine/Parse/Core.hs similarity index 94% rename from lib/RPGEngine/Internals/Parse.hs rename to lib/RPGEngine/Parse/Core.hs index 1118d85..7e704ab 100644 --- a/lib/RPGEngine/Internals/Parse.hs +++ b/lib/RPGEngine/Parse/Core.hs @@ -1,4 +1,4 @@ -module RPGEngine.Internals.Parse where +module RPGEngine.Parse.Core where import Text.Parsec import Text.Parsec.String diff --git a/lib/RPGEngine/Internals/Parse/StructureElement.hs b/lib/RPGEngine/Parse/StructureElement.hs similarity index 94% rename from lib/RPGEngine/Internals/Parse/StructureElement.hs rename to lib/RPGEngine/Parse/StructureElement.hs index f9955fd..e8f4b34 100644 --- a/lib/RPGEngine/Internals/Parse/StructureElement.hs +++ b/lib/RPGEngine/Parse/StructureElement.hs @@ -1,7 +1,7 @@ -module RPGEngine.Internals.Parse.StructureElement where +module RPGEngine.Parse.StructureElement where -import RPGEngine.Internals.Data.Internals (Action(..), Condition(..)) -import RPGEngine.Internals.Parse ( ignoreWS ) +import RPGEngine.Data.Types (Action(..), Condition(..)) +import RPGEngine.Parse.Core ( ignoreWS ) import Text.Parsec ( char, @@ -134,7 +134,7 @@ action = try $ do | script == "useItem" = UseItem arg | script == "decreaseHp" = DecreaseHp first second | script == "increasePlayerHp" = IncreasePlayerHp arg - | otherwise = RPGEngine.Internals.Data.Internals.Nothing + | otherwise = RPGEngine.Data.Types.Nothing (first, ',':second) = break (== ',') arg return $ Action answer diff --git a/lib/RPGEngine/Render.hs b/lib/RPGEngine/Render.hs index e65d589..ca39b23 100644 --- a/lib/RPGEngine/Render.hs +++ b/lib/RPGEngine/Render.hs @@ -7,8 +7,7 @@ module RPGEngine.Render , render ) where -import RPGEngine.Internals.Data.Game(Game(..)) -import RPGEngine.Internals.Data.State(State(..)) +import RPGEngine.Data.Types import Graphics.Gloss ----------------------------- Constants ------------------------------ diff --git a/rpg-engine.cabal b/rpg-engine.cabal index bc85730..4968079 100644 --- a/rpg-engine.cabal +++ b/rpg-engine.cabal @@ -13,17 +13,18 @@ library exposed-modules: RPGEngine - RPGEngine.Input - RPGEngine.Parse - RPGEngine.Render + RPGEngine.Data.Game + RPGEngine.Data.Types + RPGEngine.Data.State - RPGEngine.Internals.Data.Game - RPGEngine.Internals.Data.Internals - RPGEngine.Internals.Data.Player - RPGEngine.Internals.Data.State - RPGEngine.Internals.Input - RPGEngine.Internals.Parse - RPGEngine.Internals.Parse.StructureElement + RPGEngine.Input + RPGEngine.Input.Core + + RPGEngine.Parse + RPGEngine.Parse.Core + RPGEngine.Parse.StructureElement + + RPGEngine.Render executable rpg-engine main-is: Main.hs From d4fbcda73b8631c5279b4ebd26aee0ddb252aa1d Mon Sep 17 00:00:00 2001 From: Tibo De Peuter Date: Tue, 20 Dec 2022 16:56:22 +0100 Subject: [PATCH 06/27] Setup --- lib/RPGEngine/{Data/Types.hs => Data.hs} | 33 +- lib/RPGEngine/Data/Game.hs | 12 +- lib/RPGEngine/Data/State.hs | 2 +- lib/RPGEngine/Input.hs | 2 +- lib/RPGEngine/Parse.hs | 19 +- lib/RPGEngine/Parse/Game.hs | 22 ++ lib/RPGEngine/Parse/StructureElement.hs | 56 +++- lib/RPGEngine/Render.hs | 2 +- rpg-engine.cabal | 6 +- test/InteractionSpec.hs | 9 - .../{ParsedToGameSpec.hs => ParseGameSpec.hs} | 4 +- test/ParseStructureElementSpec.hs | 282 ++++++++++++++++++ test/ParserSpec.hs | 132 -------- 13 files changed, 412 insertions(+), 169 deletions(-) rename lib/RPGEngine/{Data/Types.hs => Data.hs} (80%) create mode 100644 lib/RPGEngine/Parse/Game.hs delete mode 100644 test/InteractionSpec.hs rename test/{ParsedToGameSpec.hs => ParseGameSpec.hs} (93%) create mode 100644 test/ParseStructureElementSpec.hs delete mode 100644 test/ParserSpec.hs diff --git a/lib/RPGEngine/Data/Types.hs b/lib/RPGEngine/Data.hs similarity index 80% rename from lib/RPGEngine/Data/Types.hs rename to lib/RPGEngine/Data.hs index af812bd..b91b7f2 100644 --- a/lib/RPGEngine/Data/Types.hs +++ b/lib/RPGEngine/Data.hs @@ -1,13 +1,33 @@ -module RPGEngine.Data.Types where +module RPGEngine.Data where -------------------------------- Game -------------------------------- -- TODO Add more data Game = Game { -- Current state of the game - state :: State + state :: State, + playing :: Level, + levels :: [Level] } +------------------------------- Level -------------------------------- + +data Level = Level { + layout :: Layout, + items :: [Item], + entities :: [Entity] +} + +type Layout = [Strip] +type Strip = [Physical] + +data Physical = Void + | Walkable + | Blocked + | Entrance + | Exit + deriving (Show, Eq) + ------------------------------- Player ------------------------------- data Player = Player { @@ -108,8 +128,9 @@ data Action = Leave ------------------------------ Direction ----------------------------- -data Direction = North - | East - | South +data Direction = North + | East + | South | West - deriving (Show) \ No newline at end of file + | Center -- Equal to 'stay where you are' + deriving (Show, Eq) \ No newline at end of file diff --git a/lib/RPGEngine/Data/Game.hs b/lib/RPGEngine/Data/Game.hs index 9f5bd8a..82750f6 100644 --- a/lib/RPGEngine/Data/Game.hs +++ b/lib/RPGEngine/Data/Game.hs @@ -5,7 +5,7 @@ module RPGEngine.Data.Game ,initGame ) where -import RPGEngine.Data.Types +import RPGEngine.Data import RPGEngine.Data.State ---------------------------------------------------------------------- @@ -13,5 +13,13 @@ import RPGEngine.Data.State -- Initialize the game initGame :: Game initGame = Game { - state = defaultState + state = defaultState, + playing = head levels, + levels = levels } + where levels = [emptyLevel] + emptyLevel = Level { + layout = [], + items = [], + entities = [] + } diff --git a/lib/RPGEngine/Data/State.hs b/lib/RPGEngine/Data/State.hs index 1953d73..42d9d3f 100644 --- a/lib/RPGEngine/Data/State.hs +++ b/lib/RPGEngine/Data/State.hs @@ -9,7 +9,7 @@ module RPGEngine.Data.State , nextState ) where -import RPGEngine.Data.Types +import RPGEngine.Data ----------------------------- Constants ------------------------------ diff --git a/lib/RPGEngine/Input.hs b/lib/RPGEngine/Input.hs index 767a8d4..04b6a6a 100644 --- a/lib/RPGEngine/Input.hs +++ b/lib/RPGEngine/Input.hs @@ -4,7 +4,7 @@ module RPGEngine.Input ( handleAllInput ) where -import RPGEngine.Data.Types +import RPGEngine.Data import RPGEngine.Data.State import RPGEngine.Input.Core diff --git a/lib/RPGEngine/Parse.hs b/lib/RPGEngine/Parse.hs index 477b150..f5838d3 100644 --- a/lib/RPGEngine/Parse.hs +++ b/lib/RPGEngine/Parse.hs @@ -1,8 +1,19 @@ module RPGEngine.Parse where -import RPGEngine.Data.Types +import RPGEngine.Data +import RPGEngine.Parse.StructureElement +import RPGEngine.Parse.Game --- TODO parseFromFile gebruiken +import Text.Parsec.String +import System.IO.Unsafe -parseToGame :: Game -parseToGame = undefined \ No newline at end of file +----------------------------- Constants ------------------------------ + +type FileName = String + +---------------------------------------------------------------------- + +parseToGame :: FileName -> Game +parseToGame filename = structureToGame structure + where (Right structure) = unsafePerformIO io + io = parseFromFile structureElement filename \ No newline at end of file diff --git a/lib/RPGEngine/Parse/Game.hs b/lib/RPGEngine/Parse/Game.hs new file mode 100644 index 0000000..85028f2 --- /dev/null +++ b/lib/RPGEngine/Parse/Game.hs @@ -0,0 +1,22 @@ +module RPGEngine.Parse.Game where + +import RPGEngine.Data +import RPGEngine.Parse.StructureElement (StructureElement) + +-------------------------------- Game -------------------------------- + +-- TODO +structureToGame :: StructureElement -> Game +structureToGame = undefined + +------------------------------- Player ------------------------------- + +-- TODO +structureToPlayer :: StructureElement -> Player +structureToPlayer = undefined + +------------------------------- Levels ------------------------------- + +-- TODO +structureToLevels :: StructureElement -> [Level] +structureToLevels = undefined \ No newline at end of file diff --git a/lib/RPGEngine/Parse/StructureElement.hs b/lib/RPGEngine/Parse/StructureElement.hs index e8f4b34..ce5d8c5 100644 --- a/lib/RPGEngine/Parse/StructureElement.hs +++ b/lib/RPGEngine/Parse/StructureElement.hs @@ -1,6 +1,6 @@ module RPGEngine.Parse.StructureElement where -import RPGEngine.Data.Types (Action(..), Condition(..)) +import RPGEngine.Data (Action(..), Condition(..), Layout, Direction(..), Physical(..), Strip) import RPGEngine.Parse.Core ( ignoreWS ) import Text.Parsec @@ -38,7 +38,7 @@ structureElement = try $ choice [block, entry, regular] block :: Parser StructureElement block = try $ do open <- ignoreWS $ oneOf openingBrackets - middle <- ignoreWS entry `sepBy` ignoreWS (char ',') + middle <- ignoreWS $ choice [entry, block] `sepBy` char ',' let closingBracket = getMatchingClosingBracket open ignoreWS $ char closingBracket return $ Block middle @@ -75,7 +75,7 @@ tag = try $ Tag <$> many1 alphaNum conditionList :: Parser Key conditionList = try $ do open <- ignoreWS $ oneOf openingBrackets - list <- try $ ignoreWS condition `sepBy` ignoreWS (char ',') + list <- ignoreWS condition `sepBy` char ',' let closingBracket = getMatchingClosingBracket open ignoreWS $ char closingBracket return $ ConditionList $ extract list @@ -102,13 +102,14 @@ data Value = String String | Integer Int | Infinite | Action Action - | Layout -- TODO Add element + | Direction Direction + | Layout Layout deriving (Show, Eq) ---------------------------------------------------------------------- value :: Parser Value -value = choice [string, integer, infinite, action] +value = choice [string, integer, infinite, action, direction] string :: Parser Value string = try $ String <$> between (char '\"') (char '\"') reading @@ -134,13 +135,52 @@ action = try $ do | script == "useItem" = UseItem arg | script == "decreaseHp" = DecreaseHp first second | script == "increasePlayerHp" = IncreasePlayerHp arg - | otherwise = RPGEngine.Data.Types.Nothing + | otherwise = RPGEngine.Data.Nothing (first, ',':second) = break (== ',') arg return $ Action answer --- TODO +direction :: Parser Value +direction = try $ do + value <- choice [ + ignoreWS $ P.string "up", + ignoreWS $ P.string "down", + ignoreWS $ P.string "left", + ignoreWS $ P.string "right" + ] + notFollowedBy alphaNum + return $ Direction $ make value + where make "up" = North + make "right" = East + make "down" = South + make "left" = West + make _ = Center + layout :: Parser Value -layout = undefined +layout = try $ do + ignoreWS $ char '|' + list <- ignoreWS strip `sepBy` ignoreWS (char '|') + return $ Layout list + +strip :: Parser Strip +strip = try $ do + 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 ------------------------------ diff --git a/lib/RPGEngine/Render.hs b/lib/RPGEngine/Render.hs index ca39b23..03e7855 100644 --- a/lib/RPGEngine/Render.hs +++ b/lib/RPGEngine/Render.hs @@ -7,7 +7,7 @@ module RPGEngine.Render , render ) where -import RPGEngine.Data.Types +import RPGEngine.Data import Graphics.Gloss ----------------------------- Constants ------------------------------ diff --git a/rpg-engine.cabal b/rpg-engine.cabal index 4968079..f6a304b 100644 --- a/rpg-engine.cabal +++ b/rpg-engine.cabal @@ -13,8 +13,8 @@ library exposed-modules: RPGEngine + RPGEngine.Data RPGEngine.Data.Game - RPGEngine.Data.Types RPGEngine.Data.State RPGEngine.Input @@ -22,6 +22,7 @@ library RPGEngine.Parse RPGEngine.Parse.Core + RPGEngine.Parse.Game RPGEngine.Parse.StructureElement RPGEngine.Render @@ -39,6 +40,5 @@ test-suite rpg-engine-test default-language: Haskell2010 build-depends: base >=4.7 && <5, hspec <= 2.10.6, hspec-discover, rpg-engine other-modules: - InteractionSpec, -- Parsing - ParsedToGameSpec, ParserSpec + ParseGameSpec, ParseStructureElementSpec diff --git a/test/InteractionSpec.hs b/test/InteractionSpec.hs deleted file mode 100644 index 63801af..0000000 --- a/test/InteractionSpec.hs +++ /dev/null @@ -1,9 +0,0 @@ -module InteractionSpec where - -import Test.Hspec - -spec :: Spec -spec = do - describe "Player with Inventory" $ do - it "TODO: Simple test" $ do - pending \ No newline at end of file diff --git a/test/ParsedToGameSpec.hs b/test/ParseGameSpec.hs similarity index 93% rename from test/ParsedToGameSpec.hs rename to test/ParseGameSpec.hs index 9a6aec6..a162246 100644 --- a/test/ParsedToGameSpec.hs +++ b/test/ParseGameSpec.hs @@ -1,7 +1,7 @@ -module ParsedToGameSpec where +module ParseGameSpec where import Test.Hspec -import RPGEngine.Internals.Parse.StructureElement +import RPGEngine.Parse.StructureElement spec :: Spec spec = do diff --git a/test/ParseStructureElementSpec.hs b/test/ParseStructureElementSpec.hs new file mode 100644 index 0000000..bc70837 --- /dev/null +++ b/test/ParseStructureElementSpec.hs @@ -0,0 +1,282 @@ +module ParseStructureElementSpec where + +import Test.Hspec + +import RPGEngine.Data +import RPGEngine.Parse.Core +import RPGEngine.Parse.StructureElement + +spec :: Spec +spec = do + describe "StructureElement" $ do + it "can parse blocks" $ do + let input = "{}" + correct = Right $ Block [] + parseWith structureElement input `shouldBe` correct + + let input = "{{}}" + correct = Right $ Block [Block []] + parseWith structureElement input `shouldBe` correct + + let input = "{{}, {}}" + correct = Right $ Block [Block [], Block []] + parseWith structureElement input `shouldBe` correct + + let input = "{ id: 1 }" + correct = Right (Block [ + Entry (Tag "id") $ Regular $ Integer 1 + ], "") + parseWithRest structureElement 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 structureElement 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 structureElement 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 structureElement 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 structureElement 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 structureElement 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 structureElement 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 = "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.StructureElement.direction input `shouldBe` correct + + let input = "right" + correct = Right $ Direction East + parseWith RPGEngine.Parse.StructureElement.direction input `shouldBe` correct + + let input = "down" + correct = Right $ Direction South + parseWith RPGEngine.Parse.StructureElement.direction input `shouldBe` correct + + let input = "left" + correct = Right $ Direction West + parseWith RPGEngine.Parse.StructureElement.direction input `shouldBe` correct + + it "can parse layouts" $ do + let input = "| * * * * * * * *\n| * s . . . . e *\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 RPGEngine.Parse.StructureElement.layout 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 diff --git a/test/ParserSpec.hs b/test/ParserSpec.hs deleted file mode 100644 index 301b282..0000000 --- a/test/ParserSpec.hs +++ /dev/null @@ -1,132 +0,0 @@ -module ParserSpec where - -import Test.Hspec -import RPGEngine.Internals.Parse -import RPGEngine.Internals.Parse.StructureElement -import RPGEngine.Internals.Data.Internals -import Data.Either - -spec :: Spec -spec = do - describe "Basics of entries" $ do - it "can parse integers" $ do - let correct = Right $ Regular $ Integer 1 - parseWith regular "1" `shouldBe` correct - it "can parse string" $ do - let input = "dit is een string" - correct = Right $ Regular $ String input - parseWith regular ("\"" ++ input ++ "\"") `shouldBe` correct - it "can parse infinite" $ do - let correct = Right $ Regular Infinite - parseWith regular "infinite" `shouldBe` correct - - let wrong = Right $ Regular Infinite - parseWith regular "infinitee" `shouldNotBe` wrong - - 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 - - describe "block: {...}" $ do - it "can parse a block with a single entry" $ do - let input = "{ id: 1}" - correct = Right (Block [ - Entry (Tag "id") $ Regular $ Integer 1 - ], "") - parseWithRest structureElement input `shouldBe` correct - - it "can parse a block with entries" $ do - 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 structureElement input `shouldBe` correct - - describe "Basics" $ do - it "can parse leave()" $ do - let input = "leave()" - correct = Right $ Action Leave - parseWith action input `shouldBe` correct - - it "can parse retrieveItem()" $ do - let input = "retrieveItem(firstId)" - correct = Right $ Action $ RetrieveItem "firstId" - parseWith action input `shouldBe` correct - - it "can parse useItem()" $ do - let input = "useItem(secondId)" - correct = Right $ Action $ UseItem "secondId" - parseWith action input `shouldBe` correct - - it "can parse decreaseHp()" $ do - let input = "decreaseHp(entityId,objectId)" - correct = Right $ Action $ DecreaseHp "entityId" "objectId" - parseWith action input `shouldBe` correct - - it "can parse increasePlayerHp()" $ do - let input = "increasePlayerHp(objectId)" - correct = Right $ Action $ IncreasePlayerHp "objectId" - parseWith action input `shouldBe` correct - - it "can parse inventoryFull()" $ do - let input = "inventoryFull()" - correct = Right (Condition InventoryFull, "") - parseWithRest condition input `shouldBe` correct - - it "can parse inventoryContains()" $ do - let input = "inventoryContains(itemId)" - correct = Right (Condition $ InventoryContains "itemId", "") - parseWithRest condition input `shouldBe` correct - - it "can parse not()" $ do - 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 - - 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 actions" $ do - 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 structureElement input `shouldBe` correct - - describe "Layouts" $ do - it "can parse layouts" $ do - pending - - describe "Lists and blocks" $ do - it "can parse entities" $ do - pending - From de02c7113f901032c07c24e3011146c0c1b5fc45 Mon Sep 17 00:00:00 2001 From: Tibo De Peuter Date: Tue, 20 Dec 2022 19:53:40 +0100 Subject: [PATCH 07/27] #18 Started conversion to Game --- lib/RPGEngine.hs | 2 +- lib/RPGEngine/Data.hs | 12 +- lib/RPGEngine/Data/Defaults.hs | 60 ++++++++ lib/RPGEngine/Data/Game.hs | 25 ---- lib/RPGEngine/Data/State.hs | 7 - lib/RPGEngine/Parse.hs | 8 +- lib/RPGEngine/Parse/Game.hs | 97 +++++++++++-- .../{StructureElement.hs => StructElement.hs} | 29 ++-- rpg-engine.cabal | 6 +- test/ParseGameSpec.hs | 132 ++++++++++++++---- ...ementSpec.hs => ParseStructElementSpec.hs} | 34 ++--- 11 files changed, 300 insertions(+), 112 deletions(-) create mode 100644 lib/RPGEngine/Data/Defaults.hs delete mode 100644 lib/RPGEngine/Data/Game.hs rename lib/RPGEngine/Parse/{StructureElement.hs => StructElement.hs} (89%) rename test/{ParseStructureElementSpec.hs => ParseStructElementSpec.hs} (91%) diff --git a/lib/RPGEngine.hs b/lib/RPGEngine.hs index ee52fc9..f9372e7 100644 --- a/lib/RPGEngine.hs +++ b/lib/RPGEngine.hs @@ -5,7 +5,7 @@ module RPGEngine ( playRPGEngine ) where -import RPGEngine.Data.Game +import RPGEngine.Data.Defaults import RPGEngine.Render import RPGEngine.Input diff --git a/lib/RPGEngine/Data.hs b/lib/RPGEngine/Data.hs index b91b7f2..13be117 100644 --- a/lib/RPGEngine/Data.hs +++ b/lib/RPGEngine/Data.hs @@ -16,7 +16,7 @@ data Level = Level { layout :: Layout, items :: [Item], entities :: [Entity] -} +} deriving (Eq, Show) type Layout = [Strip] type Strip = [Physical] @@ -26,14 +26,14 @@ data Physical = Void | Blocked | Entrance | Exit - deriving (Show, Eq) + deriving (Eq, Show) ------------------------------- Player ------------------------------- data Player = Player { playerHp :: Maybe Int, inventory :: [Item] -} +} deriving (Eq, Show) instance Living Player where hp = playerHp @@ -70,7 +70,7 @@ data Item = Item { itemActions :: [([Condition], Action)], itemValue :: Maybe Int, useTimes :: Maybe Int -} +} deriving (Eq, Show) instance Object Item where id = itemId @@ -90,8 +90,8 @@ data Entity = Entity { entityActions :: [([Condition], Action)], entityValue :: Maybe Int, entityHp :: Maybe Int, - direction :: Maybe Direction -} + direction :: Direction +} deriving (Eq, Show) instance Object Entity where id = entityId diff --git a/lib/RPGEngine/Data/Defaults.hs b/lib/RPGEngine/Data/Defaults.hs new file mode 100644 index 0000000..2ef92f2 --- /dev/null +++ b/lib/RPGEngine/Data/Defaults.hs @@ -0,0 +1,60 @@ +module RPGEngine.Data.Defaults where + +import RPGEngine.Data + +defaultEntity :: Entity +defaultEntity = Entity { + entityId = "", + entityX = 0, + entityY = 0, + entityName = "Default", + entityDescription = "", + entityActions = [], + entityValue = Prelude.Nothing, + entityHp = Prelude.Nothing, + direction = Center +} + +-- Initialize the game +initGame :: Game +initGame = Game { + state = defaultState, + playing = defaultLevel, + levels = [defaultLevel] +} + +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, Blocked, Blocked], + [Blocked, Entrance, Walkable, Walkable, Walkable, Walkable, Exit, Blocked], + [Blocked, Blocked, Blocked, Blocked, Blocked, Blocked, Blocked, Blocked] + ] + +defaultLevel :: Level +defaultLevel = Level { + layout = defaultLayout, + items = [], + entities = [] +} + +defaultPlayer :: Player +defaultPlayer = Player { + playerHp = Prelude.Nothing, -- Compares to infinity + inventory = [] +} + +-- Default state of the game, Menu +defaultState :: State +defaultState = Menu \ No newline at end of file diff --git a/lib/RPGEngine/Data/Game.hs b/lib/RPGEngine/Data/Game.hs deleted file mode 100644 index 82750f6..0000000 --- a/lib/RPGEngine/Data/Game.hs +++ /dev/null @@ -1,25 +0,0 @@ --- Representation of all the game's data - -module RPGEngine.Data.Game -( Game(..) - ,initGame -) where - -import RPGEngine.Data -import RPGEngine.Data.State - ----------------------------------------------------------------------- - --- Initialize the game -initGame :: Game -initGame = Game { - state = defaultState, - playing = head levels, - levels = levels -} - where levels = [emptyLevel] - emptyLevel = Level { - layout = [], - items = [], - entities = [] - } diff --git a/lib/RPGEngine/Data/State.hs b/lib/RPGEngine/Data/State.hs index 42d9d3f..057bc9e 100644 --- a/lib/RPGEngine/Data/State.hs +++ b/lib/RPGEngine/Data/State.hs @@ -4,19 +4,12 @@ module RPGEngine.Data.State ( State(..) -, defaultState , nextState ) where import RPGEngine.Data ------------------------------ Constants ------------------------------ - --- Default state of the game, Menu -defaultState :: State -defaultState = Menu - ---------------------------------------------------------------------- -- Get the next state based on the current state diff --git a/lib/RPGEngine/Parse.hs b/lib/RPGEngine/Parse.hs index f5838d3..a8736ad 100644 --- a/lib/RPGEngine/Parse.hs +++ b/lib/RPGEngine/Parse.hs @@ -1,7 +1,7 @@ module RPGEngine.Parse where import RPGEngine.Data -import RPGEngine.Parse.StructureElement +import RPGEngine.Parse.StructElement import RPGEngine.Parse.Game import Text.Parsec.String @@ -14,6 +14,6 @@ type FileName = String ---------------------------------------------------------------------- parseToGame :: FileName -> Game -parseToGame filename = structureToGame structure - where (Right structure) = unsafePerformIO io - io = parseFromFile structureElement filename \ No newline at end of file +parseToGame filename = structToGame struct + where (Right struct) = unsafePerformIO io + io = parseFromFile structElement filename \ No newline at end of file diff --git a/lib/RPGEngine/Parse/Game.hs b/lib/RPGEngine/Parse/Game.hs index 85028f2..9999ffd 100644 --- a/lib/RPGEngine/Parse/Game.hs +++ b/lib/RPGEngine/Parse/Game.hs @@ -1,22 +1,101 @@ module RPGEngine.Parse.Game where import RPGEngine.Data -import RPGEngine.Parse.StructureElement (StructureElement) +import RPGEngine.Data.Defaults +import RPGEngine.Parse.StructElement -------------------------------- Game -------------------------------- -- TODO -structureToGame :: StructureElement -> Game -structureToGame = undefined +structToGame :: StructElement -> Game +structToGame = undefined ------------------------------- Player ------------------------------- --- TODO -structureToPlayer :: StructureElement -> Player -structureToPlayer = undefined +structToPlayer :: StructElement -> Player +structToPlayer (Block block) = structToPlayer' block defaultPlayer +structToPlayer _ = defaultPlayer + +structToPlayer' :: [StructElement] -> Player -> Player +structToPlayer' [] p = p +structToPlayer' ((Entry(Tag "hp") val ):es) p = (structToPlayer' es p){ playerHp = structToMaybeInt val } +structToPlayer' ((Entry(Tag "inventory") (Block inv)):es) p = (structToPlayer' es p){ inventory = structToItems inv } +structToPlayer' _ _ = defaultPlayer + +structToActions :: StructElement -> [([Condition], Action)] +structToActions (Block []) = [] +structToActions (Block block) = structToActions' block [] +structToActions _ = [] + +structToActions' :: [StructElement] -> [([Condition], Action)] -> [([Condition], Action)] +structToActions' [] list = list +structToActions' ((Entry(ConditionList cs) (Regular (Action a))):as) list = structToActions' as ((cs, a):list) +structToActions' _ list = list ------------------------------- Levels ------------------------------- --- TODO -structureToLevels :: StructureElement -> [Level] -structureToLevels = undefined \ No newline at end of file +structToLevels :: StructElement -> [Level] +structToLevels (Block struct) = structToLevel <$> struct +structToLevels _ = [defaultLevel] + +structToLevel :: StructElement -> Level +structToLevel (Block entries) = structToLevel' entries defaultLevel +structToLevel _ = defaultLevel + +structToLevel' :: [StructElement] -> Level -> Level +structToLevel' ((Entry(Tag "layout") (Regular (Layout layout))):ls) l = (structToLevel' ls l){ RPGEngine.Data.layout = layout } +structToLevel' ((Entry(Tag "items") (Block items) ):ls) l = (structToLevel' ls l){ items = structToItems items } +structToLevel' ((Entry(Tag "entities") (Block entities) ):ls) l = (structToLevel' ls l){ entities = structToEntities entities } +structToLevel' _ _ = defaultLevel + +------------------------------- Items -------------------------------- + +structToItems :: [StructElement] -> [Item] +structToItems items = structToItem <$> items + +structToItem :: StructElement -> Item +structToItem (Block block) = structToItem' block defaultItem +structToItem _ = defaultItem + +structToItem' :: [StructElement] -> Item -> Item +structToItem' [] i = i +structToItem' ((Entry(Tag "id") (Regular(String id ))):is) i = (structToItem' is i){ itemId = id } +structToItem' ((Entry(Tag "x") (Regular(Integer x ))):is) i = (structToItem' is i){ itemX = x } +structToItem' ((Entry(Tag "y") (Regular(Integer y ))):is) i = (structToItem' is i){ itemY = y } +structToItem' ((Entry(Tag "name") (Regular(String name))):is) i = (structToItem' is i){ itemName = name } +structToItem' ((Entry(Tag "description") (Regular(String desc))):is) i = (structToItem' is i){ itemDescription = desc } +structToItem' ((Entry(Tag "value") val ):is) i = (structToItem' is i){ itemValue = structToMaybeInt val } +structToItem' ((Entry(Tag "actions") actions ):is) i = (structToItem' is i){ itemActions = structToActions actions } +structToItem' ((Entry (Tag "useTimes") useTimes ):is) i = (structToItem' is i){ useTimes = structToMaybeInt useTimes } +structToItem' _ _ = defaultItem + +------------------------------ Entities ------------------------------ + +structToEntities :: [StructElement] -> [Entity] +structToEntities entities = structToEntity <$> entities + +structToEntity :: StructElement -> Entity +structToEntity (Block block) = structToEntity' block defaultEntity +structToEntity _ = defaultEntity + +structToEntity' :: [StructElement] -> Entity -> Entity +structToEntity' [] e = e +structToEntity' ((Entry(Tag "id") (Regular(String id )) ):es) e = (structToEntity' es e){ entityId = id } +structToEntity' ((Entry(Tag "x") (Regular(Integer x )) ):es) e = (structToEntity' es e){ entityX = x } +structToEntity' ((Entry(Tag "y") (Regular(Integer y )) ):es) e = (structToEntity' es e){ entityY = y } +structToEntity' ((Entry(Tag "name") (Regular(String name)) ):es) e = (structToEntity' es e){ entityName = name } +structToEntity' ((Entry(Tag "description") (Regular(String desc)) ):es) e = (structToEntity' es e){ entityDescription = desc } +structToEntity' ((Entry(Tag "actions") actions ):es) e = (structToEntity' es e){ entityActions = structToActions actions } +structToEntity' ((Entry(Tag "value") val ):es) e = (structToEntity' es e){ entityValue = structToMaybeInt val } +structToEntity' ((Entry(Tag "hp") val ):es) e = (structToEntity' es e){ entityHp = structToMaybeInt val } +structToEntity' ((Entry(Tag "direction") (Regular(Direction dir))):es) e = (structToEntity' es e){ RPGEngine.Data.direction = dir } +structToEntity' _ _ = defaultEntity + +---------------------------------------------------------------------- + +structToMaybeInt :: StructElement -> Maybe Int +structToMaybeInt (Regular (Integer val)) = Just val +structToMaybeInt (Regular Infinite) = Prelude.Nothing +structToMaybeInt _ = Prelude.Nothing -- TODO + +---------------------------------------------------------------------- \ No newline at end of file diff --git a/lib/RPGEngine/Parse/StructureElement.hs b/lib/RPGEngine/Parse/StructElement.hs similarity index 89% rename from lib/RPGEngine/Parse/StructureElement.hs rename to lib/RPGEngine/Parse/StructElement.hs index ce5d8c5..35d2b08 100644 --- a/lib/RPGEngine/Parse/StructureElement.hs +++ b/lib/RPGEngine/Parse/StructElement.hs @@ -1,4 +1,4 @@ -module RPGEngine.Parse.StructureElement where +module RPGEngine.Parse.StructElement where import RPGEngine.Data (Action(..), Condition(..), Layout, Direction(..), Physical(..), Strip) import RPGEngine.Parse.Core ( ignoreWS ) @@ -18,24 +18,23 @@ import Text.Parsec sepBy ) import qualified Text.Parsec as P ( string ) import Text.Parsec.String ( Parser ) -import GHC.IO.Device (RawIO(readNonBlocking)) -------------------------- StructureElement -------------------------- -- See documentation for more details, only a short description is -- provided here. -data StructureElement = Block [StructureElement] - | Entry Key StructureElement -- Key + Value - | Regular Value -- Regular value, Integer or String or Infinite - deriving (Show, Eq) +data StructElement = Block [StructElement] + | Entry Key StructElement -- Key + Value + | Regular Value -- Regular value, Integer or String or Infinite + deriving (Eq, Show) ---------------------------------------------------------------------- -structureElement :: Parser StructureElement -structureElement = try $ choice [block, entry, regular] +structElement :: Parser StructElement +structElement = try $ choice [block, entry, regular] -- A list of entries -block :: Parser StructureElement +block :: Parser StructElement block = try $ do open <- ignoreWS $ oneOf openingBrackets middle <- ignoreWS $ choice [entry, block] `sepBy` char ',' @@ -43,26 +42,26 @@ block = try $ do ignoreWS $ char closingBracket return $ Block middle -entry :: Parser StructureElement +entry :: Parser StructElement entry = try $ do key <- ignoreWS key -- TODO Fix this oneOf ": " -- Can be left out - value <- ignoreWS structureElement + value <- ignoreWS structElement return $ Entry key value -regular :: Parser StructureElement +regular :: Parser StructElement regular = try $ Regular <$> value --------------------------------- Key -------------------------------- data Key = Tag String | ConditionList [Condition] - deriving (Show, Eq) + deriving (Eq, Show) data ConditionArgument = ArgString String | Condition Condition - deriving (Show, Eq) + deriving (Eq, Show) ---------------------------------------------------------------------- @@ -104,7 +103,7 @@ data Value = String String | Action Action | Direction Direction | Layout Layout - deriving (Show, Eq) + deriving (Eq, Show) ---------------------------------------------------------------------- diff --git a/rpg-engine.cabal b/rpg-engine.cabal index f6a304b..0823b23 100644 --- a/rpg-engine.cabal +++ b/rpg-engine.cabal @@ -14,7 +14,7 @@ library RPGEngine RPGEngine.Data - RPGEngine.Data.Game + RPGEngine.Data.Defaults RPGEngine.Data.State RPGEngine.Input @@ -23,7 +23,7 @@ library RPGEngine.Parse RPGEngine.Parse.Core RPGEngine.Parse.Game - RPGEngine.Parse.StructureElement + RPGEngine.Parse.StructElement RPGEngine.Render @@ -41,4 +41,4 @@ test-suite rpg-engine-test build-depends: base >=4.7 && <5, hspec <= 2.10.6, hspec-discover, rpg-engine other-modules: -- Parsing - ParseGameSpec, ParseStructureElementSpec + ParseGameSpec, ParseStructElementSpec diff --git a/test/ParseGameSpec.hs b/test/ParseGameSpec.hs index a162246..2a2d7d2 100644 --- a/test/ParseGameSpec.hs +++ b/test/ParseGameSpec.hs @@ -1,7 +1,10 @@ module ParseGameSpec where import Test.Hspec -import RPGEngine.Parse.StructureElement +import RPGEngine.Parse.StructElement +import RPGEngine.Data +import RPGEngine.Parse.Core +import RPGEngine.Parse.Game spec :: Spec spec = do @@ -14,39 +17,118 @@ spec = do pending describe "Player" $ do - it "TODO: Simple player" $ do - pending + it "cannot die" $ do + let input = "player: { hp: infinite, inventory: [] }" + correct = Player { + playerHp = Prelude.Nothing, + inventory = [] + } + Right (Entry (Tag "player") struct) = parseWith structElement input + structToPlayer struct `shouldBe` correct + + it "without inventory" $ do + let input = "player: { hp: 50, inventory: [] }" + correct = Player { + playerHp = Just 50, + inventory = [] + } + Right (Entry (Tag "player") struct) = parseWith structElement input + structToPlayer 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 + } + ] + } + Right (Entry (Tag "player") struct) = parseWith structElement input + structToPlayer struct `shouldBe` correct - describe "Inventory" $ do - it "TODO: Empty inventory" $ do + describe "Layout" $ do + it "simple" $ do pending - it "TODO: Singleton inventory" $ do - pending - it "TODO: Filled inventory" $ do - pending - + describe "Items" $ do - it "TODO: Simple item" $ do - pending - -- Check id - -- Check x - -- Check y - -- Check name - -- Check description - -- Check useTimes - -- Check value - -- Check actions + 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 structElement input + structToItem 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 structElement input + structToItem struct `shouldBe` correct describe "Actions" $ do - it "TODO: Simple action" $ do - pending - + it "no conditions" $ do + let input = "{[] leave()}" + correct = [([], Leave)] + Right struct = parseWith structElement input + structToActions struct `shouldBe` correct + + it "single condition" $ do + let input = "{ [inventoryFull()] useItem(itemId)}" + correct = [([InventoryFull], UseItem "itemId")] + Right struct = parseWith structElement input + structToActions 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 structElement input + structToActions struct `shouldBe` correct + describe "Entities" $ do it "TODO: Simple entity" $ do pending describe "Level" $ do - it "TODO: Simple layout" $ do - pending + 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] + ], + items = [], + entities = [] + } + Right struct = parseWith structElement input + structToLevel struct `shouldBe` correct it "TODO: Complex layout" $ do pending \ No newline at end of file diff --git a/test/ParseStructureElementSpec.hs b/test/ParseStructElementSpec.hs similarity index 91% rename from test/ParseStructureElementSpec.hs rename to test/ParseStructElementSpec.hs index bc70837..0f7464a 100644 --- a/test/ParseStructureElementSpec.hs +++ b/test/ParseStructElementSpec.hs @@ -1,10 +1,10 @@ -module ParseStructureElementSpec where +module ParseStructElementSpec where import Test.Hspec import RPGEngine.Data import RPGEngine.Parse.Core -import RPGEngine.Parse.StructureElement +import RPGEngine.Parse.StructElement spec :: Spec spec = do @@ -12,21 +12,21 @@ spec = do it "can parse blocks" $ do let input = "{}" correct = Right $ Block [] - parseWith structureElement input `shouldBe` correct + parseWith structElement input `shouldBe` correct let input = "{{}}" correct = Right $ Block [Block []] - parseWith structureElement input `shouldBe` correct + parseWith structElement input `shouldBe` correct let input = "{{}, {}}" correct = Right $ Block [Block [], Block []] - parseWith structureElement input `shouldBe` correct + parseWith structElement input `shouldBe` correct let input = "{ id: 1 }" correct = Right (Block [ Entry (Tag "id") $ Regular $ Integer 1 ], "") - parseWithRest structureElement input `shouldBe` correct + parseWithRest structElement input `shouldBe` correct let input = "{ id: \"key\", x: 3, y: 1}" correct = Right $ Block [ @@ -34,14 +34,14 @@ spec = do Entry (Tag "x") $ Regular $ Integer 3, Entry (Tag "y") $ Regular $ Integer 1 ] - parseWith structureElement input `shouldBe` correct + parseWith structElement 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 structureElement input `shouldBe` correct + parseWithRest structElement 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 [ @@ -52,7 +52,7 @@ spec = do Entry (Tag "direction") $ Regular $ Direction West, Entry (Tag "y") $ Regular $ Integer 1 ]], "") - parseWithRest structureElement input `shouldBe` correct + parseWithRest structElement 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 [ @@ -66,7 +66,7 @@ spec = do Entry (ConditionList []) $ Regular $ Action Leave ] ]], "") - parseWithRest structureElement input `shouldBe` correct + parseWithRest structElement 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 [ @@ -81,7 +81,7 @@ spec = do Entry (ConditionList []) $ Regular $ Action Leave ] ]], "") - parseWithRest structureElement input `shouldBe` correct + parseWithRest structElement input `shouldBe` correct it "can parse entries" $ do let input = "id: \"dagger\"" @@ -105,7 +105,7 @@ spec = do Entry (ConditionList [Not InventoryFull]) $ Regular $ Action $ RetrieveItem "key", Entry (ConditionList []) $ Regular $ Action Leave ], "") - parseWithRest structureElement input `shouldBe` correct + parseWithRest structElement input `shouldBe` correct it "can parse regulars" $ do let input = "this is a string" @@ -237,19 +237,19 @@ spec = do it "can parse directions" $ do let input = "up" correct = Right $ Direction North - parseWith RPGEngine.Parse.StructureElement.direction input `shouldBe` correct + parseWith RPGEngine.Parse.StructElement.direction input `shouldBe` correct let input = "right" correct = Right $ Direction East - parseWith RPGEngine.Parse.StructureElement.direction input `shouldBe` correct + parseWith RPGEngine.Parse.StructElement.direction input `shouldBe` correct let input = "down" correct = Right $ Direction South - parseWith RPGEngine.Parse.StructureElement.direction input `shouldBe` correct + parseWith RPGEngine.Parse.StructElement.direction input `shouldBe` correct let input = "left" correct = Right $ Direction West - parseWith RPGEngine.Parse.StructureElement.direction input `shouldBe` correct + parseWith RPGEngine.Parse.StructElement.direction input `shouldBe` correct it "can parse layouts" $ do let input = "| * * * * * * * *\n| * s . . . . e *\n| * * * * * * * *" @@ -258,7 +258,7 @@ spec = do [Blocked, Entrance, Walkable, Walkable, Walkable, Walkable, Exit, Blocked], [Blocked, Blocked, Blocked, Blocked, Blocked, Blocked, Blocked, Blocked] ] - parseWith RPGEngine.Parse.StructureElement.layout input `shouldBe` correct + parseWith RPGEngine.Parse.StructElement.layout input `shouldBe` correct describe "Brackets" $ do it "matches closing <" $ do From fb4bc5bb36cfaf78c5fd619dde6f408edfbaa4b9 Mon Sep 17 00:00:00 2001 From: Tibo De Peuter Date: Tue, 20 Dec 2022 22:52:06 +0100 Subject: [PATCH 08/27] #3 #2 Player render and movement --- assets/entities/player.png | Bin 0 -> 1548 bytes assets/unkown.png | Bin 0 -> 870 bytes lib/RPGEngine/Data.hs | 9 +++-- lib/RPGEngine/Data/Defaults.hs | 10 +++--- lib/RPGEngine/Input.hs | 15 +++++++- lib/RPGEngine/Input/Player.hs | 19 ++++++++++ lib/RPGEngine/Render.hs | 15 ++++++-- lib/RPGEngine/Render/Core.hs | 63 +++++++++++++++++++++++++++++++++ lib/RPGEngine/Render/GUI.hs | 10 ++++++ lib/RPGEngine/Render/Level.hs | 10 ++++++ lib/RPGEngine/Render/Player.hs | 11 ++++++ rpg-engine.cabal | 5 +++ 12 files changed, 158 insertions(+), 9 deletions(-) create mode 100644 assets/entities/player.png create mode 100644 assets/unkown.png create mode 100644 lib/RPGEngine/Input/Player.hs create mode 100644 lib/RPGEngine/Render/Core.hs create mode 100644 lib/RPGEngine/Render/GUI.hs create mode 100644 lib/RPGEngine/Render/Level.hs create mode 100644 lib/RPGEngine/Render/Player.hs diff --git a/assets/entities/player.png b/assets/entities/player.png new file mode 100644 index 0000000000000000000000000000000000000000..512b550531a4c32d733baebadb7e7f63c18d5e21 GIT binary patch literal 1548 zcmeAS@N?(olHy`uVBq!ia0vp^0wB!61|;P_|4(FKU~I{Bb`J1#c2+1T%1_J8No8Qr zm{>c}+T(D5NZbEqUF*A=U0r01LezJ!=ty4cB&gLAwJ?-@^6Y&JJe?;!dZe*KOr}OB zOg3gtX=#Bd%R%;z#giv5>Yh@O7nC9Rxb^VgAMgM2KmR8#@G5m#q^zOCm41p% zm}P$9wNs(j=1ksiFy(Uiw4HvYT`Xc7Svrk^0$e#$Z**K*H#tes^?9NE|BlzwPb{6V zn#HY;HDrNf+205CAFQLb?M+{navg0vCw}~=1oN)4Mz^zt`}$><7c#NTZ`9uVbp4ga zoBo%wb#;qacygG?NMQuI$ga#rU~}M5|gZGcd47d%8G=SO_u-u)P1x2vmX4}t^>p=fS?83{ F1OUsVNech~ literal 0 HcmV?d00001 diff --git a/assets/unkown.png b/assets/unkown.png new file mode 100644 index 0000000000000000000000000000000000000000..005de405e5848326d1858dc30647bb1e0114870e GIT binary patch literal 870 zcmV-s1DX7ZP)i%R0j>_=3B$BX!ZHof^^&Q0Te#QA zCRc?Ig@cw1D14=Kz0S{yGj)DfY$f6|;8s1gObhERY-Fvx@QCnWK36sx^QbTJr3&+N zQu}2OU=t@o7!92=Yvd>ql3I|={`QU5e6tkAW60a2f+da&S@8x%Elbyw)U@a+d2&=# z?!WW2)8k|H?!GXzKRM3Z5O{asyXrV!M~?G-44y~0C^Y>=$!YRK^lDQZJOKYDoLx3G zeHYHRAn;%&tz=E|$abgG@Olc}i_mieoy+yC8t0tcLt;aI(KYPt!?4` game{ state = Pause }) + -- Pause the game + handleKey (Char 'p') (\game -> game{ state = Pause }), + + -- Player movement + handleKey (SpecialKey KeyUp) $ movePlayer North, + handleKey (SpecialKey KeyRight) $ movePlayer East, + handleKey (SpecialKey KeyDown) $ movePlayer South, + handleKey (SpecialKey KeyLeft) $ movePlayer West, + + handleKey (Char 'w') $ movePlayer North, + handleKey (Char 'd') $ movePlayer East, + handleKey (Char 's') $ movePlayer South, + handleKey (Char 'a') $ movePlayer West ] -- Go to the next stage of the Game diff --git a/lib/RPGEngine/Input/Player.hs b/lib/RPGEngine/Input/Player.hs new file mode 100644 index 0000000..3b77917 --- /dev/null +++ b/lib/RPGEngine/Input/Player.hs @@ -0,0 +1,19 @@ +module RPGEngine.Input.Player +( movePlayer +) where + +import RPGEngine.Data (Game(..), Direction(..), Player(..), X, Y) + +movePlayer :: Direction -> Game -> Game +movePlayer dir g@Game{ player = p@Player{ coord = (x, y) }} = newGame + where newGame = g{ player = newPlayer } + newPlayer = p{ coord = newCoord } + newCoord = (x + xD, y + yD) + (xD, yD) = diffs dir + +diffs :: Direction -> (X, Y) +diffs North = (0, 1) +diffs East = (1, 0) +diffs South = (0, -1) +diffs West = (-1, 0) +diffs Center = (0, 0) diff --git a/lib/RPGEngine/Render.hs b/lib/RPGEngine/Render.hs index 03e7855..1468f77 100644 --- a/lib/RPGEngine/Render.hs +++ b/lib/RPGEngine/Render.hs @@ -8,7 +8,15 @@ module RPGEngine.Render ) where import RPGEngine.Data + ( State(..), + Game(..) ) +import RPGEngine.Render.Level + ( renderLevel ) + import Graphics.Gloss + ( white, pictures, text, Display(InWindow), Color, Picture ) +import RPGEngine.Render.Player (renderPlayer) +import RPGEngine.Render.GUI (renderGUI) ----------------------------- Constants ------------------------------ @@ -36,9 +44,12 @@ render g@Game{ state = Lose } = renderLose g renderMenu :: Game -> Picture renderMenu _ = text "[Press any key to start]" --- TODO renderPlaying :: Game -> Picture -renderPlaying _ = text "Playing" +renderPlaying g@Game{ playing = lvl, player = player } = pictures [ + renderLevel lvl, + renderPlayer player, + renderGUI g + ] -- TODO renderPause :: Game -> Picture diff --git a/lib/RPGEngine/Render/Core.hs b/lib/RPGEngine/Render/Core.hs new file mode 100644 index 0000000..0fe63ab --- /dev/null +++ b/lib/RPGEngine/Render/Core.hs @@ -0,0 +1,63 @@ +module RPGEngine.Render.Core where + +import Graphics.Gloss ( Picture, translate ) +import GHC.IO (unsafePerformIO) +import Graphics.Gloss.Juicy (loadJuicyPNG) +import Data.Maybe (fromJust) +import Graphics.Gloss.Data.Picture (scale) + +----------------------------- Constants ------------------------------ + +-- Default scale +zoom :: Float +zoom = 5.0 + +-- Resolution of the texture +resolution :: Float +resolution = 16 + +assetsFolder :: FilePath +assetsFolder = "assets/" + +unknownImage :: FilePath +unknownImage = "unkown.png" + +allEntities :: [(String, FilePath)] +allEntities = [ + ("player", "player.png"), + ("door", "door.png") + ] + +allItems :: [(String, FilePath)] +allItems = [ + ("dagger", "dagger.png"), + ("key", "key.png" ) + ] + +-- Map of all renders +library :: [(String, Picture)] +library = unknown:entities ++ environment ++ gui ++ items + where unknown = ("unkown", renderPNG (assetsFolder ++ unknownImage)) + entities = map (\(f, s) -> (f, renderPNG (assetsFolder ++ "entities/" ++ s))) allEntities + environment = [] + gui = [] + items = map (\(f, s) -> (f, renderPNG (assetsFolder ++ "items/" ++ s))) allItems + +---------------------------------------------------------------------- + +-- Turn a path to a .png file into a Picture. +renderPNG :: FilePath -> Picture +renderPNG path = scale zoom zoom $ fromJust $ unsafePerformIO $ loadJuicyPNG path + +-- 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 + +setRenderPos :: Int -> Int -> Picture -> Picture +setRenderPos x y = translate floatX floatY + where floatX = fromIntegral x * zoom * resolution + floatY = fromIntegral y * zoom * resolution \ No newline at end of file diff --git a/lib/RPGEngine/Render/GUI.hs b/lib/RPGEngine/Render/GUI.hs new file mode 100644 index 0000000..e29b012 --- /dev/null +++ b/lib/RPGEngine/Render/GUI.hs @@ -0,0 +1,10 @@ +module RPGEngine.Render.GUI +( renderGUI +) where + +import RPGEngine.Data (Game) +import Graphics.Gloss (Picture, blank) + +-- TODO +renderGUI :: Game -> Picture +renderGUI _ = blank diff --git a/lib/RPGEngine/Render/Level.hs b/lib/RPGEngine/Render/Level.hs new file mode 100644 index 0000000..267316c --- /dev/null +++ b/lib/RPGEngine/Render/Level.hs @@ -0,0 +1,10 @@ +module RPGEngine.Render.Level +( renderLevel +) where + +import Graphics.Gloss +import RPGEngine.Data + +-- TODO +renderLevel :: Level -> Picture +renderLevel _ = text "Level" \ No newline at end of file diff --git a/lib/RPGEngine/Render/Player.hs b/lib/RPGEngine/Render/Player.hs new file mode 100644 index 0000000..0d5f65c --- /dev/null +++ b/lib/RPGEngine/Render/Player.hs @@ -0,0 +1,11 @@ +module RPGEngine.Render.Player +( renderPlayer +) where + +import RPGEngine.Data (Player(..)) + +import Graphics.Gloss (Picture, text) +import RPGEngine.Render.Core (getRender, setRenderPos) + +renderPlayer :: Player -> Picture +renderPlayer Player{ coord = (x, y) } = setRenderPos x y $ getRender "player" diff --git a/rpg-engine.cabal b/rpg-engine.cabal index 0823b23..51f2809 100644 --- a/rpg-engine.cabal +++ b/rpg-engine.cabal @@ -19,6 +19,7 @@ library RPGEngine.Input RPGEngine.Input.Core + RPGEngine.Input.Player RPGEngine.Parse RPGEngine.Parse.Core @@ -26,6 +27,10 @@ library RPGEngine.Parse.StructElement RPGEngine.Render + RPGEngine.Render.Core + RPGEngine.Render.GUI + RPGEngine.Render.Level + RPGEngine.Render.Player executable rpg-engine main-is: Main.hs From 55212c14403428ac50cd5e8a8fc2d144fbb668bc Mon Sep 17 00:00:00 2001 From: Tibo De Peuter Date: Tue, 20 Dec 2022 23:59:35 +0100 Subject: [PATCH 09/27] #1 Rendering of level --- assets/entities/door.png | Bin 0 -> 1548 bytes assets/environment/entrance.png | Bin 0 -> 1548 bytes assets/environment/exit.png | Bin 0 -> 1548 bytes assets/environment/tile.png | Bin 0 -> 1548 bytes assets/environment/void.png | Bin 0 -> 1548 bytes assets/environment/wall.png | Bin 0 -> 1548 bytes assets/items/dagger.png | Bin 0 -> 734 bytes assets/items/key.png | Bin 0 -> 1548 bytes lib/RPGEngine/Render/Core.hs | 13 ++++++++- lib/RPGEngine/Render/Level.hs | 49 ++++++++++++++++++++++++++++++-- 10 files changed, 58 insertions(+), 4 deletions(-) create mode 100644 assets/entities/door.png create mode 100644 assets/environment/entrance.png create mode 100644 assets/environment/exit.png create mode 100644 assets/environment/tile.png create mode 100644 assets/environment/void.png create mode 100644 assets/environment/wall.png create mode 100644 assets/items/dagger.png create mode 100644 assets/items/key.png diff --git a/assets/entities/door.png b/assets/entities/door.png new file mode 100644 index 0000000000000000000000000000000000000000..f876321bc4b4808bab5eac8ec26ee9069a17944e GIT binary patch literal 1548 zcmeAS@N?(olHy`uVBq!ia0vp^0wB!61|;P_|4(FKU~I{Bb`J1#c2+1T%1_J8No8Qr zm{>c}+T(D5NZbEqUF*A=U0r01LezJ!=ty4cB&gLAwJ?-@^6Y&JJe?;!dZe*KOr}OB zOg3gtX=#Bd%R%;z#giv5>Yh@O7nC9Rxb^VgAMgM2KmR8#@G5m#q^zOCm41p% zm}P$9wNs(j=1ksiFy(Uiw4HvYT`Xc7Svrk^0$e#$Z**K*H#tes^?9NE|BlzwPb{6V zn#HY;HDrNf+205CAFQLb?M+{navg0vCw}~=1oN)4Mz^zt`}$><7c#NTZ`9uVbp4ga zoBo%wb#;qacygG?NMQuI$ga#rS!}jlHhlVqjpA_H=O!u@GbwV0r(WQB_##KTsK2;P%^Rj4Ubc+6-o5 z9AE=Zz4}2CJ8dz;e0%91C0q6y71P90hIXSzog+bQ95F)xOb`GW01ZV*%z@bj>hv3>ScGXE%>z zurp_1umd^^c}+T(D5NZbEqUF*A=U0r01LezJ!=ty4cB&gLAwJ?-@^6Y&JJe?;!dZe*KOr}OB zOg3gtX=#Bd%R%;z#giv5>Yh@O7nC9Rxb^VgAMgM2KmR8#@G5m#q^zOCm41p% zm}P$9wNs(j=1ksiFy(Uiw4HvYT`Xc7Svrk^0$e#$Z**K*H#tes^?9NE|BlzwPb{6V zn#HY;HDrNf+205CAFQLb?M+{navg0vCw}~=1oN)4Mz^zt`}$><7c#NTZ`9uVbp4ga zoBo%wb#;qacygG?NMQuI$ga#ke))PZx+yVqjpA_H=O!u@GbwV0r(W5vYVgRaojjh$a=>e*27(5vDo2 zu7xE1Q=4ia>Olrfm^knM+Joo+qq9ML7zW9M)PeZOY!;Zc6+zxGCYaXMP-8HcX9crQ zKKzX$2T}`jG1vfE14Fnbn9At3ei)NM1#eTU?ES6DKI_YK!lkF3nX+JWH7oIObmdKI;Vst0AJ@qssI20 literal 0 HcmV?d00001 diff --git a/assets/environment/exit.png b/assets/environment/exit.png new file mode 100644 index 0000000000000000000000000000000000000000..2d2a66e713055ebdaeadd847457cdd1371a18ce8 GIT binary patch literal 1548 zcmeAS@N?(olHy`uVBq!ia0vp^0wB!61|;P_|4(FKU~I{Bb`J1#c2+1T%1_J8No8Qr zm{>c}+T(D5NZbEqUF*A=U0r01LezJ!=ty4cB&gLAwJ?-@^6Y&JJe?;!dZe*KOr}OB zOg3gtX=#Bd%R%;z#giv5>Yh@O7nC9Rxb^VgAMgM2KmR8#@G5m#q^zOCm41p% zm}P$9wNs(j=1ksiFy(Uiw4HvYT`Xc7Svrk^0$e#$Z**K*H#tes^?9NE|BlzwPb{6V zn#HY;HDrNf+205CAFQLb?M+{navg0vCw}~=1oN)4Mz^zt`}$><7c#NTZ`9uVbp4ga zoBo%wb#;qacygG?NMQuI$ga#kjSV7V23rGBB`6d%8G=SO_u-u)O~bRDl5d3w-~BFhT^Cg-xCXS;?ta zKajZ?Y;5vO7^+AVU{Mv80y}wDtTDqy#XtsG14D-WVs%)(@h-55;qis@KzWY;X^YQ- ztzv{3km9aQlHGH5UPUzk#M9MK0}EchaSKeN6WHWY0?}Na6>0#wRtO)PJjH=H3=Dt= sB62#IC02(u6k+LLme_iR(R6?aqFcTmH+Q6+eE|~jboFyt=akR{0PvR=CjbBd literal 0 HcmV?d00001 diff --git a/assets/environment/tile.png b/assets/environment/tile.png new file mode 100644 index 0000000000000000000000000000000000000000..91f4b5d31886b61ce64043fc4209d9bdddb6fd3c GIT binary patch literal 1548 zcmeAS@N?(olHy`uVBq!ia0vp^0wB!61|;P_|4(FKU~I{Bb`J1#c2+1T%1_J8No8Qr zm{>c}+T(D5NZbEqUF*A=U0r01LezJ!=ty4cB&gLAwJ?-@^6Y&JJe?;!dZe*KOr}OB zOg3gtX=#Bd%R%;z#giv5>Yh@O7nC9Rxb^VgAMgM2KmR8#@G5m#q^zOCm41p% zm}P$9wNs(j=1ksiFy(Uiw4HvYT`Xc7Svrk^0$e#$Z**K*H#tes^?9NE|BlzwPb{6V zn#HY;HDrNf+205CAFQLb?M+{navg0vCw}~=1oN)4Mz^zt`}$><7c#NTZ`9uVbp4ga zoBo%wb#;qacygG?NMQuI$ga#rP!*7JhA6%D})P?djqeVj;*V!1De#qnU!if1on50Sh}TH`$th{{G6q zgikj}0U?0b079BU@)Q{W@;*fd5DWuCUZB_oAPZPP&H$N2Oo(H15j-7`n;s|%M39R{ b4WL38xL*@Ec}+T(D5NZbEqUF*A=U0r01LezJ!=ty4cB&gLAwJ?-@^6Y&JJe?;!dZe*KOr}OB zOg3gtX=#Bd%R%;z#giv5>Yh@O7nC9Rxb^VgAMgM2KmR8#@G5m#q^zOCm41p% zm}P$9wNs(j=1ksiFy(Uiw4HvYT`Xc7Svrk^0$e#$Z**K*H#tes^?9NE|BlzwPb{6V zn#HY;HDrNf+205CAFQLb?M+{navg0vCw}~=1oN)4Mz^zt`}$><7c#NTZ`9uVbp4ga zoBo%wb#;qacygG?NMQuI$ga#kh53BRHFXGcd47d%8G=SO_u-u)P1xs46V=AE=CMz(lrAsK!wP7#K#w gU^EOs*FVdQ&MBb@0D*YVPyhe` literal 0 HcmV?d00001 diff --git a/assets/environment/wall.png b/assets/environment/wall.png new file mode 100644 index 0000000000000000000000000000000000000000..cfe91c2f7e7c0373745f8ed8a51b25a3176cc860 GIT binary patch literal 1548 zcmeAS@N?(olHy`uVBq!ia0vp^0wB!61|;P_|4(FKU~I{Bb`J1#c2+1T%1_J8No8Qr zm{>c}+T(D5NZbEqUF*A=U0r01LezJ!=ty4cB&gLAwJ?-@^6Y&JJe?;!dZe*KOr}OB zOg3gtX=#Bd%R%;z#giv5>Yh@O7nC9Rxb^VgAMgM2KmR8#@G5m#q^zOCm41p% zm}P$9wNs(j=1ksiFy(Uiw4HvYT`Xc7Svrk^0$e#$Z**K*H#tes^?9NE|BlzwPb{6V zn#HY;HDrNf+205CAFQLb?M+{navg0vCw}~=1oN)4Mz^zt`}$><7c#NTZ`9uVbp4ga zoBo%wb#;qacygG?NMQuI$ga#rUNZ`bxezGcd47d%8G=SO_u-u)P1x=%erXAE*ok@N)1neE#tnECynu z1DG60oCQQENu!(bi9r-30S2F73^)ywyZ4llfe9{3CIjpOE_QY@wf|yZ`S|SOk_pf#Cp7%IKOw3SeR|8YG4cK;j@5fRi%FG+Y3m97qq?3%E2B ZBkq(J_-@vlC7@EC!PC{xWt~$(69CJfAY=dl literal 0 HcmV?d00001 diff --git a/assets/items/dagger.png b/assets/items/dagger.png new file mode 100644 index 0000000000000000000000000000000000000000..d1da3c30753e98239a7e53f8a81444a45a5c5be1 GIT binary patch literal 734 zcmV<40wMj0P)i%R0j>_=3B$BX!ZHof^^&Q0Te#QA zCRc?Ig@cw1D14=Kz0S{yGj)DfY$f6|;8s1gObhERY-Fvx@QCnWK36sx^QbTJr3&+N zQu}2OU=t@o7!92=Yvd>ql3I|={`QU5e6tkAW60a2f+da&S@8x%Elbyw)U@a+d2&=# z?!WW2)8k|H?!GXzKRM3Z5O{asyXrV!M~?G-44y~0C^Y>=$!YRK^lDQZJOKYDoLx3G zeHYHRAn;%&tz=E|$abgG@Olc}i_mieoy+yC8t0tcLt;aI(KYPt!?4`((uj4M8yg>gwtsHnKX1YAA^=LC6r~SOV)s@dC&ukd2|Cp$y30X=!Nz+4ukdMOOw8Mg}a% z94H%P2!=~G$MZ3q*>{=Y#f5JuTF_kplLWg2!~waa;`((4R literal 0 HcmV?d00001 diff --git a/assets/items/key.png b/assets/items/key.png new file mode 100644 index 0000000000000000000000000000000000000000..667429626659029db47946e6c28d7d7494682cb5 GIT binary patch literal 1548 zcmeAS@N?(olHy`uVBq!ia0vp^0wB!61|;P_|4(FKU~I{Bb`J1#c2+1T%1_J8No8Qr zm{>c}+T(D5NZbEqUF*A=U0r01LezJ!=ty4cB&gLAwJ?-@^6Y&JJe?;!dZe*KOr}OB zOg3gtX=#Bd%R%;z#giv5>Yh@O7nC9Rxb^VgAMgM2KmR8#@G5m#q^zOCm41p% zm}P$9wNs(j=1ksiFy(Uiw4HvYT`Xc7Svrk^0$e#$Z**K*H#tes^?9NE|BlzwPb{6V zn#HY;HDrNf+205CAFQLb?M+{navg0vCw}~=1oN)4Mz^zt`}$><7c#NTZ`9uVbp4ga zoBo%wb#;qacygG?NMQuI$ga#rOr;Pjt|6IXF)-wsU^AD237aU)xiDuk01blydnZ*G6O#r9*40dKNFzHJCjK8N phb)e10#F2(0+2W^IjjZ^9YJS%ORlr)zb*o~*wfX|Wt~$(69BPq2de-8 literal 0 HcmV?d00001 diff --git a/lib/RPGEngine/Render/Core.hs b/lib/RPGEngine/Render/Core.hs index 0fe63ab..0e51063 100644 --- a/lib/RPGEngine/Render/Core.hs +++ b/lib/RPGEngine/Render/Core.hs @@ -5,6 +5,7 @@ import GHC.IO (unsafePerformIO) import Graphics.Gloss.Juicy (loadJuicyPNG) import Data.Maybe (fromJust) import Graphics.Gloss.Data.Picture (scale) +import Graphics.Gloss.Data.Bitmap (BitmapData(..)) ----------------------------- Constants ------------------------------ @@ -28,6 +29,15 @@ allEntities = [ ("door", "door.png") ] +allEnvironment :: [(String, FilePath)] +allEnvironment = [ + ("void", "void.png"), + ("tile", "tile.png"), + ("wall", "wall.png"), + ("entrance", "entrance.png"), + ("exit", "exit.png") + ] + allItems :: [(String, FilePath)] allItems = [ ("dagger", "dagger.png"), @@ -39,7 +49,7 @@ library :: [(String, Picture)] library = unknown:entities ++ environment ++ gui ++ items where unknown = ("unkown", renderPNG (assetsFolder ++ unknownImage)) entities = map (\(f, s) -> (f, renderPNG (assetsFolder ++ "entities/" ++ s))) allEntities - environment = [] + environment = map (\(f, s) -> (f, renderPNG (assetsFolder ++ "environment/" ++ s))) allEnvironment gui = [] items = map (\(f, s) -> (f, renderPNG (assetsFolder ++ "items/" ++ s))) allItems @@ -57,6 +67,7 @@ getRender id = get filtered 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 diff --git a/lib/RPGEngine/Render/Level.hs b/lib/RPGEngine/Render/Level.hs index 267316c..4e01968 100644 --- a/lib/RPGEngine/Render/Level.hs +++ b/lib/RPGEngine/Render/Level.hs @@ -1,10 +1,53 @@ -module RPGEngine.Render.Level +module RPGEngine.Render.Level ( renderLevel ) where import Graphics.Gloss import RPGEngine.Data +import RPGEngine.Render.Core (getRender, setRenderPos, zoom, resolution) --- TODO renderLevel :: Level -> Picture -renderLevel _ = text "Level" \ No newline at end of file +renderLevel Level{ layout = l, items = i, entities = e } = level + where level = pictures [void, layout, items, entities] + void = createVoid + layout = renderLayout l + items = renderItems i + entities = renderEntities e + +renderLayout :: Layout -> Picture +renderLayout strips = pictures [setRenderPos 0 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 + +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} = setRenderPos x y image + where image = getRender id + +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) \ No newline at end of file From 5c8cee810467e85e8aa2f3761072d1784978049e Mon Sep 17 00:00:00 2001 From: Tibo De Peuter Date: Wed, 21 Dec 2022 13:37:38 +0100 Subject: [PATCH 10/27] #11 Start proper report --- .vscode/tasks.json | 19 ++++++++ README.md | 114 +++++++++++++++++++++++++++++++-------------- header.yaml | 15 ++++++ verslag.pdf | Bin 0 -> 45716 bytes 4 files changed, 113 insertions(+), 35 deletions(-) create mode 100644 header.yaml diff --git a/.vscode/tasks.json b/.vscode/tasks.json index 6c8f9a1..c27db8b 100644 --- a/.vscode/tasks.json +++ b/.vscode/tasks.json @@ -41,6 +41,25 @@ "kind": "build", "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": [ diff --git a/README.md b/README.md index 49869ed..d173f3f 100644 --- a/README.md +++ b/README.md @@ -1,12 +1,4 @@ -# RPG-Engine - -Schrijf een game-engine voor een rollenspel - -https://pixel-poem.itch.io/dungeon-assetpuck -https://kyrise.itch.io/kyrises-free-16x16-rpg-icon-pack - -# RPG Engine requirements - + + +# RPG-Engine + +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` | + +### Example playthrough + TODO -- Input commands etc -- An example playthrough +- An example playthrough, with pictures and explanations + +\pagebreak ## Writing your own stages @@ -85,7 +84,8 @@ A stage description file consists of several elements. | `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 | -We'll look at the following example to explain these concepts. +
+We'll look at the following example to explain these concepts. ```javascript player: { @@ -126,7 +126,7 @@ levels: [ id: "key", x: 3, y: 1, - name: "Doorkey", + name: "Door key", description: "Unlocks a secret door", useTimes: 1, value: 0, @@ -153,6 +153,7 @@ levels: [ } ] ``` +
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. @@ -247,4 +248,47 @@ If we look at the example, all the objects are length = 1 Condition ('inventoryContains(key)') Entry = empty ConditionList + Action ('leave()') -``` \ No newline at end of file +``` + +\pagebreak + +## Development notes + +### Assets & dependencies + +The following assets were used (and modified if specified): + +- Kyrise's Free 16x16 RPG Icon Pack[[1]](#1) + + Every needed asset was taken and put into its own `.png`, instead of in the overview. + +- 2D Pixel Dungeon Asset Pack by Pixel_Poem[[2]](#2) + + Every needed asset was taken and put into its own `.png`, instead of in the overview. + +RPG-Engine makes use of the following libraries: + +- [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. + +\pagebreak + +## References + +[1] [Kyrise's Free 16x16 RPG Icon Pack](https://kyrise.itch.io/kyrises-free-16x16-rpg-icon-pack) © 2018 + by [Kyrise](https://kyrise.itch.io/) is licensed under [CC BY 4.0](http://creativecommons.org/licenses/by/4.0/?ref=chooser-v1) + +[2] [2D Pixel Dungeon Asset Pack](https://pixel-poem.itch.io/dungeon-assetpuck) by [Pixel_Poem](https://pixel-poem.itch.io/) + is not licensed \ No newline at end of file diff --git a/header.yaml b/header.yaml new file mode 100644 index 0000000..21a20a4 --- /dev/null +++ b/header.yaml @@ -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 +--- diff --git a/verslag.pdf b/verslag.pdf index e69de29bb2d1d6434b8b29ae775ad8c2e48c5391..f2dba5d218c4dc468da027f6e2816048b04d1230 100644 GIT binary patch literal 45716 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

d>pSr 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

~)n6Oop+)(gX%XCdl)H)Ip7EVE&#>|`H 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 literal 0 HcmV?d00001 From 0786a410069125cabfd9becd319ceaf77e16e507 Mon Sep 17 00:00:00 2001 From: Tibo De Peuter Date: Wed, 21 Dec 2022 14:49:42 +0100 Subject: [PATCH 11/27] #3 Restrict player going places --- lib/RPGEngine/Data.hs | 9 +++--- lib/RPGEngine/Data/Defaults.hs | 7 +++-- lib/RPGEngine/Input/Level.hs | 27 +++++++++++++++++ lib/RPGEngine/Input/Player.hs | 53 +++++++++++++++++++++++++--------- lib/RPGEngine/Render/Player.hs | 2 +- rpg-engine.cabal | 1 + 6 files changed, 78 insertions(+), 21 deletions(-) create mode 100644 lib/RPGEngine/Input/Level.hs diff --git a/lib/RPGEngine/Data.hs b/lib/RPGEngine/Data.hs index 641f109..3a701a2 100644 --- a/lib/RPGEngine/Data.hs +++ b/lib/RPGEngine/Data.hs @@ -14,9 +14,10 @@ data Game = Game { ------------------------------- Level -------------------------------- data Level = Level { - layout :: Layout, - items :: [Item], - entities :: [Entity] + layout :: Layout, + coordlayout :: [(X, Y, Physical)], + items :: [Item], + entities :: [Entity] } deriving (Eq, Show) type Layout = [Strip] @@ -37,7 +38,7 @@ type Y = Int data Player = Player { playerHp :: Maybe Int, inventory :: [Item], - coord :: (X, Y) + position :: (X, Y) } deriving (Eq, Show) instance Living Player where diff --git a/lib/RPGEngine/Data/Defaults.hs b/lib/RPGEngine/Data/Defaults.hs index 75965a4..e3414eb 100644 --- a/lib/RPGEngine/Data/Defaults.hs +++ b/lib/RPGEngine/Data/Defaults.hs @@ -1,6 +1,8 @@ module RPGEngine.Data.Defaults where import RPGEngine.Data +import RPGEngine.Input.Player (spawnPlayer) +import RPGEngine.Input.Level (putCoords) defaultEntity :: Entity defaultEntity = Entity { @@ -21,7 +23,7 @@ initGame = Game { state = defaultState, playing = defaultLevel, levels = [defaultLevel], - player = defaultPlayer + player = spawnPlayer defaultLevel defaultPlayer } defaultItem :: Item @@ -46,6 +48,7 @@ defaultLayout = [ defaultLevel :: Level defaultLevel = Level { layout = defaultLayout, + coordlayout = putCoords defaultLevel, -- TODO This should go items = [], entities = [] } @@ -54,7 +57,7 @@ defaultPlayer :: Player defaultPlayer = Player { playerHp = Prelude.Nothing, -- Compares to infinity inventory = [], - coord = (0, 0) + position = (0, 0) } -- Default state of the game, Menu diff --git a/lib/RPGEngine/Input/Level.hs b/lib/RPGEngine/Input/Level.hs new file mode 100644 index 0000000..63391c9 --- /dev/null +++ b/lib/RPGEngine/Input/Level.hs @@ -0,0 +1,27 @@ +module RPGEngine.Input.Level +( putCoords +, findFirst +, whatIsAt +) where +import RPGEngine.Data (Level (..), Y, X, Physical(..)) + +-- 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 = zip [0::Int .. ] lay + numberedList = map (\(x, strip) -> (x, zip [0::Int ..] strip)) numberedStrips + +-- Find first position of a Physical +-- Graceful exit by giving Nothing if there is nothing found. +findFirst :: Level -> Physical -> Maybe (X, Y) +findFirst l@Level{ coordlayout = lay } physical = try + where matches = filter (\(x, y, v) -> v == physical) lay + try | not (null matches) = Just $ (\(x, y, _) -> (x, y)) $ head matches + | otherwise = Nothing + +-- What is located at a given position in the level? +whatIsAt :: (X, Y) -> Level -> Physical +whatIsAt pos lvl@Level{ coordlayout = lay } = try + where matches = map (\(_, _, v) -> v) $ filter (\(x, y, v) -> (x, y) == pos) lay + try | not (null matches) = head matches + | otherwise = Void diff --git a/lib/RPGEngine/Input/Player.hs b/lib/RPGEngine/Input/Player.hs index 3b77917..be56f27 100644 --- a/lib/RPGEngine/Input/Player.hs +++ b/lib/RPGEngine/Input/Player.hs @@ -1,19 +1,44 @@ -module RPGEngine.Input.Player -( movePlayer +module RPGEngine.Input.Player +( spawnPlayer +, movePlayer ) where -import RPGEngine.Data (Game(..), Direction(..), Player(..), X, Y) +import RPGEngine.Data (Game(..), Direction(..), Player(..), X, Y, Physical (..), Level(..)) +import RPGEngine.Input.Level (whatIsAt, findFirst) +import Data.Maybe (fromJust, isNothing) + +----------------------------- Constants ------------------------------ -movePlayer :: Direction -> Game -> Game -movePlayer dir g@Game{ player = p@Player{ coord = (x, y) }} = newGame - where newGame = g{ player = newPlayer } - newPlayer = p{ coord = newCoord } - newCoord = (x + xD, y + yD) - (xD, yD) = diffs dir diffs :: Direction -> (X, Y) -diffs North = (0, 1) -diffs East = (1, 0) -diffs South = (0, -1) -diffs West = (-1, 0) -diffs Center = (0, 0) +diffs North = ( 0, 1) +diffs East = ( 1, 0) +diffs South = ( 0, -1) +diffs West = (-1, 0) +diffs Center = ( 0, 0) + +---------------------------------------------------------------------- + +-- 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 = findFirst l Entrance + newPos | isNothing try = prevPos + | otherwise = fromJust try + +-- Move a player in a direction if possible. +movePlayer :: Direction -> Game -> Game +movePlayer dir g@Game{ player = p@Player{ position = (x, y) }} = newGame + where newGame = g{ player = newPlayer } + newPlayer = p{ position = newCoord } + newCoord | isLegalMove dir g = (x + xD, y + yD) + | otherwise = (x, y) + (xD, yD) = diffs dir + +-- Check if a move is legal by checking what is located at the new position. +isLegalMove :: Direction -> Game -> Bool +isLegalMove dir g@Game{ playing = lvl, player = p@Player{ position = (x, y) }} = legality + where legality = physical `elem` [Walkable, Entrance, Exit] + physical = whatIsAt newPos lvl + newPos = (x + xD, y + yD) + (xD, yD) = diffs dir \ No newline at end of file diff --git a/lib/RPGEngine/Render/Player.hs b/lib/RPGEngine/Render/Player.hs index 0d5f65c..7adb8b0 100644 --- a/lib/RPGEngine/Render/Player.hs +++ b/lib/RPGEngine/Render/Player.hs @@ -8,4 +8,4 @@ import Graphics.Gloss (Picture, text) import RPGEngine.Render.Core (getRender, setRenderPos) renderPlayer :: Player -> Picture -renderPlayer Player{ coord = (x, y) } = setRenderPos x y $ getRender "player" +renderPlayer Player{ position = (x, y) } = setRenderPos x y $ getRender "player" \ No newline at end of file diff --git a/rpg-engine.cabal b/rpg-engine.cabal index 51f2809..d7c3090 100644 --- a/rpg-engine.cabal +++ b/rpg-engine.cabal @@ -19,6 +19,7 @@ library RPGEngine.Input RPGEngine.Input.Core + RPGEngine.Input.Level RPGEngine.Input.Player RPGEngine.Parse From 2055ef234e1c9f9a596b56e4332440c4b2bb5036 Mon Sep 17 00:00:00 2001 From: Tibo De Peuter Date: Wed, 21 Dec 2022 16:07:05 +0100 Subject: [PATCH 12/27] #9 Added level selection render --- README.md | 34 +++++-- assets/environment/overlay.png | Bin 0 -> 494 bytes assets/{unkown.png => unknown.png} | Bin levels/level2.txt | 8 +- levels/level3.txt | 4 +- levels/level4.txt | 134 ++++++++++++++++++++++++++++ levels/level_more_levels.txt | 138 +++++++++++++++++++++++++++++ lib/RPGEngine/Data.hs | 1 + lib/RPGEngine/Data/State.hs | 2 +- lib/RPGEngine/Input.hs | 9 +- lib/RPGEngine/Input/LvlSelect.hs | 12 +++ lib/RPGEngine/Render.hs | 42 ++++++--- lib/RPGEngine/Render/Core.hs | 19 +++- lib/RPGEngine/Render/LvlSelect.hs | 15 ++++ lib/RPGEngine/Render/Player.hs | 14 ++- rpg-engine.cabal | 3 + verslag.pdf | Bin 45716 -> 47917 bytes 17 files changed, 401 insertions(+), 34 deletions(-) create mode 100644 assets/environment/overlay.png rename assets/{unkown.png => unknown.png} (100%) create mode 100644 levels/level4.txt create mode 100644 levels/level_more_levels.txt create mode 100644 lib/RPGEngine/Input/LvlSelect.hs create mode 100644 lib/RPGEngine/Render/LvlSelect.hs diff --git a/README.md b/README.md index d173f3f..cc195df 100644 --- a/README.md +++ b/README.md @@ -64,7 +64,7 @@ TODO - An example playthrough, with pictures and explanations -\pagebreak +

\pagebreak
## Writing your own stages @@ -250,10 +250,22 @@ If we look at the example, all the objects are Entry = empty ConditionList + Action ('leave()') ``` -\pagebreak +
\pagebreak
## Development notes +### Engine architecture + +TODO + +#### Monads/Monad stack + +TODO + +### Tests + +TODO + ### Assets & dependencies The following assets were used (and modified if specified): @@ -268,6 +280,7 @@ The following assets were used (and modified if specified): RPG-Engine makes use of the following libraries: +- [directory](https://hackage.haskell.org/package/directory) for listing levels in a directory - [gloss](https://hackage.haskell.org/package/gloss) for game rendering - [gloss-juicy](https://hackage.haskell.org/package/gloss-juicy) for rendering images - [hspec](https://hackage.haskell.org/package/hspec) for testing @@ -278,12 +291,23 @@ RPG-Engine makes use of the following libraries: The following ideas could (or should) be implemented in the future of this project. -- [ ] Entity system: With en ES, you can implement moving entities and repeated input. It also resembles the typical +- [ ] **Entity system:** With en ES, you can implement moving entities and repeated input. It also resembles the typical game loop more closely which can make it easier to implement other ideas in the future. +- [ ] **Game music:** Ambient game music and sound effects can improve the gaming experience I think. +- [ ] **Expand configuration file:** Implement the same methods for parsing stage description files to a configuration file, + containing keybinds, dimension sizes, even window titles, making this a truly customizable engine. +- [ ] **Camera follows player:** The camera should follow the player, making it always center. This allows for larger levels + increases the immersion of the game. -- [ ] Game music: Ambient game music and sound effects can improve the gaming experience I think. +
\pagebreak
-\pagebreak +## Conclusion + +Parsing was way harder than I initially expected. About half of my time on this project was spent writing the parser. + +TODO + +
\pagebreak
## References diff --git a/assets/environment/overlay.png b/assets/environment/overlay.png new file mode 100644 index 0000000000000000000000000000000000000000..b5d500db89f1d838bcfd00c03ec6ee645f4ca48e GIT binary patch literal 494 zcmVq5^NU#-bECLpy zjjzIgpsir5AP9;eV&xC;)ks+HB!L9cS!VCYnK_rcXTcTDXj!Xe3bHxdjLe37%h45| z`xOtpbTUa-OtXyX`S~#auCIHVRJ{wTUjN_wb9E$=mIh62!sD7@+QKpo;SJkRyd&JB zrDF-5~PEFpZvDX>lko1oO3WcIgjq~e<+FCIEP-^Y$qK7r;ZxXji4dC6(wQ}j|@^B;nD3(l|V zT6_;KwxRFQh?%j9)GXVj>+p64ol)q%h4$5IR<(0ZA7F7)e$fr=AHa~@_pbc_HOgor z9cee700009a7bBm000XU000XU0RWnu7ytkOBuPX;R5%f1R27!m%s>HPqChWL>!<+? k45MK%8U~=uF!1RB0A@D State -nextState Menu = Playing +nextState Menu = LvlSelect nextState Playing = Pause nextState Pause = Playing nextState _ = Menu diff --git a/lib/RPGEngine/Input.hs b/lib/RPGEngine/Input.hs index a737d05..2fae6bb 100644 --- a/lib/RPGEngine/Input.hs +++ b/lib/RPGEngine/Input.hs @@ -15,8 +15,9 @@ import Graphics.Gloss.Interface.IO.Game -- Handle all input for RPG-Engine handleAllInput :: InputHandler Game -handleAllInput ev g@Game{ state = Playing } = handlePlayInputs ev g -handleAllInput ev g = handleAnyKey setNextState ev g +handleAllInput ev g@Game{ state = Playing } = handlePlayInputs ev g +handleAllInput ev g@Game{ state = LvlSelect } = handleLvlSelectInput ev g +handleAllInput ev g = handleAnyKey setNextState ev g ---------------------------------------------------------------------- @@ -38,6 +39,10 @@ handlePlayInputs = composeInputHandlers [ handleKey (Char 'a') $ movePlayer West ] +-- Input for selection a level to load +handleLvlSelectInput :: InputHandler Game +handleLvlSelectInput = composeInputHandlers [] + -- Go to the next stage of the Game setNextState :: Game -> Game setNextState game = game{ state = newState } diff --git a/lib/RPGEngine/Input/LvlSelect.hs b/lib/RPGEngine/Input/LvlSelect.hs new file mode 100644 index 0000000..5b1bf35 --- /dev/null +++ b/lib/RPGEngine/Input/LvlSelect.hs @@ -0,0 +1,12 @@ +module RPGEngine.Input.LvlSelect +( getLvlList +) where + +import GHC.IO (unsafePerformIO) +import System.Directory (getDirectoryContents) + +lvlFolder :: FilePath +lvlFolder = "levels" + +getLvlList :: [FilePath] +getLvlList = unsafePerformIO $ getDirectoryContents lvlFolder \ No newline at end of file diff --git a/lib/RPGEngine/Render.hs b/lib/RPGEngine/Render.hs index 1468f77..09b9b66 100644 --- a/lib/RPGEngine/Render.hs +++ b/lib/RPGEngine/Render.hs @@ -9,14 +9,24 @@ module RPGEngine.Render import RPGEngine.Data ( State(..), - Game(..) ) -import RPGEngine.Render.Level + Game(..), Player (..) ) +import RPGEngine.Render.Level ( renderLevel ) - import Graphics.Gloss - ( white, pictures, text, Display(InWindow), Color, Picture ) -import RPGEngine.Render.Player (renderPlayer) + ( white, + pictures, + text, + Display(InWindow), + Color, + Picture, + scale, + translate ) +import RPGEngine.Render.Player (renderPlayer, focusPlayer) import RPGEngine.Render.GUI (renderGUI) +import Graphics.Gloss.Data.Picture (color) +import RPGEngine.Render.Core (overlay) +import RPGEngine.Input.LvlSelect (getLvlList) +import RPGEngine.Render.LvlSelect (renderLvlList) ----------------------------- Constants ------------------------------ @@ -32,11 +42,12 @@ initWindow = InWindow -- Render the game render :: Game -> Picture -render g@Game{ state = Menu } = renderMenu g -render g@Game{ state = Playing } = renderPlaying g -render g@Game{ state = Pause } = renderPause g -render g@Game{ state = Win } = renderWin g -render g@Game{ state = Lose } = renderLose g +render g@Game{ state = Menu } = renderMenu g +render g@Game{ state = LvlSelect } = renderLevelSelection g +render g@Game{ state = Playing } = renderPlaying g +render g@Game{ state = Pause } = renderPause g +render g@Game{ state = Win } = renderWin g +render g@Game{ state = Lose } = renderLose g ---------------------------------------------------------------------- @@ -44,6 +55,10 @@ render g@Game{ state = Lose } = renderLose g renderMenu :: Game -> Picture renderMenu _ = text "[Press any key to start]" +-- TODO +renderLevelSelection :: Game -> Picture +renderLevelSelection _ = renderLvlList getLvlList + renderPlaying :: Game -> Picture renderPlaying g@Game{ playing = lvl, player = player } = pictures [ renderLevel lvl, @@ -51,9 +66,12 @@ renderPlaying g@Game{ playing = lvl, player = player } = pictures [ renderGUI g ] --- TODO renderPause :: Game -> Picture -renderPause _ = text "[Press any key to continue]" +renderPause g = pictures [renderPlaying g, pause] + where pause = pictures [ + overlay, + color white $ scale 0.5 0.5 $ text "[Press any key to continue]" + ] -- TODO renderWin :: Game -> Picture diff --git a/lib/RPGEngine/Render/Core.hs b/lib/RPGEngine/Render/Core.hs index 0e51063..e5155f4 100644 --- a/lib/RPGEngine/Render/Core.hs +++ b/lib/RPGEngine/Render/Core.hs @@ -1,6 +1,6 @@ module RPGEngine.Render.Core where -import Graphics.Gloss ( Picture, translate ) +import Graphics.Gloss ( Picture, translate, pictures ) import GHC.IO (unsafePerformIO) import Graphics.Gloss.Juicy (loadJuicyPNG) import Data.Maybe (fromJust) @@ -21,7 +21,7 @@ assetsFolder :: FilePath assetsFolder = "assets/" unknownImage :: FilePath -unknownImage = "unkown.png" +unknownImage = "unknown.png" allEntities :: [(String, FilePath)] allEntities = [ @@ -32,6 +32,7 @@ allEntities = [ allEnvironment :: [(String, FilePath)] allEnvironment = [ ("void", "void.png"), + ("overlay", "overlay.png"), ("tile", "tile.png"), ("wall", "wall.png"), ("entrance", "entrance.png"), @@ -47,7 +48,7 @@ allItems = [ -- Map of all renders library :: [(String, Picture)] library = unknown:entities ++ environment ++ gui ++ items - where unknown = ("unkown", renderPNG (assetsFolder ++ unknownImage)) + where unknown = ("unknown", renderPNG (assetsFolder ++ unknownImage)) entities = map (\(f, s) -> (f, renderPNG (assetsFolder ++ "entities/" ++ s))) allEntities environment = map (\(f, s) -> (f, renderPNG (assetsFolder ++ "environment/" ++ s))) allEnvironment gui = [] @@ -71,4 +72,14 @@ getRender id = get filtered setRenderPos :: Int -> Int -> Picture -> Picture setRenderPos x y = translate floatX floatY where floatX = fromIntegral x * zoom * resolution - floatY = fromIntegral y * zoom * resolution \ No newline at end of file + floatY = fromIntegral y * zoom * resolution + +overlay :: Picture +overlay = setRenderPos offX offY $ pictures voids + where voids = [setRenderPos x y void | x <- [0 .. width], y <- [0 .. height]] + void = getRender "overlay" + intZoom = round zoom :: Int + height = round $ 4320 / resolution / zoom + width = round $ 7680 / resolution / zoom + offX = negate (width `div` 2) + offY = negate (height `div` 2) \ No newline at end of file diff --git a/lib/RPGEngine/Render/LvlSelect.hs b/lib/RPGEngine/Render/LvlSelect.hs new file mode 100644 index 0000000..b395e9d --- /dev/null +++ b/lib/RPGEngine/Render/LvlSelect.hs @@ -0,0 +1,15 @@ +module RPGEngine.Render.LvlSelect +( renderLvlList +) where + +import Graphics.Gloss ( Picture, pictures, translate, scale ) +import Graphics.Gloss.Data.Picture (blank, text) +import RPGEngine.Render.Core (resolution, zoom) + +-- Render all level names, under each other. +renderLvlList :: [FilePath] -> Picture +renderLvlList list = pictures $ map render entries + where entries = zip [0::Int .. ] list + render (i, path) = scale zoomed zoomed $ translate 0 (offset i) $ text path + zoomed = 0.1 * zoom + offset i = negate (2 * resolution * zoom * fromIntegral i) \ No newline at end of file diff --git a/lib/RPGEngine/Render/Player.hs b/lib/RPGEngine/Render/Player.hs index 7adb8b0..0b6a124 100644 --- a/lib/RPGEngine/Render/Player.hs +++ b/lib/RPGEngine/Render/Player.hs @@ -1,11 +1,17 @@ module RPGEngine.Render.Player ( renderPlayer +, focusPlayer ) where -import RPGEngine.Data (Player(..)) - +import RPGEngine.Data (Player(..), Game(..)) import Graphics.Gloss (Picture, text) -import RPGEngine.Render.Core (getRender, setRenderPos) +import RPGEngine.Render.Core (getRender, setRenderPos, zoom, resolution) +import Graphics.Gloss.Data.Picture (translate) renderPlayer :: Player -> Picture -renderPlayer Player{ position = (x, y) } = setRenderPos x y $ getRender "player" \ No newline at end of file +renderPlayer Player{ position = (x, y) } = setRenderPos x y $ getRender "player" + +focusPlayer :: Game -> Picture -> Picture +focusPlayer Game{ player = Player{ position = (x, y)}} = translate centerX centerY + where centerX = resolution * zoom * fromIntegral (negate x) + centerY = resolution * zoom * fromIntegral (negate y) \ No newline at end of file diff --git a/rpg-engine.cabal b/rpg-engine.cabal index d7c3090..9e11d89 100644 --- a/rpg-engine.cabal +++ b/rpg-engine.cabal @@ -8,6 +8,7 @@ library hs-source-dirs: lib build-depends: base >= 4.7 && <5, + directory >= 1.3.6.0, gloss >= 1.11 && < 1.14, gloss-juicy >= 0.2.3, parsec >= 3.1.15.1 exposed-modules: @@ -20,6 +21,7 @@ library RPGEngine.Input RPGEngine.Input.Core RPGEngine.Input.Level + RPGEngine.Input.LvlSelect RPGEngine.Input.Player RPGEngine.Parse @@ -31,6 +33,7 @@ library RPGEngine.Render.Core RPGEngine.Render.GUI RPGEngine.Render.Level + RPGEngine.Render.LvlSelect RPGEngine.Render.Player executable rpg-engine diff --git a/verslag.pdf b/verslag.pdf index f2dba5d218c4dc468da027f6e2816048b04d1230..24a6e81e610d8d2ab0dd84449bf2170ccd1ec824 100644 GIT binary patch delta 20463 zcmZs?V{m3s*DV^`w$rhbC$??db~@@LPi)(^ZKq?~HahHBr{DLxb?Tmb&yQ6#*Is+A zU8`zVjk(7dGx->-<_WAm9u$--DFKukoD;y3lmLSP{G&S_y9Ge$dDc9G4&7JT@(=Z2 zX~yVpqi|*Okufnm5cL_qDHFq@QPE|JWqZnJv9ct=;$Zl#uSzZRfji_ zar&~_?ZXG-Gl)O0wXTIMr_=r3;RQU>qyB)M>9EbCIQ0y}pD7t7Hjv48jaAH9y%;N4 zd||T&Hsg$xd0y$##NO%gbPFuLW=M=YV-Ws)-#^|i0CIom*Tt|gx;?y~4!t9LJ|KVe z*m9}vaC`?F=i$$RtvcR*)Sa{qF+nSsosy$(aQO)E`n@jM@xO7f$OFLA!N^bf7Gu3A z(Pm%9QqY|(tU?ss79Yp3*P-FCtBOoN)*n_$U{%tM0&%GS*&k)W{v1J-f2{0{5oa+>5wUdp0^sBqfbg=uDB zD$37N{?K1a&Kq#FR^`NLyN=+vE!A!D!nDI$(|qOCDiSE!(kTeGwBo|>C0nz&G-bC! zF+#VmKQfEU!waq9ghVkRr1hWwBpu!tX!PRWs(Iq%M7yx#Q8(G-H|Eag;zGkJq?|~F z;|?ZgwJKQQI5L)ASmPNM!K$yJ`+KDW%U2C{87pbEMNS>WF!z)%s*7_a4cnBI#*wd> zNG8Ku&Y$8-dc3Oc-wImPCB7>F;@D)=RgmI?@atV2C zsMBwNv+b3Z+mPE1HMQoK)07jXY+b=J6qc3SjP10L3_ldN`{Lx^KvK0FF&+FaY;;!b zRcsrPdn1LEqOp7|PWHOm6qYOf>O#!pog&jj>Vp zHJ3^sBB=!o64|U9j#Wn65CX~ycyg7D)W%YS4}Mj0M_u3)=K=nDY*ZD zGdtXor%s?u{NiZAOT_HWfTKRa98KQMLs?!m0~)e*$+kTzK&U#4Je<)xGQv zQJzA=&ivhjW;;RrXNIaYQAb(hoDtJ&$RkiS?dEUFBUO^T@d;3ASsOr4Z;b@_5m05; zfg-0kuIUFCT*qL_;FRmUJDbQ*;!O)yMPPvZxVf3p`_5m|d!NdRElc}wn(sd}R=v0i z^rJ#dsaK|8qKe3Ei!Xlx66hzWVzxD9;}jblkuQ7+Oh~M=$8t%EWI~m-3enff zQ7l|h<&hMVo~yD}iX2r?b07??1_y)7ETh}Oli}MmZTO1=c0j@l6lGx{9UPm5uNP99 z=$5Tha66@n9g)rSDa^>Xi0vrVv~xAbOkytKn~CbV#G+5f(lYhdXJUN5a_!NcsR(6n(;}))08e)fZ|om46-731u{E~~7g}|g3S^6Ls5{a%&;(~8q}9}Zh|C8|i&k0xUifO2 zpG=K(KCIOSy3?l=QvXSj&6DrxaLawuhg_#YmqMA|BS7z34i3j@)wAVv)!@p7o3Gu><07aD4OL?3^}Mtwv{firh7b9HkuH@5#z2tvkLyvd;FkFl#$E! zx~bpW>nkAdSk~8Jry_OSxx1ri*gH79yZ^E!Z9F*li5}MDu|gBy7xvKCX{aEWXQrXc z`uMXf8D@rNxAF!GHJbK97CYs^$!3WX1Coz`oM5$Bt{afGZ?F{GHxflOb5}V?RaOqv zqEkz>G8J`0>2ao>Pkl}5Ff5W*=gZsMI#L#cT8s2F99qHA9vz}rYqgK3-Y>cAhZ;RP4fU-H(Zu4&Zu}{_KqX?8=Mo-4?rEs3WTvkDWJ!M$h%N+NydfzBZ=S^%3tdgaE-B)BjfbS6wj`}`fF6H%SH z(jKx{$K4}Jw_3$%=I`$aeMd8l&gOVfcf(tQG4 z{#ESLMl^=#Jk#9=40YM;tsatykc&SEic-Q$u9gQupO{Ot4*~Q+6H+2#N1Ua((o@k zk?YO9EZ89eyW}hSJ4a^5AqQ+*vol&*4@B3gOZ~iUX82u9sLW%-*3CeM6UB3Flk`oq zbagP8Pr)H1*655&Qv_4hk5gO;6@;p8m>&HVbQHPkxP^5sn&vKE2GP4vzh_4UoCA=o z$rRn0CIe|bIWoTR5hxqlvTc^xLFm=3fZuDG1K4A=Y_T@KY}G5n*VVf17(x9l)#|hD z=&5{EPWT^Qh0l5gtptDouKmhnTXb~4ZJs^rx5jbsAtZShtHfWXPn(P2CQwIm(2qtz znvdzj5flrQg=-8(3loM=+4@VA3}M&Nj8WZzY+t> zT1C02Y4E#$R50=e$)#KQbzNw9xJ79Q9@VC@RKuLG~{hD!vFzOP&YlugIx9)54F zUI+p#H#ZT?g8|Ubfw3Vewo8jN(1SXEQ2hWz`YZ9(a-^Nsc4bG*34e5}2N$}avDWD} zT09EV&i4BS8d{PXGsR`YQqGC3%(Q(pDl!B3RYz<$qc)V^k<|H8#u*z$It3v!x|JLO zz0KomFCAmG>+C>u&aT18`;b5Jn(_&KX|@%TyzL2_S|ihRUM_M01+cxYPFC;*iq8Wz z*L!=xETxM&QKTV-#D0*kep_Lc{=q2;$$W~u=^;4J1T}JFo5qPM6{gZ%W#AS%vUAJo zDQ$xn?%Etn$IS!BGCe53=|Y;U2ivSn66_$O%rHb)6Hs7Pl*=6m<9&7o1IFnvUnj^G zg5?_w>N=XForvz%8o{D+8Qrnuofsl()mDI}1THy~Qq|FkTnbT;vfXg08?fWB5xYlZ zp~=kDW(RBzrbu)WsoO-xticDI4=J$wcT0utML#wtw*GcC*Poe!<%wG@*+o6NUm*}G z+8>EDjb=bTaZ(*Cig792!*vy$MEM_Gtj##B6oUO8u%!?$?9ZrcB?+c7_ElboQ*@8~ zT++t*P7GabmyUwh>$y*&Yz^kj#2#X9fv)r-_{}((3v%rPVciPI#tP3-9P9y|_Rqt1 z2k!cbE@94wrj4vCGiV)bf_6;CB`8BPp)0m)91Or-R@F=htT5J{+DZw1eLPa81|60o zL=n@9w~Klh+EMi-9;VQZ5ern-;_J1koHAj{6sH|RNW$z;2}V}oi&X76+N5f@cQr32 zw0)1psd(RCTGOnLtdm>8ET}rR(8Kb5@!ouU7Dr%^jc&|d{QCWfed$9?H!`J_LK1r8 zYi>Y=<>?|dea;(*`6=8)b(CtKva2chuiAB8COKOGJjIajUvDQhBaQz*)^nDGN81P0E{sbSx)F7$rN-BucryLK4?jR1+uRp2M&`zhK z16oxE{SPtW-A_}|RZLjnd#UifI%EP$lZ**tsSe5P>e8pP(O~Z=!=6$A7dpNJQ}Oa!m!V-iI+-dl;K-+IP`Bb(4V3&RPfi1PO<;)1pr z4Opw{paUidjz@gn$Msg!Or9HG)8hy%2|g!=3SGbpT6>)E(`>lmgt))U6z8}p*gz*yM-Qxkx*1Gtm++%SPDYLk&$%qZRObbrCC z{>r98j4;ov1@tiRwwWv9C(540tNIDnoNf@oK#)_kOweW$T-WE`ugjXMA6A42#IdMr=b~#CQ zSZ4Um#@~%%%CxW`Y<>YghH^Yw1-=%;Y*~x|ohQWQm1%jhhIq6~Y%Sb8uEBILK&T|G zume{1ZzJ}~k;T>qi>wFr&^Gv|WSn5kEJtNzle+G8z>buaW@5v0Mixz85pkgGLxJ@yXH1Ye-uGqU=R=C2i>!boV^rSdYnkF>n z4N!P_i9E;i56G1+?4+b;7H)7(Ztf&y9%68=|I~{ANR^h%O$YpO)Xwc~NuFxb%G$Uh2e~M7XW_D8+4Y7b^tf$QzO}1nPjLhr4%FA2LxM41I zIhY^#usay!CFxe(&m-7}wg8s&vu;$Dh`s~)SWInIn;a?jm^KPr=a4YDgGb_0*bJ7y z@w@svYM{j-VO%E8Y!MbtQ+UFDl*@C3^K09w)! zzVU8sy8G@lpJ6om^Fm>EjK5P_jbgP^s>*Ol!N5?JkY~I>zDT4FBD4ogLN?;kh(RCm zy6$S-z2OYKjvp)~4e->;kW$pBUPQQRv&lG3Bx#Luc!$vff$}EO^J-X?)CSsJ1wX|l z!}ix!j##QM&_H$QcnV+u3L(pxLs;B$c%9Par3Fc6v}afA>UL?q2RiKlEZZ}r{*ea| zW}w6I*ma~REXkdyoC2c)gysYdEmG|sO4yBgUH)=>dGugMen7c6yi1{OzF(4O8cY|(@^3Q3*b?}QltT*A9mOJU!kHC+W>?~rxGf2NrA2zmaCT!xD=d$6)KUZ(= z<_jb*7Ogs43}sE2!ppla?^mB62cyGOCoU4x+0~Z2wir>5vp;hw(eq82`V0;R7QR1L z`yxs$^pz#8HR>N8x0jbS`LYIw5eMhjGdV*H!Pou@-sX0$)Iu(%Re8Qy!y5Jl(BU_T-R;SZY$mt z6PI7BwrHbXW|x9A{AP4oM?gGBu><}v%<*7~S-)+((dgze)cYO}z4-}Er0wlU(y|sG z0h(652$3DdARkn$HSN0@u=V)_Da%8KWilsmFIl^crMD2ZR;}SY)0&+J{Q#oK-D~f1 z2S|u3#;iK0T(uHzS!Dl%Q7XNzLg2JxMnhIS5(&PnT#CvKNwN?Ye^wG83%GKZ_rnzGMON~u-*{WM@{l&hMKQzECeSgvOm_^ zJb$MI1GmiCUm+fWv>hOV69Q}x;}9*bM5MfVvfNbwfg<;+lJuhu?Kh&ZdT9(wRgEGQFJw??5`F-4vA+=^dH4jxnFCOm3VfdTVRJb01yJA?Xq?f58q7@rGA%fMv4x;4e^)bP;Tk0qIWL=5{GYWmwcao1GK_2=k_tma^$Z<9e%AAz)Tg<_-n7R z^>iFcaR#-vJN!v0gA#C^Aw4~TwOyBaMQECU#k4p;k^2S<1I;Vt33$_;8}?h?aCZ^* z0-cH)npYV29HJyf7B|bKI66Ut*M!Wca|wA)HrVz0`>Rdoi*G9*l*8(t>*u<4{t-l{Ab+s40N@kK*MOFp|`tq_0WyF|c-=C$^97md`18g?vg`WrAQVLNb+{6@lPYObg?#mOEn z)#ed=$6=rvdX)Ssy=bLMIdDTf7G()9_W>pvwzg~@Ai}(HCP^=U(O{bV#Qtn;Tl6l5 zLX@}&`dO7K<$*Gp{8*L^eI>oJ>y5ORL1wH+Ac*xL;eXB4&XL?_f8=X_p4VJ_Fl4N@ z)E1U^uws_Cva-8k_USDD@yW^aP4GyN@M+ulIr?B$^i}3BR0YwvKul8R+xR^?JNas3 zaoV;h%WGI2R@L-PZ`s5)o=1_#;3$wiT^4o8#sC+l#)!Ore|z-nuQ03I2r=p>-Zrrf zl$^sqM8^0>+5DqslAL%rlPWwAlQ1ROo6dN~Aiy~Pqj69*tqb;oCLx3&CS`*XCJhL4 zfN`-U!AS}=af#G}fpM`Xff~^?p^K-0f^q#<6G>W-0Dy6EH^E7cgMo1~|9AOIIsp`n z`(K48>jK8j@o%?albGBF3K;i)zD(65sow?$&dU6scNM7w`_(|<$;rWGA>)rhK+30~ z@BjaV6Du>ze-44*VELa?A!)`43CJdeJ{^UnDaOEhJr;>&a@cp@_ou`s{1ouR&5z zzQCdV=$1C!)+O_p3fHw(DZ!2=dbi8WF=jTZ*HU|3vxH1=Ky@-su>@M)IIg*q+sQio(PB30qiSJd}OPL~*24MjxLTR);no7D>w;@+Nf}kK=A-e3)HIEzBc) z6asQoGR@J~LcUT3nG&NuF%Yl#^{BdEY>MbYkeprxFSIWr`7OjAUR?}oyiLIn4Xc-* z)k-yA#75xZGD8s!X}`@2Na^E}FM=un9Em6)7zC<#U{y;&Gkp69E0>_fVOj(L*@!QP= ziY4hKlbOl_wJCyIr&Y(jyQ&+^fV3{$BSv$IZa?LI!t=NrZijBic895Z*Na$MfQF)v zLcBPgKy>EtfPFuV5NKQ`ozCl=`7)+Gus%J0uw*OUVqyJiU1Ie)ku$qJT627UAL!NP z^?9Uu6i1=7tF-Qew5zcx^ee?Z+db$hKel29HD|Qb@SJG=v%F1(Nwm%oSl}1ldlA)O z80_n;l}{Ir%iQFqSiw%9HL?oKQO5?OH0y^W81do=5zvIoM}fszw66|Io49uDP1?XN-juZD zb=3LB%QVU80$m*LO2$XVQbeNWaQDe8LHVR!Vk;$yj@aPD5z^yJllMdl;d&i|g;Mq( z1f(>6Rh%Qw#*p|2M%>^Dz4+)q4x]rz2DC;fV;g?U+&ol8SI5_v4JBZcVQ@2f&; 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 Tuen1fD+z_t^7CsLYSR7>vKoPk delta 18270 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 R0&HxdP&5Dn!TjU~_!lAx;`{&r From dab6fadad4886424a1c803f11bfc94b9edee2d99 Mon Sep 17 00:00:00 2001 From: Tibo De Peuter Date: Wed, 21 Dec 2022 23:30:59 +0100 Subject: [PATCH 13/27] Restructuring, #9 --- README.md | 10 ++ lib/Input.hs | 10 ++ lib/RPGEngine.hs | 31 +--- lib/RPGEngine/Config.hs | 36 ++++ lib/RPGEngine/Data.hs | 163 ++++++++---------- lib/RPGEngine/Data/Default.hs | 67 +++++++ lib/RPGEngine/Data/Defaults.hs | 65 ------- lib/RPGEngine/Data/Game.hs | 22 +++ lib/RPGEngine/Data/Level.hs | 36 ++++ lib/RPGEngine/Data/State.hs | 22 --- lib/RPGEngine/Input.hs | 49 +----- lib/RPGEngine/Input/Core.hs | 25 +-- lib/RPGEngine/Input/Level.hs | 27 --- lib/RPGEngine/Input/LevelSelection.hs | 49 ++++++ lib/RPGEngine/Input/Lose.hs | 13 ++ lib/RPGEngine/Input/LvlSelect.hs | 12 -- lib/RPGEngine/Input/Menu.hs | 36 ++++ lib/RPGEngine/Input/Paused.hs | 12 ++ lib/RPGEngine/Input/Player.hs | 44 ----- lib/RPGEngine/Input/Playing.hs | 80 +++++++++ lib/RPGEngine/Input/Win.hs | 13 ++ lib/RPGEngine/Parse.hs | 27 ++- lib/RPGEngine/Parse/Core.hs | 22 ++- lib/RPGEngine/Parse/Game.hs | 101 ----------- lib/RPGEngine/Parse/StructureToGame.hs | 120 +++++++++++++ .../{StructElement.hs => TextToStructure.hs} | 45 ++--- lib/RPGEngine/Render.hs | 92 +++------- lib/RPGEngine/Render/Core.hs | 45 +++-- lib/RPGEngine/Render/GUI.hs | 10 -- lib/RPGEngine/Render/LevelSelection.hs | 33 ++++ lib/RPGEngine/Render/Lose.hs | 14 ++ lib/RPGEngine/Render/LvlSelect.hs | 15 -- lib/RPGEngine/Render/Menu.hs | 14 ++ lib/RPGEngine/Render/Paused.hs | 20 +++ lib/RPGEngine/Render/Player.hs | 17 -- lib/RPGEngine/Render/{Level.hs => Playing.hs} | 72 ++++++-- lib/RPGEngine/Render/Win.hs | 14 ++ rpg-engine.cabal | 40 +++-- test/{ParseGameSpec.hs => Parser/GameSpec.hs} | 55 +++--- .../StructureSpec.hs} | 43 +++-- test/{RPGEngineSpec.hs => Spec.hs} | 0 41 files changed, 941 insertions(+), 680 deletions(-) create mode 100644 lib/Input.hs create mode 100644 lib/RPGEngine/Config.hs create mode 100644 lib/RPGEngine/Data/Default.hs delete mode 100644 lib/RPGEngine/Data/Defaults.hs create mode 100644 lib/RPGEngine/Data/Game.hs create mode 100644 lib/RPGEngine/Data/Level.hs delete mode 100644 lib/RPGEngine/Data/State.hs delete mode 100644 lib/RPGEngine/Input/Level.hs create mode 100644 lib/RPGEngine/Input/LevelSelection.hs create mode 100644 lib/RPGEngine/Input/Lose.hs delete mode 100644 lib/RPGEngine/Input/LvlSelect.hs create mode 100644 lib/RPGEngine/Input/Menu.hs create mode 100644 lib/RPGEngine/Input/Paused.hs delete mode 100644 lib/RPGEngine/Input/Player.hs create mode 100644 lib/RPGEngine/Input/Playing.hs create mode 100644 lib/RPGEngine/Input/Win.hs delete mode 100644 lib/RPGEngine/Parse/Game.hs create mode 100644 lib/RPGEngine/Parse/StructureToGame.hs rename lib/RPGEngine/Parse/{StructElement.hs => TextToStructure.hs} (86%) delete mode 100644 lib/RPGEngine/Render/GUI.hs create mode 100644 lib/RPGEngine/Render/LevelSelection.hs create mode 100644 lib/RPGEngine/Render/Lose.hs delete mode 100644 lib/RPGEngine/Render/LvlSelect.hs create mode 100644 lib/RPGEngine/Render/Menu.hs create mode 100644 lib/RPGEngine/Render/Paused.hs delete mode 100644 lib/RPGEngine/Render/Player.hs rename lib/RPGEngine/Render/{Level.hs => Playing.hs} (54%) create mode 100644 lib/RPGEngine/Render/Win.hs rename test/{ParseGameSpec.hs => Parser/GameSpec.hs} (78%) rename test/{ParseStructElementSpec.hs => Parser/StructureSpec.hs} (87%) rename test/{RPGEngineSpec.hs => Spec.hs} (100%) diff --git a/README.md b/README.md index cc195df..060cb52 100644 --- a/README.md +++ b/README.md @@ -258,6 +258,16 @@ If we look at the example, all the objects are TODO +`RPGEngine` is the main module. It contains the `playRPGEngine` function which bootstraps the whole game. It is also + the game loop. From here, `RPGEngine` talks to its submodules. + +These submodules are `Config`, `Data`, `Input`, `Parse` & `Render`. They are all responsible for their own part, either + containing the program configuration, data containers, everything needed to handle input, everything needed to parse a + source file & everything needed to render the game. However, each of these submodules has their own submodules to + divide the work. They are conveniently named after the state of the game that they work with, e.g. the main menu has a + module & when the game is playing is a different module. A special one is `Core`, which is kind of like a library for + every piece. It contains functions that are regularly used by the other modules. + #### Monads/Monad stack TODO diff --git a/lib/Input.hs b/lib/Input.hs new file mode 100644 index 0000000..9f63d99 --- /dev/null +++ b/lib/Input.hs @@ -0,0 +1,10 @@ +-- Go to the next stage of the Game +-- setNextState :: Game -> Game +-- setNextState game = game{ state = newState } +-- where newState = nextState $ state game + +-- -- Get the next state based on the current state +-- nextState :: State -> State +-- nextState Menu {} = defaultLvlSelect +-- nextState Pause {} = Playing +-- nextState _ = Menu diff --git a/lib/RPGEngine.hs b/lib/RPGEngine.hs index f9372e7..a2855cf 100644 --- a/lib/RPGEngine.hs +++ b/lib/RPGEngine.hs @@ -5,33 +5,18 @@ module RPGEngine ( playRPGEngine ) where -import RPGEngine.Data.Defaults -import RPGEngine.Render -import RPGEngine.Input +import RPGEngine.Config ( bgColor, winDimensions, winOffsets ) +import RPGEngine.Render ( initWindow, render, initGame ) +import RPGEngine.Input ( handleAllInput ) -import Graphics.Gloss ( - Color(..) - , white - , play - ) - ------------------------------ Constants ------------------------------ - --- Dimensions for main window -winDimensions :: (Int, Int) -winDimensions = (1280, 720) - --- Offsets for main window -winOffsets :: (Int, Int) -winOffsets = (0, 0) +import Graphics.Gloss ( play ) ---------------------------------------------------------------------- --- This is the gameloop. +-- This is the game loop. -- It can receive input and update itself. It is rendered by a renderer. playRPGEngine :: String -> Int -> IO() -playRPGEngine title fps = do - play window bgColor fps initGame render handleInputs step +playRPGEngine title fps = do + play window bgColor fps initGame render handleAllInput step where window = initWindow title winDimensions winOffsets - step _ g = g -- TODO Do something with step? Check health etc. - handleInputs = handleAllInput + step _ g = g -- TODO Do something with step? Check health etc. \ No newline at end of file diff --git a/lib/RPGEngine/Config.hs b/lib/RPGEngine/Config.hs new file mode 100644 index 0000000..a8d719c --- /dev/null +++ b/lib/RPGEngine/Config.hs @@ -0,0 +1,36 @@ +-- 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 = white + +-- Default scale +zoom :: Float +zoom = 5.0 + +-- 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/" \ No newline at end of file diff --git a/lib/RPGEngine/Data.hs b/lib/RPGEngine/Data.hs index 2ebe39b..23b25c8 100644 --- a/lib/RPGEngine/Data.hs +++ b/lib/RPGEngine/Data.hs @@ -1,28 +1,64 @@ -module RPGEngine.Data where +-- 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 -------------------------------- --- TODO Add more +-- A game is the base data container. data Game = Game { - -- Current state of the game - state :: State, - playing :: Level, - levels :: [Level], - player :: Player + state :: State, + levels :: [Level], + player :: Player } +------------------------------- State -------------------------------- + +-- Code reusability +data StateBase = StateBase { + renderer :: Renderer Game, + inputHandler :: InputHandler Game +} + + -- Main menu +data State = Menu { base :: StateBase } + -- Select the level you want to play + | LevelSelection { base :: StateBase, + levelList :: [FilePath], + selector :: ListSelector } + -- Playing a level + | Playing { base :: StateBase, + level :: Level } + -- Paused while playing a level + | Paused { base :: StateBase, + level :: Level } + -- Won a level + | Win { base :: StateBase } + -- Lost a level + | Lose { base :: StateBase } + ------------------------------- Level -------------------------------- data Level = Level { - layout :: Layout, - coordlayout :: [(X, Y, Physical)], - items :: [Item], - entities :: [Entity] + layout :: Layout, + -- All Physical pieces but with their coordinates + index :: [(X, Y, Physical)], + items :: [Item], + entities :: [Entity] } deriving (Eq, Show) -type Layout = [Strip] -type Strip = [Physical] +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 @@ -30,48 +66,12 @@ data Physical = Void | Exit deriving (Eq, Show) -------------------------------- Player ------------------------------- - -type X = Int -type Y = Int - -data Player = Player { - playerHp :: Maybe Int, - inventory :: [Item], - position :: (X, Y) -} deriving (Eq, Show) - -instance Living Player where - hp = playerHp - -------------------------------- State -------------------------------- - --- Current state of the game. -data State = Menu - | LvlSelect - | Playing - | Pause - | Win - | Lose - -------------------------------- Object ------------------------------- - -class Object a where - id :: a -> String - x :: a -> Int - y :: a -> Int - name :: a -> String - description :: a -> String - actions :: a -> [([Condition], Action)] - value :: a -> Maybe Int - -class Living a where - hp :: a -> Maybe Int +-------------------------------- Item -------------------------------- data Item = Item { itemId :: ItemId, - itemX :: Int, - itemY :: Int, + itemX :: X, + itemY :: Y, itemName :: String, itemDescription :: String, itemActions :: [([Condition], Action)], @@ -79,41 +79,37 @@ data Item = Item { useTimes :: Maybe Int } deriving (Eq, Show) -instance Object Item where - id = itemId - x = itemX - y = itemY - name = itemName - description = itemDescription - actions = itemActions - value = itemValue +type ItemId = String + +------------------------------- Entity ------------------------------- data Entity = Entity { entityId :: EntityId, - entityX :: Int, - entityY :: Int, + entityX :: X, + entityY :: Y, entityName :: String, entityDescription :: String, entityActions :: [([Condition], Action)], entityValue :: Maybe Int, - entityHp :: Maybe Int, + entityHp :: HP, direction :: Direction } deriving (Eq, Show) -instance Object Entity where - id = entityId - x = entityX - y = entityY - name = entityName - description = entityDescription - actions = entityActions - value = entityValue - -instance Living Entity where - hp = entityHp - type EntityId = String -type ItemId = 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) +} deriving (Eq, Show) ------------------------------ Condition ----------------------------- @@ -121,7 +117,7 @@ data Condition = InventoryFull | InventoryContains ItemId | Not Condition | AlwaysFalse - deriving (Show, Eq) + deriving (Eq, Show) ------------------------------- Action ------------------------------- @@ -130,14 +126,5 @@ data Action = Leave | UseItem ItemId | DecreaseHp EntityId ItemId | IncreasePlayerHp ItemId - | Nothing - deriving (Show, Eq) - ------------------------------- Direction ----------------------------- - -data Direction = North - | East - | South - | West - | Center -- Equal to 'stay where you are' - deriving (Show, Eq) \ No newline at end of file + | DoNothing + deriving (Eq, Show) \ No newline at end of file diff --git a/lib/RPGEngine/Data/Default.hs b/lib/RPGEngine/Data/Default.hs new file mode 100644 index 0000000..7129a12 --- /dev/null +++ b/lib/RPGEngine/Data/Default.hs @@ -0,0 +1,67 @@ +module RPGEngine.Data.Default +-- Everything is exported +where +import RPGEngine.Data (Entity (..), Game (..), Item (..), Layout, Player (..), Level (..), StateBase (..), State (..), Physical (..), Direction (..)) +import RPGEngine.Input.Core (ListSelector(..)) +import RPGEngine.Render.LevelSelection (renderLevelSelection) +import RPGEngine.Input.Playing (spawnPlayer) +import RPGEngine.Render.Menu (renderMenu) + +------------------------------ 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, Entrance, 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, Blocked), + (2, 2, Blocked) + ], + items = [], + entities = [] +} + +defaultPlayer :: Player +defaultPlayer = Player { + playerHp = Prelude.Nothing, -- Compares to infinity + inventory = [], + position = (0, 0) +} \ No newline at end of file diff --git a/lib/RPGEngine/Data/Defaults.hs b/lib/RPGEngine/Data/Defaults.hs deleted file mode 100644 index e3414eb..0000000 --- a/lib/RPGEngine/Data/Defaults.hs +++ /dev/null @@ -1,65 +0,0 @@ -module RPGEngine.Data.Defaults where - -import RPGEngine.Data -import RPGEngine.Input.Player (spawnPlayer) -import RPGEngine.Input.Level (putCoords) - -defaultEntity :: Entity -defaultEntity = Entity { - entityId = "", - entityX = 0, - entityY = 0, - entityName = "Default", - entityDescription = "", - entityActions = [], - entityValue = Prelude.Nothing, - entityHp = Prelude.Nothing, - direction = Center -} - --- Initialize the game -initGame :: Game -initGame = Game { - state = defaultState, - playing = defaultLevel, - levels = [defaultLevel], - player = spawnPlayer defaultLevel defaultPlayer -} - -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, Blocked, Blocked], - [Blocked, Entrance, Walkable, Walkable, Walkable, Walkable, Exit, Blocked], - [Blocked, Blocked, Blocked, Blocked, Blocked, Blocked, Blocked, Blocked] - ] - -defaultLevel :: Level -defaultLevel = Level { - layout = defaultLayout, - coordlayout = putCoords defaultLevel, -- TODO This should go - items = [], - entities = [] -} - -defaultPlayer :: Player -defaultPlayer = Player { - playerHp = Prelude.Nothing, -- Compares to infinity - inventory = [], - position = (0, 0) -} - --- Default state of the game, Menu -defaultState :: State -defaultState = Menu \ No newline at end of file diff --git a/lib/RPGEngine/Data/Game.hs b/lib/RPGEngine/Data/Game.hs new file mode 100644 index 0000000..2b21cd5 --- /dev/null +++ b/lib/RPGEngine/Data/Game.hs @@ -0,0 +1,22 @@ +module RPGEngine.Data.Game +( isLegalMove +) where + +import RPGEngine.Data + ( Player(Player, position), + Direction, + Physical(Exit, Walkable, Entrance), + State(Playing, level), + Game(Game, state, player) ) +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 \ No newline at end of file diff --git a/lib/RPGEngine/Data/Level.hs b/lib/RPGEngine/Data/Level.hs new file mode 100644 index 0000000..86ef84d --- /dev/null +++ b/lib/RPGEngine/Data/Level.hs @@ -0,0 +1,36 @@ +module RPGEngine.Data.Level +-- Everything is exported +where + +import GHC.IO (unsafePerformIO) +import System.Directory (getDirectoryContents) +import RPGEngine.Input.Core (ListSelector(..)) +import RPGEngine.Data (Level (..), Physical (..), Direction (..), Entity (..), Game (..), Item (..), Player (..), StateBase (..), State (..), X, Y, Layout) +import RPGEngine.Config (levelFolder) + +------------------------------ 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 + +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 \ No newline at end of file diff --git a/lib/RPGEngine/Data/State.hs b/lib/RPGEngine/Data/State.hs deleted file mode 100644 index 0cc5347..0000000 --- a/lib/RPGEngine/Data/State.hs +++ /dev/null @@ -1,22 +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 RPGEngine.Data.State -( State(..) - -, nextState -) where - -import RPGEngine.Data - ----------------------------------------------------------------------- - --- Get the next state based on the current state -nextState :: State -> State -nextState Menu = LvlSelect -nextState Playing = Pause -nextState Pause = Playing -nextState _ = Menu - ----------------------------------------------------------------------- diff --git a/lib/RPGEngine/Input.hs b/lib/RPGEngine/Input.hs index 2fae6bb..485affb 100644 --- a/lib/RPGEngine/Input.hs +++ b/lib/RPGEngine/Input.hs @@ -1,50 +1,15 @@ --- Input for RPG-Engine - +-- Implementations for each state can be found in their respective +-- submodules. module RPGEngine.Input ( handleAllInput ) where -import RPGEngine.Data -import RPGEngine.Data.State import RPGEngine.Input.Core -import RPGEngine.Input.Player +import RPGEngine.Data -import Graphics.Gloss.Interface.IO.Game +------------------------------ Exported ------------------------------ ----------------------------------------------------------------------- - --- Handle all input for RPG-Engine +-- Handle all input of all states of the game. handleAllInput :: InputHandler Game -handleAllInput ev g@Game{ state = Playing } = handlePlayInputs ev g -handleAllInput ev g@Game{ state = LvlSelect } = handleLvlSelectInput ev g -handleAllInput ev g = handleAnyKey setNextState ev g - ----------------------------------------------------------------------- - --- Input for 'Playing' state -handlePlayInputs :: InputHandler Game -handlePlayInputs = composeInputHandlers [ - -- Pause the game - handleKey (Char 'p') (\game -> game{ state = Pause }), - - -- Player movement - handleKey (SpecialKey KeyUp) $ movePlayer North, - handleKey (SpecialKey KeyRight) $ movePlayer East, - handleKey (SpecialKey KeyDown) $ movePlayer South, - handleKey (SpecialKey KeyLeft) $ movePlayer West, - - handleKey (Char 'w') $ movePlayer North, - handleKey (Char 'd') $ movePlayer East, - handleKey (Char 's') $ movePlayer South, - handleKey (Char 'a') $ movePlayer West - ] - --- Input for selection a level to load -handleLvlSelectInput :: InputHandler Game -handleLvlSelectInput = composeInputHandlers [] - --- Go to the next stage of the Game -setNextState :: Game -> Game -setNextState game = game{ state = newState } - where newState = nextState $ state game - +handleAllInput ev g@Game{ state = state } = handleInput ev g + where handleInput = inputHandler $ base state \ No newline at end of file diff --git a/lib/RPGEngine/Input/Core.hs b/lib/RPGEngine/Input/Core.hs index e2e81b9..9044c1d 100644 --- a/lib/RPGEngine/Input/Core.hs +++ b/lib/RPGEngine/Input/Core.hs @@ -1,21 +1,26 @@ --- Allows to create a massive inputHandler that can handle anything --- after you specify what you want it to do. - module RPGEngine.Input.Core -( InputHandler(..) +( InputHandler +, ListSelector(..) + , composeInputHandlers , handle , handleKey , handleAnyKey ) where -import Graphics.Gloss.Interface.IO.Game +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 +} + +------------------------------ Exported ------------------------------ -- Compose multiple InputHandlers into one InputHandler that handles -- all of them. @@ -26,8 +31,8 @@ composeInputHandlers (ih:ihs) ev a = composeInputHandlers ihs ev (ih ev a) -- Handle any event handle :: Event -> (a -> a) -> InputHandler a handle (EventKey key _ _ _) = handleKey key --- handle (EventMotion _) = undefined --- handle (EventResize _) = undefined +-- handle (EventMotion _) = undefined -- TODO +-- handle (EventResize _) = undefined -- TODO handle _ = const (const id) -- Handle a event by pressing a key @@ -41,7 +46,7 @@ handleAnyKey :: (a -> a) -> InputHandler a handleAnyKey f (EventKey _ Down _ _) = f handleAnyKey _ _ = id ----------------------------------------------------------------------- +--------------------------- Help functions --------------------------- handleCharKey :: Char -> (a -> a) -> InputHandler a handleCharKey c1 f (EventKey (Char c2) Down _ _) @@ -53,4 +58,4 @@ handleSpecialKey :: SpecialKey -> (a -> a) -> InputHandler a handleSpecialKey sk1 f (EventKey (SpecialKey sk2) Down _ _) | sk1 == sk2 = f | otherwise = id -handleSpecialKey _ _ _ = id +handleSpecialKey _ _ _ = id \ No newline at end of file diff --git a/lib/RPGEngine/Input/Level.hs b/lib/RPGEngine/Input/Level.hs deleted file mode 100644 index 63391c9..0000000 --- a/lib/RPGEngine/Input/Level.hs +++ /dev/null @@ -1,27 +0,0 @@ -module RPGEngine.Input.Level -( putCoords -, findFirst -, whatIsAt -) where -import RPGEngine.Data (Level (..), Y, X, Physical(..)) - --- 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 = zip [0::Int .. ] lay - numberedList = map (\(x, strip) -> (x, zip [0::Int ..] strip)) numberedStrips - --- Find first position of a Physical --- Graceful exit by giving Nothing if there is nothing found. -findFirst :: Level -> Physical -> Maybe (X, Y) -findFirst l@Level{ coordlayout = lay } physical = try - where matches = filter (\(x, y, v) -> v == physical) lay - try | not (null matches) = Just $ (\(x, y, _) -> (x, y)) $ head matches - | otherwise = Nothing - --- What is located at a given position in the level? -whatIsAt :: (X, Y) -> Level -> Physical -whatIsAt pos lvl@Level{ coordlayout = lay } = try - where matches = map (\(_, _, v) -> v) $ filter (\(x, y, v) -> (x, y) == pos) lay - try | not (null matches) = head matches - | otherwise = Void diff --git a/lib/RPGEngine/Input/LevelSelection.hs b/lib/RPGEngine/Input/LevelSelection.hs new file mode 100644 index 0000000..d2f3578 --- /dev/null +++ b/lib/RPGEngine/Input/LevelSelection.hs @@ -0,0 +1,49 @@ +module RPGEngine.Input.LevelSelection +( handleInputLevelSelection +) where + +import RPGEngine.Input.Core + ( composeInputHandlers, handleKey, InputHandler, ListSelector (..) ) + +import RPGEngine.Config ( levelFolder ) +import RPGEngine.Data ( Game (..), Direction (..), State (..), StateBase (..) ) + +import Graphics.Gloss.Interface.IO.Game + ( Key(SpecialKey), SpecialKey(KeySpace) ) +import Graphics.Gloss.Interface.IO.Interact (SpecialKey(..)) +import RPGEngine.Render.Playing (renderPlaying) +import RPGEngine.Input.Playing (handleInputPlaying) +import RPGEngine.Parse (parse) + +------------------------------ Exported ------------------------------ + +handleInputLevelSelection :: InputHandler Game +handleInputLevelSelection = composeInputHandlers [ + handleKey (SpecialKey KeySpace) selectLevel, + + handleKey (SpecialKey KeyUp) $ moveSelector North, + handleKey (SpecialKey KeyDown) $ moveSelector South + ] + +---------------------------------------------------------------------- + +-- Select a level and load it in +selectLevel :: Game -> Game +selectLevel game@Game{ state = LevelSelection{ levelList = list, selector = selector }} = newGame + where newGame = parse $ levelFolder ++ (list !! index) + index = selection selector +selectLevel g = g + +-- Move the selector either up or down +moveSelector :: Direction -> Game -> Game +moveSelector dir game@Game{ state = state@LevelSelection{ levelList = list, selector = 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 diff --git a/lib/RPGEngine/Input/Lose.hs b/lib/RPGEngine/Input/Lose.hs new file mode 100644 index 0000000..f9c6d0e --- /dev/null +++ b/lib/RPGEngine/Input/Lose.hs @@ -0,0 +1,13 @@ +module RPGEngine.Input.Lose +( handleInputLose +) where + +import RPGEngine.Input.Core ( InputHandler ) + +import RPGEngine.Data ( Game ) + +------------------------------ Exported ------------------------------ + +-- TODO +handleInputLose :: InputHandler Game +handleInputLose = undefined \ No newline at end of file diff --git a/lib/RPGEngine/Input/LvlSelect.hs b/lib/RPGEngine/Input/LvlSelect.hs deleted file mode 100644 index 5b1bf35..0000000 --- a/lib/RPGEngine/Input/LvlSelect.hs +++ /dev/null @@ -1,12 +0,0 @@ -module RPGEngine.Input.LvlSelect -( getLvlList -) where - -import GHC.IO (unsafePerformIO) -import System.Directory (getDirectoryContents) - -lvlFolder :: FilePath -lvlFolder = "levels" - -getLvlList :: [FilePath] -getLvlList = unsafePerformIO $ getDirectoryContents lvlFolder \ No newline at end of file diff --git a/lib/RPGEngine/Input/Menu.hs b/lib/RPGEngine/Input/Menu.hs new file mode 100644 index 0000000..6903d0d --- /dev/null +++ b/lib/RPGEngine/Input/Menu.hs @@ -0,0 +1,36 @@ +module RPGEngine.Input.Menu +( handleInputMenu +) where + +import RPGEngine.Input.Core ( InputHandler, composeInputHandlers, handleAnyKey, ListSelector (..) ) + +import RPGEngine.Data ( Game (..), State (..), StateBase (..) ) +import RPGEngine.Render.LevelSelection (renderLevelSelection) +import RPGEngine.Input.LevelSelection (handleInputLevelSelection) +import RPGEngine.Data.Level (getLevelList) + +------------------------------ Exported ------------------------------ + +handleInputMenu :: InputHandler Game +handleInputMenu = composeInputHandlers [ + handleAnyKey selectLevel + ] + +---------------------------------------------------------------------- + +selectLevel :: Game -> Game +selectLevel g@Game{ state = state } = g{ state = defaultLevelSelection } + +defaultLevelSelection :: State +defaultLevelSelection = LevelSelection { base = base, selector = defaultSelector, levelList = levels } + where base = StateBase { + renderer = renderLevelSelection, + inputHandler = handleInputLevelSelection + } + levels = getLevelList + +defaultSelector :: ListSelector +defaultSelector = ListSelector { + selection = 0, + selected = False +} \ No newline at end of file diff --git a/lib/RPGEngine/Input/Paused.hs b/lib/RPGEngine/Input/Paused.hs new file mode 100644 index 0000000..03522dd --- /dev/null +++ b/lib/RPGEngine/Input/Paused.hs @@ -0,0 +1,12 @@ +module RPGEngine.Input.Paused +( handleInputPaused +) where + +import RPGEngine.Input.Core ( InputHandler ) + +import RPGEngine.Data ( Game ) + +------------------------------ Exported ------------------------------ + +handleInputPaused :: InputHandler Game +handleInputPaused = undefined diff --git a/lib/RPGEngine/Input/Player.hs b/lib/RPGEngine/Input/Player.hs deleted file mode 100644 index be56f27..0000000 --- a/lib/RPGEngine/Input/Player.hs +++ /dev/null @@ -1,44 +0,0 @@ -module RPGEngine.Input.Player -( spawnPlayer -, movePlayer -) where - -import RPGEngine.Data (Game(..), Direction(..), Player(..), X, Y, Physical (..), Level(..)) -import RPGEngine.Input.Level (whatIsAt, findFirst) -import Data.Maybe (fromJust, isNothing) - ------------------------------ Constants ------------------------------ - - -diffs :: Direction -> (X, Y) -diffs North = ( 0, 1) -diffs East = ( 1, 0) -diffs South = ( 0, -1) -diffs West = (-1, 0) -diffs Center = ( 0, 0) - ----------------------------------------------------------------------- - --- 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 = findFirst l Entrance - newPos | isNothing try = prevPos - | otherwise = fromJust try - --- Move a player in a direction if possible. -movePlayer :: Direction -> Game -> Game -movePlayer dir g@Game{ player = p@Player{ position = (x, y) }} = newGame - where newGame = g{ player = newPlayer } - newPlayer = p{ position = newCoord } - newCoord | isLegalMove dir g = (x + xD, y + yD) - | otherwise = (x, y) - (xD, yD) = diffs dir - --- Check if a move is legal by checking what is located at the new position. -isLegalMove :: Direction -> Game -> Bool -isLegalMove dir g@Game{ playing = lvl, player = p@Player{ position = (x, y) }} = legality - where legality = physical `elem` [Walkable, Entrance, Exit] - physical = whatIsAt newPos lvl - newPos = (x + xD, y + yD) - (xD, yD) = diffs dir \ No newline at end of file diff --git a/lib/RPGEngine/Input/Playing.hs b/lib/RPGEngine/Input/Playing.hs new file mode 100644 index 0000000..b2638e0 --- /dev/null +++ b/lib/RPGEngine/Input/Playing.hs @@ -0,0 +1,80 @@ +module RPGEngine.Input.Playing +( handleInputPlaying +, spawnPlayer +) where + +import RPGEngine.Input.Core + ( composeInputHandlers, handleKey, InputHandler ) + +import RPGEngine.Data + ( Player(Player, position), + Direction(West, North, East, South), + Physical(Entrance), + Y, + X, + Level(Level, layout), + State(..), + StateBase(StateBase, renderer, inputHandler), + Game(Game, state, player) ) +import RPGEngine.Data.Level ( findFirstOf, directionOffsets ) +import RPGEngine.Data.Game ( isLegalMove ) +import RPGEngine.Input.Paused ( handleInputPaused ) +import RPGEngine.Render.Paused ( renderPaused ) +import Graphics.Gloss.Interface.IO.Game (Key(..)) +import Graphics.Gloss.Interface.IO.Interact (SpecialKey(..)) +import Data.Maybe (isNothing, fromJust) + +------------------------------ Exported ------------------------------ + +handleInputPlaying :: InputHandler Game +handleInputPlaying = composeInputHandlers [ + -- Pause the game + handleKey (Char 'p') pauseGame, + + -- Player movement + handleKey (SpecialKey KeyUp) $ movePlayer North, + handleKey (SpecialKey KeyRight) $ movePlayer East, + handleKey (SpecialKey KeyDown) $ movePlayer South, + handleKey (SpecialKey KeyLeft) $ movePlayer West, + + handleKey (Char 'w') $ movePlayer North, + handleKey (Char 'd') $ movePlayer East, + handleKey (Char 's') $ movePlayer South, + handleKey (Char 'a') $ movePlayer West + ] + +-- 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 + +---------------------------------------------------------------------- + +pauseGame :: Game -> Game +pauseGame g@Game{ state = Playing{ level = level } } = pausedGame + where pausedGame = g{ state = pausedState } + pausedState = Paused{ base = newBase, level = level } + newBase = StateBase { renderer = renderPaused, inputHandler = handleInputPaused } + +-- Move a player in a direction if possible. +movePlayer :: Direction -> Game -> Game +movePlayer dir g@Game{ player = p@Player{ position = (x, y) }} = newGame + where newGame = g{ player = newPlayer } + newPlayer = p{ position = newCoord } + newCoord | isLegalMove dir g = (x + xD, y + yD) + | otherwise = (x, y) + (xD, yD) = directionOffsets dir + +-- TODO +goToNextLevel :: Game -> Game +goToNextLevel = undefined + +---------------------------------------------------------------------- + +-- 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 = zip [0::Int .. ] lay + numberedList = map (\(x, strip) -> (x, zip [0::Int ..] strip)) numberedStrips \ No newline at end of file diff --git a/lib/RPGEngine/Input/Win.hs b/lib/RPGEngine/Input/Win.hs new file mode 100644 index 0000000..37434ee --- /dev/null +++ b/lib/RPGEngine/Input/Win.hs @@ -0,0 +1,13 @@ +module RPGEngine.Input.Win +( handleInputWin +) where + +import RPGEngine.Input.Core ( InputHandler ) + +import RPGEngine.Data ( Game ) + +------------------------------ Exported ------------------------------ + +-- TODO +handleInputWin :: InputHandler Game +handleInputWin = undefined \ No newline at end of file diff --git a/lib/RPGEngine/Parse.hs b/lib/RPGEngine/Parse.hs index a8736ad..9b1c976 100644 --- a/lib/RPGEngine/Parse.hs +++ b/lib/RPGEngine/Parse.hs @@ -1,19 +1,16 @@ -module RPGEngine.Parse where +module RPGEngine.Parse +( parse +) where -import RPGEngine.Data -import RPGEngine.Parse.StructElement -import RPGEngine.Parse.Game +import RPGEngine.Data ( Game ) +import RPGEngine.Parse.StructureToGame ( structureToGame ) +import GHC.IO (unsafePerformIO) +import Text.Parsec.String (parseFromFile) +import RPGEngine.Parse.TextToStructure (structure) -import Text.Parsec.String -import System.IO.Unsafe +------------------------------ Exported ------------------------------ ------------------------------ Constants ------------------------------ - -type FileName = String - ----------------------------------------------------------------------- - -parseToGame :: FileName -> Game -parseToGame filename = structToGame struct +parse :: FilePath -> Game +parse filename = structureToGame struct where (Right struct) = unsafePerformIO io - io = parseFromFile structElement filename \ No newline at end of file + io = parseFromFile structure filename \ No newline at end of file diff --git a/lib/RPGEngine/Parse/Core.hs b/lib/RPGEngine/Parse/Core.hs index 7e704ab..ff1be67 100644 --- a/lib/RPGEngine/Parse/Core.hs +++ b/lib/RPGEngine/Parse/Core.hs @@ -1,7 +1,23 @@ -module RPGEngine.Parse.Core where +module RPGEngine.Parse.Core +( parseWith +, parseWithRest +, ignoreWS +) where import Text.Parsec -import Text.Parsec.String + ( 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. @@ -14,7 +30,7 @@ parseWithRest :: Parser a -> String -> Either ParseError (a, String) parseWithRest parser = parse ((,) <$> parser <*> rest) "" where rest = manyTill anyToken eof --- Ignore all kinds of whitespaces +-- Ignore all kinds of whitespace ignoreWS :: Parser a -> Parser a ignoreWS parser = choice [skipComment, spaces] >> parser where skipComment = do{ string "#"; manyTill anyChar endOfLine; return ()} \ No newline at end of file diff --git a/lib/RPGEngine/Parse/Game.hs b/lib/RPGEngine/Parse/Game.hs deleted file mode 100644 index 9999ffd..0000000 --- a/lib/RPGEngine/Parse/Game.hs +++ /dev/null @@ -1,101 +0,0 @@ -module RPGEngine.Parse.Game where - -import RPGEngine.Data -import RPGEngine.Data.Defaults -import RPGEngine.Parse.StructElement - --------------------------------- Game -------------------------------- - --- TODO -structToGame :: StructElement -> Game -structToGame = undefined - -------------------------------- Player ------------------------------- - -structToPlayer :: StructElement -> Player -structToPlayer (Block block) = structToPlayer' block defaultPlayer -structToPlayer _ = defaultPlayer - -structToPlayer' :: [StructElement] -> Player -> Player -structToPlayer' [] p = p -structToPlayer' ((Entry(Tag "hp") val ):es) p = (structToPlayer' es p){ playerHp = structToMaybeInt val } -structToPlayer' ((Entry(Tag "inventory") (Block inv)):es) p = (structToPlayer' es p){ inventory = structToItems inv } -structToPlayer' _ _ = defaultPlayer - -structToActions :: StructElement -> [([Condition], Action)] -structToActions (Block []) = [] -structToActions (Block block) = structToActions' block [] -structToActions _ = [] - -structToActions' :: [StructElement] -> [([Condition], Action)] -> [([Condition], Action)] -structToActions' [] list = list -structToActions' ((Entry(ConditionList cs) (Regular (Action a))):as) list = structToActions' as ((cs, a):list) -structToActions' _ list = list - -------------------------------- Levels ------------------------------- - -structToLevels :: StructElement -> [Level] -structToLevels (Block struct) = structToLevel <$> struct -structToLevels _ = [defaultLevel] - -structToLevel :: StructElement -> Level -structToLevel (Block entries) = structToLevel' entries defaultLevel -structToLevel _ = defaultLevel - -structToLevel' :: [StructElement] -> Level -> Level -structToLevel' ((Entry(Tag "layout") (Regular (Layout layout))):ls) l = (structToLevel' ls l){ RPGEngine.Data.layout = layout } -structToLevel' ((Entry(Tag "items") (Block items) ):ls) l = (structToLevel' ls l){ items = structToItems items } -structToLevel' ((Entry(Tag "entities") (Block entities) ):ls) l = (structToLevel' ls l){ entities = structToEntities entities } -structToLevel' _ _ = defaultLevel - -------------------------------- Items -------------------------------- - -structToItems :: [StructElement] -> [Item] -structToItems items = structToItem <$> items - -structToItem :: StructElement -> Item -structToItem (Block block) = structToItem' block defaultItem -structToItem _ = defaultItem - -structToItem' :: [StructElement] -> Item -> Item -structToItem' [] i = i -structToItem' ((Entry(Tag "id") (Regular(String id ))):is) i = (structToItem' is i){ itemId = id } -structToItem' ((Entry(Tag "x") (Regular(Integer x ))):is) i = (structToItem' is i){ itemX = x } -structToItem' ((Entry(Tag "y") (Regular(Integer y ))):is) i = (structToItem' is i){ itemY = y } -structToItem' ((Entry(Tag "name") (Regular(String name))):is) i = (structToItem' is i){ itemName = name } -structToItem' ((Entry(Tag "description") (Regular(String desc))):is) i = (structToItem' is i){ itemDescription = desc } -structToItem' ((Entry(Tag "value") val ):is) i = (structToItem' is i){ itemValue = structToMaybeInt val } -structToItem' ((Entry(Tag "actions") actions ):is) i = (structToItem' is i){ itemActions = structToActions actions } -structToItem' ((Entry (Tag "useTimes") useTimes ):is) i = (structToItem' is i){ useTimes = structToMaybeInt useTimes } -structToItem' _ _ = defaultItem - ------------------------------- Entities ------------------------------ - -structToEntities :: [StructElement] -> [Entity] -structToEntities entities = structToEntity <$> entities - -structToEntity :: StructElement -> Entity -structToEntity (Block block) = structToEntity' block defaultEntity -structToEntity _ = defaultEntity - -structToEntity' :: [StructElement] -> Entity -> Entity -structToEntity' [] e = e -structToEntity' ((Entry(Tag "id") (Regular(String id )) ):es) e = (structToEntity' es e){ entityId = id } -structToEntity' ((Entry(Tag "x") (Regular(Integer x )) ):es) e = (structToEntity' es e){ entityX = x } -structToEntity' ((Entry(Tag "y") (Regular(Integer y )) ):es) e = (structToEntity' es e){ entityY = y } -structToEntity' ((Entry(Tag "name") (Regular(String name)) ):es) e = (structToEntity' es e){ entityName = name } -structToEntity' ((Entry(Tag "description") (Regular(String desc)) ):es) e = (structToEntity' es e){ entityDescription = desc } -structToEntity' ((Entry(Tag "actions") actions ):es) e = (structToEntity' es e){ entityActions = structToActions actions } -structToEntity' ((Entry(Tag "value") val ):es) e = (structToEntity' es e){ entityValue = structToMaybeInt val } -structToEntity' ((Entry(Tag "hp") val ):es) e = (structToEntity' es e){ entityHp = structToMaybeInt val } -structToEntity' ((Entry(Tag "direction") (Regular(Direction dir))):es) e = (structToEntity' es e){ RPGEngine.Data.direction = dir } -structToEntity' _ _ = defaultEntity - ----------------------------------------------------------------------- - -structToMaybeInt :: StructElement -> Maybe Int -structToMaybeInt (Regular (Integer val)) = Just val -structToMaybeInt (Regular Infinite) = Prelude.Nothing -structToMaybeInt _ = Prelude.Nothing -- TODO - ----------------------------------------------------------------------- \ No newline at end of file diff --git a/lib/RPGEngine/Parse/StructureToGame.hs b/lib/RPGEngine/Parse/StructureToGame.hs new file mode 100644 index 0000000..ddbcd3d --- /dev/null +++ b/lib/RPGEngine/Parse/StructureToGame.hs @@ -0,0 +1,120 @@ +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), + Game (..), State (..), StateBase (..) ) +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.Render.Playing (renderPlaying) +import RPGEngine.Input.Playing (handleInputPlaying) + +------------------------------ Exported ------------------------------ + +structureToGame :: Structure -> Game +structureToGame (Block [(Entry(Tag "player") playerBlock), (Entry(Tag "levels") levelsBlock)]) = game + where game = Game{ state = newState, levels = newLevels, player = newPlayer } + newState = Playing{ base = playingBase, level = currentLevel } + playingBase = StateBase{ renderer = renderPlaying, inputHandler = handleInputPlaying } + newLevels = structureToLevels levelsBlock + currentLevel = head newLevels + newPlayer = structureToPlayer playerBlock + +------------------------------- 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) = 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 \ No newline at end of file diff --git a/lib/RPGEngine/Parse/StructElement.hs b/lib/RPGEngine/Parse/TextToStructure.hs similarity index 86% rename from lib/RPGEngine/Parse/StructElement.hs rename to lib/RPGEngine/Parse/TextToStructure.hs index 35d2b08..6f1b060 100644 --- a/lib/RPGEngine/Parse/StructElement.hs +++ b/lib/RPGEngine/Parse/TextToStructure.hs @@ -1,13 +1,14 @@ -module RPGEngine.Parse.StructElement where +module RPGEngine.Parse.TextToStructure +-- Everything is exported for testing +where -import RPGEngine.Data (Action(..), Condition(..), Layout, Direction(..), Physical(..), Strip) import RPGEngine.Parse.Core ( ignoreWS ) +import RPGEngine.Data (Action (..), Condition (..), Direction (..), Layout, Strip, Physical (..)) + import Text.Parsec - ( char, - many, - try, - alphaNum, + ( alphaNum, + char, digit, noneOf, oneOf, @@ -15,7 +16,9 @@ import Text.Parsec choice, many1, notFollowedBy, - sepBy ) + sepBy, + many, + try ) import qualified Text.Parsec as P ( string ) import Text.Parsec.String ( Parser ) @@ -23,18 +26,18 @@ import Text.Parsec.String ( Parser ) -- See documentation for more details, only a short description is -- provided here. -data StructElement = Block [StructElement] - | Entry Key StructElement -- Key + Value +data Structure = Block [Structure] + | Entry Key Structure -- Key + Value | Regular Value -- Regular value, Integer or String or Infinite deriving (Eq, Show) ---------------------------------------------------------------------- -structElement :: Parser StructElement -structElement = try $ choice [block, entry, regular] +structure :: Parser Structure +structure = try $ choice [block, entry, regular] -- A list of entries -block :: Parser StructElement +block :: Parser Structure block = try $ do open <- ignoreWS $ oneOf openingBrackets middle <- ignoreWS $ choice [entry, block] `sepBy` char ',' @@ -42,15 +45,15 @@ block = try $ do ignoreWS $ char closingBracket return $ Block middle -entry :: Parser StructElement +entry :: Parser Structure entry = try $ do key <- ignoreWS key -- TODO Fix this oneOf ": " -- Can be left out - value <- ignoreWS structElement + value <- ignoreWS structure return $ Entry key value -regular :: Parser StructElement +regular :: Parser Structure regular = try $ Regular <$> value --------------------------------- Key -------------------------------- @@ -108,7 +111,7 @@ data Value = String String ---------------------------------------------------------------------- value :: Parser Value -value = choice [string, integer, infinite, action, direction] +value = choice [layout, string, integer, infinite, action, direction] string :: Parser Value string = try $ String <$> between (char '\"') (char '\"') reading @@ -134,7 +137,7 @@ action = try $ do | script == "useItem" = UseItem arg | script == "decreaseHp" = DecreaseHp first second | script == "increasePlayerHp" = IncreasePlayerHp arg - | otherwise = RPGEngine.Data.Nothing + | otherwise = DoNothing (first, ',':second) = break (== ',') arg return $ Action answer @@ -152,12 +155,15 @@ direction = try $ do make "right" = East make "down" = South make "left" = West - make _ = Center + make _ = Stay layout :: Parser Value layout = try $ do + open <- ignoreWS $ oneOf openingBrackets ignoreWS $ char '|' - list <- ignoreWS strip `sepBy` ignoreWS (char '|') + list <- ignoreWS $ ignoreWS strip `sepBy` ignoreWS (char '|') + let closing = getMatchingClosingBracket open + ignoreWS $ char closing return $ Layout list strip :: Parser Strip @@ -180,7 +186,6 @@ physical = try $ do make 'e' = Exit make _ = Void - ------------------------------ Brackets ------------------------------ openingBrackets :: [Char] diff --git a/lib/RPGEngine/Render.hs b/lib/RPGEngine/Render.hs index 09b9b66..fb9152b 100644 --- a/lib/RPGEngine/Render.hs +++ b/lib/RPGEngine/Render.hs @@ -1,38 +1,21 @@ --- Allows to render the played game - +-- Implementation for each state can be found in their respective +-- submodules. module RPGEngine.Render ( initWindow -, bgColor - +, initGame , render ) where -import RPGEngine.Data - ( State(..), - Game(..), Player (..) ) -import RPGEngine.Render.Level - ( renderLevel ) -import Graphics.Gloss - ( white, - pictures, - text, - Display(InWindow), - Color, - Picture, - scale, - translate ) -import RPGEngine.Render.Player (renderPlayer, focusPlayer) -import RPGEngine.Render.GUI (renderGUI) -import Graphics.Gloss.Data.Picture (color) -import RPGEngine.Render.Core (overlay) -import RPGEngine.Input.LvlSelect (getLvlList) -import RPGEngine.Render.LvlSelect (renderLvlList) +import RPGEngine.Render.Core ( Renderer(..) ) ------------------------------ Constants ------------------------------ - --- Game background color -bgColor :: Color -bgColor = white +import RPGEngine.Data ( State(..), Game(..), StateBase(..) ) +import Graphics.Gloss ( Display ) +import Graphics.Gloss.Data.Display ( Display(InWindow) ) +import Graphics.Gloss.Data.Picture (Picture) +import RPGEngine.Data.Default (defaultLevel, defaultPlayer) +import RPGEngine.Input.Playing (spawnPlayer) +import RPGEngine.Render.Menu (renderMenu) +import RPGEngine.Input.Menu (handleInputMenu) ---------------------------------------------------------------------- @@ -40,43 +23,16 @@ bgColor = white initWindow :: String -> (Int, Int) -> (Int, Int) -> Display initWindow = InWindow --- Render the game +-- Initialize the game +initGame :: Game +initGame = Game { + state = Menu{ base = StateBase{ renderer = renderMenu, inputHandler = handleInputMenu }}, + levels = [defaultLevel], + player = spawnPlayer defaultLevel defaultPlayer +} + +-- Render all different states render :: Game -> Picture -render g@Game{ state = Menu } = renderMenu g -render g@Game{ state = LvlSelect } = renderLevelSelection g -render g@Game{ state = Playing } = renderPlaying g -render g@Game{ state = Pause } = renderPause g -render g@Game{ state = Win } = renderWin g -render g@Game{ state = Lose } = renderLose g - ----------------------------------------------------------------------- - --- TODO -renderMenu :: Game -> Picture -renderMenu _ = text "[Press any key to start]" - --- TODO -renderLevelSelection :: Game -> Picture -renderLevelSelection _ = renderLvlList getLvlList - -renderPlaying :: Game -> Picture -renderPlaying g@Game{ playing = lvl, player = player } = pictures [ - renderLevel lvl, - renderPlayer player, - renderGUI g - ] - -renderPause :: Game -> Picture -renderPause g = pictures [renderPlaying g, pause] - where pause = pictures [ - overlay, - color white $ scale 0.5 0.5 $ text "[Press any key to continue]" - ] - --- TODO -renderWin :: Game -> Picture -renderWin _ = text "Win" - --- TODO -renderLose :: Game -> Picture -renderLose _ = text "Lose" \ No newline at end of file +render g@Game{ state = state } = renderFunc g + where stateBase = base state + renderFunc = renderer stateBase \ No newline at end of file diff --git a/lib/RPGEngine/Render/Core.hs b/lib/RPGEngine/Render/Core.hs index e5155f4..014843e 100644 --- a/lib/RPGEngine/Render/Core.hs +++ b/lib/RPGEngine/Render/Core.hs @@ -1,24 +1,21 @@ -module RPGEngine.Render.Core where +module RPGEngine.Render.Core +( Renderer -import Graphics.Gloss ( Picture, translate, pictures ) -import GHC.IO (unsafePerformIO) -import Graphics.Gloss.Juicy (loadJuicyPNG) -import Data.Maybe (fromJust) -import Graphics.Gloss.Data.Picture (scale) -import Graphics.Gloss.Data.Bitmap (BitmapData(..)) +, getRender +, setRenderPos +, overlay +) where + +import RPGEngine.Config + +import Data.Maybe +import Graphics.Gloss +import GHC.IO +import Graphics.Gloss.Juicy ----------------------------- Constants ------------------------------ --- Default scale -zoom :: Float -zoom = 5.0 - --- Resolution of the texture -resolution :: Float -resolution = 16 - -assetsFolder :: FilePath -assetsFolder = "assets/" +type Renderer a = a -> Picture unknownImage :: FilePath unknownImage = "unknown.png" @@ -54,11 +51,7 @@ library = unknown:entities ++ environment ++ gui ++ items gui = [] items = map (\(f, s) -> (f, renderPNG (assetsFolder ++ "items/" ++ s))) allItems ----------------------------------------------------------------------- - --- Turn a path to a .png file into a Picture. -renderPNG :: FilePath -> Picture -renderPNG path = scale zoom zoom $ fromJust $ unsafePerformIO $ loadJuicyPNG path +------------------------------ Exported ------------------------------ -- Retrieve an image from the library. If the library does not contain -- the requested image, a default is returned. @@ -82,4 +75,10 @@ overlay = setRenderPos offX offY $ pictures voids height = round $ 4320 / resolution / zoom width = round $ 7680 / resolution / zoom offX = negate (width `div` 2) - offY = negate (height `div` 2) \ No newline at end of file + 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 \ No newline at end of file diff --git a/lib/RPGEngine/Render/GUI.hs b/lib/RPGEngine/Render/GUI.hs deleted file mode 100644 index e29b012..0000000 --- a/lib/RPGEngine/Render/GUI.hs +++ /dev/null @@ -1,10 +0,0 @@ -module RPGEngine.Render.GUI -( renderGUI -) where - -import RPGEngine.Data (Game) -import Graphics.Gloss (Picture, blank) - --- TODO -renderGUI :: Game -> Picture -renderGUI _ = blank diff --git a/lib/RPGEngine/Render/LevelSelection.hs b/lib/RPGEngine/Render/LevelSelection.hs new file mode 100644 index 0000000..ee3da56 --- /dev/null +++ b/lib/RPGEngine/Render/LevelSelection.hs @@ -0,0 +1,33 @@ +module RPGEngine.Render.LevelSelection +( renderLevelSelection +) where + +import RPGEngine.Config ( resolution, zoom ) +import RPGEngine.Data ( Game (..), State (..) ) +import RPGEngine.Data.Level ( getLevelList ) +import RPGEngine.Render.Core ( Renderer ) + +import Graphics.Gloss + ( pictures, text, translate, blank, Picture, color ) +import Graphics.Gloss.Data.Picture (scale) +import RPGEngine.Input.Core (ListSelector (..)) +import Graphics.Gloss.Data.Color (red) + +------------------------------ Exported ------------------------------ + +renderLevelSelection :: Renderer Game +renderLevelSelection Game{ state = state } = result + where result = renderLevelList state + +---------------------------------------------------------------------- + +renderLevelList :: Renderer State +renderLevelList LevelSelection{ levelList = list, selector = selector } = everything + where everything = pictures $ map render entries + sel = selection selector + entries = zip [0::Int .. ] list + render (i, path) | i == sel = color red $ scale zoomed zoomed $ translate 0 (offset i) $ text path + | otherwise = scale zoomed zoomed $ translate 0 (offset i) $ text path + zoomed = 0.1 * zoom + offset i = negate (2 * resolution * zoom * fromIntegral i) +renderLevelList _ = blank \ No newline at end of file diff --git a/lib/RPGEngine/Render/Lose.hs b/lib/RPGEngine/Render/Lose.hs new file mode 100644 index 0000000..b3266e9 --- /dev/null +++ b/lib/RPGEngine/Render/Lose.hs @@ -0,0 +1,14 @@ +module RPGEngine.Render.Lose +( renderLose +) where + +import RPGEngine.Render.Core ( Renderer ) + +import RPGEngine.Data ( Game ) +import Graphics.Gloss ( text ) + +---------------------------------------------------------------------- + +-- TODO +renderLose :: Renderer Game +renderLose _ = text "Win" \ No newline at end of file diff --git a/lib/RPGEngine/Render/LvlSelect.hs b/lib/RPGEngine/Render/LvlSelect.hs deleted file mode 100644 index b395e9d..0000000 --- a/lib/RPGEngine/Render/LvlSelect.hs +++ /dev/null @@ -1,15 +0,0 @@ -module RPGEngine.Render.LvlSelect -( renderLvlList -) where - -import Graphics.Gloss ( Picture, pictures, translate, scale ) -import Graphics.Gloss.Data.Picture (blank, text) -import RPGEngine.Render.Core (resolution, zoom) - --- Render all level names, under each other. -renderLvlList :: [FilePath] -> Picture -renderLvlList list = pictures $ map render entries - where entries = zip [0::Int .. ] list - render (i, path) = scale zoomed zoomed $ translate 0 (offset i) $ text path - zoomed = 0.1 * zoom - offset i = negate (2 * resolution * zoom * fromIntegral i) \ No newline at end of file diff --git a/lib/RPGEngine/Render/Menu.hs b/lib/RPGEngine/Render/Menu.hs new file mode 100644 index 0000000..26ec414 --- /dev/null +++ b/lib/RPGEngine/Render/Menu.hs @@ -0,0 +1,14 @@ +module RPGEngine.Render.Menu +( renderMenu +) where + +import RPGEngine.Render.Core ( Renderer ) + +import RPGEngine.Data ( Game ) +import Graphics.Gloss (text) + +---------------------------------------------------------------------- + +-- TODO +renderMenu :: Renderer Game +renderMenu _ = text "[Press any key to start]" \ No newline at end of file diff --git a/lib/RPGEngine/Render/Paused.hs b/lib/RPGEngine/Render/Paused.hs new file mode 100644 index 0000000..3a49a64 --- /dev/null +++ b/lib/RPGEngine/Render/Paused.hs @@ -0,0 +1,20 @@ +module RPGEngine.Render.Paused +( renderPaused +) where + +import RPGEngine.Render.Core ( Renderer, overlay ) + +import RPGEngine.Data ( Game ) +import Graphics.Gloss ( pictures, scale, text ) +import RPGEngine.Render.Playing ( renderPlaying ) +import Graphics.Gloss.Data.Picture (color) +import Graphics.Gloss.Data.Color (white) + +------------------------------ Exported ------------------------------ + +renderPaused :: Renderer Game +renderPaused g = pictures [renderPlaying g, pause] + where pause = pictures [ + overlay, + color white $ scale 0.5 0.5 $ text "[Press any key to continue]" + ] \ No newline at end of file diff --git a/lib/RPGEngine/Render/Player.hs b/lib/RPGEngine/Render/Player.hs deleted file mode 100644 index 0b6a124..0000000 --- a/lib/RPGEngine/Render/Player.hs +++ /dev/null @@ -1,17 +0,0 @@ -module RPGEngine.Render.Player -( renderPlayer -, focusPlayer -) where - -import RPGEngine.Data (Player(..), Game(..)) -import Graphics.Gloss (Picture, text) -import RPGEngine.Render.Core (getRender, setRenderPos, zoom, resolution) -import Graphics.Gloss.Data.Picture (translate) - -renderPlayer :: Player -> Picture -renderPlayer Player{ position = (x, y) } = setRenderPos x y $ getRender "player" - -focusPlayer :: Game -> Picture -> Picture -focusPlayer Game{ player = Player{ position = (x, y)}} = translate centerX centerY - where centerX = resolution * zoom * fromIntegral (negate x) - centerY = resolution * zoom * fromIntegral (negate y) \ No newline at end of file diff --git a/lib/RPGEngine/Render/Level.hs b/lib/RPGEngine/Render/Playing.hs similarity index 54% rename from lib/RPGEngine/Render/Level.hs rename to lib/RPGEngine/Render/Playing.hs index 4e01968..0f075ba 100644 --- a/lib/RPGEngine/Render/Level.hs +++ b/lib/RPGEngine/Render/Playing.hs @@ -1,12 +1,48 @@ -module RPGEngine.Render.Level -( renderLevel +module RPGEngine.Render.Playing +( renderPlaying ) where -import Graphics.Gloss -import RPGEngine.Data -import RPGEngine.Render.Core (getRender, setRenderPos, zoom, resolution) +import RPGEngine.Render.Core ( Renderer, getRender, setRenderPos ) -renderLevel :: Level -> Picture +import RPGEngine.Data + ( Player(..), + Entity(..), + Item(..), + Physical(..), + Layout, + Level(..), + State(..), + Game(..) ) +import Graphics.Gloss ( Picture, pictures ) +import Graphics.Gloss.Data.Picture (translate) +import RPGEngine.Config (resolution, zoom) + +------------------------------ Exported ------------------------------ + +renderPlaying :: Renderer Game +renderPlaying g@Game{ state = Playing { level = lvl }, player = player } = pictures [ + renderLevel lvl, + renderPlayer player + ] + +------------------------------- Player ------------------------------- + +renderPlayer :: Renderer Player +renderPlayer Player{ position = (x, y) } = move picture + where move = setRenderPos x y + picture = 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{ player = Player{ position = (x, y)}} = move + where move = translate centerX centerY + centerX = resolution * zoom * fromIntegral (negate x) + centerY = resolution * zoom * fromIntegral (negate y) + +------------------------------- Level -------------------------------- + +renderLevel :: Renderer Level renderLevel Level{ layout = l, items = i, entities = e } = level where level = pictures [void, layout, items, entities] void = createVoid @@ -28,6 +64,18 @@ renderStrip list = pictures physicals 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 @@ -40,14 +88,4 @@ renderEntities list = pictures $ map renderEntity list renderEntity :: Entity -> Picture renderEntity Entity{ entityId = id, entityX = x, entityY = y} = setRenderPos x y image - where image = getRender id - -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) \ No newline at end of file + where image = getRender id \ No newline at end of file diff --git a/lib/RPGEngine/Render/Win.hs b/lib/RPGEngine/Render/Win.hs new file mode 100644 index 0000000..55d893b --- /dev/null +++ b/lib/RPGEngine/Render/Win.hs @@ -0,0 +1,14 @@ +module RPGEngine.Render.Win +( renderWin +) where + +import RPGEngine.Render.Core ( Renderer ) + +import RPGEngine.Data ( Game ) +import Graphics.Gloss (text) + +---------------------------------------------------------------------- + +-- TODO +renderWin :: Renderer Game +renderWin _ = text "Win" \ No newline at end of file diff --git a/rpg-engine.cabal b/rpg-engine.cabal index 9e11d89..9043757 100644 --- a/rpg-engine.cabal +++ b/rpg-engine.cabal @@ -13,28 +13,36 @@ library parsec >= 3.1.15.1 exposed-modules: RPGEngine - + + RPGEngine.Config + RPGEngine.Data - RPGEngine.Data.Defaults - RPGEngine.Data.State + RPGEngine.Data.Default + RPGEngine.Data.Game + RPGEngine.Data.Level RPGEngine.Input RPGEngine.Input.Core - RPGEngine.Input.Level - RPGEngine.Input.LvlSelect - RPGEngine.Input.Player + RPGEngine.Input.LevelSelection + RPGEngine.Input.Lose + RPGEngine.Input.Menu + RPGEngine.Input.Paused + RPGEngine.Input.Playing + RPGEngine.Input.Win RPGEngine.Parse RPGEngine.Parse.Core - RPGEngine.Parse.Game - RPGEngine.Parse.StructElement - + RPGEngine.Parse.TextToStructure + RPGEngine.Parse.StructureToGame + RPGEngine.Render RPGEngine.Render.Core - RPGEngine.Render.GUI - RPGEngine.Render.Level - RPGEngine.Render.LvlSelect - RPGEngine.Render.Player + RPGEngine.Render.LevelSelection + RPGEngine.Render.Lose + RPGEngine.Render.Menu + RPGEngine.Render.Paused + RPGEngine.Render.Playing + RPGEngine.Render.Win executable rpg-engine main-is: Main.hs @@ -44,10 +52,10 @@ executable rpg-engine test-suite rpg-engine-test type: exitcode-stdio-1.0 - main-is: RPGEngineSpec.hs + main-is: Spec.hs hs-source-dirs: test default-language: Haskell2010 build-depends: base >=4.7 && <5, hspec <= 2.10.6, hspec-discover, rpg-engine other-modules: - -- Parsing - ParseGameSpec, ParseStructElementSpec + Parser.GameSpec + Parser.StructureSpec diff --git a/test/ParseGameSpec.hs b/test/Parser/GameSpec.hs similarity index 78% rename from test/ParseGameSpec.hs rename to test/Parser/GameSpec.hs index 2a2d7d2..1f167a3 100644 --- a/test/ParseGameSpec.hs +++ b/test/Parser/GameSpec.hs @@ -1,10 +1,11 @@ -module ParseGameSpec where +module Parser.GameSpec where import Test.Hspec -import RPGEngine.Parse.StructElement + import RPGEngine.Data import RPGEngine.Parse.Core -import RPGEngine.Parse.Game +import RPGEngine.Parse.TextToStructure +import RPGEngine.Parse.StructureToGame spec :: Spec spec = do @@ -21,19 +22,21 @@ spec = do let input = "player: { hp: infinite, inventory: [] }" correct = Player { playerHp = Prelude.Nothing, - inventory = [] + inventory = [], + position = (0, 0) } - Right (Entry (Tag "player") struct) = parseWith structElement input - structToPlayer struct `shouldBe` correct + 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 = [] + inventory = [], + position = (0, 0) } - Right (Entry (Tag "player") struct) = parseWith structElement input - structToPlayer struct `shouldBe` correct + 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: {} } ] }" @@ -50,10 +53,11 @@ spec = do itemValue = Just 10, useTimes = Prelude.Nothing } - ] + ], + position = (0, 0) } - Right (Entry (Tag "player") struct) = parseWith structElement input - structToPlayer struct `shouldBe` correct + Right (Entry (Tag "player") struct) = parseWith structure input + structureToPlayer struct `shouldBe` correct describe "Layout" $ do it "simple" $ do @@ -72,8 +76,8 @@ spec = do itemActions = [], useTimes = Prelude.Nothing } - Right struct = parseWith structElement input - structToItem struct `shouldBe` correct + 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() } }" @@ -90,27 +94,27 @@ spec = do itemValue = Just 0, useTimes = Just 1 } - Right struct = parseWith structElement input - structToItem struct `shouldBe` correct + Right struct = parseWith structure input + structureToItem struct `shouldBe` correct describe "Actions" $ do it "no conditions" $ do let input = "{[] leave()}" correct = [([], Leave)] - Right struct = parseWith structElement input - structToActions struct `shouldBe` correct + 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 structElement input - structToActions struct `shouldBe` correct + 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 structElement input - structToActions struct `shouldBe` correct + Right struct = parseWith structure input + structureToActions struct `shouldBe` correct describe "Entities" $ do it "TODO: Simple entity" $ do @@ -118,7 +122,7 @@ spec = do describe "Level" $ do it "Simple layout" $ do - let input = "{ layout: { | * * * * * * \n| * s . . e *\n| * * * * * *\n}, items: [], entities: [] }" + let input = "{ layout: { | * * * * * * \n| * s . . e *\n| * * * * * * }, items: [], entities: [] }" correct = Level { RPGEngine.Data.layout = [ [Blocked, Blocked, Blocked, Blocked, Blocked, Blocked], @@ -128,7 +132,8 @@ spec = do items = [], entities = [] } - Right struct = parseWith structElement input - structToLevel struct `shouldBe` correct + Right struct = parseWith structure input + structureToLevel struct `shouldBe` correct + it "TODO: Complex layout" $ do pending \ No newline at end of file diff --git a/test/ParseStructElementSpec.hs b/test/Parser/StructureSpec.hs similarity index 87% rename from test/ParseStructElementSpec.hs rename to test/Parser/StructureSpec.hs index 0f7464a..e9296b8 100644 --- a/test/ParseStructElementSpec.hs +++ b/test/Parser/StructureSpec.hs @@ -1,10 +1,10 @@ -module ParseStructElementSpec where +module Parser.StructureSpec where import Test.Hspec import RPGEngine.Data import RPGEngine.Parse.Core -import RPGEngine.Parse.StructElement +import RPGEngine.Parse.TextToStructure spec :: Spec spec = do @@ -12,21 +12,21 @@ spec = do it "can parse blocks" $ do let input = "{}" correct = Right $ Block [] - parseWith structElement input `shouldBe` correct + parseWith structure input `shouldBe` correct let input = "{{}}" correct = Right $ Block [Block []] - parseWith structElement input `shouldBe` correct + parseWith structure input `shouldBe` correct let input = "{{}, {}}" correct = Right $ Block [Block [], Block []] - parseWith structElement input `shouldBe` correct + parseWith structure input `shouldBe` correct let input = "{ id: 1 }" correct = Right (Block [ Entry (Tag "id") $ Regular $ Integer 1 ], "") - parseWithRest structElement input `shouldBe` correct + parseWithRest structure input `shouldBe` correct let input = "{ id: \"key\", x: 3, y: 1}" correct = Right $ Block [ @@ -34,14 +34,14 @@ spec = do Entry (Tag "x") $ Regular $ Integer 3, Entry (Tag "y") $ Regular $ Integer 1 ] - parseWith structElement input `shouldBe` correct + 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 structElement input `shouldBe` correct + 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 [ @@ -52,7 +52,7 @@ spec = do Entry (Tag "direction") $ Regular $ Direction West, Entry (Tag "y") $ Regular $ Integer 1 ]], "") - parseWithRest structElement input `shouldBe` correct + 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 [ @@ -66,7 +66,7 @@ spec = do Entry (ConditionList []) $ Regular $ Action Leave ] ]], "") - parseWithRest structElement input `shouldBe` correct + 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 [ @@ -81,7 +81,7 @@ spec = do Entry (ConditionList []) $ Regular $ Action Leave ] ]], "") - parseWithRest structElement input `shouldBe` correct + parseWithRest structure input `shouldBe` correct it "can parse entries" $ do let input = "id: \"dagger\"" @@ -105,7 +105,7 @@ spec = do Entry (ConditionList [Not InventoryFull]) $ Regular $ Action $ RetrieveItem "key", Entry (ConditionList []) $ Regular $ Action Leave ], "") - parseWithRest structElement input `shouldBe` correct + parseWithRest structure input `shouldBe` correct it "can parse regulars" $ do let input = "this is a string" @@ -237,19 +237,19 @@ spec = do it "can parse directions" $ do let input = "up" correct = Right $ Direction North - parseWith RPGEngine.Parse.StructElement.direction input `shouldBe` correct + parseWith RPGEngine.Parse.TextToStructure.direction input `shouldBe` correct let input = "right" correct = Right $ Direction East - parseWith RPGEngine.Parse.StructElement.direction input `shouldBe` correct + parseWith RPGEngine.Parse.TextToStructure.direction input `shouldBe` correct let input = "down" correct = Right $ Direction South - parseWith RPGEngine.Parse.StructElement.direction input `shouldBe` correct + parseWith RPGEngine.Parse.TextToStructure.direction input `shouldBe` correct let input = "left" correct = Right $ Direction West - parseWith RPGEngine.Parse.StructElement.direction input `shouldBe` correct + parseWith RPGEngine.Parse.TextToStructure.direction input `shouldBe` correct it "can parse layouts" $ do let input = "| * * * * * * * *\n| * s . . . . e *\n| * * * * * * * *" @@ -258,7 +258,16 @@ spec = do [Blocked, Entrance, Walkable, Walkable, Walkable, Walkable, Exit, Blocked], [Blocked, Blocked, Blocked, Blocked, Blocked, Blocked, Blocked, Blocked] ] - parseWith RPGEngine.Parse.StructElement.layout input `shouldBe` correct + parseWith RPGEngine.Parse.TextToStructure.layout input `shouldBe` correct + + let input = "{ |* * * * * * * *|* s . . . . e *|* * * * * * * * }" + -- correct = Right $ Entry (Tag "layout") $ Regular $ Layout [ + 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 RPGEngine.Parse.TextToStructure.value input `shouldBe` correct describe "Brackets" $ do it "matches closing <" $ do diff --git a/test/RPGEngineSpec.hs b/test/Spec.hs similarity index 100% rename from test/RPGEngineSpec.hs rename to test/Spec.hs From b7278d6afcf2aff3ee0823a980a2ed4fca49b74d Mon Sep 17 00:00:00 2001 From: Tibo De Peuter Date: Thu, 22 Dec 2022 09:43:17 +0100 Subject: [PATCH 14/27] Jumpbacks and continue --- lib/RPGEngine/Data.hs | 30 ++++++++++++++------------ lib/RPGEngine/Data/Game.hs | 6 +++--- lib/RPGEngine/Input/Paused.hs | 11 +++++++--- lib/RPGEngine/Input/Playing.hs | 13 ++++++----- lib/RPGEngine/Render.hs | 9 ++++---- lib/RPGEngine/Render/LevelSelection.hs | 4 ++-- lib/RPGEngine/Render/Lose.hs | 4 ++-- lib/RPGEngine/Render/Menu.hs | 4 ++-- lib/RPGEngine/Render/Paused.hs | 9 ++++---- lib/RPGEngine/Render/Playing.hs | 9 +++++--- lib/RPGEngine/Render/Win.hs | 4 ++-- 11 files changed, 59 insertions(+), 44 deletions(-) diff --git a/lib/RPGEngine/Data.hs b/lib/RPGEngine/Data.hs index 23b25c8..a47cded 100644 --- a/lib/RPGEngine/Data.hs +++ b/lib/RPGEngine/Data.hs @@ -11,35 +11,37 @@ import RPGEngine.Render.Core ( Renderer ) -- A game is the base data container. data Game = Game { - state :: State, - levels :: [Level], - player :: Player + state :: State } ------------------------------- State -------------------------------- -- Code reusability data StateBase = StateBase { - renderer :: Renderer Game, + renderer :: Renderer State, inputHandler :: InputHandler Game } -- Main menu -data State = Menu { base :: StateBase } +data State = Menu { base :: StateBase } -- Select the level you want to play - | LevelSelection { base :: StateBase, - levelList :: [FilePath], - selector :: ListSelector } + | LevelSelection { base :: StateBase, + levelList :: [FilePath], + selector :: ListSelector } -- Playing a level - | Playing { base :: StateBase, - level :: Level } + | Playing { base :: StateBase, + levels :: [Level], + level :: Level, + player :: Player, + restart :: State } -- Paused while playing a level - | Paused { base :: StateBase, - level :: Level } + | Paused { base :: StateBase, + continue :: State } -- Won a level - | Win { base :: StateBase } + | Win { base :: StateBase } -- Lost a level - | Lose { base :: StateBase } + | Lose { base :: StateBase, + restart :: State } ------------------------------- Level -------------------------------- diff --git a/lib/RPGEngine/Data/Game.hs b/lib/RPGEngine/Data/Game.hs index 2b21cd5..37fe826 100644 --- a/lib/RPGEngine/Data/Game.hs +++ b/lib/RPGEngine/Data/Game.hs @@ -6,15 +6,15 @@ import RPGEngine.Data ( Player(Player, position), Direction, Physical(Exit, Walkable, Entrance), - State(Playing, level), - Game(Game, state, player) ) + 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 +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) diff --git a/lib/RPGEngine/Input/Paused.hs b/lib/RPGEngine/Input/Paused.hs index 03522dd..5420296 100644 --- a/lib/RPGEngine/Input/Paused.hs +++ b/lib/RPGEngine/Input/Paused.hs @@ -2,11 +2,16 @@ module RPGEngine.Input.Paused ( handleInputPaused ) where -import RPGEngine.Input.Core ( InputHandler ) +import RPGEngine.Input.Core ( InputHandler, handleAnyKey ) -import RPGEngine.Data ( Game ) +import RPGEngine.Data ( Game (..), State (..) ) ------------------------------ Exported ------------------------------ handleInputPaused :: InputHandler Game -handleInputPaused = undefined +handleInputPaused = handleAnyKey continueGame + +continueGame :: Game -> Game +continueGame g@Game{ state = Paused{ continue = state }} = newGame + where newGame = g{ state = state } +continueGame g = g \ No newline at end of file diff --git a/lib/RPGEngine/Input/Playing.hs b/lib/RPGEngine/Input/Playing.hs index b2638e0..c02a548 100644 --- a/lib/RPGEngine/Input/Playing.hs +++ b/lib/RPGEngine/Input/Playing.hs @@ -15,7 +15,7 @@ import RPGEngine.Data Level(Level, layout), State(..), StateBase(StateBase, renderer, inputHandler), - Game(Game, state, player) ) + Game(..) ) import RPGEngine.Data.Level ( findFirstOf, directionOffsets ) import RPGEngine.Data.Game ( isLegalMove ) import RPGEngine.Input.Paused ( handleInputPaused ) @@ -53,19 +53,22 @@ spawnPlayer l@Level{ layout = lay } p@Player{ position = prevPos } = p{ position ---------------------------------------------------------------------- pauseGame :: Game -> Game -pauseGame g@Game{ state = Playing{ level = level } } = pausedGame +pauseGame g@Game{ state = playing@Playing{} } = pausedGame where pausedGame = g{ state = pausedState } - pausedState = Paused{ base = newBase, level = level } + pausedState = Paused{ base = newBase, continue = playing } newBase = StateBase { renderer = renderPaused, inputHandler = handleInputPaused } +pauseGame g = g -- Move a player in a direction if possible. movePlayer :: Direction -> Game -> Game -movePlayer dir g@Game{ player = p@Player{ position = (x, y) }} = newGame - where newGame = g{ player = newPlayer } +movePlayer dir g@Game{ state = s@Playing{ player = p@Player{ position = (x, y) }}} = newGame + 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 -- TODO goToNextLevel :: Game -> Game diff --git a/lib/RPGEngine/Render.hs b/lib/RPGEngine/Render.hs index fb9152b..0699d29 100644 --- a/lib/RPGEngine/Render.hs +++ b/lib/RPGEngine/Render.hs @@ -26,13 +26,14 @@ initWindow = InWindow -- Initialize the game initGame :: Game initGame = Game { - state = Menu{ base = StateBase{ renderer = renderMenu, inputHandler = handleInputMenu }}, - levels = [defaultLevel], - player = spawnPlayer defaultLevel defaultPlayer + state = Menu{ base = StateBase{ + renderer = renderMenu, + inputHandler = handleInputMenu + }} } -- Render all different states render :: Game -> Picture -render g@Game{ state = state } = renderFunc g +render g@Game{ state = state } = renderFunc state where stateBase = base state renderFunc = renderer stateBase \ No newline at end of file diff --git a/lib/RPGEngine/Render/LevelSelection.hs b/lib/RPGEngine/Render/LevelSelection.hs index ee3da56..f00e157 100644 --- a/lib/RPGEngine/Render/LevelSelection.hs +++ b/lib/RPGEngine/Render/LevelSelection.hs @@ -15,8 +15,8 @@ import Graphics.Gloss.Data.Color (red) ------------------------------ Exported ------------------------------ -renderLevelSelection :: Renderer Game -renderLevelSelection Game{ state = state } = result +renderLevelSelection :: Renderer State +renderLevelSelection state = result where result = renderLevelList state ---------------------------------------------------------------------- diff --git a/lib/RPGEngine/Render/Lose.hs b/lib/RPGEngine/Render/Lose.hs index b3266e9..3154bea 100644 --- a/lib/RPGEngine/Render/Lose.hs +++ b/lib/RPGEngine/Render/Lose.hs @@ -4,11 +4,11 @@ module RPGEngine.Render.Lose import RPGEngine.Render.Core ( Renderer ) -import RPGEngine.Data ( Game ) +import RPGEngine.Data ( State ) import Graphics.Gloss ( text ) ---------------------------------------------------------------------- -- TODO -renderLose :: Renderer Game +renderLose :: Renderer State renderLose _ = text "Win" \ No newline at end of file diff --git a/lib/RPGEngine/Render/Menu.hs b/lib/RPGEngine/Render/Menu.hs index 26ec414..6905814 100644 --- a/lib/RPGEngine/Render/Menu.hs +++ b/lib/RPGEngine/Render/Menu.hs @@ -4,11 +4,11 @@ module RPGEngine.Render.Menu import RPGEngine.Render.Core ( Renderer ) -import RPGEngine.Data ( Game ) +import RPGEngine.Data ( State ) import Graphics.Gloss (text) ---------------------------------------------------------------------- -- TODO -renderMenu :: Renderer Game +renderMenu :: Renderer State renderMenu _ = text "[Press any key to start]" \ No newline at end of file diff --git a/lib/RPGEngine/Render/Paused.hs b/lib/RPGEngine/Render/Paused.hs index 3a49a64..bc54169 100644 --- a/lib/RPGEngine/Render/Paused.hs +++ b/lib/RPGEngine/Render/Paused.hs @@ -4,7 +4,7 @@ module RPGEngine.Render.Paused import RPGEngine.Render.Core ( Renderer, overlay ) -import RPGEngine.Data ( Game ) +import RPGEngine.Data ( State (..) ) import Graphics.Gloss ( pictures, scale, text ) import RPGEngine.Render.Playing ( renderPlaying ) import Graphics.Gloss.Data.Picture (color) @@ -12,9 +12,10 @@ import Graphics.Gloss.Data.Color (white) ------------------------------ Exported ------------------------------ -renderPaused :: Renderer Game -renderPaused g = pictures [renderPlaying g, pause] - where pause = pictures [ +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]" ] \ No newline at end of file diff --git a/lib/RPGEngine/Render/Playing.hs b/lib/RPGEngine/Render/Playing.hs index 0f075ba..dd9f739 100644 --- a/lib/RPGEngine/Render/Playing.hs +++ b/lib/RPGEngine/Render/Playing.hs @@ -16,14 +16,17 @@ import RPGEngine.Data import Graphics.Gloss ( Picture, pictures ) import Graphics.Gloss.Data.Picture (translate) import RPGEngine.Config (resolution, zoom) +import Graphics.Gloss (text) +import Graphics.Gloss (blank) ------------------------------ Exported ------------------------------ -renderPlaying :: Renderer Game -renderPlaying g@Game{ state = Playing { level = lvl }, player = player } = pictures [ +renderPlaying :: Renderer State +renderPlaying Playing { level = lvl, player = player } = pictures [ renderLevel lvl, renderPlayer player ] +renderPlaying _ = blank ------------------------------- Player ------------------------------- @@ -35,7 +38,7 @@ renderPlayer Player{ position = (x, y) } = move picture -- 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{ player = Player{ position = (x, y)}} = move +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) diff --git a/lib/RPGEngine/Render/Win.hs b/lib/RPGEngine/Render/Win.hs index 55d893b..62c93b5 100644 --- a/lib/RPGEngine/Render/Win.hs +++ b/lib/RPGEngine/Render/Win.hs @@ -4,11 +4,11 @@ module RPGEngine.Render.Win import RPGEngine.Render.Core ( Renderer ) -import RPGEngine.Data ( Game ) +import RPGEngine.Data ( State ) import Graphics.Gloss (text) ---------------------------------------------------------------------- -- TODO -renderWin :: Renderer Game +renderWin :: Renderer State renderWin _ = text "Win" \ No newline at end of file From f529fc52372c8297ee9874e3a3595f571471dfcf Mon Sep 17 00:00:00 2001 From: Tibo De Peuter Date: Thu, 22 Dec 2022 13:31:46 +0100 Subject: [PATCH 15/27] Fix dependency loop --- lib/Input.hs | 10 ----- lib/RPGEngine.hs | 62 +++++++++++++++++++++++++- lib/RPGEngine/Data.hs | 23 +++------- lib/RPGEngine/Data/Default.hs | 27 +++++++---- lib/RPGEngine/Data/Game.hs | 18 +++++++- lib/RPGEngine/Data/Level.hs | 2 +- lib/RPGEngine/Input.hs | 19 ++++++-- lib/RPGEngine/Input/Core.hs | 2 + lib/RPGEngine/Input/LevelSelection.hs | 17 +++---- lib/RPGEngine/Input/Lose.hs | 5 +-- lib/RPGEngine/Input/Menu.hs | 29 ++++-------- lib/RPGEngine/Input/Paused.hs | 9 ++-- lib/RPGEngine/Input/Playing.hs | 56 +++++++++++------------ lib/RPGEngine/Input/Win.hs | 15 ++++--- lib/RPGEngine/Parse.hs | 4 +- lib/RPGEngine/Parse/StructureToGame.hs | 18 ++++---- lib/RPGEngine/Parse/TextToStructure.hs | 6 +-- lib/RPGEngine/Render.hs | 39 ++++++++-------- lib/RPGEngine/Render/LevelSelection.hs | 15 +++---- lib/RPGEngine/Render/Lose.hs | 13 +++--- lib/RPGEngine/Render/Menu.hs | 7 +-- lib/RPGEngine/Render/Paused.hs | 11 +++-- lib/RPGEngine/Render/Playing.hs | 20 +++------ lib/RPGEngine/Render/Win.hs | 11 ++--- rpg-engine.cabal | 12 ++--- 25 files changed, 251 insertions(+), 199 deletions(-) delete mode 100644 lib/Input.hs diff --git a/lib/Input.hs b/lib/Input.hs deleted file mode 100644 index 9f63d99..0000000 --- a/lib/Input.hs +++ /dev/null @@ -1,10 +0,0 @@ --- Go to the next stage of the Game --- setNextState :: Game -> Game --- setNextState game = game{ state = newState } --- where newState = nextState $ state game - --- -- Get the next state based on the current state --- nextState :: State -> State --- nextState Menu {} = defaultLvlSelect --- nextState Pause {} = Playing --- nextState _ = Menu diff --git a/lib/RPGEngine.hs b/lib/RPGEngine.hs index a2855cf..db19c09 100644 --- a/lib/RPGEngine.hs +++ b/lib/RPGEngine.hs @@ -6,8 +6,11 @@ module RPGEngine ) where import RPGEngine.Config ( bgColor, winDimensions, winOffsets ) -import RPGEngine.Render ( initWindow, render, initGame ) +import RPGEngine.Render ( initWindow, render ) import RPGEngine.Input ( handleAllInput ) +import RPGEngine.Input.Playing ( checkPlaying, spawnPlayer ) +import RPGEngine.Data (Game (..), State (..), Layout, Level (..), Physical (..)) +import RPGEngine.Data.Default (defaultLevel, defaultPlayer) import Graphics.Gloss ( play ) @@ -19,4 +22,59 @@ playRPGEngine :: String -> Int -> IO() playRPGEngine title fps = do play window bgColor fps initGame render handleAllInput step where window = initWindow title winDimensions winOffsets - step _ g = g -- TODO Do something with step? Check health etc. \ No newline at end of file + step _ = checkPlaying -- TODO Do something with step? Check health etc. + +-- TODO revert this +-- Initialize the game +initGame :: Game +-- initGame = Game { +-- state = Menu{ base = StateBase{ +-- renderer = renderMenu, +-- inputHandler = handleInputMenu +-- }} +-- } +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, 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, Exit), + (2, 3, Blocked), + (0, 4, Blocked), + (1, 4, Blocked), + (2, 4, Blocked) + ], + items = [], + entities = [] +} \ No newline at end of file diff --git a/lib/RPGEngine/Data.hs b/lib/RPGEngine/Data.hs index a47cded..2c6cfa1 100644 --- a/lib/RPGEngine/Data.hs +++ b/lib/RPGEngine/Data.hs @@ -16,32 +16,23 @@ data Game = Game { ------------------------------- State -------------------------------- --- Code reusability -data StateBase = StateBase { - renderer :: Renderer State, - inputHandler :: InputHandler Game -} - -- Main menu -data State = Menu { base :: StateBase } +data State = Menu -- Select the level you want to play - | LevelSelection { base :: StateBase, - levelList :: [FilePath], + | LevelSelection { levelList :: [FilePath], selector :: ListSelector } -- Playing a level - | Playing { base :: StateBase, - levels :: [Level], + | Playing { levels :: [Level], + count :: Int, level :: Level, player :: Player, restart :: State } -- Paused while playing a level - | Paused { base :: StateBase, - continue :: State } + | Paused { continue :: State } -- Won a level - | Win { base :: StateBase } + | Win -- Lost a level - | Lose { base :: StateBase, - restart :: State } + | Lose { restart :: State } ------------------------------- Level -------------------------------- diff --git a/lib/RPGEngine/Data/Default.hs b/lib/RPGEngine/Data/Default.hs index 7129a12..f877f7f 100644 --- a/lib/RPGEngine/Data/Default.hs +++ b/lib/RPGEngine/Data/Default.hs @@ -1,11 +1,8 @@ module RPGEngine.Data.Default -- Everything is exported where -import RPGEngine.Data (Entity (..), Game (..), Item (..), Layout, Player (..), Level (..), StateBase (..), State (..), Physical (..), Direction (..)) +import RPGEngine.Data (Entity (..), Game (..), Item (..), Layout, Player (..), Level (..), State (..), Physical (..), Direction (..)) import RPGEngine.Input.Core (ListSelector(..)) -import RPGEngine.Render.LevelSelection (renderLevelSelection) -import RPGEngine.Input.Playing (spawnPlayer) -import RPGEngine.Render.Menu (renderMenu) ------------------------------ Defaults ------------------------------ @@ -36,9 +33,9 @@ defaultItem = Item { defaultLayout :: Layout defaultLayout = [ - [Blocked, Blocked, Blocked], - [Blocked, Entrance, Blocked], - [Blocked, Blocked, Blocked] + [Blocked, Blocked, Blocked, Blocked, Blocked], + [Blocked, Entrance, Walkable, Exit, Blocked], + [Blocked, Blocked, Blocked, Blocked, Blocked] ] defaultLevel :: Level @@ -52,8 +49,14 @@ defaultLevel = Level { (1, 1, Entrance), (1, 2, Blocked), (2, 0, Blocked), - (2, 1, Blocked), - (2, 2, 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 = [] @@ -64,4 +67,10 @@ defaultPlayer = Player { playerHp = Prelude.Nothing, -- Compares to infinity inventory = [], position = (0, 0) +} + +defaultSelector :: ListSelector +defaultSelector = ListSelector { + selection = 0, + selected = False } \ No newline at end of file diff --git a/lib/RPGEngine/Data/Game.hs b/lib/RPGEngine/Data/Game.hs index 37fe826..bd1cae4 100644 --- a/lib/RPGEngine/Data/Game.hs +++ b/lib/RPGEngine/Data/Game.hs @@ -1,5 +1,7 @@ module RPGEngine.Data.Game ( isLegalMove +, isPlayerAtExit +, isPlayerDead ) where import RPGEngine.Data @@ -14,9 +16,21 @@ import RPGEngine.Data.Level (findAt, directionOffsets) -- 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 +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 \ No newline at end of file +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 _ = False \ No newline at end of file diff --git a/lib/RPGEngine/Data/Level.hs b/lib/RPGEngine/Data/Level.hs index 86ef84d..f41a0c1 100644 --- a/lib/RPGEngine/Data/Level.hs +++ b/lib/RPGEngine/Data/Level.hs @@ -5,7 +5,7 @@ where import GHC.IO (unsafePerformIO) import System.Directory (getDirectoryContents) import RPGEngine.Input.Core (ListSelector(..)) -import RPGEngine.Data (Level (..), Physical (..), Direction (..), Entity (..), Game (..), Item (..), Player (..), StateBase (..), State (..), X, Y, Layout) +import RPGEngine.Data (Level (..), Physical (..), Direction (..), Entity (..), Game (..), Item (..), Player (..), State (..), X, Y, Layout) import RPGEngine.Config (levelFolder) ------------------------------ Exported ------------------------------ diff --git a/lib/RPGEngine/Input.hs b/lib/RPGEngine/Input.hs index 485affb..1314c9e 100644 --- a/lib/RPGEngine/Input.hs +++ b/lib/RPGEngine/Input.hs @@ -4,12 +4,23 @@ module RPGEngine.Input ( handleAllInput ) where -import RPGEngine.Input.Core -import RPGEngine.Data +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 ) ------------------------------ Exported ------------------------------ -- Handle all input of all states of the game. handleAllInput :: InputHandler Game -handleAllInput ev g@Game{ state = state } = handleInput ev g - where handleInput = inputHandler $ base state \ No newline at end of file +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 \ No newline at end of file diff --git a/lib/RPGEngine/Input/Core.hs b/lib/RPGEngine/Input/Core.hs index 9044c1d..780a87b 100644 --- a/lib/RPGEngine/Input/Core.hs +++ b/lib/RPGEngine/Input/Core.hs @@ -6,6 +6,8 @@ module RPGEngine.Input.Core , handle , handleKey , handleAnyKey + +, SpecialKey(..) ) where import Graphics.Gloss.Interface.Pure.Game diff --git a/lib/RPGEngine/Input/LevelSelection.hs b/lib/RPGEngine/Input/LevelSelection.hs index d2f3578..33bdce8 100644 --- a/lib/RPGEngine/Input/LevelSelection.hs +++ b/lib/RPGEngine/Input/LevelSelection.hs @@ -1,18 +1,13 @@ -module RPGEngine.Input.LevelSelection +module RPGEngine.Input.LevelSelection ( handleInputLevelSelection ) where -import RPGEngine.Input.Core - ( composeInputHandlers, handleKey, InputHandler, ListSelector (..) ) +import RPGEngine.Input.Core (InputHandler, composeInputHandlers, handleKey, ListSelector (..)) -import RPGEngine.Config ( levelFolder ) -import RPGEngine.Data ( Game (..), Direction (..), State (..), StateBase (..) ) - -import Graphics.Gloss.Interface.IO.Game - ( Key(SpecialKey), SpecialKey(KeySpace) ) +import RPGEngine.Data (Game (..), State (..), Direction (..)) +import Graphics.Gloss.Interface.IO.Game (Key(..)) import Graphics.Gloss.Interface.IO.Interact (SpecialKey(..)) -import RPGEngine.Render.Playing (renderPlaying) -import RPGEngine.Input.Playing (handleInputPlaying) +import RPGEngine.Config (levelFolder) import RPGEngine.Parse (parse) ------------------------------ Exported ------------------------------ @@ -46,4 +41,4 @@ moveSelector dir game@Game{ state = state@LevelSelection{ levelList = list, sele diff | dir == North = -1 | dir == South = 1 | otherwise = 0 -moveSelector _ g = g +moveSelector _ g = g \ No newline at end of file diff --git a/lib/RPGEngine/Input/Lose.hs b/lib/RPGEngine/Input/Lose.hs index f9c6d0e..007a25f 100644 --- a/lib/RPGEngine/Input/Lose.hs +++ b/lib/RPGEngine/Input/Lose.hs @@ -2,12 +2,11 @@ module RPGEngine.Input.Lose ( handleInputLose ) where -import RPGEngine.Input.Core ( InputHandler ) +import RPGEngine.Input.Core (InputHandler) -import RPGEngine.Data ( Game ) +import RPGEngine.Data (Game) ------------------------------ Exported ------------------------------ --- TODO handleInputLose :: InputHandler Game handleInputLose = undefined \ No newline at end of file diff --git a/lib/RPGEngine/Input/Menu.hs b/lib/RPGEngine/Input/Menu.hs index 6903d0d..9dd27d8 100644 --- a/lib/RPGEngine/Input/Menu.hs +++ b/lib/RPGEngine/Input/Menu.hs @@ -2,35 +2,22 @@ module RPGEngine.Input.Menu ( handleInputMenu ) where -import RPGEngine.Input.Core ( InputHandler, composeInputHandlers, handleAnyKey, ListSelector (..) ) - -import RPGEngine.Data ( Game (..), State (..), StateBase (..) ) -import RPGEngine.Render.LevelSelection (renderLevelSelection) -import RPGEngine.Input.LevelSelection (handleInputLevelSelection) +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 selectLevel + handleAnyKey (\game -> game{ state = startLevelSelection }) ] ---------------------------------------------------------------------- -selectLevel :: Game -> Game -selectLevel g@Game{ state = state } = g{ state = defaultLevelSelection } - -defaultLevelSelection :: State -defaultLevelSelection = LevelSelection { base = base, selector = defaultSelector, levelList = levels } - where base = StateBase { - renderer = renderLevelSelection, - inputHandler = handleInputLevelSelection - } - levels = getLevelList - -defaultSelector :: ListSelector -defaultSelector = ListSelector { - selection = 0, - selected = False +startLevelSelection :: State +startLevelSelection = LevelSelection { + levelList = getLevelList, + selector = defaultSelector } \ No newline at end of file diff --git a/lib/RPGEngine/Input/Paused.hs b/lib/RPGEngine/Input/Paused.hs index 5420296..7ef6c63 100644 --- a/lib/RPGEngine/Input/Paused.hs +++ b/lib/RPGEngine/Input/Paused.hs @@ -1,16 +1,17 @@ -module RPGEngine.Input.Paused +module RPGEngine.Input.Paused ( handleInputPaused ) where -import RPGEngine.Input.Core ( InputHandler, handleAnyKey ) - -import RPGEngine.Data ( Game (..), State (..) ) +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 } diff --git a/lib/RPGEngine/Input/Playing.hs b/lib/RPGEngine/Input/Playing.hs index c02a548..e8b6ab0 100644 --- a/lib/RPGEngine/Input/Playing.hs +++ b/lib/RPGEngine/Input/Playing.hs @@ -1,28 +1,17 @@ -module RPGEngine.Input.Playing +module RPGEngine.Input.Playing ( handleInputPlaying +, checkPlaying , spawnPlayer ) where -import RPGEngine.Input.Core - ( composeInputHandlers, handleKey, InputHandler ) +import RPGEngine.Input.Core (InputHandler, handleKey, composeInputHandlers) -import RPGEngine.Data - ( Player(Player, position), - Direction(West, North, East, South), - Physical(Entrance), - Y, - X, - Level(Level, layout), - State(..), - StateBase(StateBase, renderer, inputHandler), - Game(..) ) -import RPGEngine.Data.Level ( findFirstOf, directionOffsets ) -import RPGEngine.Data.Game ( isLegalMove ) -import RPGEngine.Input.Paused ( handleInputPaused ) -import RPGEngine.Render.Paused ( renderPaused ) +import RPGEngine.Data (Game (..), Layout(..), Level(..), Physical(..), Player(..), State(..), X, Y, Direction (..)) +import RPGEngine.Data.Game (isLegalMove, isPlayerDead, isPlayerAtExit) +import RPGEngine.Data.Level (directionOffsets, findFirstOf) +import Data.Maybe (fromJust, isNothing) import Graphics.Gloss.Interface.IO.Game (Key(..)) import Graphics.Gloss.Interface.IO.Interact (SpecialKey(..)) -import Data.Maybe (isNothing, fromJust) ------------------------------ Exported ------------------------------ @@ -43,6 +32,8 @@ handleInputPlaying = composeInputHandlers [ handleKey (Char 'a') $ movePlayer West ] +---------------------------------------------------------------------- + -- 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 } @@ -50,15 +41,30 @@ spawnPlayer l@Level{ layout = lay } p@Player{ position = prevPos } = p{ position 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 = restart } +checkPlaying g = g pauseGame :: Game -> Game pauseGame g@Game{ state = playing@Playing{} } = pausedGame - where pausedGame = g{ state = pausedState } - pausedState = Paused{ base = newBase, continue = playing } - newBase = StateBase { renderer = renderPaused, inputHandler = handleInputPaused } + where pausedGame = g{ state = Paused playing } pauseGame g = g +-- 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 -- Either the next level or winState + nextState | (count + 1) < length levels = nextLevelState + | otherwise = Win + nextLevelState = s{ level = nextLevel, count = count + 1, player = movedPlayer } + 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) }}} = newGame @@ -70,12 +76,6 @@ movePlayer dir g@Game{ state = s@Playing{ player = p@Player{ position = (x, y) } (xD, yD) = directionOffsets dir movePlayer _ g = g --- TODO -goToNextLevel :: Game -> Game -goToNextLevel = undefined - ----------------------------------------------------------------------- - -- 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 diff --git a/lib/RPGEngine/Input/Win.hs b/lib/RPGEngine/Input/Win.hs index 37434ee..3eeaf5d 100644 --- a/lib/RPGEngine/Input/Win.hs +++ b/lib/RPGEngine/Input/Win.hs @@ -1,13 +1,16 @@ -module RPGEngine.Input.Win +module RPGEngine.Input.Win ( handleInputWin ) where -import RPGEngine.Input.Core ( InputHandler ) - -import RPGEngine.Data ( Game ) +import RPGEngine.Input.Core (InputHandler, handleAnyKey) +import RPGEngine.Data (Game (..), State (Menu)) ------------------------------ Exported ------------------------------ --- TODO handleInputWin :: InputHandler Game -handleInputWin = undefined \ No newline at end of file +handleInputWin = handleAnyKey goToMenu + +---------------------------------------------------------------------- + +goToMenu :: Game -> Game +goToMenu g = g{ state = Menu } \ No newline at end of file diff --git a/lib/RPGEngine/Parse.hs b/lib/RPGEngine/Parse.hs index 9b1c976..c12e8da 100644 --- a/lib/RPGEngine/Parse.hs +++ b/lib/RPGEngine/Parse.hs @@ -13,4 +13,6 @@ import RPGEngine.Parse.TextToStructure (structure) parse :: FilePath -> Game parse filename = structureToGame struct where (Right struct) = unsafePerformIO io - io = parseFromFile structure filename \ No newline at end of file + io = parseFromFile structure filename + +tempParse = parseFromFile \ No newline at end of file diff --git a/lib/RPGEngine/Parse/StructureToGame.hs b/lib/RPGEngine/Parse/StructureToGame.hs index ddbcd3d..7e81274 100644 --- a/lib/RPGEngine/Parse/StructureToGame.hs +++ b/lib/RPGEngine/Parse/StructureToGame.hs @@ -11,24 +11,24 @@ import RPGEngine.Data Item(itemId, itemX, itemY, itemName, itemDescription, itemValue, itemActions, useTimes), Level(layout, items, entities), - Game (..), State (..), StateBase (..) ) + 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.Render.Playing (renderPlaying) -import RPGEngine.Input.Playing (handleInputPlaying) ------------------------------ Exported ------------------------------ structureToGame :: Structure -> Game -structureToGame (Block [(Entry(Tag "player") playerBlock), (Entry(Tag "levels") levelsBlock)]) = game - where game = Game{ state = newState, levels = newLevels, player = newPlayer } - newState = Playing{ base = playingBase, level = currentLevel } - playingBase = StateBase{ renderer = renderPlaying, inputHandler = handleInputPlaying } - newLevels = structureToLevels levelsBlock - currentLevel = head newLevels +-- structureToGame [Entry(Tag "player") playerBlock, Entry(Tag "levels") levelsBlock] = game +structureToGame (Entry (Tag "player") playerBlock) = game + where game = Game{ state = newState } + newState = Playing{ levels = newLevels, level = currentLevel, player = newPlayer, restart = newState } + -- newLevels = structureToLevels levelsBlock + -- currentLevel = head newLevels + newLevels = [defaultLevel] + currentLevel = defaultLevel newPlayer = structureToPlayer playerBlock ------------------------------- Player ------------------------------- diff --git a/lib/RPGEngine/Parse/TextToStructure.hs b/lib/RPGEngine/Parse/TextToStructure.hs index 6f1b060..dc76003 100644 --- a/lib/RPGEngine/Parse/TextToStructure.hs +++ b/lib/RPGEngine/Parse/TextToStructure.hs @@ -27,9 +27,9 @@ import Text.Parsec.String ( Parser ) -- 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) + | Entry Key Structure -- Key + Value + | Regular Value -- Regular value, Integer or String or Infinite + deriving (Eq, Show) ---------------------------------------------------------------------- diff --git a/lib/RPGEngine/Render.hs b/lib/RPGEngine/Render.hs index 0699d29..907f3e5 100644 --- a/lib/RPGEngine/Render.hs +++ b/lib/RPGEngine/Render.hs @@ -2,20 +2,22 @@ -- submodules. module RPGEngine.Render ( initWindow -, initGame , render ) where import RPGEngine.Render.Core ( Renderer(..) ) -import RPGEngine.Data ( State(..), Game(..), StateBase(..) ) -import Graphics.Gloss ( Display ) -import Graphics.Gloss.Data.Display ( Display(InWindow) ) -import Graphics.Gloss.Data.Picture (Picture) -import RPGEngine.Data.Default (defaultLevel, defaultPlayer) -import RPGEngine.Input.Playing (spawnPlayer) -import RPGEngine.Render.Menu (renderMenu) -import RPGEngine.Input.Menu (handleInputMenu) +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) +import Graphics.Gloss.Data.Picture (Picture, blank) +import Graphics.Gloss.Data.Display (Display(..)) ---------------------------------------------------------------------- @@ -23,17 +25,12 @@ import RPGEngine.Input.Menu (handleInputMenu) initWindow :: String -> (Int, Int) -> (Int, Int) -> Display initWindow = InWindow --- Initialize the game -initGame :: Game -initGame = Game { - state = Menu{ base = StateBase{ - renderer = renderMenu, - inputHandler = handleInputMenu - }} -} - -- Render all different states render :: Game -> Picture -render g@Game{ state = state } = renderFunc state - where stateBase = base state - renderFunc = renderer stateBase \ No newline at end of file +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 _ = blank \ No newline at end of file diff --git a/lib/RPGEngine/Render/LevelSelection.hs b/lib/RPGEngine/Render/LevelSelection.hs index f00e157..f6e0912 100644 --- a/lib/RPGEngine/Render/LevelSelection.hs +++ b/lib/RPGEngine/Render/LevelSelection.hs @@ -2,16 +2,15 @@ module RPGEngine.Render.LevelSelection ( renderLevelSelection ) where -import RPGEngine.Config ( resolution, zoom ) -import RPGEngine.Data ( Game (..), State (..) ) -import RPGEngine.Data.Level ( getLevelList ) -import RPGEngine.Render.Core ( Renderer ) +import RPGEngine.Render.Core (Renderer) -import Graphics.Gloss - ( pictures, text, translate, blank, Picture, color ) -import Graphics.Gloss.Data.Picture (scale) -import RPGEngine.Input.Core (ListSelector (..)) +import RPGEngine.Config (resolution, zoom) +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 ------------------------------ diff --git a/lib/RPGEngine/Render/Lose.hs b/lib/RPGEngine/Render/Lose.hs index 3154bea..1e6d730 100644 --- a/lib/RPGEngine/Render/Lose.hs +++ b/lib/RPGEngine/Render/Lose.hs @@ -1,14 +1,15 @@ -module RPGEngine.Render.Lose +module RPGEngine.Render.Lose ( renderLose ) where -import RPGEngine.Render.Core ( Renderer ) +import RPGEngine.Render.Core (Renderer) -import RPGEngine.Data ( State ) -import Graphics.Gloss ( text ) +import RPGEngine.Data (State) ----------------------------------------------------------------------- +import Graphics.Gloss (text) + +------------------------------ Exported ------------------------------ -- TODO renderLose :: Renderer State -renderLose _ = text "Win" \ No newline at end of file +renderLose _ = text "You lose" \ No newline at end of file diff --git a/lib/RPGEngine/Render/Menu.hs b/lib/RPGEngine/Render/Menu.hs index 6905814..e5f66d8 100644 --- a/lib/RPGEngine/Render/Menu.hs +++ b/lib/RPGEngine/Render/Menu.hs @@ -2,12 +2,13 @@ module RPGEngine.Render.Menu ( renderMenu ) where -import RPGEngine.Render.Core ( Renderer ) +import RPGEngine.Render.Core (Renderer) + +import RPGEngine.Data (State) -import RPGEngine.Data ( State ) import Graphics.Gloss (text) ----------------------------------------------------------------------- +------------------------------ Exported ------------------------------ -- TODO renderMenu :: Renderer State diff --git a/lib/RPGEngine/Render/Paused.hs b/lib/RPGEngine/Render/Paused.hs index bc54169..6fa3d95 100644 --- a/lib/RPGEngine/Render/Paused.hs +++ b/lib/RPGEngine/Render/Paused.hs @@ -2,13 +2,12 @@ module RPGEngine.Render.Paused ( renderPaused ) where -import RPGEngine.Render.Core ( Renderer, overlay ) +import RPGEngine.Render.Core (Renderer, overlay) -import RPGEngine.Data ( State (..) ) -import Graphics.Gloss ( pictures, scale, text ) -import RPGEngine.Render.Playing ( renderPlaying ) -import Graphics.Gloss.Data.Picture (color) -import Graphics.Gloss.Data.Color (white) +import RPGEngine.Data (State(..)) +import RPGEngine.Render.Playing (renderPlaying) + +import Graphics.Gloss (pictures, white, color, Color(..), text, scale) ------------------------------ Exported ------------------------------ diff --git a/lib/RPGEngine/Render/Playing.hs b/lib/RPGEngine/Render/Playing.hs index dd9f739..6c5a47b 100644 --- a/lib/RPGEngine/Render/Playing.hs +++ b/lib/RPGEngine/Render/Playing.hs @@ -2,22 +2,13 @@ module RPGEngine.Render.Playing ( renderPlaying ) where -import RPGEngine.Render.Core ( Renderer, getRender, setRenderPos ) +import RPGEngine.Render.Core (Renderer, getRender, setRenderPos) -import RPGEngine.Data - ( Player(..), - Entity(..), - Item(..), - Physical(..), - Layout, - Level(..), - State(..), - Game(..) ) -import Graphics.Gloss ( Picture, pictures ) -import Graphics.Gloss.Data.Picture (translate) import RPGEngine.Config (resolution, zoom) -import Graphics.Gloss (text) -import Graphics.Gloss (blank) +import RPGEngine.Data (State(..), Player (..), Game (..), Level (..), Layout, Physical (..), Item (..), Entity (..)) + +import Graphics.Gloss ( pictures, Picture, translate ) +import Graphics.Gloss.Data.Picture (blank) ------------------------------ Exported ------------------------------ @@ -42,6 +33,7 @@ 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 -------------------------------- diff --git a/lib/RPGEngine/Render/Win.hs b/lib/RPGEngine/Render/Win.hs index 62c93b5..189cef8 100644 --- a/lib/RPGEngine/Render/Win.hs +++ b/lib/RPGEngine/Render/Win.hs @@ -1,14 +1,15 @@ -module RPGEngine.Render.Win +module RPGEngine.Render.Win ( renderWin ) where -import RPGEngine.Render.Core ( Renderer ) +import RPGEngine.Render.Core (Renderer) + +import RPGEngine.Data (State) -import RPGEngine.Data ( State ) import Graphics.Gloss (text) ----------------------------------------------------------------------- +------------------------------ Exported ------------------------------ -- TODO renderWin :: Renderer State -renderWin _ = text "Win" \ No newline at end of file +renderWin _ = text "You win!\nPress any key to return to the menu." \ No newline at end of file diff --git a/rpg-engine.cabal b/rpg-engine.cabal index 9043757..76a2eee 100644 --- a/rpg-engine.cabal +++ b/rpg-engine.cabal @@ -23,12 +23,12 @@ library RPGEngine.Input RPGEngine.Input.Core - RPGEngine.Input.LevelSelection - RPGEngine.Input.Lose RPGEngine.Input.Menu - RPGEngine.Input.Paused + RPGEngine.Input.LevelSelection RPGEngine.Input.Playing + RPGEngine.Input.Paused RPGEngine.Input.Win + RPGEngine.Input.Lose RPGEngine.Parse RPGEngine.Parse.Core @@ -37,12 +37,12 @@ library RPGEngine.Render RPGEngine.Render.Core - RPGEngine.Render.LevelSelection - RPGEngine.Render.Lose RPGEngine.Render.Menu - RPGEngine.Render.Paused + RPGEngine.Render.LevelSelection RPGEngine.Render.Playing + RPGEngine.Render.Paused RPGEngine.Render.Win + RPGEngine.Render.Lose executable rpg-engine main-is: Main.hs From d0302c3156ac1bd9c007748cdb59998ebc687202 Mon Sep 17 00:00:00 2001 From: Tibo De Peuter Date: Thu, 22 Dec 2022 14:35:58 +0100 Subject: [PATCH 16/27] #5 Render inventory when pressing `i` --- README.md | 13 ++++++----- lib/RPGEngine/Config.hs | 4 ++++ lib/RPGEngine/Data.hs | 8 ++++--- lib/RPGEngine/Input/Core.hs | 30 ++++++++++++------------ lib/RPGEngine/Input/LevelSelection.hs | 8 +++---- lib/RPGEngine/Input/Playing.hs | 32 +++++++++++++++++--------- lib/RPGEngine/Render.hs | 3 +-- lib/RPGEngine/Render/LevelSelection.hs | 9 ++++---- lib/RPGEngine/Render/Playing.hs | 21 ++++++++++++----- 9 files changed, 76 insertions(+), 52 deletions(-) diff --git a/README.md b/README.md index 060cb52..b82a687 100644 --- a/README.md +++ b/README.md @@ -51,12 +51,13 @@ This README serves as both documentation and project report, so excuse the detai 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` | +| Action | Primary | Secondary | +| -------------- | ------------- | ----------- | +| Move up | `Arrow Up` | `w` | +| Move left | `Arrow Left` | `a` | +| Move down | `Arrow Down` | `s` | +| Move right | `Arrow Right` | `d` | +| Show inventory | `i` | | ### Example playthrough diff --git a/lib/RPGEngine/Config.hs b/lib/RPGEngine/Config.hs index a8d719c..b752af4 100644 --- a/lib/RPGEngine/Config.hs +++ b/lib/RPGEngine/Config.hs @@ -23,6 +23,10 @@ bgColor = white zoom :: Float zoom = 5.0 +-- UI scale +uizoom :: Float +uizoom = 0.5 + -- Resolution of the texture resolution :: Float resolution = 16 diff --git a/lib/RPGEngine/Data.hs b/lib/RPGEngine/Data.hs index 2c6cfa1..01efa75 100644 --- a/lib/RPGEngine/Data.hs +++ b/lib/RPGEngine/Data.hs @@ -99,9 +99,11 @@ data Direction = North deriving (Eq, Show) data Player = Player { - playerHp :: HP, - inventory :: [Item], - position :: (X, Y) + playerHp :: HP, + inventory :: [Item], + position :: (X, Y), + showHp :: Bool, + showInventory :: Bool } deriving (Eq, Show) ------------------------------ Condition ----------------------------- diff --git a/lib/RPGEngine/Input/Core.hs b/lib/RPGEngine/Input/Core.hs index 780a87b..07ea182 100644 --- a/lib/RPGEngine/Input/Core.hs +++ b/lib/RPGEngine/Input/Core.hs @@ -32,16 +32,16 @@ composeInputHandlers (ih:ihs) ev a = composeInputHandlers ihs ev (ih ev a) -- Handle any event handle :: Event -> (a -> a) -> InputHandler a -handle (EventKey key _ _ _) = handleKey key +handle (EventKey key state _ _) = handleKey key state -- handle (EventMotion _) = undefined -- TODO -- handle (EventResize _) = undefined -- TODO -handle _ = const (const id) +handle _ = const (const id) -- Handle a event by pressing a key -handleKey :: Key -> (a -> a) -> InputHandler a -handleKey (SpecialKey sk) = handleSpecialKey sk -handleKey (Char c ) = handleCharKey c -handleKey (MouseButton _ ) = const (const id) +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 @@ -50,14 +50,14 @@ handleAnyKey _ _ = id --------------------------- Help functions --------------------------- -handleCharKey :: Char -> (a -> a) -> InputHandler a -handleCharKey c1 f (EventKey (Char c2) Down _ _) - | c1 == c2 = f - | otherwise = id -handleCharKey _ _ _ = id +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 -> (a -> a) -> InputHandler a -handleSpecialKey sk1 f (EventKey (SpecialKey sk2) Down _ _) - | sk1 == sk2 = f +handleSpecialKey :: SpecialKey -> KeyState -> (a -> a) -> InputHandler a +handleSpecialKey sk1 s1 f (EventKey (SpecialKey sk2) s2 _ _) + | sk1 == sk2 && s1 == s2 = f | otherwise = id -handleSpecialKey _ _ _ = id \ No newline at end of file +handleSpecialKey _ _ _ _ = id \ No newline at end of file diff --git a/lib/RPGEngine/Input/LevelSelection.hs b/lib/RPGEngine/Input/LevelSelection.hs index 33bdce8..5266782 100644 --- a/lib/RPGEngine/Input/LevelSelection.hs +++ b/lib/RPGEngine/Input/LevelSelection.hs @@ -6,7 +6,7 @@ import RPGEngine.Input.Core (InputHandler, composeInputHandlers, handleKey, List import RPGEngine.Data (Game (..), State (..), Direction (..)) import Graphics.Gloss.Interface.IO.Game (Key(..)) -import Graphics.Gloss.Interface.IO.Interact (SpecialKey(..)) +import Graphics.Gloss.Interface.IO.Interact (SpecialKey(..), KeyState(..)) import RPGEngine.Config (levelFolder) import RPGEngine.Parse (parse) @@ -14,10 +14,10 @@ import RPGEngine.Parse (parse) handleInputLevelSelection :: InputHandler Game handleInputLevelSelection = composeInputHandlers [ - handleKey (SpecialKey KeySpace) selectLevel, + handleKey (SpecialKey KeySpace) Down selectLevel, - handleKey (SpecialKey KeyUp) $ moveSelector North, - handleKey (SpecialKey KeyDown) $ moveSelector South + handleKey (SpecialKey KeyUp) Down $ moveSelector North, + handleKey (SpecialKey KeyDown) Down $ moveSelector South ] ---------------------------------------------------------------------- diff --git a/lib/RPGEngine/Input/Playing.hs b/lib/RPGEngine/Input/Playing.hs index e8b6ab0..6ca8f47 100644 --- a/lib/RPGEngine/Input/Playing.hs +++ b/lib/RPGEngine/Input/Playing.hs @@ -4,32 +4,36 @@ module RPGEngine.Input.Playing , spawnPlayer ) where -import RPGEngine.Input.Core (InputHandler, handleKey, composeInputHandlers) +import RPGEngine.Input.Core (InputHandler, handle, handleKey, composeInputHandlers) import RPGEngine.Data (Game (..), Layout(..), Level(..), Physical(..), Player(..), State(..), X, Y, Direction (..)) import RPGEngine.Data.Game (isLegalMove, isPlayerDead, isPlayerAtExit) import RPGEngine.Data.Level (directionOffsets, findFirstOf) + import Data.Maybe (fromJust, isNothing) import Graphics.Gloss.Interface.IO.Game (Key(..)) -import Graphics.Gloss.Interface.IO.Interact (SpecialKey(..)) +import Graphics.Gloss.Interface.IO.Interact (SpecialKey(..), KeyState(..), Event(..), KeyState(..)) ------------------------------ Exported ------------------------------ handleInputPlaying :: InputHandler Game handleInputPlaying = composeInputHandlers [ -- Pause the game - handleKey (Char 'p') pauseGame, + handleKey (Char 'p') Down pauseGame, -- Player movement - handleKey (SpecialKey KeyUp) $ movePlayer North, - handleKey (SpecialKey KeyRight) $ movePlayer East, - handleKey (SpecialKey KeyDown) $ movePlayer South, - handleKey (SpecialKey KeyLeft) $ movePlayer West, + 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') $ movePlayer North, - handleKey (Char 'd') $ movePlayer East, - handleKey (Char 's') $ movePlayer South, - handleKey (Char 'a') $ 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, + + handleKey (Char 'i') Down $ toggleInventoryShown True, + handleKey (Char 'i') Up $ toggleInventoryShown False ] ---------------------------------------------------------------------- @@ -76,6 +80,12 @@ movePlayer dir g@Game{ state = s@Playing{ player = p@Player{ position = (x, y) } (xD, yD) = directionOffsets dir movePlayer _ g = g +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 } + -- 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 diff --git a/lib/RPGEngine/Render.hs b/lib/RPGEngine/Render.hs index 907f3e5..a9c2e74 100644 --- a/lib/RPGEngine/Render.hs +++ b/lib/RPGEngine/Render.hs @@ -32,5 +32,4 @@ 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 _ = blank \ No newline at end of file +render Game{ state = s@Lose{} } = renderLose s \ No newline at end of file diff --git a/lib/RPGEngine/Render/LevelSelection.hs b/lib/RPGEngine/Render/LevelSelection.hs index f6e0912..85dc81c 100644 --- a/lib/RPGEngine/Render/LevelSelection.hs +++ b/lib/RPGEngine/Render/LevelSelection.hs @@ -4,7 +4,7 @@ module RPGEngine.Render.LevelSelection import RPGEngine.Render.Core (Renderer) -import RPGEngine.Config (resolution, zoom) +import RPGEngine.Config (resolution, zoom, uizoom) import RPGEngine.Data (State (..)) import Graphics.Gloss ( pictures, color, text, translate, blank ) @@ -25,8 +25,7 @@ renderLevelList LevelSelection{ levelList = list, selector = selector } = everyt where everything = pictures $ map render entries sel = selection selector entries = zip [0::Int .. ] list - render (i, path) | i == sel = color red $ scale zoomed zoomed $ translate 0 (offset i) $ text path - | otherwise = scale zoomed zoomed $ translate 0 (offset i) $ text path - zoomed = 0.1 * zoom - offset i = negate (2 * resolution * zoom * fromIntegral i) + render (i, path) | i == sel = color red $ scale uizoom uizoom $ translate 0 (offset i) $ text path + | otherwise = scale uizoom uizoom $ translate 0 (offset i) $ text path + offset i = negate (250 * uizoom * fromIntegral i) renderLevelList _ = blank \ No newline at end of file diff --git a/lib/RPGEngine/Render/Playing.hs b/lib/RPGEngine/Render/Playing.hs index 6c5a47b..b996d80 100644 --- a/lib/RPGEngine/Render/Playing.hs +++ b/lib/RPGEngine/Render/Playing.hs @@ -2,20 +2,21 @@ module RPGEngine.Render.Playing ( renderPlaying ) where -import RPGEngine.Render.Core (Renderer, getRender, setRenderPos) +import RPGEngine.Render.Core (Renderer, getRender, setRenderPos, overlay) -import RPGEngine.Config (resolution, zoom) +import RPGEngine.Config (resolution, zoom, uizoom) import RPGEngine.Data (State(..), Player (..), Game (..), Level (..), Layout, Physical (..), Item (..), Entity (..)) -import Graphics.Gloss ( pictures, Picture, translate ) -import Graphics.Gloss.Data.Picture (blank) +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 + renderPlayer player, + renderInventory player ] renderPlaying _ = blank @@ -83,4 +84,12 @@ renderEntities list = pictures $ map renderEntity list renderEntity :: Entity -> Picture renderEntity Entity{ entityId = id, entityX = x, entityY = y} = setRenderPos x y image - where image = getRender id \ No newline at end of file + where image = 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) \ No newline at end of file From 1dc8aac4c7a4f471c4c2e6e17f78a8cffc483e99 Mon Sep 17 00:00:00 2001 From: Tibo De Peuter Date: Thu, 22 Dec 2022 15:32:49 +0100 Subject: [PATCH 17/27] Render HP --- README.md | 2 -- assets/gui/health.png | Bin 0 -> 237 bytes lib/RPGEngine/Render/Core.hs | 11 ++++++++--- lib/RPGEngine/Render/Playing.hs | 30 +++++++++++++++++++++++------- verslag.pdf | Bin 47917 -> 50036 bytes 5 files changed, 31 insertions(+), 12 deletions(-) create mode 100644 assets/gui/health.png diff --git a/README.md b/README.md index b82a687..832883e 100644 --- a/README.md +++ b/README.md @@ -282,8 +282,6 @@ These submodules are `Config`, `Data`, `Input`, `Parse` & `Render`. They are all The following assets were used (and modified if specified): - Kyrise's Free 16x16 RPG Icon Pack[[1]](#1) - - Every needed asset was taken and put into its own `.png`, instead of in the overview. - 2D Pixel Dungeon Asset Pack by Pixel_Poem[[2]](#2) diff --git a/assets/gui/health.png b/assets/gui/health.png new file mode 100644 index 0000000000000000000000000000000000000000..a2adedad807ba78764642cb2f8bcaec6e734c814 GIT binary patch literal 237 zcmeAS@N?(olHy`uVBq!ia0vp^0wB!61|;P_|4#%`jKx9jP7LeL$-D$|SkfJR9T^xl z_H+M9WCij$3p^r=85sBugD~Uq{1quc!6r`^#}Etuy}buH4=C`s?ydgv=%(PVKJSu+ z^X{w-7SZYK?G4LN+QM(g@oU~QZ=>0>_OwrEJX>AL@>JQ4=Rl (f, renderPNG (assetsFolder ++ "entities/" ++ s))) allEntities + entities = map (\(f, s) -> (f, renderPNG (assetsFolder ++ "entities/" ++ s))) allEntities environment = map (\(f, s) -> (f, renderPNG (assetsFolder ++ "environment/" ++ s))) allEnvironment - gui = [] - items = map (\(f, s) -> (f, renderPNG (assetsFolder ++ "items/" ++ s))) allItems + gui = map (\(f, s) -> (f, renderPNG (assetsFolder ++ "gui/" ++ s))) allGui + items = map (\(f, s) -> (f, renderPNG (assetsFolder ++ "items/" ++ s))) allItems ------------------------------ Exported ------------------------------ diff --git a/lib/RPGEngine/Render/Playing.hs b/lib/RPGEngine/Render/Playing.hs index b996d80..6a2f589 100644 --- a/lib/RPGEngine/Render/Playing.hs +++ b/lib/RPGEngine/Render/Playing.hs @@ -5,8 +5,9 @@ module RPGEngine.Render.Playing 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 (..)) +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 ) @@ -23,9 +24,9 @@ renderPlaying _ = blank ------------------------------- Player ------------------------------- renderPlayer :: Renderer Player -renderPlayer Player{ position = (x, y) } = move picture - where move = setRenderPos x y - picture = getRender "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. @@ -83,8 +84,8 @@ renderEntities :: [Entity] -> Picture renderEntities list = pictures $ map renderEntity list renderEntity :: Entity -> Picture -renderEntity Entity{ entityId = id, entityX = x, entityY = y} = setRenderPos x y image - where image = getRender id +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 @@ -92,4 +93,19 @@ 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) \ No newline at end of file + 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 \ No newline at end of file diff --git a/verslag.pdf b/verslag.pdf index 24a6e81e610d8d2ab0dd84449bf2170ccd1ec824..c974837aa88e97c54e182754cc11fc07894d420f 100644 GIT binary patch delta 24646 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 r%e$FtJ;|q0 From becd305e01695226e66d920c4681a048272cb689 Mon Sep 17 00:00:00 2001 From: Tibo De Peuter Date: Thu, 22 Dec 2022 15:41:47 +0100 Subject: [PATCH 18/27] Closes #7 --- lib/RPGEngine/Config.hs | 10 +++++++--- lib/RPGEngine/Render/Menu.hs | 8 +++++--- 2 files changed, 12 insertions(+), 6 deletions(-) diff --git a/lib/RPGEngine/Config.hs b/lib/RPGEngine/Config.hs index b752af4..3d4f938 100644 --- a/lib/RPGEngine/Config.hs +++ b/lib/RPGEngine/Config.hs @@ -17,13 +17,17 @@ winOffsets = (0, 0) -- Game background color bgColor :: Color -bgColor = white +bgColor = makeColor (37 / 256) (19 / 256) (26 / 256) 1 + +-- Text color +textColor :: Color +textColor = white -- Default scale zoom :: Float -zoom = 5.0 +zoom = 5 --- UI scale +-- UI scale, number between 0 (small) and 1 (big) uizoom :: Float uizoom = 0.5 diff --git a/lib/RPGEngine/Render/Menu.hs b/lib/RPGEngine/Render/Menu.hs index e5f66d8..473c96c 100644 --- a/lib/RPGEngine/Render/Menu.hs +++ b/lib/RPGEngine/Render/Menu.hs @@ -4,12 +4,14 @@ module RPGEngine.Render.Menu import RPGEngine.Render.Core (Renderer) +import RPGEngine.Config ( uizoom, textColor ) import RPGEngine.Data (State) -import Graphics.Gloss (text) +import Graphics.Gloss (text, scale, color, white, translate) ------------------------------ Exported ------------------------------ --- TODO renderMenu :: Renderer State -renderMenu _ = text "[Press any key to start]" \ No newline at end of file +renderMenu _ = scaled $ center $ color textColor $ text "[Press any key to start]" + where scaled = scale uizoom uizoom + center = translate (-750) 0 \ No newline at end of file From 5cc96cbdba190607634f1b17584b2cfe3350341b Mon Sep 17 00:00:00 2001 From: Tibo De Peuter Date: Thu, 22 Dec 2022 16:25:29 +0100 Subject: [PATCH 19/27] #6 Win/End state #8 End screen #9 level selection --- assets/gui/main.png | Bin 0 -> 5460 bytes lib/RPGEngine/Config.hs | 4 ++++ lib/RPGEngine/Data/Game.hs | 5 +++-- lib/RPGEngine/Input/Lose.hs | 11 ++++++++--- lib/RPGEngine/Input/Playing.hs | 2 +- lib/RPGEngine/Render/Core.hs | 1 + lib/RPGEngine/Render/LevelSelection.hs | 7 ++++--- lib/RPGEngine/Render/Lose.hs | 14 ++++++++++---- lib/RPGEngine/Render/Menu.hs | 17 ++++++++++++----- lib/RPGEngine/Render/Win.hs | 13 ++++++++++--- 10 files changed, 53 insertions(+), 21 deletions(-) create mode 100644 assets/gui/main.png diff --git a/assets/gui/main.png b/assets/gui/main.png new file mode 100644 index 0000000000000000000000000000000000000000..1d2ae6e88aba0dccbbd3024533d794ede19940bf GIT binary patch literal 5460 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 Bool -isPlayerDead _ = False \ No newline at end of file +isPlayerDead g@Game{ state = Playing{ player = Player{ playerHp = (Just hp)}}} = hp <= 0 +isPlayerDead _ = False diff --git a/lib/RPGEngine/Input/Lose.hs b/lib/RPGEngine/Input/Lose.hs index 007a25f..a7ff57e 100644 --- a/lib/RPGEngine/Input/Lose.hs +++ b/lib/RPGEngine/Input/Lose.hs @@ -2,11 +2,16 @@ module RPGEngine.Input.Lose ( handleInputLose ) where -import RPGEngine.Input.Core (InputHandler) +import RPGEngine.Input.Core (InputHandler, handleAnyKey) -import RPGEngine.Data (Game) +import RPGEngine.Data (Game(..), State(..)) ------------------------------ Exported ------------------------------ handleInputLose :: InputHandler Game -handleInputLose = undefined \ No newline at end of file +handleInputLose = handleAnyKey retry + +---------------------------------------------------------------------- + +retry :: Game -> Game +retry g@Game{ state = Lose{ restart = restart }} = g{ state = restart } \ No newline at end of file diff --git a/lib/RPGEngine/Input/Playing.hs b/lib/RPGEngine/Input/Playing.hs index 6ca8f47..3068d24 100644 --- a/lib/RPGEngine/Input/Playing.hs +++ b/lib/RPGEngine/Input/Playing.hs @@ -50,7 +50,7 @@ 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 = restart } + loseGame = g{ state = Lose{ restart = restart }} checkPlaying g = g pauseGame :: Game -> Game diff --git a/lib/RPGEngine/Render/Core.hs b/lib/RPGEngine/Render/Core.hs index e901243..0c6754c 100644 --- a/lib/RPGEngine/Render/Core.hs +++ b/lib/RPGEngine/Render/Core.hs @@ -44,6 +44,7 @@ allItems = [ allGui :: [(String, FilePath)] allGui = [ + ("main", "main.png"), ("health", "health.png") ] diff --git a/lib/RPGEngine/Render/LevelSelection.hs b/lib/RPGEngine/Render/LevelSelection.hs index 85dc81c..082a8a1 100644 --- a/lib/RPGEngine/Render/LevelSelection.hs +++ b/lib/RPGEngine/Render/LevelSelection.hs @@ -4,7 +4,7 @@ module RPGEngine.Render.LevelSelection import RPGEngine.Render.Core (Renderer) -import RPGEngine.Config (resolution, zoom, uizoom) +import RPGEngine.Config (resolution, zoom, uizoom, textColor, selectionColor ) import RPGEngine.Data (State (..)) import Graphics.Gloss ( pictures, color, text, translate, blank ) @@ -25,7 +25,8 @@ renderLevelList LevelSelection{ levelList = list, selector = selector } = everyt where everything = pictures $ map render entries sel = selection selector entries = zip [0::Int .. ] list - render (i, path) | i == sel = color red $ scale uizoom uizoom $ translate 0 (offset i) $ text path - | otherwise = scale uizoom uizoom $ translate 0 (offset i) $ text path + 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 \ No newline at end of file diff --git a/lib/RPGEngine/Render/Lose.hs b/lib/RPGEngine/Render/Lose.hs index 1e6d730..cd1cfad 100644 --- a/lib/RPGEngine/Render/Lose.hs +++ b/lib/RPGEngine/Render/Lose.hs @@ -1,15 +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) +import Graphics.Gloss (text, scale, color, translate) + +------------------------------ Constants ----------------------------- + +message :: String +message = "You lose! Press any key to retry." ------------------------------ Exported ------------------------------ --- TODO renderLose :: Renderer State -renderLose _ = text "You lose" \ No newline at end of file +renderLose _ = scaled $ center $ color textColor $ text message + where scaled = scale uizoom uizoom + center = translate (-1200) 0 \ No newline at end of file diff --git a/lib/RPGEngine/Render/Menu.hs b/lib/RPGEngine/Render/Menu.hs index 473c96c..e9251e4 100644 --- a/lib/RPGEngine/Render/Menu.hs +++ b/lib/RPGEngine/Render/Menu.hs @@ -2,16 +2,23 @@ module RPGEngine.Render.Menu ( renderMenu ) where -import RPGEngine.Render.Core (Renderer) +import RPGEngine.Render.Core (Renderer, getRender) import RPGEngine.Config ( uizoom, textColor ) import RPGEngine.Data (State) -import Graphics.Gloss (text, scale, color, white, translate) +import Graphics.Gloss (text, scale, color, translate, pictures) + +------------------------------ Constants ----------------------------- + +message :: String +message = "[Press any key to start]" ------------------------------ Exported ------------------------------ renderMenu :: Renderer State -renderMenu _ = scaled $ center $ color textColor $ text "[Press any key to start]" - where scaled = scale uizoom uizoom - center = translate (-750) 0 \ No newline at end of file +renderMenu _ = pictures [main, pressAny] + where pressAny = scaled $ center $ color textColor $ text message + scaled = scale uizoom uizoom + center = translate (-800) (-320) + main = getRender "main" \ No newline at end of file diff --git a/lib/RPGEngine/Render/Win.hs b/lib/RPGEngine/Render/Win.hs index 189cef8..abaa095 100644 --- a/lib/RPGEngine/Render/Win.hs +++ b/lib/RPGEngine/Render/Win.hs @@ -4,12 +4,19 @@ module RPGEngine.Render.Win import RPGEngine.Render.Core (Renderer) +import RPGEngine.Config (uizoom, textColor) import RPGEngine.Data (State) -import Graphics.Gloss (text) +import Graphics.Gloss (text, scale, color, translate) + +------------------------------ Constants ----------------------------- + +message :: String +message = "You win! Press any key to return to the menu." ------------------------------ Exported ------------------------------ --- TODO renderWin :: Renderer State -renderWin _ = text "You win!\nPress any key to return to the menu." \ No newline at end of file +renderWin _ = scaled $ center $ color textColor $ text message + where scaled = scale uizoom uizoom + center = translate (-1500) 0 \ No newline at end of file From f3bce9912095270350536ed0f0dcf0b716a12daf Mon Sep 17 00:00:00 2001 From: Tibo De Peuter Date: Thu, 22 Dec 2022 22:05:25 +0100 Subject: [PATCH 20/27] #10 #18 Fix parsing --- README.md | 2 + levels/level1.txt | 2 +- levels/level2.txt | 10 +-- levels/level3.txt | 2 +- levels/level4.txt | 2 +- levels/level_more_levels.txt | 10 +-- lib/RPGEngine.hs | 55 +++++++++------ lib/RPGEngine/Data.hs | 3 +- lib/RPGEngine/Data/Default.hs | 27 ++++++- lib/RPGEngine/Input/Core.hs | 2 +- lib/RPGEngine/Input/Playing.hs | 11 ++- lib/RPGEngine/Parse.hs | 6 +- lib/RPGEngine/Parse/StructureToGame.hs | 25 +++---- lib/RPGEngine/Parse/TextToStructure.hs | 19 ++--- rpg-engine.cabal | 8 ++- test/Parser/GameSpec.hs | 91 +++++++++++++++++------- test/Parser/StructureSpec.hs | 98 ++++++++++++++++++++++++-- test/Spec.hs | 19 ++++- 18 files changed, 289 insertions(+), 103 deletions(-) diff --git a/README.md b/README.md index 832883e..455e7af 100644 --- a/README.md +++ b/README.md @@ -58,6 +58,8 @@ These are the keybinds *in* the game. All other keybinds in the menus should be | Move down | `Arrow Down` | `s` | | Move right | `Arrow Right` | `d` | | Show inventory | `i` | | +| Restart level | `r` | | +| Quit game | `Esc` | | ### Example playthrough diff --git a/levels/level1.txt b/levels/level1.txt index 42ba56a..02bc322 100644 --- a/levels/level1.txt +++ b/levels/level1.txt @@ -15,4 +15,4 @@ levels: [ entities: [] } -] +] \ No newline at end of file diff --git a/levels/level2.txt b/levels/level2.txt index ca3a220..641cc56 100644 --- a/levels/level2.txt +++ b/levels/level2.txt @@ -19,8 +19,8 @@ levels: [ items: [ { id: "key", - x: 0, - y: 1, + x: 1, + y: 2, name: "Sleutel", description: "Deze sleutel kan een deur openen", useTimes: 1, @@ -35,8 +35,8 @@ levels: [ entities: [ { id: "door", - x: 0, - y: 3, + x: 1, + y: 4, name: "Deur", description: "Deze deur kan geopend worden met een sleutel", direction: up, @@ -48,4 +48,4 @@ levels: [ } ] } -] +] \ No newline at end of file diff --git a/levels/level3.txt b/levels/level3.txt index f943fe7..349c63c 100644 --- a/levels/level3.txt +++ b/levels/level3.txt @@ -76,4 +76,4 @@ levels: [ } ] } -] +] \ No newline at end of file diff --git a/levels/level4.txt b/levels/level4.txt index f7ec761..ba5b8e5 100644 --- a/levels/level4.txt +++ b/levels/level4.txt @@ -131,4 +131,4 @@ levels: [ } ] } -] +] \ No newline at end of file diff --git a/levels/level_more_levels.txt b/levels/level_more_levels.txt index 879dc6b..3dd557e 100644 --- a/levels/level_more_levels.txt +++ b/levels/level_more_levels.txt @@ -1,9 +1,6 @@ -# Dit gehele bestand is een Block - -# Dit is een entry: key + value -player: { # Value is hier een block +player: { hp: 50, - inventory: [ # BlockList + inventory: [ { id: "dagger", x: 0, @@ -18,7 +15,6 @@ player: { # Value is hier een block ] } -# Dit is een entry levels: [ { layout: { @@ -135,4 +131,4 @@ levels: [ } ] } -] +] \ No newline at end of file diff --git a/lib/RPGEngine.hs b/lib/RPGEngine.hs index db19c09..e7cbf72 100644 --- a/lib/RPGEngine.hs +++ b/lib/RPGEngine.hs @@ -9,7 +9,7 @@ import RPGEngine.Config ( bgColor, winDimensions, winOffsets ) import RPGEngine.Render ( initWindow, render ) import RPGEngine.Input ( handleAllInput ) import RPGEngine.Input.Playing ( checkPlaying, spawnPlayer ) -import RPGEngine.Data (Game (..), State (..), Layout, Level (..), Physical (..)) +import RPGEngine.Data (Game (..), State (..), Layout, Level (..), Physical (..), Entity(..), Direction(..), Player(..)) import RPGEngine.Data.Default (defaultLevel, defaultPlayer) import Graphics.Gloss ( play ) @@ -27,22 +27,15 @@ playRPGEngine title fps = do -- TODO revert this -- Initialize the game initGame :: Game --- initGame = Game { --- state = Menu{ base = StateBase{ --- renderer = renderMenu, --- inputHandler = handleInputMenu --- }} --- } -initGame = Game{ - state = initState -} - where initState = Playing{ - levels = [defaultLevel, otherLevel], - count = 0, - level = defaultLevel, - player = spawnPlayer defaultLevel defaultPlayer, - restart = initState - } +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 @@ -50,6 +43,8 @@ otherLayout = [ [Blocked, Blocked, Blocked], [Blocked, Entrance, Blocked], [Blocked, Walkable, Blocked], + [Blocked, Walkable, Blocked], + [Blocked, Walkable, Blocked], [Blocked, Exit, Blocked], [Blocked, Blocked, Blocked] ] @@ -69,12 +64,30 @@ otherLevel = Level { (1, 2, Walkable), (2, 2, Blocked), (0, 3, Blocked), - (1, 3, Exit), + (1, 3, Walkable), (2, 3, Blocked), (0, 4, Blocked), - (1, 4, Blocked), - (2, 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 = [] + entities = [ + Entity{ + entityId = "door", + entityX = 1, + entityY = 3, + entityName = "Epic door", + entityDescription = "epic description", + entityActions = [], + entityValue = Nothing, + entityHp = Nothing, + direction = North + } + ] } \ No newline at end of file diff --git a/lib/RPGEngine/Data.hs b/lib/RPGEngine/Data.hs index 01efa75..534c36b 100644 --- a/lib/RPGEngine/Data.hs +++ b/lib/RPGEngine/Data.hs @@ -12,7 +12,7 @@ import RPGEngine.Render.Core ( Renderer ) -- A game is the base data container. data Game = Game { state :: State -} +} deriving (Eq, Show) ------------------------------- State -------------------------------- @@ -33,6 +33,7 @@ data State = Menu | Win -- Lost a level | Lose { restart :: State } + deriving (Eq, Show) ------------------------------- Level -------------------------------- diff --git a/lib/RPGEngine/Data/Default.hs b/lib/RPGEngine/Data/Default.hs index f877f7f..c2e2814 100644 --- a/lib/RPGEngine/Data/Default.hs +++ b/lib/RPGEngine/Data/Default.hs @@ -64,9 +64,30 @@ defaultLevel = Level { defaultPlayer :: Player defaultPlayer = Player { - playerHp = Prelude.Nothing, -- Compares to infinity - inventory = [], - position = (0, 0) + -- 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 diff --git a/lib/RPGEngine/Input/Core.hs b/lib/RPGEngine/Input/Core.hs index 07ea182..467e149 100644 --- a/lib/RPGEngine/Input/Core.hs +++ b/lib/RPGEngine/Input/Core.hs @@ -20,7 +20,7 @@ type InputHandler a = Event -> (a -> a) data ListSelector = ListSelector { selection :: Int, selected :: Bool -} +} deriving (Eq, Show) ------------------------------ Exported ------------------------------ diff --git a/lib/RPGEngine/Input/Playing.hs b/lib/RPGEngine/Input/Playing.hs index 3068d24..9703560 100644 --- a/lib/RPGEngine/Input/Playing.hs +++ b/lib/RPGEngine/Input/Playing.hs @@ -2,6 +2,7 @@ module RPGEngine.Input.Playing ( handleInputPlaying , checkPlaying , spawnPlayer +, putCoords ) where import RPGEngine.Input.Core (InputHandler, handle, handleKey, composeInputHandlers) @@ -32,6 +33,8 @@ handleInputPlaying = composeInputHandlers [ handleKey (Char 's') Down $ movePlayer South, handleKey (Char 'a') Down $ movePlayer West, + handleKey (Char 'r') Down restartGame, + handleKey (Char 'i') Down $ toggleInventoryShown True, handleKey (Char 'i') Up $ toggleInventoryShown False ] @@ -58,13 +61,15 @@ 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 } + -- 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 -- Either the next level or winState - nextState | (count + 1) < length levels = nextLevelState + where nextState | (count + 1) < length levels = nextLevelState | otherwise = Win - nextLevelState = s{ level = nextLevel, count = count + 1, player = movedPlayer } + nextLevelState = s{ level = nextLevel, count = count + 1, player = movedPlayer, restart = nextLevelState } nextLevel = levels !! (count + 1) movedPlayer = spawnPlayer nextLevel player goToNextLevel s = s diff --git a/lib/RPGEngine/Parse.hs b/lib/RPGEngine/Parse.hs index c12e8da..c63afd3 100644 --- a/lib/RPGEngine/Parse.hs +++ b/lib/RPGEngine/Parse.hs @@ -6,13 +6,11 @@ import RPGEngine.Data ( Game ) import RPGEngine.Parse.StructureToGame ( structureToGame ) import GHC.IO (unsafePerformIO) import Text.Parsec.String (parseFromFile) -import RPGEngine.Parse.TextToStructure (structure) +import RPGEngine.Parse.TextToStructure ( gameFile ) ------------------------------ Exported ------------------------------ parse :: FilePath -> Game parse filename = structureToGame struct where (Right struct) = unsafePerformIO io - io = parseFromFile structure filename - -tempParse = parseFromFile \ No newline at end of file + io = parseFromFile gameFile filename \ No newline at end of file diff --git a/lib/RPGEngine/Parse/StructureToGame.hs b/lib/RPGEngine/Parse/StructureToGame.hs index 7e81274..09e9e83 100644 --- a/lib/RPGEngine/Parse/StructureToGame.hs +++ b/lib/RPGEngine/Parse/StructureToGame.hs @@ -10,26 +10,25 @@ import RPGEngine.Data entityActions, entityValue, entityHp, direction), Item(itemId, itemX, itemY, itemName, itemDescription, itemValue, itemActions, useTimes), - Level(layout, items, entities), + 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 -structureToGame (Entry (Tag "player") playerBlock) = game - where game = Game{ state = newState } - newState = Playing{ levels = newLevels, level = currentLevel, player = newPlayer, restart = newState } - -- newLevels = structureToLevels levelsBlock - -- currentLevel = head newLevels - newLevels = [defaultLevel] - currentLevel = defaultLevel - newPlayer = structureToPlayer playerBlock +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 ------------------------------- @@ -60,7 +59,9 @@ structureToLevels (Block struct) = structureToLevel <$> struct structureToLevels _ = [defaultLevel] structureToLevel :: Structure -> Level -structureToLevel (Block entries) = structureToLevel' entries defaultLevel +structureToLevel (Block entries) = indexIsSet + where indexIsSet = level{ index = putCoords level } + level = structureToLevel' entries defaultLevel structureToLevel _ = defaultLevel structureToLevel' :: [Structure] -> Level -> Level diff --git a/lib/RPGEngine/Parse/TextToStructure.hs b/lib/RPGEngine/Parse/TextToStructure.hs index dc76003..fa2486e 100644 --- a/lib/RPGEngine/Parse/TextToStructure.hs +++ b/lib/RPGEngine/Parse/TextToStructure.hs @@ -18,9 +18,13 @@ import Text.Parsec notFollowedBy, sepBy, many, - try ) + 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 -------------------------- @@ -111,7 +115,7 @@ data Value = String String ---------------------------------------------------------------------- value :: Parser Value -value = choice [layout, string, integer, infinite, action, direction] +value = choice [layout, string, integer, infinite, direction, action] string :: Parser Value string = try $ String <$> between (char '\"') (char '\"') reading @@ -149,7 +153,7 @@ direction = try $ do ignoreWS $ P.string "left", ignoreWS $ P.string "right" ] - notFollowedBy alphaNum + -- lookAhead $ char ',' return $ Direction $ make value where make "up" = North make "right" = East @@ -160,15 +164,12 @@ direction = try $ do layout :: Parser Value layout = try $ do open <- ignoreWS $ oneOf openingBrackets - ignoreWS $ char '|' - list <- ignoreWS $ ignoreWS strip `sepBy` ignoreWS (char '|') let closing = getMatchingClosingBracket open - ignoreWS $ char closing - return $ Layout list + value <- many1 strip <* ignoreWS (char closing) + return $ Layout value strip :: Parser Strip -strip = try $ do - physical `sepBy` char ' ' +strip = try $ do ignoreWS (char '|') *> ignoreWS (physical `sepBy` char ' ') physical :: Parser Physical physical = try $ do diff --git a/rpg-engine.cabal b/rpg-engine.cabal index 76a2eee..3323cdb 100644 --- a/rpg-engine.cabal +++ b/rpg-engine.cabal @@ -55,7 +55,11 @@ test-suite rpg-engine-test main-is: Spec.hs hs-source-dirs: test default-language: Haskell2010 - build-depends: base >=4.7 && <5, hspec <= 2.10.6, hspec-discover, 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 + Parser.StructureSpec \ No newline at end of file diff --git a/test/Parser/GameSpec.hs b/test/Parser/GameSpec.hs index 1f167a3..b2b7371 100644 --- a/test/Parser/GameSpec.hs +++ b/test/Parser/GameSpec.hs @@ -6,24 +6,50 @@ import RPGEngine.Data import RPGEngine.Parse.Core import RPGEngine.Parse.TextToStructure import RPGEngine.Parse.StructureToGame +import RPGEngine.Parse.TextToStructure (gameFile) spec :: Spec spec = do describe "Game" $ do - it "TODO: Simple game" $ do - pending - it "TODO: More complex game" $ do - pending - it "TODO: Game with multiple levels" $ do - pending + -- TODO There is a weird bug that caused this to go in an infinite loop. Fix later. + xit "Simple game" $ 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 = 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 "fix parsing first" + it "Game with multiple levels" $ do + pendingWith "fix parsing first" describe "Player" $ do it "cannot die" $ do let input = "player: { hp: infinite, inventory: [] }" correct = Player { - playerHp = Prelude.Nothing, - inventory = [], - position = (0, 0) + playerHp = Prelude.Nothing, + inventory = [], + position = (0, 0), + showHp = True, + showInventory = False } Right (Entry (Tag "player") struct) = parseWith structure input structureToPlayer struct `shouldBe` correct @@ -31,9 +57,11 @@ spec = do it "without inventory" $ do let input = "player: { hp: 50, inventory: [] }" correct = Player { - playerHp = Just 50, - inventory = [], - position = (0, 0) + playerHp = Just 50, + inventory = [], + position = (0, 0), + showHp = True, + showInventory = False } Right (Entry (Tag "player") struct) = parseWith structure input structureToPlayer struct `shouldBe` correct @@ -54,14 +82,12 @@ spec = do useTimes = Prelude.Nothing } ], - position = (0, 0) + position = (0, 0), + showHp = True, + showInventory = False } Right (Entry (Tag "player") struct) = parseWith structure input structureToPlayer struct `shouldBe` correct - - describe "Layout" $ do - it "simple" $ do - pending describe "Items" $ do it "simple" $ do @@ -117,23 +143,40 @@ spec = do structureToActions struct `shouldBe` correct describe "Entities" $ do - it "TODO: Simple entity" $ do - pending + it "Simple entity" $ do + pendingWith "fix parsing first" describe "Level" $ do it "Simple layout" $ do - let input = "{ layout: { | * * * * * * \n| * s . . e *\n| * * * * * * }, items: [], entities: [] }" + 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 - - it "TODO: Complex layout" $ do - pending \ No newline at end of file + structureToLevel struct `shouldBe` correct \ No newline at end of file diff --git a/test/Parser/StructureSpec.hs b/test/Parser/StructureSpec.hs index e9296b8..b084a02 100644 --- a/test/Parser/StructureSpec.hs +++ b/test/Parser/StructureSpec.hs @@ -5,6 +5,8 @@ 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 @@ -68,7 +70,7 @@ spec = do ]], "") 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() } } ]" + 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, @@ -83,6 +85,17 @@ spec = do ]], "") 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" @@ -252,22 +265,21 @@ spec = do parseWith RPGEngine.Parse.TextToStructure.direction input `shouldBe` correct it "can parse layouts" $ do - let input = "| * * * * * * * *\n| * s . . . . e *\n| * * * * * * * *" + 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 RPGEngine.Parse.TextToStructure.layout input `shouldBe` correct + parseWith value input `shouldBe` correct - let input = "{ |* * * * * * * *|* s . . . . e *|* * * * * * * * }" - -- correct = Right $ Entry (Tag "layout") $ Regular $ Layout [ - correct = Right $ Layout [ + 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 RPGEngine.Parse.TextToStructure.value input `shouldBe` correct + parseWith structure input `shouldBe` correct describe "Brackets" $ do it "matches closing <" $ do @@ -289,3 +301,75 @@ spec = 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 \ No newline at end of file diff --git a/test/Spec.hs b/test/Spec.hs index 52ef578..bf4362e 100644 --- a/test/Spec.hs +++ b/test/Spec.hs @@ -1 +1,18 @@ -{-# OPTIONS_GHC -F -pgmF hspec-discover #-} \ No newline at end of file +{-# 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 ... \ No newline at end of file From ef784c2dbc9babf8a113c2d90734c4eefaf9f50a Mon Sep 17 00:00:00 2001 From: Tibo De Peuter Date: Thu, 22 Dec 2022 23:06:59 +0100 Subject: [PATCH 21/27] Fix rendering issue --- assets/entities/devil.png | Bin 0 -> 1548 bytes assets/items/potion.png | Bin 0 -> 1548 bytes assets/items/sword.png | Bin 0 -> 247 bytes levels/level3.txt | 12 ++++++------ levels/level4.txt | 20 ++++++++++---------- lib/RPGEngine/Input/Playing.hs | 2 +- lib/RPGEngine/Render/Core.hs | 7 +++++-- lib/RPGEngine/Render/Playing.hs | 2 +- test/Parser/GameSpec.hs | 10 +++++----- 9 files changed, 28 insertions(+), 25 deletions(-) create mode 100644 assets/entities/devil.png create mode 100644 assets/items/potion.png create mode 100644 assets/items/sword.png diff --git a/assets/entities/devil.png b/assets/entities/devil.png new file mode 100644 index 0000000000000000000000000000000000000000..1ab3ef5e835c0ab5e02a4d93a4f4271c14cef8c3 GIT binary patch literal 1548 zcmeAS@N?(olHy`uVBq!ia0vp^0wB!61|;P_|4(FKU~I{Bb`J1#c2+1T%1_J8No8Qr zm{>c}+T(D5NZbEqUF*A=U0r01LezJ!=ty4cB&gLAwJ?-@^6Y&JJe?;!dZe*KOr}OB zOg3gtX=#Bd%R%;z#giv5>Yh@O7nC9Rxb^VgAMgM2KmR8#@G5m#q^zOCm41p% zm}P$9wNs(j=1ksiFy(Uiw4HvYT`Xc7Svrk^0$e#$Z**K*H#tes^?9NE|BlzwPb{6V zn#HY;HDrNf+205CAFQLb?M+{navg0vCw}~=1oN)4Mz^zt`}$><7c#NTZ`9uVbp4ga zoBo%wb#;qacygG?NMQuI$ga#rQ?o4*iNVU|?X8_H=O!u@GbwV0r%=sDcJSRaojj*~Wr2C#}BnA4J12 zKEqgWNv=4wjbZ9FsC0%K*hRPBK4U~z4;KL022%iH+s@dQTt zOdtkK1I)?DnnCg~gFtL_0QUk|5ztl`h_Lg5G2t}GU10c@Hc}+T(D5NZbEqUF*A=U0r01LezJ!=ty4cB&gLAwJ?-@^6Y&JJe?;!dZe*KOr}OB zOg3gtX=#Bd%R%;z#giv5>Yh@O7nC9Rxb^VgAMgM2KmR8#@G5m#q^zOCm41p% zm}P$9wNs(j=1ksiFy(Uiw4HvYT`Xc7Svrk^0$e#$Z**K*H#tes^?9NE|BlzwPb{6V zn#HY;HDrNf+205CAFQLb?M+{navg0vCw}~=1oN)4Mz^zt`}$><7c#NTZ`9uVbp4ga zoBo%wb#;qacygG?NMQuI$ga#rOrRI)6Uj%)r1R?djqeVj;*V!1De#P{ptYs=`wLK^UJWS#U{$G|q}O z21}m?K4JfYOxA}G%vk;8?65E?OR~FBFLKon;}ftOa+NW>#8y!YsRGxrvV@r z{1OlcW0-5O8G>N|%=vd;e}t=_ar8a|$PjD+iV_Csns?uN2R8)7$goxfdkr}(FkArB zfvy=O4hs{Q8W4@u0A*2rWHn&6w;1+hkKzT8N|-mW*@i7DF${ni0=5>7K=wPD)BtiT WC-|!PZafnXa)PI;pUXO@geCxxa~uZ% literal 0 HcmV?d00001 diff --git a/assets/items/sword.png b/assets/items/sword.png new file mode 100644 index 0000000000000000000000000000000000000000..ba6438963489b3d5e2cb53c187f28576e595b3c5 GIT binary patch literal 247 zcmeAS@N?(olHy`uVBq!ia0vp^0wB!61|;P_|4#%`jKx9jP7LeL$-D$|SkfJR9T^xl z_H+M9WCij$3p^r=85sBugD~Uq{1quc!5&W+#}Etus}oQ2HYf(IiZ}nK}B_nFTJXTiq6$e zny_`7HbXCq;jx0s5QZ+67J(f+g6AH2$m=b5vvm3bW^hh?Ow?e7D|vdw7Zz&|M6ku6{1-oD!M [(X, Y, Physical)] putCoords l@Level{ layout = lay } = concatMap (\(a, bs) -> map (\(b, c) -> (b, a, c)) bs) numberedList - where numberedStrips = zip [0::Int .. ] lay + where numberedStrips = reverse $ zip [0::Int .. ] $ reverse lay numberedList = map (\(x, strip) -> (x, zip [0::Int ..] strip)) numberedStrips \ No newline at end of file diff --git a/lib/RPGEngine/Render/Core.hs b/lib/RPGEngine/Render/Core.hs index 0c6754c..b04c9fe 100644 --- a/lib/RPGEngine/Render/Core.hs +++ b/lib/RPGEngine/Render/Core.hs @@ -23,7 +23,8 @@ unknownImage = "unknown.png" allEntities :: [(String, FilePath)] allEntities = [ ("player", "player.png"), - ("door", "door.png") + ("devil", "devil.png" ), + ("door", "door.png") ] allEnvironment :: [(String, FilePath)] @@ -39,7 +40,9 @@ allEnvironment = [ allItems :: [(String, FilePath)] allItems = [ ("dagger", "dagger.png"), - ("key", "key.png" ) + ("key", "key.png" ), + ("potion", "potion.png"), + ("sword", "sword.png" ) ] allGui :: [(String, FilePath)] diff --git a/lib/RPGEngine/Render/Playing.hs b/lib/RPGEngine/Render/Playing.hs index 6a2f589..f902178 100644 --- a/lib/RPGEngine/Render/Playing.hs +++ b/lib/RPGEngine/Render/Playing.hs @@ -48,7 +48,7 @@ renderLevel Level{ layout = l, items = i, entities = e } = level entities = renderEntities e renderLayout :: Layout -> Picture -renderLayout strips = pictures [setRenderPos 0 y (renderStrip (strips !! y)) | y <- [0 .. count]] +renderLayout strips = pictures [setRenderPos 0 (count - y) (renderStrip (strips !! y)) | y <- [0 .. count]] where count = length strips - 1 renderStrip :: [Physical] -> Picture diff --git a/test/Parser/GameSpec.hs b/test/Parser/GameSpec.hs index b2b7371..c441a22 100644 --- a/test/Parser/GameSpec.hs +++ b/test/Parser/GameSpec.hs @@ -11,8 +11,8 @@ import RPGEngine.Parse.TextToStructure (gameFile) spec :: Spec spec = do describe "Game" $ do - -- TODO There is a weird bug that caused this to go in an infinite loop. Fix later. - xit "Simple 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 { @@ -37,9 +37,9 @@ spec = do (Right struct) = parseWith gameFile input structureToGame struct `shouldBe` correct it "More complex game" $ do - pendingWith "fix parsing first" + pendingWith "Still need to write this" it "Game with multiple levels" $ do - pendingWith "fix parsing first" + pendingWith "Still need to write this" describe "Player" $ do it "cannot die" $ do @@ -144,7 +144,7 @@ spec = do describe "Entities" $ do it "Simple entity" $ do - pendingWith "fix parsing first" + pendingWith "still need to write this" describe "Level" $ do it "Simple layout" $ do From 9addf1ed07f0d0fdeec44b8c6637b64b06bd07be Mon Sep 17 00:00:00 2001 From: Tibo De Peuter Date: Fri, 23 Dec 2022 09:42:34 +0100 Subject: [PATCH 22/27] #4 Setup interaction --- README.md | 3 +- lib/RPGEngine/Data.hs | 26 +++++--- lib/RPGEngine/Data/Level.hs | 41 ++++++++++++- lib/RPGEngine/Input.hs | 5 +- lib/RPGEngine/Input/ActionSelection.hs | 76 ++++++++++++++++++++++++ lib/RPGEngine/Input/LevelSelection.hs | 9 +-- lib/RPGEngine/Input/Playing.hs | 55 +++++++++++++---- lib/RPGEngine/Render.hs | 8 ++- lib/RPGEngine/Render/ActionSelection.hs | 26 ++++++++ lib/RPGEngine/Render/LevelSelection.hs | 2 +- lib/RPGEngine/Render/Playing.hs | 3 +- rpg-engine.cabal | 2 + verslag.pdf | Bin 50036 -> 50213 bytes 13 files changed, 223 insertions(+), 33 deletions(-) create mode 100644 lib/RPGEngine/Input/ActionSelection.hs create mode 100644 lib/RPGEngine/Render/ActionSelection.hs diff --git a/README.md b/README.md index 455e7af..51913b2 100644 --- a/README.md +++ b/README.md @@ -57,7 +57,8 @@ These are the keybinds *in* the game. All other keybinds in the menus should be | Move left | `Arrow Left` | `a` | | Move down | `Arrow Down` | `s` | | Move right | `Arrow Right` | `d` | -| Show inventory | `i` | | +| Interaction | `Space` | `f` | +| Show inventory | `i` | `Tab` | | Restart level | `r` | | | Quit game | `Esc` | | diff --git a/lib/RPGEngine/Data.hs b/lib/RPGEngine/Data.hs index 534c36b..46c65d2 100644 --- a/lib/RPGEngine/Data.hs +++ b/lib/RPGEngine/Data.hs @@ -19,22 +19,30 @@ data Game = Game { -- Main menu data State = Menu -- Select the level you want to play - | LevelSelection { levelList :: [FilePath], - selector :: ListSelector } + | LevelSelection { levelList :: [FilePath], + selector :: ListSelector } -- Playing a level - | Playing { levels :: [Level], - count :: Int, - level :: Level, - player :: Player, - restart :: State } + | 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 } + | Paused { continue :: State } -- Won a level | Win -- Lost a level - | Lose { restart :: State } + | Lose { restart :: State } + | Error Message deriving (Eq, Show) +type Message = String + ------------------------------- Level -------------------------------- data Level = Level { diff --git a/lib/RPGEngine/Data/Level.hs b/lib/RPGEngine/Data/Level.hs index f41a0c1..f415844 100644 --- a/lib/RPGEngine/Data/Level.hs +++ b/lib/RPGEngine/Data/Level.hs @@ -5,8 +5,9 @@ where import GHC.IO (unsafePerformIO) import System.Directory (getDirectoryContents) import RPGEngine.Input.Core (ListSelector(..)) -import RPGEngine.Data (Level (..), Physical (..), Direction (..), Entity (..), Game (..), Item (..), Player (..), State (..), X, Y, Layout) +import RPGEngine.Data (Action(..), Level (..), Physical (..), Direction (..), Entity (..), Game (..), Item (..), Player (..), State (..), X, Y, Layout, Condition (InventoryFull, InventoryContains, Not, AlwaysFalse)) import RPGEngine.Config (levelFolder) +import Data.Foldable (find) ------------------------------ Exported ------------------------------ @@ -25,6 +26,17 @@ findAt pos lvl@Level{ index = index } = try 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) + directionOffsets :: Direction -> (X, Y) directionOffsets North = ( 0, 1) directionOffsets East = ( 1, 0) @@ -33,4 +45,29 @@ directionOffsets West = (-1, 0) directionOffsets Stay = ( 0, 0) getLevelList :: [FilePath] -getLevelList = drop 2 $ unsafePerformIO $ getDirectoryContents levelFolder \ No newline at end of file +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 _ = "ERROR" + +-- TODO Check conditions +-- Filter based on the conditions, keep only the actions of which the +-- conditions are met. +filterActions :: [([Condition], Action)] -> [Action] +filterActions [] = [] +filterActions ((conditions, action):others) = action:filterActions others + +-- Check if a condition is met or not. +meetsCondition :: Condition -> Bool +meetsCondition InventoryFull = False -- TODO +meetsCondition (InventoryContains id) = True -- TODO +meetsCondition (Not condition) = not $ meetsCondition condition +meetsCondition AlwaysFalse = False \ No newline at end of file diff --git a/lib/RPGEngine/Input.hs b/lib/RPGEngine/Input.hs index 1314c9e..8fc594c 100644 --- a/lib/RPGEngine/Input.hs +++ b/lib/RPGEngine/Input.hs @@ -13,6 +13,7 @@ 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 ------------------------------ @@ -23,4 +24,6 @@ handleAllInput ev g@Game{ state = LevelSelection{} } = handleInputLevelSelection 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 \ No newline at end of file +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 \ No newline at end of file diff --git a/lib/RPGEngine/Input/ActionSelection.hs b/lib/RPGEngine/Input/ActionSelection.hs new file mode 100644 index 0000000..99e6cfe --- /dev/null +++ b/lib/RPGEngine/Input/ActionSelection.hs @@ -0,0 +1,76 @@ +module RPGEngine.Input.ActionSelection +( handleInputActionSelection +) where + +import RPGEngine.Input.Core (InputHandler, handleKey, composeInputHandlers, ListSelector (selection)) + +import RPGEngine.Data (Game (..), State (..), Direction (..), Action (..), ItemId, EntityId) +import Graphics.Gloss.Interface.IO.Game (Key(SpecialKey), SpecialKey (KeyUp, KeyDown)) +import Graphics.Gloss.Interface.IO.Interact + ( SpecialKey(..), KeyState(..) ) + +------------------------------ 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 selection continue } = newGame + where newGame = game{ state = execute selectedAction continue } + selectedAction = Leave +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 = increasePlayerHp iid s +execute _ s = s + +-- Pick up the item with itemId and put it in the players inventory +pickUpItem :: ItemId -> State -> State +pickUpItem _ s = s -- TODO + +-- Use an item on the player +useItem :: ItemId -> State -> State -- TODO +useItem _ s = s -- TODO + +-- Attack an entity using an item +decreaseHp :: EntityId -> ItemId -> State -> State +decreaseHp _ _ s = s +-- TODO DecreaseHp of monster +-- TODO Check if monster is dead +-- TODO Entity attack player +-- TODO Decrease durability of item +-- TODO Break item if durability below zero + +-- Heal a bit +increasePlayerHp :: ItemId -> State -> State +increasePlayerHp _ s = s +-- TODO Increase playerHp +-- TODO Decrease durability of item +-- TODO Remove item if durability below zero \ No newline at end of file diff --git a/lib/RPGEngine/Input/LevelSelection.hs b/lib/RPGEngine/Input/LevelSelection.hs index 5266782..84420c0 100644 --- a/lib/RPGEngine/Input/LevelSelection.hs +++ b/lib/RPGEngine/Input/LevelSelection.hs @@ -24,14 +24,15 @@ handleInputLevelSelection = composeInputHandlers [ -- Select a level and load it in selectLevel :: Game -> Game -selectLevel game@Game{ state = LevelSelection{ levelList = list, selector = selector }} = newGame +selectLevel game@Game{ state = LevelSelection list selector } = newGame where newGame = parse $ levelFolder ++ (list !! index) index = selection selector -selectLevel g = g +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{ levelList = list, selector = selector } } = newGame +moveSelector dir game@Game{ state = state@(LevelSelection list selector) } = newGame where newGame = game{ state = newState } newState = state{ selector = newSelector } newSelector | constraint = selector{ selection = newSelection } @@ -41,4 +42,4 @@ moveSelector dir game@Game{ state = state@LevelSelection{ levelList = list, sele diff | dir == North = -1 | dir == South = 1 | otherwise = 0 -moveSelector _ g = g \ No newline at end of file +moveSelector _ g = g{ state = Error "Something went wrong while moving the selector up or down"} \ No newline at end of file diff --git a/lib/RPGEngine/Input/Playing.hs b/lib/RPGEngine/Input/Playing.hs index cff2b86..f2c3f1a 100644 --- a/lib/RPGEngine/Input/Playing.hs +++ b/lib/RPGEngine/Input/Playing.hs @@ -5,15 +5,16 @@ module RPGEngine.Input.Playing , putCoords ) where -import RPGEngine.Input.Core (InputHandler, handle, handleKey, composeInputHandlers) +import RPGEngine.Input.Core (InputHandler, handle, handleKey, composeInputHandlers, ListSelector (..)) -import RPGEngine.Data (Game (..), Layout(..), Level(..), Physical(..), Player(..), State(..), X, Y, Direction (..)) +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) +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 ------------------------------ @@ -28,15 +29,21 @@ handleInputPlaying = composeInputHandlers [ 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, + handleKey (Char 'w') Down $ movePlayer North, + handleKey (Char 'd') Down $ movePlayer East, + handleKey (Char 's') Down $ movePlayer South, + handleKey (Char 'a') Down $ movePlayer West, - handleKey (Char 'r') Down restartGame, + -- 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 (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 ] ---------------------------------------------------------------------- @@ -63,6 +70,7 @@ 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 @@ -83,16 +91,39 @@ movePlayer dir g@Game{ state = s@Playing{ player = p@Player{ position = (x, y) } newCoord | isLegalMove dir g = (x + xD, y + yD) | otherwise = (x, y) (xD, yD) = directionOffsets dir -movePlayer _ g = g +movePlayer _ g = g{ state = Error "something went wrong while moving the player" } + +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 $ 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 \ No newline at end of file + 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 \ No newline at end of file diff --git a/lib/RPGEngine/Render.hs b/lib/RPGEngine/Render.hs index a9c2e74..2c2e158 100644 --- a/lib/RPGEngine/Render.hs +++ b/lib/RPGEngine/Render.hs @@ -15,9 +15,11 @@ import RPGEngine.Render.Paused ( renderPaused ) import RPGEngine.Render.Win ( renderWin ) import RPGEngine.Render.Lose ( renderLose ) -import Graphics.Gloss (Display) +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) ---------------------------------------------------------------------- @@ -32,4 +34,6 @@ 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 \ No newline at end of file +render Game{ state = s@Lose{} } = renderLose s +render Game{ state = s@ActionSelection{}} = renderActionSelection s +render Game{ state = Error message } = color textColor $ text message \ No newline at end of file diff --git a/lib/RPGEngine/Render/ActionSelection.hs b/lib/RPGEngine/Render/ActionSelection.hs new file mode 100644 index 0000000..164719e --- /dev/null +++ b/lib/RPGEngine/Render/ActionSelection.hs @@ -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 \ No newline at end of file diff --git a/lib/RPGEngine/Render/LevelSelection.hs b/lib/RPGEngine/Render/LevelSelection.hs index 082a8a1..e952b17 100644 --- a/lib/RPGEngine/Render/LevelSelection.hs +++ b/lib/RPGEngine/Render/LevelSelection.hs @@ -21,7 +21,7 @@ renderLevelSelection state = result ---------------------------------------------------------------------- renderLevelList :: Renderer State -renderLevelList LevelSelection{ levelList = list, selector = selector } = everything +renderLevelList (LevelSelection list selector) = everything where everything = pictures $ map render entries sel = selection selector entries = zip [0::Int .. ] list diff --git a/lib/RPGEngine/Render/Playing.hs b/lib/RPGEngine/Render/Playing.hs index f902178..98252c2 100644 --- a/lib/RPGEngine/Render/Playing.hs +++ b/lib/RPGEngine/Render/Playing.hs @@ -42,7 +42,8 @@ focusPlayer _ = id renderLevel :: Renderer Level renderLevel Level{ layout = l, items = i, entities = e } = level where level = pictures [void, layout, items, entities] - void = createVoid + -- void = createVoid + void = blank layout = renderLayout l items = renderItems i entities = renderEntities e diff --git a/rpg-engine.cabal b/rpg-engine.cabal index 3323cdb..bfd1afa 100644 --- a/rpg-engine.cabal +++ b/rpg-engine.cabal @@ -23,6 +23,7 @@ library RPGEngine.Input RPGEngine.Input.Core + RPGEngine.Input.ActionSelection RPGEngine.Input.Menu RPGEngine.Input.LevelSelection RPGEngine.Input.Playing @@ -37,6 +38,7 @@ library RPGEngine.Render RPGEngine.Render.Core + RPGEngine.Render.ActionSelection RPGEngine.Render.Menu RPGEngine.Render.LevelSelection RPGEngine.Render.Playing diff --git a/verslag.pdf b/verslag.pdf index c974837aa88e97c54e182754cc11fc07894d420f..d607e19ef4b64e0453b857b332f480b7f4627c32 100644 GIT binary patch delta 14200 zcmajFQ*b5>&@{Rm+sVeZZ6{A`+qRuNv2EM7xv_2A&c^uP@B7ciIalYRyJo8TZl-Fg zyXGkrv?Ls~zVkN-7iW@#IvVg=TiPC%1FrYBE?vMt74ZTb6ZFc%9QJ5srH!zxdAx+5 zHSPOL38f_Aq=CmRlamQ!#Aqi`!tY%olKGb3z5C-IBQL%mm-jOC4DV>)&HbX#9_miS z%*2eB!NS3I+07TxMF< z*4`_8?$7p@{2PSUr(itE-Eos1ci7#jNY}QJ}$AqkleO{dGSu> zUOHYbcoy0zIm29)X z$j_(8iw|4Eq88UX6l;6b63nWMlu8AuUjq^4N8MsnFka=?e$*0NBaqa;2GX+5;!%*v zM$rO7(ZDjQN55wrJ&RHZAb8>N4eF;i&+?}#gQ$6n_&`Ci z#wMbRnA-_TJS^k;ya+F^kM(!swb-M&p@mh}$kT$qjTmg6iY`;o#cuZI~7|nFVlYj*IihEK4Yrm3p&tst*T{tE+TQPe)g)^DTeGyc+{g} zE>k^$zLlc%ua>F~R&9!xu>eyujr#~W1mURc_{opLrly^i#sn^(Y^ZvTV>4K|lH0*3 zJ^Owzd33)kn54GfT$CsCP&+YO3!rG7wAgU#~gr|HDDcSY&{f2>7%+y9j zIvtnk|~$pcEtT zyJ9IuRw1eysEC93HentRNvsr-xx+&-@RwB4r$AzSl^hmaT2;On1$!S!{Isd!UvoK0 zBH7vEr_#(stcnYtxVAW$3bVX=H%IfQ%`LF1piC4=zmWwk2?Ztkvl|W;tfa8(^PJS( z^We-^GDe;mb!p1C;!1Tu^-pURNPT8(epH~d zYq{KII}B{2maktN!X?G&0EMzvC!c^`bQ>%D!)*LUzWz2m0flZ`A0?ZZUY#bJs44O& zo7j~M8c4-;e1C`*Z0ck!VKeq<)sskHV-y|^Xi7x$B93K&q5{)vr?jzK zhw67S_y*UT{BXlvTo2-aaeG8{L*G7`GA_zU0r##CzHyEv5lUQg)e*Pxm=rqb# zw^of&h_qM}wnI0DtZ{jiWB?U=EfrPIGALMGp_E}gZLsX~SZ#MvA{G)a*q~iEsKCk3 zGbfkH3R`mg%E$woaL%XXs5W|!XNbIqRZ=#k52P)`TBnxX;VW!;YEh$+Js4c5<(O(- zxYm4Z+-_*1yNW$mM!uqc%HoZt>K(T`$%ldywT^-6bMOq2tng$E+GO7sRM_mI78-V1 zVmY6XCYvNy7VVN04V?L4A+{tg5t zk|$J3yetxLhn~50MxfGe^$%Sh7E3B3B1HUFO~o5^`|@=Y?6aCfwBo7wU}_X&L_S(v zj1unn9kY@2Jdgcw&~s*t=uaE?3uyuGcQ_>XuSjAiQf=G#O(eV*gEqNh>v_%-?RgR6 zEAy9JRZ1a-TmkCN_5k(yge|+4cSF)7Oh?L+X3r7LP|zGtDM@I=@n7Ho^(0ZFTlq zQtjeEV<0JP!4K)^NE{<lcl_J9Z&OZ+0q`C&~p!)5x@$HUuB8^ec zySIbRTSEknE>@BNqddeO?A}Kw&g_C=@H8E%T29IN3{;jh-ROCMa|1@&@G(+OxUwH< zXCjS;c?&&h*GQXa4%lI20_8S@|6dkC_--4YWTro_z;Z(@9=iJ}+wudW3(!=6$)1Ww z-6x_>Ruq5;vz@&*2Muv9#_Ry)6{ZNRmwGmY=DF)Ub%FT)%M9x~-QR>(M>WSOB|BS9 z#*F`zropTXelh-6qSAQn6OoZCcnE7NcwJ)Z$*UE0K?g{!N$uL@C(NM+ih8~FzCCYS z|4R%Fuu}6v3Bx?Havo!4`FJn^q@CV(D3?ieNDm^dUcE#1EJ}sT{hUO7y4FaZNO>S7 z`wx~kxWIcgmY<3P#QBIb08FTAYe|Q#EI_j|Mbb2RBfGHLSY{eq*SHZDY+i|G2wRIy z%vu+<+g2A<#ridOL@^x3Oc{jui2w07I%_{7u=pSkNf=N?ADmV~7zh@P-kp|DIYcR3 zjyCv1UojApFDwlfPj57A8(6@K7GLY9Y**Z8={fMEO^77=4nM)}O3sH8Aw{y)F0>zx zAxx=3ECHovok?XuMKY7{Al%Z@qL^9<7ox?X|9Ny ztI_0LbXdBsX7Z0B`SuFO5&1do`!VKXBY;9}IsxCGt(%$Qf9Qa1Wejk>YuN(t^RLIT zPrvrM#&c@9+$U<*%jwqAGje&oe}mlu>VEf5ziHfh%|;81eC@Bly}<;T6j-zAy{IhR zLZ93-dr~KLGmq0RT1=gJWUP%HDP_}+j!R`%5B*=7n1_cH#~=E;Jezj-C?G$(+qIvy z{66jU)u|09KKL?-IM`Na>T}kc=Q1thL%2j28JC_mWb$MnJ8u4p2S1w)>_Roww|Tud zzxbrd>%C%ekUkejtvK;^YKiwX^-T2WZs}!*3w-~TP?aa(Fwf^ig!)$PtEgVraSL@Y z*&ZCkv9K0oVt3c@Wvxv6-is{3vbzXBz9gQwuV%YJ)I5I>0!OFnYh;_M6p_y@jgugP zBsI*)K~Gf zNlGSoe8nZkN`zWRNoQ&eU6@p!=Dp+r@DetA9hy?t_{N?yLfn5gV}hSoX(EYaZ_ zF&ef^_h&Q;eUi=NQ^`fR4^tVcJ4Q~So?_gv9PnaG_281|mYk9du^Q*nP$$M>T zzIW=r%k#Jy5C01S+Z<<`bk}Waud+@(0W8z$;Ck1s9Kaa>Z==GaYx;#xQO2FL10n(J zPuKlAi|t*ukXnyg6K?tSWXl;?M<-J+##xxkLfc$W7gL1gmUeAHL^PY2NMDpy7!3^LdU2> zs*Lzdgn{Srti|c^i?(Agg65Xjg*biXbYZY>x)k5XUQRuGyU8v0WcDT&3l^TB(F1&1 zzgrNXrog|X>C3|a(ig(bfQ=-RY(qt3Oi%}&dV=OlxH^_=>p5Of4O9G48;DUXy;D;D zFl-!j9iPi^jU#4pt32|3#eUSJYikh@ zPPYF{JvvYWT1Q!Z1xVz{K*$O+8^$`{ZQ%%5IsmakS3q<84W8l~fdrPkEFmTsJB;``~+*6~74mA&JEWAa!qx+?OF(b0%gRGeZ1u9rOfLJevd$Xb6{!+=VL+Ss3x$ zXA1HLmVKBr)0Jw-;P?>2?vN%;CM!Pt0+jPw1<~>K@pEKP;Y7jw zhJxYY1w)K~;~9x9j5r67$b-`{BWx+jG=xLxxI$ke8OcUa4#V$J8QA{a@?QBQ`@ zjHsDnUNh?nOHoL4*qOB^)#rXep7CVJ5(i17QZF2WSRn19!m)B!NWZ z@%o8IO2VJY&pOyeXnB(0`!fpa{-#7k#V6FUS8Ooaqu6@gFgWOgv5hLp&E?PPeWf+`IV` z$bhDg7D+j8GG9>;D&idGhxxo*4-Xu?!CnZ38PpPCRKnsbcza^yPUjajUxa@u+E3h{ z8C)J@V0_dk-#K`&f6UzpSdIBC8_^Nn4gQ9L@%RE7-~0`?z8(8gSzhb10E@spGu+bW z?ngptRg|7bLUcx<6V-&H_yZDiAOQu?#x#M_?ngS1P_)rv^^Hq%2%n*>4%$c0Jv zWv#e9WD%y zVzXXEnuj$70=z7tf-oaJRiG#97nPpk>tDi|Ra_a~kFl7+%2IP_N=H)v3T+?)cc6Av zL$n$0^}=4SNL5Dr-36h~5rKls;4>M$O9apw^F*#Z?zK7U|I!g>mu2j~LNcqW-O0}B zULv*Vg$7T)V44ORPwC)#95p-m)97l586d02@@Ryd=NX6c|51X8jzLvx%*PCw-B_?7 z9(LcS#|im~1VADYf`Ee&f;`0|Mpc*ZeCvhsY|w)}dN0n|u#RrPWdNHBBiEonskIu~I};+G;52_F_8tz3C1Gl?{-;CRoE z2DRB9i|M*vnPE@oMuz)|t@leM@@n)@NV5d;)o!&JEh!^mX67qZ%N?lG$%SB%z9ef3 zH0ZR|o=`Eg7ZEb3Yk+3UL5bG+*vn7P8Cr!h(JYIJCOnf-WQ|22 zfej5VB(K(=EB0`EhUc^zr3mQyFC!M&=%**{pW5UhZ?8;#tC|g6BpqOIuV)-n>WT8c zO>ebqsNL5w-o(FXKSvYEZ}&6! zw{JrlmD04M5lCE#X?r*&+gmDnD*%bf0W!KKViUfe4@+K;k0$Xa^bj*WCV1^Ez=97d z0eZ@86I^2U9aJOWxQ8Pj(ktgMfvho80OGu%(BvwHMD|tuG6prMl>5OQMg~{`i6Jwp zo_|m4^ml)ExMJO}<5ApLXfKAH z3A|&Riehbz+sQqenMkH_?0jol0i(D~-ygXe<>${gn*a%Ruaacn%qnO9AC!5aXtpY7 z6o~p<+@VZx5z2Z&%_AC^=@99o#iy*c-3Y^EO)eDa1YFQMbHd*qf89CQXPR9S;!FYm znNS-b6!3%Ko(L32;a%eK``?7prOirryoeAT1eTFHQTwyHu|d)@kTPn^uOi@LhMn?W=Doq z(A=7@tm}SIaSazEhCk)-K~MbXIgm}!z!E)2W?-Tnzlv-9kpUaM3Dpdw4?Z`%+PQ_IwwDj`^6gkW2G~YJn_g$fQFRq5 z2bvk;=z>@;!|7@`|NAUaVvi_HfmBO)($CC~1OaWMXEn+7l_-y6-3*x;>q~SLS#WhK zIpoIxr|feB1gFzy=pF9R$=VxrgDloA>rR7Kq0%xV*FM68KPQH}RcM10ab;mZDPsL= zrd{YC5Pvc)AKKaL^MrRcNc^%kX*ZKi2mTF{^^*CkMMH%5CC@;Fw97;>D)(0g(^GL#i)?0&+a>~9F@0*19*0g-W+ zd_FZ$x+X0-imz0HMoTKk1D$Y6cHK-oVeL?@SG7|ZZSIPRpp_Y8OlW~wqq zDw8w;HfyR%SlxD6X(pA%gIPJWK;Hbs*lV6lYKV+&R2{$9M#(XdX&-Zh5hYe=>Wn{- z_WIU|&1vl!4aCWzhYO9!)i^PJD;fhZ@7J><*fWWR`rX#WXKlu}iY;YMQgLPESKLIh zHAKoPI7uR8Nj|Ca1~nlFUE}ER!t++e-Ow1*-}8*e5MayRJM7e!+K&c`K-lWbwG~38s#Ly@QnWrxMD`I93Yti%R6n9>bb_aMGh zrH2@>I^B`l+R{;#RnlJ=z;(~J&U%-OD~G<{EWWmG%j-p_PAnSly*BDv?>nd{@Tx$f z8FBA{?Rn=)6&_iK$IW_aPd;n~t5?1e>{mh6*F9 zIM(xNLEK0MQ1TeXfZs@B)A=ltB7OZsI??Ft#`8GjJbZK%+MZT}K#1cLc%t+9^|dE> z%H|6DGeQ9Bnal{L)+%jq^27(BF{XnRj(zQ+Hz@`vCr0PDL&2{{{^F$SCBL@*jrv7YbP{H-WGw8`4t$$vsaY$dI_IA1eUG z-Wu;x%SX2HEI>xZ_LL*w9G2YM1=jSi(n!73I5AM&>NDd492*(;!~TQv)z{~rVL;^_ z@Raa<7U1)33v<>q4MLL^Fw{3+aNc7W1Qvd4WF4`HOBWZ)_MZoR$n$rw_1XzKiiW5z z??D{TKUg?38W$|cgg4s|9$u)^QL~48S|+~O@C+*ExuUweX)|OWi#vL2d~&w6rKb5* z$?eG>sl>k#CRwaC2~F6w& zjbp$38a1fVI(6eGI)}5dVAb*sW2rvX1^!}3e9i9%redtHx$tSm8|RF3myRn(4gX# T)R!}((OJYJD^lm2&e}wA;YjSM?ZmjIPql~BJUPnBq)DUZmE29_NvOdz zDei>kx4KNS<@rKdvLdITIKIPTKf!pbv6{@KOI)smZPSo|OcLW=lR;gWj;gq}z?Kwf z9s#ce!k0i(9CLS%R@B(R&YgrShCvVm^{b@?D{L)djyu$)A+WU?TUra?i|wGj=vpelAV(Y?}{+VU5@W%X#h&6AqFmsh0I`$`H89#t=VP^9f}6 z6&OKS-a{KjI_`UUjy83fimp}f0r$hL@zzE{baS~t9*^E4D<1x~!PBOC#IXS4{*Q#F zqaTY;%twm2Jf2k7E83Gh>|AUw-fHv&@^*ZUemzXYt}(2oiFrq4ZIT0G#;UccwW)UC zR7syD&yPp9c|<8Oz?p!pe9yVqKu^Ep;}IMq)$!cpGd*CT_086G_oS+XHlik)E)&X9 zL)zgwE=FITyCU#&MPljV_N8i_PI_E-RF1URSX~MwYtNVDmRI~?7RT8Hm0ND$w;hyb zG(P?B6ePT}RSG~w5S^u2k@KM&x*HGhb#LZKLf{k*QhTvU-_iPlu}6Hl#5}DySK^gj zHB(Ez)COU&#_~YWT9{w>-Ls~ipQVlWQ1tIJE7f+omiWRfBEVJxAtBmXpFdWYeXaMM zj!F6sInoJoBzrao3B$sM`>Z`c4+>TR8=!6xL`g$IMy4DozMRvLFW;W_O58wLFHP z4_6FCs@i?aeJiO7eU?NSy#o}1+>4r7s!kz2*dAn+F4|sA7RS zVnn&Al&CbR$6_+KmS=Yz7Y?woB40AtoXOk9!s81u`l1B&E%*-ydSIM07U)tK5>y7_ z29?jN4{vl>e!Se28RsTtz3VLqg+t|)g?(MKklfh#7f$yg-Ds87oHmf8JI71Y^6Z4( z_{?66q&?IW?Te983%|>$>jpc!CV!P6ctr8li^e@a0+ONg4SZy)S?x$T1ng=Xb?C23 zeZMt!?Sm20np-|mQx0hMiqxdfSYcGjpIWI-*e#57VmwwbE)sFNS2Kz7pgVGsbcCdt z7w+c+U0(dl{Fg?oF$!QT$4MCi?c=n$6b<)q-%L-)D?3e44`1J>$s}jid&WM$!7a02BpRu_QK@@UY|S4yi>R58e9K$8E<*-ED*5^D;=t! zV!gsV&U8O|6Q|qUH5{`Bsc<{+!{DXe5P~90s(|Io?8u4|2?4k_RTZRZ%NrXe{_tO* z`SRr(AGlPAf&ANW(^zEvus$((h}|VEQmid%@LnRvvsh!ndD*rzq-c^CXLid6{IW0e ziLMssS8*?>x%EIvhPoetU;AtrK|$7e=aH4^`FW#HtvD!Ll)3#M6;m{+#Ch10RdH;j z0jMsBA$QlEy&K?cp#4HmYFT0+$npr>b*&%O*w>a3v|HN^SaRS|-AEu&m1<^{^{du#iinsmVjOr5Ns9}Lyk5~8l*NloX16H8w;bJ!PEJ&#T6+Q9Uv8n=OQ zn%0;NBXv8Xdlz7w>?zFiw<`9tV;=|TpwP=7 ziQjKW?qfiUD;XM6VG=Do!Lmy3sz!eRKXhwUU+ODl_Ee-vkFF%(CZ(QSXX|1~@bSj~ zHvybu+yrk@8i6A@DJiiMaynK<&}mQStza7D0=6GX=Iqmv3OgUls_4mEXt2Fr-I$8$ zt-RD$n|>$z+A5b6_ZVF3w}6Pp$1bj7-yTqMKNt9&19`>vO=9z<_gt-Mk=*L`}rtzQlNry8|-&CjB z*jQ2HSCcFzL=U;}0g}Ihn9@;-3pJ zfB+^9GjbtLaR;P!=8)IKuYqO|^Ds4536^4a#{x^i9?ia4WGa3;f+JA!Fs*s|SD=TN z)MkyziJp?Muxdt=)5Nd)h$6AL1HlWUPQC;d&dyTiH$Y-F`yYGMCO7*UMq=eHOyXZ!>x}&nvv||yvCyW z?h-q4@^ZZswiWzerezy;5<_-Wv1I|S+BY3F3FlYCyv!6!KhaSp3obZM;LIKdFVeB- zmOohn(Mg!mH~^$zx^G5g>Dg9Qdq^bGXc}fmLSTh+91k-~StJHu%&0W>4y~zyPM`S|13ah2B2Wm@&Xyhp=Tx znl*>-Q9yOsRy1DK$ck~~N08qC1@OzzWze`N-1bqoU1iql4-%Yy<&>JIBM|LHAKLX#+v?=8iMAp!T&Z%xdeG&q zllM+u4Q8vQ4YPYDM;n9j8|~IMy_&=<^1>o>(?8R^4J&xSVPpV8Tl;288=?^1+^61 zuB)gynYk!AnaQNwY*dmiRww^KDe-JcChcsgm{d|PDUsB($Nw&WTqd!boF#%p)-3S| zK11>o>+}B&s{eYbPvHO1{|C47BRTHd6lBJa_t4IVTsa>RiwKsyk#E%~gx}NGF0k5h$p8r|jrura#dTJI%E>DnbTXdlOMZ z7gIt?QEp~NW@aX4=Kn{M#MRgh%F4+6KmU12a81ZS9QV#`$E(tDn}t%h4Hu4opd?Jp z6piI%6|CPcA9#24^NlNxVk9VS&I1GPzi>f>twu89v#G{KF_{P8caW1%>@72zRj$() z5N^Y^*%SbrDk0oPLvleu4iXwv@?*V* zqCOAi<4g+(3#5N!oG++1tWB?=A(XSi zt>6}yIM(6~(J1%R6%|ki?Z;%!N*x1ju$d>h$k3WlD!I+rFr@;575Aa5WATwt;vKSq zv=CRXOaW4IAbd(NC&JOq$%$OUOsxpEXkrq-DCDHEe-CHxvp!qc$b>tBcEDS1u{z7; z$NcUfF&VI;7nUC*N2c)k6^C4aaVH#DnQW>!1|13mEhtXuuW9Kle28M%m;y%jE}x0q z&lTRI4HbY5eJUt*o^;!1J^=2Gm=F63Tp)w}etiKNz?;?NVky+MRiSs834HEp^Uto3 zJw`w|Q3syRjlCTq3e@k(=1$ioqTmK`{F&9fV}Da}d$PY}ho(Lt%RSvosh$KGQ9&=8 zEFNopF*KP^pE?Iw@9l5nDpXD zoew7qzT8zwTBLScdArJo$47v{JNW=e>??Eo2biODHk^&>-QwFPJ z%(N^w6X99Z38H|lZ6%Omgx4#%lpk1s+4V*qSnrl;a&~3&5L{~r)UFV zd2^oii*&>1TZ?6Dasq9kr{k6oCZ-=r=@)0mdAoY1SB7aP(*~$>D6BC#UmgCGV?5r@CzrMZYJ)PgUYdeX z4gnI9v#!+-h>lp`?JZT^%trR<NzEXK-C5%ZXcu0TQV&p3QJDQ!t{=Cl9h@?Ni55*UlH|4rQ^E;MM86b@^l=%(w^Gyo!*BhQ(vFs5%}1yda-+@^+U7SYpg&$ zvyfwWmYuvzp4M6?ws{RwE%fu;>FSz~fpMBQp(u}jISk)bCDxHqspeaakG{0{?72uE z8#fw7%B5RLUaQoL<3CQpOUM>p8*g*Gnv=aA%=0AWzU;|AmWZXmb>x0o1mm0EbK%Hn z`pq^1Z566jGo_CS1!UMEE4GUdKfBFT+`%LvB78=JR-Q zABkPHb>!}B#2k0vPGy5_&Bo&`Bv|l4uItq0YlG}DiJ3j!j&^unnFePZRsG6LcHKbh zH(wxMEchRucXrE8lO=LwO4V_0Yp?Z6s0Po)b^@e1{5@i-6VFbnrtXO!+}*;Hi{-cF zxVpZshlWYdR*8D7bpA;-SXQm!$-s)re@)b}8A_tus^M2aTwlSxP*QO7;p~HSs03#R ze0x38^M_IshzT&#l#5c70PnlT=mujxZ3cDdDP*0L@#|ZDW`ytZ2($w;hC9zrDM~`N zlFel{ zNwriwakp`59pfi0qkRowpIZ8nT>Md9ux`>MYhxwOe)dxh$09la&K8d0!)HZBRaI3@ z-s%w%YW9UbzrDNa?8p=hyP==H7>GNNt+SHOQOW^#b)SCm$7cQZnOjHTe+n_h<$sDO z2tq8}=l3Dz3YI6$h2clkGyoGxz#dRQK#Myp(z#4mPOUlGFEJ9 zQ-4SI`#o=isP@2B2;si58&Yawo>g?WkJH!vpc|2N;CM#V#7ly>@^@mXFsp`BAtV_` zb_6l74V3IdMWM6p@&mK5!wF1ezKF@CZ{2;sSnE#oUE5#$hSu!mt1crx$W&p?Z5nl+ zPo}oZ_1XHnzlA?=nysj=;1+R7oOnySi-hY+9qppk_$i}D7RNEY?YtE&0X@f2)ZU#? zG_EKgvCVgWwBzdSn1F%aZ%cC!Uju}3`|TPS6!RCd4em9Xzie+6$#og= zjjxn}H0pZMx9E|t{6dA;SD7&Ca!7JRGIRs8;)vp7ymtW63yt-0n~E5l)m>R+yhOFq zC?tqIgP-4rm7k3}Hg`A-DxrULK@6bKqWV_(AA^Gl7NC|+`l3UgK&{D6-i?sauDdd5mP z!w}${E+_=V#lu_J&;CMb;9l)O!ciMXM*>Q;-H60l`vY94^!ohQTlSfXFnF2tphVK>!hOf=uR%Y|MuwBuZyWszx9|} zH%Z5y2`KwFiY&{uLo6bcolZTX?^S7=(!d34gd%Ga0)9V>_wDM|^C?DGQF~P}OJu2Z zNA+8VBGkpxBslHHBSb_G-nAwIvnUmhO+ZvP&aNgxm84r{>4Eu~kb!THtu&1ZkAZEb zs*v3-n)|AJnCQcbe{Kl)%v-M9^fyS=mse)FR$?b|$&RuSSt*K@1jc$(@iJ)1P~Q+Q z=3WVpforC!6o0uGw3QpmrUYV;5VF*IsamG2DB%1rUP9#~M9U?-F{d)#fhuRM(Y6r3 zg4B8da5F%u3!#>KR6%4ke^LjuwQ_dl{GLVgaC5tM+Wj9iaaXSGN6OMDlb~(E#@wayzGaq>qSK7Cu*dhn2HGKS}r%DxcU#D6GFr>rxl z9F`eVlXNu>g|WUs3bv1PGxXgQ4$oHIo$SVq_@p>(E52(M`9G2}l`G26U?%IWSMTck zMow8Zhk*UO#lqqbStLyy=O9x9h|5gBRAqAr__6PTn-l}23mSb@{@{IGhPwX7Ss@X; zok^Udk55{gjaiRE0Pr#v?_rznK2vS8uVL&76?~awIY{1Ke_+@ z7NU*DOG>Se>R~`AI(JtU>7KT}>1E0Q{Xd&knqV@WkGDrvudGyfqp+CT7r|}SL9KrY zN_BGztIsYDWquTVwBR4_Bl>mGVh!Jki!@}n`&~Klky1t5N(=e1_}(SO0b9pW_jNB= zJB2V)qh}Xv^ZOEgS{6zHB+umMw;|K^454G|+f>B)6N!a=pu(C=#g{ z^@10vICY^|=0jD|$r!cy`@YDLs(yvXd+KztsCjB;E!%A7@A?ON{oT`QSpO)m55|FL z)pxA=nV*z0vNv?{G@s0`mQqO*$y$>o{u{{r-n?eTf1zHhRF`jJQfxQUh|ZmaEnnMP zJu&Z|(R(D?>|HxRc{A=Z7i0vK^P zyie&W2MbL=11Ls*Dvg7krrfqaj|1&4*5^V_mdnfahc3%Uo`w;B&zSi^xtsK`XJF;f z6I4_B=?B$P0Y1FNkY%?D&Ji&th!M8Gj<~4d9XsMHAG^apr4ERQcSv0IvE#T89?=@J9O?H1 z0qrNyxkf-_a9Tqw8p;DMwR;q6kC4@H_9@+a1RTl_5Aqvz!{}4WOgpbuijmg(>GZc3 z*WM~@HUnRP_PkoN%0UOiJx>n6d|qQpzJcJ3HkVE-YQ>1Yu!P9g8+G}Gf{)u$Wf{7M;4PTQE4 z&YN=o8Rql_I8VRs5InX?pAb+p$vMyKa0!&B6MV7?*hDyP(A9A?iL-9~;JAo?wzpZ0+~&zHIHwKAf)Z>aOal ze&{-l(g=u$Fo=dOFmMi*WMfVY;Fa!V9050C@3Yo2N=#AcW`HP|!IB1;uN)iqFg5iP zrE3K9>t_RnO?7FvCBg4Y(PEV|UCKmX9}3Caf3D-*zfgvc4$>#96`f9azq-biU;>rA zsQKvmF9Lr?d**jNKj$EXhVSQee<`}ed3GG=`v}~qg6F#W{_W9xCV-9&2mW-7w)T(u zHf0|56G%s{S90?&;y&_>`f~q1xZN@K75G?+i)CkWd%iut`GVN}K#%t%w4Cbxw|BFt z2BW+Gf|U2Y^XdF@HSqKDV?KlnIiLBf-}m1W(Pv%?6+f7KV4_^E^>`23PfOp`sYDL; z$06#@bDz~{g5Z#95D0i_2OJke%P`yuALh8yE&oT)SsH5IV8v|hZz`}A;T%U>%r{M# zoWRKvFQfYb+}9W7M9Xv{wbZ8(Q|uxCZT#XI!b$Fo>-dAs30&!+z@!xpSrJ_iA`l^lQX;O?c z$07*+mw0cglf#njBF~cIrLy;h1q5i29)5khp>4G!7v0T#NcW6SppeA zavUQd3#Sg=EO<(wSP%jNP7%qKVnn`h(%=_0#|*cY zLhN#kBPayJ1IEaK3HY~l+!IS9meKfTUILSb$~$t5=tQQStO5fn4m-hoMAP*&p|sb# z`suAwr>b!uO^N_GD;G+UwtSn^eI5o~P$Z~fJK_wZ=4`Q4?xR{k2alH8!1ufK> z%T!!VmLpr|fmgn9giU-Zi_L(->g5Nau`P{CK=HtCQ&m1BKM;w)2xks0`e{Ifi7 zr8c#Fa+NKq7#tcsF_^HDPC3tupoP&C7EIkob5`hX*r|_?+!y_!*>@TlUEoEp>?A%` zO;X(UC}|X60hGcvTkojBN55>nuPD4pI)lC&AJl^S6{ZbR-F3R1bp~blpx|K4+ni_K zxm3%bQFT$;#kl|7-s3%YwD6`ctT8J%28UxA(0}1aAS}TuELiSA^V=t_x>s}mhs`N{ z4Ea6#F^(i#4^;|{3y^Iv(D75*d6oxZ$Cz*XNfJJp0PWrXN&GZhxAai$93${3G!@XY zQmB@(+(IvH(8SaVlPld_P=dX>%WaGVn{ZZZdKF&wSk1;-WrI;l-q)}tU<4ye&wXKq z?cy>(_RvgsYORcS#a#{dHac+$&&+mz_at+nC1Y34=*sVNTfJ_s$-z0*AkX zX3=eC>F*8R#Ol=k_cfad&os*lNnp09dB6Gt8Ocm z_!KHAQY&OxY>r3`UMZ9{C2EwHeyd^FeRp8UASxRtq&tY2a5VmGAYZ;MzGvXvoWBiH2qsFm;CoJ z0(wuvV`f8Np7E=@)L7Er;4Pi3u14?`ZS^%&SB73U@yW(sbA2kzWhgfss=CR^4CC%t z^teJOb`)c6$%C@~2vhaio2uW5KhooFJo6j31@#=h{CaXf_SP6YWbmsSH}yIAN52ck z2EWCfyMeVCZfVA?lSD*S>1D)DYL_?fQBDlt-}^AlbWK$Tc2KT-r28}?ZSkjFk@1S08~ zXkX884##1twUqX{oJE&;hR-dPX^F;7K>`-44Jg6cz>oIp0i@6XcPrmiifN)JO3*nn z6p>0&^gBTk1r~Ki2fVnqCZP%OIU&B7(lu`G@ z`qj54@a7I?u5K>o#`gcCIhxqOb0#N%(Lk`Xb0_Z)VggfD8x==b5O?k|zrcbBsTIgH zQa8J?&bcJomlQ~}&jv=WkILpEiO|3`H1$t6ZhvzP;gbkt7;`vK`q8J@pn!!r$~{+)D;An^22P^A1`DGVVkC{EmIOzA+<0qO<>i z@T0wM?!ic!m8TOO(9Wuasxtx`b%S6k(XYFdOs9cym6nVdmfA%!Q8C{Nz|#TrM0HJ# zN#-S;`$QH;F_Fdk%^vfGB{f!`MqlcEmAtm@{#A)zBPDEOU3@6M^|OU?JDaebymPAO zXZdH=l}kuHUH7_n=D(xf1c2ai3`Q(uvQ=Kaa52Cfx-qaWn)B8upkbpWG?02?eQB$? zLCR%zMy*(A&WA@~1L`?qNTY`CJ!?IfMo1`W?eUAr=t~5p$iPOVK)w!UVDj)7+OA36 zT;H!Cwi}pQG^N8{?&b-q`TccscX!u|VIK@JYD(N!U{#HZ9Q*C)%i3pw9vnG`4@iCi zIJ(prDO9>sfjD-P%tkPEW4u)Gf3W!lOx`x!YK-I@Rfm~h0$W7#8Dr}2Q&4p_1HuGU3=m` zp(Be_$z)aNfQuGTbVCJ+eK6&Vzb`R~ye}m+h<~qu*t}K>4cKG|$$x2HQs&h6a|sz#uhQ6s$pVl?HdK#Ri#YFjg&81o)%fvHgLP~}Oe z?_^nM?;v5AI!e5~VUcDe=k zU&Et2f#-Py{U^`CqpyEHK3vQffB5qKVysJ}zQ?St0^V$WteoF|j{dge{VGkXS>ug1 z$qXZJZ65i-^z;%h?#uZ=4E!2(u~`LBm2%KC6yH}jH#7Tk4UQu1pV`f3?@Oa{eZp9D z`#;3NITDA5FSt10+4)C;{!I=`ckly+PkQRJfxFE;ZO;AOp8kCUMu_>#UWJu1!ss)8 z?7rl|J!}(9!&VeUeYoor`$@zM?}JgqH81~1Oo6nJp9CU8tJBl{zSoxS+snH9wfnM| ze^p3&E`Kfx{v+y1=qv8audtA;AzzNhzF=hzoFs{ngXJ2!uIRR$kxKb&I9yp81-`~} zZ7i=N5-z$` zi6QGd$>s08W-s0_JC;<4HCBveliUi?uUNHd%K}LM!fHPbK6XCroNrM$XYHkO>-)-z zL;ou`9cPl3JKY^Od>1FdsDnG?EKl4U(asQ$axO@GlqW&(H#mcGkyP3ePlEl(I2@vQ zO`CE_Jh-2!*brnvgE7w>5bH=}k3l+&R-~;Y#i6cI7<2XFPaBbhu;0mzwr6GUQtKxF zAA@)zYq13X;Ymip3ICSTwdF(247<)?-Rj!8;-Qk7(J3>-ntUI+nlE_9?)iy4% zFjc0^c;yyMPJC_qMxVo{T@SIt$FmTIy*bd0bi)+^I{b1l^DSCH@wvF-C3W_l0sT$2 zFTuItUjBZsArrI(t~7E5HDKThM_A!b>P)2hh~u6+5`61H@R3XPA+Fm9i{}MvQL=<& zPWjkL!6>B=%nJPm@q>1{UCW7!yVtNmJI()|%i|wU!@1Mbv8(gD{>|P_>K%^u@f!B# z=I3sV-no0S3S1cGGU0kOemEg?b*nxpBb72&`%DqU;@O%2=Tm;=Gip}*Ex1oYyQC^t zmSyqiVCg-C9sh81{PuJAX+nuDbtzgD26?q5d~r$E>>gZaK4h7Os`YzY?GRN^3*y31DJ1y^jUF2i2ONp?8Y!Xo#1ZHjM4<<0lsU(g4mkhU z`6Arepk$D=%3cmDoQ5HPQ8d1G!3{3$iTz;%!)dBL01ER)>Q`q-EY`J_%Ix0VEPszS zUsc*+0k343{7>0CUn0Zr-1j}v=5TI)Aqyhs|3@^EDL7G^8^rBt!2FVl?C>EMV`15n z1?@D!S^v+l?WYZ=kG{5=pD+U%B4tca5@}I{q9sAgRGI?;y>KBO$nX)327X*0` zgcZ$Lr=cPt0gymVFwj@lF~gP!**9$Mt6J$}f4IxrLppnIz5d;g<{y~TX(@aP{0A40 z3JXgV&uz0&6q;APn$am`7lo|cF*VFx7ZJzesZz5AY!M}@6*L;3>;V0QR&}5lj6HN# zg$OczZG-;spZ8x%>;m+UCl( zZ$+#@9s&e*`WLNvM;A?Q0vXJT<(br^rXetm@6H~ZB1kd~j7D@rXWZ|lO3Cbs?VPNZgDNiX@4xD)vYe82f?Sj&WOjc?CfPJ1*tAcI)+-|7 z2FJ0aLg>vE?^r_ZJ_-)9^A?x_oeRkHaf`O)_lXIElwe_#3SdbK<<0=& zuzo`Ur~vUu;}B9&^kKL#7SfRs83Iw-K{Pgkan_MNSTz#{);Js`c@iqLWLS0+He*G3 z(lRux5v>qP*4PZinFZ$sXAFHb3|M$5$)M>V6Vb>5b!93VY;{;ectbdUtPiRe8Tg;T zu#Y5+WHc8F-spJrP#_i{x`ps6?rx0;Jp2QH+()J&P09L0bXP(5Ej7Ej}mR?lOhDw|6Kjvxud2Noh` z>|s>2!%9l4h;rV@OBJl)$xxAE9<-qa3uxRS3ziTbRg7#b2tx&@_RRE|KX<^v{3YYX z>$29|p7QQ^+2)_k!h+u)km$#Xh2Ccr_X$-H%xGPSokS!oTH{>tQ3l_zAFvhNBX*&l<{` z-eSBb=O=wtn4^&+{U(A7jqMe-N)aVbPH8RbZTeD%szfmIkrE&EML zGK%BMOmf@h;WR^zI6fA5v|R{(v2VMNT!P(hfL^cTU`ZhjABFUVX5mohfLPE8`CGD{ zc%z<2%^7t~TNxpnhAwcv3Wjq{gtO}WioI1VbA3*pUN(eo$>dF5BVT@4i3UVKuyIO9 zwET2ODl!`|*_tns{^z6iO)%sr2;#CgXpxSrqz-=Z;IMX|De`QDm{Q~i6!;6;_5#8k zWoVmovJoT2QItrz3U?=dS0i{vGS<>f9Qt?gb4xP*N~mo+`WH}PXB*U8ts7;Fm3cJt zA+f5=g5loE&V8D>34ZZ+O}6wa`&F!k;kSJY z^L|ZO8ky3huLA@a#2oUvCfCz|-shplVh1>%P+?54&Vt}-AfOgQ4?3VNNJ)`>P<3I% z%)p!1J-`;5Uw^C`xnCol1sa}unT6Jz#)|sX&_G!p$)JC&SR9QQRm6QHgD?I18wj;@ zi)kego#XuK$?kL9`Kr>IQr{REMN>o~M z-1VmpQ?d9X!yxIxP@Weku@ASmv(7R+r6H7tDT(y9^o9B)cqk72B>g6H|MeK9nr*k* zli;T0%4Uz#4vRxg&v2SBP zoFCO{3e?|d_Xs`I`qJtxblpQeLRLRM$|gJp2`Atu)Z>Auv-SmF{5eT)8|(ua)_ zMjZ3dDvq*k__QN}VCioj)#EA$__%}1tk|k9QhC!osq_InPI2s2eL-Mlib9#ypRZJk z&u6NNqJe>41fw*?r zcP-%kz#C+=e28r?glP|DQSRTg0-U)vC@~EwK}G})ryGDDE}b*ii=*MNUB>YX9&Vi> zVcm^+GI4Uln6AwD$F!b|qjkYq(`PCL*Qv|?A6fHAGh2k_-D{b=oKOs7XKPTU*yOzc z3@)3tv2Zx%r^3$6IoG|ca~b=}%`Yz8jUE zwxmV)i#bCoiTFr%kgdlUvs!?$WcQbm?FKnd#p_EWMc|7J4;88#)a58EU3J~m-1OR* zm0T3U&vDL2Z@nYBT}y?NH<;2+SybSMgxTaGrj^B-I_gg~y^Ku5*^bzk)~j47?^~c~ zD1$8E4GPlXE;S;p3g6g6Fvf8|^=V+xLnud!JywIdIcoi;x`fTlzNg)+N^LlskU^jz zDgH_@iw-(-7(E8scZ0$#(xQhQ#*{V-GIb_8xV@h9=ce5DtTsS)*xh;smMd0D*tW)) z_w&ip6v^NjMc^^F|Cc$Y&OVtZp=K={wso+^n zAJQToZAYWfRmzV_G8W7)u(5DEG-Fz|AH9P6Fz%fZpz3xw=B zJv#ORGTW%S|6YPzJ8>EOezmbP`pv^dhE?^Wo>mL)9~^UPR1yB$?%~Q&HH?Q&!Sj!A zgcef9{L_hxne^5SkAS6NQvj5L9rneO$M&wfzfYy=$E^?t*bLO@sT=(WQDHxmUtC~j zVQ(MB3vWC_O3uwoORg1Y_3w3AdRutNup@Yb##mkEc1tLRbE7hiv%?tcnXCRi?Bc!p z={`=d@3nu37fJt|L8$fTgTJpC`m-e{w*heFFOeaE&N`2RC!=+`vx zgFafY+^;%B7YXAqULLmCF8)a0eQ3XRVr5Uk3mc=$c_I!ZKg`p?6qt)5d}g8Bx~sT( z=9AZbjADG+bfa`;Pd%duyyD72JBO71+UrP41x705NEI2+e}Sthv&22*Ah-Tq&NnEo zZVl`ce>_&tp$Deq@U&3hN~Z}WY|1Wj(mFd&W>ZuPg6E30IOJ~KhLo^76@aWB3FjK} zJU_wKh4zENrFFH}79P#~;}obaQmB*|v3||;DF5d5w0(fG_UFy4b&~T;iZT2r;HfBg zvqSUL^;b70T)U6dt+M%-O`U$#4cVvcKZ#Zo^5SKM8x>%*lE>A9xN>!7P7Xx)e1o1t z;$8*U^)7=>X_r%Us2hv*$TK1W=}u3=W*CFaTOks31y>4HPD=NSpo6`;UO}653?B#T zqjUokSgA(=XWFSDBQA~=E!Sos*FqQNS?j_mH_xV86UX5qI~HB73R0g*D$|z+T~^le zgifVbcqb6k*NMwtA&{{>G$$11W`n0*d%kwpD`zZqRAVGnZ$Kv**K1CXigQ3|r)SA8 z%{Rm0BdiNTn21)$=~d)0{k=k*3FbK54}T$^ikJo`5Xw)1w_qEx|LV@(L3^RCDx@gH zgnxpwvE;<|d=GK4UCVyh4Msjc(p3aG;K#M}TomxT_|}vmU%>yL_G)9Zw!e*p&HC+) zrbL#JKtqTi3z0|CQYB{b5m~#`n52nHV`gKfbEcgClH%8`*A+lPGU`OgW?}GDX0WHv z<@pqWf#J{M?V~B5Z1nTqU%++wABM=<7{)AE>vcJ&i+BZXX};>9Pc6CSzt>Nd8?$l~ zx#K`Zin1eh*`%y}Kk{n<>AQJ+S2J`z#X)}uSe+OmCh%0$tLrst$;x1IYpG(nBQ8uu z{+GkaV+r9?L>TF%=3lo~e_48^S4u6@OY&qss5G*4m5A0c;-r^G0zo0Zp%3@E#(owK z-eZv?_SULhtgUe+`NUC%`Dk>wr;T<5-EKf9-$y;;#C>{k-);+PAz+$dkr5!hePPfG?<|+SIY?r+jf1NOz zr;kA{V<|sl4nes$v0n1|*djBQH}fw}1%a+DXcXBJl)0JI=ycip5=!LOI8S}|HJB;W z4~i$IKb|@uHh1!L&;evYjgC&IEPcy@0e?9~_vwCNu3j zSrlE0T_@Xxuzi|MM#ur#NkUzFJDZ>l15l7+L88jNcKcfC|0eQaWdRJSHml7OO%D;aQ=2_kDYNP_NpwR# z4<0xVyD4&|idnwRPG~5T5HzsYBU`lH@(}eUjoUJJUS9`el%%yrz#{Hp;g1o8V^UDG z7Iv#6b{P-^)-PvDM^OrD?Ud{Ab3wE_IiUnM8>1>q>CG>wLX<2EF_J^NbpEnjHtS72va-IZH6*ZGo_jIXz zOb=;}aOZY=9Tt&YHz{9T{j0q9vBb8$?I`y!3XtoBe9QFKL1?;;w2ek^XvOehc-}p` zu1{E0`UGKG{F|a@Ke9;#O{9I{0U=nsf&<_Bn5z-IgtD z|M00zIq9OuDmI4Mzy$jTa)J9ab}&i}^V_~29&lRtghCGRz2i6I@Cf5XD+JU=iGZT&pH0S|e9|@mi3G4K0EdtisD}**TXP5W-rJmYr9>>Jg z9aW-E3dh9YFx~k8DNgtT2>jD;eX?FrqX;tS%O|}>U;=UFv4)QYSS}!S|N3XONSP>^ zMG=fl6oO~k-0*#6qR(W%&a;$5k6?Bb5RB~a!v?okB? zxH@Z?OMx?I<~wXoCB%Ia$+(DAappJ%2@b#DLxs=)7iqg6FFJrkwd!m{=V+jn#PTkOPGUD;KaXto76d>_t$Lk|I1V0jl z(>3uO025H zCv5~m-W@MaD+k0k{Gmh02;aeF(bhhT3yo{ScxRNjXWYo3Gj1XdmIVT~m!N8kFPK3x zS1^TS`9L8s&i}c9CKm)E19uQ?*^+0h0E6`nLk@aXDdyL9Y+Yaq6rkv@_uQG}mIelM zsM2`Xxy)z3m8EQErR+h5dc!M`ozTM#u9eogOQTy;Ph&>Sg@Xhv}ZT%&9w$4uf zd4GebZrnf+c5Bt%nYSzLy8^3D!dH{*V*p!^TT){;`iu!t{CTWu279HFa~pD4u?6Re z>q#qymsn0 zW?d~G2JoZpl>MHh{KBT#s+W=beC7!4!k1F^9L6~eGea(9FfJ={dirVxXH7yCVf}@#)v70#wjRYSX3mY3N+kg1qA<1V=JrL}?y#MptmfYP80A46^ z@yF$IWGZAWD<(Ur3A3P)+mc1=Cdys``}e?T4JmXr2`xEFocOhTVX(o)Y{oJZa%d+d z2-pXaz66uez8Bf7D>qq73BLr{gZ}cmZXf+(xu&VlM4cjx#KAofSb~oQ$&RFTz*0o? z{s3ais>p+5!e~jBQb+~Ho{(r50Zn;UiZ@5TWN2iX9~Sq`328X0dJE7{y^z9m*(ZOJ zuqEl4oMb?fn}mf>TLevN(;P;HfLj)4sE=&mP!LQYO|+InZW~Q-(NM>RiU>47BQj19 z>L9Q#>aetgjp{4ohleyt5BXprxFn;?&`1iIG*Z9}0~%9UgQprP;&~z=fw%`1&{eSE zZH-Sc>()-F$Z+(30I%FnBWtb{3raWjXm}ikteDArJr)g-1f~nFXFE88RtHv-pe5u* z3T$~-Hq5J5wkO!!%Gh$eVU7)GVzvixN&&i|5R1B@(Dp?dkkJM9%sBW@jZ4+SiFW6 zQMd>E2}a`ytCO69KB!KJ%7C?;=jHD(Z=wRQ9f&SY>7f5QlTcZnME^_1KZ^Q6Ui6Ew7R&e=7u4 z@1}C>R7S^}TkhRv7`r4Op-42&tWmM(*{Ur(dOe;)y}6MPkiitM?bi6EA7K69)AoXwE8vYLAwhj)lEzpReb#=j^?+Cx zWs>>L9@V#7XW`A4g?UsBVyJFLI32zFe@{UA{d#?EpT> z;jHXi;zM!0N0yJQzXdl(&+O$k)i-Hh(iT2Mt_rrjQmIBS_4wxM>YF8nrZktVgz(D| zczE7?@hbuGU%j&ZAGW@!=H6X+{^)zAB2Y*~@g0TEqj5Z8@bt4>v-)@c5Npq$r!HKyO|#s5C(|nezoB~Tf^s}=Td4YW%f;o8x~rvF z&da8)b*fhr+b=;KD)y4~F@_hqoz=g$4?Q1WwePc5KCYdn?X?c846viVtIoMby=U1g zH=ywE-*c%)>e}>ntx{+`N_JPLIPM&g6E-1cMfm@9QCDxA##42{w9a?k_5T~-;Q!Ha z?mx2wgunv@A2w%QwexY-ujp{GE4eN%I$_gLs&79f9@<>oKjr|%eMxHTVbqJ(<{UV;rrJ7BbJ zuhlOFldxah$vmCw(QgH(FFD-A*>bM7YU$kq{o(_(w^`$%2>X{SHR88y@kb>EHOQvr6id*J)BG>1=g3nqYt5D;zzA1+4zCv=%a)O_OY+ zyYHu1tLXId^SS+)Rl+vDcF9suWq=NwZ+eA{J(=XNtg`#8*&aum;+fi`DV_QX+g0oQ;}7wbEm9#NY_U}(yu0uy5$WKf+Q0Sfw25XXU|bG z#(M;ieHpWAcKErppB`_2W724b&SzIEYz=x_=n)2QUrllpETU;k6blAxItcql_}C_w zsy=tp(q)y}&s2(~-`JZjTfiLr+p@zk^vBZ*$%`|Zb?a8*Eu3>GicNcABy4XyMrDm3!sc{J6jJqa9BJ2csn7@CTE z0tbdkFeSCFvdd25L^mVN#&{5LF}Lj6Bw}oe2=Vp56b|iL*ZXwysh;b)pxTeH?grlmp+3)7BO+jf?-_LMaTR(? z(s(>(%X{o~w0<*wXVL|X^gqGf>nFTiPW1!1Qxstu-&R$5Q?peOLP9Oj8c!%_h(yAg zlcsS_Ij%TQ2#5fM7EJ|>pkpdxb4eA}Dp`#q$vR8I*5XF`Q!S>+0v+ULQyNGO)nZjU z{H9{ETCP-65~t23Py!5-=uLK$vbddC$jj5_ozco0ZA9UK2Q!13ia@re+c>P`wmmvKpIe8Qh9n%U;X-tV^+q)E_^kD`}98wf-#yY zi6GqW2lS19Zh>881+)n>6acKCFX67Cy%qzga^dktFMe>9IU-|WP_gZ+zbnAlwuo73F9-OtJ44L#YZmji?%Z-80_A)Fe<)UO;fd0nY&{{cP~ncThQ1$<#~)Xk-2XOI=n>PhQQ0DJNuR?K zcxXaIJWumpFeULYAdc0o`cEa;_ z^>ca#3s$3%HjhFeeqY)^MI+qc61+YOrO{KXAdrluSJGTj5^DQn= zsTtnp?Zg`Bx|{hp*Z$-Y zmsdHLEr?qyvC{e%3Q2c}&ksuR4V}~1!nzhE9#p2ovaXBkeA*XBs)!87)_P)grsX`n z9?#9?hvgpc=_+F{isSHY8t417JUJR`VQrQfZh=M`_$cD7j7e)`trt<-u$2;7M4{C( z@XJ2T?%SH$??Q%E2bAyL8q2*|90Qua18ddG@y*8F@j2zztoNJ^dMH&nK?QBag(#L) zqCHXX8a0T=grzB0Bkfl-t%UnkaEL2Nh+Xq5#?_m4gx3u#_-!1FpbQSw@q;`9PP~aD zC!jY|$^4?@xq#lHJh#4?WEOEVJbZAooj?ywl7{n)Y$?o79+S!N*Q+8K2j$CZm#)8w zDAea@)q!rTc>sKsS__!`Dt-^ev;!lER-^n9rinB{b*sP4I9AyUTL_n8}xey$Y|^qPdB=UsTVG zJNfe04>Zk+iWBZm;U8~(F!*lH@xRQp-sR2IAZSjyr83liu_l#hKT4X5I1J0uJOGz5 zCU`CIQ?<_1F<1z~<6`Oc&R4ZIh^@4XnmZN1`&R7RG;u#vV?AocyoOeOiTs+cvmAwP z+>uJ>FL0? zTAjj?u;mN-Ug0a&kir|;Y9g^IM+62fw<(~Sa;dh}A*-A2%?;MXc}|}TXjJdG{BVH? zsl8yRTJgE~7qR%M(O{VQMK__+)*of|x=YsuR^Sc9#IXVWf?BiM(34cGj)*$G7YOln zFKwp#!Bq8U_y@TrKx%;Qd(B2uS>M=62dTLqiz7mwHYH|r)DLFaGkF!HT&u`u&bcd7 z`DS|VuV`7~(UI5PtSqe1@H$bJt>M_KS*O+f_@E9=y>nS>QEx4Kstt1V?y`>IwPhWZ z5&C$=z3OMNPDy{%?xlDa&dhO3N`@HpkdhJ;V`1l(kYp1RmE@7)mg3=N=j7%PW#QnE z5|v`*784>7`2U9(B;UdkL2$CM|F?|50V<)Z;Dp~k6N&;PC91rtxiJ!=CBXnuE|pLr zK>@H)LHb-&v3hCW31_RSl-!x;au{txxq3@a5S+D*mt%hxY@88(0nx}t` zoc}xZaTLMfws=+HtPwc@`Nx@Be7cy&hzl#@ET;V9mGZ_ Date: Fri, 23 Dec 2022 10:03:53 +0100 Subject: [PATCH 23/27] #4 Conditions --- lib/RPGEngine/Config.hs | 8 ++++++- lib/RPGEngine/Data/Level.hs | 40 ++++++++++++++++++++++------------ lib/RPGEngine/Input/Playing.hs | 2 +- 3 files changed, 34 insertions(+), 16 deletions(-) diff --git a/lib/RPGEngine/Config.hs b/lib/RPGEngine/Config.hs index 66af492..49d0cc7 100644 --- a/lib/RPGEngine/Config.hs +++ b/lib/RPGEngine/Config.hs @@ -45,4 +45,10 @@ assetsFolder = "assets/" -- Location of the level folder containing all levels levelFolder :: FilePath -levelFolder = "levels/" \ No newline at end of file +levelFolder = "levels/" + +------------------------- Game configuration ------------------------- + +-- How many items can a player keep in their inventory? +inventorySize :: Int +inventorySize = 5 \ No newline at end of file diff --git a/lib/RPGEngine/Data/Level.hs b/lib/RPGEngine/Data/Level.hs index f415844..7265e25 100644 --- a/lib/RPGEngine/Data/Level.hs +++ b/lib/RPGEngine/Data/Level.hs @@ -5,8 +5,8 @@ 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)) -import RPGEngine.Config (levelFolder) +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 ------------------------------ @@ -53,21 +53,33 @@ 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 Leave = "Leave" +getActionText (RetrieveItem _) = "Pick up" +getActionText (UseItem _) = "Use item" +getActionText (IncreasePlayerHp _) = "Take a healing potion" +getActionText (DecreaseHp _ used) = "Attack using " ++ used getActionText _ = "ERROR" --- TODO Check conditions -- Filter based on the conditions, keep only the actions of which the -- conditions are met. -filterActions :: [([Condition], Action)] -> [Action] -filterActions [] = [] -filterActions ((conditions, action):others) = action:filterActions others +-- 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 :: Condition -> Bool -meetsCondition InventoryFull = False -- TODO -meetsCondition (InventoryContains id) = True -- TODO -meetsCondition (Not condition) = not $ meetsCondition condition -meetsCondition AlwaysFalse = False \ No newline at end of file +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 \ No newline at end of file diff --git a/lib/RPGEngine/Input/Playing.hs b/lib/RPGEngine/Input/Playing.hs index f2c3f1a..02e70d6 100644 --- a/lib/RPGEngine/Input/Playing.hs +++ b/lib/RPGEngine/Input/Playing.hs @@ -104,7 +104,7 @@ checkForInteraction g = g{ state = Error "something went wrong while checking fo interact :: Game -> Game interact g@Game{ state = s@Playing{ level = level, player = player } } = g{ state = newState } where newState = ActionSelection actionList selector continue - actionList = filterActions $ getActions $ fromJust $ hasAt pos level + actionList = filterActions s $ getActions $ fromJust $ hasAt pos level selector = ListSelector 0 False pos = position player continue = s From b108b2ed656573da7bd78e7ad5704648716a98fc Mon Sep 17 00:00:00 2001 From: Tibo De Peuter Date: Fri, 23 Dec 2022 10:21:56 +0100 Subject: [PATCH 24/27] #4 Pick up items --- lib/RPGEngine/Data/Level.hs | 9 +++++++++ lib/RPGEngine/Input/ActionSelection.hs | 18 ++++++++++++++---- 2 files changed, 23 insertions(+), 4 deletions(-) diff --git a/lib/RPGEngine/Data/Level.hs b/lib/RPGEngine/Data/Level.hs index 7265e25..e3bf9f2 100644 --- a/lib/RPGEngine/Data/Level.hs +++ b/lib/RPGEngine/Data/Level.hs @@ -37,6 +37,15 @@ hasAt pos level = match firstItem firstEntity 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) diff --git a/lib/RPGEngine/Input/ActionSelection.hs b/lib/RPGEngine/Input/ActionSelection.hs index 99e6cfe..d454df9 100644 --- a/lib/RPGEngine/Input/ActionSelection.hs +++ b/lib/RPGEngine/Input/ActionSelection.hs @@ -4,10 +4,11 @@ module RPGEngine.Input.ActionSelection import RPGEngine.Input.Core (InputHandler, handleKey, composeInputHandlers, ListSelector (selection)) -import RPGEngine.Data (Game (..), State (..), Direction (..), Action (..), ItemId, EntityId) +import RPGEngine.Data (Game (..), State (..), Direction (..), Action (..), ItemId, EntityId, Level (..), Player (inventory)) import Graphics.Gloss.Interface.IO.Game (Key(SpecialKey), SpecialKey (KeyUp, KeyDown)) import Graphics.Gloss.Interface.IO.Interact ( SpecialKey(..), KeyState(..) ) +import RPGEngine.Data.Level (getWithId) ------------------------------ Exported ------------------------------ @@ -22,9 +23,10 @@ handleInputActionSelection = composeInputHandlers [ ---------------------------------------------------------------------- selectAction :: Game -> Game -selectAction game@Game{ state = ActionSelection list selection continue } = newGame +selectAction game@Game{ state = ActionSelection list selector continue } = newGame where newGame = game{ state = execute selectedAction continue } - selectedAction = Leave + selectedAction = list !! index + index = selection selector selectAction g = g -- TODO Lift this code from LevelSelection @@ -52,8 +54,16 @@ execute (IncreasePlayerHp iid) s = increasePlayerHp iid 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 _ s = s -- TODO +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 on the player useItem :: ItemId -> State -> State -- TODO From f2844138366b2ca433ca5fdeeadce9b515c84e72 Mon Sep 17 00:00:00 2001 From: Tibo De Peuter Date: Fri, 23 Dec 2022 10:50:42 +0100 Subject: [PATCH 25/27] #4 Increase playerhp --- lib/RPGEngine/Input/ActionSelection.hs | 37 +++++++++++++++++++------- 1 file changed, 27 insertions(+), 10 deletions(-) diff --git a/lib/RPGEngine/Input/ActionSelection.hs b/lib/RPGEngine/Input/ActionSelection.hs index d454df9..0a34015 100644 --- a/lib/RPGEngine/Input/ActionSelection.hs +++ b/lib/RPGEngine/Input/ActionSelection.hs @@ -4,11 +4,12 @@ module RPGEngine.Input.ActionSelection import RPGEngine.Input.Core (InputHandler, handleKey, composeInputHandlers, ListSelector (selection)) -import RPGEngine.Data (Game (..), State (..), Direction (..), Action (..), ItemId, EntityId, Level (..), Player (inventory)) +import RPGEngine.Data (Game (..), State (..), Direction (..), Action (..), ItemId, EntityId, Level (..), Player (inventory, playerHp, Player), Item (..), HP) import Graphics.Gloss.Interface.IO.Game (Key(SpecialKey), SpecialKey (KeyUp, KeyDown)) import Graphics.Gloss.Interface.IO.Interact ( SpecialKey(..), KeyState(..) ) import RPGEngine.Data.Level (getWithId) +import Data.Foldable (find) ------------------------------ Exported ------------------------------ @@ -50,7 +51,8 @@ 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 = increasePlayerHp 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 @@ -58,14 +60,14 @@ execute _ s = s 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 } + 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 on the player +-- Use an item useItem :: ItemId -> State -> State -- TODO useItem _ s = s -- TODO @@ -79,8 +81,23 @@ decreaseHp _ _ s = s -- TODO Break item if durability below zero -- Heal a bit -increasePlayerHp :: ItemId -> State -> State -increasePlayerHp _ s = s --- TODO Increase playerHp --- TODO Decrease durability of item --- TODO Remove item if durability below zero \ No newline at end of file +-- 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 + +-- Change given health by a given amount +changeHealth :: HP -> HP -> HP +changeHealth (Just health) (Just difference) = Just (health + difference) +changeHealth health _ = health \ No newline at end of file From 11eb00ea0b437dc78821c89619365c2dcda1fca8 Mon Sep 17 00:00:00 2001 From: Tibo De Peuter Date: Fri, 23 Dec 2022 12:06:46 +0100 Subject: [PATCH 26/27] #4 Attack --- lib/RPGEngine/Data/Level.hs | 8 +++++- lib/RPGEngine/Input/ActionSelection.hs | 39 ++++++++++++++++++++------ lib/RPGEngine/Parse/TextToStructure.hs | 2 +- src/Main.hs | 2 +- test/Parser/GameSpec.hs | 23 +++++++++------ test/Parser/StructureSpec.hs | 6 +++- 6 files changed, 59 insertions(+), 21 deletions(-) diff --git a/lib/RPGEngine/Data/Level.hs b/lib/RPGEngine/Data/Level.hs index e3bf9f2..875514d 100644 --- a/lib/RPGEngine/Data/Level.hs +++ b/lib/RPGEngine/Data/Level.hs @@ -91,4 +91,10 @@ 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 \ No newline at end of file +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 \ No newline at end of file diff --git a/lib/RPGEngine/Input/ActionSelection.hs b/lib/RPGEngine/Input/ActionSelection.hs index 0a34015..ad7692b 100644 --- a/lib/RPGEngine/Input/ActionSelection.hs +++ b/lib/RPGEngine/Input/ActionSelection.hs @@ -4,11 +4,11 @@ module RPGEngine.Input.ActionSelection 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) +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) +import RPGEngine.Data.Level (getWithId, itemFromInventory) import Data.Foldable (find) ------------------------------ Exported ------------------------------ @@ -72,13 +72,30 @@ useItem :: ItemId -> State -> State -- TODO useItem _ s = s -- TODO -- Attack an entity using an item +-- Should receive a Playing state decreaseHp :: EntityId -> ItemId -> State -> State -decreaseHp _ _ s = s --- TODO DecreaseHp of monster --- TODO Check if monster is dead --- TODO Entity attack player --- TODO Decrease durability of item --- TODO Break item if durability below zero +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 @@ -97,6 +114,12 @@ decreaseDurability item@Item{ useTimes = Nothing } = Just item -- Infinite uses 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) diff --git a/lib/RPGEngine/Parse/TextToStructure.hs b/lib/RPGEngine/Parse/TextToStructure.hs index fa2486e..d3c7ba0 100644 --- a/lib/RPGEngine/Parse/TextToStructure.hs +++ b/lib/RPGEngine/Parse/TextToStructure.hs @@ -139,7 +139,7 @@ action = try $ do let answer | script == "leave" = Leave | script == "retrieveItem" = RetrieveItem arg | script == "useItem" = UseItem arg - | script == "decreaseHp" = DecreaseHp first second + | script == "decreaseHp" = DecreaseHp first (filter (/= ' ') second) -- TODO Work this hack away | script == "increasePlayerHp" = IncreasePlayerHp arg | otherwise = DoNothing (first, ',':second) = break (== ',') arg diff --git a/src/Main.hs b/src/Main.hs index bb69131..0e997a8 100644 --- a/src/Main.hs +++ b/src/Main.hs @@ -1,4 +1,4 @@ -import RPGEngine +import RPGEngine ( playRPGEngine ) ----------------------------- Constants ------------------------------ diff --git a/test/Parser/GameSpec.hs b/test/Parser/GameSpec.hs index c441a22..13ad9e6 100644 --- a/test/Parser/GameSpec.hs +++ b/test/Parser/GameSpec.hs @@ -6,7 +6,6 @@ import RPGEngine.Data import RPGEngine.Parse.Core import RPGEngine.Parse.TextToStructure import RPGEngine.Parse.StructureToGame -import RPGEngine.Parse.TextToStructure (gameFile) spec :: Spec spec = do @@ -40,7 +39,7 @@ spec = 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: [] }" @@ -65,7 +64,7 @@ spec = do } 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 { @@ -95,7 +94,7 @@ spec = do correct = Item { itemId = "dagger", itemX = 0, - itemY = 0, + itemY = 0, itemName = "Dagger", itemDescription = "Basic dagger you found somewhere", itemValue = Just 10, @@ -104,7 +103,7 @@ spec = do } 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 { @@ -122,30 +121,36 @@ spec = do } 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: [] }" diff --git a/test/Parser/StructureSpec.hs b/test/Parser/StructureSpec.hs index b084a02..e4c34f5 100644 --- a/test/Parser/StructureSpec.hs +++ b/test/Parser/StructureSpec.hs @@ -146,10 +146,14 @@ spec = do correct = Right $ Regular $ Action $ UseItem "secondId" parseWith regular input `shouldBe` correct - let input = "decreaseHp(entityId,objectId)" + 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 From c3f7e477033a1a09944f480530660c13005094e3 Mon Sep 17 00:00:00 2001 From: Tibo De Peuter Date: Fri, 23 Dec 2022 13:10:15 +0100 Subject: [PATCH 27/27] #4 Decreasehp --- lib/RPGEngine/Input/ActionSelection.hs | 23 +++++++++++++++++++---- lib/RPGEngine/Input/Playing.hs | 21 ++++++++++++++++++++- 2 files changed, 39 insertions(+), 5 deletions(-) diff --git a/lib/RPGEngine/Input/ActionSelection.hs b/lib/RPGEngine/Input/ActionSelection.hs index ad7692b..d0ed414 100644 --- a/lib/RPGEngine/Input/ActionSelection.hs +++ b/lib/RPGEngine/Input/ActionSelection.hs @@ -68,8 +68,23 @@ pickUpItem id s@Playing{ level = level, player = player } = newState pickUpItem _ _ = Error "Something went wrong while picking up an item" -- Use an item -useItem :: ItemId -> State -> State -- TODO -useItem _ s = s -- TODO +-- 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 @@ -77,7 +92,7 @@ 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) + (Just usingItem) = find ((== iid) . itemId) $ inventory player usedItem = decreaseDurability usingItem newInventory = filter (/= usingItem) $ inventory player newPlayer = player{ inventory = putItemBack usedItem newInventory, playerHp = newHp } @@ -89,7 +104,7 @@ decreaseHp eid iid s@Playing{ level = level, player = player } = newState (Just (Right attackedEntity)) = getWithId eid level newLevel = level{ entities = putEntityBack dealtWithEntity newEntities } newEntities = filter ((/= eid) . entityId) $ entities level - dealtWithEntity = decreaseHealth attackedEntity damageDealAmount + dealtWithEntity = decreaseHealth attackedEntity damageDealAmount putEntityBack Nothing list = list putEntityBack (Just ent) list = ent:list damageGetAmount = inverse (entityValue attackedEntity) diff --git a/lib/RPGEngine/Input/Playing.hs b/lib/RPGEngine/Input/Playing.hs index 02e70d6..8025611 100644 --- a/lib/RPGEngine/Input/Playing.hs +++ b/lib/RPGEngine/Input/Playing.hs @@ -84,7 +84,7 @@ 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) }}} = newGame +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 } @@ -93,6 +93,25 @@ movePlayer dir g@Game{ state = s@Playing{ player = p@Player{ position = (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