spanout-0.1: src/Spanout/Gameplay.hs
{-# LANGUAGE Arrows #-}
{-# LANGUAGE MultiWayIf #-}
{-# LANGUAGE TypeOperators #-}
module Spanout.Gameplay (game) where
import Prelude hiding (id, (.))
import Spanout.Common
import Spanout.Graphics
import Spanout.Level
import qualified Spanout.Wire as Wire
import Control.Applicative
import Control.Arrow
import Control.Category
import Control.Lens
import Control.Monad
import Data.Either
import Data.Maybe
import Data.Monoid
import qualified Data.Set as Set
import qualified Graphics.Gloss.Interface.IO.Game as Gloss
import Linear
-- The reactive view of the game
game :: a ->> Gloss.Picture
game = Wire.bindW gsInit gameBegin
where
gsInit = do
bricks <- generateBricks
return GameState
{ _gsBall = ballInit
, _gsBatX = 0
, _gsBricks = bricks
}
-- Displays the level and a countdown before the actual gameplay
gameBegin :: GameState -> a ->> Gloss.Picture
gameBegin gsInit = Wire.switch $ proc _ -> do
batX <- view _x ^<< mousePos -< ()
time <- Wire.time -< ()
let
gs = set gsBatX batX gsInit
remainingTime = countdownTime - time
returnA -< if
| remainingTime > 0 -> Right $ gamePic gs <> countdownPic remainingTime
| otherwise -> Left $ gameLevel gs
-- Gameplay from an initial game state
gameLevel :: GameState -> a ->> Gloss.Picture
gameLevel gsInit = Wire.switch $ proc _ -> do
batX <- view _x ^<< mousePos -< ()
rec
-- Binding previous values
ball' <- Wire.delay $ view gsBall gsInit -< ball
bricks' <- Wire.delay $ view gsBricks gsInit -< bricks
-- Current position
pos <- Wire.accum (\dt p v -> p + dt *^ v) $ view (gsBall . ballPos) gsInit
-< view ballVel ball'
-- Collision and its normal
let
edgeNormal = ballEdgeNormal pos
batNormal = ballBatNormal batX pos
ballBrickColl = ballBrickCollision (ball' {_ballPos = pos}) bricks'
brickNormals = fst <$> ballBrickColl
normal = mfilter (faceAway $ view ballVel ball') . mergeNormalEvents $
maybeToList edgeNormal
++ maybeToList batNormal
++ fromMaybe [] brickNormals
-- Current velocity
vel <- Wire.accumE reflect $ view (gsBall . ballVel) gsInit -< normal
-- Binding current values
let
ball = Ball pos vel
bricks = fromMaybe bricks' (snd <$> ballBrickColl)
let gs = GameState {_gsBall = ball, _gsBatX = batX, _gsBricks = bricks}
spacePressed <- keyPressed $ Gloss.SpecialKey Gloss.KeySpace -< ()
returnA -< if
| spacePressed ->
Left game
| null bricks ->
Left $ levelEnd gs
| view (ballPos . _y) ball <= -screenBoundY - ballRadius ->
Left $ gameBegin gsInit
| otherwise ->
Right $ gamePic gs
-- Displays the final game state for some time after the end of the level
levelEnd :: GameState -> a ->> Gloss.Picture
levelEnd gs = Wire.switch $ Wire.forThen levelEndTime game . pure pic
where
pic = gamePic gs <> levelEndPic
-- The sum of zero or more normals
mergeNormalEvents :: (Floating a, Epsilon a) => [V2 a] -> Maybe (V2 a)
mergeNormalEvents [] = Nothing
mergeNormalEvents normals = Just . normalize $ sum normals
-- Collision between the ball and the screen edges
ballEdgeNormal :: V2 Float -> Maybe (V2 Float)
ballEdgeNormal (V2 px py)
| px <= -screenBoundX + ballRadius = Just $ unit _x
| px >= screenBoundX - ballRadius = Just $ -unit _x
| py >= screenBoundY - ballRadius = Just $ -unit _y
| otherwise = Nothing
-- Collision between the ball and the bat
ballBatNormal :: Float -> V2 Float -> Maybe (V2 Float)
ballBatNormal batX (V2 px py)
| bxl && bxr && by = Just $ batNormalAt px batX
| otherwise = Nothing
where
bxl = px >= batX - batWidth / 2
bxr = px <= batX + batWidth / 2
by = py <= batPositionY + batHeight / 2 + ballRadius
-- Collision between the ball and the bricks.
-- Calculates the resulting normals and the remaining bricks.
ballBrickCollision :: Ball -> [Brick] -> Maybe ([V2 Float], [Brick])
ballBrickCollision ball bricks =
case collisionNormals of
[] -> Nothing
_ -> Just (collisionNormals, remBricks)
where
check brick =
case ballBrickNormal brick ball of
Just normal -> Right normal
_ -> Left brick
(remBricks, collisionNormals) = partitionEithers . map check $ bricks
-- Collision between the ball and a brick
ballBrickNormal :: Brick -> Ball -> Maybe (V2 Float)
ballBrickNormal (Brick pos (Circle radius)) (Ball bpos _)
| hit = Just . normalize $ bpos - pos
| otherwise = Nothing
where
hit = distance bpos pos <= radius + ballRadius
ballBrickNormal (Brick pos@(V2 x y) (Rectangle width height)) (Ball bpos bvel)
| tooFar = Nothing
| hitX = Just normalX
| hitY = Just normalY
| hitCorner = listToMaybe . filter (faceAway bvel) $ [normalX, normalY]
| otherwise = Nothing
where
dist = bpos - pos
V2 distAbsX distAbsY = abs <$> dist
V2 ballX ballY = bpos
tooFar = distAbsX > width / 2 + ballRadius
|| distAbsY > height / 2 + ballRadius
hitX = distAbsX <= width / 2
hitY = distAbsY <= height / 2
hitCorner = quadrance (V2 (distAbsX - width / 2) (distAbsY - height / 2))
<= ballRadius ^ (2 :: Int)
normalX = signum (ballY - y) *^ unit _y
normalY = signum (ballX - x) *^ unit _x
-- The normal at a point of the bat
batNormalAt :: Float -> Float -> V2 Float
batNormalAt x batX = perp . angle $ batSpread * relX
where
relX = (batX - x) / (batWidth / 2)
-- Checks if two vectors face away from each other
faceAway :: (Num a, Ord a) => V2 a -> V2 a -> Bool
faceAway u v = u `dot` v < 0
-- The reflection of a vector based on a normal
reflect :: Num a => V2 a -> V2 a -> V2 a
reflect v normal = v - (2 * v `dot` normal) *^ normal
-- The reactive position of the mouse
mousePos :: a ->> V2 Float
mousePos = Wire.constM $ view envMouse
-- The reactive state of a keyboard button
keyPressed :: Gloss.Key -> a ->> Bool
keyPressed key = Wire.constM $ views envKeys (Set.member key)