dow-0.2.6: src/Game.hs
{-# LANGUAGE RecursiveDo #-}
module Game where
import Control.Applicative
import Control.Monad
import Control.Monad.Fix
import Data.Array
import Data.List
import Data.Maybe
import FRP.Elerea.Simple
import Actor
import GraphUtils
import HighScore
import Level
import Sprites
import Utils
import Vector
numberOfLives = 5
data LevelState = LevelState
{ level :: Level
, actors :: [Actor]
, bullets :: [(ActorType,Vec)]
, shooters :: [ActorType]
}
keySet1 = fst
keySet2 = snd
keyDir (True,_,_,_,_) = Just North
keyDir (_,True,_,_,_) = Just South
keyDir (_,_,True,_,_) = Just West
keyDir (_,_,_,True,_) = Just East
keyDir _ = Nothing
keyShoot (_,_,_,_,s) = s
keyAny k = keyShoot k || isJust (keyDir k)
game highScore storeScore (renderGame,renderMenu) closeAction newActor levels keyPress = do
let firstTrue s = do
mask <- delay False =<< transfer False (||) s
return (liftA2 (&&) (not <$> mask) s)
mkGame 0 = playGame storeScore renderGame newActor levels keyPress 1
mkGame 1 = playGame storeScore renderGame newActor levels keyPress 2
mkGame _ = return (pure (void closeAction),pure True)
keys <- memo (keySet1 <$> keyPress)
(output,_) <- switcher . flip fmap highScore $ \score -> do
(menu,pick) <- displayMenu (renderMenu score) ["ONE PLAYER GAME","TWO PLAYER GAME","QUIT"] keys
picked <- firstTrue (keyShoot <$> keys)
gameSource <- generator (toMaybe <$> picked <*> (mkGame <$> pick))
fullOutput <- menu --> gameSource
return (fst =<< fullOutput,snd =<< fullOutput)
return output
displayMenu renderMenu items keys = do
let val False = 0
val True = 1
up <- edge ((==Just North) . keyDir <$> keys)
down <- edge ((==Just South) . keyDir <$> keys)
item <- transfer2 0 (\u d i -> (i + val d - val u) `mod` length items) up down
return ((renderMenu items <$> item,pure False),item)
playGame storeScore renderFun newActor levels keyPress numPlayers = mdo
let startLevel level enemyCount = playLevel newActor keyPress level numPlayers lives' enemyCount
players = take numPlayers [YellowWorrior,BlueWorrior]
pickLevel cnt rnd = case () of
_ | cnt == 4 -> findLevel (=="arena")
_ | cnt < 8 -> findLevel ((=='b').head)
_ | cnt `mod` 6 == 1 -> findLevel (=="pit")
otherwise -> findLevel ((=='w').head)
where findLevel p = pickOne (filter (p.levelName) levels)
pickOne xs = xs !! (rnd `mod` length xs)
accumScore player state prev = max 0 $ prev +
(sum . map killScore . actors) state -
10 * (length . filter (==player) . shooters) state
where killScore a = if justDied a && action a == KilledBy player then actorValue a else 0
accumLives player state prev = max 0 $ prev + (sum . map lifeLost . actors) state
where lifeLost a = if justDied a && actorType a == player then -1 else 0
levelNoise <- noise
(state,trigLevel) <- switcher (startLevel <$> (pickLevel <$> levelCount <*> levelNoise) <*> levelCount)
trigLevel' <- delay False trigLevel
levelCount <- transfer 1 (\win cnt -> if win then cnt+1 else cnt) trigLevel'
scores <- memo =<< (sequence <$> forM players (\t -> transfer 0 (accumScore t) state))
lives <- memo =<< (sequence <$> forM players (\t -> transfer numberOfLives (accumLives t) state))
lives' <- delay (replicate numPlayers numberOfLives) lives
gameOver <- memo (all (==0) <$> lives)
return (liftA2 (>>)
((\c s -> when c (storeScore s)) <$> gameOver <*> scores)
(renderFun <$> state <*> levelCount <*> scores <*> lives)
,gameOver
)
playLevel newActor keyPress level numPlayers lives enemyCount = mdo
let mkEnemy etype = enemy (newActor etype) (if etype == Worluk then 2 else 1) level bullets
mkShot c plr = if c && canShoot
then (:[]) <$> bullet (actorType plr) (position plr) (facing plr)
else return []
where canShoot = case action plr of
Walking -> True
Shooting -> True
_ -> False
spawnEnemies oldEnemies = concatMap (spawnEnemy (length oldEnemies == 1)) oldEnemies
spawnEnemy isLast enemy = if justDied enemy then
case actorType enemy of
Burwor -> [mkEnemy Garwor <$> (position <$> head players)]
Garwor -> [mkEnemy Thorwor <$> (position <$> head players)]
Thorwor -> if isLast then [mkEnemy Worluk <$> (position <$> head players)] else []
_ -> []
else []
(lw,lh) = levelSize level
playerInit = take numPlayers [(YellowWorrior,keySet1,1),(BlueWorrior,keySet2,0)]
(players,bulletSources) <- fmap unzip $ forM (zip playerInit [0..]) $ \((worType,keySet,pix),lix) -> do
shoot <- edge (keyShoot . keySet <$> keyPress)
(player,_respawn) <- switcher . flip fmap lives $ \ls -> do
let (pedir,py,px) = entrances level !! pix
playerInit = (newActor worType (V (px*fieldSize) ((lh-py-1)*fieldSize)))
{ action = Entering pedir False
, facing = [East,West] !! pix
}
respawn plr = ls !! lix > 1 && null (animation plr)
if ls !! lix == 0
then return (pure (playerInit { animation = [] }),pure False)
else do plr <- transfer3 playerInit (movePlayer level) (keyDir . keySet <$> keyPress) shoot enemies'
return (plr, respawn <$> plr)
bulletSource <- generator (mkShot <$> shoot <*> player)
return (player,bulletSource)
newBullets <- memo (concat <$> sequence bulletSources)
bullets <- collection newBullets (notHitAnything level <$> enemies')
let shooters = fmap (map fst) . sequence =<< newBullets
initialEnemies <- replicateM enemyCount (mkEnemy Burwor (V (-3*fieldSize) (-3*fieldSize)))
spawnedEnemies <- generator (sequence <$> (sequence . spawnEnemies =<< enemies'))
enemySource <- delay initialEnemies spawnedEnemies
enemies <- collection enemySource (pure (not . null . animation))
enemies' <- delay [] enemies
return (LevelState level <$> (liftA2 (++) (sequence players) enemies) <*> bullets <*> shooters
,null <$> enemies
)
bullet src pos dir = fmap ((,) src) <$> stateful pos (+3*dirVec dir)
enemy newActor moveSpeed level bullets (V playerX playerY) = mdo
let actorInit = (newActor (fromIntegral fieldSize*startPos)) { speed = moveSpeed, animSpeed = 4 }
startPos = if abs (startX-playerX `div` fieldSize) < 3 && abs (startY-playerY `div` fieldSize) < 3
then V ((startX + lw `div` 2) `mod` lw) ((startY + lh `div` 2) `mod` lh)
else V startX startY
(lw,lh) = levelSize level
startX <- (`mod` lw) <$> getRandom
startY <- (`mod` lh) <$> getRandom
startDir <- toEnum . (`mod` 4) <$> getRandom
dirNoise <- noise
actorInput <- startDir --> newDir level <$> dirNoise <*> actor'
actor <- transfer2 actorInit (moveEnemy level) actorInput bullets
actor' <- delay actorInit actor
return actor
notHitAnything lev es (_,pos@(V px py)) =
(sy < fieldMid-bs || North `elem` legals) &&
(sx < fieldMid-bs || East `elem` legals) &&
(sy > -fieldMid+bs || South `elem` legals) &&
(sx > -fieldMid+bs || West `elem` legals) &&
not hitExplosion
where legals = legalMovesAt lev (fieldPos pos)
(sx,sy) = fieldSub pos
bs = 3
hitExplosion = any hitBy es
hitBy e = isDead e && abs (ex-px) < fieldMid && abs (ey-py) < fieldMid
where V ex ey = position e
newDir lev rnd act = if canMove lev act && (isHalfway || rnd `mod` 1000 < 500)
then Nothing
else Just pickedDir
where legal = legalMovesAt lev (fieldPos (position act))
dirPool = if length legal < 2 then legal else legal \\ [turnBack (facing act)]
pickedDir = dirPool !! (rnd `mod` length dirPool)
isHalfway = halfway (fieldSub (position act)) (facing act)
movePlayer level mov shoot enemies plr = case action plr of
Entering dir False -> let startMoving = isJust mov
in plr { action = Entering dir startMoving
, position = position plr + if startMoving then dirVec dir else 0
}
Entering dir True -> if fieldSub (position plr) == (0,0)
then plr { action = Walking }
else plr { position = position plr + dirVec dir }
_ -> case mov of
Nothing -> if action plr' /= Walking then animate plr' else plr'
Just dir -> animate (if isAlive plr then move level dir plr' else plr')
where plr' = case killed of
_ | isDead plr -> plr
Just enemy -> plr { animation = deathAnimation (skin plr)
, action = KilledBy (actorType enemy)
, animSpeed = 6
}
_ | shoot -> plr { animation = shootAnimation (skin plr)
, action = Shooting
}
otherwise -> plr
killed = find ((==getXY plr).getXY) [enemy | enemy <- enemies, isAlive enemy]
getXY = fieldPos.position
moveEnemy level dir bs act = animate (mv act')
where act' = case hit of
Just (killer,_) -> act { animation = deathAnimation (skin act)
, action = KilledBy killer
}
Nothing -> act
mv = if isAlive act' then move level dir else id
hit = find hitBy bs
V px py = position act
hitBy (_,(V bx by)) = abs (bx-px) < fieldMid && abs (by-py) < fieldMid
move :: Level -> Direction -> Actor -> Actor
move lev dir ent = mv ent $ fmap snd . find fst $
[(isHalfway dir || isAtMid && dirIsLegal, dir)
,(not isAtMid && dirIsLegal, dir')
,(isHalfway entDir || legalMove entDir, entDir)
]
where mv e Nothing = e
mv e (Just d) = e { position = position e + fromIntegral (speed e) * dirVec d
, facing = d
}
dirIsLegal = legalMove dir
legalMove d = d `elem` legalMovesAt lev (fieldPos (position ent))
isAtMid = atFieldMid sub dir
isHalfway = halfway sub
sub@(sx,sy) = fieldSub (position ent)
entDir = facing ent
dir' = case entDir of
North | sy > 0 -> South
East | sx > 0 -> West
South | sy < 0 -> North
West | sx < 0 -> East
otherwise -> entDir
canMove :: Level -> Actor -> Bool
canMove lev ent = halfway || facing ent `elem` legalMovesAt lev (fieldPos (position ent))
where (sx,sy) = fieldSub (position ent)
halfway = sx /= 0 || sy /= 0
legalMovesAt lev pos = legalMoves lev ! ix pos
where ix (x,y) = (snd (levelSize lev)-1-y,x)
animate :: Actor -> Actor
animate ent = let ent' = ent { tick = (tick ent+1) `mod` animSpeed ent }
adv = if tick ent' == 0 then 1 else 0
animation' = drop adv (animation ent') in
case action ent' of
Entering _ _ -> ent'
Walking -> ent' { animation = animation' }
Shooting -> if null animation'
then ent' { animation = walkAnimation (skin ent')
, action = Walking
}
else ent' { animation = animation' }
KilledBy _ -> ent' { animation = animation' }
dirVec North = V 0 1
dirVec East = V 1 0
dirVec South = V 0 (-1)
dirVec West = V (-1) 0