@quake2ts/test-utils 0.0.1

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
Files changed (65) hide show
  1. package/README.md +454 -0
  2. package/dist/index.cjs +5432 -0
  3. package/dist/index.cjs.map +1 -0
  4. package/dist/index.d.cts +2150 -0
  5. package/dist/index.d.ts +2150 -0
  6. package/dist/index.js +5165 -0
  7. package/dist/index.js.map +1 -0
  8. package/package.json +82 -0
  9. package/src/client/helpers/hud.ts +114 -0
  10. package/src/client/helpers/prediction.ts +136 -0
  11. package/src/client/helpers/view.ts +201 -0
  12. package/src/client/mocks/console.ts +75 -0
  13. package/src/client/mocks/download.ts +48 -0
  14. package/src/client/mocks/input.ts +246 -0
  15. package/src/client/mocks/network.ts +148 -0
  16. package/src/client/mocks/state.ts +148 -0
  17. package/src/e2e/network.ts +47 -0
  18. package/src/e2e/playwright.ts +90 -0
  19. package/src/e2e/visual.ts +172 -0
  20. package/src/engine/helpers/pipeline-test-template.ts +113 -0
  21. package/src/engine/helpers/webgpu-rendering.ts +251 -0
  22. package/src/engine/mocks/assets.ts +129 -0
  23. package/src/engine/mocks/audio.ts +152 -0
  24. package/src/engine/mocks/buffers.ts +88 -0
  25. package/src/engine/mocks/lighting.ts +64 -0
  26. package/src/engine/mocks/particles.ts +76 -0
  27. package/src/engine/mocks/renderer.ts +218 -0
  28. package/src/engine/mocks/webgl.ts +267 -0
  29. package/src/engine/mocks/webgpu.ts +262 -0
  30. package/src/engine/rendering.ts +103 -0
  31. package/src/game/factories.ts +204 -0
  32. package/src/game/helpers/physics.ts +171 -0
  33. package/src/game/helpers/save.ts +232 -0
  34. package/src/game/helpers.ts +310 -0
  35. package/src/game/mocks/ai.ts +67 -0
  36. package/src/game/mocks/combat.ts +61 -0
  37. package/src/game/mocks/items.ts +166 -0
  38. package/src/game/mocks.ts +105 -0
  39. package/src/index.ts +93 -0
  40. package/src/server/helpers/bandwidth.ts +127 -0
  41. package/src/server/helpers/multiplayer.ts +158 -0
  42. package/src/server/helpers/snapshot.ts +241 -0
  43. package/src/server/mockNetDriver.ts +106 -0
  44. package/src/server/mockTransport.ts +50 -0
  45. package/src/server/mocks/commands.ts +93 -0
  46. package/src/server/mocks/connection.ts +139 -0
  47. package/src/server/mocks/master.ts +97 -0
  48. package/src/server/mocks/physics.ts +32 -0
  49. package/src/server/mocks/state.ts +162 -0
  50. package/src/server/mocks/transport.ts +161 -0
  51. package/src/setup/audio.ts +118 -0
  52. package/src/setup/browser.ts +249 -0
  53. package/src/setup/canvas.ts +142 -0
  54. package/src/setup/node.ts +21 -0
  55. package/src/setup/storage.ts +60 -0
  56. package/src/setup/timing.ts +142 -0
  57. package/src/setup/webgl.ts +8 -0
  58. package/src/setup/webgpu.ts +113 -0
  59. package/src/shared/bsp.ts +145 -0
  60. package/src/shared/collision.ts +64 -0
  61. package/src/shared/factories.ts +88 -0
  62. package/src/shared/math.ts +65 -0
  63. package/src/shared/mocks.ts +243 -0
  64. package/src/shared/pak-loader.ts +45 -0
  65. package/src/visual/snapshots.ts +292 -0
package/package.json ADDED
@@ -0,0 +1,82 @@
1
+ {
2
+ "name": "@quake2ts/test-utils",
3
+ "version": "0.0.1",
4
+ "type": "module",
5
+ "main": "./dist/index.cjs",
6
+ "module": "./dist/index.js",
7
+ "types": "./dist/index.d.ts",
8
+ "exports": {
9
+ ".": {
10
+ "types": "./dist/index.d.ts",
11
+ "import": "./dist/index.js",
12
+ "require": "./dist/index.cjs"
13
+ },
14
+ "./src/engine/mocks/webgpu": {
15
+ "types": "./src/engine/mocks/webgpu.ts",
16
+ "import": "./src/engine/mocks/webgpu.ts",
17
+ "require": "./src/engine/mocks/webgpu.ts"
18
+ },
19
+ "./src/setup/webgpu": {
20
+ "types": "./src/setup/webgpu.ts",
21
+ "import": "./src/setup/webgpu.ts",
22
+ "require": "./src/setup/webgpu.ts"
23
+ }
24
+ },
25
+ "files": [
26
+ "dist",
27
+ "src"
28
+ ],
29
+ "dependencies": {
30
+ "gl-matrix": "^3.4.4",
31
+ "@quake2ts/game": "0.0.1",
32
+ "@quake2ts/server": "0.0.1",
33
+ "@quake2ts/engine": "^0.0.1",
34
+ "@quake2ts/shared": "0.0.1"
35
+ },
36
+ "peerDependencies": {
37
+ "@webgpu/types": "^0.1.68",
38
+ "pixelmatch": "^7.1.0",
39
+ "playwright": "^1.57.0",
40
+ "pngjs": "^7.0.0",
41
+ "vitest": "^1.6.0",
42
+ "webgpu": "^0.3.8"
43
+ },
44
+ "peerDependenciesMeta": {
45
+ "@webgpu/types": {
46
+ "optional": true
47
+ },
48
+ "pixelmatch": {
49
+ "optional": true
50
+ },
51
+ "playwright": {
52
+ "optional": true
53
+ },
54
+ "pngjs": {
55
+ "optional": true
56
+ },
57
+ "webgpu": {
58
+ "optional": true
59
+ }
60
+ },
61
+ "devDependencies": {
62
+ "@napi-rs/canvas": "^0.1.84",
63
+ "@types/jsdom": "^27.0.0",
64
+ "@types/pixelmatch": "^5.2.6",
65
+ "@types/pngjs": "^6.0.5",
66
+ "@webgpu/types": "^0.1.68",
67
+ "fake-indexeddb": "^6.0.0",
68
+ "jsdom": "^27.3.0",
69
+ "pixelmatch": "^7.1.0",
70
+ "playwright": "^1.57.0",
71
+ "pngjs": "^7.0.0",
72
+ "tsup": "^8.1.0",
73
+ "typescript": "^5.4.5",
74
+ "vitest": "^1.6.0",
75
+ "webgpu": "^0.3.8"
76
+ },
77
+ "scripts": {
78
+ "build": "tsup src/index.ts --format esm,cjs --dts",
79
+ "test": "vitest run --passWithNoTests",
80
+ "test:webgpu": "cross-env TEST_TYPE=webgpu vitest run"
81
+ }
82
+ }
@@ -0,0 +1,114 @@
1
+ import { vi } from 'vitest';
2
+ import { PlayerState } from '@quake2ts/shared';
3
+ import { PlayerClient, PowerupId, KeyId } from '@quake2ts/game';
4
+ import { FrameRenderStats } from '@quake2ts/engine';
5
+
6
+ // Since test-utils cannot import from client, we need to mock MessageSystem here or define an interface
7
+ // The Draw_Hud function expects a MessageSystem which has drawCenterPrint and drawNotifications.
8
+ // However, the signature in `hud.ts` uses the class.
9
+ // For testing purposes, we might need a mock MessageSystem interface in test-utils or import it if possible.
10
+ // But we can't import from client. So we will return a mock object that satisfies the interface.
11
+
12
+ export interface HudState {
13
+ ps: PlayerState;
14
+ client: PlayerClient;
15
+ health: number;
16
+ armor: number;
17
+ ammo: number;
18
+ stats: number[]; // Game stats (health, armor, ammo indices)
19
+ pickupIcon?: string;
20
+ damageIndicators?: any[];
21
+ renderStats?: FrameRenderStats; // Optional render stats for verification
22
+ timeMs: number;
23
+ messages: any; // Using any for now to avoid circular dependency
24
+ }
25
+
26
+ export function createMockHudState(overrides?: Partial<HudState>): HudState {
27
+ const defaultPs = {
28
+ damageAlpha: 0,
29
+ damageIndicators: [],
30
+ origin: { x: 0, y: 0, z: 0 },
31
+ velocity: { x: 0, y: 0, z: 0 },
32
+ viewAngles: { x: 0, y: 0, z: 0 },
33
+ onGround: true,
34
+ waterLevel: 0,
35
+ mins: { x: 0, y: 0, z: 0 },
36
+ maxs: { x: 0, y: 0, z: 0 },
37
+ centerPrint: null,
38
+ notify: null
39
+ } as unknown as PlayerState;
40
+
41
+ const defaultClient = {
42
+ inventory: {
43
+ armor: { armorCount: 50, armorType: 'jacket' },
44
+ currentWeapon: 1, // Blaster usually
45
+ ammo: { counts: [] },
46
+ keys: new Set<KeyId>(),
47
+ powerups: new Map<PowerupId, number>()
48
+ }
49
+ } as unknown as PlayerClient;
50
+
51
+ // Use a numeric array for game stats (EntityState.stats)
52
+ // Indexes: 1=Health, 2=Ammo, 4=Armor (based on client/src/index.ts usage)
53
+ const defaultStats = new Array(32).fill(0);
54
+ defaultStats[1] = 100; // Health
55
+ defaultStats[2] = 25; // Ammo
56
+ defaultStats[4] = 50; // Armor
57
+
58
+ const defaultMessages = {
59
+ drawCenterPrint: vi.fn(),
60
+ drawNotifications: vi.fn(),
61
+ addCenterPrint: vi.fn(),
62
+ addNotification: vi.fn(),
63
+ clear: vi.fn()
64
+ };
65
+
66
+ return {
67
+ ps: overrides?.ps ?? defaultPs,
68
+ client: overrides?.client ?? defaultClient,
69
+ health: overrides?.health ?? 100,
70
+ armor: overrides?.armor ?? 50,
71
+ ammo: overrides?.ammo ?? 25,
72
+ stats: overrides?.stats ?? defaultStats,
73
+ pickupIcon: overrides?.pickupIcon,
74
+ damageIndicators: overrides?.damageIndicators,
75
+ renderStats: overrides?.renderStats,
76
+ timeMs: overrides?.timeMs ?? 1000,
77
+ messages: overrides?.messages ?? defaultMessages
78
+ };
79
+ }
80
+
81
+ export function createMockScoreboard(players: any[] = []): any {
82
+ return {
83
+ players: players,
84
+ draw: vi.fn()
85
+ };
86
+ }
87
+
88
+ export interface MockChatMessage {
89
+ text: string;
90
+ sender?: string;
91
+ timestamp?: number;
92
+ }
93
+
94
+ export function createMockChatMessage(text: string, sender?: string, timestamp: number = Date.now()): MockChatMessage {
95
+ return {
96
+ text,
97
+ sender,
98
+ timestamp
99
+ };
100
+ }
101
+
102
+ export interface MockNotification {
103
+ type: string;
104
+ message: string;
105
+ duration?: number;
106
+ }
107
+
108
+ export function createMockNotification(type: string, message: string, duration: number = 3000): MockNotification {
109
+ return {
110
+ type,
111
+ message,
112
+ duration
113
+ };
114
+ }
@@ -0,0 +1,136 @@
1
+ import { EntityState } from '@quake2ts/shared';
2
+ import { ClientState, createMockClientState } from '../mocks/state.js';
3
+ import { UserCommand } from '@quake2ts/shared';
4
+
5
+ // -- Interfaces --
6
+
7
+ export interface PredictionScenario {
8
+ clientState: ClientState;
9
+ snapshots: EntityState[][];
10
+ lagMs: number;
11
+ }
12
+
13
+ export interface SmoothingAnalysis {
14
+ smooth: boolean;
15
+ maxError: number;
16
+ averageError: number;
17
+ jumps: number[];
18
+ }
19
+
20
+ // -- Helpers --
21
+
22
+ export const createPredictionTestScenario = (lagMs: number = 100): PredictionScenario => {
23
+ // Create a base client state using the factory
24
+ const clientState = createMockClientState({
25
+ playerNum: 0,
26
+ serverTime: 1000,
27
+ getClientName: (num: number) => 'TestPlayer'
28
+ });
29
+
30
+ // Create some snapshot history
31
+ const snapshots: EntityState[][] = [];
32
+ for (let i = 0; i < 5; i++) {
33
+ const frameEntities: EntityState[] = [
34
+ {
35
+ number: 1,
36
+ origin: { x: i * 10, y: 0, z: 0 },
37
+ angles: { x: 0, y: 0, z: 0 },
38
+ oldOrigin: { x: (i - 1) * 10, y: 0, z: 0 },
39
+ modelIndex: 0,
40
+ modelIndex2: 0,
41
+ modelIndex3: 0,
42
+ modelIndex4: 0,
43
+ frame: 0,
44
+ skinNum: 0,
45
+ effects: 0,
46
+ renderfx: 0,
47
+ solid: 0,
48
+ sound: 0,
49
+ event: 0
50
+ }
51
+ ];
52
+ snapshots.push(frameEntities);
53
+ }
54
+
55
+ return {
56
+ clientState,
57
+ snapshots,
58
+ lagMs
59
+ };
60
+ };
61
+
62
+ export const simulateClientPrediction = (
63
+ state: ClientState,
64
+ input: UserCommand,
65
+ deltaTime: number
66
+ ): ClientState => {
67
+ // This is a stub for simulating prediction
68
+ // Real implementation would invoke PMove logic
69
+ // For testing helpers, we might just update time
70
+ return {
71
+ ...state,
72
+ serverTime: state.serverTime + deltaTime * 1000
73
+ };
74
+ };
75
+
76
+ export const createInterpolationTestData = (
77
+ startState: EntityState,
78
+ endState: EntityState,
79
+ steps: number = 10
80
+ ): EntityState[] => {
81
+ const result: EntityState[] = [];
82
+
83
+ for (let i = 0; i <= steps; i++) {
84
+ const t = i / steps;
85
+ const lerp = (a: number, b: number) => a + (b - a) * t;
86
+
87
+ result.push({
88
+ ...startState,
89
+ origin: {
90
+ x: lerp(startState.origin.x, endState.origin.x),
91
+ y: lerp(startState.origin.y, endState.origin.y),
92
+ z: lerp(startState.origin.z, endState.origin.z),
93
+ },
94
+ angles: {
95
+ x: lerp(startState.angles.x, endState.angles.x),
96
+ y: lerp(startState.angles.y, endState.angles.y),
97
+ z: lerp(startState.angles.z, endState.angles.z),
98
+ }
99
+ });
100
+ }
101
+
102
+ return result;
103
+ };
104
+
105
+ export const verifySmoothing = (states: EntityState[]): SmoothingAnalysis => {
106
+ let maxError = 0;
107
+ let totalError = 0;
108
+ const jumps: number[] = [];
109
+
110
+ for (let i = 1; i < states.length; i++) {
111
+ const prev = states[i-1].origin;
112
+ const curr = states[i].origin;
113
+
114
+ const dx = curr.x - prev.x;
115
+ const dy = curr.y - prev.y;
116
+ const dz = curr.z - prev.z;
117
+ const dist = Math.sqrt(dx*dx + dy*dy + dz*dz);
118
+
119
+ // Assume constant velocity, check for large deviations
120
+ // Ideally we'd compare against expected position
121
+ // Here we just track distance changes
122
+
123
+ if (dist > 50) { // arbitrary jump threshold
124
+ jumps.push(i);
125
+ }
126
+
127
+ totalError += dist; // This isn't really error without a reference, but serves as a metric
128
+ }
129
+
130
+ return {
131
+ smooth: jumps.length === 0,
132
+ maxError,
133
+ averageError: totalError / (states.length - 1 || 1),
134
+ jumps
135
+ };
136
+ };
@@ -0,0 +1,201 @@
1
+
2
+ import { vec3 } from 'gl-matrix';
3
+ import { Camera } from '@quake2ts/engine';
4
+ import { Vec3 } from '@quake2ts/shared';
5
+
6
+ // Define RefDef interface locally if it's not exported from shared/engine,
7
+ // or import it if we find it.
8
+ // Based on grep, RefDef seems elusive or I missed it.
9
+ // Standard Quake 2 RefDef:
10
+ export interface RefDef {
11
+ x: number;
12
+ y: number;
13
+ width: number;
14
+ height: number;
15
+ fov_x: number;
16
+ fov_y: number;
17
+ vieworg: vec3;
18
+ viewangles: vec3;
19
+ time: number;
20
+ rdflags: number;
21
+ // ... other fields
22
+ }
23
+
24
+ export interface ViewState {
25
+ camera: Camera;
26
+ viewport: { x: number; y: number; width: number; height: number };
27
+ refdef: RefDef;
28
+ }
29
+
30
+ export interface ViewScenario {
31
+ viewState: ViewState;
32
+ cleanup: () => void;
33
+ }
34
+
35
+ export interface CameraInput {
36
+ forward?: number;
37
+ right?: number;
38
+ up?: number;
39
+ pitchDelta?: number;
40
+ yawDelta?: number;
41
+ rollDelta?: number;
42
+ }
43
+
44
+ export interface DemoCameraResult {
45
+ origin: Vec3;
46
+ angles: Vec3;
47
+ fov: number;
48
+ }
49
+
50
+ function toVec3(v: Vec3 | vec3 | { x: number, y: number, z: number } | number[]): vec3 {
51
+ if (v instanceof Float32Array && v.length === 3) {
52
+ return v as vec3;
53
+ }
54
+ if (Array.isArray(v) && v.length === 3) {
55
+ return vec3.fromValues(v[0], v[1], v[2]);
56
+ }
57
+ if (typeof v === 'object' && 'x' in v && 'y' in v && 'z' in v) {
58
+ return vec3.fromValues(v.x, v.y, v.z);
59
+ }
60
+ // Fallback or error? defaulting to 0,0,0
61
+ return vec3.create();
62
+ }
63
+
64
+ /**
65
+ * Creates a mock Camera instance with optional overrides.
66
+ * Accepts partial Camera properties, where position/angles can be Vec3 objects or arrays.
67
+ */
68
+ export function createMockCamera(overrides: Partial<Omit<Camera, 'position' | 'angles'> & { position: any, angles: any }> = {}): Camera {
69
+ const camera = new Camera();
70
+
71
+ if (overrides.position) {
72
+ camera.position = toVec3(overrides.position);
73
+ }
74
+ if (overrides.angles) {
75
+ camera.angles = toVec3(overrides.angles);
76
+ }
77
+ if (overrides.fov !== undefined) {
78
+ camera.fov = overrides.fov;
79
+ }
80
+ // Apply other properties if exposed by Camera class setters
81
+
82
+ return camera;
83
+ }
84
+
85
+ /**
86
+ * Creates a mock Demo Camera state result (for getDemoCamera mock).
87
+ * This returns a structure using Vec3 interface compatible with angleVectors, unlike Camera class.
88
+ */
89
+ export function createMockDemoCameraResult(overrides: Partial<DemoCameraResult> = {}): DemoCameraResult {
90
+ return {
91
+ origin: overrides.origin || { x: 0, y: 0, z: 0 },
92
+ angles: overrides.angles || { x: 0, y: 0, z: 0 },
93
+ fov: overrides.fov ?? 90
94
+ };
95
+ }
96
+
97
+ /**
98
+ * Creates a mock RefDef object.
99
+ */
100
+ export function createMockRefDef(overrides: Partial<RefDef> = {}): RefDef {
101
+ return {
102
+ x: 0,
103
+ y: 0,
104
+ width: 320,
105
+ height: 240,
106
+ fov_x: 90,
107
+ fov_y: 90,
108
+ vieworg: vec3.create(),
109
+ viewangles: vec3.create(),
110
+ time: 0,
111
+ rdflags: 0,
112
+ ...overrides
113
+ };
114
+ }
115
+
116
+ /**
117
+ * Creates a mock ViewState object.
118
+ */
119
+ export function createMockViewState(overrides: Partial<ViewState> = {}): ViewState {
120
+ return {
121
+ camera: overrides.camera || createMockCamera(),
122
+ viewport: overrides.viewport || { x: 0, y: 0, width: 800, height: 600 },
123
+ refdef: overrides.refdef || createMockRefDef(),
124
+ ...overrides
125
+ };
126
+ }
127
+
128
+ /**
129
+ * Creates a pre-configured view test scenario.
130
+ */
131
+ export function createViewTestScenario(scenarioType: 'firstPerson' | 'thirdPerson' | 'spectator'): ViewScenario {
132
+ const camera = createMockCamera();
133
+ const refdef = createMockRefDef();
134
+
135
+ switch (scenarioType) {
136
+ case 'firstPerson':
137
+ camera.position = vec3.fromValues(100, 100, 50);
138
+ camera.angles = vec3.fromValues(0, 45, 0);
139
+ vec3.copy(refdef.vieworg, camera.position);
140
+ vec3.copy(refdef.viewangles, camera.angles);
141
+ break;
142
+ case 'thirdPerson':
143
+ camera.position = vec3.fromValues(100, 100, 100); // Higher/back
144
+ camera.angles = vec3.fromValues(30, 45, 0); // Looking down
145
+ // Refdef might differ from camera in 3rd person (refdef usually is player eye, camera is offset)
146
+ vec3.set(refdef.vieworg, 100, 100, 50); // Player origin
147
+ break;
148
+ case 'spectator':
149
+ camera.position = vec3.fromValues(0, 0, 100);
150
+ camera.angles = vec3.fromValues(90, 0, 0); // Top down
151
+ break;
152
+ }
153
+
154
+ return {
155
+ viewState: {
156
+ camera,
157
+ viewport: { x: 0, y: 0, width: 800, height: 600 },
158
+ refdef
159
+ },
160
+ cleanup: () => {
161
+ // Any cleanup if needed
162
+ }
163
+ };
164
+ }
165
+
166
+ /**
167
+ * Simulates camera movement based on input delta.
168
+ * This is a helper to verify camera logic, not a replacement for full physics.
169
+ */
170
+ export function simulateCameraMovement(camera: Camera, input: CameraInput, deltaTime: number): Camera {
171
+ // Simple movement simulation
172
+ const speed = 100; // units per second
173
+
174
+ // Update angles
175
+ if (input.pitchDelta) camera.angles[0] += input.pitchDelta;
176
+ if (input.yawDelta) camera.angles[1] += input.yawDelta;
177
+ if (input.rollDelta) camera.angles[2] += input.rollDelta;
178
+
179
+ // Force update dirty flag in Camera
180
+ camera.angles = camera.angles;
181
+
182
+ // Calculate forward/right/up vectors based on new angles
183
+ // For simplicity, we can use simple trig or just assume standard axis aligned for basic tests
184
+ // But proper way is AngleVectors.
185
+ // Since we don't have AngleVectors imported here (it's in shared), we might skip complex relative movement
186
+ // or import it.
187
+
188
+ // Ideally: import { angleVectors } from '@quake2ts/shared/math/angles';
189
+ // But we want to avoid deep deps if possible.
190
+
191
+ // For now, let's just apply absolute movement if provided, or simplistic axis logic
192
+ if (input.forward || input.right || input.up) {
193
+ // simplistic impl
194
+ camera.position[0] += (input.forward || 0) * deltaTime;
195
+ camera.position[1] += (input.right || 0) * deltaTime;
196
+ camera.position[2] += (input.up || 0) * deltaTime;
197
+ camera.position = camera.position;
198
+ }
199
+
200
+ return camera;
201
+ }
@@ -0,0 +1,75 @@
1
+ import { vi } from 'vitest';
2
+
3
+ export interface MockConsole {
4
+ // Add Console interface methods here as needed, based on usage in engine/client
5
+ // Currently the Console interface is not strictly defined in shared types for test-utils
6
+ // but we mimic what is commonly used.
7
+ print: (text: string) => void;
8
+ error: (text: string) => void;
9
+ execute: (text: string) => void;
10
+ addCommand: (name: string, handler: (args: string[]) => void) => void;
11
+ getCvar: (name: string) => string | undefined;
12
+ setCvar: (name: string, value: string) => void;
13
+
14
+ // Test helpers
15
+ getHistory: () => string[];
16
+ clearHistory: () => void;
17
+ getErrors: () => string[];
18
+ }
19
+
20
+ export interface MockCommand {
21
+ name: string;
22
+ handler: (args: string[]) => void;
23
+ }
24
+
25
+ export interface CvarRegistry {
26
+ [key: string]: string;
27
+ }
28
+
29
+ export function createMockConsole(overrides?: Partial<MockConsole>): MockConsole {
30
+ const history: string[] = [];
31
+ const errors: string[] = [];
32
+ const commands: Record<string, (args: string[]) => void> = {};
33
+ const cvars: Record<string, string> = {};
34
+
35
+ return {
36
+ print: vi.fn((text: string) => {
37
+ history.push(text);
38
+ }),
39
+ error: vi.fn((text: string) => {
40
+ errors.push(text);
41
+ }),
42
+ execute: vi.fn((text: string) => {
43
+ const parts = text.trim().split(/\s+/);
44
+ const cmd = parts[0];
45
+ const args = parts.slice(1);
46
+ if (commands[cmd]) {
47
+ commands[cmd](args);
48
+ } else {
49
+ history.push(`Unknown command "${cmd}"`);
50
+ }
51
+ }),
52
+ addCommand: vi.fn((name: string, handler: (args: string[]) => void) => {
53
+ commands[name] = handler;
54
+ }),
55
+ getCvar: vi.fn((name: string) => cvars[name]),
56
+ setCvar: vi.fn((name: string, value: string) => {
57
+ cvars[name] = value;
58
+ }),
59
+ getHistory: () => history,
60
+ clearHistory: () => {
61
+ history.length = 0;
62
+ errors.length = 0;
63
+ },
64
+ getErrors: () => errors,
65
+ ...overrides
66
+ };
67
+ }
68
+
69
+ export function createMockCommand(name: string, handler: (args: string[]) => void): MockCommand {
70
+ return { name, handler };
71
+ }
72
+
73
+ export function createMockCvarRegistry(initialCvars?: Record<string, string>): CvarRegistry {
74
+ return { ...initialCvars };
75
+ }
@@ -0,0 +1,48 @@
1
+ import { vi } from 'vitest';
2
+
3
+ export interface DownloadManager {
4
+ download(url: string): Promise<ArrayBuffer>;
5
+ cancel(url: string): void;
6
+ getProgress(url: string): number;
7
+ }
8
+
9
+ export function createMockDownloadManager(overrides?: Partial<DownloadManager>): DownloadManager {
10
+ return {
11
+ download: vi.fn().mockResolvedValue(new ArrayBuffer(0)),
12
+ cancel: vi.fn(),
13
+ getProgress: vi.fn().mockReturnValue(0),
14
+ ...overrides
15
+ };
16
+ }
17
+
18
+ export interface PrecacheList {
19
+ models: string[];
20
+ sounds: string[];
21
+ images: string[];
22
+ }
23
+
24
+ export function createMockPrecacheList(
25
+ models: string[] = [],
26
+ sounds: string[] = [],
27
+ images: string[] = []
28
+ ): PrecacheList {
29
+ return {
30
+ models,
31
+ sounds,
32
+ images
33
+ };
34
+ }
35
+
36
+ export async function simulateDownload(
37
+ url: string,
38
+ progressCallback?: (percent: number) => void
39
+ ): Promise<ArrayBuffer> {
40
+ const steps = 10;
41
+ for (let i = 0; i <= steps; i++) {
42
+ if (progressCallback) {
43
+ progressCallback(i / steps);
44
+ }
45
+ await new Promise(resolve => setTimeout(resolve, 10));
46
+ }
47
+ return new ArrayBuffer(1024);
48
+ }