packages feed

boomslang-0.0.1: src/Game/Logic.hs

module Game.Logic (logic) where

-- -- -- -- -- -- -- -- -- -- -- -- -- -- -- -- -- -- -- --

import Control.Monad (replicateM, liftM2, unless, when)
import Data.List     ((\\))

import Data.Accessor.Basic ((^.), (^=), (^:))

import External.Graphics.Rendering (renderScene)
import External.Time               (readTimer, sleep)
import Game.Activity               (Activity(..))
import Game.Entity.Dot             (Dot)
import Game.Entity.Dot.Activity    (Activity(..))
import Game.G                      (G, ask, get, getRandomR, put, modify)
import Game.Level                  (Level(..), numDots, numDotsRequired)
import Game.Score                  (Score(..))
import Vector                      ((^-^), (^+^), (.*^))
import qualified External.Input.Keyboard      as Keyboard
import qualified External.Input.Keyboard.Keys as Keyboard
import qualified External.Input.Mouse         as Mouse
import qualified External.Input.Mouse.Buttons as Mouse
import qualified Game.Activity                as GameActivity
import qualified Game.Entity.Dot              as Dot
import qualified Game.Entity.Dot.Activity     as DotActivity
import qualified Game.Environment             as Environment
import qualified Game.Level                   as Level
import qualified Game.Score                   as Score
import qualified Game.State                   as State
import qualified Graphics.Rendering.OpenGL    as GL
import qualified Vector                       as Vector

-- -- -- -- -- -- -- -- -- -- -- -- -- -- -- -- -- -- -- --

logic :: G ()
logic =
    loopUntilUserQuits $ do
        t1 <- readTimer
        s <- get
        case s^.State.activity of
          Starting                               -> start
          PreparingLevel level                   -> prepareLevel level
          Playing level dots                     -> play level dots
          Colliding level activeDots roamingDots -> collide level activeDots roamingDots
          EndingLevel level dots                 -> endLevel level dots
          Quitting                               -> return ()
        t2 <- readTimer
        let diff = t2 - t1
        when (diff < limit) (sleep $ limit - diff)
  where
    limit = 1/60

-- -- -- -- -- -- -- -- -- -- -- -- -- -- -- -- -- -- -- --

unitStep :: Double -> Double
unitStep n =
    if n < 0
      then 0
      else 1

updateDotRadius :: Dot -> G Dot
updateDotRadius dot = do
    t <- readTimer
    let n = t - dot^.Dot.timestamp
    case dot^.Dot.activity of
      Roaming ->
        if n <= 0.1
          then let r = 10 * n * normal
               in return $ Dot.radius ^= r $ dot
          else return dot
      Hit ->
        if n > 3
           then updateActivity None dot
           else let r = if n < 2
                          then normal + adjust * (unitStep (n-exp(-growth))*(log n + growth))/growth
                          else splode * exp (decay * (n-2))
                in return $ Dot.radius ^= r $ dot
      None -> return $ Dot.radius ^= 0 $ dot
  where
    normal = 0.04
    splode = 0.16
    adjust = abs $ splode - normal
    growth = 5
    decay  = negate growth

updateActivity :: DotActivity.Activity -> Dot -> G Dot
updateActivity a d = do
    t <- readTimer
    let dot = Dot.activity  ^= a
            $ Dot.timestamp ^= t $ d
    return dot

activate :: [Dot] -> [Dot] -> G ([Dot],[Dot])
activate activeDots roamingDots = do
    let newActiveDots = filter (\rd -> any (areColliding rd) activeDots) roamingDots
        roamingDots' = roamingDots \\ newActiveDots
    newActiveDots' <- mapM (updateActivity Hit) newActiveDots
    return (activeDots ++ newActiveDots', roamingDots')
  where
    areColliding d0 d1 =
        xDistance < maxDistance &&
        yDistance < maxDistance &&
        vDistance < maxDistance
      where
        xDistance = abs $ x1 - x2
        yDistance = abs $ y1 - y2
        vDistance = Vector.vlen (d0p ^-^ d1p)
        maxDistance = realToFrac $ d0^.Dot.radius + d1^.Dot.radius
        d0p = d0^.Dot.position
        d1p = d1^.Dot.position
        x1 = Vector.getX d0p
        y1 = Vector.getY d0p
        x2 = Vector.getX d1p
        y2 = Vector.getY d1p

-- -- -- -- -- -- -- -- -- -- -- -- -- -- -- -- -- -- -- --

makeDots :: Integer -> G [Dot]
makeDots n =
    replicateM (fromIntegral n) makeDot

makeDot :: G Dot
makeDot = do
    e <- ask

    -- radius
    let radius = e^.Environment.normalDotRadius

    -- color
    rc <- realToFrac `fmap` randomFloatR 0 1
    gc <- realToFrac `fmap` randomFloatR 0 1
    bc <- realToFrac `fmap` randomFloatR 0 1
    let color = GL.Color4 rc gc bc 0.6

    -- position
    px <- randomDoubleR (radius-1) (1-radius)
    py <- randomDoubleR (radius-1) (1-radius)
    let position = Vector.V (realToFrac px) (realToFrac py)

    -- direction
    dx_ <- randomDoubleR (1/4) (3/4)
    let dy_ = sqrt $ 1-dx_**2
    dx <- fmap (\b -> if b then negate dx_ else dx_) randomBool
    dy <- fmap (\b -> if b then negate dy_ else dy_) randomBool
    let direction = Vector.V (realToFrac dx) (realToFrac dy)

    -- velocity
    let velocity = e^.Environment.dotVelocity

    -- activity
    let activity = Roaming

    t <- readTimer

    return $ Dot.make radius color position direction velocity activity t
  where
    randomFloatR l h  = getRandomR (l,h)              :: G Float
    randomDoubleR l h = getRandomR (l,h)              :: G Double
    randomBool        = fmap (== 0) (getRandomR (0,1) :: G Int)

-- -- -- -- -- -- -- -- -- -- -- -- -- -- -- -- -- -- -- --

updateDots :: [Dot] -> G [Dot]
updateDots =
    fmap (filter isAlive) . mapM updateDot
  where
    isAlive d = d^.Dot.activity /= None

updateDot :: Dot -> G Dot
updateDot dot =
    case dot^.Dot.activity of
      Roaming -> updateRoamingDot dot
      Hit     -> return dot
      None    -> return dot
    >>= updateDotRadius

updateRoamingDot :: Dot -> G Dot
updateRoamingDot dot = do
    let d   = dot^.Dot.direction
        dx  = Vector.getX d
        dy  = Vector.getY d
        p   = dot^.Dot.position
        r   = dot^.Dot.radius
        v   = dot^.Dot.velocity
        p'  = p ^+^ (realToFrac v .*^ d)
        p'x = realToFrac $ Vector.getX p'
        p'y = realToFrac $ Vector.getY p'
        (p'x',fdx) =
          if p'x+r > 1
            then (2-p'x-(2*r),True)
            else
              if p'x-r < -1
                then (-2-p'x+(2*r),True)
                else (p'x,False)
        (p'y',fdy) =
          if p'y+r > 1
            then (2-p'y-(2*r),True)
            else
              if p'y-r < -1
                then (-2-p'y+(2*r),True)
                else (p'y,False)
        p'' = Vector.V (realToFrac p'x') (realToFrac p'y')
        d'' = Vector.V
               (if fdx then negate dx else dx)
               (if fdy then negate dy else dy)
    return $ Dot.position  ^= p'' $
             Dot.direction ^= d'' $ dot

-- -- -- -- -- -- -- -- -- -- -- -- -- -- -- -- -- -- -- --

loopUntilUserQuits :: G () ->  G ()
loopUntilUserQuits f = do
    done <- liftM2 (||) (Keyboard.pressed Keyboard.escape) (Keyboard.pressed Keyboard.q)
    unless done (f >> loopUntilUserQuits f)

start :: G ()
start = modify $ (State.activity ^= PreparingLevel Level1) . (State.score ^= NoScore)

prepareLevel :: Level -> G ()
prepareLevel level = do
    s <- get
    dots <- makeDots nd
    put $ State.score    ^= Score nd ndr 0 (np s) $
          State.activity ^= Playing level dots    $ s
  where
    nd   = numDots         level
    ndr  = numDotsRequired level
    np s = if s^.State.score == NoScore then 0 else s^.State.score^.Score.numPoints

play :: Level -> [Dot] -> G ()
play level dots = do
    e <- ask
    dots' <- updateDots dots
    mxy   <- Mouse.clicked Mouse.left
    case mxy of
      Nothing -> modify $ State.activity ^= Playing level dots'
      Just (x,y) -> do
        t <- readTimer
        let radius    = e^.Environment.normalDotRadius
            color     = e^.Environment.placedDotColor
            velocity  = e^.Environment.dotVelocity
            position  = Vector.V (realToFrac x) (realToFrac y)
            direction = Vector.vnull
            activity  = Hit
            dot       = Dot.make radius color position direction velocity activity t
        modify $ State.activity ^= Colliding level [dot] dots'
    renderScene

collide :: Level -> [Dot] -> [Dot] -> G ()
collide level activeDots roamingDots
  | null activeDots = do
      let activity' = EndingLevel level dots'
          dots'     = map (Dot.activity ^= Hit) roamingDots
      modify $ State.activity ^= activity'
  | otherwise = do
      activeDots'  <- updateDots activeDots
      roamingDots' <- updateDots roamingDots
      (activeDots'', roamingDots'') <- activate activeDots' roamingDots'

      s <- get
      let a'  = Colliding level activeDots'' roamingDots''
          p   = fromIntegral $ length activeDots'' - length activeDots'
          sc  = s^.State.score
          sc' = Score.numDotsActivated ^: (p +) $ sc

      put $ State.activity ^= a'  $
            State.score    ^= sc' $ s
      renderScene

endLevel :: Level -> [Dot] -> G ()
endLevel level dots = do
    dots' <- updateDots dots
    s <- get
    if null dots'
      then do
          let sc = s^.State.score
          if (sc^.Score.numDotsActivated) >= (sc^.Score.numDotsRequired)
            then
              case Level.next level of
                Just level' -> put $ State.activity ^= PreparingLevel level'                      $
                                     State.score^:Score.numPoints^:(+ sc^.Score.numDotsActivated) $ s
                Nothing     -> put $ State.activity ^= Starting                                   $
                                     State.score^:Score.numPoints^:(+ sc^.Score.numDotsActivated) $ s
            else put $ State.activity ^= PreparingLevel level $ s
      else do
          put $ State.activity ^= EndingLevel level dots' $ s
          renderScene