{-# OPTIONS_GHC -Wno-unused-imports #-}

-- |
-- Module: Components Tutorial
-- Description: Introductory Tutorial
--
-- This module walks the user through creating a small game in the terminal.
--
-- [Previous Chapter: Startup Guide]("Mischief.ECS.Tutorial.Startup")
--
-- [Next Chapter: Common Patterns.]("Mischief.ECS.Tutorial.Patterns")
--
-- [Main Page]("Mischief.ECS")
module Mischief.ECS.Tutorial.Dungeon
  ( -- * Learn You an ECS for Great Mischief! - 2. Coding a Dungeon Game
    -- $intro

    -- * Creating an App
    -- $creation

    -- * Spawning the Grid
    -- $grid

    -- * Traversing the Grid
    -- $traversing

    -- * Spawning the Player
    -- $player

    -- * Adding Walls
    -- $walls

    -- * Displaying the Grid
    -- $display

    -- * Moving the Player
    -- $move

    -- * Receiving Input
    -- $input

    -- * Generating Random Positions
    -- $rand

    -- * Adding Enemies
    -- $enemies

    -- * Moving Enemies
    -- $moveE

    -- * Enemy Collision
    -- $collision

    -- * Health
    -- $health

    -- * Taking Damage
    -- $dmg

    -- * Quitting
    -- $quit

    -- * Spawning Coins
    -- $coins

    -- * Collecting Coins
    -- $collect

    -- * Next Steps
    -- $next

    -- * [Next Chapter: Common Patterns]("Mischief.ECS.Tutorial.Patterns")
  )
where

import Control.Monad (unless, void, when)
import Control.Monad.IO.Class (MonadIO (liftIO))
import Data.Default (Default)
import Data.Foldable (for_)
import Data.List ((!?))
import Data.Traversable (for)
import Mischief.ECS
import System.Exit (exitSuccess)

-- $intro
-- This module will walk you through creating a simple terminal-based dungeon crawler in Mischief.
-- The goal is to have a player which we can freely move on a 2D grid, as well as various objects placed on tiles,
-- such as enemies, coins, obstacles, etc.
--
-- If you'd rather go straight to learning about particular solutions or technical details rather than learning by example, feel free to skip to the next chapters.
-- You can always come back to this one once you have a better understanding of things.
--
-- Each section in this chapter adds its own isolated mechanics to the game, so there's no harm in reading up to a point and taking a break or
-- starting another chapter.

-- $creation
-- Let's start by creating our App and a main Plugin which will serve as the starting point of all our logic.
--
-- @
-- import "Mischief.ECS.Prelude"
--
-- main :: 'IO' ()
-- main = do
--   app <- 'newApp'
--   'addPlugin' \@MainPlugin app
--   'runApp' app
--
-- data MainPlugin
--
-- instance 'Plugin' MainPlugin where
--   init = 'pure' ()
-- @
--
-- You can already run your program and it should work! Although it doesn't do or print anything.

-- $grid
-- The game will play out on a small 2D grid. There are many ways of representing this Mischief, the way I've chosen to do it
-- is by having each tile of the grid be an entity, and the full list of entities stored in a global resource.
--
-- We'll use this @Tile@ component to mark which entities are tiles:
--
-- @
-- data Tile = Tile deriving ('Component')
-- @
--
-- Each tile will also have a @Pos@ component containing it's position on the grid:
--
-- @
-- data Pos = Pos ('Int', 'Int') deriving ('Component')
-- @
--
-- Let's make a resource that stores the list of all tile entities, and call it @Grid@:
--
-- @
-- data Grid = Grid [['Entity']] deriving ('Component')
-- @
--
-- We're also writing these two helper functions returning the width and height of the grid:
--
-- @
-- gridH :: 'Int'
-- gridH = 10
--
-- gridW :: 'Int'
-- gridW = 20
-- @
--
-- Let's write a system that spawns the tiles and initializes the grid resource:
--
-- @
-- spawnGrid :: 'System' ()
-- spawnGrid = do
--   tiles \<-
--     'for' [0 .. gridH - 1] $ \\i -\>
--       'for' [0 .. gridW - 1] $ \\j -\>
--         'spawn' (Tile, Pos (i, j))
--
--   'insertRes' $ Grid tiles
-- @
--
-- Now we just need to modify @MainPlugin@ so that it schedules @spawnGrid@ to happen when the app starts.
--
-- @
-- instance 'Plugin' MainPlugin where
--   init = do
--     'systems' spawnGrid
--       & 'schedule' \@Startup
-- @

-- $traversing
-- Next, we need to code a way for traversing between adjacent tiles. Having a tile entity, we should have easy access to the entities found above, below, to the left and right of it.
--
-- First, I've written this function which gets an entity by position, given the Grid:
--
-- @
-- getTile :: ('Int', 'Int') -> Grid -> 'Maybe' 'Entity'
-- getTile (x, y) (Grid tiles) = do
--   line <- tiles !? x
--   line !? y
-- @
--
-- Now we can write a function that receives the position of a given tile, the grid, and finds tiles offset by a certain amount:
--
-- @
-- moveBy :: ('Int', 'Int') -> Pos -> Grid -> 'Maybe' 'Entity'
-- moveBy (x, y) (Pos (x', y')) = getTile (x' + x, y' + y)
-- @

-- $player
-- Our game needs a player, so we should have a component that uniquely identifies it:
--
-- @
-- data Player = Player deriving ('Component')
-- @
--
-- We should also have a relationship to associate an entity to a tile, teling us that it's currently placed on that tile.
--
-- @
-- data OnTile = OnTile
--
-- instance 'Component' OnTile where
--   type 'IsExclusiveRel' OnTile = 'True'
-- @
--
-- Setting @IsExclusiveRel@ to True on the component instance will make it so there can only be one such relationship on an entity at a time.
-- In our case, this means the player can only be on one tile at a time, and moving them will remove the previous relationship.
--
-- Now it's finally time to spawn our player:
--
-- @
-- spawnPlayer :: 'System' ()
-- spawnPlayer = do
--   'Just' grid \<- res \@Grid
--   let 'Just' tile = getTile (5, 5) grid
--   'void' $ 'spawn' (Player, 'Rel' OnTile tile)
-- @
--
-- @Rel OnTile tile@ inserts the @(OnTile, tile)@ relationship on the player. Tile @(5, 5)@ is just an arbitrary tile I chose to place the player on.
--
-- Now we can schedule the player spawning logic to @Startup@:
--
-- @
-- instance 'Plugin' MainPlugin where
--   init = do
--     'systems' spawnGrid
--       & 'schedule' \@Startup
--
--     'systems' spawnPlayer
--       & 'schedule' \@Startup
-- @
--
-- Except there's something really wrong in the logic above!
-- If we run the app, we will get an error pointing us to the @Just grid <-@ in @spawnPlayer@.
-- We are assuming the grid already exists when that system runs, Which means we want
-- to guarantee that @spawnPlayer@ happens /after/ @spawnGrid@.
--
-- We can do this by providing an explicit order when scheduling:
--
-- @
-- instance 'Plugin' MainPlugin where
--   init = do
--     'systems' spawnGrid
--       & 'schedule' \@Startup
--
--     'systems' spawnPlayer
--       & 'after' spawnGrid
--       & 'schedule' \@Startup
-- @
--
-- The app should now run without issues!

-- $walls
-- Our game will also have Walls which can block the player's movement. We'll make yet another marker component to represent them:
--
-- @
-- data Wall = Wall deriving ('Component')
-- @
--
-- Let's write a system which spawns a wall on a specific position:
--
-- @
-- spawnWall :: ('Int', 'Int') -> 'System' ('Maybe' 'Entity')
-- spawnWall pos = do
--   'Just' grid \<- 'res' \@Grid
--   'for' (getTile pos grid) $ \\tile ->
--     'spawn' (Wall, Rel OnTile tile)
-- @
--
-- And another system that spawns walls on all the tiles at the edge of the grid:
--
-- @
-- spawnWalls :: 'System' ()
-- spawnWalls = do
--   'for_' [0 .. gridW - 1] $ \\i -> spawnWall (0, i)
--   'for_' [0 .. gridW - 1] $ \\i -> spawnWall (gridH - 1, i)
--   'for_' [1 .. gridH - 2] $ \\i -> spawnWall (i, 0)
--   'for_' [1 .. gridH - 2] $ \\i -> spawnWall (i, gridW - 1)
-- @
--
-- The system also needs to be scheduled to run, so @MainPlugin@ now looks like this:
--
-- @
-- instance 'Plugin' MainPlugin where
--   init = do
--     'systems' spawnGrid
--       & 'schedule' \@Startup
--
--     'systems' (spawnPlayer, spawnWalls)
--       & 'after' spawnGrid
--       & 'schedule' \@Startup
-- @
--
-- Additionally, it would be helpful to write a system which checks if a given tile has a wall on it:
--
-- @
-- hasWall :: 'Entity' -> 'System' 'Bool'
-- hasWall tile = 'not' . 'null' \<$\> 'query' ['q'|/With Wall, With OnTile -> tile|]
-- @
--
-- We are querying for all entities which have a @Wall@ component and a @OnTile@ relationship to the given entity, and checking whether the list is not null or not.
--
-- But it would be even more useful to have something similar but for any arbitrary component. We can write a generic variant of it like this:
--
-- @
-- tileHas :: forall c. ('Component' c) => 'Entity' -> 'System' 'Bool'
-- tileHas tile = 'not' . 'null' \<$\> 'query' ['q'|Entity / With (c, OnTile -> tile)|]
-- @
--
-- Make sure to add @{-# LANGUAGE AllowAmbiguousTypes #-}@ at the top of your .hs file, otherwise the type checker will not like that @c@ can not be inferred from the
-- function's input.
--
-- We can now write @hasWall@ as just:
--
-- @
-- hasWall :: 'Entity' -> 'System' 'Bool'
-- hasWall = tileHas \@Wall
-- @

-- $display
-- It's finally time to display our game's grid in the terminal!
--
-- Let's start by writing a system that receives a tile and returns a character to represent it based on what's placed on it:
--
-- @
-- showTile :: 'Entity' -> 'System' 'Char'
-- showTile tile = do
--   player <- tileHas \@Player tile
--   wall <- tileHas \@Wall tile
--
--   'pure' $
--     if
--       | player -> \'@\'
--       | wall -> \'#\'
--       | otherwise -> \'.\'
-- @
--
-- This requires the @MultiWayIf@ language extension, but there are many alternate ways to write it; it's just a personal preference.
--
-- Next, let's write a system which produces a String to represent the whole grid by calling the previous function on each tile:
--
-- @
-- showGrid :: 'System' 'String'
-- showGrid = do
--   'Just' (Grid tiles) <- 'res' \@Grid
--   lines <- 'for' tiles $ 'traverse' showTile
--   'pure' $ 'unlines' lines
-- @
--
-- All that's left is to write a system that prints the string, and schedule it to happen each frame. We'll do the actual
-- printing via the @printClear@ function of "Mischief.ECS.Stdout", which automatically clears the terminal, after each print.
--
-- @
-- printGrid :: 'System' ()
-- printGrid = 'Mischief.ECS.Stdout.printClear' '=<<' showGrid
-- @
--
-- And we need to schedule if, of course.
--
-- @
-- instance 'Plugin' MainPlugin where
--   init = do
--     Stdin.'Mischief.ECS.Stdin.init'
--
--     'systems' spawnGrid
--       & 'schedule' \@Startup
--
--     'systems' (spawnPlayer, spawnWalls)
--       & 'after' spawnGrid
--       & 'schedule' \@Startup
--
--     'systems' printGrid
--       & 'schedule' \@Update
-- @
--
-- If you run the app now, you should see the game's grid in your terminal!
--
-- @
-- ####################
-- \#..................#
-- \#..................#
-- \#..................#
-- \#..................#
-- \#....\@.............#
-- \#..................#
-- \#..................#
-- \#..................#
-- ####################
-- @

-- $move
-- Next, we should write a system which moves the player from one tile to another.
--
-- First, let's write the actual logic for moving in a certain direction:
--
-- @
-- movePlayerBy :: ('Int', 'Int') -> 'System' ()
-- movePlayerBy dir = do
--   'Just' grid <- 'res' \@Grid
--   ['q'|OnTile -> (Pos) / With Player|]
--     & 'qmapMaybe' (\\pos -> moveBy dir pos.comp grid)
--     & 'qfilterM' (\\_ newTile -> 'not' \<$\> hasWall newTile)
--     & 'qinsert' (\\newTile -> 'Rel' OnTile newTile)
--     & 'query_'
-- @
--
-- @qmapMaybe@ /tries/ to get the new tile, after which we run a filter to check if we the tile has a wall on it. Finally we @qmap@ the tile to the new relationship.
-- As you've seen before, @qmap@ automatically runs an insertion of the returned components.

-- $input
-- We also need to somehow get input from the user. Mischief exposes some useful functions for this in the following module:
--
-- @
-- import "Mischief.ECS.Stdin" qualified as Stdin
-- @
--
-- These functions are useful for the purpose of this tutorial but should probably never be used in a released game. Instead, you should import a dedicated
-- haskell input library, or use @mischief-input@ which is based on SDL.
--
-- In order to read input we'll need to add @Stdin.init@ to a plugin, which you'll see a bit later.
--
-- For now, we can just use the @Stdin.readLast@ to empty the input buffer and get the last character typed by the user, if any.
--
-- @
-- movePlayer :: 'System' ()
-- movePlayer = do
--   c <- Stdin.'Mischief.ECS.Stdin.readLast'
--   'for_' c $ \\case
--     \'w\' -> movePlayerBy (-1, 0)
--     \'s\' -> movePlayerBy (1, 0)
--     \'a\' -> movePlayerBy (0, -1)
--     \'d\' -> movePlayerBy (0, 1)
--     _ -> 'pure' ()
-- @
--
-- The @for_@ just \'iterates\' over the @Maybe@, applying the function if it has a value, and doing nothing otherwise.
--
-- At this point the codebase is starting to grow, so I have decided to create a new @PlayerPlugin@ which handles all the player logic (including the new movement system),
-- and make it a dependency of @MainPlugin@:
--
-- @
-- data MainPlugin
--
-- instance 'Plugin' MainPlugin where
--   init = do
--     Stdin.'Mischief.ECS.Stdin.init'
--
--     'systems' spawnGrid
--       & 'schedule' \@Startup
--
--     'systems' spawnWalls
--       & 'after' spawnGrid
--       & 'schedule' \@Startup
--
--     'systems' printGrid
--       & 'schedule' \@Update
--
--   deps = ['dep' \@PlayerPlugin]
--
-- data PlayerPlugin
--
-- instance 'Plugin' PlayerPlugin where
--   init = do
--     'systems' spawnPlayer
--       & 'after' spawnGrid
--       & 'schedule' \@Startup
--
--     'systems' movePlayer
--       & 'schedule' \@Update
-- @
--
-- You should now be able to move the player around when running the game!

-- $rand
-- For some of the next sections, an ability to choose random tiles would be very useful. So let's work on that.
--
-- I've chosen to use the @random@ package, so just add it as a dependency to your project and import it:
--
-- @
-- import System.Random
-- import System.Random.Stateful
-- @
--
-- We need some sort of mutable generator, so I'll create a resource to hold it:
--
-- @
-- data Rand = Rand (IOGenM StdGen) deriving ('Component')
--
-- newGen :: 'System' Rand
-- newGen = Rand \<$\> (newIOGenM =<< initStdGen)
-- @
--
-- Don't forget to insert the resource!
--
-- @
-- instance 'Plugin' MainPlugin where
--   init = do
--     Stdin.'Mischief.ECS.Stdin.init'
--
--     'systems' spawnGrid
--       & 'schedule' \@Startup
--
--     'systems' spawnWalls
--       & 'after' spawnGrid
--       & 'schedule' \@Startup
--
--     'systems' printGrid
--       & 'schedule' \@Update
--
--     'insertRes' =<< newGen
-- @
--
--
-- Now it's possible to write a system that generates a random position on the grid:
--
-- @
-- randomPos :: 'System' ('Int', 'Int')
-- randomPos = do
--   'Just' (Rand gen) <- res @Rand
--   i <- applyIOGen (uniformR (0, gridH - 1)) gen
--   j <- applyIOGen (uniformR (0, gridW - 1)) gen
--   return (i, j)
-- @
--
-- And a system that uses it to get the Entity of a random tile:
--
-- @
-- randomTile :: 'System' 'Entity'
-- randomTile = do
--   'Just' grid <- 'res' \@Grid
--   randomPos <- randomPos
--
--   'pure' . 'unwrap' $ getTile randomPos grid
-- @
--
-- @unwrap@ is a utility function provided by Mischief that just grabs the value out of a @Maybe@, or panics if there is no value. In this case,
-- we know there will be a value since the provided position is valid.

-- $enemies
-- It would be a pretty boring game if there were no obstacles. For that reason, we're going to add some enemies.
--
-- Here's the marker component that will be used to identify them:
--
-- @
-- data Enemy = Enemy deriving ('Component')
-- @
--
-- I'll use this system to spawn an enemy, using the @randomTile@ function defined earlier:
--
-- @
-- spawnEnemy :: 'System' Entity
-- spawnEnemy = do
--   tile <- randomTile
--   'spawn' (Enemy, 'Rel' OnTile tile)
-- @
--
-- And this as a driver to handle all enemy spawning (it just spawns 5 enemies):
--
-- @
-- spawnEnemies :: 'System' ()
-- spawnEnemies = 'for_' [0 .. 4] $ 'const' spawnEnemy
-- @
--
-- I've also modified the @showTile@ system to take enemies into account:
--
-- @
--
-- showTile :: 'Entity' -> 'System' 'Char'
-- showTile tile = do
--   player <- tileHas \@Player tile
--   enemy <- tileHas \@Enemy tile
--   wall <- tileHas \@Wall tile
--
--   'pure' $
--     if
--       | player -> \'@\'
--       | wall -> \'#\'
--       | enemy -> \'!\'
--       | otherwise -> \'.\'
-- @
--
-- Finally, we need a to schedule the enemy spawning, so I've created a new @EnemyPlugin@:
--
-- @
-- data EnemyPlugin
--
-- instance 'Plugin' EnemyPlugin where
--   init = do
--     'systems' spawnEnemies
--       & 'after' spawnGrid
--       & 'schedule' \@Startup
-- @
--
-- And added it to the list of plugins added by @MainPlugin@:
--
-- @
-- instance 'Plugin' MainPlugin where
--   init = ...
--
--   deps = ['dep' \@PlayerPlugin, 'dep' \@EnemyPlugin]
-- @
--
-- You should now see something like this when running the app:
--
-- @
-- ####################
-- \#..................#
-- \#..!...............#
-- \#....!.!...........#
-- \#..................#
-- \#....\@.............#
-- \#..................#
-- \#..................#
-- \#........!.!.......#
-- ####################
-- @

-- $moveE
-- Right now the enemies just sit there. Let's make them move!
--
-- Fist, I've written this helper @System@ that receives an enemy's position, the player's position, and decides the direction the enemy will move in.
-- (yes, it technically could be a pure function but you'll see why it's a System a bit later).
--
-- @
-- decideEnemyDir :: Pos -> Pos -> 'System' ('Int', 'Int')
-- decideEnemyDir (Pos (ex, ey)) (Pos (px, py)) = do
--   'pure' $
--     if
--       \| ex \> px -\> (-1, 0)
--       \| ey \> py -\> (0, -1)
--       \| ex \< px -\> (1, 0)
--       \| ey \< py -\> (0, 1)
--       \| otherwise -\> (0, 0)
-- @
--
-- Then, I wrote this system that grabs the player's position and then iterates over all enemies to move them:
--
-- @
-- moveEnemies :: 'System' ()
-- moveEnemies = do
--   'Just' ('From' _ playerPos) \<- 'single' ['q'|OnTile -\> (Pos) / With Player|]
--   'Just' grid <- 'res' \@Grid
--
--   ['q'|OnTile -> (Pos) / With Enemy|]
--     & 'qtraverse' (\\_ pos -> (,pos) \<$\> decideEnemyDir pos.comp playerPos)
--     & 'qmapMaybe' (\\(diff, pos) -\> moveBy diff pos.comp grid)
--     & 'qinsert' ('Rel' OnTile)
--     & 'query_'
-- @
--
-- If the lambdas get overwhelming you can always create intermediary functions that work on queries:
--
-- @
-- qdecideEnemyTile :: Grid -> Pos -> 'Query' 'System' ('From' Pos) -> 'Query' 'System' 'Entity'
-- qdecideEnemyTile grid playerPos x =
--   x
--     & 'qtraverse' (\\_ pos -> (,pos) \<$\> decideEnemyDir pos.comp playerPos)
--     & 'qmapMaybe' (\\(diff, pos) -> moveBy diff pos.comp grid)
-- @
--
-- So our system is now just:
--
-- @
-- moveEnemies :: 'System' ()
-- moveEnemies = do
--   'Just' ('From' _ playerPos) \<- 'single' ['q'|OnTile -\> (Pos) / With Player|]
--   'Just' grid <- 'res' \@Grid
--
--   ['q'|OnTile -\> (Pos) / With Enemy|]
--     & qdecideEnemyTile grid playerPos
--     & 'qinsert' ('Rel' OnTile)
--     & 'query_'
-- @
--
-- And now to schedule our enemy movement:
--
-- @
-- data EnemyPlugin
--
-- instance 'Plugin' EnemyPlugin where
--   init = do
--     'systems' spawnEnemies
--       & 'after' spawnGrid
--       & 'schedule' \@Startup
--
--     'systems' moveEnemies
--       & 'schedule' \@Update
-- @
--
-- Except there's a small problem. If you run the app now, you may notice you don't see any enemies!
-- That's because they all already got to the player and are hiding behind it! We've set @moveEnemies@
-- to happen every frame, and our frames are happening almost instantly. So we need to add some sort of timing to the enemy's movement.
--
-- There are many cleaner high-level solutions to fix this, some of them even using async systems, but instead, I'll take the opportunity to introduce you to an important notion, Time!
--
-- In order to use @Time@ utilities, you need to add the @'TimePlugin'@ to your app, so I'll add it to our @MainPlugin@:
--
-- @
-- deps = ['dep' \@PlayerPlugin, 'dep' \@EnemyPlugin, 'dep' \@TimePlugin]
-- @
--
-- We'll now import the @Time@ module which contains various utlities for keeping track of time, including @Time.delta@ which returns the time, in seconds,
-- that has passed between frames.
--
-- @
-- import "Mischief.ECS.Time" qualified as Time
-- @
--
-- Mischief also provides a handy way of keeping track of time via the @Timer@. This module contains functions to work with it.
--
-- @
-- import "Mischief.ECS.Timer" qualified as Timer
-- @
--
-- A Timer is an object that can be ticked down each frame, to trigger certain conditions only once a cerain amount of time has passed.
--
-- Let's create a @Cooldown@ component which stores a Timer.
--
-- @
-- data Cooldown = Cooldown {timer :: 'Mischief.ECS.Timer.Timer'} deriving ('Component')
-- @
--
-- We want this to always be on every Enemy, so we can make it a required component of the @Enemy@ component.
--
-- @
-- instance 'Component' Enemy where
--   'required' = 'require' \@Cooldown
-- @
--
-- In order to have that compile, we also need to provide a @Default@ instance for @Cooldown@:
--
-- @
-- instance 'Default' Cooldown where
--   'def' = Cooldown $ Timer.'Mischief.ECS.Timer.new' 1 Timer.'Mischief.ECS.Timer.Repeat'
-- @
--
-- @Timer.new@ takes a Float (the duration of the timer), and a @Mode@ which is either @Repeat@ or @Once@.
--
-- @Cooldown@ should now automatically be added on every enemy with the default value.
--
-- We can use the @Timer.tick@ function to advance the state of a timer. It returns the new state, along with a Bool that tells us whether the timer has just finished or not.
--
-- All that's left is to put all of this together. I've written this intermediary function to use with queries that will filter the query so that each enemy can only attack when their cooldown has just finished:
--
-- @
-- qfilterCooldown :: 'Query' 'System' a -> 'Query' 'System' a
-- qfilterCooldown x = do
--   x
--     & 'qextend' \['qd'|Cooldown|] (,)
--     & 'qfilterM'
--       (\\entity (_, Cooldown timer) -> do
--           delta <- Time.'Mischief.ECS.Time.delta'
--           let (timer', justFinished) = Timer.'Mischief.ECS.Timer.tick' delta timer
--           'insert' (Cooldown timer') entity
--           'pure' justFinished
--       )
--     & 'qmap' 'fst'
-- @
--
-- First we use @qextend@ to also query for the @Cooldown@ of the current entity. Then we run a small impure filter that gets the delta,
-- updates the Timer, modifies the value of Cooldown by re-inserting it on the entity, and filters based on whether the timer had just finished or not.
--
-- Notice that this time we're using @qd@ instead of @q@. That's because @qextend@ expects a /query data/.
--
-- We also use a final @qmap@ to get the query back to its original data. This makes it extremely generic!
--
-- Here's the new @moveEnemies@ function, with the timer-based filter:
--
-- @
-- moveEnemies :: 'System' ()
-- moveEnemies = do
--   'Just' ('From' _ playerPos) \<- 'single' ['q'|OnTile -\> (Pos) / With Player|]
--   'Just' grid <- 'res' \@Grid
--
--   ['q'|OnTile -\> (Pos) / With Enemy|]
--     & qfilterCooldown
--     & qdecideEnemyTile grid playerPos
--     & 'qinsert' ('Rel' OnTile)
--     & 'query_'
-- @
--
-- You can now run the app and see the enemies chasing you!

-- $collision
-- Right now the enemies just kind of overlap each other and go under the player. We can fix that by preventing them to move.
--
-- Now, let's write a function that tells us whether a certain tile is free to move on or not:
--
-- @
-- tileIsFree :: 'Entity' -> 'System' Bool
-- tileIsFree tile = do
--   wall <- tileHas \@Wall tile
--   enemy <- tileHas \@Enemy tile
--   player <- tileHas \@Player tile
--   pure $ not (wall || enemy || player)
-- @
--
-- Plus, an extra one which takes the tile's position directly rather than the entity:
--
-- @
-- tileAtPosIsFree :: ('Int', 'Int') -> 'System' 'Bool'
-- tileAtPosIsFree pos = do
--   'Just' grid \<- 'res' \@Grid
--   'maybe' ('pure' False) tileIsFree (getTile pos grid)
-- @
--
-- And let's integrate it into the system which decides the enemy's movement direction:
--
-- @
-- decideEnemyDir :: Pos -> Pos -> 'System' ('Int', 'Int')
-- decideEnemyDir (Pos (ex, ey)) (Pos (px, py)) = do
--   left <- tileAtPosIsFree (ex - 1, ey)
--   up <- tileAtPosIsFree (ex, ey - 1)
--   right <- tileAtPosIsFree (ex + 1, ey)
--   down <- tileAtPosIsFree (ex, ey + 1)
--
--   'pure' $
--     if
--       | ex \> px && left -\> (-1, 0)
--       | ey \> py && up -\> (0, -1)
--       | ex \< px && right -\> (1, 0)
--       | ey \< py && down -\> (0, 1)
--       | otherwise -> (0, 0)
-- @
--
-- (There are definitely /much/ better ways to write this, feel free to experiment and make it cleaner at home!)
--
-- I've also replaced the @hasWall@ in the @movePlayerBy@ function with @tileIsFree@, so the player can collide with enemies as well.
--
-- @
-- movePlayerBy :: ('Int', 'Int') -> 'System' ()
-- movePlayerBy dir = do
--   'Just' grid \<- 'res' \@Grid
--   ['q'|OnTile -\> (Pos) / With Player|]
--     & 'qmapMaybe' (\\pos -> moveBy dir pos.comp grid)
--     & 'qfilterM' ('const' tileIsFree)
--     & 'qinsert' ('Rel' OnTile)
--     & 'query_'
-- @
--
-- The game should now have fully working collision and feel much more solid!

-- $health
-- Here's a simple one: let's add a @Health@ component to the player and have it be displayed under the grid each frame.
--
-- I'll also give it a @Default@ instance so it can be required by the @Player@ component.
--
-- @
-- data Health = Health {hp :: 'Int'} deriving ('Component')
--
-- instance 'Default' Health where
--   'def' = Health 100
-- @
--
-- @
-- instance 'Component' Player where
--   'required' = 'require' \@Health
-- @
--
-- I've written a system that returns a string for the health:
--
-- @
-- showHealth :: 'System' 'String'
-- showHealth = do
--   'Just' health <- 'single' ['q'|Health / With Player|]
--   'pure' $ "Health: " ++ 'show' health.hp
-- @
--
-- And I added it to the printing system:
--
-- @
-- printGrid :: 'System' ()
-- printGrid = do
--   grid <- showGrid
--   health <- showHealth
--   'Mischief.ECS.Stdout.printClear' $ health ++ \"\\n\" ++ grid
-- @
--
-- Your game should now print the health at the top:
--
-- @
-- Health: 100
-- ####################
-- \#............!.....#
-- \#.....!............#
-- \#..................#
-- \#..................#
-- \#....\@!......!..!..#
-- \#..................#
-- \#..................#
-- \#..................#
-- ####################
-- @

-- $dmg
-- Let's add behavior for enemies damaging the player. I'll also take this opportunity to introduce you to Events.
--
-- We can create a damage event like this:
--
-- @
-- data Damage = Damage {amount :: 'Int'} deriving ('Event')
-- @
--
-- We also need an observer system for it:
--
-- @
-- onDamage :: Damage -> 'System' ()
-- onDamage dmg = do
--   ['q'|Health / With Player|]
--     & 'qinsert' (\\(Health x) -> Health $ 'max' (x - dmg.amount) 0)
--     & 'query_'
-- @
--
-- We also need to spawn an observer that listens to the world and triggers the event. I'll do so from the player plugin:
--
-- @
-- instance 'Plugin' PlayerPlugin where
--   init = do
--     'systems' spawnPlayer
--       & 'after' spawnGrid
--       & 'schedule' \@Startup
--
--     'systems' movePlayer
--       & 'schedule' \@Update
--
--     'void' $ 'spawn' ('Observer' onDamage)
-- @
--
-- The last thing we need is a way for enemies to trigger the event. Let us make a system which checks if an enemy is adjacent to the player and triggers the event:
--
-- @
-- tryDamage :: 'System' ()
-- tryDamage = do
--   adjacentEnemies <-
--     ['q'|OnTile -\> (Pos) / With Player|]
--       & 'qjoin' (\\pos -\> ['q'|OnTile -\> (Pos) / With Enemy|] & 'qfilter' (\\a -> isAdjacent a.comp pos.comp)) (,)
--       & 'query'
--
--   'unless' ('null' adjacentEnemies) $ do
--     'trigger' $ Damage 5
-- @
--
-- With this helper function:
--
-- @
-- isAdjacent :: Pos -> Pos -> 'Bool'
-- isAdjacent (Pos (x1, y1)) (Pos (x2, y2)) =
--   let dx = abs (x1 - x2)
--       dy = abs (y1 - y2)
--    in (dx == 1 && dy == 0) || (dx == 0 && dy == 1)
-- @
--
-- The @qjoin@ function maps each of our positions into an entirely new query, filtering the entities of that query
-- based on the position. This is actually a less powerful bind operation, as we'll see in the next chapter.
--
-- I've scheduled @tryDamage@ to happen every frame, after both the player and enemies have moved:
--
-- @
-- instance 'Plugin' EnemyPlugin where
--   init = do
--     'systems' spawnEnemies
--       & 'after' spawnGrid
--       & 'schedule' \@Startup
--
--     'systems' moveEnemies
--       & 'schedule' \@Update
--
--     'systems' tryDamage
--       & 'after' moveEnemies
--       & 'after' movePlayer
--       & 'schedule' \@Update
-- @
--
-- This now /technically/ works, except that the enemies almost instantly defeat the player on contact. That's because they deal damage every frame,
-- the same issue we had when they were moving each frame.
--
-- I'll show you a new way to solve this problem. We'll add an @Invincible@ component on the player after being hit once,
-- which causes it to not receive damage, and which is removed after a delay.
--
-- @
-- data Invincible = Invincible deriving ('Component')
-- @
--
-- I'll modify the @onDamage@ observer like so:
--
-- @
-- onDamage :: Damage -> 'System' ()
-- onDamage dmg = do
--   ['q'|Health / With Player, Without Invincible|]
--     & 'qinsert' (\\(Health x) -> (Health $ 'max' (x - dmg.amount) 0, Invincible))
--     & 'qtap' (\\e _ -> 'delay' 1000000 $ 'remove' ('C' \@Invincible) e)
--     & 'query_'
-- @
--
-- We are now only querying the player only if they  are not invincible. We are then inserting the @Invincible@ components on them, and
-- removing it after a fixed delay of 1 second. @qtap@ is the function used to apply an arbitrary side effect over the query's elements.
--
-- Now, the player will only be able to take damage once per second!

-- $quit
-- It feels weird that the player can reach 0 health but the game just keeps running. So I've added an
-- extra instruction into our @qtap@ exits the program when the hp reaches 0.
--
-- @
-- onDamage :: Damage -> System ()
-- onDamage dmg = do
--   ['q'|Health / With Player, Without Invincible|]
--     & 'qinsert' (\\(Health x) -> (Health $ 'max' (x - dmg.amount) 0, Invincible))
--     & 'qtap'
--       ( \\e (Health hp) -> do
--           'delay' 1000000 $ 'remove' ('C' \@Invincible) e
--           'when' (hp == 0) $ 'liftIO' 'exitSuccess'
--       )
--     & 'query_'
-- @
--
-- But you may notice, the player actually takes an extra hit before that condition is triggered. That's because the result of the function
-- from @qinsert@ is not actually propagated further in the query. If you look at the signature of @qinsert@ you'll notice it actually ends in:
-- @Query m a -> Query m a@. We can instead use the @qmodify@ function which maps the values, inserts them, and propagates them:
--
-- @
-- onDamage :: Damage -> System ()
-- onDamage dmg = do
--   ['q'|Health / With Player, Without Invincible|]
--     & 'qmodify' (\\(Health x) -> (Health $ 'max' (x - dmg.amount) 0, Invincible))
--     & 'qtap'
--       ( \\e (Health hp, _) -> do
--           'delay' 1000000 $ 'remove' ('C' \@Invincible) e
--           'when' (hp == 0) $ 'liftIO' 'exitSuccess'
--       )
--     & 'query_'
-- @
--
-- The logic should now work as expected.

-- $coins
-- Now, for our last bit of logic, we should add a reason for the player to not die. Let's spawn a bunch of coins!
--
-- First, we need a marker component for the coins. Each containing a value:
--
-- @
-- data Coin = Coin 'Int' deriving ('Component')
-- @
--
-- Second, here's a system that spawns a coin on a random free tile:
--
-- @
-- spawnCoin :: 'System' ()
-- spawnCoin = do
--   tile <- randomTile
--   free <- tileIsFree tile
--   if free
--     then
--       'void' $ 'spawn' (Coin 5, 'Rel' OnTile tile)
--     else
--       spawnCoin
-- @
--
-- (If the tile is not free, it will just keep looping and generating tiles until it finds one that is).
--
-- Third, we can use intervals to make the system repeat every two seconds. They're a convenient utility provided by Mischief.
--
-- @
-- import "Mischief.ECS.Interval" qualified as Interval
-- @
--
-- @
-- instance 'Plugin' MainPlugin where
--   init = do
--     Stdin.'Mischief.ECS.Stdin.init'
--
--     'systems' spawnGrid
--       & 'schedule' \@Startup
--
--     'systems' spawnWalls
--       & 'after' spawnGrid
--       & 'schedule' \@Startup
--
--     'systems' printGrid
--       & 'schedule' \@Update
--
--     interval <- Interval.'Mischief.ECS.Interval.start' 2000000 spawnCoin
--
--     'insertRes' =<< newGen
-- @
--
-- You can also use @Interval.stop@ on the returned value to stop the interval at any point, but I won't be doing that here.
--
-- Finally, we should display the coins:
--
-- @
-- showTile :: 'Entity' -> 'System' 'Char'
-- showTile tile = do
--   player <- tileHas \@Player tile
--   enemy <- tileHas \@Enemy tile
--   wall <- tileHas \@Wall tile
--   coin <- tileHas \@Coin tile
--
--   'pure' $
--     if
--       | player -> \'@\'
--       | wall -> \'#\'
--       | enemy -> \'!\'
--       | coin -> \'$\'
--       | otherwise -> \'.\'
-- @

-- $collect
-- All that's left is letting the player collect coins and keeping track of how many they have collected.
--
-- Let's make another component which will be held on the player, containing the total amount of coins they've collected:
--
-- @
-- data Coins = Coins 'Int' deriving ('Component', 'Generic', 'Default')
-- @
--
-- And make it another required component of @Player@:
--
-- @
-- instance 'Component' Player where
--   required = 'require' \@(Health, Coins)
-- @
--
-- And then update the display to also show the number of coins:
--
-- @
-- showCoins :: 'System' 'String'
-- showCoins = do
--   'Just' (Coins c) <- 'single' $ ['q'|Coins|]
--   'pure' $ \"Coins: \" ++ show c
-- @
--
-- @
-- printGrid :: 'System' ()
-- printGrid = do
--   grid <- showGrid
--   health <- showHealth
--   coins <- showCoins
--   'Mischief.ECS.Stdout.printClear' $ health ++ \"\\n\" ++ grid ++ \"\\n\" ++ coins ++ \"\\n\"
-- @
--
-- And now finally, a system that checks if there are any coins on the same tile as the player, despawns them, and increments the resource:
--
-- @
-- collectCoins :: 'System' ()
-- collectCoins = do
--   players \<- 'query' ['q'|Entity, OnTile -\> (Entity), Coins / With Player|]
--   'for_' players $ \\(player, 'From' _ tile, Coins coins) -> do
--     collected <-
--       ['q'|Coin / With OnTile -> tile|]
--         & 'qtap' (\\e _ -> despawn e)
--         & 'query'
--
--     'insert' (Coins $ 'foldr' (\\(Coin x) -> (+ x)) coins collected) player
-- @
--
-- We iterate over all players, then query all coins that share the same tile as the current player, while despawning them. We
-- then do a @foldr@ to sum the value of all coins over the @Coins@ of the player and re-insert it. There are much cleaner,
-- less imperative, ways of writing this, some of which will be covered in the next chapter.
--
-- Now to schedule it:
--
-- @
-- instance 'Plugin' PlayerPlugin where
--   init = do
--     'systems' spawnPlayer
--       & 'after' spawnGrid
--       & 'schedule' \@Startup
--
--     'systems' movePlayer
--       & 'schedule' \@Update
--
--     'systems' collectCoins
--       & 'after' movePlayer
--       & 'schedule' \@Update
--
--     'void' $ 'spawn' ('Observer' onDamage)
-- @
--
-- And that's it! Out player should now be able to collect coins!
--
-- @
-- Health: 5
-- ####################
-- \#..................#
-- \#..................#
-- \#$....!!!!!\@...$...#
-- \#$.................#
-- \#..................#
-- \#......$...........#
-- \#.$................#
-- \#..................#
-- ####################
-- Coins: 16
-- @

-- $next
-- Don't worry if there are various details that you haven't fully understood yet. The next chapters will go into detail over the many aspects of the ECS. This chapter was just meant
-- to give you a general idea of working with Mischief. You can find the whole code for this example [here](https://github.com/PVDoriginal/mischief/blob/main/mischief-ecs/examples/Dungeon.hs).