quake2ts 0.0.556 → 0.0.561

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 (31) hide show
  1. package/package.json +3 -1
  2. package/packages/client/dist/browser/index.global.js +17 -17
  3. package/packages/client/dist/browser/index.global.js.map +1 -1
  4. package/packages/client/dist/cjs/index.cjs +5369 -3577
  5. package/packages/client/dist/cjs/index.cjs.map +1 -1
  6. package/packages/client/dist/esm/index.js +5369 -3577
  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/index.d.ts.map +1 -1
  10. package/packages/engine/dist/cjs/index.cjs +2256 -534
  11. package/packages/engine/dist/cjs/index.cjs.map +1 -1
  12. package/packages/engine/dist/esm/index.js +2266 -538
  13. package/packages/engine/dist/esm/index.js.map +1 -1
  14. package/packages/engine/dist/tsconfig.tsbuildinfo +1 -1
  15. package/packages/engine/dist/types/assets/visibilityAnalyzer.d.ts +7 -2
  16. package/packages/engine/dist/types/assets/visibilityAnalyzer.d.ts.map +1 -1
  17. package/packages/engine/dist/types/render/bloom.d.ts +19 -0
  18. package/packages/engine/dist/types/render/bloom.d.ts.map +1 -0
  19. package/packages/engine/dist/types/render/frame.d.ts +2 -0
  20. package/packages/engine/dist/types/render/frame.d.ts.map +1 -1
  21. package/packages/engine/dist/types/render/renderer.d.ts +2 -0
  22. package/packages/engine/dist/types/render/renderer.d.ts.map +1 -1
  23. package/packages/game/dist/tsconfig.tsbuildinfo +1 -1
  24. package/packages/shared/dist/tsconfig.tsbuildinfo +1 -1
  25. package/packages/test-utils/dist/index.cjs +1178 -580
  26. package/packages/test-utils/dist/index.cjs.map +1 -1
  27. package/packages/test-utils/dist/index.d.cts +198 -47
  28. package/packages/test-utils/dist/index.d.ts +198 -47
  29. package/packages/test-utils/dist/index.js +1149 -582
  30. package/packages/test-utils/dist/index.js.map +1 -1
  31. package/packages/tools/dist/tsconfig.tsbuildinfo +1 -1
@@ -1,411 +1,257 @@
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
- });
1
+ // src/setup/browser.ts
2
+ import { JSDOM } from "jsdom";
3
+ import { Canvas, Image, ImageData } from "@napi-rs/canvas";
4
+ import "fake-indexeddb/auto";
81
5
 
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: []
135
- };
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
- }
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
+ };
151
98
 
152
- // src/game/factories.ts
153
- var createPlayerStateFactory = (overrides) => ({
154
- pm_type: 0,
155
- pm_time: 0,
156
- pm_flags: 0,
157
- origin: { x: 0, y: 0, z: 0 },
158
- velocity: { x: 0, y: 0, z: 0 },
159
- viewAngles: { x: 0, y: 0, z: 0 },
160
- onGround: false,
161
- waterLevel: 0,
162
- watertype: 0,
163
- mins: { x: 0, y: 0, z: 0 },
164
- maxs: { x: 0, y: 0, z: 0 },
165
- damageAlpha: 0,
166
- damageIndicators: [],
167
- blend: [0, 0, 0, 0],
168
- stats: [],
169
- kick_angles: { x: 0, y: 0, z: 0 },
170
- kick_origin: { x: 0, y: 0, z: 0 },
171
- gunoffset: { x: 0, y: 0, z: 0 },
172
- gunangles: { x: 0, y: 0, z: 0 },
173
- gunindex: 0,
174
- gun_frame: 0,
175
- rdflags: 0,
176
- fov: 90,
177
- renderfx: 0,
178
- ...overrides
179
- });
180
- var createEntityStateFactory = (overrides) => ({
181
- number: 0,
182
- origin: { x: 0, y: 0, z: 0 },
183
- angles: { x: 0, y: 0, z: 0 },
184
- oldOrigin: { x: 0, y: 0, z: 0 },
185
- modelIndex: 0,
186
- modelIndex2: 0,
187
- modelIndex3: 0,
188
- modelIndex4: 0,
189
- frame: 0,
190
- skinNum: 0,
191
- effects: 0,
192
- renderfx: 0,
193
- solid: 0,
194
- sound: 0,
195
- event: 0,
196
- ...overrides
197
- });
198
- var createGameStateSnapshotFactory = (overrides) => ({
199
- gravity: { x: 0, y: 0, z: -800 },
200
- origin: { x: 0, y: 0, z: 0 },
201
- velocity: { x: 0, y: 0, z: 0 },
202
- viewangles: { x: 0, y: 0, z: 0 },
203
- level: { timeSeconds: 0, frameNumber: 0, previousTimeSeconds: 0, deltaSeconds: 0.1 },
204
- entities: {
205
- activeCount: 0,
206
- worldClassname: "worldspawn"
207
- },
208
- packetEntities: [],
209
- pmFlags: 0,
210
- pmType: 0,
211
- waterlevel: 0,
212
- watertype: 0,
213
- deltaAngles: { x: 0, y: 0, z: 0 },
214
- health: 100,
215
- armor: 0,
216
- ammo: 0,
217
- blend: [0, 0, 0, 0],
218
- damageAlpha: 0,
219
- damageIndicators: [],
220
- stats: [],
221
- kick_angles: { x: 0, y: 0, z: 0 },
222
- kick_origin: { x: 0, y: 0, z: 0 },
223
- gunoffset: { x: 0, y: 0, z: 0 },
224
- gunangles: { x: 0, y: 0, z: 0 },
225
- gunindex: 0,
226
- pm_time: 0,
227
- gun_frame: 0,
228
- rdflags: 0,
229
- fov: 90,
230
- renderfx: 0,
231
- pm_flags: 0,
232
- pm_type: 0,
233
- ...overrides
234
- });
235
-
236
- // src/game/helpers.ts
237
- import { vi as vi2 } from "vitest";
238
- import { Entity, SpawnRegistry, ScriptHookRegistry } from "@quake2ts/game";
239
- import { createRandomGenerator } from "@quake2ts/shared";
240
- import { intersects, stairTrace, ladderTrace } from "@quake2ts/shared";
241
- var createMockEngine = () => ({
242
- sound: vi2.fn(),
243
- soundIndex: vi2.fn((sound) => 0),
244
- modelIndex: vi2.fn((model) => 0),
245
- centerprintf: vi2.fn()
246
- });
247
- var createMockGame = (seed = 12345) => {
248
- const spawnRegistry = new SpawnRegistry();
249
- const hooks = new ScriptHookRegistry();
250
- const game = {
251
- random: createRandomGenerator({ seed }),
252
- registerEntitySpawn: vi2.fn((classname, spawnFunc) => {
253
- spawnRegistry.register(classname, (entity) => spawnFunc(entity));
254
- }),
255
- unregisterEntitySpawn: vi2.fn((classname) => {
256
- spawnRegistry.unregister(classname);
257
- }),
258
- getCustomEntities: vi2.fn(() => Array.from(spawnRegistry.keys())),
259
- hooks,
260
- registerHooks: vi2.fn((newHooks) => hooks.register(newHooks)),
261
- spawnWorld: vi2.fn(() => {
262
- hooks.onMapLoad("q2dm1");
263
- }),
264
- clientBegin: vi2.fn((client) => {
265
- hooks.onPlayerSpawn({});
266
- }),
267
- damage: vi2.fn((amount) => {
268
- hooks.onDamage({}, null, null, amount, 0, 0);
269
- })
270
- };
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);
300
- }
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);
308
- }
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)
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: () => {
338
130
  },
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
131
+ compileShader: () => {
344
132
  },
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
133
+ getShaderParameter: (_, param) => {
134
+ if (param === 35713) return true;
135
+ return true;
363
136
  },
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);
137
+ getShaderInfoLog: () => "",
138
+ createProgram: () => ({}),
139
+ attachShader: () => {
140
+ },
141
+ linkProgram: () => {
142
+ },
143
+ getProgramParameter: (_, param) => {
144
+ if (param === 35714) return true;
145
+ return true;
146
+ },
147
+ getProgramInfoLog: () => "",
148
+ useProgram: () => {
149
+ },
150
+ createBuffer: () => ({}),
151
+ bindBuffer: () => {
152
+ },
153
+ bufferData: () => {
154
+ },
155
+ enableVertexAttribArray: () => {
156
+ },
157
+ vertexAttribPointer: () => {
158
+ },
159
+ enable: () => {
160
+ },
161
+ disable: () => {
162
+ },
163
+ depthMask: () => {
164
+ },
165
+ blendFunc: () => {
166
+ },
167
+ viewport: () => {
168
+ },
169
+ clearColor: () => {
170
+ },
171
+ clear: () => {
172
+ },
173
+ createTexture: () => ({}),
174
+ bindTexture: () => {
175
+ },
176
+ texImage2D: () => {
177
+ },
178
+ texParameteri: () => {
179
+ },
180
+ activeTexture: () => {
181
+ },
182
+ uniform1i: () => {
183
+ },
184
+ uniform1f: () => {
185
+ },
186
+ uniform2f: () => {
187
+ },
188
+ uniform3f: () => {
189
+ },
190
+ uniform4f: () => {
191
+ },
192
+ uniformMatrix4fv: () => {
193
+ },
194
+ getUniformLocation: () => ({}),
195
+ getAttribLocation: () => 0,
196
+ drawArrays: () => {
197
+ },
198
+ drawElements: () => {
199
+ },
200
+ createVertexArray: () => ({}),
201
+ bindVertexArray: () => {
202
+ },
203
+ deleteShader: () => {
204
+ },
205
+ deleteProgram: () => {
206
+ },
207
+ deleteBuffer: () => {
208
+ },
209
+ deleteTexture: () => {
210
+ },
211
+ deleteVertexArray: () => {
212
+ },
213
+ // WebGL2 specific
214
+ texImage3D: () => {
215
+ },
216
+ uniformBlockBinding: () => {
217
+ },
218
+ getExtension: () => null
219
+ };
220
+ return gl;
392
221
  }
393
222
 
394
223
  // src/setup/browser.ts
395
- import { JSDOM } from "jsdom";
396
- import { Canvas, Image, ImageData } from "@napi-rs/canvas";
397
- import "fake-indexeddb/auto";
398
224
  function setupBrowserEnvironment(options = {}) {
399
- const { url = "http://localhost", pretendToBeVisual = true } = options;
225
+ const {
226
+ url = "http://localhost",
227
+ pretendToBeVisual = true,
228
+ resources = void 0,
229
+ enableWebGL2 = false,
230
+ enablePointerLock = false
231
+ } = options;
400
232
  const dom = new JSDOM("<!DOCTYPE html><html><head></head><body></body></html>", {
401
233
  url,
402
- pretendToBeVisual
234
+ pretendToBeVisual,
235
+ resources
403
236
  });
404
237
  global.window = dom.window;
405
238
  global.document = dom.window.document;
406
- global.navigator = dom.window.navigator;
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
+ }
407
252
  global.location = dom.window.location;
408
253
  global.HTMLElement = dom.window.HTMLElement;
254
+ global.HTMLCanvasElement = dom.window.HTMLCanvasElement;
409
255
  global.Event = dom.window.Event;
410
256
  global.CustomEvent = dom.window.CustomEvent;
411
257
  global.DragEvent = dom.window.DragEvent;
@@ -458,6 +304,9 @@ function setupBrowserEnvironment(options = {}) {
458
304
  if (contextId === "2d") {
459
305
  return napiCanvas.getContext("2d", options3);
460
306
  }
307
+ if (enableWebGL2 && contextId === "webgl2") {
308
+ return createMockWebGL2Context(domCanvas);
309
+ }
461
310
  if (contextId === "webgl" || contextId === "webgl2") {
462
311
  return originalGetContext(contextId, options3);
463
312
  }
@@ -468,6 +317,15 @@ function setupBrowserEnvironment(options = {}) {
468
317
  }
469
318
  return originalCreateElement(tagName, options2);
470
319
  };
320
+ if (enableWebGL2) {
321
+ const originalProtoGetContext = global.HTMLCanvasElement.prototype.getContext;
322
+ global.HTMLCanvasElement.prototype.getContext = function(contextId, options2) {
323
+ if (contextId === "webgl2") {
324
+ return createMockWebGL2Context(this);
325
+ }
326
+ return originalProtoGetContext.call(this, contextId, options2);
327
+ };
328
+ }
471
329
  global.Image = Image;
472
330
  global.ImageData = ImageData;
473
331
  if (typeof global.createImageBitmap === "undefined") {
@@ -494,6 +352,24 @@ function setupBrowserEnvironment(options = {}) {
494
352
  return Buffer.from(str, "base64").toString("binary");
495
353
  };
496
354
  }
355
+ if (enablePointerLock) {
356
+ MockPointerLock.setup(global.document);
357
+ }
358
+ if (typeof global.requestAnimationFrame === "undefined") {
359
+ let lastTime = 0;
360
+ global.requestAnimationFrame = (callback) => {
361
+ const currTime = Date.now();
362
+ const timeToCall = Math.max(0, 16 - (currTime - lastTime));
363
+ const id = setTimeout(() => {
364
+ callback(currTime + timeToCall);
365
+ }, timeToCall);
366
+ lastTime = currTime + timeToCall;
367
+ return id;
368
+ };
369
+ global.cancelAnimationFrame = (id) => {
370
+ clearTimeout(id);
371
+ };
372
+ }
497
373
  }
498
374
  function teardownBrowserEnvironment() {
499
375
  delete global.window;
@@ -502,9 +378,100 @@ function teardownBrowserEnvironment() {
502
378
  delete global.localStorage;
503
379
  delete global.location;
504
380
  delete global.HTMLElement;
381
+ delete global.HTMLCanvasElement;
505
382
  delete global.Image;
506
383
  delete global.ImageData;
507
384
  delete global.createImageBitmap;
385
+ delete global.Event;
386
+ delete global.CustomEvent;
387
+ delete global.DragEvent;
388
+ delete global.MouseEvent;
389
+ delete global.KeyboardEvent;
390
+ delete global.FocusEvent;
391
+ delete global.WheelEvent;
392
+ delete global.InputEvent;
393
+ delete global.UIEvent;
394
+ }
395
+
396
+ // src/setup/canvas.ts
397
+ import { Canvas as Canvas2, Image as Image2, ImageData as ImageData2 } from "@napi-rs/canvas";
398
+ function createMockCanvas(width = 300, height = 150) {
399
+ if (typeof document !== "undefined" && document.createElement) {
400
+ const canvas2 = document.createElement("canvas");
401
+ canvas2.width = width;
402
+ canvas2.height = height;
403
+ return canvas2;
404
+ }
405
+ const canvas = new Canvas2(width, height);
406
+ const originalGetContext = canvas.getContext.bind(canvas);
407
+ canvas.getContext = function(contextId, options) {
408
+ if (contextId === "webgl2") {
409
+ return createMockWebGL2Context(canvas);
410
+ }
411
+ if (contextId === "2d") {
412
+ return originalGetContext("2d", options);
413
+ }
414
+ return originalGetContext(contextId, options);
415
+ };
416
+ return canvas;
417
+ }
418
+ function createMockCanvasContext2D(canvas) {
419
+ if (!canvas) {
420
+ canvas = createMockCanvas();
421
+ }
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;
465
+ }
466
+ }
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;
508
475
  }
509
476
 
510
477
  // src/setup/node.ts
@@ -513,236 +480,831 @@ function setupNodeEnvironment() {
513
480
  }
514
481
  }
515
482
 
516
- // src/setup/webgl.ts
517
- function createMockWebGL2Context(canvas) {
518
- const gl = {
519
- canvas,
520
- drawingBufferWidth: canvas.width,
521
- drawingBufferHeight: canvas.height,
522
- // Constants
523
- VERTEX_SHADER: 35633,
524
- FRAGMENT_SHADER: 35632,
525
- COMPILE_STATUS: 35713,
526
- LINK_STATUS: 35714,
527
- ARRAY_BUFFER: 34962,
528
- ELEMENT_ARRAY_BUFFER: 34963,
529
- STATIC_DRAW: 35044,
530
- DYNAMIC_DRAW: 35048,
531
- FLOAT: 5126,
532
- DEPTH_TEST: 2929,
533
- BLEND: 3042,
534
- SRC_ALPHA: 770,
535
- ONE_MINUS_SRC_ALPHA: 771,
536
- TEXTURE_2D: 3553,
537
- RGBA: 6408,
538
- UNSIGNED_BYTE: 5121,
539
- COLOR_BUFFER_BIT: 16384,
540
- DEPTH_BUFFER_BIT: 256,
541
- TRIANGLES: 4,
542
- TRIANGLE_STRIP: 5,
543
- TRIANGLE_FAN: 6,
544
- // Methods
545
- createShader: () => ({}),
546
- shaderSource: () => {
547
- },
548
- compileShader: () => {
549
- },
550
- getShaderParameter: (_, param) => {
551
- if (param === 35713) return true;
552
- return true;
553
- },
554
- getShaderInfoLog: () => "",
555
- createProgram: () => ({}),
556
- attachShader: () => {
557
- },
558
- linkProgram: () => {
559
- },
560
- getProgramParameter: (_, param) => {
561
- if (param === 35714) return true;
562
- return true;
563
- },
564
- getProgramInfoLog: () => "",
565
- useProgram: () => {
566
- },
567
- createBuffer: () => ({}),
568
- bindBuffer: () => {
569
- },
570
- bufferData: () => {
571
- },
572
- enableVertexAttribArray: () => {
483
+ // src/setup/storage.ts
484
+ import "fake-indexeddb/auto";
485
+ function createMockLocalStorage(initialData = {}) {
486
+ const storage = new Map(Object.entries(initialData));
487
+ return {
488
+ getItem: (key) => storage.get(key) || null,
489
+ setItem: (key, value) => storage.set(key, value),
490
+ removeItem: (key) => storage.delete(key),
491
+ clear: () => storage.clear(),
492
+ key: (index) => Array.from(storage.keys())[index] || null,
493
+ get length() {
494
+ return storage.size;
495
+ }
496
+ };
497
+ }
498
+ function createMockSessionStorage(initialData = {}) {
499
+ return createMockLocalStorage(initialData);
500
+ }
501
+ function createMockIndexedDB() {
502
+ if (typeof indexedDB === "undefined") {
503
+ throw new Error("IndexedDB mock not found. Ensure fake-indexeddb is loaded.");
504
+ }
505
+ return indexedDB;
506
+ }
507
+ function createStorageTestScenario(storageType = "local") {
508
+ const storage = storageType === "local" ? createMockLocalStorage() : createMockSessionStorage();
509
+ return {
510
+ storage,
511
+ populate(data) {
512
+ Object.entries(data).forEach(([k, v]) => storage.setItem(k, v));
573
513
  },
574
- vertexAttribPointer: () => {
514
+ verify(key, value) {
515
+ return storage.getItem(key) === value;
516
+ }
517
+ };
518
+ }
519
+
520
+ // src/setup/audio.ts
521
+ function createMockAudioContext() {
522
+ 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 () => {
575
553
  },
576
- enable: () => {
554
+ suspend: async () => {
577
555
  },
578
- disable: () => {
556
+ close: async () => {
579
557
  },
580
- depthMask: () => {
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
+ })
572
+ };
573
+ }
574
+ function setupMockAudioContext() {
575
+ if (typeof global.AudioContext === "undefined" && typeof global.window !== "undefined") {
576
+ global.AudioContext = class {
577
+ constructor() {
578
+ return createMockAudioContext();
579
+ }
580
+ };
581
+ global.window.AudioContext = global.AudioContext;
582
+ global.window.webkitAudioContext = global.AudioContext;
583
+ }
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;
590
+ }
591
+ }
592
+ function captureAudioEvents(context) {
593
+ return [];
594
+ }
595
+
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
+ });
581
624
  },
582
- blendFunc: () => {
625
+ advance(deltaMs = 16.6) {
626
+ this.tick(currentTime + deltaMs);
583
627
  },
584
- viewport: () => {
628
+ getCallbacks() {
629
+ return callbacks.map((c) => c.callback);
585
630
  },
586
- clearColor: () => {
631
+ reset() {
632
+ callbacks = [];
633
+ nextId = 1;
634
+ currentTime = 0;
587
635
  },
588
- clear: () => {
636
+ enable() {
637
+ activeMockRAF = this;
638
+ global.requestAnimationFrame = raf;
639
+ global.cancelAnimationFrame = cancel;
589
640
  },
590
- createTexture: () => ({}),
591
- bindTexture: () => {
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
592
666
  },
593
- texImage2D: () => {
667
+ clearMarks: () => {
594
668
  },
595
- texParameteri: () => {
669
+ clearMeasures: () => {
596
670
  },
597
- activeTexture: () => {
671
+ clearResourceTimings: () => {
598
672
  },
599
- uniform1i: () => {
673
+ getEntries: () => [],
674
+ getEntriesByName: () => [],
675
+ getEntriesByType: () => [],
676
+ mark: () => {
600
677
  },
601
- uniform1f: () => {
678
+ measure: () => {
602
679
  },
603
- uniform2f: () => {
680
+ setResourceTimingBufferSize: () => {
604
681
  },
605
- uniform3f: () => {
682
+ toJSON: () => ({}),
683
+ addEventListener: () => {
606
684
  },
607
- uniform4f: () => {
685
+ removeEventListener: () => {
608
686
  },
609
- uniformMatrix4fv: () => {
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);
610
728
  },
611
- getUniformLocation: () => ({}),
612
- getAttribLocation: () => 0,
613
- drawArrays: () => {
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;
614
752
  },
615
- drawElements: () => {
753
+ clear() {
754
+ timers = [];
616
755
  },
617
- createVertexArray: () => ({}),
618
- bindVertexArray: () => {
619
- },
620
- deleteShader: () => {
621
- },
622
- deleteProgram: () => {
623
- },
624
- deleteBuffer: () => {
625
- },
626
- deleteTexture: () => {
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);
627
860
  },
628
- deleteVertexArray: () => {
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
+ }
866
+ };
867
+ }
868
+ async function waitForGameReady(page, timeout = 1e4) {
869
+ try {
870
+ await page.waitForFunction(() => {
871
+ return window.gameInstance && window.gameInstance.isReady;
872
+ }, null, { timeout });
873
+ } catch (e) {
874
+ await page.waitForSelector("canvas", { timeout });
875
+ }
876
+ }
877
+ async function captureGameState(page) {
878
+ return await page.evaluate(() => {
879
+ if (window.gameInstance && window.gameInstance.getState) {
880
+ return window.gameInstance.getState();
881
+ }
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: []
925
+ };
926
+ }
927
+ function makeLeaf(contents, firstLeafBrush, numLeafBrushes) {
928
+ return { contents, cluster: 0, area: 0, firstLeafBrush, numLeafBrushes };
929
+ }
930
+ function makeLeafModel(brushes) {
931
+ const planes = brushes.flatMap((brush) => brush.sides.map((side) => side.plane));
932
+ return {
933
+ planes,
934
+ nodes: [],
935
+ leaves: [makeLeaf(0, 0, brushes.length)],
936
+ brushes,
937
+ leafBrushes: brushes.map((_, i) => i),
938
+ bmodels: []
939
+ };
940
+ }
941
+ function makeBrushFromMinsMaxs(mins, maxs, contents = CONTENTS_SOLID) {
942
+ const planes = [
943
+ makePlane({ x: 1, y: 0, z: 0 }, maxs.x),
944
+ makePlane({ x: -1, y: 0, z: 0 }, -mins.x),
945
+ makePlane({ x: 0, y: 1, z: 0 }, maxs.y),
946
+ makePlane({ x: 0, y: -1, z: 0 }, -mins.y),
947
+ makePlane({ x: 0, y: 0, z: 1 }, maxs.z),
948
+ makePlane({ x: 0, y: 0, z: -1 }, -mins.z)
949
+ ];
950
+ return {
951
+ contents,
952
+ sides: planes.map((plane) => ({ plane, surfaceFlags: 0 }))
953
+ };
954
+ }
955
+
956
+ // src/shared/mocks.ts
957
+ import { vi } from "vitest";
958
+ var createBinaryWriterMock = () => ({
959
+ writeByte: vi.fn(),
960
+ writeShort: vi.fn(),
961
+ writeLong: vi.fn(),
962
+ writeString: vi.fn(),
963
+ writeBytes: vi.fn(),
964
+ getBuffer: vi.fn(() => new Uint8Array(0)),
965
+ reset: vi.fn(),
966
+ // Legacy methods (if any)
967
+ writeInt8: vi.fn(),
968
+ writeUint8: vi.fn(),
969
+ writeInt16: vi.fn(),
970
+ writeUint16: vi.fn(),
971
+ writeInt32: vi.fn(),
972
+ writeUint32: vi.fn(),
973
+ writeFloat: vi.fn(),
974
+ getData: vi.fn(() => new Uint8Array(0))
975
+ });
976
+ var createNetChanMock = () => ({
977
+ qport: 1234,
978
+ // Sequencing
979
+ incomingSequence: 0,
980
+ outgoingSequence: 0,
981
+ incomingAcknowledged: 0,
982
+ // Reliable messaging
983
+ incomingReliableAcknowledged: false,
984
+ incomingReliableSequence: 0,
985
+ outgoingReliableSequence: 0,
986
+ reliableMessage: createBinaryWriterMock(),
987
+ reliableLength: 0,
988
+ // Fragmentation
989
+ fragmentSendOffset: 0,
990
+ fragmentBuffer: null,
991
+ fragmentLength: 0,
992
+ fragmentReceived: 0,
993
+ // Timing
994
+ lastReceived: 0,
995
+ lastSent: 0,
996
+ remoteAddress: { type: "IP", port: 1234 },
997
+ // Methods
998
+ setup: vi.fn(),
999
+ reset: vi.fn(),
1000
+ transmit: vi.fn(),
1001
+ process: vi.fn(),
1002
+ canSendReliable: vi.fn(() => true),
1003
+ writeReliableByte: vi.fn(),
1004
+ writeReliableShort: vi.fn(),
1005
+ writeReliableLong: vi.fn(),
1006
+ writeReliableString: vi.fn(),
1007
+ getReliableData: vi.fn(() => new Uint8Array(0)),
1008
+ needsKeepalive: vi.fn(() => false),
1009
+ isTimedOut: vi.fn(() => false)
1010
+ });
1011
+ var createBinaryStreamMock = () => ({
1012
+ getPosition: vi.fn(() => 0),
1013
+ getReadPosition: vi.fn(() => 0),
1014
+ getLength: vi.fn(() => 0),
1015
+ getRemaining: vi.fn(() => 0),
1016
+ seek: vi.fn(),
1017
+ setReadPosition: vi.fn(),
1018
+ hasMore: vi.fn(() => true),
1019
+ hasBytes: vi.fn((amount) => true),
1020
+ readChar: vi.fn(() => 0),
1021
+ readByte: vi.fn(() => 0),
1022
+ readShort: vi.fn(() => 0),
1023
+ readUShort: vi.fn(() => 0),
1024
+ readLong: vi.fn(() => 0),
1025
+ readULong: vi.fn(() => 0),
1026
+ readFloat: vi.fn(() => 0),
1027
+ readString: vi.fn(() => ""),
1028
+ readStringLine: vi.fn(() => ""),
1029
+ readCoord: vi.fn(() => 0),
1030
+ readAngle: vi.fn(() => 0),
1031
+ readAngle16: vi.fn(() => 0),
1032
+ readData: vi.fn((length) => new Uint8Array(length)),
1033
+ readPos: vi.fn(),
1034
+ readDir: vi.fn()
1035
+ });
1036
+
1037
+ // src/game/factories.ts
1038
+ var createPlayerStateFactory = (overrides) => ({
1039
+ pm_type: 0,
1040
+ pm_time: 0,
1041
+ pm_flags: 0,
1042
+ origin: { x: 0, y: 0, z: 0 },
1043
+ velocity: { x: 0, y: 0, z: 0 },
1044
+ viewAngles: { x: 0, y: 0, z: 0 },
1045
+ onGround: false,
1046
+ waterLevel: 0,
1047
+ watertype: 0,
1048
+ mins: { x: 0, y: 0, z: 0 },
1049
+ maxs: { x: 0, y: 0, z: 0 },
1050
+ damageAlpha: 0,
1051
+ damageIndicators: [],
1052
+ blend: [0, 0, 0, 0],
1053
+ stats: [],
1054
+ kick_angles: { x: 0, y: 0, z: 0 },
1055
+ kick_origin: { x: 0, y: 0, z: 0 },
1056
+ gunoffset: { x: 0, y: 0, z: 0 },
1057
+ gunangles: { x: 0, y: 0, z: 0 },
1058
+ gunindex: 0,
1059
+ gun_frame: 0,
1060
+ rdflags: 0,
1061
+ fov: 90,
1062
+ renderfx: 0,
1063
+ ...overrides
1064
+ });
1065
+ var createEntityStateFactory = (overrides) => ({
1066
+ number: 0,
1067
+ origin: { x: 0, y: 0, z: 0 },
1068
+ angles: { x: 0, y: 0, z: 0 },
1069
+ oldOrigin: { x: 0, y: 0, z: 0 },
1070
+ modelIndex: 0,
1071
+ modelIndex2: 0,
1072
+ modelIndex3: 0,
1073
+ modelIndex4: 0,
1074
+ frame: 0,
1075
+ skinNum: 0,
1076
+ effects: 0,
1077
+ renderfx: 0,
1078
+ solid: 0,
1079
+ sound: 0,
1080
+ event: 0,
1081
+ ...overrides
1082
+ });
1083
+ var createGameStateSnapshotFactory = (overrides) => ({
1084
+ gravity: { x: 0, y: 0, z: -800 },
1085
+ origin: { x: 0, y: 0, z: 0 },
1086
+ velocity: { x: 0, y: 0, z: 0 },
1087
+ viewangles: { x: 0, y: 0, z: 0 },
1088
+ level: { timeSeconds: 0, frameNumber: 0, previousTimeSeconds: 0, deltaSeconds: 0.1 },
1089
+ entities: {
1090
+ activeCount: 0,
1091
+ worldClassname: "worldspawn"
1092
+ },
1093
+ packetEntities: [],
1094
+ pmFlags: 0,
1095
+ pmType: 0,
1096
+ waterlevel: 0,
1097
+ watertype: 0,
1098
+ deltaAngles: { x: 0, y: 0, z: 0 },
1099
+ health: 100,
1100
+ armor: 0,
1101
+ ammo: 0,
1102
+ blend: [0, 0, 0, 0],
1103
+ damageAlpha: 0,
1104
+ damageIndicators: [],
1105
+ stats: [],
1106
+ kick_angles: { x: 0, y: 0, z: 0 },
1107
+ kick_origin: { x: 0, y: 0, z: 0 },
1108
+ gunoffset: { x: 0, y: 0, z: 0 },
1109
+ gunangles: { x: 0, y: 0, z: 0 },
1110
+ gunindex: 0,
1111
+ pm_time: 0,
1112
+ gun_frame: 0,
1113
+ rdflags: 0,
1114
+ fov: 90,
1115
+ renderfx: 0,
1116
+ pm_flags: 0,
1117
+ pm_type: 0,
1118
+ ...overrides
1119
+ });
1120
+
1121
+ // src/game/helpers.ts
1122
+ import { vi as vi2 } from "vitest";
1123
+ import { Entity, SpawnRegistry, ScriptHookRegistry } from "@quake2ts/game";
1124
+ import { createRandomGenerator } from "@quake2ts/shared";
1125
+ import { intersects, stairTrace, ladderTrace } from "@quake2ts/shared";
1126
+ var createMockEngine = () => ({
1127
+ sound: vi2.fn(),
1128
+ soundIndex: vi2.fn((sound) => 0),
1129
+ modelIndex: vi2.fn((model) => 0),
1130
+ centerprintf: vi2.fn()
1131
+ });
1132
+ var createMockGame = (seed = 12345) => {
1133
+ const spawnRegistry = new SpawnRegistry();
1134
+ const hooks = new ScriptHookRegistry();
1135
+ const game = {
1136
+ random: createRandomGenerator({ seed }),
1137
+ registerEntitySpawn: vi2.fn((classname, spawnFunc) => {
1138
+ spawnRegistry.register(classname, (entity) => spawnFunc(entity));
1139
+ }),
1140
+ unregisterEntitySpawn: vi2.fn((classname) => {
1141
+ spawnRegistry.unregister(classname);
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");
1148
+ }),
1149
+ clientBegin: vi2.fn((client) => {
1150
+ hooks.onPlayerSpawn({});
1151
+ }),
1152
+ damage: vi2.fn((amount) => {
1153
+ hooks.onDamage({}, null, null, amount, 0, 0);
1154
+ })
1155
+ };
1156
+ return { game, spawnRegistry };
1157
+ };
1158
+ function createTestContext(options) {
1159
+ const engine = createMockEngine();
1160
+ const seed = options?.seed ?? 12345;
1161
+ const { game, spawnRegistry } = createMockGame(seed);
1162
+ const traceFn = vi2.fn((start, end, mins, maxs) => ({
1163
+ fraction: 1,
1164
+ ent: null,
1165
+ allsolid: false,
1166
+ startsolid: false,
1167
+ endpos: end,
1168
+ plane: { normal: { x: 0, y: 0, z: 1 }, dist: 0 },
1169
+ surfaceFlags: 0,
1170
+ contents: 0
1171
+ }));
1172
+ const entityList = options?.initialEntities ? [...options.initialEntities] : [];
1173
+ const hooks = game.hooks;
1174
+ const entities = {
1175
+ spawn: vi2.fn(() => {
1176
+ const ent = new Entity(entityList.length + 1);
1177
+ entityList.push(ent);
1178
+ hooks.onEntitySpawn(ent);
1179
+ return ent;
1180
+ }),
1181
+ free: vi2.fn((ent) => {
1182
+ const idx = entityList.indexOf(ent);
1183
+ if (idx !== -1) {
1184
+ entityList.splice(idx, 1);
1185
+ }
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);
1193
+ }
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)
629
1223
  },
630
- // WebGL2 specific
631
- texImage3D: () => {
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
632
1229
  },
633
- uniformBlockBinding: () => {
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
634
1248
  },
635
- getExtension: () => null
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
1257
+ };
1258
+ 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()
636
1270
  };
637
- return gl;
638
1271
  }
639
-
640
- // src/e2e/input.ts
641
- var MockPointerLock = class {
642
- static setup(doc) {
643
- let _pointerLockElement = null;
644
- Object.defineProperty(doc, "pointerLockElement", {
645
- get: () => _pointerLockElement,
646
- configurable: true
647
- });
648
- doc.exitPointerLock = () => {
649
- if (_pointerLockElement) {
650
- _pointerLockElement = null;
651
- doc.dispatchEvent(new Event("pointerlockchange"));
652
- }
653
- };
654
- global.HTMLElement.prototype.requestPointerLock = function() {
655
- _pointerLockElement = this;
656
- doc.dispatchEvent(new Event("pointerlockchange"));
657
- };
658
- }
659
- };
660
- var InputInjector = class {
661
- constructor(doc, win) {
662
- this.doc = doc;
663
- this.win = win;
664
- }
665
- keyDown(key, code) {
666
- const event = new this.win.KeyboardEvent("keydown", {
667
- key,
668
- code: code || key,
669
- bubbles: true,
670
- cancelable: true,
671
- view: this.win
672
- });
673
- this.doc.dispatchEvent(event);
674
- }
675
- keyUp(key, code) {
676
- const event = new this.win.KeyboardEvent("keyup", {
677
- key,
678
- code: code || key,
679
- bubbles: true,
680
- cancelable: true,
681
- view: this.win
682
- });
683
- this.doc.dispatchEvent(event);
684
- }
685
- mouseMove(movementX, movementY, clientX = 0, clientY = 0) {
686
- const event = new this.win.MouseEvent("mousemove", {
687
- bubbles: true,
688
- cancelable: true,
689
- view: this.win,
690
- clientX,
691
- clientY,
692
- movementX,
693
- // Note: JSDOM might not support this standard property fully on event init
694
- movementY
695
- });
696
- Object.defineProperty(event, "movementX", { value: movementX });
697
- Object.defineProperty(event, "movementY", { value: movementY });
698
- const target = this.doc.pointerLockElement || this.doc;
699
- target.dispatchEvent(event);
700
- }
701
- mouseDown(button = 0) {
702
- const event = new this.win.MouseEvent("mousedown", {
703
- button,
704
- bubbles: true,
705
- cancelable: true,
706
- view: this.win
707
- });
708
- const target = this.doc.pointerLockElement || this.doc;
709
- target.dispatchEvent(event);
710
- }
711
- mouseUp(button = 0) {
712
- const event = new this.win.MouseEvent("mouseup", {
713
- button,
714
- bubbles: true,
715
- cancelable: true,
716
- view: this.win
717
- });
718
- const target = this.doc.pointerLockElement || this.doc;
719
- target.dispatchEvent(event);
720
- }
721
- wheel(deltaY) {
722
- const event = new this.win.WheelEvent("wheel", {
723
- deltaY,
724
- bubbles: true,
725
- cancelable: true,
726
- view: this.win
727
- });
728
- const target = this.doc.pointerLockElement || this.doc;
729
- target.dispatchEvent(event);
730
- }
731
- };
1272
+ function createSpawnContext() {
1273
+ return createTestContext();
1274
+ }
1275
+ function createEntity() {
1276
+ return new Entity(1);
1277
+ }
732
1278
  export {
733
1279
  InputInjector,
734
1280
  MockPointerLock,
1281
+ captureAudioEvents,
1282
+ captureCanvasDrawCalls,
1283
+ captureGameState,
735
1284
  createBinaryStreamMock,
736
1285
  createBinaryWriterMock,
1286
+ createControlledTimer,
737
1287
  createEntity,
738
1288
  createEntityStateFactory,
739
1289
  createGameStateSnapshotFactory,
1290
+ createMockAudioContext,
1291
+ createMockCanvas,
1292
+ createMockCanvasContext2D,
740
1293
  createMockEngine,
741
1294
  createMockGame,
1295
+ createMockImage,
1296
+ createMockImageData,
1297
+ createMockIndexedDB,
1298
+ createMockLocalStorage,
1299
+ createMockPerformance,
1300
+ createMockRAF,
1301
+ createMockSessionStorage,
742
1302
  createMockWebGL2Context,
743
1303
  createNetChanMock,
744
1304
  createPlayerStateFactory,
1305
+ createPlaywrightTestClient,
745
1306
  createSpawnContext,
1307
+ createStorageTestScenario,
746
1308
  createTestContext,
747
1309
  intersects,
748
1310
  ladderTrace,
@@ -754,8 +1316,13 @@ export {
754
1316
  makeNode,
755
1317
  makePlane,
756
1318
  setupBrowserEnvironment,
1319
+ setupMockAudioContext,
757
1320
  setupNodeEnvironment,
1321
+ simulateFrames,
1322
+ simulateFramesWithMock,
758
1323
  stairTrace,
759
- teardownBrowserEnvironment
1324
+ teardownBrowserEnvironment,
1325
+ teardownMockAudioContext,
1326
+ waitForGameReady
760
1327
  };
761
1328
  //# sourceMappingURL=index.js.map