module Main exposing (..) import Time exposing (..) import List exposing (..) import AnimationFrame import Keyboard.Extra exposing (Key(..)) import Window import Collage exposing (..) import Element exposing (..) import Color exposing (..) import Html exposing (Html) import Debug -- MODEL type State = NewGame | Play | GameOver | Pause -- MODEL type alias Player = { angle : Float } type Direction = Left | Right | Still type alias Game = { player : Player , direction : Direction , pressedKeys : List Key , state : State , timeStart : Time , timeTick : Time , msRunning : Float , autoRotateAngle : Float , autoRotateSpeed : Float } type alias Colors = { dark : Color , medium : Color , bright : Color } type Msg = Step Time | KeyboardMsg Keyboard.Extra.Msg ( gameWidth, gameHeight ) = ( 1024, 576 ) ( halfWidth, halfHeight ) = ( gameWidth / 2, gameHeight / 2 ) ( iHalfWidth, iHalfHeight ) = ( gameWidth // 2, gameHeight // 2 ) playerRadius : Float playerRadius = gameWidth / 10.0 playerSize : Float playerSize = 10.0 playerSpeed : Float playerSpeed = 0.12 bgBlack : Color bgBlack = rgb 20 20 20 -- UPDATE updatePlayerAngle : Float -> Direction -> Float updatePlayerAngle angle dir = let sign = if dir == Left then 1 else if dir == Right then -1 else 0 newAngle = angle + toFloat sign * playerSpeed in if newAngle < 0 then newAngle + 2 * pi else if newAngle > 2 * pi then newAngle - 2 * pi else newAngle updatePlayer : Direction -> Game -> Player updatePlayer dir { player, state } = if state == Play then let newAngle = if state == NewGame then degrees 30 else updatePlayerAngle player.angle dir in { player | angle = newAngle } else player isGameOver : Game -> Bool isGameOver { player } = False updateMsRunning : Time -> Game -> Time updateMsRunning timestamp game = case game.state of Play -> game.msRunning + timestamp - game.timeTick NewGame -> 0.0 _ -> game.msRunning updateAutoRotateAngle : Game -> Float updateAutoRotateAngle { autoRotateAngle, autoRotateSpeed } = autoRotateAngle + autoRotateSpeed updateAutoRotateSpeed : Game -> Float updateAutoRotateSpeed { msRunning, autoRotateSpeed } = 0.02 * sin (msRunning * 0.0003 |> Debug.log "φ") |> Debug.log "autoRotateSpeed" {-| Updates the game state on a keyboard command -} onUserInput : Keyboard.Extra.Msg -> Game -> ( Game, Cmd Msg ) onUserInput keyMsg game = let pressedKeys = Keyboard.Extra.update keyMsg game.pressedKeys spacebar = List.member Keyboard.Extra.Space pressedKeys && not (List.member Keyboard.Extra.Space game.pressedKeys) direction = if (Keyboard.Extra.arrows pressedKeys).x < 0 then Left else if (Keyboard.Extra.arrows pressedKeys).x > 0 then Right else Still nextState = case game.state of NewGame -> if spacebar then Play else NewGame Play -> if spacebar then Pause else Play GameOver -> if spacebar then NewGame else GameOver Pause -> if spacebar then Play else Pause in ( { game | pressedKeys = pressedKeys , direction = direction , state = nextState } , Cmd.none ) {-| Updates the game state on every frame -} onFrame : Time -> Game -> ( Game, Cmd Msg ) onFrame time game = let nextCmd = Cmd.none nextState = case game.state of NewGame -> NewGame Play -> if isGameOver game then GameOver else Play _ -> game.state in ( { game | player = updatePlayer game.direction game , state = Debug.log "state" nextState , timeStart = if game.state == NewGame then time else game.timeStart , timeTick = time , msRunning = Debug.log "msRunning" (updateMsRunning time game) , autoRotateAngle = updateAutoRotateAngle game , autoRotateSpeed = updateAutoRotateSpeed game } , nextCmd ) {-| Game loop: Transition from one state to the next. -} update : Msg -> Game -> ( Game, Cmd Msg ) update msg game = case msg of KeyboardMsg keyMsg -> onUserInput keyMsg game Step time -> onFrame time game -- VIEW moveRadial : Float -> Float -> Form -> Form moveRadial angle radius = move ( radius * cos angle, radius * sin angle ) makePlayer : Player -> Form makePlayer player = let angle = player.angle - degrees 30 in ngon 3 playerSize |> filled (hsl angle 1 0.5) |> moveRadial angle (playerRadius - playerSize) |> rotate angle hexagonElement : Int -> List ( Float, Float ) hexagonElement i = let radius = halfWidth * sqrt 2 angle0 = 60 * i |> toFloat |> degrees angle1 = 60 * (i + 1) |> toFloat |> degrees in [ ( 0.0, 0.0 ) , ( sin angle0 * radius, cos angle0 * radius ) , ( sin angle1 * radius, cos angle1 * radius ) ] makeField : Colors -> Form makeField colors = let color i = if i % 2 == 0 then colors.dark else colors.medium poly i = polygon (hexagonElement i) |> filled (color i) in group (map poly (List.range 0 5)) -- the polygon in the center: this is just decoration, so it has no own state makeCenterHole : Colors -> Game -> List Form makeCenterHole colors game = let shape = ngon 6 60 line = solid colors.bright in [ shape |> filled colors.dark |> rotate (degrees 90) , shape |> (outlined { line | width = 4.0 }) |> rotate (degrees 90) ] makeColors : Float -> Colors makeColors msRunning = let hue = 0.00005 * msRunning in { dark = (hsl hue 0.6 0.2) , medium = (hsl hue 0.6 0.3) , bright = (hsla hue 0.6 0.6 0.8) } view : Game -> Html.Html Msg view game = let bg = rect gameWidth gameHeight |> filled bgBlack colors = makeColors game.msRunning field = append [ makeField colors , makePlayer game.player ] (makeCenterHole colors game) |> group in toHtml <| container gameWidth gameHeight middle <| collage gameWidth gameHeight [ bg , field |> rotate game.autoRotateAngle ] -- SUBSCRIPTIONS subscriptions : Game -> Sub Msg subscriptions game = Sub.batch [ AnimationFrame.times (\time -> Step time) , Sub.map KeyboardMsg Keyboard.Extra.subscriptions ] --INIT init : ( Game, Cmd Msg ) init = ( { player = Player (degrees 30) , pressedKeys = [] , direction = Still , state = NewGame , timeStart = 0.0 , timeTick = 0.0 , msRunning = 0.0 , autoRotateAngle = 0.0 , autoRotateSpeed = 0.0 } , Cmd.none ) main = Html.program { init = init , update = update , view = view , subscriptions = subscriptions }