@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,310 @@
|
|
|
1
|
+
import { vi, type Mock } from 'vitest';
|
|
2
|
+
import { Entity, SpawnRegistry, ScriptHookRegistry, type SpawnContext, type EntitySystem } from '@quake2ts/game';
|
|
3
|
+
import { createRandomGenerator, type Vec3 } from '@quake2ts/shared';
|
|
4
|
+
import { type BspModel } from '@quake2ts/engine';
|
|
5
|
+
import { createTraceMock } from '../shared/collision.js';
|
|
6
|
+
|
|
7
|
+
// Re-export collision helpers from shared collision utility
|
|
8
|
+
export { intersects, stairTrace, ladderTrace, createTraceMock, createSurfaceMock } from '../shared/collision.js';
|
|
9
|
+
|
|
10
|
+
// -- Types --
|
|
11
|
+
|
|
12
|
+
export interface MockEngine {
|
|
13
|
+
sound: Mock<[Entity, number, string, number, number, number], void>;
|
|
14
|
+
soundIndex: Mock<[string], number>;
|
|
15
|
+
modelIndex: Mock<[string], number>;
|
|
16
|
+
centerprintf: Mock<[Entity, string], void>;
|
|
17
|
+
}
|
|
18
|
+
|
|
19
|
+
export interface MockGame {
|
|
20
|
+
random: ReturnType<typeof createRandomGenerator>;
|
|
21
|
+
registerEntitySpawn: Mock<[string, (entity: Entity) => void], void>;
|
|
22
|
+
unregisterEntitySpawn: Mock<[string], void>;
|
|
23
|
+
getCustomEntities: Mock<[], string[]>;
|
|
24
|
+
hooks: ScriptHookRegistry;
|
|
25
|
+
registerHooks: Mock<[any], any>;
|
|
26
|
+
spawnWorld: Mock<[], void>;
|
|
27
|
+
clientBegin: Mock<[any], void>;
|
|
28
|
+
damage: Mock<[number], void>;
|
|
29
|
+
}
|
|
30
|
+
|
|
31
|
+
export interface TestContext extends SpawnContext {
|
|
32
|
+
entities: EntitySystem;
|
|
33
|
+
game: MockGame;
|
|
34
|
+
engine: MockEngine;
|
|
35
|
+
}
|
|
36
|
+
|
|
37
|
+
// -- Factories --
|
|
38
|
+
|
|
39
|
+
export const createMockEngine = (): MockEngine => ({
|
|
40
|
+
sound: vi.fn(),
|
|
41
|
+
soundIndex: vi.fn((sound: string) => 0),
|
|
42
|
+
modelIndex: vi.fn((model: string) => 0),
|
|
43
|
+
centerprintf: vi.fn(),
|
|
44
|
+
});
|
|
45
|
+
|
|
46
|
+
export const createMockGame = (seed: number = 12345): { game: MockGame, spawnRegistry: SpawnRegistry } => {
|
|
47
|
+
const spawnRegistry = new SpawnRegistry();
|
|
48
|
+
const hooks = new ScriptHookRegistry();
|
|
49
|
+
|
|
50
|
+
const game: MockGame = {
|
|
51
|
+
random: createRandomGenerator({ seed }),
|
|
52
|
+
registerEntitySpawn: vi.fn((classname: string, spawnFunc: (entity: Entity) => void) => {
|
|
53
|
+
spawnRegistry.register(classname, (entity) => spawnFunc(entity));
|
|
54
|
+
}),
|
|
55
|
+
unregisterEntitySpawn: vi.fn((classname: string) => {
|
|
56
|
+
spawnRegistry.unregister(classname);
|
|
57
|
+
}),
|
|
58
|
+
getCustomEntities: vi.fn(() => Array.from(spawnRegistry.keys())),
|
|
59
|
+
hooks,
|
|
60
|
+
registerHooks: vi.fn((newHooks) => hooks.register(newHooks)),
|
|
61
|
+
spawnWorld: vi.fn(() => {
|
|
62
|
+
hooks.onMapLoad('q2dm1');
|
|
63
|
+
}),
|
|
64
|
+
clientBegin: vi.fn((client) => {
|
|
65
|
+
hooks.onPlayerSpawn({} as any);
|
|
66
|
+
}),
|
|
67
|
+
damage: vi.fn((amount: number) => {
|
|
68
|
+
hooks.onDamage({} as any, null, null, amount, 0, 0);
|
|
69
|
+
})
|
|
70
|
+
};
|
|
71
|
+
|
|
72
|
+
return { game, spawnRegistry };
|
|
73
|
+
};
|
|
74
|
+
|
|
75
|
+
export function createTestContext(options?: { seed?: number, initialEntities?: Entity[] }): TestContext {
|
|
76
|
+
const engine = createMockEngine();
|
|
77
|
+
const seed = options?.seed ?? 12345;
|
|
78
|
+
const { game, spawnRegistry } = createMockGame(seed);
|
|
79
|
+
|
|
80
|
+
const traceFn = vi.fn((start: Vec3, end: Vec3, mins?: Vec3, maxs?: Vec3) =>
|
|
81
|
+
createTraceMock({
|
|
82
|
+
endpos: end,
|
|
83
|
+
plane: { normal: { x: 0, y: 0, z: 1 }, dist: 0, type: 0, signbits: 0 }
|
|
84
|
+
})
|
|
85
|
+
);
|
|
86
|
+
|
|
87
|
+
const entityList: Entity[] = options?.initialEntities ? [...options.initialEntities] : [];
|
|
88
|
+
|
|
89
|
+
// Create hooks helper that interacts with the entity list
|
|
90
|
+
const hooks = game.hooks;
|
|
91
|
+
|
|
92
|
+
// We need to store the registry reference to implement registerEntityClass/getSpawnFunction
|
|
93
|
+
let currentSpawnRegistry: SpawnRegistry | undefined;
|
|
94
|
+
|
|
95
|
+
const entities = {
|
|
96
|
+
spawn: vi.fn(() => {
|
|
97
|
+
const ent = new Entity(entityList.length + 1);
|
|
98
|
+
ent.inUse = true;
|
|
99
|
+
entityList.push(ent);
|
|
100
|
+
hooks.onEntitySpawn(ent);
|
|
101
|
+
return ent;
|
|
102
|
+
}),
|
|
103
|
+
free: vi.fn((ent: Entity) => {
|
|
104
|
+
const idx = entityList.indexOf(ent);
|
|
105
|
+
if (idx !== -1) {
|
|
106
|
+
entityList.splice(idx, 1);
|
|
107
|
+
}
|
|
108
|
+
ent.inUse = false;
|
|
109
|
+
hooks.onEntityRemove(ent);
|
|
110
|
+
}),
|
|
111
|
+
finalizeSpawn: vi.fn(),
|
|
112
|
+
freeImmediate: vi.fn((ent: Entity) => {
|
|
113
|
+
const idx = entityList.indexOf(ent);
|
|
114
|
+
if (idx !== -1) {
|
|
115
|
+
entityList.splice(idx, 1);
|
|
116
|
+
}
|
|
117
|
+
ent.inUse = false;
|
|
118
|
+
}),
|
|
119
|
+
setSpawnRegistry: vi.fn((registry: SpawnRegistry) => {
|
|
120
|
+
currentSpawnRegistry = registry;
|
|
121
|
+
}),
|
|
122
|
+
registerEntityClass: vi.fn((classname: string, factory: any) => {
|
|
123
|
+
if (currentSpawnRegistry) {
|
|
124
|
+
currentSpawnRegistry.register(classname, factory);
|
|
125
|
+
}
|
|
126
|
+
}),
|
|
127
|
+
getSpawnFunction: vi.fn((classname: string) => {
|
|
128
|
+
return currentSpawnRegistry?.get(classname);
|
|
129
|
+
}),
|
|
130
|
+
timeSeconds: 10,
|
|
131
|
+
deltaSeconds: 0.1,
|
|
132
|
+
modelIndex: vi.fn(() => 0),
|
|
133
|
+
scheduleThink: vi.fn((entity: Entity, time: number) => {
|
|
134
|
+
entity.nextthink = time;
|
|
135
|
+
}),
|
|
136
|
+
linkentity: vi.fn(),
|
|
137
|
+
trace: traceFn,
|
|
138
|
+
pointcontents: vi.fn(() => 0),
|
|
139
|
+
multicast: vi.fn(),
|
|
140
|
+
unicast: vi.fn(),
|
|
141
|
+
engine,
|
|
142
|
+
scriptHooks: hooks,
|
|
143
|
+
game,
|
|
144
|
+
sound: vi.fn((ent: Entity, chan: number, sound: string, vol: number, attn: number, timeofs: number) => {
|
|
145
|
+
engine.sound(ent, chan, sound, vol, attn, timeofs);
|
|
146
|
+
}),
|
|
147
|
+
soundIndex: vi.fn((sound: string) => engine.soundIndex(sound)),
|
|
148
|
+
useTargets: vi.fn((entity: Entity, activator: Entity | null) => {
|
|
149
|
+
}),
|
|
150
|
+
findByTargetName: vi.fn(() => []),
|
|
151
|
+
pickTarget: vi.fn(() => null),
|
|
152
|
+
killBox: vi.fn(),
|
|
153
|
+
rng: createRandomGenerator({ seed }),
|
|
154
|
+
imports: {
|
|
155
|
+
configstring: vi.fn(),
|
|
156
|
+
trace: traceFn,
|
|
157
|
+
pointcontents: vi.fn(() => 0),
|
|
158
|
+
},
|
|
159
|
+
level: {
|
|
160
|
+
intermission_angle: { x: 0, y: 0, z: 0 },
|
|
161
|
+
intermission_origin: { x: 0, y: 0, z: 0 },
|
|
162
|
+
next_auto_save: 0,
|
|
163
|
+
health_bar_entities: null
|
|
164
|
+
},
|
|
165
|
+
targetNameIndex: new Map(),
|
|
166
|
+
forEachEntity: vi.fn((callback: (ent: Entity) => void) => {
|
|
167
|
+
entityList.forEach(callback);
|
|
168
|
+
}),
|
|
169
|
+
find: vi.fn((predicate: (ent: Entity) => boolean) => {
|
|
170
|
+
return entityList.find(predicate);
|
|
171
|
+
}),
|
|
172
|
+
findByClassname: vi.fn((classname: string) => {
|
|
173
|
+
return entityList.find(e => e.classname === classname);
|
|
174
|
+
}),
|
|
175
|
+
beginFrame: vi.fn((timeSeconds: number) => {
|
|
176
|
+
(entities as any).timeSeconds = timeSeconds;
|
|
177
|
+
}),
|
|
178
|
+
targetAwareness: {
|
|
179
|
+
timeSeconds: 10,
|
|
180
|
+
frameNumber: 1,
|
|
181
|
+
sightEntity: null,
|
|
182
|
+
soundEntity: null,
|
|
183
|
+
},
|
|
184
|
+
warn: vi.fn(), // Added warn to entities as it is sometimes used there too, though typically on SpawnContext
|
|
185
|
+
// Adding missing properties to satisfy EntitySystem interface partially or fully
|
|
186
|
+
// We cast to unknown first anyway, but filling these in makes it safer for consumers
|
|
187
|
+
skill: 1,
|
|
188
|
+
deathmatch: false,
|
|
189
|
+
coop: false,
|
|
190
|
+
activeCount: entityList.length,
|
|
191
|
+
world: entityList.find(e => e.classname === 'worldspawn') || new Entity(0),
|
|
192
|
+
// ... other EntitySystem properties would go here
|
|
193
|
+
} as unknown as EntitySystem;
|
|
194
|
+
|
|
195
|
+
return {
|
|
196
|
+
keyValues: {},
|
|
197
|
+
entities,
|
|
198
|
+
game,
|
|
199
|
+
engine,
|
|
200
|
+
health_multiplier: 1,
|
|
201
|
+
warn: vi.fn(),
|
|
202
|
+
free: vi.fn(),
|
|
203
|
+
// Mock precache functions if they are part of SpawnContext in future or TestContext extensions
|
|
204
|
+
precacheModel: vi.fn(),
|
|
205
|
+
precacheSound: vi.fn(),
|
|
206
|
+
precacheImage: vi.fn(),
|
|
207
|
+
} as unknown as TestContext;
|
|
208
|
+
}
|
|
209
|
+
|
|
210
|
+
export function createSpawnTestContext(mapName?: string): TestContext {
|
|
211
|
+
const ctx = createTestContext();
|
|
212
|
+
// Simulate map load if needed
|
|
213
|
+
if (mapName) {
|
|
214
|
+
ctx.game.spawnWorld();
|
|
215
|
+
}
|
|
216
|
+
return ctx;
|
|
217
|
+
}
|
|
218
|
+
|
|
219
|
+
export function createCombatTestContext(): TestContext {
|
|
220
|
+
return createTestContext();
|
|
221
|
+
}
|
|
222
|
+
|
|
223
|
+
export function createPhysicsTestContext(bspModel?: BspModel): TestContext {
|
|
224
|
+
const context = createTestContext();
|
|
225
|
+
|
|
226
|
+
if (bspModel) {
|
|
227
|
+
// If a BSP model is provided, we can set up the trace mock to be more realistic.
|
|
228
|
+
// For now, we'll just store the model on the context if we extended TestContext,
|
|
229
|
+
// but the task specifically asks to "Include collision world, traces".
|
|
230
|
+
|
|
231
|
+
// In a real scenario, we might want to hook up a real BSP trace function here
|
|
232
|
+
// or a mock that uses the BSP data.
|
|
233
|
+
// Since we don't have a full BSP physics engine mock ready to drop in,
|
|
234
|
+
// we will stick with the default trace mock which is already set up in createTestContext,
|
|
235
|
+
// but we acknowledge the bspModel parameter for future expansion where we might
|
|
236
|
+
// use it to seed the trace results.
|
|
237
|
+
}
|
|
238
|
+
|
|
239
|
+
return context;
|
|
240
|
+
}
|
|
241
|
+
|
|
242
|
+
export function createEntity(): Entity {
|
|
243
|
+
return new Entity(1);
|
|
244
|
+
}
|
|
245
|
+
|
|
246
|
+
/**
|
|
247
|
+
* Creates mock imports and engine for use with createGame() from @quake2ts/game.
|
|
248
|
+
* This is a convenience helper that provides all the commonly mocked functions
|
|
249
|
+
* needed to instantiate a real Game instance in tests.
|
|
250
|
+
*
|
|
251
|
+
* @param overrides Optional overrides for specific mock functions
|
|
252
|
+
* @returns An object containing both imports and engine mocks
|
|
253
|
+
*
|
|
254
|
+
* @example
|
|
255
|
+
* ```typescript
|
|
256
|
+
* import { createGame } from '@quake2ts/game';
|
|
257
|
+
* import { createGameImportsAndEngine } from '@quake2ts/test-utils';
|
|
258
|
+
*
|
|
259
|
+
* const { imports, engine } = createGameImportsAndEngine();
|
|
260
|
+
* const game = createGame(imports, engine, { gravity: { x: 0, y: 0, z: -800 } });
|
|
261
|
+
* ```
|
|
262
|
+
*/
|
|
263
|
+
export function createGameImportsAndEngine(overrides?: {
|
|
264
|
+
imports?: Partial<{
|
|
265
|
+
trace: Mock;
|
|
266
|
+
pointcontents: Mock;
|
|
267
|
+
linkentity: Mock;
|
|
268
|
+
multicast: Mock;
|
|
269
|
+
unicast: Mock;
|
|
270
|
+
}>;
|
|
271
|
+
engine?: Partial<{
|
|
272
|
+
trace: Mock;
|
|
273
|
+
sound: Mock;
|
|
274
|
+
centerprintf: Mock;
|
|
275
|
+
modelIndex: Mock;
|
|
276
|
+
soundIndex: Mock;
|
|
277
|
+
}>;
|
|
278
|
+
}) {
|
|
279
|
+
// Default trace result - matches the pattern from original monster tests
|
|
280
|
+
const defaultTraceResult = {
|
|
281
|
+
fraction: 1.0,
|
|
282
|
+
endpos: { x: 0, y: 0, z: 0 },
|
|
283
|
+
allsolid: false,
|
|
284
|
+
startsolid: false,
|
|
285
|
+
plane: { normal: { x: 0, y: 0, z: 1 }, dist: 0, type: 0, signbits: 0 },
|
|
286
|
+
ent: null,
|
|
287
|
+
};
|
|
288
|
+
|
|
289
|
+
const defaultTrace = vi.fn().mockReturnValue(defaultTraceResult);
|
|
290
|
+
|
|
291
|
+
const imports = {
|
|
292
|
+
trace: defaultTrace,
|
|
293
|
+
pointcontents: vi.fn().mockReturnValue(0),
|
|
294
|
+
linkentity: vi.fn(),
|
|
295
|
+
multicast: vi.fn(),
|
|
296
|
+
unicast: vi.fn(),
|
|
297
|
+
...overrides?.imports,
|
|
298
|
+
};
|
|
299
|
+
|
|
300
|
+
const engine = {
|
|
301
|
+
trace: vi.fn().mockReturnValue(defaultTraceResult),
|
|
302
|
+
sound: vi.fn(),
|
|
303
|
+
centerprintf: vi.fn(),
|
|
304
|
+
modelIndex: vi.fn().mockReturnValue(1),
|
|
305
|
+
soundIndex: vi.fn().mockReturnValue(1),
|
|
306
|
+
...overrides?.engine,
|
|
307
|
+
};
|
|
308
|
+
|
|
309
|
+
return { imports, engine };
|
|
310
|
+
}
|
|
@@ -0,0 +1,67 @@
|
|
|
1
|
+
import { vi, type Mock } from 'vitest';
|
|
2
|
+
import { Entity, EntitySystem, MonsterMove, MonsterAction, AIAction } from '@quake2ts/game';
|
|
3
|
+
|
|
4
|
+
export interface MockAI {
|
|
5
|
+
checkAttack: Mock;
|
|
6
|
+
findTarget: Mock;
|
|
7
|
+
visible: Mock;
|
|
8
|
+
infront: Mock;
|
|
9
|
+
}
|
|
10
|
+
|
|
11
|
+
export interface MockMonsterAI {
|
|
12
|
+
stand: Mock;
|
|
13
|
+
walk: Mock;
|
|
14
|
+
run: Mock;
|
|
15
|
+
dodge: Mock;
|
|
16
|
+
attack: Mock;
|
|
17
|
+
melee: Mock;
|
|
18
|
+
sight: Mock;
|
|
19
|
+
idle: Mock;
|
|
20
|
+
}
|
|
21
|
+
|
|
22
|
+
export function createMockAI(overrides: Partial<MockAI> = {}): MockAI {
|
|
23
|
+
return {
|
|
24
|
+
checkAttack: vi.fn(() => false),
|
|
25
|
+
findTarget: vi.fn(() => null),
|
|
26
|
+
visible: vi.fn(() => true),
|
|
27
|
+
infront: vi.fn(() => true),
|
|
28
|
+
...overrides
|
|
29
|
+
};
|
|
30
|
+
}
|
|
31
|
+
|
|
32
|
+
export function createMockMonsterAI(overrides: Partial<MockMonsterAI> = {}): MockMonsterAI {
|
|
33
|
+
return {
|
|
34
|
+
stand: vi.fn(),
|
|
35
|
+
walk: vi.fn(),
|
|
36
|
+
run: vi.fn(),
|
|
37
|
+
dodge: vi.fn(),
|
|
38
|
+
attack: vi.fn(),
|
|
39
|
+
melee: vi.fn(),
|
|
40
|
+
sight: vi.fn(),
|
|
41
|
+
idle: vi.fn(),
|
|
42
|
+
...overrides
|
|
43
|
+
};
|
|
44
|
+
}
|
|
45
|
+
|
|
46
|
+
export function createMockMonsterMove(
|
|
47
|
+
first: number,
|
|
48
|
+
last: number,
|
|
49
|
+
think: (self: Entity, context: EntitySystem) => void,
|
|
50
|
+
action: (self: Entity, dist: number, context: EntitySystem) => void
|
|
51
|
+
): MonsterMove {
|
|
52
|
+
const frames = [];
|
|
53
|
+
for (let i = first; i <= last; i++) {
|
|
54
|
+
frames.push({
|
|
55
|
+
ai: action,
|
|
56
|
+
dist: 0,
|
|
57
|
+
think: think
|
|
58
|
+
});
|
|
59
|
+
}
|
|
60
|
+
|
|
61
|
+
return {
|
|
62
|
+
firstframe: first,
|
|
63
|
+
lastframe: last,
|
|
64
|
+
frames,
|
|
65
|
+
endfunc: null
|
|
66
|
+
};
|
|
67
|
+
}
|
|
@@ -0,0 +1,61 @@
|
|
|
1
|
+
import { vi, type Mock } from 'vitest';
|
|
2
|
+
import { DamageMod, Entity, EntitySystem, WeaponState } from '@quake2ts/game';
|
|
3
|
+
|
|
4
|
+
export interface MockDamageInfo {
|
|
5
|
+
damage: number;
|
|
6
|
+
mod: DamageMod;
|
|
7
|
+
knockback: number;
|
|
8
|
+
attacker: Entity | null;
|
|
9
|
+
inflictor: Entity | null;
|
|
10
|
+
dir: { x: number, y: number, z: number } | null;
|
|
11
|
+
point: { x: number, y: number, z: number } | null;
|
|
12
|
+
}
|
|
13
|
+
|
|
14
|
+
export function createMockDamageInfo(overrides: Partial<MockDamageInfo> = {}): MockDamageInfo {
|
|
15
|
+
return {
|
|
16
|
+
damage: 10,
|
|
17
|
+
mod: DamageMod.UNKNOWN,
|
|
18
|
+
knockback: 0,
|
|
19
|
+
attacker: null,
|
|
20
|
+
inflictor: null,
|
|
21
|
+
dir: null,
|
|
22
|
+
point: null,
|
|
23
|
+
...overrides
|
|
24
|
+
};
|
|
25
|
+
}
|
|
26
|
+
|
|
27
|
+
const WEAPON_NAMES: Record<string, string> = {
|
|
28
|
+
'weapon_blaster': 'Blaster',
|
|
29
|
+
'weapon_shotgun': 'Shotgun',
|
|
30
|
+
'weapon_supershotgun': 'Super Shotgun',
|
|
31
|
+
'weapon_machinegun': 'Machinegun',
|
|
32
|
+
'weapon_chaingun': 'Chaingun',
|
|
33
|
+
'weapon_grenadelauncher': 'Grenade Launcher',
|
|
34
|
+
'weapon_rocketlauncher': 'Rocket Launcher',
|
|
35
|
+
'weapon_hyperblaster': 'HyperBlaster',
|
|
36
|
+
'weapon_railgun': 'Railgun',
|
|
37
|
+
'weapon_bfg': 'BFG10K',
|
|
38
|
+
};
|
|
39
|
+
|
|
40
|
+
export function createMockWeapon(name: string = 'Mock Weapon') {
|
|
41
|
+
const displayName = WEAPON_NAMES[name] || name;
|
|
42
|
+
return {
|
|
43
|
+
name: displayName,
|
|
44
|
+
ammoType: 'bullets',
|
|
45
|
+
ammoUse: 1,
|
|
46
|
+
selection: vi.fn(),
|
|
47
|
+
think: vi.fn(),
|
|
48
|
+
command: vi.fn(),
|
|
49
|
+
};
|
|
50
|
+
}
|
|
51
|
+
|
|
52
|
+
export const mockMonsterAttacks = {
|
|
53
|
+
fireBlaster: vi.fn(),
|
|
54
|
+
fireRocket: vi.fn(),
|
|
55
|
+
fireGrenade: vi.fn(),
|
|
56
|
+
fireHeat: vi.fn(),
|
|
57
|
+
fireBullet: vi.fn(),
|
|
58
|
+
fireShotgun: vi.fn(),
|
|
59
|
+
fireRailgun: vi.fn(),
|
|
60
|
+
fireBFG: vi.fn(),
|
|
61
|
+
};
|
|
@@ -0,0 +1,166 @@
|
|
|
1
|
+
import {
|
|
2
|
+
type PlayerInventory,
|
|
3
|
+
createPlayerInventory,
|
|
4
|
+
WeaponId,
|
|
5
|
+
PowerupId,
|
|
6
|
+
KeyId,
|
|
7
|
+
createAmmoInventory
|
|
8
|
+
} from '@quake2ts/game';
|
|
9
|
+
import {
|
|
10
|
+
type BaseItem,
|
|
11
|
+
type WeaponItem,
|
|
12
|
+
type HealthItem,
|
|
13
|
+
type ArmorItem,
|
|
14
|
+
type PowerupItem,
|
|
15
|
+
type PowerArmorItem,
|
|
16
|
+
type KeyItem,
|
|
17
|
+
type FlagItem,
|
|
18
|
+
WEAPON_ITEMS,
|
|
19
|
+
HEALTH_ITEMS,
|
|
20
|
+
ARMOR_ITEMS,
|
|
21
|
+
POWERUP_ITEMS,
|
|
22
|
+
POWER_ARMOR_ITEMS,
|
|
23
|
+
KEY_ITEMS,
|
|
24
|
+
FLAG_ITEMS,
|
|
25
|
+
getAmmoItemDefinition,
|
|
26
|
+
AmmoItemId
|
|
27
|
+
} from '@quake2ts/game';
|
|
28
|
+
// import { getAmmoItemDefinition, AmmoItemId } from '@quake2ts/game/src/inventory/ammo.js';
|
|
29
|
+
|
|
30
|
+
/**
|
|
31
|
+
* Creates a mock player inventory with default values suitable for testing.
|
|
32
|
+
* Can be customized with overrides.
|
|
33
|
+
*/
|
|
34
|
+
export function createMockInventory(overrides: Partial<PlayerInventory> = {}): PlayerInventory {
|
|
35
|
+
const defaultInventory = createPlayerInventory();
|
|
36
|
+
|
|
37
|
+
// Merge simple properties
|
|
38
|
+
const inventory: PlayerInventory = {
|
|
39
|
+
...defaultInventory,
|
|
40
|
+
...overrides
|
|
41
|
+
};
|
|
42
|
+
|
|
43
|
+
// If overrides.ammo is provided (as a full object), it replaces the default.
|
|
44
|
+
// We don't merge deeper here because the caller usually provides a complete mock
|
|
45
|
+
// or is happy with the default structure.
|
|
46
|
+
|
|
47
|
+
return inventory;
|
|
48
|
+
}
|
|
49
|
+
|
|
50
|
+
/**
|
|
51
|
+
* Generic factory for any item type.
|
|
52
|
+
* Attempts to find a predefined item by ID first, then applies overrides.
|
|
53
|
+
*/
|
|
54
|
+
export function createMockItem(id: string, overrides: Partial<BaseItem> = {}): BaseItem {
|
|
55
|
+
let base: BaseItem | undefined;
|
|
56
|
+
|
|
57
|
+
// Search in all registries
|
|
58
|
+
base = WEAPON_ITEMS[id] ||
|
|
59
|
+
HEALTH_ITEMS[id] ||
|
|
60
|
+
ARMOR_ITEMS[id] ||
|
|
61
|
+
POWERUP_ITEMS[id] ||
|
|
62
|
+
POWER_ARMOR_ITEMS[id] ||
|
|
63
|
+
KEY_ITEMS[id] ||
|
|
64
|
+
FLAG_ITEMS[id];
|
|
65
|
+
|
|
66
|
+
if (!base) {
|
|
67
|
+
// If not found, create a generic minimal item
|
|
68
|
+
base = {
|
|
69
|
+
id,
|
|
70
|
+
name: `Mock Item ${id}`
|
|
71
|
+
};
|
|
72
|
+
}
|
|
73
|
+
|
|
74
|
+
return {
|
|
75
|
+
...base,
|
|
76
|
+
...overrides
|
|
77
|
+
};
|
|
78
|
+
}
|
|
79
|
+
|
|
80
|
+
/**
|
|
81
|
+
* Creates a mock WeaponItem
|
|
82
|
+
*/
|
|
83
|
+
export function createMockWeaponItem(weaponId: WeaponId, overrides: Partial<WeaponItem> = {}): WeaponItem {
|
|
84
|
+
// Find the item definition for this weaponId
|
|
85
|
+
const found = Object.values(WEAPON_ITEMS).find(w => w.weaponId === weaponId);
|
|
86
|
+
|
|
87
|
+
const base: WeaponItem = found ? { ...found } : {
|
|
88
|
+
type: 'weapon',
|
|
89
|
+
id: `weapon_${weaponId}`,
|
|
90
|
+
name: `Mock Weapon ${weaponId}`,
|
|
91
|
+
weaponId,
|
|
92
|
+
ammoType: null,
|
|
93
|
+
initialAmmo: 0,
|
|
94
|
+
pickupAmmo: 0,
|
|
95
|
+
fireRate: 1
|
|
96
|
+
};
|
|
97
|
+
|
|
98
|
+
return { ...base, ...overrides };
|
|
99
|
+
}
|
|
100
|
+
|
|
101
|
+
/**
|
|
102
|
+
* Creates a mock HealthItem
|
|
103
|
+
*/
|
|
104
|
+
export function createMockHealthItem(amount: number, overrides: Partial<HealthItem> = {}): HealthItem {
|
|
105
|
+
return {
|
|
106
|
+
type: 'health',
|
|
107
|
+
id: 'item_health_mock',
|
|
108
|
+
name: 'Mock Health',
|
|
109
|
+
amount,
|
|
110
|
+
max: 100,
|
|
111
|
+
...overrides
|
|
112
|
+
};
|
|
113
|
+
}
|
|
114
|
+
|
|
115
|
+
/**
|
|
116
|
+
* Creates a mock ArmorItem
|
|
117
|
+
*/
|
|
118
|
+
export function createMockArmorItem(amount: number, overrides: Partial<ArmorItem> = {}): ArmorItem {
|
|
119
|
+
return {
|
|
120
|
+
type: 'armor',
|
|
121
|
+
id: 'item_armor_mock',
|
|
122
|
+
name: 'Mock Armor',
|
|
123
|
+
amount,
|
|
124
|
+
...overrides
|
|
125
|
+
};
|
|
126
|
+
}
|
|
127
|
+
|
|
128
|
+
/**
|
|
129
|
+
* Creates a mock AmmoItem
|
|
130
|
+
*/
|
|
131
|
+
export function createMockAmmoItem(ammoItemId: AmmoItemId, overrides: Partial<BaseItem> = {}): BaseItem {
|
|
132
|
+
const def = getAmmoItemDefinition(ammoItemId);
|
|
133
|
+
if (!def) {
|
|
134
|
+
throw new Error(`Unknown ammo item id: ${ammoItemId}`);
|
|
135
|
+
}
|
|
136
|
+
|
|
137
|
+
const base: BaseItem = {
|
|
138
|
+
id: def.id,
|
|
139
|
+
name: `Mock Ammo ${def.id}`
|
|
140
|
+
};
|
|
141
|
+
|
|
142
|
+
return {
|
|
143
|
+
...base,
|
|
144
|
+
...overrides
|
|
145
|
+
};
|
|
146
|
+
}
|
|
147
|
+
|
|
148
|
+
|
|
149
|
+
/**
|
|
150
|
+
* Creates a mock PowerupItem
|
|
151
|
+
*/
|
|
152
|
+
export function createMockPowerupItem(id: string, duration: number, overrides: Partial<PowerupItem> = {}): PowerupItem {
|
|
153
|
+
const found = POWERUP_ITEMS[id];
|
|
154
|
+
const base: PowerupItem = found ? { ...found } : {
|
|
155
|
+
type: 'powerup',
|
|
156
|
+
id,
|
|
157
|
+
name: `Mock Powerup ${id}`,
|
|
158
|
+
timer: duration
|
|
159
|
+
};
|
|
160
|
+
|
|
161
|
+
if (duration !== undefined && !found) {
|
|
162
|
+
base.timer = duration;
|
|
163
|
+
}
|
|
164
|
+
|
|
165
|
+
return { ...base, ...overrides };
|
|
166
|
+
}
|
|
@@ -0,0 +1,105 @@
|
|
|
1
|
+
import { Entity, GameExports, GameImports } from '@quake2ts/game';
|
|
2
|
+
import { vi } from 'vitest';
|
|
3
|
+
|
|
4
|
+
/**
|
|
5
|
+
* Interface for mock GameState.
|
|
6
|
+
*/
|
|
7
|
+
export interface GameState {
|
|
8
|
+
levelName: string;
|
|
9
|
+
time: number;
|
|
10
|
+
entities: Entity[];
|
|
11
|
+
clients: any[]; // Mock client objects
|
|
12
|
+
}
|
|
13
|
+
|
|
14
|
+
/**
|
|
15
|
+
* Creates a mock game state object.
|
|
16
|
+
* @param overrides Optional overrides for the game state.
|
|
17
|
+
*/
|
|
18
|
+
export function createMockGameState(overrides?: Partial<GameState>): GameState {
|
|
19
|
+
return {
|
|
20
|
+
levelName: 'test_level',
|
|
21
|
+
time: 0,
|
|
22
|
+
entities: [],
|
|
23
|
+
clients: [],
|
|
24
|
+
...overrides
|
|
25
|
+
};
|
|
26
|
+
}
|
|
27
|
+
|
|
28
|
+
/**
|
|
29
|
+
* Creates a mock GameExports object.
|
|
30
|
+
*/
|
|
31
|
+
export function createMockGameExports(overrides?: Partial<GameExports>): GameExports {
|
|
32
|
+
return {
|
|
33
|
+
init: vi.fn(),
|
|
34
|
+
shutdown: vi.fn(),
|
|
35
|
+
spawnWorld: vi.fn(),
|
|
36
|
+
frame: vi.fn().mockReturnValue({ state: {} }),
|
|
37
|
+
clientConnect: vi.fn().mockReturnValue(true),
|
|
38
|
+
clientBegin: vi.fn().mockReturnValue({ index: 1, origin: { x: 0, y: 0, z: 0 } }),
|
|
39
|
+
clientDisconnect: vi.fn(),
|
|
40
|
+
clientThink: vi.fn(),
|
|
41
|
+
respawn: vi.fn(),
|
|
42
|
+
entities: {
|
|
43
|
+
getByIndex: vi.fn(),
|
|
44
|
+
forEachEntity: vi.fn(),
|
|
45
|
+
findByRadius: vi.fn(),
|
|
46
|
+
find: vi.fn(),
|
|
47
|
+
checkAnyCollision: vi.fn(),
|
|
48
|
+
trace: vi.fn(),
|
|
49
|
+
pointcontents: vi.fn(),
|
|
50
|
+
link: vi.fn(),
|
|
51
|
+
unlink: vi.fn(),
|
|
52
|
+
spawn: vi.fn(),
|
|
53
|
+
free: vi.fn(),
|
|
54
|
+
activeCount: 0,
|
|
55
|
+
world: { classname: 'worldspawn' } as any,
|
|
56
|
+
} as any,
|
|
57
|
+
multicast: vi.fn(),
|
|
58
|
+
unicast: vi.fn(),
|
|
59
|
+
configstring: vi.fn(),
|
|
60
|
+
serverCommand: vi.fn(),
|
|
61
|
+
sound: vi.fn(),
|
|
62
|
+
soundIndex: vi.fn(),
|
|
63
|
+
centerprintf: vi.fn(),
|
|
64
|
+
trace: vi.fn(),
|
|
65
|
+
time: 0,
|
|
66
|
+
deathmatch: false,
|
|
67
|
+
skill: 1,
|
|
68
|
+
rogue: false,
|
|
69
|
+
xatrix: false,
|
|
70
|
+
coop: false,
|
|
71
|
+
friendlyFire: false,
|
|
72
|
+
random: {
|
|
73
|
+
next: vi.fn(),
|
|
74
|
+
nextFloat: vi.fn(),
|
|
75
|
+
range: vi.fn(),
|
|
76
|
+
crandom: vi.fn(),
|
|
77
|
+
getState: vi.fn(),
|
|
78
|
+
setState: vi.fn()
|
|
79
|
+
} as any,
|
|
80
|
+
createSave: vi.fn(),
|
|
81
|
+
loadSave: vi.fn(),
|
|
82
|
+
serialize: vi.fn(),
|
|
83
|
+
loadState: vi.fn(),
|
|
84
|
+
setGodMode: vi.fn(),
|
|
85
|
+
setNoclip: vi.fn(),
|
|
86
|
+
setNotarget: vi.fn(),
|
|
87
|
+
giveItem: vi.fn(),
|
|
88
|
+
damage: vi.fn(),
|
|
89
|
+
teleport: vi.fn(),
|
|
90
|
+
registerHooks: vi.fn(),
|
|
91
|
+
hooks: {
|
|
92
|
+
onMapLoad: vi.fn(),
|
|
93
|
+
onMapUnload: vi.fn(),
|
|
94
|
+
onPlayerSpawn: vi.fn(),
|
|
95
|
+
onPlayerDeath: vi.fn(),
|
|
96
|
+
register: vi.fn(),
|
|
97
|
+
onPickup: vi.fn(), // Added onPickup mock
|
|
98
|
+
} as any,
|
|
99
|
+
setSpectator: vi.fn(),
|
|
100
|
+
registerEntitySpawn: vi.fn(),
|
|
101
|
+
unregisterEntitySpawn: vi.fn(),
|
|
102
|
+
getCustomEntities: vi.fn(),
|
|
103
|
+
...overrides
|
|
104
|
+
};
|
|
105
|
+
}
|