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