module Game.Entities (Entities, Entity, mkEntities, updateAll, render) where import Control.Monad import Data.Bits (Bits (..)) import Data.Foldable (find, traverse_) import Data.IORef import Data.List (sort) import qualified Game.Controller as C import Game.Entities.Common import Game.Entities.Const import Game.Entities.Effect import Game.Entities.Pickup import Game.Entities.Player import Game.Entities.Robot import Game.Entities.Slime import Game.Entities.Types import qualified Game.Map as M import qualified Game.Sprites as S import qualified Game.State as GS import qualified SDL mkEntities :: S.SpriteSheet -> M.Map -> IORef C.Controls -> IORef GS.State -> IO Entities mkEntities sprites m controls stateRef = do player <- case find M.isPlayer (M.objects m) of Just (M.PlayerEntity x y) -> mkPlayer sprites x y controls (M.isBlocked m) _ -> error "No player entity in map" playerRef <- newIORef player entities <- traverse (toEntity playerRef) $ sort $ filter (not . M.isPlayer) (M.objects m) -- the entities list has always player first pure $ Entities sprites playerRef stateRef (player : entities) where toEntity :: IORef Entity -> M.Object -> IO Entity toEntity playerRef (M.SlimeEntity x y) = mkSlime sprites x y (collision playerRef 16) (M.isBlocked m) (hitPlayer stateRef) toEntity playerRef (M.RobotEntity x y) = mkRobot sprites x y (collision playerRef 24) (M.isBlocked m) (hitPlayer stateRef) toEntity playerRef (M.BatteryEntity x y) = mkBattery sprites x y (collision playerRef 16) (collectedBattery stateRef) toEntity _ (M.PlayerEntity _ _) = error "Player already processed" processSpawn :: S.SpriteSheet -> Spawn -> IO Entity processSpawn sprites (DustEffectSpawn x y) = mkEffect sprites x y "dust" updateAll :: Entities -> IO Entities updateAll es = do -- update the player first (including the reference) updatedPlayer <- player.update player void $ writeIORef es.player updatedPlayer state <- readIORef es.state -- update hit delay if the player was hit let playerWasHit = state.hitDelay > 0 when playerWasHit (writeIORef es.state state {GS.hitDelay = state.hitDelay - 1}) -- then the other entities updated <- (updatedPlayer :) <$> traverse (updateFilter playerWasHit) others -- collect new entities new <- traverse (processSpawn es.sprites) (concatMap (\e -> e.spawns) updated) -- is the player dead? updated' <- do s <- readIORef es.state pure $ if s.lives == 0 && updatedPlayer.dir /= Dying then (head updated) {dir = Dying, gravity = gravityUp} : tail updated else updated -- clear spawns (new entities), filter out destroyed entities, and add the new ones pure es {entities = map (\e -> e {spawns = []}) (filter (\e -> not e.destroy) updated') ++ new} where player = head es.entities others = tail es.entities -- Update entities skipping enemies if the player was hit. updateFilter :: Bool -> Entity -> IO Entity updateFilter False e = e.update e updateFilter True e | notEnemy e = e.update e | otherwise = pure e notEnemy :: Entity -> Bool notEnemy ent = case ent.typ of TypeEnemy -> False _ -> True render :: SDL.Renderer -> Entities -> IO () render renderer es = do state <- readIORef es.state -- if the player was hit, make the enemies wiggle before unfreezing if state.hitDelay == 0 || state.hitDelay > hitDelay `div` 3 then traverse_ renderOne others else traverse_ (renderWiggling ((.&.) 2 state.hitDelay)) others -- always render player last -- won't draw all the frames if the player was hit if testBit state.hitDelay 2 then pure () else renderOne player where player = head es.entities others = tail es.entities renderWiggling :: Int -> Entity -> IO () renderWiggling m e = case e.typ of TypeEnemy -> renderOne e {x = e.x + m} _ -> renderOne e renderOne :: Entity -> IO () renderOne e = S.render renderer e.sprite e.x e.y set e.frame where set = toSpriteSet e.dir