@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
package/README.md CHANGED
@@ -1,72 +1,75 @@
1
- # Inglorious Engine
2
-
3
- [![License: MIT](https://img.shields.io/badge/License-MIT-yellow.svg)](https://opensource.org/licenses/MIT)
4
-
5
- A JavaScript game engine written with global state, immutability, and pure functions in mind. Have fun(ctional programming) with it!
6
-
7
- ## Features
8
-
9
- - **Functional & Data-Oriented**: Uses a single, immutable state object as the source of truth, inspired by functional programming principles.
10
- - **Composable by Design**: Build complex behaviors by composing pure functions and decorators, offering a powerful alternative to inheritance.
11
- - **Renderer Agnostic**: The engine is headless. You can use any rendering technology you like, from Canvas2D and HTML to React components.
12
- - **Zero Build Step**: Write plain JavaScript and run it directly in the browser. No complex build configurations to worry about.
13
-
14
- ## Documentation
15
-
16
- The best way to get started is with the official documentation, which includes a **[Quick Start Guide](https://inglorious-engine.vercel.app/?path=/docs/quick-start--docs)**.
17
-
18
- Full documentation is available at: **[https://inglorious-engine.vercel.app/](https://inglorious-engine.vercel.app/)**
19
-
20
- ## Why Functional Programming?
21
-
22
- What makes this engine different from all the others is that, instead of Object Oriented Programming (OOP), which seems the most obvious choice for a game engine, this one is based on Functional Programming (FP).
23
-
24
- FP has many advantages:
25
-
26
- 1. A single source of truth means that the game state is just one huge JSON structure. This entails that you have control over the whole game state at every moment, instead of having pieces of state scattered through multiple objects. To overcome the issue of scattered state, many modern game engines use an approach that is called Data-Oriented. Well, guess what: functional programming has always used it.
27
- 2. Immutability of state means that every time you want to change something a new state will be created with that change, instead of modifying the state directly. Isn't that slow? Nope, the new state is a shallow copy of the old one. Also, immutability makes it easier to compare what was before with what is now, and in certain scenarios (such as webapps) it allows for very performant re-rendering techniques.
28
- 3. Pure functions are functions that return a value that depends only on the input parameters, no side-effects involved. They are the most predictable, testable, and reusable functions, so why should we rely on void methods belonging to a specific class?
29
- 4. Another important concept related to FP is function composition, which means combining multiple functions together and then applying them to some input. Think of it as a pipeline of operations: you take x, then do some transformation on it, then pass the result to some other function, and so on. This can be used as a way more powerful tool than object inheritance: you can combine multiple behaviours on some object, in a way that is very similar to the Decorator pattern we have in OOP.
30
-
31
- ## Contributing
32
-
33
- We welcome contributions from the community! Whether you're fixing a bug, adding a feature, or improving the documentation, your help is appreciated.
34
-
35
- ### Development Setup
36
-
37
- 1. Fork and clone the repository.
38
- 2. Install dependencies using pnpm:
39
- ```bash
40
- pnpm install
41
- ```
42
- 3. Run the Storybook documentation locally:
43
- ```bash
44
- pnpm storybook
45
- ```
46
- 4. Run the unit tests:
47
- ```bash
48
- pnpm test
49
- ```
50
-
51
- ### Code Style
52
-
53
- The project uses ESLint for linting and Prettier for formatting. Please ensure your code adheres to the project's style to avoid introducing errors.
54
-
55
- A note on code style, particularly regarding "magic numbers" for vector components:
56
-
57
- ```js
58
- const X = 0
59
- const Y = 1
60
- const Z = 2
61
- const x = instance.position[X]
62
- const y = instance.position[Y]
63
- const z = instance.position[Z]
64
- ```
65
-
66
- I find it cleaner to do like so:
67
-
68
- ```js
69
- const [x, y, z] = instance.position
70
- ```
71
-
72
- There are a few exceptions: in the `/docs` folder I prefer the first version because not everyone is used to destructuring and I wanted to make the examples as readable as possible for people coming from, say, Godot. In that case I would put the `X`, `Y`, and `Z` constants on top of the file, right below the imports.
1
+ # Inglorious Engine
2
+
3
+ [![License: MIT](https://img.shields.io/badge/License-MIT-yellow.svg)](https://opensource.org/licenses/MIT)
4
+
5
+ A JavaScript game engine written with global state, immutability, and pure functions in mind. Have fun(ctional programming) with it!
6
+
7
+ ## Features
8
+
9
+ - **Functional & Data-Oriented**: Uses a single, immutable state object as the source of truth, inspired by functional programming principles.
10
+ - **Composable by Design**: Build complex behaviors by composing pure functions and decorators, offering a powerful alternative to inheritance.
11
+ - **Renderer Agnostic**: The engine is headless. You can use any rendering technology you like, from Canvas2D and HTML to React components.
12
+ - **Zero Build Step**: Write plain JavaScript and run it directly in the browser. No complex build configurations to worry about.
13
+
14
+ ## Documentation
15
+
16
+ The best way to get started is with the official documentation, which includes a **[Quick Start Guide](https://inglorious-engine.vercel.app/?path=/docs/quick-start--docs)**.
17
+
18
+ Full documentation is available at: **[https://inglorious-engine.vercel.app/](https://inglorious-engine.vercel.app/)**
19
+
20
+ ## Why Functional Programming?
21
+
22
+ What makes this engine different from all the others is that, instead of Object Oriented Programming (OOP), which seems the most obvious choice for a game engine, this one is based on Functional Programming (FP).
23
+
24
+ FP has many advantages:
25
+
26
+ 1. **Single Source of Truth**: Your entire game state is a single, plain JavaScript object. This gives you complete control over your game's world at any moment, rather than having state scattered across countless objects. This is the core idea behind the Data-Oriented Design (DOD) paradigm that many modern engines are now adopting. With this engine, you get that benefit naturally.
27
+
28
+ 2. **Efficient Immutability**: A common misconception is that creating a new state on every change is slow. This engine uses structural sharing (via Immer), meaning only the parts of the state that actually change are copied. The rest of the state tree is shared by reference, making updates extremely fast. This provides a huge benefit:
29
+ - **Optimized Rendering**: Detecting changes becomes trivial and fast. A simple reference check (`prevState === nextState`) is all that's needed to determine if data has changed, enabling highly performant UIs (especially with libraries like React). This is much faster than the deep, recursive comparisons required in mutable systems.
30
+
31
+ 3. **Pure Functions**: Game logic is built with pure functions — functions that return a value based only on their inputs, with no side effects. This makes your game logic predictable, easy to test in isolation, and highly reusable, freeing you from the complexity of class methods with hidden side effects.
32
+
33
+ 4. **Composition over Inheritance**: Instead of complex class hierarchies, you build entities by composing functions. Think of it as a pipeline of operations applied to your data. This is a more flexible and powerful alternative to inheritance. You can mix and match behaviors (e.g., `canBeControlledByPlayer`, `canBeHurt`, `canShoot`) on the fly, avoiding the rigidity and common problems of deep inheritance chains.
34
+
35
+ 5. **Dynamic by Nature**: JavaScript objects are dynamic. You can add or remove properties from an entity at any time without being constrained by a rigid class definition. This is perfect for game development, where an entity's state can change unpredictably (e.g., gaining a temporary power-up). This flexibility allows for more emergent game mechanics.
36
+
37
+ 6. **Unparalleled Debugging and Tooling**: Because the entire game state is a single, serializable object, you can unlock powerful development patterns that are difficult to achieve in traditional OOP engines.
38
+ - **Time-Travel Debugging**: Save the state at any frame. You can step backward and forward through state changes to find exactly when a bug was introduced.
39
+ - **Hot-Reloading**: Modify your game's logic and instantly see the results without restarting. The engine can reload the code and re-apply it to the current state, dramatically speeding up iteration.
40
+ - **Simplified Persistence**: Saving and loading a game is as simple as serializing and deserializing a single JSON object.
41
+ - **Simplified Networking**: For multiplayer games, you don't need to synchronize complex objects. You just send small, serializable `event` objects over the network. Each client processes the same event with the same pure event handler, guaranteeing their game states stay in sync.
42
+
43
+ 7. **Leverage the Full JavaScript Ecosystem**: As a pure JavaScript engine, you have immediate access to the world's largest software repository: npm. Need advanced physics, complex AI, or a specific UI library? Integrate it with a simple `npm install`. You aren't limited to the built-in features or proprietary plugin ecosystem of a monolithic engine like Godot or Unity.
44
+
45
+ ## Architecture: State Management
46
+
47
+ The engine's state management is inspired by Redux, but it's specifically tailored for the demands of game development. If you're familiar with Redux, you'll recognize the core pattern: the UI (or game view) is a projection of the state, and the only way to change the state is to dispatch an action.
48
+
49
+ However, there are several key differences that make it unique:
50
+
51
+ 1. **Events, not Actions**: In Redux, you "dispatch actions." Here, we "notify of events." This is a deliberate semantic choice. An event handler is a function that reacts to a specific occurrence in the game world (e.g., `playerMove`, `enemyDestroy`). The naming convention is similar to standard JavaScript event handlers like `onClick`, where the handler name describes the event it's listening for.
52
+
53
+ 2. **Asynchronous Event Queue**: Unlike Redux's synchronous dispatch, events are not processed immediately. They are added to a central event queue. The engine's main loop processes this queue once per frame. This approach has several advantages:
54
+ - It decouples game logic from state updates.
55
+ - It ensures state changes happen at a predictable point in the game loop, preventing race conditions or cascading updates within a single frame.
56
+ - It allows for event batching and provides a solid foundation for networking and time-travel debugging.
57
+
58
+ 3. **Core Engine Events & Naming Convention**: The engine has a few built-in, **single-word** events that drive its core functionality. To avoid conflicts, you should use **multi-word `camelCase`** names for your own custom game events (`playerJump`, `itemCollect`). This convention is similar to how custom HTML elements require a hyphen to distinguish them from standard elements. Key engine events include:
59
+ - `update`: Fired on every frame, typically carrying the `deltaTime` since the last frame. This is where you'll put most of your continuous game logic (like movement).
60
+ - `add`: Used to add a new entity to the game state.
61
+ - `remove`: Used to remove an entity from the game state.
62
+ - `morph`: Used to dynamically change the behaviors associated with an entity's type.
63
+
64
+ 4. **Ergonomic Immutability with Immer**: The state is immutable, but to make this easy to work with, we use Immer. Inside your event handlers, you can write code that looks like it's mutating the state directly. Immer handles the magic behind the scenes, producing a new, updated state with structural sharing, giving you the performance benefits of immutability with the developer experience of mutable code.
65
+
66
+ 5. **Composable Handlers via Function Piping**: Instead of large, monolithic "reducers," you build event handlers by composing smaller, pure functions. The engine encourages a pipeline pattern where an event and the current state are passed through a series of decorators or transformations. This makes your logic highly modular, reusable, and easy to test in isolation.
67
+
68
+ 6. **Handlers Can Issue New Events (Controlled Side-Effects)**: In a strict Redux pattern, reducers must be pure. We relax this rule for a pragmatic reason: event handlers in this engine **can notify of new events**. This allows you to create powerful, reactive chains of logic. For example, an `enemy:take_damage` handler might check the enemy's health and, if it drops to zero, notify of a new `enemy:destroyed` event.
69
+ - **How it works**: Any event notified from within a handler is simply added to the end of the main event queue. It will be processed in a subsequent pass of the game loop, not immediately. This prevents synchronous, cascading updates within a single frame and makes the flow of logic easier to trace.
70
+
71
+ - **A Word of Caution**: This power comes with responsibility. It is possible to create infinite loops (e.g., event A's handler notifies of event B, and event B's handler notifies of event A). Developers should be mindful of this when designing their event chains.
72
+
73
+ ## Contributing
74
+
75
+ We welcome contributions from the community! Whether you're fixing a bug, adding a feature, or improving the documentation, your help is appreciated. Please read our Contributing Guidelines for details on how to get started.
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@inglorious/engine",
3
- "version": "0.1.1",
3
+ "version": "0.3.0",
4
4
  "description": "A JavaScript game engine written with global state, immutability, and pure functions in mind. Have fun(ctional programming) with it!",
5
5
  "author": "IceOnFire <antony.mistretta@gmail.com> (https://ingloriouscoderz.it)",
6
6
  "license": "MIT",
@@ -9,13 +9,15 @@
9
9
  "url": "git+https://github.com/IngloriousCoderz/inglorious-engine.git"
10
10
  },
11
11
  "homepage": "https://inglorious-engine.vercel.app/",
12
+ "bugs": {
13
+ "url": "https://github.com/IngloriousCoderz/inglorious-engine/issues"
14
+ },
12
15
  "keywords": [
13
- "game-engine",
16
+ "data-oriented",
17
+ "ecs",
14
18
  "functional-programming",
15
19
  "gamedev",
16
- "javascript",
17
- "ecs",
18
- "data-oriented"
20
+ "game-engine"
19
21
  ],
20
22
  "type": "module",
21
23
  "files": [
@@ -31,45 +33,21 @@
31
33
  },
32
34
  "dependencies": {
33
35
  "immer": "^10.1.1",
34
- "react": "^19.0.0",
35
- "react-dom": "^19.0.0",
36
- "react-redux": "^9.2.0"
36
+ "@inglorious/utils": "1.1.0"
37
37
  },
38
38
  "devDependencies": {
39
- "@chromatic-com/storybook": "^3.2.6",
40
- "@eslint/js": "^9.23.0",
41
- "@storybook/addon-essentials": "^8.6.12",
42
- "@storybook/addon-interactions": "^8.6.12",
43
- "@storybook/addon-links": "^8.6.12",
44
- "@storybook/blocks": "^8.6.12",
45
- "@storybook/manager-api": "^8.6.12",
46
- "@storybook/react": "^8.6.12",
47
- "@storybook/react-vite": "^8.6.12",
48
- "@storybook/test": "^8.6.12",
49
- "@storybook/theming": "^8.6.12",
50
- "@types/react": "^18.2.15",
51
- "@types/react-dom": "^18.2.7",
52
- "@vitejs/plugin-react": "^4.0.3",
53
- "eslint": "^9.23.0",
54
- "eslint-plugin-react": "^7.37.4",
55
- "eslint-plugin-simple-import-sort": "^10.0.0",
56
- "eslint-plugin-storybook": "^0.12.0",
57
- "globals": "^16.0.0",
58
39
  "husky": "^9.1.7",
59
40
  "prettier": "^3.5.3",
60
- "sass": "^1.66.1",
61
- "serve": "^14.2.4",
62
- "storybook": "^8.6.12",
63
- "vite": "^4.4.5",
64
- "vitest": "^0.34.3"
41
+ "vite": "^7.1.3",
42
+ "vitest": "^1.6.0"
43
+ },
44
+ "engines": {
45
+ "node": ">= 22"
65
46
  },
66
47
  "scripts": {
67
- "lint": "eslint . --ext js,jsx --report-unused-disable-directives --max-warnings 0",
68
48
  "format": "prettier --write '**/*.{js,jsx}'",
49
+ "lint": "eslint . --ext js,jsx --report-unused-disable-directives --max-warnings 0",
69
50
  "test:watch": "vitest",
70
- "test": "vitest run",
71
- "start": "serve",
72
- "storybook": "storybook dev -p 6006",
73
- "build-storybook": "storybook build"
51
+ "test": "vitest run"
74
52
  }
75
53
  }
@@ -13,28 +13,28 @@ const DEFAULT_ORIENTATION = 0
13
13
 
14
14
  const HALF_ANGULAR_ACCELERATION = 0.5
15
15
 
16
- export default function align(
17
- instance,
16
+ export function align(
17
+ entity,
18
18
  target,
19
+ dt,
19
20
  {
20
- dt,
21
21
  targetRadius = DEFAULT_TARGET_RADIUS,
22
22
  slowRadius = DEFAULT_SLOW_RADIUS,
23
23
  timeToTarget = DEFAULT_TIME_TO_TARGET,
24
- },
24
+ } = {},
25
25
  ) {
26
26
  const maxAngularAcceleration =
27
- instance.maxAngularAcceleration ?? DEFAULT_MAX_ANGULAR_ACCELERATION
28
- const maxAngularSpeed = instance.maxAngularSpeed ?? DEFAULT_MAX_ANGULAR_SPEED
27
+ entity.maxAngularAcceleration ?? DEFAULT_MAX_ANGULAR_ACCELERATION
28
+ const maxAngularSpeed = entity.maxAngularSpeed ?? DEFAULT_MAX_ANGULAR_SPEED
29
29
 
30
- let angularSpeed = instance.angularSpeed ?? DEFAULT_ANGULAR_SPEED
31
- let orientation = instance.orientation ?? DEFAULT_ORIENTATION
30
+ let angularSpeed = entity.angularSpeed ?? DEFAULT_ANGULAR_SPEED
31
+ let orientation = entity.orientation ?? DEFAULT_ORIENTATION
32
32
 
33
33
  const direction = toRange(target.orientation - orientation)
34
34
  const distance = abs(direction)
35
35
 
36
36
  if (distance < targetRadius) {
37
- return instance
37
+ return entity
38
38
  }
39
39
 
40
40
  let targetAngularSpeed
@@ -1,34 +1,33 @@
1
+ import { matchVelocity } from "@inglorious/engine/ai/movement/dynamic/match-velocity.js"
1
2
  import {
2
3
  magnitude,
3
4
  setMagnitude,
4
5
  } from "@inglorious/utils/math/linear-algebra/vector.js"
5
6
  import { subtract } from "@inglorious/utils/math/linear-algebra/vectors.js"
6
7
 
7
- import matchVelocity from "./match-velocity.js"
8
-
9
8
  export const DEFAULT_TARGET_RADIUS = 1
10
9
  export const DEFAULT_SLOW_RADIUS = 100
11
10
  export const DEFAULT_TIME_TO_TARGET = 0.1
12
11
 
13
12
  const DEFAULT_MAX_SPEED = 0
14
13
 
15
- export default function arrive(
16
- instance,
14
+ export function arrive(
15
+ entity,
17
16
  target,
17
+ dt,
18
18
  {
19
- dt,
20
19
  targetRadius = DEFAULT_TARGET_RADIUS,
21
20
  slowRadius = DEFAULT_SLOW_RADIUS,
22
21
  timeToTarget = DEFAULT_TIME_TO_TARGET,
23
- },
22
+ } = {},
24
23
  ) {
25
- const maxSpeed = instance.maxSpeed ?? DEFAULT_MAX_SPEED
24
+ const maxSpeed = entity.maxSpeed ?? DEFAULT_MAX_SPEED
26
25
 
27
- const direction = subtract(target.position, instance.position)
26
+ const direction = subtract(target.position, entity.position)
28
27
  const distance = magnitude(direction)
29
28
 
30
29
  if (distance < targetRadius) {
31
- return instance
30
+ return entity
32
31
  }
33
32
 
34
33
  let speed
@@ -39,5 +38,5 @@ export default function arrive(
39
38
  }
40
39
  const velocity = setMagnitude(direction, speed)
41
40
 
42
- return matchVelocity(instance, { velocity }, { dt, timeToTarget })
41
+ return matchVelocity(entity, { velocity }, dt, { timeToTarget })
43
42
  }
@@ -1,3 +1,4 @@
1
+ import { flee } from "@inglorious/engine/ai/movement/dynamic/flee.js"
1
2
  import {
2
3
  magnitude,
3
4
  multiply,
@@ -5,22 +6,21 @@ import {
5
6
  } from "@inglorious/utils/math/linear-algebra/vector.js"
6
7
  import { subtract, sum } from "@inglorious/utils/math/linear-algebra/vectors.js"
7
8
 
8
- import flee from "./flee.js"
9
-
10
9
  export const DEFAULT_MAX_PREDICTION = 10
11
10
 
12
- export default function evade(
13
- instance,
11
+ export function evade(
12
+ entity,
14
13
  target,
15
- { dt, maxPrediction = DEFAULT_MAX_PREDICTION },
14
+ dt,
15
+ { maxPrediction = DEFAULT_MAX_PREDICTION } = {},
16
16
  ) {
17
- let velocity = instance.velocity ?? zero()
17
+ let velocity = entity.velocity ?? zero()
18
18
 
19
- const direction = subtract(target.position, instance.position)
19
+ const direction = subtract(target.position, entity.position)
20
20
  const distance = magnitude(direction)
21
21
 
22
22
  if (!distance) {
23
- return instance
23
+ return entity
24
24
  }
25
25
 
26
26
  const speed = magnitude(velocity)
@@ -34,5 +34,5 @@ export default function evade(
34
34
 
35
35
  const position = sum(target.position, multiply(target.velocity, prediction))
36
36
 
37
- return flee(instance, { ...target, position }, { dt })
37
+ return flee(entity, { ...target, position }, dt)
38
38
  }
@@ -1,20 +1,19 @@
1
+ import { align } from "@inglorious/engine/ai/movement/dynamic/align.js"
1
2
  import {
2
3
  angle,
3
4
  magnitude,
4
5
  } from "@inglorious/utils/math/linear-algebra/vector.js"
5
6
  import { subtract } from "@inglorious/utils/math/linear-algebra/vectors.js"
6
7
 
7
- import align from "./align.js"
8
-
9
- export default function face(instance, target, options) {
10
- const direction = subtract(target.position, instance.position)
8
+ export function face(entity, target, dt, options) {
9
+ const direction = subtract(target.position, entity.position)
11
10
  const distance = magnitude(direction)
12
11
 
13
12
  if (!distance) {
14
- return instance
13
+ return entity
15
14
  }
16
15
 
17
16
  const orientation = angle(direction)
18
17
 
19
- return align(instance, { ...target, orientation }, options)
18
+ return align(entity, { ...target, orientation }, dt, options)
20
19
  }
@@ -15,25 +15,26 @@ const MIN_SPEED = 0
15
15
 
16
16
  const HALF_ACCELERATION = 0.5
17
17
 
18
- export default function seek(instance, target, { dt }) {
19
- const maxAcceleration = instance.maxAcceleration ?? DEFAULT_MAX_ACCELERATION
20
- const maxSpeed = instance.maxSpeed ?? DEFAULT_MAX_SPEED
18
+ export function flee(entity, target, dt) {
19
+ const maxAcceleration = entity.maxAcceleration ?? DEFAULT_MAX_ACCELERATION
20
+ const maxSpeed = entity.maxSpeed ?? DEFAULT_MAX_SPEED
21
21
 
22
- const direction = subtract(target.position, instance.position)
22
+ let velocity = entity.velocity ?? zero()
23
+
24
+ const direction = subtract(entity.position, target.position)
23
25
  const distance = magnitude(direction)
24
26
 
25
27
  if (!distance) {
26
- return instance
28
+ return entity
27
29
  }
28
30
 
29
31
  const acceleration = setMagnitude(direction, maxAcceleration)
30
32
 
31
- let velocity = instance.velocity ?? zero()
32
33
  velocity = sum(velocity, multiply(acceleration, dt))
33
34
  velocity = clamp(velocity, MIN_SPEED, maxSpeed)
34
35
 
35
36
  const position = sum(
36
- instance.position,
37
+ entity.position,
37
38
  multiply(velocity, dt),
38
39
  multiply(acceleration, HALF_ACCELERATION * dt * dt),
39
40
  )
@@ -0,0 +1,16 @@
1
+ import { align } from "@inglorious/engine/ai/movement/dynamic/align.js"
2
+ import {
3
+ angle,
4
+ magnitude,
5
+ zero,
6
+ } from "@inglorious/utils/math/linear-algebra/vector.js"
7
+
8
+ export function lookWhereYoureGoing(entity, dt, options) {
9
+ const velocity = entity.velocity ?? zero()
10
+
11
+ if (!magnitude(velocity)) {
12
+ return entity
13
+ }
14
+
15
+ return align(entity, { orientation: angle(velocity) }, dt, options)
16
+ }
@@ -19,17 +19,18 @@ const MIN_SPEED = 0
19
19
 
20
20
  const HALF_ACCELERATION = 0.5
21
21
 
22
- export default function matchVelocity(
23
- instance,
22
+ export function matchVelocity(
23
+ entity,
24
24
  target,
25
- { dt, timeToTarget = DEFAULT_TIME_TO_TARGET },
25
+ dt,
26
+ { timeToTarget = DEFAULT_TIME_TO_TARGET } = {},
26
27
  ) {
27
- const maxAcceleration = instance.maxAcceleration ?? DEFAULT_MAX_ACCELERATION
28
- const maxSpeed = instance.maxSpeed ?? DEFAULT_MAX_SPEED
28
+ const maxAcceleration = entity.maxAcceleration ?? DEFAULT_MAX_ACCELERATION
29
+ const maxSpeed = entity.maxSpeed ?? DEFAULT_MAX_SPEED
29
30
 
30
- let orientation = instance.orientation ?? DEFAULT_ORIENTATION
31
+ let orientation = entity.orientation ?? DEFAULT_ORIENTATION
31
32
 
32
- let velocity = instance.velocity ?? zero()
33
+ let velocity = entity.velocity ?? zero()
33
34
  const velocityDelta = subtract(target.velocity, velocity)
34
35
 
35
36
  let acceleration = divide(velocityDelta, timeToTarget)
@@ -39,7 +40,7 @@ export default function matchVelocity(
39
40
  velocity = clamp(velocity, MIN_SPEED, maxSpeed)
40
41
 
41
42
  const position = sum(
42
- instance.position,
43
+ entity.position,
43
44
  multiply(velocity, dt),
44
45
  multiply(acceleration, HALF_ACCELERATION * dt * dt),
45
46
  )
@@ -1,3 +1,4 @@
1
+ import { seek } from "@inglorious/engine/ai/movement/dynamic/seek.js"
1
2
  import {
2
3
  magnitude,
3
4
  multiply,
@@ -5,22 +6,21 @@ import {
5
6
  } from "@inglorious/utils/math/linear-algebra/vector.js"
6
7
  import { subtract, sum } from "@inglorious/utils/math/linear-algebra/vectors.js"
7
8
 
8
- import seek from "./seek.js"
9
-
10
9
  export const DEFAULT_MAX_PREDICTION = 10
11
10
 
12
- export default function pursue(
13
- instance,
11
+ export function pursue(
12
+ entity,
14
13
  target,
15
- { dt, maxPrediction = DEFAULT_MAX_PREDICTION },
14
+ dt,
15
+ { maxPrediction = DEFAULT_MAX_PREDICTION } = {},
16
16
  ) {
17
- const velocity = instance.velocity ?? zero()
17
+ const velocity = entity.velocity ?? zero()
18
18
 
19
- const direction = subtract(target.position, instance.position)
19
+ const direction = subtract(target.position, entity.position)
20
20
  const distance = magnitude(direction)
21
21
 
22
22
  if (!distance) {
23
- return instance
23
+ return entity
24
24
  }
25
25
 
26
26
  const speed = magnitude(velocity)
@@ -34,5 +34,5 @@ export default function pursue(
34
34
 
35
35
  const position = sum(target.position, multiply(target.velocity, prediction))
36
36
 
37
- return seek(instance, { ...target, position }, { dt })
37
+ return seek(entity, { ...target, position }, dt)
38
38
  }
@@ -15,26 +15,25 @@ const MIN_SPEED = 0
15
15
 
16
16
  const HALF_ACCELERATION = 0.5
17
17
 
18
- export default function flee(instance, target, { dt }) {
19
- const maxAcceleration = instance.maxAcceleration ?? DEFAULT_MAX_ACCELERATION
20
- const maxSpeed = instance.maxSpeed ?? DEFAULT_MAX_SPEED
18
+ export function seek(entity, target, dt) {
19
+ const maxAcceleration = entity.maxAcceleration ?? DEFAULT_MAX_ACCELERATION
20
+ const maxSpeed = entity.maxSpeed ?? DEFAULT_MAX_SPEED
21
21
 
22
- let velocity = instance.velocity ?? zero()
23
-
24
- const direction = subtract(instance.position, target.position)
22
+ const direction = subtract(target.position, entity.position)
25
23
  const distance = magnitude(direction)
26
24
 
27
25
  if (!distance) {
28
- return instance
26
+ return entity
29
27
  }
30
28
 
31
29
  const acceleration = setMagnitude(direction, maxAcceleration)
32
30
 
31
+ let velocity = entity.velocity ?? zero()
33
32
  velocity = sum(velocity, multiply(acceleration, dt))
34
33
  velocity = clamp(velocity, MIN_SPEED, maxSpeed)
35
34
 
36
35
  const position = sum(
37
- instance.position,
36
+ entity.position,
38
37
  multiply(velocity, dt),
39
38
  multiply(acceleration, HALF_ACCELERATION * dt * dt),
40
39
  )
@@ -1,9 +1,8 @@
1
+ import { seek } from "@inglorious/engine/ai/movement/dynamic/seek.js"
1
2
  import { createVector } from "@inglorious/utils/math/linear-algebra/vector.js"
2
3
  import { sum } from "@inglorious/utils/math/linear-algebra/vectors.js"
3
4
  import { randomBinomial } from "@inglorious/utils/math/rng.js"
4
5
 
5
- import seek from "./seek.js"
6
-
7
6
  export const DEFAULT_WANDER_OFFSET = 100
8
7
  export const DEFAULT_WANDER_RADIUS = 100
9
8
 
@@ -11,22 +10,22 @@ const DEFAULT_MAX_ANGULAR_SPEED = 0
11
10
 
12
11
  const DEFAULT_ORIENTATION = 0
13
12
 
14
- export default function wander(
15
- instance,
13
+ export function wander(
14
+ entity,
15
+ dt,
16
16
  {
17
17
  wanderOffset = DEFAULT_WANDER_OFFSET,
18
18
  wanderRadius = DEFAULT_WANDER_RADIUS,
19
- ...options
20
- },
19
+ } = {},
21
20
  ) {
22
- const maxAngularSpeed = instance.maxAngularSpeed ?? DEFAULT_MAX_ANGULAR_SPEED
21
+ const maxAngularSpeed = entity.maxAngularSpeed ?? DEFAULT_MAX_ANGULAR_SPEED
23
22
 
24
- let orientation = instance.orientation ?? DEFAULT_ORIENTATION
23
+ let orientation = entity.orientation ?? DEFAULT_ORIENTATION
25
24
 
26
- let position = sum(instance.position, createVector(wanderOffset, orientation))
25
+ let position = sum(entity.position, createVector(wanderOffset, orientation))
27
26
 
28
27
  orientation += randomBinomial() * maxAngularSpeed
29
28
  position = sum(position, createVector(wanderRadius, orientation))
30
29
 
31
- return seek(instance, { position }, options)
30
+ return seek(entity, { position }, dt)
32
31
  }
@@ -8,24 +8,24 @@ const DEFAULT_MAX_ANGULAR_SPEED = 0
8
8
 
9
9
  const DEFAULT_ORIENTATION = 0
10
10
 
11
- export default function align(
12
- instance,
11
+ export function align(
12
+ entity,
13
13
  target,
14
+ dt,
14
15
  {
15
- dt,
16
16
  targetRadius = DEFAULT_TARGET_RADIUS,
17
17
  timeToTarget = DEFAULT_TIME_TO_TARGET,
18
- },
18
+ } = {},
19
19
  ) {
20
- const maxAngularSpeed = instance.maxAngularSpeed ?? DEFAULT_MAX_ANGULAR_SPEED
20
+ const maxAngularSpeed = entity.maxAngularSpeed ?? DEFAULT_MAX_ANGULAR_SPEED
21
21
 
22
- let orientation = instance.orientation ?? DEFAULT_ORIENTATION
22
+ let orientation = entity.orientation ?? DEFAULT_ORIENTATION
23
23
 
24
24
  const direction = toRange(target.orientation - orientation)
25
25
  const distance = abs(direction)
26
26
 
27
27
  if (distance < targetRadius) {
28
- return instance
28
+ return entity
29
29
  }
30
30
 
31
31
  let angularVelocity = direction / timeToTarget