@inglorious/engine 1.1.0 → 2.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/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@inglorious/engine",
3
- "version": "1.1.0",
3
+ "version": "2.1.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",
@@ -31,12 +31,17 @@
31
31
  },
32
32
  "dependencies": {
33
33
  "@inglorious/store": "4.0.0",
34
- "@inglorious/utils": "3.2.0"
34
+ "@inglorious/utils": "3.5.0"
35
+ },
36
+ "peerDependencies": {
37
+ "@inglorious/store": "4.0.0",
38
+ "@inglorious/utils": "3.5.0"
35
39
  },
36
40
  "devDependencies": {
37
41
  "prettier": "^3.5.3",
38
42
  "vite": "^7.1.3",
39
- "vitest": "^1.6.0"
43
+ "vitest": "^1.6.0",
44
+ "@inglorious/eslint-config": "1.0.0"
40
45
  },
41
46
  "engines": {
42
47
  "node": ">= 22"
@@ -57,7 +57,7 @@ export function camera(params) {
57
57
  }
58
58
  },
59
59
 
60
- mouseWheel(entity, { deltaY }) {
60
+ mouseWheel(entity, deltaY) {
61
61
  const delta = Math.sign(deltaY)
62
62
  // Scrolling down (positive deltaY) should zoom out (decrease zoom value)
63
63
  entity.targetZoom -= delta * entity.zoomSpeed * entity.zoomSensitivity
@@ -1,37 +1,21 @@
1
- import { extend } from "@inglorious/utils/data-structures/objects.js"
2
-
3
1
  import { createGamepad, gamepadListener, gamepadsPoller } from "./gamepad.js"
4
2
  import { createInput, input } from "./input.js"
5
3
  import { createKeyboard, keyboard } from "./keyboard.js"
6
4
 
7
- const DEFAULT_PARAMS = {
8
- name: "input0",
9
- }
10
-
11
- export function setupControls(params) {
12
- params = extend(DEFAULT_PARAMS, params)
13
-
5
+ export function controls(...targetIds) {
14
6
  return {
15
- types: {
16
- keyboard: [keyboard(params)],
17
- gamepads_poller: [gamepadsPoller(params)],
18
- gamepad_listener: [gamepadListener(params)],
19
- input: [input(params)],
20
- },
21
- entities: {
22
- gamepads: { type: "gamepads_poller" },
23
- },
7
+ keyboard: [keyboard()],
8
+ gamepads_poller: [gamepadsPoller(targetIds)],
9
+ gamepad_listener: [gamepadListener()],
10
+ input: [input()],
24
11
  }
25
12
  }
26
13
 
27
- export function controlsEntities(
28
- name = DEFAULT_PARAMS.name,
29
- targetIds,
30
- mapping = {},
31
- ) {
14
+ export function createControls(targetId, mapping = {}) {
32
15
  return {
33
- [`keyboard_${name}`]: createKeyboard(`keyboard_${name}`, name, mapping),
34
- [`gamepad_${name}`]: createGamepad(`gamepad_${name}`, name, mapping),
35
- [name]: createInput(name, targetIds, mapping),
16
+ gamepads: { type: "gamepads_poller" },
17
+ [`keyboard_${targetId}`]: createKeyboard(targetId, mapping),
18
+ [`gamepad_${targetId}`]: createGamepad(targetId, mapping),
19
+ [`input_${targetId}`]: createInput(targetId, mapping),
36
20
  }
37
21
  }
@@ -1,8 +1,4 @@
1
- const DEFAULT_PARAMS = {
2
- name: "gamepad_input0",
3
- }
4
-
5
- export function gamepadsPoller() {
1
+ export function gamepadsPoller(targetIds = []) {
6
2
  return {
7
3
  create(entity, entityId) {
8
4
  if (entityId !== entity.id) return
@@ -12,9 +8,7 @@ export function gamepadsPoller() {
12
8
 
13
9
  update(entity, dt, api) {
14
10
  navigator.getGamepads().forEach((gamepad) => {
15
- if (gamepad == null) {
16
- return
17
- }
11
+ if (gamepad == null) return
18
12
 
19
13
  const cache = (entity.gamepadStateCache[gamepad.index] ??= {
20
14
  axes: [],
@@ -22,12 +16,10 @@ export function gamepadsPoller() {
22
16
  })
23
17
 
24
18
  gamepad.axes.forEach((axis, index) => {
25
- if (axis === cache.axes[index]) {
26
- return
27
- }
19
+ if (axis === cache.axes[index]) return
28
20
 
29
21
  api.notify("gamepadAxis", {
30
- gamepadIndex: gamepad.index,
22
+ targetId: targetIds[gamepad.index],
31
23
  axis: `Axis${index}`,
32
24
  value: axis,
33
25
  })
@@ -40,12 +32,12 @@ export function gamepadsPoller() {
40
32
 
41
33
  if (isPressed && !wasPressed) {
42
34
  api.notify("gamepadPress", {
43
- gamepadIndex: gamepad.index,
35
+ targetId: targetIds[gamepad.index],
44
36
  button: `Btn${index}`,
45
37
  })
46
38
  } else if (!isPressed && wasPressed) {
47
39
  api.notify("gamepadRelease", {
48
- gamepadIndex: gamepad.index,
40
+ targetId: targetIds[gamepad.index],
49
41
  button: `Btn${index}`,
50
42
  })
51
43
  }
@@ -59,58 +51,42 @@ export function gamepadsPoller() {
59
51
 
60
52
  export function gamepadListener() {
61
53
  return {
62
- gamepadAxis(entity, { gamepadIndex, axis, value }, api) {
63
- if (entity.id !== `gamepad_input${gamepadIndex}`) {
64
- return
65
- }
54
+ gamepadAxis(entity, { targetId, axis, value }, api) {
55
+ if (targetId !== entity.targetId) return
66
56
 
67
57
  const action = entity.mapping[axis]
68
- if (!action) {
69
- return
70
- }
58
+ if (!action) return
71
59
 
72
60
  entity[action] = value
73
- api.notify("inputAxis", { controlId: entity.id, action, value })
61
+ api.notify("inputAxis", { targetId, action, value })
74
62
  },
75
63
 
76
- gamepadPress(entity, { gamepadIndex, button }, api) {
77
- if (entity.id !== `gamepad_input${gamepadIndex}`) {
78
- return
79
- }
64
+ gamepadPress(entity, { targetId, button }, api) {
65
+ if (targetId !== entity.targetId) return
80
66
 
81
67
  const action = entity.mapping[button]
82
- if (!action) {
83
- return
84
- }
68
+ if (!action) return
85
69
 
86
70
  if (!entity[action]) {
87
71
  entity[action] = true
88
- api.notify("inputPress", { controlId: entity.id, action })
72
+ api.notify("inputPress", { targetId, action })
89
73
  }
90
74
  },
91
75
 
92
- gamepadRelease(entity, { gamepadIndex, button }, api) {
93
- if (entity.id !== `gamepad_input${gamepadIndex}`) {
94
- return
95
- }
76
+ gamepadRelease(entity, { targetId, button }, api) {
77
+ if (targetId !== entity.targetId) return
96
78
 
97
79
  const action = entity.mapping[button]
98
- if (!action) {
99
- return
100
- }
80
+ if (!action) return
101
81
 
102
82
  if (entity[action]) {
103
83
  entity[action] = false
104
- api.notify("inputRelease", { controlId: entity.id, action })
84
+ api.notify("inputRelease", { targetId, action })
105
85
  }
106
86
  },
107
87
  }
108
88
  }
109
89
 
110
- export function createGamepad(
111
- name = DEFAULT_PARAMS.name,
112
- targetInput,
113
- mapping = {},
114
- ) {
115
- return { id: name, type: "gamepad_listener", targetInput, mapping }
90
+ export function createGamepad(targetId, mapping = {}) {
91
+ return { type: "gamepad_listener", targetId, mapping }
116
92
  }
@@ -1,48 +1,31 @@
1
- const DEFAULT_PARAMS = {
2
- name: "input0",
3
- }
4
-
5
1
  export function input() {
6
2
  return {
7
- inputAxis(entity, { controlId, action, value }, api) {
8
- if (!controlId.endsWith(entity.id)) {
9
- return
10
- }
3
+ inputAxis(entity, { targetId, action, value }, api) {
4
+ if (targetId !== entity.targetId) return
5
+
11
6
  entity[action] = value
12
7
 
13
- entity.targetIds.forEach((targetId) => {
14
- api.notify(action, { entityId: targetId, value })
15
- })
8
+ api.notify(action, { entityId: entity.targetId, value })
16
9
  },
17
10
 
18
- inputPress(entity, { controlId, action }, api) {
19
- if (!controlId.endsWith(entity.id)) {
20
- return
21
- }
11
+ inputPress(entity, { targetId, action }, api) {
12
+ if (targetId !== entity.targetId) return
13
+
22
14
  entity[action] = true
23
15
 
24
- entity.targetIds.forEach((targetId) => {
25
- api.notify(action, targetId)
26
- })
16
+ api.notify(action, entity.targetId)
27
17
  },
28
18
 
29
- inputRelease(entity, { controlId, action }, api) {
30
- if (!controlId.endsWith(entity.id)) {
31
- return
32
- }
19
+ inputRelease(entity, { targetId, action }, api) {
20
+ if (targetId !== entity.targetId) return
21
+
33
22
  entity[action] = false
34
23
 
35
- entity.targetIds.forEach((targetId) => {
36
- api.notify(`${action}End`, targetId)
37
- })
24
+ api.notify(`${action}End`, entity.targetId)
38
25
  },
39
26
  }
40
27
  }
41
28
 
42
- export function createInput(
43
- name = DEFAULT_PARAMS.name,
44
- targetIds = [],
45
- mapping = {},
46
- ) {
47
- return { id: name, type: "input", targetIds, mapping }
29
+ export function createInput(targetId, mapping = {}) {
30
+ return { type: "input", targetId, mapping }
48
31
  }
@@ -1,7 +1,3 @@
1
- const DEFAULT_PARAMS = {
2
- name: "keyboard0",
3
- }
4
-
5
1
  export function keyboard() {
6
2
  let handleKeyDown, handleKeyUp
7
3
  let currentDocument = null
@@ -26,36 +22,28 @@ export function keyboard() {
26
22
 
27
23
  keyboardKeyDown(entity, keyCode, api) {
28
24
  const action = entity.mapping[keyCode]
29
- if (!action) {
30
- return
31
- }
25
+ if (!action) return
32
26
 
33
27
  if (!entity[action]) {
34
28
  entity[action] = true
35
- api.notify("inputPress", { controlId: entity.id, action })
29
+ api.notify("inputPress", { targetId: entity.targetId, action })
36
30
  }
37
31
  },
38
32
 
39
33
  keyboardKeyUp(entity, keyCode, api) {
40
34
  const action = entity.mapping[keyCode]
41
- if (!action) {
42
- return
43
- }
35
+ if (!action) return
44
36
 
45
37
  if (entity[action]) {
46
38
  entity[action] = false
47
- api.notify("inputRelease", { controlId: entity.id, action })
39
+ api.notify("inputRelease", { targetId: entity.targetId, action })
48
40
  }
49
41
  },
50
42
  }
51
43
  }
52
44
 
53
- export function createKeyboard(
54
- name = DEFAULT_PARAMS.name,
55
- targetInput,
56
- mapping = {},
57
- ) {
58
- return { id: name, type: "keyboard", targetInput, mapping }
45
+ export function createKeyboard(targetId, mapping = {}) {
46
+ return { type: "keyboard", targetId, mapping }
59
47
  }
60
48
 
61
49
  function createKeyboardHandler(id, api) {
@@ -1,12 +1,10 @@
1
1
  import { findCollision } from "@inglorious/engine/collision/detection.js"
2
2
  import { clampToBounds } from "@inglorious/engine/physics/bounds.js"
3
3
  import { zero } from "@inglorious/utils/math/vector.js"
4
-
5
- const DEFAULT_PARAMS = {
6
- name: "mouse",
7
- }
4
+ import { v } from "@inglorious/utils/v.js"
8
5
 
9
6
  const NO_Y = 0
7
+ const HALF = 2
10
8
 
11
9
  export function mouse() {
12
10
  return {
@@ -37,10 +35,10 @@ export function mouse() {
37
35
  }
38
36
  }
39
37
 
40
- export function track(parent, options) {
41
- const handleMouseMove = createHandler("mouseMove", parent, options)
42
- const handleClick = createHandler("mouseClick", parent, options)
43
- const handleWheel = createHandler("mouseWheel", parent, options)
38
+ export function trackMouse(parent, api) {
39
+ const handleMouseMove = createHandler("mouseMove", parent, api)
40
+ const handleClick = createHandler("mouseClick", parent, api)
41
+ const handleWheel = createHandler("mouseWheel", parent, api)
44
42
 
45
43
  return {
46
44
  onMouseMove: handleMouseMove,
@@ -49,9 +47,8 @@ export function track(parent, options) {
49
47
  }
50
48
  }
51
49
 
52
- export function createMouse(name = DEFAULT_PARAMS.name, overrides = {}) {
50
+ export function createMouse(overrides = {}) {
53
51
  return {
54
- id: name,
55
52
  type: "mouse",
56
53
  layer: 999, // A high layer value to ensure it's always rendered on top
57
54
  position: zero(),
@@ -61,30 +58,50 @@ export function createMouse(name = DEFAULT_PARAMS.name, overrides = {}) {
61
58
 
62
59
  function createHandler(type, parent, api) {
63
60
  return (event) => {
61
+ event.preventDefault()
64
62
  event.stopPropagation()
65
63
 
66
64
  if (parent == null) {
67
65
  return
68
66
  }
69
67
 
70
- // For wheel events, the payload is different from other mouse events.
71
68
  if (type === "mouseWheel") {
72
- api.notify(type, { deltaY: event.deltaY })
69
+ api.notify(type, event.deltaY)
73
70
  return
74
71
  }
75
72
 
76
- // For move and click events, the payload is the calculated position.
77
- const payload = calculatePosition(event, parent)
73
+ const payload = calculatePosition(event, parent, api)
78
74
  api.notify(type, payload)
79
75
  }
80
76
  }
81
77
 
82
- function calculatePosition(event, parent) {
78
+ function calculatePosition(event, parent, api) {
83
79
  const { clientX, clientY } = event
84
- const { left, bottom } = parent.getBoundingClientRect()
80
+ const {
81
+ left,
82
+ bottom,
83
+ width: canvasWidth,
84
+ height: canvasHeight,
85
+ } = parent.getBoundingClientRect()
85
86
 
86
87
  const x = clientX - left
87
- const z = bottom - clientY
88
+ const y = bottom - clientY
89
+
90
+ const game = api.getEntity("game")
91
+ const [gameWidth, gameHeight] = game.size
92
+
93
+ const scaleX = canvasWidth / gameWidth
94
+ const scaleY = canvasHeight / gameHeight
95
+ const scale = Math.min(scaleX, scaleY)
96
+
97
+ const scaledGameWidth = gameWidth * scale
98
+ const scaledGameHeight = gameHeight * scale
99
+
100
+ const offsetX = (canvasWidth - scaledGameWidth) / HALF
101
+ const offsetY = (canvasHeight - scaledGameHeight) / HALF
102
+
103
+ const gameX = (x - offsetX) / scale
104
+ const gameY = (y - offsetY) / scale
88
105
 
89
- return [x, NO_Y, z]
106
+ return v(gameX, NO_Y, gameY)
90
107
  }
@@ -0,0 +1,127 @@
1
+ import { findCollision } from "@inglorious/engine/collision/detection.js"
2
+ import { clampToBounds } from "@inglorious/engine/physics/bounds.js"
3
+ import { magnitude, zero } from "@inglorious/utils/math/vector.js"
4
+ import { subtract } from "@inglorious/utils/math/vectors.js"
5
+ import { v } from "@inglorious/utils/v.js"
6
+
7
+ const NO_Y = 0
8
+ const MOVEMENT_THRESHOLD = 5
9
+ const HALF = 2
10
+
11
+ export function touch() {
12
+ return {
13
+ create(entity, entityId) {
14
+ if (entityId !== entity.id) return
15
+
16
+ entity.collisions ??= {}
17
+ entity.collisions.bounds ??= { shape: "point" }
18
+ },
19
+
20
+ touchStart(entity, position) {
21
+ entity.isSwiping = false
22
+ entity.position = position
23
+ },
24
+
25
+ touchMove(entity, position, api) {
26
+ const game = api.getEntity("game")
27
+
28
+ const delta = subtract(position, entity.position)
29
+
30
+ if (magnitude(delta) > MOVEMENT_THRESHOLD) {
31
+ entity.isSwiping = true
32
+ }
33
+
34
+ entity.position = position
35
+
36
+ clampToBounds(entity, game.size)
37
+ },
38
+
39
+ touchEnd(entity, _, api) {
40
+ if (entity.isSwiping) {
41
+ api.notify("swipe", v(...entity.position))
42
+ entity.isSwiping = false
43
+ return
44
+ }
45
+
46
+ const entities = api.getEntities()
47
+ const clickedEntity = findCollision(entity, entities)
48
+ if (clickedEntity) {
49
+ api.notify("entityTouch", clickedEntity.id)
50
+ } else {
51
+ api.notify("sceneTouch", v(...entity.position))
52
+ }
53
+ },
54
+ }
55
+ }
56
+
57
+ export function trackTouch(parent, api) {
58
+ const handleTouchStart = createHandler("touchStart", parent, api)
59
+ const handleTouchMove = createHandler("touchMove", parent, api)
60
+ const handleTouchEnd = createHandler("touchEnd", parent, api)
61
+
62
+ return {
63
+ onTouchStart: handleTouchStart,
64
+ onTouchMove: handleTouchMove,
65
+ onTouchEnd: handleTouchEnd,
66
+ }
67
+ }
68
+
69
+ export function createTouch(overrides = {}) {
70
+ return {
71
+ type: "touch",
72
+ layer: 999, // A high layer value to ensure it's always rendered on top
73
+ position: zero(),
74
+ ...overrides,
75
+ }
76
+ }
77
+
78
+ function createHandler(type, parent, api) {
79
+ return (event) => {
80
+ event.preventDefault()
81
+ event.stopPropagation()
82
+
83
+ if (parent == null) {
84
+ return
85
+ }
86
+
87
+ if (type === "touchEnd") {
88
+ api.notify(type)
89
+ return
90
+ }
91
+
92
+ const payload = calculatePosition(event, parent, api)
93
+ api.notify(type, payload)
94
+ }
95
+ }
96
+
97
+ function calculatePosition(event, parent, api) {
98
+ const [touch] = event.touches
99
+ const { clientX, clientY } = touch
100
+ const {
101
+ left,
102
+ bottom,
103
+ width: canvasWidth,
104
+ height: canvasHeight,
105
+ } = parent.getBoundingClientRect()
106
+
107
+ const x = clientX - left
108
+ const y = bottom - clientY
109
+
110
+ const game = api.getEntity("game")
111
+ const [gameWidth, gameHeight] = game.size
112
+
113
+ const scaleX = canvasWidth / gameWidth
114
+ const scaleY = canvasHeight / gameHeight
115
+ const scale = Math.min(scaleX, scaleY)
116
+
117
+ const scaledGameWidth = gameWidth * scale
118
+ const scaledGameHeight = gameHeight * scale
119
+
120
+ const offsetX = (canvasWidth - scaledGameWidth) / HALF
121
+ const offsetY = (canvasHeight - scaledGameHeight) / HALF
122
+
123
+ const gameX = (x - offsetX) / scale
124
+ const gameY = (y - offsetY) / scale
125
+
126
+ return v(gameX, NO_Y, gameY)
127
+ }
@@ -12,5 +12,8 @@ export const coreEvents = [
12
12
  "inputRelease",
13
13
  "mouseMove",
14
14
  "mouseClick",
15
+ "touchStart",
16
+ "touchMove",
17
+ "touchEnd",
15
18
  "spriteAnimationEnd",
16
19
  ]
@@ -1,20 +0,0 @@
1
- import { extend } from "@inglorious/utils/data-structures/objects.js"
2
-
3
- const DEFAULT_PARAMS = {
4
- onState: "default",
5
- }
6
-
7
- export function collidable(params) {
8
- params = extend(DEFAULT_PARAMS, params)
9
-
10
- return (type) =>
11
- extend(type, {
12
- states: {
13
- [params.onState]: {
14
- update(entity, dt, api) {
15
- type.states?.[params.onState].update?.(entity, dt, api)
16
- },
17
- },
18
- },
19
- })
20
- }