packages feed

cadence-0.1.0.0: src/Cadence/Systems.hs

{-# LANGUAGE TemplateHaskellQuotes #-}
{-# LANGUAGE RankNTypes #-}
{-# LANGUAGE FlexibleContexts #-}
{-# LANGUAGE TypeApplications #-}
{-# LANGUAGE ScopedTypeVariables #-}

module Cadence.Systems (initialise, run, makeWorld', stepAnimations, getMaybe) where

import Apecs
import Cadence.Types
import Cadence.Texture
import Cadence.Font (FontMap(..), loadFont)
import qualified SDL
import qualified SDL.Font as TTF
import qualified SDL.Image as IMG
import qualified Data.Text as T
import Language.Haskell.TH.Syntax
import Control.Monad (unless)
import qualified SDL.Raw
import qualified Data.Map as Map
import Data.Maybe (fromMaybe, isNothing, fromJust)
import System.Exit (exitSuccess)
import Linear

-- | Initialise the SDL window and renderer
initialise :: forall w.
            (Set w IO Renderer
            , Set w IO Window
            , Set w IO Config
            , Set w IO FontMap)
           => w
           -> Config -- ^ Game config
           -> IO (SDL.Window, SDL.Renderer) -- ^ Returns the created window and renderer contexts
initialise world config = do
    SDL.initialize [SDL.InitVideo]
    TTF.initialize
    IMG.initialize []
    let (w,h) = windowDimensions config
        title = windowTitle config
        windowConfig = SDL.defaultWindow { SDL.windowInitialSize = V2 (fromIntegral w) (fromIntegral h),
                                           SDL.windowMode = SDL.Windowed,
                                           SDL.windowResizable = False }
    window <- SDL.createWindow (T.pack title) windowConfig
    runWith world (set global $ Window $ Just window)
    runWith world (set global config)

    let rendererType = case targetFPS config of
            VSync -> SDL.AcceleratedVSyncRenderer
            _ -> SDL.AcceleratedRenderer
        rendererConfig = SDL.defaultRenderer { SDL.rendererType = rendererType,
                                               SDL.rendererTargetTexture = True }
    renderer <- SDL.createRenderer window (-1) rendererConfig
    runWith world (set global $ Renderer $ Just renderer)
    runWith world (loadFont "resources/Roboto-Regular.ttf" "Roboto-Regular" 24)

    return (window, renderer)

-- | Main game loop
run :: forall w.
     (Has w IO Time
     , Has w IO TextureMap
     , Get w IO Renderable
     , Has w IO IsVisible
     , Get w IO Config)
     => w -- ^ World state
     -> SDL.Renderer -- ^ SDL renderer context
     -> SDL.Window -- ^ SDL window context
     -> (Float -> System w ()) -- ^ World step function
     -> ([SDL.EventPayload] -> System w ()) -- ^ Event handler
     -> (SDL.Renderer -> FPS -> System w ()) -- ^ Draw function, receives the renderer and current FPS
     -> IO ()
run w r window step eventHandler draw = do
    SDL.showWindow window
    let loop prevTicks prevPerf tickAcc fpsAcc = do
            ticks <- SDL.ticks
            perf <- SDL.Raw.getPerformanceCounter
            freq <- SDL.Raw.getPerformanceFrequency
            payload <- map SDL.eventPayload <$> SDL.pollEvents
            let quit = SDL.QuitEvent `elem` payload
                dt = ticks - prevTicks
                tickAcc' = tickAcc + dt
                avgFps = 1000.0 / (fromIntegral tickAcc' / fromIntegral fpsAcc)
                elapsed = fromIntegral (perf - prevPerf) / fromIntegral freq * 1000
            runSystem (eventHandler payload) w
            runSystem (do
                let dt' = fromIntegral dt / 1000
                modify global $ \(Time t) -> Time (t + dt')
                stepAnimations dt'
                step dt') w
            runSystem (do
                c <- get global
                liftIO $ SDL.rendererDrawColor r SDL.$= backgroundColor c) w
            SDL.clear r
            runSystem (draw r (round avgFps)) w
            SDL.present r
            runSystem (do
                c <- get global
                case targetFPS c of
                    Limited fps -> let
                            frameTime = 1000 / fromIntegral fps
                            delayTime = max 0 (frameTime - elapsed)
                        in SDL.delay $ floor delayTime
                    _ -> return ()) w
            unless quit $ loop ticks perf tickAcc' (fpsAcc + 1)
    loop 0 0 0 0
    SDL.destroyRenderer r
    SDL.destroyWindow window
    TTF.quit
    IMG.quit
    SDL.quit
    exitSuccess

-- | Template Haskell function to generate the world type and instances for the given component types. See the [Apecs documentation](https://hackage.haskell.org/package/apecs-0.9.6/docs/Apecs.html#v:makeWorld) for more details.
makeWorld' :: [Name] -> Q [Dec]
makeWorld' cTypes = makeWorld "World" (cTypes ++ [''TextureMap
                                                 , ''FontMap
                                                 , ''Position
                                                 , ''Time
                                                 , ''Renderable
                                                 , ''Renderer
                                                 , ''Window
                                                 , ''Camera
                                                 , ''Config
                                                 , ''Layer
                                                 , ''IsVisible
                                                 , ''Colour])

stepAnimations :: forall w.
                (Has w IO Time
                , Has w IO TextureMap
                , Get w IO Renderable
                , Set w IO Renderable
                , Has w IO IsVisible
                , Members w IO Renderable)
                => Float
                -> System w ()
stepAnimations dt = cmapM $ \(r, e) -> do
    case r of
        Texture t -> getMaybe e (Proxy @IsVisible) >>= \res -> if isNothing res || (let IsVisible visible = fromJust res in visible) then do
                Time t' <- get global
                TextureMap m <- get global
                let tex = textureRef t `Map.lookup` m
                case tex of
                    Nothing -> return r
                    Just tex' -> case animation tex' of
                            Just a -> do
                                let trigger = floor (t' / frameSpeed a) /= floor ((t' + dt) / frameSpeed a)
                                    nextTex = next a `Map.lookup` m
                                if trigger then do
                                    let frame = fromMaybe 0 (animationFrame t)
                                        newFrame = (frame + 1) `mod` frameCount a
                                    if newFrame == 0 then
                                        case nextTex of
                                            Just nextTex' -> case animation nextTex' of
                                                Just _ -> return $ Texture t { textureRef = next a, animationFrame = Just 0 }
                                                Nothing -> return $ Texture t { textureRef = next a, animationFrame = Nothing }
                                            Nothing -> return $ Texture t { animationFrame = Just frame }
                                    else
                                        return $ Texture t { animationFrame = Just newFrame }
                                else return r
                            Nothing -> return r
            else return r
        _ -> return r

-- | Checks if an entity hsa a component. If so, it returns that component in a Just. Otherwise, it returns Nothing
getMaybe :: forall w m c. Get w m c => Entity -> Proxy c -> SystemT w m (Maybe c)
getMaybe e p = exists e p >>= \y -> if y then Just <$> get e else return Nothing