@inglorious/engine 0.1.1 → 0.3.0

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
Files changed (201) hide show
  1. package/README.md +75 -72
  2. package/package.json +15 -37
  3. package/src/{engine/ai → ai}/movement/dynamic/align.js +9 -9
  4. package/src/{engine/ai → ai}/movement/dynamic/arrive.js +9 -10
  5. package/src/{engine/ai → ai}/movement/dynamic/evade.js +9 -9
  6. package/src/{engine/ai → ai}/movement/dynamic/face.js +5 -6
  7. package/src/{engine/ai/movement/dynamic/seek.js → ai/movement/dynamic/flee.js} +8 -7
  8. package/src/ai/movement/dynamic/look-where-youre-going.js +16 -0
  9. package/src/{engine/ai → ai}/movement/dynamic/match-velocity.js +9 -8
  10. package/src/{engine/ai → ai}/movement/dynamic/pursue.js +9 -9
  11. package/src/{engine/ai/movement/dynamic/flee.js → ai/movement/dynamic/seek.js} +7 -8
  12. package/src/{engine/ai → ai}/movement/dynamic/wander.js +9 -10
  13. package/src/{engine/ai → ai}/movement/kinematic/align.js +7 -7
  14. package/src/{engine/ai → ai}/movement/kinematic/arrive.js +8 -8
  15. package/src/{engine/ai → ai}/movement/kinematic/face.js +5 -6
  16. package/src/{engine/ai → ai}/movement/kinematic/flee.js +5 -5
  17. package/src/{engine/ai → ai}/movement/kinematic/seek.js +5 -5
  18. package/src/{engine/ai → ai}/movement/kinematic/seek.test.js +10 -10
  19. package/src/{engine/ai → ai}/movement/kinematic/wander-as-seek.js +9 -9
  20. package/src/{engine/ai → ai}/movement/kinematic/wander.js +5 -5
  21. package/src/animation/sprite.js +101 -0
  22. package/src/animation/ticker.js +38 -0
  23. package/src/behaviors/camera.js +68 -0
  24. package/src/behaviors/controls/dynamic/modern.js +76 -0
  25. package/src/behaviors/controls/dynamic/shooter.js +84 -0
  26. package/src/behaviors/controls/dynamic/tank.js +69 -0
  27. package/src/behaviors/controls/event-handlers.js +17 -0
  28. package/src/behaviors/controls/kinematic/modern.js +76 -0
  29. package/src/behaviors/controls/kinematic/shooter.js +82 -0
  30. package/src/behaviors/controls/kinematic/tank.js +67 -0
  31. package/src/behaviors/debug/collision.js +29 -0
  32. package/src/behaviors/fps.js +29 -0
  33. package/src/behaviors/fsm.js +33 -0
  34. package/src/{game/decorators → behaviors}/fsm.test.js +15 -22
  35. package/src/behaviors/game.js +15 -0
  36. package/src/behaviors/input/controls.js +37 -0
  37. package/src/behaviors/input/gamepad.js +114 -0
  38. package/src/behaviors/input/input.js +48 -0
  39. package/src/behaviors/input/keyboard.js +64 -0
  40. package/src/behaviors/input/mouse.js +91 -0
  41. package/src/behaviors/physics/bouncy.js +25 -0
  42. package/src/behaviors/physics/clamped.js +36 -0
  43. package/src/{game/decorators/collisions.js → behaviors/physics/collidable.js} +3 -7
  44. package/src/behaviors/physics/jumpable.js +145 -0
  45. package/src/behaviors/ui/button.js +17 -0
  46. package/src/collision/detection.js +110 -0
  47. package/src/core/api.js +34 -0
  48. package/src/core/dev-tools.js +135 -0
  49. package/src/core/engine.js +119 -0
  50. package/src/core/loop.js +15 -0
  51. package/src/{engine/loop → core/loops}/animation-frame.js +1 -2
  52. package/src/{engine/loop → core/loops}/elapsed.js +1 -2
  53. package/src/{engine/loop → core/loops}/fixed.js +1 -2
  54. package/src/{engine/loop → core/loops}/flash.js +1 -2
  55. package/src/{engine/loop → core/loops}/lag.js +1 -2
  56. package/src/core/select.js +26 -0
  57. package/src/core/store.js +178 -0
  58. package/src/core/store.test.js +110 -0
  59. package/src/main.js +7 -2
  60. package/src/{engine/movement → movement}/dynamic/modern.js +3 -6
  61. package/src/{engine/movement → movement}/dynamic/tank.js +9 -9
  62. package/src/{engine/movement → movement}/kinematic/modern.js +3 -3
  63. package/src/movement/kinematic/modern.test.js +27 -0
  64. package/src/{engine/movement → movement}/kinematic/tank.js +5 -5
  65. package/src/physics/bounds.js +138 -0
  66. package/src/physics/position.js +43 -0
  67. package/src/physics/position.test.js +80 -0
  68. package/src/systems/sprite-animation.js +27 -0
  69. package/src/engine/ai/movement/dynamic/look-where-youre-going.js +0 -17
  70. package/src/engine/collision/detection.js +0 -115
  71. package/src/engine/loop.js +0 -15
  72. package/src/engine/movement/kinematic/modern.test.js +0 -27
  73. package/src/engine/store.js +0 -174
  74. package/src/engine/store.test.js +0 -256
  75. package/src/engine.js +0 -74
  76. package/src/game/animation.js +0 -26
  77. package/src/game/bounds.js +0 -66
  78. package/src/game/decorators/character.js +0 -5
  79. package/src/game/decorators/clamp-to-bounds.js +0 -15
  80. package/src/game/decorators/controls/dynamic/modern.js +0 -48
  81. package/src/game/decorators/controls/dynamic/shooter.js +0 -47
  82. package/src/game/decorators/controls/dynamic/tank.js +0 -55
  83. package/src/game/decorators/controls/kinematic/modern.js +0 -49
  84. package/src/game/decorators/controls/kinematic/shooter.js +0 -45
  85. package/src/game/decorators/controls/kinematic/tank.js +0 -52
  86. package/src/game/decorators/debug/collisions.js +0 -32
  87. package/src/game/decorators/double-jump.js +0 -70
  88. package/src/game/decorators/fps.js +0 -30
  89. package/src/game/decorators/fsm.js +0 -27
  90. package/src/game/decorators/game.js +0 -11
  91. package/src/game/decorators/image/image.js +0 -5
  92. package/src/game/decorators/image/sprite.js +0 -5
  93. package/src/game/decorators/image/tilemap.js +0 -5
  94. package/src/game/decorators/input/controls.js +0 -27
  95. package/src/game/decorators/input/gamepad.js +0 -74
  96. package/src/game/decorators/input/input.js +0 -41
  97. package/src/game/decorators/input/keyboard.js +0 -49
  98. package/src/game/decorators/input/mouse.js +0 -65
  99. package/src/game/decorators/jump.js +0 -72
  100. package/src/game/decorators/platform.js +0 -5
  101. package/src/game/decorators/ui/button.js +0 -21
  102. package/src/game/sprite.js +0 -119
  103. package/src/ui/canvas/absolute-position.js +0 -17
  104. package/src/ui/canvas/character.js +0 -35
  105. package/src/ui/canvas/form/button.js +0 -25
  106. package/src/ui/canvas/fps.js +0 -18
  107. package/src/ui/canvas/image/hitmask.js +0 -37
  108. package/src/ui/canvas/image/image.js +0 -37
  109. package/src/ui/canvas/image/sprite.js +0 -49
  110. package/src/ui/canvas/image/tilemap.js +0 -64
  111. package/src/ui/canvas/mouse.js +0 -37
  112. package/src/ui/canvas/shapes/circle.js +0 -31
  113. package/src/ui/canvas/shapes/rectangle.js +0 -31
  114. package/src/ui/canvas.js +0 -81
  115. package/src/ui/react/game/character/character.module.scss +0 -17
  116. package/src/ui/react/game/character/index.jsx +0 -30
  117. package/src/ui/react/game/cursor/cursor.module.scss +0 -47
  118. package/src/ui/react/game/cursor/index.jsx +0 -20
  119. package/src/ui/react/game/form/fields/field/field.module.scss +0 -5
  120. package/src/ui/react/game/form/fields/field/index.jsx +0 -56
  121. package/src/ui/react/game/form/fields/fields.module.scss +0 -48
  122. package/src/ui/react/game/form/fields/index.jsx +0 -12
  123. package/src/ui/react/game/form/form.module.scss +0 -18
  124. package/src/ui/react/game/form/index.jsx +0 -22
  125. package/src/ui/react/game/fps/index.jsx +0 -16
  126. package/src/ui/react/game/game.jsx +0 -71
  127. package/src/ui/react/game/index.jsx +0 -29
  128. package/src/ui/react/game/platform/index.jsx +0 -30
  129. package/src/ui/react/game/platform/platform.module.scss +0 -7
  130. package/src/ui/react/game/scene/index.jsx +0 -25
  131. package/src/ui/react/game/scene/scene.module.scss +0 -9
  132. package/src/ui/react/game/sprite/index.jsx +0 -58
  133. package/src/ui/react/game/sprite/sprite.module.css +0 -3
  134. package/src/ui/react/game/stats/index.jsx +0 -22
  135. package/src/ui/react/hocs/with-absolute-position/index.jsx +0 -20
  136. package/src/ui/react/hocs/with-absolute-position/with-absolute-position.module.scss +0 -5
  137. package/src/ui/react/index.jsx +0 -9
  138. package/src/utils/algorithms/decision-tree.js +0 -24
  139. package/src/utils/algorithms/decision-tree.test.js +0 -102
  140. package/src/utils/algorithms/path-finding.js +0 -155
  141. package/src/utils/algorithms/path-finding.test.js +0 -151
  142. package/src/utils/algorithms/types.d.ts +0 -28
  143. package/src/utils/data-structures/array.js +0 -83
  144. package/src/utils/data-structures/array.test.js +0 -173
  145. package/src/utils/data-structures/board.js +0 -159
  146. package/src/utils/data-structures/board.test.js +0 -242
  147. package/src/utils/data-structures/boolean.js +0 -9
  148. package/src/utils/data-structures/heap.js +0 -164
  149. package/src/utils/data-structures/heap.test.js +0 -103
  150. package/src/utils/data-structures/object.js +0 -102
  151. package/src/utils/data-structures/object.test.js +0 -121
  152. package/src/utils/data-structures/objects.js +0 -48
  153. package/src/utils/data-structures/objects.test.js +0 -99
  154. package/src/utils/data-structures/tree.js +0 -36
  155. package/src/utils/data-structures/tree.test.js +0 -33
  156. package/src/utils/data-structures/types.d.ts +0 -4
  157. package/src/utils/functions/functions.js +0 -19
  158. package/src/utils/functions/functions.test.js +0 -23
  159. package/src/utils/math/geometry/circle.js +0 -117
  160. package/src/utils/math/geometry/circle.test.js +0 -97
  161. package/src/utils/math/geometry/hitmask.js +0 -39
  162. package/src/utils/math/geometry/hitmask.test.js +0 -84
  163. package/src/utils/math/geometry/line.js +0 -35
  164. package/src/utils/math/geometry/line.test.js +0 -49
  165. package/src/utils/math/geometry/platform.js +0 -42
  166. package/src/utils/math/geometry/platform.test.js +0 -133
  167. package/src/utils/math/geometry/point.js +0 -71
  168. package/src/utils/math/geometry/point.test.js +0 -81
  169. package/src/utils/math/geometry/rectangle.js +0 -45
  170. package/src/utils/math/geometry/rectangle.test.js +0 -42
  171. package/src/utils/math/geometry/segment.js +0 -80
  172. package/src/utils/math/geometry/segment.test.js +0 -183
  173. package/src/utils/math/geometry/triangle.js +0 -15
  174. package/src/utils/math/geometry/triangle.test.js +0 -11
  175. package/src/utils/math/geometry/types.d.ts +0 -23
  176. package/src/utils/math/linear-algebra/2d.js +0 -28
  177. package/src/utils/math/linear-algebra/2d.test.js +0 -17
  178. package/src/utils/math/linear-algebra/quaternion.js +0 -22
  179. package/src/utils/math/linear-algebra/quaternion.test.js +0 -25
  180. package/src/utils/math/linear-algebra/quaternions.js +0 -20
  181. package/src/utils/math/linear-algebra/quaternions.test.js +0 -29
  182. package/src/utils/math/linear-algebra/types.d.ts +0 -4
  183. package/src/utils/math/linear-algebra/vector.js +0 -302
  184. package/src/utils/math/linear-algebra/vector.test.js +0 -257
  185. package/src/utils/math/linear-algebra/vectors.js +0 -122
  186. package/src/utils/math/linear-algebra/vectors.test.js +0 -65
  187. package/src/utils/math/numbers.js +0 -90
  188. package/src/utils/math/numbers.test.js +0 -137
  189. package/src/utils/math/rng.js +0 -44
  190. package/src/utils/math/rng.test.js +0 -39
  191. package/src/utils/math/statistics.js +0 -43
  192. package/src/utils/math/statistics.test.js +0 -47
  193. package/src/utils/math/trigonometry.js +0 -89
  194. package/src/utils/math/trigonometry.test.js +0 -52
  195. package/src/utils/physics/acceleration.js +0 -63
  196. package/src/utils/physics/friction.js +0 -30
  197. package/src/utils/physics/friction.test.js +0 -44
  198. package/src/utils/physics/gravity.js +0 -71
  199. package/src/utils/physics/gravity.test.js +0 -80
  200. package/src/utils/physics/jump.js +0 -41
  201. package/src/utils/physics/velocity.js +0 -38
@@ -0,0 +1,91 @@
1
+ import { findCollision } from "@inglorious/engine/collision/detection.js"
2
+ import { clampToBounds } from "@inglorious/engine/physics/bounds.js"
3
+ import { zero } from "@inglorious/utils/math/linear-algebra/vector.js"
4
+
5
+ const DEFAULT_PARAMS = {
6
+ name: "mouse",
7
+ }
8
+
9
+ const NO_Y = 0
10
+
11
+ export function mouse() {
12
+ return {
13
+ start(entity) {
14
+ entity.collisions ??= {}
15
+ entity.collisions.bounds ??= { shape: "point" }
16
+ },
17
+
18
+ mouseMove(entity, position, api) {
19
+ const game = api.getEntity("game")
20
+
21
+ entity.position = position
22
+
23
+ clampToBounds(entity, game.bounds)
24
+ },
25
+
26
+ mouseClick(entity, position, api) {
27
+ const entities = api.getEntities()
28
+ const clickedEntity = findCollision(entity, entities)
29
+ if (clickedEntity) {
30
+ api.notify("entityClick", clickedEntity.id)
31
+ } else {
32
+ api.notify("sceneClick", position)
33
+ }
34
+ },
35
+ }
36
+ }
37
+
38
+ export function track(parent, options) {
39
+ const handleMouseMove = createHandler("mouseMove", parent, options)
40
+ const handleClick = createHandler("mouseClick", parent, options)
41
+ const handleWheel = createHandler("mouseWheel", parent, options)
42
+
43
+ return {
44
+ onMouseMove: handleMouseMove,
45
+ onClick: handleClick,
46
+ onWheel: handleWheel,
47
+ }
48
+ }
49
+
50
+ export function createMouse(name = DEFAULT_PARAMS.name, overrides = {}) {
51
+ return {
52
+ id: name,
53
+ type: "mouse",
54
+ layer: 999, // A high layer value to ensure it's always rendered on top
55
+ position: zero(),
56
+ ...overrides,
57
+ }
58
+ }
59
+
60
+ function createHandler(type, parent, api) {
61
+ return (event) => {
62
+ event.stopPropagation()
63
+
64
+ if (parent == null) {
65
+ return
66
+ }
67
+
68
+ // For wheel events, the payload is different from other mouse events.
69
+ if (type === "mouseWheel") {
70
+ api.notify(type, { deltaY: event.deltaY })
71
+ return
72
+ }
73
+
74
+ // For move and click events, the payload is the calculated position.
75
+ const payload = calculatePosition({
76
+ clientX: event.clientX,
77
+ clientY: event.clientY,
78
+ parent,
79
+ })
80
+ api.notify(type, payload)
81
+ }
82
+ }
83
+
84
+ function calculatePosition({ clientX, clientY, parent }) {
85
+ const bounds = parent.getBoundingClientRect()
86
+
87
+ const x = clientX - bounds.left
88
+ const z = bounds.bottom - clientY
89
+
90
+ return [x, NO_Y, z]
91
+ }
@@ -0,0 +1,25 @@
1
+ import { defaults, extend } from "@inglorious/utils/data-structures/objects.js"
2
+ import { jump } from "@inglorious/utils/physics/jump.js"
3
+
4
+ const DEFAULT_PARAMS = {
5
+ bounciness: 1,
6
+ }
7
+
8
+ export function bouncy(params) {
9
+ params = extend(DEFAULT_PARAMS, params)
10
+
11
+ return (type) =>
12
+ extend(type, {
13
+ start(entity) {
14
+ type.start?.(entity)
15
+ defaults(entity, params)
16
+ },
17
+
18
+ landed(entity, { entityId }) {
19
+ if (entity.id === entityId) {
20
+ entity.vy = jump(entity) * entity.bounciness
21
+ entity.groundObject = undefined
22
+ }
23
+ },
24
+ })
25
+ }
@@ -0,0 +1,36 @@
1
+ import { clampToBounds } from "@inglorious/engine/physics/bounds.js"
2
+ import { extend, merge } from "@inglorious/utils/data-structures/objects.js"
3
+
4
+ const DEFAULT_PARAMS = {
5
+ collisionGroup: "bounds",
6
+ depthAxis: "y",
7
+ }
8
+
9
+ export function clamped(params) {
10
+ params = extend(DEFAULT_PARAMS, params)
11
+
12
+ return (type) =>
13
+ extend(type, {
14
+ start(entity, api) {
15
+ type.start?.(entity, api)
16
+
17
+ entity.collisions ??= {}
18
+ entity.collisions[params.collisionGroup] ??= {}
19
+ entity.collisions[params.collisionGroup].shape ??= "rectangle"
20
+ },
21
+
22
+ update(entity, dt, api) {
23
+ type.update?.(entity, dt, api)
24
+
25
+ const game = api.getEntity("game")
26
+ merge(entity, {
27
+ position: clampToBounds(
28
+ entity,
29
+ game.bounds,
30
+ params.collisionGroup,
31
+ params.depthAxis,
32
+ ),
33
+ })
34
+ },
35
+ })
36
+ }
@@ -4,19 +4,15 @@ const DEFAULT_PARAMS = {
4
4
  onState: "default",
5
5
  }
6
6
 
7
- export function enableCollisions(params) {
7
+ export function collidable(params) {
8
8
  params = extend(DEFAULT_PARAMS, params)
9
9
 
10
10
  return (type) =>
11
11
  extend(type, {
12
12
  states: {
13
13
  [params.onState]: {
14
- "game:update"(instance, event, options) {
15
- type.states?.[params.onState]["game:update"]?.(
16
- instance,
17
- event,
18
- options,
19
- )
14
+ update(entity, dt, api) {
15
+ type.states?.[params.onState].update?.(entity, dt, api)
20
16
  },
21
17
  },
22
18
  },
@@ -0,0 +1,145 @@
1
+ import { findCollision } from "@inglorious/engine/collision/detection.js"
2
+ import { defaults, extend } from "@inglorious/utils/data-structures/objects.js"
3
+ import {
4
+ angle,
5
+ magnitude,
6
+ zero,
7
+ } from "@inglorious/utils/math/linear-algebra/vector.js"
8
+ import { applyGravity } from "@inglorious/utils/physics/gravity.js"
9
+ import { jump } from "@inglorious/utils/physics/jump.js"
10
+
11
+ const DEFAULT_PARAMS = {
12
+ maxSpeed: 250,
13
+ maxJump: 100,
14
+ maxLeap: 100,
15
+ maxJumps: 1,
16
+ }
17
+
18
+ const DOUBLE = 2
19
+ const HALF = 2
20
+ const X = 0
21
+ const Y = 1
22
+ const NO_VELOCITY = 0
23
+ const NO_PENETRAION = 0
24
+
25
+ export function jumpable(params) {
26
+ params = extend(DEFAULT_PARAMS, params)
27
+
28
+ return (type) =>
29
+ extend(type, {
30
+ start(entity, api) {
31
+ type.start?.(entity, api)
32
+ defaults(entity, params)
33
+ entity.jumpsLeft ??= entity.maxJumps
34
+
35
+ entity.velocity ??= zero()
36
+ entity.vy ??= 0
37
+ },
38
+
39
+ jump(entity, { entityId }) {
40
+ if (entityId === entity.id && entity.jumpsLeft) {
41
+ entity.vy = jump(entity)
42
+ entity.groundObject = undefined
43
+ entity.jumpsLeft--
44
+ }
45
+ },
46
+
47
+ update(entity, dt, api) {
48
+ type.update?.(entity, dt, api)
49
+
50
+ const entities = api.getEntities()
51
+
52
+ entity.collisions ??= {}
53
+ entity.collisions.platform ??= {}
54
+ entity.collisions.platform.shape ??= "rectangle"
55
+
56
+ let width, height
57
+ if (entity.collisions.platform.shape === "circle") {
58
+ width = entity.collisions.platform.radius * DOUBLE
59
+ height = entity.collisions.platform.radius * DOUBLE
60
+ } else {
61
+ ;[width, height] = entity.size
62
+ }
63
+ const [prevX, prevY] = [...entity.position]
64
+
65
+ // 1. HORIZONTAL MOVEMENT & RESOLUTION
66
+ entity.position[X] += entity.velocity[X] * dt
67
+ const entityLeft = entity.position[X] - width / HALF
68
+ const entityRight = entity.position[X] + width / HALF
69
+
70
+ const collisionX = findCollision(entity, entities, "platform")
71
+ if (collisionX) {
72
+ const vx = entity.velocity[X]
73
+
74
+ // Check if moving right and crossing the platform's left edge
75
+ const prevRight = prevX + width / HALF
76
+ const platformLeft =
77
+ collisionX.position[X] - collisionX.size[X] / HALF
78
+ if (vx > NO_VELOCITY && prevRight <= platformLeft) {
79
+ const penetration = entityRight - platformLeft
80
+ if (penetration > NO_PENETRAION) entity.position[X] -= penetration
81
+ entity.velocity[X] = 0
82
+ }
83
+
84
+ // Check if moving left and crossing the platform's right edge
85
+ const prevLeft = prevX - width / HALF
86
+ const platformRight =
87
+ collisionX.position[X] + collisionX.size[X] / HALF
88
+ if (vx < NO_VELOCITY && prevLeft >= platformRight) {
89
+ const penetration = platformRight - entityLeft
90
+ if (penetration > NO_PENETRAION) entity.position[X] += penetration
91
+ entity.velocity[X] = 0
92
+ }
93
+ }
94
+
95
+ // 2. VERTICAL MOVEMENT & RESOLUTION
96
+ const wasOnGround = entity.groundObject
97
+ entity.groundObject = undefined
98
+
99
+ const { vy, position: nextGravityPosition } = applyGravity(entity, dt)
100
+ entity.vy = vy
101
+ entity.position[Y] = nextGravityPosition[Y]
102
+
103
+ const collisionY = findCollision(entity, entities, "platform")
104
+ if (collisionY) {
105
+ const prevBottom = prevY - height / HALF
106
+ const platformTop = collisionY.position[Y] + collisionY.size[Y] / HALF
107
+
108
+ // Landing on top of a platform (one-way platform logic)
109
+ if (entity.vy <= NO_VELOCITY && prevBottom >= platformTop) {
110
+ const entityBottom = entity.position[Y] - height / HALF
111
+ const penetration = platformTop - entityBottom
112
+ if (penetration > NO_PENETRAION) entity.position[Y] += penetration
113
+ entity.vy = 0
114
+ entity.groundObject = collisionY
115
+ entity.jumpsLeft = entity.maxJumps
116
+
117
+ // Only notify on the frame we actually land, not every frame we're on the ground.
118
+ if (!wasOnGround) {
119
+ api.notify("landed", {
120
+ entityId: entity.id,
121
+ targetId: collisionY.id,
122
+ })
123
+ }
124
+ }
125
+
126
+ // Hitting head on bottom of a platform
127
+ // else if (entity.vy > 0) {
128
+ // const prevTop = prevY + height / HALF
129
+ // const platformBottom =
130
+ // collisionY.position[Y] - collisionY.size[Y] / HALF
131
+ // if (prevTop <= platformBottom) {
132
+ // const entityTop = entity.position[Y] + height / HALF
133
+ // const penetration = entityTop - platformBottom
134
+ // if (penetration > 0) entity.position[Y] -= penetration
135
+ // entity.vy = 0
136
+ // }
137
+ // }
138
+ }
139
+
140
+ entity.orientation = magnitude(entity.velocity)
141
+ ? angle(entity.velocity)
142
+ : entity.orientation
143
+ },
144
+ })
145
+ }
@@ -0,0 +1,17 @@
1
+ export function button() {
2
+ return {
3
+ states: {
4
+ default: {
5
+ entityClick(entity) {
6
+ entity.state = "pressed"
7
+ },
8
+ },
9
+
10
+ pressed: {
11
+ entityRelease(entity) {
12
+ entity.state = "default"
13
+ },
14
+ },
15
+ },
16
+ }
17
+ }
@@ -0,0 +1,110 @@
1
+ import { filter } from "@inglorious/utils/data-structures/object.js"
2
+ import * as circle from "@inglorious/utils/math/geometry/circle.js"
3
+ import * as hitmask from "@inglorious/utils/math/geometry/hitmask.js"
4
+ import * as line from "@inglorious/utils/math/geometry/line.js"
5
+ import * as point from "@inglorious/utils/math/geometry/point.js"
6
+ import * as rectangle from "@inglorious/utils/math/geometry/rectangle.js"
7
+ import * as segment from "@inglorious/utils/math/geometry/segment.js"
8
+ import { zero } from "@inglorious/utils/math/linear-algebra/vector.js"
9
+ import { add } from "@inglorious/utils/math/linear-algebra/vectors.js"
10
+
11
+ const Z = 2 // Z-axis index.
12
+
13
+ const Shape = {
14
+ circle,
15
+ line,
16
+ point,
17
+ rectangle,
18
+ segment,
19
+ hitmask,
20
+ }
21
+
22
+ /**
23
+ * Finds the first collision between a point and a list of entities.
24
+ *
25
+ * @param {Point} entity - The point to check for collisions.
26
+ * @param {Options} options - Options for collision detection.
27
+ * @returns {Entity | undefined} The first entity that collides with the point, or undefined if none are found.
28
+ */
29
+ export function findCollision(entity, entities, collisionGroup = "hitbox") {
30
+ const otherEntities = filter(
31
+ entities,
32
+ (id, { collisions }) => id !== entity.id && collisions?.[collisionGroup],
33
+ )
34
+
35
+ return Object.values(otherEntities)
36
+ .toSorted((a, b) => a.position[Z] - b.position[Z])
37
+ .find((target) => collidesWith(entity, target, collisionGroup))
38
+ }
39
+
40
+ export function collidesWith(entity, target, collisionGroup = "hitbox") {
41
+ const entityShape = getCollisionShape(entity, collisionGroup)
42
+ const targetShape = getCollisionShape(target, collisionGroup)
43
+
44
+ if (!entityShape || !targetShape) {
45
+ return false
46
+ }
47
+
48
+ return shapeCollidesWith(entityShape, targetShape)
49
+ }
50
+
51
+ function shapeCollidesWith(entity, target) {
52
+ const shapeFns = Shape[entity.shape]
53
+
54
+ switch (target.shape) {
55
+ case "circle":
56
+ return shapeFns.intersectsCircle(entity, target)
57
+
58
+ case "line":
59
+ return shapeFns.intersectsLine(entity, target)
60
+
61
+ case "point":
62
+ return shapeFns.intersectsPoint(entity, target)
63
+
64
+ case "rectangle":
65
+ return shapeFns.intersectsRectangle(entity, target)
66
+
67
+ case "segment":
68
+ return shapeFns.intersectsSegment(entity, target)
69
+ }
70
+ }
71
+
72
+ export function findCollisions(entity, target, collisionGroup = "hitbox") {
73
+ const entityShape = getCollisionShape(entity, collisionGroup)
74
+ const targetShape = getCollisionShape(target, collisionGroup)
75
+
76
+ if (!entityShape || !targetShape) {
77
+ return false
78
+ }
79
+
80
+ const shapeFns = Shape[entityShape.shape]
81
+ if (!shapeFns || !shapeFns.findCollisions) {
82
+ return false
83
+ }
84
+
85
+ return shapeFns.findCollisions(entityShape, targetShape)
86
+ }
87
+
88
+ /**
89
+ * Correctly calculates the absolute position and size of an entity's
90
+ * collision shape, including any offsets.
91
+ */
92
+ function getCollisionShape(entity, collisionGroup = "hitbox") {
93
+ const collision = entity.collisions[collisionGroup]
94
+ if (!collision) {
95
+ return null
96
+ }
97
+
98
+ const position = add(
99
+ entity.position,
100
+ collision.offset ?? zero(),
101
+ entity.offset ?? zero(),
102
+ )
103
+
104
+ return {
105
+ ...collision,
106
+ position,
107
+ size: collision.size ?? entity.size,
108
+ radius: collision.radius ?? entity.radius,
109
+ }
110
+ }
@@ -0,0 +1,34 @@
1
+ import { createSelector as _createSelector } from "./select.js"
2
+
3
+ export function createApi(store) {
4
+ const createSelector = (inputSelectors, resultFunc) => {
5
+ const selector = _createSelector(inputSelectors, resultFunc)
6
+ return () => selector(store.getState())
7
+ }
8
+
9
+ const getTypes = () => store.getTypes()
10
+
11
+ const getEntities = () => store.getState().entities
12
+
13
+ const getEntity = (id) => getEntities()[id]
14
+
15
+ const notify = (type, payload) => {
16
+ store.notify(type, payload)
17
+ }
18
+
19
+ const dispatch = (action) => {
20
+ store.dispatch(action)
21
+ }
22
+
23
+ const getType = (id) => store.getOriginalTypes()?.[id]
24
+
25
+ return {
26
+ createSelector,
27
+ getTypes,
28
+ getEntities,
29
+ getEntity,
30
+ getType,
31
+ notify,
32
+ dispatch,
33
+ }
34
+ }
@@ -0,0 +1,135 @@
1
+ export const ACTION_BLACKLIST = [
2
+ "update",
3
+ "gamepadAxis",
4
+ "gamepadPress",
5
+ "gamepadRelease",
6
+ "keyboardKeyDown",
7
+ "keyboardKeyUp",
8
+ "inputAxis",
9
+ "inputPress",
10
+ "inputRelease",
11
+ "mouseMove",
12
+ "mouseClick",
13
+ "spriteAnimationEnd",
14
+ ]
15
+
16
+ const LAST_STATE = 1
17
+
18
+ let devToolsInstance = null
19
+ let unsubscribe = null
20
+
21
+ export function initDevTools(store) {
22
+ // Prevent multiple connections
23
+ if (devToolsInstance) {
24
+ return
25
+ }
26
+
27
+ if (typeof window === "undefined" || !window.__REDUX_DEVTOOLS_EXTENSION__) {
28
+ return
29
+ }
30
+
31
+ devToolsInstance = window.__REDUX_DEVTOOLS_EXTENSION__.connect({
32
+ name: "Inglorious Engine",
33
+ predicate: (state, action) => !ACTION_BLACKLIST.includes(action.type),
34
+ actionCreators: {
35
+ jump: () => ({ type: "jump", payload: { inputId: "input0" } }),
36
+ },
37
+ // @see https://github.com/reduxjs/redux-devtools/blob/main/extension/docs/API/Arguments.md#features
38
+ features: {
39
+ pause: true, // start/pause recording of dispatched actions
40
+ lock: true, // lock/unlock dispatching actions and side effects
41
+ persist: true, // persist states on page reloading
42
+ export: true, // export history of actions in a file
43
+ import: "custom", // import history of actions from a file
44
+ jump: false, // jump back and forth (time travelling)
45
+ skip: false, // skip (cancel) actions
46
+ reorder: false, // drag and drop actions in the history list
47
+ dispatch: true, // dispatch custom actions or action creators
48
+ test: false, // generate tests for the selected actions
49
+ },
50
+ })
51
+
52
+ unsubscribe = devToolsInstance.subscribe((message) => {
53
+ switch (message.type) {
54
+ case "DISPATCH":
55
+ handleDispatch(message, store)
56
+ break
57
+
58
+ case "ACTION":
59
+ handleAction(message, store)
60
+ break
61
+ }
62
+ })
63
+
64
+ devToolsInstance.init(store.getState())
65
+ }
66
+
67
+ export function disconnectDevTools() {
68
+ // The `disconnect` method on the devToolsInstance is not available in all
69
+ // environments or versions of the extension.
70
+ // The safest way to "disconnect" is to unsubscribe from any listeners
71
+ // and release our reference to the instance, which prevents any further
72
+ // actions from being sent.
73
+ if (unsubscribe) {
74
+ unsubscribe()
75
+ unsubscribe = null
76
+ devToolsInstance = null
77
+ }
78
+ }
79
+
80
+ export function sendAction(action, state) {
81
+ if (devToolsInstance) {
82
+ devToolsInstance.send(action, state)
83
+ }
84
+ }
85
+
86
+ function handleDispatch(message, store) {
87
+ switch (message.payload.type) {
88
+ // reset button
89
+ case "RESET": {
90
+ store.reset()
91
+ devToolsInstance.init(store.getState())
92
+ break
93
+ }
94
+
95
+ // revert button
96
+ case "ROLLBACK": {
97
+ const newState = JSON.parse(message.state)
98
+ store.setState(newState)
99
+ break
100
+ }
101
+
102
+ // commit button
103
+ case "COMMIT": {
104
+ devToolsInstance.init(store.getState())
105
+ break
106
+ }
107
+
108
+ // import from file button
109
+ case "IMPORT_STATE": {
110
+ const { computedStates, actionsById } = message.payload.nextLiftedState
111
+
112
+ const [firstComputedState] = computedStates
113
+ const lastComputedState =
114
+ computedStates[computedStates.length - LAST_STATE]
115
+ if (lastComputedState) {
116
+ store.setState(lastComputedState.state)
117
+ }
118
+
119
+ const flattenedActions = Object.values(actionsById)
120
+ .flatMap(({ action }) => action.payload ?? action)
121
+ .map((action, index) => [index, action])
122
+
123
+ devToolsInstance.init(
124
+ firstComputedState.state,
125
+ Object.fromEntries(flattenedActions),
126
+ )
127
+ break
128
+ }
129
+ }
130
+ }
131
+
132
+ function handleAction(message, store) {
133
+ const action = JSON.parse(message.payload)
134
+ store.dispatch(action)
135
+ }