packages feed

Eternal10Seconds-0.1: Stage.hs

module Stage where

import Maybe
import Monad
import Random
import qualified Graphics.UI.SDL as SDL
import Resource
import Input
import Pos
import Number

-- 画面サイズ
screenWidth = 800 :: Int
screenHeight = 600 :: Int

-- 自機
data Player = Player {
    playerPos :: Pos,
    playerAmmo :: Int
}

playerPaint p = let (x, y) = playerPos p in
    (playerTip, Just (SDL.Rect (x-15) (y-15) 30 30))

updatePlayer :: Bool -> Player -> SDL.SDLKey -> Player
updatePlayer isShift p key = let
    (x, y) = playerPos p
    speed = if isShift then 2 else 5 in
    case key of
        SDL.SDLK_UP -> boundArea $ p { playerPos = (x, y - speed) }
        SDL.SDLK_DOWN -> boundArea $ p { playerPos = (x, y + speed) }
        SDL.SDLK_LEFT -> boundArea $ p { playerPos = (x - speed, y) }
        SDL.SDLK_RIGHT -> boundArea $ p { playerPos = (x + speed, y) }
        otherwise -> p

-- 敵
data Enemy = Enemy {
    enemyPos :: Pos,
    enemySpeed :: Int,
    enemyType :: EnemyType
}

data EnemyType = Small | Normal | Large

enemySize :: Enemy -> (Int, Int)
enemySize em = case enemyType em of
    Small -> (50, 43)
    Normal -> (100, 87)
    Large -> (200, 173)

enemyTip :: Enemy -> PaintTip
enemyTip em = case enemyType em of
    Small -> enemy50Tip
    Normal -> enemy100Tip
    Large -> enemy200Tip

enemyPaint em = let
    (x, y) = enemyPos em
    (w, h) = enemySize em in
    (enemyTip em, Just $ SDL.Rect x y w h)

moveEnemy :: Enemy -> Enemy
moveEnemy (Enemy (x, y) spd t) = Enemy (x, y+spd) spd t


-- 弾
data Bullet = Bullet {
    bulletPos :: Pos
}

bulletSpeed = 20

moveBullet :: Bullet -> Bullet
moveBullet b@Bullet { bulletPos = (x, y) } = b { bulletPos = (x, y-bulletSpeed) }

bulletPaint bl = let
    (x, y) = bulletPos bl in
    (bulletTip, Just (SDL.Rect (x-5) (y-5) 10 10))

-- 経過時間
type Time = Int

milisec :: Time -> Int
milisec time = div (time * 1000) 60

-- ステージ画面
data Stage = Stage {
    stagePlayer :: Player,
    stageEnemy :: [Enemy],
    stageBullet :: [Bullet],
    stageTime :: Time
}

initStage = Stage (Player (200, 500) 3) [] [] 0

stagePaint = (("./resource/image/stage.bmp", Nothing), Nothing)
sidePaint = (("./resource/image/side.bmp", Nothing), Just (SDL.Rect 500 0 300 600))
scorePaint = (("./resource/image/score.bmp", Nothing), Just (SDL.Rect 550 70 150 30))
bulletsPaint = (("./resource/image/bullets.bmp", Nothing), Just (SDL.Rect 550 170 150 30))
extendPaint = (("./resource/image/extend.bmp", Nothing), Just (SDL.Rect 550 260 100 20))
playerTip = ("./resource/image/player.bmp", Nothing)
enemy50Tip = ("./resource/image/enemy50.bmp", Nothing)
enemy100Tip = ("./resource/image/enemy100.bmp", Nothing)
enemy200Tip = ("./resource/image/enemy200.bmp", Nothing)
bulletTip = ("./resource/image/bullet.bmp", Nothing)

barBmp = "./resource/image/bar.bmp"

barPaint :: Time -> Paint
barPaint time = let
    width = 200 - (mod (div ((milisec time)*200) 10000) 200) in
    ((barBmp, Just (SDL.Rect 0 0 width 10)), Just (SDL.Rect 570 290 width 10))

stagePaints = [stagePaint, sidePaint, scorePaint, bulletsPaint, extendPaint]
stageTips = [playerTip, enemy50Tip, enemy100Tip, enemy200Tip, bulletTip]
stageBmps = [numberBmp, barBmp]
stageImgFiles = stageBmps ++ (map fst stageTips) ++ (map (fst.fst) stagePaints)

stageBgm = "./resource/sound/002_STG_fly_high_oggmst.ogg"


-- 敵の生成
-- 60秒ごとにNormal、Small、Largeが多く出るように循環
createEnemyType :: Time -> IO EnemyType
createEnemyType time = let
    normal = (mod ((div (milisec time) 20000)+1) 3) * 85 - 80 in do
    gen <- getStdRandom (randomR (0::Int, 100))
    if gen < normal then return Small else do
    if gen < normal + 90 then return Normal else do
    return Large

-- 敵を生成するか
isCreateEnemy :: Time -> EnemyType -> IO Bool
isCreateEnemy time et = do
    gen <- getStdRandom (randomR (0::Int, 100))
    return $ case et of
        Small -> gen > 87 - div (milisec time) 60000 -- 71
        Normal -> gen > 92 - div (milisec time) 180000 -- 87
        Large -> gen > 97 - div (milisec time) 360000 -- 95

-- 敵の速度
-- 30秒ごとに増加するが180秒経過でリセット
createEnemySpeed :: Time -> IO Int
createEnemySpeed time = let
    base = 3 + (div (milisec time) 360000) in do
    getStdRandom (randomR (1, base + (div (mod (milisec time) 180000) 30000))) -- 10

createEnemies :: Time -> IO [Enemy]
createEnemies time = do
    et <- createEnemyType time
    
    flg <- isCreateEnemy time et
    if not flg then return [] else do
    
    speed <- createEnemySpeed time
    
    x <- getStdRandom (randomR (-80, 500))
    let cand = Enemy (x, 0) speed et
        (ew, eh) = enemySize cand
    return [cand { enemyPos = (x, -eh) }]

createPlayerBullet :: Player -> ([Bullet], Player)
createPlayerBullet p =
    if playerAmmo p > 0 then
    ([Bullet (playerPos p)],
        Player (playerPos p) ((playerAmmo p)-1)) else
    ([], p)


-- 当り判定
isHitEnemy :: Enemy -> Pos -> Bool
isHitEnemy em (x, y) = let
    (ex, ey) = enemyPos em
    (ew, eh) = enemySize em
    (dx, dy) = (fromIntegral ((x - ex)::Int)::Float, fromIntegral ((y - ey)::Int)::Float) in
    and [dy > 0.0, (-(sqrt 3))*dx+dy < 0.0, (sqrt 3)*dx+dy < (sqrt 3)*(fromIntegral ew)]

isHitEnemyPlayer e p = isHitEnemy e (playerPos p)

isLineCross :: (Pos, Pos) -> (Pos, Pos) -> Bool
isLineCross (p1, p2) (p3, p4) = let -- 桁あふれ対策
    (x1, y1) = fromPos p1 :: RealPos
    (x2, y2) = fromPos p2 :: RealPos
    (x3, y3) = fromPos p3 :: RealPos
    (x4, y4) = fromPos p4 :: RealPos in
    ((x1-x2)*(y3-y1)+(y1-y2)*(x1-x3)) * ((x1-x2)*(y4-y1)+(y1-y2)*(x1-x4)) < 0 &&
    ((x3-x4)*(y1-y3)+(y3-y4)*(x3-x1)) * ((x3-x4)*(y2-y3)+(y3-y4)*(x3-x2)) < 0

isLineCrossYInv :: (Pos, Pos) -> (Pos, Pos) -> Bool
isLineCrossYInv ((x1, y1), (x2, y2)) ((x3, y3), (x4, y4)) =
    isLineCross ((x1, -y1), (x2, -y2)) ((x3, -y3), (x4, -y4))

isHitEnemyBullet :: Enemy -> Bullet -> Bool
isHitEnemyBullet em blt = let
    (ex, ey) = enemyPos em
    (ew, eh) = enemySize em
    (bx, by) = bulletPos blt in
    or $ isHitEnemy em (bx, by) : map (isLineCrossYInv ((bx, by), (bx, by+bulletSpeed)))
        [((ex, ey), (ex+ew, ey)),
        ((ex, ey), (ex+(div ew 2), ey+eh)),
        ((ex+ew, ey), (ex+(div ew 2), ey+eh))]

mostFrontEnemy :: [Enemy] -> Maybe Enemy
mostFrontEnemy [] = Nothing
mostFrontEnemy [e] = Just e
mostFrontEnemy (e:f:ems) = if (snd $ enemyPos e) > (snd $ enemyPos f) then
    mostFrontEnemy $ e:ems else
    mostFrontEnemy $ f:ems


-- 更新
isInArea :: Pos -> Bool
isInArea (x, y) = if x >= 0 && x <= 500 && y >= 0 && y < screenHeight+15 then True
    else False

isInAreaEnemy :: Enemy -> Bool
isInAreaEnemy em = let
    (ex, ey) = enemyPos em
    (w, h) = enemySize em in
    isInArea (ex, ey) || isInArea (ex, ey+h) || isInArea (ex+w, ey) || isInArea (ex+w, ey+h)

boundArea :: Player -> Player
boundArea p = let
    (x, y) = playerPos p
    newX = if x < 0 then 0 else if x > 500 then 500 else x
    newY = if y < 0 then 0 else if y > screenHeight then screenHeight else y in
    Player (newX, newY) (playerAmmo p)

moveObjects :: Stage -> Input -> Stage
moveObjects s@Stage { stagePlayer = p, stageEnemy = ems, stageBullet = bls } input = let
    isShift = hasKey input SDL.SDLK_LSHIFT
    movedPly = foldl (updatePlayer isShift) p (inputKey input)
    movedEms = filter (isInAreaEnemy) $ map moveEnemy ems
    movedBls = filter (isInArea.bulletPos) $ map moveBullet bls in
    s { stagePlayer = movedPly, stageEnemy = movedEms, stageBullet = movedBls}

chargeAmmo :: Stage -> Stage
chargeAmmo s@Stage { stagePlayer = p, stageTime = t } = let
    ammo = playerAmmo p
    newAmmo = if t /= 0 && (mod t 600) == 0 then ammo + 1 else ammo in
    s { stagePlayer = (Player (playerPos p) newAmmo) }

filterAlive :: Stage -> Maybe Stage
filterAlive s@Stage { stagePlayer = p, stageEnemy = ems, stageBullet = bls } = let
    hits = mapMaybe (\b -> mostFrontEnemy [e|e<-ems, isHitEnemyBullet e b]) bls
    aliveEms = filter (\e -> and $ map (\h->(enemyPos h)/=(enemyPos e)) hits) ems
    aliveBls = filter (\b -> and $ map (not.(`isHitEnemyBullet` b)) ems) bls in
    if and $ map (not.(`isHitEnemyPlayer` p)) aliveEms then
        Just $ s { stageEnemy = aliveEms, stageBullet = aliveBls } else
        Nothing

-- 描画
renderTime :: Time -> SDL.Surface -> Resource -> IO ()
renderTime time surf res = renderNumber surf res numberBmp numberSize (600,100) (milisec time)

renderAmmo :: Player -> SDL.Surface -> Resource -> IO ()
renderAmmo p surf res = renderNumber surf res numberBmp numberSize (600,200) (playerAmmo p)

renderStage :: Stage -> SDL.Surface -> Resource -> IO ()
renderStage (Stage p ems bls time) surf res = do
    if time == 0 then playSound res stageBgm (-1) else return ()
    
    renderPaint res surf stagePaint
    
    renderPaint res surf $ playerPaint p
    mapM_ (renderPaint res surf) $ map enemyPaint ems
    mapM_ (renderPaint res surf) $ map bulletPaint bls
    
    mapM_ (renderPaint res surf) [sidePaint, bulletsPaint, scorePaint, extendPaint]
    renderAmmo p surf res    
    renderTime time surf res
    renderPaint res surf $ barPaint time
    
    return ()

-- リソースの更新
addStageResource :: Resource -> IO Resource
addStageResource res = do
    ires <- foldM addImageResource res stageImgFiles
    addSoundResource ires stageBgm