@inglorious/engine 0.1.0

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
Files changed (256) hide show
  1. package/LICENSE +9 -0
  2. package/README.md +72 -0
  3. package/package.json +76 -0
  4. package/src/docs/ai/movement/dynamic/align.js +131 -0
  5. package/src/docs/ai/movement/dynamic/arrive.js +88 -0
  6. package/src/docs/ai/movement/dynamic/dynamic.mdx +99 -0
  7. package/src/docs/ai/movement/dynamic/dynamic.stories.js +58 -0
  8. package/src/docs/ai/movement/dynamic/evade.js +72 -0
  9. package/src/docs/ai/movement/dynamic/face.js +90 -0
  10. package/src/docs/ai/movement/dynamic/flee.js +38 -0
  11. package/src/docs/ai/movement/dynamic/look-where-youre-going.js +114 -0
  12. package/src/docs/ai/movement/dynamic/match-velocity.js +92 -0
  13. package/src/docs/ai/movement/dynamic/pursue.js +72 -0
  14. package/src/docs/ai/movement/dynamic/seek.js +37 -0
  15. package/src/docs/ai/movement/dynamic/wander.js +71 -0
  16. package/src/docs/ai/movement/kinematic/align.js +122 -0
  17. package/src/docs/ai/movement/kinematic/arrive.js +78 -0
  18. package/src/docs/ai/movement/kinematic/face.js +82 -0
  19. package/src/docs/ai/movement/kinematic/flee.js +36 -0
  20. package/src/docs/ai/movement/kinematic/kinematic.mdx +67 -0
  21. package/src/docs/ai/movement/kinematic/kinematic.stories.js +42 -0
  22. package/src/docs/ai/movement/kinematic/seek.js +34 -0
  23. package/src/docs/ai/movement/kinematic/wander-as-seek.js +62 -0
  24. package/src/docs/ai/movement/kinematic/wander.js +28 -0
  25. package/src/docs/bounds.js +7 -0
  26. package/src/docs/code-reuse.js +35 -0
  27. package/src/docs/collision/circles.js +58 -0
  28. package/src/docs/collision/collision.mdx +27 -0
  29. package/src/docs/collision/collision.stories.js +22 -0
  30. package/src/docs/collision/platform.js +76 -0
  31. package/src/docs/collision/tilemap.js +181 -0
  32. package/src/docs/empty.js +1 -0
  33. package/src/docs/engine.mdx +81 -0
  34. package/src/docs/engine.stories.js +37 -0
  35. package/src/docs/event-handlers.js +68 -0
  36. package/src/docs/framerate.js +37 -0
  37. package/src/docs/game.jsx +15 -0
  38. package/src/docs/image/image.js +19 -0
  39. package/src/docs/image/image.stories.js +22 -0
  40. package/src/docs/image/sprite.js +39 -0
  41. package/src/docs/image/tilemap.js +84 -0
  42. package/src/docs/input/controls.js +67 -0
  43. package/src/docs/input/gamepad.js +67 -0
  44. package/src/docs/input/input.mdx +55 -0
  45. package/src/docs/input/input.stories.js +27 -0
  46. package/src/docs/input/keyboard.js +58 -0
  47. package/src/docs/input/mouse.js +32 -0
  48. package/src/docs/instances.js +49 -0
  49. package/src/docs/player/dynamic/double-jump.js +90 -0
  50. package/src/docs/player/dynamic/dynamic.stories.js +32 -0
  51. package/src/docs/player/dynamic/jump.js +83 -0
  52. package/src/docs/player/dynamic/modern-controls.js +57 -0
  53. package/src/docs/player/dynamic/shooter-controls.js +51 -0
  54. package/src/docs/player/dynamic/tank-controls.js +44 -0
  55. package/src/docs/player/kinematic/double-jump.js +90 -0
  56. package/src/docs/player/kinematic/jump.js +82 -0
  57. package/src/docs/player/kinematic/kinematic.stories.js +32 -0
  58. package/src/docs/player/kinematic/modern-controls.js +56 -0
  59. package/src/docs/player/kinematic/shooter-controls.js +48 -0
  60. package/src/docs/player/kinematic/tank-controls.js +42 -0
  61. package/src/docs/quick-start/first-game.js +49 -0
  62. package/src/docs/quick-start/hello-world.js +1 -0
  63. package/src/docs/quick-start.mdx +127 -0
  64. package/src/docs/quick-start.stories.js +17 -0
  65. package/src/docs/recipes/add-and-remove.js +71 -0
  66. package/src/docs/recipes/add-instance.js +42 -0
  67. package/src/docs/recipes/decision-tree.js +169 -0
  68. package/src/docs/recipes/random-instances.js +25 -0
  69. package/src/docs/recipes/recipes.mdx +81 -0
  70. package/src/docs/recipes/recipes.stories.js +37 -0
  71. package/src/docs/recipes/remove-instance.js +52 -0
  72. package/src/docs/recipes/states.js +64 -0
  73. package/src/docs/ui/button.js +28 -0
  74. package/src/docs/ui/form.stories.js +55 -0
  75. package/src/docs/ui-chooser.jsx +6 -0
  76. package/src/docs/utils/data-structures/object.mdx +47 -0
  77. package/src/docs/utils/data-structures/objects.mdx +30 -0
  78. package/src/docs/utils/functions/functions.mdx +34 -0
  79. package/src/docs/utils/math/geometry/circle.mdx +55 -0
  80. package/src/docs/utils/math/geometry/point.mdx +38 -0
  81. package/src/docs/utils/math/geometry/rectangle.mdx +24 -0
  82. package/src/docs/utils/math/geometry/segment.mdx +55 -0
  83. package/src/docs/utils/math/geometry/triangle.mdx +22 -0
  84. package/src/docs/utils/math/linear-algebra/2d.mdx +22 -0
  85. package/src/docs/utils/math/linear-algebra/quaternion.mdx +21 -0
  86. package/src/docs/utils/math/linear-algebra/quaternions.mdx +22 -0
  87. package/src/docs/utils/math/linear-algebra/vector.mdx +177 -0
  88. package/src/docs/utils/math/linear-algebra/vectors.mdx +58 -0
  89. package/src/docs/utils/math/numbers.mdx +76 -0
  90. package/src/docs/utils/math/random.mdx +35 -0
  91. package/src/docs/utils/math/statistics.mdx +38 -0
  92. package/src/docs/utils/math/trigonometry.mdx +85 -0
  93. package/src/docs/utils/physics/friction.mdx +20 -0
  94. package/src/docs/utils/physics/gravity.mdx +28 -0
  95. package/src/engine/ai/movement/dynamic/align.js +63 -0
  96. package/src/engine/ai/movement/dynamic/arrive.js +43 -0
  97. package/src/engine/ai/movement/dynamic/evade.js +38 -0
  98. package/src/engine/ai/movement/dynamic/face.js +20 -0
  99. package/src/engine/ai/movement/dynamic/flee.js +45 -0
  100. package/src/engine/ai/movement/dynamic/look-where-youre-going.js +17 -0
  101. package/src/engine/ai/movement/dynamic/match-velocity.js +50 -0
  102. package/src/engine/ai/movement/dynamic/pursue.js +38 -0
  103. package/src/engine/ai/movement/dynamic/seek.js +44 -0
  104. package/src/engine/ai/movement/dynamic/wander.js +32 -0
  105. package/src/engine/ai/movement/kinematic/align.js +37 -0
  106. package/src/engine/ai/movement/kinematic/arrive.js +42 -0
  107. package/src/engine/ai/movement/kinematic/face.js +20 -0
  108. package/src/engine/ai/movement/kinematic/flee.js +26 -0
  109. package/src/engine/ai/movement/kinematic/seek.js +26 -0
  110. package/src/engine/ai/movement/kinematic/seek.test.js +42 -0
  111. package/src/engine/ai/movement/kinematic/wander-as-seek.js +31 -0
  112. package/src/engine/ai/movement/kinematic/wander.js +27 -0
  113. package/src/engine/collision/detection.js +115 -0
  114. package/src/engine/loop/animation-frame.js +26 -0
  115. package/src/engine/loop/elapsed.js +23 -0
  116. package/src/engine/loop/fixed.js +28 -0
  117. package/src/engine/loop/flash.js +14 -0
  118. package/src/engine/loop/lag.js +27 -0
  119. package/src/engine/loop.js +15 -0
  120. package/src/engine/movement/dynamic/modern.js +24 -0
  121. package/src/engine/movement/dynamic/tank.js +43 -0
  122. package/src/engine/movement/kinematic/modern.js +16 -0
  123. package/src/engine/movement/kinematic/modern.test.js +27 -0
  124. package/src/engine/movement/kinematic/tank.js +27 -0
  125. package/src/engine/store.js +174 -0
  126. package/src/engine/store.test.js +256 -0
  127. package/src/engine.js +74 -0
  128. package/src/game/animation.js +26 -0
  129. package/src/game/bounds.js +66 -0
  130. package/src/game/decorators/character.js +5 -0
  131. package/src/game/decorators/clamp-to-bounds.js +15 -0
  132. package/src/game/decorators/collisions.js +24 -0
  133. package/src/game/decorators/controls/dynamic/modern.js +48 -0
  134. package/src/game/decorators/controls/dynamic/shooter.js +47 -0
  135. package/src/game/decorators/controls/dynamic/tank.js +55 -0
  136. package/src/game/decorators/controls/kinematic/modern.js +49 -0
  137. package/src/game/decorators/controls/kinematic/shooter.js +45 -0
  138. package/src/game/decorators/controls/kinematic/tank.js +52 -0
  139. package/src/game/decorators/debug/collisions.js +32 -0
  140. package/src/game/decorators/double-jump.js +70 -0
  141. package/src/game/decorators/fps.js +30 -0
  142. package/src/game/decorators/fsm.js +27 -0
  143. package/src/game/decorators/fsm.test.js +56 -0
  144. package/src/game/decorators/game.js +11 -0
  145. package/src/game/decorators/image/image.js +5 -0
  146. package/src/game/decorators/image/sprite.js +5 -0
  147. package/src/game/decorators/image/tilemap.js +5 -0
  148. package/src/game/decorators/input/controls.js +27 -0
  149. package/src/game/decorators/input/gamepad.js +74 -0
  150. package/src/game/decorators/input/input.js +41 -0
  151. package/src/game/decorators/input/keyboard.js +49 -0
  152. package/src/game/decorators/input/mouse.js +65 -0
  153. package/src/game/decorators/jump.js +72 -0
  154. package/src/game/decorators/platform.js +5 -0
  155. package/src/game/decorators/ui/button.js +21 -0
  156. package/src/game/sprite.js +119 -0
  157. package/src/main.js +5 -0
  158. package/src/ui/canvas/absolute-position.js +17 -0
  159. package/src/ui/canvas/character.js +35 -0
  160. package/src/ui/canvas/form/button.js +25 -0
  161. package/src/ui/canvas/fps.js +18 -0
  162. package/src/ui/canvas/image/hitmask.js +37 -0
  163. package/src/ui/canvas/image/image.js +37 -0
  164. package/src/ui/canvas/image/sprite.js +49 -0
  165. package/src/ui/canvas/image/tilemap.js +64 -0
  166. package/src/ui/canvas/mouse.js +37 -0
  167. package/src/ui/canvas/shapes/circle.js +31 -0
  168. package/src/ui/canvas/shapes/rectangle.js +31 -0
  169. package/src/ui/canvas.js +81 -0
  170. package/src/ui/react/game/character/character.module.scss +17 -0
  171. package/src/ui/react/game/character/index.jsx +30 -0
  172. package/src/ui/react/game/cursor/cursor.module.scss +47 -0
  173. package/src/ui/react/game/cursor/index.jsx +20 -0
  174. package/src/ui/react/game/form/fields/field/field.module.scss +5 -0
  175. package/src/ui/react/game/form/fields/field/index.jsx +56 -0
  176. package/src/ui/react/game/form/fields/fields.module.scss +48 -0
  177. package/src/ui/react/game/form/fields/index.jsx +12 -0
  178. package/src/ui/react/game/form/form.module.scss +18 -0
  179. package/src/ui/react/game/form/index.jsx +22 -0
  180. package/src/ui/react/game/fps/index.jsx +16 -0
  181. package/src/ui/react/game/game.jsx +71 -0
  182. package/src/ui/react/game/index.jsx +29 -0
  183. package/src/ui/react/game/platform/index.jsx +30 -0
  184. package/src/ui/react/game/platform/platform.module.scss +7 -0
  185. package/src/ui/react/game/scene/index.jsx +25 -0
  186. package/src/ui/react/game/scene/scene.module.scss +9 -0
  187. package/src/ui/react/game/sprite/index.jsx +58 -0
  188. package/src/ui/react/game/sprite/sprite.module.css +3 -0
  189. package/src/ui/react/game/stats/index.jsx +22 -0
  190. package/src/ui/react/hocs/with-absolute-position/index.jsx +20 -0
  191. package/src/ui/react/hocs/with-absolute-position/with-absolute-position.module.scss +5 -0
  192. package/src/ui/react/index.jsx +9 -0
  193. package/src/utils/algorithms/decision-tree.js +24 -0
  194. package/src/utils/algorithms/decision-tree.test.js +102 -0
  195. package/src/utils/algorithms/path-finding.js +155 -0
  196. package/src/utils/algorithms/path-finding.test.js +151 -0
  197. package/src/utils/algorithms/types.d.ts +28 -0
  198. package/src/utils/data-structures/array.js +83 -0
  199. package/src/utils/data-structures/array.test.js +173 -0
  200. package/src/utils/data-structures/board.js +159 -0
  201. package/src/utils/data-structures/board.test.js +242 -0
  202. package/src/utils/data-structures/boolean.js +9 -0
  203. package/src/utils/data-structures/heap.js +164 -0
  204. package/src/utils/data-structures/heap.test.js +103 -0
  205. package/src/utils/data-structures/object.js +102 -0
  206. package/src/utils/data-structures/object.test.js +121 -0
  207. package/src/utils/data-structures/objects.js +48 -0
  208. package/src/utils/data-structures/objects.test.js +99 -0
  209. package/src/utils/data-structures/tree.js +36 -0
  210. package/src/utils/data-structures/tree.test.js +33 -0
  211. package/src/utils/data-structures/types.d.ts +4 -0
  212. package/src/utils/functions/functions.js +19 -0
  213. package/src/utils/functions/functions.test.js +23 -0
  214. package/src/utils/math/geometry/circle.js +117 -0
  215. package/src/utils/math/geometry/circle.test.js +97 -0
  216. package/src/utils/math/geometry/hitmask.js +39 -0
  217. package/src/utils/math/geometry/hitmask.test.js +84 -0
  218. package/src/utils/math/geometry/line.js +35 -0
  219. package/src/utils/math/geometry/line.test.js +49 -0
  220. package/src/utils/math/geometry/platform.js +42 -0
  221. package/src/utils/math/geometry/platform.test.js +133 -0
  222. package/src/utils/math/geometry/point.js +71 -0
  223. package/src/utils/math/geometry/point.test.js +81 -0
  224. package/src/utils/math/geometry/rectangle.js +45 -0
  225. package/src/utils/math/geometry/rectangle.test.js +42 -0
  226. package/src/utils/math/geometry/segment.js +80 -0
  227. package/src/utils/math/geometry/segment.test.js +183 -0
  228. package/src/utils/math/geometry/triangle.js +15 -0
  229. package/src/utils/math/geometry/triangle.test.js +11 -0
  230. package/src/utils/math/geometry/types.d.ts +23 -0
  231. package/src/utils/math/linear-algebra/2d.js +28 -0
  232. package/src/utils/math/linear-algebra/2d.test.js +17 -0
  233. package/src/utils/math/linear-algebra/quaternion.js +22 -0
  234. package/src/utils/math/linear-algebra/quaternion.test.js +25 -0
  235. package/src/utils/math/linear-algebra/quaternions.js +20 -0
  236. package/src/utils/math/linear-algebra/quaternions.test.js +29 -0
  237. package/src/utils/math/linear-algebra/types.d.ts +4 -0
  238. package/src/utils/math/linear-algebra/vector.js +302 -0
  239. package/src/utils/math/linear-algebra/vector.test.js +257 -0
  240. package/src/utils/math/linear-algebra/vectors.js +122 -0
  241. package/src/utils/math/linear-algebra/vectors.test.js +65 -0
  242. package/src/utils/math/numbers.js +90 -0
  243. package/src/utils/math/numbers.test.js +137 -0
  244. package/src/utils/math/rng.js +44 -0
  245. package/src/utils/math/rng.test.js +39 -0
  246. package/src/utils/math/statistics.js +43 -0
  247. package/src/utils/math/statistics.test.js +47 -0
  248. package/src/utils/math/trigonometry.js +89 -0
  249. package/src/utils/math/trigonometry.test.js +52 -0
  250. package/src/utils/physics/acceleration.js +63 -0
  251. package/src/utils/physics/friction.js +30 -0
  252. package/src/utils/physics/friction.test.js +44 -0
  253. package/src/utils/physics/gravity.js +71 -0
  254. package/src/utils/physics/gravity.test.js +80 -0
  255. package/src/utils/physics/jump.js +41 -0
  256. package/src/utils/physics/velocity.js +38 -0
package/LICENSE ADDED
@@ -0,0 +1,9 @@
1
+ The MIT License (MIT)
2
+
3
+ Copyright © 2025 Inglorious Coderz Srl.
4
+
5
+ Permission is hereby granted, free of charge, to any person obtaining a copy of this software and associated documentation files (the “Software”), to deal in the Software without restriction, including without limitation the rights to use, copy, modify, merge, publish, distribute, sublicense, and/or sell copies of the Software, and to permit persons to whom the Software is furnished to do so, subject to the following conditions:
6
+
7
+ The above copyright notice and this permission notice shall be included in all copies or substantial portions of the Software.
8
+
9
+ THE SOFTWARE IS PROVIDED “AS IS”, WITHOUT WARRANTY OF ANY KIND, EXPRESS OR IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE SOFTWARE.
package/README.md ADDED
@@ -0,0 +1,72 @@
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.
package/package.json ADDED
@@ -0,0 +1,76 @@
1
+ {
2
+ "name": "@inglorious/engine",
3
+ "version": "0.1.0",
4
+ "description": "A JavaScript game engine written with global state, immutability, and pure functions in mind. Have fun(ctional programming) with it!",
5
+ "author": "IceOnFire <antony.mistretta@gmail.com> (https://ingloriouscoderz.it)",
6
+ "license": "MIT",
7
+ "repository": {
8
+ "type": "git",
9
+ "url": "git+https://github.com/IngloriousCoderz/inglorious-engine.git"
10
+ },
11
+ "homepage": "https://inglorious-engine.vercel.app/",
12
+ "keywords": [
13
+ "game-engine",
14
+ "functional-programming",
15
+ "gamedev",
16
+ "javascript",
17
+ "ecs",
18
+ "data-oriented"
19
+ ],
20
+ "type": "module",
21
+ "files": [
22
+ "src",
23
+ "README.md",
24
+ "LICENSE"
25
+ ],
26
+ "exports": {
27
+ "./*": "./src/*"
28
+ },
29
+ "publishConfig": {
30
+ "access": "public"
31
+ },
32
+ "scripts": {
33
+ "lint": "eslint . --ext js,jsx --report-unused-disable-directives --max-warnings 0",
34
+ "format": "prettier --write '**/*.{js,jsx}'",
35
+ "test:watch": "vitest",
36
+ "test": "vitest run",
37
+ "start": "serve",
38
+ "storybook": "storybook dev -p 6006",
39
+ "build-storybook": "storybook build",
40
+ "prepare": "husky"
41
+ },
42
+ "dependencies": {
43
+ "immer": "^10.1.1",
44
+ "react": "^19.0.0",
45
+ "react-dom": "^19.0.0",
46
+ "react-redux": "^9.2.0"
47
+ },
48
+ "devDependencies": {
49
+ "@chromatic-com/storybook": "^3.2.6",
50
+ "@eslint/js": "^9.23.0",
51
+ "@storybook/addon-essentials": "^8.6.12",
52
+ "@storybook/addon-interactions": "^8.6.12",
53
+ "@storybook/addon-links": "^8.6.12",
54
+ "@storybook/blocks": "^8.6.12",
55
+ "@storybook/manager-api": "^8.6.12",
56
+ "@storybook/react": "^8.6.12",
57
+ "@storybook/react-vite": "^8.6.12",
58
+ "@storybook/test": "^8.6.12",
59
+ "@storybook/theming": "^8.6.12",
60
+ "@types/react": "^18.2.15",
61
+ "@types/react-dom": "^18.2.7",
62
+ "@vitejs/plugin-react": "^4.0.3",
63
+ "eslint": "^9.23.0",
64
+ "eslint-plugin-react": "^7.37.4",
65
+ "eslint-plugin-simple-import-sort": "^10.0.0",
66
+ "eslint-plugin-storybook": "^0.12.0",
67
+ "globals": "^16.0.0",
68
+ "husky": "^9.1.7",
69
+ "prettier": "^3.5.3",
70
+ "sass": "^1.66.1",
71
+ "serve": "^14.2.4",
72
+ "storybook": "^8.6.12",
73
+ "vite": "^4.4.5",
74
+ "vitest": "^0.34.3"
75
+ }
76
+ }
@@ -0,0 +1,131 @@
1
+ import align, {
2
+ DEFAULT_SLOW_RADIUS,
3
+ DEFAULT_TARGET_RADIUS,
4
+ DEFAULT_TIME_TO_TARGET,
5
+ } from "@inglorious/engine/ai/movement/dynamic/align.js"
6
+ import { clampToBounds } from "@inglorious/game/bounds.js"
7
+ import { enableCharacter } from "@inglorious/game/decorators/character.js"
8
+ import {
9
+ createControls,
10
+ enableControls,
11
+ } from "@inglorious/game/decorators/input/controls.js"
12
+ import { enableMouse } from "@inglorious/game/decorators/input/mouse.js"
13
+ import { merge } from "@inglorious/utils/data-structures/objects.js"
14
+ import { clamp } from "@inglorious/utils/math/numbers.js"
15
+ import { pi } from "@inglorious/utils/math/trigonometry.js"
16
+
17
+ export default {
18
+ types: {
19
+ mouse: [
20
+ enableMouse(),
21
+ {
22
+ "field:change"(instance, event) {
23
+ const { id, value } = event.payload
24
+ if (id === "targetOrientation") {
25
+ instance.orientation = -value * pi()
26
+ }
27
+ },
28
+
29
+ "game:update"(instance, event, options) {
30
+ const { input0 } = options.instances
31
+
32
+ if (input0.left || input0.up) {
33
+ instance.orientation += 0.1
34
+ } else if (input0.right || input0.down) {
35
+ instance.orientation -= 0.1
36
+ }
37
+ instance.orientation = clamp(instance.orientation, -pi(), pi())
38
+ },
39
+ },
40
+ ],
41
+
42
+ ...enableControls(),
43
+
44
+ character: [
45
+ enableCharacter(),
46
+ {
47
+ "game:update"(instance, event, { dt, instances }) {
48
+ const target = instances.mouse
49
+ const { fields } = instances.parameters.groups.align
50
+
51
+ merge(
52
+ instance,
53
+ align(instance, target, {
54
+ dt,
55
+ targetRadius: fields.targetRadius.value,
56
+ slowRadius: fields.slowRadius.value,
57
+ timeToTarget: fields.timeToTarget.value,
58
+ }),
59
+ )
60
+
61
+ clampToBounds(instance, instances.game.bounds)
62
+ },
63
+ },
64
+ ],
65
+
66
+ form: {
67
+ "field:change"(instance, event) {
68
+ const { id, value } = event.payload
69
+ instance.groups.align.fields[id].value = value
70
+ },
71
+ },
72
+ },
73
+
74
+ instances: {
75
+ mouse: {
76
+ type: "mouse",
77
+ position: [400, 0, 300],
78
+ orientation: 0,
79
+ },
80
+
81
+ ...createControls("input0", {
82
+ ArrowLeft: "left",
83
+ ArrowRight: "right",
84
+ ArrowDown: "down",
85
+ ArrowUp: "up",
86
+ }),
87
+
88
+ character: {
89
+ type: "character",
90
+ maxAngularSpeed: pi() / 4,
91
+ maxAngularAcceleration: 10,
92
+ position: [400, 0, 300],
93
+ },
94
+
95
+ parameters: {
96
+ type: "form",
97
+ position: [800 - 328, 0, 600],
98
+ groups: {
99
+ align: {
100
+ title: "Dynamic Align",
101
+ fields: {
102
+ targetRadius: {
103
+ label: "Target Radius",
104
+ inputType: "number",
105
+ defaultValue: DEFAULT_TARGET_RADIUS,
106
+ },
107
+ slowRadius: {
108
+ label: "Slow Radius",
109
+ inputType: "number",
110
+ defaultValue: DEFAULT_SLOW_RADIUS,
111
+ },
112
+ timeToTarget: {
113
+ label: "Time To Target",
114
+ inputType: "number",
115
+ step: 0.1,
116
+ defaultValue: DEFAULT_TIME_TO_TARGET,
117
+ },
118
+ targetOrientation: {
119
+ label: "Target Orientation",
120
+ inputType: "number",
121
+ step: 0.25,
122
+ min: -1,
123
+ max: 1,
124
+ defaultValue: 0,
125
+ },
126
+ },
127
+ },
128
+ },
129
+ },
130
+ },
131
+ }
@@ -0,0 +1,88 @@
1
+ import arrive, {
2
+ DEFAULT_SLOW_RADIUS,
3
+ DEFAULT_TARGET_RADIUS,
4
+ DEFAULT_TIME_TO_TARGET,
5
+ } from "@inglorious/engine/ai/movement/dynamic/arrive.js"
6
+ import { clampToBounds } from "@inglorious/game/bounds.js"
7
+ import { enableCharacter } from "@inglorious/game/decorators/character.js"
8
+ import { enableMouse } from "@inglorious/game/decorators/input/mouse.js"
9
+ import { merge } from "@inglorious/utils/data-structures/objects.js"
10
+
11
+ export default {
12
+ types: {
13
+ mouse: [enableMouse()],
14
+
15
+ character: [
16
+ enableCharacter(),
17
+ {
18
+ "game:update"(instance, event, { dt, instances }) {
19
+ const target = instances.mouse
20
+ const { fields } = instances.parameters.groups.arrive
21
+
22
+ merge(
23
+ instance,
24
+ arrive(instance, target, {
25
+ dt,
26
+ targetRadius: fields.targetRadius.value,
27
+ slowRadius: fields.slowRadius.value,
28
+ timeToTarget: fields.timeToTarget.value,
29
+ }),
30
+ )
31
+
32
+ clampToBounds(instance, instances.game.bounds)
33
+ },
34
+ },
35
+ ],
36
+
37
+ form: {
38
+ "field:change"(instance, event) {
39
+ const { id, value } = event.payload
40
+ instance.groups.arrive.fields[id].value = value
41
+ },
42
+ },
43
+ },
44
+
45
+ instances: {
46
+ mouse: {
47
+ type: "mouse",
48
+ position: [400, 0, 300],
49
+ },
50
+
51
+ character: {
52
+ type: "character",
53
+ maxAcceleration: 1000,
54
+ maxSpeed: 250,
55
+ position: [400, 0, 300],
56
+ },
57
+
58
+ parameters: {
59
+ type: "form",
60
+ position: [800 - 328, 0, 600],
61
+ groups: {
62
+ arrive: {
63
+ title: "Dynamic Arrive",
64
+ fields: {
65
+ targetRadius: {
66
+ label: "Target Radius",
67
+ inputType: "number",
68
+ step: 0.1,
69
+ defaultValue: DEFAULT_TARGET_RADIUS,
70
+ },
71
+ slowRadius: {
72
+ label: "Slow Radius",
73
+ inputType: "number",
74
+ step: 0.1,
75
+ defaultValue: DEFAULT_SLOW_RADIUS,
76
+ },
77
+ timeToTarget: {
78
+ label: "Time To Target",
79
+ inputType: "number",
80
+ step: 0.1,
81
+ defaultValue: DEFAULT_TIME_TO_TARGET,
82
+ },
83
+ },
84
+ },
85
+ },
86
+ },
87
+ },
88
+ }
@@ -0,0 +1,99 @@
1
+ import { Canvas, Meta, Source } from '@storybook/blocks'
2
+ import * as Dynamic from './dynamic.stories'
3
+
4
+ import arrive from './arrive?raw'
5
+ import flee from './flee?raw'
6
+ import pursue from './pursue?raw'
7
+ import evade from './evade?raw'
8
+ import seek from './seek?raw'
9
+ import wander from './wander?raw'
10
+ import lookWhereYoureGoing from './look-where-youre-going?raw'
11
+ import face from './face?raw'
12
+ import align from './align?raw'
13
+ import matchVelocity from './match-velocity?raw'
14
+
15
+ <Meta of={Dynamic} />
16
+
17
+ # Dynamic Movement
18
+
19
+ The algorithms described here make an NPC instance move by applying forces, which in turn affect its acceleration and velocity. The movement is much smoother and more realistic than its [kinematic](/docs/ai-movement-kinematic--docs) counterpart.
20
+
21
+ Every algorithm returns a steering object, which contains a linear and an angular acceleration. These can be used to update the instance's velocity and orientation.
22
+
23
+ Most examples here make use of user [input](/?path=/docs/games-input--docs), so check those out if you haven't already.
24
+
25
+ ## Seek
26
+
27
+ The seek algorithm makes the instance chase a target. The movement is much smoother than its kinematic counterpart.
28
+
29
+ <Source dark code={seek} />
30
+
31
+ <Canvas of={Dynamic.Seek} />
32
+
33
+ ## Flee
34
+
35
+ The flee algorithm is the exact opposite of seek: the instance will run away from the target.
36
+
37
+ <Source dark code={flee} />
38
+
39
+ <Canvas of={Dynamic.Flee} />
40
+
41
+ ## Arrive
42
+
43
+ The arrive algorithm is a more refined version of seek: the instance will slow down as it approaches the target, and the movement is much smoother.
44
+
45
+ <Source dark code={arrive} />
46
+
47
+ <Canvas of={Dynamic.Arrive} />
48
+
49
+ ## Pursue
50
+
51
+ The pursue algorithm is an enhanced version of seek. It predicts the future position of a moving target and seeks that position.
52
+
53
+ <Source dark code={pursue} />
54
+
55
+ <Canvas of={Dynamic.Pursue} />
56
+
57
+ ## Evade
58
+
59
+ Evade is the opposite of pursue. It predicts the future position of a moving target and flees from it.
60
+
61
+ <Source dark code={evade} />
62
+
63
+ <Canvas of={Dynamic.Evade} />
64
+
65
+ ## Wander
66
+
67
+ The wander algorithm makes the instance move around randomly, but in a smooth and realistic way.
68
+
69
+ <Source dark code={wander} />
70
+
71
+ <Canvas of={Dynamic.Wander} />
72
+
73
+ ## Look Where You're Going
74
+
75
+ A simple but effective algorithm that aligns the character's orientation with its direction of movement.
76
+
77
+ <Source dark code={lookWhereYoureGoing} />
78
+
79
+ <Canvas of={Dynamic.LookWhereYoureGoing} />
80
+
81
+ ## Align and Face
82
+
83
+ The `align` algorithm makes the instance rotate to match the target's orientation, while `face` makes it rotate to look at the target's position. They are smoother versions of their kinematic counterparts.
84
+
85
+ <Source dark code={align} />
86
+
87
+ <Canvas of={Dynamic.Align} />
88
+
89
+ <Source dark code={face} />
90
+
91
+ <Canvas of={Dynamic.Face} />
92
+
93
+ ## Match Velocity
94
+
95
+ This algorithm adjusts the instance's velocity to match the target's velocity. It's a key component for flocking behaviors.
96
+
97
+ <Source dark code={matchVelocity} />
98
+
99
+ <Canvas of={Dynamic.MatchVelocity} />
@@ -0,0 +1,58 @@
1
+ import UiChooser from "@inglorious/docs/ui-chooser.jsx"
2
+
3
+ import align from "./align.js"
4
+ import arrive from "./arrive.js"
5
+ import evade from "./evade.js"
6
+ import face from "./face.js"
7
+ import flee from "./flee.js"
8
+ import lookWhereYoureGoing from "./look-where-youre-going.js"
9
+ import matchVelocity from "./match-velocity.js"
10
+ import pursue from "./pursue.js"
11
+ import seek from "./seek.js"
12
+ import wander from "./wander.js"
13
+
14
+ export default {
15
+ title: "Engine/AI/Movement/Dynamic",
16
+ component: UiChooser,
17
+ }
18
+
19
+ export const Seek = {
20
+ args: { config: seek },
21
+ }
22
+
23
+ export const Flee = {
24
+ args: { config: flee },
25
+ }
26
+
27
+ export const Arrive = {
28
+ args: { config: arrive },
29
+ }
30
+
31
+ export const Pursue = {
32
+ args: { config: pursue },
33
+ }
34
+
35
+ export const Evade = {
36
+ args: { config: evade },
37
+ }
38
+
39
+ export const Wander = {
40
+ args: { config: wander },
41
+ }
42
+
43
+ export const LookWhereYoureGoing = {
44
+ name: "Look Where You're Going",
45
+ args: { config: lookWhereYoureGoing },
46
+ }
47
+
48
+ export const Align = {
49
+ args: { config: align },
50
+ }
51
+
52
+ export const Face = {
53
+ args: { config: face },
54
+ }
55
+
56
+ export const MatchVelocity = {
57
+ args: { config: matchVelocity },
58
+ }
@@ -0,0 +1,72 @@
1
+ import evade, {
2
+ DEFAULT_MAX_PREDICTION,
3
+ } from "@inglorious/engine/ai/movement/dynamic/evade.js"
4
+ import { clampToBounds } from "@inglorious/game/bounds.js"
5
+ import { enableCharacter } from "@inglorious/game/decorators/character.js"
6
+ import { enableMouse } from "@inglorious/game/decorators/input/mouse.js"
7
+ import { merge } from "@inglorious/utils/data-structures/objects.js"
8
+
9
+ export default {
10
+ types: {
11
+ mouse: [enableMouse()],
12
+
13
+ character: [
14
+ enableCharacter(),
15
+ {
16
+ "game:update"(instance, event, { dt, instances }) {
17
+ const target = instances.mouse
18
+ const { fields } = instances.parameters.groups.evade
19
+
20
+ merge(
21
+ instance,
22
+ evade(instance, target, {
23
+ dt,
24
+ maxPrediction: fields.maxPrediction.value,
25
+ }),
26
+ )
27
+
28
+ clampToBounds(instance, instances.game.bounds)
29
+ },
30
+ },
31
+ ],
32
+
33
+ form: {
34
+ "field:change"(instance, event) {
35
+ const { id, value } = event.payload
36
+ instance.groups.evade.fields[id].value = value
37
+ },
38
+ },
39
+ },
40
+
41
+ instances: {
42
+ mouse: {
43
+ type: "mouse",
44
+ position: [400, 0, 300],
45
+ velocity: [0, 0, 0],
46
+ },
47
+
48
+ character: {
49
+ type: "character",
50
+ maxAcceleration: 1000,
51
+ maxSpeed: 250,
52
+ position: [400, 0, 300],
53
+ },
54
+
55
+ parameters: {
56
+ type: "form",
57
+ position: [800 - 343, 0, 600],
58
+ groups: {
59
+ evade: {
60
+ title: "Evade",
61
+ fields: {
62
+ maxPrediction: {
63
+ label: "Max Prediction",
64
+ inputType: "number",
65
+ defaultValue: DEFAULT_MAX_PREDICTION,
66
+ },
67
+ },
68
+ },
69
+ },
70
+ },
71
+ },
72
+ }
@@ -0,0 +1,90 @@
1
+ import {
2
+ DEFAULT_SLOW_RADIUS,
3
+ DEFAULT_TARGET_RADIUS,
4
+ DEFAULT_TIME_TO_TARGET,
5
+ } from "@inglorious/engine/ai/movement/dynamic/align.js"
6
+ import face from "@inglorious/engine/ai/movement/dynamic/face.js"
7
+ import { clampToBounds } from "@inglorious/game/bounds.js"
8
+ import { enableCharacter } from "@inglorious/game/decorators/character.js"
9
+ import { enableMouse } from "@inglorious/game/decorators/input/mouse.js"
10
+ import { merge } from "@inglorious/utils/data-structures/objects.js"
11
+ import { pi } from "@inglorious/utils/math/trigonometry.js"
12
+
13
+ export default {
14
+ types: {
15
+ mouse: [enableMouse()],
16
+
17
+ character: [
18
+ enableCharacter(),
19
+ {
20
+ "game:update"(instance, event, { dt, instances }) {
21
+ const target = instances.mouse
22
+ const { fields } = instances.parameters.groups.face
23
+
24
+ merge(
25
+ instance,
26
+ face(instance, target, {
27
+ dt,
28
+ targetRadius: fields.targetRadius.value,
29
+ slowRadius: fields.slowRadius.value,
30
+ timeToTarget: fields.timeToTarget.value,
31
+ }),
32
+ )
33
+
34
+ clampToBounds(instance, instances.game.bounds)
35
+ },
36
+ },
37
+ ],
38
+
39
+ form: {
40
+ "field:change"(instance, event) {
41
+ const { id, value } = event.payload
42
+ instance.groups.face.fields[id].value = value
43
+ },
44
+ },
45
+ },
46
+
47
+ instances: {
48
+ mouse: {
49
+ type: "mouse",
50
+ position: [400, 0, 300],
51
+ },
52
+
53
+ character: {
54
+ type: "character",
55
+ maxAngularSpeed: pi() / 4,
56
+ maxAngularAcceleration: 1000,
57
+ position: [400, 0, 300],
58
+ },
59
+
60
+ parameters: {
61
+ type: "form",
62
+ position: [800 - 328, 0, 600],
63
+ groups: {
64
+ face: {
65
+ title: "Face",
66
+ fields: {
67
+ targetRadius: {
68
+ label: "Target Radius",
69
+ inputType: "number",
70
+ step: 0.1,
71
+ defaultValue: DEFAULT_TARGET_RADIUS,
72
+ },
73
+ slowRadius: {
74
+ label: "Slow Radius",
75
+ inputType: "number",
76
+ step: 0.1,
77
+ defaultValue: DEFAULT_SLOW_RADIUS,
78
+ },
79
+ timeToTarget: {
80
+ label: "Time To Target",
81
+ inputType: "number",
82
+ step: 0.1,
83
+ defaultValue: DEFAULT_TIME_TO_TARGET,
84
+ },
85
+ },
86
+ },
87
+ },
88
+ },
89
+ },
90
+ }