@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.
- package/README.md +454 -0
- package/dist/index.cjs +5432 -0
- package/dist/index.cjs.map +1 -0
- package/dist/index.d.cts +2150 -0
- package/dist/index.d.ts +2150 -0
- package/dist/index.js +5165 -0
- package/dist/index.js.map +1 -0
- package/package.json +82 -0
- package/src/client/helpers/hud.ts +114 -0
- package/src/client/helpers/prediction.ts +136 -0
- package/src/client/helpers/view.ts +201 -0
- package/src/client/mocks/console.ts +75 -0
- package/src/client/mocks/download.ts +48 -0
- package/src/client/mocks/input.ts +246 -0
- package/src/client/mocks/network.ts +148 -0
- package/src/client/mocks/state.ts +148 -0
- package/src/e2e/network.ts +47 -0
- package/src/e2e/playwright.ts +90 -0
- package/src/e2e/visual.ts +172 -0
- package/src/engine/helpers/pipeline-test-template.ts +113 -0
- package/src/engine/helpers/webgpu-rendering.ts +251 -0
- package/src/engine/mocks/assets.ts +129 -0
- package/src/engine/mocks/audio.ts +152 -0
- package/src/engine/mocks/buffers.ts +88 -0
- package/src/engine/mocks/lighting.ts +64 -0
- package/src/engine/mocks/particles.ts +76 -0
- package/src/engine/mocks/renderer.ts +218 -0
- package/src/engine/mocks/webgl.ts +267 -0
- package/src/engine/mocks/webgpu.ts +262 -0
- package/src/engine/rendering.ts +103 -0
- package/src/game/factories.ts +204 -0
- package/src/game/helpers/physics.ts +171 -0
- package/src/game/helpers/save.ts +232 -0
- package/src/game/helpers.ts +310 -0
- package/src/game/mocks/ai.ts +67 -0
- package/src/game/mocks/combat.ts +61 -0
- package/src/game/mocks/items.ts +166 -0
- package/src/game/mocks.ts +105 -0
- package/src/index.ts +93 -0
- package/src/server/helpers/bandwidth.ts +127 -0
- package/src/server/helpers/multiplayer.ts +158 -0
- package/src/server/helpers/snapshot.ts +241 -0
- package/src/server/mockNetDriver.ts +106 -0
- package/src/server/mockTransport.ts +50 -0
- package/src/server/mocks/commands.ts +93 -0
- package/src/server/mocks/connection.ts +139 -0
- package/src/server/mocks/master.ts +97 -0
- package/src/server/mocks/physics.ts +32 -0
- package/src/server/mocks/state.ts +162 -0
- package/src/server/mocks/transport.ts +161 -0
- package/src/setup/audio.ts +118 -0
- package/src/setup/browser.ts +249 -0
- package/src/setup/canvas.ts +142 -0
- package/src/setup/node.ts +21 -0
- package/src/setup/storage.ts +60 -0
- package/src/setup/timing.ts +142 -0
- package/src/setup/webgl.ts +8 -0
- package/src/setup/webgpu.ts +113 -0
- package/src/shared/bsp.ts +145 -0
- package/src/shared/collision.ts +64 -0
- package/src/shared/factories.ts +88 -0
- package/src/shared/math.ts +65 -0
- package/src/shared/mocks.ts +243 -0
- package/src/shared/pak-loader.ts +45 -0
- package/src/visual/snapshots.ts +292 -0
|
@@ -0,0 +1,60 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Helper to create a storage-like object (localStorage/sessionStorage).
|
|
3
|
+
*/
|
|
4
|
+
function createStorageMock(initialData: Record<string, string> = {}): Storage {
|
|
5
|
+
const store = new Map<string, string>(Object.entries(initialData));
|
|
6
|
+
|
|
7
|
+
return {
|
|
8
|
+
getItem: (key: string) => store.get(key) || null,
|
|
9
|
+
setItem: (key: string, value: string) => store.set(key, value.toString()),
|
|
10
|
+
removeItem: (key: string) => store.delete(key),
|
|
11
|
+
clear: () => store.clear(),
|
|
12
|
+
key: (index: number) => Array.from(store.keys())[index] || null,
|
|
13
|
+
get length() { return store.size; }
|
|
14
|
+
} as Storage;
|
|
15
|
+
}
|
|
16
|
+
|
|
17
|
+
/**
|
|
18
|
+
* Creates a mock LocalStorage instance.
|
|
19
|
+
*/
|
|
20
|
+
export function createMockLocalStorage(initialData: Record<string, string> = {}): Storage {
|
|
21
|
+
return createStorageMock(initialData);
|
|
22
|
+
}
|
|
23
|
+
|
|
24
|
+
/**
|
|
25
|
+
* Creates a mock SessionStorage instance.
|
|
26
|
+
*/
|
|
27
|
+
export function createMockSessionStorage(initialData: Record<string, string> = {}): Storage {
|
|
28
|
+
return createStorageMock(initialData);
|
|
29
|
+
}
|
|
30
|
+
|
|
31
|
+
/**
|
|
32
|
+
* Creates a mock IndexedDB factory.
|
|
33
|
+
* Wraps fake-indexeddb.
|
|
34
|
+
*/
|
|
35
|
+
export function createMockIndexedDB(databases?: IDBDatabase[]): IDBFactory {
|
|
36
|
+
// fake-indexeddb/auto already sets global.indexedDB
|
|
37
|
+
// If we need to return a specific instance or customized one, we can do it here.
|
|
38
|
+
// For now, return the global one or a fresh 'fake-indexeddb' instance if we were to import it directly.
|
|
39
|
+
|
|
40
|
+
// Since we imported 'fake-indexeddb/auto' in setup/browser.ts, global.indexedDB is already mocked.
|
|
41
|
+
// We can return that.
|
|
42
|
+
return global.indexedDB;
|
|
43
|
+
}
|
|
44
|
+
|
|
45
|
+
export interface StorageScenario {
|
|
46
|
+
localStorage: Storage;
|
|
47
|
+
sessionStorage: Storage;
|
|
48
|
+
indexedDB: IDBFactory;
|
|
49
|
+
}
|
|
50
|
+
|
|
51
|
+
/**
|
|
52
|
+
* Creates a complete storage test scenario.
|
|
53
|
+
*/
|
|
54
|
+
export function createStorageTestScenario(storageType: 'local' | 'session' | 'indexed' = 'local'): StorageScenario {
|
|
55
|
+
return {
|
|
56
|
+
localStorage: createMockLocalStorage(),
|
|
57
|
+
sessionStorage: createMockSessionStorage(),
|
|
58
|
+
indexedDB: createMockIndexedDB()
|
|
59
|
+
};
|
|
60
|
+
}
|
|
@@ -0,0 +1,142 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Interface for the mock RequestAnimationFrame implementation.
|
|
3
|
+
*/
|
|
4
|
+
export interface MockRAF {
|
|
5
|
+
/**
|
|
6
|
+
* Advances time by one tick (simulating one frame).
|
|
7
|
+
* @param time Timestamp to pass to callbacks (default: calls Date.now())
|
|
8
|
+
*/
|
|
9
|
+
tick(time?: number): void;
|
|
10
|
+
/**
|
|
11
|
+
* Advances time by a specific amount, triggering multiple frames if necessary.
|
|
12
|
+
* Not fully implemented in simple version, acts as alias to tick() with specific time.
|
|
13
|
+
*/
|
|
14
|
+
advance(ms: number): void;
|
|
15
|
+
/**
|
|
16
|
+
* Returns current pending callbacks.
|
|
17
|
+
*/
|
|
18
|
+
getCallbacks(): Array<{id: number, callback: FrameRequestCallback}>;
|
|
19
|
+
}
|
|
20
|
+
|
|
21
|
+
/**
|
|
22
|
+
* Creates a mock RequestAnimationFrame implementation.
|
|
23
|
+
* Replaces global.requestAnimationFrame and cancelAnimationFrame.
|
|
24
|
+
*/
|
|
25
|
+
export function createMockRAF(): MockRAF {
|
|
26
|
+
let callbacks: Array<{id: number, callback: FrameRequestCallback}> = [];
|
|
27
|
+
let lastId = 0;
|
|
28
|
+
let currentTime = 0;
|
|
29
|
+
|
|
30
|
+
const originalRAF = global.requestAnimationFrame;
|
|
31
|
+
const originalCancelRAF = global.cancelAnimationFrame;
|
|
32
|
+
|
|
33
|
+
global.requestAnimationFrame = (callback: FrameRequestCallback): number => {
|
|
34
|
+
lastId++;
|
|
35
|
+
callbacks.push({ id: lastId, callback });
|
|
36
|
+
return lastId;
|
|
37
|
+
};
|
|
38
|
+
|
|
39
|
+
global.cancelAnimationFrame = (id: number): void => {
|
|
40
|
+
callbacks = callbacks.filter(cb => cb.id !== id);
|
|
41
|
+
};
|
|
42
|
+
|
|
43
|
+
return {
|
|
44
|
+
tick(time?: number) {
|
|
45
|
+
if (time) currentTime = time;
|
|
46
|
+
else currentTime += 16.66; // ~60fps
|
|
47
|
+
|
|
48
|
+
const currentCallbacks = [...callbacks];
|
|
49
|
+
callbacks = []; // Clear before execution to allow re-scheduling
|
|
50
|
+
|
|
51
|
+
currentCallbacks.forEach(cb => cb.callback(currentTime));
|
|
52
|
+
},
|
|
53
|
+
advance(ms: number) {
|
|
54
|
+
// Simple implementation: just advance time and process one batch
|
|
55
|
+
// For more complex simulation, we might loop
|
|
56
|
+
currentTime += ms;
|
|
57
|
+
this.tick(currentTime);
|
|
58
|
+
},
|
|
59
|
+
getCallbacks() {
|
|
60
|
+
return callbacks;
|
|
61
|
+
}
|
|
62
|
+
};
|
|
63
|
+
}
|
|
64
|
+
|
|
65
|
+
/**
|
|
66
|
+
* Creates a mock Performance object.
|
|
67
|
+
*/
|
|
68
|
+
export function createMockPerformance(startTime: number = 0): Performance {
|
|
69
|
+
let now = startTime;
|
|
70
|
+
|
|
71
|
+
const mockPerformance = {
|
|
72
|
+
now: () => now,
|
|
73
|
+
timeOrigin: startTime,
|
|
74
|
+
timing: {
|
|
75
|
+
navigationStart: startTime,
|
|
76
|
+
},
|
|
77
|
+
mark: (_name: string) => {},
|
|
78
|
+
measure: (_name: string, _start: string, _end: string) => {},
|
|
79
|
+
getEntries: () => [],
|
|
80
|
+
getEntriesByName: (_name: string) => [],
|
|
81
|
+
getEntriesByType: (_type: string) => [],
|
|
82
|
+
clearMarks: (_name?: string) => {},
|
|
83
|
+
clearMeasures: (_name?: string) => {},
|
|
84
|
+
clearResourceTimings: () => {},
|
|
85
|
+
setResourceTimingBufferSize: (_maxSize: number) => {},
|
|
86
|
+
onresourcetimingbufferfull: null,
|
|
87
|
+
toJSON: () => ({})
|
|
88
|
+
} as unknown as Performance;
|
|
89
|
+
|
|
90
|
+
// Polyfill global if needed, or return for injection
|
|
91
|
+
if (typeof global.performance === 'undefined') {
|
|
92
|
+
global.performance = mockPerformance;
|
|
93
|
+
}
|
|
94
|
+
|
|
95
|
+
return mockPerformance;
|
|
96
|
+
}
|
|
97
|
+
|
|
98
|
+
export interface ControlledTimer {
|
|
99
|
+
/**
|
|
100
|
+
* Advances virtual time by ms.
|
|
101
|
+
*/
|
|
102
|
+
advanceBy(ms: number): void;
|
|
103
|
+
/**
|
|
104
|
+
* Runs all pending timers.
|
|
105
|
+
*/
|
|
106
|
+
runAll(): void;
|
|
107
|
+
/**
|
|
108
|
+
* Restores original timer functions.
|
|
109
|
+
*/
|
|
110
|
+
clear(): void;
|
|
111
|
+
}
|
|
112
|
+
|
|
113
|
+
/**
|
|
114
|
+
* Creates controlled timers (setTimeout/setInterval).
|
|
115
|
+
* Note: Use verify's useFakeTimers() for better integration with test runner.
|
|
116
|
+
* This is a lightweight alternative or specific helper.
|
|
117
|
+
*/
|
|
118
|
+
export function createControlledTimer(): ControlledTimer {
|
|
119
|
+
// This functionality is best provided by vitest/jest directly via vi.useFakeTimers()
|
|
120
|
+
// Wrapping it here for convenience if needed, but for now we'll recommend `vi`.
|
|
121
|
+
console.warn('createControlledTimer: Recommend using vi.useFakeTimers() instead.');
|
|
122
|
+
|
|
123
|
+
return {
|
|
124
|
+
advanceBy: (ms: number) => { /* delegate to vi.advanceTimersByTime(ms) in consumer */ },
|
|
125
|
+
runAll: () => { /* delegate to vi.runAllTimers() */ },
|
|
126
|
+
clear: () => { /* delegate to vi.useRealTimers() */ }
|
|
127
|
+
};
|
|
128
|
+
}
|
|
129
|
+
|
|
130
|
+
/**
|
|
131
|
+
* Simulates multiple RAF frames.
|
|
132
|
+
*/
|
|
133
|
+
export function simulateFrames(count: number, frameTime: number = 16, callback?: (frameIndex: number) => void): void {
|
|
134
|
+
for (let i = 0; i < count; i++) {
|
|
135
|
+
// If using the global mock RAF from createMockRAF, we just rely on callbacks being scheduled.
|
|
136
|
+
// However, if we need to manually trigger them, we assume existing RAF loop.
|
|
137
|
+
// This helper assumes a synchronous execution where we can just wait or tick.
|
|
138
|
+
// With `vi.useFakeTimers()`, we would `vi.advanceTimersByTime(frameTime)`.
|
|
139
|
+
|
|
140
|
+
if (callback) callback(i);
|
|
141
|
+
}
|
|
142
|
+
}
|
|
@@ -0,0 +1,8 @@
|
|
|
1
|
+
import { createMockWebGL2Context } from '../engine/mocks/webgl.js';
|
|
2
|
+
|
|
3
|
+
// Re-export for compatibility during migration
|
|
4
|
+
// But actually we prefer to use the one from engine/mocks/webgl.ts
|
|
5
|
+
// This file is kept if necessary for backward compatibility of file path imports
|
|
6
|
+
// but the implementation is now delegating to the consolidated one.
|
|
7
|
+
|
|
8
|
+
export { createMockWebGL2Context };
|
|
@@ -0,0 +1,113 @@
|
|
|
1
|
+
|
|
2
|
+
// Types for our setup
|
|
3
|
+
export interface HeadlessWebGPUSetup {
|
|
4
|
+
adapter: GPUAdapter;
|
|
5
|
+
device: GPUDevice;
|
|
6
|
+
cleanup: () => Promise<void>;
|
|
7
|
+
}
|
|
8
|
+
|
|
9
|
+
export interface WebGPUContextState {
|
|
10
|
+
adapter: GPUAdapter;
|
|
11
|
+
device: GPUDevice;
|
|
12
|
+
queue: GPUQueue;
|
|
13
|
+
format: GPUTextureFormat;
|
|
14
|
+
}
|
|
15
|
+
|
|
16
|
+
/**
|
|
17
|
+
* Initialize WebGPU in a headless Node.js environment using @webgpu/dawn (via webgpu package)
|
|
18
|
+
*/
|
|
19
|
+
export async function initHeadlessWebGPU(
|
|
20
|
+
options?: {
|
|
21
|
+
powerPreference?: 'low-power' | 'high-performance';
|
|
22
|
+
requiredFeatures?: GPUFeatureName[];
|
|
23
|
+
}
|
|
24
|
+
): Promise<HeadlessWebGPUSetup> {
|
|
25
|
+
// Check if we are in Node.js environment
|
|
26
|
+
if (typeof process === 'undefined' || process.release?.name !== 'node') {
|
|
27
|
+
throw new Error('initHeadlessWebGPU should only be called in a Node.js environment');
|
|
28
|
+
}
|
|
29
|
+
|
|
30
|
+
// Dynamic import to avoid hard dependency
|
|
31
|
+
let create, globals;
|
|
32
|
+
try {
|
|
33
|
+
const webgpu = await import('webgpu');
|
|
34
|
+
create = webgpu.create;
|
|
35
|
+
globals = webgpu.globals;
|
|
36
|
+
} catch (e) {
|
|
37
|
+
throw new Error(`Failed to load "webgpu" package. Please ensure it is installed. Original error: ${e}`);
|
|
38
|
+
}
|
|
39
|
+
|
|
40
|
+
// Inject WebGPU globals into globalThis if not already present
|
|
41
|
+
// Note: we need to handle navigator specifically as it might be read-only in some envs (like jsdom)
|
|
42
|
+
// or simply missing.
|
|
43
|
+
if (!globalThis.navigator) {
|
|
44
|
+
// @ts-ignore
|
|
45
|
+
globalThis.navigator = {};
|
|
46
|
+
}
|
|
47
|
+
|
|
48
|
+
if (!globalThis.navigator.gpu) {
|
|
49
|
+
// Create the GPU instance using the 'webgpu' package's create function
|
|
50
|
+
// This is more robust than relying on 'globals' if 'globals' expects a clean environment
|
|
51
|
+
const gpu = create([]);
|
|
52
|
+
|
|
53
|
+
try {
|
|
54
|
+
Object.defineProperty(globalThis.navigator, 'gpu', {
|
|
55
|
+
value: gpu,
|
|
56
|
+
writable: true,
|
|
57
|
+
configurable: true
|
|
58
|
+
});
|
|
59
|
+
} catch (e) {
|
|
60
|
+
// Fallback if defineProperty fails (e.g. read-only navigator in strict mode)
|
|
61
|
+
// But usually in tests we can modify it.
|
|
62
|
+
// If we are in JSDOM, navigator might be tricky.
|
|
63
|
+
console.warn('Could not define navigator.gpu, trying direct assignment');
|
|
64
|
+
// @ts-ignore
|
|
65
|
+
globalThis.navigator.gpu = gpu;
|
|
66
|
+
}
|
|
67
|
+
|
|
68
|
+
// Also inject other globals like GPUAdapter, GPUDevice etc.
|
|
69
|
+
Object.assign(globalThis, globals);
|
|
70
|
+
}
|
|
71
|
+
|
|
72
|
+
// Request Adapter
|
|
73
|
+
const adapter = await navigator.gpu.requestAdapter({
|
|
74
|
+
powerPreference: options?.powerPreference || 'high-performance',
|
|
75
|
+
});
|
|
76
|
+
|
|
77
|
+
if (!adapter) {
|
|
78
|
+
throw new Error('Failed to create WebGPU adapter');
|
|
79
|
+
}
|
|
80
|
+
|
|
81
|
+
// Request Device
|
|
82
|
+
const device = await adapter.requestDevice({
|
|
83
|
+
requiredFeatures: options?.requiredFeatures || [],
|
|
84
|
+
});
|
|
85
|
+
|
|
86
|
+
if (!device) {
|
|
87
|
+
throw new Error('Failed to create WebGPU device');
|
|
88
|
+
}
|
|
89
|
+
|
|
90
|
+
return {
|
|
91
|
+
adapter,
|
|
92
|
+
device,
|
|
93
|
+
cleanup: async () => {
|
|
94
|
+
device.destroy();
|
|
95
|
+
// 'webgpu' package doesn't expose a way to explicitly destroy the adapter or the global instance
|
|
96
|
+
// cleanly other than letting it be garbage collected or process exit.
|
|
97
|
+
// However, destroying the device is usually sufficient for tests.
|
|
98
|
+
}
|
|
99
|
+
};
|
|
100
|
+
}
|
|
101
|
+
|
|
102
|
+
/**
|
|
103
|
+
* Creates a complete context state for testing
|
|
104
|
+
*/
|
|
105
|
+
export async function createHeadlessTestContext(): Promise<WebGPUContextState> {
|
|
106
|
+
const { adapter, device } = await initHeadlessWebGPU();
|
|
107
|
+
return {
|
|
108
|
+
adapter,
|
|
109
|
+
device,
|
|
110
|
+
queue: device.queue,
|
|
111
|
+
format: 'rgba8unorm'
|
|
112
|
+
};
|
|
113
|
+
}
|
|
@@ -0,0 +1,145 @@
|
|
|
1
|
+
import {
|
|
2
|
+
computePlaneSignBits,
|
|
3
|
+
type CollisionBrush,
|
|
4
|
+
type CollisionModel,
|
|
5
|
+
type CollisionPlane,
|
|
6
|
+
type CollisionNode,
|
|
7
|
+
type CollisionLeaf,
|
|
8
|
+
CONTENTS_SOLID,
|
|
9
|
+
type Vec3
|
|
10
|
+
} from '@quake2ts/shared';
|
|
11
|
+
|
|
12
|
+
/**
|
|
13
|
+
* Creates a collision plane with the specified normal and distance.
|
|
14
|
+
* Automatically calculates the plane type and signbits.
|
|
15
|
+
*
|
|
16
|
+
* @param normal - The normal vector of the plane.
|
|
17
|
+
* @param dist - The distance from the origin.
|
|
18
|
+
* @returns A CollisionPlane object.
|
|
19
|
+
*/
|
|
20
|
+
export function makePlane(normal: Vec3, dist: number): CollisionPlane {
|
|
21
|
+
return {
|
|
22
|
+
normal,
|
|
23
|
+
dist,
|
|
24
|
+
type: Math.abs(normal.x) === 1 ? 0 : Math.abs(normal.y) === 1 ? 1 : Math.abs(normal.z) === 1 ? 2 : 3,
|
|
25
|
+
signbits: computePlaneSignBits(normal),
|
|
26
|
+
};
|
|
27
|
+
}
|
|
28
|
+
|
|
29
|
+
/**
|
|
30
|
+
* Creates a simple axis-aligned cubic brush for testing.
|
|
31
|
+
*
|
|
32
|
+
* @param size - The size of the cube (width, height, depth).
|
|
33
|
+
* @param contents - The content flags for the brush (default: CONTENTS_SOLID).
|
|
34
|
+
* @returns A CollisionBrush object.
|
|
35
|
+
*/
|
|
36
|
+
export function makeAxisBrush(size: number, contents = CONTENTS_SOLID): CollisionBrush {
|
|
37
|
+
const half = size / 2;
|
|
38
|
+
const planes = [
|
|
39
|
+
makePlane({ x: 1, y: 0, z: 0 }, half),
|
|
40
|
+
makePlane({ x: -1, y: 0, z: 0 }, half),
|
|
41
|
+
makePlane({ x: 0, y: 1, z: 0 }, half),
|
|
42
|
+
makePlane({ x: 0, y: -1, z: 0 }, half),
|
|
43
|
+
makePlane({ x: 0, y: 0, z: 1 }, half),
|
|
44
|
+
makePlane({ x: 0, y: 0, z: -1 }, half),
|
|
45
|
+
];
|
|
46
|
+
|
|
47
|
+
return {
|
|
48
|
+
contents,
|
|
49
|
+
sides: planes.map((plane) => ({ plane, surfaceFlags: 0 })),
|
|
50
|
+
};
|
|
51
|
+
}
|
|
52
|
+
|
|
53
|
+
/**
|
|
54
|
+
* Creates a BSP node.
|
|
55
|
+
*
|
|
56
|
+
* @param plane - The splitting plane for this node.
|
|
57
|
+
* @param children - Indices of the children (positive for nodes, negative for leaves).
|
|
58
|
+
* @returns A CollisionNode object.
|
|
59
|
+
*/
|
|
60
|
+
export function makeNode(plane: CollisionPlane, children: [number, number]): CollisionNode {
|
|
61
|
+
return { plane, children };
|
|
62
|
+
}
|
|
63
|
+
|
|
64
|
+
/**
|
|
65
|
+
* Constructs a full CollisionModel from components.
|
|
66
|
+
*
|
|
67
|
+
* @param planes - Array of planes.
|
|
68
|
+
* @param nodes - Array of nodes.
|
|
69
|
+
* @param leaves - Array of leaves.
|
|
70
|
+
* @param brushes - Array of brushes.
|
|
71
|
+
* @param leafBrushes - Array of leaf brush indices.
|
|
72
|
+
* @returns A CollisionModel object.
|
|
73
|
+
*/
|
|
74
|
+
export function makeBspModel(
|
|
75
|
+
planes: CollisionPlane[],
|
|
76
|
+
nodes: CollisionNode[],
|
|
77
|
+
leaves: CollisionLeaf[],
|
|
78
|
+
brushes: CollisionBrush[],
|
|
79
|
+
leafBrushes: number[]
|
|
80
|
+
): CollisionModel {
|
|
81
|
+
return {
|
|
82
|
+
planes,
|
|
83
|
+
nodes,
|
|
84
|
+
leaves,
|
|
85
|
+
brushes,
|
|
86
|
+
leafBrushes,
|
|
87
|
+
bmodels: [],
|
|
88
|
+
};
|
|
89
|
+
}
|
|
90
|
+
|
|
91
|
+
/**
|
|
92
|
+
* Creates a BSP leaf.
|
|
93
|
+
*
|
|
94
|
+
* @param contents - The content flags for this leaf.
|
|
95
|
+
* @param firstLeafBrush - Index into the leafBrushes array.
|
|
96
|
+
* @param numLeafBrushes - Number of brushes in this leaf.
|
|
97
|
+
* @returns A CollisionLeaf object.
|
|
98
|
+
*/
|
|
99
|
+
export function makeLeaf(contents: number, firstLeafBrush: number, numLeafBrushes: number): CollisionLeaf {
|
|
100
|
+
return { contents, cluster: 0, area: 0, firstLeafBrush, numLeafBrushes };
|
|
101
|
+
}
|
|
102
|
+
|
|
103
|
+
/**
|
|
104
|
+
* Creates a simplified CollisionModel consisting of a single leaf containing the provided brushes.
|
|
105
|
+
* Useful for testing collision against a set of brushes without full BSP tree traversal.
|
|
106
|
+
*
|
|
107
|
+
* @param brushes - Array of CollisionBrushes to include.
|
|
108
|
+
* @returns A CollisionModel object.
|
|
109
|
+
*/
|
|
110
|
+
export function makeLeafModel(brushes: CollisionBrush[]): CollisionModel {
|
|
111
|
+
const planes = brushes.flatMap((brush) => brush.sides.map((side) => side.plane));
|
|
112
|
+
|
|
113
|
+
return {
|
|
114
|
+
planes,
|
|
115
|
+
nodes: [],
|
|
116
|
+
leaves: [makeLeaf(0, 0, brushes.length)],
|
|
117
|
+
brushes,
|
|
118
|
+
leafBrushes: brushes.map((_, i) => i),
|
|
119
|
+
bmodels: [],
|
|
120
|
+
};
|
|
121
|
+
}
|
|
122
|
+
|
|
123
|
+
/**
|
|
124
|
+
* Creates a brush defined by min and max bounds.
|
|
125
|
+
*
|
|
126
|
+
* @param mins - Minimum coordinates (x, y, z).
|
|
127
|
+
* @param maxs - Maximum coordinates (x, y, z).
|
|
128
|
+
* @param contents - Content flags (default: CONTENTS_SOLID).
|
|
129
|
+
* @returns A CollisionBrush object.
|
|
130
|
+
*/
|
|
131
|
+
export function makeBrushFromMinsMaxs(mins: Vec3, maxs: Vec3, contents = CONTENTS_SOLID): CollisionBrush {
|
|
132
|
+
const planes = [
|
|
133
|
+
makePlane({ x: 1, y: 0, z: 0 }, maxs.x),
|
|
134
|
+
makePlane({ x: -1, y: 0, z: 0 }, -mins.x),
|
|
135
|
+
makePlane({ x: 0, y: 1, z: 0 }, maxs.y),
|
|
136
|
+
makePlane({ x: 0, y: -1, z: 0 }, -mins.y),
|
|
137
|
+
makePlane({ x: 0, y: 0, z: 1 }, maxs.z),
|
|
138
|
+
makePlane({ x: 0, y: 0, z: -1 }, -mins.z),
|
|
139
|
+
];
|
|
140
|
+
|
|
141
|
+
return {
|
|
142
|
+
contents,
|
|
143
|
+
sides: planes.map((plane) => ({ plane, surfaceFlags: 0 })),
|
|
144
|
+
};
|
|
145
|
+
}
|
|
@@ -0,0 +1,64 @@
|
|
|
1
|
+
import type { Vec3 } from '@quake2ts/shared/math/vec3';
|
|
2
|
+
import type { TraceResult, CollisionPlane } from '@quake2ts/shared/bsp/collision';
|
|
3
|
+
|
|
4
|
+
// Re-export trace helpers from shared if they exist there now, or redefine them here if needed
|
|
5
|
+
// The plan says "Move trace helpers from game/helpers.ts to shared/collision.ts"
|
|
6
|
+
// But currently `game/helpers.ts` re-exports them from `@quake2ts/shared`.
|
|
7
|
+
// `intersects`, `stairTrace`, `ladderTrace` are in `packages/shared/src/testing.ts`.
|
|
8
|
+
// I will re-export them here for test-utils consumers.
|
|
9
|
+
|
|
10
|
+
export { intersects, stairTrace, ladderTrace } from '@quake2ts/shared';
|
|
11
|
+
|
|
12
|
+
/**
|
|
13
|
+
* Interface for a TraceResult mock, including all standard properties.
|
|
14
|
+
*/
|
|
15
|
+
export interface TraceMock extends Partial<TraceResult> {
|
|
16
|
+
fraction: number;
|
|
17
|
+
endpos: Vec3;
|
|
18
|
+
plane: CollisionPlane;
|
|
19
|
+
surface: { flags: number, name?: string, value?: number };
|
|
20
|
+
contents: number;
|
|
21
|
+
ent: any; // Using any to avoid circular dependency with Entity
|
|
22
|
+
allsolid: boolean;
|
|
23
|
+
startsolid: boolean;
|
|
24
|
+
}
|
|
25
|
+
|
|
26
|
+
/**
|
|
27
|
+
* Creates a mock TraceResult.
|
|
28
|
+
*
|
|
29
|
+
* @param overrides - Optional overrides for trace properties.
|
|
30
|
+
* @returns A TraceMock object.
|
|
31
|
+
*/
|
|
32
|
+
export const createTraceMock = (overrides?: Partial<TraceMock>): TraceMock => ({
|
|
33
|
+
fraction: 1.0,
|
|
34
|
+
endpos: { x: 0, y: 0, z: 0 },
|
|
35
|
+
plane: { normal: { x: 0, y: 0, z: 0 }, dist: 0, type: 0, signbits: 0 },
|
|
36
|
+
surface: { flags: 0 },
|
|
37
|
+
contents: 0,
|
|
38
|
+
ent: null,
|
|
39
|
+
allsolid: false,
|
|
40
|
+
startsolid: false,
|
|
41
|
+
...overrides
|
|
42
|
+
});
|
|
43
|
+
|
|
44
|
+
/**
|
|
45
|
+
* Interface for a Surface mock.
|
|
46
|
+
*/
|
|
47
|
+
export interface SurfaceMock {
|
|
48
|
+
flags: number;
|
|
49
|
+
name: string;
|
|
50
|
+
value: number;
|
|
51
|
+
}
|
|
52
|
+
|
|
53
|
+
/**
|
|
54
|
+
* Creates a mock Surface.
|
|
55
|
+
*
|
|
56
|
+
* @param overrides - Optional overrides for surface properties.
|
|
57
|
+
* @returns A SurfaceMock object.
|
|
58
|
+
*/
|
|
59
|
+
export const createSurfaceMock = (overrides?: Partial<SurfaceMock>): SurfaceMock => ({
|
|
60
|
+
flags: 0,
|
|
61
|
+
name: 'default',
|
|
62
|
+
value: 0,
|
|
63
|
+
...overrides
|
|
64
|
+
});
|
|
@@ -0,0 +1,88 @@
|
|
|
1
|
+
import { Cvar, type ConfigStringEntry } from '@quake2ts/engine';
|
|
2
|
+
import { CvarFlags, PlayerState } from '@quake2ts/shared';
|
|
3
|
+
|
|
4
|
+
// Re-export Cvar for convenience if needed, but the factory returns Cvar instance
|
|
5
|
+
export { Cvar } from '@quake2ts/engine';
|
|
6
|
+
export type { ConfigStringEntry } from '@quake2ts/engine';
|
|
7
|
+
|
|
8
|
+
/**
|
|
9
|
+
* Creates a mock ConfigStringEntry.
|
|
10
|
+
*
|
|
11
|
+
* @param index - The config string index.
|
|
12
|
+
* @param value - The config string value.
|
|
13
|
+
* @returns A ConfigStringEntry object.
|
|
14
|
+
*/
|
|
15
|
+
export function createConfigStringMock(index: number, value: string): ConfigStringEntry {
|
|
16
|
+
return { index, value };
|
|
17
|
+
}
|
|
18
|
+
|
|
19
|
+
/**
|
|
20
|
+
* Creates an array of ConfigStringEntry objects from a record.
|
|
21
|
+
*
|
|
22
|
+
* @param entries - A record of index-value pairs (optional).
|
|
23
|
+
* @returns An array of ConfigStringEntry objects.
|
|
24
|
+
*/
|
|
25
|
+
export function createConfigStringArrayMock(entries?: Record<number, string>): ConfigStringEntry[] {
|
|
26
|
+
const result: ConfigStringEntry[] = [];
|
|
27
|
+
if (entries) {
|
|
28
|
+
for (const [key, value] of Object.entries(entries)) {
|
|
29
|
+
result.push({ index: Number(key), value });
|
|
30
|
+
}
|
|
31
|
+
}
|
|
32
|
+
return result;
|
|
33
|
+
}
|
|
34
|
+
|
|
35
|
+
/**
|
|
36
|
+
* Creates a mock Cvar.
|
|
37
|
+
*
|
|
38
|
+
* @param name - The name of the cvar.
|
|
39
|
+
* @param value - The initial value of the cvar.
|
|
40
|
+
* @param flags - Cvar flags (default: CvarFlags.None).
|
|
41
|
+
* @returns A Cvar instance.
|
|
42
|
+
*/
|
|
43
|
+
export function createCvarMock(name: string, value: string, flags: number = CvarFlags.None): Cvar {
|
|
44
|
+
// We instantiate a real Cvar because it encapsulates logic (latched, default value, etc.)
|
|
45
|
+
// effectively acting as its own mock/implementation for testing purposes.
|
|
46
|
+
return new Cvar({
|
|
47
|
+
name,
|
|
48
|
+
defaultValue: value,
|
|
49
|
+
flags,
|
|
50
|
+
});
|
|
51
|
+
}
|
|
52
|
+
|
|
53
|
+
/**
|
|
54
|
+
* Creates a mock PlayerState with sensible defaults.
|
|
55
|
+
* Corresponds to player_state_t in q_shared.h.
|
|
56
|
+
*
|
|
57
|
+
* @param overrides - Partial PlayerState to override defaults.
|
|
58
|
+
* @returns A complete PlayerState object.
|
|
59
|
+
*/
|
|
60
|
+
export function createMockPlayerState(overrides?: Partial<PlayerState>): PlayerState {
|
|
61
|
+
return {
|
|
62
|
+
origin: { x: 0, y: 0, z: 0 },
|
|
63
|
+
velocity: { x: 0, y: 0, z: 0 },
|
|
64
|
+
viewAngles: { x: 0, y: 0, z: 0 },
|
|
65
|
+
onGround: false,
|
|
66
|
+
waterLevel: 0,
|
|
67
|
+
watertype: 0,
|
|
68
|
+
mins: { x: -16, y: -16, z: -24 },
|
|
69
|
+
maxs: { x: 16, y: 16, z: 32 },
|
|
70
|
+
damageAlpha: 0,
|
|
71
|
+
damageIndicators: [],
|
|
72
|
+
blend: [0, 0, 0, 0],
|
|
73
|
+
stats: new Array(32).fill(0),
|
|
74
|
+
kick_angles: { x: 0, y: 0, z: 0 },
|
|
75
|
+
kick_origin: { x: 0, y: 0, z: 0 },
|
|
76
|
+
gunoffset: { x: 0, y: 0, z: 0 },
|
|
77
|
+
gunangles: { x: 0, y: 0, z: 0 },
|
|
78
|
+
gunindex: 0,
|
|
79
|
+
pm_type: 0,
|
|
80
|
+
pm_time: 0,
|
|
81
|
+
pm_flags: 0,
|
|
82
|
+
gun_frame: 0,
|
|
83
|
+
rdflags: 0,
|
|
84
|
+
fov: 90,
|
|
85
|
+
renderfx: 0,
|
|
86
|
+
...overrides
|
|
87
|
+
};
|
|
88
|
+
}
|
|
@@ -0,0 +1,65 @@
|
|
|
1
|
+
import type { Vec3, Bounds3 } from '@quake2ts/shared/math/vec3';
|
|
2
|
+
|
|
3
|
+
/**
|
|
4
|
+
* Creates a Vector3 object.
|
|
5
|
+
*
|
|
6
|
+
* @param x - The X coordinate (default: 0).
|
|
7
|
+
* @param y - The Y coordinate (default: 0).
|
|
8
|
+
* @param z - The Z coordinate (default: 0).
|
|
9
|
+
* @returns A Vec3 object.
|
|
10
|
+
*/
|
|
11
|
+
export const createVector3 = (x: number = 0, y: number = 0, z: number = 0): Vec3 => ({
|
|
12
|
+
x,
|
|
13
|
+
y,
|
|
14
|
+
z,
|
|
15
|
+
});
|
|
16
|
+
|
|
17
|
+
/**
|
|
18
|
+
* Creates a bounds object (min/max vectors).
|
|
19
|
+
*
|
|
20
|
+
* @param mins - The minimum bounds vector (default: 0,0,0).
|
|
21
|
+
* @param maxs - The maximum bounds vector (default: 1,1,1).
|
|
22
|
+
* @returns A Bounds3 object.
|
|
23
|
+
*/
|
|
24
|
+
export const createBounds = (
|
|
25
|
+
mins: Vec3 = createVector3(0, 0, 0),
|
|
26
|
+
maxs: Vec3 = createVector3(1, 1, 1)
|
|
27
|
+
): Bounds3 => ({
|
|
28
|
+
mins,
|
|
29
|
+
maxs,
|
|
30
|
+
});
|
|
31
|
+
|
|
32
|
+
/**
|
|
33
|
+
* Interface representing a transformation (position, rotation, scale).
|
|
34
|
+
*/
|
|
35
|
+
export interface Transform {
|
|
36
|
+
position: Vec3;
|
|
37
|
+
rotation: Vec3; // Euler angles
|
|
38
|
+
scale: Vec3;
|
|
39
|
+
}
|
|
40
|
+
|
|
41
|
+
/**
|
|
42
|
+
* Creates a Transform object.
|
|
43
|
+
*
|
|
44
|
+
* @param overrides - Optional overrides for transform properties.
|
|
45
|
+
* @returns A Transform object.
|
|
46
|
+
*/
|
|
47
|
+
export const createTransform = (overrides?: Partial<Transform>): Transform => ({
|
|
48
|
+
position: createVector3(),
|
|
49
|
+
rotation: createVector3(),
|
|
50
|
+
scale: createVector3(1, 1, 1),
|
|
51
|
+
...overrides
|
|
52
|
+
});
|
|
53
|
+
|
|
54
|
+
/**
|
|
55
|
+
* Generates a random Vector3 within the specified range.
|
|
56
|
+
*
|
|
57
|
+
* @param min - Minimum value for each component (default: -100).
|
|
58
|
+
* @param max - Maximum value for each component (default: 100).
|
|
59
|
+
* @returns A random Vec3 object.
|
|
60
|
+
*/
|
|
61
|
+
export const randomVector3 = (min: number = -100, max: number = 100): Vec3 => ({
|
|
62
|
+
x: Math.random() * (max - min) + min,
|
|
63
|
+
y: Math.random() * (max - min) + min,
|
|
64
|
+
z: Math.random() * (max - min) + min,
|
|
65
|
+
});
|