quake2ts 0.0.561 → 0.0.562

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 +1422 -1192
  35. package/packages/test-utils/dist/index.cjs.map +1 -1
  36. package/packages/test-utils/dist/index.d.cts +268 -131
  37. package/packages/test-utils/dist/index.d.ts +268 -131
  38. package/packages/test-utils/dist/index.js +1415 -1194
  39. package/packages/test-utils/dist/index.js.map +1 -1
  40. package/packages/server/dist/index.d.cts +0 -161
@@ -1,1283 +1,1496 @@
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
- };
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
+ });
98
81
 
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
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)
93
+ };
94
+ }
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 }))
108
+ };
109
+ }
110
+ function makeNode(plane, children) {
111
+ return { plane, children };
112
+ }
113
+ function makeBspModel(planes, nodes, leaves, brushes, leafBrushes) {
114
+ return {
115
+ planes,
116
+ nodes,
117
+ leaves,
118
+ brushes,
119
+ leafBrushes,
120
+ bmodels: []
121
+ };
122
+ }
123
+ function makeLeaf(contents, firstLeafBrush, numLeafBrushes) {
124
+ return { contents, cluster: 0, area: 0, firstLeafBrush, numLeafBrushes };
125
+ }
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: []
219
135
  };
220
- return gl;
221
136
  }
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
+ };
150
+ }
151
+
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
+ });
222
235
 
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);
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
+ })
319
270
  };
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);
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;
295
+ }),
296
+ free: vi2.fn((ent) => {
297
+ const idx = entityList.indexOf(ent);
298
+ if (idx !== -1) {
299
+ entityList.splice(idx, 1);
325
300
  }
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;
301
+ hooks.onEntityRemove(ent);
302
+ }),
303
+ finalizeSpawn: vi2.fn(),
304
+ freeImmediate: vi2.fn((ent) => {
305
+ const idx = entityList.indexOf(ent);
306
+ if (idx !== -1) {
307
+ entityList.splice(idx, 1);
340
308
  }
341
- const canvas = new Canvas(100, 100);
342
- return canvas;
343
- };
309
+ }),
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)
338
+ },
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
344
+ },
345
+ targetNameIndex: /* @__PURE__ */ new Map(),
346
+ forEachEntity: vi2.fn((callback) => {
347
+ entityList.forEach(callback);
348
+ }),
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()
385
+ };
386
+ }
387
+ function createSpawnContext() {
388
+ return createTestContext();
389
+ }
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
+ });
344
421
  }
345
- if (typeof global.btoa === "undefined") {
346
- global.btoa = function(str) {
347
- return Buffer.from(str, "binary").toString("base64");
348
- };
422
+ /**
423
+ * Start listening on the specified port.
424
+ */
425
+ async listen(port) {
426
+ return this.listenSpy(port);
349
427
  }
350
- if (typeof global.atob === "undefined") {
351
- global.atob = function(str) {
352
- return Buffer.from(str, "base64").toString("binary");
353
- };
428
+ /**
429
+ * Close the transport.
430
+ */
431
+ close() {
432
+ this.closeSpy();
354
433
  }
355
- if (enablePointerLock) {
356
- MockPointerLock.setup(global.document);
434
+ /**
435
+ * Register a callback for new connections.
436
+ */
437
+ onConnection(callback) {
438
+ this.onConnectionCallback = callback;
357
439
  }
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
- };
440
+ /**
441
+ * Register a callback for errors.
442
+ */
443
+ onError(callback) {
444
+ this.onErrorCallback = callback;
372
445
  }
373
- }
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;
446
+ /**
447
+ * Check if the transport is currently listening.
448
+ */
449
+ isListening() {
450
+ return this.listening;
404
451
  }
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);
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);
413
460
  }
414
- return originalGetContext(contextId, options);
415
- };
416
- return canvas;
417
- }
418
- function createMockCanvasContext2D(canvas) {
419
- if (!canvas) {
420
- canvas = createMockCanvas();
421
461
  }
422
- return canvas.getContext("2d");
423
- }
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;
455
- }
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;
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);
465
469
  }
466
470
  }
467
- return imageData;
468
- }
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;
475
- }
476
-
477
- // src/setup/node.ts
478
- function setupNodeEnvironment() {
479
- if (typeof global.fetch === "undefined") {
480
- }
481
- }
482
-
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
- }
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
496
480
  };
481
+ return socket;
497
482
  }
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;
483
+ function createMockNetworkAddress(ip = "127.0.0.1", port = 27910) {
484
+ return { ip, port };
506
485
  }
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
- }
517
- };
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;
518
492
  }
519
493
 
520
- // src/setup/audio.ts
521
- function createMockAudioContext() {
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) {
522
499
  return {
523
- createGain: () => ({
524
- connect: () => {
525
- },
526
- gain: { value: 1, setValueAtTime: () => {
527
- } }
528
- }),
529
- createOscillator: () => ({
530
- connect: () => {
531
- },
532
- start: () => {
533
- },
534
- stop: () => {
535
- },
536
- frequency: { value: 440 }
537
- }),
538
- createBufferSource: () => ({
539
- connect: () => {
540
- },
541
- start: () => {
542
- },
543
- stop: () => {
544
- },
545
- buffer: null,
546
- playbackRate: { value: 1 },
547
- loop: false
548
- }),
549
- destination: {},
550
- currentTime: 0,
551
- state: "running",
552
- resume: async () => {
553
- },
554
- suspend: async () => {
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
512
+ };
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
524
+ };
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()
555
554
  },
556
- close: async () => {
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
557
589
  },
558
- decodeAudioData: async (buffer) => ({
559
- duration: 1,
560
- length: 44100,
561
- sampleRate: 44100,
562
- numberOfChannels: 2,
563
- getChannelData: () => new Float32Array(44100)
564
- }),
565
- createBuffer: (channels, length, sampleRate) => ({
566
- duration: length / sampleRate,
567
- length,
568
- sampleRate,
569
- numberOfChannels: channels,
570
- getChannelData: () => new Float32Array(length)
571
- })
590
+ ...overrides
572
591
  };
573
592
  }
574
- function setupMockAudioContext() {
575
- if (typeof global.AudioContext === "undefined" && typeof global.window !== "undefined") {
576
- global.AudioContext = class {
577
- constructor() {
578
- return createMockAudioContext();
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"));
579
623
  }
580
624
  };
581
- global.window.AudioContext = global.AudioContext;
582
- global.window.webkitAudioContext = global.AudioContext;
625
+ global.HTMLElement.prototype.requestPointerLock = function() {
626
+ _pointerLockElement = this;
627
+ doc.dispatchEvent(new Event("pointerlockchange"));
628
+ };
583
629
  }
584
- }
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;
630
+ };
631
+ var InputInjector = class {
632
+ constructor(doc, win) {
633
+ this.doc = doc;
634
+ this.win = win;
590
635
  }
591
- }
592
- function captureAudioEvents(context) {
593
- return [];
594
- }
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
+ };
595
703
 
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;
608
- };
609
- const cancel = (id) => {
610
- callbacks = callbacks.filter((cb) => cb.id !== id);
611
- };
612
- const mock = {
613
- tick(timestamp) {
614
- if (typeof timestamp !== "number") {
615
- currentTime += 16.6;
616
- } else {
617
- currentTime = timestamp;
618
- }
619
- const currentCallbacks = [...callbacks];
620
- callbacks = [];
621
- currentCallbacks.forEach(({ callback }) => {
622
- callback(currentTime);
623
- });
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: () => {
624
763
  },
625
- advance(deltaMs = 16.6) {
626
- this.tick(currentTime + deltaMs);
764
+ enable: () => {
627
765
  },
628
- getCallbacks() {
629
- return callbacks.map((c) => c.callback);
766
+ disable: () => {
630
767
  },
631
- reset() {
632
- callbacks = [];
633
- nextId = 1;
634
- currentTime = 0;
768
+ depthMask: () => {
635
769
  },
636
- enable() {
637
- activeMockRAF = this;
638
- global.requestAnimationFrame = raf;
639
- global.cancelAnimationFrame = cancel;
770
+ blendFunc: () => {
640
771
  },
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
772
+ viewport: () => {
666
773
  },
667
- clearMarks: () => {
774
+ clearColor: () => {
668
775
  },
669
- clearMeasures: () => {
776
+ clear: () => {
670
777
  },
671
- clearResourceTimings: () => {
778
+ createTexture: () => ({}),
779
+ bindTexture: () => {
672
780
  },
673
- getEntries: () => [],
674
- getEntriesByName: () => [],
675
- getEntriesByType: () => [],
676
- mark: () => {
781
+ texImage2D: () => {
677
782
  },
678
- measure: () => {
783
+ texParameteri: () => {
679
784
  },
680
- setResourceTimingBufferSize: () => {
785
+ activeTexture: () => {
681
786
  },
682
- toJSON: () => ({}),
683
- addEventListener: () => {
787
+ uniform1i: () => {
684
788
  },
685
- removeEventListener: () => {
789
+ uniform1f: () => {
686
790
  },
687
- dispatchEvent: () => true
688
- };
689
- mockPerf.advance = (deltaMs) => {
690
- currentTime += deltaMs;
691
- };
692
- mockPerf.setTime = (time) => {
693
- currentTime = time;
694
- };
695
- return mockPerf;
696
- }
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);
791
+ uniform2f: () => {
728
792
  },
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
- }
737
- }
738
- if (!earliest || earliest.dueTime > targetTime) {
739
- break;
740
- }
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);
748
- }
749
- callback(...args);
750
- }
751
- currentTime = targetTime;
793
+ uniform3f: () => {
752
794
  },
753
- clear() {
754
- timers = [];
795
+ uniform4f: () => {
755
796
  },
756
- restore() {
757
- global.setTimeout = originalSetTimeout;
758
- global.clearTimeout = originalClearTimeout;
759
- global.setInterval = originalSetInterval;
760
- global.clearInterval = originalClearInterval;
761
- }
762
- };
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.");
767
- }
768
- for (let i = 0; i < count; i++) {
769
- if (callback) callback(i);
770
- activeMockRAF.advance(frameTimeMs);
771
- }
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);
777
- }
778
- }
779
-
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
- });
816
- }
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)}`;
847
- }
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);
797
+ uniformMatrix4fv: () => {
798
+ },
799
+ getUniformLocation: () => ({}),
800
+ getAttribLocation: () => 0,
801
+ drawArrays: () => {
802
+ },
803
+ drawElements: () => {
804
+ },
805
+ createVertexArray: () => ({}),
806
+ bindVertexArray: () => {
807
+ },
808
+ deleteShader: () => {
809
+ },
810
+ deleteProgram: () => {
811
+ },
812
+ deleteBuffer: () => {
813
+ },
814
+ deleteTexture: () => {
815
+ },
816
+ deleteVertexArray: () => {
860
817
  },
861
- injectInput: async (type, data) => {
862
- await page.evaluate(({ type: type2, data: data2 }) => {
863
- if (window.injectGameInput) window.injectGameInput(type2, data2);
864
- }, { type, data });
865
- }
818
+ // WebGL2 specific
819
+ texImage3D: () => {
820
+ },
821
+ uniformBlockBinding: () => {
822
+ },
823
+ getExtension: () => null
866
824
  };
825
+ return gl;
867
826
  }
868
- async function waitForGameReady(page, timeout = 1e4) {
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;
869
844
  try {
870
- await page.waitForFunction(() => {
871
- return window.gameInstance && window.gameInstance.isReady;
872
- }, null, { timeout });
845
+ global.navigator = dom.window.navigator;
873
846
  } catch (e) {
874
- await page.waitForSelector("canvas", { timeout });
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
+ }
875
856
  }
876
- }
877
- async function captureGameState(page) {
878
- return await page.evaluate(() => {
879
- if (window.gameInstance && window.gameInstance.getState) {
880
- return window.gameInstance.getState();
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);
911
+ }
912
+ if (enableWebGL2 && contextId === "webgl2") {
913
+ return createMockWebGL2Context(domCanvas);
914
+ }
915
+ if (contextId === "webgl" || contextId === "webgl2") {
916
+ return originalGetContext(contextId, options3);
917
+ }
918
+ return napiCanvas.getContext(contextId, options3);
919
+ };
920
+ domCanvas.__napiCanvas = napiCanvas;
921
+ return domCanvas;
881
922
  }
882
- return {};
883
- });
884
- }
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
- };
913
- }
914
- function makeNode(plane, children) {
915
- return { plane, children };
916
- }
917
- function makeBspModel(planes, nodes, leaves, brushes, leafBrushes) {
918
- return {
919
- planes,
920
- nodes,
921
- leaves,
922
- brushes,
923
- leafBrushes,
924
- bmodels: []
923
+ return originalCreateElement(tagName, options2);
925
924
  };
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
+ };
933
+ }
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
+ };
949
+ }
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
+ };
977
+ }
926
978
  }
927
- function makeLeaf(contents, firstLeafBrush, numLeafBrushes) {
928
- return { contents, cluster: 0, area: 0, firstLeafBrush, numLeafBrushes };
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;
929
999
  }
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: []
1000
+
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;
1009
+ }
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);
1015
+ }
1016
+ if (contextId === "2d") {
1017
+ return originalGetContext("2d", options);
1018
+ }
1019
+ return originalGetContext(contextId, options);
939
1020
  };
1021
+ return canvas;
940
1022
  }
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)
1023
+ function createMockCanvasContext2D(canvas) {
1024
+ if (!canvas) {
1025
+ canvas = createMockCanvas();
1026
+ }
1027
+ return canvas.getContext("2d");
1028
+ }
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"
949
1049
  ];
950
- return {
951
- contents,
952
- sides: planes.map((plane) => ({ plane, surfaceFlags: 0 }))
953
- };
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
+ };
1057
+ }
1058
+ });
1059
+ return drawCalls;
1060
+ }
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;
1073
+ }
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;
954
1080
  }
955
1081
 
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
- });
1082
+ // src/setup/node.ts
1083
+ function setupNodeEnvironment() {
1084
+ if (typeof global.fetch === "undefined") {
1085
+ }
1086
+ }
1036
1087
 
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
- });
1088
+ // src/setup/storage.ts
1089
+ import "fake-indexeddb/auto";
1090
+ function createMockLocalStorage(initialData = {}) {
1091
+ const storage = new Map(Object.entries(initialData));
1092
+ return {
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
+ }
1101
+ };
1102
+ }
1103
+ function createMockSessionStorage(initialData = {}) {
1104
+ return createMockLocalStorage(initialData);
1105
+ }
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;
1111
+ }
1112
+ function createStorageTestScenario(storageType = "local") {
1113
+ const storage = storageType === "local" ? createMockLocalStorage() : createMockSessionStorage();
1114
+ return {
1115
+ storage,
1116
+ populate(data) {
1117
+ Object.entries(data).forEach(([k, v]) => storage.setItem(k, v));
1118
+ },
1119
+ verify(key, value) {
1120
+ return storage.getItem(key) === value;
1121
+ }
1122
+ };
1123
+ }
1120
1124
 
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));
1125
+ // src/setup/audio.ts
1126
+ function createMockAudioContext() {
1127
+ return {
1128
+ createGain: () => ({
1129
+ connect: () => {
1130
+ },
1131
+ gain: { value: 1, setValueAtTime: () => {
1132
+ } }
1139
1133
  }),
1140
- unregisterEntitySpawn: vi2.fn((classname) => {
1141
- spawnRegistry.unregister(classname);
1134
+ createOscillator: () => ({
1135
+ connect: () => {
1136
+ },
1137
+ start: () => {
1138
+ },
1139
+ stop: () => {
1140
+ },
1141
+ frequency: { value: 440 }
1142
1142
  }),
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");
1143
+ createBufferSource: () => ({
1144
+ connect: () => {
1145
+ },
1146
+ start: () => {
1147
+ },
1148
+ stop: () => {
1149
+ },
1150
+ buffer: null,
1151
+ playbackRate: { value: 1 },
1152
+ loop: false
1148
1153
  }),
1149
- clientBegin: vi2.fn((client) => {
1150
- hooks.onPlayerSpawn({});
1154
+ destination: {},
1155
+ currentTime: 0,
1156
+ state: "running",
1157
+ resume: async () => {
1158
+ },
1159
+ suspend: async () => {
1160
+ },
1161
+ close: async () => {
1162
+ },
1163
+ decodeAudioData: async (buffer) => ({
1164
+ duration: 1,
1165
+ length: 44100,
1166
+ sampleRate: 44100,
1167
+ numberOfChannels: 2,
1168
+ getChannelData: () => new Float32Array(44100)
1151
1169
  }),
1152
- damage: vi2.fn((amount) => {
1153
- hooks.onDamage({}, null, null, amount, 0, 0);
1170
+ createBuffer: (channels, length, sampleRate) => ({
1171
+ duration: length / sampleRate,
1172
+ length,
1173
+ sampleRate,
1174
+ numberOfChannels: channels,
1175
+ getChannelData: () => new Float32Array(length)
1154
1176
  })
1155
1177
  };
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;
1180
- }),
1181
- free: vi2.fn((ent) => {
1182
- const idx = entityList.indexOf(ent);
1183
- if (idx !== -1) {
1184
- entityList.splice(idx, 1);
1178
+ }
1179
+ function setupMockAudioContext() {
1180
+ if (typeof global.AudioContext === "undefined" && typeof global.window !== "undefined") {
1181
+ global.AudioContext = class {
1182
+ constructor() {
1183
+ return createMockAudioContext();
1185
1184
  }
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);
1185
+ };
1186
+ global.window.AudioContext = global.AudioContext;
1187
+ global.window.webkitAudioContext = global.AudioContext;
1188
+ }
1189
+ }
1190
+ function teardownMockAudioContext() {
1191
+ if (global.AudioContext && global.AudioContext.toString().includes("class")) {
1192
+ delete global.AudioContext;
1193
+ delete global.window.AudioContext;
1194
+ delete global.window.webkitAudioContext;
1195
+ }
1196
+ }
1197
+ function captureAudioEvents(context) {
1198
+ return [];
1199
+ }
1200
+
1201
+ // src/setup/timing.ts
1202
+ var activeMockRAF;
1203
+ function createMockRAF() {
1204
+ let callbacks = [];
1205
+ let nextId = 1;
1206
+ let currentTime = 0;
1207
+ const originalRAF = global.requestAnimationFrame;
1208
+ const originalCancelRAF = global.cancelAnimationFrame;
1209
+ const raf = (callback) => {
1210
+ const id = nextId++;
1211
+ callbacks.push({ id, callback });
1212
+ return id;
1213
+ };
1214
+ const cancel = (id) => {
1215
+ callbacks = callbacks.filter((cb) => cb.id !== id);
1216
+ };
1217
+ const mock = {
1218
+ tick(timestamp) {
1219
+ if (typeof timestamp !== "number") {
1220
+ currentTime += 16.6;
1221
+ } else {
1222
+ currentTime = timestamp;
1193
1223
  }
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)
1224
+ const currentCallbacks = [...callbacks];
1225
+ callbacks = [];
1226
+ currentCallbacks.forEach(({ callback }) => {
1227
+ callback(currentTime);
1228
+ });
1229
+ },
1230
+ advance(deltaMs = 16.6) {
1231
+ this.tick(currentTime + deltaMs);
1232
+ },
1233
+ getCallbacks() {
1234
+ return callbacks.map((c) => c.callback);
1235
+ },
1236
+ reset() {
1237
+ callbacks = [];
1238
+ nextId = 1;
1239
+ currentTime = 0;
1240
+ },
1241
+ enable() {
1242
+ activeMockRAF = this;
1243
+ global.requestAnimationFrame = raf;
1244
+ global.cancelAnimationFrame = cancel;
1245
+ },
1246
+ disable() {
1247
+ if (activeMockRAF === this) {
1248
+ activeMockRAF = void 0;
1249
+ }
1250
+ if (originalRAF) {
1251
+ global.requestAnimationFrame = originalRAF;
1252
+ } else {
1253
+ delete global.requestAnimationFrame;
1254
+ }
1255
+ if (originalCancelRAF) {
1256
+ global.cancelAnimationFrame = originalCancelRAF;
1257
+ } else {
1258
+ delete global.cancelAnimationFrame;
1259
+ }
1260
+ }
1261
+ };
1262
+ return mock;
1263
+ }
1264
+ function createMockPerformance(startTime = 0) {
1265
+ let currentTime = startTime;
1266
+ const mockPerf = {
1267
+ now: () => currentTime,
1268
+ timeOrigin: startTime,
1269
+ timing: {
1270
+ navigationStart: startTime
1223
1271
  },
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
1272
+ clearMarks: () => {
1229
1273
  },
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
1274
+ clearMeasures: () => {
1248
1275
  },
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
1276
+ clearResourceTimings: () => {
1277
+ },
1278
+ getEntries: () => [],
1279
+ getEntriesByName: () => [],
1280
+ getEntriesByType: () => [],
1281
+ mark: () => {
1282
+ },
1283
+ measure: () => {
1284
+ },
1285
+ setResourceTimingBufferSize: () => {
1286
+ },
1287
+ toJSON: () => ({}),
1288
+ addEventListener: () => {
1289
+ },
1290
+ removeEventListener: () => {
1291
+ },
1292
+ dispatchEvent: () => true
1293
+ };
1294
+ mockPerf.advance = (deltaMs) => {
1295
+ currentTime += deltaMs;
1296
+ };
1297
+ mockPerf.setTime = (time) => {
1298
+ currentTime = time;
1299
+ };
1300
+ return mockPerf;
1301
+ }
1302
+ function createControlledTimer() {
1303
+ let currentTime = 0;
1304
+ let timers = [];
1305
+ let nextId = 1;
1306
+ const originalSetTimeout = global.setTimeout;
1307
+ const originalClearTimeout = global.clearTimeout;
1308
+ const originalSetInterval = global.setInterval;
1309
+ const originalClearInterval = global.clearInterval;
1310
+ const mockSetTimeout = (callback, delay = 0, ...args) => {
1311
+ const id = nextId++;
1312
+ timers.push({ id, callback, dueTime: currentTime + delay, args });
1313
+ return id;
1314
+ };
1315
+ const mockClearTimeout = (id) => {
1316
+ timers = timers.filter((t) => t.id !== id);
1317
+ };
1318
+ const mockSetInterval = (callback, delay = 0, ...args) => {
1319
+ const id = nextId++;
1320
+ timers.push({ id, callback, dueTime: currentTime + delay, interval: delay, args });
1321
+ return id;
1322
+ };
1323
+ const mockClearInterval = (id) => {
1324
+ timers = timers.filter((t) => t.id !== id);
1257
1325
  };
1326
+ global.setTimeout = mockSetTimeout;
1327
+ global.clearTimeout = mockClearTimeout;
1328
+ global.setInterval = mockSetInterval;
1329
+ global.clearInterval = mockClearInterval;
1258
1330
  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()
1331
+ tick() {
1332
+ this.advanceBy(0);
1333
+ },
1334
+ advanceBy(ms) {
1335
+ const targetTime = currentTime + ms;
1336
+ while (true) {
1337
+ let earliest = null;
1338
+ for (const t of timers) {
1339
+ if (!earliest || t.dueTime < earliest.dueTime) {
1340
+ earliest = t;
1341
+ }
1342
+ }
1343
+ if (!earliest || earliest.dueTime > targetTime) {
1344
+ break;
1345
+ }
1346
+ currentTime = earliest.dueTime;
1347
+ const { callback, args, interval, id } = earliest;
1348
+ if (interval !== void 0) {
1349
+ earliest.dueTime += interval;
1350
+ if (interval === 0) earliest.dueTime += 1;
1351
+ } else {
1352
+ timers = timers.filter((t) => t.id !== id);
1353
+ }
1354
+ callback(...args);
1355
+ }
1356
+ currentTime = targetTime;
1357
+ },
1358
+ clear() {
1359
+ timers = [];
1360
+ },
1361
+ restore() {
1362
+ global.setTimeout = originalSetTimeout;
1363
+ global.clearTimeout = originalClearTimeout;
1364
+ global.setInterval = originalSetInterval;
1365
+ global.clearInterval = originalClearInterval;
1366
+ }
1270
1367
  };
1271
1368
  }
1272
- function createSpawnContext() {
1273
- return createTestContext();
1369
+ function simulateFrames(count, frameTimeMs = 16.6, callback) {
1370
+ if (!activeMockRAF) {
1371
+ throw new Error("simulateFrames requires an active MockRAF. Ensure createMockRAF().enable() is called.");
1372
+ }
1373
+ for (let i = 0; i < count; i++) {
1374
+ if (callback) callback(i);
1375
+ activeMockRAF.advance(frameTimeMs);
1376
+ }
1274
1377
  }
1275
- function createEntity() {
1276
- return new Entity(1);
1378
+ function simulateFramesWithMock(mock, count, frameTimeMs = 16.6, callback) {
1379
+ for (let i = 0; i < count; i++) {
1380
+ if (callback) callback(i);
1381
+ mock.advance(frameTimeMs);
1382
+ }
1383
+ }
1384
+
1385
+ // src/e2e/playwright.ts
1386
+ import { chromium } from "playwright";
1387
+ import { createServer } from "http";
1388
+ import handler from "serve-handler";
1389
+ async function createPlaywrightTestClient(options = {}) {
1390
+ let staticServer;
1391
+ let clientUrl = options.clientUrl;
1392
+ const rootPath = options.rootPath || process.cwd();
1393
+ if (!clientUrl) {
1394
+ staticServer = createServer((request, response) => {
1395
+ return handler(request, response, {
1396
+ public: rootPath,
1397
+ cleanUrls: false,
1398
+ headers: [
1399
+ {
1400
+ source: "**/*",
1401
+ headers: [
1402
+ { key: "Cache-Control", value: "no-cache" },
1403
+ { key: "Access-Control-Allow-Origin", value: "*" },
1404
+ { key: "Cross-Origin-Opener-Policy", value: "same-origin" },
1405
+ { key: "Cross-Origin-Embedder-Policy", value: "require-corp" }
1406
+ ]
1407
+ }
1408
+ ]
1409
+ });
1410
+ });
1411
+ await new Promise((resolve) => {
1412
+ if (!staticServer) return;
1413
+ staticServer.listen(0, () => {
1414
+ const addr = staticServer?.address();
1415
+ const port = typeof addr === "object" ? addr?.port : 0;
1416
+ clientUrl = `http://localhost:${port}`;
1417
+ console.log(`Test client serving from ${rootPath} at ${clientUrl}`);
1418
+ resolve();
1419
+ });
1420
+ });
1421
+ }
1422
+ const browser = await chromium.launch({
1423
+ headless: options.headless ?? true,
1424
+ args: [
1425
+ "--use-gl=egl",
1426
+ "--ignore-gpu-blocklist",
1427
+ ...options.launchOptions?.args || []
1428
+ ],
1429
+ ...options.launchOptions
1430
+ });
1431
+ const width = options.width || 1280;
1432
+ const height = options.height || 720;
1433
+ const context = await browser.newContext({
1434
+ viewport: { width, height },
1435
+ deviceScaleFactor: 1,
1436
+ ...options.contextOptions
1437
+ });
1438
+ const page = await context.newPage();
1439
+ const close = async () => {
1440
+ await browser.close();
1441
+ if (staticServer) {
1442
+ staticServer.close();
1443
+ }
1444
+ };
1445
+ const navigate = async (url) => {
1446
+ const targetUrl = url || clientUrl;
1447
+ if (!targetUrl) throw new Error("No URL to navigate to");
1448
+ let finalUrl = targetUrl;
1449
+ if (options.serverUrl && !targetUrl.includes("connect=")) {
1450
+ const separator = targetUrl.includes("?") ? "&" : "?";
1451
+ finalUrl = `${targetUrl}${separator}connect=${encodeURIComponent(options.serverUrl)}`;
1452
+ }
1453
+ console.log(`Navigating to: ${finalUrl}`);
1454
+ await page.goto(finalUrl, { waitUntil: "domcontentloaded" });
1455
+ };
1456
+ return {
1457
+ browser,
1458
+ context,
1459
+ page,
1460
+ server: staticServer,
1461
+ close,
1462
+ navigate,
1463
+ waitForGame: async (timeout = 1e4) => {
1464
+ await waitForGameReady(page, timeout);
1465
+ },
1466
+ injectInput: async (type, data) => {
1467
+ await page.evaluate(({ type: type2, data: data2 }) => {
1468
+ if (window.injectGameInput) window.injectGameInput(type2, data2);
1469
+ }, { type, data });
1470
+ }
1471
+ };
1472
+ }
1473
+ async function waitForGameReady(page, timeout = 1e4) {
1474
+ try {
1475
+ await page.waitForFunction(() => {
1476
+ return window.gameInstance && window.gameInstance.isReady;
1477
+ }, null, { timeout });
1478
+ } catch (e) {
1479
+ await page.waitForSelector("canvas", { timeout });
1480
+ }
1481
+ }
1482
+ async function captureGameState(page) {
1483
+ return await page.evaluate(() => {
1484
+ if (window.gameInstance && window.gameInstance.getState) {
1485
+ return window.gameInstance.getState();
1486
+ }
1487
+ return {};
1488
+ });
1277
1489
  }
1278
1490
  export {
1279
1491
  InputInjector,
1280
1492
  MockPointerLock,
1493
+ MockTransport,
1281
1494
  captureAudioEvents,
1282
1495
  captureCanvasDrawCalls,
1283
1496
  captureGameState,
@@ -1292,13 +1505,21 @@ export {
1292
1505
  createMockCanvasContext2D,
1293
1506
  createMockEngine,
1294
1507
  createMockGame,
1508
+ createMockGameState,
1295
1509
  createMockImage,
1296
1510
  createMockImageData,
1297
1511
  createMockIndexedDB,
1298
1512
  createMockLocalStorage,
1513
+ createMockNetworkAddress,
1299
1514
  createMockPerformance,
1300
1515
  createMockRAF,
1516
+ createMockServer,
1517
+ createMockServerClient,
1518
+ createMockServerState,
1519
+ createMockServerStatic,
1301
1520
  createMockSessionStorage,
1521
+ createMockTransport,
1522
+ createMockUDPSocket,
1302
1523
  createMockWebGL2Context,
1303
1524
  createNetChanMock,
1304
1525
  createPlayerStateFactory,