@inglorious/engine 3.0.0 → 4.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/package.json
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "@inglorious/engine",
|
|
3
|
-
"version": "
|
|
3
|
+
"version": "4.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/
|
|
34
|
-
"@inglorious/
|
|
33
|
+
"@inglorious/utils": "3.6.0",
|
|
34
|
+
"@inglorious/store": "5.1.0"
|
|
35
35
|
},
|
|
36
36
|
"peerDependencies": {
|
|
37
|
-
"@inglorious/
|
|
38
|
-
"@inglorious/
|
|
37
|
+
"@inglorious/utils": "3.6.0",
|
|
38
|
+
"@inglorious/store": "5.1.0"
|
|
39
39
|
},
|
|
40
40
|
"devDependencies": {
|
|
41
41
|
"prettier": "^3.5.3",
|
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
|
|
|
@@ -111,7 +117,7 @@ export class Engine {
|
|
|
111
117
|
const newDevMode = state.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
|
}
|
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
|
-
}
|