module Game (main) where import Control.Monad import Data.IORef import Data.Maybe (fromMaybe) import Data.Text (pack) import Foreign.C.Types (CInt) import qualified Game.BitmapFont as BF import qualified Game.Controller as C import qualified Game.Entities as E import qualified Game.Hud as H import qualified Game.Map as M import qualified Game.Sprites as S import qualified Game.State as GS import qualified Game.Utils as U import SDL (($=), ($~)) import qualified SDL import qualified SDL.Image import SDL.Vect (V2 (..)) name :: String name = "Haskell gamedev [Space Platformer]" gameWidth, gameHeight :: CInt (gameWidth, gameHeight) = (320, 192) gameScale :: CInt gameScale = 3 maxLives :: Int maxLives = 4 windowWidth, windowHeight :: CInt (windowWidth, windowHeight) = (gameWidth * gameScale, gameHeight * gameScale) version :: String version = "0.1.0" data Env = Env { window :: SDL.Window, renderer :: SDL.Renderer, canvas :: SDL.Texture, fullscreen :: Bool, renderRect :: SDL.Rectangle CInt, controls :: IORef C.Controls, map :: M.Map, sprites :: S.SpriteSheet, font :: BF.BitmapFont, entities :: IORef E.Entities, hud :: H.Hud, state :: IORef GS.State } defaultRenderRect :: SDL.Rectangle CInt defaultRenderRect = SDL.Rectangle (SDL.P $ V2 0 0) (V2 windowWidth windowHeight) main :: IO () main = do SDL.initialize [SDL.InitVideo, SDL.InitGameController] window <- SDL.createWindow (pack $ name ++ " " ++ version) SDL.defaultWindow {SDL.windowInitialSize = SDL.V2 windowWidth windowHeight} renderer <- SDL.createRenderer window (-1) SDL.RendererConfig { SDL.rendererType = SDL.AcceleratedVSyncRenderer, SDL.rendererTargetTexture = True } SDL.HintRenderScaleQuality $= SDL.ScaleNearest canvas <- SDL.createTexture renderer SDL.RGBA8888 SDL.TextureAccessTarget (V2 gameWidth gameHeight) tsTexture <- SDL.Image.loadTexture renderer "data/tiles.png" ssTexture <- SDL.Image.loadTexture renderer "data/sprites.png" bfTexture <- SDL.Image.loadTexture renderer "data/font.png" controls <- newIORef =<< C.init map' <- M.load "data/map.json" tsTexture sprites <- S.load "data/sprites.json" ssTexture font <- BF.load "data/font.json" bfTexture state <- newIORef GS.State { batteries = 0, totalBatteries = M.totalBatteries map', lives = maxLives, totalLives = maxLives, hitDelay = 0, gameOverDelay = 0 } hud <- H.mkHud sprites state entities <- newIORef =<< E.mkEntities sprites map' controls state gameLoop Env { window = window, renderer = renderer, canvas = canvas, fullscreen = False, renderRect = defaultRenderRect, controls = controls, map = map', sprites = sprites, font = font, entities = entities, hud = hud, state = state } SDL.destroyWindow window SDL.quit toggleFullscreen :: Env -> IO Env toggleFullscreen env = do let fullscreen = not env.fullscreen renderer = env.renderer window = env.window let mode = if fullscreen then SDL.FullscreenDesktop else SDL.Windowed in SDL.setWindowMode window mode vp <- if fullscreen then SDL.get $ SDL.rendererViewport renderer else pure Nothing let renderRect = case vp of Nothing -> defaultRenderRect Just (SDL.Rectangle _ (V2 w h)) -> SDL.Rectangle (SDL.P $ V2 rx ry) (V2 rw rh) where scale = min (w `div` gameWidth) (h `div` gameHeight) rx = (w - (gameWidth * scale)) `div` 2 ry = (h - (gameHeight * scale)) `div` 2 rw = gameWidth * scale rh = gameHeight * scale pure env {fullscreen = fullscreen, renderRect = renderRect} gameLoop :: Env -> IO () gameLoop e = do events <- map SDL.eventPayload <$> SDL.pollEvents -- F11 for fullscreen / windowed env <- if fromMaybe False $ U.isPressed SDL.KeycodeF11 events then toggleFullscreen e else pure e let renderer = env.renderer canvas = env.canvas renderRect = env.renderRect controls = env.controls stateRef = env.state -- ESC or close the window to quit let quit = fromMaybe False (U.isPressed SDL.KeycodeEscape events) || SDL.QuitEvent `elem` events unless quit $ do -- update controls controls $~ flip C.update events SDL.rendererRenderTarget renderer $= Just canvas SDL.clear renderer state <- readIORef stateRef when (state.gameOverDelay > 1) $ stateRef $= state {GS.gameOverDelay = state.gameOverDelay - 1} when (state.gameOverDelay /= 1) playLoop when (state.gameOverDelay == 1) gameOverLoop SDL.rendererRenderTarget renderer $= Nothing SDL.clear renderer SDL.copy renderer canvas Nothing (Just renderRect) SDL.present renderer gameLoop env where playLoop :: IO () playLoop = do let renderer = e.renderer map' = e.map entities = e.entities hud = e.hud updated <- E.updateAll =<< readIORef entities entities $= updated -- to update the map viewport let (px, py) = E.playerPosition updated -- render map and entities -- set the SDL viewport viewport <- M.viewport renderer map' px py (fromIntegral gameWidth) (fromIntegral gameHeight - H.height) (Just (0, H.height)) M.render renderer map' viewport E.renderVisible renderer updated viewport -- reset viewport to draw the HUD SDL.rendererViewport renderer $= Nothing H.render renderer hud gameOverLoop :: IO () gameOverLoop = do let renderer = e.renderer sprites = e.sprites hud = e.hud H.render renderer hud title <- S.get sprites "game-over" S.render renderer title 112 80 0 0