packages feed

starrover2-0.0.9: src/Combat.hs

module Combat(combatLoop, newCombat, randomEnemy, describeEnemy,
  ShipProp(..),
  randomPolice,
  Enemy,
  fightership, intermediate, cargovessel)
where

import System.Random
import Data.Maybe
import Data.Either
import Control.Monad
import Control.Monad.State as State

import Graphics.Rendering.OpenGL as OpenGL
import Graphics.UI.SDL as SDL
import qualified Data.Edison.Seq.SimpleQueue as S
import qualified Data.Edison.Assoc.StandardMap as M

import OpenGLUtils
import Entity
import Space
import AObject
import Cargo
import Utils
import SDLUtils

data AIMode = Human
            | Dummy
            | Shooter GLdouble
  deriving (Show)

data ShipProp = ShipProp {
    maxaccel   :: GLdouble
  , maxturn    :: GLdouble
  , maxhealth  :: Int
  , holdspace  :: Int
  , shipdescr  :: String
  , shippoints :: Int
  }

modHoldspace :: (Int -> Int) -> ShipProp -> ShipProp
modHoldspace f t = t{holdspace = f (holdspace t)}

randomAI :: IO AIMode
randomAI = do
  n <- randomRIO (0, 100 :: Int)
  return $ Shooter (fromIntegral n / 100)

fightership = ShipProp
  0.003
  2.0
  3
  6
  "Fighter ship"
  200

intermediate = ShipProp
  0.002
  1.5
  5
  10
  "Trader ship"
  100

cargovessel = ShipProp
  0.001
  1.0
  8
  15
  "Cargo vessel"
  50

randomShipProp :: IO ShipProp
randomShipProp = do
  n <- randomRIO (1, 3 :: Int)
  case n of
    1 -> return fightership
    2 -> return intermediate
    _ -> return cargovessel

data Ship = Ship {
    shipentity :: Entity
  , health     :: Int
  , aimode     :: AIMode
  , shipprop   :: ShipProp
  , allegiance :: String
  }

modShipEntity :: (Entity -> Entity) -> Ship -> Ship
modShipEntity f t = t{shipentity = f (shipentity t)}

modHealth :: (Int -> Int) -> Ship -> Ship
modHealth f t = t{health = f (health t)}

data Combat = Combat {
    ship1          :: Ship
  , ship2          :: Ship
  , lasers         :: S.Seq Entity
  , combatPaused   :: Bool
  }

modShip1 :: (Ship -> Ship) -> Combat -> Combat
modShip1 f t = t{ship1 = f (ship1 t)}

modShip2 :: (Ship -> Ship) -> Combat -> Combat
modShip2 f t = t{ship2 = f (ship2 t)}

modLasers :: (S.Seq Entity -> S.Seq Entity) -> Combat -> Combat
modLasers f t = t{lasers = f (lasers t)}

modCombatPaused :: (Bool -> Bool) -> Combat -> Combat
modCombatPaused f t = t{combatPaused = f (combatPaused t)}

modShipN :: Int -> (Ship -> Ship) -> Combat -> Combat
modShipN 1 f = modShip1 f
modShipN 2 f = modShip2 f
modShipN _ _ = id

newStdShip :: String -> Maybe Int -> ShipProp -> GLvector3 -> Color4 GLfloat -> GLdouble -> AIMode -> Ship
newStdShip alleg mh prop pos c rot mode = 
  Ship (newStdShipEntity pos c rot)
       (fromMaybe (maxhealth prop) mh) mode prop alleg

type Enemy = (AIMode, ShipProp)

randomEnemy :: GLdouble -> IO Enemy
randomEnemy shift = do
  m <- randomAI
  case m of
    Shooter v -> do
      let m' = Shooter (clamp 0 1 (v + shift))
      if v < 0.15
        then return (m', cargovessel)
        else if v > 0.85
               then return (m', fightership)
               else do
                 n <- randomShipProp
                 return (m', n)
    t         -> do
      n <- randomShipProp
      return (t, n)

randomPolice :: GLdouble -> IO Enemy
randomPolice shift = do
  n <- randomRIO (40, 80 :: Int)
  return (Shooter (clamp 0 1 (shift + (fromIntegral n / 100))), modHoldspace (const 0) fightership)

describeEnemy :: Enemy -> String
describeEnemy (_, p) = shipdescr p

newCombat :: String -> String -> ShipProp -> Int -> GLvector3 -> GLvector3 -> GLdouble -> GLdouble -> Enemy -> Combat
newCombat alleg1 alleg2 plprop plhealth plpos enpos rot rot2 (mode, enprop) = 
  Combat (newStdShip alleg1 (Just plhealth) plprop plpos playerShipColor rot Human)
   (newStdShip alleg2 Nothing enprop enpos enemyShipColor rot2 mode)
   S.empty
   False

accelerateCombat n a = modify $ modShipN n $ modShipEntity $ modifyAcceleration (const (0.0,  a, 0.0))
turnCombat n a = modify $ modShipN n $ modShipEntity $ modifyAngVelocity (+a)
setTurnCombat n a = modify $ modShipN n $ modShipEntity $ modifyAngVelocity (const a)

laserLength :: GLdouble
laserLength = 1

laserSpeed :: GLdouble
laserSpeed = 1

shipNShoot :: Int -> StateT Combat IO ()
shipNShoot n = do
  state <- State.get
  let men = case n of
              1 -> Just $ shipentity $ ship1 state
              2 -> Just $ shipentity $ ship2 state
              _ -> Nothing
  case men of
    Nothing -> return ()
    Just en -> do
      let shippos = Entity.position en
          shipvel = Entity.velocity en
          shiprot = Entity.rotation en + 90
          lookVector = (cos (degToRad shiprot), sin (degToRad shiprot), 0)
          laserpos = shippos *+* (lookVector *** 3)
          laservel = shipvel *+* (lookVector *** laserSpeed)
          laserrot = shiprot
          lasercol = if n == 1 then Color4 0.0 1.0 0.0 1.0 else Color4 1.0 0.0 0.0 1.0
      let nent = Entity laserpos laservel glVector3Null laserrot 0 0 lasercol Lines [(1.0, 0.0, 0.0), (-1.0, 0.0, 0.0)] (glVector3AllUnit *** laserLength)
      modify $ modLasers $ S.rcons nent

combatMapping sprop = 
  [ (SDLK_w,     (accelerateCombat 1 (maxaccel sprop),    accelerateCombat 1 0))
  , (SDLK_s,     (accelerateCombat 1 (-maxaccel sprop), accelerateCombat 1 0))
  , (SDLK_a,     (turnCombat 1 (maxturn sprop), turnCombat 1 (-maxturn sprop)))
  , (SDLK_d,     (turnCombat 1 (-maxturn sprop), turnCombat 1 (maxturn sprop)))
  , (SDLK_UP,    (accelerateCombat 1 (maxaccel sprop), accelerateCombat 1 0))
  , (SDLK_DOWN,  (accelerateCombat 1 (-maxaccel sprop), accelerateCombat 1 0))
  , (SDLK_LEFT,  (turnCombat 1 (maxturn sprop), turnCombat 1 (-maxturn sprop)))
  , (SDLK_RIGHT, (turnCombat 1 (-maxturn sprop), turnCombat 1 (maxturn sprop)))
  , (SDLK_p,     (modify $ modCombatPaused not, return ()))
  , (SDLK_SPACE, (shipNShoot 1, return ()))
  , (SDLK_i,     (showCombatInfo, return ()))
  ]

showCombatInfo :: StateT Combat IO ()
showCombatInfo = do
  state <- State.get
  let mpos = Entity.position (shipentity $ ship2 state)
  let epos = Entity.position (shipentity $ ship1 state)
  let evel = Entity.velocity (shipentity $ ship1 state)
  let mvel = Entity.velocity (shipentity $ ship2 state)
  let mtgtpos = findHitpoint (epos *-* mpos) (evel *-* mvel) laserSpeed
  liftIO $ putStrLn $ "Pos diff: " ++ (show (epos *-* mpos))
  liftIO $ putStrLn $ "Player Velocity: " ++ (show evel)
  liftIO $ putStrLn $ "Hit point: " ++ (show mtgtpos)

handleTooFar :: StateT Combat IO Bool
handleTooFar = do
  state <- State.get
  let mpos = Entity.position (shipentity $ ship2 state)
  let epos = Entity.position (shipentity $ ship1 state)
  return $ OpenGLUtils.length (mpos *-* epos) > 200.0

-- Returns (player health, won points, won cargo (if any))
combatLoop :: StateT Combat IO (Int, Int, Maybe Cargo)
combatLoop = do
  liftIO $ delay 10
  state <- State.get
  drawCombat
  oneDead <- if combatPaused state
               then return 0
               else updateCombatState
  handleCombatAI
  toofar <- handleTooFar
  quits <- handleCombatEvents
  if quits || oneDead == 1
    then return (0, 0, Nothing)
    else if oneDead == 2
      then do
        c <- liftIO $ createRandomCargo (holdspace $ shipprop $ ship2 state)
        s' <- State.get
        return (health $ ship1 s', shippoints . shipprop $ ship2 s', Just c)
      else if toofar
             then return (health $ ship1 state, 0, Nothing)
             else combatLoop

createRandomCargo :: Int -> IO Cargo
createRandomCargo 0       = return M.empty
createRandomCargo maxhold = do
  let maxnumtypes = 3
  difftypes <- randomRIO (1, maxnumtypes :: Int)
  cnames <- replicateM difftypes randomCargo
  cargoquantities <- replicateM difftypes (randomRIO (1, maxhold `div` difftypes))
  return $ M.fromSeqWith (+) (zip cnames cargoquantities)

handleCombatAI :: StateT Combat IO ()
handleCombatAI = do
  state <- State.get
  case aimode (ship2 state) of
    Human     -> return ()
    Dummy     -> accelerateCombat 2 (maxaccel (shipprop (ship2 state)) * 0.5)
    Shooter n -> doShooter n

findHitpoint :: GLvector3 -- ^ target position relative to (0, 0, _)
  -> GLvector3 -- ^ target velocity
  -> GLdouble  -- ^ expanding rate of circle radius from own position
  -> Maybe GLvector3 -- ^ meeting point of target and radius
findHitpoint (a, q, _) (d, e, _) c 
  | c == 0 && d == 0 && e == 0 = Nothing
  | otherwise                  =
    let (t1, t2) = tps a q d e c
        tf = filter (>= 0) [t1, t2]
        tf' = minimum tf
    in if null tf
         then Nothing
         else Just (a + d * tf', q + e * tf', 0)

tps x y p q c = ((term1 - term2) / term3, (term1 + term2) / term3)
  where term1 = p * x + q * y
        term2 = sqrt (c*c*x*x - q*q*x*x + 2 * p * q * x * y + c*c*y*y - p*p*y*y)
        term3 = c*c - p*p - q*q

chargeTarget :: GLdouble -> StateT Combat IO ()
chargeTarget angleToTarget = do
  state <- State.get
  let myangle = degToRad $ wrapDegrees $ Entity.rotation (shipentity $ ship2 state) + 90
  let epsilon = 0.01
  let turnRate = maxturn $ shipprop $ ship2 state
  if myangle + epsilon < angleToTarget
    then setTurnCombat 2 turnRate
    else if myangle - epsilon > angleToTarget
           then setTurnCombat 2 (-turnRate)
           else setTurnCombat 2 0
  when (abs (myangle - angleToTarget) < 0.3) $ do
    val <- liftIO $ randomRIO (0, 15 :: Int)
    when (val == 0) $ shipNShoot 2
  accelerateCombat 2 (maxaccel $ shipprop $ ship2 state)

doShooter :: GLdouble -> StateT Combat IO ()
doShooter n = do
  state <- State.get
  let mpos = Entity.position (shipentity $ ship2 state)
  let epos = Entity.position (shipentity $ ship1 state)
  let evel = Entity.velocity (shipentity $ ship1 state)
  let estEnemyVel = evel *** n
  let mvel = Entity.velocity (shipentity $ ship2 state)
  let mtgtpos = findHitpoint (epos *-* mpos) (estEnemyVel *-* mvel) laserSpeed
  let angleToEnemy = case mtgtpos of
                       Nothing     -> angleFromToRad mpos epos
                       Just tgtpos -> angleFromToRad glVector3Null tgtpos 
  chargeTarget angleToEnemy

drawCombat :: StateT Combat IO ()
drawCombat = do
  state <- State.get
  let (x1, y1, _) = Entity.position (shipentity $ ship1 state)
      (x2, y2, _) = Entity.position (shipentity $ ship2 state)
      ((minx', maxx'), (miny', maxy')) = boxThatIncludes (x1, x2) (y1, y2) 10 10 width height
  inOrthoBoxDraw minx' maxx' miny' maxy' (-10) 10
    (liftIO $ 
        drawGLScreen Nothing 
            ([shipentity (ship1 state), shipentity (ship2 state)] ++ (S.toList (lasers state))) 
            nullAObjTree)

updateCombatState :: StateT Combat IO Int
updateCombatState = do
  modify $ modLasers $ S.map (updateEntity 1)
  modify $ modShip1 $ modShipEntity (updateEntity 1)
  modify $ modShip2 $ modShipEntity (updateEntity 1)
  handleCombatCollisions
  state <- State.get
  if (health (ship1 state) <= 0)
    then return 1
    else if (health (ship2 state) <= 0)
           then return 2
           else return 0

handleCombatEvents :: StateT Combat IO Bool
handleCombatEvents = do
  events <- liftIO $ pollAllSDLEvents
  state <- State.get
  processEvents (combatMapping (shipprop $ ship1 state)) events
  return $ isQuit events

checkCollision ::
     ((GLdouble, GLdouble), (GLdouble, GLdouble))
     -> ((GLdouble, GLdouble), (GLdouble, GLdouble))
     -> Entity
     -> Either Int Entity
checkCollision plbox1 plbox2 las =
  if collides2d plbox1 ((minx, maxx), (miny, maxy))
    then Left 1
    else if collides2d plbox2 ((minx, maxx), (miny, maxy))
           then Left 2
           else Right las
   where (lposx, lposy, _) = Entity.position las
         lrot = degToRad $ Entity.rotation las
         lx1 = lposx - laserLength * cos lrot
         lx2 = lposx + laserLength * cos lrot
         ly1 = lposy - laserLength * sin lrot
         ly2 = lposy + laserLength * sin lrot
         minx = min lx1 lx2
         maxx = max lx1 lx2
         miny = min ly1 ly2
         maxy = max ly1 ly2

handleCombatCollisions :: StateT Combat IO ()
handleCombatCollisions = do
  state <- State.get
  let plbox1 = getShipBox (shipentity $ ship1 state)
  let plbox2 = getShipBox (shipentity $ ship2 state)
  let colls = map (checkCollision plbox1 plbox2) (S.toList $ lasers state)
  let (hits, newlasers) = partitionEithers colls
  let numhits1 = Prelude.length $ filter (==1) hits
  let numhits2 = Prelude.length $ filter (==2) hits
  -- when (numhits1 > 0) $ liftIO $ putStrLn "Ship 1 hit!"
  -- when (numhits2 > 0) $ liftIO $ putStrLn "Ship 2 hit!"
  modify $ modLasers $ const (S.fromList newlasers)
  modify $ modShipN 1 $ modHealth (subtract numhits1)
  modify $ modShipN 2 $ modHealth (subtract numhits2)