modd-network 1.0.1 → 1.0.3
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 +18 -19
- package/dist/binary-input.d.ts +53 -0
- package/dist/binary-input.js +168 -0
- package/dist/game-input-codec.d.ts +55 -0
- package/dist/game-input-codec.js +263 -0
- package/dist/index.d.ts +1 -1
- package/dist/index.js +7 -1
- package/dist/input-codec.d.ts +33 -0
- package/dist/input-codec.js +192 -0
- package/dist/modd-engine.d.ts +77 -56
- package/dist/modd-engine.js +479 -244
- package/dist/modd-engine.test.d.ts +1 -0
- package/dist/modd-engine.test.js +190 -0
- package/dist/modd-network.d.ts +55 -22
- package/dist/modd-network.js +497 -352
- package/dist/state-codec.d.ts +81 -0
- package/dist/state-codec.js +337 -0
- package/package.json +5 -2
package/dist/modd-engine.js
CHANGED
|
@@ -1,40 +1,98 @@
|
|
|
1
1
|
"use strict";
|
|
2
2
|
/**
|
|
3
3
|
* MODD Engine SDK
|
|
4
|
-
*
|
|
5
|
-
*
|
|
6
|
-
*
|
|
7
|
-
* -
|
|
8
|
-
*
|
|
4
|
+
* Unified game networking with state synchronization
|
|
5
|
+
*
|
|
6
|
+
* Usage:
|
|
7
|
+
* const game = await moddEngine.connect('my-app-id', 'my-room', {
|
|
8
|
+
* stateSync: { lerpSpeed: 0.3 },
|
|
9
|
+
* onConnect: (snapshot, inputs, frame) => { ... }
|
|
10
|
+
* });
|
|
11
|
+
*
|
|
12
|
+
* game.setLocalPlayer(playerId);
|
|
13
|
+
* game.addPlayer(playerId, { x, y, z });
|
|
14
|
+
* game.registerEntity('box1', adapter);
|
|
15
|
+
* game.tick();
|
|
9
16
|
*/
|
|
17
|
+
var __createBinding = (this && this.__createBinding) || (Object.create ? (function(o, m, k, k2) {
|
|
18
|
+
if (k2 === undefined) k2 = k;
|
|
19
|
+
var desc = Object.getOwnPropertyDescriptor(m, k);
|
|
20
|
+
if (!desc || ("get" in desc ? !m.__esModule : desc.writable || desc.configurable)) {
|
|
21
|
+
desc = { enumerable: true, get: function() { return m[k]; } };
|
|
22
|
+
}
|
|
23
|
+
Object.defineProperty(o, k2, desc);
|
|
24
|
+
}) : (function(o, m, k, k2) {
|
|
25
|
+
if (k2 === undefined) k2 = k;
|
|
26
|
+
o[k2] = m[k];
|
|
27
|
+
}));
|
|
28
|
+
var __setModuleDefault = (this && this.__setModuleDefault) || (Object.create ? (function(o, v) {
|
|
29
|
+
Object.defineProperty(o, "default", { enumerable: true, value: v });
|
|
30
|
+
}) : function(o, v) {
|
|
31
|
+
o["default"] = v;
|
|
32
|
+
});
|
|
33
|
+
var __importStar = (this && this.__importStar) || (function () {
|
|
34
|
+
var ownKeys = function(o) {
|
|
35
|
+
ownKeys = Object.getOwnPropertyNames || function (o) {
|
|
36
|
+
var ar = [];
|
|
37
|
+
for (var k in o) if (Object.prototype.hasOwnProperty.call(o, k)) ar[ar.length] = k;
|
|
38
|
+
return ar;
|
|
39
|
+
};
|
|
40
|
+
return ownKeys(o);
|
|
41
|
+
};
|
|
42
|
+
return function (mod) {
|
|
43
|
+
if (mod && mod.__esModule) return mod;
|
|
44
|
+
var result = {};
|
|
45
|
+
if (mod != null) for (var k = ownKeys(mod), i = 0; i < k.length; i++) if (k[i] !== "default") __createBinding(result, mod, k[i]);
|
|
46
|
+
__setModuleDefault(result, mod);
|
|
47
|
+
return result;
|
|
48
|
+
};
|
|
49
|
+
})();
|
|
10
50
|
Object.defineProperty(exports, "__esModule", { value: true });
|
|
11
|
-
exports.
|
|
51
|
+
exports.connectToRoom = void 0;
|
|
52
|
+
exports.connect = connect;
|
|
12
53
|
exports.rapierAdapter = rapierAdapter;
|
|
13
54
|
exports.generic2DAdapter = generic2DAdapter;
|
|
55
|
+
exports.init = init;
|
|
56
|
+
exports.getThree = getThree;
|
|
57
|
+
exports.getRapier = getRapier;
|
|
58
|
+
exports.createStateSync = createStateSync;
|
|
59
|
+
const modd_network_1 = require("./modd-network");
|
|
14
60
|
// ============================================
|
|
15
|
-
//
|
|
61
|
+
// State Sync Factory (internal)
|
|
16
62
|
// ============================================
|
|
17
|
-
function
|
|
18
|
-
//
|
|
19
|
-
const
|
|
20
|
-
|
|
63
|
+
function createStateSync(options = {}) {
|
|
64
|
+
// State sync config with defaults
|
|
65
|
+
const syncConfig = {
|
|
66
|
+
lerpSpeed: options.lerpSpeed ?? 0.3,
|
|
21
67
|
positionThreshold: options.positionThreshold ?? 0.01,
|
|
22
68
|
velocityThreshold: options.velocityThreshold ?? 0.1,
|
|
23
69
|
rotationThreshold: options.rotationThreshold ?? 0.01,
|
|
24
70
|
restVelocityThreshold: options.restVelocityThreshold ?? 0.05,
|
|
25
71
|
restFramesRequired: options.restFramesRequired ?? 30,
|
|
26
|
-
|
|
27
|
-
|
|
72
|
+
authorityRadius: options.authorityRadius ?? 50,
|
|
73
|
+
playerSendRate: options.playerSendRate ?? 3, // Send every 3 ticks = 10Hz
|
|
74
|
+
entitySendRate: options.entitySendRate ?? 3,
|
|
75
|
+
staleThreshold: options.staleThreshold ?? 500, // 500ms without updates = likely AFK
|
|
28
76
|
};
|
|
29
77
|
// State
|
|
30
78
|
const entities = new Map();
|
|
31
79
|
const players = new Map();
|
|
32
80
|
let localPlayerId = null;
|
|
33
|
-
let
|
|
81
|
+
let frame = 0;
|
|
34
82
|
let connection = null;
|
|
83
|
+
let roomId = null;
|
|
35
84
|
// ==========================================
|
|
36
|
-
//
|
|
85
|
+
// Utility
|
|
37
86
|
// ==========================================
|
|
87
|
+
function lerp(a, b, t) {
|
|
88
|
+
return a + (b - a) * t;
|
|
89
|
+
}
|
|
90
|
+
function distance(a, b) {
|
|
91
|
+
const dx = a.x - b.x;
|
|
92
|
+
const dy = a.y - b.y;
|
|
93
|
+
const dz = a.z - b.z;
|
|
94
|
+
return Math.sqrt(dx * dx + dy * dy + dz * dz);
|
|
95
|
+
}
|
|
38
96
|
function simpleHash(str) {
|
|
39
97
|
let h = 0;
|
|
40
98
|
for (let i = 0; i < str.length; i++) {
|
|
@@ -42,7 +100,96 @@ function createEngine(options = {}) {
|
|
|
42
100
|
}
|
|
43
101
|
return Math.abs(h);
|
|
44
102
|
}
|
|
45
|
-
|
|
103
|
+
// ==========================================
|
|
104
|
+
// Delta Detection
|
|
105
|
+
// ==========================================
|
|
106
|
+
function hasPositionChanged(curr, last) {
|
|
107
|
+
return Math.abs(curr.x - last.x) > syncConfig.positionThreshold ||
|
|
108
|
+
Math.abs(curr.y - last.y) > syncConfig.positionThreshold ||
|
|
109
|
+
Math.abs(curr.z - last.z) > syncConfig.positionThreshold;
|
|
110
|
+
}
|
|
111
|
+
function hasVelocityChanged(curr, last) {
|
|
112
|
+
return Math.abs((curr.vx || 0) - (last.vx || 0)) > syncConfig.velocityThreshold ||
|
|
113
|
+
Math.abs((curr.vy || 0) - (last.vy || 0)) > syncConfig.velocityThreshold ||
|
|
114
|
+
Math.abs((curr.vz || 0) - (last.vz || 0)) > syncConfig.velocityThreshold;
|
|
115
|
+
}
|
|
116
|
+
function hasRotationChanged(curr, last) {
|
|
117
|
+
// Check euler rotY (simple Y rotation)
|
|
118
|
+
if (curr.rotY !== undefined || last.rotY !== undefined) {
|
|
119
|
+
if (Math.abs((curr.rotY || 0) - (last.rotY || 0)) > syncConfig.rotationThreshold) {
|
|
120
|
+
return true;
|
|
121
|
+
}
|
|
122
|
+
}
|
|
123
|
+
// Check quaternion rotation
|
|
124
|
+
return Math.abs((curr.qx || 0) - (last.qx || 0)) > syncConfig.rotationThreshold ||
|
|
125
|
+
Math.abs((curr.qy || 0) - (last.qy || 0)) > syncConfig.rotationThreshold ||
|
|
126
|
+
Math.abs((curr.qz || 0) - (last.qz || 0)) > syncConfig.rotationThreshold ||
|
|
127
|
+
Math.abs((curr.qw || 1) - (last.qw || 1)) > syncConfig.rotationThreshold;
|
|
128
|
+
}
|
|
129
|
+
// Known physics fields that have special threshold handling
|
|
130
|
+
const PHYSICS_FIELDS = new Set(['x', 'y', 'z', 'vx', 'vy', 'vz', 'qx', 'qy', 'qz', 'qw', 'rotY', 'avx', 'avy', 'avz']);
|
|
131
|
+
function computeDelta(curr, last) {
|
|
132
|
+
const delta = {};
|
|
133
|
+
let hasChange = false;
|
|
134
|
+
// Check physics fields with thresholds
|
|
135
|
+
if (hasPositionChanged(curr, last)) {
|
|
136
|
+
delta.x = curr.x;
|
|
137
|
+
delta.y = curr.y;
|
|
138
|
+
delta.z = curr.z;
|
|
139
|
+
hasChange = true;
|
|
140
|
+
}
|
|
141
|
+
if (hasVelocityChanged(curr, last)) {
|
|
142
|
+
delta.vx = curr.vx;
|
|
143
|
+
delta.vy = curr.vy;
|
|
144
|
+
delta.vz = curr.vz;
|
|
145
|
+
hasChange = true;
|
|
146
|
+
}
|
|
147
|
+
if (hasRotationChanged(curr, last)) {
|
|
148
|
+
// Include rotY if present (euler Y rotation)
|
|
149
|
+
if (curr.rotY !== undefined) {
|
|
150
|
+
delta.rotY = curr.rotY;
|
|
151
|
+
}
|
|
152
|
+
// Include quaternion if present
|
|
153
|
+
if (curr.qx !== undefined || curr.qy !== undefined || curr.qz !== undefined || curr.qw !== undefined) {
|
|
154
|
+
delta.qx = curr.qx;
|
|
155
|
+
delta.qy = curr.qy;
|
|
156
|
+
delta.qz = curr.qz;
|
|
157
|
+
delta.qw = curr.qw;
|
|
158
|
+
}
|
|
159
|
+
hasChange = true;
|
|
160
|
+
}
|
|
161
|
+
if (hasChange && (curr.avx !== undefined || curr.avy !== undefined || curr.avz !== undefined)) {
|
|
162
|
+
delta.avx = curr.avx;
|
|
163
|
+
delta.avy = curr.avy;
|
|
164
|
+
delta.avz = curr.avz;
|
|
165
|
+
}
|
|
166
|
+
// Check custom fields (hp, dead, etc.) - any change triggers delta
|
|
167
|
+
for (const key of Object.keys(curr)) {
|
|
168
|
+
if (PHYSICS_FIELDS.has(key))
|
|
169
|
+
continue;
|
|
170
|
+
if (curr[key] !== last[key]) {
|
|
171
|
+
delta[key] = curr[key];
|
|
172
|
+
hasChange = true;
|
|
173
|
+
}
|
|
174
|
+
}
|
|
175
|
+
return hasChange ? delta : null;
|
|
176
|
+
}
|
|
177
|
+
// ==========================================
|
|
178
|
+
// Rest Detection
|
|
179
|
+
// ==========================================
|
|
180
|
+
function isAtRest(state) {
|
|
181
|
+
const speed = Math.sqrt((state.vx || 0) ** 2 +
|
|
182
|
+
(state.vy || 0) ** 2 +
|
|
183
|
+
(state.vz || 0) ** 2);
|
|
184
|
+
const angSpeed = Math.sqrt((state.avx || 0) ** 2 +
|
|
185
|
+
(state.avy || 0) ** 2 +
|
|
186
|
+
(state.avz || 0) ** 2);
|
|
187
|
+
return speed < syncConfig.restVelocityThreshold && angSpeed < syncConfig.restVelocityThreshold;
|
|
188
|
+
}
|
|
189
|
+
// ==========================================
|
|
190
|
+
// Authority
|
|
191
|
+
// ==========================================
|
|
192
|
+
function computeEntityAuthority(entityId) {
|
|
46
193
|
const entity = entities.get(entityId);
|
|
47
194
|
if (!entity)
|
|
48
195
|
return null;
|
|
@@ -53,20 +200,10 @@ function createEngine(options = {}) {
|
|
|
53
200
|
let bestScore = -Infinity;
|
|
54
201
|
for (const pid of playerIds) {
|
|
55
202
|
const player = players.get(pid);
|
|
56
|
-
|
|
203
|
+
const dist = distance(entity.localState, player.state);
|
|
204
|
+
if (dist > syncConfig.authorityRadius)
|
|
57
205
|
continue;
|
|
58
|
-
|
|
59
|
-
const dx = player.x - entity.state.x;
|
|
60
|
-
const dz = player.z - entity.state.z;
|
|
61
|
-
const dist = Math.sqrt(dx * dx + dz * dz);
|
|
62
|
-
let score = 1000 - dist * 10;
|
|
63
|
-
// Touch bonus (decays over time)
|
|
64
|
-
if (entity.lastTouchedBy === pid) {
|
|
65
|
-
const framesSinceTouch = currentFrame - (entity.lastTouchFrame || 0);
|
|
66
|
-
const decayRate = config.touchBonusScore / config.touchBonusDecay;
|
|
67
|
-
score += Math.max(0, config.touchBonusScore - framesSinceTouch * decayRate);
|
|
68
|
-
}
|
|
69
|
-
// Deterministic tiebreaker
|
|
206
|
+
let score = syncConfig.authorityRadius - dist;
|
|
70
207
|
score += simpleHash(pid + entityId) * 0.0001;
|
|
71
208
|
if (score > bestScore) {
|
|
72
209
|
bestScore = score;
|
|
@@ -75,175 +212,257 @@ function createEngine(options = {}) {
|
|
|
75
212
|
}
|
|
76
213
|
return bestPlayer;
|
|
77
214
|
}
|
|
78
|
-
function
|
|
79
|
-
return localPlayerId !== null && computeAuthority(entityId) === localPlayerId;
|
|
80
|
-
}
|
|
81
|
-
// ==========================================
|
|
82
|
-
// Delta Compression
|
|
83
|
-
// ==========================================
|
|
84
|
-
function hasStateChanged(entityId) {
|
|
215
|
+
function updateEntityAuthority(entityId) {
|
|
85
216
|
const entity = entities.get(entityId);
|
|
86
|
-
if (!entity
|
|
87
|
-
return
|
|
88
|
-
|
|
89
|
-
const last = entity.lastSentState;
|
|
90
|
-
// Position
|
|
91
|
-
if (Math.abs(curr.x - last.x) > config.positionThreshold)
|
|
92
|
-
return true;
|
|
93
|
-
if (Math.abs(curr.y - last.y) > config.positionThreshold)
|
|
94
|
-
return true;
|
|
95
|
-
if (Math.abs(curr.z - last.z) > config.positionThreshold)
|
|
96
|
-
return true;
|
|
97
|
-
// Velocity
|
|
98
|
-
if (Math.abs((curr.vx || 0) - (last.vx || 0)) > config.velocityThreshold)
|
|
99
|
-
return true;
|
|
100
|
-
if (Math.abs((curr.vy || 0) - (last.vy || 0)) > config.velocityThreshold)
|
|
101
|
-
return true;
|
|
102
|
-
if (Math.abs((curr.vz || 0) - (last.vz || 0)) > config.velocityThreshold)
|
|
103
|
-
return true;
|
|
104
|
-
// Rotation
|
|
105
|
-
if (Math.abs((curr.qx || 0) - (last.qx || 0)) > config.rotationThreshold)
|
|
106
|
-
return true;
|
|
107
|
-
if (Math.abs((curr.qy || 0) - (last.qy || 0)) > config.rotationThreshold)
|
|
108
|
-
return true;
|
|
109
|
-
if (Math.abs((curr.qz || 0) - (last.qz || 0)) > config.rotationThreshold)
|
|
110
|
-
return true;
|
|
111
|
-
if (Math.abs((curr.qw || 1) - (last.qw || 1)) > config.rotationThreshold)
|
|
112
|
-
return true;
|
|
113
|
-
return false;
|
|
217
|
+
if (!entity)
|
|
218
|
+
return;
|
|
219
|
+
entity.isAuthority = computeEntityAuthority(entityId) === localPlayerId;
|
|
114
220
|
}
|
|
115
221
|
// ==========================================
|
|
116
|
-
//
|
|
222
|
+
// Interpolation
|
|
117
223
|
// ==========================================
|
|
118
|
-
function
|
|
119
|
-
|
|
120
|
-
|
|
121
|
-
|
|
122
|
-
|
|
123
|
-
|
|
124
|
-
(
|
|
125
|
-
(
|
|
126
|
-
|
|
127
|
-
|
|
128
|
-
|
|
129
|
-
|
|
130
|
-
|
|
131
|
-
|
|
132
|
-
|
|
133
|
-
|
|
134
|
-
|
|
135
|
-
|
|
136
|
-
|
|
137
|
-
|
|
138
|
-
|
|
139
|
-
const entity = entities.get(entityId);
|
|
140
|
-
if (entity) {
|
|
141
|
-
entity.restFrames = 0;
|
|
142
|
-
}
|
|
224
|
+
function interpolateState(current, target, t) {
|
|
225
|
+
// Start with all properties from current and target (preserves custom props like hp, dead)
|
|
226
|
+
return {
|
|
227
|
+
...current,
|
|
228
|
+
...target,
|
|
229
|
+
// Override with interpolated physics values
|
|
230
|
+
x: lerp(current.x, target.x, t),
|
|
231
|
+
y: lerp(current.y, target.y, t),
|
|
232
|
+
z: lerp(current.z, target.z, t),
|
|
233
|
+
vx: target.vx,
|
|
234
|
+
vy: target.vy,
|
|
235
|
+
vz: target.vz,
|
|
236
|
+
rotY: target.rotY !== undefined ? lerp(current.rotY || 0, target.rotY, t) : current.rotY,
|
|
237
|
+
qx: lerp(current.qx || 0, target.qx || 0, t),
|
|
238
|
+
qy: lerp(current.qy || 0, target.qy || 0, t),
|
|
239
|
+
qz: lerp(current.qz || 0, target.qz || 0, t),
|
|
240
|
+
qw: lerp(current.qw || 1, target.qw || 1, t),
|
|
241
|
+
avx: target.avx,
|
|
242
|
+
avy: target.avy,
|
|
243
|
+
avz: target.avz,
|
|
244
|
+
};
|
|
143
245
|
}
|
|
144
246
|
// ==========================================
|
|
145
|
-
//
|
|
247
|
+
// Sending State (rate limited)
|
|
146
248
|
// ==========================================
|
|
147
|
-
function
|
|
148
|
-
|
|
149
|
-
|
|
249
|
+
function sendPlayerState() {
|
|
250
|
+
if (!connection?.connected || !localPlayerId)
|
|
251
|
+
return;
|
|
252
|
+
// Rate limit: only send every N ticks
|
|
253
|
+
if (frame % syncConfig.playerSendRate !== 0)
|
|
150
254
|
return;
|
|
151
|
-
|
|
152
|
-
if (
|
|
255
|
+
const player = players.get(localPlayerId);
|
|
256
|
+
if (!player)
|
|
153
257
|
return;
|
|
154
|
-
const
|
|
155
|
-
|
|
156
|
-
|
|
157
|
-
|
|
158
|
-
|
|
159
|
-
|
|
160
|
-
|
|
161
|
-
|
|
162
|
-
|
|
163
|
-
|
|
164
|
-
entity.state.qx = targetState.qx;
|
|
165
|
-
entity.state.qy = targetState.qy;
|
|
166
|
-
entity.state.qz = targetState.qz;
|
|
167
|
-
entity.state.qw = targetState.qw;
|
|
168
|
-
// Hard sync angular velocity
|
|
169
|
-
entity.state.avx = targetState.avx;
|
|
170
|
-
entity.state.avy = targetState.avy;
|
|
171
|
-
entity.state.avz = targetState.avz;
|
|
172
|
-
// Apply to physics
|
|
173
|
-
entity.adapter.setState(entity.state);
|
|
174
|
-
// Wake the entity
|
|
175
|
-
wakeEntity(entityId);
|
|
258
|
+
const delta = computeDelta(player.state, player.targetState);
|
|
259
|
+
if (!delta)
|
|
260
|
+
return;
|
|
261
|
+
connection.send({
|
|
262
|
+
type: 'playerState',
|
|
263
|
+
id: localPlayerId,
|
|
264
|
+
...delta,
|
|
265
|
+
frame
|
|
266
|
+
});
|
|
267
|
+
Object.assign(player.targetState, player.state);
|
|
176
268
|
}
|
|
177
|
-
|
|
178
|
-
|
|
179
|
-
|
|
180
|
-
|
|
181
|
-
if (
|
|
269
|
+
function sendEntityStates() {
|
|
270
|
+
if (!connection?.connected || !localPlayerId)
|
|
271
|
+
return;
|
|
272
|
+
// Rate limit: only send every N ticks
|
|
273
|
+
if (frame % syncConfig.entitySendRate !== 0)
|
|
182
274
|
return;
|
|
183
275
|
for (const [id, entity] of entities) {
|
|
184
|
-
|
|
185
|
-
if (!isLocalAuthority(id))
|
|
276
|
+
if (!entity.isAuthority)
|
|
186
277
|
continue;
|
|
187
|
-
|
|
188
|
-
if (isAtRest(
|
|
189
|
-
|
|
190
|
-
|
|
191
|
-
|
|
278
|
+
entity.localState = entity.adapter.getState();
|
|
279
|
+
if (isAtRest(entity.localState)) {
|
|
280
|
+
entity.restFrames++;
|
|
281
|
+
if (entity.restFrames > syncConfig.restFramesRequired)
|
|
282
|
+
continue;
|
|
283
|
+
}
|
|
284
|
+
else {
|
|
285
|
+
entity.restFrames = 0;
|
|
286
|
+
}
|
|
287
|
+
const delta = computeDelta(entity.localState, entity.lastSentState);
|
|
288
|
+
if (!delta)
|
|
192
289
|
continue;
|
|
193
|
-
// Sync state from physics
|
|
194
|
-
entity.state = entity.adapter.getState();
|
|
195
|
-
// Send update
|
|
196
290
|
connection.send({
|
|
197
291
|
type: 'entityState',
|
|
198
292
|
id,
|
|
199
|
-
...
|
|
293
|
+
...delta,
|
|
294
|
+
frame
|
|
200
295
|
});
|
|
201
|
-
|
|
202
|
-
|
|
296
|
+
Object.assign(entity.lastSentState, entity.localState);
|
|
297
|
+
}
|
|
298
|
+
}
|
|
299
|
+
// ==========================================
|
|
300
|
+
// Receiving State
|
|
301
|
+
// ==========================================
|
|
302
|
+
// Apply all received properties to target state
|
|
303
|
+
function applyStateUpdate(target, state) {
|
|
304
|
+
for (const key of Object.keys(state)) {
|
|
305
|
+
if (key === 'type' || key === 'id' || key === 'frame')
|
|
306
|
+
continue; // Skip metadata
|
|
307
|
+
if (state[key] !== undefined) {
|
|
308
|
+
target[key] = state[key];
|
|
309
|
+
}
|
|
310
|
+
}
|
|
311
|
+
}
|
|
312
|
+
function receivePlayerState(id, state) {
|
|
313
|
+
if (id === localPlayerId)
|
|
314
|
+
return;
|
|
315
|
+
let player = players.get(id);
|
|
316
|
+
if (!player) {
|
|
317
|
+
const fullState = {
|
|
318
|
+
x: state.x ?? 0,
|
|
319
|
+
y: state.y ?? 0,
|
|
320
|
+
z: state.z ?? 0,
|
|
321
|
+
vx: state.vx ?? 0,
|
|
322
|
+
vy: state.vy ?? 0,
|
|
323
|
+
vz: state.vz ?? 0,
|
|
324
|
+
};
|
|
325
|
+
// Apply any additional properties from initial state
|
|
326
|
+
applyStateUpdate(fullState, state);
|
|
327
|
+
player = {
|
|
328
|
+
id,
|
|
329
|
+
state: { ...fullState },
|
|
330
|
+
targetState: { ...fullState },
|
|
331
|
+
lastUpdate: Date.now(),
|
|
332
|
+
};
|
|
333
|
+
players.set(id, player);
|
|
334
|
+
return;
|
|
335
|
+
}
|
|
336
|
+
// Apply all received properties to target state
|
|
337
|
+
applyStateUpdate(player.targetState, state);
|
|
338
|
+
// For non-interpolated properties, apply directly to current state too
|
|
339
|
+
for (const key of Object.keys(state)) {
|
|
340
|
+
if (PHYSICS_FIELDS.has(key))
|
|
341
|
+
continue; // Physics fields are interpolated
|
|
342
|
+
if (key === 'type' || key === 'id' || key === 'frame')
|
|
343
|
+
continue;
|
|
344
|
+
if (state[key] !== undefined) {
|
|
345
|
+
player.state[key] = state[key];
|
|
346
|
+
}
|
|
203
347
|
}
|
|
348
|
+
player.lastUpdate = Date.now();
|
|
204
349
|
}
|
|
205
|
-
function
|
|
350
|
+
function receiveEntityState(id, state) {
|
|
206
351
|
const entity = entities.get(id);
|
|
207
352
|
if (!entity)
|
|
208
353
|
return;
|
|
209
|
-
|
|
210
|
-
if (isLocalAuthority(id))
|
|
354
|
+
if (entity.isAuthority)
|
|
211
355
|
return;
|
|
212
|
-
|
|
356
|
+
// Apply all received properties to target state
|
|
357
|
+
applyStateUpdate(entity.targetState, state);
|
|
358
|
+
entity.lastUpdate = Date.now();
|
|
359
|
+
entity.restFrames = 0;
|
|
213
360
|
}
|
|
214
|
-
|
|
215
|
-
|
|
216
|
-
|
|
217
|
-
|
|
218
|
-
|
|
219
|
-
|
|
361
|
+
// ==========================================
|
|
362
|
+
// Tick
|
|
363
|
+
// ==========================================
|
|
364
|
+
function tick() {
|
|
365
|
+
frame++;
|
|
366
|
+
for (const [id] of entities) {
|
|
367
|
+
updateEntityAuthority(id);
|
|
368
|
+
}
|
|
369
|
+
for (const [id, player] of players) {
|
|
370
|
+
if (id === localPlayerId)
|
|
371
|
+
continue;
|
|
372
|
+
player.state = interpolateState(player.state, player.targetState, syncConfig.lerpSpeed);
|
|
220
373
|
}
|
|
374
|
+
for (const [, entity] of entities) {
|
|
375
|
+
if (entity.isAuthority) {
|
|
376
|
+
entity.localState = entity.adapter.getState();
|
|
377
|
+
}
|
|
378
|
+
else {
|
|
379
|
+
entity.localState = interpolateState(entity.localState, entity.targetState, syncConfig.lerpSpeed);
|
|
380
|
+
entity.adapter.setState(entity.localState);
|
|
381
|
+
}
|
|
382
|
+
}
|
|
383
|
+
sendPlayerState();
|
|
384
|
+
sendEntityStates();
|
|
221
385
|
}
|
|
222
386
|
// ==========================================
|
|
223
|
-
//
|
|
387
|
+
// Process Input
|
|
224
388
|
// ==========================================
|
|
225
|
-
|
|
226
|
-
|
|
227
|
-
|
|
228
|
-
|
|
389
|
+
function processInput(input) {
|
|
390
|
+
if (input.type === 'playerState') {
|
|
391
|
+
receivePlayerState(input.id, input);
|
|
392
|
+
}
|
|
393
|
+
else if (input.type === 'entityState') {
|
|
394
|
+
receiveEntityState(input.id, input);
|
|
395
|
+
}
|
|
396
|
+
}
|
|
397
|
+
// ==========================================
|
|
398
|
+
// Game Object
|
|
399
|
+
// ==========================================
|
|
400
|
+
const game = {
|
|
401
|
+
get connected() { return connection?.connected ?? false; },
|
|
402
|
+
get room() { return roomId; },
|
|
403
|
+
get node() { return connection?.node ?? null; },
|
|
404
|
+
get bandwidthIn() { return connection?.bandwidthIn ?? 0; },
|
|
405
|
+
get bandwidthOut() { return connection?.bandwidthOut ?? 0; },
|
|
406
|
+
get frame() { return frame; },
|
|
407
|
+
get localPlayerId() { return localPlayerId; },
|
|
408
|
+
get config() { return { ...syncConfig }; },
|
|
409
|
+
disconnect() {
|
|
410
|
+
connection?.close();
|
|
229
411
|
},
|
|
230
|
-
|
|
231
|
-
|
|
232
|
-
|
|
412
|
+
send(data) {
|
|
413
|
+
connection?.send(data);
|
|
414
|
+
},
|
|
415
|
+
setLocalPlayer(id) {
|
|
416
|
+
localPlayerId = id;
|
|
417
|
+
},
|
|
418
|
+
addPlayer(id, initialState) {
|
|
419
|
+
players.set(id, {
|
|
420
|
+
id,
|
|
421
|
+
state: { ...initialState },
|
|
422
|
+
targetState: { ...initialState },
|
|
423
|
+
lastUpdate: Date.now(),
|
|
424
|
+
});
|
|
425
|
+
},
|
|
426
|
+
removePlayer(id) {
|
|
427
|
+
players.delete(id);
|
|
233
428
|
},
|
|
234
|
-
|
|
235
|
-
|
|
429
|
+
getPlayer(id) {
|
|
430
|
+
return players.get(id);
|
|
236
431
|
},
|
|
237
|
-
|
|
238
|
-
|
|
432
|
+
getPlayers() {
|
|
433
|
+
return Array.from(players.values());
|
|
434
|
+
},
|
|
435
|
+
isPlayerStale(id) {
|
|
436
|
+
if (id === localPlayerId)
|
|
437
|
+
return false; // Local player is never stale
|
|
438
|
+
const player = players.get(id);
|
|
439
|
+
if (!player)
|
|
440
|
+
return false;
|
|
441
|
+
return Date.now() - player.lastUpdate > syncConfig.staleThreshold;
|
|
442
|
+
},
|
|
443
|
+
getStalePlayers() {
|
|
444
|
+
const now = Date.now();
|
|
445
|
+
return Array.from(players.values()).filter(p => p.id !== localPlayerId && now - p.lastUpdate > syncConfig.staleThreshold);
|
|
446
|
+
},
|
|
447
|
+
updateLocalPlayerState(state) {
|
|
448
|
+
if (!localPlayerId)
|
|
449
|
+
return;
|
|
450
|
+
const player = players.get(localPlayerId);
|
|
451
|
+
if (player) {
|
|
452
|
+
player.state = { ...state };
|
|
453
|
+
}
|
|
239
454
|
},
|
|
240
|
-
// Entity management
|
|
241
455
|
registerEntity(id, adapter) {
|
|
456
|
+
const state = adapter.getState();
|
|
242
457
|
entities.set(id, {
|
|
243
458
|
id,
|
|
244
459
|
adapter,
|
|
245
|
-
|
|
246
|
-
|
|
460
|
+
localState: { ...state },
|
|
461
|
+
targetState: { ...state },
|
|
462
|
+
lastSentState: { ...state },
|
|
463
|
+
restFrames: 0,
|
|
464
|
+
isAuthority: false,
|
|
465
|
+
lastUpdate: Date.now(),
|
|
247
466
|
});
|
|
248
467
|
},
|
|
249
468
|
unregisterEntity(id) {
|
|
@@ -252,90 +471,53 @@ function createEngine(options = {}) {
|
|
|
252
471
|
getEntity(id) {
|
|
253
472
|
return entities.get(id);
|
|
254
473
|
},
|
|
255
|
-
|
|
256
|
-
|
|
257
|
-
players.set(id, { id, x, y, z, dead });
|
|
474
|
+
getEntities() {
|
|
475
|
+
return Array.from(entities.values());
|
|
258
476
|
},
|
|
259
|
-
|
|
260
|
-
|
|
261
|
-
},
|
|
262
|
-
// Authority
|
|
263
|
-
computeAuthority,
|
|
264
|
-
isLocalAuthority,
|
|
265
|
-
// Sync operations
|
|
266
|
-
syncFromPhysics(entityId) {
|
|
267
|
-
const entity = entities.get(entityId);
|
|
268
|
-
if (entity) {
|
|
269
|
-
entity.state = entity.adapter.getState();
|
|
270
|
-
}
|
|
477
|
+
isAuthority(entityId) {
|
|
478
|
+
return entities.get(entityId)?.isAuthority ?? false;
|
|
271
479
|
},
|
|
272
|
-
|
|
273
|
-
|
|
274
|
-
|
|
275
|
-
|
|
480
|
+
tick,
|
|
481
|
+
processInput,
|
|
482
|
+
// Internal: set connection after async connect
|
|
483
|
+
setConnection(conn) {
|
|
484
|
+
connection = conn;
|
|
276
485
|
},
|
|
277
|
-
|
|
278
|
-
|
|
279
|
-
|
|
280
|
-
|
|
281
|
-
|
|
282
|
-
|
|
283
|
-
|
|
284
|
-
|
|
285
|
-
|
|
286
|
-
|
|
287
|
-
|
|
288
|
-
|
|
289
|
-
|
|
290
|
-
|
|
291
|
-
|
|
292
|
-
|
|
293
|
-
id,
|
|
294
|
-
touchedBy: playerId,
|
|
295
|
-
frame: currentFrame
|
|
296
|
-
});
|
|
297
|
-
}
|
|
486
|
+
};
|
|
487
|
+
return game;
|
|
488
|
+
}
|
|
489
|
+
// ============================================
|
|
490
|
+
// Main Connect Function
|
|
491
|
+
// ============================================
|
|
492
|
+
async function connect(appId, room, options = {}) {
|
|
493
|
+
// Create the game/state sync object
|
|
494
|
+
const game = createStateSync(options.stateSync);
|
|
495
|
+
// Set up onTick to process inputs
|
|
496
|
+
const userOnTick = options.onTick;
|
|
497
|
+
const gameOptions = {
|
|
498
|
+
...options,
|
|
499
|
+
onTick: (frame, inputs) => {
|
|
500
|
+
for (const input of inputs) {
|
|
501
|
+
game.processInput(input.data);
|
|
298
502
|
}
|
|
299
|
-
|
|
300
|
-
|
|
301
|
-
if (!connection?.connected)
|
|
302
|
-
return;
|
|
303
|
-
const entityIds = Array.from(entities.keys());
|
|
304
|
-
for (let i = 0; i < entityIds.length; i++) {
|
|
305
|
-
const idA = entityIds[i];
|
|
306
|
-
if (!isLocalAuthority(idA))
|
|
307
|
-
continue;
|
|
308
|
-
const entityA = entities.get(idA);
|
|
309
|
-
for (let j = i + 1; j < entityIds.length; j++) {
|
|
310
|
-
const idB = entityIds[j];
|
|
311
|
-
const entityB = entities.get(idB);
|
|
312
|
-
const dx = entityA.state.x - entityB.state.x;
|
|
313
|
-
const dz = entityA.state.z - entityB.state.z;
|
|
314
|
-
const dist = Math.sqrt(dx * dx + dz * dz);
|
|
315
|
-
if (dist < touchDistance && entityB.lastTouchedBy !== localPlayerId) {
|
|
316
|
-
connection.send({
|
|
317
|
-
type: 'entityTouch',
|
|
318
|
-
id: idB,
|
|
319
|
-
touchedBy: localPlayerId,
|
|
320
|
-
frame: currentFrame
|
|
321
|
-
});
|
|
322
|
-
}
|
|
323
|
-
}
|
|
503
|
+
if (userOnTick) {
|
|
504
|
+
userOnTick(frame, inputs);
|
|
324
505
|
}
|
|
325
506
|
},
|
|
326
|
-
// Utility
|
|
327
|
-
wakeEntity,
|
|
328
|
-
isAtRest,
|
|
329
|
-
hasStateChanged,
|
|
330
507
|
};
|
|
508
|
+
// Connect to network
|
|
509
|
+
const connection = await (0, modd_network_1.connect)(appId, room, gameOptions);
|
|
510
|
+
game.setConnection(connection);
|
|
511
|
+
// Return public interface (without setConnection)
|
|
512
|
+
return game;
|
|
331
513
|
}
|
|
332
514
|
// ============================================
|
|
333
515
|
// Physics Adapters
|
|
334
516
|
// ============================================
|
|
335
|
-
/**
|
|
336
|
-
* Rapier 3D physics adapter
|
|
337
|
-
*/
|
|
338
517
|
function rapierAdapter(body) {
|
|
518
|
+
if (!body || typeof body.translation !== 'function') {
|
|
519
|
+
throw new Error('rapierAdapter: Invalid Rapier body');
|
|
520
|
+
}
|
|
339
521
|
return {
|
|
340
522
|
getState() {
|
|
341
523
|
const pos = body.translation();
|
|
@@ -357,9 +539,6 @@ function rapierAdapter(body) {
|
|
|
357
539
|
}
|
|
358
540
|
};
|
|
359
541
|
}
|
|
360
|
-
/**
|
|
361
|
-
* Generic 2D physics adapter (for Matter.js, Planck, etc.)
|
|
362
|
-
*/
|
|
363
542
|
function generic2DAdapter(getBody, setBody) {
|
|
364
543
|
return {
|
|
365
544
|
getState() {
|
|
@@ -374,16 +553,72 @@ function generic2DAdapter(getBody, setBody) {
|
|
|
374
553
|
};
|
|
375
554
|
},
|
|
376
555
|
setState(state) {
|
|
377
|
-
setBody({
|
|
378
|
-
x: state.x,
|
|
379
|
-
y: state.y,
|
|
380
|
-
vx: state.vx,
|
|
381
|
-
vy: state.vy
|
|
382
|
-
});
|
|
556
|
+
setBody({ x: state.x, y: state.y, vx: state.vx, vy: state.vy });
|
|
383
557
|
}
|
|
384
558
|
};
|
|
385
559
|
}
|
|
386
|
-
//
|
|
560
|
+
// ============================================
|
|
561
|
+
// Auto-load Dependencies
|
|
562
|
+
// ============================================
|
|
563
|
+
let THREE = null;
|
|
564
|
+
let RAPIER = null;
|
|
565
|
+
let initPromise = null;
|
|
566
|
+
const DEFAULT_THREE_URL = 'https://cdn.jsdelivr.net/npm/three@0.160.0/build/three.module.js';
|
|
567
|
+
const DEFAULT_RAPIER_URL = 'https://cdn.jsdelivr.net/npm/@dimforge/rapier3d-deterministic-compat@0.19.0/rapier.mjs';
|
|
568
|
+
async function loadModule(url) {
|
|
569
|
+
return Promise.resolve(`${url}`).then(s => __importStar(require(s)));
|
|
570
|
+
}
|
|
571
|
+
async function init(options = {}) {
|
|
572
|
+
if (initPromise)
|
|
573
|
+
return initPromise.then(() => ({ THREE, RAPIER }));
|
|
574
|
+
initPromise = (async () => {
|
|
575
|
+
const promises = [];
|
|
576
|
+
if (!options.skipThree) {
|
|
577
|
+
promises.push(loadModule(options.threeUrl || DEFAULT_THREE_URL).then((mod) => {
|
|
578
|
+
THREE = mod;
|
|
579
|
+
if (typeof window !== 'undefined') {
|
|
580
|
+
window.THREE = THREE;
|
|
581
|
+
}
|
|
582
|
+
}));
|
|
583
|
+
}
|
|
584
|
+
if (!options.skipRapier) {
|
|
585
|
+
promises.push(loadModule(options.rapierUrl || DEFAULT_RAPIER_URL).then(async (mod) => {
|
|
586
|
+
RAPIER = mod.default || mod;
|
|
587
|
+
await RAPIER.init();
|
|
588
|
+
if (typeof window !== 'undefined') {
|
|
589
|
+
window.RAPIER = RAPIER;
|
|
590
|
+
}
|
|
591
|
+
}));
|
|
592
|
+
}
|
|
593
|
+
await Promise.all(promises);
|
|
594
|
+
})();
|
|
595
|
+
await initPromise;
|
|
596
|
+
return { THREE, RAPIER };
|
|
597
|
+
}
|
|
598
|
+
function getThree() {
|
|
599
|
+
if (!THREE)
|
|
600
|
+
throw new Error('THREE not loaded. Call moddEngine.init() first.');
|
|
601
|
+
return THREE;
|
|
602
|
+
}
|
|
603
|
+
function getRapier() {
|
|
604
|
+
if (!RAPIER)
|
|
605
|
+
throw new Error('RAPIER not loaded. Call moddEngine.init() first.');
|
|
606
|
+
return RAPIER;
|
|
607
|
+
}
|
|
608
|
+
// Alias for backwards compat
|
|
609
|
+
exports.connectToRoom = connect;
|
|
610
|
+
// Browser global
|
|
387
611
|
if (typeof window !== 'undefined') {
|
|
388
|
-
window.moddEngine = {
|
|
612
|
+
window.moddEngine = {
|
|
613
|
+
connect,
|
|
614
|
+
createStateSync,
|
|
615
|
+
rapierAdapter,
|
|
616
|
+
generic2DAdapter,
|
|
617
|
+
init,
|
|
618
|
+
getThree,
|
|
619
|
+
getRapier,
|
|
620
|
+
get THREE() { return THREE; },
|
|
621
|
+
get RAPIER() { return RAPIER; },
|
|
622
|
+
VERSION: '2.0.0'
|
|
623
|
+
};
|
|
389
624
|
}
|