@inglorious/engine 0.1.1 → 0.3.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 (201) hide show
  1. package/README.md +75 -72
  2. package/package.json +15 -37
  3. package/src/{engine/ai → ai}/movement/dynamic/align.js +9 -9
  4. package/src/{engine/ai → ai}/movement/dynamic/arrive.js +9 -10
  5. package/src/{engine/ai → ai}/movement/dynamic/evade.js +9 -9
  6. package/src/{engine/ai → ai}/movement/dynamic/face.js +5 -6
  7. package/src/{engine/ai/movement/dynamic/seek.js → ai/movement/dynamic/flee.js} +8 -7
  8. package/src/ai/movement/dynamic/look-where-youre-going.js +16 -0
  9. package/src/{engine/ai → ai}/movement/dynamic/match-velocity.js +9 -8
  10. package/src/{engine/ai → ai}/movement/dynamic/pursue.js +9 -9
  11. package/src/{engine/ai/movement/dynamic/flee.js → ai/movement/dynamic/seek.js} +7 -8
  12. package/src/{engine/ai → ai}/movement/dynamic/wander.js +9 -10
  13. package/src/{engine/ai → ai}/movement/kinematic/align.js +7 -7
  14. package/src/{engine/ai → ai}/movement/kinematic/arrive.js +8 -8
  15. package/src/{engine/ai → ai}/movement/kinematic/face.js +5 -6
  16. package/src/{engine/ai → ai}/movement/kinematic/flee.js +5 -5
  17. package/src/{engine/ai → ai}/movement/kinematic/seek.js +5 -5
  18. package/src/{engine/ai → ai}/movement/kinematic/seek.test.js +10 -10
  19. package/src/{engine/ai → ai}/movement/kinematic/wander-as-seek.js +9 -9
  20. package/src/{engine/ai → ai}/movement/kinematic/wander.js +5 -5
  21. package/src/animation/sprite.js +101 -0
  22. package/src/animation/ticker.js +38 -0
  23. package/src/behaviors/camera.js +68 -0
  24. package/src/behaviors/controls/dynamic/modern.js +76 -0
  25. package/src/behaviors/controls/dynamic/shooter.js +84 -0
  26. package/src/behaviors/controls/dynamic/tank.js +69 -0
  27. package/src/behaviors/controls/event-handlers.js +17 -0
  28. package/src/behaviors/controls/kinematic/modern.js +76 -0
  29. package/src/behaviors/controls/kinematic/shooter.js +82 -0
  30. package/src/behaviors/controls/kinematic/tank.js +67 -0
  31. package/src/behaviors/debug/collision.js +29 -0
  32. package/src/behaviors/fps.js +29 -0
  33. package/src/behaviors/fsm.js +33 -0
  34. package/src/{game/decorators → behaviors}/fsm.test.js +15 -22
  35. package/src/behaviors/game.js +15 -0
  36. package/src/behaviors/input/controls.js +37 -0
  37. package/src/behaviors/input/gamepad.js +114 -0
  38. package/src/behaviors/input/input.js +48 -0
  39. package/src/behaviors/input/keyboard.js +64 -0
  40. package/src/behaviors/input/mouse.js +91 -0
  41. package/src/behaviors/physics/bouncy.js +25 -0
  42. package/src/behaviors/physics/clamped.js +36 -0
  43. package/src/{game/decorators/collisions.js → behaviors/physics/collidable.js} +3 -7
  44. package/src/behaviors/physics/jumpable.js +145 -0
  45. package/src/behaviors/ui/button.js +17 -0
  46. package/src/collision/detection.js +110 -0
  47. package/src/core/api.js +34 -0
  48. package/src/core/dev-tools.js +135 -0
  49. package/src/core/engine.js +119 -0
  50. package/src/core/loop.js +15 -0
  51. package/src/{engine/loop → core/loops}/animation-frame.js +1 -2
  52. package/src/{engine/loop → core/loops}/elapsed.js +1 -2
  53. package/src/{engine/loop → core/loops}/fixed.js +1 -2
  54. package/src/{engine/loop → core/loops}/flash.js +1 -2
  55. package/src/{engine/loop → core/loops}/lag.js +1 -2
  56. package/src/core/select.js +26 -0
  57. package/src/core/store.js +178 -0
  58. package/src/core/store.test.js +110 -0
  59. package/src/main.js +7 -2
  60. package/src/{engine/movement → movement}/dynamic/modern.js +3 -6
  61. package/src/{engine/movement → movement}/dynamic/tank.js +9 -9
  62. package/src/{engine/movement → movement}/kinematic/modern.js +3 -3
  63. package/src/movement/kinematic/modern.test.js +27 -0
  64. package/src/{engine/movement → movement}/kinematic/tank.js +5 -5
  65. package/src/physics/bounds.js +138 -0
  66. package/src/physics/position.js +43 -0
  67. package/src/physics/position.test.js +80 -0
  68. package/src/systems/sprite-animation.js +27 -0
  69. package/src/engine/ai/movement/dynamic/look-where-youre-going.js +0 -17
  70. package/src/engine/collision/detection.js +0 -115
  71. package/src/engine/loop.js +0 -15
  72. package/src/engine/movement/kinematic/modern.test.js +0 -27
  73. package/src/engine/store.js +0 -174
  74. package/src/engine/store.test.js +0 -256
  75. package/src/engine.js +0 -74
  76. package/src/game/animation.js +0 -26
  77. package/src/game/bounds.js +0 -66
  78. package/src/game/decorators/character.js +0 -5
  79. package/src/game/decorators/clamp-to-bounds.js +0 -15
  80. package/src/game/decorators/controls/dynamic/modern.js +0 -48
  81. package/src/game/decorators/controls/dynamic/shooter.js +0 -47
  82. package/src/game/decorators/controls/dynamic/tank.js +0 -55
  83. package/src/game/decorators/controls/kinematic/modern.js +0 -49
  84. package/src/game/decorators/controls/kinematic/shooter.js +0 -45
  85. package/src/game/decorators/controls/kinematic/tank.js +0 -52
  86. package/src/game/decorators/debug/collisions.js +0 -32
  87. package/src/game/decorators/double-jump.js +0 -70
  88. package/src/game/decorators/fps.js +0 -30
  89. package/src/game/decorators/fsm.js +0 -27
  90. package/src/game/decorators/game.js +0 -11
  91. package/src/game/decorators/image/image.js +0 -5
  92. package/src/game/decorators/image/sprite.js +0 -5
  93. package/src/game/decorators/image/tilemap.js +0 -5
  94. package/src/game/decorators/input/controls.js +0 -27
  95. package/src/game/decorators/input/gamepad.js +0 -74
  96. package/src/game/decorators/input/input.js +0 -41
  97. package/src/game/decorators/input/keyboard.js +0 -49
  98. package/src/game/decorators/input/mouse.js +0 -65
  99. package/src/game/decorators/jump.js +0 -72
  100. package/src/game/decorators/platform.js +0 -5
  101. package/src/game/decorators/ui/button.js +0 -21
  102. package/src/game/sprite.js +0 -119
  103. package/src/ui/canvas/absolute-position.js +0 -17
  104. package/src/ui/canvas/character.js +0 -35
  105. package/src/ui/canvas/form/button.js +0 -25
  106. package/src/ui/canvas/fps.js +0 -18
  107. package/src/ui/canvas/image/hitmask.js +0 -37
  108. package/src/ui/canvas/image/image.js +0 -37
  109. package/src/ui/canvas/image/sprite.js +0 -49
  110. package/src/ui/canvas/image/tilemap.js +0 -64
  111. package/src/ui/canvas/mouse.js +0 -37
  112. package/src/ui/canvas/shapes/circle.js +0 -31
  113. package/src/ui/canvas/shapes/rectangle.js +0 -31
  114. package/src/ui/canvas.js +0 -81
  115. package/src/ui/react/game/character/character.module.scss +0 -17
  116. package/src/ui/react/game/character/index.jsx +0 -30
  117. package/src/ui/react/game/cursor/cursor.module.scss +0 -47
  118. package/src/ui/react/game/cursor/index.jsx +0 -20
  119. package/src/ui/react/game/form/fields/field/field.module.scss +0 -5
  120. package/src/ui/react/game/form/fields/field/index.jsx +0 -56
  121. package/src/ui/react/game/form/fields/fields.module.scss +0 -48
  122. package/src/ui/react/game/form/fields/index.jsx +0 -12
  123. package/src/ui/react/game/form/form.module.scss +0 -18
  124. package/src/ui/react/game/form/index.jsx +0 -22
  125. package/src/ui/react/game/fps/index.jsx +0 -16
  126. package/src/ui/react/game/game.jsx +0 -71
  127. package/src/ui/react/game/index.jsx +0 -29
  128. package/src/ui/react/game/platform/index.jsx +0 -30
  129. package/src/ui/react/game/platform/platform.module.scss +0 -7
  130. package/src/ui/react/game/scene/index.jsx +0 -25
  131. package/src/ui/react/game/scene/scene.module.scss +0 -9
  132. package/src/ui/react/game/sprite/index.jsx +0 -58
  133. package/src/ui/react/game/sprite/sprite.module.css +0 -3
  134. package/src/ui/react/game/stats/index.jsx +0 -22
  135. package/src/ui/react/hocs/with-absolute-position/index.jsx +0 -20
  136. package/src/ui/react/hocs/with-absolute-position/with-absolute-position.module.scss +0 -5
  137. package/src/ui/react/index.jsx +0 -9
  138. package/src/utils/algorithms/decision-tree.js +0 -24
  139. package/src/utils/algorithms/decision-tree.test.js +0 -102
  140. package/src/utils/algorithms/path-finding.js +0 -155
  141. package/src/utils/algorithms/path-finding.test.js +0 -151
  142. package/src/utils/algorithms/types.d.ts +0 -28
  143. package/src/utils/data-structures/array.js +0 -83
  144. package/src/utils/data-structures/array.test.js +0 -173
  145. package/src/utils/data-structures/board.js +0 -159
  146. package/src/utils/data-structures/board.test.js +0 -242
  147. package/src/utils/data-structures/boolean.js +0 -9
  148. package/src/utils/data-structures/heap.js +0 -164
  149. package/src/utils/data-structures/heap.test.js +0 -103
  150. package/src/utils/data-structures/object.js +0 -102
  151. package/src/utils/data-structures/object.test.js +0 -121
  152. package/src/utils/data-structures/objects.js +0 -48
  153. package/src/utils/data-structures/objects.test.js +0 -99
  154. package/src/utils/data-structures/tree.js +0 -36
  155. package/src/utils/data-structures/tree.test.js +0 -33
  156. package/src/utils/data-structures/types.d.ts +0 -4
  157. package/src/utils/functions/functions.js +0 -19
  158. package/src/utils/functions/functions.test.js +0 -23
  159. package/src/utils/math/geometry/circle.js +0 -117
  160. package/src/utils/math/geometry/circle.test.js +0 -97
  161. package/src/utils/math/geometry/hitmask.js +0 -39
  162. package/src/utils/math/geometry/hitmask.test.js +0 -84
  163. package/src/utils/math/geometry/line.js +0 -35
  164. package/src/utils/math/geometry/line.test.js +0 -49
  165. package/src/utils/math/geometry/platform.js +0 -42
  166. package/src/utils/math/geometry/platform.test.js +0 -133
  167. package/src/utils/math/geometry/point.js +0 -71
  168. package/src/utils/math/geometry/point.test.js +0 -81
  169. package/src/utils/math/geometry/rectangle.js +0 -45
  170. package/src/utils/math/geometry/rectangle.test.js +0 -42
  171. package/src/utils/math/geometry/segment.js +0 -80
  172. package/src/utils/math/geometry/segment.test.js +0 -183
  173. package/src/utils/math/geometry/triangle.js +0 -15
  174. package/src/utils/math/geometry/triangle.test.js +0 -11
  175. package/src/utils/math/geometry/types.d.ts +0 -23
  176. package/src/utils/math/linear-algebra/2d.js +0 -28
  177. package/src/utils/math/linear-algebra/2d.test.js +0 -17
  178. package/src/utils/math/linear-algebra/quaternion.js +0 -22
  179. package/src/utils/math/linear-algebra/quaternion.test.js +0 -25
  180. package/src/utils/math/linear-algebra/quaternions.js +0 -20
  181. package/src/utils/math/linear-algebra/quaternions.test.js +0 -29
  182. package/src/utils/math/linear-algebra/types.d.ts +0 -4
  183. package/src/utils/math/linear-algebra/vector.js +0 -302
  184. package/src/utils/math/linear-algebra/vector.test.js +0 -257
  185. package/src/utils/math/linear-algebra/vectors.js +0 -122
  186. package/src/utils/math/linear-algebra/vectors.test.js +0 -65
  187. package/src/utils/math/numbers.js +0 -90
  188. package/src/utils/math/numbers.test.js +0 -137
  189. package/src/utils/math/rng.js +0 -44
  190. package/src/utils/math/rng.test.js +0 -39
  191. package/src/utils/math/statistics.js +0 -43
  192. package/src/utils/math/statistics.test.js +0 -47
  193. package/src/utils/math/trigonometry.js +0 -89
  194. package/src/utils/math/trigonometry.test.js +0 -52
  195. package/src/utils/physics/acceleration.js +0 -63
  196. package/src/utils/physics/friction.js +0 -30
  197. package/src/utils/physics/friction.test.js +0 -44
  198. package/src/utils/physics/gravity.js +0 -71
  199. package/src/utils/physics/gravity.test.js +0 -80
  200. package/src/utils/physics/jump.js +0 -41
  201. package/src/utils/physics/velocity.js +0 -38
@@ -0,0 +1,119 @@
1
+ import { game } from "@inglorious/engine/behaviors/game.js"
2
+ import { extend } from "@inglorious/utils/data-structures/objects.js"
3
+
4
+ import { createApi } from "./api.js"
5
+ import {
6
+ ACTION_BLACKLIST,
7
+ disconnectDevTools,
8
+ initDevTools,
9
+ sendAction,
10
+ } from "./dev-tools.js"
11
+ import Loop from "./loop.js"
12
+ import { createStore } from "./store.js"
13
+
14
+ // Default game configuration
15
+ // loop.type specifies the type of loop to use (defaults to "animationFrame").
16
+ const DEFAULT_GAME_CONFIG = {
17
+ loop: { type: "animationFrame", fps: 60 },
18
+
19
+ systems: [],
20
+
21
+ types: {
22
+ game: [game()],
23
+ },
24
+
25
+ entities: {
26
+ // eslint-disable-next-line no-magic-numbers
27
+ game: { type: "game", bounds: [0, 0, 800, 600] },
28
+ },
29
+ }
30
+
31
+ const ONE_SECOND = 1000 // Number of milliseconds in one second.
32
+
33
+ // Delta time for the final update call when stopping the engine.
34
+ const FINAL_UPDATE_DELTA_TIME = 0 // This ensures any pending events (like 'stop') are processed before shutdown.
35
+
36
+ /**
37
+ * Engine class responsible for managing the game loop, state, and rendering.
38
+ */
39
+ export class Engine {
40
+ _devMode = false
41
+
42
+ /**
43
+ * @param {Object} [gameConfig] - Game-specific configuration.
44
+ * @param {Object} [renderer] - UI entity responsible for rendering. It must have a `render` method.
45
+ */
46
+ constructor(gameConfig) {
47
+ this._config = extend(DEFAULT_GAME_CONFIG, gameConfig)
48
+
49
+ const systems = [...(this._config.systems ?? [])]
50
+ if (this._config.renderer) {
51
+ systems.push(...this._config.renderer.getSystems())
52
+ }
53
+
54
+ this._store = createStore({ ...this._config, systems })
55
+ this._loop = new Loop[this._config.loop.type]()
56
+ this._api = createApi(this._store)
57
+
58
+ // The renderer might need the engine instance to initialize itself (e.g., to set up DOM events).
59
+ this._config.renderer?.init(this)
60
+
61
+ // Determine devMode from the entities config.
62
+ const devMode = this._config.entities.game?.devMode
63
+ this._devMode = devMode
64
+ if (devMode) {
65
+ initDevTools(this._store)
66
+ }
67
+ }
68
+
69
+ /**
70
+ * Starts the game engine, initializing the loop and notifying the store.
71
+ */
72
+ start() {
73
+ this._store.notify("start", this._api)
74
+ this._loop.start(this, ONE_SECOND / this._config.loop.fps)
75
+ this.isRunning = true
76
+ }
77
+
78
+ /**
79
+ * Stops the game engine, halting the loop and notifying the store.
80
+ */
81
+ stop() {
82
+ this._store.notify("stop", this._api)
83
+ this._store.update(FINAL_UPDATE_DELTA_TIME, this._api)
84
+ this._loop.stop()
85
+ this.isRunning = false
86
+ }
87
+
88
+ /**
89
+ * Updates the game state.
90
+ * @param {number} dt - Delta time since the last update in milliseconds.
91
+ */
92
+ update(dt) {
93
+ const processedEvents = this._store.update(dt, this._api)
94
+ const state = this._store.getState()
95
+
96
+ // Check for devMode changes and connect/disconnect dev tools accordingly.
97
+ const newDevMode = state.entities.game?.devMode
98
+ if (newDevMode !== this._devMode) {
99
+ if (newDevMode) {
100
+ initDevTools(this._store)
101
+ } else {
102
+ disconnectDevTools()
103
+ }
104
+ this._devMode = newDevMode
105
+ }
106
+
107
+ const eventsToLog = processedEvents.filter(
108
+ ({ type }) => !ACTION_BLACKLIST.includes(type),
109
+ )
110
+
111
+ if (eventsToLog.length) {
112
+ const action = {
113
+ type: eventsToLog.map(({ type }) => type).join("|"),
114
+ payload: eventsToLog,
115
+ }
116
+ sendAction(action, state)
117
+ }
118
+ }
119
+ }
@@ -0,0 +1,15 @@
1
+ import { AnimationFrameLoop } from "./loops/animation-frame.js"
2
+ import { ElapsedLoop } from "./loops/elapsed.js"
3
+ import { FixedLoop } from "./loops/fixed.js"
4
+ import { FlashLoop } from "./loops/flash.js"
5
+ import { LagLoop } from "./loops/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
+ }
@@ -1,6 +1,6 @@
1
1
  const ONE_SECOND = 1000
2
2
 
3
- export default class AnimationFrameLoop {
3
+ export class AnimationFrameLoop {
4
4
  _id = null
5
5
  _previousTime = new Date()
6
6
 
@@ -19,7 +19,6 @@ export default class AnimationFrameLoop {
19
19
  const dt = currentTime - this._previousTime
20
20
 
21
21
  engine.update(dt / ONE_SECOND)
22
- engine.render(dt / ONE_SECOND)
23
22
 
24
23
  this._previousTime = currentTime
25
24
  }
@@ -1,6 +1,6 @@
1
1
  const ONE_SECOND = 1000
2
2
 
3
- export default class ElapsedLoop {
3
+ export class ElapsedLoop {
4
4
  _shouldStop = false
5
5
 
6
6
  start(engine) {
@@ -11,7 +11,6 @@ export default class ElapsedLoop {
11
11
  const dt = currentTime - previousTime
12
12
 
13
13
  engine.update(dt / ONE_SECOND)
14
- engine.render(dt / ONE_SECOND)
15
14
 
16
15
  previousTime = currentTime
17
16
  }
@@ -1,6 +1,6 @@
1
1
  const ONE_SECOND = 1000
2
2
 
3
- export default class NapLoop {
3
+ export class FixedLoop {
4
4
  _shouldStop = false
5
5
 
6
6
  async start(engine, msPerUpdate) {
@@ -11,7 +11,6 @@ export default class NapLoop {
11
11
  const dt = currentTime - previousTime
12
12
 
13
13
  engine.update(dt / ONE_SECOND)
14
- engine.render(dt / ONE_SECOND)
15
14
 
16
15
  previousTime = currentTime
17
16
  await sleep(Date.now() - currentTime + msPerUpdate)
@@ -1,10 +1,9 @@
1
- export default class FlashLoop {
1
+ export class FlashLoop {
2
2
  _shouldStop = false
3
3
 
4
4
  start(engine) {
5
5
  while (!this._shouldStop) {
6
6
  engine.update()
7
- engine.render()
8
7
  }
9
8
  }
10
9
 
@@ -1,6 +1,6 @@
1
1
  const ONE_SECOND = 1000
2
2
 
3
- export default class LagLoop {
3
+ export class LagLoop {
4
4
  _shouldStop = false
5
5
 
6
6
  start(engine, msPerUpdate) {
@@ -15,7 +15,6 @@ export default class LagLoop {
15
15
 
16
16
  while (lag >= msPerUpdate) {
17
17
  engine.update(dt / ONE_SECOND)
18
- engine.render(dt / ONE_SECOND)
19
18
  lag -= msPerUpdate
20
19
  }
21
20
  }
@@ -0,0 +1,26 @@
1
+ /**
2
+ * Creates a memoized selector function.
3
+ * NB: this implementation does not support spreading the input selectors for clarity, please just put them in an array.
4
+ * @param {Array<Function>} inputSelectors - An array of input selector functions.
5
+ * @param {Function} resultFunc - A function that receives the results of the input selectors and returns a computed value.
6
+ * @returns {Function} A memoized selector function that, when called, returns the selected state.
7
+ */
8
+ export function createSelector(inputSelectors, resultFunc) {
9
+ let lastInputs = []
10
+ let lastResult = null
11
+
12
+ return (state) => {
13
+ const nextInputs = inputSelectors.map((selector) => selector(state))
14
+ const inputsChanged =
15
+ lastInputs.length !== nextInputs.length ||
16
+ nextInputs.some((input, index) => input !== lastInputs[index])
17
+
18
+ if (!inputsChanged) {
19
+ return lastResult
20
+ }
21
+
22
+ lastInputs = nextInputs
23
+ lastResult = resultFunc(...nextInputs)
24
+ return lastResult
25
+ }
26
+ }
@@ -0,0 +1,178 @@
1
+ import { map } from "@inglorious/utils/data-structures/object.js"
2
+ import { extend } from "@inglorious/utils/data-structures/objects.js"
3
+ import { pipe } from "@inglorious/utils/functions/functions.js"
4
+ import { produce } from "immer"
5
+
6
+ /**
7
+ * Creates a store to manage state and events.
8
+ * @param {Object} config - Configuration options for the store.
9
+ * @param {Object} [config.types] - The initial types configuration.
10
+ * @param {Object} [config.entities] - The initial entities configuration.
11
+ * @returns {Object} The store with methods to interact with state and events.
12
+ */
13
+ export function createStore({
14
+ types: originalTypes,
15
+ entities: originalEntities,
16
+ systems = [],
17
+ }) {
18
+ const listeners = new Set()
19
+ let incomingEvents = []
20
+
21
+ let types = augmentTypes(originalTypes)
22
+ let entities = augmentEntities(originalEntities)
23
+
24
+ const initialState = { entities }
25
+ let state = initialState
26
+
27
+ return {
28
+ subscribe,
29
+ update,
30
+ notify,
31
+ dispatch, // needed for compatibility with Redux
32
+ getTypes,
33
+ getOriginalTypes,
34
+ getState,
35
+ setState,
36
+ reset,
37
+ }
38
+
39
+ /**
40
+ * Subscribes a listener to state updates.
41
+ * @param {Function} listener - The listener function to call on updates.
42
+ * @returns {Function} A function to unsubscribe the listener.
43
+ */
44
+ function subscribe(listener) {
45
+ listeners.add(listener)
46
+
47
+ return function unsubscribe() {
48
+ listeners.delete(listener)
49
+ }
50
+ }
51
+
52
+ /**
53
+ * Updates the state based on elapsed time and processes events.
54
+ * @param {number} dt - The delta time since the last update in milliseconds.
55
+ * @param {Object} api - The engine's public API.
56
+ */
57
+ function update(dt, api) {
58
+ const processedEvents = []
59
+
60
+ state = produce(state, (state) => {
61
+ incomingEvents.push({ type: "update", payload: dt })
62
+
63
+ while (incomingEvents.length) {
64
+ const event = incomingEvents.shift()
65
+ processedEvents.push(event)
66
+
67
+ if (event.type === "morph") {
68
+ const { id, type } = event.payload
69
+ originalTypes[id] = type
70
+ types = augmentTypes(originalTypes)
71
+ }
72
+
73
+ if (event.type === "add") {
74
+ const { id, ...entity } = event.payload
75
+ state.entities[id] = augmentEntity(id, entity)
76
+ }
77
+
78
+ if (event.type === "remove") {
79
+ const id = event.payload
80
+ delete state.entities[id]
81
+ }
82
+
83
+ for (const id in state.entities) {
84
+ const entity = state.entities[id]
85
+ const type = types[entity.type]
86
+ const handle = type[event.type]
87
+ handle?.(entity, event.payload, api)
88
+ }
89
+
90
+ systems.forEach((system) => {
91
+ const handle = system[event.type]
92
+ handle?.(state, event.payload, api)
93
+ })
94
+ }
95
+ })
96
+
97
+ listeners.forEach((onUpdate) => onUpdate())
98
+
99
+ return processedEvents
100
+ }
101
+
102
+ /**
103
+ * Notifies the store of a new event.
104
+ * @param {string} type - The event object type to notify.
105
+ * @param {any} payload - The event object payload to notify.
106
+ */
107
+ function notify(type, payload) {
108
+ dispatch({ type, payload })
109
+ }
110
+
111
+ /**
112
+ * Dispatches an event to be processed in the next update cycle.
113
+ * @param {Object} event - The event object.
114
+ * @param {string} event.type - The type of the event.
115
+ * @param {any} [event.payload] - The payload of the event.
116
+ */
117
+ function dispatch(event) {
118
+ incomingEvents.push(event)
119
+ }
120
+
121
+ /**
122
+ * Retrieves the augmented types configuration.
123
+ * This includes composed behaviors and event handlers wrapped for immutability.
124
+ * @returns {Object} The augmented types configuration.
125
+ */
126
+ function getTypes() {
127
+ return types
128
+ }
129
+
130
+ /**
131
+ * Retrieves the original, un-augmented types configuration.
132
+ * @returns {Object} The original types configuration.
133
+ */
134
+ function getOriginalTypes() {
135
+ return originalTypes
136
+ }
137
+
138
+ /**
139
+ * Retrieves the current state.
140
+ * @returns {Object} The current state.
141
+ */
142
+ function getState() {
143
+ return state
144
+ }
145
+
146
+ function setState(newState) {
147
+ state = newState
148
+ }
149
+
150
+ function reset() {
151
+ state = initialState // Reset state to its originally computed value
152
+ }
153
+ }
154
+
155
+ function augmentTypes(types) {
156
+ return pipe(applyBehaviors)(types)
157
+ }
158
+
159
+ function applyBehaviors(types) {
160
+ return map(types, (_, type) => {
161
+ if (!Array.isArray(type)) {
162
+ return type
163
+ }
164
+
165
+ const behaviors = type.map((fn) =>
166
+ typeof fn !== "function" ? (type) => extend(type, fn) : fn,
167
+ )
168
+ return pipe(...behaviors)({})
169
+ })
170
+ }
171
+
172
+ function augmentEntities(entities) {
173
+ return map(entities, augmentEntity)
174
+ }
175
+
176
+ function augmentEntity(id, entity) {
177
+ return { ...entity, id }
178
+ }
@@ -0,0 +1,110 @@
1
+ import { expect, test } from "vitest"
2
+
3
+ import { createStore } from "./store.js"
4
+
5
+ test("it should process events by mutating state inside handlers", () => {
6
+ const config = {
7
+ types: {
8
+ kitty: {
9
+ feed(entity) {
10
+ entity.isFed = true
11
+ },
12
+ update(entity) {
13
+ entity.isMeowing = true
14
+ },
15
+ },
16
+ },
17
+ entities: {
18
+ kitty1: { type: "kitty" },
19
+ },
20
+ }
21
+ const afterState = {
22
+ entities: {
23
+ kitty1: {
24
+ id: "kitty1",
25
+ type: "kitty",
26
+ isFed: true,
27
+ isMeowing: true,
28
+ },
29
+ },
30
+ }
31
+
32
+ const store = createStore(config)
33
+ store.notify("feed")
34
+ store.update(0, {})
35
+
36
+ const state = store.getState()
37
+ expect(state).toStrictEqual(afterState)
38
+ })
39
+
40
+ test("it should send an event from an entity and process it in the same update cycle", () => {
41
+ const config = {
42
+ types: {
43
+ doggo: {
44
+ update(entity, dt, api) {
45
+ api.notify("bark")
46
+ },
47
+ },
48
+ kitty: {
49
+ bark(entity) {
50
+ entity.position = "far"
51
+ },
52
+ },
53
+ },
54
+ entities: {
55
+ doggo1: { type: "doggo" },
56
+ kitty1: { type: "kitty", position: "near" },
57
+ },
58
+ }
59
+ const afterState = {
60
+ entities: {
61
+ doggo1: { id: "doggo1", type: "doggo" },
62
+ kitty1: { id: "kitty1", type: "kitty", position: "far" },
63
+ },
64
+ }
65
+
66
+ const store = createStore(config)
67
+ const api = { notify: store.notify }
68
+ store.update(0, api)
69
+
70
+ const state = store.getState()
71
+ expect(state).toStrictEqual(afterState)
72
+ })
73
+
74
+ test("it should add an entity via an 'add' event", () => {
75
+ const config = {
76
+ types: {
77
+ kitty: {},
78
+ },
79
+ entities: {},
80
+ }
81
+ const newEntity = { id: "kitty1", type: "kitty" }
82
+
83
+ const store = createStore(config)
84
+ store.notify("add", newEntity)
85
+ store.update(0, {})
86
+
87
+ const state = store.getState()
88
+ expect(state).toStrictEqual({
89
+ entities: {
90
+ kitty1: { id: "kitty1", type: "kitty" },
91
+ },
92
+ })
93
+ })
94
+
95
+ test("it should remove an entity via a 'remove' event", () => {
96
+ const config = {
97
+ types: {},
98
+ entities: {
99
+ kitty1: { type: "kitty" },
100
+ },
101
+ }
102
+ const store = createStore(config)
103
+
104
+ store.notify("remove", "kitty1")
105
+
106
+ store.update(0, {})
107
+
108
+ const state = store.getState()
109
+ expect(state.entities.kitty1).toBeUndefined()
110
+ })
package/src/main.js CHANGED
@@ -1,5 +1,10 @@
1
- import { start } from "@inglorious/ui/canvas.js"
1
+ import { Engine } from "@inglorious/engine/core/engine.js"
2
+ import { CanvasRenderer } from "@inglorious/renderer-2d/canvas-renderer.js"
2
3
  import game from "game"
3
4
 
4
5
  const canvas = document.getElementById("canvas")
5
- window.addEventListener("load", () => start(game, canvas))
6
+ window.addEventListener("load", () => {
7
+ const renderer = new CanvasRenderer(canvas)
8
+ const engine = new Engine({ ...game, renderer })
9
+ engine.start()
10
+ })
@@ -8,13 +8,10 @@ const DEFAULT_ORIENTATION = 0
8
8
 
9
9
  const ORIENTATION_CHANGE_THRESHOLD = 4
10
10
 
11
- export default function modernMove(instance, options) {
12
- const { acceleration, velocity, position } = applyAcceleration(
13
- instance,
14
- options,
15
- )
11
+ export function modernMove(entity, dt) {
12
+ const { acceleration, velocity, position } = applyAcceleration(entity, dt)
16
13
 
17
- let orientation = instance.orientation ?? DEFAULT_ORIENTATION
14
+ let orientation = entity.orientation ?? DEFAULT_ORIENTATION
18
15
  orientation =
19
16
  magnitude(velocity) > ORIENTATION_CHANGE_THRESHOLD
20
17
  ? angle(velocity)
@@ -16,25 +16,25 @@ const DEFAULT_ORIENTATION = 0
16
16
 
17
17
  const HALF_ACCELERATION = 0.5
18
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
19
+ export function tankMove(entity, dt) {
20
+ const maxAcceleration = entity.maxAcceleration ?? DEFAULT_MAX_ACCELERATION
21
+ const maxSpeed = entity.maxSpeed ?? DEFAULT_MAX_SPEED
22
+ const friction = entity.friction ?? DEFAULT_FRICTION
23
23
 
24
- let orientation = instance.orientation ?? DEFAULT_ORIENTATION
24
+ let orientation = entity.orientation ?? DEFAULT_ORIENTATION
25
25
  orientation = toRange(orientation)
26
26
 
27
- let acceleration = instance.acceleration ?? zero()
27
+ let acceleration = entity.acceleration ?? zero()
28
28
  acceleration = rotate(acceleration, orientation)
29
29
  acceleration = clamp(acceleration, -maxAcceleration, maxAcceleration)
30
30
 
31
- let velocity = instance.velocity ?? zero()
31
+ let velocity = entity.velocity ?? zero()
32
32
  velocity = sum(velocity, multiply(acceleration, dt))
33
33
  velocity = clamp(velocity, -maxSpeed, maxSpeed)
34
- velocity = applyFriction({ velocity, friction }, { dt })
34
+ velocity = applyFriction({ velocity, friction }, dt)
35
35
 
36
36
  const position = sum(
37
- instance.position,
37
+ entity.position,
38
38
  multiply(velocity, dt),
39
39
  multiply(acceleration, HALF_ACCELERATION * dt * dt),
40
40
  )
@@ -6,10 +6,10 @@ import { applyVelocity } from "@inglorious/utils/physics/velocity.js"
6
6
 
7
7
  const DEFAULT_ORIENTATION = 0
8
8
 
9
- export default function modernMove(instance, options) {
10
- const { velocity, position } = applyVelocity(instance, options)
9
+ export function modernMove(entity, dt) {
10
+ const { velocity, position } = applyVelocity(entity, dt)
11
11
 
12
- let orientation = instance.orientation ?? DEFAULT_ORIENTATION
12
+ let orientation = entity.orientation ?? DEFAULT_ORIENTATION
13
13
  orientation = magnitude(velocity) ? angle(velocity) : orientation
14
14
 
15
15
  return { velocity, position, orientation }
@@ -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 entity = { maxSpeed: 1, velocity: [1, 0, 0], position: [0, 0, 0] }
7
+ const dt = 1
8
+ const expectedResult = {
9
+ velocity: [1, 0, 0],
10
+ position: [1, 0, 0],
11
+ orientation: 0,
12
+ }
13
+
14
+ expect(modernMove(entity, dt)).toStrictEqual(expectedResult)
15
+ })
16
+
17
+ test("it should limit the velocity to the max speed", () => {
18
+ const entity = { maxSpeed: 1, velocity: [10, 0, 0], position: [0, 0, 0] }
19
+ const dt = 1
20
+ const expectedResult = {
21
+ velocity: [1, 0, 0],
22
+ position: [1, 0, 0],
23
+ orientation: 0,
24
+ }
25
+
26
+ expect(modernMove(entity, dt)).toStrictEqual(expectedResult)
27
+ })
@@ -11,17 +11,17 @@ const DEFAULT_MAX_SPEED = 0
11
11
 
12
12
  const DEFAULT_ORIENTATION = 0
13
13
 
14
- export default function tankMove(instance, { dt }) {
15
- const maxSpeed = instance.maxSpeed ?? DEFAULT_MAX_SPEED
14
+ export function tankMove(entity, dt) {
15
+ const maxSpeed = entity.maxSpeed ?? DEFAULT_MAX_SPEED
16
16
 
17
- let orientation = instance.orientation ?? DEFAULT_ORIENTATION
17
+ let orientation = entity.orientation ?? DEFAULT_ORIENTATION
18
18
  orientation = toRange(orientation)
19
19
 
20
- let velocity = instance.velocity ?? zero()
20
+ let velocity = entity.velocity ?? zero()
21
21
  velocity = rotate(velocity, orientation)
22
22
  velocity = clamp(velocity, -maxSpeed, maxSpeed)
23
23
 
24
- const position = sum(instance.position, multiply(velocity, dt))
24
+ const position = sum(entity.position, multiply(velocity, dt))
25
25
 
26
26
  return { velocity, position, orientation }
27
27
  }