@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.
- package/LICENSE +9 -0
- package/README.md +72 -0
- package/package.json +76 -0
- package/src/docs/ai/movement/dynamic/align.js +131 -0
- package/src/docs/ai/movement/dynamic/arrive.js +88 -0
- package/src/docs/ai/movement/dynamic/dynamic.mdx +99 -0
- package/src/docs/ai/movement/dynamic/dynamic.stories.js +58 -0
- package/src/docs/ai/movement/dynamic/evade.js +72 -0
- package/src/docs/ai/movement/dynamic/face.js +90 -0
- package/src/docs/ai/movement/dynamic/flee.js +38 -0
- package/src/docs/ai/movement/dynamic/look-where-youre-going.js +114 -0
- package/src/docs/ai/movement/dynamic/match-velocity.js +92 -0
- package/src/docs/ai/movement/dynamic/pursue.js +72 -0
- package/src/docs/ai/movement/dynamic/seek.js +37 -0
- package/src/docs/ai/movement/dynamic/wander.js +71 -0
- package/src/docs/ai/movement/kinematic/align.js +122 -0
- package/src/docs/ai/movement/kinematic/arrive.js +78 -0
- package/src/docs/ai/movement/kinematic/face.js +82 -0
- package/src/docs/ai/movement/kinematic/flee.js +36 -0
- package/src/docs/ai/movement/kinematic/kinematic.mdx +67 -0
- package/src/docs/ai/movement/kinematic/kinematic.stories.js +42 -0
- package/src/docs/ai/movement/kinematic/seek.js +34 -0
- package/src/docs/ai/movement/kinematic/wander-as-seek.js +62 -0
- package/src/docs/ai/movement/kinematic/wander.js +28 -0
- package/src/docs/bounds.js +7 -0
- package/src/docs/code-reuse.js +35 -0
- package/src/docs/collision/circles.js +58 -0
- package/src/docs/collision/collision.mdx +27 -0
- package/src/docs/collision/collision.stories.js +22 -0
- package/src/docs/collision/platform.js +76 -0
- package/src/docs/collision/tilemap.js +181 -0
- package/src/docs/empty.js +1 -0
- package/src/docs/engine.mdx +81 -0
- package/src/docs/engine.stories.js +37 -0
- package/src/docs/event-handlers.js +68 -0
- package/src/docs/framerate.js +37 -0
- package/src/docs/game.jsx +15 -0
- package/src/docs/image/image.js +19 -0
- package/src/docs/image/image.stories.js +22 -0
- package/src/docs/image/sprite.js +39 -0
- package/src/docs/image/tilemap.js +84 -0
- package/src/docs/input/controls.js +67 -0
- package/src/docs/input/gamepad.js +67 -0
- package/src/docs/input/input.mdx +55 -0
- package/src/docs/input/input.stories.js +27 -0
- package/src/docs/input/keyboard.js +58 -0
- package/src/docs/input/mouse.js +32 -0
- package/src/docs/instances.js +49 -0
- package/src/docs/player/dynamic/double-jump.js +90 -0
- package/src/docs/player/dynamic/dynamic.stories.js +32 -0
- package/src/docs/player/dynamic/jump.js +83 -0
- package/src/docs/player/dynamic/modern-controls.js +57 -0
- package/src/docs/player/dynamic/shooter-controls.js +51 -0
- package/src/docs/player/dynamic/tank-controls.js +44 -0
- package/src/docs/player/kinematic/double-jump.js +90 -0
- package/src/docs/player/kinematic/jump.js +82 -0
- package/src/docs/player/kinematic/kinematic.stories.js +32 -0
- package/src/docs/player/kinematic/modern-controls.js +56 -0
- package/src/docs/player/kinematic/shooter-controls.js +48 -0
- package/src/docs/player/kinematic/tank-controls.js +42 -0
- package/src/docs/quick-start/first-game.js +49 -0
- package/src/docs/quick-start/hello-world.js +1 -0
- package/src/docs/quick-start.mdx +127 -0
- package/src/docs/quick-start.stories.js +17 -0
- package/src/docs/recipes/add-and-remove.js +71 -0
- package/src/docs/recipes/add-instance.js +42 -0
- package/src/docs/recipes/decision-tree.js +169 -0
- package/src/docs/recipes/random-instances.js +25 -0
- package/src/docs/recipes/recipes.mdx +81 -0
- package/src/docs/recipes/recipes.stories.js +37 -0
- package/src/docs/recipes/remove-instance.js +52 -0
- package/src/docs/recipes/states.js +64 -0
- package/src/docs/ui/button.js +28 -0
- package/src/docs/ui/form.stories.js +55 -0
- package/src/docs/ui-chooser.jsx +6 -0
- package/src/docs/utils/data-structures/object.mdx +47 -0
- package/src/docs/utils/data-structures/objects.mdx +30 -0
- package/src/docs/utils/functions/functions.mdx +34 -0
- package/src/docs/utils/math/geometry/circle.mdx +55 -0
- package/src/docs/utils/math/geometry/point.mdx +38 -0
- package/src/docs/utils/math/geometry/rectangle.mdx +24 -0
- package/src/docs/utils/math/geometry/segment.mdx +55 -0
- package/src/docs/utils/math/geometry/triangle.mdx +22 -0
- package/src/docs/utils/math/linear-algebra/2d.mdx +22 -0
- package/src/docs/utils/math/linear-algebra/quaternion.mdx +21 -0
- package/src/docs/utils/math/linear-algebra/quaternions.mdx +22 -0
- package/src/docs/utils/math/linear-algebra/vector.mdx +177 -0
- package/src/docs/utils/math/linear-algebra/vectors.mdx +58 -0
- package/src/docs/utils/math/numbers.mdx +76 -0
- package/src/docs/utils/math/random.mdx +35 -0
- package/src/docs/utils/math/statistics.mdx +38 -0
- package/src/docs/utils/math/trigonometry.mdx +85 -0
- package/src/docs/utils/physics/friction.mdx +20 -0
- package/src/docs/utils/physics/gravity.mdx +28 -0
- package/src/engine/ai/movement/dynamic/align.js +63 -0
- package/src/engine/ai/movement/dynamic/arrive.js +43 -0
- package/src/engine/ai/movement/dynamic/evade.js +38 -0
- package/src/engine/ai/movement/dynamic/face.js +20 -0
- package/src/engine/ai/movement/dynamic/flee.js +45 -0
- package/src/engine/ai/movement/dynamic/look-where-youre-going.js +17 -0
- package/src/engine/ai/movement/dynamic/match-velocity.js +50 -0
- package/src/engine/ai/movement/dynamic/pursue.js +38 -0
- package/src/engine/ai/movement/dynamic/seek.js +44 -0
- package/src/engine/ai/movement/dynamic/wander.js +32 -0
- package/src/engine/ai/movement/kinematic/align.js +37 -0
- package/src/engine/ai/movement/kinematic/arrive.js +42 -0
- package/src/engine/ai/movement/kinematic/face.js +20 -0
- package/src/engine/ai/movement/kinematic/flee.js +26 -0
- package/src/engine/ai/movement/kinematic/seek.js +26 -0
- package/src/engine/ai/movement/kinematic/seek.test.js +42 -0
- package/src/engine/ai/movement/kinematic/wander-as-seek.js +31 -0
- package/src/engine/ai/movement/kinematic/wander.js +27 -0
- package/src/engine/collision/detection.js +115 -0
- package/src/engine/loop/animation-frame.js +26 -0
- package/src/engine/loop/elapsed.js +23 -0
- package/src/engine/loop/fixed.js +28 -0
- package/src/engine/loop/flash.js +14 -0
- package/src/engine/loop/lag.js +27 -0
- package/src/engine/loop.js +15 -0
- package/src/engine/movement/dynamic/modern.js +24 -0
- package/src/engine/movement/dynamic/tank.js +43 -0
- package/src/engine/movement/kinematic/modern.js +16 -0
- package/src/engine/movement/kinematic/modern.test.js +27 -0
- package/src/engine/movement/kinematic/tank.js +27 -0
- package/src/engine/store.js +174 -0
- package/src/engine/store.test.js +256 -0
- package/src/engine.js +74 -0
- package/src/game/animation.js +26 -0
- package/src/game/bounds.js +66 -0
- package/src/game/decorators/character.js +5 -0
- package/src/game/decorators/clamp-to-bounds.js +15 -0
- package/src/game/decorators/collisions.js +24 -0
- package/src/game/decorators/controls/dynamic/modern.js +48 -0
- package/src/game/decorators/controls/dynamic/shooter.js +47 -0
- package/src/game/decorators/controls/dynamic/tank.js +55 -0
- package/src/game/decorators/controls/kinematic/modern.js +49 -0
- package/src/game/decorators/controls/kinematic/shooter.js +45 -0
- package/src/game/decorators/controls/kinematic/tank.js +52 -0
- package/src/game/decorators/debug/collisions.js +32 -0
- package/src/game/decorators/double-jump.js +70 -0
- package/src/game/decorators/fps.js +30 -0
- package/src/game/decorators/fsm.js +27 -0
- package/src/game/decorators/fsm.test.js +56 -0
- package/src/game/decorators/game.js +11 -0
- package/src/game/decorators/image/image.js +5 -0
- package/src/game/decorators/image/sprite.js +5 -0
- package/src/game/decorators/image/tilemap.js +5 -0
- package/src/game/decorators/input/controls.js +27 -0
- package/src/game/decorators/input/gamepad.js +74 -0
- package/src/game/decorators/input/input.js +41 -0
- package/src/game/decorators/input/keyboard.js +49 -0
- package/src/game/decorators/input/mouse.js +65 -0
- package/src/game/decorators/jump.js +72 -0
- package/src/game/decorators/platform.js +5 -0
- package/src/game/decorators/ui/button.js +21 -0
- package/src/game/sprite.js +119 -0
- package/src/main.js +5 -0
- package/src/ui/canvas/absolute-position.js +17 -0
- package/src/ui/canvas/character.js +35 -0
- package/src/ui/canvas/form/button.js +25 -0
- package/src/ui/canvas/fps.js +18 -0
- package/src/ui/canvas/image/hitmask.js +37 -0
- package/src/ui/canvas/image/image.js +37 -0
- package/src/ui/canvas/image/sprite.js +49 -0
- package/src/ui/canvas/image/tilemap.js +64 -0
- package/src/ui/canvas/mouse.js +37 -0
- package/src/ui/canvas/shapes/circle.js +31 -0
- package/src/ui/canvas/shapes/rectangle.js +31 -0
- package/src/ui/canvas.js +81 -0
- package/src/ui/react/game/character/character.module.scss +17 -0
- package/src/ui/react/game/character/index.jsx +30 -0
- package/src/ui/react/game/cursor/cursor.module.scss +47 -0
- package/src/ui/react/game/cursor/index.jsx +20 -0
- package/src/ui/react/game/form/fields/field/field.module.scss +5 -0
- package/src/ui/react/game/form/fields/field/index.jsx +56 -0
- package/src/ui/react/game/form/fields/fields.module.scss +48 -0
- package/src/ui/react/game/form/fields/index.jsx +12 -0
- package/src/ui/react/game/form/form.module.scss +18 -0
- package/src/ui/react/game/form/index.jsx +22 -0
- package/src/ui/react/game/fps/index.jsx +16 -0
- package/src/ui/react/game/game.jsx +71 -0
- package/src/ui/react/game/index.jsx +29 -0
- package/src/ui/react/game/platform/index.jsx +30 -0
- package/src/ui/react/game/platform/platform.module.scss +7 -0
- package/src/ui/react/game/scene/index.jsx +25 -0
- package/src/ui/react/game/scene/scene.module.scss +9 -0
- package/src/ui/react/game/sprite/index.jsx +58 -0
- package/src/ui/react/game/sprite/sprite.module.css +3 -0
- package/src/ui/react/game/stats/index.jsx +22 -0
- package/src/ui/react/hocs/with-absolute-position/index.jsx +20 -0
- package/src/ui/react/hocs/with-absolute-position/with-absolute-position.module.scss +5 -0
- package/src/ui/react/index.jsx +9 -0
- package/src/utils/algorithms/decision-tree.js +24 -0
- package/src/utils/algorithms/decision-tree.test.js +102 -0
- package/src/utils/algorithms/path-finding.js +155 -0
- package/src/utils/algorithms/path-finding.test.js +151 -0
- package/src/utils/algorithms/types.d.ts +28 -0
- package/src/utils/data-structures/array.js +83 -0
- package/src/utils/data-structures/array.test.js +173 -0
- package/src/utils/data-structures/board.js +159 -0
- package/src/utils/data-structures/board.test.js +242 -0
- package/src/utils/data-structures/boolean.js +9 -0
- package/src/utils/data-structures/heap.js +164 -0
- package/src/utils/data-structures/heap.test.js +103 -0
- package/src/utils/data-structures/object.js +102 -0
- package/src/utils/data-structures/object.test.js +121 -0
- package/src/utils/data-structures/objects.js +48 -0
- package/src/utils/data-structures/objects.test.js +99 -0
- package/src/utils/data-structures/tree.js +36 -0
- package/src/utils/data-structures/tree.test.js +33 -0
- package/src/utils/data-structures/types.d.ts +4 -0
- package/src/utils/functions/functions.js +19 -0
- package/src/utils/functions/functions.test.js +23 -0
- package/src/utils/math/geometry/circle.js +117 -0
- package/src/utils/math/geometry/circle.test.js +97 -0
- package/src/utils/math/geometry/hitmask.js +39 -0
- package/src/utils/math/geometry/hitmask.test.js +84 -0
- package/src/utils/math/geometry/line.js +35 -0
- package/src/utils/math/geometry/line.test.js +49 -0
- package/src/utils/math/geometry/platform.js +42 -0
- package/src/utils/math/geometry/platform.test.js +133 -0
- package/src/utils/math/geometry/point.js +71 -0
- package/src/utils/math/geometry/point.test.js +81 -0
- package/src/utils/math/geometry/rectangle.js +45 -0
- package/src/utils/math/geometry/rectangle.test.js +42 -0
- package/src/utils/math/geometry/segment.js +80 -0
- package/src/utils/math/geometry/segment.test.js +183 -0
- package/src/utils/math/geometry/triangle.js +15 -0
- package/src/utils/math/geometry/triangle.test.js +11 -0
- package/src/utils/math/geometry/types.d.ts +23 -0
- package/src/utils/math/linear-algebra/2d.js +28 -0
- package/src/utils/math/linear-algebra/2d.test.js +17 -0
- package/src/utils/math/linear-algebra/quaternion.js +22 -0
- package/src/utils/math/linear-algebra/quaternion.test.js +25 -0
- package/src/utils/math/linear-algebra/quaternions.js +20 -0
- package/src/utils/math/linear-algebra/quaternions.test.js +29 -0
- package/src/utils/math/linear-algebra/types.d.ts +4 -0
- package/src/utils/math/linear-algebra/vector.js +302 -0
- package/src/utils/math/linear-algebra/vector.test.js +257 -0
- package/src/utils/math/linear-algebra/vectors.js +122 -0
- package/src/utils/math/linear-algebra/vectors.test.js +65 -0
- package/src/utils/math/numbers.js +90 -0
- package/src/utils/math/numbers.test.js +137 -0
- package/src/utils/math/rng.js +44 -0
- package/src/utils/math/rng.test.js +39 -0
- package/src/utils/math/statistics.js +43 -0
- package/src/utils/math/statistics.test.js +47 -0
- package/src/utils/math/trigonometry.js +89 -0
- package/src/utils/math/trigonometry.test.js +52 -0
- package/src/utils/physics/acceleration.js +63 -0
- package/src/utils/physics/friction.js +30 -0
- package/src/utils/physics/friction.test.js +44 -0
- package/src/utils/physics/gravity.js +71 -0
- package/src/utils/physics/gravity.test.js +80 -0
- package/src/utils/physics/jump.js +41 -0
- package/src/utils/physics/velocity.js +38 -0
|
@@ -0,0 +1,27 @@
|
|
|
1
|
+
const ONE_SECOND = 1000
|
|
2
|
+
|
|
3
|
+
export default class LagLoop {
|
|
4
|
+
_shouldStop = false
|
|
5
|
+
|
|
6
|
+
start(engine, msPerUpdate) {
|
|
7
|
+
let previousTime = Date.now()
|
|
8
|
+
let lag = 0
|
|
9
|
+
|
|
10
|
+
while (!this._shouldStop) {
|
|
11
|
+
const currentTime = Date.now()
|
|
12
|
+
const dt = currentTime - previousTime
|
|
13
|
+
previousTime = currentTime
|
|
14
|
+
lag += dt
|
|
15
|
+
|
|
16
|
+
while (lag >= msPerUpdate) {
|
|
17
|
+
engine.update(dt / ONE_SECOND)
|
|
18
|
+
engine.render(dt / ONE_SECOND)
|
|
19
|
+
lag -= msPerUpdate
|
|
20
|
+
}
|
|
21
|
+
}
|
|
22
|
+
}
|
|
23
|
+
|
|
24
|
+
stop() {
|
|
25
|
+
this._shouldStop = true
|
|
26
|
+
}
|
|
27
|
+
}
|
|
@@ -0,0 +1,15 @@
|
|
|
1
|
+
import AnimationFrameLoop from "./loop/animation-frame.js"
|
|
2
|
+
import ElapsedLoop from "./loop/elapsed.js"
|
|
3
|
+
import FixedLoop from "./loop/fixed.js"
|
|
4
|
+
import FlashLoop from "./loop/flash.js"
|
|
5
|
+
import LagLoop from "./loop/lag.js"
|
|
6
|
+
|
|
7
|
+
// @see https://gameprogrammingpatterns.com/game-loop.html
|
|
8
|
+
|
|
9
|
+
export default {
|
|
10
|
+
flash: FlashLoop,
|
|
11
|
+
fixed: FixedLoop,
|
|
12
|
+
elapsed: ElapsedLoop,
|
|
13
|
+
lag: LagLoop,
|
|
14
|
+
animationFrame: AnimationFrameLoop,
|
|
15
|
+
}
|
|
@@ -0,0 +1,24 @@
|
|
|
1
|
+
import {
|
|
2
|
+
angle,
|
|
3
|
+
magnitude,
|
|
4
|
+
} from "@inglorious/utils/math/linear-algebra/vector.js"
|
|
5
|
+
import { applyAcceleration } from "@inglorious/utils/physics/acceleration.js"
|
|
6
|
+
|
|
7
|
+
const DEFAULT_ORIENTATION = 0
|
|
8
|
+
|
|
9
|
+
const ORIENTATION_CHANGE_THRESHOLD = 4
|
|
10
|
+
|
|
11
|
+
export default function modernMove(instance, options) {
|
|
12
|
+
const { acceleration, velocity, position } = applyAcceleration(
|
|
13
|
+
instance,
|
|
14
|
+
options,
|
|
15
|
+
)
|
|
16
|
+
|
|
17
|
+
let orientation = instance.orientation ?? DEFAULT_ORIENTATION
|
|
18
|
+
orientation =
|
|
19
|
+
magnitude(velocity) > ORIENTATION_CHANGE_THRESHOLD
|
|
20
|
+
? angle(velocity)
|
|
21
|
+
: orientation
|
|
22
|
+
|
|
23
|
+
return { acceleration, velocity, position, orientation }
|
|
24
|
+
}
|
|
@@ -0,0 +1,43 @@
|
|
|
1
|
+
import {
|
|
2
|
+
clamp,
|
|
3
|
+
multiply,
|
|
4
|
+
rotate,
|
|
5
|
+
zero,
|
|
6
|
+
} from "@inglorious/utils/math/linear-algebra/vector.js"
|
|
7
|
+
import { sum } from "@inglorious/utils/math/linear-algebra/vectors.js"
|
|
8
|
+
import { toRange } from "@inglorious/utils/math/trigonometry.js"
|
|
9
|
+
import { applyFriction } from "@inglorious/utils/physics/friction.js"
|
|
10
|
+
|
|
11
|
+
const DEFAULT_MAX_ACCELERATION = 0
|
|
12
|
+
const DEFAULT_MAX_SPEED = 0
|
|
13
|
+
const DEFAULT_FRICTION = 0
|
|
14
|
+
|
|
15
|
+
const DEFAULT_ORIENTATION = 0
|
|
16
|
+
|
|
17
|
+
const HALF_ACCELERATION = 0.5
|
|
18
|
+
|
|
19
|
+
export default function tankMove(instance, { dt }) {
|
|
20
|
+
const maxAcceleration = instance.maxAcceleration ?? DEFAULT_MAX_ACCELERATION
|
|
21
|
+
const maxSpeed = instance.maxSpeed ?? DEFAULT_MAX_SPEED
|
|
22
|
+
const friction = instance.friction ?? DEFAULT_FRICTION
|
|
23
|
+
|
|
24
|
+
let orientation = instance.orientation ?? DEFAULT_ORIENTATION
|
|
25
|
+
orientation = toRange(orientation)
|
|
26
|
+
|
|
27
|
+
let acceleration = instance.acceleration ?? zero()
|
|
28
|
+
acceleration = rotate(acceleration, orientation)
|
|
29
|
+
acceleration = clamp(acceleration, -maxAcceleration, maxAcceleration)
|
|
30
|
+
|
|
31
|
+
let velocity = instance.velocity ?? zero()
|
|
32
|
+
velocity = sum(velocity, multiply(acceleration, dt))
|
|
33
|
+
velocity = clamp(velocity, -maxSpeed, maxSpeed)
|
|
34
|
+
velocity = applyFriction({ velocity, friction }, { dt })
|
|
35
|
+
|
|
36
|
+
const position = sum(
|
|
37
|
+
instance.position,
|
|
38
|
+
multiply(velocity, dt),
|
|
39
|
+
multiply(acceleration, HALF_ACCELERATION * dt * dt),
|
|
40
|
+
)
|
|
41
|
+
|
|
42
|
+
return { velocity, position, orientation }
|
|
43
|
+
}
|
|
@@ -0,0 +1,16 @@
|
|
|
1
|
+
import {
|
|
2
|
+
angle,
|
|
3
|
+
magnitude,
|
|
4
|
+
} from "@inglorious/utils/math/linear-algebra/vector.js"
|
|
5
|
+
import { applyVelocity } from "@inglorious/utils/physics/velocity.js"
|
|
6
|
+
|
|
7
|
+
const DEFAULT_ORIENTATION = 0
|
|
8
|
+
|
|
9
|
+
export default function modernMove(instance, options) {
|
|
10
|
+
const { velocity, position } = applyVelocity(instance, options)
|
|
11
|
+
|
|
12
|
+
let orientation = instance.orientation ?? DEFAULT_ORIENTATION
|
|
13
|
+
orientation = magnitude(velocity) ? angle(velocity) : orientation
|
|
14
|
+
|
|
15
|
+
return { velocity, position, orientation }
|
|
16
|
+
}
|
|
@@ -0,0 +1,27 @@
|
|
|
1
|
+
import { expect, test } from "vitest"
|
|
2
|
+
|
|
3
|
+
import modernMove from "./modern.js"
|
|
4
|
+
|
|
5
|
+
test("it should move following its velocity", () => {
|
|
6
|
+
const instance = { maxSpeed: 1, velocity: [1, 0, 0], position: [0, 0, 0] }
|
|
7
|
+
const options = { dt: 1 }
|
|
8
|
+
const expectedResult = {
|
|
9
|
+
velocity: [1, 0, 0],
|
|
10
|
+
position: [1, 0, 0],
|
|
11
|
+
orientation: 0,
|
|
12
|
+
}
|
|
13
|
+
|
|
14
|
+
expect(modernMove(instance, options)).toStrictEqual(expectedResult)
|
|
15
|
+
})
|
|
16
|
+
|
|
17
|
+
test("it should limit the velocity to the max speed", () => {
|
|
18
|
+
const instance = { maxSpeed: 1, velocity: [10, 0, 0], position: [0, 0, 0] }
|
|
19
|
+
const options = { dt: 1 }
|
|
20
|
+
const expectedResult = {
|
|
21
|
+
velocity: [1, 0, 0],
|
|
22
|
+
position: [1, 0, 0],
|
|
23
|
+
orientation: 0,
|
|
24
|
+
}
|
|
25
|
+
|
|
26
|
+
expect(modernMove(instance, options)).toStrictEqual(expectedResult)
|
|
27
|
+
})
|
|
@@ -0,0 +1,27 @@
|
|
|
1
|
+
import {
|
|
2
|
+
clamp,
|
|
3
|
+
multiply,
|
|
4
|
+
rotate,
|
|
5
|
+
zero,
|
|
6
|
+
} from "@inglorious/utils/math/linear-algebra/vector.js"
|
|
7
|
+
import { sum } from "@inglorious/utils/math/linear-algebra/vectors.js"
|
|
8
|
+
import { toRange } from "@inglorious/utils/math/trigonometry.js"
|
|
9
|
+
|
|
10
|
+
const DEFAULT_MAX_SPEED = 0
|
|
11
|
+
|
|
12
|
+
const DEFAULT_ORIENTATION = 0
|
|
13
|
+
|
|
14
|
+
export default function tankMove(instance, { dt }) {
|
|
15
|
+
const maxSpeed = instance.maxSpeed ?? DEFAULT_MAX_SPEED
|
|
16
|
+
|
|
17
|
+
let orientation = instance.orientation ?? DEFAULT_ORIENTATION
|
|
18
|
+
orientation = toRange(orientation)
|
|
19
|
+
|
|
20
|
+
let velocity = instance.velocity ?? zero()
|
|
21
|
+
velocity = rotate(velocity, orientation)
|
|
22
|
+
velocity = clamp(velocity, -maxSpeed, maxSpeed)
|
|
23
|
+
|
|
24
|
+
const position = sum(instance.position, multiply(velocity, dt))
|
|
25
|
+
|
|
26
|
+
return { velocity, position, orientation }
|
|
27
|
+
}
|
|
@@ -0,0 +1,174 @@
|
|
|
1
|
+
import { enableGame } from "@inglorious/game/decorators/game.js"
|
|
2
|
+
import { map } from "@inglorious/utils/data-structures/object.js"
|
|
3
|
+
import { extend } from "@inglorious/utils/data-structures/objects.js"
|
|
4
|
+
import { pipe } from "@inglorious/utils/functions/functions.js"
|
|
5
|
+
import { produce } from "immer"
|
|
6
|
+
|
|
7
|
+
const DEFAULT_TYPES = {
|
|
8
|
+
game: [enableGame()],
|
|
9
|
+
}
|
|
10
|
+
|
|
11
|
+
const DEFAULT_INSTANCES = {
|
|
12
|
+
// eslint-disable-next-line no-magic-numbers
|
|
13
|
+
game: { type: "game", bounds: [0, 0, 800, 600] }, // Default game instance with bounds.
|
|
14
|
+
}
|
|
15
|
+
|
|
16
|
+
const DEFAULT_LAYER = 0
|
|
17
|
+
|
|
18
|
+
/**
|
|
19
|
+
* Creates a store to manage state and events.
|
|
20
|
+
* @param {Object} options - Configuration options for the store.
|
|
21
|
+
* @param {Object} options.instances - Initial instances to include in the store.
|
|
22
|
+
* @param {Object} options.originalConfig - Additional configuration for the store.
|
|
23
|
+
* @returns {Object} The store with methods to interact with state and events.
|
|
24
|
+
*/
|
|
25
|
+
export function createStore({
|
|
26
|
+
types: originalTypes,
|
|
27
|
+
instances: originalInstances,
|
|
28
|
+
}) {
|
|
29
|
+
const listeners = new Set()
|
|
30
|
+
let incomingEvents = []
|
|
31
|
+
|
|
32
|
+
let types = extend(DEFAULT_TYPES, originalTypes)
|
|
33
|
+
types = augmentTypes(types)
|
|
34
|
+
|
|
35
|
+
let instances = extend(DEFAULT_INSTANCES, originalInstances)
|
|
36
|
+
instances = augmentInstances(instances)
|
|
37
|
+
|
|
38
|
+
let state = { events: [], instances }
|
|
39
|
+
|
|
40
|
+
return {
|
|
41
|
+
subscribe,
|
|
42
|
+
update,
|
|
43
|
+
notify,
|
|
44
|
+
dispatch: notify, // needed for react-redux
|
|
45
|
+
getTypes,
|
|
46
|
+
getState,
|
|
47
|
+
}
|
|
48
|
+
|
|
49
|
+
/**
|
|
50
|
+
* Subscribes a listener to state updates.
|
|
51
|
+
* @param {Function} listener - The listener function to call on updates.
|
|
52
|
+
* @returns {Function} A function to unsubscribe the listener.
|
|
53
|
+
*/
|
|
54
|
+
function subscribe(listener) {
|
|
55
|
+
listeners.add(listener)
|
|
56
|
+
|
|
57
|
+
return function unsubscribe() {
|
|
58
|
+
listeners.delete(listener)
|
|
59
|
+
}
|
|
60
|
+
}
|
|
61
|
+
|
|
62
|
+
/**
|
|
63
|
+
* Updates the state based on elapsed time and processes events.
|
|
64
|
+
* @param {number} dt - The delta time since the last update.
|
|
65
|
+
*/
|
|
66
|
+
function update(dt) {
|
|
67
|
+
state = { ...state }
|
|
68
|
+
|
|
69
|
+
state.events.push(...incomingEvents, { id: "game:update" })
|
|
70
|
+
incomingEvents = []
|
|
71
|
+
|
|
72
|
+
while (state.events.length) {
|
|
73
|
+
const event = state.events.shift()
|
|
74
|
+
|
|
75
|
+
if (event.id === "instance:add") {
|
|
76
|
+
add(event.payload.id, event.payload)
|
|
77
|
+
}
|
|
78
|
+
|
|
79
|
+
state.instances = map(state.instances, (_, instance, instances) => {
|
|
80
|
+
const type = types[instance.type]
|
|
81
|
+
const handle = type[event.id]
|
|
82
|
+
return (
|
|
83
|
+
handle?.(instance, event, {
|
|
84
|
+
dt,
|
|
85
|
+
type: originalTypes[instance.type],
|
|
86
|
+
instances,
|
|
87
|
+
notify,
|
|
88
|
+
}) ?? instance
|
|
89
|
+
)
|
|
90
|
+
})
|
|
91
|
+
|
|
92
|
+
if (event.id === "instance:remove") {
|
|
93
|
+
remove(event.payload)
|
|
94
|
+
}
|
|
95
|
+
}
|
|
96
|
+
|
|
97
|
+
listeners.forEach((onUpdate) => onUpdate())
|
|
98
|
+
}
|
|
99
|
+
|
|
100
|
+
/**
|
|
101
|
+
* Adds a new instance to the state.
|
|
102
|
+
* @param {string} id - The ID of the instance to add.
|
|
103
|
+
* @param {Object} instance - The instance object to add.
|
|
104
|
+
*/
|
|
105
|
+
function add(id, instance) {
|
|
106
|
+
state = { ...state }
|
|
107
|
+
state.instances[id] = augmentInstance(id, instance)
|
|
108
|
+
}
|
|
109
|
+
|
|
110
|
+
/**
|
|
111
|
+
* Removes an instance from the state.
|
|
112
|
+
* @param {string} id - The ID of the instance to remove.
|
|
113
|
+
*/
|
|
114
|
+
function remove(id) {
|
|
115
|
+
state = { ...state }
|
|
116
|
+
delete state.instances[id]
|
|
117
|
+
}
|
|
118
|
+
|
|
119
|
+
/**
|
|
120
|
+
* Notifies the store of a new event.
|
|
121
|
+
* @param {Object} event - The event object to notify.
|
|
122
|
+
*/
|
|
123
|
+
function notify(event) {
|
|
124
|
+
incomingEvents.push(event)
|
|
125
|
+
}
|
|
126
|
+
|
|
127
|
+
/**
|
|
128
|
+
* Retrieves the types configuration.
|
|
129
|
+
* @returns {Object} The types configuration.
|
|
130
|
+
*/
|
|
131
|
+
function getTypes() {
|
|
132
|
+
return types
|
|
133
|
+
}
|
|
134
|
+
|
|
135
|
+
/**
|
|
136
|
+
* Retrieves the current state.
|
|
137
|
+
* @returns {Object} The current state.
|
|
138
|
+
*/
|
|
139
|
+
function getState() {
|
|
140
|
+
return state
|
|
141
|
+
}
|
|
142
|
+
}
|
|
143
|
+
|
|
144
|
+
function augmentTypes(types) {
|
|
145
|
+
return pipe(applyDecorators, enableMutability)(types)
|
|
146
|
+
}
|
|
147
|
+
|
|
148
|
+
function applyDecorators(types) {
|
|
149
|
+
return map(types, (_, type) => {
|
|
150
|
+
if (!Array.isArray(type)) {
|
|
151
|
+
return type
|
|
152
|
+
}
|
|
153
|
+
|
|
154
|
+
const decorators = type.map((fn) =>
|
|
155
|
+
typeof fn !== "function" ? (type) => extend(type, fn) : fn,
|
|
156
|
+
)
|
|
157
|
+
return pipe(...decorators)({})
|
|
158
|
+
})
|
|
159
|
+
}
|
|
160
|
+
|
|
161
|
+
function enableMutability(types) {
|
|
162
|
+
return map(types, (_, { draw, ...events }) => ({
|
|
163
|
+
draw,
|
|
164
|
+
...map(events, (_, event) => produce(event)),
|
|
165
|
+
}))
|
|
166
|
+
}
|
|
167
|
+
|
|
168
|
+
function augmentInstances(instances) {
|
|
169
|
+
return map(instances, augmentInstance)
|
|
170
|
+
}
|
|
171
|
+
|
|
172
|
+
function augmentInstance(id, instance) {
|
|
173
|
+
return { ...instance, layer: instance.layer ?? DEFAULT_LAYER, id }
|
|
174
|
+
}
|
|
@@ -0,0 +1,256 @@
|
|
|
1
|
+
import { expect, test } from "vitest"
|
|
2
|
+
|
|
3
|
+
import { createStore } from "./store.js"
|
|
4
|
+
|
|
5
|
+
test("it should add an event to the event queue", () => {
|
|
6
|
+
const event = { id: "something:happened" }
|
|
7
|
+
const config = {
|
|
8
|
+
types: {
|
|
9
|
+
kitty: {
|
|
10
|
+
[event.id](instance) {
|
|
11
|
+
return { ...instance, wasNotified: true }
|
|
12
|
+
},
|
|
13
|
+
},
|
|
14
|
+
},
|
|
15
|
+
instances: {
|
|
16
|
+
instance1: { type: "kitty" },
|
|
17
|
+
},
|
|
18
|
+
}
|
|
19
|
+
const store = createStore(config)
|
|
20
|
+
const afterState = {
|
|
21
|
+
events: [],
|
|
22
|
+
instances: {
|
|
23
|
+
game: {
|
|
24
|
+
id: "game",
|
|
25
|
+
type: "game",
|
|
26
|
+
layer: 0,
|
|
27
|
+
bounds: [0, 0, 800, 600],
|
|
28
|
+
},
|
|
29
|
+
instance1: {
|
|
30
|
+
id: "instance1",
|
|
31
|
+
type: "kitty",
|
|
32
|
+
layer: 0,
|
|
33
|
+
wasNotified: true,
|
|
34
|
+
},
|
|
35
|
+
},
|
|
36
|
+
}
|
|
37
|
+
|
|
38
|
+
store.notify(event)
|
|
39
|
+
store.update()
|
|
40
|
+
|
|
41
|
+
const state = store.getState()
|
|
42
|
+
expect(state).toStrictEqual(afterState)
|
|
43
|
+
})
|
|
44
|
+
|
|
45
|
+
test("it should process the event queue", () => {
|
|
46
|
+
const event = { id: "something:happened" }
|
|
47
|
+
const config = {
|
|
48
|
+
types: {
|
|
49
|
+
kitty: {
|
|
50
|
+
"game:update"(instance) {
|
|
51
|
+
return { ...instance, wasUpdated: true }
|
|
52
|
+
},
|
|
53
|
+
|
|
54
|
+
[event.id](instance) {
|
|
55
|
+
return { ...instance, wasNotified: true }
|
|
56
|
+
},
|
|
57
|
+
},
|
|
58
|
+
},
|
|
59
|
+
instances: {
|
|
60
|
+
instance1: { type: "kitty" },
|
|
61
|
+
},
|
|
62
|
+
}
|
|
63
|
+
const store = createStore(config)
|
|
64
|
+
store.notify(event)
|
|
65
|
+
const afterState = {
|
|
66
|
+
events: [],
|
|
67
|
+
instances: {
|
|
68
|
+
game: {
|
|
69
|
+
id: "game",
|
|
70
|
+
type: "game",
|
|
71
|
+
layer: 0,
|
|
72
|
+
bounds: [0, 0, 800, 600],
|
|
73
|
+
},
|
|
74
|
+
instance1: {
|
|
75
|
+
id: "instance1",
|
|
76
|
+
type: "kitty",
|
|
77
|
+
layer: 0,
|
|
78
|
+
wasNotified: true,
|
|
79
|
+
wasUpdated: true,
|
|
80
|
+
},
|
|
81
|
+
},
|
|
82
|
+
}
|
|
83
|
+
|
|
84
|
+
store.update()
|
|
85
|
+
|
|
86
|
+
const state = store.getState()
|
|
87
|
+
expect(state).toStrictEqual(afterState)
|
|
88
|
+
})
|
|
89
|
+
|
|
90
|
+
test("it should send an event from an instance", () => {
|
|
91
|
+
const event = {
|
|
92
|
+
id: "doge:message",
|
|
93
|
+
payload: { id: "inu", message: "Woof!" },
|
|
94
|
+
}
|
|
95
|
+
const config = {
|
|
96
|
+
types: {
|
|
97
|
+
doge: {
|
|
98
|
+
"game:update"(instance, event, { instances, notify }) {
|
|
99
|
+
if (instances.instance2.position === "near") {
|
|
100
|
+
notify(event)
|
|
101
|
+
}
|
|
102
|
+
},
|
|
103
|
+
},
|
|
104
|
+
kitty: {
|
|
105
|
+
[event.id](instance) {
|
|
106
|
+
if (event.payload.id === "inu" && event.payload.message === "Woof!") {
|
|
107
|
+
instance.position = "far"
|
|
108
|
+
}
|
|
109
|
+
},
|
|
110
|
+
},
|
|
111
|
+
},
|
|
112
|
+
|
|
113
|
+
instances: {
|
|
114
|
+
instance1: {
|
|
115
|
+
type: "doge",
|
|
116
|
+
},
|
|
117
|
+
instance2: {
|
|
118
|
+
type: "kitty",
|
|
119
|
+
position: "near",
|
|
120
|
+
},
|
|
121
|
+
},
|
|
122
|
+
}
|
|
123
|
+
const store = createStore(config)
|
|
124
|
+
const afterState = {
|
|
125
|
+
events: [],
|
|
126
|
+
instances: {
|
|
127
|
+
game: {
|
|
128
|
+
id: "game",
|
|
129
|
+
type: "game",
|
|
130
|
+
layer: 0,
|
|
131
|
+
bounds: [0, 0, 800, 600],
|
|
132
|
+
},
|
|
133
|
+
instance1: {
|
|
134
|
+
id: "instance1",
|
|
135
|
+
type: "doge",
|
|
136
|
+
layer: 0,
|
|
137
|
+
},
|
|
138
|
+
instance2: {
|
|
139
|
+
id: "instance2",
|
|
140
|
+
type: "kitty",
|
|
141
|
+
layer: 0,
|
|
142
|
+
position: "near", // should do nothing at first
|
|
143
|
+
},
|
|
144
|
+
},
|
|
145
|
+
}
|
|
146
|
+
|
|
147
|
+
store.update()
|
|
148
|
+
|
|
149
|
+
const state = store.getState()
|
|
150
|
+
expect(state).toStrictEqual(afterState)
|
|
151
|
+
})
|
|
152
|
+
|
|
153
|
+
test("it should receive an event from an instance", () => {
|
|
154
|
+
const event = {
|
|
155
|
+
id: "doge:message",
|
|
156
|
+
payload: { id: "inu", message: "Woof!" },
|
|
157
|
+
}
|
|
158
|
+
const config = {
|
|
159
|
+
types: {
|
|
160
|
+
doge: {
|
|
161
|
+
"game:update"(instance, event, { instances, notify }) {
|
|
162
|
+
if (instances.instance2.position === "near") {
|
|
163
|
+
notify(event)
|
|
164
|
+
}
|
|
165
|
+
},
|
|
166
|
+
},
|
|
167
|
+
kitty: {
|
|
168
|
+
[event.id](instance, event) {
|
|
169
|
+
if (event.payload.id === "inu" && event.payload.message === "Woof!") {
|
|
170
|
+
instance.position = "far"
|
|
171
|
+
}
|
|
172
|
+
},
|
|
173
|
+
},
|
|
174
|
+
},
|
|
175
|
+
|
|
176
|
+
instances: {
|
|
177
|
+
instance1: {
|
|
178
|
+
type: "doge",
|
|
179
|
+
},
|
|
180
|
+
instance2: {
|
|
181
|
+
type: "kitty",
|
|
182
|
+
position: "near",
|
|
183
|
+
},
|
|
184
|
+
},
|
|
185
|
+
}
|
|
186
|
+
const store = createStore(config)
|
|
187
|
+
store.notify(event)
|
|
188
|
+
const afterState = {
|
|
189
|
+
events: [],
|
|
190
|
+
instances: {
|
|
191
|
+
game: {
|
|
192
|
+
id: "game",
|
|
193
|
+
type: "game",
|
|
194
|
+
layer: 0,
|
|
195
|
+
bounds: [0, 0, 800, 600],
|
|
196
|
+
},
|
|
197
|
+
instance1: {
|
|
198
|
+
id: "instance1",
|
|
199
|
+
type: "doge",
|
|
200
|
+
layer: 0,
|
|
201
|
+
},
|
|
202
|
+
instance2: {
|
|
203
|
+
id: "instance2",
|
|
204
|
+
type: "kitty",
|
|
205
|
+
layer: 0,
|
|
206
|
+
position: "far", // position changed
|
|
207
|
+
},
|
|
208
|
+
},
|
|
209
|
+
}
|
|
210
|
+
|
|
211
|
+
store.update()
|
|
212
|
+
|
|
213
|
+
const state = store.getState()
|
|
214
|
+
expect(state).toStrictEqual(afterState)
|
|
215
|
+
})
|
|
216
|
+
|
|
217
|
+
test("it should mutate state in an immutable way", () => {
|
|
218
|
+
const config = {
|
|
219
|
+
types: {
|
|
220
|
+
kitty: {
|
|
221
|
+
"game:update"(instance) {
|
|
222
|
+
instance.wasUpdated = true
|
|
223
|
+
},
|
|
224
|
+
},
|
|
225
|
+
},
|
|
226
|
+
|
|
227
|
+
instances: {
|
|
228
|
+
instance1: {
|
|
229
|
+
type: "kitty",
|
|
230
|
+
},
|
|
231
|
+
},
|
|
232
|
+
}
|
|
233
|
+
const store = createStore(config)
|
|
234
|
+
const afterState = {
|
|
235
|
+
events: [],
|
|
236
|
+
instances: {
|
|
237
|
+
game: {
|
|
238
|
+
id: "game",
|
|
239
|
+
type: "game",
|
|
240
|
+
layer: 0,
|
|
241
|
+
bounds: [0, 0, 800, 600],
|
|
242
|
+
},
|
|
243
|
+
instance1: {
|
|
244
|
+
id: "instance1",
|
|
245
|
+
type: "kitty",
|
|
246
|
+
layer: 0,
|
|
247
|
+
wasUpdated: true,
|
|
248
|
+
},
|
|
249
|
+
},
|
|
250
|
+
}
|
|
251
|
+
|
|
252
|
+
store.update()
|
|
253
|
+
|
|
254
|
+
const state = store.getState()
|
|
255
|
+
expect(state).toStrictEqual(afterState)
|
|
256
|
+
})
|
package/src/engine.js
ADDED
|
@@ -0,0 +1,74 @@
|
|
|
1
|
+
import { extend } from "@inglorious/utils/data-structures/objects.js"
|
|
2
|
+
|
|
3
|
+
import Loop from "./engine/loop.js"
|
|
4
|
+
import { createStore } from "./engine/store.js"
|
|
5
|
+
|
|
6
|
+
// Default configuration for the engine
|
|
7
|
+
// loop.type specifies the type of loop to use (defaults to "animationFrame").
|
|
8
|
+
const DEFAULT_CONFIG = {
|
|
9
|
+
loop: { type: "animationFrame" },
|
|
10
|
+
}
|
|
11
|
+
const ONE_SECOND = 1000 // Number of milliseconds in one second.
|
|
12
|
+
|
|
13
|
+
/**
|
|
14
|
+
* Engine class responsible for managing the game loop, state, and rendering.
|
|
15
|
+
*/
|
|
16
|
+
export default class Engine {
|
|
17
|
+
/**
|
|
18
|
+
* @param {Object} game - Game-specific configuration.
|
|
19
|
+
* @param {Object} ui - UI instance responsible for rendering.
|
|
20
|
+
*/
|
|
21
|
+
constructor(game, ui) {
|
|
22
|
+
this._config = extend(DEFAULT_CONFIG, game)
|
|
23
|
+
this._store = createStore(this._config)
|
|
24
|
+
this._loop = new Loop[this._config.loop.type]()
|
|
25
|
+
this._ui = ui
|
|
26
|
+
}
|
|
27
|
+
|
|
28
|
+
/**
|
|
29
|
+
* Starts the game engine, initializing the loop and notifying the store.
|
|
30
|
+
*/
|
|
31
|
+
start() {
|
|
32
|
+
this._store.notify({ id: "game:start" })
|
|
33
|
+
this._loop.start(this, ONE_SECOND / this._config.loop.fps)
|
|
34
|
+
this.isRunning = true
|
|
35
|
+
}
|
|
36
|
+
|
|
37
|
+
/**
|
|
38
|
+
* Updates the game state.
|
|
39
|
+
* @param {number} dt - Delta time since the last update in milliseconds.
|
|
40
|
+
*/
|
|
41
|
+
update(dt) {
|
|
42
|
+
this._store.update(dt)
|
|
43
|
+
}
|
|
44
|
+
|
|
45
|
+
/**
|
|
46
|
+
* Renders the game state using the UI.
|
|
47
|
+
* @param {number} dt - Delta time since the last render in milliseconds.
|
|
48
|
+
*/
|
|
49
|
+
render(dt) {
|
|
50
|
+
this._ui?.render({
|
|
51
|
+
dt,
|
|
52
|
+
types: this._store.getTypes(),
|
|
53
|
+
instances: this._store.getState().instances,
|
|
54
|
+
})
|
|
55
|
+
}
|
|
56
|
+
|
|
57
|
+
/**
|
|
58
|
+
* Notifies the store of an event.
|
|
59
|
+
* @param {Object} event - Event object to notify the store with.
|
|
60
|
+
*/
|
|
61
|
+
notify = (event) => {
|
|
62
|
+
this._store.notify(event)
|
|
63
|
+
}
|
|
64
|
+
|
|
65
|
+
/**
|
|
66
|
+
* Stops the game engine, halting the loop and notifying the store.
|
|
67
|
+
*/
|
|
68
|
+
stop() {
|
|
69
|
+
this._store.notify({ id: "game:stop" })
|
|
70
|
+
this._store.update()
|
|
71
|
+
this._loop.stop()
|
|
72
|
+
this.isRunning = false
|
|
73
|
+
}
|
|
74
|
+
}
|