quake2ts 0.0.403 → 0.0.407

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 (42) 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 +405 -13
  5. package/packages/client/dist/cjs/index.cjs.map +1 -1
  6. package/packages/client/dist/esm/index.js +404 -13
  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/client/dist/types/ui/demo-controls.d.ts +2 -1
  14. package/packages/client/dist/types/ui/demo-controls.d.ts.map +1 -1
  15. package/packages/engine/dist/browser/index.global.js +16 -16
  16. package/packages/engine/dist/browser/index.global.js.map +1 -1
  17. package/packages/engine/dist/cjs/index.cjs +362 -4
  18. package/packages/engine/dist/cjs/index.cjs.map +1 -1
  19. package/packages/engine/dist/esm/index.js +361 -4
  20. package/packages/engine/dist/esm/index.js.map +1 -1
  21. package/packages/engine/dist/tsconfig.tsbuildinfo +1 -1
  22. package/packages/engine/dist/types/assets/mapStatistics.d.ts +20 -0
  23. package/packages/engine/dist/types/assets/mapStatistics.d.ts.map +1 -0
  24. package/packages/engine/dist/types/audio/api.d.ts +1 -0
  25. package/packages/engine/dist/types/audio/api.d.ts.map +1 -1
  26. package/packages/engine/dist/types/audio/context.d.ts +1 -0
  27. package/packages/engine/dist/types/audio/context.d.ts.map +1 -1
  28. package/packages/engine/dist/types/audio/system.d.ts +3 -0
  29. package/packages/engine/dist/types/audio/system.d.ts.map +1 -1
  30. package/packages/engine/dist/types/commands.d.ts +3 -0
  31. package/packages/engine/dist/types/commands.d.ts.map +1 -1
  32. package/packages/engine/dist/types/cvars.d.ts +11 -0
  33. package/packages/engine/dist/types/cvars.d.ts.map +1 -1
  34. package/packages/engine/dist/types/demo/analysis.d.ts +33 -0
  35. package/packages/engine/dist/types/demo/analysis.d.ts.map +1 -1
  36. package/packages/engine/dist/types/demo/analyzer.d.ts +28 -0
  37. package/packages/engine/dist/types/demo/analyzer.d.ts.map +1 -0
  38. package/packages/engine/dist/types/demo/playback.d.ts +16 -1
  39. package/packages/engine/dist/types/demo/playback.d.ts.map +1 -1
  40. package/packages/engine/dist/types/index.d.ts +1 -0
  41. package/packages/engine/dist/types/index.d.ts.map +1 -1
  42. package/packages/game/dist/tsconfig.tsbuildinfo +1 -1
@@ -1821,6 +1821,9 @@ var CommandRegistry = class {
1821
1821
  this.commands.set(name, command);
1822
1822
  return command;
1823
1823
  }
1824
+ registerCommand(name, callback) {
1825
+ this.register(name, callback);
1826
+ }
1824
1827
  get(name) {
1825
1828
  return this.commands.get(name);
1826
1829
  }
@@ -1836,10 +1839,33 @@ var CommandRegistry = class {
1836
1839
  command.execute(args);
1837
1840
  return true;
1838
1841
  }
1842
+ this.onConsoleOutput?.(`Unknown command "${name}"`);
1839
1843
  return false;
1840
1844
  }
1845
+ executeCommand(cmd) {
1846
+ this.execute(cmd);
1847
+ }
1841
1848
  tokenize(text) {
1842
- return text.trim().split(/\s+/);
1849
+ const args = [];
1850
+ let currentArg = "";
1851
+ let inQuote = false;
1852
+ for (let i = 0; i < text.length; i++) {
1853
+ const char = text[i];
1854
+ if (char === '"') {
1855
+ inQuote = !inQuote;
1856
+ } else if (char === " " && !inQuote) {
1857
+ if (currentArg.length > 0) {
1858
+ args.push(currentArg);
1859
+ currentArg = "";
1860
+ }
1861
+ } else {
1862
+ currentArg += char;
1863
+ }
1864
+ }
1865
+ if (currentArg.length > 0) {
1866
+ args.push(currentArg);
1867
+ }
1868
+ return args;
1843
1869
  }
1844
1870
  list() {
1845
1871
  return [...this.commands.values()].sort((a, b) => a.name.localeCompare(b.name));
@@ -2924,13 +2950,21 @@ var CvarRegistry = class {
2924
2950
  if (existing) {
2925
2951
  return existing;
2926
2952
  }
2927
- const cvar = new Cvar(def);
2953
+ const originalOnChange = def.onChange;
2954
+ const wrappedOnChange = (cvar2, prev) => {
2955
+ originalOnChange?.(cvar2, prev);
2956
+ this.onCvarChange?.(cvar2.name, cvar2.string);
2957
+ };
2958
+ const cvar = new Cvar({ ...def, onChange: wrappedOnChange });
2928
2959
  this.cvars.set(def.name, cvar);
2929
2960
  return cvar;
2930
2961
  }
2931
2962
  get(name) {
2932
2963
  return this.cvars.get(name);
2933
2964
  }
2965
+ getCvar(name) {
2966
+ return this.get(name);
2967
+ }
2934
2968
  setValue(name, value) {
2935
2969
  const cvar = this.get(name);
2936
2970
  if (!cvar) {
@@ -2939,6 +2973,9 @@ var CvarRegistry = class {
2939
2973
  cvar.set(value);
2940
2974
  return cvar;
2941
2975
  }
2976
+ setCvar(name, value) {
2977
+ this.setValue(name, value);
2978
+ }
2942
2979
  resetAll() {
2943
2980
  for (const cvar of this.cvars.values()) {
2944
2981
  cvar.reset();
@@ -2954,6 +2991,15 @@ var CvarRegistry = class {
2954
2991
  list() {
2955
2992
  return [...this.cvars.values()].sort((a, b) => a.name.localeCompare(b.name));
2956
2993
  }
2994
+ listCvars() {
2995
+ return this.list().map((cvar) => ({
2996
+ name: cvar.name,
2997
+ value: cvar.string,
2998
+ defaultValue: cvar.defaultValue,
2999
+ flags: cvar.flags,
3000
+ description: cvar.description
3001
+ }));
3002
+ }
2957
3003
  };
2958
3004
  var EngineHost = class {
2959
3005
  constructor(game, client, options = {}) {
@@ -8762,6 +8808,162 @@ var NetworkMessageParser = class _NetworkMessageParser {
8762
8808
  }
8763
8809
  }
8764
8810
  };
8811
+ var DemoAnalyzer = class {
8812
+ constructor(buffer) {
8813
+ this.events = [];
8814
+ this.summary = {
8815
+ totalKills: 0,
8816
+ totalDeaths: 0,
8817
+ damageDealt: 0,
8818
+ damageReceived: 0,
8819
+ weaponUsage: /* @__PURE__ */ new Map()
8820
+ };
8821
+ this.header = null;
8822
+ this.configStrings = /* @__PURE__ */ new Map();
8823
+ this.serverInfo = {};
8824
+ this.statistics = null;
8825
+ this.playerStats = /* @__PURE__ */ new Map();
8826
+ this.weaponStats = /* @__PURE__ */ new Map();
8827
+ this.buffer = buffer;
8828
+ }
8829
+ analyze() {
8830
+ const reader = new DemoReader(this.buffer);
8831
+ let currentFrameIndex = -1;
8832
+ let currentTime = 0;
8833
+ let frameDuration = 0.1;
8834
+ let protocolVersion = 0;
8835
+ const handler = {
8836
+ onServerData: (protocol, serverCount, attractLoop, gameDir, playerNum, levelName, tickRate, demoType) => {
8837
+ protocolVersion = protocol;
8838
+ this.header = {
8839
+ protocolVersion: protocol,
8840
+ gameDir,
8841
+ levelName,
8842
+ playerNum,
8843
+ serverCount,
8844
+ spawnCount: serverCount,
8845
+ // Mapping generic arg
8846
+ tickRate,
8847
+ demoType
8848
+ };
8849
+ if (tickRate && tickRate > 0) {
8850
+ frameDuration = 1 / tickRate;
8851
+ }
8852
+ },
8853
+ onConfigString: (index, str3) => {
8854
+ this.configStrings.set(index, str3);
8855
+ if (index === 0) {
8856
+ this.parseServerInfo(str3);
8857
+ }
8858
+ },
8859
+ onSpawnBaseline: (entity) => {
8860
+ },
8861
+ onFrame: (frame) => {
8862
+ },
8863
+ onPrint: (level, msg) => {
8864
+ if (msg.includes("died") || msg.includes("killed")) {
8865
+ this.summary.totalDeaths++;
8866
+ this.recordEvent({
8867
+ type: 4,
8868
+ frame: currentFrameIndex,
8869
+ time: currentTime,
8870
+ description: msg.trim()
8871
+ });
8872
+ }
8873
+ },
8874
+ onCenterPrint: () => {
8875
+ },
8876
+ onStuffText: () => {
8877
+ },
8878
+ onSound: () => {
8879
+ },
8880
+ onTempEntity: () => {
8881
+ },
8882
+ onLayout: () => {
8883
+ },
8884
+ onInventory: () => {
8885
+ },
8886
+ onMuzzleFlash: (ent, weapon) => {
8887
+ this.handleWeaponFire(ent, weapon, currentFrameIndex, currentTime);
8888
+ },
8889
+ onMuzzleFlash2: (ent, weapon) => {
8890
+ this.handleWeaponFire(ent, weapon, currentFrameIndex, currentTime);
8891
+ },
8892
+ onMuzzleFlash3: (ent, weapon) => {
8893
+ this.handleWeaponFire(ent, weapon, currentFrameIndex, currentTime);
8894
+ },
8895
+ onDisconnect: () => {
8896
+ },
8897
+ onReconnect: () => {
8898
+ },
8899
+ onDownload: () => {
8900
+ },
8901
+ // Rerelease specific
8902
+ onDamage: (indicators) => {
8903
+ for (const ind of indicators) {
8904
+ this.recordEvent({
8905
+ type: 2,
8906
+ frame: currentFrameIndex,
8907
+ time: currentTime,
8908
+ value: ind.damage,
8909
+ position: ind.dir,
8910
+ description: `Took ${ind.damage} damage`
8911
+ });
8912
+ this.summary.damageReceived += ind.damage;
8913
+ }
8914
+ }
8915
+ };
8916
+ while (reader.hasMore()) {
8917
+ const block = reader.readNextBlock();
8918
+ if (!block) break;
8919
+ currentFrameIndex++;
8920
+ currentTime = currentFrameIndex * frameDuration;
8921
+ const parser = new NetworkMessageParser(block.data, handler);
8922
+ parser.setProtocolVersion(protocolVersion);
8923
+ parser.parseMessage();
8924
+ protocolVersion = parser.getProtocolVersion();
8925
+ }
8926
+ this.statistics = {
8927
+ duration: currentTime,
8928
+ frameCount: currentFrameIndex + 1,
8929
+ averageFps: (currentFrameIndex + 1) / (currentTime || 1),
8930
+ mapName: this.header?.levelName || "unknown",
8931
+ playerCount: 1
8932
+ // Default to 1 for SP/client demo
8933
+ };
8934
+ return {
8935
+ events: this.events,
8936
+ summary: this.summary,
8937
+ header: this.header,
8938
+ configStrings: this.configStrings,
8939
+ serverInfo: this.serverInfo,
8940
+ statistics: this.statistics
8941
+ };
8942
+ }
8943
+ handleWeaponFire(ent, weapon, frame, time) {
8944
+ this.recordEvent({
8945
+ type: 0,
8946
+ frame,
8947
+ time,
8948
+ entityId: ent,
8949
+ value: weapon,
8950
+ description: `Weapon ${weapon} fired by ${ent}`
8951
+ });
8952
+ const count = this.summary.weaponUsage.get(weapon) || 0;
8953
+ this.summary.weaponUsage.set(weapon, count + 1);
8954
+ }
8955
+ recordEvent(event) {
8956
+ this.events.push(event);
8957
+ }
8958
+ parseServerInfo(str3) {
8959
+ const parts = str3.split("\\");
8960
+ for (let i = 1; i < parts.length; i += 2) {
8961
+ if (i + 1 < parts.length) {
8962
+ this.serverInfo[parts[i]] = parts[i + 1];
8963
+ }
8964
+ }
8965
+ }
8966
+ };
8765
8967
  var PlaybackState = /* @__PURE__ */ ((PlaybackState22) => {
8766
8968
  PlaybackState22[PlaybackState22["Stopped"] = 0] = "Stopped";
8767
8969
  PlaybackState22[PlaybackState22["Playing"] = 1] = "Playing";
@@ -8772,6 +8974,7 @@ var PlaybackState = /* @__PURE__ */ ((PlaybackState22) => {
8772
8974
  var DemoPlaybackController = class {
8773
8975
  constructor() {
8774
8976
  this.reader = null;
8977
+ this.buffer = null;
8775
8978
  this.state = 0;
8776
8979
  this.playbackSpeed = 1;
8777
8980
  this.currentProtocolVersion = 0;
@@ -8781,6 +8984,12 @@ var DemoPlaybackController = class {
8781
8984
  this.frameDuration = 100;
8782
8985
  this.snapshotInterval = 100;
8783
8986
  this.snapshots = /* @__PURE__ */ new Map();
8987
+ this.cachedEvents = null;
8988
+ this.cachedSummary = null;
8989
+ this.cachedHeader = null;
8990
+ this.cachedConfigStrings = null;
8991
+ this.cachedServerInfo = null;
8992
+ this.cachedStatistics = null;
8784
8993
  }
8785
8994
  setHandler(handler) {
8786
8995
  this.handler = handler;
@@ -8789,6 +8998,7 @@ var DemoPlaybackController = class {
8789
8998
  this.callbacks = callbacks;
8790
8999
  }
8791
9000
  loadDemo(buffer) {
9001
+ this.buffer = buffer;
8792
9002
  this.reader = new DemoReader(buffer);
8793
9003
  this.transitionState(
8794
9004
  0
@@ -8799,6 +9009,12 @@ var DemoPlaybackController = class {
8799
9009
  this.currentFrameIndex = -1;
8800
9010
  this.snapshots.clear();
8801
9011
  this.lastFrameData = null;
9012
+ this.cachedEvents = null;
9013
+ this.cachedSummary = null;
9014
+ this.cachedHeader = null;
9015
+ this.cachedConfigStrings = null;
9016
+ this.cachedServerInfo = null;
9017
+ this.cachedStatistics = null;
8802
9018
  }
8803
9019
  play() {
8804
9020
  if (this.reader && this.state !== 1) {
@@ -9126,6 +9342,57 @@ var DemoPlaybackController = class {
9126
9342
  this.seek(originalFrame);
9127
9343
  return trajectory;
9128
9344
  }
9345
+ // 3.2.3 Event Log Extraction & 3.3 Metadata
9346
+ getDemoEvents() {
9347
+ this.ensureAnalysis();
9348
+ return this.cachedEvents || [];
9349
+ }
9350
+ filterEvents(type, entityId) {
9351
+ const events = this.getDemoEvents();
9352
+ return events.filter((e) => {
9353
+ if (e.type !== type) return false;
9354
+ if (entityId !== void 0 && e.entityId !== entityId) return false;
9355
+ return true;
9356
+ });
9357
+ }
9358
+ getEventSummary() {
9359
+ this.ensureAnalysis();
9360
+ return this.cachedSummary || {
9361
+ totalKills: 0,
9362
+ totalDeaths: 0,
9363
+ damageDealt: 0,
9364
+ damageReceived: 0,
9365
+ weaponUsage: /* @__PURE__ */ new Map()
9366
+ };
9367
+ }
9368
+ getDemoHeader() {
9369
+ this.ensureAnalysis();
9370
+ return this.cachedHeader;
9371
+ }
9372
+ getDemoConfigStrings() {
9373
+ this.ensureAnalysis();
9374
+ return this.cachedConfigStrings || /* @__PURE__ */ new Map();
9375
+ }
9376
+ getDemoServerInfo() {
9377
+ this.ensureAnalysis();
9378
+ return this.cachedServerInfo || {};
9379
+ }
9380
+ getDemoStatistics() {
9381
+ this.ensureAnalysis();
9382
+ return this.cachedStatistics;
9383
+ }
9384
+ ensureAnalysis() {
9385
+ if (!this.cachedEvents && this.buffer) {
9386
+ const analyzer = new DemoAnalyzer(this.buffer);
9387
+ const result = analyzer.analyze();
9388
+ this.cachedEvents = result.events;
9389
+ this.cachedSummary = result.summary;
9390
+ this.cachedHeader = result.header;
9391
+ this.cachedConfigStrings = result.configStrings;
9392
+ this.cachedServerInfo = result.serverInfo;
9393
+ this.cachedStatistics = result.statistics;
9394
+ }
9395
+ }
9129
9396
  };
9130
9397
  var DemoRecorder = class {
9131
9398
  // -1 means start of demo
@@ -9205,12 +9472,56 @@ var __export3 = (target, all) => {
9205
9472
  };
9206
9473
  var ZERO_VEC3 = { x: 0, y: 0, z: 0 };
9207
9474
  var DEG_TO_RAD2 = Math.PI / 180;
9475
+ var PITCH = 0;
9476
+ var YAW = 1;
9477
+ var ROLL = 2;
9208
9478
  var DEG2RAD_FACTOR2 = Math.PI / 180;
9209
9479
  var RAD2DEG_FACTOR2 = 180 / Math.PI;
9480
+ function axisComponent(vec, axis) {
9481
+ switch (axis) {
9482
+ case PITCH:
9483
+ return vec.x;
9484
+ case YAW:
9485
+ return vec.y;
9486
+ case ROLL:
9487
+ default:
9488
+ return vec.z;
9489
+ }
9490
+ }
9491
+ function degToRad(degrees) {
9492
+ return degrees * DEG2RAD_FACTOR2;
9493
+ }
9210
9494
  function angleMod(angle2) {
9211
9495
  const value = angle2 % 360;
9212
9496
  return value < 0 ? 360 + value : value;
9213
9497
  }
9498
+ function angleVectors(angles) {
9499
+ const yaw = degToRad(axisComponent(angles, YAW));
9500
+ const pitch = degToRad(axisComponent(angles, PITCH));
9501
+ const roll = degToRad(axisComponent(angles, ROLL));
9502
+ const sy = Math.sin(yaw);
9503
+ const cy = Math.cos(yaw);
9504
+ const sp = Math.sin(pitch);
9505
+ const cp = Math.cos(pitch);
9506
+ const sr = Math.sin(roll);
9507
+ const cr = Math.cos(roll);
9508
+ const forward = {
9509
+ x: cp * cy,
9510
+ y: cp * sy,
9511
+ z: -sp
9512
+ };
9513
+ const right = {
9514
+ x: -sr * sp * cy - cr * -sy,
9515
+ y: -sr * sp * sy - cr * cy,
9516
+ z: -sr * cp
9517
+ };
9518
+ const up = {
9519
+ x: cr * sp * cy - sr * -sy,
9520
+ y: cr * sp * sy - sr * cy,
9521
+ z: cr * cp
9522
+ };
9523
+ return { forward, right, up };
9524
+ }
9214
9525
  var ANORMS2 = [
9215
9526
  [-0.525731, 0, 0.850651],
9216
9527
  [-0.442863, 0.238856, 0.864188],
@@ -13106,10 +13417,11 @@ var MultiplayerConnection = class {
13106
13417
 
13107
13418
  // src/ui/demo-controls.ts
13108
13419
  var DemoControls = class {
13109
- constructor(playback) {
13420
+ constructor(playback, onSpeedChange) {
13110
13421
  this.isVisible = true;
13111
13422
  this.demoName = null;
13112
13423
  this.playback = playback;
13424
+ this.onSpeedChange = onSpeedChange;
13113
13425
  }
13114
13426
  setDemoName(name) {
13115
13427
  this.demoName = name;
@@ -13187,6 +13499,9 @@ var DemoControls = class {
13187
13499
  if (newSpeed < 0.1) newSpeed = 0.1;
13188
13500
  if (newSpeed > 16) newSpeed = 16;
13189
13501
  this.playback.setSpeed(newSpeed);
13502
+ if (this.onSpeedChange) {
13503
+ this.onSpeedChange(newSpeed);
13504
+ }
13190
13505
  }
13191
13506
  formatTime(ms) {
13192
13507
  const totalSeconds = Math.floor(ms / 1e3);
@@ -13196,6 +13511,15 @@ var DemoControls = class {
13196
13511
  }
13197
13512
  };
13198
13513
 
13514
+ // src/demo/camera.ts
13515
+ var DemoCameraMode = /* @__PURE__ */ ((DemoCameraMode2) => {
13516
+ DemoCameraMode2[DemoCameraMode2["FirstPerson"] = 0] = "FirstPerson";
13517
+ DemoCameraMode2[DemoCameraMode2["ThirdPerson"] = 1] = "ThirdPerson";
13518
+ DemoCameraMode2[DemoCameraMode2["Free"] = 2] = "Free";
13519
+ DemoCameraMode2[DemoCameraMode2["Follow"] = 3] = "Follow";
13520
+ return DemoCameraMode2;
13521
+ })(DemoCameraMode || {});
13522
+
13199
13523
  // src/effects.ts
13200
13524
  function addDLight(dlights, origin, color, intensity, minLight = 0, die = 0) {
13201
13525
  dlights.push({
@@ -13886,13 +14210,28 @@ function createClient(imports) {
13886
14210
  const prediction = new ClientPrediction2(imports.engine.trace, pointContents);
13887
14211
  const view = new ViewEffects2();
13888
14212
  const demoPlayback = new DemoPlaybackController();
13889
- const demoControls = new DemoControls(demoPlayback);
14213
+ const demoControls = new DemoControls(demoPlayback, (speed) => {
14214
+ if (imports.engine.audio) {
14215
+ const audio = imports.engine.audio;
14216
+ if (typeof audio.setPlaybackRate === "function") {
14217
+ audio.setPlaybackRate(speed);
14218
+ }
14219
+ }
14220
+ });
13890
14221
  const demoHandler = new ClientNetworkHandler(imports);
13891
14222
  const demoRecorder = new DemoRecorder();
13892
14223
  demoHandler.setView(view);
13893
14224
  let isDemoPlaying = false;
13894
14225
  let currentDemoName = null;
13895
14226
  let clientMode = 0 /* Normal */;
14227
+ const demoCameraState = {
14228
+ mode: 0 /* FirstPerson */,
14229
+ thirdPersonDistance: 80,
14230
+ thirdPersonOffset: { x: 0, y: 0, z: 0 },
14231
+ freeCameraOrigin: { x: 0, y: 0, z: 0 },
14232
+ freeCameraAngles: { x: 0, y: 0, z: 0 },
14233
+ followEntityId: -1
14234
+ };
13896
14235
  const menuSystem = new MenuSystem();
13897
14236
  const loadingScreen = new LoadingScreen();
13898
14237
  const errorDialog = new ErrorDialog();
@@ -14107,6 +14446,8 @@ function createClient(imports) {
14107
14446
  },
14108
14447
  handleInput(key, down) {
14109
14448
  if (isDemoPlaying) {
14449
+ if (demoCameraState.mode === 2 /* Free */) {
14450
+ }
14110
14451
  if (demoControls.handleInput(key, down)) {
14111
14452
  return true;
14112
14453
  }
@@ -14192,18 +14533,51 @@ function createClient(imports) {
14192
14533
  lastRenderTime = now;
14193
14534
  demoPlayback.update(dt);
14194
14535
  lastRendered = demoHandler.getPredictionState(demoPlayback.getCurrentTime());
14195
- const frameDuration = 100;
14196
14536
  renderEntities = demoHandler.getRenderableEntities(1, configStrings);
14197
14537
  if (demoHandler.latestFrame && demoHandler.latestFrame.packetEntities) {
14198
14538
  currentPacketEntities = demoHandler.latestFrame.packetEntities.entities;
14199
14539
  }
14200
14540
  if (lastRendered) {
14201
14541
  const demoCamera = demoHandler.getDemoCamera(1);
14202
- if (demoCamera) {
14203
- lastRendered.origin = demoCamera.origin;
14204
- lastRendered.viewAngles = demoCamera.angles;
14205
- if (demoCamera.fov) {
14206
- lastRendered.fov = demoCamera.fov;
14542
+ if (demoCameraState.mode === 0 /* FirstPerson */) {
14543
+ if (demoCamera) {
14544
+ lastRendered.origin = demoCamera.origin;
14545
+ lastRendered.viewAngles = demoCamera.angles;
14546
+ if (demoCamera.fov) {
14547
+ lastRendered.fov = demoCamera.fov;
14548
+ }
14549
+ }
14550
+ } else if (demoCameraState.mode === 1 /* ThirdPerson */) {
14551
+ if (demoCamera) {
14552
+ const vectors = angleVectors(demoCamera.angles);
14553
+ const forward = vectors.forward;
14554
+ const dist2 = demoCameraState.thirdPersonDistance;
14555
+ const camOrigin = { ...demoCamera.origin };
14556
+ camOrigin.x -= forward.x * dist2;
14557
+ camOrigin.y -= forward.y * dist2;
14558
+ camOrigin.z -= forward.z * dist2;
14559
+ const traceStart = demoCamera.origin;
14560
+ const traceEnd = camOrigin;
14561
+ const mins = { x: -4, y: -4, z: -4 };
14562
+ const maxs = { x: 4, y: 4, z: 4 };
14563
+ const trace = imports.engine.trace(traceStart, traceEnd, mins, maxs);
14564
+ if (trace.fraction < 1) {
14565
+ lastRendered.origin = trace.endpos;
14566
+ } else {
14567
+ lastRendered.origin = camOrigin;
14568
+ }
14569
+ lastRendered.viewAngles = demoCamera.angles;
14570
+ }
14571
+ } else if (demoCameraState.mode === 2 /* Free */) {
14572
+ lastRendered.origin = demoCameraState.freeCameraOrigin;
14573
+ lastRendered.viewAngles = demoCameraState.freeCameraAngles;
14574
+ } else if (demoCameraState.mode === 3 /* Follow */) {
14575
+ if (demoCameraState.followEntityId !== -1) {
14576
+ const ent = renderEntities.find((e) => e.id === demoCameraState.followEntityId);
14577
+ if (ent) {
14578
+ const mat = ent.transform;
14579
+ lastRendered.origin = { x: mat[12], y: mat[13], z: mat[14] };
14580
+ }
14207
14581
  }
14208
14582
  }
14209
14583
  }
@@ -14243,8 +14617,10 @@ function createClient(imports) {
14243
14617
  const { origin, viewAngles } = lastRendered;
14244
14618
  camera = new Camera();
14245
14619
  camera.position = vec3_exports.fromValues(origin.x, origin.y, origin.z);
14246
- const viewOffset = lastView?.offset ?? { x: 0, y: 0, z: 0 };
14247
- vec3_exports.add(camera.position, camera.position, [viewOffset.x, viewOffset.y, viewOffset.z]);
14620
+ if (!isDemoPlaying || demoCameraState.mode === 0 /* FirstPerson */) {
14621
+ const viewOffset = lastView?.offset ?? { x: 0, y: 0, z: 0 };
14622
+ vec3_exports.add(camera.position, camera.position, [viewOffset.x, viewOffset.y, viewOffset.z]);
14623
+ }
14248
14624
  const effectAngles = lastView?.angles ?? { x: 0, y: 0, z: 0 };
14249
14625
  camera.angles = vec3_exports.fromValues(viewAngles.x + effectAngles.x, viewAngles.y + effectAngles.y, viewAngles.z + effectAngles.z);
14250
14626
  if (isDemoPlaying && lastRendered.fov) {
@@ -14477,7 +14853,21 @@ function createClient(imports) {
14477
14853
  },
14478
14854
  demoHandler,
14479
14855
  multiplayer,
14480
- configStrings
14856
+ configStrings,
14857
+ // Demo Camera API
14858
+ setDemoCameraMode(mode) {
14859
+ demoCameraState.mode = mode;
14860
+ },
14861
+ setDemoThirdPersonDistance(dist2) {
14862
+ demoCameraState.thirdPersonDistance = dist2;
14863
+ },
14864
+ setDemoThirdPersonOffset(offset) {
14865
+ demoCameraState.thirdPersonOffset = offset;
14866
+ },
14867
+ setDemoFreeCamera(origin, angles) {
14868
+ demoCameraState.freeCameraOrigin = origin;
14869
+ demoCameraState.freeCameraAngles = angles;
14870
+ }
14481
14871
  };
14482
14872
  return clientExports;
14483
14873
  }
@@ -14485,6 +14875,7 @@ export {
14485
14875
  ClientConfigStrings,
14486
14876
  ClientMode,
14487
14877
  ClientPrediction3 as ClientPrediction,
14878
+ DemoCameraMode,
14488
14879
  GameSession,
14489
14880
  InputAction,
14490
14881
  InputBindings,