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.
@@ -1,396 +1,3 @@
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
- });
81
-
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
- }
151
-
152
- // src/game/factories.ts
153
- var createPlayerStateFactory = (overrides) => ({
154
- pm_type: 0,
155
- pm_time: 0,
156
- pm_flags: 0,
157
- origin: { x: 0, y: 0, z: 0 },
158
- velocity: { x: 0, y: 0, z: 0 },
159
- viewAngles: { x: 0, y: 0, z: 0 },
160
- onGround: false,
161
- waterLevel: 0,
162
- watertype: 0,
163
- mins: { x: 0, y: 0, z: 0 },
164
- maxs: { x: 0, y: 0, z: 0 },
165
- damageAlpha: 0,
166
- damageIndicators: [],
167
- blend: [0, 0, 0, 0],
168
- stats: [],
169
- kick_angles: { x: 0, y: 0, z: 0 },
170
- kick_origin: { x: 0, y: 0, z: 0 },
171
- gunoffset: { x: 0, y: 0, z: 0 },
172
- gunangles: { x: 0, y: 0, z: 0 },
173
- gunindex: 0,
174
- gun_frame: 0,
175
- rdflags: 0,
176
- fov: 90,
177
- renderfx: 0,
178
- ...overrides
179
- });
180
- var createEntityStateFactory = (overrides) => ({
181
- number: 0,
182
- origin: { x: 0, y: 0, z: 0 },
183
- angles: { x: 0, y: 0, z: 0 },
184
- oldOrigin: { x: 0, y: 0, z: 0 },
185
- modelIndex: 0,
186
- modelIndex2: 0,
187
- modelIndex3: 0,
188
- modelIndex4: 0,
189
- frame: 0,
190
- skinNum: 0,
191
- effects: 0,
192
- renderfx: 0,
193
- solid: 0,
194
- sound: 0,
195
- event: 0,
196
- ...overrides
197
- });
198
- var createGameStateSnapshotFactory = (overrides) => ({
199
- gravity: { x: 0, y: 0, z: -800 },
200
- origin: { x: 0, y: 0, z: 0 },
201
- velocity: { x: 0, y: 0, z: 0 },
202
- viewangles: { x: 0, y: 0, z: 0 },
203
- level: { timeSeconds: 0, frameNumber: 0, previousTimeSeconds: 0, deltaSeconds: 0.1 },
204
- entities: {
205
- activeCount: 0,
206
- worldClassname: "worldspawn"
207
- },
208
- packetEntities: [],
209
- pmFlags: 0,
210
- pmType: 0,
211
- waterlevel: 0,
212
- watertype: 0,
213
- deltaAngles: { x: 0, y: 0, z: 0 },
214
- health: 100,
215
- armor: 0,
216
- ammo: 0,
217
- blend: [0, 0, 0, 0],
218
- damageAlpha: 0,
219
- damageIndicators: [],
220
- stats: [],
221
- kick_angles: { x: 0, y: 0, z: 0 },
222
- kick_origin: { x: 0, y: 0, z: 0 },
223
- gunoffset: { x: 0, y: 0, z: 0 },
224
- gunangles: { x: 0, y: 0, z: 0 },
225
- gunindex: 0,
226
- pm_time: 0,
227
- gun_frame: 0,
228
- rdflags: 0,
229
- fov: 90,
230
- renderfx: 0,
231
- pm_flags: 0,
232
- pm_type: 0,
233
- ...overrides
234
- });
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)
338
- },
339
- level: {
340
- intermission_angle: { x: 0, y: 0, z: 0 },
341
- intermission_origin: { x: 0, y: 0, z: 0 },
342
- next_auto_save: 0,
343
- health_bar_entities: null
344
- },
345
- targetNameIndex: /* @__PURE__ */ new Map(),
346
- forEachEntity: vi2.fn((callback) => {
347
- entityList.forEach(callback);
348
- }),
349
- find: vi2.fn((predicate) => {
350
- return entityList.find(predicate);
351
- }),
352
- findByClassname: vi2.fn((classname) => {
353
- return entityList.find((e) => e.classname === classname);
354
- }),
355
- beginFrame: vi2.fn((timeSeconds) => {
356
- entities.timeSeconds = timeSeconds;
357
- }),
358
- targetAwareness: {
359
- timeSeconds: 10,
360
- frameNumber: 1,
361
- sightEntity: null,
362
- soundEntity: null
363
- },
364
- // Adding missing properties to satisfy EntitySystem interface partially or fully
365
- // We cast to unknown first anyway, but filling these in makes it safer for consumers
366
- skill: 1,
367
- deathmatch: false,
368
- coop: false,
369
- activeCount: entityList.length,
370
- world: entityList.find((e) => e.classname === "worldspawn") || new Entity(0)
371
- // ... other EntitySystem properties would go here
372
- };
373
- return {
374
- keyValues: {},
375
- entities,
376
- game,
377
- engine,
378
- health_multiplier: 1,
379
- warn: vi2.fn(),
380
- free: vi2.fn(),
381
- // Mock precache functions if they are part of SpawnContext in future or TestContext extensions
382
- precacheModel: vi2.fn(),
383
- precacheSound: vi2.fn(),
384
- precacheImage: vi2.fn()
385
- };
386
- }
387
- function createSpawnContext() {
388
- return createTestContext();
389
- }
390
- function createEntity() {
391
- return new Entity(1);
392
- }
393
-
394
1
  // src/setup/browser.ts
395
2
  import { JSDOM } from "jsdom";
396
3
  import { Canvas, Image, ImageData } from "@napi-rs/canvas";
@@ -786,12 +393,6 @@ function teardownBrowserEnvironment() {
786
393
  delete global.UIEvent;
787
394
  }
788
395
 
789
- // src/setup/node.ts
790
- function setupNodeEnvironment() {
791
- if (typeof global.fetch === "undefined") {
792
- }
793
- }
794
-
795
396
  // src/setup/canvas.ts
796
397
  import { Canvas as Canvas2, Image as Image2, ImageData as ImageData2 } from "@napi-rs/canvas";
797
398
  function createMockCanvas(width = 300, height = 150) {
@@ -873,7 +474,127 @@ function createMockImage(width, height, src) {
873
474
  return img;
874
475
  }
875
476
 
477
+ // src/setup/node.ts
478
+ function setupNodeEnvironment() {
479
+ if (typeof global.fetch === "undefined") {
480
+ }
481
+ }
482
+
483
+ // src/setup/storage.ts
484
+ import "fake-indexeddb/auto";
485
+ function createMockLocalStorage(initialData = {}) {
486
+ const storage = new Map(Object.entries(initialData));
487
+ return {
488
+ getItem: (key) => storage.get(key) || null,
489
+ setItem: (key, value) => storage.set(key, value),
490
+ removeItem: (key) => storage.delete(key),
491
+ clear: () => storage.clear(),
492
+ key: (index) => Array.from(storage.keys())[index] || null,
493
+ get length() {
494
+ return storage.size;
495
+ }
496
+ };
497
+ }
498
+ function createMockSessionStorage(initialData = {}) {
499
+ return createMockLocalStorage(initialData);
500
+ }
501
+ function createMockIndexedDB() {
502
+ if (typeof indexedDB === "undefined") {
503
+ throw new Error("IndexedDB mock not found. Ensure fake-indexeddb is loaded.");
504
+ }
505
+ return indexedDB;
506
+ }
507
+ function createStorageTestScenario(storageType = "local") {
508
+ const storage = storageType === "local" ? createMockLocalStorage() : createMockSessionStorage();
509
+ return {
510
+ storage,
511
+ populate(data) {
512
+ Object.entries(data).forEach(([k, v]) => storage.setItem(k, v));
513
+ },
514
+ verify(key, value) {
515
+ return storage.getItem(key) === value;
516
+ }
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 () => {
553
+ },
554
+ suspend: async () => {
555
+ },
556
+ close: async () => {
557
+ },
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
+
876
596
  // src/setup/timing.ts
597
+ var activeMockRAF;
877
598
  function createMockRAF() {
878
599
  let callbacks = [];
879
600
  let nextId = 1;
@@ -913,10 +634,14 @@ function createMockRAF() {
913
634
  currentTime = 0;
914
635
  },
915
636
  enable() {
637
+ activeMockRAF = this;
916
638
  global.requestAnimationFrame = raf;
917
639
  global.cancelAnimationFrame = cancel;
918
640
  },
919
641
  disable() {
642
+ if (activeMockRAF === this) {
643
+ activeMockRAF = void 0;
644
+ }
920
645
  if (originalRAF) {
921
646
  global.requestAnimationFrame = originalRAF;
922
647
  } else {
@@ -939,7 +664,6 @@ function createMockPerformance(startTime = 0) {
939
664
  timing: {
940
665
  navigationStart: startTime
941
666
  },
942
- // Add minimal navigation/resource timing interfaces to satisfy types if needed
943
667
  clearMarks: () => {
944
668
  },
945
669
  clearMeasures: () => {
@@ -968,330 +692,598 @@ function createMockPerformance(startTime = 0) {
968
692
  mockPerf.setTime = (time) => {
969
693
  currentTime = time;
970
694
  };
971
- return mockPerf;
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);
728
+ },
729
+ advanceBy(ms) {
730
+ const targetTime = currentTime + ms;
731
+ while (true) {
732
+ let earliest = null;
733
+ for (const t of timers) {
734
+ if (!earliest || t.dueTime < earliest.dueTime) {
735
+ earliest = t;
736
+ }
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;
752
+ },
753
+ clear() {
754
+ timers = [];
755
+ },
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);
860
+ },
861
+ injectInput: async (type, data) => {
862
+ await page.evaluate(({ type: type2, data: data2 }) => {
863
+ if (window.injectGameInput) window.injectGameInput(type2, data2);
864
+ }, { type, data });
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
+ };
972
898
  }
973
- function simulateFrames(count, frameTimeMs = 16.6, callback) {
974
- const raf = global.requestAnimationFrame;
975
- if (!raf) return;
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
+ };
976
913
  }
977
- function simulateFramesWithMock(mock, count, frameTimeMs = 16.6, callback) {
978
- for (let i = 0; i < count; i++) {
979
- if (callback) callback(i);
980
- mock.advance(frameTimeMs);
981
- }
914
+ function makeNode(plane, children) {
915
+ return { plane, children };
982
916
  }
983
-
984
- // src/setup/storage.ts
985
- import "fake-indexeddb/auto";
986
- function createMockLocalStorage(initialData = {}) {
987
- const storage = new Map(Object.entries(initialData));
917
+ function makeBspModel(planes, nodes, leaves, brushes, leafBrushes) {
988
918
  return {
989
- getItem: (key) => storage.get(key) || null,
990
- setItem: (key, value) => storage.set(key, value),
991
- removeItem: (key) => storage.delete(key),
992
- clear: () => storage.clear(),
993
- key: (index) => Array.from(storage.keys())[index] || null,
994
- get length() {
995
- return storage.size;
996
- }
919
+ planes,
920
+ nodes,
921
+ leaves,
922
+ brushes,
923
+ leafBrushes,
924
+ bmodels: []
997
925
  };
998
926
  }
999
- function createMockSessionStorage(initialData = {}) {
1000
- return createMockLocalStorage(initialData);
927
+ function makeLeaf(contents, firstLeafBrush, numLeafBrushes) {
928
+ return { contents, cluster: 0, area: 0, firstLeafBrush, numLeafBrushes };
1001
929
  }
1002
- function createMockIndexedDB() {
1003
- if (typeof indexedDB === "undefined") {
1004
- throw new Error("IndexedDB mock not found. Ensure fake-indexeddb is loaded.");
1005
- }
1006
- return indexedDB;
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
+ };
1007
940
  }
1008
- function createStorageTestScenario(storageType = "local") {
1009
- const storage = storageType === "local" ? createMockLocalStorage() : createMockSessionStorage();
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
+ ];
1010
950
  return {
1011
- storage,
1012
- populate(data) {
1013
- Object.entries(data).forEach(([k, v]) => storage.setItem(k, v));
1014
- },
1015
- verify(key, value) {
1016
- return storage.getItem(key) === value;
1017
- }
951
+ contents,
952
+ sides: planes.map((plane) => ({ plane, surfaceFlags: 0 }))
1018
953
  };
1019
954
  }
1020
955
 
1021
- // src/setup/audio.ts
1022
- function createMockAudioContext() {
1023
- return {
1024
- createGain: () => ({
1025
- connect: () => {
1026
- },
1027
- gain: { value: 1, setValueAtTime: () => {
1028
- } }
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));
1029
1139
  }),
1030
- createOscillator: () => ({
1031
- connect: () => {
1032
- },
1033
- start: () => {
1034
- },
1035
- stop: () => {
1036
- },
1037
- frequency: { value: 440 }
1140
+ unregisterEntitySpawn: vi2.fn((classname) => {
1141
+ spawnRegistry.unregister(classname);
1038
1142
  }),
1039
- createBufferSource: () => ({
1040
- connect: () => {
1041
- },
1042
- start: () => {
1043
- },
1044
- stop: () => {
1045
- },
1046
- buffer: null,
1047
- playbackRate: { value: 1 },
1048
- loop: false
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");
1049
1148
  }),
1050
- destination: {},
1051
- currentTime: 0,
1052
- state: "running",
1053
- resume: async () => {
1054
- },
1055
- suspend: async () => {
1056
- },
1057
- close: async () => {
1058
- },
1059
- decodeAudioData: async (buffer) => ({
1060
- duration: 1,
1061
- length: 44100,
1062
- sampleRate: 44100,
1063
- numberOfChannels: 2,
1064
- getChannelData: () => new Float32Array(44100)
1149
+ clientBegin: vi2.fn((client) => {
1150
+ hooks.onPlayerSpawn({});
1065
1151
  }),
1066
- createBuffer: (channels, length, sampleRate) => ({
1067
- duration: length / sampleRate,
1068
- length,
1069
- sampleRate,
1070
- numberOfChannels: channels,
1071
- getChannelData: () => new Float32Array(length)
1152
+ damage: vi2.fn((amount) => {
1153
+ hooks.onDamage({}, null, null, amount, 0, 0);
1072
1154
  })
1073
1155
  };
1074
- }
1075
- function setupMockAudioContext() {
1076
- if (typeof global.AudioContext === "undefined" && typeof global.window !== "undefined") {
1077
- global.AudioContext = class {
1078
- constructor() {
1079
- return createMockAudioContext();
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);
1080
1185
  }
1081
- };
1082
- global.window.AudioContext = global.AudioContext;
1083
- global.window.webkitAudioContext = global.AudioContext;
1084
- }
1085
- }
1086
- function teardownMockAudioContext() {
1087
- if (global.AudioContext && global.AudioContext.toString().includes("class")) {
1088
- delete global.AudioContext;
1089
- delete global.window.AudioContext;
1090
- delete global.window.webkitAudioContext;
1091
- }
1092
- }
1093
- function captureAudioEvents(context) {
1094
- return [];
1095
- }
1096
-
1097
- // src/e2e/playwright.ts
1098
- import { chromium } from "playwright";
1099
- async function createPlaywrightTestClient(options = {}) {
1100
- const browser = await chromium.launch({
1101
- headless: options.headless ?? true,
1102
- args: options.args || [
1103
- "--use-gl=egl",
1104
- "--ignore-gpu-blocklist",
1105
- "--no-sandbox",
1106
- "--disable-setuid-sandbox"
1107
- ]
1108
- });
1109
- const context = await browser.newContext({
1110
- viewport: options.viewport || { width: 1280, height: 720 },
1111
- recordVideo: options.recordVideo,
1112
- deviceScaleFactor: 1
1113
- });
1114
- const page = await context.newPage();
1115
- const waitForGame = async (timeout = 1e4) => {
1116
- try {
1117
- await page.waitForFunction(() => {
1118
- return window.game || document.querySelector("canvas");
1119
- }, null, { timeout });
1120
- } catch (e) {
1121
- throw new Error(`Game did not initialize within ${timeout}ms`);
1122
- }
1123
- };
1124
- const client = {
1125
- browser,
1126
- context,
1127
- page,
1128
- async navigate(url) {
1129
- await page.goto(url, { waitUntil: "domcontentloaded" });
1130
- },
1131
- async waitForGame(timeout) {
1132
- await waitForGame(timeout);
1133
- },
1134
- async injectInput(type, key) {
1135
- if (type === "keydown") {
1136
- await page.keyboard.down(key);
1137
- } else {
1138
- await page.keyboard.up(key);
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);
1139
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)
1140
1223
  },
1141
- async injectMouse(type, x = 0, y = 0, button = 0) {
1142
- if (type === "move") {
1143
- await page.mouse.move(x, y);
1144
- } else if (type === "down") {
1145
- await page.mouse.down({ button: button === 0 ? "left" : button === 2 ? "right" : "middle" });
1146
- } else if (type === "up") {
1147
- await page.mouse.up({ button: button === 0 ? "left" : button === 2 ? "right" : "middle" });
1148
- }
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
1149
1229
  },
1150
- async screenshot(path2) {
1151
- await page.screenshot({ path: path2 });
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
1152
1248
  },
1153
- async close() {
1154
- await browser.close();
1155
- }
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
1156
1257
  };
1157
- return client;
1158
- }
1159
- async function waitForGameReady(page, timeout = 1e4) {
1160
- await page.waitForFunction(() => {
1161
- return window.game || document.querySelector("canvas");
1162
- }, null, { timeout });
1163
- }
1164
- async function captureGameState(page) {
1165
- return await page.evaluate(() => {
1166
- const game = window.game;
1167
- if (!game) return {};
1168
- return {};
1169
- });
1170
- }
1171
-
1172
- // src/e2e/network.ts
1173
- var CONDITIONS = {
1174
- "good": {
1175
- offline: false,
1176
- downloadThroughput: 10 * 1024 * 1024,
1177
- // 10 Mbps
1178
- uploadThroughput: 5 * 1024 * 1024,
1179
- // 5 Mbps
1180
- latency: 20
1181
- },
1182
- "slow": {
1183
- offline: false,
1184
- downloadThroughput: 500 * 1024,
1185
- // 500 Kbps
1186
- uploadThroughput: 500 * 1024,
1187
- latency: 400
1188
- },
1189
- "unstable": {
1190
- offline: false,
1191
- downloadThroughput: 1 * 1024 * 1024,
1192
- uploadThroughput: 1 * 1024 * 1024,
1193
- latency: 100
1194
- // Jitter needs logic not simple preset
1195
- },
1196
- "offline": {
1197
- offline: true,
1198
- downloadThroughput: 0,
1199
- uploadThroughput: 0,
1200
- latency: 0
1201
- }
1202
- };
1203
- function simulateNetworkCondition(condition) {
1204
- const config = CONDITIONS[condition];
1205
- return createCustomNetworkCondition(config.latency, 0, 0, config);
1206
- }
1207
- function createCustomNetworkCondition(latency, jitter = 0, packetLoss = 0, baseConfig) {
1208
1258
  return {
1209
- async apply(page) {
1210
- const client = await page.context().newCDPSession(page);
1211
- await client.send("Network.enable");
1212
- await client.send("Network.emulateNetworkConditions", {
1213
- offline: baseConfig?.offline || false,
1214
- latency: latency + Math.random() * jitter,
1215
- // Basic jitter application on setup (static)
1216
- downloadThroughput: baseConfig?.downloadThroughput || -1,
1217
- uploadThroughput: baseConfig?.uploadThroughput || -1
1218
- });
1219
- },
1220
- async clear(page) {
1221
- const client = await page.context().newCDPSession(page);
1222
- await client.send("Network.emulateNetworkConditions", {
1223
- offline: false,
1224
- latency: 0,
1225
- downloadThroughput: -1,
1226
- uploadThroughput: -1
1227
- });
1228
- }
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()
1229
1270
  };
1230
1271
  }
1231
- async function throttleBandwidth(page, bytesPerSecond) {
1232
- const simulator = createCustomNetworkCondition(0, 0, 0, {
1233
- offline: false,
1234
- latency: 0,
1235
- downloadThroughput: bytesPerSecond,
1236
- uploadThroughput: bytesPerSecond
1237
- });
1238
- await simulator.apply(page);
1239
- }
1240
-
1241
- // src/e2e/visual.ts
1242
- import path from "path";
1243
- import fs from "fs/promises";
1244
- async function captureGameScreenshot(page, name, options = {}) {
1245
- const dir = options.dir || "__screenshots__";
1246
- const screenshotPath = path.join(dir, `${name}.png`);
1247
- await fs.mkdir(dir, { recursive: true });
1248
- return await page.screenshot({
1249
- path: screenshotPath,
1250
- fullPage: options.fullPage ?? false,
1251
- animations: "disabled",
1252
- // Try to freeze animations if possible
1253
- caret: "hide"
1254
- });
1255
- }
1256
- async function compareScreenshots(baseline, current, threshold = 0.1) {
1257
- if (baseline.equals(current)) {
1258
- return { pixelDiff: 0, matched: true };
1259
- }
1260
- return {
1261
- pixelDiff: -1,
1262
- // Unknown magnitude
1263
- matched: false
1264
- };
1272
+ function createSpawnContext() {
1273
+ return createTestContext();
1265
1274
  }
1266
- function createVisualTestScenario(page, sceneName) {
1267
- return {
1268
- async capture(snapshotName) {
1269
- return await captureGameScreenshot(page, `${sceneName}-${snapshotName}`);
1270
- },
1271
- async compare(snapshotName, baselineDir) {
1272
- const name = `${sceneName}-${snapshotName}`;
1273
- const current = await captureGameScreenshot(page, name, { dir: "__screenshots__/current" });
1274
- try {
1275
- const baselinePath = path.join(baselineDir, `${name}.png`);
1276
- const baseline = await fs.readFile(baselinePath);
1277
- return await compareScreenshots(baseline, current);
1278
- } catch (e) {
1279
- return { pixelDiff: -1, matched: false };
1280
- }
1281
- }
1282
- };
1275
+ function createEntity() {
1276
+ return new Entity(1);
1283
1277
  }
1284
1278
  export {
1285
1279
  InputInjector,
1286
1280
  MockPointerLock,
1287
1281
  captureAudioEvents,
1288
1282
  captureCanvasDrawCalls,
1289
- captureGameScreenshot,
1290
1283
  captureGameState,
1291
- compareScreenshots,
1292
1284
  createBinaryStreamMock,
1293
1285
  createBinaryWriterMock,
1294
- createCustomNetworkCondition,
1286
+ createControlledTimer,
1295
1287
  createEntity,
1296
1288
  createEntityStateFactory,
1297
1289
  createGameStateSnapshotFactory,
@@ -1314,7 +1306,6 @@ export {
1314
1306
  createSpawnContext,
1315
1307
  createStorageTestScenario,
1316
1308
  createTestContext,
1317
- createVisualTestScenario,
1318
1309
  intersects,
1319
1310
  ladderTrace,
1320
1311
  makeAxisBrush,
@@ -1329,11 +1320,9 @@ export {
1329
1320
  setupNodeEnvironment,
1330
1321
  simulateFrames,
1331
1322
  simulateFramesWithMock,
1332
- simulateNetworkCondition,
1333
1323
  stairTrace,
1334
1324
  teardownBrowserEnvironment,
1335
1325
  teardownMockAudioContext,
1336
- throttleBandwidth,
1337
1326
  waitForGameReady
1338
1327
  };
1339
1328
  //# sourceMappingURL=index.js.map