@quake2ts/test-utils 0.0.1

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
Files changed (65) hide show
  1. package/README.md +454 -0
  2. package/dist/index.cjs +5432 -0
  3. package/dist/index.cjs.map +1 -0
  4. package/dist/index.d.cts +2150 -0
  5. package/dist/index.d.ts +2150 -0
  6. package/dist/index.js +5165 -0
  7. package/dist/index.js.map +1 -0
  8. package/package.json +82 -0
  9. package/src/client/helpers/hud.ts +114 -0
  10. package/src/client/helpers/prediction.ts +136 -0
  11. package/src/client/helpers/view.ts +201 -0
  12. package/src/client/mocks/console.ts +75 -0
  13. package/src/client/mocks/download.ts +48 -0
  14. package/src/client/mocks/input.ts +246 -0
  15. package/src/client/mocks/network.ts +148 -0
  16. package/src/client/mocks/state.ts +148 -0
  17. package/src/e2e/network.ts +47 -0
  18. package/src/e2e/playwright.ts +90 -0
  19. package/src/e2e/visual.ts +172 -0
  20. package/src/engine/helpers/pipeline-test-template.ts +113 -0
  21. package/src/engine/helpers/webgpu-rendering.ts +251 -0
  22. package/src/engine/mocks/assets.ts +129 -0
  23. package/src/engine/mocks/audio.ts +152 -0
  24. package/src/engine/mocks/buffers.ts +88 -0
  25. package/src/engine/mocks/lighting.ts +64 -0
  26. package/src/engine/mocks/particles.ts +76 -0
  27. package/src/engine/mocks/renderer.ts +218 -0
  28. package/src/engine/mocks/webgl.ts +267 -0
  29. package/src/engine/mocks/webgpu.ts +262 -0
  30. package/src/engine/rendering.ts +103 -0
  31. package/src/game/factories.ts +204 -0
  32. package/src/game/helpers/physics.ts +171 -0
  33. package/src/game/helpers/save.ts +232 -0
  34. package/src/game/helpers.ts +310 -0
  35. package/src/game/mocks/ai.ts +67 -0
  36. package/src/game/mocks/combat.ts +61 -0
  37. package/src/game/mocks/items.ts +166 -0
  38. package/src/game/mocks.ts +105 -0
  39. package/src/index.ts +93 -0
  40. package/src/server/helpers/bandwidth.ts +127 -0
  41. package/src/server/helpers/multiplayer.ts +158 -0
  42. package/src/server/helpers/snapshot.ts +241 -0
  43. package/src/server/mockNetDriver.ts +106 -0
  44. package/src/server/mockTransport.ts +50 -0
  45. package/src/server/mocks/commands.ts +93 -0
  46. package/src/server/mocks/connection.ts +139 -0
  47. package/src/server/mocks/master.ts +97 -0
  48. package/src/server/mocks/physics.ts +32 -0
  49. package/src/server/mocks/state.ts +162 -0
  50. package/src/server/mocks/transport.ts +161 -0
  51. package/src/setup/audio.ts +118 -0
  52. package/src/setup/browser.ts +249 -0
  53. package/src/setup/canvas.ts +142 -0
  54. package/src/setup/node.ts +21 -0
  55. package/src/setup/storage.ts +60 -0
  56. package/src/setup/timing.ts +142 -0
  57. package/src/setup/webgl.ts +8 -0
  58. package/src/setup/webgpu.ts +113 -0
  59. package/src/shared/bsp.ts +145 -0
  60. package/src/shared/collision.ts +64 -0
  61. package/src/shared/factories.ts +88 -0
  62. package/src/shared/math.ts +65 -0
  63. package/src/shared/mocks.ts +243 -0
  64. package/src/shared/pak-loader.ts +45 -0
  65. package/src/visual/snapshots.ts +292 -0
package/src/index.ts ADDED
@@ -0,0 +1,93 @@
1
+ // Export all test utilities
2
+ export * from './shared/mocks.js';
3
+ export * from './shared/bsp.js';
4
+ export * from './shared/pak-loader.js';
5
+ export * from './shared/math.js';
6
+ export * from './shared/collision.js';
7
+ export * from './shared/factories.js';
8
+ export * from './game/factories.js';
9
+ export * from './game/helpers.js';
10
+ export * from './game/helpers/physics.js';
11
+ export * from './game/helpers/save.js';
12
+ export * from './game/mocks/ai.js';
13
+ export * from './game/mocks/combat.js';
14
+ export * from './game/mocks/items.js';
15
+ export * from './game/mocks.js';
16
+ export * from './server/mocks/transport.js';
17
+ export * from './server/mockTransport.js';
18
+ export * from './server/mockNetDriver.js';
19
+ export * from './server/mocks/state.js';
20
+ export * from './server/mocks/connection.js';
21
+ export * from './server/mocks/commands.js';
22
+ export * from './server/mocks/master.js';
23
+ export * from './server/mocks/physics.js';
24
+ export * from './server/helpers/multiplayer.js';
25
+ export * from './server/helpers/snapshot.js';
26
+ export * from './server/helpers/bandwidth.js';
27
+
28
+ // Setup
29
+ export * from './setup/browser.js';
30
+ export * from './setup/canvas.js';
31
+ export * from './setup/webgpu.js';
32
+ export * from './engine/mocks/webgpu.js';
33
+ export * from './setup/timing.js';
34
+ export * from './setup/node.js';
35
+ export * from './engine/mocks/webgl.js';
36
+ export * from './engine/mocks/audio.js';
37
+ export * from './engine/mocks/renderer.js';
38
+ export * from './engine/mocks/assets.js';
39
+ export * from './engine/mocks/buffers.js';
40
+ export * from './engine/mocks/lighting.js';
41
+ export * from './engine/mocks/particles.js';
42
+ export * from './engine/rendering.js';
43
+ export * from './setup/storage.js';
44
+ export * from './setup/audio.js';
45
+ export * from './engine/helpers/webgpu-rendering.js';
46
+ export * from './engine/helpers/pipeline-test-template.js';
47
+
48
+ // Client Mocks
49
+ export * from './client/mocks/input.js';
50
+ export * from './client/helpers/view.js';
51
+ export * from './client/helpers/hud.js';
52
+ export * from './client/mocks/network.js';
53
+ export * from './client/mocks/download.js';
54
+ export * from './client/mocks/state.js';
55
+ export * from './client/mocks/console.js';
56
+ export * from './client/helpers/prediction.js';
57
+
58
+ // Visual Testing
59
+ export * from './visual/snapshots.js';
60
+
61
+ // E2E
62
+ export * from './e2e/playwright.js';
63
+ export * from './e2e/network.js';
64
+ export * from './e2e/visual.js';
65
+
66
+ // Export types
67
+ export type { BrowserSetupOptions } from './setup/browser.js';
68
+ export type { NodeSetupOptions } from './setup/node.js';
69
+ export type { MockRAF } from './setup/timing.js';
70
+ export type { StorageScenario } from './setup/storage.js';
71
+ export type { NetworkSimulator, NetworkCondition } from './e2e/network.js';
72
+ export type { VisualScenario, VisualDiff } from './e2e/visual.js';
73
+ export type { HeadlessWebGPUSetup, WebGPUContextState } from './setup/webgpu.js';
74
+ export type { RenderTestSetup, ComputeTestSetup } from './engine/helpers/webgpu-rendering.js';
75
+ export type { GeometryBuffers } from './engine/helpers/pipeline-test-template.js';
76
+
77
+ // Shared Types
78
+ export type {
79
+ BinaryWriterMock,
80
+ BinaryStreamMock,
81
+ MessageWriterMock,
82
+ MessageReaderMock,
83
+ PacketMock
84
+ } from './shared/mocks.js';
85
+ export type { TraceMock, SurfaceMock } from './shared/collision.js';
86
+ export type { Transform } from './shared/math.js';
87
+ export type {
88
+ CaptureOptions,
89
+ ComparisonResult,
90
+ ComparisonOptions,
91
+ SnapshotTestOptions
92
+ } from './visual/snapshots.js';
93
+ export type { MockCollisionEntityIndex } from './server/mocks/physics.js';
@@ -0,0 +1,127 @@
1
+ import { Client, ClientFrame } from '@quake2ts/server';
2
+
3
+ export interface RateLimiter {
4
+ bytesPerSecond: number;
5
+ allow(bytes: number): boolean;
6
+ update(deltaSeconds: number): void;
7
+ reset(): void;
8
+ getUsage(): number;
9
+ }
10
+
11
+ export interface Message {
12
+ data: Uint8Array;
13
+ size: number;
14
+ timestamp: number;
15
+ }
16
+
17
+ export interface BandwidthScenario {
18
+ bandwidth: number;
19
+ clients: Client[];
20
+ duration: number;
21
+ totalBytesSent: number;
22
+ totalBytesReceived: number;
23
+ droppedPackets: number;
24
+ }
25
+
26
+ /**
27
+ * Creates a mock rate limiter for bandwidth testing
28
+ * @param bytesPerSecond - Maximum bytes per second allowed
29
+ */
30
+ export function createMockRateLimiter(bytesPerSecond: number): RateLimiter {
31
+ let bucket = bytesPerSecond;
32
+ const maxBucket = bytesPerSecond; // Max burst size equal to 1 second of bandwidth
33
+ let usage = 0;
34
+
35
+ return {
36
+ bytesPerSecond,
37
+ allow(bytes: number): boolean {
38
+ if (bucket >= bytes) {
39
+ bucket -= bytes;
40
+ usage += bytes;
41
+ return true;
42
+ }
43
+ return false;
44
+ },
45
+ update(deltaSeconds: number): void {
46
+ bucket += bytesPerSecond * deltaSeconds;
47
+ if (bucket > maxBucket) {
48
+ bucket = maxBucket;
49
+ }
50
+ },
51
+ reset(): void {
52
+ bucket = maxBucket;
53
+ usage = 0;
54
+ },
55
+ getUsage(): number {
56
+ return usage;
57
+ }
58
+ };
59
+ }
60
+
61
+ /**
62
+ * Simulates bandwidth limiting on a stream of messages
63
+ * @param messages - Array of messages to process
64
+ * @param bandwidth - Bandwidth limit in bytes per second
65
+ * @returns Filtered array of messages that passed the bandwidth check
66
+ */
67
+ export function simulateBandwidthLimit(messages: Message[], bandwidth: number): Message[] {
68
+ const limiter = createMockRateLimiter(bandwidth);
69
+ const result: Message[] = [];
70
+ let lastTime = messages.length > 0 ? messages[0].timestamp : 0;
71
+
72
+ for (const msg of messages) {
73
+ const delta = (msg.timestamp - lastTime) / 1000;
74
+ if (delta > 0) {
75
+ limiter.update(delta);
76
+ }
77
+ lastTime = msg.timestamp;
78
+
79
+ if (limiter.allow(msg.size)) {
80
+ result.push(msg);
81
+ }
82
+ }
83
+
84
+ return result;
85
+ }
86
+
87
+ /**
88
+ * Calculates the size of a client snapshot in bytes
89
+ * @param snapshot - The client frame snapshot
90
+ */
91
+ export function measureSnapshotSize(snapshot: ClientFrame): number {
92
+ let size = 0;
93
+
94
+ // Area bits
95
+ size += snapshot.areaBytes;
96
+
97
+ // Player state (approximate based on struct size or serialization)
98
+ // This is an estimation, as actual network serialization compresses this
99
+ size += 200; // Baseline size for PlayerState
100
+
101
+ // Entities
102
+ // Each EntityState is approx 20-30 bytes compressed
103
+ size += snapshot.entities.length * 20;
104
+
105
+ return size;
106
+ }
107
+
108
+ /**
109
+ * Creates a scenario for testing bandwidth limits with multiple clients
110
+ * @param bandwidth - Bandwidth limit per client or total
111
+ * @param numClients - Number of clients to simulate
112
+ */
113
+ export function createBandwidthTestScenario(bandwidth: number, numClients: number): BandwidthScenario {
114
+ const clients: Client[] = [];
115
+ // Populate with mock clients if needed (currently creating dummy objects to satisfy type)
116
+ // In a real scenario we might use createClient factory but that requires net driver
117
+ // For now we assume caller will populate fully or we return empty array if 0
118
+
119
+ return {
120
+ bandwidth,
121
+ clients,
122
+ duration: 0,
123
+ totalBytesSent: 0,
124
+ totalBytesReceived: 0,
125
+ droppedPackets: 0
126
+ };
127
+ }
@@ -0,0 +1,158 @@
1
+ import { Server, Client, ClientState, ServerState, ServerStatic } from '@quake2ts/server';
2
+ import { Entity } from '@quake2ts/game';
3
+ import { UserCommand } from '@quake2ts/shared';
4
+ import { createMockServerState } from '../mocks/state.js';
5
+ import { createMockServerClient } from '../mocks/state.js';
6
+ import { createMockUserInfo, serializeUserInfo, UserInfo } from '../mocks/connection.js';
7
+
8
+ // Define a type that combines Server and ServerStatic for convenience in tests
9
+ // since the real server implementation splits these but tests often need both.
10
+ export type MockServerContext = Server & {
11
+ clients: (Client | null)[];
12
+ entities?: Entity[];
13
+ };
14
+
15
+ export interface MultiplayerScenario {
16
+ server: MockServerContext;
17
+ clients: Client[];
18
+ entities: Entity[];
19
+ }
20
+
21
+ /**
22
+ * Creates a multiplayer test scenario with a mock server and a number of clients.
23
+ * @param numPlayers Number of players to simulate.
24
+ */
25
+ export function createMultiplayerTestScenario(numPlayers: number = 2): MultiplayerScenario {
26
+ const baseServer = createMockServerState();
27
+ const clients: Client[] = [];
28
+ const entities: Entity[] = [];
29
+
30
+ const server: MockServerContext = {
31
+ ...baseServer,
32
+ clients: new Array(numPlayers).fill(null),
33
+ entities: []
34
+ };
35
+
36
+ for (let i = 0; i < numPlayers; i++) {
37
+ // Create client
38
+ const client = createMockServerClient(i, {
39
+ state: ClientState.Active,
40
+ userInfo: serializeUserInfo(createMockUserInfo({ name: `Player${i}` }))
41
+ });
42
+
43
+ // Create player entity
44
+ const entity = {
45
+ classname: 'player',
46
+ s: { origin: { x: 0, y: 0, z: 0 }, number: i + 1 },
47
+ client: client
48
+ } as unknown as Entity;
49
+
50
+ client.edict = entity;
51
+ server.clients[i] = client;
52
+ clients.push(client);
53
+ entities.push(entity);
54
+ }
55
+
56
+ // Assign entities to server
57
+ server.entities = entities;
58
+
59
+ return {
60
+ server,
61
+ clients,
62
+ entities
63
+ };
64
+ }
65
+
66
+ /**
67
+ * Simulates a player joining the server.
68
+ * @param server The mock server instance.
69
+ * @param userInfo Optional user info overrides.
70
+ */
71
+ export async function simulatePlayerJoin(server: MockServerContext, userInfo?: Partial<UserInfo>): Promise<Client> {
72
+ // Find free client slot
73
+ const index = server.clients.findIndex((c) => !c || c.state === ClientState.Free);
74
+ if (index === -1) {
75
+ throw new Error('Server full');
76
+ }
77
+
78
+ const client = createMockServerClient(index, {
79
+ state: ClientState.Connected,
80
+ userInfo: serializeUserInfo(createMockUserInfo(userInfo))
81
+ });
82
+
83
+ server.clients[index] = client;
84
+
85
+ // Simulate connection process
86
+ client.state = ClientState.Active;
87
+
88
+ // Create entity
89
+ const entity = {
90
+ classname: 'player',
91
+ s: { origin: { x: 0, y: 0, z: 0 }, number: index + 1 },
92
+ client: client
93
+ } as unknown as Entity;
94
+ client.edict = entity;
95
+
96
+ // Add to entities list if possible (mock behavior)
97
+ if (server.entities && Array.isArray(server.entities)) {
98
+ // This is a simplified view, normally entities are in a fixed array
99
+ (server.entities as Entity[])[index + 1] = entity;
100
+ }
101
+
102
+ return client;
103
+ }
104
+
105
+ /**
106
+ * Simulates a player leaving the server.
107
+ * @param server The mock server instance.
108
+ * @param clientNum The client number to disconnect.
109
+ */
110
+ export function simulatePlayerLeave(server: MockServerContext, clientNum: number): void {
111
+ const client = server.clients[clientNum];
112
+ if (client) {
113
+ client.state = ClientState.Free;
114
+ client.edict = null;
115
+ }
116
+ }
117
+
118
+ /**
119
+ * Simulates a single server frame update.
120
+ * @param server The mock server instance.
121
+ * @param deltaTime Time step in seconds (default: 0.1).
122
+ */
123
+ export function simulateServerTick(server: MockServerContext, deltaTime: number = 0.1): void {
124
+ server.time += deltaTime;
125
+ server.frame++;
126
+ // In a real server, we would call RunFrame, Physics, etc.
127
+ // For mocks, we might just update client lastCmd times
128
+
129
+ server.clients.forEach((client: Client | null) => {
130
+ if (client && client.state === ClientState.Active) {
131
+ // Update client logic if needed
132
+ }
133
+ });
134
+ }
135
+
136
+ /**
137
+ * Simulates player input for a specific client.
138
+ * @param client The server client.
139
+ * @param input The input command.
140
+ */
141
+ export function simulatePlayerInput(client: Client, input: Partial<UserCommand>): void {
142
+ const cmd: UserCommand = {
143
+ msec: 100,
144
+ buttons: 0,
145
+ angles: { x: 0, y: 0, z: 0 },
146
+ forwardmove: 0,
147
+ sidemove: 0,
148
+ upmove: 0,
149
+ sequence: client.lastCmd.sequence + 1,
150
+ lightlevel: 0,
151
+ impulse: 0,
152
+ ...input
153
+ };
154
+
155
+ client.lastCmd = cmd;
156
+ client.commandQueue.push(cmd);
157
+ client.commandCount++;
158
+ }
@@ -0,0 +1,241 @@
1
+ import { Server } from '@quake2ts/server';
2
+ import { EntityState, BinaryStream, ProtocolPlayerState } from '@quake2ts/shared';
3
+
4
+ export interface Snapshot {
5
+ serverTime: number;
6
+ playerState: any; // Simplified PlayerState
7
+ entities: EntityState[];
8
+ }
9
+
10
+ export interface DeltaSnapshot {
11
+ snapshot: Snapshot;
12
+ deltaEntities: EntityState[];
13
+ removedEntities: number[];
14
+ }
15
+
16
+ export interface ConsistencyReport {
17
+ valid: boolean;
18
+ errors: string[];
19
+ }
20
+
21
+ /**
22
+ * Creates a client-specific snapshot from the server state.
23
+ * @param serverState The current server state.
24
+ * @param clientNum The client number to generate snapshot for.
25
+ */
26
+ export function createServerSnapshot(serverState: Server, clientNum: number): Snapshot {
27
+ // Collect visible entities
28
+ const visibleEntities: EntityState[] = [];
29
+
30
+ // In a real implementation, this would use PVS/PHS
31
+ // For mock, we just take all entities that have a model or are solid
32
+ if (serverState.baselines) {
33
+ serverState.baselines.forEach((ent: EntityState | null, index: number) => {
34
+ if (ent && index !== clientNum + 1) { // Skip self in packet entities usually
35
+ visibleEntities.push({ ...ent });
36
+ }
37
+ });
38
+ }
39
+
40
+ return {
41
+ serverTime: serverState.time,
42
+ playerState: {
43
+ origin: { x: 0, y: 0, z: 0 },
44
+ viewangles: { x: 0, y: 0, z: 0 },
45
+ pm_type: 0
46
+ },
47
+ entities: visibleEntities
48
+ };
49
+ }
50
+
51
+ /**
52
+ * Calculates the delta between two snapshots.
53
+ * @param oldSnapshot The baseline snapshot.
54
+ * @param newSnapshot The current snapshot.
55
+ */
56
+ export function createDeltaSnapshot(oldSnapshot: Snapshot, newSnapshot: Snapshot): DeltaSnapshot {
57
+ const deltaEntities: EntityState[] = [];
58
+ const removedEntities: number[] = [];
59
+
60
+ const oldMap = new Map(oldSnapshot.entities.map(e => [e.number, e]));
61
+ const newMap = new Map(newSnapshot.entities.map(e => [e.number, e]));
62
+
63
+ // Find changed or new entities
64
+ newSnapshot.entities.forEach(newEnt => {
65
+ const oldEnt = oldMap.get(newEnt.number);
66
+ if (!oldEnt) {
67
+ // New entity
68
+ deltaEntities.push(newEnt);
69
+ } else if (JSON.stringify(newEnt) !== JSON.stringify(oldEnt)) {
70
+ // Changed entity (simplified check)
71
+ deltaEntities.push(newEnt);
72
+ }
73
+ });
74
+
75
+ // Find removed entities
76
+ oldSnapshot.entities.forEach(oldEnt => {
77
+ if (!newMap.has(oldEnt.number)) {
78
+ removedEntities.push(oldEnt.number);
79
+ }
80
+ });
81
+
82
+ return {
83
+ snapshot: newSnapshot,
84
+ deltaEntities,
85
+ removedEntities
86
+ };
87
+ }
88
+
89
+ /**
90
+ * Verifies the consistency of a sequence of snapshots.
91
+ * @param snapshots Array of snapshots ordered by time.
92
+ */
93
+ export function verifySnapshotConsistency(snapshots: Snapshot[]): ConsistencyReport {
94
+ const report: ConsistencyReport = { valid: true, errors: [] };
95
+
96
+ if (snapshots.length < 2) return report;
97
+
98
+ for (let i = 1; i < snapshots.length; i++) {
99
+ const prev = snapshots[i-1];
100
+ const curr = snapshots[i];
101
+
102
+ if (curr.serverTime <= prev.serverTime) {
103
+ report.valid = false;
104
+ report.errors.push(`Snapshot ${i} has time ${curr.serverTime} <= prev ${prev.serverTime}`);
105
+ }
106
+ }
107
+
108
+ return report;
109
+ }
110
+
111
+ /**
112
+ * Simulates network delivery of a snapshot with potential packet loss.
113
+ * @param snapshot The snapshot to deliver.
114
+ * @param reliability Probability of successful delivery (0.0 to 1.0).
115
+ */
116
+ export async function simulateSnapshotDelivery(snapshot: Snapshot, reliability: number = 1.0): Promise<Snapshot | null> {
117
+ if (Math.random() > reliability) {
118
+ return null;
119
+ }
120
+ return snapshot;
121
+ }
122
+
123
+ /**
124
+ * Parses a ProtocolPlayerState from a binary buffer.
125
+ * Useful for testing player state serialization.
126
+ * logic adapted from packages/engine/src/demo/parser.ts
127
+ * @param data The binary data to parse.
128
+ */
129
+ export function parseProtocolPlayerState(data: Uint8Array): ProtocolPlayerState {
130
+ const stream = new BinaryStream(data.buffer as ArrayBuffer);
131
+ const ps: ProtocolPlayerState = {
132
+ pm_type: 0,
133
+ origin: { x: 0, y: 0, z: 0 },
134
+ velocity: { x: 0, y: 0, z: 0 },
135
+ pm_time: 0,
136
+ pm_flags: 0,
137
+ gravity: 0,
138
+ delta_angles: { x: 0, y: 0, z: 0 },
139
+ viewoffset: { x: 0, y: 0, z: 0 },
140
+ viewangles: { x: 0, y: 0, z: 0 },
141
+ kick_angles: { x: 0, y: 0, z: 0 },
142
+ gun_index: 0,
143
+ gun_frame: 0,
144
+ gun_offset: { x: 0, y: 0, z: 0 },
145
+ gun_angles: { x: 0, y: 0, z: 0 },
146
+ blend: [0, 0, 0, 0],
147
+ fov: 0,
148
+ rdflags: 0,
149
+ stats: new Array(32).fill(0),
150
+ watertype: 0
151
+ };
152
+
153
+ const flags = stream.readShort();
154
+
155
+ if (flags & 1) ps.pm_type = stream.readByte();
156
+
157
+ if (flags & 2) {
158
+ const x = stream.readShort() * 0.125;
159
+ const y = stream.readShort() * 0.125;
160
+ const z = stream.readShort() * 0.125;
161
+ ps.origin = { x, y, z };
162
+ }
163
+
164
+ if (flags & 4) {
165
+ const x = stream.readShort() * 0.125;
166
+ const y = stream.readShort() * 0.125;
167
+ const z = stream.readShort() * 0.125;
168
+ ps.velocity = { x, y, z };
169
+ }
170
+
171
+ if (flags & 8) ps.pm_time = stream.readByte();
172
+ if (flags & 16) ps.pm_flags = stream.readByte();
173
+ if (flags & 32) ps.gravity = stream.readShort();
174
+
175
+ if (flags & 64) {
176
+ const x = stream.readShort() * (180 / 32768);
177
+ const y = stream.readShort() * (180 / 32768);
178
+ const z = stream.readShort() * (180 / 32768);
179
+ ps.delta_angles = { x, y, z };
180
+ }
181
+
182
+ if (flags & 128) {
183
+ const x = stream.readChar() * 0.25;
184
+ const y = stream.readChar() * 0.25;
185
+ const z = stream.readChar() * 0.25;
186
+ ps.viewoffset = { x, y, z };
187
+ }
188
+
189
+ if (flags & 256) {
190
+ const x = stream.readAngle16();
191
+ const y = stream.readAngle16();
192
+ const z = stream.readAngle16();
193
+ ps.viewangles = { x, y, z };
194
+ }
195
+
196
+ if (flags & 512) {
197
+ const x = stream.readChar() * 0.25;
198
+ const y = stream.readChar() * 0.25;
199
+ const z = stream.readChar() * 0.25;
200
+ ps.kick_angles = { x, y, z };
201
+ }
202
+
203
+ if (flags & 4096) ps.gun_index = stream.readByte();
204
+
205
+ if (flags & 8192) {
206
+ ps.gun_frame = stream.readByte();
207
+ const ox = stream.readChar() * 0.25;
208
+ const oy = stream.readChar() * 0.25;
209
+ const oz = stream.readChar() * 0.25;
210
+ ps.gun_offset = { x: ox, y: oy, z: oz };
211
+
212
+ const ax = stream.readChar() * 0.25;
213
+ const ay = stream.readChar() * 0.25;
214
+ const az = stream.readChar() * 0.25;
215
+ ps.gun_angles = { x: ax, y: ay, z: az };
216
+ }
217
+
218
+ if (flags & 1024) {
219
+ ps.blend = [
220
+ stream.readByte(),
221
+ stream.readByte(),
222
+ stream.readByte(),
223
+ stream.readByte()
224
+ ];
225
+ }
226
+
227
+ if (flags & 2048) ps.fov = stream.readByte();
228
+ if (flags & 16384) ps.rdflags = stream.readByte();
229
+
230
+ // New: watertype (bit 15)
231
+ if (flags & 32768) ps.watertype = stream.readByte();
232
+
233
+ const statbits = stream.readLong();
234
+ for (let i = 0; i < 32; i++) {
235
+ if (statbits & (1 << i)) {
236
+ ps.stats[i] = stream.readShort();
237
+ }
238
+ }
239
+
240
+ return ps;
241
+ }