@inglorious/engine 0.1.0

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
Files changed (256) hide show
  1. package/LICENSE +9 -0
  2. package/README.md +72 -0
  3. package/package.json +76 -0
  4. package/src/docs/ai/movement/dynamic/align.js +131 -0
  5. package/src/docs/ai/movement/dynamic/arrive.js +88 -0
  6. package/src/docs/ai/movement/dynamic/dynamic.mdx +99 -0
  7. package/src/docs/ai/movement/dynamic/dynamic.stories.js +58 -0
  8. package/src/docs/ai/movement/dynamic/evade.js +72 -0
  9. package/src/docs/ai/movement/dynamic/face.js +90 -0
  10. package/src/docs/ai/movement/dynamic/flee.js +38 -0
  11. package/src/docs/ai/movement/dynamic/look-where-youre-going.js +114 -0
  12. package/src/docs/ai/movement/dynamic/match-velocity.js +92 -0
  13. package/src/docs/ai/movement/dynamic/pursue.js +72 -0
  14. package/src/docs/ai/movement/dynamic/seek.js +37 -0
  15. package/src/docs/ai/movement/dynamic/wander.js +71 -0
  16. package/src/docs/ai/movement/kinematic/align.js +122 -0
  17. package/src/docs/ai/movement/kinematic/arrive.js +78 -0
  18. package/src/docs/ai/movement/kinematic/face.js +82 -0
  19. package/src/docs/ai/movement/kinematic/flee.js +36 -0
  20. package/src/docs/ai/movement/kinematic/kinematic.mdx +67 -0
  21. package/src/docs/ai/movement/kinematic/kinematic.stories.js +42 -0
  22. package/src/docs/ai/movement/kinematic/seek.js +34 -0
  23. package/src/docs/ai/movement/kinematic/wander-as-seek.js +62 -0
  24. package/src/docs/ai/movement/kinematic/wander.js +28 -0
  25. package/src/docs/bounds.js +7 -0
  26. package/src/docs/code-reuse.js +35 -0
  27. package/src/docs/collision/circles.js +58 -0
  28. package/src/docs/collision/collision.mdx +27 -0
  29. package/src/docs/collision/collision.stories.js +22 -0
  30. package/src/docs/collision/platform.js +76 -0
  31. package/src/docs/collision/tilemap.js +181 -0
  32. package/src/docs/empty.js +1 -0
  33. package/src/docs/engine.mdx +81 -0
  34. package/src/docs/engine.stories.js +37 -0
  35. package/src/docs/event-handlers.js +68 -0
  36. package/src/docs/framerate.js +37 -0
  37. package/src/docs/game.jsx +15 -0
  38. package/src/docs/image/image.js +19 -0
  39. package/src/docs/image/image.stories.js +22 -0
  40. package/src/docs/image/sprite.js +39 -0
  41. package/src/docs/image/tilemap.js +84 -0
  42. package/src/docs/input/controls.js +67 -0
  43. package/src/docs/input/gamepad.js +67 -0
  44. package/src/docs/input/input.mdx +55 -0
  45. package/src/docs/input/input.stories.js +27 -0
  46. package/src/docs/input/keyboard.js +58 -0
  47. package/src/docs/input/mouse.js +32 -0
  48. package/src/docs/instances.js +49 -0
  49. package/src/docs/player/dynamic/double-jump.js +90 -0
  50. package/src/docs/player/dynamic/dynamic.stories.js +32 -0
  51. package/src/docs/player/dynamic/jump.js +83 -0
  52. package/src/docs/player/dynamic/modern-controls.js +57 -0
  53. package/src/docs/player/dynamic/shooter-controls.js +51 -0
  54. package/src/docs/player/dynamic/tank-controls.js +44 -0
  55. package/src/docs/player/kinematic/double-jump.js +90 -0
  56. package/src/docs/player/kinematic/jump.js +82 -0
  57. package/src/docs/player/kinematic/kinematic.stories.js +32 -0
  58. package/src/docs/player/kinematic/modern-controls.js +56 -0
  59. package/src/docs/player/kinematic/shooter-controls.js +48 -0
  60. package/src/docs/player/kinematic/tank-controls.js +42 -0
  61. package/src/docs/quick-start/first-game.js +49 -0
  62. package/src/docs/quick-start/hello-world.js +1 -0
  63. package/src/docs/quick-start.mdx +127 -0
  64. package/src/docs/quick-start.stories.js +17 -0
  65. package/src/docs/recipes/add-and-remove.js +71 -0
  66. package/src/docs/recipes/add-instance.js +42 -0
  67. package/src/docs/recipes/decision-tree.js +169 -0
  68. package/src/docs/recipes/random-instances.js +25 -0
  69. package/src/docs/recipes/recipes.mdx +81 -0
  70. package/src/docs/recipes/recipes.stories.js +37 -0
  71. package/src/docs/recipes/remove-instance.js +52 -0
  72. package/src/docs/recipes/states.js +64 -0
  73. package/src/docs/ui/button.js +28 -0
  74. package/src/docs/ui/form.stories.js +55 -0
  75. package/src/docs/ui-chooser.jsx +6 -0
  76. package/src/docs/utils/data-structures/object.mdx +47 -0
  77. package/src/docs/utils/data-structures/objects.mdx +30 -0
  78. package/src/docs/utils/functions/functions.mdx +34 -0
  79. package/src/docs/utils/math/geometry/circle.mdx +55 -0
  80. package/src/docs/utils/math/geometry/point.mdx +38 -0
  81. package/src/docs/utils/math/geometry/rectangle.mdx +24 -0
  82. package/src/docs/utils/math/geometry/segment.mdx +55 -0
  83. package/src/docs/utils/math/geometry/triangle.mdx +22 -0
  84. package/src/docs/utils/math/linear-algebra/2d.mdx +22 -0
  85. package/src/docs/utils/math/linear-algebra/quaternion.mdx +21 -0
  86. package/src/docs/utils/math/linear-algebra/quaternions.mdx +22 -0
  87. package/src/docs/utils/math/linear-algebra/vector.mdx +177 -0
  88. package/src/docs/utils/math/linear-algebra/vectors.mdx +58 -0
  89. package/src/docs/utils/math/numbers.mdx +76 -0
  90. package/src/docs/utils/math/random.mdx +35 -0
  91. package/src/docs/utils/math/statistics.mdx +38 -0
  92. package/src/docs/utils/math/trigonometry.mdx +85 -0
  93. package/src/docs/utils/physics/friction.mdx +20 -0
  94. package/src/docs/utils/physics/gravity.mdx +28 -0
  95. package/src/engine/ai/movement/dynamic/align.js +63 -0
  96. package/src/engine/ai/movement/dynamic/arrive.js +43 -0
  97. package/src/engine/ai/movement/dynamic/evade.js +38 -0
  98. package/src/engine/ai/movement/dynamic/face.js +20 -0
  99. package/src/engine/ai/movement/dynamic/flee.js +45 -0
  100. package/src/engine/ai/movement/dynamic/look-where-youre-going.js +17 -0
  101. package/src/engine/ai/movement/dynamic/match-velocity.js +50 -0
  102. package/src/engine/ai/movement/dynamic/pursue.js +38 -0
  103. package/src/engine/ai/movement/dynamic/seek.js +44 -0
  104. package/src/engine/ai/movement/dynamic/wander.js +32 -0
  105. package/src/engine/ai/movement/kinematic/align.js +37 -0
  106. package/src/engine/ai/movement/kinematic/arrive.js +42 -0
  107. package/src/engine/ai/movement/kinematic/face.js +20 -0
  108. package/src/engine/ai/movement/kinematic/flee.js +26 -0
  109. package/src/engine/ai/movement/kinematic/seek.js +26 -0
  110. package/src/engine/ai/movement/kinematic/seek.test.js +42 -0
  111. package/src/engine/ai/movement/kinematic/wander-as-seek.js +31 -0
  112. package/src/engine/ai/movement/kinematic/wander.js +27 -0
  113. package/src/engine/collision/detection.js +115 -0
  114. package/src/engine/loop/animation-frame.js +26 -0
  115. package/src/engine/loop/elapsed.js +23 -0
  116. package/src/engine/loop/fixed.js +28 -0
  117. package/src/engine/loop/flash.js +14 -0
  118. package/src/engine/loop/lag.js +27 -0
  119. package/src/engine/loop.js +15 -0
  120. package/src/engine/movement/dynamic/modern.js +24 -0
  121. package/src/engine/movement/dynamic/tank.js +43 -0
  122. package/src/engine/movement/kinematic/modern.js +16 -0
  123. package/src/engine/movement/kinematic/modern.test.js +27 -0
  124. package/src/engine/movement/kinematic/tank.js +27 -0
  125. package/src/engine/store.js +174 -0
  126. package/src/engine/store.test.js +256 -0
  127. package/src/engine.js +74 -0
  128. package/src/game/animation.js +26 -0
  129. package/src/game/bounds.js +66 -0
  130. package/src/game/decorators/character.js +5 -0
  131. package/src/game/decorators/clamp-to-bounds.js +15 -0
  132. package/src/game/decorators/collisions.js +24 -0
  133. package/src/game/decorators/controls/dynamic/modern.js +48 -0
  134. package/src/game/decorators/controls/dynamic/shooter.js +47 -0
  135. package/src/game/decorators/controls/dynamic/tank.js +55 -0
  136. package/src/game/decorators/controls/kinematic/modern.js +49 -0
  137. package/src/game/decorators/controls/kinematic/shooter.js +45 -0
  138. package/src/game/decorators/controls/kinematic/tank.js +52 -0
  139. package/src/game/decorators/debug/collisions.js +32 -0
  140. package/src/game/decorators/double-jump.js +70 -0
  141. package/src/game/decorators/fps.js +30 -0
  142. package/src/game/decorators/fsm.js +27 -0
  143. package/src/game/decorators/fsm.test.js +56 -0
  144. package/src/game/decorators/game.js +11 -0
  145. package/src/game/decorators/image/image.js +5 -0
  146. package/src/game/decorators/image/sprite.js +5 -0
  147. package/src/game/decorators/image/tilemap.js +5 -0
  148. package/src/game/decorators/input/controls.js +27 -0
  149. package/src/game/decorators/input/gamepad.js +74 -0
  150. package/src/game/decorators/input/input.js +41 -0
  151. package/src/game/decorators/input/keyboard.js +49 -0
  152. package/src/game/decorators/input/mouse.js +65 -0
  153. package/src/game/decorators/jump.js +72 -0
  154. package/src/game/decorators/platform.js +5 -0
  155. package/src/game/decorators/ui/button.js +21 -0
  156. package/src/game/sprite.js +119 -0
  157. package/src/main.js +5 -0
  158. package/src/ui/canvas/absolute-position.js +17 -0
  159. package/src/ui/canvas/character.js +35 -0
  160. package/src/ui/canvas/form/button.js +25 -0
  161. package/src/ui/canvas/fps.js +18 -0
  162. package/src/ui/canvas/image/hitmask.js +37 -0
  163. package/src/ui/canvas/image/image.js +37 -0
  164. package/src/ui/canvas/image/sprite.js +49 -0
  165. package/src/ui/canvas/image/tilemap.js +64 -0
  166. package/src/ui/canvas/mouse.js +37 -0
  167. package/src/ui/canvas/shapes/circle.js +31 -0
  168. package/src/ui/canvas/shapes/rectangle.js +31 -0
  169. package/src/ui/canvas.js +81 -0
  170. package/src/ui/react/game/character/character.module.scss +17 -0
  171. package/src/ui/react/game/character/index.jsx +30 -0
  172. package/src/ui/react/game/cursor/cursor.module.scss +47 -0
  173. package/src/ui/react/game/cursor/index.jsx +20 -0
  174. package/src/ui/react/game/form/fields/field/field.module.scss +5 -0
  175. package/src/ui/react/game/form/fields/field/index.jsx +56 -0
  176. package/src/ui/react/game/form/fields/fields.module.scss +48 -0
  177. package/src/ui/react/game/form/fields/index.jsx +12 -0
  178. package/src/ui/react/game/form/form.module.scss +18 -0
  179. package/src/ui/react/game/form/index.jsx +22 -0
  180. package/src/ui/react/game/fps/index.jsx +16 -0
  181. package/src/ui/react/game/game.jsx +71 -0
  182. package/src/ui/react/game/index.jsx +29 -0
  183. package/src/ui/react/game/platform/index.jsx +30 -0
  184. package/src/ui/react/game/platform/platform.module.scss +7 -0
  185. package/src/ui/react/game/scene/index.jsx +25 -0
  186. package/src/ui/react/game/scene/scene.module.scss +9 -0
  187. package/src/ui/react/game/sprite/index.jsx +58 -0
  188. package/src/ui/react/game/sprite/sprite.module.css +3 -0
  189. package/src/ui/react/game/stats/index.jsx +22 -0
  190. package/src/ui/react/hocs/with-absolute-position/index.jsx +20 -0
  191. package/src/ui/react/hocs/with-absolute-position/with-absolute-position.module.scss +5 -0
  192. package/src/ui/react/index.jsx +9 -0
  193. package/src/utils/algorithms/decision-tree.js +24 -0
  194. package/src/utils/algorithms/decision-tree.test.js +102 -0
  195. package/src/utils/algorithms/path-finding.js +155 -0
  196. package/src/utils/algorithms/path-finding.test.js +151 -0
  197. package/src/utils/algorithms/types.d.ts +28 -0
  198. package/src/utils/data-structures/array.js +83 -0
  199. package/src/utils/data-structures/array.test.js +173 -0
  200. package/src/utils/data-structures/board.js +159 -0
  201. package/src/utils/data-structures/board.test.js +242 -0
  202. package/src/utils/data-structures/boolean.js +9 -0
  203. package/src/utils/data-structures/heap.js +164 -0
  204. package/src/utils/data-structures/heap.test.js +103 -0
  205. package/src/utils/data-structures/object.js +102 -0
  206. package/src/utils/data-structures/object.test.js +121 -0
  207. package/src/utils/data-structures/objects.js +48 -0
  208. package/src/utils/data-structures/objects.test.js +99 -0
  209. package/src/utils/data-structures/tree.js +36 -0
  210. package/src/utils/data-structures/tree.test.js +33 -0
  211. package/src/utils/data-structures/types.d.ts +4 -0
  212. package/src/utils/functions/functions.js +19 -0
  213. package/src/utils/functions/functions.test.js +23 -0
  214. package/src/utils/math/geometry/circle.js +117 -0
  215. package/src/utils/math/geometry/circle.test.js +97 -0
  216. package/src/utils/math/geometry/hitmask.js +39 -0
  217. package/src/utils/math/geometry/hitmask.test.js +84 -0
  218. package/src/utils/math/geometry/line.js +35 -0
  219. package/src/utils/math/geometry/line.test.js +49 -0
  220. package/src/utils/math/geometry/platform.js +42 -0
  221. package/src/utils/math/geometry/platform.test.js +133 -0
  222. package/src/utils/math/geometry/point.js +71 -0
  223. package/src/utils/math/geometry/point.test.js +81 -0
  224. package/src/utils/math/geometry/rectangle.js +45 -0
  225. package/src/utils/math/geometry/rectangle.test.js +42 -0
  226. package/src/utils/math/geometry/segment.js +80 -0
  227. package/src/utils/math/geometry/segment.test.js +183 -0
  228. package/src/utils/math/geometry/triangle.js +15 -0
  229. package/src/utils/math/geometry/triangle.test.js +11 -0
  230. package/src/utils/math/geometry/types.d.ts +23 -0
  231. package/src/utils/math/linear-algebra/2d.js +28 -0
  232. package/src/utils/math/linear-algebra/2d.test.js +17 -0
  233. package/src/utils/math/linear-algebra/quaternion.js +22 -0
  234. package/src/utils/math/linear-algebra/quaternion.test.js +25 -0
  235. package/src/utils/math/linear-algebra/quaternions.js +20 -0
  236. package/src/utils/math/linear-algebra/quaternions.test.js +29 -0
  237. package/src/utils/math/linear-algebra/types.d.ts +4 -0
  238. package/src/utils/math/linear-algebra/vector.js +302 -0
  239. package/src/utils/math/linear-algebra/vector.test.js +257 -0
  240. package/src/utils/math/linear-algebra/vectors.js +122 -0
  241. package/src/utils/math/linear-algebra/vectors.test.js +65 -0
  242. package/src/utils/math/numbers.js +90 -0
  243. package/src/utils/math/numbers.test.js +137 -0
  244. package/src/utils/math/rng.js +44 -0
  245. package/src/utils/math/rng.test.js +39 -0
  246. package/src/utils/math/statistics.js +43 -0
  247. package/src/utils/math/statistics.test.js +47 -0
  248. package/src/utils/math/trigonometry.js +89 -0
  249. package/src/utils/math/trigonometry.test.js +52 -0
  250. package/src/utils/physics/acceleration.js +63 -0
  251. package/src/utils/physics/friction.js +30 -0
  252. package/src/utils/physics/friction.test.js +44 -0
  253. package/src/utils/physics/gravity.js +71 -0
  254. package/src/utils/physics/gravity.test.js +80 -0
  255. package/src/utils/physics/jump.js +41 -0
  256. package/src/utils/physics/velocity.js +38 -0
@@ -0,0 +1,14 @@
1
+ export default class FlashLoop {
2
+ _shouldStop = false
3
+
4
+ start(engine) {
5
+ while (!this._shouldStop) {
6
+ engine.update()
7
+ engine.render()
8
+ }
9
+ }
10
+
11
+ stop() {
12
+ this._shouldStop = true
13
+ }
14
+ }
@@ -0,0 +1,27 @@
1
+ const ONE_SECOND = 1000
2
+
3
+ export default class LagLoop {
4
+ _shouldStop = false
5
+
6
+ start(engine, msPerUpdate) {
7
+ let previousTime = Date.now()
8
+ let lag = 0
9
+
10
+ while (!this._shouldStop) {
11
+ const currentTime = Date.now()
12
+ const dt = currentTime - previousTime
13
+ previousTime = currentTime
14
+ lag += dt
15
+
16
+ while (lag >= msPerUpdate) {
17
+ engine.update(dt / ONE_SECOND)
18
+ engine.render(dt / ONE_SECOND)
19
+ lag -= msPerUpdate
20
+ }
21
+ }
22
+ }
23
+
24
+ stop() {
25
+ this._shouldStop = true
26
+ }
27
+ }
@@ -0,0 +1,15 @@
1
+ import AnimationFrameLoop from "./loop/animation-frame.js"
2
+ import ElapsedLoop from "./loop/elapsed.js"
3
+ import FixedLoop from "./loop/fixed.js"
4
+ import FlashLoop from "./loop/flash.js"
5
+ import LagLoop from "./loop/lag.js"
6
+
7
+ // @see https://gameprogrammingpatterns.com/game-loop.html
8
+
9
+ export default {
10
+ flash: FlashLoop,
11
+ fixed: FixedLoop,
12
+ elapsed: ElapsedLoop,
13
+ lag: LagLoop,
14
+ animationFrame: AnimationFrameLoop,
15
+ }
@@ -0,0 +1,24 @@
1
+ import {
2
+ angle,
3
+ magnitude,
4
+ } from "@inglorious/utils/math/linear-algebra/vector.js"
5
+ import { applyAcceleration } from "@inglorious/utils/physics/acceleration.js"
6
+
7
+ const DEFAULT_ORIENTATION = 0
8
+
9
+ const ORIENTATION_CHANGE_THRESHOLD = 4
10
+
11
+ export default function modernMove(instance, options) {
12
+ const { acceleration, velocity, position } = applyAcceleration(
13
+ instance,
14
+ options,
15
+ )
16
+
17
+ let orientation = instance.orientation ?? DEFAULT_ORIENTATION
18
+ orientation =
19
+ magnitude(velocity) > ORIENTATION_CHANGE_THRESHOLD
20
+ ? angle(velocity)
21
+ : orientation
22
+
23
+ return { acceleration, velocity, position, orientation }
24
+ }
@@ -0,0 +1,43 @@
1
+ import {
2
+ clamp,
3
+ multiply,
4
+ rotate,
5
+ zero,
6
+ } from "@inglorious/utils/math/linear-algebra/vector.js"
7
+ import { sum } from "@inglorious/utils/math/linear-algebra/vectors.js"
8
+ import { toRange } from "@inglorious/utils/math/trigonometry.js"
9
+ import { applyFriction } from "@inglorious/utils/physics/friction.js"
10
+
11
+ const DEFAULT_MAX_ACCELERATION = 0
12
+ const DEFAULT_MAX_SPEED = 0
13
+ const DEFAULT_FRICTION = 0
14
+
15
+ const DEFAULT_ORIENTATION = 0
16
+
17
+ const HALF_ACCELERATION = 0.5
18
+
19
+ export default function tankMove(instance, { dt }) {
20
+ const maxAcceleration = instance.maxAcceleration ?? DEFAULT_MAX_ACCELERATION
21
+ const maxSpeed = instance.maxSpeed ?? DEFAULT_MAX_SPEED
22
+ const friction = instance.friction ?? DEFAULT_FRICTION
23
+
24
+ let orientation = instance.orientation ?? DEFAULT_ORIENTATION
25
+ orientation = toRange(orientation)
26
+
27
+ let acceleration = instance.acceleration ?? zero()
28
+ acceleration = rotate(acceleration, orientation)
29
+ acceleration = clamp(acceleration, -maxAcceleration, maxAcceleration)
30
+
31
+ let velocity = instance.velocity ?? zero()
32
+ velocity = sum(velocity, multiply(acceleration, dt))
33
+ velocity = clamp(velocity, -maxSpeed, maxSpeed)
34
+ velocity = applyFriction({ velocity, friction }, { dt })
35
+
36
+ const position = sum(
37
+ instance.position,
38
+ multiply(velocity, dt),
39
+ multiply(acceleration, HALF_ACCELERATION * dt * dt),
40
+ )
41
+
42
+ return { velocity, position, orientation }
43
+ }
@@ -0,0 +1,16 @@
1
+ import {
2
+ angle,
3
+ magnitude,
4
+ } from "@inglorious/utils/math/linear-algebra/vector.js"
5
+ import { applyVelocity } from "@inglorious/utils/physics/velocity.js"
6
+
7
+ const DEFAULT_ORIENTATION = 0
8
+
9
+ export default function modernMove(instance, options) {
10
+ const { velocity, position } = applyVelocity(instance, options)
11
+
12
+ let orientation = instance.orientation ?? DEFAULT_ORIENTATION
13
+ orientation = magnitude(velocity) ? angle(velocity) : orientation
14
+
15
+ return { velocity, position, orientation }
16
+ }
@@ -0,0 +1,27 @@
1
+ import { expect, test } from "vitest"
2
+
3
+ import modernMove from "./modern.js"
4
+
5
+ test("it should move following its velocity", () => {
6
+ const instance = { maxSpeed: 1, velocity: [1, 0, 0], position: [0, 0, 0] }
7
+ const options = { dt: 1 }
8
+ const expectedResult = {
9
+ velocity: [1, 0, 0],
10
+ position: [1, 0, 0],
11
+ orientation: 0,
12
+ }
13
+
14
+ expect(modernMove(instance, options)).toStrictEqual(expectedResult)
15
+ })
16
+
17
+ test("it should limit the velocity to the max speed", () => {
18
+ const instance = { maxSpeed: 1, velocity: [10, 0, 0], position: [0, 0, 0] }
19
+ const options = { dt: 1 }
20
+ const expectedResult = {
21
+ velocity: [1, 0, 0],
22
+ position: [1, 0, 0],
23
+ orientation: 0,
24
+ }
25
+
26
+ expect(modernMove(instance, options)).toStrictEqual(expectedResult)
27
+ })
@@ -0,0 +1,27 @@
1
+ import {
2
+ clamp,
3
+ multiply,
4
+ rotate,
5
+ zero,
6
+ } from "@inglorious/utils/math/linear-algebra/vector.js"
7
+ import { sum } from "@inglorious/utils/math/linear-algebra/vectors.js"
8
+ import { toRange } from "@inglorious/utils/math/trigonometry.js"
9
+
10
+ const DEFAULT_MAX_SPEED = 0
11
+
12
+ const DEFAULT_ORIENTATION = 0
13
+
14
+ export default function tankMove(instance, { dt }) {
15
+ const maxSpeed = instance.maxSpeed ?? DEFAULT_MAX_SPEED
16
+
17
+ let orientation = instance.orientation ?? DEFAULT_ORIENTATION
18
+ orientation = toRange(orientation)
19
+
20
+ let velocity = instance.velocity ?? zero()
21
+ velocity = rotate(velocity, orientation)
22
+ velocity = clamp(velocity, -maxSpeed, maxSpeed)
23
+
24
+ const position = sum(instance.position, multiply(velocity, dt))
25
+
26
+ return { velocity, position, orientation }
27
+ }
@@ -0,0 +1,174 @@
1
+ import { enableGame } from "@inglorious/game/decorators/game.js"
2
+ import { map } from "@inglorious/utils/data-structures/object.js"
3
+ import { extend } from "@inglorious/utils/data-structures/objects.js"
4
+ import { pipe } from "@inglorious/utils/functions/functions.js"
5
+ import { produce } from "immer"
6
+
7
+ const DEFAULT_TYPES = {
8
+ game: [enableGame()],
9
+ }
10
+
11
+ const DEFAULT_INSTANCES = {
12
+ // eslint-disable-next-line no-magic-numbers
13
+ game: { type: "game", bounds: [0, 0, 800, 600] }, // Default game instance with bounds.
14
+ }
15
+
16
+ const DEFAULT_LAYER = 0
17
+
18
+ /**
19
+ * Creates a store to manage state and events.
20
+ * @param {Object} options - Configuration options for the store.
21
+ * @param {Object} options.instances - Initial instances to include in the store.
22
+ * @param {Object} options.originalConfig - Additional configuration for the store.
23
+ * @returns {Object} The store with methods to interact with state and events.
24
+ */
25
+ export function createStore({
26
+ types: originalTypes,
27
+ instances: originalInstances,
28
+ }) {
29
+ const listeners = new Set()
30
+ let incomingEvents = []
31
+
32
+ let types = extend(DEFAULT_TYPES, originalTypes)
33
+ types = augmentTypes(types)
34
+
35
+ let instances = extend(DEFAULT_INSTANCES, originalInstances)
36
+ instances = augmentInstances(instances)
37
+
38
+ let state = { events: [], instances }
39
+
40
+ return {
41
+ subscribe,
42
+ update,
43
+ notify,
44
+ dispatch: notify, // needed for react-redux
45
+ getTypes,
46
+ getState,
47
+ }
48
+
49
+ /**
50
+ * Subscribes a listener to state updates.
51
+ * @param {Function} listener - The listener function to call on updates.
52
+ * @returns {Function} A function to unsubscribe the listener.
53
+ */
54
+ function subscribe(listener) {
55
+ listeners.add(listener)
56
+
57
+ return function unsubscribe() {
58
+ listeners.delete(listener)
59
+ }
60
+ }
61
+
62
+ /**
63
+ * Updates the state based on elapsed time and processes events.
64
+ * @param {number} dt - The delta time since the last update.
65
+ */
66
+ function update(dt) {
67
+ state = { ...state }
68
+
69
+ state.events.push(...incomingEvents, { id: "game:update" })
70
+ incomingEvents = []
71
+
72
+ while (state.events.length) {
73
+ const event = state.events.shift()
74
+
75
+ if (event.id === "instance:add") {
76
+ add(event.payload.id, event.payload)
77
+ }
78
+
79
+ state.instances = map(state.instances, (_, instance, instances) => {
80
+ const type = types[instance.type]
81
+ const handle = type[event.id]
82
+ return (
83
+ handle?.(instance, event, {
84
+ dt,
85
+ type: originalTypes[instance.type],
86
+ instances,
87
+ notify,
88
+ }) ?? instance
89
+ )
90
+ })
91
+
92
+ if (event.id === "instance:remove") {
93
+ remove(event.payload)
94
+ }
95
+ }
96
+
97
+ listeners.forEach((onUpdate) => onUpdate())
98
+ }
99
+
100
+ /**
101
+ * Adds a new instance to the state.
102
+ * @param {string} id - The ID of the instance to add.
103
+ * @param {Object} instance - The instance object to add.
104
+ */
105
+ function add(id, instance) {
106
+ state = { ...state }
107
+ state.instances[id] = augmentInstance(id, instance)
108
+ }
109
+
110
+ /**
111
+ * Removes an instance from the state.
112
+ * @param {string} id - The ID of the instance to remove.
113
+ */
114
+ function remove(id) {
115
+ state = { ...state }
116
+ delete state.instances[id]
117
+ }
118
+
119
+ /**
120
+ * Notifies the store of a new event.
121
+ * @param {Object} event - The event object to notify.
122
+ */
123
+ function notify(event) {
124
+ incomingEvents.push(event)
125
+ }
126
+
127
+ /**
128
+ * Retrieves the types configuration.
129
+ * @returns {Object} The types configuration.
130
+ */
131
+ function getTypes() {
132
+ return types
133
+ }
134
+
135
+ /**
136
+ * Retrieves the current state.
137
+ * @returns {Object} The current state.
138
+ */
139
+ function getState() {
140
+ return state
141
+ }
142
+ }
143
+
144
+ function augmentTypes(types) {
145
+ return pipe(applyDecorators, enableMutability)(types)
146
+ }
147
+
148
+ function applyDecorators(types) {
149
+ return map(types, (_, type) => {
150
+ if (!Array.isArray(type)) {
151
+ return type
152
+ }
153
+
154
+ const decorators = type.map((fn) =>
155
+ typeof fn !== "function" ? (type) => extend(type, fn) : fn,
156
+ )
157
+ return pipe(...decorators)({})
158
+ })
159
+ }
160
+
161
+ function enableMutability(types) {
162
+ return map(types, (_, { draw, ...events }) => ({
163
+ draw,
164
+ ...map(events, (_, event) => produce(event)),
165
+ }))
166
+ }
167
+
168
+ function augmentInstances(instances) {
169
+ return map(instances, augmentInstance)
170
+ }
171
+
172
+ function augmentInstance(id, instance) {
173
+ return { ...instance, layer: instance.layer ?? DEFAULT_LAYER, id }
174
+ }
@@ -0,0 +1,256 @@
1
+ import { expect, test } from "vitest"
2
+
3
+ import { createStore } from "./store.js"
4
+
5
+ test("it should add an event to the event queue", () => {
6
+ const event = { id: "something:happened" }
7
+ const config = {
8
+ types: {
9
+ kitty: {
10
+ [event.id](instance) {
11
+ return { ...instance, wasNotified: true }
12
+ },
13
+ },
14
+ },
15
+ instances: {
16
+ instance1: { type: "kitty" },
17
+ },
18
+ }
19
+ const store = createStore(config)
20
+ const afterState = {
21
+ events: [],
22
+ instances: {
23
+ game: {
24
+ id: "game",
25
+ type: "game",
26
+ layer: 0,
27
+ bounds: [0, 0, 800, 600],
28
+ },
29
+ instance1: {
30
+ id: "instance1",
31
+ type: "kitty",
32
+ layer: 0,
33
+ wasNotified: true,
34
+ },
35
+ },
36
+ }
37
+
38
+ store.notify(event)
39
+ store.update()
40
+
41
+ const state = store.getState()
42
+ expect(state).toStrictEqual(afterState)
43
+ })
44
+
45
+ test("it should process the event queue", () => {
46
+ const event = { id: "something:happened" }
47
+ const config = {
48
+ types: {
49
+ kitty: {
50
+ "game:update"(instance) {
51
+ return { ...instance, wasUpdated: true }
52
+ },
53
+
54
+ [event.id](instance) {
55
+ return { ...instance, wasNotified: true }
56
+ },
57
+ },
58
+ },
59
+ instances: {
60
+ instance1: { type: "kitty" },
61
+ },
62
+ }
63
+ const store = createStore(config)
64
+ store.notify(event)
65
+ const afterState = {
66
+ events: [],
67
+ instances: {
68
+ game: {
69
+ id: "game",
70
+ type: "game",
71
+ layer: 0,
72
+ bounds: [0, 0, 800, 600],
73
+ },
74
+ instance1: {
75
+ id: "instance1",
76
+ type: "kitty",
77
+ layer: 0,
78
+ wasNotified: true,
79
+ wasUpdated: true,
80
+ },
81
+ },
82
+ }
83
+
84
+ store.update()
85
+
86
+ const state = store.getState()
87
+ expect(state).toStrictEqual(afterState)
88
+ })
89
+
90
+ test("it should send an event from an instance", () => {
91
+ const event = {
92
+ id: "doge:message",
93
+ payload: { id: "inu", message: "Woof!" },
94
+ }
95
+ const config = {
96
+ types: {
97
+ doge: {
98
+ "game:update"(instance, event, { instances, notify }) {
99
+ if (instances.instance2.position === "near") {
100
+ notify(event)
101
+ }
102
+ },
103
+ },
104
+ kitty: {
105
+ [event.id](instance) {
106
+ if (event.payload.id === "inu" && event.payload.message === "Woof!") {
107
+ instance.position = "far"
108
+ }
109
+ },
110
+ },
111
+ },
112
+
113
+ instances: {
114
+ instance1: {
115
+ type: "doge",
116
+ },
117
+ instance2: {
118
+ type: "kitty",
119
+ position: "near",
120
+ },
121
+ },
122
+ }
123
+ const store = createStore(config)
124
+ const afterState = {
125
+ events: [],
126
+ instances: {
127
+ game: {
128
+ id: "game",
129
+ type: "game",
130
+ layer: 0,
131
+ bounds: [0, 0, 800, 600],
132
+ },
133
+ instance1: {
134
+ id: "instance1",
135
+ type: "doge",
136
+ layer: 0,
137
+ },
138
+ instance2: {
139
+ id: "instance2",
140
+ type: "kitty",
141
+ layer: 0,
142
+ position: "near", // should do nothing at first
143
+ },
144
+ },
145
+ }
146
+
147
+ store.update()
148
+
149
+ const state = store.getState()
150
+ expect(state).toStrictEqual(afterState)
151
+ })
152
+
153
+ test("it should receive an event from an instance", () => {
154
+ const event = {
155
+ id: "doge:message",
156
+ payload: { id: "inu", message: "Woof!" },
157
+ }
158
+ const config = {
159
+ types: {
160
+ doge: {
161
+ "game:update"(instance, event, { instances, notify }) {
162
+ if (instances.instance2.position === "near") {
163
+ notify(event)
164
+ }
165
+ },
166
+ },
167
+ kitty: {
168
+ [event.id](instance, event) {
169
+ if (event.payload.id === "inu" && event.payload.message === "Woof!") {
170
+ instance.position = "far"
171
+ }
172
+ },
173
+ },
174
+ },
175
+
176
+ instances: {
177
+ instance1: {
178
+ type: "doge",
179
+ },
180
+ instance2: {
181
+ type: "kitty",
182
+ position: "near",
183
+ },
184
+ },
185
+ }
186
+ const store = createStore(config)
187
+ store.notify(event)
188
+ const afterState = {
189
+ events: [],
190
+ instances: {
191
+ game: {
192
+ id: "game",
193
+ type: "game",
194
+ layer: 0,
195
+ bounds: [0, 0, 800, 600],
196
+ },
197
+ instance1: {
198
+ id: "instance1",
199
+ type: "doge",
200
+ layer: 0,
201
+ },
202
+ instance2: {
203
+ id: "instance2",
204
+ type: "kitty",
205
+ layer: 0,
206
+ position: "far", // position changed
207
+ },
208
+ },
209
+ }
210
+
211
+ store.update()
212
+
213
+ const state = store.getState()
214
+ expect(state).toStrictEqual(afterState)
215
+ })
216
+
217
+ test("it should mutate state in an immutable way", () => {
218
+ const config = {
219
+ types: {
220
+ kitty: {
221
+ "game:update"(instance) {
222
+ instance.wasUpdated = true
223
+ },
224
+ },
225
+ },
226
+
227
+ instances: {
228
+ instance1: {
229
+ type: "kitty",
230
+ },
231
+ },
232
+ }
233
+ const store = createStore(config)
234
+ const afterState = {
235
+ events: [],
236
+ instances: {
237
+ game: {
238
+ id: "game",
239
+ type: "game",
240
+ layer: 0,
241
+ bounds: [0, 0, 800, 600],
242
+ },
243
+ instance1: {
244
+ id: "instance1",
245
+ type: "kitty",
246
+ layer: 0,
247
+ wasUpdated: true,
248
+ },
249
+ },
250
+ }
251
+
252
+ store.update()
253
+
254
+ const state = store.getState()
255
+ expect(state).toStrictEqual(afterState)
256
+ })
package/src/engine.js ADDED
@@ -0,0 +1,74 @@
1
+ import { extend } from "@inglorious/utils/data-structures/objects.js"
2
+
3
+ import Loop from "./engine/loop.js"
4
+ import { createStore } from "./engine/store.js"
5
+
6
+ // Default configuration for the engine
7
+ // loop.type specifies the type of loop to use (defaults to "animationFrame").
8
+ const DEFAULT_CONFIG = {
9
+ loop: { type: "animationFrame" },
10
+ }
11
+ const ONE_SECOND = 1000 // Number of milliseconds in one second.
12
+
13
+ /**
14
+ * Engine class responsible for managing the game loop, state, and rendering.
15
+ */
16
+ export default class Engine {
17
+ /**
18
+ * @param {Object} game - Game-specific configuration.
19
+ * @param {Object} ui - UI instance responsible for rendering.
20
+ */
21
+ constructor(game, ui) {
22
+ this._config = extend(DEFAULT_CONFIG, game)
23
+ this._store = createStore(this._config)
24
+ this._loop = new Loop[this._config.loop.type]()
25
+ this._ui = ui
26
+ }
27
+
28
+ /**
29
+ * Starts the game engine, initializing the loop and notifying the store.
30
+ */
31
+ start() {
32
+ this._store.notify({ id: "game:start" })
33
+ this._loop.start(this, ONE_SECOND / this._config.loop.fps)
34
+ this.isRunning = true
35
+ }
36
+
37
+ /**
38
+ * Updates the game state.
39
+ * @param {number} dt - Delta time since the last update in milliseconds.
40
+ */
41
+ update(dt) {
42
+ this._store.update(dt)
43
+ }
44
+
45
+ /**
46
+ * Renders the game state using the UI.
47
+ * @param {number} dt - Delta time since the last render in milliseconds.
48
+ */
49
+ render(dt) {
50
+ this._ui?.render({
51
+ dt,
52
+ types: this._store.getTypes(),
53
+ instances: this._store.getState().instances,
54
+ })
55
+ }
56
+
57
+ /**
58
+ * Notifies the store of an event.
59
+ * @param {Object} event - Event object to notify the store with.
60
+ */
61
+ notify = (event) => {
62
+ this._store.notify(event)
63
+ }
64
+
65
+ /**
66
+ * Stops the game engine, halting the loop and notifying the store.
67
+ */
68
+ stop() {
69
+ this._store.notify({ id: "game:stop" })
70
+ this._store.update()
71
+ this._loop.stop()
72
+ this.isRunning = false
73
+ }
74
+ }