@inglorious/engine 0.8.0 → 0.10.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/README.md CHANGED
@@ -31,9 +31,9 @@ npm install @inglorious/engine
31
31
 
32
32
  ## API
33
33
 
34
- ### `new Engine(gameConfig)`
34
+ ### `new Engine(...gameConfigs)`
35
35
 
36
- Creates a new `Engine` instance.
36
+ Creates a new `Engine` instance, given one or more configuration objects.
37
37
 
38
38
  **Parameters:**
39
39
 
@@ -41,10 +41,9 @@ Creates a new `Engine` instance.
41
41
  - `loop` (object, optional): Configuration for the game loop.
42
42
  - `type` (string, optional): The type of loop to use (`animationFrame` or `fixed`). Defaults to `animationFrame`.
43
43
  - `fps` (number, optional): The target frames per second. Only used with the `fixed` loop type. Defaults to `60`.
44
- - `systems` (array, optional): An array of system objects, which define behaviors for the whole state.
45
44
  - `types` (object, optional): A map of entity types.
46
45
  - `entities` (object, optional): A map of initial entities.
47
- - `renderer` (object, optional): A renderer entity responsible for drawing the game. It must implement `getSystems()` and `init(engine)` methods.
46
+ - `systems` (array, optional): An array of system objects, which define behaviors for the whole state.
48
47
 
49
48
  **Returns:**
50
49
 
@@ -72,7 +71,7 @@ Here is a complete example showing how to set up and run a game using the engine
72
71
  <!DOCTYPE html>
73
72
  <html lang="en">
74
73
  <body>
75
- <canvas id="canvas"></canvas>
74
+ <canvas id="canvas" width="800" height="600"></canvas>
76
75
 
77
76
  <script type="text/javascript">
78
77
  window.process = { env: "development" }
@@ -81,11 +80,11 @@ Here is a complete example showing how to set up and run a game using the engine
81
80
  <script type="importmap">
82
81
  {
83
82
  "imports": {
84
- "immer": "https://unpkg.com/immer@10.1.1/dist/immer.mjs",
85
- "@inglorious/utils/": "https://unpkg.com/@inglorious%2Futils@1.2.0/",
86
- "@inglorious/store/": "https://unpkg.com/@inglorious%2Fstore@2.0.0/",
87
- "@inglorious/engine/": "https://unpkg.com/@inglorious%2Fengine@0.7.0/",
88
- "@inglorious/renderers/": "https://unpkg.com/@inglorious%2Frenderer-2d@0.2.0/",
83
+ "immer": "https://unpkg.com/immer@latest/dist/immer.mjs",
84
+ "@inglorious/utils/": "https://unpkg.com/@inglorious%2Futils@latest/src/",
85
+ "@inglorious/store/": "https://unpkg.com/@inglorious%2Fstore@latest/src/",
86
+ "@inglorious/engine/": "https://unpkg.com/@inglorious%2Fengine@latest/src/",
87
+ "@inglorious/renderer-2d/": "https://unpkg.com/@inglorious%2Frenderer-2d@latest/src/",
89
88
  "game": "/game.js"
90
89
  }
91
90
  }
@@ -93,7 +92,7 @@ Here is a complete example showing how to set up and run a game using the engine
93
92
 
94
93
  <script
95
94
  type="module"
96
- src="https://unpkg.com/@inglorious%2Fengine@0.7.0/main.js"
95
+ src="https://unpkg.com/@inglorious%2Fengine@latest/src/main.js"
97
96
  ></script>
98
97
  </body>
99
98
  </html>
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@inglorious/engine",
3
- "version": "0.8.0",
3
+ "version": "0.10.0",
4
4
  "description": "A JavaScript game engine written with global state, immutability, and pure functions in mind. Have fun(ctional programming) with it!",
5
5
  "author": "IceOnFire <antony.mistretta@gmail.com> (https://ingloriouscoderz.it)",
6
6
  "license": "MIT",
@@ -24,14 +24,14 @@
24
24
  "./*": "./src/*"
25
25
  },
26
26
  "files": [
27
- "src/*"
27
+ "src"
28
28
  ],
29
29
  "publishConfig": {
30
30
  "access": "public"
31
31
  },
32
32
  "dependencies": {
33
- "@inglorious/store": "3.0.0",
34
- "@inglorious/utils": "2.0.0"
33
+ "@inglorious/utils": "2.2.0",
34
+ "@inglorious/store": "3.0.0"
35
35
  },
36
36
  "devDependencies": {
37
37
  "prettier": "^3.5.3",
@@ -1,13 +1,17 @@
1
+ const DEFAULT_VOLUME = 1
2
+
1
3
  export function audio() {
2
4
  const audioContext = new (window.AudioContext || window.webkitAudioContext)()
5
+
3
6
  const audioBufferCache = new Map()
7
+ const activeSources = new Map()
4
8
 
5
9
  return {
6
10
  async init(entity) {
7
11
  const sounds = entity.sounds || {}
8
12
 
9
13
  await Promise.all(
10
- Object.entries(sounds).map(async ([name, url]) => {
14
+ Object.entries(sounds).map(async ([name, { url }]) => {
11
15
  const response = await fetch(url)
12
16
  const arrayBuffer = await response.arrayBuffer()
13
17
  const audioBuffer = await audioContext.decodeAudioData(arrayBuffer)
@@ -16,13 +20,36 @@ export function audio() {
16
20
  )
17
21
  },
18
22
 
19
- playSound(entity, name) {
23
+ soundPlay(entity, name) {
24
+ const { volume = DEFAULT_VOLUME, loop } = entity.sounds[name]
20
25
  const audioBuffer = audioBufferCache.get(name)
21
26
 
22
27
  const source = audioContext.createBufferSource()
28
+ const gainNode = audioContext.createGain()
29
+
23
30
  source.buffer = audioBuffer
24
- source.connect(audioContext.destination)
31
+ gainNode.gain.value = volume
32
+
33
+ source.connect(gainNode)
34
+ gainNode.connect(audioContext.destination)
35
+
36
+ source.loop = loop
25
37
  source.start()
38
+
39
+ activeSources.set(name, source)
40
+ },
41
+
42
+ soundStop(entity, name) {
43
+ const source = activeSources.get(name)
44
+ source?.stop()
45
+ activeSources.delete(name)
46
+ },
47
+
48
+ stop() {
49
+ for (const source of activeSources.values()) {
50
+ source.stop()
51
+ }
52
+ activeSources.clear()
26
53
  },
27
54
  }
28
55
  }
@@ -1,5 +1,13 @@
1
1
  export function game() {
2
2
  return {
3
+ pause(entity) {
4
+ entity.paused = true
5
+ },
6
+
7
+ resume(entity) {
8
+ entity.paused = false
9
+ },
10
+
3
11
  keyboardKeyUp(entity, code) {
4
12
  switch (code) {
5
13
  case "KeyC":
@@ -22,7 +22,7 @@ export function mouse() {
22
22
 
23
23
  entity.position = position
24
24
 
25
- clampToBounds(entity, game.bounds)
25
+ clampToBounds(entity, game.size)
26
26
  },
27
27
 
28
28
  mouseClick(entity, position, api) {
@@ -74,20 +74,17 @@ function createHandler(type, parent, api) {
74
74
  }
75
75
 
76
76
  // For move and click events, the payload is the calculated position.
77
- const payload = calculatePosition({
78
- clientX: event.clientX,
79
- clientY: event.clientY,
80
- parent,
81
- })
77
+ const payload = calculatePosition(event, parent)
82
78
  api.notify(type, payload)
83
79
  }
84
80
  }
85
81
 
86
- function calculatePosition({ clientX, clientY, parent }) {
87
- const bounds = parent.getBoundingClientRect()
82
+ function calculatePosition(event, parent) {
83
+ const { clientX, clientY } = event
84
+ const { left, bottom } = parent.getBoundingClientRect()
88
85
 
89
- const x = clientX - bounds.left
90
- const z = bounds.bottom - clientY
86
+ const x = clientX - left
87
+ const z = bottom - clientY
91
88
 
92
89
  return [x, NO_Y, z]
93
90
  }
@@ -28,7 +28,7 @@ export function clamped(params) {
28
28
  merge(entity, {
29
29
  position: clampToBounds(
30
30
  entity,
31
- game.bounds,
31
+ game.size,
32
32
  params.collisionGroup,
33
33
  params.depthAxis,
34
34
  ),
@@ -2,7 +2,10 @@ import { audio } from "@inglorious/engine/behaviors/audio.js"
2
2
  import { game } from "@inglorious/engine/behaviors/game.js"
3
3
  import { createApi } from "@inglorious/store/api.js"
4
4
  import { createStore } from "@inglorious/store/store.js"
5
- import { extend } from "@inglorious/utils/data-structures/objects.js"
5
+ import { augmentType } from "@inglorious/store/types.js"
6
+ import { isArray } from "@inglorious/utils/data-structures/array.js"
7
+ import { extendWith } from "@inglorious/utils/data-structures/objects.js"
8
+ import { isVector } from "@inglorious/utils/math/linear-algebra/vector.js"
6
9
 
7
10
  import { coreEvents } from "./core-events.js"
8
11
  import { disconnectDevTools, initDevTools, sendAction } from "./dev-tools.js"
@@ -21,12 +24,12 @@ const DEFAULT_GAME_CONFIG = {
21
24
 
22
25
  types: {
23
26
  game: [game()],
24
- audio: audio(),
27
+ audio: [audio()],
25
28
  },
26
29
 
27
30
  entities: {
28
31
  // eslint-disable-next-line no-magic-numbers
29
- game: { type: "game", bounds: [0, 0, 800, 600] },
32
+ game: { type: "game", size: [800, 600] },
30
33
  audio: { type: "audio", sounds: {} },
31
34
  },
32
35
  }
@@ -41,8 +44,8 @@ export class Engine {
41
44
  * @param {Object} [gameConfig] - Game-specific configuration.
42
45
  * @param {Object} [renderer] - UI entity responsible for rendering. It must have a `render` method.
43
46
  */
44
- constructor(gameConfig) {
45
- this._config = extend(DEFAULT_GAME_CONFIG, gameConfig)
47
+ constructor(...gameConfigs) {
48
+ this._config = extendWith(merger, DEFAULT_GAME_CONFIG, ...gameConfigs)
46
49
 
47
50
  // Determine devMode from the entities config.
48
51
  const devMode = this._config.entities.game?.devMode
@@ -50,9 +53,6 @@ export class Engine {
50
53
 
51
54
  // Add user-defined systems
52
55
  const systems = [...(this._config.systems ?? [])]
53
- if (this._config.renderer) {
54
- systems.push(...this._config.renderer.getSystems())
55
- }
56
56
 
57
57
  this._store = createStore({ ...this._config, systems })
58
58
 
@@ -86,10 +86,13 @@ export class Engine {
86
86
  }
87
87
 
88
88
  async init() {
89
- return Promise.all([
90
- this._config.types.audio.init(this._config.entities.audio),
91
- this._config.renderer?.init(this),
92
- ])
89
+ return Promise.all(
90
+ Object.values(this._config.entities).map((entity) => {
91
+ const originalType = this._config.types[entity.type]
92
+ const type = augmentType(originalType)
93
+ return type.init?.(entity, null, this._api)
94
+ }),
95
+ )
93
96
  }
94
97
 
95
98
  /**
@@ -98,7 +101,6 @@ export class Engine {
98
101
  start() {
99
102
  this._api.notify("start")
100
103
  this._loop.start(this, ONE_SECOND / this._config.loop.fps)
101
- this.isRunning = true
102
104
  }
103
105
 
104
106
  /**
@@ -108,8 +110,6 @@ export class Engine {
108
110
  this._api.notify("stop")
109
111
  this._store.update(this._api)
110
112
  this._loop.stop()
111
- this._config.renderer?.destroy()
112
- this.isRunning = false
113
113
  }
114
114
 
115
115
  /**
@@ -145,3 +145,14 @@ export class Engine {
145
145
  }
146
146
  }
147
147
  }
148
+
149
+ function merger(targetValue, sourceValue) {
150
+ if (
151
+ isArray(targetValue) &&
152
+ !isVector(targetValue) &&
153
+ isArray(sourceValue) &&
154
+ !isVector(sourceValue)
155
+ ) {
156
+ return [...targetValue, ...sourceValue]
157
+ }
158
+ }
package/src/main.js CHANGED
@@ -1,11 +1,11 @@
1
1
  import { Engine } from "@inglorious/engine/core/engine.js"
2
- import { Renderer2D } from "@inglorious/renderer-2d/index.js"
2
+ import { createRenderer } from "@inglorious/renderer-2d/index.js"
3
3
  import game from "game"
4
4
 
5
- const canvas = document.getElementById("canvas")
6
5
  window.addEventListener("load", async () => {
7
- const renderer = new Renderer2D(canvas)
8
- const engine = new Engine({ ...game, renderer })
6
+ const canvas = document.getElementById("canvas")
7
+ const renderer = createRenderer(canvas)
8
+ const engine = new Engine(renderer, game)
9
9
  await engine.init()
10
10
  engine.start()
11
11
  })
@@ -9,20 +9,21 @@ import {
9
9
  import { sum } from "@inglorious/utils/math/linear-algebra/vectors.js"
10
10
  import { abs } from "@inglorious/utils/math/numbers.js"
11
11
 
12
+ const ORIGIN = 0
12
13
  const DOUBLE = 2
13
14
  const HALF = 2
14
15
  const X = 0
15
16
  const Z = 2
16
17
 
17
- export function bounce(entity, dt, [minX, minZ, maxX, maxZ]) {
18
+ export function bounce(entity, dt, [maxX, maxZ]) {
18
19
  const [x, , z] = entity.position
19
20
 
20
21
  const velocity = createVector(entity.maxSpeed, entity.orientation)
21
- if (x < minX || x >= maxX) {
22
+ if (x < ORIGIN || x >= maxX) {
22
23
  velocity[X] = -velocity[X]
23
24
  }
24
25
 
25
- if (z < minZ || z >= maxZ) {
26
+ if (z < ORIGIN || z >= maxZ) {
26
27
  velocity[Z] = -velocity[Z]
27
28
  }
28
29
 
@@ -33,7 +34,7 @@ export function bounce(entity, dt, [minX, minZ, maxX, maxZ]) {
33
34
  }
34
35
 
35
36
  const ClampToBoundsByShape = {
36
- rectangle(entity, [minX, minZ, maxX, maxZ], collisionGroup) {
37
+ rectangle(entity, [maxX, maxZ], collisionGroup) {
37
38
  const [width, height, depth] =
38
39
  entity.collisions[collisionGroup].size ?? entity.size
39
40
 
@@ -43,31 +44,31 @@ const ClampToBoundsByShape = {
43
44
 
44
45
  return clamp(
45
46
  entity.position,
46
- [minX + halfWidth, minZ + halfHeight, minZ + halfDepth],
47
+ [halfWidth, halfHeight, halfDepth],
47
48
  [maxX - halfWidth, maxZ - halfHeight, maxZ - halfDepth],
48
49
  )
49
50
  },
50
51
 
51
- circle(entity, [minX, minY, maxX, maxY], collisionGroup, depthAxis = "y") {
52
+ circle(entity, [maxX, maxY], collisionGroup, depthAxis = "y") {
52
53
  const radius = entity.collisions[collisionGroup].radius ?? entity.radius
53
54
 
54
55
  if (depthAxis === "z") {
55
56
  return clamp(
56
57
  entity.position,
57
- [minX + radius, minY + radius, minY],
58
+ [radius, radius, ORIGIN],
58
59
  [maxX - radius, maxY - radius, maxY],
59
60
  )
60
61
  }
61
62
 
62
63
  return clamp(
63
64
  entity.position,
64
- [minX + radius, minY, minY + radius],
65
+ [radius, ORIGIN, radius],
65
66
  [maxX - radius, maxY, maxY - radius],
66
67
  )
67
68
  },
68
69
 
69
- point(entity, [minX, minZ, maxX, maxZ]) {
70
- return clamp(entity.position, [minX, minZ, minZ], [maxX, maxZ, maxZ])
70
+ point(entity, [maxX, maxZ]) {
71
+ return clamp(entity.position, zero(), [maxX, maxZ, maxZ])
71
72
  },
72
73
  }
73
74
 
@@ -82,7 +83,7 @@ export function clampToBounds(
82
83
  return handler(entity, bounds, collisionGroup, depthAxis)
83
84
  }
84
85
 
85
- export function flip(entity, [minX, minZ, maxX, maxZ]) {
86
+ export function flip(entity, [maxX, maxZ]) {
86
87
  const [x, , z] = entity.position
87
88
 
88
89
  entity.collisions ??= {}
@@ -111,20 +112,20 @@ export function flip(entity, [minX, minZ, maxX, maxZ]) {
111
112
  const direction = fromAngle(entity.orientation)
112
113
 
113
114
  if (
114
- left < minX ||
115
+ left < ORIGIN ||
115
116
  right >= maxX ||
116
- bottom < minZ ||
117
+ bottom < ORIGIN ||
117
118
  top >= maxZ ||
118
- back < minZ ||
119
+ back < ORIGIN ||
119
120
  front >= maxZ
120
121
  ) {
121
- if (left < minX) {
122
+ if (left < ORIGIN) {
122
123
  direction[X] = abs(direction[X])
123
124
  } else if (right >= maxX) {
124
125
  direction[X] = -abs(direction[X])
125
126
  }
126
127
 
127
- if (back < minZ) {
128
+ if (back < ORIGIN) {
128
129
  direction[Z] = abs(direction[Z])
129
130
  } else if (front >= maxZ) {
130
131
  direction[Z] = -abs(direction[Z])