packages feed

freekick2-0.1.0: src/Match/Match.hs

{-# LANGUAGE TemplateHaskell #-}
module Match.Match(playMatch, TeamOwner(..),
  MatchTextureSet(..), PlayerTextureSet(..)
  )
where

import Control.Monad
import Control.Monad.State as State
import Data.List
import Data.Maybe
import qualified Data.IntMap as M
import System.CPUTime
import Data.Function
import Text.Printf
import System.Random
import Control.Applicative

import Graphics.Rendering.OpenGL as OpenGL
import Graphics.UI.SDL as SDL hiding (flip)
import Graphics.Rendering.FTGL as FTGL

import SDLUtils
import Drawing
import FVector
import Utils
import Gen

import Match.DrawPitch
import Match.Ball
import Match.Player

import Match.State.MatchState
import Match.State.MatchBase
import Match.State.AI
import Match.State.Actions
import Match.State.Formation
import Match.State.Controls

data MatchTextureSet = MatchTextureSet {
    pitchtexture      :: TextureObject
  , hometextureset    :: PlayerTextureSet
  , awaytextureset    :: PlayerTextureSet
  , playershadowinfo  :: ImageInfo
  , ballimginfo       :: ImageInfo
  , ballshadowinfo    :: ImageInfo
  , humandrawsize     :: FRange
  , goal1             :: ImageInfo
  , goal2             :: ImageInfo
  , goal1shadow       :: ImageInfo
  , goal2shadow       :: ImageInfo
  }

playMatch :: Either Int StdGen
          -> MatchTextureSet 
          -> Font 
          -> Font
          -> (GenTeam, GenFormation, TeamOwner) 
          -> (GenTeam, GenFormation, TeamOwner) 
          -> IO ()
playMatch gen texs f f2 ht at = do
  let psize = (68, 105)
      contr = Nothing
  plist <- liftIO $ defineNewList Compile (drawPitch (pitchtexture texs) (16, 16) psize)
  evalStateT runMatch (initMatchState gen plist psize (20, 40) texs ht at contr f f2)
  (w, h) <- liftIO $ getWindowSize
  setCamera ((0, 0), (w, h))

initMatchState :: Either Int StdGen
               -> DisplayList 
               -> FRange -> FRange 
               -> MatchTextureSet 
               -> (GenTeam, GenFormation, TeamOwner)
               -> (GenTeam, GenFormation, TeamOwner)
               -> Maybe PlayerID 
               -> Font -> Font
               -> MatchState
initMatchState gen plist psize cpos pltexs (ht, htac, ho) (at, atac, ao) c f1 f2 = 
  MatchState plist (goal1 pltexs) (goal2 pltexs) (goal1shadow pltexs) (goal2shadow pltexs)
             [] psize cpos 
             (Team hps (genFormationToFormation hps htac) 0 (genteamname ht) ho) 
             (Team aps (genFormationToFormation aps atac) 0 (genteamname at) ao) 
             c BeforeKickoff 
             (initialBall onPitchZ psize (ballimginfo pltexs) (ballshadowinfo pltexs))
             [] Nothing f1 f2 (getgen gen) (False, 0) False 0 0.020 False True
             defaultParams
  where hps = createPlayers True pltexs psize ht
        aps = createPlayers False pltexs psize at
        getgen (Left n)  = mkStdGen n
        getgen (Right g) = g

onPitchZ :: Float
onPitchZ = 1

genTeamToTeam :: MatchTextureSet -> FRange -> Bool -> Formation -> TeamOwner -> GenTeam -> Team
genTeamToTeam pltexs psize home frm town g = Team pls frm 0 tnam town
  where tnam = genteamname g
        pls  = createPlayers home pltexs psize g

createPlayers :: Bool -> MatchTextureSet -> FRange -> GenTeam -> PlayerMap
createPlayers home texs psize t =
  let (d, m, f) = teamtactics t
      g  = take 1 $ filter (\p -> isGoalkeeper p) (teamplayers t)
      ds = take d $ filter (\p -> isDefender p) (teamplayers t)
      ms = take m $ filter (\p -> isMidfielder p) (teamplayers t)
      fs = take f $ filter (\p -> isAttacker p) (teamplayers t)
      pllist = g ++ ds ++ ms ++ fs
      plnums = map plnumber pllist
  in M.fromList 
       (zip plnums 
            (map (genPlayerToPlayer onPitchZ home
                                     (if home then hometextureset texs else awaytextureset texs)
                                     (playershadowinfo texs)
                                     (humandrawsize texs) 
                                     psize) 
                 pllist))

camZoomLevel :: (Num a) => a
camZoomLevel = 20

cameraCenter w h s =
   let (bx, by) =
         case ballplay s of
           OutOfPlayWaiting _ r -> getRestartPoint r
           OutOfPlay _ r        -> getRestartPoint r
           RestartPlay r        -> getRestartPoint r
           _                    -> to2D (ballposition $ ball s)
       (px, py) = pitchsize s
       nx = clamp 0 px bx
       ny = clamp 0 py by
   in (nx - (w / (2 * camZoomLevel)), ny - (h / (2 * camZoomLevel)))

drawMatch :: Match ()
drawMatch = do
  s <- State.get
  (w, h) <- liftIO $ getWindowSize
  let text = printf "%16s %d - %d %-16s" (hometeamname s) (homegoals s) (awaygoals s) (awayteamname s)
      coords = (fromIntegral w / 2, 50)
  liftIO $ do
    clear [ColorBuffer, DepthBuffer]
    let cpos = cameraCenter (fromIntegral w) (fromIntegral h) s
        (px, py) = pitchsize s
    setCamera' (cpos, (fromIntegral (w `div` camZoomLevel), fromIntegral (h `div` camZoomLevel)))
    callList (pitchlist s)
    drawSprite' (imgtexture $ matchgoal1shadow s) ((px / 2 - 3.66 - 0.4, py - 1.0), imgsize $ matchgoal1shadow s) (-1)
    drawSprite' (imgtexture $ matchgoal2shadow s) ((px / 2 - 3.66 - 0.4, -2.44 - 1.0), imgsize $ matchgoal2shadow s) (-1)
    drawSprite' (imgtexture $ matchgoal1 s) ((px / 2 - 3.66, py), imgsize $ matchgoal1 s) (-1)
    drawBallShadow $ ball s
    mapM_ drawPlayerShadow (allPlayers s)
    mapM_ drawSprite $ sortBy (compare `on` getDepth) (SB (ball s) : map SB (allPlayers s))
    drawSprite' (imgtexture $ matchgoal2 s) ((px / 2 - 3.66, -2.44), imgsize $ matchgoal2 s) (-1)
    case controlledpl s of
      Nothing            -> return ()
      Just i@(pid, _) -> case findPlayer i s of
                              Nothing -> return ()
                              Just pl -> do
                                let (plx, ply) = plposition pl
                                writeOnPitch (matchfont2 s) (show pid) (plx, ply + 2)
    -- writeTexts come in last, as they reset the camera.
    when (pausedBallplay s) $ writeText True w h (matchfont1 s) text coords
    when (inPlay (ballplay s)) $ writeText False w h 
                                     (matchfont2 s) 
                                     (fromMaybe "" 
                                         (liftM (\p -> show (playerNumber p) ++ " " ++ Match.Player.plname p)
                                                (lasttouch s >>= flip findPlayer s))) 
                                     (10, fromIntegral h - 20)
    writeText False w h (matchfont2 s) (printf "%d min" (floor (snd (matchtime s)) `div` (60 :: Int))) (10, fromIntegral h - 50)
    glSwapBuffers

uniformScale :: GLfloat -> IO ()
uniformScale n = scale n n n

writeOnPitch :: Font -> String -> FRange -> IO ()
writeOnPitch f str (x, y) = do
  loadIdentity
  color $ Color3 1 1 (1 :: GLfloat)
  translate $ Vector3 (realToFrac x) (realToFrac y) (2 :: GLfloat)
  uniformScale (1 / camZoomLevel)
  textlen <- getFontAdvance f str
  translate $ Vector3 (-(realToFrac textlen / 2)) 0 (0 :: GLfloat)
  renderFont f str FTGL.Front

writeText :: Bool -> Int -> Int -> Font -> String -> FRange -> IO ()
writeText centered w h f str (x, y) = do
  loadIdentity
  setCamera ((0, 0), (w, h))
  color $ Color3 1 1 (1 :: GLfloat)
  translate $ Vector3 (realToFrac x) (realToFrac y) (2 :: GLfloat)
  when centered $ do
    textlen <- getFontAdvance f str
    translate $ Vector3 (-(realToFrac textlen / 2)) 0 (0 :: GLfloat)
  renderFont f str FTGL.Front

playerOnHisSide :: MatchState -> Player -> Bool
playerOnHisSide m p =
  let (_, y) = absToRel m (plposition p)
      lower  = playerHome p == homeattacksup m
  in if lower then y <= 0.5 else y >= 0.5

updateBallPlay :: Match ()
updateBallPlay = do
  s <- State.get
  let (px, py) = pitchsize s
      frameTime = floor $ 1000 * frametime s
  case ballplay s of
    BeforeKickoff -> do
      when (all (playerOnHisSide s) (allPlayers s)) $
        sModBallplay (const $ WaitForKickoff (kickofftimer (params s)))
    WaitForKickoff timer -> do
      when (timer < (kickoffballtimer (params s))) $ sModBall $ modBallposition $ const $ to3D (px / 2, py / 2) 0
      sModBall $ modBallvelocity $ const nullFVector3
      if timer < 0
        then sModBallplay (const DoKickoff)
        else sModBallplay (const $ WaitForKickoff (timer - frameTime))
    DoKickoff -> do
      return () -- updated by handleMatchEvent BallKicked
    InPlay -> do
      let (bx, by, bz) = ballposition $ ball s
      when (bx < 0) $ do -- throwin from the left
        let restartpos = (0, by)
        sModBallplay $ const $ OutOfPlayWaiting (oopthrowintimer (params s)) (ThrowIn restartpos)
      when (bx > px) $ do -- throwin from the right
        let restartpos = (px, by)
        sModBallplay $ const $ OutOfPlayWaiting (oopthrowintimer (params s)) (ThrowIn restartpos)
      when (by < 0) $ do  -- corner kick or goal kick on bottom half
        if bx > px / 2 - 3.66 && bx < px / 2 + 3.66 && bz < 2.44 -- goal
          then do
            let restartpos = (px / 2, py / 2)
            if homeattacksup s
              then do
                sModAwaygoals succ
                sModHomekickoff $ const True
              else do
                sModHomegoals succ
                sModHomekickoff $ const False
            sModBallplay $ const BeforeKickoff
          else
            if homeattacksup s == not (homeRestarts s) -- corner kick
              then do
                let restartpos =
                      if bx < px / 2
                        then (0, 0)
                        else (px, 0)
                sModBallplay $ const $ OutOfPlayWaiting (oopcornerkicktimer (params s)) (CornerKick restartpos)
              else do -- goal kick
                let restartpos =
                      if bx < px / 2
                        then (px / 2 - 9.15, 5.5) -- TODO: clean up pitch constants
                        else (px / 2 + 9.15, 5.5)
                sModBallplay $ const $ OutOfPlayWaiting (oopgoalkicktimer (params s)) (GoalKick restartpos)
      when (by > py) $ do  -- corner kick of goal kick on upper half
        if bx > px / 2 - 3.66 && bx < px / 2 + 3.66 && bz < 2.44 -- goal
          then do
            let restartpos = (px / 2, py / 2)
            if homeattacksup s
              then do
                sModHomegoals succ
                sModHomekickoff $ const False
              else do
                sModAwaygoals succ
                sModHomekickoff $ const True
            sModBallplay $ const BeforeKickoff
          else
            if homeattacksup s == not (homeRestarts s) -- goal kick
              then do
                let restartpos =
                      if bx < px / 2
                        then (px / 2 - 9.15, py - 5.5)
                        else (px / 2 + 9.15, py - 5.5)
                sModBallplay $ const $ OutOfPlayWaiting (oopgoalkicktimer (params s)) (GoalKick restartpos)
              else do -- corner kick
                let restartpos =
                      if bx < px / 2
                        then (0, py)
                        else (px, py)
                sModBallplay $ const $ OutOfPlayWaiting (oopcornerkicktimer (params s)) (CornerKick restartpos)
    OutOfPlayWaiting timer restart -> 
      if timer > 0
        then sModBallplay $ const $ OutOfPlayWaiting (timer - frameTime) restart
        else sModBallplay $ const $ OutOfPlay (ooptimer (params s)) restart
    OutOfPlay timer restart ->
      if timer > 0
        then do
          when (timer < (oopmoveballtimer (params s))) $ sModBall $ modBallposition $ const $ to3D (getRestartPoint restart) 0
          sModBall $ modBallvelocity $ const nullFVector3
          sModBallplay $ const $ OutOfPlay (timer - frameTime) restart
        else sModBallplay $ const $ RestartPlay restart
    RestartPlay _ -> do
      when ((to2D $ ballposition (ball s)) `inside2s` ((0, 0), (pitchsize s))) $
        sModBallplay $ const InPlay
      -- in case the ball is kicked further away from pitch
      when (not $ (to2D $ ballposition (ball s)) `inside2s` ((-0.2, -0.2), (px + 0.2, py + 0.2))) $
        sModBallplay $ const InPlay
    Finished t ->
      sModBallplay $ const $ Finished $ (t + frameTime)

handleMatchEvent :: MatchEvent -> Match ()
handleMatchEvent BallKicked = do
  s <- State.get
  case ballplay s of
    DoKickoff -> do
      sModBallplay $ const InPlay
    _ -> return ()

handleMatchEvents :: Match ()
handleMatchEvents = do
  s <- State.get
  mapM_ handleMatchEvent (pendingevents s)
  sModPendingevents $ const []

updateBallPosition :: Match ()
updateBallPosition = do
  s <- State.get
  let dt = frametime s
      (px, py) = pitchsize s
      postradius = 0.25
      postdiameter = 2 * postradius
      postheight = 2.44
      goaldepth = 2.44
      post1 = CylinderZ (px / 2 - 3.66, 0, -1) postdiameter postheight
      post2 = CylinderZ (px / 2 + 3.66, 0, -1) postdiameter postheight
      post3 = CylinderZ (px / 2 - 3.66, py, -1) postdiameter postheight
      post4 = CylinderZ (px / 2 + 3.66, py, -1) postdiameter postheight
      bar1 = CylinderX (px / 2 - 3.66, 0, postheight) postdiameter 7.32
      bar2 = CylinderX (px / 2 - 3.66, py, postheight) postdiameter 7.32
      wall1 = (((px / 2 - 3.66 - postradius, -goaldepth - postdiameter,     -1 - postradius), 
                (0 + postdiameter,            goaldepth + postradius,       postheight + 1 + postdiameter)), 
                (-0.1, 1, 1))
      wall2 = (((px / 2 - 3.66 - postradius, -goaldepth - postdiameter,     -1 - postradius), 
                (7.32 + postdiameter,         0 + postradius,               postheight + 1 + postdiameter)), 
                (1, -0.1, 1))
      wall3 = (((px / 2 + 3.66 - postradius, -goaldepth - postdiameter,     -1 - postradius), 
                (0 + postdiameter,           goaldepth + postradius,        postheight + 1 + postdiameter)), 
                (-0.1, 1, 1))
      wall4 = (((px / 2 - 3.66 - postradius, -goaldepth - postdiameter,     postheight - postradius), 
                (7.32 + postdiameter,        goaldepth + postradius,        0 + postdiameter)), 
                (1, 1, -0.5))
      wall5 = (((px / 2 - 3.66 - postradius, py + postdiameter,             -1 - postradius), 
                (0 + postdiameter,           goaldepth + postradius,        postheight + 1 + postdiameter)), 
                (-0.1, 1, 1))
      wall6 = (((px / 2 - 3.66 - postradius, py + goaldepth + postdiameter, -1 - postradius), 
                (7.32 + postdiameter,        0 + postradius,                postheight + 1 + postdiameter)), 
                (1, -0.1, 1))
      wall7 = (((px / 2 + 3.66 - postradius, py + postdiameter,             -1 - postradius), 
                (0 + postdiameter,           goaldepth + postradius,        postheight + 1 + postdiameter)), 
                (-0.1, 1, 1))
      wall8 = (((px / 2 - 3.66 - postradius, py + postdiameter,             postheight - postradius), 
                (7.32  + postdiameter,       goaldepth + postradius,        0 + postdiameter)), 
                (1, 1, -0.5))
  sModBall $ modBallposition (*+* ((ballvelocity (ball s)) *** dt))
  sModBall $ collCheckGroundBall (ballbounciness (params s))
  sModBall $ collCheckPostsBall dt [post1, post2, post3, post4, bar1, bar2]
  sModBall $ collCheckNetBall dt [wall1, wall2, wall3, wall4, wall5, wall6, wall7, wall8]
  sModBall $ gravitateBall (ballgravitypull (params s)) dt
  sModBall $ slowDownBall (ballairviscosity (params s)) (ballrollfriction (params s)) dt

execAI :: Match ()
execAI = do
  s <- State.get
  let plactions = doAI s
  mapM_ plact plactions

modFst :: (a -> a) -> (a, b) -> (a, b)
modFst f (a, b) = (f a, b)

modSnd :: (b -> b) -> (a, b) -> (a, b)
modSnd f (a, b) = (a, f b)

updateTimers :: Match ()
updateTimers = do
  s <- State.get
  let dt = frametime s
  sModAllPlayers (modKicktimer (\t -> max 0 (t - floor (dt * 1000))))
  when (inPlay (ballplay s)) $ do
    sModMatchtime $ modSnd $ (+ (dt * matchtimedelta (params s)))
    case fst (matchtime s) of
      False -> do
        when (snd (matchtime s) > 45 * 60) $ do
          sModMatchtime $ const (True, 45 * 60)
          sModBallplay $ const BeforeKickoff
          sModHomeattacksup $ not
          sModHomekickoff $ const True
      True  -> do
        when (snd (matchtime s) > 90 * 60) $ do
          sModBallplay $ const $ Finished 0
          let (px, py) = pitchsize s
          sModBall $ modBallposition $ const $ to3D (px / 2, py / 2) 0
          sModBall $ modBallvelocity $ const nullFVector3

controllable :: BallPlay -> Bool
controllable InPlay                     = True
controllable DoKickoff                  = False
controllable (RestartPlay (GoalKick _)) = False
controllable (RestartPlay _)            = False
controllable _                          = False

setControl :: Bool -> Match ()
setControl h = do
  s <- State.get
  let p = if h
            then nearestToBallHwoGK s
            else nearestToBallAwoGK s
  sModControlledpl $ const $ Just $ playerid p

setControlledPlayer :: Match ()
setControlledPlayer = do
  s <- State.get
  if (controllable (ballplay s))
    then
      if teamowner (hometeam s) == HumanOwner
        then setControl True
        else if teamowner (awayteam s) == HumanOwner
               then setControl False
               else sModControlledpl $ const Nothing
     else sModControlledpl $ const Nothing

runMatch :: Match ()
runMatch = do
  t1 <- liftIO $ getCPUTime
  dt <- frametime <$> State.get
  quitting <- handleInput (floor (1000 * dt) :: Int)
  if quitting
    then return ()
    else do
      drawMatch
      setControlledPlayer
      pause <- paused <$> State.get
      if (not pause)
        then do
          execAI
          handleMatchEvents
          updateBallPosition
          updateBallPlay
          updateTimers
          t2 <- liftIO $ getCPUTime
          let tdiff = fromIntegral (t2 - t1) * (1e-12 :: Float)
          sModFrametime $ const ((dt * 3 + tdiff) / 4)
        else liftIO $ SDL.delay 20
      runMatch