@inglorious/engine 0.2.0 → 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 (169) hide show
  1. package/README.md +75 -75
  2. package/package.json +13 -23
  3. package/src/{engine/ai → ai}/movement/dynamic/align.js +63 -63
  4. package/src/{engine/ai → ai}/movement/dynamic/arrive.js +42 -42
  5. package/src/{engine/ai → ai}/movement/dynamic/evade.js +38 -38
  6. package/src/{engine/ai → ai}/movement/dynamic/face.js +19 -19
  7. package/src/{engine/ai → ai}/movement/dynamic/flee.js +45 -45
  8. package/src/{engine/ai → ai}/movement/dynamic/look-where-youre-going.js +16 -16
  9. package/src/{engine/ai → ai}/movement/dynamic/match-velocity.js +51 -51
  10. package/src/{engine/ai → ai}/movement/dynamic/pursue.js +38 -38
  11. package/src/{engine/ai → ai}/movement/dynamic/seek.js +44 -44
  12. package/src/{engine/ai → ai}/movement/dynamic/wander.js +31 -31
  13. package/src/{engine/ai → ai}/movement/kinematic/align.js +37 -37
  14. package/src/{engine/ai → ai}/movement/kinematic/arrive.js +42 -42
  15. package/src/{engine/ai → ai}/movement/kinematic/face.js +19 -19
  16. package/src/{engine/ai → ai}/movement/kinematic/flee.js +26 -26
  17. package/src/{engine/ai → ai}/movement/kinematic/seek.js +26 -26
  18. package/src/{engine/ai → ai}/movement/kinematic/seek.test.js +42 -42
  19. package/src/{engine/ai → ai}/movement/kinematic/wander-as-seek.js +31 -31
  20. package/src/{engine/ai → ai}/movement/kinematic/wander.js +27 -27
  21. package/src/{engine/animation → animation}/sprite.js +101 -101
  22. package/src/{engine/animation → animation}/ticker.js +38 -38
  23. package/src/{engine/behaviors → behaviors}/camera.js +68 -68
  24. package/src/{engine/behaviors → behaviors}/controls/dynamic/modern.js +76 -76
  25. package/src/{engine/behaviors → behaviors}/controls/dynamic/shooter.js +84 -84
  26. package/src/{engine/behaviors → behaviors}/controls/dynamic/tank.js +69 -69
  27. package/src/{engine/behaviors → behaviors}/controls/event-handlers.js +17 -17
  28. package/src/{engine/behaviors → behaviors}/controls/kinematic/modern.js +76 -76
  29. package/src/{engine/behaviors → behaviors}/controls/kinematic/shooter.js +82 -82
  30. package/src/{engine/behaviors → behaviors}/controls/kinematic/tank.js +67 -67
  31. package/src/behaviors/debug/collision.js +29 -0
  32. package/src/{engine/behaviors → behaviors}/fps.js +29 -29
  33. package/src/{engine/behaviors → behaviors}/fsm.js +33 -33
  34. package/src/{engine/behaviors → behaviors}/fsm.test.js +49 -49
  35. package/src/{engine/behaviors → behaviors}/game.js +15 -15
  36. package/src/{engine/behaviors → behaviors}/input/controls.js +37 -37
  37. package/src/{engine/behaviors → behaviors}/input/gamepad.js +114 -114
  38. package/src/{engine/behaviors → behaviors}/input/input.js +48 -48
  39. package/src/{engine/behaviors → behaviors}/input/keyboard.js +64 -64
  40. package/src/{engine/behaviors → behaviors}/input/mouse.js +91 -91
  41. package/src/{engine/behaviors → behaviors}/physics/bouncy.js +25 -25
  42. package/src/{engine/behaviors → behaviors}/physics/clamped.js +36 -36
  43. package/src/{engine/behaviors → behaviors}/physics/collidable.js +20 -20
  44. package/src/{engine/behaviors → behaviors}/physics/jumpable.js +145 -145
  45. package/src/{engine/behaviors → behaviors}/ui/button.js +17 -17
  46. package/src/{engine/collision → collision}/detection.js +110 -110
  47. package/src/{engine/core → core}/api.js +34 -34
  48. package/src/{engine/core → core}/dev-tools.js +135 -135
  49. package/src/{engine/core → core}/engine.js +119 -119
  50. package/src/{engine/core → core}/loop.js +15 -15
  51. package/src/{engine/core → core}/loops/animation-frame.js +25 -25
  52. package/src/{engine/core → core}/loops/elapsed.js +22 -22
  53. package/src/{engine/core → core}/loops/fixed.js +27 -27
  54. package/src/{engine/core → core}/loops/flash.js +13 -13
  55. package/src/{engine/core → core}/loops/lag.js +26 -26
  56. package/src/{engine/core → core}/select.js +26 -26
  57. package/src/{engine/core → core}/store.js +178 -178
  58. package/src/{engine/core → core}/store.test.js +110 -110
  59. package/src/main.js +10 -10
  60. package/src/{engine/movement → movement}/dynamic/modern.js +21 -21
  61. package/src/{engine/movement → movement}/dynamic/tank.js +43 -43
  62. package/src/{engine/movement → movement}/kinematic/modern.js +16 -16
  63. package/src/{engine/movement → movement}/kinematic/modern.test.js +27 -27
  64. package/src/{engine/movement → movement}/kinematic/tank.js +27 -27
  65. package/src/{engine/physics → physics}/bounds.js +138 -138
  66. package/src/{engine/physics → physics}/position.js +43 -43
  67. package/src/{engine/physics → physics}/position.test.js +80 -80
  68. package/src/{engine/systems → systems}/sprite-animation.js +27 -27
  69. package/src/engine/behaviors/debug/collision.js +0 -35
  70. package/src/renderers/canvas/absolute-position.js +0 -18
  71. package/src/renderers/canvas/camera.js +0 -13
  72. package/src/renderers/canvas/canvas-renderer.js +0 -68
  73. package/src/renderers/canvas/character.js +0 -38
  74. package/src/renderers/canvas/form/button.js +0 -25
  75. package/src/renderers/canvas/fps.js +0 -18
  76. package/src/renderers/canvas/image/hitmask.js +0 -51
  77. package/src/renderers/canvas/image/image.js +0 -34
  78. package/src/renderers/canvas/image/sprite.js +0 -49
  79. package/src/renderers/canvas/image/tilemap.js +0 -66
  80. package/src/renderers/canvas/mouse.js +0 -37
  81. package/src/renderers/canvas/rendering-system.js +0 -79
  82. package/src/renderers/canvas/shapes/circle.js +0 -29
  83. package/src/renderers/canvas/shapes/rectangle.js +0 -27
  84. package/src/renderers/react/game/character/character.module.scss +0 -17
  85. package/src/renderers/react/game/character/index.jsx +0 -20
  86. package/src/renderers/react/game/cursor/cursor.module.scss +0 -47
  87. package/src/renderers/react/game/cursor/index.jsx +0 -20
  88. package/src/renderers/react/game/form/fields/field/field.module.scss +0 -5
  89. package/src/renderers/react/game/form/fields/field/index.jsx +0 -56
  90. package/src/renderers/react/game/form/fields/fields.module.scss +0 -48
  91. package/src/renderers/react/game/form/fields/index.jsx +0 -12
  92. package/src/renderers/react/game/form/form.module.scss +0 -18
  93. package/src/renderers/react/game/form/index.jsx +0 -22
  94. package/src/renderers/react/game/fps/index.jsx +0 -16
  95. package/src/renderers/react/game/game.jsx +0 -72
  96. package/src/renderers/react/game/index.jsx +0 -29
  97. package/src/renderers/react/game/platform/index.jsx +0 -30
  98. package/src/renderers/react/game/platform/platform.module.scss +0 -7
  99. package/src/renderers/react/game/scene/index.jsx +0 -27
  100. package/src/renderers/react/game/scene/scene.module.scss +0 -9
  101. package/src/renderers/react/game/sprite/index.jsx +0 -60
  102. package/src/renderers/react/game/sprite/sprite.module.css +0 -3
  103. package/src/renderers/react/game/stats/index.jsx +0 -22
  104. package/src/renderers/react/hocs/with-absolute-position/index.jsx +0 -20
  105. package/src/renderers/react/hocs/with-absolute-position/with-absolute-position.module.scss +0 -5
  106. package/src/renderers/react/index.jsx +0 -9
  107. package/src/utils/algorithms/decision-tree.js +0 -24
  108. package/src/utils/algorithms/decision-tree.test.js +0 -153
  109. package/src/utils/algorithms/path-finding.js +0 -155
  110. package/src/utils/algorithms/path-finding.test.js +0 -151
  111. package/src/utils/algorithms/types.d.ts +0 -28
  112. package/src/utils/data-structures/array.js +0 -83
  113. package/src/utils/data-structures/array.test.js +0 -173
  114. package/src/utils/data-structures/board.js +0 -159
  115. package/src/utils/data-structures/board.test.js +0 -242
  116. package/src/utils/data-structures/boolean.js +0 -9
  117. package/src/utils/data-structures/heap.js +0 -164
  118. package/src/utils/data-structures/heap.test.js +0 -103
  119. package/src/utils/data-structures/object.js +0 -138
  120. package/src/utils/data-structures/object.test.js +0 -218
  121. package/src/utils/data-structures/objects.js +0 -66
  122. package/src/utils/data-structures/objects.test.js +0 -99
  123. package/src/utils/data-structures/tree.js +0 -36
  124. package/src/utils/data-structures/tree.test.js +0 -33
  125. package/src/utils/data-structures/types.d.ts +0 -4
  126. package/src/utils/functions/functions.js +0 -19
  127. package/src/utils/functions/functions.test.js +0 -23
  128. package/src/utils/math/geometry/circle.js +0 -70
  129. package/src/utils/math/geometry/circle.test.js +0 -97
  130. package/src/utils/math/geometry/hitmask.js +0 -70
  131. package/src/utils/math/geometry/hitmask.test.js +0 -155
  132. package/src/utils/math/geometry/line.js +0 -35
  133. package/src/utils/math/geometry/line.test.js +0 -49
  134. package/src/utils/math/geometry/point.js +0 -78
  135. package/src/utils/math/geometry/point.test.js +0 -81
  136. package/src/utils/math/geometry/rectangle.js +0 -76
  137. package/src/utils/math/geometry/rectangle.test.js +0 -42
  138. package/src/utils/math/geometry/segment.js +0 -80
  139. package/src/utils/math/geometry/segment.test.js +0 -183
  140. package/src/utils/math/geometry/triangle.js +0 -15
  141. package/src/utils/math/geometry/triangle.test.js +0 -11
  142. package/src/utils/math/geometry/types.d.ts +0 -23
  143. package/src/utils/math/linear-algebra/2d.js +0 -28
  144. package/src/utils/math/linear-algebra/2d.test.js +0 -17
  145. package/src/utils/math/linear-algebra/quaternion.js +0 -22
  146. package/src/utils/math/linear-algebra/quaternion.test.js +0 -25
  147. package/src/utils/math/linear-algebra/quaternions.js +0 -20
  148. package/src/utils/math/linear-algebra/quaternions.test.js +0 -29
  149. package/src/utils/math/linear-algebra/types.d.ts +0 -4
  150. package/src/utils/math/linear-algebra/vector.js +0 -327
  151. package/src/utils/math/linear-algebra/vector.test.js +0 -265
  152. package/src/utils/math/linear-algebra/vectors.js +0 -122
  153. package/src/utils/math/linear-algebra/vectors.test.js +0 -65
  154. package/src/utils/math/linear-interpolation.js +0 -9
  155. package/src/utils/math/numbers.js +0 -90
  156. package/src/utils/math/numbers.test.js +0 -137
  157. package/src/utils/math/rng.js +0 -44
  158. package/src/utils/math/rng.test.js +0 -39
  159. package/src/utils/math/statistics.js +0 -43
  160. package/src/utils/math/statistics.test.js +0 -47
  161. package/src/utils/math/trigonometry.js +0 -89
  162. package/src/utils/math/trigonometry.test.js +0 -52
  163. package/src/utils/physics/acceleration.js +0 -61
  164. package/src/utils/physics/friction.js +0 -28
  165. package/src/utils/physics/friction.test.js +0 -42
  166. package/src/utils/physics/gravity.js +0 -69
  167. package/src/utils/physics/gravity.test.js +0 -77
  168. package/src/utils/physics/jump.js +0 -31
  169. package/src/utils/physics/velocity.js +0 -36
package/README.md CHANGED
@@ -1,75 +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. **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.
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.2.0",
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,33 +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": "^4.1.1",
40
- "@storybook/addon-docs": "^9.1.3",
41
- "@storybook/addon-links": "^9.1.3",
42
- "@storybook/react-vite": "^9.1.3",
43
- "@types/react": "^18.2.15",
44
- "@types/react-dom": "^18.2.7",
45
- "@vitejs/plugin-react": "^4.3.1",
46
39
  "husky": "^9.1.7",
47
40
  "prettier": "^3.5.3",
48
- "sass": "^1.77.8",
49
- "serve": "^14.2.4",
50
- "storybook": "^9.1.3",
51
41
  "vite": "^7.1.3",
52
42
  "vitest": "^1.6.0"
53
43
  },
44
+ "engines": {
45
+ "node": ">= 22"
46
+ },
54
47
  "scripts": {
55
- "lint": "eslint . --ext js,jsx --report-unused-disable-directives --max-warnings 0",
56
48
  "format": "prettier --write '**/*.{js,jsx}'",
49
+ "lint": "eslint . --ext js,jsx --report-unused-disable-directives --max-warnings 0",
57
50
  "test:watch": "vitest",
58
- "test": "vitest run",
59
- "start": "serve",
60
- "storybook": "storybook dev -p 6006",
61
- "build-storybook": "storybook build"
51
+ "test": "vitest run"
62
52
  }
63
53
  }
@@ -1,63 +1,63 @@
1
- import { abs, clamp } from "@inglorious/utils/math/numbers.js"
2
- import { toRange } from "@inglorious/utils/math/trigonometry.js"
3
-
4
- export const DEFAULT_TARGET_RADIUS = 0.1
5
- export const DEFAULT_SLOW_RADIUS = 0.1
6
- export const DEFAULT_TIME_TO_TARGET = 0.1
7
-
8
- const DEFAULT_MAX_ANGULAR_ACCELERATION = 0
9
- const DEFAULT_MAX_ANGULAR_SPEED = 0
10
-
11
- const DEFAULT_ANGULAR_SPEED = 0
12
- const DEFAULT_ORIENTATION = 0
13
-
14
- const HALF_ANGULAR_ACCELERATION = 0.5
15
-
16
- export function align(
17
- entity,
18
- target,
19
- dt,
20
- {
21
- targetRadius = DEFAULT_TARGET_RADIUS,
22
- slowRadius = DEFAULT_SLOW_RADIUS,
23
- timeToTarget = DEFAULT_TIME_TO_TARGET,
24
- } = {},
25
- ) {
26
- const maxAngularAcceleration =
27
- entity.maxAngularAcceleration ?? DEFAULT_MAX_ANGULAR_ACCELERATION
28
- const maxAngularSpeed = entity.maxAngularSpeed ?? DEFAULT_MAX_ANGULAR_SPEED
29
-
30
- let angularSpeed = entity.angularSpeed ?? DEFAULT_ANGULAR_SPEED
31
- let orientation = entity.orientation ?? DEFAULT_ORIENTATION
32
-
33
- const direction = toRange(target.orientation - orientation)
34
- const distance = abs(direction)
35
-
36
- if (distance < targetRadius) {
37
- return entity
38
- }
39
-
40
- let targetAngularSpeed
41
- if (distance > slowRadius) {
42
- targetAngularSpeed = maxAngularSpeed
43
- } else {
44
- targetAngularSpeed = (distance * maxAngularSpeed) / slowRadius
45
- }
46
- targetAngularSpeed *= direction / distance // restore rotation sign
47
-
48
- const angularSpeedDelta = targetAngularSpeed - angularSpeed
49
-
50
- let angularAcceleration = angularSpeedDelta / timeToTarget
51
- angularAcceleration = clamp(
52
- angularAcceleration,
53
- -maxAngularAcceleration,
54
- maxAngularAcceleration,
55
- )
56
-
57
- angularSpeed += angularAcceleration * dt
58
- orientation +=
59
- angularSpeed * dt +
60
- angularAcceleration * HALF_ANGULAR_ACCELERATION * dt * dt
61
-
62
- return { angularSpeed, orientation }
63
- }
1
+ import { abs, clamp } from "@inglorious/utils/math/numbers.js"
2
+ import { toRange } from "@inglorious/utils/math/trigonometry.js"
3
+
4
+ export const DEFAULT_TARGET_RADIUS = 0.1
5
+ export const DEFAULT_SLOW_RADIUS = 0.1
6
+ export const DEFAULT_TIME_TO_TARGET = 0.1
7
+
8
+ const DEFAULT_MAX_ANGULAR_ACCELERATION = 0
9
+ const DEFAULT_MAX_ANGULAR_SPEED = 0
10
+
11
+ const DEFAULT_ANGULAR_SPEED = 0
12
+ const DEFAULT_ORIENTATION = 0
13
+
14
+ const HALF_ANGULAR_ACCELERATION = 0.5
15
+
16
+ export function align(
17
+ entity,
18
+ target,
19
+ dt,
20
+ {
21
+ targetRadius = DEFAULT_TARGET_RADIUS,
22
+ slowRadius = DEFAULT_SLOW_RADIUS,
23
+ timeToTarget = DEFAULT_TIME_TO_TARGET,
24
+ } = {},
25
+ ) {
26
+ const maxAngularAcceleration =
27
+ entity.maxAngularAcceleration ?? DEFAULT_MAX_ANGULAR_ACCELERATION
28
+ const maxAngularSpeed = entity.maxAngularSpeed ?? DEFAULT_MAX_ANGULAR_SPEED
29
+
30
+ let angularSpeed = entity.angularSpeed ?? DEFAULT_ANGULAR_SPEED
31
+ let orientation = entity.orientation ?? DEFAULT_ORIENTATION
32
+
33
+ const direction = toRange(target.orientation - orientation)
34
+ const distance = abs(direction)
35
+
36
+ if (distance < targetRadius) {
37
+ return entity
38
+ }
39
+
40
+ let targetAngularSpeed
41
+ if (distance > slowRadius) {
42
+ targetAngularSpeed = maxAngularSpeed
43
+ } else {
44
+ targetAngularSpeed = (distance * maxAngularSpeed) / slowRadius
45
+ }
46
+ targetAngularSpeed *= direction / distance // restore rotation sign
47
+
48
+ const angularSpeedDelta = targetAngularSpeed - angularSpeed
49
+
50
+ let angularAcceleration = angularSpeedDelta / timeToTarget
51
+ angularAcceleration = clamp(
52
+ angularAcceleration,
53
+ -maxAngularAcceleration,
54
+ maxAngularAcceleration,
55
+ )
56
+
57
+ angularSpeed += angularAcceleration * dt
58
+ orientation +=
59
+ angularSpeed * dt +
60
+ angularAcceleration * HALF_ANGULAR_ACCELERATION * dt * dt
61
+
62
+ return { angularSpeed, orientation }
63
+ }
@@ -1,42 +1,42 @@
1
- import { matchVelocity } from "@inglorious/engine/ai/movement/dynamic/match-velocity.js"
2
- import {
3
- magnitude,
4
- setMagnitude,
5
- } from "@inglorious/utils/math/linear-algebra/vector.js"
6
- import { subtract } from "@inglorious/utils/math/linear-algebra/vectors.js"
7
-
8
- export const DEFAULT_TARGET_RADIUS = 1
9
- export const DEFAULT_SLOW_RADIUS = 100
10
- export const DEFAULT_TIME_TO_TARGET = 0.1
11
-
12
- const DEFAULT_MAX_SPEED = 0
13
-
14
- export function arrive(
15
- entity,
16
- target,
17
- dt,
18
- {
19
- targetRadius = DEFAULT_TARGET_RADIUS,
20
- slowRadius = DEFAULT_SLOW_RADIUS,
21
- timeToTarget = DEFAULT_TIME_TO_TARGET,
22
- } = {},
23
- ) {
24
- const maxSpeed = entity.maxSpeed ?? DEFAULT_MAX_SPEED
25
-
26
- const direction = subtract(target.position, entity.position)
27
- const distance = magnitude(direction)
28
-
29
- if (distance < targetRadius) {
30
- return entity
31
- }
32
-
33
- let speed
34
- if (distance > slowRadius) {
35
- speed = maxSpeed
36
- } else {
37
- speed = (distance * maxSpeed) / slowRadius
38
- }
39
- const velocity = setMagnitude(direction, speed)
40
-
41
- return matchVelocity(entity, { velocity }, dt, { timeToTarget })
42
- }
1
+ import { matchVelocity } from "@inglorious/engine/ai/movement/dynamic/match-velocity.js"
2
+ import {
3
+ magnitude,
4
+ setMagnitude,
5
+ } from "@inglorious/utils/math/linear-algebra/vector.js"
6
+ import { subtract } from "@inglorious/utils/math/linear-algebra/vectors.js"
7
+
8
+ export const DEFAULT_TARGET_RADIUS = 1
9
+ export const DEFAULT_SLOW_RADIUS = 100
10
+ export const DEFAULT_TIME_TO_TARGET = 0.1
11
+
12
+ const DEFAULT_MAX_SPEED = 0
13
+
14
+ export function arrive(
15
+ entity,
16
+ target,
17
+ dt,
18
+ {
19
+ targetRadius = DEFAULT_TARGET_RADIUS,
20
+ slowRadius = DEFAULT_SLOW_RADIUS,
21
+ timeToTarget = DEFAULT_TIME_TO_TARGET,
22
+ } = {},
23
+ ) {
24
+ const maxSpeed = entity.maxSpeed ?? DEFAULT_MAX_SPEED
25
+
26
+ const direction = subtract(target.position, entity.position)
27
+ const distance = magnitude(direction)
28
+
29
+ if (distance < targetRadius) {
30
+ return entity
31
+ }
32
+
33
+ let speed
34
+ if (distance > slowRadius) {
35
+ speed = maxSpeed
36
+ } else {
37
+ speed = (distance * maxSpeed) / slowRadius
38
+ }
39
+ const velocity = setMagnitude(direction, speed)
40
+
41
+ return matchVelocity(entity, { velocity }, dt, { timeToTarget })
42
+ }
@@ -1,38 +1,38 @@
1
- import { flee } from "@inglorious/engine/ai/movement/dynamic/flee.js"
2
- import {
3
- magnitude,
4
- multiply,
5
- zero,
6
- } from "@inglorious/utils/math/linear-algebra/vector.js"
7
- import { subtract, sum } from "@inglorious/utils/math/linear-algebra/vectors.js"
8
-
9
- export const DEFAULT_MAX_PREDICTION = 10
10
-
11
- export function evade(
12
- entity,
13
- target,
14
- dt,
15
- { maxPrediction = DEFAULT_MAX_PREDICTION } = {},
16
- ) {
17
- let velocity = entity.velocity ?? zero()
18
-
19
- const direction = subtract(target.position, entity.position)
20
- const distance = magnitude(direction)
21
-
22
- if (!distance) {
23
- return entity
24
- }
25
-
26
- const speed = magnitude(velocity)
27
-
28
- let prediction
29
- if (speed <= distance / maxPrediction) {
30
- prediction = maxPrediction
31
- } else {
32
- prediction = distance / speed
33
- }
34
-
35
- const position = sum(target.position, multiply(target.velocity, prediction))
36
-
37
- return flee(entity, { ...target, position }, dt)
38
- }
1
+ import { flee } from "@inglorious/engine/ai/movement/dynamic/flee.js"
2
+ import {
3
+ magnitude,
4
+ multiply,
5
+ zero,
6
+ } from "@inglorious/utils/math/linear-algebra/vector.js"
7
+ import { subtract, sum } from "@inglorious/utils/math/linear-algebra/vectors.js"
8
+
9
+ export const DEFAULT_MAX_PREDICTION = 10
10
+
11
+ export function evade(
12
+ entity,
13
+ target,
14
+ dt,
15
+ { maxPrediction = DEFAULT_MAX_PREDICTION } = {},
16
+ ) {
17
+ let velocity = entity.velocity ?? zero()
18
+
19
+ const direction = subtract(target.position, entity.position)
20
+ const distance = magnitude(direction)
21
+
22
+ if (!distance) {
23
+ return entity
24
+ }
25
+
26
+ const speed = magnitude(velocity)
27
+
28
+ let prediction
29
+ if (speed <= distance / maxPrediction) {
30
+ prediction = maxPrediction
31
+ } else {
32
+ prediction = distance / speed
33
+ }
34
+
35
+ const position = sum(target.position, multiply(target.velocity, prediction))
36
+
37
+ return flee(entity, { ...target, position }, dt)
38
+ }
@@ -1,19 +1,19 @@
1
- import { align } from "@inglorious/engine/ai/movement/dynamic/align.js"
2
- import {
3
- angle,
4
- magnitude,
5
- } from "@inglorious/utils/math/linear-algebra/vector.js"
6
- import { subtract } from "@inglorious/utils/math/linear-algebra/vectors.js"
7
-
8
- export function face(entity, target, dt, options) {
9
- const direction = subtract(target.position, entity.position)
10
- const distance = magnitude(direction)
11
-
12
- if (!distance) {
13
- return entity
14
- }
15
-
16
- const orientation = angle(direction)
17
-
18
- return align(entity, { ...target, orientation }, dt, options)
19
- }
1
+ import { align } from "@inglorious/engine/ai/movement/dynamic/align.js"
2
+ import {
3
+ angle,
4
+ magnitude,
5
+ } from "@inglorious/utils/math/linear-algebra/vector.js"
6
+ import { subtract } from "@inglorious/utils/math/linear-algebra/vectors.js"
7
+
8
+ export function face(entity, target, dt, options) {
9
+ const direction = subtract(target.position, entity.position)
10
+ const distance = magnitude(direction)
11
+
12
+ if (!distance) {
13
+ return entity
14
+ }
15
+
16
+ const orientation = angle(direction)
17
+
18
+ return align(entity, { ...target, orientation }, dt, options)
19
+ }
@@ -1,45 +1,45 @@
1
- import {
2
- angle,
3
- clamp,
4
- magnitude,
5
- multiply,
6
- setMagnitude,
7
- zero,
8
- } from "@inglorious/utils/math/linear-algebra/vector.js"
9
- import { subtract, sum } from "@inglorious/utils/math/linear-algebra/vectors.js"
10
-
11
- const DEFAULT_MAX_ACCELERATION = 0
12
- const DEFAULT_MAX_SPEED = 0
13
-
14
- const MIN_SPEED = 0
15
-
16
- const HALF_ACCELERATION = 0.5
17
-
18
- export function flee(entity, target, dt) {
19
- const maxAcceleration = entity.maxAcceleration ?? DEFAULT_MAX_ACCELERATION
20
- const maxSpeed = entity.maxSpeed ?? DEFAULT_MAX_SPEED
21
-
22
- let velocity = entity.velocity ?? zero()
23
-
24
- const direction = subtract(entity.position, target.position)
25
- const distance = magnitude(direction)
26
-
27
- if (!distance) {
28
- return entity
29
- }
30
-
31
- const acceleration = setMagnitude(direction, maxAcceleration)
32
-
33
- velocity = sum(velocity, multiply(acceleration, dt))
34
- velocity = clamp(velocity, MIN_SPEED, maxSpeed)
35
-
36
- const position = sum(
37
- entity.position,
38
- multiply(velocity, dt),
39
- multiply(acceleration, HALF_ACCELERATION * dt * dt),
40
- )
41
-
42
- const orientation = angle(velocity)
43
-
44
- return { velocity, position, orientation }
45
- }
1
+ import {
2
+ angle,
3
+ clamp,
4
+ magnitude,
5
+ multiply,
6
+ setMagnitude,
7
+ zero,
8
+ } from "@inglorious/utils/math/linear-algebra/vector.js"
9
+ import { subtract, sum } from "@inglorious/utils/math/linear-algebra/vectors.js"
10
+
11
+ const DEFAULT_MAX_ACCELERATION = 0
12
+ const DEFAULT_MAX_SPEED = 0
13
+
14
+ const MIN_SPEED = 0
15
+
16
+ const HALF_ACCELERATION = 0.5
17
+
18
+ export function flee(entity, target, dt) {
19
+ const maxAcceleration = entity.maxAcceleration ?? DEFAULT_MAX_ACCELERATION
20
+ const maxSpeed = entity.maxSpeed ?? DEFAULT_MAX_SPEED
21
+
22
+ let velocity = entity.velocity ?? zero()
23
+
24
+ const direction = subtract(entity.position, target.position)
25
+ const distance = magnitude(direction)
26
+
27
+ if (!distance) {
28
+ return entity
29
+ }
30
+
31
+ const acceleration = setMagnitude(direction, maxAcceleration)
32
+
33
+ velocity = sum(velocity, multiply(acceleration, dt))
34
+ velocity = clamp(velocity, MIN_SPEED, maxSpeed)
35
+
36
+ const position = sum(
37
+ entity.position,
38
+ multiply(velocity, dt),
39
+ multiply(acceleration, HALF_ACCELERATION * dt * dt),
40
+ )
41
+
42
+ const orientation = angle(velocity)
43
+
44
+ return { velocity, position, orientation }
45
+ }