@inglorious/engine 3.0.1 → 5.0.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
|
@@ -100,6 +100,14 @@ Here is a complete example showing how to set up and run a game using the engine
|
|
|
100
100
|
|
|
101
101
|
---
|
|
102
102
|
|
|
103
|
+
## License
|
|
104
|
+
|
|
105
|
+
MIT © [Matteo Antony Mistretta](https://github.com/IngloriousCoderz)
|
|
106
|
+
|
|
107
|
+
This is free and open-source software. Use it however you want!
|
|
108
|
+
|
|
109
|
+
---
|
|
110
|
+
|
|
103
111
|
## Contributing
|
|
104
112
|
|
|
105
113
|
We welcome contributions from the community\! Whether you're fixing a bug, adding a feature, or improving the documentation, your help is appreciated. Please read our [Contributing Guidelines](https://github.com/IngloriousCoderz/inglorious-engine/blob/main/CONTRIBUTING.md) for details on how to get started.
|
package/package.json
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "@inglorious/engine",
|
|
3
|
-
"version": "
|
|
3
|
+
"version": "5.0.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",
|
|
@@ -30,12 +30,12 @@
|
|
|
30
30
|
"access": "public"
|
|
31
31
|
},
|
|
32
32
|
"dependencies": {
|
|
33
|
-
"@inglorious/store": "5.0
|
|
34
|
-
"@inglorious/utils": "3.6.
|
|
33
|
+
"@inglorious/store": "5.2.0",
|
|
34
|
+
"@inglorious/utils": "3.6.1"
|
|
35
35
|
},
|
|
36
36
|
"peerDependencies": {
|
|
37
|
-
"@inglorious/store": "5.0
|
|
38
|
-
"@inglorious/utils": "3.6.
|
|
37
|
+
"@inglorious/store": "5.2.0",
|
|
38
|
+
"@inglorious/utils": "3.6.1"
|
|
39
39
|
},
|
|
40
40
|
"devDependencies": {
|
|
41
41
|
"prettier": "^3.5.3",
|
|
@@ -29,13 +29,11 @@ test("it should add a finite state machine", () => {
|
|
|
29
29
|
},
|
|
30
30
|
}
|
|
31
31
|
const afterState = {
|
|
32
|
-
|
|
33
|
-
|
|
34
|
-
|
|
35
|
-
|
|
36
|
-
|
|
37
|
-
treats: 1,
|
|
38
|
-
},
|
|
32
|
+
entity1: {
|
|
33
|
+
id: "entity1",
|
|
34
|
+
type: "kitty",
|
|
35
|
+
state: "meowing",
|
|
36
|
+
treats: 1,
|
|
39
37
|
},
|
|
40
38
|
}
|
|
41
39
|
|
package/src/core/engine.js
CHANGED
|
@@ -1,6 +1,12 @@
|
|
|
1
1
|
import { audio } from "@inglorious/engine/behaviors/audio.js"
|
|
2
2
|
import { game } from "@inglorious/engine/behaviors/game.js"
|
|
3
3
|
import { images } from "@inglorious/engine/behaviors/images.js"
|
|
4
|
+
import {
|
|
5
|
+
connectDevTools,
|
|
6
|
+
disconnectDevTools,
|
|
7
|
+
sendAction,
|
|
8
|
+
} from "@inglorious/store/client/dev-tools.js"
|
|
9
|
+
import { multiplayerMiddleware } from "@inglorious/store/client/multiplayer-middleware.js"
|
|
4
10
|
import { createStore } from "@inglorious/store/store.js"
|
|
5
11
|
import { augmentType } from "@inglorious/store/types.js"
|
|
6
12
|
import { isArray } from "@inglorious/utils/data-structures/array.js"
|
|
@@ -9,10 +15,8 @@ import { isVector } from "@inglorious/utils/math/vector.js"
|
|
|
9
15
|
import { v } from "@inglorious/utils/v.js"
|
|
10
16
|
|
|
11
17
|
import { coreEvents } from "./core-events.js"
|
|
12
|
-
import { disconnectDevTools, initDevTools, sendAction } from "./dev-tools.js"
|
|
13
18
|
import { Loop } from "./loops/index.js"
|
|
14
19
|
import { entityPoolMiddleware } from "./middlewares/entity-pool/entity-pool-middleware.js"
|
|
15
|
-
import { multiplayerMiddleware } from "./middlewares/multiplayer-middleware.js"
|
|
16
20
|
|
|
17
21
|
// Default game configuration
|
|
18
22
|
// loop.type specifies the type of loop to use (defaults to "animationFrame").
|
|
@@ -59,7 +63,9 @@ export class Engine {
|
|
|
59
63
|
// Add multiplayer middleware if needed
|
|
60
64
|
const multiplayer = this._config.entities.game?.multiplayer
|
|
61
65
|
if (multiplayer) {
|
|
62
|
-
middlewares.push(
|
|
66
|
+
middlewares.push(
|
|
67
|
+
multiplayerMiddleware({ ...multiplayer, skippedEvents: coreEvents }),
|
|
68
|
+
)
|
|
63
69
|
}
|
|
64
70
|
|
|
65
71
|
this._store = createStore({ ...this._config, middlewares })
|
|
@@ -67,7 +73,7 @@ export class Engine {
|
|
|
67
73
|
this._loop = new Loop[this._config.loop.type]()
|
|
68
74
|
|
|
69
75
|
if (this._devMode) {
|
|
70
|
-
|
|
76
|
+
connectDevTools(this._store, { skippedEvents: coreEvents })
|
|
71
77
|
}
|
|
72
78
|
}
|
|
73
79
|
|
|
@@ -105,13 +111,13 @@ export class Engine {
|
|
|
105
111
|
update(dt) {
|
|
106
112
|
this._api.notify("update", dt)
|
|
107
113
|
const processedEvents = this._store.update()
|
|
108
|
-
const
|
|
114
|
+
const entities = this._store.getState()
|
|
109
115
|
|
|
110
116
|
// Check for devMode changes and connect/disconnect dev tools accordingly.
|
|
111
|
-
const newDevMode =
|
|
117
|
+
const newDevMode = entities.game?.devMode
|
|
112
118
|
if (newDevMode !== this._devMode) {
|
|
113
119
|
if (newDevMode) {
|
|
114
|
-
|
|
120
|
+
connectDevTools(this._store, { skippedEvents: coreEvents })
|
|
115
121
|
} else {
|
|
116
122
|
disconnectDevTools()
|
|
117
123
|
}
|
|
@@ -127,7 +133,7 @@ export class Engine {
|
|
|
127
133
|
type: eventsToLog.map(({ type }) => type).join("|"),
|
|
128
134
|
payload: eventsToLog,
|
|
129
135
|
}
|
|
130
|
-
sendAction(action,
|
|
136
|
+
sendAction(action, entities)
|
|
131
137
|
}
|
|
132
138
|
}
|
|
133
139
|
}
|
|
@@ -5,16 +5,16 @@ import { Sprite } from "@inglorious/engine/animation/sprite.js"
|
|
|
5
5
|
* with an active animation state.
|
|
6
6
|
*
|
|
7
7
|
* @param {Object} state - The game state, containing all entities.
|
|
8
|
-
* @param {Object<string, Object>}
|
|
8
|
+
* @param {Object<string, Object>} entities - A map of entity IDs to entity objects.
|
|
9
9
|
* @param {number} dt - The delta time since the last update.
|
|
10
10
|
* @param {Object} api - The game API.
|
|
11
11
|
* @param {Function} api.notify - A function to notify about events.
|
|
12
12
|
*/
|
|
13
13
|
export function spriteAnimationSystem() {
|
|
14
14
|
return {
|
|
15
|
-
update(
|
|
16
|
-
for (const id in
|
|
17
|
-
const entity =
|
|
15
|
+
update(entities, dt, api) {
|
|
16
|
+
for (const id in entities) {
|
|
17
|
+
const entity = entities[id]
|
|
18
18
|
|
|
19
19
|
if (!entity.sprite || !entity.sprite.state) {
|
|
20
20
|
continue
|
package/src/core/dev-tools.js
DELETED
|
@@ -1,122 +0,0 @@
|
|
|
1
|
-
import { coreEvents } from "./core-events.js"
|
|
2
|
-
|
|
3
|
-
const LAST_STATE = 1
|
|
4
|
-
|
|
5
|
-
let devToolsInstance = null
|
|
6
|
-
let unsubscribe = null
|
|
7
|
-
|
|
8
|
-
export function initDevTools(store) {
|
|
9
|
-
// Prevent multiple connections
|
|
10
|
-
if (devToolsInstance) {
|
|
11
|
-
return
|
|
12
|
-
}
|
|
13
|
-
|
|
14
|
-
if (typeof window === "undefined" || !window.__REDUX_DEVTOOLS_EXTENSION__) {
|
|
15
|
-
return
|
|
16
|
-
}
|
|
17
|
-
|
|
18
|
-
devToolsInstance = window.__REDUX_DEVTOOLS_EXTENSION__.connect({
|
|
19
|
-
name: "Inglorious Engine",
|
|
20
|
-
predicate: (state, action) => !coreEvents.includes(action.type),
|
|
21
|
-
actionCreators: {
|
|
22
|
-
jump: () => ({ type: "jump", payload: { inputId: "input0" } }),
|
|
23
|
-
},
|
|
24
|
-
// @see https://github.com/reduxjs/redux-devtools/blob/main/extension/docs/API/Arguments.md#features
|
|
25
|
-
features: {
|
|
26
|
-
pause: true, // start/pause recording of dispatched actions
|
|
27
|
-
lock: true, // lock/unlock dispatching actions and side effects
|
|
28
|
-
persist: true, // persist states on page reloading
|
|
29
|
-
export: true, // export history of actions in a file
|
|
30
|
-
import: "custom", // import history of actions from a file
|
|
31
|
-
jump: false, // jump back and forth (time travelling)
|
|
32
|
-
skip: false, // skip (cancel) actions
|
|
33
|
-
reorder: false, // drag and drop actions in the history list
|
|
34
|
-
dispatch: true, // dispatch custom actions or action creators
|
|
35
|
-
test: false, // generate tests for the selected actions
|
|
36
|
-
},
|
|
37
|
-
})
|
|
38
|
-
|
|
39
|
-
unsubscribe = devToolsInstance.subscribe((message) => {
|
|
40
|
-
switch (message.type) {
|
|
41
|
-
case "DISPATCH":
|
|
42
|
-
handleDispatch(message, store)
|
|
43
|
-
break
|
|
44
|
-
|
|
45
|
-
case "ACTION":
|
|
46
|
-
handleAction(message, store)
|
|
47
|
-
break
|
|
48
|
-
}
|
|
49
|
-
})
|
|
50
|
-
|
|
51
|
-
devToolsInstance.init(store.getState())
|
|
52
|
-
}
|
|
53
|
-
|
|
54
|
-
export function disconnectDevTools() {
|
|
55
|
-
// The `disconnect` method on the devToolsInstance is not available in all
|
|
56
|
-
// environments or versions of the extension.
|
|
57
|
-
// The safest way to "disconnect" is to unsubscribe from any listeners
|
|
58
|
-
// and release our reference to the instance, which prevents any further
|
|
59
|
-
// actions from being sent.
|
|
60
|
-
if (unsubscribe) {
|
|
61
|
-
unsubscribe()
|
|
62
|
-
unsubscribe = null
|
|
63
|
-
devToolsInstance = null
|
|
64
|
-
}
|
|
65
|
-
}
|
|
66
|
-
|
|
67
|
-
export function sendAction(action, state) {
|
|
68
|
-
if (devToolsInstance) {
|
|
69
|
-
devToolsInstance.send(action, state)
|
|
70
|
-
}
|
|
71
|
-
}
|
|
72
|
-
|
|
73
|
-
function handleDispatch(message, store) {
|
|
74
|
-
switch (message.payload.type) {
|
|
75
|
-
// reset button
|
|
76
|
-
case "RESET": {
|
|
77
|
-
store.reset()
|
|
78
|
-
devToolsInstance.init(store.getState())
|
|
79
|
-
break
|
|
80
|
-
}
|
|
81
|
-
|
|
82
|
-
// revert button
|
|
83
|
-
case "ROLLBACK": {
|
|
84
|
-
const newState = JSON.parse(message.state)
|
|
85
|
-
store.setState(newState)
|
|
86
|
-
break
|
|
87
|
-
}
|
|
88
|
-
|
|
89
|
-
// commit button
|
|
90
|
-
case "COMMIT": {
|
|
91
|
-
devToolsInstance.init(store.getState())
|
|
92
|
-
break
|
|
93
|
-
}
|
|
94
|
-
|
|
95
|
-
// import from file button
|
|
96
|
-
case "IMPORT_STATE": {
|
|
97
|
-
const { computedStates, actionsById } = message.payload.nextLiftedState
|
|
98
|
-
|
|
99
|
-
const [firstComputedState] = computedStates
|
|
100
|
-
const lastComputedState =
|
|
101
|
-
computedStates[computedStates.length - LAST_STATE]
|
|
102
|
-
if (lastComputedState) {
|
|
103
|
-
store.setState(lastComputedState.state)
|
|
104
|
-
}
|
|
105
|
-
|
|
106
|
-
const flattenedActions = Object.values(actionsById)
|
|
107
|
-
.flatMap(({ action }) => action.payload ?? action)
|
|
108
|
-
.map((action, index) => [index, action])
|
|
109
|
-
|
|
110
|
-
devToolsInstance.init(
|
|
111
|
-
firstComputedState.state,
|
|
112
|
-
Object.fromEntries(flattenedActions),
|
|
113
|
-
)
|
|
114
|
-
break
|
|
115
|
-
}
|
|
116
|
-
}
|
|
117
|
-
}
|
|
118
|
-
|
|
119
|
-
function handleAction(message, store) {
|
|
120
|
-
const action = JSON.parse(message.payload)
|
|
121
|
-
store.dispatch(action)
|
|
122
|
-
}
|
|
@@ -1,95 +0,0 @@
|
|
|
1
|
-
import {
|
|
2
|
-
deserialize,
|
|
3
|
-
serialize,
|
|
4
|
-
} from "@inglorious/utils/data-structures/object.js"
|
|
5
|
-
import { extend } from "@inglorious/utils/data-structures/objects.js"
|
|
6
|
-
|
|
7
|
-
import { coreEvents } from "../core-events.js"
|
|
8
|
-
|
|
9
|
-
// A constant for the server's WebSocket URL.
|
|
10
|
-
const DEFAULT_SERVER_URL = `ws://${window.location.hostname}:3000`
|
|
11
|
-
const DEFAULT_RECONNECTION_DELAY = 1000
|
|
12
|
-
|
|
13
|
-
/**
|
|
14
|
-
* Creates and returns the multiplayer middleware.
|
|
15
|
-
* @returns {Function} The middleware function.
|
|
16
|
-
*/
|
|
17
|
-
export function multiplayerMiddleware(config = {}) {
|
|
18
|
-
const serverUrl = config.serverUrl ?? DEFAULT_SERVER_URL
|
|
19
|
-
const reconnectionDelay =
|
|
20
|
-
config.reconnectionDelay ?? DEFAULT_RECONNECTION_DELAY
|
|
21
|
-
|
|
22
|
-
let ws = null
|
|
23
|
-
const localQueue = []
|
|
24
|
-
|
|
25
|
-
// The middleware function that will be applied to the store.
|
|
26
|
-
return (store) => (next) => (event) => {
|
|
27
|
-
if (coreEvents.includes(event.type)) {
|
|
28
|
-
return next(event)
|
|
29
|
-
}
|
|
30
|
-
|
|
31
|
-
// Establish the connection on the first event.
|
|
32
|
-
if (!ws) {
|
|
33
|
-
establishConnection(store)
|
|
34
|
-
}
|
|
35
|
-
|
|
36
|
-
// Only send the event to the server if it didn't come from the server.
|
|
37
|
-
if (!event.fromServer) {
|
|
38
|
-
if (ws?.readyState === WebSocket.OPEN) {
|
|
39
|
-
// If the connection is open, send the event immediately.
|
|
40
|
-
ws.send(serialize(event))
|
|
41
|
-
} else {
|
|
42
|
-
// If the connection is not open, queue the event for later.
|
|
43
|
-
localQueue.push(event)
|
|
44
|
-
}
|
|
45
|
-
}
|
|
46
|
-
|
|
47
|
-
// Pass the event to the next middleware in the chain,
|
|
48
|
-
// which is eventually the store's original dispatch function.
|
|
49
|
-
return next(event)
|
|
50
|
-
}
|
|
51
|
-
|
|
52
|
-
/**
|
|
53
|
-
* Attempts to establish a WebSocket connection to the server.
|
|
54
|
-
*/
|
|
55
|
-
function establishConnection(store) {
|
|
56
|
-
// If a connection already exists, close it first.
|
|
57
|
-
if (ws) {
|
|
58
|
-
ws.close()
|
|
59
|
-
}
|
|
60
|
-
ws = new WebSocket(serverUrl)
|
|
61
|
-
|
|
62
|
-
// =====================================================================
|
|
63
|
-
// WebSocket Event Handlers
|
|
64
|
-
// =====================================================================
|
|
65
|
-
|
|
66
|
-
ws.onopen = () => {
|
|
67
|
-
// Send any queued events to the server.
|
|
68
|
-
while (localQueue.length) {
|
|
69
|
-
ws.send(serialize(localQueue.shift()))
|
|
70
|
-
}
|
|
71
|
-
}
|
|
72
|
-
|
|
73
|
-
ws.onmessage = (event) => {
|
|
74
|
-
const serverEvent = deserialize(event.data)
|
|
75
|
-
|
|
76
|
-
if (serverEvent.type === "initialState") {
|
|
77
|
-
// Merge the server's initial state with the client's local state.
|
|
78
|
-
const nextState = extend(store.getState(), serverEvent.payload)
|
|
79
|
-
store.setState(nextState)
|
|
80
|
-
} else {
|
|
81
|
-
// Dispatch the event to the local store to update the client's state.
|
|
82
|
-
store.dispatch({ ...serverEvent, fromServer: true })
|
|
83
|
-
}
|
|
84
|
-
}
|
|
85
|
-
|
|
86
|
-
ws.onclose = () => {
|
|
87
|
-
// Attempt to reconnect after a short delay.
|
|
88
|
-
setTimeout(() => establishConnection(store), reconnectionDelay)
|
|
89
|
-
}
|
|
90
|
-
|
|
91
|
-
ws.onerror = () => {
|
|
92
|
-
ws.close() // The 'onclose' handler will trigger the reconnect.
|
|
93
|
-
}
|
|
94
|
-
}
|
|
95
|
-
}
|