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