@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,246 @@
|
|
|
1
|
+
|
|
2
|
+
/**
|
|
3
|
+
* Client Input System Mocks
|
|
4
|
+
*
|
|
5
|
+
* Provides utilities for simulating user input (keyboard, mouse, pointer lock)
|
|
6
|
+
* in a test environment (JSDOM/Browser).
|
|
7
|
+
*/
|
|
8
|
+
|
|
9
|
+
export interface KeyModifiers {
|
|
10
|
+
ctrl?: boolean;
|
|
11
|
+
alt?: boolean;
|
|
12
|
+
shift?: boolean;
|
|
13
|
+
meta?: boolean;
|
|
14
|
+
}
|
|
15
|
+
|
|
16
|
+
/**
|
|
17
|
+
* Factory for creating a mock KeyboardEvent.
|
|
18
|
+
*/
|
|
19
|
+
export function createMockKeyboardEvent(key: string, type: 'keydown' | 'keyup' = 'keydown', modifiers: KeyModifiers = {}): KeyboardEvent {
|
|
20
|
+
return new KeyboardEvent(type, {
|
|
21
|
+
key,
|
|
22
|
+
code: key, // Default code to key if not specified (caller can override property if needed)
|
|
23
|
+
ctrlKey: modifiers.ctrl,
|
|
24
|
+
altKey: modifiers.alt,
|
|
25
|
+
shiftKey: modifiers.shift,
|
|
26
|
+
metaKey: modifiers.meta,
|
|
27
|
+
bubbles: true,
|
|
28
|
+
cancelable: true,
|
|
29
|
+
view: window
|
|
30
|
+
});
|
|
31
|
+
}
|
|
32
|
+
|
|
33
|
+
/**
|
|
34
|
+
* Factory for creating a mock MouseEvent with support for movementX/Y.
|
|
35
|
+
*/
|
|
36
|
+
export function createMockMouseEvent(type: string, options: MouseEventInit = {}): MouseEvent {
|
|
37
|
+
const event = new MouseEvent(type, {
|
|
38
|
+
bubbles: true,
|
|
39
|
+
cancelable: true,
|
|
40
|
+
view: window,
|
|
41
|
+
...options
|
|
42
|
+
});
|
|
43
|
+
|
|
44
|
+
// Patch movementX/Y if needed as JSDOM MouseEvent might not initialize them from options
|
|
45
|
+
if (options.movementX !== undefined) {
|
|
46
|
+
Object.defineProperty(event, 'movementX', { value: options.movementX });
|
|
47
|
+
}
|
|
48
|
+
if (options.movementY !== undefined) {
|
|
49
|
+
Object.defineProperty(event, 'movementY', { value: options.movementY });
|
|
50
|
+
}
|
|
51
|
+
|
|
52
|
+
return event;
|
|
53
|
+
}
|
|
54
|
+
|
|
55
|
+
/**
|
|
56
|
+
* Factory for creating a mock WheelEvent.
|
|
57
|
+
*/
|
|
58
|
+
export function createMockWheelEvent(deltaX: number = 0, deltaY: number = 0): WheelEvent {
|
|
59
|
+
return new WheelEvent('wheel', {
|
|
60
|
+
deltaX,
|
|
61
|
+
deltaY,
|
|
62
|
+
bubbles: true,
|
|
63
|
+
cancelable: true,
|
|
64
|
+
view: window
|
|
65
|
+
});
|
|
66
|
+
}
|
|
67
|
+
|
|
68
|
+
/**
|
|
69
|
+
* Class implementing a mock for the Pointer Lock API.
|
|
70
|
+
* Handles patching the document and element prototypes to simulate pointer lock.
|
|
71
|
+
*/
|
|
72
|
+
export class MockPointerLock {
|
|
73
|
+
private _doc: Document;
|
|
74
|
+
|
|
75
|
+
constructor(doc: Document = document) {
|
|
76
|
+
this._doc = doc;
|
|
77
|
+
this.setup();
|
|
78
|
+
}
|
|
79
|
+
|
|
80
|
+
private setup() {
|
|
81
|
+
// Check if already patched to avoid conflicts
|
|
82
|
+
if ((this._doc as any).__mockPointerLockInstalled) return;
|
|
83
|
+
|
|
84
|
+
let _pointerLockElement: Element | null = null;
|
|
85
|
+
const doc = this._doc;
|
|
86
|
+
|
|
87
|
+
// Patch document.pointerLockElement
|
|
88
|
+
Object.defineProperty(doc, 'pointerLockElement', {
|
|
89
|
+
get: () => _pointerLockElement,
|
|
90
|
+
configurable: true
|
|
91
|
+
});
|
|
92
|
+
|
|
93
|
+
// Patch document.exitPointerLock
|
|
94
|
+
doc.exitPointerLock = () => {
|
|
95
|
+
if (_pointerLockElement) {
|
|
96
|
+
_pointerLockElement = null;
|
|
97
|
+
doc.dispatchEvent(new Event('pointerlockchange'));
|
|
98
|
+
}
|
|
99
|
+
};
|
|
100
|
+
|
|
101
|
+
// Patch HTMLElement.prototype.requestPointerLock
|
|
102
|
+
// Save original if needed, but in JSDOM it might not exist or do nothing
|
|
103
|
+
if (!(global.HTMLElement.prototype as any).__originalRequestPointerLock) {
|
|
104
|
+
(global.HTMLElement.prototype as any).__originalRequestPointerLock = (global.HTMLElement.prototype as any).requestPointerLock;
|
|
105
|
+
}
|
|
106
|
+
|
|
107
|
+
(global.HTMLElement.prototype as any).requestPointerLock = function() {
|
|
108
|
+
_pointerLockElement = this;
|
|
109
|
+
doc.dispatchEvent(new Event('pointerlockchange'));
|
|
110
|
+
};
|
|
111
|
+
|
|
112
|
+
(doc as any).__mockPointerLockInstalled = true;
|
|
113
|
+
}
|
|
114
|
+
|
|
115
|
+
get element(): Element | null {
|
|
116
|
+
return this._doc.pointerLockElement;
|
|
117
|
+
}
|
|
118
|
+
|
|
119
|
+
get locked(): boolean {
|
|
120
|
+
return !!this.element;
|
|
121
|
+
}
|
|
122
|
+
|
|
123
|
+
request(element: HTMLElement) {
|
|
124
|
+
element.requestPointerLock();
|
|
125
|
+
}
|
|
126
|
+
|
|
127
|
+
exit() {
|
|
128
|
+
this._doc.exitPointerLock();
|
|
129
|
+
}
|
|
130
|
+
|
|
131
|
+
isLocked(): boolean {
|
|
132
|
+
return this.locked;
|
|
133
|
+
}
|
|
134
|
+
}
|
|
135
|
+
|
|
136
|
+
/**
|
|
137
|
+
* Class for simulating input events.
|
|
138
|
+
*/
|
|
139
|
+
export class InputInjector {
|
|
140
|
+
constructor(private doc: Document = document, private win: Window = window) {}
|
|
141
|
+
|
|
142
|
+
keyDown(key: string, code?: string, modifiers?: KeyModifiers) {
|
|
143
|
+
const event = createMockKeyboardEvent(key, 'keydown', modifiers);
|
|
144
|
+
if (code) {
|
|
145
|
+
Object.defineProperty(event, 'code', { value: code });
|
|
146
|
+
}
|
|
147
|
+
this.doc.dispatchEvent(event);
|
|
148
|
+
}
|
|
149
|
+
|
|
150
|
+
keyUp(key: string, code?: string, modifiers?: KeyModifiers) {
|
|
151
|
+
const event = createMockKeyboardEvent(key, 'keyup', modifiers);
|
|
152
|
+
if (code) {
|
|
153
|
+
Object.defineProperty(event, 'code', { value: code });
|
|
154
|
+
}
|
|
155
|
+
this.doc.dispatchEvent(event);
|
|
156
|
+
}
|
|
157
|
+
|
|
158
|
+
mouseMove(movementX: number, movementY: number, clientX = 0, clientY = 0) {
|
|
159
|
+
const event = createMockMouseEvent('mousemove', {
|
|
160
|
+
clientX,
|
|
161
|
+
clientY,
|
|
162
|
+
movementX,
|
|
163
|
+
movementY
|
|
164
|
+
});
|
|
165
|
+
this.dispatchToTarget(event);
|
|
166
|
+
}
|
|
167
|
+
|
|
168
|
+
mouseButton(button: number, state: 'down' | 'up' = 'down') {
|
|
169
|
+
if (state === 'down') {
|
|
170
|
+
this.mouseDown(button);
|
|
171
|
+
} else {
|
|
172
|
+
this.mouseUp(button);
|
|
173
|
+
}
|
|
174
|
+
}
|
|
175
|
+
|
|
176
|
+
mouseDown(button: number = 0) {
|
|
177
|
+
const event = createMockMouseEvent('mousedown', { button });
|
|
178
|
+
this.dispatchToTarget(event);
|
|
179
|
+
}
|
|
180
|
+
|
|
181
|
+
mouseUp(button: number = 0) {
|
|
182
|
+
const event = createMockMouseEvent('mouseup', { button });
|
|
183
|
+
this.dispatchToTarget(event);
|
|
184
|
+
}
|
|
185
|
+
|
|
186
|
+
mouseWheel(deltaY: number) {
|
|
187
|
+
const event = createMockWheelEvent(0, deltaY);
|
|
188
|
+
this.dispatchToTarget(event);
|
|
189
|
+
}
|
|
190
|
+
|
|
191
|
+
// Alias for backward compatibility/ease of use
|
|
192
|
+
wheel(deltaY: number) {
|
|
193
|
+
this.mouseWheel(deltaY);
|
|
194
|
+
}
|
|
195
|
+
|
|
196
|
+
private dispatchToTarget(event: Event) {
|
|
197
|
+
const target = this.doc.pointerLockElement || this.doc;
|
|
198
|
+
target.dispatchEvent(event);
|
|
199
|
+
}
|
|
200
|
+
}
|
|
201
|
+
|
|
202
|
+
/**
|
|
203
|
+
* An implementation of InputSource that listens to DOM events.
|
|
204
|
+
* Useful for tests that want to verify interaction with real/mocked DOM events.
|
|
205
|
+
*/
|
|
206
|
+
export class BrowserInputSource {
|
|
207
|
+
constructor(private target: EventTarget = document) {}
|
|
208
|
+
|
|
209
|
+
on(event: string, handler: Function): void {
|
|
210
|
+
if (event === 'keydown' || event === 'keyup') {
|
|
211
|
+
this.target.addEventListener(event, (e: any) => {
|
|
212
|
+
handler(e.code);
|
|
213
|
+
});
|
|
214
|
+
} else if (event === 'mousedown' || event === 'mouseup') {
|
|
215
|
+
this.target.addEventListener(event, (e: any) => {
|
|
216
|
+
handler(e.button);
|
|
217
|
+
});
|
|
218
|
+
} else if (event === 'mousemove') {
|
|
219
|
+
this.target.addEventListener(event, (e: any) => {
|
|
220
|
+
handler(e.movementX, e.movementY);
|
|
221
|
+
});
|
|
222
|
+
}
|
|
223
|
+
}
|
|
224
|
+
}
|
|
225
|
+
|
|
226
|
+
/**
|
|
227
|
+
* Factory for creating a MockPointerLock instance.
|
|
228
|
+
*/
|
|
229
|
+
export function createMockPointerLock(element?: HTMLElement): MockPointerLock {
|
|
230
|
+
const mock = new MockPointerLock();
|
|
231
|
+
if (element) {
|
|
232
|
+
mock.request(element);
|
|
233
|
+
}
|
|
234
|
+
return mock;
|
|
235
|
+
}
|
|
236
|
+
|
|
237
|
+
/**
|
|
238
|
+
* Factory for creating an InputInjector.
|
|
239
|
+
*/
|
|
240
|
+
export function createInputInjector(target?: EventTarget): InputInjector {
|
|
241
|
+
// If target is provided and is a Document, use it.
|
|
242
|
+
// Otherwise default to global document.
|
|
243
|
+
const doc = (target instanceof Document) ? target : document;
|
|
244
|
+
const win = (doc.defaultView) ? doc.defaultView : window;
|
|
245
|
+
return new InputInjector(doc, win);
|
|
246
|
+
}
|
|
@@ -0,0 +1,148 @@
|
|
|
1
|
+
|
|
2
|
+
import {
|
|
3
|
+
ServerCommand,
|
|
4
|
+
ClientCommand,
|
|
5
|
+
Vec3
|
|
6
|
+
} from '@quake2ts/shared';
|
|
7
|
+
import {
|
|
8
|
+
EntityState,
|
|
9
|
+
FrameData,
|
|
10
|
+
ProtocolPlayerState,
|
|
11
|
+
createEmptyEntityState,
|
|
12
|
+
createEmptyProtocolPlayerState,
|
|
13
|
+
DamageIndicator,
|
|
14
|
+
FogData
|
|
15
|
+
} from '@quake2ts/engine';
|
|
16
|
+
|
|
17
|
+
/**
|
|
18
|
+
* Mocks a server message by just providing the type and data buffer.
|
|
19
|
+
* In a real scenario, this would be constructing a binary packet.
|
|
20
|
+
*/
|
|
21
|
+
export interface MockServerMessage {
|
|
22
|
+
type: number;
|
|
23
|
+
data: Uint8Array;
|
|
24
|
+
}
|
|
25
|
+
|
|
26
|
+
export function createMockServerMessage(type: number, data: Uint8Array = new Uint8Array()): MockServerMessage {
|
|
27
|
+
return { type, data };
|
|
28
|
+
}
|
|
29
|
+
|
|
30
|
+
/**
|
|
31
|
+
* Creates a mock Snapshot (FrameData) for testing.
|
|
32
|
+
*/
|
|
33
|
+
export function createMockSnapshot(
|
|
34
|
+
serverFrame: number,
|
|
35
|
+
entities: EntityState[] = [],
|
|
36
|
+
playerState?: Partial<ProtocolPlayerState>,
|
|
37
|
+
deltaFrame: number = 0
|
|
38
|
+
): FrameData {
|
|
39
|
+
return {
|
|
40
|
+
serverFrame,
|
|
41
|
+
deltaFrame,
|
|
42
|
+
surpressCount: 0,
|
|
43
|
+
areaBytes: 0,
|
|
44
|
+
areaBits: new Uint8Array(),
|
|
45
|
+
playerState: {
|
|
46
|
+
...createEmptyProtocolPlayerState(),
|
|
47
|
+
...playerState
|
|
48
|
+
},
|
|
49
|
+
packetEntities: {
|
|
50
|
+
delta: false,
|
|
51
|
+
entities
|
|
52
|
+
}
|
|
53
|
+
};
|
|
54
|
+
}
|
|
55
|
+
|
|
56
|
+
/**
|
|
57
|
+
* Creates a mock Delta Frame (FrameData with delta flag).
|
|
58
|
+
*/
|
|
59
|
+
export function createMockDeltaFrame(
|
|
60
|
+
serverFrame: number,
|
|
61
|
+
deltaFrame: number,
|
|
62
|
+
entities: EntityState[] = [],
|
|
63
|
+
playerState?: Partial<ProtocolPlayerState>
|
|
64
|
+
): FrameData {
|
|
65
|
+
return {
|
|
66
|
+
serverFrame,
|
|
67
|
+
deltaFrame,
|
|
68
|
+
surpressCount: 0,
|
|
69
|
+
areaBytes: 0,
|
|
70
|
+
areaBits: new Uint8Array(),
|
|
71
|
+
playerState: {
|
|
72
|
+
...createEmptyProtocolPlayerState(),
|
|
73
|
+
...playerState
|
|
74
|
+
},
|
|
75
|
+
packetEntities: {
|
|
76
|
+
delta: true,
|
|
77
|
+
entities
|
|
78
|
+
}
|
|
79
|
+
};
|
|
80
|
+
}
|
|
81
|
+
|
|
82
|
+
/**
|
|
83
|
+
* Simulates network delay for a sequence of messages.
|
|
84
|
+
*/
|
|
85
|
+
export function simulateNetworkDelay<T>(messages: T[], delayMs: number): Promise<T[]> {
|
|
86
|
+
return new Promise(resolve => {
|
|
87
|
+
setTimeout(() => {
|
|
88
|
+
resolve(messages);
|
|
89
|
+
}, delayMs);
|
|
90
|
+
});
|
|
91
|
+
}
|
|
92
|
+
|
|
93
|
+
/**
|
|
94
|
+
* Simulates packet loss by randomly filtering messages.
|
|
95
|
+
* @param messages The messages to process
|
|
96
|
+
* @param lossPercent Percentage of packet loss (0-100)
|
|
97
|
+
*/
|
|
98
|
+
export function simulatePacketLoss<T>(messages: T[], lossPercent: number): T[] {
|
|
99
|
+
return messages.filter(() => Math.random() * 100 >= lossPercent);
|
|
100
|
+
}
|
|
101
|
+
|
|
102
|
+
/**
|
|
103
|
+
* Factory for creating a mock EntityState.
|
|
104
|
+
*/
|
|
105
|
+
export function createMockEntityState(
|
|
106
|
+
number: number,
|
|
107
|
+
modelIndex: number = 0,
|
|
108
|
+
origin: Partial<Vec3> = { x: 0, y: 0, z: 0 },
|
|
109
|
+
overrides?: Partial<EntityState>
|
|
110
|
+
): EntityState {
|
|
111
|
+
const state = createEmptyEntityState();
|
|
112
|
+
state.number = number;
|
|
113
|
+
state.modelindex = modelIndex;
|
|
114
|
+
state.origin.x = origin.x ?? 0;
|
|
115
|
+
state.origin.y = origin.y ?? 0;
|
|
116
|
+
state.origin.z = origin.z ?? 0;
|
|
117
|
+
|
|
118
|
+
if (overrides) {
|
|
119
|
+
Object.assign(state, overrides);
|
|
120
|
+
}
|
|
121
|
+
return state;
|
|
122
|
+
}
|
|
123
|
+
|
|
124
|
+
export function createMockDamageIndicator(
|
|
125
|
+
damage: number,
|
|
126
|
+
dir: Vec3 = { x: 0, y: 0, z: 0 },
|
|
127
|
+
health = true,
|
|
128
|
+
armor = false,
|
|
129
|
+
power = false
|
|
130
|
+
): DamageIndicator {
|
|
131
|
+
return {
|
|
132
|
+
damage,
|
|
133
|
+
dir,
|
|
134
|
+
health,
|
|
135
|
+
armor,
|
|
136
|
+
power
|
|
137
|
+
};
|
|
138
|
+
}
|
|
139
|
+
|
|
140
|
+
export function createMockFogData(overrides: Partial<FogData> = {}): FogData {
|
|
141
|
+
return {
|
|
142
|
+
density: 0.1,
|
|
143
|
+
red: 100,
|
|
144
|
+
green: 100,
|
|
145
|
+
blue: 100,
|
|
146
|
+
...overrides
|
|
147
|
+
};
|
|
148
|
+
}
|
|
@@ -0,0 +1,148 @@
|
|
|
1
|
+
import { EntityState, PmoveTraceResult, Vec3 } from '@quake2ts/shared';
|
|
2
|
+
|
|
3
|
+
// Mock ConfigStringIndex to match shared
|
|
4
|
+
enum ConfigStringIndex {
|
|
5
|
+
Models = 32,
|
|
6
|
+
Sounds = 288,
|
|
7
|
+
Images = 544,
|
|
8
|
+
Players = 544 + 256, // Base for players, usually logic handles offset
|
|
9
|
+
}
|
|
10
|
+
|
|
11
|
+
const MAX_MODELS = 256;
|
|
12
|
+
const MAX_SOUNDS = 256;
|
|
13
|
+
const MAX_IMAGES = 256;
|
|
14
|
+
|
|
15
|
+
// Minimal ClientConfigStrings implementation for testing
|
|
16
|
+
export class MockClientConfigStrings {
|
|
17
|
+
private readonly strings: Map<number, string> = new Map();
|
|
18
|
+
private readonly models: string[] = [];
|
|
19
|
+
private readonly sounds: string[] = [];
|
|
20
|
+
private readonly images: string[] = [];
|
|
21
|
+
|
|
22
|
+
constructor() {}
|
|
23
|
+
|
|
24
|
+
public set(index: number, value: string): void {
|
|
25
|
+
this.strings.set(index, value);
|
|
26
|
+
|
|
27
|
+
if (index >= ConfigStringIndex.Models && index < ConfigStringIndex.Models + MAX_MODELS) {
|
|
28
|
+
this.models[index - ConfigStringIndex.Models] = value;
|
|
29
|
+
} else if (index >= ConfigStringIndex.Sounds && index < ConfigStringIndex.Sounds + MAX_SOUNDS) {
|
|
30
|
+
this.sounds[index - ConfigStringIndex.Sounds] = value;
|
|
31
|
+
} else if (index >= ConfigStringIndex.Images && index < ConfigStringIndex.Images + MAX_IMAGES) {
|
|
32
|
+
this.images[index - ConfigStringIndex.Images] = value;
|
|
33
|
+
}
|
|
34
|
+
}
|
|
35
|
+
|
|
36
|
+
public get(index: number): string | undefined {
|
|
37
|
+
return this.strings.get(index);
|
|
38
|
+
}
|
|
39
|
+
|
|
40
|
+
public getModelName(index: number): string | undefined {
|
|
41
|
+
return this.models[index];
|
|
42
|
+
}
|
|
43
|
+
|
|
44
|
+
public getSoundName(index: number): string | undefined {
|
|
45
|
+
return this.sounds[index];
|
|
46
|
+
}
|
|
47
|
+
|
|
48
|
+
public getImageName(index: number): string | undefined {
|
|
49
|
+
return this.images[index];
|
|
50
|
+
}
|
|
51
|
+
|
|
52
|
+
public getPlayerName(playernum: number): string | undefined {
|
|
53
|
+
// Stub logic
|
|
54
|
+
const info = this.strings.get(ConfigStringIndex.Players + playernum);
|
|
55
|
+
if (!info) return undefined;
|
|
56
|
+
const parts = info.split('\\');
|
|
57
|
+
for (let i = 1; i < parts.length; i += 2) {
|
|
58
|
+
if (parts[i] === 'name') {
|
|
59
|
+
return parts[i + 1];
|
|
60
|
+
}
|
|
61
|
+
}
|
|
62
|
+
return undefined;
|
|
63
|
+
}
|
|
64
|
+
|
|
65
|
+
public clear(): void {
|
|
66
|
+
this.strings.clear();
|
|
67
|
+
this.models.length = 0;
|
|
68
|
+
this.sounds.length = 0;
|
|
69
|
+
this.images.length = 0;
|
|
70
|
+
}
|
|
71
|
+
}
|
|
72
|
+
|
|
73
|
+
// Define ClientState interface locally as it's not exported from client
|
|
74
|
+
export interface ClientStateProvider {
|
|
75
|
+
tickRate: number;
|
|
76
|
+
frameTimeMs: number;
|
|
77
|
+
serverFrame: number;
|
|
78
|
+
serverProtocol: number;
|
|
79
|
+
configStrings: MockClientConfigStrings;
|
|
80
|
+
getClientName(num: number): string;
|
|
81
|
+
getKeyBinding(key: string): string;
|
|
82
|
+
inAutoDemo: boolean;
|
|
83
|
+
}
|
|
84
|
+
|
|
85
|
+
export interface ClientInfo {
|
|
86
|
+
name: string;
|
|
87
|
+
skin: string;
|
|
88
|
+
model: string;
|
|
89
|
+
icon: string;
|
|
90
|
+
}
|
|
91
|
+
|
|
92
|
+
export interface ClientState extends ClientStateProvider {
|
|
93
|
+
playerNum: number;
|
|
94
|
+
serverTime: number;
|
|
95
|
+
parseEntities: number;
|
|
96
|
+
}
|
|
97
|
+
|
|
98
|
+
export interface Frame {
|
|
99
|
+
serverFrame: number;
|
|
100
|
+
deltaFrame: number;
|
|
101
|
+
valid: boolean;
|
|
102
|
+
entities: EntityState[];
|
|
103
|
+
}
|
|
104
|
+
|
|
105
|
+
export interface ConnectionState {
|
|
106
|
+
state: 'disconnected' | 'connecting' | 'connected' | 'active';
|
|
107
|
+
}
|
|
108
|
+
|
|
109
|
+
// -- Factories --
|
|
110
|
+
|
|
111
|
+
export const createMockClientState = (overrides?: Partial<ClientState>): ClientState => {
|
|
112
|
+
const configStrings = new MockClientConfigStrings();
|
|
113
|
+
|
|
114
|
+
return {
|
|
115
|
+
tickRate: 10,
|
|
116
|
+
frameTimeMs: 100,
|
|
117
|
+
serverFrame: 0,
|
|
118
|
+
serverProtocol: 34,
|
|
119
|
+
configStrings,
|
|
120
|
+
playerNum: 0,
|
|
121
|
+
serverTime: 0,
|
|
122
|
+
parseEntities: 0,
|
|
123
|
+
inAutoDemo: false,
|
|
124
|
+
getClientName: (num: number) => `Player${num}`,
|
|
125
|
+
getKeyBinding: (key: string) => '',
|
|
126
|
+
...overrides
|
|
127
|
+
};
|
|
128
|
+
};
|
|
129
|
+
|
|
130
|
+
export const createMockFrame = (overrides?: Partial<Frame>): Frame => ({
|
|
131
|
+
serverFrame: 0,
|
|
132
|
+
deltaFrame: -1,
|
|
133
|
+
valid: true,
|
|
134
|
+
entities: [],
|
|
135
|
+
...overrides
|
|
136
|
+
});
|
|
137
|
+
|
|
138
|
+
export const createMockClientInfo = (overrides?: Partial<ClientInfo>): ClientInfo => ({
|
|
139
|
+
name: 'Player',
|
|
140
|
+
skin: 'male/grunt',
|
|
141
|
+
model: 'male',
|
|
142
|
+
icon: 'pics/icon.pcx',
|
|
143
|
+
...overrides
|
|
144
|
+
});
|
|
145
|
+
|
|
146
|
+
export const createMockConnectionState = (state: ConnectionState['state'] = 'connected'): ConnectionState => ({
|
|
147
|
+
state
|
|
148
|
+
});
|
|
@@ -0,0 +1,47 @@
|
|
|
1
|
+
export type NetworkCondition = 'good' | 'slow' | 'unstable' | 'offline' | 'custom';
|
|
2
|
+
|
|
3
|
+
export interface NetworkSimulator {
|
|
4
|
+
latency: number; // ms
|
|
5
|
+
jitter: number; // ms
|
|
6
|
+
packetLoss: number; // percentage 0-1
|
|
7
|
+
bandwidth: number; // bytes per second
|
|
8
|
+
}
|
|
9
|
+
|
|
10
|
+
/**
|
|
11
|
+
* Simulates network conditions.
|
|
12
|
+
*/
|
|
13
|
+
export function simulateNetworkCondition(condition: NetworkCondition): NetworkSimulator {
|
|
14
|
+
switch (condition) {
|
|
15
|
+
case 'good':
|
|
16
|
+
return { latency: 20, jitter: 5, packetLoss: 0, bandwidth: 10 * 1024 * 1024 };
|
|
17
|
+
case 'slow':
|
|
18
|
+
return { latency: 150, jitter: 20, packetLoss: 0.01, bandwidth: 1 * 1024 * 1024 };
|
|
19
|
+
case 'unstable':
|
|
20
|
+
return { latency: 100, jitter: 100, packetLoss: 0.05, bandwidth: 512 * 1024 };
|
|
21
|
+
case 'offline':
|
|
22
|
+
return { latency: 0, jitter: 0, packetLoss: 1, bandwidth: 0 };
|
|
23
|
+
case 'custom':
|
|
24
|
+
default:
|
|
25
|
+
return { latency: 0, jitter: 0, packetLoss: 0, bandwidth: Infinity };
|
|
26
|
+
}
|
|
27
|
+
}
|
|
28
|
+
|
|
29
|
+
/**
|
|
30
|
+
* Creates a custom network condition.
|
|
31
|
+
*/
|
|
32
|
+
export function createCustomNetworkCondition(latency: number, jitter: number, packetLoss: number): NetworkSimulator {
|
|
33
|
+
return {
|
|
34
|
+
latency,
|
|
35
|
+
jitter,
|
|
36
|
+
packetLoss,
|
|
37
|
+
bandwidth: Infinity // Default to unlimited unless specified
|
|
38
|
+
};
|
|
39
|
+
}
|
|
40
|
+
|
|
41
|
+
/**
|
|
42
|
+
* Helper to throttle bandwidth (e.g. for Playwright).
|
|
43
|
+
*/
|
|
44
|
+
export function throttleBandwidth(bytesPerSecond: number): void {
|
|
45
|
+
// This function would interface with Playwright's CDPSession in an E2E test.
|
|
46
|
+
// It returns void here as a placeholder for the logic to be used within a test context.
|
|
47
|
+
}
|
|
@@ -0,0 +1,90 @@
|
|
|
1
|
+
export interface PlaywrightOptions {
|
|
2
|
+
headless?: boolean;
|
|
3
|
+
viewport?: { width: number; height: number };
|
|
4
|
+
}
|
|
5
|
+
|
|
6
|
+
export interface PlaywrightTestClient {
|
|
7
|
+
page: any; // Type as 'Page' from playwright in real usage, but keeping it generic here to avoid hard dependency on playwright types in this util file if not needed
|
|
8
|
+
browser: any;
|
|
9
|
+
navigate(url: string): Promise<void>;
|
|
10
|
+
waitForGame(): Promise<void>;
|
|
11
|
+
injectInput(type: string, data: any): Promise<void>;
|
|
12
|
+
screenshot(name: string): Promise<Buffer>;
|
|
13
|
+
close(): Promise<void>;
|
|
14
|
+
}
|
|
15
|
+
|
|
16
|
+
/**
|
|
17
|
+
* Creates a Playwright test client.
|
|
18
|
+
* Note: Requires playwright to be installed in the project.
|
|
19
|
+
*/
|
|
20
|
+
export async function createPlaywrightTestClient(options: PlaywrightOptions = {}): Promise<PlaywrightTestClient> {
|
|
21
|
+
// Dynamic import to avoid hard dependency if not used
|
|
22
|
+
let playwright;
|
|
23
|
+
try {
|
|
24
|
+
playwright = await import('playwright');
|
|
25
|
+
} catch (e) {
|
|
26
|
+
throw new Error('Playwright is not installed. Please install it to use this utility.');
|
|
27
|
+
}
|
|
28
|
+
|
|
29
|
+
const browser = await playwright.chromium.launch({
|
|
30
|
+
headless: options.headless ?? true,
|
|
31
|
+
});
|
|
32
|
+
const context = await browser.newContext({
|
|
33
|
+
viewport: options.viewport || { width: 1280, height: 720 },
|
|
34
|
+
});
|
|
35
|
+
const page = await context.newPage();
|
|
36
|
+
|
|
37
|
+
return {
|
|
38
|
+
page,
|
|
39
|
+
browser,
|
|
40
|
+
async navigate(url: string) {
|
|
41
|
+
await page.goto(url);
|
|
42
|
+
},
|
|
43
|
+
async waitForGame() {
|
|
44
|
+
await waitForGameReady(page);
|
|
45
|
+
},
|
|
46
|
+
async injectInput(type: string, data: any) {
|
|
47
|
+
// Simulate input injection via evaluate
|
|
48
|
+
await page.evaluate(({ type, data }: any) => {
|
|
49
|
+
// Assumes a global function or event listener exists to receive injected input
|
|
50
|
+
console.log('Injecting input', type, data);
|
|
51
|
+
// (window as any).game.injectInput(type, data);
|
|
52
|
+
}, { type, data });
|
|
53
|
+
},
|
|
54
|
+
async screenshot(name: string) {
|
|
55
|
+
return await page.screenshot({ path: `${name}.png` });
|
|
56
|
+
},
|
|
57
|
+
async close() {
|
|
58
|
+
await browser.close();
|
|
59
|
+
}
|
|
60
|
+
};
|
|
61
|
+
}
|
|
62
|
+
|
|
63
|
+
/**
|
|
64
|
+
* Waits for the game to be ready.
|
|
65
|
+
*/
|
|
66
|
+
export async function waitForGameReady(page: any, timeout: number = 10000): Promise<void> {
|
|
67
|
+
await page.waitForFunction(() => {
|
|
68
|
+
// Check for some global game state or canvas presence
|
|
69
|
+
return (window as any).game && (window as any).game.isRunning;
|
|
70
|
+
}, { timeout });
|
|
71
|
+
}
|
|
72
|
+
|
|
73
|
+
export interface GameStateCapture {
|
|
74
|
+
time: number;
|
|
75
|
+
entities: number;
|
|
76
|
+
// Add other state properties
|
|
77
|
+
}
|
|
78
|
+
|
|
79
|
+
/**
|
|
80
|
+
* Captures current game state from the browser.
|
|
81
|
+
*/
|
|
82
|
+
export async function captureGameState(page: any): Promise<GameStateCapture> {
|
|
83
|
+
return await page.evaluate(() => {
|
|
84
|
+
const game = (window as any).game;
|
|
85
|
+
return {
|
|
86
|
+
time: game ? game.time : 0,
|
|
87
|
+
entities: game && game.entities ? game.entities.length : 0
|
|
88
|
+
};
|
|
89
|
+
});
|
|
90
|
+
}
|