@inglorious/engine 0.6.1 → 0.7.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
@@ -50,6 +50,10 @@ Creates a new `Engine` instance.
50
50
 
51
51
  - A new `Engine` instance.
52
52
 
53
+ ### `await engine.init()`
54
+
55
+ An asynchronous function that initializes the resources required for the game to load.
56
+
53
57
  ### `engine.start()`
54
58
 
55
59
  Starts the game loop, triggering the first `update` and `render` calls.
@@ -65,52 +69,32 @@ Halts the game loop and cleans up any resources. This method also processes a fi
65
69
  Here is a complete example showing how to set up and run a game using the engine.
66
70
 
67
71
  ```html
68
- <!doctype html>
72
+ <!DOCTYPE html>
69
73
  <html lang="en">
70
-  
71
- <head>
72
-    
73
- <meta charset="UTF-8" />
74
-    
75
- <link rel="icon" type="image/svg+xml" href="/logo.png" />
76
-    
77
- <link rel="stylesheet" href="/style.css" />
78
-    
79
- <meta name="viewport" content="width=device-width, initial-scale=1.0" />
80
-
81
-    
82
- <title>Inglorious Engine</title>
83
-  
84
- </head>
85
-  
86
74
  <body>
87
-     <canvas id="canvas"></canvas>
75
+ <canvas id="canvas"></canvas>
88
76
 
89
-    
90
77
  <script type="text/javascript">
91
78
  window.process = { env: "development" }
92
79
  </script>
93
80
 
94
-    
95
81
  <script type="importmap">
96
82
  {
97
83
  "imports": {
98
84
  "immer": "https://unpkg.com/immer@10.1.1/dist/immer.mjs",
99
85
  "@inglorious/utils/": "https://unpkg.com/@inglorious%2Futils@1.1.0/",
100
- "@inglorious/store/": "https://unpkg.com/@inglorious%2Fstore@0.1.0/",
101
- "@inglorious/engine/": "https://unpkg.com/@inglorious%2Fengine@0.4.0/",
86
+ "@inglorious/store/": "https://unpkg.com/@inglorious%2Fstore@2.0.0/",
87
+ "@inglorious/engine/": "https://unpkg.com/@inglorious%2Fengine@0.7.0/",
102
88
  "@inglorious/renderers/": "https://unpkg.com/@inglorious%2Frenderer-2d@0.2.0/",
103
89
  "game": "/game.js"
104
90
  }
105
91
  }
106
92
  </script>
107
93
 
108
-    
109
94
  <script
110
95
  type="module"
111
- src="https://unpkg.com/@inglorious%2Fengine@0.2.0/src/main.js"
96
+ src="https://unpkg.com/@inglorious%2Fengine@0.7.0/main.js"
112
97
  ></script>
113
-  
114
98
  </body>
115
99
  </html>
116
100
  ```
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@inglorious/engine",
3
- "version": "0.6.1",
3
+ "version": "0.7.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",
@@ -32,8 +32,8 @@
32
32
  "access": "public"
33
33
  },
34
34
  "dependencies": {
35
- "@inglorious/store": "2.0.0",
36
- "@inglorious/utils": "1.1.0"
35
+ "@inglorious/utils": "1.1.0",
36
+ "@inglorious/store": "2.0.0"
37
37
  },
38
38
  "devDependencies": {
39
39
  "prettier": "^3.5.3",
@@ -0,0 +1,28 @@
1
+ export function audio() {
2
+ const audioContext = new (window.AudioContext || window.webkitAudioContext)()
3
+ const audioBufferCache = new Map()
4
+
5
+ return {
6
+ async init(entity) {
7
+ const sounds = entity.sounds || {}
8
+
9
+ await Promise.all(
10
+ Object.entries(sounds).map(async ([name, url]) => {
11
+ const response = await fetch(url)
12
+ const arrayBuffer = await response.arrayBuffer()
13
+ const audioBuffer = await audioContext.decodeAudioData(arrayBuffer)
14
+ audioBufferCache.set(name, audioBuffer)
15
+ }),
16
+ )
17
+ },
18
+
19
+ playSound(entity, name) {
20
+ const audioBuffer = audioBufferCache.get(name)
21
+
22
+ const source = audioContext.createBufferSource()
23
+ source.buffer = audioBuffer
24
+ source.connect(audioContext.destination)
25
+ source.start()
26
+ },
27
+ }
28
+ }
@@ -22,7 +22,9 @@ export function camera(params) {
22
22
  params = extend(DEFAULT_PARAMS, params)
23
23
 
24
24
  return {
25
- create(entity) {
25
+ create(entity, entityId) {
26
+ if (entityId !== entity.id) return
27
+
26
28
  defaults(entity, params)
27
29
  entity.targetZoom = entity.zoom
28
30
  // Cache the initial size to calculate the viewport in dev mode
@@ -24,8 +24,10 @@ export function modernAcceleration(params) {
24
24
  "moveUpDown",
25
25
  ]),
26
26
 
27
- create(entity, event, api) {
28
- type.create?.(entity, event, api)
27
+ create(entity, entityId, api) {
28
+ type.create?.(entity, entityId, api)
29
+
30
+ if (entityId !== entity.id) return
29
31
 
30
32
  entity.maxAcceleration ??= params.maxAcceleration
31
33
  entity.movement ??= {}
@@ -33,8 +33,10 @@ export function shooterControls(params) {
33
33
  "turn",
34
34
  ]),
35
35
 
36
- create(entity, event, api) {
37
- type.create?.(entity, event, api)
36
+ create(entity, entityId, api) {
37
+ type.create?.(entity, entityId, api)
38
+
39
+ if (entityId !== entity.id) return
38
40
 
39
41
  entity.maxSpeed ??= params.maxSpeed
40
42
  entity.maxAngularSpeed ??= params.maxAngularSpeed
@@ -27,8 +27,10 @@ export function tankControls(params) {
27
27
  "turn",
28
28
  ]),
29
29
 
30
- create(entity, event, api) {
31
- type.create?.(entity, event, api)
30
+ create(entity, entityId, api) {
31
+ type.create?.(entity, entityId, api)
32
+
33
+ if (entityId !== entity.id) return
32
34
 
33
35
  entity.maxSpeed ??= params.maxSpeed
34
36
  entity.maxAngularSpeed ??= params.maxAngularSpeed
@@ -1,12 +1,20 @@
1
1
  export function createMovementEventHandlers(events) {
2
2
  return events.reduce((acc, eventName) => {
3
- acc[eventName] = (entity, { entityId, value }) => {
3
+ acc[eventName] = (entity, event) => {
4
+ let entityId, value
5
+ if (typeof event === "string") {
6
+ entityId = event
7
+ } else {
8
+ entityId = event.entityId
9
+ value = event.value
10
+ }
11
+
4
12
  if (entityId === entity.id) {
5
13
  entity.movement[eventName] = value ?? true
6
14
  }
7
15
  }
8
16
 
9
- acc[`${eventName}End`] = (entity, { entityId }) => {
17
+ acc[`${eventName}End`] = (entity, entityId) => {
10
18
  if (entityId === entity.id) {
11
19
  entity.movement[eventName] = false
12
20
  }
@@ -24,8 +24,10 @@ export function modernVelocity(params) {
24
24
  "moveUpDown",
25
25
  ]),
26
26
 
27
- create(entity, event, api) {
28
- type.create?.(entity, event, api)
27
+ create(entity, entityId, api) {
28
+ type.create?.(entity, entityId, api)
29
+
30
+ if (entityId !== entity.id) return
29
31
 
30
32
  entity.maxSpeed ??= params.maxSpeed
31
33
  entity.movement ??= {}
@@ -32,8 +32,10 @@ export function shooterControls(params) {
32
32
  "turn",
33
33
  ]),
34
34
 
35
- create(entity, event, api) {
36
- type.create?.(entity, event, api)
35
+ create(entity, entityId, api) {
36
+ type.create?.(entity, entityId, api)
37
+
38
+ if (entityId !== entity.id) return
37
39
 
38
40
  entity.maxSpeed ??= params.maxSpeed
39
41
  entity.maxAngularSpeed ??= params.maxAngularSpeed
@@ -26,8 +26,10 @@ export function tankControls(params) {
26
26
  "turn",
27
27
  ]),
28
28
 
29
- create(entity, event, api) {
30
- type.create?.(entity, event, api)
29
+ create(entity, entityId, api) {
30
+ type.create?.(entity, entityId, api)
31
+
32
+ if (entityId !== entity.id) return
31
33
 
32
34
  entity.maxSpeed ??= params.maxSpeed
33
35
  entity.maxAngularSpeed ??= params.maxAngularSpeed
@@ -12,7 +12,9 @@ export function fps(params) {
12
12
  params = extend(DEFAULT_PARAMS, params)
13
13
 
14
14
  return {
15
- create(entity) {
15
+ create(entity, entityId) {
16
+ if (entityId !== entity.id) return
17
+
16
18
  entity.dt ??= { ...params }
17
19
  },
18
20
 
@@ -9,8 +9,10 @@ export function fsm(states) {
9
9
 
10
10
  return (type) => {
11
11
  return extend(type, {
12
- create(entity, event, api) {
13
- type.create?.(entity, event, api)
12
+ create(entity, entityId, api) {
13
+ type.create?.(entity, entityId, api)
14
+
15
+ if (entityId !== entity.id) return
14
16
 
15
17
  entity.state ??= DEFAULT_STATE
16
18
  },
@@ -4,7 +4,9 @@ const DEFAULT_PARAMS = {
4
4
 
5
5
  export function gamepadsPoller() {
6
6
  return {
7
- create(entity) {
7
+ create(entity, entityId) {
8
+ if (entityId !== entity.id) return
9
+
8
10
  entity.gamepadStateCache ??= {}
9
11
  },
10
12
 
@@ -22,7 +22,7 @@ export function input() {
22
22
  entity[action] = true
23
23
 
24
24
  entity.targetIds.forEach((targetId) => {
25
- api.notify(action, { entityId: targetId })
25
+ api.notify(action, targetId)
26
26
  })
27
27
  },
28
28
 
@@ -33,7 +33,7 @@ export function input() {
33
33
  entity[action] = false
34
34
 
35
35
  entity.targetIds.forEach((targetId) => {
36
- api.notify(`${action}End`, { entityId: targetId })
36
+ api.notify(`${action}End`, targetId)
37
37
  })
38
38
  },
39
39
  }
@@ -7,7 +7,9 @@ export function keyboard() {
7
7
  let currentDocument = null
8
8
 
9
9
  return {
10
- create(entity, event, api) {
10
+ create(entity, entityId, api) {
11
+ if (entityId !== entity.id) return
12
+
11
13
  currentDocument = document.body.ownerDocument || document
12
14
 
13
15
  handleKeyDown = createKeyboardHandler("keyboardKeyDown", api)
@@ -10,7 +10,9 @@ const NO_Y = 0
10
10
 
11
11
  export function mouse() {
12
12
  return {
13
- create(entity) {
13
+ create(entity, entityId) {
14
+ if (entityId !== entity.id) return
15
+
14
16
  entity.collisions ??= {}
15
17
  entity.collisions.bounds ??= { shape: "point" }
16
18
  },
@@ -10,12 +10,15 @@ export function bouncy(params) {
10
10
 
11
11
  return (type) =>
12
12
  extend(type, {
13
- create(entity) {
14
- type.create?.(entity)
13
+ create(entity, entityId, api) {
14
+ type.create?.(entity, entityId, api)
15
+
16
+ if (entityId !== entity.id) return
17
+
15
18
  defaults(entity, params)
16
19
  },
17
20
 
18
- landed(entity, { entityId }) {
21
+ land(entity, entityId) {
19
22
  if (entity.id === entityId) {
20
23
  entity.vy = jump(entity) * entity.bounciness
21
24
  entity.groundObject = undefined
@@ -11,8 +11,10 @@ export function clamped(params) {
11
11
 
12
12
  return (type) =>
13
13
  extend(type, {
14
- create(entity, event, api) {
15
- type.create?.(entity, event, api)
14
+ create(entity, entityId, api) {
15
+ type.create?.(entity, entityId, api)
16
+
17
+ if (entityId !== entity.id) return
16
18
 
17
19
  entity.collisions ??= {}
18
20
  entity.collisions[params.collisionGroup] ??= {}
@@ -27,8 +27,11 @@ export function jumpable(params) {
27
27
 
28
28
  return (type) =>
29
29
  extend(type, {
30
- create(entity, event, api) {
31
- type.create?.(entity, event, api)
30
+ create(entity, entityId, api) {
31
+ type.create?.(entity, entityId, api)
32
+
33
+ if (entityId !== entity.id) return
34
+
32
35
  defaults(entity, params)
33
36
  entity.jumpsLeft ??= entity.maxJumps
34
37
 
@@ -36,7 +39,9 @@ export function jumpable(params) {
36
39
  entity.vy ??= 0
37
40
  },
38
41
 
39
- jump(entity, { entityId }) {
42
+ jump(entity, entityId, api) {
43
+ type.jump?.(entity, entityId, api)
44
+
40
45
  if (entityId === entity.id && entity.jumpsLeft) {
41
46
  entity.vy = jump(entity)
42
47
  entity.groundObject = undefined
@@ -116,7 +121,7 @@ export function jumpable(params) {
116
121
 
117
122
  // Only notify on the frame we actually land, not every frame we're on the ground.
118
123
  if (!wasOnGround) {
119
- api.notify("landed", {
124
+ api.notify("land", {
120
125
  entityId: entity.id,
121
126
  targetId: collisionY.id,
122
127
  })
@@ -1,3 +1,4 @@
1
+ import { audio } from "@inglorious/engine/behaviors/audio.js"
1
2
  import { game } from "@inglorious/engine/behaviors/game.js"
2
3
  import { createApi } from "@inglorious/store/api.js"
3
4
  import { createStore } from "@inglorious/store/store.js"
@@ -20,11 +21,13 @@ const DEFAULT_GAME_CONFIG = {
20
21
 
21
22
  types: {
22
23
  game: [game()],
24
+ audio: audio(),
23
25
  },
24
26
 
25
27
  entities: {
26
28
  // eslint-disable-next-line no-magic-numbers
27
29
  game: { type: "game", bounds: [0, 0, 800, 600] },
30
+ audio: { type: "audio", sounds: {} },
28
31
  },
29
32
  }
30
33
 
@@ -77,14 +80,18 @@ export class Engine {
77
80
 
78
81
  this._loop = new Loops[this._config.loop.type]()
79
82
 
80
- // The renderer might need the engine instance to initialize itself (e.g., to set up DOM events).
81
- this._config.renderer?.init(this)
82
-
83
83
  if (this._devMode) {
84
84
  initDevTools(this._store)
85
85
  }
86
86
  }
87
87
 
88
+ async init() {
89
+ return Promise.all([
90
+ this._config.types.audio.init(this._config.entities.audio),
91
+ this._config.renderer?.init(this),
92
+ ])
93
+ }
94
+
88
95
  /**
89
96
  * Starts the game engine, initializing the loop and notifying the store.
90
97
  */
@@ -101,6 +108,7 @@ export class Engine {
101
108
  this._api.notify("stop")
102
109
  this._store.update(this._api)
103
110
  this._loop.stop()
111
+ this._config.renderer?.destroy()
104
112
  this.isRunning = false
105
113
  }
106
114
 
@@ -117,7 +125,7 @@ export class Engine {
117
125
  const newDevMode = state.entities.game?.devMode
118
126
  if (newDevMode !== this._devMode) {
119
127
  if (newDevMode) {
120
- initDevTools(this._api)
128
+ initDevTools(this._store)
121
129
  } else {
122
130
  disconnectDevTools()
123
131
  }
package/src/main.js CHANGED
@@ -1,10 +1,11 @@
1
1
  import { Engine } from "@inglorious/engine/core/engine.js"
2
- import { CanvasRenderer } from "@inglorious/renderer-2d/canvas-renderer.js"
2
+ import { Renderer2D } from "@inglorious/renderer-2d/index.js"
3
3
  import game from "game"
4
4
 
5
5
  const canvas = document.getElementById("canvas")
6
- window.addEventListener("load", () => {
7
- const renderer = new CanvasRenderer(canvas)
6
+ window.addEventListener("load", async () => {
7
+ const renderer = new Renderer2D(canvas)
8
8
  const engine = new Engine({ ...game, renderer })
9
+ await engine.init()
9
10
  engine.start()
10
11
  })
@@ -1,30 +0,0 @@
1
- import { Ticker } from "@inglorious/engine/animation/ticker.js"
2
-
3
- const DEFAULT_SPAWN_INTERVAL = 1 // Time in seconds between spawning entities.
4
-
5
- /**
6
- * A system responsible for creating new entities, like asteroids, at regular intervals.
7
- *
8
- * @param {object} params Configuration parameters for the system.
9
- * @param {number} [params.spawnInterval=1] The time in seconds between spawning entities.
10
- * @param {function} params.factory A function that returns the properties for a new entity.
11
- * @returns {object} The configured entity creator system.
12
- */
13
- export function entityCreator(params = {}) {
14
- const spawnInterval = params.spawnInterval ?? DEFAULT_SPAWN_INTERVAL
15
- const factory = params.factory
16
-
17
- const ticker = { speed: spawnInterval }
18
-
19
- return {
20
- update(state, dt, api) {
21
- Ticker.tick({
22
- target: ticker,
23
- dt,
24
- onTick: () => {
25
- api.notify("add", factory())
26
- },
27
- })
28
- },
29
- }
30
- }