quake2ts 0.0.561 → 0.0.563

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 (40) hide show
  1. package/package.json +1 -1
  2. package/packages/client/dist/browser/index.global.js +15 -15
  3. package/packages/client/dist/browser/index.global.js.map +1 -1
  4. package/packages/client/dist/cjs/index.cjs +273 -1
  5. package/packages/client/dist/cjs/index.cjs.map +1 -1
  6. package/packages/client/dist/esm/index.js +273 -1
  7. package/packages/client/dist/esm/index.js.map +1 -1
  8. package/packages/client/dist/tsconfig.tsbuildinfo +1 -1
  9. package/packages/client/dist/types/net/connection.d.ts +2 -0
  10. package/packages/client/dist/types/net/connection.d.ts.map +1 -1
  11. package/packages/server/dist/client.d.ts +51 -0
  12. package/packages/server/dist/client.js +100 -0
  13. package/packages/server/dist/dedicated.d.ts +69 -0
  14. package/packages/server/dist/dedicated.js +1013 -0
  15. package/packages/server/dist/index.cjs +27 -2
  16. package/packages/server/dist/index.d.ts +7 -161
  17. package/packages/server/dist/index.js +26 -2
  18. package/packages/server/dist/net/nodeWsDriver.d.ts +16 -0
  19. package/packages/server/dist/net/nodeWsDriver.js +122 -0
  20. package/packages/server/dist/protocol/player.d.ts +23 -0
  21. package/packages/server/dist/protocol/player.js +137 -0
  22. package/packages/server/dist/protocol/write.d.ts +7 -0
  23. package/packages/server/dist/protocol/write.js +167 -0
  24. package/packages/server/dist/protocol.d.ts +17 -0
  25. package/packages/server/dist/protocol.js +71 -0
  26. package/packages/server/dist/server.d.ts +50 -0
  27. package/packages/server/dist/server.js +12 -0
  28. package/packages/server/dist/server.test.d.ts +1 -0
  29. package/packages/server/dist/server.test.js +69 -0
  30. package/packages/server/dist/transport.d.ts +7 -0
  31. package/packages/server/dist/transport.js +1 -0
  32. package/packages/server/dist/transports/websocket.d.ts +11 -0
  33. package/packages/server/dist/transports/websocket.js +38 -0
  34. package/packages/test-utils/dist/index.cjs +1610 -1188
  35. package/packages/test-utils/dist/index.cjs.map +1 -1
  36. package/packages/test-utils/dist/index.d.cts +326 -132
  37. package/packages/test-utils/dist/index.d.ts +326 -132
  38. package/packages/test-utils/dist/index.js +1596 -1189
  39. package/packages/test-utils/dist/index.js.map +1 -1
  40. package/packages/server/dist/index.d.cts +0 -161
@@ -32,12 +32,16 @@ var index_exports = {};
32
32
  __export(index_exports, {
33
33
  InputInjector: () => InputInjector,
34
34
  MockPointerLock: () => MockPointerLock,
35
+ MockTransport: () => MockTransport,
35
36
  captureAudioEvents: () => captureAudioEvents,
36
37
  captureCanvasDrawCalls: () => captureCanvasDrawCalls,
38
+ captureGameScreenshot: () => captureGameScreenshot,
37
39
  captureGameState: () => captureGameState,
40
+ compareScreenshots: () => compareScreenshots,
38
41
  createBinaryStreamMock: () => createBinaryStreamMock,
39
42
  createBinaryWriterMock: () => createBinaryWriterMock,
40
43
  createControlledTimer: () => createControlledTimer,
44
+ createCustomNetworkCondition: () => createCustomNetworkCondition,
41
45
  createEntity: () => createEntity,
42
46
  createEntityStateFactory: () => createEntityStateFactory,
43
47
  createGameStateSnapshotFactory: () => createGameStateSnapshotFactory,
@@ -46,13 +50,21 @@ __export(index_exports, {
46
50
  createMockCanvasContext2D: () => createMockCanvasContext2D,
47
51
  createMockEngine: () => createMockEngine,
48
52
  createMockGame: () => createMockGame,
53
+ createMockGameState: () => createMockGameState,
49
54
  createMockImage: () => createMockImage,
50
55
  createMockImageData: () => createMockImageData,
51
56
  createMockIndexedDB: () => createMockIndexedDB,
52
57
  createMockLocalStorage: () => createMockLocalStorage,
58
+ createMockNetworkAddress: () => createMockNetworkAddress,
53
59
  createMockPerformance: () => createMockPerformance,
54
60
  createMockRAF: () => createMockRAF,
61
+ createMockServer: () => createMockServer,
62
+ createMockServerClient: () => createMockServerClient,
63
+ createMockServerState: () => createMockServerState,
64
+ createMockServerStatic: () => createMockServerStatic,
55
65
  createMockSessionStorage: () => createMockSessionStorage,
66
+ createMockTransport: () => createMockTransport,
67
+ createMockUDPSocket: () => createMockUDPSocket,
56
68
  createMockWebGL2Context: () => createMockWebGL2Context,
57
69
  createNetChanMock: () => createNetChanMock,
58
70
  createPlayerStateFactory: () => createPlayerStateFactory,
@@ -60,6 +72,7 @@ __export(index_exports, {
60
72
  createSpawnContext: () => createSpawnContext,
61
73
  createStorageTestScenario: () => createStorageTestScenario,
62
74
  createTestContext: () => createTestContext,
75
+ createVisualTestScenario: () => createVisualTestScenario,
63
76
  intersects: () => import_shared3.intersects,
64
77
  ladderTrace: () => import_shared3.ladderTrace,
65
78
  makeAxisBrush: () => makeAxisBrush,
@@ -74,1297 +87,1695 @@ __export(index_exports, {
74
87
  setupNodeEnvironment: () => setupNodeEnvironment,
75
88
  simulateFrames: () => simulateFrames,
76
89
  simulateFramesWithMock: () => simulateFramesWithMock,
90
+ simulateNetworkCondition: () => simulateNetworkCondition,
77
91
  stairTrace: () => import_shared3.stairTrace,
78
92
  teardownBrowserEnvironment: () => teardownBrowserEnvironment,
79
93
  teardownMockAudioContext: () => teardownMockAudioContext,
94
+ throttleBandwidth: () => throttleBandwidth,
80
95
  waitForGameReady: () => waitForGameReady
81
96
  });
82
97
  module.exports = __toCommonJS(index_exports);
83
98
 
84
- // src/setup/browser.ts
85
- var import_jsdom = require("jsdom");
86
- var import_canvas = require("@napi-rs/canvas");
87
- var import_auto = require("fake-indexeddb/auto");
88
-
89
- // src/e2e/input.ts
90
- var MockPointerLock = class {
91
- static setup(doc) {
92
- let _pointerLockElement = null;
93
- Object.defineProperty(doc, "pointerLockElement", {
94
- get: () => _pointerLockElement,
95
- configurable: true
96
- });
97
- doc.exitPointerLock = () => {
98
- if (_pointerLockElement) {
99
- _pointerLockElement = null;
100
- doc.dispatchEvent(new Event("pointerlockchange"));
101
- }
102
- };
103
- global.HTMLElement.prototype.requestPointerLock = function() {
104
- _pointerLockElement = this;
105
- doc.dispatchEvent(new Event("pointerlockchange"));
106
- };
107
- }
108
- };
109
- var InputInjector = class {
110
- constructor(doc, win) {
111
- this.doc = doc;
112
- this.win = win;
113
- }
114
- keyDown(key, code) {
115
- const event = new this.win.KeyboardEvent("keydown", {
116
- key,
117
- code: code || key,
118
- bubbles: true,
119
- cancelable: true,
120
- view: this.win
121
- });
122
- this.doc.dispatchEvent(event);
123
- }
124
- keyUp(key, code) {
125
- const event = new this.win.KeyboardEvent("keyup", {
126
- key,
127
- code: code || key,
128
- bubbles: true,
129
- cancelable: true,
130
- view: this.win
131
- });
132
- this.doc.dispatchEvent(event);
133
- }
134
- mouseMove(movementX, movementY, clientX = 0, clientY = 0) {
135
- const event = new this.win.MouseEvent("mousemove", {
136
- bubbles: true,
137
- cancelable: true,
138
- view: this.win,
139
- clientX,
140
- clientY,
141
- movementX,
142
- // Note: JSDOM might not support this standard property fully on event init
143
- movementY
144
- });
145
- Object.defineProperty(event, "movementX", { value: movementX });
146
- Object.defineProperty(event, "movementY", { value: movementY });
147
- const target = this.doc.pointerLockElement || this.doc;
148
- target.dispatchEvent(event);
149
- }
150
- mouseDown(button = 0) {
151
- const event = new this.win.MouseEvent("mousedown", {
152
- button,
153
- bubbles: true,
154
- cancelable: true,
155
- view: this.win
156
- });
157
- const target = this.doc.pointerLockElement || this.doc;
158
- target.dispatchEvent(event);
159
- }
160
- mouseUp(button = 0) {
161
- const event = new this.win.MouseEvent("mouseup", {
162
- button,
163
- bubbles: true,
164
- cancelable: true,
165
- view: this.win
166
- });
167
- const target = this.doc.pointerLockElement || this.doc;
168
- target.dispatchEvent(event);
169
- }
170
- wheel(deltaY) {
171
- const event = new this.win.WheelEvent("wheel", {
172
- deltaY,
173
- bubbles: true,
174
- cancelable: true,
175
- view: this.win
176
- });
177
- const target = this.doc.pointerLockElement || this.doc;
178
- target.dispatchEvent(event);
179
- }
180
- };
181
-
182
- // src/setup/webgl.ts
183
- function createMockWebGL2Context(canvas) {
184
- const gl = {
185
- canvas,
186
- drawingBufferWidth: canvas.width,
187
- drawingBufferHeight: canvas.height,
188
- // Constants
189
- VERTEX_SHADER: 35633,
190
- FRAGMENT_SHADER: 35632,
191
- COMPILE_STATUS: 35713,
192
- LINK_STATUS: 35714,
193
- ARRAY_BUFFER: 34962,
194
- ELEMENT_ARRAY_BUFFER: 34963,
195
- STATIC_DRAW: 35044,
196
- DYNAMIC_DRAW: 35048,
197
- FLOAT: 5126,
198
- DEPTH_TEST: 2929,
199
- BLEND: 3042,
200
- SRC_ALPHA: 770,
201
- ONE_MINUS_SRC_ALPHA: 771,
202
- TEXTURE_2D: 3553,
203
- RGBA: 6408,
204
- UNSIGNED_BYTE: 5121,
205
- COLOR_BUFFER_BIT: 16384,
206
- DEPTH_BUFFER_BIT: 256,
207
- TRIANGLES: 4,
208
- TRIANGLE_STRIP: 5,
209
- TRIANGLE_FAN: 6,
210
- // Methods
211
- createShader: () => ({}),
212
- shaderSource: () => {
213
- },
214
- compileShader: () => {
215
- },
216
- getShaderParameter: (_, param) => {
217
- if (param === 35713) return true;
218
- return true;
219
- },
220
- getShaderInfoLog: () => "",
221
- createProgram: () => ({}),
222
- attachShader: () => {
223
- },
224
- linkProgram: () => {
225
- },
226
- getProgramParameter: (_, param) => {
227
- if (param === 35714) return true;
228
- return true;
229
- },
230
- getProgramInfoLog: () => "",
231
- useProgram: () => {
232
- },
233
- createBuffer: () => ({}),
234
- bindBuffer: () => {
235
- },
236
- bufferData: () => {
237
- },
238
- enableVertexAttribArray: () => {
239
- },
240
- vertexAttribPointer: () => {
241
- },
242
- enable: () => {
243
- },
244
- disable: () => {
245
- },
246
- depthMask: () => {
247
- },
248
- blendFunc: () => {
249
- },
250
- viewport: () => {
251
- },
252
- clearColor: () => {
253
- },
254
- clear: () => {
255
- },
256
- createTexture: () => ({}),
257
- bindTexture: () => {
258
- },
259
- texImage2D: () => {
260
- },
261
- texParameteri: () => {
262
- },
263
- activeTexture: () => {
264
- },
265
- uniform1i: () => {
266
- },
267
- uniform1f: () => {
268
- },
269
- uniform2f: () => {
270
- },
271
- uniform3f: () => {
272
- },
273
- uniform4f: () => {
274
- },
275
- uniformMatrix4fv: () => {
276
- },
277
- getUniformLocation: () => ({}),
278
- getAttribLocation: () => 0,
279
- drawArrays: () => {
280
- },
281
- drawElements: () => {
282
- },
283
- createVertexArray: () => ({}),
284
- bindVertexArray: () => {
285
- },
286
- deleteShader: () => {
287
- },
288
- deleteProgram: () => {
289
- },
290
- deleteBuffer: () => {
291
- },
292
- deleteTexture: () => {
293
- },
294
- deleteVertexArray: () => {
295
- },
296
- // WebGL2 specific
297
- texImage3D: () => {
298
- },
299
- uniformBlockBinding: () => {
300
- },
301
- getExtension: () => null
302
- };
303
- return gl;
304
- }
305
-
306
- // src/setup/browser.ts
307
- function setupBrowserEnvironment(options = {}) {
308
- const {
309
- url = "http://localhost",
310
- pretendToBeVisual = true,
311
- resources = void 0,
312
- enableWebGL2 = false,
313
- enablePointerLock = false
314
- } = options;
315
- const dom = new import_jsdom.JSDOM("<!DOCTYPE html><html><head></head><body></body></html>", {
316
- url,
317
- pretendToBeVisual,
318
- resources
319
- });
320
- global.window = dom.window;
321
- global.document = dom.window.document;
322
- try {
323
- global.navigator = dom.window.navigator;
324
- } catch (e) {
325
- try {
326
- Object.defineProperty(global, "navigator", {
327
- value: dom.window.navigator,
328
- writable: true,
329
- configurable: true
330
- });
331
- } catch (e2) {
332
- console.warn("Could not assign global.navigator, skipping.");
333
- }
334
- }
335
- global.location = dom.window.location;
336
- global.HTMLElement = dom.window.HTMLElement;
337
- global.HTMLCanvasElement = dom.window.HTMLCanvasElement;
338
- global.Event = dom.window.Event;
339
- global.CustomEvent = dom.window.CustomEvent;
340
- global.DragEvent = dom.window.DragEvent;
341
- global.MouseEvent = dom.window.MouseEvent;
342
- global.KeyboardEvent = dom.window.KeyboardEvent;
343
- global.FocusEvent = dom.window.FocusEvent;
344
- global.WheelEvent = dom.window.WheelEvent;
345
- global.InputEvent = dom.window.InputEvent;
346
- global.UIEvent = dom.window.UIEvent;
347
- try {
348
- global.localStorage = dom.window.localStorage;
349
- } catch (e) {
350
- }
351
- if (!global.localStorage) {
352
- const storage = /* @__PURE__ */ new Map();
353
- global.localStorage = {
354
- getItem: (key) => storage.get(key) || null,
355
- setItem: (key, value) => storage.set(key, value),
356
- removeItem: (key) => storage.delete(key),
357
- clear: () => storage.clear(),
358
- key: (index) => Array.from(storage.keys())[index] || null,
359
- get length() {
360
- return storage.size;
361
- }
362
- };
363
- }
364
- const originalCreateElement = document.createElement.bind(document);
365
- document.createElement = function(tagName, options2) {
366
- if (tagName.toLowerCase() === "canvas") {
367
- const napiCanvas = new import_canvas.Canvas(300, 150);
368
- const domCanvas = originalCreateElement("canvas", options2);
369
- Object.defineProperty(domCanvas, "width", {
370
- get: () => napiCanvas.width,
371
- set: (value) => {
372
- napiCanvas.width = value;
373
- },
374
- enumerable: true,
375
- configurable: true
376
- });
377
- Object.defineProperty(domCanvas, "height", {
378
- get: () => napiCanvas.height,
379
- set: (value) => {
380
- napiCanvas.height = value;
381
- },
382
- enumerable: true,
383
- configurable: true
384
- });
385
- const originalGetContext = domCanvas.getContext.bind(domCanvas);
386
- domCanvas.getContext = function(contextId, options3) {
387
- if (contextId === "2d") {
388
- return napiCanvas.getContext("2d", options3);
389
- }
390
- if (enableWebGL2 && contextId === "webgl2") {
391
- return createMockWebGL2Context(domCanvas);
392
- }
393
- if (contextId === "webgl" || contextId === "webgl2") {
394
- return originalGetContext(contextId, options3);
395
- }
396
- return napiCanvas.getContext(contextId, options3);
397
- };
398
- domCanvas.__napiCanvas = napiCanvas;
399
- return domCanvas;
400
- }
401
- return originalCreateElement(tagName, options2);
99
+ // src/shared/mocks.ts
100
+ var import_vitest = require("vitest");
101
+ var createBinaryWriterMock = () => ({
102
+ writeByte: import_vitest.vi.fn(),
103
+ writeShort: import_vitest.vi.fn(),
104
+ writeLong: import_vitest.vi.fn(),
105
+ writeString: import_vitest.vi.fn(),
106
+ writeBytes: import_vitest.vi.fn(),
107
+ getBuffer: import_vitest.vi.fn(() => new Uint8Array(0)),
108
+ reset: import_vitest.vi.fn(),
109
+ // Legacy methods (if any)
110
+ writeInt8: import_vitest.vi.fn(),
111
+ writeUint8: import_vitest.vi.fn(),
112
+ writeInt16: import_vitest.vi.fn(),
113
+ writeUint16: import_vitest.vi.fn(),
114
+ writeInt32: import_vitest.vi.fn(),
115
+ writeUint32: import_vitest.vi.fn(),
116
+ writeFloat: import_vitest.vi.fn(),
117
+ getData: import_vitest.vi.fn(() => new Uint8Array(0))
118
+ });
119
+ var createNetChanMock = () => ({
120
+ qport: 1234,
121
+ // Sequencing
122
+ incomingSequence: 0,
123
+ outgoingSequence: 0,
124
+ incomingAcknowledged: 0,
125
+ // Reliable messaging
126
+ incomingReliableAcknowledged: false,
127
+ incomingReliableSequence: 0,
128
+ outgoingReliableSequence: 0,
129
+ reliableMessage: createBinaryWriterMock(),
130
+ reliableLength: 0,
131
+ // Fragmentation
132
+ fragmentSendOffset: 0,
133
+ fragmentBuffer: null,
134
+ fragmentLength: 0,
135
+ fragmentReceived: 0,
136
+ // Timing
137
+ lastReceived: 0,
138
+ lastSent: 0,
139
+ remoteAddress: { type: "IP", port: 1234 },
140
+ // Methods
141
+ setup: import_vitest.vi.fn(),
142
+ reset: import_vitest.vi.fn(),
143
+ transmit: import_vitest.vi.fn(),
144
+ process: import_vitest.vi.fn(),
145
+ canSendReliable: import_vitest.vi.fn(() => true),
146
+ writeReliableByte: import_vitest.vi.fn(),
147
+ writeReliableShort: import_vitest.vi.fn(),
148
+ writeReliableLong: import_vitest.vi.fn(),
149
+ writeReliableString: import_vitest.vi.fn(),
150
+ getReliableData: import_vitest.vi.fn(() => new Uint8Array(0)),
151
+ needsKeepalive: import_vitest.vi.fn(() => false),
152
+ isTimedOut: import_vitest.vi.fn(() => false)
153
+ });
154
+ var createBinaryStreamMock = () => ({
155
+ getPosition: import_vitest.vi.fn(() => 0),
156
+ getReadPosition: import_vitest.vi.fn(() => 0),
157
+ getLength: import_vitest.vi.fn(() => 0),
158
+ getRemaining: import_vitest.vi.fn(() => 0),
159
+ seek: import_vitest.vi.fn(),
160
+ setReadPosition: import_vitest.vi.fn(),
161
+ hasMore: import_vitest.vi.fn(() => true),
162
+ hasBytes: import_vitest.vi.fn((amount) => true),
163
+ readChar: import_vitest.vi.fn(() => 0),
164
+ readByte: import_vitest.vi.fn(() => 0),
165
+ readShort: import_vitest.vi.fn(() => 0),
166
+ readUShort: import_vitest.vi.fn(() => 0),
167
+ readLong: import_vitest.vi.fn(() => 0),
168
+ readULong: import_vitest.vi.fn(() => 0),
169
+ readFloat: import_vitest.vi.fn(() => 0),
170
+ readString: import_vitest.vi.fn(() => ""),
171
+ readStringLine: import_vitest.vi.fn(() => ""),
172
+ readCoord: import_vitest.vi.fn(() => 0),
173
+ readAngle: import_vitest.vi.fn(() => 0),
174
+ readAngle16: import_vitest.vi.fn(() => 0),
175
+ readData: import_vitest.vi.fn((length) => new Uint8Array(length)),
176
+ readPos: import_vitest.vi.fn(),
177
+ readDir: import_vitest.vi.fn()
178
+ });
179
+
180
+ // src/shared/bsp.ts
181
+ var import_shared = require("@quake2ts/shared");
182
+ function makePlane(normal, dist) {
183
+ return {
184
+ normal,
185
+ dist,
186
+ type: Math.abs(normal.x) === 1 ? 0 : Math.abs(normal.y) === 1 ? 1 : Math.abs(normal.z) === 1 ? 2 : 3,
187
+ signbits: (0, import_shared.computePlaneSignBits)(normal)
402
188
  };
403
- if (enableWebGL2) {
404
- const originalProtoGetContext = global.HTMLCanvasElement.prototype.getContext;
405
- global.HTMLCanvasElement.prototype.getContext = function(contextId, options2) {
406
- if (contextId === "webgl2") {
407
- return createMockWebGL2Context(this);
408
- }
409
- return originalProtoGetContext.call(this, contextId, options2);
410
- };
411
- }
412
- global.Image = import_canvas.Image;
413
- global.ImageData = import_canvas.ImageData;
414
- if (typeof global.createImageBitmap === "undefined") {
415
- global.createImageBitmap = async function(image, _options) {
416
- if (image && typeof image.width === "number" && typeof image.height === "number") {
417
- const canvas2 = new import_canvas.Canvas(image.width, image.height);
418
- const ctx = canvas2.getContext("2d");
419
- if (image.data) {
420
- ctx.putImageData(image, 0, 0);
421
- }
422
- return canvas2;
423
- }
424
- const canvas = new import_canvas.Canvas(100, 100);
425
- return canvas;
426
- };
427
- }
428
- if (typeof global.btoa === "undefined") {
429
- global.btoa = function(str) {
430
- return Buffer.from(str, "binary").toString("base64");
431
- };
432
- }
433
- if (typeof global.atob === "undefined") {
434
- global.atob = function(str) {
435
- return Buffer.from(str, "base64").toString("binary");
436
- };
437
- }
438
- if (enablePointerLock) {
439
- MockPointerLock.setup(global.document);
440
- }
441
- if (typeof global.requestAnimationFrame === "undefined") {
442
- let lastTime = 0;
443
- global.requestAnimationFrame = (callback) => {
444
- const currTime = Date.now();
445
- const timeToCall = Math.max(0, 16 - (currTime - lastTime));
446
- const id = setTimeout(() => {
447
- callback(currTime + timeToCall);
448
- }, timeToCall);
449
- lastTime = currTime + timeToCall;
450
- return id;
451
- };
452
- global.cancelAnimationFrame = (id) => {
453
- clearTimeout(id);
454
- };
455
- }
456
- }
457
- function teardownBrowserEnvironment() {
458
- delete global.window;
459
- delete global.document;
460
- delete global.navigator;
461
- delete global.localStorage;
462
- delete global.location;
463
- delete global.HTMLElement;
464
- delete global.HTMLCanvasElement;
465
- delete global.Image;
466
- delete global.ImageData;
467
- delete global.createImageBitmap;
468
- delete global.Event;
469
- delete global.CustomEvent;
470
- delete global.DragEvent;
471
- delete global.MouseEvent;
472
- delete global.KeyboardEvent;
473
- delete global.FocusEvent;
474
- delete global.WheelEvent;
475
- delete global.InputEvent;
476
- delete global.UIEvent;
477
189
  }
478
-
479
- // src/setup/canvas.ts
480
- var import_canvas2 = require("@napi-rs/canvas");
481
- function createMockCanvas(width = 300, height = 150) {
482
- if (typeof document !== "undefined" && document.createElement) {
483
- const canvas2 = document.createElement("canvas");
484
- canvas2.width = width;
485
- canvas2.height = height;
486
- return canvas2;
487
- }
488
- const canvas = new import_canvas2.Canvas(width, height);
489
- const originalGetContext = canvas.getContext.bind(canvas);
490
- canvas.getContext = function(contextId, options) {
491
- if (contextId === "webgl2") {
492
- return createMockWebGL2Context(canvas);
493
- }
494
- if (contextId === "2d") {
495
- return originalGetContext("2d", options);
496
- }
497
- return originalGetContext(contextId, options);
190
+ function makeAxisBrush(size, contents = import_shared.CONTENTS_SOLID) {
191
+ const half = size / 2;
192
+ const planes = [
193
+ makePlane({ x: 1, y: 0, z: 0 }, half),
194
+ makePlane({ x: -1, y: 0, z: 0 }, half),
195
+ makePlane({ x: 0, y: 1, z: 0 }, half),
196
+ makePlane({ x: 0, y: -1, z: 0 }, half),
197
+ makePlane({ x: 0, y: 0, z: 1 }, half),
198
+ makePlane({ x: 0, y: 0, z: -1 }, half)
199
+ ];
200
+ return {
201
+ contents,
202
+ sides: planes.map((plane) => ({ plane, surfaceFlags: 0 }))
498
203
  };
499
- return canvas;
500
204
  }
501
- function createMockCanvasContext2D(canvas) {
502
- if (!canvas) {
503
- canvas = createMockCanvas();
504
- }
505
- return canvas.getContext("2d");
205
+ function makeNode(plane, children) {
206
+ return { plane, children };
506
207
  }
507
- function captureCanvasDrawCalls(context) {
508
- const drawCalls = [];
509
- const methodsToSpy = [
510
- "fillRect",
511
- "strokeRect",
512
- "clearRect",
513
- "fillText",
514
- "strokeText",
515
- "drawImage",
516
- "beginPath",
517
- "closePath",
518
- "moveTo",
519
- "lineTo",
520
- "arc",
521
- "arcTo",
522
- "bezierCurveTo",
523
- "quadraticCurveTo",
524
- "stroke",
525
- "fill",
526
- "putImageData"
527
- ];
528
- methodsToSpy.forEach((method) => {
529
- const original = context[method];
530
- if (typeof original === "function") {
531
- context[method] = function(...args) {
532
- drawCalls.push({ method, args });
533
- return original.apply(this, args);
534
- };
535
- }
536
- });
537
- return drawCalls;
208
+ function makeBspModel(planes, nodes, leaves, brushes, leafBrushes) {
209
+ return {
210
+ planes,
211
+ nodes,
212
+ leaves,
213
+ brushes,
214
+ leafBrushes,
215
+ bmodels: []
216
+ };
538
217
  }
539
- function createMockImageData(width, height, fillColor) {
540
- const imageData = new import_canvas2.ImageData(width, height);
541
- if (fillColor) {
542
- const [r, g, b, a] = fillColor;
543
- for (let i = 0; i < imageData.data.length; i += 4) {
544
- imageData.data[i] = r;
545
- imageData.data[i + 1] = g;
546
- imageData.data[i + 2] = b;
547
- imageData.data[i + 3] = a;
548
- }
549
- }
550
- return imageData;
218
+ function makeLeaf(contents, firstLeafBrush, numLeafBrushes) {
219
+ return { contents, cluster: 0, area: 0, firstLeafBrush, numLeafBrushes };
551
220
  }
552
- function createMockImage(width, height, src) {
553
- const img = new import_canvas2.Image();
554
- if (width) img.width = width;
555
- if (height) img.height = height;
556
- if (src) img.src = src;
557
- return img;
221
+ function makeLeafModel(brushes) {
222
+ const planes = brushes.flatMap((brush) => brush.sides.map((side) => side.plane));
223
+ return {
224
+ planes,
225
+ nodes: [],
226
+ leaves: [makeLeaf(0, 0, brushes.length)],
227
+ brushes,
228
+ leafBrushes: brushes.map((_, i) => i),
229
+ bmodels: []
230
+ };
558
231
  }
559
-
560
- // src/setup/node.ts
561
- function setupNodeEnvironment() {
562
- if (typeof global.fetch === "undefined") {
563
- }
232
+ function makeBrushFromMinsMaxs(mins, maxs, contents = import_shared.CONTENTS_SOLID) {
233
+ const planes = [
234
+ makePlane({ x: 1, y: 0, z: 0 }, maxs.x),
235
+ makePlane({ x: -1, y: 0, z: 0 }, -mins.x),
236
+ makePlane({ x: 0, y: 1, z: 0 }, maxs.y),
237
+ makePlane({ x: 0, y: -1, z: 0 }, -mins.y),
238
+ makePlane({ x: 0, y: 0, z: 1 }, maxs.z),
239
+ makePlane({ x: 0, y: 0, z: -1 }, -mins.z)
240
+ ];
241
+ return {
242
+ contents,
243
+ sides: planes.map((plane) => ({ plane, surfaceFlags: 0 }))
244
+ };
564
245
  }
565
246
 
566
- // src/setup/storage.ts
567
- var import_auto2 = require("fake-indexeddb/auto");
568
- function createMockLocalStorage(initialData = {}) {
569
- const storage = new Map(Object.entries(initialData));
570
- return {
571
- getItem: (key) => storage.get(key) || null,
572
- setItem: (key, value) => storage.set(key, value),
573
- removeItem: (key) => storage.delete(key),
574
- clear: () => storage.clear(),
575
- key: (index) => Array.from(storage.keys())[index] || null,
576
- get length() {
577
- return storage.size;
578
- }
579
- };
580
- }
581
- function createMockSessionStorage(initialData = {}) {
582
- return createMockLocalStorage(initialData);
583
- }
584
- function createMockIndexedDB() {
585
- if (typeof indexedDB === "undefined") {
586
- throw new Error("IndexedDB mock not found. Ensure fake-indexeddb is loaded.");
587
- }
588
- return indexedDB;
589
- }
590
- function createStorageTestScenario(storageType = "local") {
591
- const storage = storageType === "local" ? createMockLocalStorage() : createMockSessionStorage();
592
- return {
593
- storage,
594
- populate(data) {
595
- Object.entries(data).forEach(([k, v]) => storage.setItem(k, v));
596
- },
597
- verify(key, value) {
598
- return storage.getItem(key) === value;
599
- }
247
+ // src/game/factories.ts
248
+ var createPlayerStateFactory = (overrides) => ({
249
+ pm_type: 0,
250
+ pm_time: 0,
251
+ pm_flags: 0,
252
+ origin: { x: 0, y: 0, z: 0 },
253
+ velocity: { x: 0, y: 0, z: 0 },
254
+ viewAngles: { x: 0, y: 0, z: 0 },
255
+ onGround: false,
256
+ waterLevel: 0,
257
+ watertype: 0,
258
+ mins: { x: 0, y: 0, z: 0 },
259
+ maxs: { x: 0, y: 0, z: 0 },
260
+ damageAlpha: 0,
261
+ damageIndicators: [],
262
+ blend: [0, 0, 0, 0],
263
+ stats: [],
264
+ kick_angles: { x: 0, y: 0, z: 0 },
265
+ kick_origin: { x: 0, y: 0, z: 0 },
266
+ gunoffset: { x: 0, y: 0, z: 0 },
267
+ gunangles: { x: 0, y: 0, z: 0 },
268
+ gunindex: 0,
269
+ gun_frame: 0,
270
+ rdflags: 0,
271
+ fov: 90,
272
+ renderfx: 0,
273
+ ...overrides
274
+ });
275
+ var createEntityStateFactory = (overrides) => ({
276
+ number: 0,
277
+ origin: { x: 0, y: 0, z: 0 },
278
+ angles: { x: 0, y: 0, z: 0 },
279
+ oldOrigin: { x: 0, y: 0, z: 0 },
280
+ modelIndex: 0,
281
+ modelIndex2: 0,
282
+ modelIndex3: 0,
283
+ modelIndex4: 0,
284
+ frame: 0,
285
+ skinNum: 0,
286
+ effects: 0,
287
+ renderfx: 0,
288
+ solid: 0,
289
+ sound: 0,
290
+ event: 0,
291
+ ...overrides
292
+ });
293
+ var createGameStateSnapshotFactory = (overrides) => ({
294
+ gravity: { x: 0, y: 0, z: -800 },
295
+ origin: { x: 0, y: 0, z: 0 },
296
+ velocity: { x: 0, y: 0, z: 0 },
297
+ viewangles: { x: 0, y: 0, z: 0 },
298
+ level: { timeSeconds: 0, frameNumber: 0, previousTimeSeconds: 0, deltaSeconds: 0.1 },
299
+ entities: {
300
+ activeCount: 0,
301
+ worldClassname: "worldspawn"
302
+ },
303
+ packetEntities: [],
304
+ pmFlags: 0,
305
+ pmType: 0,
306
+ waterlevel: 0,
307
+ watertype: 0,
308
+ deltaAngles: { x: 0, y: 0, z: 0 },
309
+ health: 100,
310
+ armor: 0,
311
+ ammo: 0,
312
+ blend: [0, 0, 0, 0],
313
+ damageAlpha: 0,
314
+ damageIndicators: [],
315
+ stats: [],
316
+ kick_angles: { x: 0, y: 0, z: 0 },
317
+ kick_origin: { x: 0, y: 0, z: 0 },
318
+ gunoffset: { x: 0, y: 0, z: 0 },
319
+ gunangles: { x: 0, y: 0, z: 0 },
320
+ gunindex: 0,
321
+ pm_time: 0,
322
+ gun_frame: 0,
323
+ rdflags: 0,
324
+ fov: 90,
325
+ renderfx: 0,
326
+ pm_flags: 0,
327
+ pm_type: 0,
328
+ ...overrides
329
+ });
330
+
331
+ // src/game/helpers.ts
332
+ var import_vitest2 = require("vitest");
333
+ var import_game = require("@quake2ts/game");
334
+ var import_shared2 = require("@quake2ts/shared");
335
+ var import_shared3 = require("@quake2ts/shared");
336
+ var createMockEngine = () => ({
337
+ sound: import_vitest2.vi.fn(),
338
+ soundIndex: import_vitest2.vi.fn((sound) => 0),
339
+ modelIndex: import_vitest2.vi.fn((model) => 0),
340
+ centerprintf: import_vitest2.vi.fn()
341
+ });
342
+ var createMockGame = (seed = 12345) => {
343
+ const spawnRegistry = new import_game.SpawnRegistry();
344
+ const hooks = new import_game.ScriptHookRegistry();
345
+ const game = {
346
+ random: (0, import_shared2.createRandomGenerator)({ seed }),
347
+ registerEntitySpawn: import_vitest2.vi.fn((classname, spawnFunc) => {
348
+ spawnRegistry.register(classname, (entity) => spawnFunc(entity));
349
+ }),
350
+ unregisterEntitySpawn: import_vitest2.vi.fn((classname) => {
351
+ spawnRegistry.unregister(classname);
352
+ }),
353
+ getCustomEntities: import_vitest2.vi.fn(() => Array.from(spawnRegistry.keys())),
354
+ hooks,
355
+ registerHooks: import_vitest2.vi.fn((newHooks) => hooks.register(newHooks)),
356
+ spawnWorld: import_vitest2.vi.fn(() => {
357
+ hooks.onMapLoad("q2dm1");
358
+ }),
359
+ clientBegin: import_vitest2.vi.fn((client) => {
360
+ hooks.onPlayerSpawn({});
361
+ }),
362
+ damage: import_vitest2.vi.fn((amount) => {
363
+ hooks.onDamage({}, null, null, amount, 0, 0);
364
+ })
600
365
  };
601
- }
602
-
603
- // src/setup/audio.ts
604
- function createMockAudioContext() {
605
- return {
606
- createGain: () => ({
607
- connect: () => {
608
- },
609
- gain: { value: 1, setValueAtTime: () => {
610
- } }
366
+ return { game, spawnRegistry };
367
+ };
368
+ function createTestContext(options) {
369
+ const engine = createMockEngine();
370
+ const seed = options?.seed ?? 12345;
371
+ const { game, spawnRegistry } = createMockGame(seed);
372
+ const traceFn = import_vitest2.vi.fn((start, end, mins, maxs) => ({
373
+ fraction: 1,
374
+ ent: null,
375
+ allsolid: false,
376
+ startsolid: false,
377
+ endpos: end,
378
+ plane: { normal: { x: 0, y: 0, z: 1 }, dist: 0 },
379
+ surfaceFlags: 0,
380
+ contents: 0
381
+ }));
382
+ const entityList = options?.initialEntities ? [...options.initialEntities] : [];
383
+ const hooks = game.hooks;
384
+ const entities = {
385
+ spawn: import_vitest2.vi.fn(() => {
386
+ const ent = new import_game.Entity(entityList.length + 1);
387
+ entityList.push(ent);
388
+ hooks.onEntitySpawn(ent);
389
+ return ent;
611
390
  }),
612
- createOscillator: () => ({
613
- connect: () => {
614
- },
615
- start: () => {
616
- },
617
- stop: () => {
618
- },
619
- frequency: { value: 440 }
391
+ free: import_vitest2.vi.fn((ent) => {
392
+ const idx = entityList.indexOf(ent);
393
+ if (idx !== -1) {
394
+ entityList.splice(idx, 1);
395
+ }
396
+ hooks.onEntityRemove(ent);
620
397
  }),
621
- createBufferSource: () => ({
622
- connect: () => {
623
- },
624
- start: () => {
625
- },
626
- stop: () => {
627
- },
628
- buffer: null,
629
- playbackRate: { value: 1 },
630
- loop: false
398
+ finalizeSpawn: import_vitest2.vi.fn(),
399
+ freeImmediate: import_vitest2.vi.fn((ent) => {
400
+ const idx = entityList.indexOf(ent);
401
+ if (idx !== -1) {
402
+ entityList.splice(idx, 1);
403
+ }
631
404
  }),
632
- destination: {},
633
- currentTime: 0,
634
- state: "running",
635
- resume: async () => {
636
- },
637
- suspend: async () => {
405
+ setSpawnRegistry: import_vitest2.vi.fn(),
406
+ timeSeconds: 10,
407
+ deltaSeconds: 0.1,
408
+ modelIndex: import_vitest2.vi.fn(() => 0),
409
+ scheduleThink: import_vitest2.vi.fn((entity, time) => {
410
+ entity.nextthink = time;
411
+ }),
412
+ linkentity: import_vitest2.vi.fn(),
413
+ trace: traceFn,
414
+ pointcontents: import_vitest2.vi.fn(() => 0),
415
+ multicast: import_vitest2.vi.fn(),
416
+ unicast: import_vitest2.vi.fn(),
417
+ engine,
418
+ game,
419
+ sound: import_vitest2.vi.fn((ent, chan, sound, vol, attn, timeofs) => {
420
+ engine.sound(ent, chan, sound, vol, attn, timeofs);
421
+ }),
422
+ soundIndex: import_vitest2.vi.fn((sound) => engine.soundIndex(sound)),
423
+ useTargets: import_vitest2.vi.fn((entity, activator) => {
424
+ }),
425
+ findByTargetName: import_vitest2.vi.fn(() => []),
426
+ pickTarget: import_vitest2.vi.fn(() => null),
427
+ killBox: import_vitest2.vi.fn(),
428
+ rng: (0, import_shared2.createRandomGenerator)({ seed }),
429
+ imports: {
430
+ configstring: import_vitest2.vi.fn(),
431
+ trace: traceFn,
432
+ pointcontents: import_vitest2.vi.fn(() => 0)
638
433
  },
639
- close: async () => {
434
+ level: {
435
+ intermission_angle: { x: 0, y: 0, z: 0 },
436
+ intermission_origin: { x: 0, y: 0, z: 0 },
437
+ next_auto_save: 0,
438
+ health_bar_entities: null
640
439
  },
641
- decodeAudioData: async (buffer) => ({
642
- duration: 1,
643
- length: 44100,
644
- sampleRate: 44100,
645
- numberOfChannels: 2,
646
- getChannelData: () => new Float32Array(44100)
440
+ targetNameIndex: /* @__PURE__ */ new Map(),
441
+ forEachEntity: import_vitest2.vi.fn((callback) => {
442
+ entityList.forEach(callback);
647
443
  }),
648
- createBuffer: (channels, length, sampleRate) => ({
649
- duration: length / sampleRate,
650
- length,
651
- sampleRate,
652
- numberOfChannels: channels,
653
- getChannelData: () => new Float32Array(length)
654
- })
444
+ find: import_vitest2.vi.fn((predicate) => {
445
+ return entityList.find(predicate);
446
+ }),
447
+ findByClassname: import_vitest2.vi.fn((classname) => {
448
+ return entityList.find((e) => e.classname === classname);
449
+ }),
450
+ beginFrame: import_vitest2.vi.fn((timeSeconds) => {
451
+ entities.timeSeconds = timeSeconds;
452
+ }),
453
+ targetAwareness: {
454
+ timeSeconds: 10,
455
+ frameNumber: 1,
456
+ sightEntity: null,
457
+ soundEntity: null
458
+ },
459
+ // Adding missing properties to satisfy EntitySystem interface partially or fully
460
+ // We cast to unknown first anyway, but filling these in makes it safer for consumers
461
+ skill: 1,
462
+ deathmatch: false,
463
+ coop: false,
464
+ activeCount: entityList.length,
465
+ world: entityList.find((e) => e.classname === "worldspawn") || new import_game.Entity(0)
466
+ // ... other EntitySystem properties would go here
467
+ };
468
+ return {
469
+ keyValues: {},
470
+ entities,
471
+ game,
472
+ engine,
473
+ health_multiplier: 1,
474
+ warn: import_vitest2.vi.fn(),
475
+ free: import_vitest2.vi.fn(),
476
+ // Mock precache functions if they are part of SpawnContext in future or TestContext extensions
477
+ precacheModel: import_vitest2.vi.fn(),
478
+ precacheSound: import_vitest2.vi.fn(),
479
+ precacheImage: import_vitest2.vi.fn()
655
480
  };
656
481
  }
657
- function setupMockAudioContext() {
658
- if (typeof global.AudioContext === "undefined" && typeof global.window !== "undefined") {
659
- global.AudioContext = class {
660
- constructor() {
661
- return createMockAudioContext();
662
- }
663
- };
664
- global.window.AudioContext = global.AudioContext;
665
- global.window.webkitAudioContext = global.AudioContext;
666
- }
482
+ function createSpawnContext() {
483
+ return createTestContext();
667
484
  }
668
- function teardownMockAudioContext() {
669
- if (global.AudioContext && global.AudioContext.toString().includes("class")) {
670
- delete global.AudioContext;
671
- delete global.window.AudioContext;
672
- delete global.window.webkitAudioContext;
485
+ function createEntity() {
486
+ return new import_game.Entity(1);
487
+ }
488
+
489
+ // src/game/mocks.ts
490
+ function createMockGameState(overrides) {
491
+ return {
492
+ levelName: "test_level",
493
+ time: 0,
494
+ entities: [],
495
+ clients: [],
496
+ ...overrides
497
+ };
498
+ }
499
+
500
+ // src/server/mocks/transport.ts
501
+ var import_vitest3 = require("vitest");
502
+ var MockTransport = class {
503
+ constructor() {
504
+ this.address = "127.0.0.1";
505
+ this.port = 27910;
506
+ this.sentMessages = [];
507
+ this.receivedMessages = [];
508
+ this.listening = false;
509
+ this.listenSpy = import_vitest3.vi.fn().mockImplementation(async (port) => {
510
+ this.port = port;
511
+ this.listening = true;
512
+ });
513
+ this.closeSpy = import_vitest3.vi.fn().mockImplementation(() => {
514
+ this.listening = false;
515
+ });
516
+ }
517
+ /**
518
+ * Start listening on the specified port.
519
+ */
520
+ async listen(port) {
521
+ return this.listenSpy(port);
522
+ }
523
+ /**
524
+ * Close the transport.
525
+ */
526
+ close() {
527
+ this.closeSpy();
528
+ }
529
+ /**
530
+ * Register a callback for new connections.
531
+ */
532
+ onConnection(callback) {
533
+ this.onConnectionCallback = callback;
534
+ }
535
+ /**
536
+ * Register a callback for errors.
537
+ */
538
+ onError(callback) {
539
+ this.onErrorCallback = callback;
673
540
  }
541
+ /**
542
+ * Check if the transport is currently listening.
543
+ */
544
+ isListening() {
545
+ return this.listening;
546
+ }
547
+ /**
548
+ * Helper to simulate a new connection.
549
+ * @param driver The network driver for the connection.
550
+ * @param info Optional connection info.
551
+ */
552
+ simulateConnection(driver, info) {
553
+ if (this.onConnectionCallback) {
554
+ this.onConnectionCallback(driver, info);
555
+ }
556
+ }
557
+ /**
558
+ * Helper to simulate an error.
559
+ * @param error The error to simulate.
560
+ */
561
+ simulateError(error) {
562
+ if (this.onErrorCallback) {
563
+ this.onErrorCallback(error);
564
+ }
565
+ }
566
+ };
567
+ function createMockUDPSocket(overrides) {
568
+ const socket = {
569
+ send: import_vitest3.vi.fn(),
570
+ on: import_vitest3.vi.fn(),
571
+ close: import_vitest3.vi.fn(),
572
+ bind: import_vitest3.vi.fn(),
573
+ address: import_vitest3.vi.fn().mockReturnValue({ address: "127.0.0.1", family: "IPv4", port: 0 }),
574
+ ...overrides
575
+ };
576
+ return socket;
674
577
  }
675
- function captureAudioEvents(context) {
676
- return [];
578
+ function createMockNetworkAddress(ip = "127.0.0.1", port = 27910) {
579
+ return { ip, port };
580
+ }
581
+ function createMockTransport(address = "127.0.0.1", port = 27910, overrides) {
582
+ const transport = new MockTransport();
583
+ transport.address = address;
584
+ transport.port = port;
585
+ Object.assign(transport, overrides);
586
+ return transport;
677
587
  }
678
588
 
679
- // src/setup/timing.ts
680
- var activeMockRAF;
681
- function createMockRAF() {
682
- let callbacks = [];
683
- let nextId = 1;
684
- let currentTime = 0;
685
- const originalRAF = global.requestAnimationFrame;
686
- const originalCancelRAF = global.cancelAnimationFrame;
687
- const raf = (callback) => {
688
- const id = nextId++;
689
- callbacks.push({ id, callback });
690
- return id;
589
+ // src/server/mocks/state.ts
590
+ var import_server = require("@quake2ts/server");
591
+ var import_shared4 = require("@quake2ts/shared");
592
+ var import_vitest4 = require("vitest");
593
+ function createMockServerState(overrides) {
594
+ return {
595
+ state: import_server.ServerState.Game,
596
+ attractLoop: false,
597
+ loadGame: false,
598
+ startTime: Date.now(),
599
+ time: 0,
600
+ frame: 0,
601
+ name: "test_map",
602
+ collisionModel: null,
603
+ configStrings: new Array(import_shared4.MAX_CONFIGSTRINGS).fill(""),
604
+ baselines: new Array(import_shared4.MAX_EDICTS).fill(null),
605
+ multicastBuf: new Uint8Array(0),
606
+ ...overrides
691
607
  };
692
- const cancel = (id) => {
693
- callbacks = callbacks.filter((cb) => cb.id !== id);
608
+ }
609
+ function createMockServerStatic(maxClients = 16, overrides) {
610
+ return {
611
+ initialized: true,
612
+ realTime: Date.now(),
613
+ mapCmd: "",
614
+ spawnCount: 1,
615
+ clients: new Array(maxClients).fill(null),
616
+ lastHeartbeat: 0,
617
+ challenges: [],
618
+ ...overrides
694
619
  };
695
- const mock = {
696
- tick(timestamp) {
697
- if (typeof timestamp !== "number") {
698
- currentTime += 16.6;
699
- } else {
700
- currentTime = timestamp;
620
+ }
621
+ function createMockServerClient(clientNum, overrides) {
622
+ const mockNet = {
623
+ connect: import_vitest4.vi.fn(),
624
+ disconnect: import_vitest4.vi.fn(),
625
+ send: import_vitest4.vi.fn(),
626
+ onMessage: import_vitest4.vi.fn(),
627
+ onClose: import_vitest4.vi.fn(),
628
+ onError: import_vitest4.vi.fn(),
629
+ isConnected: import_vitest4.vi.fn().mockReturnValue(true)
630
+ };
631
+ return {
632
+ index: clientNum,
633
+ state: import_server.ClientState.Connected,
634
+ edict: { index: clientNum + 1 },
635
+ net: mockNet,
636
+ netchan: {
637
+ qport: 0,
638
+ remoteAddress: "127.0.0.1",
639
+ incomingSequence: 0,
640
+ outgoingSequence: 0,
641
+ lastReceived: 0,
642
+ process: import_vitest4.vi.fn(),
643
+ transmit: import_vitest4.vi.fn(),
644
+ writeReliableByte: import_vitest4.vi.fn(),
645
+ writeReliableShort: import_vitest4.vi.fn(),
646
+ writeReliableLong: import_vitest4.vi.fn(),
647
+ writeReliableString: import_vitest4.vi.fn(),
648
+ writeReliableData: import_vitest4.vi.fn()
649
+ },
650
+ // Cast as any because NetChan might be complex to fully mock here
651
+ userInfo: "",
652
+ lastMessage: 0,
653
+ lastCommandTime: 0,
654
+ commandCount: 0,
655
+ messageQueue: [],
656
+ frames: [],
657
+ lastFrame: 0,
658
+ lastPacketEntities: [],
659
+ challenge: 0,
660
+ lastConnect: 0,
661
+ ping: 0,
662
+ rate: 0,
663
+ name: `Client${clientNum}`,
664
+ messageLevel: 0,
665
+ datagram: new Uint8Array(0),
666
+ downloadSize: 0,
667
+ downloadCount: 0,
668
+ commandMsec: 0,
669
+ frameLatency: [],
670
+ messageSize: [],
671
+ suppressCount: 0,
672
+ commandQueue: [],
673
+ lastCmd: {
674
+ msec: 0,
675
+ buttons: 0,
676
+ angles: { x: 0, y: 0, z: 0 },
677
+ forwardmove: 0,
678
+ sidemove: 0,
679
+ upmove: 0,
680
+ sequence: 0,
681
+ lightlevel: 0,
682
+ impulse: 0,
683
+ serverFrame: 0
684
+ },
685
+ ...overrides
686
+ };
687
+ }
688
+ function createMockServer(overrides) {
689
+ return {
690
+ start: import_vitest4.vi.fn().mockResolvedValue(void 0),
691
+ stop: import_vitest4.vi.fn(),
692
+ multicast: import_vitest4.vi.fn(),
693
+ unicast: import_vitest4.vi.fn(),
694
+ configstring: import_vitest4.vi.fn(),
695
+ kickPlayer: import_vitest4.vi.fn(),
696
+ changeMap: import_vitest4.vi.fn().mockResolvedValue(void 0),
697
+ ...overrides
698
+ };
699
+ }
700
+
701
+ // src/setup/browser.ts
702
+ var import_jsdom = require("jsdom");
703
+ var import_canvas = require("@napi-rs/canvas");
704
+ var import_auto = require("fake-indexeddb/auto");
705
+
706
+ // src/e2e/input.ts
707
+ var MockPointerLock = class {
708
+ static setup(doc) {
709
+ let _pointerLockElement = null;
710
+ Object.defineProperty(doc, "pointerLockElement", {
711
+ get: () => _pointerLockElement,
712
+ configurable: true
713
+ });
714
+ doc.exitPointerLock = () => {
715
+ if (_pointerLockElement) {
716
+ _pointerLockElement = null;
717
+ doc.dispatchEvent(new Event("pointerlockchange"));
701
718
  }
702
- const currentCallbacks = [...callbacks];
703
- callbacks = [];
704
- currentCallbacks.forEach(({ callback }) => {
705
- callback(currentTime);
706
- });
719
+ };
720
+ global.HTMLElement.prototype.requestPointerLock = function() {
721
+ _pointerLockElement = this;
722
+ doc.dispatchEvent(new Event("pointerlockchange"));
723
+ };
724
+ }
725
+ };
726
+ var InputInjector = class {
727
+ constructor(doc, win) {
728
+ this.doc = doc;
729
+ this.win = win;
730
+ }
731
+ keyDown(key, code) {
732
+ const event = new this.win.KeyboardEvent("keydown", {
733
+ key,
734
+ code: code || key,
735
+ bubbles: true,
736
+ cancelable: true,
737
+ view: this.win
738
+ });
739
+ this.doc.dispatchEvent(event);
740
+ }
741
+ keyUp(key, code) {
742
+ const event = new this.win.KeyboardEvent("keyup", {
743
+ key,
744
+ code: code || key,
745
+ bubbles: true,
746
+ cancelable: true,
747
+ view: this.win
748
+ });
749
+ this.doc.dispatchEvent(event);
750
+ }
751
+ mouseMove(movementX, movementY, clientX = 0, clientY = 0) {
752
+ const event = new this.win.MouseEvent("mousemove", {
753
+ bubbles: true,
754
+ cancelable: true,
755
+ view: this.win,
756
+ clientX,
757
+ clientY,
758
+ movementX,
759
+ // Note: JSDOM might not support this standard property fully on event init
760
+ movementY
761
+ });
762
+ Object.defineProperty(event, "movementX", { value: movementX });
763
+ Object.defineProperty(event, "movementY", { value: movementY });
764
+ const target = this.doc.pointerLockElement || this.doc;
765
+ target.dispatchEvent(event);
766
+ }
767
+ mouseDown(button = 0) {
768
+ const event = new this.win.MouseEvent("mousedown", {
769
+ button,
770
+ bubbles: true,
771
+ cancelable: true,
772
+ view: this.win
773
+ });
774
+ const target = this.doc.pointerLockElement || this.doc;
775
+ target.dispatchEvent(event);
776
+ }
777
+ mouseUp(button = 0) {
778
+ const event = new this.win.MouseEvent("mouseup", {
779
+ button,
780
+ bubbles: true,
781
+ cancelable: true,
782
+ view: this.win
783
+ });
784
+ const target = this.doc.pointerLockElement || this.doc;
785
+ target.dispatchEvent(event);
786
+ }
787
+ wheel(deltaY) {
788
+ const event = new this.win.WheelEvent("wheel", {
789
+ deltaY,
790
+ bubbles: true,
791
+ cancelable: true,
792
+ view: this.win
793
+ });
794
+ const target = this.doc.pointerLockElement || this.doc;
795
+ target.dispatchEvent(event);
796
+ }
797
+ };
798
+
799
+ // src/setup/webgl.ts
800
+ function createMockWebGL2Context(canvas) {
801
+ const gl = {
802
+ canvas,
803
+ drawingBufferWidth: canvas.width,
804
+ drawingBufferHeight: canvas.height,
805
+ // Constants
806
+ VERTEX_SHADER: 35633,
807
+ FRAGMENT_SHADER: 35632,
808
+ COMPILE_STATUS: 35713,
809
+ LINK_STATUS: 35714,
810
+ ARRAY_BUFFER: 34962,
811
+ ELEMENT_ARRAY_BUFFER: 34963,
812
+ STATIC_DRAW: 35044,
813
+ DYNAMIC_DRAW: 35048,
814
+ FLOAT: 5126,
815
+ DEPTH_TEST: 2929,
816
+ BLEND: 3042,
817
+ SRC_ALPHA: 770,
818
+ ONE_MINUS_SRC_ALPHA: 771,
819
+ TEXTURE_2D: 3553,
820
+ RGBA: 6408,
821
+ UNSIGNED_BYTE: 5121,
822
+ COLOR_BUFFER_BIT: 16384,
823
+ DEPTH_BUFFER_BIT: 256,
824
+ TRIANGLES: 4,
825
+ TRIANGLE_STRIP: 5,
826
+ TRIANGLE_FAN: 6,
827
+ // Methods
828
+ createShader: () => ({}),
829
+ shaderSource: () => {
830
+ },
831
+ compileShader: () => {
832
+ },
833
+ getShaderParameter: (_, param) => {
834
+ if (param === 35713) return true;
835
+ return true;
836
+ },
837
+ getShaderInfoLog: () => "",
838
+ createProgram: () => ({}),
839
+ attachShader: () => {
840
+ },
841
+ linkProgram: () => {
842
+ },
843
+ getProgramParameter: (_, param) => {
844
+ if (param === 35714) return true;
845
+ return true;
846
+ },
847
+ getProgramInfoLog: () => "",
848
+ useProgram: () => {
849
+ },
850
+ createBuffer: () => ({}),
851
+ bindBuffer: () => {
852
+ },
853
+ bufferData: () => {
854
+ },
855
+ enableVertexAttribArray: () => {
856
+ },
857
+ vertexAttribPointer: () => {
858
+ },
859
+ enable: () => {
860
+ },
861
+ disable: () => {
862
+ },
863
+ depthMask: () => {
864
+ },
865
+ blendFunc: () => {
866
+ },
867
+ viewport: () => {
868
+ },
869
+ clearColor: () => {
707
870
  },
708
- advance(deltaMs = 16.6) {
709
- this.tick(currentTime + deltaMs);
871
+ clear: () => {
710
872
  },
711
- getCallbacks() {
712
- return callbacks.map((c) => c.callback);
873
+ createTexture: () => ({}),
874
+ bindTexture: () => {
713
875
  },
714
- reset() {
715
- callbacks = [];
716
- nextId = 1;
717
- currentTime = 0;
876
+ texImage2D: () => {
718
877
  },
719
- enable() {
720
- activeMockRAF = this;
721
- global.requestAnimationFrame = raf;
722
- global.cancelAnimationFrame = cancel;
878
+ texParameteri: () => {
723
879
  },
724
- disable() {
725
- if (activeMockRAF === this) {
726
- activeMockRAF = void 0;
727
- }
728
- if (originalRAF) {
729
- global.requestAnimationFrame = originalRAF;
730
- } else {
731
- delete global.requestAnimationFrame;
732
- }
733
- if (originalCancelRAF) {
734
- global.cancelAnimationFrame = originalCancelRAF;
735
- } else {
736
- delete global.cancelAnimationFrame;
737
- }
738
- }
739
- };
740
- return mock;
741
- }
742
- function createMockPerformance(startTime = 0) {
743
- let currentTime = startTime;
744
- const mockPerf = {
745
- now: () => currentTime,
746
- timeOrigin: startTime,
747
- timing: {
748
- navigationStart: startTime
880
+ activeTexture: () => {
749
881
  },
750
- clearMarks: () => {
882
+ uniform1i: () => {
751
883
  },
752
- clearMeasures: () => {
884
+ uniform1f: () => {
753
885
  },
754
- clearResourceTimings: () => {
886
+ uniform2f: () => {
755
887
  },
756
- getEntries: () => [],
757
- getEntriesByName: () => [],
758
- getEntriesByType: () => [],
759
- mark: () => {
888
+ uniform3f: () => {
760
889
  },
761
- measure: () => {
890
+ uniform4f: () => {
762
891
  },
763
- setResourceTimingBufferSize: () => {
892
+ uniformMatrix4fv: () => {
764
893
  },
765
- toJSON: () => ({}),
766
- addEventListener: () => {
894
+ getUniformLocation: () => ({}),
895
+ getAttribLocation: () => 0,
896
+ drawArrays: () => {
767
897
  },
768
- removeEventListener: () => {
898
+ drawElements: () => {
769
899
  },
770
- dispatchEvent: () => true
771
- };
772
- mockPerf.advance = (deltaMs) => {
773
- currentTime += deltaMs;
774
- };
775
- mockPerf.setTime = (time) => {
776
- currentTime = time;
900
+ createVertexArray: () => ({}),
901
+ bindVertexArray: () => {
902
+ },
903
+ deleteShader: () => {
904
+ },
905
+ deleteProgram: () => {
906
+ },
907
+ deleteBuffer: () => {
908
+ },
909
+ deleteTexture: () => {
910
+ },
911
+ deleteVertexArray: () => {
912
+ },
913
+ // WebGL2 specific
914
+ texImage3D: () => {
915
+ },
916
+ uniformBlockBinding: () => {
917
+ },
918
+ getExtension: () => null
777
919
  };
778
- return mockPerf;
920
+ return gl;
779
921
  }
780
- function createControlledTimer() {
781
- let currentTime = 0;
782
- let timers = [];
783
- let nextId = 1;
784
- const originalSetTimeout = global.setTimeout;
785
- const originalClearTimeout = global.clearTimeout;
786
- const originalSetInterval = global.setInterval;
787
- const originalClearInterval = global.clearInterval;
788
- const mockSetTimeout = (callback, delay = 0, ...args) => {
789
- const id = nextId++;
790
- timers.push({ id, callback, dueTime: currentTime + delay, args });
791
- return id;
792
- };
793
- const mockClearTimeout = (id) => {
794
- timers = timers.filter((t) => t.id !== id);
795
- };
796
- const mockSetInterval = (callback, delay = 0, ...args) => {
797
- const id = nextId++;
798
- timers.push({ id, callback, dueTime: currentTime + delay, interval: delay, args });
799
- return id;
800
- };
801
- const mockClearInterval = (id) => {
802
- timers = timers.filter((t) => t.id !== id);
803
- };
804
- global.setTimeout = mockSetTimeout;
805
- global.clearTimeout = mockClearTimeout;
806
- global.setInterval = mockSetInterval;
807
- global.clearInterval = mockClearInterval;
808
- return {
809
- tick() {
810
- this.advanceBy(0);
811
- },
812
- advanceBy(ms) {
813
- const targetTime = currentTime + ms;
814
- while (true) {
815
- let earliest = null;
816
- for (const t of timers) {
817
- if (!earliest || t.dueTime < earliest.dueTime) {
818
- earliest = t;
819
- }
922
+
923
+ // src/setup/browser.ts
924
+ function setupBrowserEnvironment(options = {}) {
925
+ const {
926
+ url = "http://localhost",
927
+ pretendToBeVisual = true,
928
+ resources = void 0,
929
+ enableWebGL2 = false,
930
+ enablePointerLock = false
931
+ } = options;
932
+ const dom = new import_jsdom.JSDOM("<!DOCTYPE html><html><head></head><body></body></html>", {
933
+ url,
934
+ pretendToBeVisual,
935
+ resources
936
+ });
937
+ global.window = dom.window;
938
+ global.document = dom.window.document;
939
+ try {
940
+ global.navigator = dom.window.navigator;
941
+ } catch (e) {
942
+ try {
943
+ Object.defineProperty(global, "navigator", {
944
+ value: dom.window.navigator,
945
+ writable: true,
946
+ configurable: true
947
+ });
948
+ } catch (e2) {
949
+ console.warn("Could not assign global.navigator, skipping.");
950
+ }
951
+ }
952
+ global.location = dom.window.location;
953
+ global.HTMLElement = dom.window.HTMLElement;
954
+ global.HTMLCanvasElement = dom.window.HTMLCanvasElement;
955
+ global.Event = dom.window.Event;
956
+ global.CustomEvent = dom.window.CustomEvent;
957
+ global.DragEvent = dom.window.DragEvent;
958
+ global.MouseEvent = dom.window.MouseEvent;
959
+ global.KeyboardEvent = dom.window.KeyboardEvent;
960
+ global.FocusEvent = dom.window.FocusEvent;
961
+ global.WheelEvent = dom.window.WheelEvent;
962
+ global.InputEvent = dom.window.InputEvent;
963
+ global.UIEvent = dom.window.UIEvent;
964
+ try {
965
+ global.localStorage = dom.window.localStorage;
966
+ } catch (e) {
967
+ }
968
+ if (!global.localStorage) {
969
+ const storage = /* @__PURE__ */ new Map();
970
+ global.localStorage = {
971
+ getItem: (key) => storage.get(key) || null,
972
+ setItem: (key, value) => storage.set(key, value),
973
+ removeItem: (key) => storage.delete(key),
974
+ clear: () => storage.clear(),
975
+ key: (index) => Array.from(storage.keys())[index] || null,
976
+ get length() {
977
+ return storage.size;
978
+ }
979
+ };
980
+ }
981
+ const originalCreateElement = document.createElement.bind(document);
982
+ document.createElement = function(tagName, options2) {
983
+ if (tagName.toLowerCase() === "canvas") {
984
+ const napiCanvas = new import_canvas.Canvas(300, 150);
985
+ const domCanvas = originalCreateElement("canvas", options2);
986
+ Object.defineProperty(domCanvas, "width", {
987
+ get: () => napiCanvas.width,
988
+ set: (value) => {
989
+ napiCanvas.width = value;
990
+ },
991
+ enumerable: true,
992
+ configurable: true
993
+ });
994
+ Object.defineProperty(domCanvas, "height", {
995
+ get: () => napiCanvas.height,
996
+ set: (value) => {
997
+ napiCanvas.height = value;
998
+ },
999
+ enumerable: true,
1000
+ configurable: true
1001
+ });
1002
+ const originalGetContext = domCanvas.getContext.bind(domCanvas);
1003
+ domCanvas.getContext = function(contextId, options3) {
1004
+ if (contextId === "2d") {
1005
+ return napiCanvas.getContext("2d", options3);
820
1006
  }
821
- if (!earliest || earliest.dueTime > targetTime) {
822
- break;
1007
+ if (enableWebGL2 && contextId === "webgl2") {
1008
+ return createMockWebGL2Context(domCanvas);
823
1009
  }
824
- currentTime = earliest.dueTime;
825
- const { callback, args, interval, id } = earliest;
826
- if (interval !== void 0) {
827
- earliest.dueTime += interval;
828
- if (interval === 0) earliest.dueTime += 1;
829
- } else {
830
- timers = timers.filter((t) => t.id !== id);
1010
+ if (contextId === "webgl" || contextId === "webgl2") {
1011
+ return originalGetContext(contextId, options3);
1012
+ }
1013
+ return napiCanvas.getContext(contextId, options3);
1014
+ };
1015
+ domCanvas.__napiCanvas = napiCanvas;
1016
+ return domCanvas;
1017
+ }
1018
+ return originalCreateElement(tagName, options2);
1019
+ };
1020
+ if (enableWebGL2) {
1021
+ const originalProtoGetContext = global.HTMLCanvasElement.prototype.getContext;
1022
+ global.HTMLCanvasElement.prototype.getContext = function(contextId, options2) {
1023
+ if (contextId === "webgl2") {
1024
+ return createMockWebGL2Context(this);
1025
+ }
1026
+ return originalProtoGetContext.call(this, contextId, options2);
1027
+ };
1028
+ }
1029
+ global.Image = import_canvas.Image;
1030
+ global.ImageData = import_canvas.ImageData;
1031
+ if (typeof global.createImageBitmap === "undefined") {
1032
+ global.createImageBitmap = async function(image, _options) {
1033
+ if (image && typeof image.width === "number" && typeof image.height === "number") {
1034
+ const canvas2 = new import_canvas.Canvas(image.width, image.height);
1035
+ const ctx = canvas2.getContext("2d");
1036
+ if (image.data) {
1037
+ ctx.putImageData(image, 0, 0);
831
1038
  }
832
- callback(...args);
1039
+ return canvas2;
833
1040
  }
834
- currentTime = targetTime;
835
- },
836
- clear() {
837
- timers = [];
838
- },
839
- restore() {
840
- global.setTimeout = originalSetTimeout;
841
- global.clearTimeout = originalClearTimeout;
842
- global.setInterval = originalSetInterval;
843
- global.clearInterval = originalClearInterval;
844
- }
845
- };
846
- }
847
- function simulateFrames(count, frameTimeMs = 16.6, callback) {
848
- if (!activeMockRAF) {
849
- throw new Error("simulateFrames requires an active MockRAF. Ensure createMockRAF().enable() is called.");
1041
+ const canvas = new import_canvas.Canvas(100, 100);
1042
+ return canvas;
1043
+ };
850
1044
  }
851
- for (let i = 0; i < count; i++) {
852
- if (callback) callback(i);
853
- activeMockRAF.advance(frameTimeMs);
1045
+ if (typeof global.btoa === "undefined") {
1046
+ global.btoa = function(str) {
1047
+ return Buffer.from(str, "binary").toString("base64");
1048
+ };
854
1049
  }
855
- }
856
- function simulateFramesWithMock(mock, count, frameTimeMs = 16.6, callback) {
857
- for (let i = 0; i < count; i++) {
858
- if (callback) callback(i);
859
- mock.advance(frameTimeMs);
1050
+ if (typeof global.atob === "undefined") {
1051
+ global.atob = function(str) {
1052
+ return Buffer.from(str, "base64").toString("binary");
1053
+ };
1054
+ }
1055
+ if (enablePointerLock) {
1056
+ MockPointerLock.setup(global.document);
1057
+ }
1058
+ if (typeof global.requestAnimationFrame === "undefined") {
1059
+ let lastTime = 0;
1060
+ global.requestAnimationFrame = (callback) => {
1061
+ const currTime = Date.now();
1062
+ const timeToCall = Math.max(0, 16 - (currTime - lastTime));
1063
+ const id = setTimeout(() => {
1064
+ callback(currTime + timeToCall);
1065
+ }, timeToCall);
1066
+ lastTime = currTime + timeToCall;
1067
+ return id;
1068
+ };
1069
+ global.cancelAnimationFrame = (id) => {
1070
+ clearTimeout(id);
1071
+ };
860
1072
  }
861
1073
  }
1074
+ function teardownBrowserEnvironment() {
1075
+ delete global.window;
1076
+ delete global.document;
1077
+ delete global.navigator;
1078
+ delete global.localStorage;
1079
+ delete global.location;
1080
+ delete global.HTMLElement;
1081
+ delete global.HTMLCanvasElement;
1082
+ delete global.Image;
1083
+ delete global.ImageData;
1084
+ delete global.createImageBitmap;
1085
+ delete global.Event;
1086
+ delete global.CustomEvent;
1087
+ delete global.DragEvent;
1088
+ delete global.MouseEvent;
1089
+ delete global.KeyboardEvent;
1090
+ delete global.FocusEvent;
1091
+ delete global.WheelEvent;
1092
+ delete global.InputEvent;
1093
+ delete global.UIEvent;
1094
+ }
862
1095
 
863
- // src/e2e/playwright.ts
864
- var import_playwright = require("playwright");
865
- var import_http = require("http");
866
- var import_serve_handler = __toESM(require("serve-handler"), 1);
867
- async function createPlaywrightTestClient(options = {}) {
868
- let staticServer;
869
- let clientUrl = options.clientUrl;
870
- const rootPath = options.rootPath || process.cwd();
871
- if (!clientUrl) {
872
- staticServer = (0, import_http.createServer)((request, response) => {
873
- return (0, import_serve_handler.default)(request, response, {
874
- public: rootPath,
875
- cleanUrls: false,
876
- headers: [
877
- {
878
- source: "**/*",
879
- headers: [
880
- { key: "Cache-Control", value: "no-cache" },
881
- { key: "Access-Control-Allow-Origin", value: "*" },
882
- { key: "Cross-Origin-Opener-Policy", value: "same-origin" },
883
- { key: "Cross-Origin-Embedder-Policy", value: "require-corp" }
884
- ]
885
- }
886
- ]
887
- });
888
- });
889
- await new Promise((resolve) => {
890
- if (!staticServer) return;
891
- staticServer.listen(0, () => {
892
- const addr = staticServer?.address();
893
- const port = typeof addr === "object" ? addr?.port : 0;
894
- clientUrl = `http://localhost:${port}`;
895
- console.log(`Test client serving from ${rootPath} at ${clientUrl}`);
896
- resolve();
897
- });
898
- });
1096
+ // src/setup/canvas.ts
1097
+ var import_canvas2 = require("@napi-rs/canvas");
1098
+ function createMockCanvas(width = 300, height = 150) {
1099
+ if (typeof document !== "undefined" && document.createElement) {
1100
+ const canvas2 = document.createElement("canvas");
1101
+ canvas2.width = width;
1102
+ canvas2.height = height;
1103
+ return canvas2;
899
1104
  }
900
- const browser = await import_playwright.chromium.launch({
901
- headless: options.headless ?? true,
902
- args: [
903
- "--use-gl=egl",
904
- "--ignore-gpu-blocklist",
905
- ...options.launchOptions?.args || []
906
- ],
907
- ...options.launchOptions
908
- });
909
- const width = options.width || 1280;
910
- const height = options.height || 720;
911
- const context = await browser.newContext({
912
- viewport: { width, height },
913
- deviceScaleFactor: 1,
914
- ...options.contextOptions
915
- });
916
- const page = await context.newPage();
917
- const close = async () => {
918
- await browser.close();
919
- if (staticServer) {
920
- staticServer.close();
921
- }
922
- };
923
- const navigate = async (url) => {
924
- const targetUrl = url || clientUrl;
925
- if (!targetUrl) throw new Error("No URL to navigate to");
926
- let finalUrl = targetUrl;
927
- if (options.serverUrl && !targetUrl.includes("connect=")) {
928
- const separator = targetUrl.includes("?") ? "&" : "?";
929
- finalUrl = `${targetUrl}${separator}connect=${encodeURIComponent(options.serverUrl)}`;
1105
+ const canvas = new import_canvas2.Canvas(width, height);
1106
+ const originalGetContext = canvas.getContext.bind(canvas);
1107
+ canvas.getContext = function(contextId, options) {
1108
+ if (contextId === "webgl2") {
1109
+ return createMockWebGL2Context(canvas);
930
1110
  }
931
- console.log(`Navigating to: ${finalUrl}`);
932
- await page.goto(finalUrl, { waitUntil: "domcontentloaded" });
933
- };
934
- return {
935
- browser,
936
- context,
937
- page,
938
- server: staticServer,
939
- close,
940
- navigate,
941
- waitForGame: async (timeout = 1e4) => {
942
- await waitForGameReady(page, timeout);
943
- },
944
- injectInput: async (type, data) => {
945
- await page.evaluate(({ type: type2, data: data2 }) => {
946
- if (window.injectGameInput) window.injectGameInput(type2, data2);
947
- }, { type, data });
1111
+ if (contextId === "2d") {
1112
+ return originalGetContext("2d", options);
948
1113
  }
1114
+ return originalGetContext(contextId, options);
949
1115
  };
1116
+ return canvas;
950
1117
  }
951
- async function waitForGameReady(page, timeout = 1e4) {
952
- try {
953
- await page.waitForFunction(() => {
954
- return window.gameInstance && window.gameInstance.isReady;
955
- }, null, { timeout });
956
- } catch (e) {
957
- await page.waitForSelector("canvas", { timeout });
1118
+ function createMockCanvasContext2D(canvas) {
1119
+ if (!canvas) {
1120
+ canvas = createMockCanvas();
958
1121
  }
1122
+ return canvas.getContext("2d");
959
1123
  }
960
- async function captureGameState(page) {
961
- return await page.evaluate(() => {
962
- if (window.gameInstance && window.gameInstance.getState) {
963
- return window.gameInstance.getState();
1124
+ function captureCanvasDrawCalls(context) {
1125
+ const drawCalls = [];
1126
+ const methodsToSpy = [
1127
+ "fillRect",
1128
+ "strokeRect",
1129
+ "clearRect",
1130
+ "fillText",
1131
+ "strokeText",
1132
+ "drawImage",
1133
+ "beginPath",
1134
+ "closePath",
1135
+ "moveTo",
1136
+ "lineTo",
1137
+ "arc",
1138
+ "arcTo",
1139
+ "bezierCurveTo",
1140
+ "quadraticCurveTo",
1141
+ "stroke",
1142
+ "fill",
1143
+ "putImageData"
1144
+ ];
1145
+ methodsToSpy.forEach((method) => {
1146
+ const original = context[method];
1147
+ if (typeof original === "function") {
1148
+ context[method] = function(...args) {
1149
+ drawCalls.push({ method, args });
1150
+ return original.apply(this, args);
1151
+ };
964
1152
  }
965
- return {};
966
1153
  });
1154
+ return drawCalls;
967
1155
  }
968
-
969
- // src/shared/bsp.ts
970
- var import_shared = require("@quake2ts/shared");
971
- function makePlane(normal, dist) {
972
- return {
973
- normal,
974
- dist,
975
- type: Math.abs(normal.x) === 1 ? 0 : Math.abs(normal.y) === 1 ? 1 : Math.abs(normal.z) === 1 ? 2 : 3,
976
- signbits: (0, import_shared.computePlaneSignBits)(normal)
977
- };
978
- }
979
- function makeAxisBrush(size, contents = import_shared.CONTENTS_SOLID) {
980
- const half = size / 2;
981
- const planes = [
982
- makePlane({ x: 1, y: 0, z: 0 }, half),
983
- makePlane({ x: -1, y: 0, z: 0 }, half),
984
- makePlane({ x: 0, y: 1, z: 0 }, half),
985
- makePlane({ x: 0, y: -1, z: 0 }, half),
986
- makePlane({ x: 0, y: 0, z: 1 }, half),
987
- makePlane({ x: 0, y: 0, z: -1 }, half)
988
- ];
989
- return {
990
- contents,
991
- sides: planes.map((plane) => ({ plane, surfaceFlags: 0 }))
992
- };
1156
+ function createMockImageData(width, height, fillColor) {
1157
+ const imageData = new import_canvas2.ImageData(width, height);
1158
+ if (fillColor) {
1159
+ const [r, g, b, a] = fillColor;
1160
+ for (let i = 0; i < imageData.data.length; i += 4) {
1161
+ imageData.data[i] = r;
1162
+ imageData.data[i + 1] = g;
1163
+ imageData.data[i + 2] = b;
1164
+ imageData.data[i + 3] = a;
1165
+ }
1166
+ }
1167
+ return imageData;
993
1168
  }
994
- function makeNode(plane, children) {
995
- return { plane, children };
1169
+ function createMockImage(width, height, src) {
1170
+ const img = new import_canvas2.Image();
1171
+ if (width) img.width = width;
1172
+ if (height) img.height = height;
1173
+ if (src) img.src = src;
1174
+ return img;
996
1175
  }
997
- function makeBspModel(planes, nodes, leaves, brushes, leafBrushes) {
1176
+
1177
+ // src/setup/node.ts
1178
+ function setupNodeEnvironment(options = {}) {
1179
+ if (options.polyfillFetch && typeof global.fetch === "undefined") {
1180
+ }
1181
+ }
1182
+
1183
+ // src/setup/storage.ts
1184
+ var import_auto2 = require("fake-indexeddb/auto");
1185
+ function createMockLocalStorage(initialData = {}) {
1186
+ const storage = new Map(Object.entries(initialData));
998
1187
  return {
999
- planes,
1000
- nodes,
1001
- leaves,
1002
- brushes,
1003
- leafBrushes,
1004
- bmodels: []
1188
+ getItem: (key) => storage.get(key) || null,
1189
+ setItem: (key, value) => storage.set(key, value),
1190
+ removeItem: (key) => storage.delete(key),
1191
+ clear: () => storage.clear(),
1192
+ key: (index) => Array.from(storage.keys())[index] || null,
1193
+ get length() {
1194
+ return storage.size;
1195
+ }
1005
1196
  };
1006
1197
  }
1007
- function makeLeaf(contents, firstLeafBrush, numLeafBrushes) {
1008
- return { contents, cluster: 0, area: 0, firstLeafBrush, numLeafBrushes };
1198
+ function createMockSessionStorage(initialData = {}) {
1199
+ return createMockLocalStorage(initialData);
1009
1200
  }
1010
- function makeLeafModel(brushes) {
1011
- const planes = brushes.flatMap((brush) => brush.sides.map((side) => side.plane));
1012
- return {
1013
- planes,
1014
- nodes: [],
1015
- leaves: [makeLeaf(0, 0, brushes.length)],
1016
- brushes,
1017
- leafBrushes: brushes.map((_, i) => i),
1018
- bmodels: []
1019
- };
1201
+ function createMockIndexedDB() {
1202
+ if (typeof indexedDB === "undefined") {
1203
+ throw new Error("IndexedDB mock not found. Ensure fake-indexeddb is loaded.");
1204
+ }
1205
+ return indexedDB;
1020
1206
  }
1021
- function makeBrushFromMinsMaxs(mins, maxs, contents = import_shared.CONTENTS_SOLID) {
1022
- const planes = [
1023
- makePlane({ x: 1, y: 0, z: 0 }, maxs.x),
1024
- makePlane({ x: -1, y: 0, z: 0 }, -mins.x),
1025
- makePlane({ x: 0, y: 1, z: 0 }, maxs.y),
1026
- makePlane({ x: 0, y: -1, z: 0 }, -mins.y),
1027
- makePlane({ x: 0, y: 0, z: 1 }, maxs.z),
1028
- makePlane({ x: 0, y: 0, z: -1 }, -mins.z)
1029
- ];
1207
+ function createStorageTestScenario(storageType = "local") {
1208
+ if (storageType === "indexed") {
1209
+ const dbName = `test-db-${Math.random().toString(36).substring(7)}`;
1210
+ const storeName = "test-store";
1211
+ const storage2 = createMockIndexedDB();
1212
+ return {
1213
+ storage: storage2,
1214
+ populate: async (data) => {
1215
+ return new Promise((resolve, reject) => {
1216
+ const req = storage2.open(dbName, 1);
1217
+ req.onupgradeneeded = (e) => {
1218
+ const db = e.target.result;
1219
+ db.createObjectStore(storeName);
1220
+ };
1221
+ req.onsuccess = (e) => {
1222
+ const db = e.target.result;
1223
+ const tx = db.transaction(storeName, "readwrite");
1224
+ const store = tx.objectStore(storeName);
1225
+ Object.entries(data).forEach(([k, v]) => store.put(v, k));
1226
+ tx.oncomplete = () => {
1227
+ db.close();
1228
+ resolve();
1229
+ };
1230
+ tx.onerror = () => reject(tx.error);
1231
+ };
1232
+ req.onerror = () => reject(req.error);
1233
+ });
1234
+ },
1235
+ verify: async (key, value) => {
1236
+ return new Promise((resolve, reject) => {
1237
+ const req = storage2.open(dbName, 1);
1238
+ req.onsuccess = (e) => {
1239
+ const db = e.target.result;
1240
+ if (!db.objectStoreNames.contains(storeName)) {
1241
+ db.close();
1242
+ resolve(false);
1243
+ return;
1244
+ }
1245
+ const tx = db.transaction(storeName, "readonly");
1246
+ const store = tx.objectStore(storeName);
1247
+ const getReq = store.get(key);
1248
+ getReq.onsuccess = () => {
1249
+ const result = getReq.result === value;
1250
+ db.close();
1251
+ resolve(result);
1252
+ };
1253
+ getReq.onerror = () => {
1254
+ db.close();
1255
+ resolve(false);
1256
+ };
1257
+ };
1258
+ req.onerror = () => reject(req.error);
1259
+ });
1260
+ }
1261
+ };
1262
+ }
1263
+ const storage = storageType === "local" ? createMockLocalStorage() : createMockSessionStorage();
1030
1264
  return {
1031
- contents,
1032
- sides: planes.map((plane) => ({ plane, surfaceFlags: 0 }))
1033
- };
1034
- }
1035
-
1036
- // src/shared/mocks.ts
1037
- var import_vitest = require("vitest");
1038
- var createBinaryWriterMock = () => ({
1039
- writeByte: import_vitest.vi.fn(),
1040
- writeShort: import_vitest.vi.fn(),
1041
- writeLong: import_vitest.vi.fn(),
1042
- writeString: import_vitest.vi.fn(),
1043
- writeBytes: import_vitest.vi.fn(),
1044
- getBuffer: import_vitest.vi.fn(() => new Uint8Array(0)),
1045
- reset: import_vitest.vi.fn(),
1046
- // Legacy methods (if any)
1047
- writeInt8: import_vitest.vi.fn(),
1048
- writeUint8: import_vitest.vi.fn(),
1049
- writeInt16: import_vitest.vi.fn(),
1050
- writeUint16: import_vitest.vi.fn(),
1051
- writeInt32: import_vitest.vi.fn(),
1052
- writeUint32: import_vitest.vi.fn(),
1053
- writeFloat: import_vitest.vi.fn(),
1054
- getData: import_vitest.vi.fn(() => new Uint8Array(0))
1055
- });
1056
- var createNetChanMock = () => ({
1057
- qport: 1234,
1058
- // Sequencing
1059
- incomingSequence: 0,
1060
- outgoingSequence: 0,
1061
- incomingAcknowledged: 0,
1062
- // Reliable messaging
1063
- incomingReliableAcknowledged: false,
1064
- incomingReliableSequence: 0,
1065
- outgoingReliableSequence: 0,
1066
- reliableMessage: createBinaryWriterMock(),
1067
- reliableLength: 0,
1068
- // Fragmentation
1069
- fragmentSendOffset: 0,
1070
- fragmentBuffer: null,
1071
- fragmentLength: 0,
1072
- fragmentReceived: 0,
1073
- // Timing
1074
- lastReceived: 0,
1075
- lastSent: 0,
1076
- remoteAddress: { type: "IP", port: 1234 },
1077
- // Methods
1078
- setup: import_vitest.vi.fn(),
1079
- reset: import_vitest.vi.fn(),
1080
- transmit: import_vitest.vi.fn(),
1081
- process: import_vitest.vi.fn(),
1082
- canSendReliable: import_vitest.vi.fn(() => true),
1083
- writeReliableByte: import_vitest.vi.fn(),
1084
- writeReliableShort: import_vitest.vi.fn(),
1085
- writeReliableLong: import_vitest.vi.fn(),
1086
- writeReliableString: import_vitest.vi.fn(),
1087
- getReliableData: import_vitest.vi.fn(() => new Uint8Array(0)),
1088
- needsKeepalive: import_vitest.vi.fn(() => false),
1089
- isTimedOut: import_vitest.vi.fn(() => false)
1090
- });
1091
- var createBinaryStreamMock = () => ({
1092
- getPosition: import_vitest.vi.fn(() => 0),
1093
- getReadPosition: import_vitest.vi.fn(() => 0),
1094
- getLength: import_vitest.vi.fn(() => 0),
1095
- getRemaining: import_vitest.vi.fn(() => 0),
1096
- seek: import_vitest.vi.fn(),
1097
- setReadPosition: import_vitest.vi.fn(),
1098
- hasMore: import_vitest.vi.fn(() => true),
1099
- hasBytes: import_vitest.vi.fn((amount) => true),
1100
- readChar: import_vitest.vi.fn(() => 0),
1101
- readByte: import_vitest.vi.fn(() => 0),
1102
- readShort: import_vitest.vi.fn(() => 0),
1103
- readUShort: import_vitest.vi.fn(() => 0),
1104
- readLong: import_vitest.vi.fn(() => 0),
1105
- readULong: import_vitest.vi.fn(() => 0),
1106
- readFloat: import_vitest.vi.fn(() => 0),
1107
- readString: import_vitest.vi.fn(() => ""),
1108
- readStringLine: import_vitest.vi.fn(() => ""),
1109
- readCoord: import_vitest.vi.fn(() => 0),
1110
- readAngle: import_vitest.vi.fn(() => 0),
1111
- readAngle16: import_vitest.vi.fn(() => 0),
1112
- readData: import_vitest.vi.fn((length) => new Uint8Array(length)),
1113
- readPos: import_vitest.vi.fn(),
1114
- readDir: import_vitest.vi.fn()
1115
- });
1116
-
1117
- // src/game/factories.ts
1118
- var createPlayerStateFactory = (overrides) => ({
1119
- pm_type: 0,
1120
- pm_time: 0,
1121
- pm_flags: 0,
1122
- origin: { x: 0, y: 0, z: 0 },
1123
- velocity: { x: 0, y: 0, z: 0 },
1124
- viewAngles: { x: 0, y: 0, z: 0 },
1125
- onGround: false,
1126
- waterLevel: 0,
1127
- watertype: 0,
1128
- mins: { x: 0, y: 0, z: 0 },
1129
- maxs: { x: 0, y: 0, z: 0 },
1130
- damageAlpha: 0,
1131
- damageIndicators: [],
1132
- blend: [0, 0, 0, 0],
1133
- stats: [],
1134
- kick_angles: { x: 0, y: 0, z: 0 },
1135
- kick_origin: { x: 0, y: 0, z: 0 },
1136
- gunoffset: { x: 0, y: 0, z: 0 },
1137
- gunangles: { x: 0, y: 0, z: 0 },
1138
- gunindex: 0,
1139
- gun_frame: 0,
1140
- rdflags: 0,
1141
- fov: 90,
1142
- renderfx: 0,
1143
- ...overrides
1144
- });
1145
- var createEntityStateFactory = (overrides) => ({
1146
- number: 0,
1147
- origin: { x: 0, y: 0, z: 0 },
1148
- angles: { x: 0, y: 0, z: 0 },
1149
- oldOrigin: { x: 0, y: 0, z: 0 },
1150
- modelIndex: 0,
1151
- modelIndex2: 0,
1152
- modelIndex3: 0,
1153
- modelIndex4: 0,
1154
- frame: 0,
1155
- skinNum: 0,
1156
- effects: 0,
1157
- renderfx: 0,
1158
- solid: 0,
1159
- sound: 0,
1160
- event: 0,
1161
- ...overrides
1162
- });
1163
- var createGameStateSnapshotFactory = (overrides) => ({
1164
- gravity: { x: 0, y: 0, z: -800 },
1165
- origin: { x: 0, y: 0, z: 0 },
1166
- velocity: { x: 0, y: 0, z: 0 },
1167
- viewangles: { x: 0, y: 0, z: 0 },
1168
- level: { timeSeconds: 0, frameNumber: 0, previousTimeSeconds: 0, deltaSeconds: 0.1 },
1169
- entities: {
1170
- activeCount: 0,
1171
- worldClassname: "worldspawn"
1172
- },
1173
- packetEntities: [],
1174
- pmFlags: 0,
1175
- pmType: 0,
1176
- waterlevel: 0,
1177
- watertype: 0,
1178
- deltaAngles: { x: 0, y: 0, z: 0 },
1179
- health: 100,
1180
- armor: 0,
1181
- ammo: 0,
1182
- blend: [0, 0, 0, 0],
1183
- damageAlpha: 0,
1184
- damageIndicators: [],
1185
- stats: [],
1186
- kick_angles: { x: 0, y: 0, z: 0 },
1187
- kick_origin: { x: 0, y: 0, z: 0 },
1188
- gunoffset: { x: 0, y: 0, z: 0 },
1189
- gunangles: { x: 0, y: 0, z: 0 },
1190
- gunindex: 0,
1191
- pm_time: 0,
1192
- gun_frame: 0,
1193
- rdflags: 0,
1194
- fov: 90,
1195
- renderfx: 0,
1196
- pm_flags: 0,
1197
- pm_type: 0,
1198
- ...overrides
1199
- });
1200
-
1201
- // src/game/helpers.ts
1202
- var import_vitest2 = require("vitest");
1203
- var import_game = require("@quake2ts/game");
1204
- var import_shared2 = require("@quake2ts/shared");
1205
- var import_shared3 = require("@quake2ts/shared");
1206
- var createMockEngine = () => ({
1207
- sound: import_vitest2.vi.fn(),
1208
- soundIndex: import_vitest2.vi.fn((sound) => 0),
1209
- modelIndex: import_vitest2.vi.fn((model) => 0),
1210
- centerprintf: import_vitest2.vi.fn()
1211
- });
1212
- var createMockGame = (seed = 12345) => {
1213
- const spawnRegistry = new import_game.SpawnRegistry();
1214
- const hooks = new import_game.ScriptHookRegistry();
1215
- const game = {
1216
- random: (0, import_shared2.createRandomGenerator)({ seed }),
1217
- registerEntitySpawn: import_vitest2.vi.fn((classname, spawnFunc) => {
1218
- spawnRegistry.register(classname, (entity) => spawnFunc(entity));
1265
+ storage,
1266
+ populate(data) {
1267
+ Object.entries(data).forEach(([k, v]) => storage.setItem(k, v));
1268
+ },
1269
+ verify(key, value) {
1270
+ return storage.getItem(key) === value;
1271
+ }
1272
+ };
1273
+ }
1274
+
1275
+ // src/setup/audio.ts
1276
+ function createMockAudioContext() {
1277
+ const context = {
1278
+ createGain: () => ({
1279
+ connect: () => {
1280
+ },
1281
+ gain: { value: 1, setValueAtTime: () => {
1282
+ } }
1219
1283
  }),
1220
- unregisterEntitySpawn: import_vitest2.vi.fn((classname) => {
1221
- spawnRegistry.unregister(classname);
1284
+ createOscillator: () => ({
1285
+ connect: () => {
1286
+ },
1287
+ start: () => {
1288
+ },
1289
+ stop: () => {
1290
+ },
1291
+ frequency: { value: 440 }
1222
1292
  }),
1223
- getCustomEntities: import_vitest2.vi.fn(() => Array.from(spawnRegistry.keys())),
1224
- hooks,
1225
- registerHooks: import_vitest2.vi.fn((newHooks) => hooks.register(newHooks)),
1226
- spawnWorld: import_vitest2.vi.fn(() => {
1227
- hooks.onMapLoad("q2dm1");
1293
+ createBufferSource: () => ({
1294
+ connect: () => {
1295
+ },
1296
+ start: () => {
1297
+ },
1298
+ stop: () => {
1299
+ },
1300
+ buffer: null,
1301
+ playbackRate: { value: 1 },
1302
+ loop: false
1228
1303
  }),
1229
- clientBegin: import_vitest2.vi.fn((client) => {
1230
- hooks.onPlayerSpawn({});
1304
+ destination: {},
1305
+ currentTime: 0,
1306
+ state: "running",
1307
+ resume: async () => {
1308
+ },
1309
+ suspend: async () => {
1310
+ },
1311
+ close: async () => {
1312
+ },
1313
+ decodeAudioData: async (buffer) => ({
1314
+ duration: 1,
1315
+ length: 44100,
1316
+ sampleRate: 44100,
1317
+ numberOfChannels: 2,
1318
+ getChannelData: () => new Float32Array(44100)
1231
1319
  }),
1232
- damage: import_vitest2.vi.fn((amount) => {
1233
- hooks.onDamage({}, null, null, amount, 0, 0);
1234
- })
1235
- };
1236
- return { game, spawnRegistry };
1237
- };
1238
- function createTestContext(options) {
1239
- const engine = createMockEngine();
1240
- const seed = options?.seed ?? 12345;
1241
- const { game, spawnRegistry } = createMockGame(seed);
1242
- const traceFn = import_vitest2.vi.fn((start, end, mins, maxs) => ({
1243
- fraction: 1,
1244
- ent: null,
1245
- allsolid: false,
1246
- startsolid: false,
1247
- endpos: end,
1248
- plane: { normal: { x: 0, y: 0, z: 1 }, dist: 0 },
1249
- surfaceFlags: 0,
1250
- contents: 0
1251
- }));
1252
- const entityList = options?.initialEntities ? [...options.initialEntities] : [];
1253
- const hooks = game.hooks;
1254
- const entities = {
1255
- spawn: import_vitest2.vi.fn(() => {
1256
- const ent = new import_game.Entity(entityList.length + 1);
1257
- entityList.push(ent);
1258
- hooks.onEntitySpawn(ent);
1259
- return ent;
1320
+ createBuffer: (channels, length, sampleRate) => ({
1321
+ duration: length / sampleRate,
1322
+ length,
1323
+ sampleRate,
1324
+ numberOfChannels: channels,
1325
+ getChannelData: () => new Float32Array(length)
1260
1326
  }),
1261
- free: import_vitest2.vi.fn((ent) => {
1262
- const idx = entityList.indexOf(ent);
1263
- if (idx !== -1) {
1264
- entityList.splice(idx, 1);
1327
+ // Helper to track events if needed
1328
+ _events: []
1329
+ };
1330
+ return new Proxy(context, {
1331
+ get(target, prop, receiver) {
1332
+ if (prop === "_events") return target._events;
1333
+ const value = Reflect.get(target, prop, receiver);
1334
+ if (typeof value === "function") {
1335
+ return (...args) => {
1336
+ target._events.push({ type: String(prop), args });
1337
+ return Reflect.apply(value, target, args);
1338
+ };
1265
1339
  }
1266
- hooks.onEntityRemove(ent);
1267
- }),
1268
- finalizeSpawn: import_vitest2.vi.fn(),
1269
- freeImmediate: import_vitest2.vi.fn((ent) => {
1270
- const idx = entityList.indexOf(ent);
1271
- if (idx !== -1) {
1272
- entityList.splice(idx, 1);
1340
+ return value;
1341
+ }
1342
+ });
1343
+ }
1344
+ function setupMockAudioContext() {
1345
+ if (typeof global.AudioContext === "undefined" && typeof global.window !== "undefined") {
1346
+ global.AudioContext = class {
1347
+ constructor() {
1348
+ return createMockAudioContext();
1349
+ }
1350
+ };
1351
+ global.window.AudioContext = global.AudioContext;
1352
+ global.window.webkitAudioContext = global.AudioContext;
1353
+ }
1354
+ }
1355
+ function teardownMockAudioContext() {
1356
+ if (global.AudioContext && global.AudioContext.toString().includes("class")) {
1357
+ delete global.AudioContext;
1358
+ delete global.window.AudioContext;
1359
+ delete global.window.webkitAudioContext;
1360
+ }
1361
+ }
1362
+ function captureAudioEvents(context) {
1363
+ return context._events || [];
1364
+ }
1365
+
1366
+ // src/setup/timing.ts
1367
+ var activeMockRAF;
1368
+ function createMockRAF() {
1369
+ let callbacks = [];
1370
+ let nextId = 1;
1371
+ let currentTime = 0;
1372
+ const originalRAF = global.requestAnimationFrame;
1373
+ const originalCancelRAF = global.cancelAnimationFrame;
1374
+ const raf = (callback) => {
1375
+ const id = nextId++;
1376
+ callbacks.push({ id, callback });
1377
+ return id;
1378
+ };
1379
+ const cancel = (id) => {
1380
+ callbacks = callbacks.filter((cb) => cb.id !== id);
1381
+ };
1382
+ const mock = {
1383
+ tick(timestamp) {
1384
+ if (typeof timestamp !== "number") {
1385
+ currentTime += 16.6;
1386
+ } else {
1387
+ currentTime = timestamp;
1388
+ }
1389
+ const currentCallbacks = [...callbacks];
1390
+ callbacks = [];
1391
+ currentCallbacks.forEach(({ callback }) => {
1392
+ callback(currentTime);
1393
+ });
1394
+ },
1395
+ advance(deltaMs = 16.6) {
1396
+ this.tick(currentTime + deltaMs);
1397
+ },
1398
+ getCallbacks() {
1399
+ return callbacks.map((c) => c.callback);
1400
+ },
1401
+ reset() {
1402
+ callbacks = [];
1403
+ nextId = 1;
1404
+ currentTime = 0;
1405
+ },
1406
+ enable() {
1407
+ activeMockRAF = this;
1408
+ global.requestAnimationFrame = raf;
1409
+ global.cancelAnimationFrame = cancel;
1410
+ },
1411
+ disable() {
1412
+ if (activeMockRAF === this) {
1413
+ activeMockRAF = void 0;
1414
+ }
1415
+ if (originalRAF) {
1416
+ global.requestAnimationFrame = originalRAF;
1417
+ } else {
1418
+ delete global.requestAnimationFrame;
1419
+ }
1420
+ if (originalCancelRAF) {
1421
+ global.cancelAnimationFrame = originalCancelRAF;
1422
+ } else {
1423
+ delete global.cancelAnimationFrame;
1424
+ }
1425
+ }
1426
+ };
1427
+ return mock;
1428
+ }
1429
+ function createMockPerformance(startTime = 0) {
1430
+ let currentTime = startTime;
1431
+ const mockPerf = {
1432
+ now: () => currentTime,
1433
+ timeOrigin: startTime,
1434
+ timing: {
1435
+ navigationStart: startTime
1436
+ },
1437
+ clearMarks: () => {
1438
+ },
1439
+ clearMeasures: () => {
1440
+ },
1441
+ clearResourceTimings: () => {
1442
+ },
1443
+ getEntries: () => [],
1444
+ getEntriesByName: () => [],
1445
+ getEntriesByType: () => [],
1446
+ mark: () => {
1447
+ },
1448
+ measure: () => {
1449
+ },
1450
+ setResourceTimingBufferSize: () => {
1451
+ },
1452
+ toJSON: () => ({}),
1453
+ addEventListener: () => {
1454
+ },
1455
+ removeEventListener: () => {
1456
+ },
1457
+ dispatchEvent: () => true
1458
+ };
1459
+ mockPerf.advance = (deltaMs) => {
1460
+ currentTime += deltaMs;
1461
+ };
1462
+ mockPerf.setTime = (time) => {
1463
+ currentTime = time;
1464
+ };
1465
+ return mockPerf;
1466
+ }
1467
+ function createControlledTimer() {
1468
+ let currentTime = 0;
1469
+ let timers = [];
1470
+ let nextId = 1;
1471
+ const originalSetTimeout = global.setTimeout;
1472
+ const originalClearTimeout = global.clearTimeout;
1473
+ const originalSetInterval = global.setInterval;
1474
+ const originalClearInterval = global.clearInterval;
1475
+ const mockSetTimeout = (callback, delay = 0, ...args) => {
1476
+ const id = nextId++;
1477
+ timers.push({ id, callback, dueTime: currentTime + delay, args });
1478
+ return id;
1479
+ };
1480
+ const mockClearTimeout = (id) => {
1481
+ timers = timers.filter((t) => t.id !== id);
1482
+ };
1483
+ const mockSetInterval = (callback, delay = 0, ...args) => {
1484
+ const id = nextId++;
1485
+ timers.push({ id, callback, dueTime: currentTime + delay, interval: delay, args });
1486
+ return id;
1487
+ };
1488
+ const mockClearInterval = (id) => {
1489
+ timers = timers.filter((t) => t.id !== id);
1490
+ };
1491
+ global.setTimeout = mockSetTimeout;
1492
+ global.clearTimeout = mockClearTimeout;
1493
+ global.setInterval = mockSetInterval;
1494
+ global.clearInterval = mockClearInterval;
1495
+ return {
1496
+ tick() {
1497
+ this.advanceBy(0);
1498
+ },
1499
+ advanceBy(ms) {
1500
+ const targetTime = currentTime + ms;
1501
+ while (true) {
1502
+ let earliest = null;
1503
+ for (const t of timers) {
1504
+ if (!earliest || t.dueTime < earliest.dueTime) {
1505
+ earliest = t;
1506
+ }
1507
+ }
1508
+ if (!earliest || earliest.dueTime > targetTime) {
1509
+ break;
1510
+ }
1511
+ currentTime = earliest.dueTime;
1512
+ const { callback, args, interval, id } = earliest;
1513
+ if (interval !== void 0) {
1514
+ earliest.dueTime += interval;
1515
+ if (interval === 0) earliest.dueTime += 1;
1516
+ } else {
1517
+ timers = timers.filter((t) => t.id !== id);
1518
+ }
1519
+ callback(...args);
1273
1520
  }
1274
- }),
1275
- setSpawnRegistry: import_vitest2.vi.fn(),
1276
- timeSeconds: 10,
1277
- deltaSeconds: 0.1,
1278
- modelIndex: import_vitest2.vi.fn(() => 0),
1279
- scheduleThink: import_vitest2.vi.fn((entity, time) => {
1280
- entity.nextthink = time;
1281
- }),
1282
- linkentity: import_vitest2.vi.fn(),
1283
- trace: traceFn,
1284
- pointcontents: import_vitest2.vi.fn(() => 0),
1285
- multicast: import_vitest2.vi.fn(),
1286
- unicast: import_vitest2.vi.fn(),
1287
- engine,
1288
- game,
1289
- sound: import_vitest2.vi.fn((ent, chan, sound, vol, attn, timeofs) => {
1290
- engine.sound(ent, chan, sound, vol, attn, timeofs);
1291
- }),
1292
- soundIndex: import_vitest2.vi.fn((sound) => engine.soundIndex(sound)),
1293
- useTargets: import_vitest2.vi.fn((entity, activator) => {
1294
- }),
1295
- findByTargetName: import_vitest2.vi.fn(() => []),
1296
- pickTarget: import_vitest2.vi.fn(() => null),
1297
- killBox: import_vitest2.vi.fn(),
1298
- rng: (0, import_shared2.createRandomGenerator)({ seed }),
1299
- imports: {
1300
- configstring: import_vitest2.vi.fn(),
1301
- trace: traceFn,
1302
- pointcontents: import_vitest2.vi.fn(() => 0)
1521
+ currentTime = targetTime;
1303
1522
  },
1304
- level: {
1305
- intermission_angle: { x: 0, y: 0, z: 0 },
1306
- intermission_origin: { x: 0, y: 0, z: 0 },
1307
- next_auto_save: 0,
1308
- health_bar_entities: null
1523
+ clear() {
1524
+ timers = [];
1309
1525
  },
1310
- targetNameIndex: /* @__PURE__ */ new Map(),
1311
- forEachEntity: import_vitest2.vi.fn((callback) => {
1312
- entityList.forEach(callback);
1313
- }),
1314
- find: import_vitest2.vi.fn((predicate) => {
1315
- return entityList.find(predicate);
1316
- }),
1317
- findByClassname: import_vitest2.vi.fn((classname) => {
1318
- return entityList.find((e) => e.classname === classname);
1319
- }),
1320
- beginFrame: import_vitest2.vi.fn((timeSeconds) => {
1321
- entities.timeSeconds = timeSeconds;
1322
- }),
1323
- targetAwareness: {
1324
- timeSeconds: 10,
1325
- frameNumber: 1,
1326
- sightEntity: null,
1327
- soundEntity: null
1526
+ restore() {
1527
+ global.setTimeout = originalSetTimeout;
1528
+ global.clearTimeout = originalClearTimeout;
1529
+ global.setInterval = originalSetInterval;
1530
+ global.clearInterval = originalClearInterval;
1531
+ }
1532
+ };
1533
+ }
1534
+ function simulateFrames(count, frameTimeMs = 16.6, callback) {
1535
+ if (!activeMockRAF) {
1536
+ throw new Error("simulateFrames requires an active MockRAF. Ensure createMockRAF().enable() is called.");
1537
+ }
1538
+ for (let i = 0; i < count; i++) {
1539
+ if (callback) callback(i);
1540
+ activeMockRAF.advance(frameTimeMs);
1541
+ }
1542
+ }
1543
+ function simulateFramesWithMock(mock, count, frameTimeMs = 16.6, callback) {
1544
+ for (let i = 0; i < count; i++) {
1545
+ if (callback) callback(i);
1546
+ mock.advance(frameTimeMs);
1547
+ }
1548
+ }
1549
+
1550
+ // src/e2e/playwright.ts
1551
+ var import_playwright = require("playwright");
1552
+ var import_http = require("http");
1553
+ var import_serve_handler = __toESM(require("serve-handler"), 1);
1554
+ async function createPlaywrightTestClient(options = {}) {
1555
+ let staticServer;
1556
+ let clientUrl = options.clientUrl;
1557
+ const rootPath = options.rootPath || process.cwd();
1558
+ if (!clientUrl) {
1559
+ staticServer = (0, import_http.createServer)((request, response) => {
1560
+ return (0, import_serve_handler.default)(request, response, {
1561
+ public: rootPath,
1562
+ cleanUrls: false,
1563
+ headers: [
1564
+ {
1565
+ source: "**/*",
1566
+ headers: [
1567
+ { key: "Cache-Control", value: "no-cache" },
1568
+ { key: "Access-Control-Allow-Origin", value: "*" },
1569
+ { key: "Cross-Origin-Opener-Policy", value: "same-origin" },
1570
+ { key: "Cross-Origin-Embedder-Policy", value: "require-corp" }
1571
+ ]
1572
+ }
1573
+ ]
1574
+ });
1575
+ });
1576
+ await new Promise((resolve) => {
1577
+ if (!staticServer) return;
1578
+ staticServer.listen(0, () => {
1579
+ const addr = staticServer?.address();
1580
+ const port = typeof addr === "object" ? addr?.port : 0;
1581
+ clientUrl = `http://localhost:${port}`;
1582
+ console.log(`Test client serving from ${rootPath} at ${clientUrl}`);
1583
+ resolve();
1584
+ });
1585
+ });
1586
+ }
1587
+ const browser = await import_playwright.chromium.launch({
1588
+ headless: options.headless ?? true,
1589
+ args: [
1590
+ "--use-gl=egl",
1591
+ "--ignore-gpu-blocklist",
1592
+ ...options.launchOptions?.args || []
1593
+ ],
1594
+ ...options.launchOptions
1595
+ });
1596
+ const width = options.width || 1280;
1597
+ const height = options.height || 720;
1598
+ const context = await browser.newContext({
1599
+ viewport: { width, height },
1600
+ deviceScaleFactor: 1,
1601
+ ...options.contextOptions
1602
+ });
1603
+ const page = await context.newPage();
1604
+ const close = async () => {
1605
+ await browser.close();
1606
+ if (staticServer) {
1607
+ staticServer.close();
1608
+ }
1609
+ };
1610
+ const navigate = async (url) => {
1611
+ const targetUrl = url || clientUrl;
1612
+ if (!targetUrl) throw new Error("No URL to navigate to");
1613
+ let finalUrl = targetUrl;
1614
+ if (options.serverUrl && !targetUrl.includes("connect=")) {
1615
+ const separator = targetUrl.includes("?") ? "&" : "?";
1616
+ finalUrl = `${targetUrl}${separator}connect=${encodeURIComponent(options.serverUrl)}`;
1617
+ }
1618
+ console.log(`Navigating to: ${finalUrl}`);
1619
+ await page.goto(finalUrl, { waitUntil: "domcontentloaded" });
1620
+ };
1621
+ return {
1622
+ browser,
1623
+ context,
1624
+ page,
1625
+ server: staticServer,
1626
+ close,
1627
+ navigate,
1628
+ waitForGame: async (timeout = 1e4) => {
1629
+ await waitForGameReady(page, timeout);
1328
1630
  },
1329
- // Adding missing properties to satisfy EntitySystem interface partially or fully
1330
- // We cast to unknown first anyway, but filling these in makes it safer for consumers
1331
- skill: 1,
1332
- deathmatch: false,
1333
- coop: false,
1334
- activeCount: entityList.length,
1335
- world: entityList.find((e) => e.classname === "worldspawn") || new import_game.Entity(0)
1336
- // ... other EntitySystem properties would go here
1631
+ injectInput: async (type, data) => {
1632
+ await page.evaluate(({ type: type2, data: data2 }) => {
1633
+ if (window.injectGameInput) window.injectGameInput(type2, data2);
1634
+ }, { type, data });
1635
+ }
1337
1636
  };
1637
+ }
1638
+ async function waitForGameReady(page, timeout = 1e4) {
1639
+ try {
1640
+ await page.waitForFunction(() => {
1641
+ return window.gameInstance && window.gameInstance.isReady;
1642
+ }, null, { timeout });
1643
+ } catch (e) {
1644
+ await page.waitForSelector("canvas", { timeout });
1645
+ }
1646
+ }
1647
+ async function captureGameState(page) {
1648
+ return await page.evaluate(() => {
1649
+ if (window.gameInstance && window.gameInstance.getState) {
1650
+ return window.gameInstance.getState();
1651
+ }
1652
+ return {};
1653
+ });
1654
+ }
1655
+
1656
+ // src/e2e/network.ts
1657
+ var CONDITIONS = {
1658
+ "good": {
1659
+ offline: false,
1660
+ downloadThroughput: 10 * 1024 * 1024,
1661
+ // 10 Mbps
1662
+ uploadThroughput: 5 * 1024 * 1024,
1663
+ // 5 Mbps
1664
+ latency: 20
1665
+ },
1666
+ "slow": {
1667
+ offline: false,
1668
+ downloadThroughput: 500 * 1024,
1669
+ // 500 Kbps
1670
+ uploadThroughput: 500 * 1024,
1671
+ latency: 400
1672
+ },
1673
+ "unstable": {
1674
+ offline: false,
1675
+ downloadThroughput: 1 * 1024 * 1024,
1676
+ uploadThroughput: 1 * 1024 * 1024,
1677
+ latency: 100
1678
+ },
1679
+ "offline": {
1680
+ offline: true,
1681
+ downloadThroughput: 0,
1682
+ uploadThroughput: 0,
1683
+ latency: 0
1684
+ }
1685
+ };
1686
+ function simulateNetworkCondition(condition) {
1687
+ const config = CONDITIONS[condition];
1688
+ return createCustomNetworkCondition(config.latency, 0, 0, config);
1689
+ }
1690
+ function createCustomNetworkCondition(latency, jitter = 0, packetLoss = 0, baseConfig) {
1338
1691
  return {
1339
- keyValues: {},
1340
- entities,
1341
- game,
1342
- engine,
1343
- health_multiplier: 1,
1344
- warn: import_vitest2.vi.fn(),
1345
- free: import_vitest2.vi.fn(),
1346
- // Mock precache functions if they are part of SpawnContext in future or TestContext extensions
1347
- precacheModel: import_vitest2.vi.fn(),
1348
- precacheSound: import_vitest2.vi.fn(),
1349
- precacheImage: import_vitest2.vi.fn()
1692
+ async apply(page) {
1693
+ const client = await page.context().newCDPSession(page);
1694
+ await client.send("Network.enable");
1695
+ await client.send("Network.emulateNetworkConditions", {
1696
+ offline: baseConfig?.offline || false,
1697
+ latency: latency + Math.random() * jitter,
1698
+ downloadThroughput: baseConfig?.downloadThroughput || -1,
1699
+ uploadThroughput: baseConfig?.uploadThroughput || -1
1700
+ });
1701
+ },
1702
+ async clear(page) {
1703
+ const client = await page.context().newCDPSession(page);
1704
+ await client.send("Network.emulateNetworkConditions", {
1705
+ offline: false,
1706
+ latency: 0,
1707
+ downloadThroughput: -1,
1708
+ uploadThroughput: -1
1709
+ });
1710
+ }
1350
1711
  };
1351
1712
  }
1352
- function createSpawnContext() {
1353
- return createTestContext();
1713
+ async function throttleBandwidth(page, bytesPerSecond) {
1714
+ const simulator = createCustomNetworkCondition(0, 0, 0, {
1715
+ offline: false,
1716
+ latency: 0,
1717
+ downloadThroughput: bytesPerSecond,
1718
+ uploadThroughput: bytesPerSecond
1719
+ });
1720
+ await simulator.apply(page);
1354
1721
  }
1355
- function createEntity() {
1356
- return new import_game.Entity(1);
1722
+
1723
+ // src/e2e/visual.ts
1724
+ var import_path = __toESM(require("path"), 1);
1725
+ var import_promises = __toESM(require("fs/promises"), 1);
1726
+ async function captureGameScreenshot(page, name, options = {}) {
1727
+ const dir = options.dir || "__screenshots__";
1728
+ const screenshotPath = import_path.default.join(dir, `${name}.png`);
1729
+ await import_promises.default.mkdir(dir, { recursive: true });
1730
+ return await page.screenshot({
1731
+ path: screenshotPath,
1732
+ fullPage: options.fullPage ?? false,
1733
+ animations: "disabled",
1734
+ caret: "hide"
1735
+ });
1736
+ }
1737
+ async function compareScreenshots(baseline, current, threshold = 0.1) {
1738
+ if (baseline.equals(current)) {
1739
+ return { pixelDiff: 0, matched: true };
1740
+ }
1741
+ return {
1742
+ pixelDiff: -1,
1743
+ // Unknown magnitude
1744
+ matched: false
1745
+ };
1746
+ }
1747
+ function createVisualTestScenario(page, sceneName) {
1748
+ return {
1749
+ async capture(snapshotName) {
1750
+ return await captureGameScreenshot(page, `${sceneName}-${snapshotName}`);
1751
+ },
1752
+ async compare(snapshotName, baselineDir) {
1753
+ const name = `${sceneName}-${snapshotName}`;
1754
+ const current = await captureGameScreenshot(page, name, { dir: "__screenshots__/current" });
1755
+ try {
1756
+ const baselinePath = import_path.default.join(baselineDir, `${name}.png`);
1757
+ const baseline = await import_promises.default.readFile(baselinePath);
1758
+ return await compareScreenshots(baseline, current);
1759
+ } catch (e) {
1760
+ return { pixelDiff: -1, matched: false };
1761
+ }
1762
+ }
1763
+ };
1357
1764
  }
1358
1765
  // Annotate the CommonJS export names for ESM import in node:
1359
1766
  0 && (module.exports = {
1360
1767
  InputInjector,
1361
1768
  MockPointerLock,
1769
+ MockTransport,
1362
1770
  captureAudioEvents,
1363
1771
  captureCanvasDrawCalls,
1772
+ captureGameScreenshot,
1364
1773
  captureGameState,
1774
+ compareScreenshots,
1365
1775
  createBinaryStreamMock,
1366
1776
  createBinaryWriterMock,
1367
1777
  createControlledTimer,
1778
+ createCustomNetworkCondition,
1368
1779
  createEntity,
1369
1780
  createEntityStateFactory,
1370
1781
  createGameStateSnapshotFactory,
@@ -1373,13 +1784,21 @@ function createEntity() {
1373
1784
  createMockCanvasContext2D,
1374
1785
  createMockEngine,
1375
1786
  createMockGame,
1787
+ createMockGameState,
1376
1788
  createMockImage,
1377
1789
  createMockImageData,
1378
1790
  createMockIndexedDB,
1379
1791
  createMockLocalStorage,
1792
+ createMockNetworkAddress,
1380
1793
  createMockPerformance,
1381
1794
  createMockRAF,
1795
+ createMockServer,
1796
+ createMockServerClient,
1797
+ createMockServerState,
1798
+ createMockServerStatic,
1382
1799
  createMockSessionStorage,
1800
+ createMockTransport,
1801
+ createMockUDPSocket,
1383
1802
  createMockWebGL2Context,
1384
1803
  createNetChanMock,
1385
1804
  createPlayerStateFactory,
@@ -1387,6 +1806,7 @@ function createEntity() {
1387
1806
  createSpawnContext,
1388
1807
  createStorageTestScenario,
1389
1808
  createTestContext,
1809
+ createVisualTestScenario,
1390
1810
  intersects,
1391
1811
  ladderTrace,
1392
1812
  makeAxisBrush,
@@ -1401,9 +1821,11 @@ function createEntity() {
1401
1821
  setupNodeEnvironment,
1402
1822
  simulateFrames,
1403
1823
  simulateFramesWithMock,
1824
+ simulateNetworkCondition,
1404
1825
  stairTrace,
1405
1826
  teardownBrowserEnvironment,
1406
1827
  teardownMockAudioContext,
1828
+ throttleBandwidth,
1407
1829
  waitForGameReady
1408
1830
  });
1409
1831
  //# sourceMappingURL=index.cjs.map