@inglorious/engine 0.3.0 → 0.4.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 +1 -0
- package/package.json +2 -3
- package/src/behaviors/fsm.test.js +1 -1
- package/src/core/engine.js +2 -2
- package/src/core/api.js +0 -34
- package/src/core/select.js +0 -26
- package/src/core/store.js +0 -178
- package/src/core/store.test.js +0 -110
package/README.md
CHANGED
|
@@ -1,5 +1,6 @@
|
|
|
1
1
|
# Inglorious Engine
|
|
2
2
|
|
|
3
|
+
[](https://www.npmjs.com/package/@inglorious/engine)
|
|
3
4
|
[](https://opensource.org/licenses/MIT)
|
|
4
5
|
|
|
5
6
|
A JavaScript game engine written with global state, immutability, and pure functions in mind. Have fun(ctional programming) with it!
|
package/package.json
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "@inglorious/engine",
|
|
3
|
-
"version": "0.
|
|
3
|
+
"version": "0.4.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,11 +32,10 @@
|
|
|
32
32
|
"access": "public"
|
|
33
33
|
},
|
|
34
34
|
"dependencies": {
|
|
35
|
-
"
|
|
35
|
+
"@inglorious/store": "1.0.0",
|
|
36
36
|
"@inglorious/utils": "1.1.0"
|
|
37
37
|
},
|
|
38
38
|
"devDependencies": {
|
|
39
|
-
"husky": "^9.1.7",
|
|
40
39
|
"prettier": "^3.5.3",
|
|
41
40
|
"vite": "^7.1.3",
|
|
42
41
|
"vitest": "^1.6.0"
|
package/src/core/engine.js
CHANGED
|
@@ -1,7 +1,8 @@
|
|
|
1
1
|
import { game } from "@inglorious/engine/behaviors/game.js"
|
|
2
|
+
import { createApi } from "@inglorious/store/api.js"
|
|
3
|
+
import { createStore } from "@inglorious/store/store.js"
|
|
2
4
|
import { extend } from "@inglorious/utils/data-structures/objects.js"
|
|
3
5
|
|
|
4
|
-
import { createApi } from "./api.js"
|
|
5
6
|
import {
|
|
6
7
|
ACTION_BLACKLIST,
|
|
7
8
|
disconnectDevTools,
|
|
@@ -9,7 +10,6 @@ import {
|
|
|
9
10
|
sendAction,
|
|
10
11
|
} from "./dev-tools.js"
|
|
11
12
|
import Loop from "./loop.js"
|
|
12
|
-
import { createStore } from "./store.js"
|
|
13
13
|
|
|
14
14
|
// Default game configuration
|
|
15
15
|
// loop.type specifies the type of loop to use (defaults to "animationFrame").
|
package/src/core/api.js
DELETED
|
@@ -1,34 +0,0 @@
|
|
|
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
|
-
}
|
package/src/core/select.js
DELETED
|
@@ -1,26 +0,0 @@
|
|
|
1
|
-
/**
|
|
2
|
-
* Creates a memoized selector function.
|
|
3
|
-
* NB: this implementation does not support spreading the input selectors for clarity, please just put them in an array.
|
|
4
|
-
* @param {Array<Function>} inputSelectors - An array of input selector functions.
|
|
5
|
-
* @param {Function} resultFunc - A function that receives the results of the input selectors and returns a computed value.
|
|
6
|
-
* @returns {Function} A memoized selector function that, when called, returns the selected state.
|
|
7
|
-
*/
|
|
8
|
-
export function createSelector(inputSelectors, resultFunc) {
|
|
9
|
-
let lastInputs = []
|
|
10
|
-
let lastResult = null
|
|
11
|
-
|
|
12
|
-
return (state) => {
|
|
13
|
-
const nextInputs = inputSelectors.map((selector) => selector(state))
|
|
14
|
-
const inputsChanged =
|
|
15
|
-
lastInputs.length !== nextInputs.length ||
|
|
16
|
-
nextInputs.some((input, index) => input !== lastInputs[index])
|
|
17
|
-
|
|
18
|
-
if (!inputsChanged) {
|
|
19
|
-
return lastResult
|
|
20
|
-
}
|
|
21
|
-
|
|
22
|
-
lastInputs = nextInputs
|
|
23
|
-
lastResult = resultFunc(...nextInputs)
|
|
24
|
-
return lastResult
|
|
25
|
-
}
|
|
26
|
-
}
|
package/src/core/store.js
DELETED
|
@@ -1,178 +0,0 @@
|
|
|
1
|
-
import { map } from "@inglorious/utils/data-structures/object.js"
|
|
2
|
-
import { extend } from "@inglorious/utils/data-structures/objects.js"
|
|
3
|
-
import { pipe } from "@inglorious/utils/functions/functions.js"
|
|
4
|
-
import { produce } from "immer"
|
|
5
|
-
|
|
6
|
-
/**
|
|
7
|
-
* Creates a store to manage state and events.
|
|
8
|
-
* @param {Object} config - Configuration options for the store.
|
|
9
|
-
* @param {Object} [config.types] - The initial types configuration.
|
|
10
|
-
* @param {Object} [config.entities] - The initial entities configuration.
|
|
11
|
-
* @returns {Object} The store with methods to interact with state and events.
|
|
12
|
-
*/
|
|
13
|
-
export function createStore({
|
|
14
|
-
types: originalTypes,
|
|
15
|
-
entities: originalEntities,
|
|
16
|
-
systems = [],
|
|
17
|
-
}) {
|
|
18
|
-
const listeners = new Set()
|
|
19
|
-
let incomingEvents = []
|
|
20
|
-
|
|
21
|
-
let types = augmentTypes(originalTypes)
|
|
22
|
-
let entities = augmentEntities(originalEntities)
|
|
23
|
-
|
|
24
|
-
const initialState = { entities }
|
|
25
|
-
let state = initialState
|
|
26
|
-
|
|
27
|
-
return {
|
|
28
|
-
subscribe,
|
|
29
|
-
update,
|
|
30
|
-
notify,
|
|
31
|
-
dispatch, // needed for compatibility with Redux
|
|
32
|
-
getTypes,
|
|
33
|
-
getOriginalTypes,
|
|
34
|
-
getState,
|
|
35
|
-
setState,
|
|
36
|
-
reset,
|
|
37
|
-
}
|
|
38
|
-
|
|
39
|
-
/**
|
|
40
|
-
* Subscribes a listener to state updates.
|
|
41
|
-
* @param {Function} listener - The listener function to call on updates.
|
|
42
|
-
* @returns {Function} A function to unsubscribe the listener.
|
|
43
|
-
*/
|
|
44
|
-
function subscribe(listener) {
|
|
45
|
-
listeners.add(listener)
|
|
46
|
-
|
|
47
|
-
return function unsubscribe() {
|
|
48
|
-
listeners.delete(listener)
|
|
49
|
-
}
|
|
50
|
-
}
|
|
51
|
-
|
|
52
|
-
/**
|
|
53
|
-
* Updates the state based on elapsed time and processes events.
|
|
54
|
-
* @param {number} dt - The delta time since the last update in milliseconds.
|
|
55
|
-
* @param {Object} api - The engine's public API.
|
|
56
|
-
*/
|
|
57
|
-
function update(dt, api) {
|
|
58
|
-
const processedEvents = []
|
|
59
|
-
|
|
60
|
-
state = produce(state, (state) => {
|
|
61
|
-
incomingEvents.push({ type: "update", payload: dt })
|
|
62
|
-
|
|
63
|
-
while (incomingEvents.length) {
|
|
64
|
-
const event = incomingEvents.shift()
|
|
65
|
-
processedEvents.push(event)
|
|
66
|
-
|
|
67
|
-
if (event.type === "morph") {
|
|
68
|
-
const { id, type } = event.payload
|
|
69
|
-
originalTypes[id] = type
|
|
70
|
-
types = augmentTypes(originalTypes)
|
|
71
|
-
}
|
|
72
|
-
|
|
73
|
-
if (event.type === "add") {
|
|
74
|
-
const { id, ...entity } = event.payload
|
|
75
|
-
state.entities[id] = augmentEntity(id, entity)
|
|
76
|
-
}
|
|
77
|
-
|
|
78
|
-
if (event.type === "remove") {
|
|
79
|
-
const id = event.payload
|
|
80
|
-
delete state.entities[id]
|
|
81
|
-
}
|
|
82
|
-
|
|
83
|
-
for (const id in state.entities) {
|
|
84
|
-
const entity = state.entities[id]
|
|
85
|
-
const type = types[entity.type]
|
|
86
|
-
const handle = type[event.type]
|
|
87
|
-
handle?.(entity, event.payload, api)
|
|
88
|
-
}
|
|
89
|
-
|
|
90
|
-
systems.forEach((system) => {
|
|
91
|
-
const handle = system[event.type]
|
|
92
|
-
handle?.(state, event.payload, api)
|
|
93
|
-
})
|
|
94
|
-
}
|
|
95
|
-
})
|
|
96
|
-
|
|
97
|
-
listeners.forEach((onUpdate) => onUpdate())
|
|
98
|
-
|
|
99
|
-
return processedEvents
|
|
100
|
-
}
|
|
101
|
-
|
|
102
|
-
/**
|
|
103
|
-
* Notifies the store of a new event.
|
|
104
|
-
* @param {string} type - The event object type to notify.
|
|
105
|
-
* @param {any} payload - The event object payload to notify.
|
|
106
|
-
*/
|
|
107
|
-
function notify(type, payload) {
|
|
108
|
-
dispatch({ type, payload })
|
|
109
|
-
}
|
|
110
|
-
|
|
111
|
-
/**
|
|
112
|
-
* Dispatches an event to be processed in the next update cycle.
|
|
113
|
-
* @param {Object} event - The event object.
|
|
114
|
-
* @param {string} event.type - The type of the event.
|
|
115
|
-
* @param {any} [event.payload] - The payload of the event.
|
|
116
|
-
*/
|
|
117
|
-
function dispatch(event) {
|
|
118
|
-
incomingEvents.push(event)
|
|
119
|
-
}
|
|
120
|
-
|
|
121
|
-
/**
|
|
122
|
-
* Retrieves the augmented types configuration.
|
|
123
|
-
* This includes composed behaviors and event handlers wrapped for immutability.
|
|
124
|
-
* @returns {Object} The augmented types configuration.
|
|
125
|
-
*/
|
|
126
|
-
function getTypes() {
|
|
127
|
-
return types
|
|
128
|
-
}
|
|
129
|
-
|
|
130
|
-
/**
|
|
131
|
-
* Retrieves the original, un-augmented types configuration.
|
|
132
|
-
* @returns {Object} The original types configuration.
|
|
133
|
-
*/
|
|
134
|
-
function getOriginalTypes() {
|
|
135
|
-
return originalTypes
|
|
136
|
-
}
|
|
137
|
-
|
|
138
|
-
/**
|
|
139
|
-
* Retrieves the current state.
|
|
140
|
-
* @returns {Object} The current state.
|
|
141
|
-
*/
|
|
142
|
-
function getState() {
|
|
143
|
-
return state
|
|
144
|
-
}
|
|
145
|
-
|
|
146
|
-
function setState(newState) {
|
|
147
|
-
state = newState
|
|
148
|
-
}
|
|
149
|
-
|
|
150
|
-
function reset() {
|
|
151
|
-
state = initialState // Reset state to its originally computed value
|
|
152
|
-
}
|
|
153
|
-
}
|
|
154
|
-
|
|
155
|
-
function augmentTypes(types) {
|
|
156
|
-
return pipe(applyBehaviors)(types)
|
|
157
|
-
}
|
|
158
|
-
|
|
159
|
-
function applyBehaviors(types) {
|
|
160
|
-
return map(types, (_, type) => {
|
|
161
|
-
if (!Array.isArray(type)) {
|
|
162
|
-
return type
|
|
163
|
-
}
|
|
164
|
-
|
|
165
|
-
const behaviors = type.map((fn) =>
|
|
166
|
-
typeof fn !== "function" ? (type) => extend(type, fn) : fn,
|
|
167
|
-
)
|
|
168
|
-
return pipe(...behaviors)({})
|
|
169
|
-
})
|
|
170
|
-
}
|
|
171
|
-
|
|
172
|
-
function augmentEntities(entities) {
|
|
173
|
-
return map(entities, augmentEntity)
|
|
174
|
-
}
|
|
175
|
-
|
|
176
|
-
function augmentEntity(id, entity) {
|
|
177
|
-
return { ...entity, id }
|
|
178
|
-
}
|
package/src/core/store.test.js
DELETED
|
@@ -1,110 +0,0 @@
|
|
|
1
|
-
import { expect, test } from "vitest"
|
|
2
|
-
|
|
3
|
-
import { createStore } from "./store.js"
|
|
4
|
-
|
|
5
|
-
test("it should process events by mutating state inside handlers", () => {
|
|
6
|
-
const config = {
|
|
7
|
-
types: {
|
|
8
|
-
kitty: {
|
|
9
|
-
feed(entity) {
|
|
10
|
-
entity.isFed = true
|
|
11
|
-
},
|
|
12
|
-
update(entity) {
|
|
13
|
-
entity.isMeowing = true
|
|
14
|
-
},
|
|
15
|
-
},
|
|
16
|
-
},
|
|
17
|
-
entities: {
|
|
18
|
-
kitty1: { type: "kitty" },
|
|
19
|
-
},
|
|
20
|
-
}
|
|
21
|
-
const afterState = {
|
|
22
|
-
entities: {
|
|
23
|
-
kitty1: {
|
|
24
|
-
id: "kitty1",
|
|
25
|
-
type: "kitty",
|
|
26
|
-
isFed: true,
|
|
27
|
-
isMeowing: true,
|
|
28
|
-
},
|
|
29
|
-
},
|
|
30
|
-
}
|
|
31
|
-
|
|
32
|
-
const store = createStore(config)
|
|
33
|
-
store.notify("feed")
|
|
34
|
-
store.update(0, {})
|
|
35
|
-
|
|
36
|
-
const state = store.getState()
|
|
37
|
-
expect(state).toStrictEqual(afterState)
|
|
38
|
-
})
|
|
39
|
-
|
|
40
|
-
test("it should send an event from an entity and process it in the same update cycle", () => {
|
|
41
|
-
const config = {
|
|
42
|
-
types: {
|
|
43
|
-
doggo: {
|
|
44
|
-
update(entity, dt, api) {
|
|
45
|
-
api.notify("bark")
|
|
46
|
-
},
|
|
47
|
-
},
|
|
48
|
-
kitty: {
|
|
49
|
-
bark(entity) {
|
|
50
|
-
entity.position = "far"
|
|
51
|
-
},
|
|
52
|
-
},
|
|
53
|
-
},
|
|
54
|
-
entities: {
|
|
55
|
-
doggo1: { type: "doggo" },
|
|
56
|
-
kitty1: { type: "kitty", position: "near" },
|
|
57
|
-
},
|
|
58
|
-
}
|
|
59
|
-
const afterState = {
|
|
60
|
-
entities: {
|
|
61
|
-
doggo1: { id: "doggo1", type: "doggo" },
|
|
62
|
-
kitty1: { id: "kitty1", type: "kitty", position: "far" },
|
|
63
|
-
},
|
|
64
|
-
}
|
|
65
|
-
|
|
66
|
-
const store = createStore(config)
|
|
67
|
-
const api = { notify: store.notify }
|
|
68
|
-
store.update(0, api)
|
|
69
|
-
|
|
70
|
-
const state = store.getState()
|
|
71
|
-
expect(state).toStrictEqual(afterState)
|
|
72
|
-
})
|
|
73
|
-
|
|
74
|
-
test("it should add an entity via an 'add' event", () => {
|
|
75
|
-
const config = {
|
|
76
|
-
types: {
|
|
77
|
-
kitty: {},
|
|
78
|
-
},
|
|
79
|
-
entities: {},
|
|
80
|
-
}
|
|
81
|
-
const newEntity = { id: "kitty1", type: "kitty" }
|
|
82
|
-
|
|
83
|
-
const store = createStore(config)
|
|
84
|
-
store.notify("add", newEntity)
|
|
85
|
-
store.update(0, {})
|
|
86
|
-
|
|
87
|
-
const state = store.getState()
|
|
88
|
-
expect(state).toStrictEqual({
|
|
89
|
-
entities: {
|
|
90
|
-
kitty1: { id: "kitty1", type: "kitty" },
|
|
91
|
-
},
|
|
92
|
-
})
|
|
93
|
-
})
|
|
94
|
-
|
|
95
|
-
test("it should remove an entity via a 'remove' event", () => {
|
|
96
|
-
const config = {
|
|
97
|
-
types: {},
|
|
98
|
-
entities: {
|
|
99
|
-
kitty1: { type: "kitty" },
|
|
100
|
-
},
|
|
101
|
-
}
|
|
102
|
-
const store = createStore(config)
|
|
103
|
-
|
|
104
|
-
store.notify("remove", "kitty1")
|
|
105
|
-
|
|
106
|
-
store.update(0, {})
|
|
107
|
-
|
|
108
|
-
const state = store.getState()
|
|
109
|
-
expect(state.entities.kitty1).toBeUndefined()
|
|
110
|
-
})
|