quake2ts 0.0.556 → 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 +17 -17
- package/packages/client/dist/browser/index.global.js.map +1 -1
- package/packages/client/dist/cjs/index.cjs +5369 -3577
- package/packages/client/dist/cjs/index.cjs.map +1 -1
- package/packages/client/dist/esm/index.js +5369 -3577
- 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/cjs/index.cjs +2256 -534
- package/packages/engine/dist/cjs/index.cjs.map +1 -1
- package/packages/engine/dist/esm/index.js +2266 -538
- package/packages/engine/dist/esm/index.js.map +1 -1
- package/packages/engine/dist/tsconfig.tsbuildinfo +1 -1
- package/packages/engine/dist/types/assets/visibilityAnalyzer.d.ts +7 -2
- package/packages/engine/dist/types/assets/visibilityAnalyzer.d.ts.map +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 +1178 -580
- package/packages/test-utils/dist/index.cjs.map +1 -1
- package/packages/test-utils/dist/index.d.cts +198 -47
- package/packages/test-utils/dist/index.d.ts +198 -47
- package/packages/test-utils/dist/index.js +1149 -582
- package/packages/test-utils/dist/index.js.map +1 -1
- package/packages/tools/dist/tsconfig.tsbuildinfo +1 -1
|
@@ -1,411 +1,257 @@
|
|
|
1
|
-
// src/
|
|
2
|
-
import {
|
|
3
|
-
|
|
4
|
-
|
|
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
|
-
});
|
|
1
|
+
// src/setup/browser.ts
|
|
2
|
+
import { JSDOM } from "jsdom";
|
|
3
|
+
import { Canvas, Image, ImageData } from "@napi-rs/canvas";
|
|
4
|
+
import "fake-indexeddb/auto";
|
|
81
5
|
|
|
82
|
-
// src/
|
|
83
|
-
|
|
84
|
-
|
|
85
|
-
|
|
86
|
-
|
|
87
|
-
|
|
88
|
-
|
|
89
|
-
|
|
90
|
-
|
|
91
|
-
|
|
92
|
-
|
|
93
|
-
|
|
94
|
-
}
|
|
95
|
-
|
|
96
|
-
|
|
97
|
-
|
|
98
|
-
|
|
99
|
-
|
|
100
|
-
|
|
101
|
-
|
|
102
|
-
|
|
103
|
-
|
|
104
|
-
|
|
105
|
-
|
|
106
|
-
|
|
107
|
-
|
|
108
|
-
|
|
109
|
-
|
|
110
|
-
|
|
111
|
-
|
|
112
|
-
|
|
113
|
-
|
|
114
|
-
|
|
115
|
-
|
|
116
|
-
|
|
117
|
-
|
|
118
|
-
|
|
119
|
-
|
|
120
|
-
|
|
121
|
-
|
|
122
|
-
|
|
123
|
-
|
|
124
|
-
|
|
125
|
-
|
|
126
|
-
|
|
127
|
-
|
|
128
|
-
|
|
129
|
-
|
|
130
|
-
|
|
131
|
-
|
|
132
|
-
|
|
133
|
-
|
|
134
|
-
|
|
135
|
-
|
|
136
|
-
|
|
137
|
-
|
|
138
|
-
|
|
139
|
-
|
|
140
|
-
|
|
141
|
-
|
|
142
|
-
|
|
143
|
-
|
|
144
|
-
|
|
145
|
-
|
|
146
|
-
|
|
147
|
-
|
|
148
|
-
|
|
149
|
-
|
|
150
|
-
|
|
6
|
+
// src/e2e/input.ts
|
|
7
|
+
var MockPointerLock = class {
|
|
8
|
+
static setup(doc) {
|
|
9
|
+
let _pointerLockElement = null;
|
|
10
|
+
Object.defineProperty(doc, "pointerLockElement", {
|
|
11
|
+
get: () => _pointerLockElement,
|
|
12
|
+
configurable: true
|
|
13
|
+
});
|
|
14
|
+
doc.exitPointerLock = () => {
|
|
15
|
+
if (_pointerLockElement) {
|
|
16
|
+
_pointerLockElement = null;
|
|
17
|
+
doc.dispatchEvent(new Event("pointerlockchange"));
|
|
18
|
+
}
|
|
19
|
+
};
|
|
20
|
+
global.HTMLElement.prototype.requestPointerLock = function() {
|
|
21
|
+
_pointerLockElement = this;
|
|
22
|
+
doc.dispatchEvent(new Event("pointerlockchange"));
|
|
23
|
+
};
|
|
24
|
+
}
|
|
25
|
+
};
|
|
26
|
+
var InputInjector = class {
|
|
27
|
+
constructor(doc, win) {
|
|
28
|
+
this.doc = doc;
|
|
29
|
+
this.win = win;
|
|
30
|
+
}
|
|
31
|
+
keyDown(key, code) {
|
|
32
|
+
const event = new this.win.KeyboardEvent("keydown", {
|
|
33
|
+
key,
|
|
34
|
+
code: code || key,
|
|
35
|
+
bubbles: true,
|
|
36
|
+
cancelable: true,
|
|
37
|
+
view: this.win
|
|
38
|
+
});
|
|
39
|
+
this.doc.dispatchEvent(event);
|
|
40
|
+
}
|
|
41
|
+
keyUp(key, code) {
|
|
42
|
+
const event = new this.win.KeyboardEvent("keyup", {
|
|
43
|
+
key,
|
|
44
|
+
code: code || key,
|
|
45
|
+
bubbles: true,
|
|
46
|
+
cancelable: true,
|
|
47
|
+
view: this.win
|
|
48
|
+
});
|
|
49
|
+
this.doc.dispatchEvent(event);
|
|
50
|
+
}
|
|
51
|
+
mouseMove(movementX, movementY, clientX = 0, clientY = 0) {
|
|
52
|
+
const event = new this.win.MouseEvent("mousemove", {
|
|
53
|
+
bubbles: true,
|
|
54
|
+
cancelable: true,
|
|
55
|
+
view: this.win,
|
|
56
|
+
clientX,
|
|
57
|
+
clientY,
|
|
58
|
+
movementX,
|
|
59
|
+
// Note: JSDOM might not support this standard property fully on event init
|
|
60
|
+
movementY
|
|
61
|
+
});
|
|
62
|
+
Object.defineProperty(event, "movementX", { value: movementX });
|
|
63
|
+
Object.defineProperty(event, "movementY", { value: movementY });
|
|
64
|
+
const target = this.doc.pointerLockElement || this.doc;
|
|
65
|
+
target.dispatchEvent(event);
|
|
66
|
+
}
|
|
67
|
+
mouseDown(button = 0) {
|
|
68
|
+
const event = new this.win.MouseEvent("mousedown", {
|
|
69
|
+
button,
|
|
70
|
+
bubbles: true,
|
|
71
|
+
cancelable: true,
|
|
72
|
+
view: this.win
|
|
73
|
+
});
|
|
74
|
+
const target = this.doc.pointerLockElement || this.doc;
|
|
75
|
+
target.dispatchEvent(event);
|
|
76
|
+
}
|
|
77
|
+
mouseUp(button = 0) {
|
|
78
|
+
const event = new this.win.MouseEvent("mouseup", {
|
|
79
|
+
button,
|
|
80
|
+
bubbles: true,
|
|
81
|
+
cancelable: true,
|
|
82
|
+
view: this.win
|
|
83
|
+
});
|
|
84
|
+
const target = this.doc.pointerLockElement || this.doc;
|
|
85
|
+
target.dispatchEvent(event);
|
|
86
|
+
}
|
|
87
|
+
wheel(deltaY) {
|
|
88
|
+
const event = new this.win.WheelEvent("wheel", {
|
|
89
|
+
deltaY,
|
|
90
|
+
bubbles: true,
|
|
91
|
+
cancelable: true,
|
|
92
|
+
view: this.win
|
|
93
|
+
});
|
|
94
|
+
const target = this.doc.pointerLockElement || this.doc;
|
|
95
|
+
target.dispatchEvent(event);
|
|
96
|
+
}
|
|
97
|
+
};
|
|
151
98
|
|
|
152
|
-
// src/
|
|
153
|
-
|
|
154
|
-
|
|
155
|
-
|
|
156
|
-
|
|
157
|
-
|
|
158
|
-
|
|
159
|
-
|
|
160
|
-
|
|
161
|
-
|
|
162
|
-
|
|
163
|
-
|
|
164
|
-
|
|
165
|
-
|
|
166
|
-
|
|
167
|
-
|
|
168
|
-
|
|
169
|
-
|
|
170
|
-
|
|
171
|
-
|
|
172
|
-
|
|
173
|
-
|
|
174
|
-
|
|
175
|
-
|
|
176
|
-
|
|
177
|
-
|
|
178
|
-
|
|
179
|
-
|
|
180
|
-
|
|
181
|
-
|
|
182
|
-
|
|
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)
|
|
99
|
+
// src/setup/webgl.ts
|
|
100
|
+
function createMockWebGL2Context(canvas) {
|
|
101
|
+
const gl = {
|
|
102
|
+
canvas,
|
|
103
|
+
drawingBufferWidth: canvas.width,
|
|
104
|
+
drawingBufferHeight: canvas.height,
|
|
105
|
+
// Constants
|
|
106
|
+
VERTEX_SHADER: 35633,
|
|
107
|
+
FRAGMENT_SHADER: 35632,
|
|
108
|
+
COMPILE_STATUS: 35713,
|
|
109
|
+
LINK_STATUS: 35714,
|
|
110
|
+
ARRAY_BUFFER: 34962,
|
|
111
|
+
ELEMENT_ARRAY_BUFFER: 34963,
|
|
112
|
+
STATIC_DRAW: 35044,
|
|
113
|
+
DYNAMIC_DRAW: 35048,
|
|
114
|
+
FLOAT: 5126,
|
|
115
|
+
DEPTH_TEST: 2929,
|
|
116
|
+
BLEND: 3042,
|
|
117
|
+
SRC_ALPHA: 770,
|
|
118
|
+
ONE_MINUS_SRC_ALPHA: 771,
|
|
119
|
+
TEXTURE_2D: 3553,
|
|
120
|
+
RGBA: 6408,
|
|
121
|
+
UNSIGNED_BYTE: 5121,
|
|
122
|
+
COLOR_BUFFER_BIT: 16384,
|
|
123
|
+
DEPTH_BUFFER_BIT: 256,
|
|
124
|
+
TRIANGLES: 4,
|
|
125
|
+
TRIANGLE_STRIP: 5,
|
|
126
|
+
TRIANGLE_FAN: 6,
|
|
127
|
+
// Methods
|
|
128
|
+
createShader: () => ({}),
|
|
129
|
+
shaderSource: () => {
|
|
338
130
|
},
|
|
339
|
-
|
|
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
|
|
131
|
+
compileShader: () => {
|
|
344
132
|
},
|
|
345
|
-
|
|
346
|
-
|
|
347
|
-
|
|
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
|
|
133
|
+
getShaderParameter: (_, param) => {
|
|
134
|
+
if (param === 35713) return true;
|
|
135
|
+
return true;
|
|
363
136
|
},
|
|
364
|
-
|
|
365
|
-
|
|
366
|
-
|
|
367
|
-
|
|
368
|
-
|
|
369
|
-
|
|
370
|
-
|
|
371
|
-
|
|
372
|
-
|
|
373
|
-
|
|
374
|
-
|
|
375
|
-
|
|
376
|
-
|
|
377
|
-
|
|
378
|
-
|
|
379
|
-
|
|
380
|
-
|
|
381
|
-
|
|
382
|
-
|
|
383
|
-
|
|
384
|
-
|
|
385
|
-
|
|
386
|
-
|
|
387
|
-
|
|
388
|
-
|
|
389
|
-
}
|
|
390
|
-
|
|
391
|
-
|
|
137
|
+
getShaderInfoLog: () => "",
|
|
138
|
+
createProgram: () => ({}),
|
|
139
|
+
attachShader: () => {
|
|
140
|
+
},
|
|
141
|
+
linkProgram: () => {
|
|
142
|
+
},
|
|
143
|
+
getProgramParameter: (_, param) => {
|
|
144
|
+
if (param === 35714) return true;
|
|
145
|
+
return true;
|
|
146
|
+
},
|
|
147
|
+
getProgramInfoLog: () => "",
|
|
148
|
+
useProgram: () => {
|
|
149
|
+
},
|
|
150
|
+
createBuffer: () => ({}),
|
|
151
|
+
bindBuffer: () => {
|
|
152
|
+
},
|
|
153
|
+
bufferData: () => {
|
|
154
|
+
},
|
|
155
|
+
enableVertexAttribArray: () => {
|
|
156
|
+
},
|
|
157
|
+
vertexAttribPointer: () => {
|
|
158
|
+
},
|
|
159
|
+
enable: () => {
|
|
160
|
+
},
|
|
161
|
+
disable: () => {
|
|
162
|
+
},
|
|
163
|
+
depthMask: () => {
|
|
164
|
+
},
|
|
165
|
+
blendFunc: () => {
|
|
166
|
+
},
|
|
167
|
+
viewport: () => {
|
|
168
|
+
},
|
|
169
|
+
clearColor: () => {
|
|
170
|
+
},
|
|
171
|
+
clear: () => {
|
|
172
|
+
},
|
|
173
|
+
createTexture: () => ({}),
|
|
174
|
+
bindTexture: () => {
|
|
175
|
+
},
|
|
176
|
+
texImage2D: () => {
|
|
177
|
+
},
|
|
178
|
+
texParameteri: () => {
|
|
179
|
+
},
|
|
180
|
+
activeTexture: () => {
|
|
181
|
+
},
|
|
182
|
+
uniform1i: () => {
|
|
183
|
+
},
|
|
184
|
+
uniform1f: () => {
|
|
185
|
+
},
|
|
186
|
+
uniform2f: () => {
|
|
187
|
+
},
|
|
188
|
+
uniform3f: () => {
|
|
189
|
+
},
|
|
190
|
+
uniform4f: () => {
|
|
191
|
+
},
|
|
192
|
+
uniformMatrix4fv: () => {
|
|
193
|
+
},
|
|
194
|
+
getUniformLocation: () => ({}),
|
|
195
|
+
getAttribLocation: () => 0,
|
|
196
|
+
drawArrays: () => {
|
|
197
|
+
},
|
|
198
|
+
drawElements: () => {
|
|
199
|
+
},
|
|
200
|
+
createVertexArray: () => ({}),
|
|
201
|
+
bindVertexArray: () => {
|
|
202
|
+
},
|
|
203
|
+
deleteShader: () => {
|
|
204
|
+
},
|
|
205
|
+
deleteProgram: () => {
|
|
206
|
+
},
|
|
207
|
+
deleteBuffer: () => {
|
|
208
|
+
},
|
|
209
|
+
deleteTexture: () => {
|
|
210
|
+
},
|
|
211
|
+
deleteVertexArray: () => {
|
|
212
|
+
},
|
|
213
|
+
// WebGL2 specific
|
|
214
|
+
texImage3D: () => {
|
|
215
|
+
},
|
|
216
|
+
uniformBlockBinding: () => {
|
|
217
|
+
},
|
|
218
|
+
getExtension: () => null
|
|
219
|
+
};
|
|
220
|
+
return gl;
|
|
392
221
|
}
|
|
393
222
|
|
|
394
223
|
// src/setup/browser.ts
|
|
395
|
-
import { JSDOM } from "jsdom";
|
|
396
|
-
import { Canvas, Image, ImageData } from "@napi-rs/canvas";
|
|
397
|
-
import "fake-indexeddb/auto";
|
|
398
224
|
function setupBrowserEnvironment(options = {}) {
|
|
399
|
-
const {
|
|
225
|
+
const {
|
|
226
|
+
url = "http://localhost",
|
|
227
|
+
pretendToBeVisual = true,
|
|
228
|
+
resources = void 0,
|
|
229
|
+
enableWebGL2 = false,
|
|
230
|
+
enablePointerLock = false
|
|
231
|
+
} = options;
|
|
400
232
|
const dom = new JSDOM("<!DOCTYPE html><html><head></head><body></body></html>", {
|
|
401
233
|
url,
|
|
402
|
-
pretendToBeVisual
|
|
234
|
+
pretendToBeVisual,
|
|
235
|
+
resources
|
|
403
236
|
});
|
|
404
237
|
global.window = dom.window;
|
|
405
238
|
global.document = dom.window.document;
|
|
406
|
-
|
|
239
|
+
try {
|
|
240
|
+
global.navigator = dom.window.navigator;
|
|
241
|
+
} catch (e) {
|
|
242
|
+
try {
|
|
243
|
+
Object.defineProperty(global, "navigator", {
|
|
244
|
+
value: dom.window.navigator,
|
|
245
|
+
writable: true,
|
|
246
|
+
configurable: true
|
|
247
|
+
});
|
|
248
|
+
} catch (e2) {
|
|
249
|
+
console.warn("Could not assign global.navigator, skipping.");
|
|
250
|
+
}
|
|
251
|
+
}
|
|
407
252
|
global.location = dom.window.location;
|
|
408
253
|
global.HTMLElement = dom.window.HTMLElement;
|
|
254
|
+
global.HTMLCanvasElement = dom.window.HTMLCanvasElement;
|
|
409
255
|
global.Event = dom.window.Event;
|
|
410
256
|
global.CustomEvent = dom.window.CustomEvent;
|
|
411
257
|
global.DragEvent = dom.window.DragEvent;
|
|
@@ -458,6 +304,9 @@ function setupBrowserEnvironment(options = {}) {
|
|
|
458
304
|
if (contextId === "2d") {
|
|
459
305
|
return napiCanvas.getContext("2d", options3);
|
|
460
306
|
}
|
|
307
|
+
if (enableWebGL2 && contextId === "webgl2") {
|
|
308
|
+
return createMockWebGL2Context(domCanvas);
|
|
309
|
+
}
|
|
461
310
|
if (contextId === "webgl" || contextId === "webgl2") {
|
|
462
311
|
return originalGetContext(contextId, options3);
|
|
463
312
|
}
|
|
@@ -468,6 +317,15 @@ function setupBrowserEnvironment(options = {}) {
|
|
|
468
317
|
}
|
|
469
318
|
return originalCreateElement(tagName, options2);
|
|
470
319
|
};
|
|
320
|
+
if (enableWebGL2) {
|
|
321
|
+
const originalProtoGetContext = global.HTMLCanvasElement.prototype.getContext;
|
|
322
|
+
global.HTMLCanvasElement.prototype.getContext = function(contextId, options2) {
|
|
323
|
+
if (contextId === "webgl2") {
|
|
324
|
+
return createMockWebGL2Context(this);
|
|
325
|
+
}
|
|
326
|
+
return originalProtoGetContext.call(this, contextId, options2);
|
|
327
|
+
};
|
|
328
|
+
}
|
|
471
329
|
global.Image = Image;
|
|
472
330
|
global.ImageData = ImageData;
|
|
473
331
|
if (typeof global.createImageBitmap === "undefined") {
|
|
@@ -494,6 +352,24 @@ function setupBrowserEnvironment(options = {}) {
|
|
|
494
352
|
return Buffer.from(str, "base64").toString("binary");
|
|
495
353
|
};
|
|
496
354
|
}
|
|
355
|
+
if (enablePointerLock) {
|
|
356
|
+
MockPointerLock.setup(global.document);
|
|
357
|
+
}
|
|
358
|
+
if (typeof global.requestAnimationFrame === "undefined") {
|
|
359
|
+
let lastTime = 0;
|
|
360
|
+
global.requestAnimationFrame = (callback) => {
|
|
361
|
+
const currTime = Date.now();
|
|
362
|
+
const timeToCall = Math.max(0, 16 - (currTime - lastTime));
|
|
363
|
+
const id = setTimeout(() => {
|
|
364
|
+
callback(currTime + timeToCall);
|
|
365
|
+
}, timeToCall);
|
|
366
|
+
lastTime = currTime + timeToCall;
|
|
367
|
+
return id;
|
|
368
|
+
};
|
|
369
|
+
global.cancelAnimationFrame = (id) => {
|
|
370
|
+
clearTimeout(id);
|
|
371
|
+
};
|
|
372
|
+
}
|
|
497
373
|
}
|
|
498
374
|
function teardownBrowserEnvironment() {
|
|
499
375
|
delete global.window;
|
|
@@ -502,9 +378,100 @@ function teardownBrowserEnvironment() {
|
|
|
502
378
|
delete global.localStorage;
|
|
503
379
|
delete global.location;
|
|
504
380
|
delete global.HTMLElement;
|
|
381
|
+
delete global.HTMLCanvasElement;
|
|
505
382
|
delete global.Image;
|
|
506
383
|
delete global.ImageData;
|
|
507
384
|
delete global.createImageBitmap;
|
|
385
|
+
delete global.Event;
|
|
386
|
+
delete global.CustomEvent;
|
|
387
|
+
delete global.DragEvent;
|
|
388
|
+
delete global.MouseEvent;
|
|
389
|
+
delete global.KeyboardEvent;
|
|
390
|
+
delete global.FocusEvent;
|
|
391
|
+
delete global.WheelEvent;
|
|
392
|
+
delete global.InputEvent;
|
|
393
|
+
delete global.UIEvent;
|
|
394
|
+
}
|
|
395
|
+
|
|
396
|
+
// src/setup/canvas.ts
|
|
397
|
+
import { Canvas as Canvas2, Image as Image2, ImageData as ImageData2 } from "@napi-rs/canvas";
|
|
398
|
+
function createMockCanvas(width = 300, height = 150) {
|
|
399
|
+
if (typeof document !== "undefined" && document.createElement) {
|
|
400
|
+
const canvas2 = document.createElement("canvas");
|
|
401
|
+
canvas2.width = width;
|
|
402
|
+
canvas2.height = height;
|
|
403
|
+
return canvas2;
|
|
404
|
+
}
|
|
405
|
+
const canvas = new Canvas2(width, height);
|
|
406
|
+
const originalGetContext = canvas.getContext.bind(canvas);
|
|
407
|
+
canvas.getContext = function(contextId, options) {
|
|
408
|
+
if (contextId === "webgl2") {
|
|
409
|
+
return createMockWebGL2Context(canvas);
|
|
410
|
+
}
|
|
411
|
+
if (contextId === "2d") {
|
|
412
|
+
return originalGetContext("2d", options);
|
|
413
|
+
}
|
|
414
|
+
return originalGetContext(contextId, options);
|
|
415
|
+
};
|
|
416
|
+
return canvas;
|
|
417
|
+
}
|
|
418
|
+
function createMockCanvasContext2D(canvas) {
|
|
419
|
+
if (!canvas) {
|
|
420
|
+
canvas = createMockCanvas();
|
|
421
|
+
}
|
|
422
|
+
return canvas.getContext("2d");
|
|
423
|
+
}
|
|
424
|
+
function captureCanvasDrawCalls(context) {
|
|
425
|
+
const drawCalls = [];
|
|
426
|
+
const methodsToSpy = [
|
|
427
|
+
"fillRect",
|
|
428
|
+
"strokeRect",
|
|
429
|
+
"clearRect",
|
|
430
|
+
"fillText",
|
|
431
|
+
"strokeText",
|
|
432
|
+
"drawImage",
|
|
433
|
+
"beginPath",
|
|
434
|
+
"closePath",
|
|
435
|
+
"moveTo",
|
|
436
|
+
"lineTo",
|
|
437
|
+
"arc",
|
|
438
|
+
"arcTo",
|
|
439
|
+
"bezierCurveTo",
|
|
440
|
+
"quadraticCurveTo",
|
|
441
|
+
"stroke",
|
|
442
|
+
"fill",
|
|
443
|
+
"putImageData"
|
|
444
|
+
];
|
|
445
|
+
methodsToSpy.forEach((method) => {
|
|
446
|
+
const original = context[method];
|
|
447
|
+
if (typeof original === "function") {
|
|
448
|
+
context[method] = function(...args) {
|
|
449
|
+
drawCalls.push({ method, args });
|
|
450
|
+
return original.apply(this, args);
|
|
451
|
+
};
|
|
452
|
+
}
|
|
453
|
+
});
|
|
454
|
+
return drawCalls;
|
|
455
|
+
}
|
|
456
|
+
function createMockImageData(width, height, fillColor) {
|
|
457
|
+
const imageData = new ImageData2(width, height);
|
|
458
|
+
if (fillColor) {
|
|
459
|
+
const [r, g, b, a] = fillColor;
|
|
460
|
+
for (let i = 0; i < imageData.data.length; i += 4) {
|
|
461
|
+
imageData.data[i] = r;
|
|
462
|
+
imageData.data[i + 1] = g;
|
|
463
|
+
imageData.data[i + 2] = b;
|
|
464
|
+
imageData.data[i + 3] = a;
|
|
465
|
+
}
|
|
466
|
+
}
|
|
467
|
+
return imageData;
|
|
468
|
+
}
|
|
469
|
+
function createMockImage(width, height, src) {
|
|
470
|
+
const img = new Image2();
|
|
471
|
+
if (width) img.width = width;
|
|
472
|
+
if (height) img.height = height;
|
|
473
|
+
if (src) img.src = src;
|
|
474
|
+
return img;
|
|
508
475
|
}
|
|
509
476
|
|
|
510
477
|
// src/setup/node.ts
|
|
@@ -513,236 +480,831 @@ function setupNodeEnvironment() {
|
|
|
513
480
|
}
|
|
514
481
|
}
|
|
515
482
|
|
|
516
|
-
// src/setup/
|
|
517
|
-
|
|
518
|
-
|
|
519
|
-
|
|
520
|
-
|
|
521
|
-
|
|
522
|
-
|
|
523
|
-
|
|
524
|
-
|
|
525
|
-
|
|
526
|
-
|
|
527
|
-
|
|
528
|
-
|
|
529
|
-
|
|
530
|
-
|
|
531
|
-
|
|
532
|
-
|
|
533
|
-
|
|
534
|
-
|
|
535
|
-
|
|
536
|
-
|
|
537
|
-
|
|
538
|
-
|
|
539
|
-
|
|
540
|
-
|
|
541
|
-
|
|
542
|
-
|
|
543
|
-
|
|
544
|
-
|
|
545
|
-
|
|
546
|
-
shaderSource: () => {
|
|
547
|
-
},
|
|
548
|
-
compileShader: () => {
|
|
549
|
-
},
|
|
550
|
-
getShaderParameter: (_, param) => {
|
|
551
|
-
if (param === 35713) return true;
|
|
552
|
-
return true;
|
|
553
|
-
},
|
|
554
|
-
getShaderInfoLog: () => "",
|
|
555
|
-
createProgram: () => ({}),
|
|
556
|
-
attachShader: () => {
|
|
557
|
-
},
|
|
558
|
-
linkProgram: () => {
|
|
559
|
-
},
|
|
560
|
-
getProgramParameter: (_, param) => {
|
|
561
|
-
if (param === 35714) return true;
|
|
562
|
-
return true;
|
|
563
|
-
},
|
|
564
|
-
getProgramInfoLog: () => "",
|
|
565
|
-
useProgram: () => {
|
|
566
|
-
},
|
|
567
|
-
createBuffer: () => ({}),
|
|
568
|
-
bindBuffer: () => {
|
|
569
|
-
},
|
|
570
|
-
bufferData: () => {
|
|
571
|
-
},
|
|
572
|
-
enableVertexAttribArray: () => {
|
|
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));
|
|
573
513
|
},
|
|
574
|
-
|
|
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 () => {
|
|
575
553
|
},
|
|
576
|
-
|
|
554
|
+
suspend: async () => {
|
|
577
555
|
},
|
|
578
|
-
|
|
556
|
+
close: async () => {
|
|
579
557
|
},
|
|
580
|
-
|
|
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
|
+
|
|
596
|
+
// src/setup/timing.ts
|
|
597
|
+
var activeMockRAF;
|
|
598
|
+
function createMockRAF() {
|
|
599
|
+
let callbacks = [];
|
|
600
|
+
let nextId = 1;
|
|
601
|
+
let currentTime = 0;
|
|
602
|
+
const originalRAF = global.requestAnimationFrame;
|
|
603
|
+
const originalCancelRAF = global.cancelAnimationFrame;
|
|
604
|
+
const raf = (callback) => {
|
|
605
|
+
const id = nextId++;
|
|
606
|
+
callbacks.push({ id, callback });
|
|
607
|
+
return id;
|
|
608
|
+
};
|
|
609
|
+
const cancel = (id) => {
|
|
610
|
+
callbacks = callbacks.filter((cb) => cb.id !== id);
|
|
611
|
+
};
|
|
612
|
+
const mock = {
|
|
613
|
+
tick(timestamp) {
|
|
614
|
+
if (typeof timestamp !== "number") {
|
|
615
|
+
currentTime += 16.6;
|
|
616
|
+
} else {
|
|
617
|
+
currentTime = timestamp;
|
|
618
|
+
}
|
|
619
|
+
const currentCallbacks = [...callbacks];
|
|
620
|
+
callbacks = [];
|
|
621
|
+
currentCallbacks.forEach(({ callback }) => {
|
|
622
|
+
callback(currentTime);
|
|
623
|
+
});
|
|
581
624
|
},
|
|
582
|
-
|
|
625
|
+
advance(deltaMs = 16.6) {
|
|
626
|
+
this.tick(currentTime + deltaMs);
|
|
583
627
|
},
|
|
584
|
-
|
|
628
|
+
getCallbacks() {
|
|
629
|
+
return callbacks.map((c) => c.callback);
|
|
585
630
|
},
|
|
586
|
-
|
|
631
|
+
reset() {
|
|
632
|
+
callbacks = [];
|
|
633
|
+
nextId = 1;
|
|
634
|
+
currentTime = 0;
|
|
587
635
|
},
|
|
588
|
-
|
|
636
|
+
enable() {
|
|
637
|
+
activeMockRAF = this;
|
|
638
|
+
global.requestAnimationFrame = raf;
|
|
639
|
+
global.cancelAnimationFrame = cancel;
|
|
589
640
|
},
|
|
590
|
-
|
|
591
|
-
|
|
641
|
+
disable() {
|
|
642
|
+
if (activeMockRAF === this) {
|
|
643
|
+
activeMockRAF = void 0;
|
|
644
|
+
}
|
|
645
|
+
if (originalRAF) {
|
|
646
|
+
global.requestAnimationFrame = originalRAF;
|
|
647
|
+
} else {
|
|
648
|
+
delete global.requestAnimationFrame;
|
|
649
|
+
}
|
|
650
|
+
if (originalCancelRAF) {
|
|
651
|
+
global.cancelAnimationFrame = originalCancelRAF;
|
|
652
|
+
} else {
|
|
653
|
+
delete global.cancelAnimationFrame;
|
|
654
|
+
}
|
|
655
|
+
}
|
|
656
|
+
};
|
|
657
|
+
return mock;
|
|
658
|
+
}
|
|
659
|
+
function createMockPerformance(startTime = 0) {
|
|
660
|
+
let currentTime = startTime;
|
|
661
|
+
const mockPerf = {
|
|
662
|
+
now: () => currentTime,
|
|
663
|
+
timeOrigin: startTime,
|
|
664
|
+
timing: {
|
|
665
|
+
navigationStart: startTime
|
|
592
666
|
},
|
|
593
|
-
|
|
667
|
+
clearMarks: () => {
|
|
594
668
|
},
|
|
595
|
-
|
|
669
|
+
clearMeasures: () => {
|
|
596
670
|
},
|
|
597
|
-
|
|
671
|
+
clearResourceTimings: () => {
|
|
598
672
|
},
|
|
599
|
-
|
|
673
|
+
getEntries: () => [],
|
|
674
|
+
getEntriesByName: () => [],
|
|
675
|
+
getEntriesByType: () => [],
|
|
676
|
+
mark: () => {
|
|
600
677
|
},
|
|
601
|
-
|
|
678
|
+
measure: () => {
|
|
602
679
|
},
|
|
603
|
-
|
|
680
|
+
setResourceTimingBufferSize: () => {
|
|
604
681
|
},
|
|
605
|
-
|
|
682
|
+
toJSON: () => ({}),
|
|
683
|
+
addEventListener: () => {
|
|
606
684
|
},
|
|
607
|
-
|
|
685
|
+
removeEventListener: () => {
|
|
608
686
|
},
|
|
609
|
-
|
|
687
|
+
dispatchEvent: () => true
|
|
688
|
+
};
|
|
689
|
+
mockPerf.advance = (deltaMs) => {
|
|
690
|
+
currentTime += deltaMs;
|
|
691
|
+
};
|
|
692
|
+
mockPerf.setTime = (time) => {
|
|
693
|
+
currentTime = time;
|
|
694
|
+
};
|
|
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);
|
|
610
728
|
},
|
|
611
|
-
|
|
612
|
-
|
|
613
|
-
|
|
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;
|
|
614
752
|
},
|
|
615
|
-
|
|
753
|
+
clear() {
|
|
754
|
+
timers = [];
|
|
616
755
|
},
|
|
617
|
-
|
|
618
|
-
|
|
619
|
-
|
|
620
|
-
|
|
621
|
-
|
|
622
|
-
|
|
623
|
-
|
|
624
|
-
|
|
625
|
-
|
|
626
|
-
|
|
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);
|
|
627
860
|
},
|
|
628
|
-
|
|
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
|
+
};
|
|
898
|
+
}
|
|
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
|
+
};
|
|
913
|
+
}
|
|
914
|
+
function makeNode(plane, children) {
|
|
915
|
+
return { plane, children };
|
|
916
|
+
}
|
|
917
|
+
function makeBspModel(planes, nodes, leaves, brushes, leafBrushes) {
|
|
918
|
+
return {
|
|
919
|
+
planes,
|
|
920
|
+
nodes,
|
|
921
|
+
leaves,
|
|
922
|
+
brushes,
|
|
923
|
+
leafBrushes,
|
|
924
|
+
bmodels: []
|
|
925
|
+
};
|
|
926
|
+
}
|
|
927
|
+
function makeLeaf(contents, firstLeafBrush, numLeafBrushes) {
|
|
928
|
+
return { contents, cluster: 0, area: 0, firstLeafBrush, numLeafBrushes };
|
|
929
|
+
}
|
|
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
|
+
};
|
|
940
|
+
}
|
|
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
|
+
];
|
|
950
|
+
return {
|
|
951
|
+
contents,
|
|
952
|
+
sides: planes.map((plane) => ({ plane, surfaceFlags: 0 }))
|
|
953
|
+
};
|
|
954
|
+
}
|
|
955
|
+
|
|
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));
|
|
1139
|
+
}),
|
|
1140
|
+
unregisterEntitySpawn: vi2.fn((classname) => {
|
|
1141
|
+
spawnRegistry.unregister(classname);
|
|
1142
|
+
}),
|
|
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");
|
|
1148
|
+
}),
|
|
1149
|
+
clientBegin: vi2.fn((client) => {
|
|
1150
|
+
hooks.onPlayerSpawn({});
|
|
1151
|
+
}),
|
|
1152
|
+
damage: vi2.fn((amount) => {
|
|
1153
|
+
hooks.onDamage({}, null, null, amount, 0, 0);
|
|
1154
|
+
})
|
|
1155
|
+
};
|
|
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);
|
|
1185
|
+
}
|
|
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);
|
|
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)
|
|
629
1223
|
},
|
|
630
|
-
|
|
631
|
-
|
|
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
|
|
632
1229
|
},
|
|
633
|
-
|
|
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
|
|
634
1248
|
},
|
|
635
|
-
|
|
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
|
|
1257
|
+
};
|
|
1258
|
+
return {
|
|
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()
|
|
636
1270
|
};
|
|
637
|
-
return gl;
|
|
638
1271
|
}
|
|
639
|
-
|
|
640
|
-
|
|
641
|
-
|
|
642
|
-
|
|
643
|
-
|
|
644
|
-
|
|
645
|
-
get: () => _pointerLockElement,
|
|
646
|
-
configurable: true
|
|
647
|
-
});
|
|
648
|
-
doc.exitPointerLock = () => {
|
|
649
|
-
if (_pointerLockElement) {
|
|
650
|
-
_pointerLockElement = null;
|
|
651
|
-
doc.dispatchEvent(new Event("pointerlockchange"));
|
|
652
|
-
}
|
|
653
|
-
};
|
|
654
|
-
global.HTMLElement.prototype.requestPointerLock = function() {
|
|
655
|
-
_pointerLockElement = this;
|
|
656
|
-
doc.dispatchEvent(new Event("pointerlockchange"));
|
|
657
|
-
};
|
|
658
|
-
}
|
|
659
|
-
};
|
|
660
|
-
var InputInjector = class {
|
|
661
|
-
constructor(doc, win) {
|
|
662
|
-
this.doc = doc;
|
|
663
|
-
this.win = win;
|
|
664
|
-
}
|
|
665
|
-
keyDown(key, code) {
|
|
666
|
-
const event = new this.win.KeyboardEvent("keydown", {
|
|
667
|
-
key,
|
|
668
|
-
code: code || key,
|
|
669
|
-
bubbles: true,
|
|
670
|
-
cancelable: true,
|
|
671
|
-
view: this.win
|
|
672
|
-
});
|
|
673
|
-
this.doc.dispatchEvent(event);
|
|
674
|
-
}
|
|
675
|
-
keyUp(key, code) {
|
|
676
|
-
const event = new this.win.KeyboardEvent("keyup", {
|
|
677
|
-
key,
|
|
678
|
-
code: code || key,
|
|
679
|
-
bubbles: true,
|
|
680
|
-
cancelable: true,
|
|
681
|
-
view: this.win
|
|
682
|
-
});
|
|
683
|
-
this.doc.dispatchEvent(event);
|
|
684
|
-
}
|
|
685
|
-
mouseMove(movementX, movementY, clientX = 0, clientY = 0) {
|
|
686
|
-
const event = new this.win.MouseEvent("mousemove", {
|
|
687
|
-
bubbles: true,
|
|
688
|
-
cancelable: true,
|
|
689
|
-
view: this.win,
|
|
690
|
-
clientX,
|
|
691
|
-
clientY,
|
|
692
|
-
movementX,
|
|
693
|
-
// Note: JSDOM might not support this standard property fully on event init
|
|
694
|
-
movementY
|
|
695
|
-
});
|
|
696
|
-
Object.defineProperty(event, "movementX", { value: movementX });
|
|
697
|
-
Object.defineProperty(event, "movementY", { value: movementY });
|
|
698
|
-
const target = this.doc.pointerLockElement || this.doc;
|
|
699
|
-
target.dispatchEvent(event);
|
|
700
|
-
}
|
|
701
|
-
mouseDown(button = 0) {
|
|
702
|
-
const event = new this.win.MouseEvent("mousedown", {
|
|
703
|
-
button,
|
|
704
|
-
bubbles: true,
|
|
705
|
-
cancelable: true,
|
|
706
|
-
view: this.win
|
|
707
|
-
});
|
|
708
|
-
const target = this.doc.pointerLockElement || this.doc;
|
|
709
|
-
target.dispatchEvent(event);
|
|
710
|
-
}
|
|
711
|
-
mouseUp(button = 0) {
|
|
712
|
-
const event = new this.win.MouseEvent("mouseup", {
|
|
713
|
-
button,
|
|
714
|
-
bubbles: true,
|
|
715
|
-
cancelable: true,
|
|
716
|
-
view: this.win
|
|
717
|
-
});
|
|
718
|
-
const target = this.doc.pointerLockElement || this.doc;
|
|
719
|
-
target.dispatchEvent(event);
|
|
720
|
-
}
|
|
721
|
-
wheel(deltaY) {
|
|
722
|
-
const event = new this.win.WheelEvent("wheel", {
|
|
723
|
-
deltaY,
|
|
724
|
-
bubbles: true,
|
|
725
|
-
cancelable: true,
|
|
726
|
-
view: this.win
|
|
727
|
-
});
|
|
728
|
-
const target = this.doc.pointerLockElement || this.doc;
|
|
729
|
-
target.dispatchEvent(event);
|
|
730
|
-
}
|
|
731
|
-
};
|
|
1272
|
+
function createSpawnContext() {
|
|
1273
|
+
return createTestContext();
|
|
1274
|
+
}
|
|
1275
|
+
function createEntity() {
|
|
1276
|
+
return new Entity(1);
|
|
1277
|
+
}
|
|
732
1278
|
export {
|
|
733
1279
|
InputInjector,
|
|
734
1280
|
MockPointerLock,
|
|
1281
|
+
captureAudioEvents,
|
|
1282
|
+
captureCanvasDrawCalls,
|
|
1283
|
+
captureGameState,
|
|
735
1284
|
createBinaryStreamMock,
|
|
736
1285
|
createBinaryWriterMock,
|
|
1286
|
+
createControlledTimer,
|
|
737
1287
|
createEntity,
|
|
738
1288
|
createEntityStateFactory,
|
|
739
1289
|
createGameStateSnapshotFactory,
|
|
1290
|
+
createMockAudioContext,
|
|
1291
|
+
createMockCanvas,
|
|
1292
|
+
createMockCanvasContext2D,
|
|
740
1293
|
createMockEngine,
|
|
741
1294
|
createMockGame,
|
|
1295
|
+
createMockImage,
|
|
1296
|
+
createMockImageData,
|
|
1297
|
+
createMockIndexedDB,
|
|
1298
|
+
createMockLocalStorage,
|
|
1299
|
+
createMockPerformance,
|
|
1300
|
+
createMockRAF,
|
|
1301
|
+
createMockSessionStorage,
|
|
742
1302
|
createMockWebGL2Context,
|
|
743
1303
|
createNetChanMock,
|
|
744
1304
|
createPlayerStateFactory,
|
|
1305
|
+
createPlaywrightTestClient,
|
|
745
1306
|
createSpawnContext,
|
|
1307
|
+
createStorageTestScenario,
|
|
746
1308
|
createTestContext,
|
|
747
1309
|
intersects,
|
|
748
1310
|
ladderTrace,
|
|
@@ -754,8 +1316,13 @@ export {
|
|
|
754
1316
|
makeNode,
|
|
755
1317
|
makePlane,
|
|
756
1318
|
setupBrowserEnvironment,
|
|
1319
|
+
setupMockAudioContext,
|
|
757
1320
|
setupNodeEnvironment,
|
|
1321
|
+
simulateFrames,
|
|
1322
|
+
simulateFramesWithMock,
|
|
758
1323
|
stairTrace,
|
|
759
|
-
teardownBrowserEnvironment
|
|
1324
|
+
teardownBrowserEnvironment,
|
|
1325
|
+
teardownMockAudioContext,
|
|
1326
|
+
waitForGameReady
|
|
760
1327
|
};
|
|
761
1328
|
//# sourceMappingURL=index.js.map
|