@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
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
|
+
}
|