quake2ts 0.0.402 → 0.0.405

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 (30) hide show
  1. package/package.json +1 -1
  2. package/packages/client/dist/browser/index.global.js +16 -16
  3. package/packages/client/dist/browser/index.global.js.map +1 -1
  4. package/packages/client/dist/cjs/index.cjs +383 -11
  5. package/packages/client/dist/cjs/index.cjs.map +1 -1
  6. package/packages/client/dist/esm/index.js +382 -11
  7. package/packages/client/dist/esm/index.js.map +1 -1
  8. package/packages/client/dist/tsconfig.tsbuildinfo +1 -1
  9. package/packages/client/dist/types/demo/camera.d.ts +16 -0
  10. package/packages/client/dist/types/demo/camera.d.ts.map +1 -0
  11. package/packages/client/dist/types/index.d.ts +7 -1
  12. package/packages/client/dist/types/index.d.ts.map +1 -1
  13. package/packages/engine/dist/browser/index.global.js +16 -16
  14. package/packages/engine/dist/browser/index.global.js.map +1 -1
  15. package/packages/engine/dist/cjs/index.cjs +274 -2
  16. package/packages/engine/dist/cjs/index.cjs.map +1 -1
  17. package/packages/engine/dist/esm/index.js +274 -2
  18. package/packages/engine/dist/esm/index.js.map +1 -1
  19. package/packages/engine/dist/tsconfig.tsbuildinfo +1 -1
  20. package/packages/engine/dist/types/commands.d.ts +3 -0
  21. package/packages/engine/dist/types/commands.d.ts.map +1 -1
  22. package/packages/engine/dist/types/cvars.d.ts +11 -0
  23. package/packages/engine/dist/types/cvars.d.ts.map +1 -1
  24. package/packages/engine/dist/types/demo/analysis.d.ts +33 -0
  25. package/packages/engine/dist/types/demo/analysis.d.ts.map +1 -1
  26. package/packages/engine/dist/types/demo/analyzer.d.ts +28 -0
  27. package/packages/engine/dist/types/demo/analyzer.d.ts.map +1 -0
  28. package/packages/engine/dist/types/demo/playback.d.ts +16 -1
  29. package/packages/engine/dist/types/demo/playback.d.ts.map +1 -1
  30. package/packages/game/dist/tsconfig.tsbuildinfo +1 -1
@@ -23,6 +23,7 @@ __export(index_exports, {
23
23
  ClientConfigStrings: () => ClientConfigStrings,
24
24
  ClientMode: () => ClientMode,
25
25
  ClientPrediction: () => import_cgame5.ClientPrediction,
26
+ DemoCameraMode: () => DemoCameraMode,
26
27
  GameSession: () => GameSession,
27
28
  InputAction: () => InputAction,
28
29
  InputBindings: () => InputBindings,
@@ -1855,6 +1856,9 @@ var CommandRegistry = class {
1855
1856
  this.commands.set(name, command);
1856
1857
  return command;
1857
1858
  }
1859
+ registerCommand(name, callback) {
1860
+ this.register(name, callback);
1861
+ }
1858
1862
  get(name) {
1859
1863
  return this.commands.get(name);
1860
1864
  }
@@ -1870,10 +1874,33 @@ var CommandRegistry = class {
1870
1874
  command.execute(args);
1871
1875
  return true;
1872
1876
  }
1877
+ this.onConsoleOutput?.(`Unknown command "${name}"`);
1873
1878
  return false;
1874
1879
  }
1880
+ executeCommand(cmd) {
1881
+ this.execute(cmd);
1882
+ }
1875
1883
  tokenize(text) {
1876
- return text.trim().split(/\s+/);
1884
+ const args = [];
1885
+ let currentArg = "";
1886
+ let inQuote = false;
1887
+ for (let i = 0; i < text.length; i++) {
1888
+ const char = text[i];
1889
+ if (char === '"') {
1890
+ inQuote = !inQuote;
1891
+ } else if (char === " " && !inQuote) {
1892
+ if (currentArg.length > 0) {
1893
+ args.push(currentArg);
1894
+ currentArg = "";
1895
+ }
1896
+ } else {
1897
+ currentArg += char;
1898
+ }
1899
+ }
1900
+ if (currentArg.length > 0) {
1901
+ args.push(currentArg);
1902
+ }
1903
+ return args;
1877
1904
  }
1878
1905
  list() {
1879
1906
  return [...this.commands.values()].sort((a, b) => a.name.localeCompare(b.name));
@@ -2958,13 +2985,21 @@ var CvarRegistry = class {
2958
2985
  if (existing) {
2959
2986
  return existing;
2960
2987
  }
2961
- const cvar = new Cvar(def);
2988
+ const originalOnChange = def.onChange;
2989
+ const wrappedOnChange = (cvar2, prev) => {
2990
+ originalOnChange?.(cvar2, prev);
2991
+ this.onCvarChange?.(cvar2.name, cvar2.string);
2992
+ };
2993
+ const cvar = new Cvar({ ...def, onChange: wrappedOnChange });
2962
2994
  this.cvars.set(def.name, cvar);
2963
2995
  return cvar;
2964
2996
  }
2965
2997
  get(name) {
2966
2998
  return this.cvars.get(name);
2967
2999
  }
3000
+ getCvar(name) {
3001
+ return this.get(name);
3002
+ }
2968
3003
  setValue(name, value) {
2969
3004
  const cvar = this.get(name);
2970
3005
  if (!cvar) {
@@ -2973,6 +3008,9 @@ var CvarRegistry = class {
2973
3008
  cvar.set(value);
2974
3009
  return cvar;
2975
3010
  }
3011
+ setCvar(name, value) {
3012
+ this.setValue(name, value);
3013
+ }
2976
3014
  resetAll() {
2977
3015
  for (const cvar of this.cvars.values()) {
2978
3016
  cvar.reset();
@@ -2988,6 +3026,15 @@ var CvarRegistry = class {
2988
3026
  list() {
2989
3027
  return [...this.cvars.values()].sort((a, b) => a.name.localeCompare(b.name));
2990
3028
  }
3029
+ listCvars() {
3030
+ return this.list().map((cvar) => ({
3031
+ name: cvar.name,
3032
+ value: cvar.string,
3033
+ defaultValue: cvar.defaultValue,
3034
+ flags: cvar.flags,
3035
+ description: cvar.description
3036
+ }));
3037
+ }
2991
3038
  };
2992
3039
  var EngineHost = class {
2993
3040
  constructor(game, client, options = {}) {
@@ -8796,6 +8843,162 @@ var NetworkMessageParser = class _NetworkMessageParser {
8796
8843
  }
8797
8844
  }
8798
8845
  };
8846
+ var DemoAnalyzer = class {
8847
+ constructor(buffer) {
8848
+ this.events = [];
8849
+ this.summary = {
8850
+ totalKills: 0,
8851
+ totalDeaths: 0,
8852
+ damageDealt: 0,
8853
+ damageReceived: 0,
8854
+ weaponUsage: /* @__PURE__ */ new Map()
8855
+ };
8856
+ this.header = null;
8857
+ this.configStrings = /* @__PURE__ */ new Map();
8858
+ this.serverInfo = {};
8859
+ this.statistics = null;
8860
+ this.playerStats = /* @__PURE__ */ new Map();
8861
+ this.weaponStats = /* @__PURE__ */ new Map();
8862
+ this.buffer = buffer;
8863
+ }
8864
+ analyze() {
8865
+ const reader = new DemoReader(this.buffer);
8866
+ let currentFrameIndex = -1;
8867
+ let currentTime = 0;
8868
+ let frameDuration = 0.1;
8869
+ let protocolVersion = 0;
8870
+ const handler = {
8871
+ onServerData: (protocol, serverCount, attractLoop, gameDir, playerNum, levelName, tickRate, demoType) => {
8872
+ protocolVersion = protocol;
8873
+ this.header = {
8874
+ protocolVersion: protocol,
8875
+ gameDir,
8876
+ levelName,
8877
+ playerNum,
8878
+ serverCount,
8879
+ spawnCount: serverCount,
8880
+ // Mapping generic arg
8881
+ tickRate,
8882
+ demoType
8883
+ };
8884
+ if (tickRate && tickRate > 0) {
8885
+ frameDuration = 1 / tickRate;
8886
+ }
8887
+ },
8888
+ onConfigString: (index, str3) => {
8889
+ this.configStrings.set(index, str3);
8890
+ if (index === 0) {
8891
+ this.parseServerInfo(str3);
8892
+ }
8893
+ },
8894
+ onSpawnBaseline: (entity) => {
8895
+ },
8896
+ onFrame: (frame) => {
8897
+ },
8898
+ onPrint: (level, msg) => {
8899
+ if (msg.includes("died") || msg.includes("killed")) {
8900
+ this.summary.totalDeaths++;
8901
+ this.recordEvent({
8902
+ type: 4,
8903
+ frame: currentFrameIndex,
8904
+ time: currentTime,
8905
+ description: msg.trim()
8906
+ });
8907
+ }
8908
+ },
8909
+ onCenterPrint: () => {
8910
+ },
8911
+ onStuffText: () => {
8912
+ },
8913
+ onSound: () => {
8914
+ },
8915
+ onTempEntity: () => {
8916
+ },
8917
+ onLayout: () => {
8918
+ },
8919
+ onInventory: () => {
8920
+ },
8921
+ onMuzzleFlash: (ent, weapon) => {
8922
+ this.handleWeaponFire(ent, weapon, currentFrameIndex, currentTime);
8923
+ },
8924
+ onMuzzleFlash2: (ent, weapon) => {
8925
+ this.handleWeaponFire(ent, weapon, currentFrameIndex, currentTime);
8926
+ },
8927
+ onMuzzleFlash3: (ent, weapon) => {
8928
+ this.handleWeaponFire(ent, weapon, currentFrameIndex, currentTime);
8929
+ },
8930
+ onDisconnect: () => {
8931
+ },
8932
+ onReconnect: () => {
8933
+ },
8934
+ onDownload: () => {
8935
+ },
8936
+ // Rerelease specific
8937
+ onDamage: (indicators) => {
8938
+ for (const ind of indicators) {
8939
+ this.recordEvent({
8940
+ type: 2,
8941
+ frame: currentFrameIndex,
8942
+ time: currentTime,
8943
+ value: ind.damage,
8944
+ position: ind.dir,
8945
+ description: `Took ${ind.damage} damage`
8946
+ });
8947
+ this.summary.damageReceived += ind.damage;
8948
+ }
8949
+ }
8950
+ };
8951
+ while (reader.hasMore()) {
8952
+ const block = reader.readNextBlock();
8953
+ if (!block) break;
8954
+ currentFrameIndex++;
8955
+ currentTime = currentFrameIndex * frameDuration;
8956
+ const parser = new NetworkMessageParser(block.data, handler);
8957
+ parser.setProtocolVersion(protocolVersion);
8958
+ parser.parseMessage();
8959
+ protocolVersion = parser.getProtocolVersion();
8960
+ }
8961
+ this.statistics = {
8962
+ duration: currentTime,
8963
+ frameCount: currentFrameIndex + 1,
8964
+ averageFps: (currentFrameIndex + 1) / (currentTime || 1),
8965
+ mapName: this.header?.levelName || "unknown",
8966
+ playerCount: 1
8967
+ // Default to 1 for SP/client demo
8968
+ };
8969
+ return {
8970
+ events: this.events,
8971
+ summary: this.summary,
8972
+ header: this.header,
8973
+ configStrings: this.configStrings,
8974
+ serverInfo: this.serverInfo,
8975
+ statistics: this.statistics
8976
+ };
8977
+ }
8978
+ handleWeaponFire(ent, weapon, frame, time) {
8979
+ this.recordEvent({
8980
+ type: 0,
8981
+ frame,
8982
+ time,
8983
+ entityId: ent,
8984
+ value: weapon,
8985
+ description: `Weapon ${weapon} fired by ${ent}`
8986
+ });
8987
+ const count = this.summary.weaponUsage.get(weapon) || 0;
8988
+ this.summary.weaponUsage.set(weapon, count + 1);
8989
+ }
8990
+ recordEvent(event) {
8991
+ this.events.push(event);
8992
+ }
8993
+ parseServerInfo(str3) {
8994
+ const parts = str3.split("\\");
8995
+ for (let i = 1; i < parts.length; i += 2) {
8996
+ if (i + 1 < parts.length) {
8997
+ this.serverInfo[parts[i]] = parts[i + 1];
8998
+ }
8999
+ }
9000
+ }
9001
+ };
8799
9002
  var PlaybackState = /* @__PURE__ */ ((PlaybackState22) => {
8800
9003
  PlaybackState22[PlaybackState22["Stopped"] = 0] = "Stopped";
8801
9004
  PlaybackState22[PlaybackState22["Playing"] = 1] = "Playing";
@@ -8806,6 +9009,7 @@ var PlaybackState = /* @__PURE__ */ ((PlaybackState22) => {
8806
9009
  var DemoPlaybackController = class {
8807
9010
  constructor() {
8808
9011
  this.reader = null;
9012
+ this.buffer = null;
8809
9013
  this.state = 0;
8810
9014
  this.playbackSpeed = 1;
8811
9015
  this.currentProtocolVersion = 0;
@@ -8815,6 +9019,12 @@ var DemoPlaybackController = class {
8815
9019
  this.frameDuration = 100;
8816
9020
  this.snapshotInterval = 100;
8817
9021
  this.snapshots = /* @__PURE__ */ new Map();
9022
+ this.cachedEvents = null;
9023
+ this.cachedSummary = null;
9024
+ this.cachedHeader = null;
9025
+ this.cachedConfigStrings = null;
9026
+ this.cachedServerInfo = null;
9027
+ this.cachedStatistics = null;
8818
9028
  }
8819
9029
  setHandler(handler) {
8820
9030
  this.handler = handler;
@@ -8823,6 +9033,7 @@ var DemoPlaybackController = class {
8823
9033
  this.callbacks = callbacks;
8824
9034
  }
8825
9035
  loadDemo(buffer) {
9036
+ this.buffer = buffer;
8826
9037
  this.reader = new DemoReader(buffer);
8827
9038
  this.transitionState(
8828
9039
  0
@@ -8833,6 +9044,12 @@ var DemoPlaybackController = class {
8833
9044
  this.currentFrameIndex = -1;
8834
9045
  this.snapshots.clear();
8835
9046
  this.lastFrameData = null;
9047
+ this.cachedEvents = null;
9048
+ this.cachedSummary = null;
9049
+ this.cachedHeader = null;
9050
+ this.cachedConfigStrings = null;
9051
+ this.cachedServerInfo = null;
9052
+ this.cachedStatistics = null;
8836
9053
  }
8837
9054
  play() {
8838
9055
  if (this.reader && this.state !== 1) {
@@ -9160,6 +9377,57 @@ var DemoPlaybackController = class {
9160
9377
  this.seek(originalFrame);
9161
9378
  return trajectory;
9162
9379
  }
9380
+ // 3.2.3 Event Log Extraction & 3.3 Metadata
9381
+ getDemoEvents() {
9382
+ this.ensureAnalysis();
9383
+ return this.cachedEvents || [];
9384
+ }
9385
+ filterEvents(type, entityId) {
9386
+ const events = this.getDemoEvents();
9387
+ return events.filter((e) => {
9388
+ if (e.type !== type) return false;
9389
+ if (entityId !== void 0 && e.entityId !== entityId) return false;
9390
+ return true;
9391
+ });
9392
+ }
9393
+ getEventSummary() {
9394
+ this.ensureAnalysis();
9395
+ return this.cachedSummary || {
9396
+ totalKills: 0,
9397
+ totalDeaths: 0,
9398
+ damageDealt: 0,
9399
+ damageReceived: 0,
9400
+ weaponUsage: /* @__PURE__ */ new Map()
9401
+ };
9402
+ }
9403
+ getDemoHeader() {
9404
+ this.ensureAnalysis();
9405
+ return this.cachedHeader;
9406
+ }
9407
+ getDemoConfigStrings() {
9408
+ this.ensureAnalysis();
9409
+ return this.cachedConfigStrings || /* @__PURE__ */ new Map();
9410
+ }
9411
+ getDemoServerInfo() {
9412
+ this.ensureAnalysis();
9413
+ return this.cachedServerInfo || {};
9414
+ }
9415
+ getDemoStatistics() {
9416
+ this.ensureAnalysis();
9417
+ return this.cachedStatistics;
9418
+ }
9419
+ ensureAnalysis() {
9420
+ if (!this.cachedEvents && this.buffer) {
9421
+ const analyzer = new DemoAnalyzer(this.buffer);
9422
+ const result = analyzer.analyze();
9423
+ this.cachedEvents = result.events;
9424
+ this.cachedSummary = result.summary;
9425
+ this.cachedHeader = result.header;
9426
+ this.cachedConfigStrings = result.configStrings;
9427
+ this.cachedServerInfo = result.serverInfo;
9428
+ this.cachedStatistics = result.statistics;
9429
+ }
9430
+ }
9163
9431
  };
9164
9432
  var DemoRecorder = class {
9165
9433
  // -1 means start of demo
@@ -9239,12 +9507,56 @@ var __export3 = (target, all) => {
9239
9507
  };
9240
9508
  var ZERO_VEC3 = { x: 0, y: 0, z: 0 };
9241
9509
  var DEG_TO_RAD2 = Math.PI / 180;
9510
+ var PITCH = 0;
9511
+ var YAW = 1;
9512
+ var ROLL = 2;
9242
9513
  var DEG2RAD_FACTOR2 = Math.PI / 180;
9243
9514
  var RAD2DEG_FACTOR2 = 180 / Math.PI;
9515
+ function axisComponent(vec, axis) {
9516
+ switch (axis) {
9517
+ case PITCH:
9518
+ return vec.x;
9519
+ case YAW:
9520
+ return vec.y;
9521
+ case ROLL:
9522
+ default:
9523
+ return vec.z;
9524
+ }
9525
+ }
9526
+ function degToRad(degrees) {
9527
+ return degrees * DEG2RAD_FACTOR2;
9528
+ }
9244
9529
  function angleMod(angle2) {
9245
9530
  const value = angle2 % 360;
9246
9531
  return value < 0 ? 360 + value : value;
9247
9532
  }
9533
+ function angleVectors(angles) {
9534
+ const yaw = degToRad(axisComponent(angles, YAW));
9535
+ const pitch = degToRad(axisComponent(angles, PITCH));
9536
+ const roll = degToRad(axisComponent(angles, ROLL));
9537
+ const sy = Math.sin(yaw);
9538
+ const cy = Math.cos(yaw);
9539
+ const sp = Math.sin(pitch);
9540
+ const cp = Math.cos(pitch);
9541
+ const sr = Math.sin(roll);
9542
+ const cr = Math.cos(roll);
9543
+ const forward = {
9544
+ x: cp * cy,
9545
+ y: cp * sy,
9546
+ z: -sp
9547
+ };
9548
+ const right = {
9549
+ x: -sr * sp * cy - cr * -sy,
9550
+ y: -sr * sp * sy - cr * cy,
9551
+ z: -sr * cp
9552
+ };
9553
+ const up = {
9554
+ x: cr * sp * cy - sr * -sy,
9555
+ y: cr * sp * sy - sr * cy,
9556
+ z: cr * cp
9557
+ };
9558
+ return { forward, right, up };
9559
+ }
9248
9560
  var ANORMS2 = [
9249
9561
  [-0.525731, 0, 0.850651],
9250
9562
  [-0.442863, 0.238856, 0.864188],
@@ -13230,6 +13542,15 @@ var DemoControls = class {
13230
13542
  }
13231
13543
  };
13232
13544
 
13545
+ // src/demo/camera.ts
13546
+ var DemoCameraMode = /* @__PURE__ */ ((DemoCameraMode2) => {
13547
+ DemoCameraMode2[DemoCameraMode2["FirstPerson"] = 0] = "FirstPerson";
13548
+ DemoCameraMode2[DemoCameraMode2["ThirdPerson"] = 1] = "ThirdPerson";
13549
+ DemoCameraMode2[DemoCameraMode2["Free"] = 2] = "Free";
13550
+ DemoCameraMode2[DemoCameraMode2["Follow"] = 3] = "Follow";
13551
+ return DemoCameraMode2;
13552
+ })(DemoCameraMode || {});
13553
+
13233
13554
  // src/effects.ts
13234
13555
  function addDLight(dlights, origin, color, intensity, minLight = 0, die = 0) {
13235
13556
  dlights.push({
@@ -13924,6 +14245,14 @@ function createClient(imports) {
13924
14245
  let isDemoPlaying = false;
13925
14246
  let currentDemoName = null;
13926
14247
  let clientMode = 0 /* Normal */;
14248
+ const demoCameraState = {
14249
+ mode: 0 /* FirstPerson */,
14250
+ thirdPersonDistance: 80,
14251
+ thirdPersonOffset: { x: 0, y: 0, z: 0 },
14252
+ freeCameraOrigin: { x: 0, y: 0, z: 0 },
14253
+ freeCameraAngles: { x: 0, y: 0, z: 0 },
14254
+ followEntityId: -1
14255
+ };
13927
14256
  const menuSystem = new MenuSystem();
13928
14257
  const loadingScreen = new LoadingScreen();
13929
14258
  const errorDialog = new ErrorDialog();
@@ -14138,6 +14467,8 @@ function createClient(imports) {
14138
14467
  },
14139
14468
  handleInput(key, down) {
14140
14469
  if (isDemoPlaying) {
14470
+ if (demoCameraState.mode === 2 /* Free */) {
14471
+ }
14141
14472
  if (demoControls.handleInput(key, down)) {
14142
14473
  return true;
14143
14474
  }
@@ -14223,18 +14554,42 @@ function createClient(imports) {
14223
14554
  lastRenderTime = now;
14224
14555
  demoPlayback.update(dt);
14225
14556
  lastRendered = demoHandler.getPredictionState(demoPlayback.getCurrentTime());
14226
- const frameDuration = 100;
14227
14557
  renderEntities = demoHandler.getRenderableEntities(1, configStrings);
14228
14558
  if (demoHandler.latestFrame && demoHandler.latestFrame.packetEntities) {
14229
14559
  currentPacketEntities = demoHandler.latestFrame.packetEntities.entities;
14230
14560
  }
14231
14561
  if (lastRendered) {
14232
14562
  const demoCamera = demoHandler.getDemoCamera(1);
14233
- if (demoCamera) {
14234
- lastRendered.origin = demoCamera.origin;
14235
- lastRendered.viewAngles = demoCamera.angles;
14236
- if (demoCamera.fov) {
14237
- lastRendered.fov = demoCamera.fov;
14563
+ if (demoCameraState.mode === 0 /* FirstPerson */) {
14564
+ if (demoCamera) {
14565
+ lastRendered.origin = demoCamera.origin;
14566
+ lastRendered.viewAngles = demoCamera.angles;
14567
+ if (demoCamera.fov) {
14568
+ lastRendered.fov = demoCamera.fov;
14569
+ }
14570
+ }
14571
+ } else if (demoCameraState.mode === 1 /* ThirdPerson */) {
14572
+ if (demoCamera) {
14573
+ const vectors = angleVectors(demoCamera.angles);
14574
+ const forward = vectors.forward;
14575
+ const dist2 = demoCameraState.thirdPersonDistance;
14576
+ const camOrigin = { ...demoCamera.origin };
14577
+ camOrigin.x -= forward.x * dist2;
14578
+ camOrigin.y -= forward.y * dist2;
14579
+ camOrigin.z -= forward.z * dist2;
14580
+ lastRendered.origin = camOrigin;
14581
+ lastRendered.viewAngles = demoCamera.angles;
14582
+ }
14583
+ } else if (demoCameraState.mode === 2 /* Free */) {
14584
+ lastRendered.origin = demoCameraState.freeCameraOrigin;
14585
+ lastRendered.viewAngles = demoCameraState.freeCameraAngles;
14586
+ } else if (demoCameraState.mode === 3 /* Follow */) {
14587
+ if (demoCameraState.followEntityId !== -1) {
14588
+ const ent = renderEntities.find((e) => e.id === demoCameraState.followEntityId);
14589
+ if (ent) {
14590
+ const mat = ent.transform;
14591
+ lastRendered.origin = { x: mat[12], y: mat[13], z: mat[14] };
14592
+ }
14238
14593
  }
14239
14594
  }
14240
14595
  }
@@ -14274,8 +14629,10 @@ function createClient(imports) {
14274
14629
  const { origin, viewAngles } = lastRendered;
14275
14630
  camera = new Camera();
14276
14631
  camera.position = vec3_exports.fromValues(origin.x, origin.y, origin.z);
14277
- const viewOffset = lastView?.offset ?? { x: 0, y: 0, z: 0 };
14278
- vec3_exports.add(camera.position, camera.position, [viewOffset.x, viewOffset.y, viewOffset.z]);
14632
+ if (!isDemoPlaying || demoCameraState.mode === 0 /* FirstPerson */) {
14633
+ const viewOffset = lastView?.offset ?? { x: 0, y: 0, z: 0 };
14634
+ vec3_exports.add(camera.position, camera.position, [viewOffset.x, viewOffset.y, viewOffset.z]);
14635
+ }
14279
14636
  const effectAngles = lastView?.angles ?? { x: 0, y: 0, z: 0 };
14280
14637
  camera.angles = vec3_exports.fromValues(viewAngles.x + effectAngles.x, viewAngles.y + effectAngles.y, viewAngles.z + effectAngles.z);
14281
14638
  if (isDemoPlaying && lastRendered.fov) {
@@ -14508,7 +14865,21 @@ function createClient(imports) {
14508
14865
  },
14509
14866
  demoHandler,
14510
14867
  multiplayer,
14511
- configStrings
14868
+ configStrings,
14869
+ // Demo Camera API
14870
+ setDemoCameraMode(mode) {
14871
+ demoCameraState.mode = mode;
14872
+ },
14873
+ setDemoThirdPersonDistance(dist2) {
14874
+ demoCameraState.thirdPersonDistance = dist2;
14875
+ },
14876
+ setDemoThirdPersonOffset(offset) {
14877
+ demoCameraState.thirdPersonOffset = offset;
14878
+ },
14879
+ setDemoFreeCamera(origin, angles) {
14880
+ demoCameraState.freeCameraOrigin = origin;
14881
+ demoCameraState.freeCameraAngles = angles;
14882
+ }
14512
14883
  };
14513
14884
  return clientExports;
14514
14885
  }
@@ -14517,6 +14888,7 @@ function createClient(imports) {
14517
14888
  ClientConfigStrings,
14518
14889
  ClientMode,
14519
14890
  ClientPrediction,
14891
+ DemoCameraMode,
14520
14892
  GameSession,
14521
14893
  InputAction,
14522
14894
  InputBindings,