aboutsummaryrefslogtreecommitdiff
path: root/src/Game/Entities.hs
blob: 4e5a686ed0c187f098d1e9a5bd3c0e35209c7633 (plain)
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
77
78
79
80
81
82
83
84
85
86
87
88
89
90
91
92
93
94
95
96
97
98
99
100
101
102
103
104
105
106
107
108
109
110
111
112
113
114
115
116
117
118
module Game.Entities (Entities, Entity, mkEntities, updateAll, render, renderVisible, playerPosition) 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"

-- | Return the player's entity position (x, y).
playerPosition :: Entities -> (Int, Int)
playerPosition (Entities _ _ _ entities) =
  (player.x, player.y)
  where
    player = head entities

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, frame = 0} : 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 only visible entities according to the provided viewport.
renderVisible :: SDL.Renderer -> Entities -> M.Viewport -> IO ()
renderVisible renderer (Entities sprites player state entities) v =
  render renderer (Entities sprites player state visible)
  where
    -- FIXME: entities should have size so we can be exact here and
    -- avoid the hardcoded size
    visible = filter (\e -> isVisible v e.x e.y 16 16) entities
    isVisible :: M.Viewport -> Int -> Int -> Int -> Int -> Bool
    isVisible (M.Viewport vx vy vw vh) x y w h =
      x < vx + vw && vx < x + w && y < vy + vh && vy < y + h

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