packages feed

nyx-game-0.1.0.0: app/Enemy.hs

{-# LANGUAGE FunctionalDependencies #-}
{-# LANGUAGE MultiParamTypeClasses  #-}
{-# LANGUAGE DuplicateRecordFields  #-}
{-# LANGUAGE FlexibleInstances  #-}
{-# LANGUAGE TemplateHaskell #-}
{-# LANGUAGE TypeApplications #-}

module Enemy where

import qualified SDL

import Data.Maybe
import Play.Engine
import Control.Lens hiding (parts)
import Control.DeepSeq
import qualified Control.Monad.State as SM
import qualified Data.DList as DL

import Bullet
import qualified Attack as A
import qualified DecorationObject as DO
import qualified DecObj.SplitTexture as ST
import qualified Play.Engine.Movement as MV
import qualified Play.Engine.Sprite as Spr



data Enemy
  = Enemy
  { _pos :: {-# UNPACK #-} !IPoint
  , _size :: {-# UNPACK #-} !Size
  , _sprite :: {-# UNPACK #-} !Spr.Sprite
  , _hitbox :: {-# UNPACK #-} !Hitbox
  , _movement :: {-# UNPACK #-} !MV.Movement
  , _direction :: {-# UNPACK #-} !FPoint
  , _directionChanger :: Size -> Enemy -> FPoint
  , _attack :: {-# UNPACK #-} !A.Attack
  , _attackId :: {-# UNPACK #-} !Int
  , _attackChanger :: Enemy -> Int -> Maybe A.Attack
  , _health :: {-# UNPACK #-} !Int
  , _deathTime :: {-# UNPACK #-} !Int
  , _deathParts :: {-# UNPACK #-} !IPoint
  , _deathTexture :: {-# UNPACK #-} !SDL.Texture
  , _timers :: {-# UNPACK #-} !EnemyTimers
  }

data EnemyTimers
  = EnemyTimers
  { _hitTimer :: {-# UNPACK #-} !Int
  , _gracePeriodTimer :: {-# UNPACK #-} !Int
  }


instance NFData Enemy where
  rnf (Enemy {_pos, _size, _movement, _health, _timers}) =
    rnf _pos
    `seq` rnf _size
    `seq` rnf _movement
    `seq` rnf _timers
    `seq` rnf _health
    `seq` rnf _attack
    `seq` rnf _deathTime
    `seq` rnf _deathParts

instance NFData EnemyTimers where
  rnf (EnemyTimers {_hitTimer}) =
    rnf _hitTimer
    `seq` rnf _gracePeriodTimer


makeFieldsNoPrefix ''Enemy
makeFieldsNoPrefix ''EnemyTimers

instance Eq Enemy where
  e1 == e2 =
    e1 ^. pos == e2 ^. pos
    && e1 ^. size == e2 ^. size

instance Ord Enemy where
  e1 <= e2 =
    e1 ^. pos <= e2 ^. pos
    && e1 ^. size <= e2 ^. size


data MakeEnemy
  = MakeEnemy
  { mkePos :: {-# UNPACK #-} !IPoint
  , mkeSize :: {-# UNPACK #-} !Size
  , mkeSprite :: {-# UNPACK #-} !Spr.Sprite
  , mkeHitbox :: {-# UNPACK #-} !Hitbox
  , mkeMov :: {-# UNPACK #-} !MV.Movement
  , mkeHealth :: {-# UNPACK #-} !Int
  , mkeDirChanger :: Size -> Enemy -> FPoint
  , mkeAtk :: {-# UNPACK #-} !A.Attack
  , mkeAtkChanger :: Enemy -> Int -> Maybe A.Attack
  , mkeDeathTime :: {-# UNPACK #-} !Int
  , mkeDeathParts :: {-# UNPACK #-} !IPoint
  , mkeDeathTexture :: {-# UNPACK #-} !SDL.Texture
  }

mkEnemy :: MakeEnemy -> Enemy
mkEnemy MakeEnemy{..} =
  Enemy
    { _pos = mkePos
    , _size = mkeSize
    , _sprite = mkeSprite
    , _hitbox = mkeHitbox
    , _direction = Point 0 0
    , _movement = mkeMov
    , _directionChanger = mkeDirChanger
    , _attack = mkeAtk
    , _attackId = 0
    , _attackChanger = mkeAtkChanger
    , _health = mkeHealth
    , _deathTime = mkeDeathTime
    , _deathParts = mkeDeathParts
    , _deathTexture = mkeDeathTexture
    , _timers = initEnemyTimers
    }

initEnemyTimers :: EnemyTimers
initEnemyTimers = EnemyTimers
  { _hitTimer = -1
  , _gracePeriodTimer = 60 * 5
  }

update :: Input -> Enemy -> Result ([Enemy], DL.DList Bullet -> DL.DList Bullet, [DO.DecorationObject])
update _ enemy = do
  wsize <- _windowSize <$> SM.get
  let
    dir = (enemy ^. directionChanger) wsize enemy
    (mv, move) =
      MV.update dir
        $ (enemy ^. movement)

    (newBullets, attack') =
      (enemy ^. attack . A.attackUpdate) (enemy ^. pos) (enemy ^. size) (enemy ^. attack)

    changedAttack =
      (enemy ^. attackChanger) enemy (enemy ^. attackId)

    enemy' =
      enemy
      & over pos (if isAlive enemy then (`addPoint` move) else id)
      & over sprite (Spr.update Nothing False)
      & set movement mv
      & over timers updateTimers
      & set direction dir
      & set attack (fromMaybe attack' changedAttack)
      & over attackId (maybe id (const (+1)) changedAttack)

  parts <-
    if isDead enemy'
      then
        ST.make $ ST.MakeSplitTexture
          { mkPos = enemy' ^. pos
          , mkSize = enemy' ^. sprite . size
          , mkSplit = enemy' ^. deathParts
          , mkTexture = enemy' ^. deathTexture
          , mkTexturePos = Point 0 0
          , mkDeathTime = enemy' ^. deathTime
          }
      else pure []

  pure
    ( if isDead enemy'
        || not (isInWindow wsize (enemy ^. pos) (enemy ^. size))
           && (enemy' ^. timers . gracePeriodTimer) < 0
        then []
        else pure enemy'
    , if isDead enemy'
        || not (isInWindow wsize (enemy ^. pos) (Point 0 0))
        then id
        else DL.append newBullets
    , parts
    )

updateTimers :: EnemyTimers -> EnemyTimers
updateTimers et =
  et
    & over hitTimer (\t -> if t <= 0 then -1 else t - 1)
    & over gracePeriodTimer (\t -> if t <= 0 then -1 else t - 1)


checkHit :: DL.DList Bullet -> Enemy -> Enemy
checkHit bullets enemy
  | any (isJust . flip isTouchingCircleRect enemy) bullets && enemy ^. health > 0
  = enemy
    & over health (flip (-) (DL.head bullets ^. damage))
    & \enemy' ->
      enemy'
        & set
           (timers . hitTimer)
           ( if
               | enemy' ^. health <= 0 ->
                 hitTimeout * 4
               | enemy' ^. timers . hitTimer < 0 ->
                 hitTimeout
               | otherwise ->
                 enemy' ^. timers . hitTimer
           )
  | otherwise
  = enemy

hitTimeout = 20

isDead :: Enemy -> Bool
isDead enemy =
  enemy ^. health <= 0
  && enemy ^. timers . hitTimer < 0

isAlive :: Enemy -> Bool
isAlive enemy =
  enemy ^. health > 0

render :: SDL.Renderer -> Camera -> Enemy -> IO ()
render renderer cam enemy = do
  let
    -- h = fromIntegral $ 255 - max 0 (enemy ^. health * 2)
    isHit = enemy ^. timers . hitTimer > 0 && enemy ^. timers . hitTimer `mod` 8 < 4

  Spr.render renderer cam (enemy ^. pos) (enemy ^. size) (if isHit then 100 else 255) (enemy ^. sprite)

  -- when isHit $ do
  --   let
  --     colour = Vect.V4 255 (255 - h) (255 - h) 150
  --     radius = fromIntegral $ enemy ^. size . x `div` 2
  --     center =
  --       Vect.V2
  --         (fromIntegral (cam (enemy ^. pos) ^. x) + radius)
  --         (fromIntegral (cam (enemy ^. pos) ^. y) + radius)
  --   SDL.circle renderer center radius colour
  --   SDL.fillCircle renderer center radius colour