/website/Shpadoinkle/Website/Page/FourOhFour.hs

https://gitlab.com/platonic/shpadoinkle · Haskell · 175 lines · 125 code · 36 blank · 14 comment · 8 complexity · 113b9c05a663393544b25dbb9561eaa7 MD5 · raw file

  1. {-# LANGUAGE DataKinds #-}
  2. {-# LANGUAGE DeriveAnyClass #-}
  3. {-# LANGUAGE DeriveGeneric #-}
  4. {-# LANGUAGE DerivingStrategies #-}
  5. {-# LANGUAGE ExtendedDefaultRules #-}
  6. {-# LANGUAGE GeneralizedNewtypeDeriving #-}
  7. {-# LANGUAGE KindSignatures #-}
  8. {-# LANGUAGE LambdaCase #-}
  9. {-# LANGUAGE OverloadedStrings #-}
  10. {-# LANGUAGE PatternSynonyms #-}
  11. {-# LANGUAGE ScopedTypeVariables #-}
  12. {-# LANGUAGE TemplateHaskell #-}
  13. {-# LANGUAGE TupleSections #-}
  14. {-# OPTIONS_GHC -fno-warn-type-defaults #-}
  15. module Shpadoinkle.Website.Page.FourOhFour where
  16. import Control.Concurrent.STM (TVar, atomically,
  17. modifyTVar, retry)
  18. import Control.Monad.IO.Class (liftIO)
  19. import Data.Text (Text)
  20. import GHC.Generics (Generic)
  21. import GHCJS.DOM (currentDocumentUnchecked,
  22. currentWindowUnchecked)
  23. import GHCJS.DOM.Document (createElement)
  24. import GHCJS.DOM.Element (setId)
  25. import GHCJS.DOM.NonElementParentNode (getElementById)
  26. import GHCJS.DOM.RequestAnimationFrameCallback (RequestAnimationFrameCallback,
  27. newRequestAnimationFrameCallback)
  28. import GHCJS.DOM.Window (Window,
  29. requestAnimationFrame)
  30. import Language.Javascript.JSaddle (toJSVal)
  31. import Shpadoinkle (JSM, NFData,
  32. RawNode (..),
  33. newTVarIO,
  34. shpadoinkle)
  35. import Shpadoinkle.Backend.Snabbdom (runSnabbdom)
  36. import Shpadoinkle.Html as H
  37. import Shpadoinkle.Html.TH.AssetLink (assetLink)
  38. import Shpadoinkle.Keyboard (pattern Ctrl,
  39. pattern LeftArrow,
  40. pattern RightArrow)
  41. import UnliftIO.Concurrent (forkIO, threadDelay)
  42. default (Text)
  43. data Direction = FaceLeft | FaceRight
  44. deriving (Eq, Ord, Show, Generic, NFData)
  45. data State = Idle | Walking | Shooting Clock
  46. deriving (Eq, Ord, Show, Generic, NFData)
  47. newtype Position = Position { unPosition :: Float }
  48. deriving stock Generic
  49. deriving newtype (Eq, Ord, Num, Show) deriving anyclass (NFData)
  50. newtype Clock = Clock { unClock :: Double }
  51. deriving stock Generic
  52. deriving newtype (Eq, Ord, Num, Show) deriving anyclass (NFData)
  53. data Game = Game
  54. { position :: Position
  55. , clock :: Clock
  56. , state :: State
  57. , direction :: Direction
  58. } deriving (Eq, Ord, Show, Generic, NFData)
  59. spriteDim :: Int
  60. spriteDim = 48
  61. game :: Game -> Html m Game
  62. game g = H.div [id' "game"] . pure $ H.div'
  63. [ id' "avatar"
  64. , styleProp styles
  65. , onGlobalKeyDownNoRepeat $ \case
  66. LeftArrow -> \g' -> g' { state = Walking, direction = FaceLeft }
  67. RightArrow -> \g' -> g' { state = Walking, direction = FaceRight }
  68. Ctrl -> \g' -> g' { state = Shooting (clock g') }
  69. _ -> id
  70. , onGlobalKeyUp $ \case
  71. LeftArrow -> \g' -> g' { state = Idle }
  72. RightArrow -> \g' -> g' { state = Idle }
  73. _ -> id
  74. ]
  75. where
  76. styles =
  77. [ ("height", px spriteDim)
  78. , ("width", px spriteDim)
  79. , ("background-image", "url(" <> spriteImage <> ")")
  80. , ("background-position", px (spriteDim * (spriteCount - spriteTime)) <> ", 0")
  81. , ("position", "absolute")
  82. , ("bottom", "0")
  83. , ("transform",
  84. "translate3d(" <> px (position g) <> ",0,0) "
  85. <> "scaleX(" <> (case direction g of FaceLeft -> "-1"; FaceRight -> "1") <> ")"
  86. )
  87. ]
  88. spriteImage = toSpriteImage $ state g
  89. spriteCount = toSpriteCount $ state g
  90. spriteTime = toSpriteTime (clock g) (state g)
  91. fps :: Num n => n
  92. fps = 12
  93. toSpriteTime :: Clock -> State -> Int
  94. toSpriteTime c gs = floor (unClock since / (1000 / fps)) `mod` toSpriteCount gs
  95. where since = case gs of Shooting c' -> c - c'; _ -> c
  96. toSpriteImage :: State -> Text
  97. toSpriteImage = \case
  98. Shooting _ -> $(assetLink "/assets/game/CowBoyShoot.png")
  99. Walking -> $(assetLink "/assets/game/CowBoyWalking.png")
  100. Idle -> $(assetLink "/assets/game/CowBoyIdle.png")
  101. toSpriteCount :: State -> Int
  102. toSpriteCount = \case
  103. Shooting _ -> 5
  104. Walking -> 8
  105. Idle -> 8
  106. tick :: Clock -> Game -> Game
  107. tick d g = g
  108. { clock = d
  109. , position = if state g /= Walking then position g else
  110. let moveBy = Position . realToFrac $ 100 * (unClock (d - clock g) / 1000) in case direction g of
  111. FaceLeft -> position g - moveBy
  112. FaceRight -> position g + moveBy
  113. , state = case state g of
  114. Shooting _ | toSpriteTime d (state g) == toSpriteCount (state g) - 1 -> Idle
  115. _ -> state g
  116. }
  117. animate :: Window -> TVar Game -> JSM RequestAnimationFrameCallback
  118. animate win model = newRequestAnimationFrameCallback $ \d -> () <$ do
  119. liftIO . atomically . modifyTVar model . tick $ Clock d
  120. requestAnimationFrame win =<< animate win model
  121. play :: JSM RawNode
  122. play = do
  123. let gameId = "game"
  124. doc <- currentDocumentUnchecked
  125. isSubsequent <- traverse toJSVal =<< getElementById doc gameId
  126. case isSubsequent of
  127. Just raw -> return $ RawNode raw
  128. Nothing -> do
  129. win <- currentWindowUnchecked
  130. elm <- createElement doc "div"
  131. setId elm gameId
  132. model <- newTVarIO $ Game 24 0 Idle FaceRight
  133. _ <- requestAnimationFrame win =<< animate win model
  134. raw <- RawNode <$> toJSVal elm
  135. _ <- forkIO $ threadDelay 1
  136. >> shpadoinkle id runSnabbdom model game (pure raw)
  137. return raw
  138. view :: Html m a
  139. view = baked $ (, retry) <$> play