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.
@@ -1,40 +1,98 @@
1
1
  "use strict";
2
2
  /**
3
3
  * MODD Engine SDK
4
- * Game synchronization utilities for multiplayer games
5
- * - Proximity-based authority
6
- * - Delta compression
7
- * - Rest detection
8
- * - Soft sync / interpolation
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.createEngine = createEngine;
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
- // Engine Implementation
61
+ // State Sync Factory (internal)
16
62
  // ============================================
17
- function createEngine(options = {}) {
18
- // Configuration with defaults
19
- const config = {
20
- softSyncLerp: options.softSyncLerp ?? 0.5,
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
- touchBonusScore: options.touchBonusScore ?? 50,
27
- touchBonusDecay: options.touchBonusDecay ?? 100,
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 currentFrame = 0;
81
+ let frame = 0;
34
82
  let connection = null;
83
+ let roomId = null;
35
84
  // ==========================================
36
- // Authority Computation
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
- function computeAuthority(entityId) {
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
- if (player.dead)
203
+ const dist = distance(entity.localState, player.state);
204
+ if (dist > syncConfig.authorityRadius)
57
205
  continue;
58
- // Distance-based score (closer = higher)
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 isLocalAuthority(entityId) {
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 || !entity.lastSentState)
87
- return true;
88
- const curr = entity.state;
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
- // Rest Detection
222
+ // Interpolation
117
223
  // ==========================================
118
- function isAtRest(entityId) {
119
- const entity = entities.get(entityId);
120
- if (!entity)
121
- return false;
122
- const state = entity.state;
123
- const speed = Math.sqrt((state.vx || 0) ** 2 +
124
- (state.vy || 0) ** 2 +
125
- (state.vz || 0) ** 2);
126
- const angSpeed = Math.sqrt((state.avx || 0) ** 2 +
127
- (state.avy || 0) ** 2 +
128
- (state.avz || 0) ** 2);
129
- if (speed < config.restVelocityThreshold && angSpeed < config.restVelocityThreshold) {
130
- entity.restFrames++;
131
- return entity.restFrames > config.restFramesRequired;
132
- }
133
- else {
134
- entity.restFrames = 0;
135
- return false;
136
- }
137
- }
138
- function wakeEntity(entityId) {
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
- // Soft Sync
247
+ // Sending State (rate limited)
146
248
  // ==========================================
147
- function applySoftSync(entityId, targetState) {
148
- const entity = entities.get(entityId);
149
- if (!entity)
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
- // Skip if we're authority
152
- if (isLocalAuthority(entityId))
255
+ const player = players.get(localPlayerId);
256
+ if (!player)
153
257
  return;
154
- const lerp = config.softSyncLerp;
155
- // Lerp position
156
- entity.state.x += (targetState.x - entity.state.x) * lerp;
157
- entity.state.y += (targetState.y - entity.state.y) * lerp;
158
- entity.state.z += (targetState.z - entity.state.z) * lerp;
159
- // Hard sync velocity
160
- entity.state.vx = targetState.vx;
161
- entity.state.vy = targetState.vy;
162
- entity.state.vz = targetState.vz;
163
- // Hard sync rotation
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
- // Network Integration
179
- // ==========================================
180
- function sendEntityUpdates() {
181
- if (!connection?.connected)
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
- // Skip if not authority
185
- if (!isLocalAuthority(id))
276
+ if (!entity.isAuthority)
186
277
  continue;
187
- // Skip if at rest
188
- if (isAtRest(id))
189
- continue;
190
- // Skip if no meaningful change
191
- if (!hasStateChanged(id))
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
- ...entity.state
293
+ ...delta,
294
+ frame
200
295
  });
201
- // Remember what we sent
202
- entity.lastSentState = { ...entity.state };
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 handleEntityState(id, state) {
350
+ function receiveEntityState(id, state) {
206
351
  const entity = entities.get(id);
207
352
  if (!entity)
208
353
  return;
209
- // Skip if we're authority
210
- if (isLocalAuthority(id))
354
+ if (entity.isAuthority)
211
355
  return;
212
- applySoftSync(id, state);
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
- function handleEntityTouch(id, touchedBy, frame) {
215
- const entity = entities.get(id);
216
- if (entity) {
217
- entity.lastTouchedBy = touchedBy;
218
- entity.lastTouchFrame = frame;
219
- wakeEntity(id);
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
- // Public API
387
+ // Process Input
224
388
  // ==========================================
225
- return {
226
- // Configuration
227
- get config() {
228
- return { ...config };
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
- // Setup
231
- setConnection(conn) {
232
- connection = conn;
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
- setLocalPlayer(playerId) {
235
- localPlayerId = playerId;
429
+ getPlayer(id) {
430
+ return players.get(id);
236
431
  },
237
- setFrame(frame) {
238
- currentFrame = frame;
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
- state: adapter.getState(),
246
- restFrames: 0
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
- // Player management (for authority calculation)
256
- updatePlayer(id, x, y, z, dead = false) {
257
- players.set(id, { id, x, y, z, dead });
474
+ getEntities() {
475
+ return Array.from(entities.values());
258
476
  },
259
- removePlayer(id) {
260
- players.delete(id);
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
- syncAllFromPhysics() {
273
- for (const entity of entities.values()) {
274
- entity.state = entity.adapter.getState();
275
- }
480
+ tick,
481
+ processInput,
482
+ // Internal: set connection after async connect
483
+ setConnection(conn) {
484
+ connection = conn;
276
485
  },
277
- // Network operations (call each tick)
278
- sendUpdates: sendEntityUpdates,
279
- // Input handlers (call from processInput)
280
- handleEntityState,
281
- handleEntityTouch,
282
- // Touch detection (call after physics step)
283
- checkPlayerTouches(playerId, playerX, playerZ, touchDistance = 2.0) {
284
- if (!connection?.connected || playerId !== localPlayerId)
285
- return;
286
- for (const [id, entity] of entities) {
287
- const dx = playerX - entity.state.x;
288
- const dz = playerZ - entity.state.z;
289
- const dist = Math.sqrt(dx * dx + dz * dz);
290
- if (dist < touchDistance && entity.lastTouchedBy !== playerId) {
291
- connection.send({
292
- type: 'entityTouch',
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
- checkEntityCollisions(touchDistance = 4.0) {
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
- // Browser global fallback
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 = { createEngine, rapierAdapter, generic2DAdapter };
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
  }