main.ecal 7.3 KB

123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116117118119120121122123124125126127128129130131132133134135136137138139140141142143144145146147148149150151152153154155156157158159160161162163164165166167168169170171172173174175176177178179180181182183184185186187188189190191192193194195196197198199200201202203204205206207208209210211212213214215216217218219220221222223224225226227228229230231232233234235236237238239240241242243244245246247248249250251252253254255256257258259260261262263264265266267268269270271272273274275276277278279280281282
  1. import "./templates.ecal" as tmpl
  2. import "./const.ecal" as const
  3. import "./helper.ecal" as hlp
  4. import "./engine.ecal" as engine
  5. /*
  6. GameState holds the runtime state of all active games.
  7. */
  8. GameState := {}
  9. /*
  10. Websocket holds all active websocket connections.
  11. */
  12. Websocket := {}
  13. /*
  14. Get details of a game world.
  15. Endpoint: /db/ecal/game
  16. */
  17. sink GetGameWorld
  18. kindmatch ["db.web.ecal"]
  19. statematch {"path" : "game", "method" : "GET"}
  20. priority 10
  21. {
  22. let gameWorld
  23. try {
  24. let gameName := event.state.query.gameName[0]
  25. if gameName != "main" {
  26. raise(const.Errors.EntityNotFound, "Game world {{gameName}} not found")
  27. }
  28. gameWorld := tmpl.newGameWorld(gameName)
  29. mutex GameStateMutex {
  30. if GameState[gameName] == null {
  31. db.storeNode(gameName, gameWorld)
  32. sprites := []
  33. for i in range(1, 8) {
  34. posX := math.floor(rand() * gameWorld.screenWidth - 100) + 100
  35. posY := math.floor(rand() * gameWorld.screenHeight - 100) + 100
  36. size := math.floor(rand() * 30) + 20
  37. rot := rand() * math.Pi * 2
  38. sprites := add(sprites, tmpl.newAsteroid("asteroid-{{i}}", posX, posY, size, rot, 0.005))
  39. }
  40. GameState[gameName] := {
  41. "players" : {},
  42. "sprites" : sprites,
  43. "stats" : {},
  44. "world" : gameWorld
  45. }
  46. }
  47. }
  48. } except e {
  49. error(e)
  50. db.raiseWebEventHandled({"status" : const.ErrorCodes[e.type], "body" : {"error" : e.type}})
  51. } otherwise {
  52. db.raiseWebEventHandled({"cstatusode" : 200, "body" : {"result" : "success", "gameworld" : gameWorld}})
  53. }
  54. }
  55. /*
  56. Register a new websocket connection
  57. Endpoint: wss://<host>/db/sock/gamestate
  58. */
  59. sink WebSocketRegister
  60. kindmatch ["db.web.sock"]
  61. statematch {"path" : "gamestate", "method" : "GET"}
  62. priority 0
  63. {
  64. let gameName := event.state.query.gameName[0]
  65. let playerName := event.state.query.playerName[0]
  66. let commID := event.state.commID
  67. mutex WebsocketMutex {
  68. Websocket[commID] := {"gamename" : gameName}
  69. }
  70. log("Register websocket for player: ", playerName, " in game: ", gameName, " commID: ", commID)
  71. }
  72. /*
  73. Register a new player.
  74. Endpoint: /db/ecal/player
  75. */
  76. sink RegisterNewPlayer
  77. kindmatch ["db.web.ecal"]
  78. statematch {"path" : "player", "method" : "POST"}
  79. priority 10
  80. {
  81. let sprite
  82. let gameWorld
  83. try {
  84. let playerName := event.state.bodyJSON.player
  85. let gameName := event.state.bodyJSON.gameName
  86. let gameWorld := GameState[gameName].world
  87. mutex GameStateMutex {
  88. if GameState[gameName].players[playerName] == null {
  89. posY := math.floor(rand() * gameWorld.screenHeight - 100) + 100
  90. GameState[gameName].players[playerName] := tmpl.newPlayer(event.state.bodyJSON.player, 20, posY)
  91. /*
  92. Reset any player score
  93. */
  94. addEvent("changescore", "main.gamescore", {
  95. "id" : playerName,
  96. "part" : gameName,
  97. "changeFunc" : func (s) {
  98. s.score := 0
  99. }
  100. })
  101. log("Registered player: ", playerName, " for game:", gameName)
  102. }
  103. }
  104. } except e {
  105. error(e)
  106. db.raiseWebEventHandled({"status" : const.ErrorCodes["InternalError"], "body" : {"error" : const.Errors.InternalError}})
  107. } otherwise {
  108. db.raiseWebEventHandled({"status" : 200, "body" : {
  109. "result" : "success",
  110. "sprite" : sprite,
  111. "gameworld" : gameWorld
  112. }})
  113. }
  114. }
  115. /*
  116. Handle player input - send over an established websocket connection.
  117. */
  118. sink WebSocketHandler
  119. kindmatch ["db.web.sock.data"]
  120. statematch {"path" : "gamestate", "method" : "GET"}
  121. priority 0
  122. {
  123. try {
  124. let playerName := event.state.data.player
  125. let gameName := event.state.data.gameName
  126. let action := event.state.data.action
  127. let state := event.state.data.state
  128. mutex GameStateMutex {
  129. if GameState[gameName].players[playerName] != null {
  130. for [k, v] in state {
  131. GameState[gameName].players[playerName][k] := v
  132. }
  133. if not action in ["move", "stop move"] and GameState[gameName].players[playerName].action == null {
  134. GameState[gameName].players[playerName].action := action
  135. }
  136. } else {
  137. log("Someone didn't know they were gone: ", playerName)
  138. addEventAndWait("StateUpdate", "db.web.sock.msg", {"commID" : event.state.commID, "payload" : {"toRemovePlayerIds" : [playerName]}})
  139. }
  140. }
  141. } except e {
  142. error(e)
  143. }
  144. }
  145. /*
  146. GameScore sink.
  147. */
  148. sink MainGameScore
  149. kindmatch ["main.gamescore"]
  150. priority 100
  151. {
  152. try {
  153. let scoreObj := db.fetchNode(event.state.part, event.state.id, const.ObjectKinds.ScoreObject)
  154. if scoreObj == null {
  155. scoreObj := {
  156. "key" : event.state.id,
  157. "kind" : const.ObjectKinds.ScoreObject,
  158. "score" : 0
  159. }
  160. }
  161. event.state.changeFunc(scoreObj)
  162. db.storeNode(event.state.part, scoreObj)
  163. } except e {
  164. error("GameScore:", e)
  165. }
  166. }
  167. /*
  168. Object for main game engine.
  169. */
  170. MainGameEngine := new(engine.GameEngine, "main", tmpl.DefaultGameWorld, GameState, Websocket)
  171. /*
  172. GameLoop sink.
  173. */
  174. sink MainGameLoop
  175. kindmatch ["main.gameloop"]
  176. priority 100
  177. {
  178. try {
  179. MainGameEngine.moveLoop()
  180. } except e {
  181. error("Game loop:", e)
  182. }
  183. }
  184. /*
  185. Period game events loop.
  186. */
  187. sink PeriodicGameEvents
  188. kindmatch ["main.periodicgameevents"]
  189. priority 100
  190. {
  191. try {
  192. mutex GameStateMutex {
  193. for [gameName, state] in GameState {
  194. let gameWorld := state.world
  195. if len(GameState[gameName].sprites) < 10 {
  196. log("Adding more asteroids", gameWorld.screenWidth)
  197. sprites := GameState[gameName].sprites
  198. for i in range(1, 4) {
  199. posX := math.floor(rand() * gameWorld.screenWidth - 100) + 100
  200. posY := gameWorld.screenHeight - 100
  201. size := math.floor(rand() * 40) + 20
  202. rot := rand() * math.Pi * 2
  203. sprites := add(sprites, tmpl.newAsteroid("asteroid-{{now()}}-{{i}}", posX, posY, size, rot, 0.005))
  204. }
  205. GameState[gameName].sprites := sprites
  206. }
  207. }
  208. }
  209. } except e {
  210. error("Periodic events loop:", e)
  211. }
  212. }
  213. /*
  214. Trigger the main game loop in a set interval (microseconds). The interval here
  215. must always be greater than the total time of the move loop (see the time_total_move
  216. stat recorded in engine.ecal).
  217. 35000 - 35 milli seconds - smooth animation calculated in the backend, frontend only needs to display
  218. */
  219. setPulseTrigger(35000, "Main Game Loop", "main.gameloop")
  220. /*
  221. Trigger periodic events in the game
  222. 1000000 - 1 second
  223. */
  224. setPulseTrigger(1000000, "Periodic Game Events", "main.periodicgameevents")