@gamerstake/game-core 0.1.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.
Files changed (48) hide show
  1. package/.eslintrc.json +22 -0
  2. package/.testing-guide-summary.md +261 -0
  3. package/DEVELOPER_GUIDE.md +996 -0
  4. package/MANUAL_TESTING.md +369 -0
  5. package/QUICK_START.md +368 -0
  6. package/README.md +379 -0
  7. package/TESTING_OVERVIEW.md +378 -0
  8. package/dist/index.d.ts +1266 -0
  9. package/dist/index.js +1632 -0
  10. package/dist/index.js.map +1 -0
  11. package/examples/simple-game/README.md +176 -0
  12. package/examples/simple-game/client.ts +201 -0
  13. package/examples/simple-game/package.json +14 -0
  14. package/examples/simple-game/server.ts +233 -0
  15. package/jest.config.ts +39 -0
  16. package/package.json +54 -0
  17. package/src/core/GameLoop.ts +214 -0
  18. package/src/core/GameRules.ts +103 -0
  19. package/src/core/GameServer.ts +200 -0
  20. package/src/core/Room.ts +368 -0
  21. package/src/entities/Entity.ts +118 -0
  22. package/src/entities/Registry.ts +161 -0
  23. package/src/index.ts +51 -0
  24. package/src/input/Command.ts +41 -0
  25. package/src/input/InputQueue.ts +130 -0
  26. package/src/network/Network.ts +112 -0
  27. package/src/network/Snapshot.ts +59 -0
  28. package/src/physics/AABB.ts +104 -0
  29. package/src/physics/Movement.ts +124 -0
  30. package/src/spatial/Grid.ts +202 -0
  31. package/src/types/index.ts +117 -0
  32. package/src/types/protocol.ts +161 -0
  33. package/src/utils/Logger.ts +112 -0
  34. package/src/utils/RingBuffer.ts +116 -0
  35. package/tests/AABB.test.ts +38 -0
  36. package/tests/Entity.test.ts +35 -0
  37. package/tests/GameLoop.test.ts +58 -0
  38. package/tests/GameServer.test.ts +64 -0
  39. package/tests/Grid.test.ts +28 -0
  40. package/tests/InputQueue.test.ts +42 -0
  41. package/tests/Movement.test.ts +37 -0
  42. package/tests/Network.test.ts +39 -0
  43. package/tests/Registry.test.ts +36 -0
  44. package/tests/RingBuffer.test.ts +38 -0
  45. package/tests/Room.test.ts +80 -0
  46. package/tests/Snapshot.test.ts +19 -0
  47. package/tsconfig.json +28 -0
  48. package/tsup.config.ts +14 -0
package/dist/index.js ADDED
@@ -0,0 +1,1632 @@
1
+ import pino from 'pino';
2
+
3
+ var __defProp = Object.defineProperty;
4
+ var __name = (target, value) => __defProp(target, "name", { value, configurable: true });
5
+ var defaultLogger = pino({
6
+ level: process.env.LOG_LEVEL || "info",
7
+ transport: process.env.NODE_ENV === "development" ? {
8
+ target: "pino-pretty",
9
+ options: {
10
+ colorize: true,
11
+ translateTime: "HH:MM:ss",
12
+ ignore: "pid,hostname"
13
+ }
14
+ } : void 0,
15
+ base: {
16
+ package: "@gamerstake/game-core"
17
+ }
18
+ });
19
+ var logger = defaultLogger;
20
+ function setLogger(customLogger) {
21
+ logger = customLogger;
22
+ }
23
+ __name(setLogger, "setLogger");
24
+ function createChildLogger(bindings) {
25
+ return logger.child(bindings);
26
+ }
27
+ __name(createChildLogger, "createChildLogger");
28
+ var Logger = class _Logger {
29
+ static {
30
+ __name(this, "Logger");
31
+ }
32
+ logger;
33
+ constructor(bindings) {
34
+ this.logger = bindings ? logger.child(bindings) : logger;
35
+ }
36
+ debug(objOrMsg, msg) {
37
+ if (typeof objOrMsg === "string") {
38
+ this.logger.debug(objOrMsg);
39
+ } else {
40
+ this.logger.debug(objOrMsg, msg);
41
+ }
42
+ }
43
+ info(objOrMsg, msg) {
44
+ if (typeof objOrMsg === "string") {
45
+ this.logger.info(objOrMsg);
46
+ } else {
47
+ this.logger.info(objOrMsg, msg);
48
+ }
49
+ }
50
+ warn(objOrMsg, msg) {
51
+ if (typeof objOrMsg === "string") {
52
+ this.logger.warn(objOrMsg);
53
+ } else {
54
+ this.logger.warn(objOrMsg, msg);
55
+ }
56
+ }
57
+ error(objOrMsg, msg) {
58
+ if (typeof objOrMsg === "string") {
59
+ this.logger.error(objOrMsg);
60
+ } else {
61
+ this.logger.error(objOrMsg, msg);
62
+ }
63
+ }
64
+ child(bindings) {
65
+ const childLogger = new _Logger();
66
+ childLogger.logger = this.logger.child(bindings);
67
+ return childLogger;
68
+ }
69
+ };
70
+
71
+ // src/core/GameLoop.ts
72
+ var GameLoop = class {
73
+ static {
74
+ __name(this, "GameLoop");
75
+ }
76
+ tickRate;
77
+ tickMs;
78
+ tickNumber = 0;
79
+ lastTickTime = 0;
80
+ running = false;
81
+ timeout = null;
82
+ handlers = [];
83
+ // Performance metrics
84
+ tickTimes = [];
85
+ metricsWindowSize = 100;
86
+ /**
87
+ * Create a new game loop.
88
+ *
89
+ * @param tickRate - Ticks per second (default: 20)
90
+ */
91
+ constructor(tickRate = 20) {
92
+ this.tickRate = tickRate;
93
+ this.tickMs = 1e3 / tickRate;
94
+ }
95
+ /**
96
+ * Register a tick handler.
97
+ */
98
+ addHandler(handler) {
99
+ this.handlers.push(handler);
100
+ }
101
+ /**
102
+ * Remove a tick handler.
103
+ */
104
+ removeHandler(handler) {
105
+ const index = this.handlers.indexOf(handler);
106
+ if (index !== -1) {
107
+ this.handlers.splice(index, 1);
108
+ }
109
+ }
110
+ /**
111
+ * Start the game loop.
112
+ */
113
+ start() {
114
+ if (this.running) {
115
+ logger.warn("Game loop already running");
116
+ return;
117
+ }
118
+ this.running = true;
119
+ this.lastTickTime = performance.now();
120
+ this.tickNumber = 0;
121
+ logger.info({
122
+ tickRate: this.tickRate,
123
+ tickMs: this.tickMs
124
+ }, "Game loop started");
125
+ this.scheduleNextTick();
126
+ }
127
+ /**
128
+ * Stop the game loop.
129
+ */
130
+ stop() {
131
+ this.running = false;
132
+ if (this.timeout) {
133
+ clearTimeout(this.timeout);
134
+ this.timeout = null;
135
+ }
136
+ logger.info({
137
+ totalTicks: this.tickNumber
138
+ }, "Game loop stopped");
139
+ }
140
+ /**
141
+ * Check if loop is running.
142
+ */
143
+ isRunning() {
144
+ return this.running;
145
+ }
146
+ /**
147
+ * Get current tick number.
148
+ */
149
+ getCurrentTick() {
150
+ return this.tickNumber;
151
+ }
152
+ /**
153
+ * Get tick rate (TPS).
154
+ */
155
+ getTickRate() {
156
+ return this.tickRate;
157
+ }
158
+ /**
159
+ * Get tick duration (ms).
160
+ */
161
+ getTickMs() {
162
+ return this.tickMs;
163
+ }
164
+ /**
165
+ * Get performance metrics.
166
+ */
167
+ getMetrics() {
168
+ if (this.tickTimes.length === 0) {
169
+ return {
170
+ avgTickTime: 0,
171
+ maxTickTime: 0,
172
+ minTickTime: 0,
173
+ ticksPerSecond: 0
174
+ };
175
+ }
176
+ const sum = this.tickTimes.reduce((a, b) => a + b, 0);
177
+ const avg = sum / this.tickTimes.length;
178
+ return {
179
+ avgTickTime: avg,
180
+ maxTickTime: Math.max(...this.tickTimes),
181
+ minTickTime: Math.min(...this.tickTimes),
182
+ ticksPerSecond: 1e3 / avg
183
+ };
184
+ }
185
+ /**
186
+ * Schedule the next tick with drift compensation.
187
+ */
188
+ scheduleNextTick() {
189
+ if (!this.running) return;
190
+ const now = performance.now();
191
+ const elapsed = now - this.lastTickTime;
192
+ const drift = elapsed - this.tickMs;
193
+ const nextDelay = Math.max(0, this.tickMs - drift);
194
+ this.timeout = setTimeout(() => this.tick(), nextDelay);
195
+ }
196
+ /**
197
+ * Execute one tick.
198
+ */
199
+ tick() {
200
+ const tickStart = performance.now();
201
+ const deltaMs = tickStart - this.lastTickTime;
202
+ this.lastTickTime = tickStart;
203
+ this.tickNumber++;
204
+ try {
205
+ for (const handler of this.handlers) {
206
+ handler.onTick(this.tickNumber, deltaMs);
207
+ }
208
+ } catch (error) {
209
+ logger.error({
210
+ error,
211
+ tick: this.tickNumber
212
+ }, "Error in tick handler");
213
+ }
214
+ const tickDuration = performance.now() - tickStart;
215
+ this.recordTickTime(tickDuration);
216
+ if (tickDuration > this.tickMs * 0.8) {
217
+ logger.warn({
218
+ tickNumber: this.tickNumber,
219
+ duration: tickDuration,
220
+ budget: this.tickMs
221
+ }, "Tick took too long");
222
+ }
223
+ this.scheduleNextTick();
224
+ }
225
+ /**
226
+ * Record tick time for metrics.
227
+ */
228
+ recordTickTime(duration) {
229
+ this.tickTimes.push(duration);
230
+ if (this.tickTimes.length > this.metricsWindowSize) {
231
+ this.tickTimes.shift();
232
+ }
233
+ }
234
+ };
235
+
236
+ // src/entities/Registry.ts
237
+ var Registry = class {
238
+ static {
239
+ __name(this, "Registry");
240
+ }
241
+ entities = /* @__PURE__ */ new Map();
242
+ dirtyEntities = /* @__PURE__ */ new Set();
243
+ // Pre-allocated buffers to avoid allocations in hot paths
244
+ dirtyBuffer = [];
245
+ allBuffer = [];
246
+ /**
247
+ * Add an entity to the registry.
248
+ */
249
+ add(entity) {
250
+ if (this.entities.has(entity.id)) {
251
+ logger.warn({
252
+ entityId: entity.id
253
+ }, "Entity already exists in registry");
254
+ return;
255
+ }
256
+ this.entities.set(entity.id, entity);
257
+ this.dirtyEntities.add(entity.id);
258
+ logger.debug({
259
+ entityId: entity.id
260
+ }, "Entity added to registry");
261
+ }
262
+ /**
263
+ * Remove an entity from the registry.
264
+ */
265
+ remove(entityId) {
266
+ const entity = this.entities.get(entityId);
267
+ if (!entity) return false;
268
+ this.entities.delete(entityId);
269
+ this.dirtyEntities.delete(entityId);
270
+ logger.debug({
271
+ entityId
272
+ }, "Entity removed from registry");
273
+ return true;
274
+ }
275
+ /**
276
+ * Get an entity by ID.
277
+ */
278
+ get(entityId) {
279
+ return this.entities.get(entityId);
280
+ }
281
+ /**
282
+ * Check if entity exists.
283
+ */
284
+ has(entityId) {
285
+ return this.entities.has(entityId);
286
+ }
287
+ /**
288
+ * Mark an entity as dirty (needs broadcast).
289
+ */
290
+ markDirty(entityId) {
291
+ if (this.entities.has(entityId)) {
292
+ this.dirtyEntities.add(entityId);
293
+ }
294
+ }
295
+ /**
296
+ * Get all dirty entities and clear dirty flags.
297
+ * OPTIMIZED: Reuses buffer array to avoid allocations.
298
+ *
299
+ * @returns Array of dirty entities (reused buffer - don't store reference!)
300
+ */
301
+ getDirtyEntities() {
302
+ this.dirtyBuffer.length = 0;
303
+ for (const entityId of this.dirtyEntities) {
304
+ const entity = this.entities.get(entityId);
305
+ if (entity) {
306
+ this.dirtyBuffer.push(entity);
307
+ entity.markClean();
308
+ }
309
+ }
310
+ this.dirtyEntities.clear();
311
+ return this.dirtyBuffer;
312
+ }
313
+ /**
314
+ * Get all entities.
315
+ * OPTIMIZED: Reuses buffer array to avoid allocations.
316
+ *
317
+ * @returns Array of all entities (reused buffer - don't store reference!)
318
+ */
319
+ getAll() {
320
+ this.allBuffer.length = 0;
321
+ for (const entity of this.entities.values()) {
322
+ this.allBuffer.push(entity);
323
+ }
324
+ return this.allBuffer;
325
+ }
326
+ /**
327
+ * Get entity count.
328
+ */
329
+ size() {
330
+ return this.entities.size;
331
+ }
332
+ /**
333
+ * Get dirty entity count.
334
+ */
335
+ dirtyCount() {
336
+ return this.dirtyEntities.size;
337
+ }
338
+ /**
339
+ * Clear all entities.
340
+ */
341
+ clear() {
342
+ this.entities.clear();
343
+ this.dirtyEntities.clear();
344
+ logger.debug("Registry cleared");
345
+ }
346
+ /**
347
+ * Execute callback for each entity.
348
+ */
349
+ forEach(callback) {
350
+ for (const entity of this.entities.values()) {
351
+ callback(entity);
352
+ }
353
+ }
354
+ /**
355
+ * Filter entities by predicate.
356
+ *
357
+ * @param predicate - Filter function
358
+ * @returns New array of matching entities
359
+ */
360
+ filter(predicate) {
361
+ const result = [];
362
+ for (const entity of this.entities.values()) {
363
+ if (predicate(entity)) {
364
+ result.push(entity);
365
+ }
366
+ }
367
+ return result;
368
+ }
369
+ };
370
+
371
+ // src/spatial/Grid.ts
372
+ var Grid = class {
373
+ static {
374
+ __name(this, "Grid");
375
+ }
376
+ cellSize;
377
+ cells = /* @__PURE__ */ new Map();
378
+ entityCells = /* @__PURE__ */ new Map();
379
+ /**
380
+ * Create a new spatial grid.
381
+ *
382
+ * @param cellSize - Size of each cell in world units (default: 512)
383
+ */
384
+ constructor(cellSize = 512) {
385
+ this.cellSize = cellSize;
386
+ }
387
+ /**
388
+ * Convert world coordinates to cell key.
389
+ */
390
+ getCellKey(worldX, worldY) {
391
+ const cellX = Math.floor(worldX / this.cellSize);
392
+ const cellY = Math.floor(worldY / this.cellSize);
393
+ return `${cellX},${cellY}`;
394
+ }
395
+ /**
396
+ * Get or create a cell at world coordinates.
397
+ */
398
+ getCell(worldX, worldY) {
399
+ const key = this.getCellKey(worldX, worldY);
400
+ let cell = this.cells.get(key);
401
+ if (!cell) {
402
+ const cellX = Math.floor(worldX / this.cellSize);
403
+ const cellY = Math.floor(worldY / this.cellSize);
404
+ cell = {
405
+ x: cellX,
406
+ y: cellY,
407
+ entities: /* @__PURE__ */ new Set()
408
+ };
409
+ this.cells.set(key, cell);
410
+ }
411
+ return cell;
412
+ }
413
+ /**
414
+ * Add an entity to the grid.
415
+ */
416
+ addEntity(entityId, x, y) {
417
+ const cell = this.getCell(x, y);
418
+ cell.entities.add(entityId);
419
+ this.entityCells.set(entityId, this.getCellKey(x, y));
420
+ logger.debug({
421
+ entityId,
422
+ cellX: cell.x,
423
+ cellY: cell.y
424
+ }, "Entity added to grid");
425
+ }
426
+ /**
427
+ * Remove an entity from the grid.
428
+ */
429
+ removeEntity(entityId) {
430
+ const cellKey = this.entityCells.get(entityId);
431
+ if (!cellKey) return;
432
+ const cell = this.cells.get(cellKey);
433
+ if (cell) {
434
+ cell.entities.delete(entityId);
435
+ if (cell.entities.size === 0) {
436
+ this.cells.delete(cellKey);
437
+ }
438
+ }
439
+ this.entityCells.delete(entityId);
440
+ logger.debug({
441
+ entityId
442
+ }, "Entity removed from grid");
443
+ }
444
+ /**
445
+ * Move an entity to a new position.
446
+ * Returns true if the entity changed cells.
447
+ */
448
+ moveEntity(entityId, oldX, oldY, newX, newY) {
449
+ const oldKey = this.getCellKey(oldX, oldY);
450
+ const newKey = this.getCellKey(newX, newY);
451
+ if (oldKey === newKey) {
452
+ return false;
453
+ }
454
+ const oldCell = this.cells.get(oldKey);
455
+ if (oldCell) {
456
+ oldCell.entities.delete(entityId);
457
+ if (oldCell.entities.size === 0) {
458
+ this.cells.delete(oldKey);
459
+ }
460
+ }
461
+ const newCell = this.getCell(newX, newY);
462
+ newCell.entities.add(entityId);
463
+ this.entityCells.set(entityId, newKey);
464
+ logger.debug({
465
+ entityId,
466
+ oldKey,
467
+ newKey
468
+ }, "Entity moved to new cell");
469
+ return true;
470
+ }
471
+ /**
472
+ * Get all entities in nearby cells.
473
+ *
474
+ * @param worldX - World X coordinate
475
+ * @param worldY - World Y coordinate
476
+ * @param range - Range in grid cells (default: 1 = 3x3 area)
477
+ */
478
+ getNearbyEntities(worldX, worldY, range = 1) {
479
+ const centerCellX = Math.floor(worldX / this.cellSize);
480
+ const centerCellY = Math.floor(worldY / this.cellSize);
481
+ const entities = [];
482
+ for (let dx = -range; dx <= range; dx++) {
483
+ for (let dy = -range; dy <= range; dy++) {
484
+ const key = `${centerCellX + dx},${centerCellY + dy}`;
485
+ const cell = this.cells.get(key);
486
+ if (cell) {
487
+ entities.push(...cell.entities);
488
+ }
489
+ }
490
+ }
491
+ return entities;
492
+ }
493
+ /**
494
+ * Get entities in a specific cell.
495
+ */
496
+ getEntitiesInCell(worldX, worldY) {
497
+ const cell = this.cells.get(this.getCellKey(worldX, worldY));
498
+ return cell ? Array.from(cell.entities) : [];
499
+ }
500
+ /**
501
+ * Get the cell an entity is in.
502
+ */
503
+ getEntityCell(entityId) {
504
+ return this.entityCells.get(entityId);
505
+ }
506
+ /**
507
+ * Get total number of active cells.
508
+ */
509
+ getCellCount() {
510
+ return this.cells.size;
511
+ }
512
+ /**
513
+ * Get total entities in grid.
514
+ */
515
+ getEntityCount() {
516
+ return this.entityCells.size;
517
+ }
518
+ /**
519
+ * Clear all cells and entities.
520
+ */
521
+ clear() {
522
+ this.cells.clear();
523
+ this.entityCells.clear();
524
+ logger.debug("Grid cleared");
525
+ }
526
+ };
527
+
528
+ // src/utils/RingBuffer.ts
529
+ var RingBuffer = class {
530
+ static {
531
+ __name(this, "RingBuffer");
532
+ }
533
+ capacity;
534
+ buffer;
535
+ head;
536
+ tail;
537
+ count;
538
+ constructor(capacity) {
539
+ this.capacity = capacity;
540
+ this.head = 0;
541
+ this.tail = 0;
542
+ this.count = 0;
543
+ this.buffer = new Array(capacity);
544
+ }
545
+ /**
546
+ * Add item to buffer. O(1)
547
+ * Returns false if buffer is full.
548
+ */
549
+ push(item) {
550
+ if (this.count >= this.capacity) {
551
+ return false;
552
+ }
553
+ this.buffer[this.head] = item;
554
+ this.head = (this.head + 1) % this.capacity;
555
+ this.count++;
556
+ return true;
557
+ }
558
+ /**
559
+ * Add item, overwriting oldest if full. O(1)
560
+ */
561
+ pushOverwrite(item) {
562
+ let dropped;
563
+ if (this.count >= this.capacity) {
564
+ dropped = this.buffer[this.tail];
565
+ this.tail = (this.tail + 1) % this.capacity;
566
+ this.count--;
567
+ }
568
+ this.buffer[this.head] = item;
569
+ this.head = (this.head + 1) % this.capacity;
570
+ this.count++;
571
+ return dropped;
572
+ }
573
+ /**
574
+ * Remove and return oldest item. O(1)
575
+ */
576
+ shift() {
577
+ if (this.count === 0) {
578
+ return void 0;
579
+ }
580
+ const item = this.buffer[this.tail];
581
+ this.buffer[this.tail] = void 0;
582
+ this.tail = (this.tail + 1) % this.capacity;
583
+ this.count--;
584
+ return item;
585
+ }
586
+ /**
587
+ * Peek at oldest item without removing. O(1)
588
+ */
589
+ peek() {
590
+ if (this.count === 0) {
591
+ return void 0;
592
+ }
593
+ return this.buffer[this.tail];
594
+ }
595
+ /**
596
+ * Get current size.
597
+ */
598
+ size() {
599
+ return this.count;
600
+ }
601
+ /**
602
+ * Check if empty.
603
+ */
604
+ isEmpty() {
605
+ return this.count === 0;
606
+ }
607
+ /**
608
+ * Check if full.
609
+ */
610
+ isFull() {
611
+ return this.count >= this.capacity;
612
+ }
613
+ /**
614
+ * Clear all items. O(1)
615
+ */
616
+ clear() {
617
+ this.head = 0;
618
+ this.tail = 0;
619
+ this.count = 0;
620
+ }
621
+ /**
622
+ * Get capacity.
623
+ */
624
+ getCapacity() {
625
+ return this.capacity;
626
+ }
627
+ };
628
+
629
+ // src/input/InputQueue.ts
630
+ var InputQueue = class {
631
+ static {
632
+ __name(this, "InputQueue");
633
+ }
634
+ queues = /* @__PURE__ */ new Map();
635
+ maxQueueSize;
636
+ /**
637
+ * Create a new input queue manager.
638
+ *
639
+ * @param maxQueueSize - Maximum inputs per player (default: 100)
640
+ */
641
+ constructor(maxQueueSize = 100) {
642
+ this.maxQueueSize = maxQueueSize;
643
+ }
644
+ /**
645
+ * Get or create a player's queue.
646
+ */
647
+ getQueue(playerId) {
648
+ let queue = this.queues.get(playerId);
649
+ if (!queue) {
650
+ queue = new RingBuffer(this.maxQueueSize);
651
+ this.queues.set(playerId, queue);
652
+ }
653
+ return queue;
654
+ }
655
+ /**
656
+ * Add an input to a player's queue. O(1)
657
+ */
658
+ push(playerId, input) {
659
+ const queue = this.getQueue(playerId);
660
+ const dropped = queue.pushOverwrite(input);
661
+ if (dropped) {
662
+ logger.debug({
663
+ playerId
664
+ }, "Input queue overflow, dropped oldest");
665
+ }
666
+ }
667
+ /**
668
+ * Get and remove the next input for a player. O(1)
669
+ */
670
+ pop(playerId) {
671
+ const queue = this.queues.get(playerId);
672
+ if (!queue) return void 0;
673
+ return queue.shift();
674
+ }
675
+ /**
676
+ * Peek at the next input without removing. O(1)
677
+ */
678
+ peek(playerId) {
679
+ const queue = this.queues.get(playerId);
680
+ return queue?.peek();
681
+ }
682
+ /**
683
+ * Get all inputs for a player and clear.
684
+ */
685
+ drain(playerId) {
686
+ const queue = this.queues.get(playerId);
687
+ if (!queue) return [];
688
+ const inputs = [];
689
+ while (!queue.isEmpty()) {
690
+ const input = queue.shift();
691
+ if (input) inputs.push(input);
692
+ }
693
+ return inputs;
694
+ }
695
+ /**
696
+ * Get queue size for a player.
697
+ */
698
+ size(playerId) {
699
+ return this.queues.get(playerId)?.size() ?? 0;
700
+ }
701
+ /**
702
+ * Clear a player's queue.
703
+ */
704
+ clear(playerId) {
705
+ const queue = this.queues.get(playerId);
706
+ if (queue) {
707
+ queue.clear();
708
+ }
709
+ }
710
+ /**
711
+ * Remove a player's queue entirely.
712
+ */
713
+ remove(playerId) {
714
+ this.queues.delete(playerId);
715
+ }
716
+ /**
717
+ * Clear all queues.
718
+ */
719
+ clearAll() {
720
+ this.queues.clear();
721
+ }
722
+ /**
723
+ * Get total queued inputs across all players.
724
+ */
725
+ getTotalSize() {
726
+ let total = 0;
727
+ for (const queue of this.queues.values()) {
728
+ total += queue.size();
729
+ }
730
+ return total;
731
+ }
732
+ };
733
+
734
+ // src/network/Network.ts
735
+ var Network = class {
736
+ static {
737
+ __name(this, "Network");
738
+ }
739
+ io = null;
740
+ sockets = /* @__PURE__ */ new Map();
741
+ /**
742
+ * Set the Socket.io server instance.
743
+ */
744
+ setServer(io) {
745
+ this.io = io;
746
+ }
747
+ /**
748
+ * Register a socket connection.
749
+ */
750
+ registerSocket(playerId, socket) {
751
+ this.sockets.set(playerId, socket);
752
+ logger.debug({
753
+ playerId
754
+ }, "Socket registered");
755
+ }
756
+ /**
757
+ * Unregister a socket connection.
758
+ */
759
+ unregisterSocket(playerId) {
760
+ this.sockets.delete(playerId);
761
+ logger.debug({
762
+ playerId
763
+ }, "Socket unregistered");
764
+ }
765
+ /**
766
+ * Get a socket by player ID.
767
+ */
768
+ getSocket(playerId) {
769
+ return this.sockets.get(playerId);
770
+ }
771
+ /**
772
+ * Broadcast event to all connected players.
773
+ */
774
+ broadcast(event) {
775
+ if (!this.io) {
776
+ logger.warn("Cannot broadcast - no Socket.io server set");
777
+ return;
778
+ }
779
+ this.io.emit(event.op, event);
780
+ logger.debug({
781
+ opcode: event.op
782
+ }, "Broadcast event");
783
+ }
784
+ /**
785
+ * Broadcast event to all players in a room.
786
+ */
787
+ broadcastToRoom(roomId, event) {
788
+ if (!this.io) {
789
+ logger.warn("Cannot broadcast - no Socket.io server set");
790
+ return;
791
+ }
792
+ this.io.to(roomId).emit(event.op, event);
793
+ logger.debug({
794
+ roomId,
795
+ opcode: event.op
796
+ }, "Broadcast to room");
797
+ }
798
+ /**
799
+ * Send event to a specific player.
800
+ */
801
+ sendTo(playerId, event) {
802
+ const socket = this.sockets.get(playerId);
803
+ if (!socket) {
804
+ logger.warn({
805
+ playerId
806
+ }, "Cannot send - socket not found");
807
+ return;
808
+ }
809
+ socket.emit(event.op, event);
810
+ logger.debug({
811
+ playerId,
812
+ opcode: event.op
813
+ }, "Sent to player");
814
+ }
815
+ /**
816
+ * Send event to multiple players.
817
+ */
818
+ sendToMany(playerIds, event) {
819
+ for (const playerId of playerIds) {
820
+ this.sendTo(playerId, event);
821
+ }
822
+ }
823
+ /**
824
+ * Get connected player count.
825
+ */
826
+ getPlayerCount() {
827
+ return this.sockets.size;
828
+ }
829
+ /**
830
+ * Clear all sockets.
831
+ */
832
+ clear() {
833
+ this.sockets.clear();
834
+ }
835
+ };
836
+
837
+ // src/network/Snapshot.ts
838
+ var Snapshot = class {
839
+ static {
840
+ __name(this, "Snapshot");
841
+ }
842
+ /**
843
+ * Create a full state snapshot from entities.
844
+ *
845
+ * @param tick - Current tick number
846
+ * @param entities - Array of entities to snapshot
847
+ * @returns Full state snapshot
848
+ */
849
+ static create(tick, entities) {
850
+ return {
851
+ tick,
852
+ timestamp: Date.now(),
853
+ entities: entities.map((e) => this.entityToSnapshot(e))
854
+ };
855
+ }
856
+ /**
857
+ * Convert an entity to a snapshot.
858
+ */
859
+ static entityToSnapshot(entity) {
860
+ return {
861
+ id: entity.id,
862
+ x: entity.x,
863
+ y: entity.y,
864
+ vx: entity.vx,
865
+ vy: entity.vy
866
+ };
867
+ }
868
+ /**
869
+ * Create a delta snapshot (only dirty entities).
870
+ *
871
+ * @param tick - Current tick number
872
+ * @param dirtyEntities - Array of dirty entities
873
+ * @returns Delta snapshot
874
+ */
875
+ static createDelta(tick, dirtyEntities) {
876
+ return {
877
+ tick,
878
+ timestamp: Date.now(),
879
+ entities: dirtyEntities.map((e) => this.entityToSnapshot(e))
880
+ };
881
+ }
882
+ };
883
+
884
+ // src/core/Room.ts
885
+ var Room = class {
886
+ static {
887
+ __name(this, "Room");
888
+ }
889
+ /** Room identifier */
890
+ id;
891
+ /** Game-specific logic */
892
+ rules;
893
+ // Core systems
894
+ registry;
895
+ grid;
896
+ loop;
897
+ inputQueue;
898
+ network;
899
+ // State
900
+ players = /* @__PURE__ */ new Map();
901
+ tickCount = 0;
902
+ startTime = 0;
903
+ config;
904
+ /**
905
+ * Create a new room.
906
+ *
907
+ * @param id - Unique room identifier
908
+ * @param rules - Game-specific logic
909
+ * @param config - Room configuration
910
+ */
911
+ constructor(id, rules, config) {
912
+ this.id = id;
913
+ this.rules = rules;
914
+ this.config = {
915
+ tickRate: config?.tickRate ?? 20,
916
+ cellSize: config?.cellSize ?? 512,
917
+ maxInputQueueSize: config?.maxInputQueueSize ?? 100,
918
+ maxEntities: config?.maxEntities ?? 1e3,
919
+ visibilityRange: config?.visibilityRange ?? 1
920
+ };
921
+ this.registry = new Registry();
922
+ this.grid = new Grid(this.config.cellSize);
923
+ this.loop = new GameLoop(this.config.tickRate);
924
+ this.inputQueue = new InputQueue(this.config.maxInputQueueSize);
925
+ this.network = new Network();
926
+ this.loop.addHandler(this);
927
+ logger.info({
928
+ roomId: id,
929
+ config: this.config
930
+ }, "Room created");
931
+ }
932
+ /**
933
+ * Start the room (begins tick loop).
934
+ */
935
+ start() {
936
+ this.startTime = Date.now();
937
+ try {
938
+ this.rules.onRoomCreated(this);
939
+ } catch (error) {
940
+ logger.error({
941
+ error,
942
+ roomId: this.id
943
+ }, "Error in onRoomCreated");
944
+ }
945
+ this.loop.start();
946
+ logger.info({
947
+ roomId: this.id
948
+ }, "Room started");
949
+ }
950
+ /**
951
+ * Stop the room (ends tick loop).
952
+ */
953
+ stop() {
954
+ this.loop.stop();
955
+ logger.info({
956
+ roomId: this.id,
957
+ totalTicks: this.tickCount
958
+ }, "Room stopped");
959
+ }
960
+ /**
961
+ * Add a player to the room.
962
+ */
963
+ addPlayer(player) {
964
+ if (this.players.has(player.id)) {
965
+ logger.warn({
966
+ playerId: player.id
967
+ }, "Player already in room");
968
+ return;
969
+ }
970
+ this.players.set(player.id, player);
971
+ this.registry.add(player);
972
+ this.grid.addEntity(player.id, player.x, player.y);
973
+ try {
974
+ this.rules.onPlayerJoin(this, player);
975
+ } catch (error) {
976
+ logger.error({
977
+ error,
978
+ playerId: player.id
979
+ }, "Error in onPlayerJoin");
980
+ }
981
+ logger.info({
982
+ roomId: this.id,
983
+ playerId: player.id
984
+ }, "Player joined");
985
+ }
986
+ /**
987
+ * Remove a player from the room.
988
+ */
989
+ removePlayer(playerId) {
990
+ const player = this.players.get(playerId);
991
+ if (!player) return;
992
+ this.players.delete(playerId);
993
+ this.registry.remove(playerId);
994
+ this.grid.removeEntity(playerId);
995
+ this.inputQueue.remove(playerId);
996
+ try {
997
+ this.rules.onPlayerLeave(this, playerId);
998
+ } catch (error) {
999
+ logger.error({
1000
+ error,
1001
+ playerId
1002
+ }, "Error in onPlayerLeave");
1003
+ }
1004
+ logger.info({
1005
+ roomId: this.id,
1006
+ playerId
1007
+ }, "Player left");
1008
+ }
1009
+ /**
1010
+ * Spawn a non-player entity.
1011
+ */
1012
+ spawnEntity(entity) {
1013
+ if (this.registry.size() >= this.config.maxEntities) {
1014
+ logger.warn({
1015
+ roomId: this.id
1016
+ }, "Max entities reached, cannot spawn");
1017
+ return;
1018
+ }
1019
+ this.registry.add(entity);
1020
+ this.grid.addEntity(entity.id, entity.x, entity.y);
1021
+ logger.debug({
1022
+ entityId: entity.id
1023
+ }, "Entity spawned");
1024
+ }
1025
+ /**
1026
+ * Destroy an entity.
1027
+ */
1028
+ destroyEntity(entityId) {
1029
+ const entity = this.registry.get(entityId);
1030
+ if (!entity) return;
1031
+ this.registry.remove(entityId);
1032
+ this.grid.removeEntity(entityId);
1033
+ logger.debug({
1034
+ entityId
1035
+ }, "Entity destroyed");
1036
+ }
1037
+ /**
1038
+ * Queue a player input.
1039
+ */
1040
+ queueInput(playerId, command) {
1041
+ if (!this.players.has(playerId)) {
1042
+ logger.warn({
1043
+ playerId
1044
+ }, "Input from non-player");
1045
+ return;
1046
+ }
1047
+ this.inputQueue.push(playerId, command);
1048
+ }
1049
+ /**
1050
+ * Broadcast event to all players.
1051
+ */
1052
+ broadcast(event) {
1053
+ this.network.broadcastToRoom(this.id, event);
1054
+ }
1055
+ /**
1056
+ * Send event to specific player.
1057
+ */
1058
+ sendTo(playerId, event) {
1059
+ this.network.sendTo(playerId, event);
1060
+ }
1061
+ /**
1062
+ * Get full state snapshot.
1063
+ */
1064
+ getSnapshot() {
1065
+ return Snapshot.create(this.tickCount, this.registry.getAll());
1066
+ }
1067
+ /**
1068
+ * Get delta snapshot (only dirty entities).
1069
+ */
1070
+ getDeltaSnapshot() {
1071
+ return Snapshot.createDelta(this.tickCount, this.registry.getDirtyEntities());
1072
+ }
1073
+ /**
1074
+ * Main tick function (called by GameLoop).
1075
+ */
1076
+ onTick(tickNumber, deltaMs) {
1077
+ this.tickCount = tickNumber;
1078
+ for (const [playerId] of this.players) {
1079
+ const inputs = this.inputQueue.drain(playerId);
1080
+ for (const input of inputs) {
1081
+ try {
1082
+ this.rules.onCommand(this, playerId, input);
1083
+ } catch (error) {
1084
+ logger.error({
1085
+ error,
1086
+ playerId,
1087
+ command: input.type
1088
+ }, "Error in onCommand");
1089
+ }
1090
+ }
1091
+ }
1092
+ try {
1093
+ this.rules.onTick(this, deltaMs);
1094
+ } catch (error) {
1095
+ logger.error({
1096
+ error,
1097
+ tick: tickNumber
1098
+ }, "Error in onTick");
1099
+ }
1100
+ const dirtyEntities = this.registry.getDirtyEntities();
1101
+ if (dirtyEntities.length > 0) {
1102
+ const delta = Snapshot.createDelta(this.tickCount, dirtyEntities);
1103
+ this.broadcast({
1104
+ op: "S_UPDATE",
1105
+ ...delta
1106
+ });
1107
+ }
1108
+ try {
1109
+ if (this.rules.shouldEndRoom(this)) {
1110
+ logger.info({
1111
+ roomId: this.id
1112
+ }, "Room ending per game rules");
1113
+ this.stop();
1114
+ }
1115
+ } catch (error) {
1116
+ logger.error({
1117
+ error
1118
+ }, "Error in shouldEndRoom");
1119
+ }
1120
+ }
1121
+ // Getters
1122
+ /**
1123
+ * Get all players.
1124
+ */
1125
+ getPlayers() {
1126
+ return this.players;
1127
+ }
1128
+ /**
1129
+ * Get entity registry.
1130
+ */
1131
+ getRegistry() {
1132
+ return this.registry;
1133
+ }
1134
+ /**
1135
+ * Get spatial grid.
1136
+ */
1137
+ getGrid() {
1138
+ return this.grid;
1139
+ }
1140
+ /**
1141
+ * Get network layer.
1142
+ */
1143
+ getNetwork() {
1144
+ return this.network;
1145
+ }
1146
+ /**
1147
+ * Get input queue.
1148
+ */
1149
+ getInputQueue() {
1150
+ return this.inputQueue;
1151
+ }
1152
+ /**
1153
+ * Get current tick number.
1154
+ */
1155
+ getTickCount() {
1156
+ return this.tickCount;
1157
+ }
1158
+ /**
1159
+ * Get uptime in milliseconds.
1160
+ */
1161
+ getUptime() {
1162
+ return Date.now() - this.startTime;
1163
+ }
1164
+ /**
1165
+ * Check if room is running.
1166
+ */
1167
+ isRunning() {
1168
+ return this.loop.isRunning();
1169
+ }
1170
+ /**
1171
+ * Get room configuration.
1172
+ */
1173
+ getConfig() {
1174
+ return {
1175
+ ...this.config
1176
+ };
1177
+ }
1178
+ /**
1179
+ * Get room metrics.
1180
+ */
1181
+ getMetrics() {
1182
+ const loopMetrics = this.loop.getMetrics();
1183
+ return {
1184
+ roomId: this.id,
1185
+ tickCount: this.tickCount,
1186
+ uptime: this.getUptime(),
1187
+ playerCount: this.players.size,
1188
+ entityCount: this.registry.size(),
1189
+ cellCount: this.grid.getCellCount(),
1190
+ queuedInputs: this.inputQueue.getTotalSize(),
1191
+ ...loopMetrics
1192
+ };
1193
+ }
1194
+ };
1195
+
1196
+ // src/core/GameServer.ts
1197
+ var GameServer = class {
1198
+ static {
1199
+ __name(this, "GameServer");
1200
+ }
1201
+ rooms = /* @__PURE__ */ new Map();
1202
+ io = null;
1203
+ /**
1204
+ * Set the Socket.io server instance.
1205
+ *
1206
+ * This should be called before creating rooms to enable
1207
+ * network functionality.
1208
+ */
1209
+ setServer(io) {
1210
+ this.io = io;
1211
+ for (const room of this.rooms.values()) {
1212
+ room.getNetwork().setServer(io);
1213
+ }
1214
+ logger.info("Socket.io server set");
1215
+ }
1216
+ /**
1217
+ * Create a new room.
1218
+ *
1219
+ * @param id - Unique room identifier
1220
+ * @param rules - Game-specific logic
1221
+ * @param config - Room configuration
1222
+ * @returns The created room
1223
+ * @throws If room with same ID already exists
1224
+ */
1225
+ createRoom(id, rules, config) {
1226
+ if (this.rooms.has(id)) {
1227
+ throw new Error(`Room ${id} already exists`);
1228
+ }
1229
+ const room = new Room(id, rules, config);
1230
+ if (this.io) {
1231
+ room.getNetwork().setServer(this.io);
1232
+ }
1233
+ this.rooms.set(id, room);
1234
+ room.start();
1235
+ logger.info({
1236
+ roomId: id
1237
+ }, "Room created and started");
1238
+ return room;
1239
+ }
1240
+ /**
1241
+ * Destroy a room.
1242
+ *
1243
+ * Stops the room's tick loop and removes it from the server.
1244
+ *
1245
+ * @param id - Room identifier
1246
+ * @returns True if room was destroyed, false if not found
1247
+ */
1248
+ destroyRoom(id) {
1249
+ const room = this.rooms.get(id);
1250
+ if (!room) {
1251
+ logger.warn({
1252
+ roomId: id
1253
+ }, "Cannot destroy - room not found");
1254
+ return false;
1255
+ }
1256
+ room.stop();
1257
+ this.rooms.delete(id);
1258
+ logger.info({
1259
+ roomId: id
1260
+ }, "Room destroyed");
1261
+ return true;
1262
+ }
1263
+ /**
1264
+ * Get a room by ID.
1265
+ */
1266
+ getRoom(id) {
1267
+ return this.rooms.get(id);
1268
+ }
1269
+ /**
1270
+ * Get a room with specific entity type.
1271
+ *
1272
+ * @param id - Room identifier
1273
+ * @returns Room cast to specific entity type, or undefined
1274
+ */
1275
+ getRoomAs(id) {
1276
+ return this.rooms.get(id);
1277
+ }
1278
+ /**
1279
+ * Check if room exists.
1280
+ */
1281
+ hasRoom(id) {
1282
+ return this.rooms.has(id);
1283
+ }
1284
+ /**
1285
+ * Get all rooms.
1286
+ */
1287
+ getRooms() {
1288
+ return this.rooms;
1289
+ }
1290
+ /**
1291
+ * Get room count.
1292
+ */
1293
+ getRoomCount() {
1294
+ return this.rooms.size;
1295
+ }
1296
+ /**
1297
+ * Get total player count across all rooms.
1298
+ */
1299
+ getTotalPlayerCount() {
1300
+ let total = 0;
1301
+ for (const room of this.rooms.values()) {
1302
+ total += room.getPlayers().size;
1303
+ }
1304
+ return total;
1305
+ }
1306
+ /**
1307
+ * Get server health metrics.
1308
+ */
1309
+ getMetrics() {
1310
+ return {
1311
+ roomCount: this.rooms.size,
1312
+ totalPlayers: this.getTotalPlayerCount(),
1313
+ rooms: Array.from(this.rooms.values()).map((room) => ({
1314
+ id: room.id,
1315
+ playerCount: room.getPlayers().size,
1316
+ entityCount: room.getRegistry().size(),
1317
+ tickCount: room.getTickCount(),
1318
+ isRunning: room.isRunning(),
1319
+ avgTickTime: room.getMetrics().avgTickTime,
1320
+ ticksPerSecond: room.getMetrics().ticksPerSecond
1321
+ }))
1322
+ };
1323
+ }
1324
+ /**
1325
+ * Stop all rooms.
1326
+ */
1327
+ stopAll() {
1328
+ logger.info({
1329
+ roomCount: this.rooms.size
1330
+ }, "Stopping all rooms");
1331
+ for (const room of this.rooms.values()) {
1332
+ room.stop();
1333
+ }
1334
+ }
1335
+ /**
1336
+ * Destroy all rooms.
1337
+ */
1338
+ destroyAll() {
1339
+ logger.info({
1340
+ roomCount: this.rooms.size
1341
+ }, "Destroying all rooms");
1342
+ for (const room of this.rooms.values()) {
1343
+ room.stop();
1344
+ }
1345
+ this.rooms.clear();
1346
+ }
1347
+ };
1348
+
1349
+ // src/entities/Entity.ts
1350
+ var Entity = class {
1351
+ static {
1352
+ __name(this, "Entity");
1353
+ }
1354
+ /** Unique entity identifier */
1355
+ id;
1356
+ /** World X position */
1357
+ x;
1358
+ /** World Y position */
1359
+ y;
1360
+ /** Velocity X (units/second) */
1361
+ vx;
1362
+ /** Velocity Y (units/second) */
1363
+ vy;
1364
+ /** Dirty flag - entity needs to be broadcast */
1365
+ dirty;
1366
+ /** Last update timestamp */
1367
+ lastUpdate;
1368
+ /**
1369
+ * Create a new entity.
1370
+ *
1371
+ * @param id - Unique identifier
1372
+ * @param x - Initial X position
1373
+ * @param y - Initial Y position
1374
+ */
1375
+ constructor(id, x, y) {
1376
+ this.id = id;
1377
+ this.x = x;
1378
+ this.y = y;
1379
+ this.vx = 0;
1380
+ this.vy = 0;
1381
+ this.dirty = true;
1382
+ this.lastUpdate = Date.now();
1383
+ }
1384
+ /**
1385
+ * Update position based on velocity and delta time.
1386
+ *
1387
+ * @param deltaMs - Time since last update (milliseconds)
1388
+ */
1389
+ updatePosition(deltaMs) {
1390
+ if (this.vx !== 0 || this.vy !== 0) {
1391
+ const deltaSec = deltaMs / 1e3;
1392
+ this.x += this.vx * deltaSec;
1393
+ this.y += this.vy * deltaSec;
1394
+ this.dirty = true;
1395
+ this.lastUpdate = Date.now();
1396
+ }
1397
+ }
1398
+ /**
1399
+ * Set velocity.
1400
+ */
1401
+ setVelocity(vx, vy) {
1402
+ if (this.vx !== vx || this.vy !== vy) {
1403
+ this.vx = vx;
1404
+ this.vy = vy;
1405
+ this.dirty = true;
1406
+ this.lastUpdate = Date.now();
1407
+ }
1408
+ }
1409
+ /**
1410
+ * Teleport to position (no velocity integration).
1411
+ */
1412
+ setPosition(x, y) {
1413
+ if (this.x !== x || this.y !== y) {
1414
+ this.x = x;
1415
+ this.y = y;
1416
+ this.dirty = true;
1417
+ this.lastUpdate = Date.now();
1418
+ }
1419
+ }
1420
+ /**
1421
+ * Mark entity as clean (after broadcast).
1422
+ */
1423
+ markClean() {
1424
+ this.dirty = false;
1425
+ }
1426
+ /**
1427
+ * Mark entity as dirty (needs broadcast).
1428
+ */
1429
+ markDirty() {
1430
+ this.dirty = true;
1431
+ }
1432
+ /**
1433
+ * Get entity as plain object for serialization.
1434
+ */
1435
+ toJSON() {
1436
+ return {
1437
+ id: this.id,
1438
+ x: this.x,
1439
+ y: this.y,
1440
+ vx: this.vx,
1441
+ vy: this.vy
1442
+ };
1443
+ }
1444
+ };
1445
+
1446
+ // src/physics/AABB.ts
1447
+ var AABBCollision = class {
1448
+ static {
1449
+ __name(this, "AABBCollision");
1450
+ }
1451
+ /**
1452
+ * Check if two AABBs overlap.
1453
+ */
1454
+ static overlaps(a, b) {
1455
+ const aHalfW = a.width / 2;
1456
+ const aHalfH = a.height / 2;
1457
+ const bHalfW = b.width / 2;
1458
+ const bHalfH = b.height / 2;
1459
+ return Math.abs(a.x - b.x) < aHalfW + bHalfW && Math.abs(a.y - b.y) < aHalfH + bHalfH;
1460
+ }
1461
+ /**
1462
+ * Check if a point is inside an AABB.
1463
+ */
1464
+ static containsPoint(aabb, x, y) {
1465
+ const halfW = aabb.width / 2;
1466
+ const halfH = aabb.height / 2;
1467
+ return x >= aabb.x - halfW && x <= aabb.x + halfW && y >= aabb.y - halfH && y <= aabb.y + halfH;
1468
+ }
1469
+ /**
1470
+ * Create AABB from entity.
1471
+ */
1472
+ static fromEntity(entity, width, height) {
1473
+ return {
1474
+ x: entity.x,
1475
+ y: entity.y,
1476
+ width,
1477
+ height
1478
+ };
1479
+ }
1480
+ /**
1481
+ * Get the distance between two AABBs.
1482
+ * Returns 0 if overlapping.
1483
+ */
1484
+ static distance(a, b) {
1485
+ const dx = Math.abs(a.x - b.x);
1486
+ const dy = Math.abs(a.y - b.y);
1487
+ const combinedHalfW = (a.width + b.width) / 2;
1488
+ const combinedHalfH = (a.height + b.height) / 2;
1489
+ const gapX = Math.max(0, dx - combinedHalfW);
1490
+ const gapY = Math.max(0, dy - combinedHalfH);
1491
+ return Math.sqrt(gapX * gapX + gapY * gapY);
1492
+ }
1493
+ /**
1494
+ * Compute the overlap amount between two AABBs.
1495
+ * Returns { x: 0, y: 0 } if not overlapping.
1496
+ */
1497
+ static overlap(a, b) {
1498
+ const dx = a.x - b.x;
1499
+ const dy = a.y - b.y;
1500
+ const combinedHalfW = (a.width + b.width) / 2;
1501
+ const combinedHalfH = (a.height + b.height) / 2;
1502
+ const overlapX = combinedHalfW - Math.abs(dx);
1503
+ const overlapY = combinedHalfH - Math.abs(dy);
1504
+ if (overlapX > 0 && overlapY > 0) {
1505
+ return {
1506
+ x: overlapX * Math.sign(dx),
1507
+ y: overlapY * Math.sign(dy)
1508
+ };
1509
+ }
1510
+ return {
1511
+ x: 0,
1512
+ y: 0
1513
+ };
1514
+ }
1515
+ };
1516
+
1517
+ // src/physics/Movement.ts
1518
+ var Movement = class {
1519
+ static {
1520
+ __name(this, "Movement");
1521
+ }
1522
+ /**
1523
+ * Update entity position based on velocity.
1524
+ *
1525
+ * @param entity - Entity to update
1526
+ * @param deltaMs - Time delta in milliseconds
1527
+ */
1528
+ static integrate(entity, deltaMs) {
1529
+ if (entity.vx === 0 && entity.vy === 0) return;
1530
+ const deltaSec = deltaMs / 1e3;
1531
+ entity.x += entity.vx * deltaSec;
1532
+ entity.y += entity.vy * deltaSec;
1533
+ entity.markDirty();
1534
+ }
1535
+ /**
1536
+ * Apply velocity to entity.
1537
+ */
1538
+ static applyVelocity(entity, vx, vy) {
1539
+ entity.setVelocity(vx, vy);
1540
+ }
1541
+ /**
1542
+ * Stop entity movement.
1543
+ */
1544
+ static stop(entity) {
1545
+ entity.setVelocity(0, 0);
1546
+ }
1547
+ /**
1548
+ * Constrain entity to boundary.
1549
+ *
1550
+ * @param entity - Entity to constrain
1551
+ * @param boundary - Boundary constraints
1552
+ * @returns True if entity was clamped
1553
+ */
1554
+ static constrainToBoundary(entity, boundary) {
1555
+ let clamped = false;
1556
+ if (entity.x < boundary.minX) {
1557
+ entity.x = boundary.minX;
1558
+ entity.vx = 0;
1559
+ clamped = true;
1560
+ } else if (entity.x > boundary.maxX) {
1561
+ entity.x = boundary.maxX;
1562
+ entity.vx = 0;
1563
+ clamped = true;
1564
+ }
1565
+ if (entity.y < boundary.minY) {
1566
+ entity.y = boundary.minY;
1567
+ entity.vy = 0;
1568
+ clamped = true;
1569
+ } else if (entity.y > boundary.maxY) {
1570
+ entity.y = boundary.maxY;
1571
+ entity.vy = 0;
1572
+ clamped = true;
1573
+ }
1574
+ if (clamped) {
1575
+ entity.markDirty();
1576
+ }
1577
+ return clamped;
1578
+ }
1579
+ /**
1580
+ * Calculate distance between two entities.
1581
+ */
1582
+ static distance(a, b) {
1583
+ const dx = a.x - b.x;
1584
+ const dy = a.y - b.y;
1585
+ return Math.sqrt(dx * dx + dy * dy);
1586
+ }
1587
+ /**
1588
+ * Normalize a direction vector.
1589
+ */
1590
+ static normalize(x, y) {
1591
+ const length = Math.sqrt(x * x + y * y);
1592
+ if (length === 0) return {
1593
+ x: 0,
1594
+ y: 0
1595
+ };
1596
+ return {
1597
+ x: x / length,
1598
+ y: y / length
1599
+ };
1600
+ }
1601
+ /**
1602
+ * Calculate velocity from direction and speed.
1603
+ */
1604
+ static velocityFromDirection(dirX, dirY, speed) {
1605
+ const normalized = this.normalize(dirX, dirY);
1606
+ return {
1607
+ vx: normalized.x * speed,
1608
+ vy: normalized.y * speed
1609
+ };
1610
+ }
1611
+ };
1612
+
1613
+ // src/types/protocol.ts
1614
+ var ClientOpcode;
1615
+ (function(ClientOpcode2) {
1616
+ ClientOpcode2["C_MOVE"] = "C_MOVE";
1617
+ ClientOpcode2["C_STOP"] = "C_STOP";
1618
+ ClientOpcode2["C_ACTION"] = "C_ACTION";
1619
+ })(ClientOpcode || (ClientOpcode = {}));
1620
+ var ServerOpcode;
1621
+ (function(ServerOpcode2) {
1622
+ ServerOpcode2["S_INIT"] = "S_INIT";
1623
+ ServerOpcode2["S_UPDATE"] = "S_UPDATE";
1624
+ ServerOpcode2["S_SPAWN"] = "S_SPAWN";
1625
+ ServerOpcode2["S_DESPAWN"] = "S_DESPAWN";
1626
+ ServerOpcode2["S_EVENT"] = "S_EVENT";
1627
+ ServerOpcode2["S_ERROR"] = "S_ERROR";
1628
+ })(ServerOpcode || (ServerOpcode = {}));
1629
+
1630
+ export { AABBCollision, ClientOpcode, Entity, GameLoop, GameServer, Grid, InputQueue, Logger, Movement, Network, Registry, RingBuffer, Room, ServerOpcode, Snapshot, createChildLogger, logger, setLogger };
1631
+ //# sourceMappingURL=index.js.map
1632
+ //# sourceMappingURL=index.js.map