quake2ts 0.0.557 → 0.0.561
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/package.json +3 -1
- package/packages/client/dist/browser/index.global.js +12 -12
- package/packages/client/dist/browser/index.global.js.map +1 -1
- package/packages/client/dist/cjs/index.cjs +70 -0
- package/packages/client/dist/cjs/index.cjs.map +1 -1
- package/packages/client/dist/esm/index.js +70 -0
- package/packages/client/dist/esm/index.js.map +1 -1
- package/packages/client/dist/tsconfig.tsbuildinfo +1 -1
- package/packages/client/dist/types/index.d.ts.map +1 -1
- package/packages/engine/dist/tsconfig.tsbuildinfo +1 -1
- package/packages/engine/dist/types/render/bloom.d.ts +19 -0
- package/packages/engine/dist/types/render/bloom.d.ts.map +1 -0
- package/packages/engine/dist/types/render/frame.d.ts +2 -0
- package/packages/engine/dist/types/render/frame.d.ts.map +1 -1
- package/packages/engine/dist/types/render/renderer.d.ts +2 -0
- package/packages/engine/dist/types/render/renderer.d.ts.map +1 -1
- package/packages/game/dist/tsconfig.tsbuildinfo +1 -1
- package/packages/shared/dist/tsconfig.tsbuildinfo +1 -1
- package/packages/test-utils/dist/index.cjs +677 -693
- package/packages/test-utils/dist/index.cjs.map +1 -1
- package/packages/test-utils/dist/index.d.cts +198 -266
- package/packages/test-utils/dist/index.d.ts +198 -266
- package/packages/test-utils/dist/index.js +679 -690
- package/packages/test-utils/dist/index.js.map +1 -1
- package/packages/tools/dist/tsconfig.tsbuildinfo +1 -1
|
@@ -1,396 +1,3 @@
|
|
|
1
|
-
// src/shared/mocks.ts
|
|
2
|
-
import { vi } from "vitest";
|
|
3
|
-
var createBinaryWriterMock = () => ({
|
|
4
|
-
writeByte: vi.fn(),
|
|
5
|
-
writeShort: vi.fn(),
|
|
6
|
-
writeLong: vi.fn(),
|
|
7
|
-
writeString: vi.fn(),
|
|
8
|
-
writeBytes: vi.fn(),
|
|
9
|
-
getBuffer: vi.fn(() => new Uint8Array(0)),
|
|
10
|
-
reset: vi.fn(),
|
|
11
|
-
// Legacy methods (if any)
|
|
12
|
-
writeInt8: vi.fn(),
|
|
13
|
-
writeUint8: vi.fn(),
|
|
14
|
-
writeInt16: vi.fn(),
|
|
15
|
-
writeUint16: vi.fn(),
|
|
16
|
-
writeInt32: vi.fn(),
|
|
17
|
-
writeUint32: vi.fn(),
|
|
18
|
-
writeFloat: vi.fn(),
|
|
19
|
-
getData: vi.fn(() => new Uint8Array(0))
|
|
20
|
-
});
|
|
21
|
-
var createNetChanMock = () => ({
|
|
22
|
-
qport: 1234,
|
|
23
|
-
// Sequencing
|
|
24
|
-
incomingSequence: 0,
|
|
25
|
-
outgoingSequence: 0,
|
|
26
|
-
incomingAcknowledged: 0,
|
|
27
|
-
// Reliable messaging
|
|
28
|
-
incomingReliableAcknowledged: false,
|
|
29
|
-
incomingReliableSequence: 0,
|
|
30
|
-
outgoingReliableSequence: 0,
|
|
31
|
-
reliableMessage: createBinaryWriterMock(),
|
|
32
|
-
reliableLength: 0,
|
|
33
|
-
// Fragmentation
|
|
34
|
-
fragmentSendOffset: 0,
|
|
35
|
-
fragmentBuffer: null,
|
|
36
|
-
fragmentLength: 0,
|
|
37
|
-
fragmentReceived: 0,
|
|
38
|
-
// Timing
|
|
39
|
-
lastReceived: 0,
|
|
40
|
-
lastSent: 0,
|
|
41
|
-
remoteAddress: { type: "IP", port: 1234 },
|
|
42
|
-
// Methods
|
|
43
|
-
setup: vi.fn(),
|
|
44
|
-
reset: vi.fn(),
|
|
45
|
-
transmit: vi.fn(),
|
|
46
|
-
process: vi.fn(),
|
|
47
|
-
canSendReliable: vi.fn(() => true),
|
|
48
|
-
writeReliableByte: vi.fn(),
|
|
49
|
-
writeReliableShort: vi.fn(),
|
|
50
|
-
writeReliableLong: vi.fn(),
|
|
51
|
-
writeReliableString: vi.fn(),
|
|
52
|
-
getReliableData: vi.fn(() => new Uint8Array(0)),
|
|
53
|
-
needsKeepalive: vi.fn(() => false),
|
|
54
|
-
isTimedOut: vi.fn(() => false)
|
|
55
|
-
});
|
|
56
|
-
var createBinaryStreamMock = () => ({
|
|
57
|
-
getPosition: vi.fn(() => 0),
|
|
58
|
-
getReadPosition: vi.fn(() => 0),
|
|
59
|
-
getLength: vi.fn(() => 0),
|
|
60
|
-
getRemaining: vi.fn(() => 0),
|
|
61
|
-
seek: vi.fn(),
|
|
62
|
-
setReadPosition: vi.fn(),
|
|
63
|
-
hasMore: vi.fn(() => true),
|
|
64
|
-
hasBytes: vi.fn((amount) => true),
|
|
65
|
-
readChar: vi.fn(() => 0),
|
|
66
|
-
readByte: vi.fn(() => 0),
|
|
67
|
-
readShort: vi.fn(() => 0),
|
|
68
|
-
readUShort: vi.fn(() => 0),
|
|
69
|
-
readLong: vi.fn(() => 0),
|
|
70
|
-
readULong: vi.fn(() => 0),
|
|
71
|
-
readFloat: vi.fn(() => 0),
|
|
72
|
-
readString: vi.fn(() => ""),
|
|
73
|
-
readStringLine: vi.fn(() => ""),
|
|
74
|
-
readCoord: vi.fn(() => 0),
|
|
75
|
-
readAngle: vi.fn(() => 0),
|
|
76
|
-
readAngle16: vi.fn(() => 0),
|
|
77
|
-
readData: vi.fn((length) => new Uint8Array(length)),
|
|
78
|
-
readPos: vi.fn(),
|
|
79
|
-
readDir: vi.fn()
|
|
80
|
-
});
|
|
81
|
-
|
|
82
|
-
// src/shared/bsp.ts
|
|
83
|
-
import {
|
|
84
|
-
computePlaneSignBits,
|
|
85
|
-
CONTENTS_SOLID
|
|
86
|
-
} from "@quake2ts/shared";
|
|
87
|
-
function makePlane(normal, dist) {
|
|
88
|
-
return {
|
|
89
|
-
normal,
|
|
90
|
-
dist,
|
|
91
|
-
type: Math.abs(normal.x) === 1 ? 0 : Math.abs(normal.y) === 1 ? 1 : Math.abs(normal.z) === 1 ? 2 : 3,
|
|
92
|
-
signbits: computePlaneSignBits(normal)
|
|
93
|
-
};
|
|
94
|
-
}
|
|
95
|
-
function makeAxisBrush(size, contents = CONTENTS_SOLID) {
|
|
96
|
-
const half = size / 2;
|
|
97
|
-
const planes = [
|
|
98
|
-
makePlane({ x: 1, y: 0, z: 0 }, half),
|
|
99
|
-
makePlane({ x: -1, y: 0, z: 0 }, half),
|
|
100
|
-
makePlane({ x: 0, y: 1, z: 0 }, half),
|
|
101
|
-
makePlane({ x: 0, y: -1, z: 0 }, half),
|
|
102
|
-
makePlane({ x: 0, y: 0, z: 1 }, half),
|
|
103
|
-
makePlane({ x: 0, y: 0, z: -1 }, half)
|
|
104
|
-
];
|
|
105
|
-
return {
|
|
106
|
-
contents,
|
|
107
|
-
sides: planes.map((plane) => ({ plane, surfaceFlags: 0 }))
|
|
108
|
-
};
|
|
109
|
-
}
|
|
110
|
-
function makeNode(plane, children) {
|
|
111
|
-
return { plane, children };
|
|
112
|
-
}
|
|
113
|
-
function makeBspModel(planes, nodes, leaves, brushes, leafBrushes) {
|
|
114
|
-
return {
|
|
115
|
-
planes,
|
|
116
|
-
nodes,
|
|
117
|
-
leaves,
|
|
118
|
-
brushes,
|
|
119
|
-
leafBrushes,
|
|
120
|
-
bmodels: []
|
|
121
|
-
};
|
|
122
|
-
}
|
|
123
|
-
function makeLeaf(contents, firstLeafBrush, numLeafBrushes) {
|
|
124
|
-
return { contents, cluster: 0, area: 0, firstLeafBrush, numLeafBrushes };
|
|
125
|
-
}
|
|
126
|
-
function makeLeafModel(brushes) {
|
|
127
|
-
const planes = brushes.flatMap((brush) => brush.sides.map((side) => side.plane));
|
|
128
|
-
return {
|
|
129
|
-
planes,
|
|
130
|
-
nodes: [],
|
|
131
|
-
leaves: [makeLeaf(0, 0, brushes.length)],
|
|
132
|
-
brushes,
|
|
133
|
-
leafBrushes: brushes.map((_, i) => i),
|
|
134
|
-
bmodels: []
|
|
135
|
-
};
|
|
136
|
-
}
|
|
137
|
-
function makeBrushFromMinsMaxs(mins, maxs, contents = CONTENTS_SOLID) {
|
|
138
|
-
const planes = [
|
|
139
|
-
makePlane({ x: 1, y: 0, z: 0 }, maxs.x),
|
|
140
|
-
makePlane({ x: -1, y: 0, z: 0 }, -mins.x),
|
|
141
|
-
makePlane({ x: 0, y: 1, z: 0 }, maxs.y),
|
|
142
|
-
makePlane({ x: 0, y: -1, z: 0 }, -mins.y),
|
|
143
|
-
makePlane({ x: 0, y: 0, z: 1 }, maxs.z),
|
|
144
|
-
makePlane({ x: 0, y: 0, z: -1 }, -mins.z)
|
|
145
|
-
];
|
|
146
|
-
return {
|
|
147
|
-
contents,
|
|
148
|
-
sides: planes.map((plane) => ({ plane, surfaceFlags: 0 }))
|
|
149
|
-
};
|
|
150
|
-
}
|
|
151
|
-
|
|
152
|
-
// src/game/factories.ts
|
|
153
|
-
var createPlayerStateFactory = (overrides) => ({
|
|
154
|
-
pm_type: 0,
|
|
155
|
-
pm_time: 0,
|
|
156
|
-
pm_flags: 0,
|
|
157
|
-
origin: { x: 0, y: 0, z: 0 },
|
|
158
|
-
velocity: { x: 0, y: 0, z: 0 },
|
|
159
|
-
viewAngles: { x: 0, y: 0, z: 0 },
|
|
160
|
-
onGround: false,
|
|
161
|
-
waterLevel: 0,
|
|
162
|
-
watertype: 0,
|
|
163
|
-
mins: { x: 0, y: 0, z: 0 },
|
|
164
|
-
maxs: { x: 0, y: 0, z: 0 },
|
|
165
|
-
damageAlpha: 0,
|
|
166
|
-
damageIndicators: [],
|
|
167
|
-
blend: [0, 0, 0, 0],
|
|
168
|
-
stats: [],
|
|
169
|
-
kick_angles: { x: 0, y: 0, z: 0 },
|
|
170
|
-
kick_origin: { x: 0, y: 0, z: 0 },
|
|
171
|
-
gunoffset: { x: 0, y: 0, z: 0 },
|
|
172
|
-
gunangles: { x: 0, y: 0, z: 0 },
|
|
173
|
-
gunindex: 0,
|
|
174
|
-
gun_frame: 0,
|
|
175
|
-
rdflags: 0,
|
|
176
|
-
fov: 90,
|
|
177
|
-
renderfx: 0,
|
|
178
|
-
...overrides
|
|
179
|
-
});
|
|
180
|
-
var createEntityStateFactory = (overrides) => ({
|
|
181
|
-
number: 0,
|
|
182
|
-
origin: { x: 0, y: 0, z: 0 },
|
|
183
|
-
angles: { x: 0, y: 0, z: 0 },
|
|
184
|
-
oldOrigin: { x: 0, y: 0, z: 0 },
|
|
185
|
-
modelIndex: 0,
|
|
186
|
-
modelIndex2: 0,
|
|
187
|
-
modelIndex3: 0,
|
|
188
|
-
modelIndex4: 0,
|
|
189
|
-
frame: 0,
|
|
190
|
-
skinNum: 0,
|
|
191
|
-
effects: 0,
|
|
192
|
-
renderfx: 0,
|
|
193
|
-
solid: 0,
|
|
194
|
-
sound: 0,
|
|
195
|
-
event: 0,
|
|
196
|
-
...overrides
|
|
197
|
-
});
|
|
198
|
-
var createGameStateSnapshotFactory = (overrides) => ({
|
|
199
|
-
gravity: { x: 0, y: 0, z: -800 },
|
|
200
|
-
origin: { x: 0, y: 0, z: 0 },
|
|
201
|
-
velocity: { x: 0, y: 0, z: 0 },
|
|
202
|
-
viewangles: { x: 0, y: 0, z: 0 },
|
|
203
|
-
level: { timeSeconds: 0, frameNumber: 0, previousTimeSeconds: 0, deltaSeconds: 0.1 },
|
|
204
|
-
entities: {
|
|
205
|
-
activeCount: 0,
|
|
206
|
-
worldClassname: "worldspawn"
|
|
207
|
-
},
|
|
208
|
-
packetEntities: [],
|
|
209
|
-
pmFlags: 0,
|
|
210
|
-
pmType: 0,
|
|
211
|
-
waterlevel: 0,
|
|
212
|
-
watertype: 0,
|
|
213
|
-
deltaAngles: { x: 0, y: 0, z: 0 },
|
|
214
|
-
health: 100,
|
|
215
|
-
armor: 0,
|
|
216
|
-
ammo: 0,
|
|
217
|
-
blend: [0, 0, 0, 0],
|
|
218
|
-
damageAlpha: 0,
|
|
219
|
-
damageIndicators: [],
|
|
220
|
-
stats: [],
|
|
221
|
-
kick_angles: { x: 0, y: 0, z: 0 },
|
|
222
|
-
kick_origin: { x: 0, y: 0, z: 0 },
|
|
223
|
-
gunoffset: { x: 0, y: 0, z: 0 },
|
|
224
|
-
gunangles: { x: 0, y: 0, z: 0 },
|
|
225
|
-
gunindex: 0,
|
|
226
|
-
pm_time: 0,
|
|
227
|
-
gun_frame: 0,
|
|
228
|
-
rdflags: 0,
|
|
229
|
-
fov: 90,
|
|
230
|
-
renderfx: 0,
|
|
231
|
-
pm_flags: 0,
|
|
232
|
-
pm_type: 0,
|
|
233
|
-
...overrides
|
|
234
|
-
});
|
|
235
|
-
|
|
236
|
-
// src/game/helpers.ts
|
|
237
|
-
import { vi as vi2 } from "vitest";
|
|
238
|
-
import { Entity, SpawnRegistry, ScriptHookRegistry } from "@quake2ts/game";
|
|
239
|
-
import { createRandomGenerator } from "@quake2ts/shared";
|
|
240
|
-
import { intersects, stairTrace, ladderTrace } from "@quake2ts/shared";
|
|
241
|
-
var createMockEngine = () => ({
|
|
242
|
-
sound: vi2.fn(),
|
|
243
|
-
soundIndex: vi2.fn((sound) => 0),
|
|
244
|
-
modelIndex: vi2.fn((model) => 0),
|
|
245
|
-
centerprintf: vi2.fn()
|
|
246
|
-
});
|
|
247
|
-
var createMockGame = (seed = 12345) => {
|
|
248
|
-
const spawnRegistry = new SpawnRegistry();
|
|
249
|
-
const hooks = new ScriptHookRegistry();
|
|
250
|
-
const game = {
|
|
251
|
-
random: createRandomGenerator({ seed }),
|
|
252
|
-
registerEntitySpawn: vi2.fn((classname, spawnFunc) => {
|
|
253
|
-
spawnRegistry.register(classname, (entity) => spawnFunc(entity));
|
|
254
|
-
}),
|
|
255
|
-
unregisterEntitySpawn: vi2.fn((classname) => {
|
|
256
|
-
spawnRegistry.unregister(classname);
|
|
257
|
-
}),
|
|
258
|
-
getCustomEntities: vi2.fn(() => Array.from(spawnRegistry.keys())),
|
|
259
|
-
hooks,
|
|
260
|
-
registerHooks: vi2.fn((newHooks) => hooks.register(newHooks)),
|
|
261
|
-
spawnWorld: vi2.fn(() => {
|
|
262
|
-
hooks.onMapLoad("q2dm1");
|
|
263
|
-
}),
|
|
264
|
-
clientBegin: vi2.fn((client) => {
|
|
265
|
-
hooks.onPlayerSpawn({});
|
|
266
|
-
}),
|
|
267
|
-
damage: vi2.fn((amount) => {
|
|
268
|
-
hooks.onDamage({}, null, null, amount, 0, 0);
|
|
269
|
-
})
|
|
270
|
-
};
|
|
271
|
-
return { game, spawnRegistry };
|
|
272
|
-
};
|
|
273
|
-
function createTestContext(options) {
|
|
274
|
-
const engine = createMockEngine();
|
|
275
|
-
const seed = options?.seed ?? 12345;
|
|
276
|
-
const { game, spawnRegistry } = createMockGame(seed);
|
|
277
|
-
const traceFn = vi2.fn((start, end, mins, maxs) => ({
|
|
278
|
-
fraction: 1,
|
|
279
|
-
ent: null,
|
|
280
|
-
allsolid: false,
|
|
281
|
-
startsolid: false,
|
|
282
|
-
endpos: end,
|
|
283
|
-
plane: { normal: { x: 0, y: 0, z: 1 }, dist: 0 },
|
|
284
|
-
surfaceFlags: 0,
|
|
285
|
-
contents: 0
|
|
286
|
-
}));
|
|
287
|
-
const entityList = options?.initialEntities ? [...options.initialEntities] : [];
|
|
288
|
-
const hooks = game.hooks;
|
|
289
|
-
const entities = {
|
|
290
|
-
spawn: vi2.fn(() => {
|
|
291
|
-
const ent = new Entity(entityList.length + 1);
|
|
292
|
-
entityList.push(ent);
|
|
293
|
-
hooks.onEntitySpawn(ent);
|
|
294
|
-
return ent;
|
|
295
|
-
}),
|
|
296
|
-
free: vi2.fn((ent) => {
|
|
297
|
-
const idx = entityList.indexOf(ent);
|
|
298
|
-
if (idx !== -1) {
|
|
299
|
-
entityList.splice(idx, 1);
|
|
300
|
-
}
|
|
301
|
-
hooks.onEntityRemove(ent);
|
|
302
|
-
}),
|
|
303
|
-
finalizeSpawn: vi2.fn(),
|
|
304
|
-
freeImmediate: vi2.fn((ent) => {
|
|
305
|
-
const idx = entityList.indexOf(ent);
|
|
306
|
-
if (idx !== -1) {
|
|
307
|
-
entityList.splice(idx, 1);
|
|
308
|
-
}
|
|
309
|
-
}),
|
|
310
|
-
setSpawnRegistry: vi2.fn(),
|
|
311
|
-
timeSeconds: 10,
|
|
312
|
-
deltaSeconds: 0.1,
|
|
313
|
-
modelIndex: vi2.fn(() => 0),
|
|
314
|
-
scheduleThink: vi2.fn((entity, time) => {
|
|
315
|
-
entity.nextthink = time;
|
|
316
|
-
}),
|
|
317
|
-
linkentity: vi2.fn(),
|
|
318
|
-
trace: traceFn,
|
|
319
|
-
pointcontents: vi2.fn(() => 0),
|
|
320
|
-
multicast: vi2.fn(),
|
|
321
|
-
unicast: vi2.fn(),
|
|
322
|
-
engine,
|
|
323
|
-
game,
|
|
324
|
-
sound: vi2.fn((ent, chan, sound, vol, attn, timeofs) => {
|
|
325
|
-
engine.sound(ent, chan, sound, vol, attn, timeofs);
|
|
326
|
-
}),
|
|
327
|
-
soundIndex: vi2.fn((sound) => engine.soundIndex(sound)),
|
|
328
|
-
useTargets: vi2.fn((entity, activator) => {
|
|
329
|
-
}),
|
|
330
|
-
findByTargetName: vi2.fn(() => []),
|
|
331
|
-
pickTarget: vi2.fn(() => null),
|
|
332
|
-
killBox: vi2.fn(),
|
|
333
|
-
rng: createRandomGenerator({ seed }),
|
|
334
|
-
imports: {
|
|
335
|
-
configstring: vi2.fn(),
|
|
336
|
-
trace: traceFn,
|
|
337
|
-
pointcontents: vi2.fn(() => 0)
|
|
338
|
-
},
|
|
339
|
-
level: {
|
|
340
|
-
intermission_angle: { x: 0, y: 0, z: 0 },
|
|
341
|
-
intermission_origin: { x: 0, y: 0, z: 0 },
|
|
342
|
-
next_auto_save: 0,
|
|
343
|
-
health_bar_entities: null
|
|
344
|
-
},
|
|
345
|
-
targetNameIndex: /* @__PURE__ */ new Map(),
|
|
346
|
-
forEachEntity: vi2.fn((callback) => {
|
|
347
|
-
entityList.forEach(callback);
|
|
348
|
-
}),
|
|
349
|
-
find: vi2.fn((predicate) => {
|
|
350
|
-
return entityList.find(predicate);
|
|
351
|
-
}),
|
|
352
|
-
findByClassname: vi2.fn((classname) => {
|
|
353
|
-
return entityList.find((e) => e.classname === classname);
|
|
354
|
-
}),
|
|
355
|
-
beginFrame: vi2.fn((timeSeconds) => {
|
|
356
|
-
entities.timeSeconds = timeSeconds;
|
|
357
|
-
}),
|
|
358
|
-
targetAwareness: {
|
|
359
|
-
timeSeconds: 10,
|
|
360
|
-
frameNumber: 1,
|
|
361
|
-
sightEntity: null,
|
|
362
|
-
soundEntity: null
|
|
363
|
-
},
|
|
364
|
-
// Adding missing properties to satisfy EntitySystem interface partially or fully
|
|
365
|
-
// We cast to unknown first anyway, but filling these in makes it safer for consumers
|
|
366
|
-
skill: 1,
|
|
367
|
-
deathmatch: false,
|
|
368
|
-
coop: false,
|
|
369
|
-
activeCount: entityList.length,
|
|
370
|
-
world: entityList.find((e) => e.classname === "worldspawn") || new Entity(0)
|
|
371
|
-
// ... other EntitySystem properties would go here
|
|
372
|
-
};
|
|
373
|
-
return {
|
|
374
|
-
keyValues: {},
|
|
375
|
-
entities,
|
|
376
|
-
game,
|
|
377
|
-
engine,
|
|
378
|
-
health_multiplier: 1,
|
|
379
|
-
warn: vi2.fn(),
|
|
380
|
-
free: vi2.fn(),
|
|
381
|
-
// Mock precache functions if they are part of SpawnContext in future or TestContext extensions
|
|
382
|
-
precacheModel: vi2.fn(),
|
|
383
|
-
precacheSound: vi2.fn(),
|
|
384
|
-
precacheImage: vi2.fn()
|
|
385
|
-
};
|
|
386
|
-
}
|
|
387
|
-
function createSpawnContext() {
|
|
388
|
-
return createTestContext();
|
|
389
|
-
}
|
|
390
|
-
function createEntity() {
|
|
391
|
-
return new Entity(1);
|
|
392
|
-
}
|
|
393
|
-
|
|
394
1
|
// src/setup/browser.ts
|
|
395
2
|
import { JSDOM } from "jsdom";
|
|
396
3
|
import { Canvas, Image, ImageData } from "@napi-rs/canvas";
|
|
@@ -786,12 +393,6 @@ function teardownBrowserEnvironment() {
|
|
|
786
393
|
delete global.UIEvent;
|
|
787
394
|
}
|
|
788
395
|
|
|
789
|
-
// src/setup/node.ts
|
|
790
|
-
function setupNodeEnvironment() {
|
|
791
|
-
if (typeof global.fetch === "undefined") {
|
|
792
|
-
}
|
|
793
|
-
}
|
|
794
|
-
|
|
795
396
|
// src/setup/canvas.ts
|
|
796
397
|
import { Canvas as Canvas2, Image as Image2, ImageData as ImageData2 } from "@napi-rs/canvas";
|
|
797
398
|
function createMockCanvas(width = 300, height = 150) {
|
|
@@ -873,7 +474,127 @@ function createMockImage(width, height, src) {
|
|
|
873
474
|
return img;
|
|
874
475
|
}
|
|
875
476
|
|
|
477
|
+
// src/setup/node.ts
|
|
478
|
+
function setupNodeEnvironment() {
|
|
479
|
+
if (typeof global.fetch === "undefined") {
|
|
480
|
+
}
|
|
481
|
+
}
|
|
482
|
+
|
|
483
|
+
// src/setup/storage.ts
|
|
484
|
+
import "fake-indexeddb/auto";
|
|
485
|
+
function createMockLocalStorage(initialData = {}) {
|
|
486
|
+
const storage = new Map(Object.entries(initialData));
|
|
487
|
+
return {
|
|
488
|
+
getItem: (key) => storage.get(key) || null,
|
|
489
|
+
setItem: (key, value) => storage.set(key, value),
|
|
490
|
+
removeItem: (key) => storage.delete(key),
|
|
491
|
+
clear: () => storage.clear(),
|
|
492
|
+
key: (index) => Array.from(storage.keys())[index] || null,
|
|
493
|
+
get length() {
|
|
494
|
+
return storage.size;
|
|
495
|
+
}
|
|
496
|
+
};
|
|
497
|
+
}
|
|
498
|
+
function createMockSessionStorage(initialData = {}) {
|
|
499
|
+
return createMockLocalStorage(initialData);
|
|
500
|
+
}
|
|
501
|
+
function createMockIndexedDB() {
|
|
502
|
+
if (typeof indexedDB === "undefined") {
|
|
503
|
+
throw new Error("IndexedDB mock not found. Ensure fake-indexeddb is loaded.");
|
|
504
|
+
}
|
|
505
|
+
return indexedDB;
|
|
506
|
+
}
|
|
507
|
+
function createStorageTestScenario(storageType = "local") {
|
|
508
|
+
const storage = storageType === "local" ? createMockLocalStorage() : createMockSessionStorage();
|
|
509
|
+
return {
|
|
510
|
+
storage,
|
|
511
|
+
populate(data) {
|
|
512
|
+
Object.entries(data).forEach(([k, v]) => storage.setItem(k, v));
|
|
513
|
+
},
|
|
514
|
+
verify(key, value) {
|
|
515
|
+
return storage.getItem(key) === value;
|
|
516
|
+
}
|
|
517
|
+
};
|
|
518
|
+
}
|
|
519
|
+
|
|
520
|
+
// src/setup/audio.ts
|
|
521
|
+
function createMockAudioContext() {
|
|
522
|
+
return {
|
|
523
|
+
createGain: () => ({
|
|
524
|
+
connect: () => {
|
|
525
|
+
},
|
|
526
|
+
gain: { value: 1, setValueAtTime: () => {
|
|
527
|
+
} }
|
|
528
|
+
}),
|
|
529
|
+
createOscillator: () => ({
|
|
530
|
+
connect: () => {
|
|
531
|
+
},
|
|
532
|
+
start: () => {
|
|
533
|
+
},
|
|
534
|
+
stop: () => {
|
|
535
|
+
},
|
|
536
|
+
frequency: { value: 440 }
|
|
537
|
+
}),
|
|
538
|
+
createBufferSource: () => ({
|
|
539
|
+
connect: () => {
|
|
540
|
+
},
|
|
541
|
+
start: () => {
|
|
542
|
+
},
|
|
543
|
+
stop: () => {
|
|
544
|
+
},
|
|
545
|
+
buffer: null,
|
|
546
|
+
playbackRate: { value: 1 },
|
|
547
|
+
loop: false
|
|
548
|
+
}),
|
|
549
|
+
destination: {},
|
|
550
|
+
currentTime: 0,
|
|
551
|
+
state: "running",
|
|
552
|
+
resume: async () => {
|
|
553
|
+
},
|
|
554
|
+
suspend: async () => {
|
|
555
|
+
},
|
|
556
|
+
close: async () => {
|
|
557
|
+
},
|
|
558
|
+
decodeAudioData: async (buffer) => ({
|
|
559
|
+
duration: 1,
|
|
560
|
+
length: 44100,
|
|
561
|
+
sampleRate: 44100,
|
|
562
|
+
numberOfChannels: 2,
|
|
563
|
+
getChannelData: () => new Float32Array(44100)
|
|
564
|
+
}),
|
|
565
|
+
createBuffer: (channels, length, sampleRate) => ({
|
|
566
|
+
duration: length / sampleRate,
|
|
567
|
+
length,
|
|
568
|
+
sampleRate,
|
|
569
|
+
numberOfChannels: channels,
|
|
570
|
+
getChannelData: () => new Float32Array(length)
|
|
571
|
+
})
|
|
572
|
+
};
|
|
573
|
+
}
|
|
574
|
+
function setupMockAudioContext() {
|
|
575
|
+
if (typeof global.AudioContext === "undefined" && typeof global.window !== "undefined") {
|
|
576
|
+
global.AudioContext = class {
|
|
577
|
+
constructor() {
|
|
578
|
+
return createMockAudioContext();
|
|
579
|
+
}
|
|
580
|
+
};
|
|
581
|
+
global.window.AudioContext = global.AudioContext;
|
|
582
|
+
global.window.webkitAudioContext = global.AudioContext;
|
|
583
|
+
}
|
|
584
|
+
}
|
|
585
|
+
function teardownMockAudioContext() {
|
|
586
|
+
if (global.AudioContext && global.AudioContext.toString().includes("class")) {
|
|
587
|
+
delete global.AudioContext;
|
|
588
|
+
delete global.window.AudioContext;
|
|
589
|
+
delete global.window.webkitAudioContext;
|
|
590
|
+
}
|
|
591
|
+
}
|
|
592
|
+
function captureAudioEvents(context) {
|
|
593
|
+
return [];
|
|
594
|
+
}
|
|
595
|
+
|
|
876
596
|
// src/setup/timing.ts
|
|
597
|
+
var activeMockRAF;
|
|
877
598
|
function createMockRAF() {
|
|
878
599
|
let callbacks = [];
|
|
879
600
|
let nextId = 1;
|
|
@@ -913,10 +634,14 @@ function createMockRAF() {
|
|
|
913
634
|
currentTime = 0;
|
|
914
635
|
},
|
|
915
636
|
enable() {
|
|
637
|
+
activeMockRAF = this;
|
|
916
638
|
global.requestAnimationFrame = raf;
|
|
917
639
|
global.cancelAnimationFrame = cancel;
|
|
918
640
|
},
|
|
919
641
|
disable() {
|
|
642
|
+
if (activeMockRAF === this) {
|
|
643
|
+
activeMockRAF = void 0;
|
|
644
|
+
}
|
|
920
645
|
if (originalRAF) {
|
|
921
646
|
global.requestAnimationFrame = originalRAF;
|
|
922
647
|
} else {
|
|
@@ -939,7 +664,6 @@ function createMockPerformance(startTime = 0) {
|
|
|
939
664
|
timing: {
|
|
940
665
|
navigationStart: startTime
|
|
941
666
|
},
|
|
942
|
-
// Add minimal navigation/resource timing interfaces to satisfy types if needed
|
|
943
667
|
clearMarks: () => {
|
|
944
668
|
},
|
|
945
669
|
clearMeasures: () => {
|
|
@@ -968,330 +692,598 @@ function createMockPerformance(startTime = 0) {
|
|
|
968
692
|
mockPerf.setTime = (time) => {
|
|
969
693
|
currentTime = time;
|
|
970
694
|
};
|
|
971
|
-
return mockPerf;
|
|
695
|
+
return mockPerf;
|
|
696
|
+
}
|
|
697
|
+
function createControlledTimer() {
|
|
698
|
+
let currentTime = 0;
|
|
699
|
+
let timers = [];
|
|
700
|
+
let nextId = 1;
|
|
701
|
+
const originalSetTimeout = global.setTimeout;
|
|
702
|
+
const originalClearTimeout = global.clearTimeout;
|
|
703
|
+
const originalSetInterval = global.setInterval;
|
|
704
|
+
const originalClearInterval = global.clearInterval;
|
|
705
|
+
const mockSetTimeout = (callback, delay = 0, ...args) => {
|
|
706
|
+
const id = nextId++;
|
|
707
|
+
timers.push({ id, callback, dueTime: currentTime + delay, args });
|
|
708
|
+
return id;
|
|
709
|
+
};
|
|
710
|
+
const mockClearTimeout = (id) => {
|
|
711
|
+
timers = timers.filter((t) => t.id !== id);
|
|
712
|
+
};
|
|
713
|
+
const mockSetInterval = (callback, delay = 0, ...args) => {
|
|
714
|
+
const id = nextId++;
|
|
715
|
+
timers.push({ id, callback, dueTime: currentTime + delay, interval: delay, args });
|
|
716
|
+
return id;
|
|
717
|
+
};
|
|
718
|
+
const mockClearInterval = (id) => {
|
|
719
|
+
timers = timers.filter((t) => t.id !== id);
|
|
720
|
+
};
|
|
721
|
+
global.setTimeout = mockSetTimeout;
|
|
722
|
+
global.clearTimeout = mockClearTimeout;
|
|
723
|
+
global.setInterval = mockSetInterval;
|
|
724
|
+
global.clearInterval = mockClearInterval;
|
|
725
|
+
return {
|
|
726
|
+
tick() {
|
|
727
|
+
this.advanceBy(0);
|
|
728
|
+
},
|
|
729
|
+
advanceBy(ms) {
|
|
730
|
+
const targetTime = currentTime + ms;
|
|
731
|
+
while (true) {
|
|
732
|
+
let earliest = null;
|
|
733
|
+
for (const t of timers) {
|
|
734
|
+
if (!earliest || t.dueTime < earliest.dueTime) {
|
|
735
|
+
earliest = t;
|
|
736
|
+
}
|
|
737
|
+
}
|
|
738
|
+
if (!earliest || earliest.dueTime > targetTime) {
|
|
739
|
+
break;
|
|
740
|
+
}
|
|
741
|
+
currentTime = earliest.dueTime;
|
|
742
|
+
const { callback, args, interval, id } = earliest;
|
|
743
|
+
if (interval !== void 0) {
|
|
744
|
+
earliest.dueTime += interval;
|
|
745
|
+
if (interval === 0) earliest.dueTime += 1;
|
|
746
|
+
} else {
|
|
747
|
+
timers = timers.filter((t) => t.id !== id);
|
|
748
|
+
}
|
|
749
|
+
callback(...args);
|
|
750
|
+
}
|
|
751
|
+
currentTime = targetTime;
|
|
752
|
+
},
|
|
753
|
+
clear() {
|
|
754
|
+
timers = [];
|
|
755
|
+
},
|
|
756
|
+
restore() {
|
|
757
|
+
global.setTimeout = originalSetTimeout;
|
|
758
|
+
global.clearTimeout = originalClearTimeout;
|
|
759
|
+
global.setInterval = originalSetInterval;
|
|
760
|
+
global.clearInterval = originalClearInterval;
|
|
761
|
+
}
|
|
762
|
+
};
|
|
763
|
+
}
|
|
764
|
+
function simulateFrames(count, frameTimeMs = 16.6, callback) {
|
|
765
|
+
if (!activeMockRAF) {
|
|
766
|
+
throw new Error("simulateFrames requires an active MockRAF. Ensure createMockRAF().enable() is called.");
|
|
767
|
+
}
|
|
768
|
+
for (let i = 0; i < count; i++) {
|
|
769
|
+
if (callback) callback(i);
|
|
770
|
+
activeMockRAF.advance(frameTimeMs);
|
|
771
|
+
}
|
|
772
|
+
}
|
|
773
|
+
function simulateFramesWithMock(mock, count, frameTimeMs = 16.6, callback) {
|
|
774
|
+
for (let i = 0; i < count; i++) {
|
|
775
|
+
if (callback) callback(i);
|
|
776
|
+
mock.advance(frameTimeMs);
|
|
777
|
+
}
|
|
778
|
+
}
|
|
779
|
+
|
|
780
|
+
// src/e2e/playwright.ts
|
|
781
|
+
import { chromium } from "playwright";
|
|
782
|
+
import { createServer } from "http";
|
|
783
|
+
import handler from "serve-handler";
|
|
784
|
+
async function createPlaywrightTestClient(options = {}) {
|
|
785
|
+
let staticServer;
|
|
786
|
+
let clientUrl = options.clientUrl;
|
|
787
|
+
const rootPath = options.rootPath || process.cwd();
|
|
788
|
+
if (!clientUrl) {
|
|
789
|
+
staticServer = createServer((request, response) => {
|
|
790
|
+
return handler(request, response, {
|
|
791
|
+
public: rootPath,
|
|
792
|
+
cleanUrls: false,
|
|
793
|
+
headers: [
|
|
794
|
+
{
|
|
795
|
+
source: "**/*",
|
|
796
|
+
headers: [
|
|
797
|
+
{ key: "Cache-Control", value: "no-cache" },
|
|
798
|
+
{ key: "Access-Control-Allow-Origin", value: "*" },
|
|
799
|
+
{ key: "Cross-Origin-Opener-Policy", value: "same-origin" },
|
|
800
|
+
{ key: "Cross-Origin-Embedder-Policy", value: "require-corp" }
|
|
801
|
+
]
|
|
802
|
+
}
|
|
803
|
+
]
|
|
804
|
+
});
|
|
805
|
+
});
|
|
806
|
+
await new Promise((resolve) => {
|
|
807
|
+
if (!staticServer) return;
|
|
808
|
+
staticServer.listen(0, () => {
|
|
809
|
+
const addr = staticServer?.address();
|
|
810
|
+
const port = typeof addr === "object" ? addr?.port : 0;
|
|
811
|
+
clientUrl = `http://localhost:${port}`;
|
|
812
|
+
console.log(`Test client serving from ${rootPath} at ${clientUrl}`);
|
|
813
|
+
resolve();
|
|
814
|
+
});
|
|
815
|
+
});
|
|
816
|
+
}
|
|
817
|
+
const browser = await chromium.launch({
|
|
818
|
+
headless: options.headless ?? true,
|
|
819
|
+
args: [
|
|
820
|
+
"--use-gl=egl",
|
|
821
|
+
"--ignore-gpu-blocklist",
|
|
822
|
+
...options.launchOptions?.args || []
|
|
823
|
+
],
|
|
824
|
+
...options.launchOptions
|
|
825
|
+
});
|
|
826
|
+
const width = options.width || 1280;
|
|
827
|
+
const height = options.height || 720;
|
|
828
|
+
const context = await browser.newContext({
|
|
829
|
+
viewport: { width, height },
|
|
830
|
+
deviceScaleFactor: 1,
|
|
831
|
+
...options.contextOptions
|
|
832
|
+
});
|
|
833
|
+
const page = await context.newPage();
|
|
834
|
+
const close = async () => {
|
|
835
|
+
await browser.close();
|
|
836
|
+
if (staticServer) {
|
|
837
|
+
staticServer.close();
|
|
838
|
+
}
|
|
839
|
+
};
|
|
840
|
+
const navigate = async (url) => {
|
|
841
|
+
const targetUrl = url || clientUrl;
|
|
842
|
+
if (!targetUrl) throw new Error("No URL to navigate to");
|
|
843
|
+
let finalUrl = targetUrl;
|
|
844
|
+
if (options.serverUrl && !targetUrl.includes("connect=")) {
|
|
845
|
+
const separator = targetUrl.includes("?") ? "&" : "?";
|
|
846
|
+
finalUrl = `${targetUrl}${separator}connect=${encodeURIComponent(options.serverUrl)}`;
|
|
847
|
+
}
|
|
848
|
+
console.log(`Navigating to: ${finalUrl}`);
|
|
849
|
+
await page.goto(finalUrl, { waitUntil: "domcontentloaded" });
|
|
850
|
+
};
|
|
851
|
+
return {
|
|
852
|
+
browser,
|
|
853
|
+
context,
|
|
854
|
+
page,
|
|
855
|
+
server: staticServer,
|
|
856
|
+
close,
|
|
857
|
+
navigate,
|
|
858
|
+
waitForGame: async (timeout = 1e4) => {
|
|
859
|
+
await waitForGameReady(page, timeout);
|
|
860
|
+
},
|
|
861
|
+
injectInput: async (type, data) => {
|
|
862
|
+
await page.evaluate(({ type: type2, data: data2 }) => {
|
|
863
|
+
if (window.injectGameInput) window.injectGameInput(type2, data2);
|
|
864
|
+
}, { type, data });
|
|
865
|
+
}
|
|
866
|
+
};
|
|
867
|
+
}
|
|
868
|
+
async function waitForGameReady(page, timeout = 1e4) {
|
|
869
|
+
try {
|
|
870
|
+
await page.waitForFunction(() => {
|
|
871
|
+
return window.gameInstance && window.gameInstance.isReady;
|
|
872
|
+
}, null, { timeout });
|
|
873
|
+
} catch (e) {
|
|
874
|
+
await page.waitForSelector("canvas", { timeout });
|
|
875
|
+
}
|
|
876
|
+
}
|
|
877
|
+
async function captureGameState(page) {
|
|
878
|
+
return await page.evaluate(() => {
|
|
879
|
+
if (window.gameInstance && window.gameInstance.getState) {
|
|
880
|
+
return window.gameInstance.getState();
|
|
881
|
+
}
|
|
882
|
+
return {};
|
|
883
|
+
});
|
|
884
|
+
}
|
|
885
|
+
|
|
886
|
+
// src/shared/bsp.ts
|
|
887
|
+
import {
|
|
888
|
+
computePlaneSignBits,
|
|
889
|
+
CONTENTS_SOLID
|
|
890
|
+
} from "@quake2ts/shared";
|
|
891
|
+
function makePlane(normal, dist) {
|
|
892
|
+
return {
|
|
893
|
+
normal,
|
|
894
|
+
dist,
|
|
895
|
+
type: Math.abs(normal.x) === 1 ? 0 : Math.abs(normal.y) === 1 ? 1 : Math.abs(normal.z) === 1 ? 2 : 3,
|
|
896
|
+
signbits: computePlaneSignBits(normal)
|
|
897
|
+
};
|
|
972
898
|
}
|
|
973
|
-
function
|
|
974
|
-
const
|
|
975
|
-
|
|
899
|
+
function makeAxisBrush(size, contents = CONTENTS_SOLID) {
|
|
900
|
+
const half = size / 2;
|
|
901
|
+
const planes = [
|
|
902
|
+
makePlane({ x: 1, y: 0, z: 0 }, half),
|
|
903
|
+
makePlane({ x: -1, y: 0, z: 0 }, half),
|
|
904
|
+
makePlane({ x: 0, y: 1, z: 0 }, half),
|
|
905
|
+
makePlane({ x: 0, y: -1, z: 0 }, half),
|
|
906
|
+
makePlane({ x: 0, y: 0, z: 1 }, half),
|
|
907
|
+
makePlane({ x: 0, y: 0, z: -1 }, half)
|
|
908
|
+
];
|
|
909
|
+
return {
|
|
910
|
+
contents,
|
|
911
|
+
sides: planes.map((plane) => ({ plane, surfaceFlags: 0 }))
|
|
912
|
+
};
|
|
976
913
|
}
|
|
977
|
-
function
|
|
978
|
-
|
|
979
|
-
if (callback) callback(i);
|
|
980
|
-
mock.advance(frameTimeMs);
|
|
981
|
-
}
|
|
914
|
+
function makeNode(plane, children) {
|
|
915
|
+
return { plane, children };
|
|
982
916
|
}
|
|
983
|
-
|
|
984
|
-
// src/setup/storage.ts
|
|
985
|
-
import "fake-indexeddb/auto";
|
|
986
|
-
function createMockLocalStorage(initialData = {}) {
|
|
987
|
-
const storage = new Map(Object.entries(initialData));
|
|
917
|
+
function makeBspModel(planes, nodes, leaves, brushes, leafBrushes) {
|
|
988
918
|
return {
|
|
989
|
-
|
|
990
|
-
|
|
991
|
-
|
|
992
|
-
|
|
993
|
-
|
|
994
|
-
|
|
995
|
-
return storage.size;
|
|
996
|
-
}
|
|
919
|
+
planes,
|
|
920
|
+
nodes,
|
|
921
|
+
leaves,
|
|
922
|
+
brushes,
|
|
923
|
+
leafBrushes,
|
|
924
|
+
bmodels: []
|
|
997
925
|
};
|
|
998
926
|
}
|
|
999
|
-
function
|
|
1000
|
-
return
|
|
927
|
+
function makeLeaf(contents, firstLeafBrush, numLeafBrushes) {
|
|
928
|
+
return { contents, cluster: 0, area: 0, firstLeafBrush, numLeafBrushes };
|
|
1001
929
|
}
|
|
1002
|
-
function
|
|
1003
|
-
|
|
1004
|
-
|
|
1005
|
-
|
|
1006
|
-
|
|
930
|
+
function makeLeafModel(brushes) {
|
|
931
|
+
const planes = brushes.flatMap((brush) => brush.sides.map((side) => side.plane));
|
|
932
|
+
return {
|
|
933
|
+
planes,
|
|
934
|
+
nodes: [],
|
|
935
|
+
leaves: [makeLeaf(0, 0, brushes.length)],
|
|
936
|
+
brushes,
|
|
937
|
+
leafBrushes: brushes.map((_, i) => i),
|
|
938
|
+
bmodels: []
|
|
939
|
+
};
|
|
1007
940
|
}
|
|
1008
|
-
function
|
|
1009
|
-
const
|
|
941
|
+
function makeBrushFromMinsMaxs(mins, maxs, contents = CONTENTS_SOLID) {
|
|
942
|
+
const planes = [
|
|
943
|
+
makePlane({ x: 1, y: 0, z: 0 }, maxs.x),
|
|
944
|
+
makePlane({ x: -1, y: 0, z: 0 }, -mins.x),
|
|
945
|
+
makePlane({ x: 0, y: 1, z: 0 }, maxs.y),
|
|
946
|
+
makePlane({ x: 0, y: -1, z: 0 }, -mins.y),
|
|
947
|
+
makePlane({ x: 0, y: 0, z: 1 }, maxs.z),
|
|
948
|
+
makePlane({ x: 0, y: 0, z: -1 }, -mins.z)
|
|
949
|
+
];
|
|
1010
950
|
return {
|
|
1011
|
-
|
|
1012
|
-
|
|
1013
|
-
Object.entries(data).forEach(([k, v]) => storage.setItem(k, v));
|
|
1014
|
-
},
|
|
1015
|
-
verify(key, value) {
|
|
1016
|
-
return storage.getItem(key) === value;
|
|
1017
|
-
}
|
|
951
|
+
contents,
|
|
952
|
+
sides: planes.map((plane) => ({ plane, surfaceFlags: 0 }))
|
|
1018
953
|
};
|
|
1019
954
|
}
|
|
1020
955
|
|
|
1021
|
-
// src/
|
|
1022
|
-
|
|
1023
|
-
|
|
1024
|
-
|
|
1025
|
-
|
|
1026
|
-
|
|
1027
|
-
|
|
1028
|
-
|
|
956
|
+
// src/shared/mocks.ts
|
|
957
|
+
import { vi } from "vitest";
|
|
958
|
+
var createBinaryWriterMock = () => ({
|
|
959
|
+
writeByte: vi.fn(),
|
|
960
|
+
writeShort: vi.fn(),
|
|
961
|
+
writeLong: vi.fn(),
|
|
962
|
+
writeString: vi.fn(),
|
|
963
|
+
writeBytes: vi.fn(),
|
|
964
|
+
getBuffer: vi.fn(() => new Uint8Array(0)),
|
|
965
|
+
reset: vi.fn(),
|
|
966
|
+
// Legacy methods (if any)
|
|
967
|
+
writeInt8: vi.fn(),
|
|
968
|
+
writeUint8: vi.fn(),
|
|
969
|
+
writeInt16: vi.fn(),
|
|
970
|
+
writeUint16: vi.fn(),
|
|
971
|
+
writeInt32: vi.fn(),
|
|
972
|
+
writeUint32: vi.fn(),
|
|
973
|
+
writeFloat: vi.fn(),
|
|
974
|
+
getData: vi.fn(() => new Uint8Array(0))
|
|
975
|
+
});
|
|
976
|
+
var createNetChanMock = () => ({
|
|
977
|
+
qport: 1234,
|
|
978
|
+
// Sequencing
|
|
979
|
+
incomingSequence: 0,
|
|
980
|
+
outgoingSequence: 0,
|
|
981
|
+
incomingAcknowledged: 0,
|
|
982
|
+
// Reliable messaging
|
|
983
|
+
incomingReliableAcknowledged: false,
|
|
984
|
+
incomingReliableSequence: 0,
|
|
985
|
+
outgoingReliableSequence: 0,
|
|
986
|
+
reliableMessage: createBinaryWriterMock(),
|
|
987
|
+
reliableLength: 0,
|
|
988
|
+
// Fragmentation
|
|
989
|
+
fragmentSendOffset: 0,
|
|
990
|
+
fragmentBuffer: null,
|
|
991
|
+
fragmentLength: 0,
|
|
992
|
+
fragmentReceived: 0,
|
|
993
|
+
// Timing
|
|
994
|
+
lastReceived: 0,
|
|
995
|
+
lastSent: 0,
|
|
996
|
+
remoteAddress: { type: "IP", port: 1234 },
|
|
997
|
+
// Methods
|
|
998
|
+
setup: vi.fn(),
|
|
999
|
+
reset: vi.fn(),
|
|
1000
|
+
transmit: vi.fn(),
|
|
1001
|
+
process: vi.fn(),
|
|
1002
|
+
canSendReliable: vi.fn(() => true),
|
|
1003
|
+
writeReliableByte: vi.fn(),
|
|
1004
|
+
writeReliableShort: vi.fn(),
|
|
1005
|
+
writeReliableLong: vi.fn(),
|
|
1006
|
+
writeReliableString: vi.fn(),
|
|
1007
|
+
getReliableData: vi.fn(() => new Uint8Array(0)),
|
|
1008
|
+
needsKeepalive: vi.fn(() => false),
|
|
1009
|
+
isTimedOut: vi.fn(() => false)
|
|
1010
|
+
});
|
|
1011
|
+
var createBinaryStreamMock = () => ({
|
|
1012
|
+
getPosition: vi.fn(() => 0),
|
|
1013
|
+
getReadPosition: vi.fn(() => 0),
|
|
1014
|
+
getLength: vi.fn(() => 0),
|
|
1015
|
+
getRemaining: vi.fn(() => 0),
|
|
1016
|
+
seek: vi.fn(),
|
|
1017
|
+
setReadPosition: vi.fn(),
|
|
1018
|
+
hasMore: vi.fn(() => true),
|
|
1019
|
+
hasBytes: vi.fn((amount) => true),
|
|
1020
|
+
readChar: vi.fn(() => 0),
|
|
1021
|
+
readByte: vi.fn(() => 0),
|
|
1022
|
+
readShort: vi.fn(() => 0),
|
|
1023
|
+
readUShort: vi.fn(() => 0),
|
|
1024
|
+
readLong: vi.fn(() => 0),
|
|
1025
|
+
readULong: vi.fn(() => 0),
|
|
1026
|
+
readFloat: vi.fn(() => 0),
|
|
1027
|
+
readString: vi.fn(() => ""),
|
|
1028
|
+
readStringLine: vi.fn(() => ""),
|
|
1029
|
+
readCoord: vi.fn(() => 0),
|
|
1030
|
+
readAngle: vi.fn(() => 0),
|
|
1031
|
+
readAngle16: vi.fn(() => 0),
|
|
1032
|
+
readData: vi.fn((length) => new Uint8Array(length)),
|
|
1033
|
+
readPos: vi.fn(),
|
|
1034
|
+
readDir: vi.fn()
|
|
1035
|
+
});
|
|
1036
|
+
|
|
1037
|
+
// src/game/factories.ts
|
|
1038
|
+
var createPlayerStateFactory = (overrides) => ({
|
|
1039
|
+
pm_type: 0,
|
|
1040
|
+
pm_time: 0,
|
|
1041
|
+
pm_flags: 0,
|
|
1042
|
+
origin: { x: 0, y: 0, z: 0 },
|
|
1043
|
+
velocity: { x: 0, y: 0, z: 0 },
|
|
1044
|
+
viewAngles: { x: 0, y: 0, z: 0 },
|
|
1045
|
+
onGround: false,
|
|
1046
|
+
waterLevel: 0,
|
|
1047
|
+
watertype: 0,
|
|
1048
|
+
mins: { x: 0, y: 0, z: 0 },
|
|
1049
|
+
maxs: { x: 0, y: 0, z: 0 },
|
|
1050
|
+
damageAlpha: 0,
|
|
1051
|
+
damageIndicators: [],
|
|
1052
|
+
blend: [0, 0, 0, 0],
|
|
1053
|
+
stats: [],
|
|
1054
|
+
kick_angles: { x: 0, y: 0, z: 0 },
|
|
1055
|
+
kick_origin: { x: 0, y: 0, z: 0 },
|
|
1056
|
+
gunoffset: { x: 0, y: 0, z: 0 },
|
|
1057
|
+
gunangles: { x: 0, y: 0, z: 0 },
|
|
1058
|
+
gunindex: 0,
|
|
1059
|
+
gun_frame: 0,
|
|
1060
|
+
rdflags: 0,
|
|
1061
|
+
fov: 90,
|
|
1062
|
+
renderfx: 0,
|
|
1063
|
+
...overrides
|
|
1064
|
+
});
|
|
1065
|
+
var createEntityStateFactory = (overrides) => ({
|
|
1066
|
+
number: 0,
|
|
1067
|
+
origin: { x: 0, y: 0, z: 0 },
|
|
1068
|
+
angles: { x: 0, y: 0, z: 0 },
|
|
1069
|
+
oldOrigin: { x: 0, y: 0, z: 0 },
|
|
1070
|
+
modelIndex: 0,
|
|
1071
|
+
modelIndex2: 0,
|
|
1072
|
+
modelIndex3: 0,
|
|
1073
|
+
modelIndex4: 0,
|
|
1074
|
+
frame: 0,
|
|
1075
|
+
skinNum: 0,
|
|
1076
|
+
effects: 0,
|
|
1077
|
+
renderfx: 0,
|
|
1078
|
+
solid: 0,
|
|
1079
|
+
sound: 0,
|
|
1080
|
+
event: 0,
|
|
1081
|
+
...overrides
|
|
1082
|
+
});
|
|
1083
|
+
var createGameStateSnapshotFactory = (overrides) => ({
|
|
1084
|
+
gravity: { x: 0, y: 0, z: -800 },
|
|
1085
|
+
origin: { x: 0, y: 0, z: 0 },
|
|
1086
|
+
velocity: { x: 0, y: 0, z: 0 },
|
|
1087
|
+
viewangles: { x: 0, y: 0, z: 0 },
|
|
1088
|
+
level: { timeSeconds: 0, frameNumber: 0, previousTimeSeconds: 0, deltaSeconds: 0.1 },
|
|
1089
|
+
entities: {
|
|
1090
|
+
activeCount: 0,
|
|
1091
|
+
worldClassname: "worldspawn"
|
|
1092
|
+
},
|
|
1093
|
+
packetEntities: [],
|
|
1094
|
+
pmFlags: 0,
|
|
1095
|
+
pmType: 0,
|
|
1096
|
+
waterlevel: 0,
|
|
1097
|
+
watertype: 0,
|
|
1098
|
+
deltaAngles: { x: 0, y: 0, z: 0 },
|
|
1099
|
+
health: 100,
|
|
1100
|
+
armor: 0,
|
|
1101
|
+
ammo: 0,
|
|
1102
|
+
blend: [0, 0, 0, 0],
|
|
1103
|
+
damageAlpha: 0,
|
|
1104
|
+
damageIndicators: [],
|
|
1105
|
+
stats: [],
|
|
1106
|
+
kick_angles: { x: 0, y: 0, z: 0 },
|
|
1107
|
+
kick_origin: { x: 0, y: 0, z: 0 },
|
|
1108
|
+
gunoffset: { x: 0, y: 0, z: 0 },
|
|
1109
|
+
gunangles: { x: 0, y: 0, z: 0 },
|
|
1110
|
+
gunindex: 0,
|
|
1111
|
+
pm_time: 0,
|
|
1112
|
+
gun_frame: 0,
|
|
1113
|
+
rdflags: 0,
|
|
1114
|
+
fov: 90,
|
|
1115
|
+
renderfx: 0,
|
|
1116
|
+
pm_flags: 0,
|
|
1117
|
+
pm_type: 0,
|
|
1118
|
+
...overrides
|
|
1119
|
+
});
|
|
1120
|
+
|
|
1121
|
+
// src/game/helpers.ts
|
|
1122
|
+
import { vi as vi2 } from "vitest";
|
|
1123
|
+
import { Entity, SpawnRegistry, ScriptHookRegistry } from "@quake2ts/game";
|
|
1124
|
+
import { createRandomGenerator } from "@quake2ts/shared";
|
|
1125
|
+
import { intersects, stairTrace, ladderTrace } from "@quake2ts/shared";
|
|
1126
|
+
var createMockEngine = () => ({
|
|
1127
|
+
sound: vi2.fn(),
|
|
1128
|
+
soundIndex: vi2.fn((sound) => 0),
|
|
1129
|
+
modelIndex: vi2.fn((model) => 0),
|
|
1130
|
+
centerprintf: vi2.fn()
|
|
1131
|
+
});
|
|
1132
|
+
var createMockGame = (seed = 12345) => {
|
|
1133
|
+
const spawnRegistry = new SpawnRegistry();
|
|
1134
|
+
const hooks = new ScriptHookRegistry();
|
|
1135
|
+
const game = {
|
|
1136
|
+
random: createRandomGenerator({ seed }),
|
|
1137
|
+
registerEntitySpawn: vi2.fn((classname, spawnFunc) => {
|
|
1138
|
+
spawnRegistry.register(classname, (entity) => spawnFunc(entity));
|
|
1029
1139
|
}),
|
|
1030
|
-
|
|
1031
|
-
|
|
1032
|
-
},
|
|
1033
|
-
start: () => {
|
|
1034
|
-
},
|
|
1035
|
-
stop: () => {
|
|
1036
|
-
},
|
|
1037
|
-
frequency: { value: 440 }
|
|
1140
|
+
unregisterEntitySpawn: vi2.fn((classname) => {
|
|
1141
|
+
spawnRegistry.unregister(classname);
|
|
1038
1142
|
}),
|
|
1039
|
-
|
|
1040
|
-
|
|
1041
|
-
|
|
1042
|
-
|
|
1043
|
-
|
|
1044
|
-
stop: () => {
|
|
1045
|
-
},
|
|
1046
|
-
buffer: null,
|
|
1047
|
-
playbackRate: { value: 1 },
|
|
1048
|
-
loop: false
|
|
1143
|
+
getCustomEntities: vi2.fn(() => Array.from(spawnRegistry.keys())),
|
|
1144
|
+
hooks,
|
|
1145
|
+
registerHooks: vi2.fn((newHooks) => hooks.register(newHooks)),
|
|
1146
|
+
spawnWorld: vi2.fn(() => {
|
|
1147
|
+
hooks.onMapLoad("q2dm1");
|
|
1049
1148
|
}),
|
|
1050
|
-
|
|
1051
|
-
|
|
1052
|
-
state: "running",
|
|
1053
|
-
resume: async () => {
|
|
1054
|
-
},
|
|
1055
|
-
suspend: async () => {
|
|
1056
|
-
},
|
|
1057
|
-
close: async () => {
|
|
1058
|
-
},
|
|
1059
|
-
decodeAudioData: async (buffer) => ({
|
|
1060
|
-
duration: 1,
|
|
1061
|
-
length: 44100,
|
|
1062
|
-
sampleRate: 44100,
|
|
1063
|
-
numberOfChannels: 2,
|
|
1064
|
-
getChannelData: () => new Float32Array(44100)
|
|
1149
|
+
clientBegin: vi2.fn((client) => {
|
|
1150
|
+
hooks.onPlayerSpawn({});
|
|
1065
1151
|
}),
|
|
1066
|
-
|
|
1067
|
-
|
|
1068
|
-
length,
|
|
1069
|
-
sampleRate,
|
|
1070
|
-
numberOfChannels: channels,
|
|
1071
|
-
getChannelData: () => new Float32Array(length)
|
|
1152
|
+
damage: vi2.fn((amount) => {
|
|
1153
|
+
hooks.onDamage({}, null, null, amount, 0, 0);
|
|
1072
1154
|
})
|
|
1073
1155
|
};
|
|
1074
|
-
}
|
|
1075
|
-
|
|
1076
|
-
|
|
1077
|
-
|
|
1078
|
-
|
|
1079
|
-
|
|
1156
|
+
return { game, spawnRegistry };
|
|
1157
|
+
};
|
|
1158
|
+
function createTestContext(options) {
|
|
1159
|
+
const engine = createMockEngine();
|
|
1160
|
+
const seed = options?.seed ?? 12345;
|
|
1161
|
+
const { game, spawnRegistry } = createMockGame(seed);
|
|
1162
|
+
const traceFn = vi2.fn((start, end, mins, maxs) => ({
|
|
1163
|
+
fraction: 1,
|
|
1164
|
+
ent: null,
|
|
1165
|
+
allsolid: false,
|
|
1166
|
+
startsolid: false,
|
|
1167
|
+
endpos: end,
|
|
1168
|
+
plane: { normal: { x: 0, y: 0, z: 1 }, dist: 0 },
|
|
1169
|
+
surfaceFlags: 0,
|
|
1170
|
+
contents: 0
|
|
1171
|
+
}));
|
|
1172
|
+
const entityList = options?.initialEntities ? [...options.initialEntities] : [];
|
|
1173
|
+
const hooks = game.hooks;
|
|
1174
|
+
const entities = {
|
|
1175
|
+
spawn: vi2.fn(() => {
|
|
1176
|
+
const ent = new Entity(entityList.length + 1);
|
|
1177
|
+
entityList.push(ent);
|
|
1178
|
+
hooks.onEntitySpawn(ent);
|
|
1179
|
+
return ent;
|
|
1180
|
+
}),
|
|
1181
|
+
free: vi2.fn((ent) => {
|
|
1182
|
+
const idx = entityList.indexOf(ent);
|
|
1183
|
+
if (idx !== -1) {
|
|
1184
|
+
entityList.splice(idx, 1);
|
|
1080
1185
|
}
|
|
1081
|
-
|
|
1082
|
-
|
|
1083
|
-
|
|
1084
|
-
|
|
1085
|
-
|
|
1086
|
-
|
|
1087
|
-
|
|
1088
|
-
delete global.AudioContext;
|
|
1089
|
-
delete global.window.AudioContext;
|
|
1090
|
-
delete global.window.webkitAudioContext;
|
|
1091
|
-
}
|
|
1092
|
-
}
|
|
1093
|
-
function captureAudioEvents(context) {
|
|
1094
|
-
return [];
|
|
1095
|
-
}
|
|
1096
|
-
|
|
1097
|
-
// src/e2e/playwright.ts
|
|
1098
|
-
import { chromium } from "playwright";
|
|
1099
|
-
async function createPlaywrightTestClient(options = {}) {
|
|
1100
|
-
const browser = await chromium.launch({
|
|
1101
|
-
headless: options.headless ?? true,
|
|
1102
|
-
args: options.args || [
|
|
1103
|
-
"--use-gl=egl",
|
|
1104
|
-
"--ignore-gpu-blocklist",
|
|
1105
|
-
"--no-sandbox",
|
|
1106
|
-
"--disable-setuid-sandbox"
|
|
1107
|
-
]
|
|
1108
|
-
});
|
|
1109
|
-
const context = await browser.newContext({
|
|
1110
|
-
viewport: options.viewport || { width: 1280, height: 720 },
|
|
1111
|
-
recordVideo: options.recordVideo,
|
|
1112
|
-
deviceScaleFactor: 1
|
|
1113
|
-
});
|
|
1114
|
-
const page = await context.newPage();
|
|
1115
|
-
const waitForGame = async (timeout = 1e4) => {
|
|
1116
|
-
try {
|
|
1117
|
-
await page.waitForFunction(() => {
|
|
1118
|
-
return window.game || document.querySelector("canvas");
|
|
1119
|
-
}, null, { timeout });
|
|
1120
|
-
} catch (e) {
|
|
1121
|
-
throw new Error(`Game did not initialize within ${timeout}ms`);
|
|
1122
|
-
}
|
|
1123
|
-
};
|
|
1124
|
-
const client = {
|
|
1125
|
-
browser,
|
|
1126
|
-
context,
|
|
1127
|
-
page,
|
|
1128
|
-
async navigate(url) {
|
|
1129
|
-
await page.goto(url, { waitUntil: "domcontentloaded" });
|
|
1130
|
-
},
|
|
1131
|
-
async waitForGame(timeout) {
|
|
1132
|
-
await waitForGame(timeout);
|
|
1133
|
-
},
|
|
1134
|
-
async injectInput(type, key) {
|
|
1135
|
-
if (type === "keydown") {
|
|
1136
|
-
await page.keyboard.down(key);
|
|
1137
|
-
} else {
|
|
1138
|
-
await page.keyboard.up(key);
|
|
1186
|
+
hooks.onEntityRemove(ent);
|
|
1187
|
+
}),
|
|
1188
|
+
finalizeSpawn: vi2.fn(),
|
|
1189
|
+
freeImmediate: vi2.fn((ent) => {
|
|
1190
|
+
const idx = entityList.indexOf(ent);
|
|
1191
|
+
if (idx !== -1) {
|
|
1192
|
+
entityList.splice(idx, 1);
|
|
1139
1193
|
}
|
|
1194
|
+
}),
|
|
1195
|
+
setSpawnRegistry: vi2.fn(),
|
|
1196
|
+
timeSeconds: 10,
|
|
1197
|
+
deltaSeconds: 0.1,
|
|
1198
|
+
modelIndex: vi2.fn(() => 0),
|
|
1199
|
+
scheduleThink: vi2.fn((entity, time) => {
|
|
1200
|
+
entity.nextthink = time;
|
|
1201
|
+
}),
|
|
1202
|
+
linkentity: vi2.fn(),
|
|
1203
|
+
trace: traceFn,
|
|
1204
|
+
pointcontents: vi2.fn(() => 0),
|
|
1205
|
+
multicast: vi2.fn(),
|
|
1206
|
+
unicast: vi2.fn(),
|
|
1207
|
+
engine,
|
|
1208
|
+
game,
|
|
1209
|
+
sound: vi2.fn((ent, chan, sound, vol, attn, timeofs) => {
|
|
1210
|
+
engine.sound(ent, chan, sound, vol, attn, timeofs);
|
|
1211
|
+
}),
|
|
1212
|
+
soundIndex: vi2.fn((sound) => engine.soundIndex(sound)),
|
|
1213
|
+
useTargets: vi2.fn((entity, activator) => {
|
|
1214
|
+
}),
|
|
1215
|
+
findByTargetName: vi2.fn(() => []),
|
|
1216
|
+
pickTarget: vi2.fn(() => null),
|
|
1217
|
+
killBox: vi2.fn(),
|
|
1218
|
+
rng: createRandomGenerator({ seed }),
|
|
1219
|
+
imports: {
|
|
1220
|
+
configstring: vi2.fn(),
|
|
1221
|
+
trace: traceFn,
|
|
1222
|
+
pointcontents: vi2.fn(() => 0)
|
|
1140
1223
|
},
|
|
1141
|
-
|
|
1142
|
-
|
|
1143
|
-
|
|
1144
|
-
|
|
1145
|
-
|
|
1146
|
-
} else if (type === "up") {
|
|
1147
|
-
await page.mouse.up({ button: button === 0 ? "left" : button === 2 ? "right" : "middle" });
|
|
1148
|
-
}
|
|
1224
|
+
level: {
|
|
1225
|
+
intermission_angle: { x: 0, y: 0, z: 0 },
|
|
1226
|
+
intermission_origin: { x: 0, y: 0, z: 0 },
|
|
1227
|
+
next_auto_save: 0,
|
|
1228
|
+
health_bar_entities: null
|
|
1149
1229
|
},
|
|
1150
|
-
|
|
1151
|
-
|
|
1230
|
+
targetNameIndex: /* @__PURE__ */ new Map(),
|
|
1231
|
+
forEachEntity: vi2.fn((callback) => {
|
|
1232
|
+
entityList.forEach(callback);
|
|
1233
|
+
}),
|
|
1234
|
+
find: vi2.fn((predicate) => {
|
|
1235
|
+
return entityList.find(predicate);
|
|
1236
|
+
}),
|
|
1237
|
+
findByClassname: vi2.fn((classname) => {
|
|
1238
|
+
return entityList.find((e) => e.classname === classname);
|
|
1239
|
+
}),
|
|
1240
|
+
beginFrame: vi2.fn((timeSeconds) => {
|
|
1241
|
+
entities.timeSeconds = timeSeconds;
|
|
1242
|
+
}),
|
|
1243
|
+
targetAwareness: {
|
|
1244
|
+
timeSeconds: 10,
|
|
1245
|
+
frameNumber: 1,
|
|
1246
|
+
sightEntity: null,
|
|
1247
|
+
soundEntity: null
|
|
1152
1248
|
},
|
|
1153
|
-
|
|
1154
|
-
|
|
1155
|
-
|
|
1249
|
+
// Adding missing properties to satisfy EntitySystem interface partially or fully
|
|
1250
|
+
// We cast to unknown first anyway, but filling these in makes it safer for consumers
|
|
1251
|
+
skill: 1,
|
|
1252
|
+
deathmatch: false,
|
|
1253
|
+
coop: false,
|
|
1254
|
+
activeCount: entityList.length,
|
|
1255
|
+
world: entityList.find((e) => e.classname === "worldspawn") || new Entity(0)
|
|
1256
|
+
// ... other EntitySystem properties would go here
|
|
1156
1257
|
};
|
|
1157
|
-
return client;
|
|
1158
|
-
}
|
|
1159
|
-
async function waitForGameReady(page, timeout = 1e4) {
|
|
1160
|
-
await page.waitForFunction(() => {
|
|
1161
|
-
return window.game || document.querySelector("canvas");
|
|
1162
|
-
}, null, { timeout });
|
|
1163
|
-
}
|
|
1164
|
-
async function captureGameState(page) {
|
|
1165
|
-
return await page.evaluate(() => {
|
|
1166
|
-
const game = window.game;
|
|
1167
|
-
if (!game) return {};
|
|
1168
|
-
return {};
|
|
1169
|
-
});
|
|
1170
|
-
}
|
|
1171
|
-
|
|
1172
|
-
// src/e2e/network.ts
|
|
1173
|
-
var CONDITIONS = {
|
|
1174
|
-
"good": {
|
|
1175
|
-
offline: false,
|
|
1176
|
-
downloadThroughput: 10 * 1024 * 1024,
|
|
1177
|
-
// 10 Mbps
|
|
1178
|
-
uploadThroughput: 5 * 1024 * 1024,
|
|
1179
|
-
// 5 Mbps
|
|
1180
|
-
latency: 20
|
|
1181
|
-
},
|
|
1182
|
-
"slow": {
|
|
1183
|
-
offline: false,
|
|
1184
|
-
downloadThroughput: 500 * 1024,
|
|
1185
|
-
// 500 Kbps
|
|
1186
|
-
uploadThroughput: 500 * 1024,
|
|
1187
|
-
latency: 400
|
|
1188
|
-
},
|
|
1189
|
-
"unstable": {
|
|
1190
|
-
offline: false,
|
|
1191
|
-
downloadThroughput: 1 * 1024 * 1024,
|
|
1192
|
-
uploadThroughput: 1 * 1024 * 1024,
|
|
1193
|
-
latency: 100
|
|
1194
|
-
// Jitter needs logic not simple preset
|
|
1195
|
-
},
|
|
1196
|
-
"offline": {
|
|
1197
|
-
offline: true,
|
|
1198
|
-
downloadThroughput: 0,
|
|
1199
|
-
uploadThroughput: 0,
|
|
1200
|
-
latency: 0
|
|
1201
|
-
}
|
|
1202
|
-
};
|
|
1203
|
-
function simulateNetworkCondition(condition) {
|
|
1204
|
-
const config = CONDITIONS[condition];
|
|
1205
|
-
return createCustomNetworkCondition(config.latency, 0, 0, config);
|
|
1206
|
-
}
|
|
1207
|
-
function createCustomNetworkCondition(latency, jitter = 0, packetLoss = 0, baseConfig) {
|
|
1208
1258
|
return {
|
|
1209
|
-
|
|
1210
|
-
|
|
1211
|
-
|
|
1212
|
-
|
|
1213
|
-
|
|
1214
|
-
|
|
1215
|
-
|
|
1216
|
-
|
|
1217
|
-
|
|
1218
|
-
|
|
1219
|
-
|
|
1220
|
-
async clear(page) {
|
|
1221
|
-
const client = await page.context().newCDPSession(page);
|
|
1222
|
-
await client.send("Network.emulateNetworkConditions", {
|
|
1223
|
-
offline: false,
|
|
1224
|
-
latency: 0,
|
|
1225
|
-
downloadThroughput: -1,
|
|
1226
|
-
uploadThroughput: -1
|
|
1227
|
-
});
|
|
1228
|
-
}
|
|
1259
|
+
keyValues: {},
|
|
1260
|
+
entities,
|
|
1261
|
+
game,
|
|
1262
|
+
engine,
|
|
1263
|
+
health_multiplier: 1,
|
|
1264
|
+
warn: vi2.fn(),
|
|
1265
|
+
free: vi2.fn(),
|
|
1266
|
+
// Mock precache functions if they are part of SpawnContext in future or TestContext extensions
|
|
1267
|
+
precacheModel: vi2.fn(),
|
|
1268
|
+
precacheSound: vi2.fn(),
|
|
1269
|
+
precacheImage: vi2.fn()
|
|
1229
1270
|
};
|
|
1230
1271
|
}
|
|
1231
|
-
|
|
1232
|
-
|
|
1233
|
-
offline: false,
|
|
1234
|
-
latency: 0,
|
|
1235
|
-
downloadThroughput: bytesPerSecond,
|
|
1236
|
-
uploadThroughput: bytesPerSecond
|
|
1237
|
-
});
|
|
1238
|
-
await simulator.apply(page);
|
|
1239
|
-
}
|
|
1240
|
-
|
|
1241
|
-
// src/e2e/visual.ts
|
|
1242
|
-
import path from "path";
|
|
1243
|
-
import fs from "fs/promises";
|
|
1244
|
-
async function captureGameScreenshot(page, name, options = {}) {
|
|
1245
|
-
const dir = options.dir || "__screenshots__";
|
|
1246
|
-
const screenshotPath = path.join(dir, `${name}.png`);
|
|
1247
|
-
await fs.mkdir(dir, { recursive: true });
|
|
1248
|
-
return await page.screenshot({
|
|
1249
|
-
path: screenshotPath,
|
|
1250
|
-
fullPage: options.fullPage ?? false,
|
|
1251
|
-
animations: "disabled",
|
|
1252
|
-
// Try to freeze animations if possible
|
|
1253
|
-
caret: "hide"
|
|
1254
|
-
});
|
|
1255
|
-
}
|
|
1256
|
-
async function compareScreenshots(baseline, current, threshold = 0.1) {
|
|
1257
|
-
if (baseline.equals(current)) {
|
|
1258
|
-
return { pixelDiff: 0, matched: true };
|
|
1259
|
-
}
|
|
1260
|
-
return {
|
|
1261
|
-
pixelDiff: -1,
|
|
1262
|
-
// Unknown magnitude
|
|
1263
|
-
matched: false
|
|
1264
|
-
};
|
|
1272
|
+
function createSpawnContext() {
|
|
1273
|
+
return createTestContext();
|
|
1265
1274
|
}
|
|
1266
|
-
function
|
|
1267
|
-
return
|
|
1268
|
-
async capture(snapshotName) {
|
|
1269
|
-
return await captureGameScreenshot(page, `${sceneName}-${snapshotName}`);
|
|
1270
|
-
},
|
|
1271
|
-
async compare(snapshotName, baselineDir) {
|
|
1272
|
-
const name = `${sceneName}-${snapshotName}`;
|
|
1273
|
-
const current = await captureGameScreenshot(page, name, { dir: "__screenshots__/current" });
|
|
1274
|
-
try {
|
|
1275
|
-
const baselinePath = path.join(baselineDir, `${name}.png`);
|
|
1276
|
-
const baseline = await fs.readFile(baselinePath);
|
|
1277
|
-
return await compareScreenshots(baseline, current);
|
|
1278
|
-
} catch (e) {
|
|
1279
|
-
return { pixelDiff: -1, matched: false };
|
|
1280
|
-
}
|
|
1281
|
-
}
|
|
1282
|
-
};
|
|
1275
|
+
function createEntity() {
|
|
1276
|
+
return new Entity(1);
|
|
1283
1277
|
}
|
|
1284
1278
|
export {
|
|
1285
1279
|
InputInjector,
|
|
1286
1280
|
MockPointerLock,
|
|
1287
1281
|
captureAudioEvents,
|
|
1288
1282
|
captureCanvasDrawCalls,
|
|
1289
|
-
captureGameScreenshot,
|
|
1290
1283
|
captureGameState,
|
|
1291
|
-
compareScreenshots,
|
|
1292
1284
|
createBinaryStreamMock,
|
|
1293
1285
|
createBinaryWriterMock,
|
|
1294
|
-
|
|
1286
|
+
createControlledTimer,
|
|
1295
1287
|
createEntity,
|
|
1296
1288
|
createEntityStateFactory,
|
|
1297
1289
|
createGameStateSnapshotFactory,
|
|
@@ -1314,7 +1306,6 @@ export {
|
|
|
1314
1306
|
createSpawnContext,
|
|
1315
1307
|
createStorageTestScenario,
|
|
1316
1308
|
createTestContext,
|
|
1317
|
-
createVisualTestScenario,
|
|
1318
1309
|
intersects,
|
|
1319
1310
|
ladderTrace,
|
|
1320
1311
|
makeAxisBrush,
|
|
@@ -1329,11 +1320,9 @@ export {
|
|
|
1329
1320
|
setupNodeEnvironment,
|
|
1330
1321
|
simulateFrames,
|
|
1331
1322
|
simulateFramesWithMock,
|
|
1332
|
-
simulateNetworkCondition,
|
|
1333
1323
|
stairTrace,
|
|
1334
1324
|
teardownBrowserEnvironment,
|
|
1335
1325
|
teardownMockAudioContext,
|
|
1336
|
-
throttleBandwidth,
|
|
1337
1326
|
waitForGameReady
|
|
1338
1327
|
};
|
|
1339
1328
|
//# sourceMappingURL=index.js.map
|