quake2ts 0.0.557 → 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.
@@ -34,12 +34,10 @@ __export(index_exports, {
34
34
  MockPointerLock: () => MockPointerLock,
35
35
  captureAudioEvents: () => captureAudioEvents,
36
36
  captureCanvasDrawCalls: () => captureCanvasDrawCalls,
37
- captureGameScreenshot: () => captureGameScreenshot,
38
37
  captureGameState: () => captureGameState,
39
- compareScreenshots: () => compareScreenshots,
40
38
  createBinaryStreamMock: () => createBinaryStreamMock,
41
39
  createBinaryWriterMock: () => createBinaryWriterMock,
42
- createCustomNetworkCondition: () => createCustomNetworkCondition,
40
+ createControlledTimer: () => createControlledTimer,
43
41
  createEntity: () => createEntity,
44
42
  createEntityStateFactory: () => createEntityStateFactory,
45
43
  createGameStateSnapshotFactory: () => createGameStateSnapshotFactory,
@@ -62,7 +60,6 @@ __export(index_exports, {
62
60
  createSpawnContext: () => createSpawnContext,
63
61
  createStorageTestScenario: () => createStorageTestScenario,
64
62
  createTestContext: () => createTestContext,
65
- createVisualTestScenario: () => createVisualTestScenario,
66
63
  intersects: () => import_shared3.intersects,
67
64
  ladderTrace: () => import_shared3.ladderTrace,
68
65
  makeAxisBrush: () => makeAxisBrush,
@@ -77,405 +74,13 @@ __export(index_exports, {
77
74
  setupNodeEnvironment: () => setupNodeEnvironment,
78
75
  simulateFrames: () => simulateFrames,
79
76
  simulateFramesWithMock: () => simulateFramesWithMock,
80
- simulateNetworkCondition: () => simulateNetworkCondition,
81
77
  stairTrace: () => import_shared3.stairTrace,
82
78
  teardownBrowserEnvironment: () => teardownBrowserEnvironment,
83
79
  teardownMockAudioContext: () => teardownMockAudioContext,
84
- throttleBandwidth: () => throttleBandwidth,
85
80
  waitForGameReady: () => waitForGameReady
86
81
  });
87
82
  module.exports = __toCommonJS(index_exports);
88
83
 
89
- // src/shared/mocks.ts
90
- var import_vitest = require("vitest");
91
- var createBinaryWriterMock = () => ({
92
- writeByte: import_vitest.vi.fn(),
93
- writeShort: import_vitest.vi.fn(),
94
- writeLong: import_vitest.vi.fn(),
95
- writeString: import_vitest.vi.fn(),
96
- writeBytes: import_vitest.vi.fn(),
97
- getBuffer: import_vitest.vi.fn(() => new Uint8Array(0)),
98
- reset: import_vitest.vi.fn(),
99
- // Legacy methods (if any)
100
- writeInt8: import_vitest.vi.fn(),
101
- writeUint8: import_vitest.vi.fn(),
102
- writeInt16: import_vitest.vi.fn(),
103
- writeUint16: import_vitest.vi.fn(),
104
- writeInt32: import_vitest.vi.fn(),
105
- writeUint32: import_vitest.vi.fn(),
106
- writeFloat: import_vitest.vi.fn(),
107
- getData: import_vitest.vi.fn(() => new Uint8Array(0))
108
- });
109
- var createNetChanMock = () => ({
110
- qport: 1234,
111
- // Sequencing
112
- incomingSequence: 0,
113
- outgoingSequence: 0,
114
- incomingAcknowledged: 0,
115
- // Reliable messaging
116
- incomingReliableAcknowledged: false,
117
- incomingReliableSequence: 0,
118
- outgoingReliableSequence: 0,
119
- reliableMessage: createBinaryWriterMock(),
120
- reliableLength: 0,
121
- // Fragmentation
122
- fragmentSendOffset: 0,
123
- fragmentBuffer: null,
124
- fragmentLength: 0,
125
- fragmentReceived: 0,
126
- // Timing
127
- lastReceived: 0,
128
- lastSent: 0,
129
- remoteAddress: { type: "IP", port: 1234 },
130
- // Methods
131
- setup: import_vitest.vi.fn(),
132
- reset: import_vitest.vi.fn(),
133
- transmit: import_vitest.vi.fn(),
134
- process: import_vitest.vi.fn(),
135
- canSendReliable: import_vitest.vi.fn(() => true),
136
- writeReliableByte: import_vitest.vi.fn(),
137
- writeReliableShort: import_vitest.vi.fn(),
138
- writeReliableLong: import_vitest.vi.fn(),
139
- writeReliableString: import_vitest.vi.fn(),
140
- getReliableData: import_vitest.vi.fn(() => new Uint8Array(0)),
141
- needsKeepalive: import_vitest.vi.fn(() => false),
142
- isTimedOut: import_vitest.vi.fn(() => false)
143
- });
144
- var createBinaryStreamMock = () => ({
145
- getPosition: import_vitest.vi.fn(() => 0),
146
- getReadPosition: import_vitest.vi.fn(() => 0),
147
- getLength: import_vitest.vi.fn(() => 0),
148
- getRemaining: import_vitest.vi.fn(() => 0),
149
- seek: import_vitest.vi.fn(),
150
- setReadPosition: import_vitest.vi.fn(),
151
- hasMore: import_vitest.vi.fn(() => true),
152
- hasBytes: import_vitest.vi.fn((amount) => true),
153
- readChar: import_vitest.vi.fn(() => 0),
154
- readByte: import_vitest.vi.fn(() => 0),
155
- readShort: import_vitest.vi.fn(() => 0),
156
- readUShort: import_vitest.vi.fn(() => 0),
157
- readLong: import_vitest.vi.fn(() => 0),
158
- readULong: import_vitest.vi.fn(() => 0),
159
- readFloat: import_vitest.vi.fn(() => 0),
160
- readString: import_vitest.vi.fn(() => ""),
161
- readStringLine: import_vitest.vi.fn(() => ""),
162
- readCoord: import_vitest.vi.fn(() => 0),
163
- readAngle: import_vitest.vi.fn(() => 0),
164
- readAngle16: import_vitest.vi.fn(() => 0),
165
- readData: import_vitest.vi.fn((length) => new Uint8Array(length)),
166
- readPos: import_vitest.vi.fn(),
167
- readDir: import_vitest.vi.fn()
168
- });
169
-
170
- // src/shared/bsp.ts
171
- var import_shared = require("@quake2ts/shared");
172
- function makePlane(normal, dist) {
173
- return {
174
- normal,
175
- dist,
176
- type: Math.abs(normal.x) === 1 ? 0 : Math.abs(normal.y) === 1 ? 1 : Math.abs(normal.z) === 1 ? 2 : 3,
177
- signbits: (0, import_shared.computePlaneSignBits)(normal)
178
- };
179
- }
180
- function makeAxisBrush(size, contents = import_shared.CONTENTS_SOLID) {
181
- const half = size / 2;
182
- const planes = [
183
- makePlane({ x: 1, y: 0, z: 0 }, half),
184
- makePlane({ x: -1, y: 0, z: 0 }, half),
185
- makePlane({ x: 0, y: 1, z: 0 }, half),
186
- makePlane({ x: 0, y: -1, z: 0 }, half),
187
- makePlane({ x: 0, y: 0, z: 1 }, half),
188
- makePlane({ x: 0, y: 0, z: -1 }, half)
189
- ];
190
- return {
191
- contents,
192
- sides: planes.map((plane) => ({ plane, surfaceFlags: 0 }))
193
- };
194
- }
195
- function makeNode(plane, children) {
196
- return { plane, children };
197
- }
198
- function makeBspModel(planes, nodes, leaves, brushes, leafBrushes) {
199
- return {
200
- planes,
201
- nodes,
202
- leaves,
203
- brushes,
204
- leafBrushes,
205
- bmodels: []
206
- };
207
- }
208
- function makeLeaf(contents, firstLeafBrush, numLeafBrushes) {
209
- return { contents, cluster: 0, area: 0, firstLeafBrush, numLeafBrushes };
210
- }
211
- function makeLeafModel(brushes) {
212
- const planes = brushes.flatMap((brush) => brush.sides.map((side) => side.plane));
213
- return {
214
- planes,
215
- nodes: [],
216
- leaves: [makeLeaf(0, 0, brushes.length)],
217
- brushes,
218
- leafBrushes: brushes.map((_, i) => i),
219
- bmodels: []
220
- };
221
- }
222
- function makeBrushFromMinsMaxs(mins, maxs, contents = import_shared.CONTENTS_SOLID) {
223
- const planes = [
224
- makePlane({ x: 1, y: 0, z: 0 }, maxs.x),
225
- makePlane({ x: -1, y: 0, z: 0 }, -mins.x),
226
- makePlane({ x: 0, y: 1, z: 0 }, maxs.y),
227
- makePlane({ x: 0, y: -1, z: 0 }, -mins.y),
228
- makePlane({ x: 0, y: 0, z: 1 }, maxs.z),
229
- makePlane({ x: 0, y: 0, z: -1 }, -mins.z)
230
- ];
231
- return {
232
- contents,
233
- sides: planes.map((plane) => ({ plane, surfaceFlags: 0 }))
234
- };
235
- }
236
-
237
- // src/game/factories.ts
238
- var createPlayerStateFactory = (overrides) => ({
239
- pm_type: 0,
240
- pm_time: 0,
241
- pm_flags: 0,
242
- origin: { x: 0, y: 0, z: 0 },
243
- velocity: { x: 0, y: 0, z: 0 },
244
- viewAngles: { x: 0, y: 0, z: 0 },
245
- onGround: false,
246
- waterLevel: 0,
247
- watertype: 0,
248
- mins: { x: 0, y: 0, z: 0 },
249
- maxs: { x: 0, y: 0, z: 0 },
250
- damageAlpha: 0,
251
- damageIndicators: [],
252
- blend: [0, 0, 0, 0],
253
- stats: [],
254
- kick_angles: { x: 0, y: 0, z: 0 },
255
- kick_origin: { x: 0, y: 0, z: 0 },
256
- gunoffset: { x: 0, y: 0, z: 0 },
257
- gunangles: { x: 0, y: 0, z: 0 },
258
- gunindex: 0,
259
- gun_frame: 0,
260
- rdflags: 0,
261
- fov: 90,
262
- renderfx: 0,
263
- ...overrides
264
- });
265
- var createEntityStateFactory = (overrides) => ({
266
- number: 0,
267
- origin: { x: 0, y: 0, z: 0 },
268
- angles: { x: 0, y: 0, z: 0 },
269
- oldOrigin: { x: 0, y: 0, z: 0 },
270
- modelIndex: 0,
271
- modelIndex2: 0,
272
- modelIndex3: 0,
273
- modelIndex4: 0,
274
- frame: 0,
275
- skinNum: 0,
276
- effects: 0,
277
- renderfx: 0,
278
- solid: 0,
279
- sound: 0,
280
- event: 0,
281
- ...overrides
282
- });
283
- var createGameStateSnapshotFactory = (overrides) => ({
284
- gravity: { x: 0, y: 0, z: -800 },
285
- origin: { x: 0, y: 0, z: 0 },
286
- velocity: { x: 0, y: 0, z: 0 },
287
- viewangles: { x: 0, y: 0, z: 0 },
288
- level: { timeSeconds: 0, frameNumber: 0, previousTimeSeconds: 0, deltaSeconds: 0.1 },
289
- entities: {
290
- activeCount: 0,
291
- worldClassname: "worldspawn"
292
- },
293
- packetEntities: [],
294
- pmFlags: 0,
295
- pmType: 0,
296
- waterlevel: 0,
297
- watertype: 0,
298
- deltaAngles: { x: 0, y: 0, z: 0 },
299
- health: 100,
300
- armor: 0,
301
- ammo: 0,
302
- blend: [0, 0, 0, 0],
303
- damageAlpha: 0,
304
- damageIndicators: [],
305
- stats: [],
306
- kick_angles: { x: 0, y: 0, z: 0 },
307
- kick_origin: { x: 0, y: 0, z: 0 },
308
- gunoffset: { x: 0, y: 0, z: 0 },
309
- gunangles: { x: 0, y: 0, z: 0 },
310
- gunindex: 0,
311
- pm_time: 0,
312
- gun_frame: 0,
313
- rdflags: 0,
314
- fov: 90,
315
- renderfx: 0,
316
- pm_flags: 0,
317
- pm_type: 0,
318
- ...overrides
319
- });
320
-
321
- // src/game/helpers.ts
322
- var import_vitest2 = require("vitest");
323
- var import_game = require("@quake2ts/game");
324
- var import_shared2 = require("@quake2ts/shared");
325
- var import_shared3 = require("@quake2ts/shared");
326
- var createMockEngine = () => ({
327
- sound: import_vitest2.vi.fn(),
328
- soundIndex: import_vitest2.vi.fn((sound) => 0),
329
- modelIndex: import_vitest2.vi.fn((model) => 0),
330
- centerprintf: import_vitest2.vi.fn()
331
- });
332
- var createMockGame = (seed = 12345) => {
333
- const spawnRegistry = new import_game.SpawnRegistry();
334
- const hooks = new import_game.ScriptHookRegistry();
335
- const game = {
336
- random: (0, import_shared2.createRandomGenerator)({ seed }),
337
- registerEntitySpawn: import_vitest2.vi.fn((classname, spawnFunc) => {
338
- spawnRegistry.register(classname, (entity) => spawnFunc(entity));
339
- }),
340
- unregisterEntitySpawn: import_vitest2.vi.fn((classname) => {
341
- spawnRegistry.unregister(classname);
342
- }),
343
- getCustomEntities: import_vitest2.vi.fn(() => Array.from(spawnRegistry.keys())),
344
- hooks,
345
- registerHooks: import_vitest2.vi.fn((newHooks) => hooks.register(newHooks)),
346
- spawnWorld: import_vitest2.vi.fn(() => {
347
- hooks.onMapLoad("q2dm1");
348
- }),
349
- clientBegin: import_vitest2.vi.fn((client) => {
350
- hooks.onPlayerSpawn({});
351
- }),
352
- damage: import_vitest2.vi.fn((amount) => {
353
- hooks.onDamage({}, null, null, amount, 0, 0);
354
- })
355
- };
356
- return { game, spawnRegistry };
357
- };
358
- function createTestContext(options) {
359
- const engine = createMockEngine();
360
- const seed = options?.seed ?? 12345;
361
- const { game, spawnRegistry } = createMockGame(seed);
362
- const traceFn = import_vitest2.vi.fn((start, end, mins, maxs) => ({
363
- fraction: 1,
364
- ent: null,
365
- allsolid: false,
366
- startsolid: false,
367
- endpos: end,
368
- plane: { normal: { x: 0, y: 0, z: 1 }, dist: 0 },
369
- surfaceFlags: 0,
370
- contents: 0
371
- }));
372
- const entityList = options?.initialEntities ? [...options.initialEntities] : [];
373
- const hooks = game.hooks;
374
- const entities = {
375
- spawn: import_vitest2.vi.fn(() => {
376
- const ent = new import_game.Entity(entityList.length + 1);
377
- entityList.push(ent);
378
- hooks.onEntitySpawn(ent);
379
- return ent;
380
- }),
381
- free: import_vitest2.vi.fn((ent) => {
382
- const idx = entityList.indexOf(ent);
383
- if (idx !== -1) {
384
- entityList.splice(idx, 1);
385
- }
386
- hooks.onEntityRemove(ent);
387
- }),
388
- finalizeSpawn: import_vitest2.vi.fn(),
389
- freeImmediate: import_vitest2.vi.fn((ent) => {
390
- const idx = entityList.indexOf(ent);
391
- if (idx !== -1) {
392
- entityList.splice(idx, 1);
393
- }
394
- }),
395
- setSpawnRegistry: import_vitest2.vi.fn(),
396
- timeSeconds: 10,
397
- deltaSeconds: 0.1,
398
- modelIndex: import_vitest2.vi.fn(() => 0),
399
- scheduleThink: import_vitest2.vi.fn((entity, time) => {
400
- entity.nextthink = time;
401
- }),
402
- linkentity: import_vitest2.vi.fn(),
403
- trace: traceFn,
404
- pointcontents: import_vitest2.vi.fn(() => 0),
405
- multicast: import_vitest2.vi.fn(),
406
- unicast: import_vitest2.vi.fn(),
407
- engine,
408
- game,
409
- sound: import_vitest2.vi.fn((ent, chan, sound, vol, attn, timeofs) => {
410
- engine.sound(ent, chan, sound, vol, attn, timeofs);
411
- }),
412
- soundIndex: import_vitest2.vi.fn((sound) => engine.soundIndex(sound)),
413
- useTargets: import_vitest2.vi.fn((entity, activator) => {
414
- }),
415
- findByTargetName: import_vitest2.vi.fn(() => []),
416
- pickTarget: import_vitest2.vi.fn(() => null),
417
- killBox: import_vitest2.vi.fn(),
418
- rng: (0, import_shared2.createRandomGenerator)({ seed }),
419
- imports: {
420
- configstring: import_vitest2.vi.fn(),
421
- trace: traceFn,
422
- pointcontents: import_vitest2.vi.fn(() => 0)
423
- },
424
- level: {
425
- intermission_angle: { x: 0, y: 0, z: 0 },
426
- intermission_origin: { x: 0, y: 0, z: 0 },
427
- next_auto_save: 0,
428
- health_bar_entities: null
429
- },
430
- targetNameIndex: /* @__PURE__ */ new Map(),
431
- forEachEntity: import_vitest2.vi.fn((callback) => {
432
- entityList.forEach(callback);
433
- }),
434
- find: import_vitest2.vi.fn((predicate) => {
435
- return entityList.find(predicate);
436
- }),
437
- findByClassname: import_vitest2.vi.fn((classname) => {
438
- return entityList.find((e) => e.classname === classname);
439
- }),
440
- beginFrame: import_vitest2.vi.fn((timeSeconds) => {
441
- entities.timeSeconds = timeSeconds;
442
- }),
443
- targetAwareness: {
444
- timeSeconds: 10,
445
- frameNumber: 1,
446
- sightEntity: null,
447
- soundEntity: null
448
- },
449
- // Adding missing properties to satisfy EntitySystem interface partially or fully
450
- // We cast to unknown first anyway, but filling these in makes it safer for consumers
451
- skill: 1,
452
- deathmatch: false,
453
- coop: false,
454
- activeCount: entityList.length,
455
- world: entityList.find((e) => e.classname === "worldspawn") || new import_game.Entity(0)
456
- // ... other EntitySystem properties would go here
457
- };
458
- return {
459
- keyValues: {},
460
- entities,
461
- game,
462
- engine,
463
- health_multiplier: 1,
464
- warn: import_vitest2.vi.fn(),
465
- free: import_vitest2.vi.fn(),
466
- // Mock precache functions if they are part of SpawnContext in future or TestContext extensions
467
- precacheModel: import_vitest2.vi.fn(),
468
- precacheSound: import_vitest2.vi.fn(),
469
- precacheImage: import_vitest2.vi.fn()
470
- };
471
- }
472
- function createSpawnContext() {
473
- return createTestContext();
474
- }
475
- function createEntity() {
476
- return new import_game.Entity(1);
477
- }
478
-
479
84
  // src/setup/browser.ts
480
85
  var import_jsdom = require("jsdom");
481
86
  var import_canvas = require("@napi-rs/canvas");
@@ -871,12 +476,6 @@ function teardownBrowserEnvironment() {
871
476
  delete global.UIEvent;
872
477
  }
873
478
 
874
- // src/setup/node.ts
875
- function setupNodeEnvironment() {
876
- if (typeof global.fetch === "undefined") {
877
- }
878
- }
879
-
880
479
  // src/setup/canvas.ts
881
480
  var import_canvas2 = require("@napi-rs/canvas");
882
481
  function createMockCanvas(width = 300, height = 150) {
@@ -958,7 +557,127 @@ function createMockImage(width, height, src) {
958
557
  return img;
959
558
  }
960
559
 
560
+ // src/setup/node.ts
561
+ function setupNodeEnvironment() {
562
+ if (typeof global.fetch === "undefined") {
563
+ }
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;
578
+ }
579
+ };
580
+ }
581
+ function createMockSessionStorage(initialData = {}) {
582
+ return createMockLocalStorage(initialData);
583
+ }
584
+ function createMockIndexedDB() {
585
+ if (typeof indexedDB === "undefined") {
586
+ throw new Error("IndexedDB mock not found. Ensure fake-indexeddb is loaded.");
587
+ }
588
+ return indexedDB;
589
+ }
590
+ function createStorageTestScenario(storageType = "local") {
591
+ const storage = storageType === "local" ? createMockLocalStorage() : createMockSessionStorage();
592
+ return {
593
+ storage,
594
+ populate(data) {
595
+ Object.entries(data).forEach(([k, v]) => storage.setItem(k, v));
596
+ },
597
+ verify(key, value) {
598
+ return storage.getItem(key) === value;
599
+ }
600
+ };
601
+ }
602
+
603
+ // src/setup/audio.ts
604
+ function createMockAudioContext() {
605
+ 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 () => {
638
+ },
639
+ close: async () => {
640
+ },
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
+ })
655
+ };
656
+ }
657
+ function setupMockAudioContext() {
658
+ if (typeof global.AudioContext === "undefined" && typeof global.window !== "undefined") {
659
+ global.AudioContext = class {
660
+ constructor() {
661
+ return createMockAudioContext();
662
+ }
663
+ };
664
+ global.window.AudioContext = global.AudioContext;
665
+ global.window.webkitAudioContext = global.AudioContext;
666
+ }
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;
673
+ }
674
+ }
675
+ function captureAudioEvents(context) {
676
+ return [];
677
+ }
678
+
961
679
  // src/setup/timing.ts
680
+ var activeMockRAF;
962
681
  function createMockRAF() {
963
682
  let callbacks = [];
964
683
  let nextId = 1;
@@ -998,10 +717,14 @@ function createMockRAF() {
998
717
  currentTime = 0;
999
718
  },
1000
719
  enable() {
720
+ activeMockRAF = this;
1001
721
  global.requestAnimationFrame = raf;
1002
722
  global.cancelAnimationFrame = cancel;
1003
723
  },
1004
724
  disable() {
725
+ if (activeMockRAF === this) {
726
+ activeMockRAF = void 0;
727
+ }
1005
728
  if (originalRAF) {
1006
729
  global.requestAnimationFrame = originalRAF;
1007
730
  } else {
@@ -1024,7 +747,6 @@ function createMockPerformance(startTime = 0) {
1024
747
  timing: {
1025
748
  navigationStart: startTime
1026
749
  },
1027
- // Add minimal navigation/resource timing interfaces to satisfy types if needed
1028
750
  clearMarks: () => {
1029
751
  },
1030
752
  clearMeasures: () => {
@@ -1053,318 +775,585 @@ function createMockPerformance(startTime = 0) {
1053
775
  mockPerf.setTime = (time) => {
1054
776
  currentTime = time;
1055
777
  };
1056
- return mockPerf;
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);
811
+ },
812
+ advanceBy(ms) {
813
+ const targetTime = currentTime + ms;
814
+ while (true) {
815
+ let earliest = null;
816
+ for (const t of timers) {
817
+ if (!earliest || t.dueTime < earliest.dueTime) {
818
+ earliest = t;
819
+ }
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;
835
+ },
836
+ clear() {
837
+ timers = [];
838
+ },
839
+ restore() {
840
+ global.setTimeout = originalSetTimeout;
841
+ global.clearTimeout = originalClearTimeout;
842
+ global.setInterval = originalSetInterval;
843
+ global.clearInterval = originalClearInterval;
844
+ }
845
+ };
846
+ }
847
+ function simulateFrames(count, frameTimeMs = 16.6, callback) {
848
+ if (!activeMockRAF) {
849
+ throw new Error("simulateFrames requires an active MockRAF. Ensure createMockRAF().enable() is called.");
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);
943
+ },
944
+ injectInput: async (type, data) => {
945
+ await page.evaluate(({ type: type2, data: data2 }) => {
946
+ if (window.injectGameInput) window.injectGameInput(type2, data2);
947
+ }, { type, data });
948
+ }
949
+ };
950
+ }
951
+ async function waitForGameReady(page, timeout = 1e4) {
952
+ try {
953
+ await page.waitForFunction(() => {
954
+ return window.gameInstance && window.gameInstance.isReady;
955
+ }, null, { timeout });
956
+ } catch (e) {
957
+ await page.waitForSelector("canvas", { timeout });
958
+ }
959
+ }
960
+ async function captureGameState(page) {
961
+ return await page.evaluate(() => {
962
+ if (window.gameInstance && window.gameInstance.getState) {
963
+ return window.gameInstance.getState();
964
+ }
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
+ };
1057
978
  }
1058
- function simulateFrames(count, frameTimeMs = 16.6, callback) {
1059
- const raf = global.requestAnimationFrame;
1060
- if (!raf) return;
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
+ };
1061
993
  }
1062
- function simulateFramesWithMock(mock, count, frameTimeMs = 16.6, callback) {
1063
- for (let i = 0; i < count; i++) {
1064
- if (callback) callback(i);
1065
- mock.advance(frameTimeMs);
1066
- }
994
+ function makeNode(plane, children) {
995
+ return { plane, children };
1067
996
  }
1068
-
1069
- // src/setup/storage.ts
1070
- var import_auto2 = require("fake-indexeddb/auto");
1071
- function createMockLocalStorage(initialData = {}) {
1072
- const storage = new Map(Object.entries(initialData));
997
+ function makeBspModel(planes, nodes, leaves, brushes, leafBrushes) {
1073
998
  return {
1074
- getItem: (key) => storage.get(key) || null,
1075
- setItem: (key, value) => storage.set(key, value),
1076
- removeItem: (key) => storage.delete(key),
1077
- clear: () => storage.clear(),
1078
- key: (index) => Array.from(storage.keys())[index] || null,
1079
- get length() {
1080
- return storage.size;
1081
- }
999
+ planes,
1000
+ nodes,
1001
+ leaves,
1002
+ brushes,
1003
+ leafBrushes,
1004
+ bmodels: []
1082
1005
  };
1083
1006
  }
1084
- function createMockSessionStorage(initialData = {}) {
1085
- return createMockLocalStorage(initialData);
1007
+ function makeLeaf(contents, firstLeafBrush, numLeafBrushes) {
1008
+ return { contents, cluster: 0, area: 0, firstLeafBrush, numLeafBrushes };
1086
1009
  }
1087
- function createMockIndexedDB() {
1088
- if (typeof indexedDB === "undefined") {
1089
- throw new Error("IndexedDB mock not found. Ensure fake-indexeddb is loaded.");
1090
- }
1091
- return indexedDB;
1010
+ function makeLeafModel(brushes) {
1011
+ const planes = brushes.flatMap((brush) => brush.sides.map((side) => side.plane));
1012
+ return {
1013
+ planes,
1014
+ nodes: [],
1015
+ leaves: [makeLeaf(0, 0, brushes.length)],
1016
+ brushes,
1017
+ leafBrushes: brushes.map((_, i) => i),
1018
+ bmodels: []
1019
+ };
1092
1020
  }
1093
- function createStorageTestScenario(storageType = "local") {
1094
- const storage = storageType === "local" ? createMockLocalStorage() : createMockSessionStorage();
1021
+ function makeBrushFromMinsMaxs(mins, maxs, contents = import_shared.CONTENTS_SOLID) {
1022
+ const planes = [
1023
+ makePlane({ x: 1, y: 0, z: 0 }, maxs.x),
1024
+ makePlane({ x: -1, y: 0, z: 0 }, -mins.x),
1025
+ makePlane({ x: 0, y: 1, z: 0 }, maxs.y),
1026
+ makePlane({ x: 0, y: -1, z: 0 }, -mins.y),
1027
+ makePlane({ x: 0, y: 0, z: 1 }, maxs.z),
1028
+ makePlane({ x: 0, y: 0, z: -1 }, -mins.z)
1029
+ ];
1095
1030
  return {
1096
- storage,
1097
- populate(data) {
1098
- Object.entries(data).forEach(([k, v]) => storage.setItem(k, v));
1099
- },
1100
- verify(key, value) {
1101
- return storage.getItem(key) === value;
1102
- }
1031
+ contents,
1032
+ sides: planes.map((plane) => ({ plane, surfaceFlags: 0 }))
1103
1033
  };
1104
1034
  }
1105
1035
 
1106
- // src/setup/audio.ts
1107
- function createMockAudioContext() {
1108
- return {
1109
- createGain: () => ({
1110
- connect: () => {
1111
- },
1112
- gain: { value: 1, setValueAtTime: () => {
1113
- } }
1036
+ // src/shared/mocks.ts
1037
+ var import_vitest = require("vitest");
1038
+ var createBinaryWriterMock = () => ({
1039
+ writeByte: import_vitest.vi.fn(),
1040
+ writeShort: import_vitest.vi.fn(),
1041
+ writeLong: import_vitest.vi.fn(),
1042
+ writeString: import_vitest.vi.fn(),
1043
+ writeBytes: import_vitest.vi.fn(),
1044
+ getBuffer: import_vitest.vi.fn(() => new Uint8Array(0)),
1045
+ reset: import_vitest.vi.fn(),
1046
+ // Legacy methods (if any)
1047
+ writeInt8: import_vitest.vi.fn(),
1048
+ writeUint8: import_vitest.vi.fn(),
1049
+ writeInt16: import_vitest.vi.fn(),
1050
+ writeUint16: import_vitest.vi.fn(),
1051
+ writeInt32: import_vitest.vi.fn(),
1052
+ writeUint32: import_vitest.vi.fn(),
1053
+ writeFloat: import_vitest.vi.fn(),
1054
+ getData: import_vitest.vi.fn(() => new Uint8Array(0))
1055
+ });
1056
+ var createNetChanMock = () => ({
1057
+ qport: 1234,
1058
+ // Sequencing
1059
+ incomingSequence: 0,
1060
+ outgoingSequence: 0,
1061
+ incomingAcknowledged: 0,
1062
+ // Reliable messaging
1063
+ incomingReliableAcknowledged: false,
1064
+ incomingReliableSequence: 0,
1065
+ outgoingReliableSequence: 0,
1066
+ reliableMessage: createBinaryWriterMock(),
1067
+ reliableLength: 0,
1068
+ // Fragmentation
1069
+ fragmentSendOffset: 0,
1070
+ fragmentBuffer: null,
1071
+ fragmentLength: 0,
1072
+ fragmentReceived: 0,
1073
+ // Timing
1074
+ lastReceived: 0,
1075
+ lastSent: 0,
1076
+ remoteAddress: { type: "IP", port: 1234 },
1077
+ // Methods
1078
+ setup: import_vitest.vi.fn(),
1079
+ reset: import_vitest.vi.fn(),
1080
+ transmit: import_vitest.vi.fn(),
1081
+ process: import_vitest.vi.fn(),
1082
+ canSendReliable: import_vitest.vi.fn(() => true),
1083
+ writeReliableByte: import_vitest.vi.fn(),
1084
+ writeReliableShort: import_vitest.vi.fn(),
1085
+ writeReliableLong: import_vitest.vi.fn(),
1086
+ writeReliableString: import_vitest.vi.fn(),
1087
+ getReliableData: import_vitest.vi.fn(() => new Uint8Array(0)),
1088
+ needsKeepalive: import_vitest.vi.fn(() => false),
1089
+ isTimedOut: import_vitest.vi.fn(() => false)
1090
+ });
1091
+ var createBinaryStreamMock = () => ({
1092
+ getPosition: import_vitest.vi.fn(() => 0),
1093
+ getReadPosition: import_vitest.vi.fn(() => 0),
1094
+ getLength: import_vitest.vi.fn(() => 0),
1095
+ getRemaining: import_vitest.vi.fn(() => 0),
1096
+ seek: import_vitest.vi.fn(),
1097
+ setReadPosition: import_vitest.vi.fn(),
1098
+ hasMore: import_vitest.vi.fn(() => true),
1099
+ hasBytes: import_vitest.vi.fn((amount) => true),
1100
+ readChar: import_vitest.vi.fn(() => 0),
1101
+ readByte: import_vitest.vi.fn(() => 0),
1102
+ readShort: import_vitest.vi.fn(() => 0),
1103
+ readUShort: import_vitest.vi.fn(() => 0),
1104
+ readLong: import_vitest.vi.fn(() => 0),
1105
+ readULong: import_vitest.vi.fn(() => 0),
1106
+ readFloat: import_vitest.vi.fn(() => 0),
1107
+ readString: import_vitest.vi.fn(() => ""),
1108
+ readStringLine: import_vitest.vi.fn(() => ""),
1109
+ readCoord: import_vitest.vi.fn(() => 0),
1110
+ readAngle: import_vitest.vi.fn(() => 0),
1111
+ readAngle16: import_vitest.vi.fn(() => 0),
1112
+ readData: import_vitest.vi.fn((length) => new Uint8Array(length)),
1113
+ readPos: import_vitest.vi.fn(),
1114
+ readDir: import_vitest.vi.fn()
1115
+ });
1116
+
1117
+ // src/game/factories.ts
1118
+ var createPlayerStateFactory = (overrides) => ({
1119
+ pm_type: 0,
1120
+ pm_time: 0,
1121
+ pm_flags: 0,
1122
+ origin: { x: 0, y: 0, z: 0 },
1123
+ velocity: { x: 0, y: 0, z: 0 },
1124
+ viewAngles: { x: 0, y: 0, z: 0 },
1125
+ onGround: false,
1126
+ waterLevel: 0,
1127
+ watertype: 0,
1128
+ mins: { x: 0, y: 0, z: 0 },
1129
+ maxs: { x: 0, y: 0, z: 0 },
1130
+ damageAlpha: 0,
1131
+ damageIndicators: [],
1132
+ blend: [0, 0, 0, 0],
1133
+ stats: [],
1134
+ kick_angles: { x: 0, y: 0, z: 0 },
1135
+ kick_origin: { x: 0, y: 0, z: 0 },
1136
+ gunoffset: { x: 0, y: 0, z: 0 },
1137
+ gunangles: { x: 0, y: 0, z: 0 },
1138
+ gunindex: 0,
1139
+ gun_frame: 0,
1140
+ rdflags: 0,
1141
+ fov: 90,
1142
+ renderfx: 0,
1143
+ ...overrides
1144
+ });
1145
+ var createEntityStateFactory = (overrides) => ({
1146
+ number: 0,
1147
+ origin: { x: 0, y: 0, z: 0 },
1148
+ angles: { x: 0, y: 0, z: 0 },
1149
+ oldOrigin: { x: 0, y: 0, z: 0 },
1150
+ modelIndex: 0,
1151
+ modelIndex2: 0,
1152
+ modelIndex3: 0,
1153
+ modelIndex4: 0,
1154
+ frame: 0,
1155
+ skinNum: 0,
1156
+ effects: 0,
1157
+ renderfx: 0,
1158
+ solid: 0,
1159
+ sound: 0,
1160
+ event: 0,
1161
+ ...overrides
1162
+ });
1163
+ var createGameStateSnapshotFactory = (overrides) => ({
1164
+ gravity: { x: 0, y: 0, z: -800 },
1165
+ origin: { x: 0, y: 0, z: 0 },
1166
+ velocity: { x: 0, y: 0, z: 0 },
1167
+ viewangles: { x: 0, y: 0, z: 0 },
1168
+ level: { timeSeconds: 0, frameNumber: 0, previousTimeSeconds: 0, deltaSeconds: 0.1 },
1169
+ entities: {
1170
+ activeCount: 0,
1171
+ worldClassname: "worldspawn"
1172
+ },
1173
+ packetEntities: [],
1174
+ pmFlags: 0,
1175
+ pmType: 0,
1176
+ waterlevel: 0,
1177
+ watertype: 0,
1178
+ deltaAngles: { x: 0, y: 0, z: 0 },
1179
+ health: 100,
1180
+ armor: 0,
1181
+ ammo: 0,
1182
+ blend: [0, 0, 0, 0],
1183
+ damageAlpha: 0,
1184
+ damageIndicators: [],
1185
+ stats: [],
1186
+ kick_angles: { x: 0, y: 0, z: 0 },
1187
+ kick_origin: { x: 0, y: 0, z: 0 },
1188
+ gunoffset: { x: 0, y: 0, z: 0 },
1189
+ gunangles: { x: 0, y: 0, z: 0 },
1190
+ gunindex: 0,
1191
+ pm_time: 0,
1192
+ gun_frame: 0,
1193
+ rdflags: 0,
1194
+ fov: 90,
1195
+ renderfx: 0,
1196
+ pm_flags: 0,
1197
+ pm_type: 0,
1198
+ ...overrides
1199
+ });
1200
+
1201
+ // src/game/helpers.ts
1202
+ var import_vitest2 = require("vitest");
1203
+ var import_game = require("@quake2ts/game");
1204
+ var import_shared2 = require("@quake2ts/shared");
1205
+ var import_shared3 = require("@quake2ts/shared");
1206
+ var createMockEngine = () => ({
1207
+ sound: import_vitest2.vi.fn(),
1208
+ soundIndex: import_vitest2.vi.fn((sound) => 0),
1209
+ modelIndex: import_vitest2.vi.fn((model) => 0),
1210
+ centerprintf: import_vitest2.vi.fn()
1211
+ });
1212
+ var createMockGame = (seed = 12345) => {
1213
+ const spawnRegistry = new import_game.SpawnRegistry();
1214
+ const hooks = new import_game.ScriptHookRegistry();
1215
+ const game = {
1216
+ random: (0, import_shared2.createRandomGenerator)({ seed }),
1217
+ registerEntitySpawn: import_vitest2.vi.fn((classname, spawnFunc) => {
1218
+ spawnRegistry.register(classname, (entity) => spawnFunc(entity));
1114
1219
  }),
1115
- createOscillator: () => ({
1116
- connect: () => {
1117
- },
1118
- start: () => {
1119
- },
1120
- stop: () => {
1121
- },
1122
- frequency: { value: 440 }
1220
+ unregisterEntitySpawn: import_vitest2.vi.fn((classname) => {
1221
+ spawnRegistry.unregister(classname);
1123
1222
  }),
1124
- createBufferSource: () => ({
1125
- connect: () => {
1126
- },
1127
- start: () => {
1128
- },
1129
- stop: () => {
1130
- },
1131
- buffer: null,
1132
- playbackRate: { value: 1 },
1133
- loop: false
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");
1134
1228
  }),
1135
- destination: {},
1136
- currentTime: 0,
1137
- state: "running",
1138
- resume: async () => {
1139
- },
1140
- suspend: async () => {
1141
- },
1142
- close: async () => {
1143
- },
1144
- decodeAudioData: async (buffer) => ({
1145
- duration: 1,
1146
- length: 44100,
1147
- sampleRate: 44100,
1148
- numberOfChannels: 2,
1149
- getChannelData: () => new Float32Array(44100)
1229
+ clientBegin: import_vitest2.vi.fn((client) => {
1230
+ hooks.onPlayerSpawn({});
1150
1231
  }),
1151
- createBuffer: (channels, length, sampleRate) => ({
1152
- duration: length / sampleRate,
1153
- length,
1154
- sampleRate,
1155
- numberOfChannels: channels,
1156
- getChannelData: () => new Float32Array(length)
1232
+ damage: import_vitest2.vi.fn((amount) => {
1233
+ hooks.onDamage({}, null, null, amount, 0, 0);
1157
1234
  })
1158
1235
  };
1159
- }
1160
- function setupMockAudioContext() {
1161
- if (typeof global.AudioContext === "undefined" && typeof global.window !== "undefined") {
1162
- global.AudioContext = class {
1163
- constructor() {
1164
- return createMockAudioContext();
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);
1165
1265
  }
1166
- };
1167
- global.window.AudioContext = global.AudioContext;
1168
- global.window.webkitAudioContext = global.AudioContext;
1169
- }
1170
- }
1171
- function teardownMockAudioContext() {
1172
- if (global.AudioContext && global.AudioContext.toString().includes("class")) {
1173
- delete global.AudioContext;
1174
- delete global.window.AudioContext;
1175
- delete global.window.webkitAudioContext;
1176
- }
1177
- }
1178
- function captureAudioEvents(context) {
1179
- return [];
1180
- }
1181
-
1182
- // src/e2e/playwright.ts
1183
- var import_playwright = require("playwright");
1184
- async function createPlaywrightTestClient(options = {}) {
1185
- const browser = await import_playwright.chromium.launch({
1186
- headless: options.headless ?? true,
1187
- args: options.args || [
1188
- "--use-gl=egl",
1189
- "--ignore-gpu-blocklist",
1190
- "--no-sandbox",
1191
- "--disable-setuid-sandbox"
1192
- ]
1193
- });
1194
- const context = await browser.newContext({
1195
- viewport: options.viewport || { width: 1280, height: 720 },
1196
- recordVideo: options.recordVideo,
1197
- deviceScaleFactor: 1
1198
- });
1199
- const page = await context.newPage();
1200
- const waitForGame = async (timeout = 1e4) => {
1201
- try {
1202
- await page.waitForFunction(() => {
1203
- return window.game || document.querySelector("canvas");
1204
- }, null, { timeout });
1205
- } catch (e) {
1206
- throw new Error(`Game did not initialize within ${timeout}ms`);
1207
- }
1208
- };
1209
- const client = {
1210
- browser,
1211
- context,
1212
- page,
1213
- async navigate(url) {
1214
- await page.goto(url, { waitUntil: "domcontentloaded" });
1215
- },
1216
- async waitForGame(timeout) {
1217
- await waitForGame(timeout);
1218
- },
1219
- async injectInput(type, key) {
1220
- if (type === "keydown") {
1221
- await page.keyboard.down(key);
1222
- } else {
1223
- await page.keyboard.up(key);
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);
1224
1273
  }
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)
1225
1303
  },
1226
- async injectMouse(type, x = 0, y = 0, button = 0) {
1227
- if (type === "move") {
1228
- await page.mouse.move(x, y);
1229
- } else if (type === "down") {
1230
- await page.mouse.down({ button: button === 0 ? "left" : button === 2 ? "right" : "middle" });
1231
- } else if (type === "up") {
1232
- await page.mouse.up({ button: button === 0 ? "left" : button === 2 ? "right" : "middle" });
1233
- }
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
1234
1309
  },
1235
- async screenshot(path2) {
1236
- await page.screenshot({ path: path2 });
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
1237
1328
  },
1238
- async close() {
1239
- await browser.close();
1240
- }
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
1241
1337
  };
1242
- return client;
1243
- }
1244
- async function waitForGameReady(page, timeout = 1e4) {
1245
- await page.waitForFunction(() => {
1246
- return window.game || document.querySelector("canvas");
1247
- }, null, { timeout });
1248
- }
1249
- async function captureGameState(page) {
1250
- return await page.evaluate(() => {
1251
- const game = window.game;
1252
- if (!game) return {};
1253
- return {};
1254
- });
1255
- }
1256
-
1257
- // src/e2e/network.ts
1258
- var CONDITIONS = {
1259
- "good": {
1260
- offline: false,
1261
- downloadThroughput: 10 * 1024 * 1024,
1262
- // 10 Mbps
1263
- uploadThroughput: 5 * 1024 * 1024,
1264
- // 5 Mbps
1265
- latency: 20
1266
- },
1267
- "slow": {
1268
- offline: false,
1269
- downloadThroughput: 500 * 1024,
1270
- // 500 Kbps
1271
- uploadThroughput: 500 * 1024,
1272
- latency: 400
1273
- },
1274
- "unstable": {
1275
- offline: false,
1276
- downloadThroughput: 1 * 1024 * 1024,
1277
- uploadThroughput: 1 * 1024 * 1024,
1278
- latency: 100
1279
- // Jitter needs logic not simple preset
1280
- },
1281
- "offline": {
1282
- offline: true,
1283
- downloadThroughput: 0,
1284
- uploadThroughput: 0,
1285
- latency: 0
1286
- }
1287
- };
1288
- function simulateNetworkCondition(condition) {
1289
- const config = CONDITIONS[condition];
1290
- return createCustomNetworkCondition(config.latency, 0, 0, config);
1291
- }
1292
- function createCustomNetworkCondition(latency, jitter = 0, packetLoss = 0, baseConfig) {
1293
1338
  return {
1294
- async apply(page) {
1295
- const client = await page.context().newCDPSession(page);
1296
- await client.send("Network.enable");
1297
- await client.send("Network.emulateNetworkConditions", {
1298
- offline: baseConfig?.offline || false,
1299
- latency: latency + Math.random() * jitter,
1300
- // Basic jitter application on setup (static)
1301
- downloadThroughput: baseConfig?.downloadThroughput || -1,
1302
- uploadThroughput: baseConfig?.uploadThroughput || -1
1303
- });
1304
- },
1305
- async clear(page) {
1306
- const client = await page.context().newCDPSession(page);
1307
- await client.send("Network.emulateNetworkConditions", {
1308
- offline: false,
1309
- latency: 0,
1310
- downloadThroughput: -1,
1311
- uploadThroughput: -1
1312
- });
1313
- }
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()
1314
1350
  };
1315
1351
  }
1316
- async function throttleBandwidth(page, bytesPerSecond) {
1317
- const simulator = createCustomNetworkCondition(0, 0, 0, {
1318
- offline: false,
1319
- latency: 0,
1320
- downloadThroughput: bytesPerSecond,
1321
- uploadThroughput: bytesPerSecond
1322
- });
1323
- await simulator.apply(page);
1324
- }
1325
-
1326
- // src/e2e/visual.ts
1327
- var import_path = __toESM(require("path"), 1);
1328
- var import_promises = __toESM(require("fs/promises"), 1);
1329
- async function captureGameScreenshot(page, name, options = {}) {
1330
- const dir = options.dir || "__screenshots__";
1331
- const screenshotPath = import_path.default.join(dir, `${name}.png`);
1332
- await import_promises.default.mkdir(dir, { recursive: true });
1333
- return await page.screenshot({
1334
- path: screenshotPath,
1335
- fullPage: options.fullPage ?? false,
1336
- animations: "disabled",
1337
- // Try to freeze animations if possible
1338
- caret: "hide"
1339
- });
1340
- }
1341
- async function compareScreenshots(baseline, current, threshold = 0.1) {
1342
- if (baseline.equals(current)) {
1343
- return { pixelDiff: 0, matched: true };
1344
- }
1345
- return {
1346
- pixelDiff: -1,
1347
- // Unknown magnitude
1348
- matched: false
1349
- };
1352
+ function createSpawnContext() {
1353
+ return createTestContext();
1350
1354
  }
1351
- function createVisualTestScenario(page, sceneName) {
1352
- return {
1353
- async capture(snapshotName) {
1354
- return await captureGameScreenshot(page, `${sceneName}-${snapshotName}`);
1355
- },
1356
- async compare(snapshotName, baselineDir) {
1357
- const name = `${sceneName}-${snapshotName}`;
1358
- const current = await captureGameScreenshot(page, name, { dir: "__screenshots__/current" });
1359
- try {
1360
- const baselinePath = import_path.default.join(baselineDir, `${name}.png`);
1361
- const baseline = await import_promises.default.readFile(baselinePath);
1362
- return await compareScreenshots(baseline, current);
1363
- } catch (e) {
1364
- return { pixelDiff: -1, matched: false };
1365
- }
1366
- }
1367
- };
1355
+ function createEntity() {
1356
+ return new import_game.Entity(1);
1368
1357
  }
1369
1358
  // Annotate the CommonJS export names for ESM import in node:
1370
1359
  0 && (module.exports = {
@@ -1372,12 +1361,10 @@ function createVisualTestScenario(page, sceneName) {
1372
1361
  MockPointerLock,
1373
1362
  captureAudioEvents,
1374
1363
  captureCanvasDrawCalls,
1375
- captureGameScreenshot,
1376
1364
  captureGameState,
1377
- compareScreenshots,
1378
1365
  createBinaryStreamMock,
1379
1366
  createBinaryWriterMock,
1380
- createCustomNetworkCondition,
1367
+ createControlledTimer,
1381
1368
  createEntity,
1382
1369
  createEntityStateFactory,
1383
1370
  createGameStateSnapshotFactory,
@@ -1400,7 +1387,6 @@ function createVisualTestScenario(page, sceneName) {
1400
1387
  createSpawnContext,
1401
1388
  createStorageTestScenario,
1402
1389
  createTestContext,
1403
- createVisualTestScenario,
1404
1390
  intersects,
1405
1391
  ladderTrace,
1406
1392
  makeAxisBrush,
@@ -1415,11 +1401,9 @@ function createVisualTestScenario(page, sceneName) {
1415
1401
  setupNodeEnvironment,
1416
1402
  simulateFrames,
1417
1403
  simulateFramesWithMock,
1418
- simulateNetworkCondition,
1419
1404
  stairTrace,
1420
1405
  teardownBrowserEnvironment,
1421
1406
  teardownMockAudioContext,
1422
- throttleBandwidth,
1423
1407
  waitForGameReady
1424
1408
  });
1425
1409
  //# sourceMappingURL=index.cjs.map