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
@@ -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],
@@ -13140,10 +13452,11 @@ var MultiplayerConnection = class {
13140
13452
 
13141
13453
  // src/ui/demo-controls.ts
13142
13454
  var DemoControls = class {
13143
- constructor(playback) {
13455
+ constructor(playback, onSpeedChange) {
13144
13456
  this.isVisible = true;
13145
13457
  this.demoName = null;
13146
13458
  this.playback = playback;
13459
+ this.onSpeedChange = onSpeedChange;
13147
13460
  }
13148
13461
  setDemoName(name) {
13149
13462
  this.demoName = name;
@@ -13221,6 +13534,9 @@ var DemoControls = class {
13221
13534
  if (newSpeed < 0.1) newSpeed = 0.1;
13222
13535
  if (newSpeed > 16) newSpeed = 16;
13223
13536
  this.playback.setSpeed(newSpeed);
13537
+ if (this.onSpeedChange) {
13538
+ this.onSpeedChange(newSpeed);
13539
+ }
13224
13540
  }
13225
13541
  formatTime(ms) {
13226
13542
  const totalSeconds = Math.floor(ms / 1e3);
@@ -13230,6 +13546,15 @@ var DemoControls = class {
13230
13546
  }
13231
13547
  };
13232
13548
 
13549
+ // src/demo/camera.ts
13550
+ var DemoCameraMode = /* @__PURE__ */ ((DemoCameraMode2) => {
13551
+ DemoCameraMode2[DemoCameraMode2["FirstPerson"] = 0] = "FirstPerson";
13552
+ DemoCameraMode2[DemoCameraMode2["ThirdPerson"] = 1] = "ThirdPerson";
13553
+ DemoCameraMode2[DemoCameraMode2["Free"] = 2] = "Free";
13554
+ DemoCameraMode2[DemoCameraMode2["Follow"] = 3] = "Follow";
13555
+ return DemoCameraMode2;
13556
+ })(DemoCameraMode || {});
13557
+
13233
13558
  // src/effects.ts
13234
13559
  function addDLight(dlights, origin, color, intensity, minLight = 0, die = 0) {
13235
13560
  dlights.push({
@@ -13917,13 +14242,28 @@ function createClient(imports) {
13917
14242
  const prediction = new import_cgame3.ClientPrediction(imports.engine.trace, pointContents);
13918
14243
  const view = new import_cgame4.ViewEffects();
13919
14244
  const demoPlayback = new DemoPlaybackController();
13920
- const demoControls = new DemoControls(demoPlayback);
14245
+ const demoControls = new DemoControls(demoPlayback, (speed) => {
14246
+ if (imports.engine.audio) {
14247
+ const audio = imports.engine.audio;
14248
+ if (typeof audio.setPlaybackRate === "function") {
14249
+ audio.setPlaybackRate(speed);
14250
+ }
14251
+ }
14252
+ });
13921
14253
  const demoHandler = new ClientNetworkHandler(imports);
13922
14254
  const demoRecorder = new DemoRecorder();
13923
14255
  demoHandler.setView(view);
13924
14256
  let isDemoPlaying = false;
13925
14257
  let currentDemoName = null;
13926
14258
  let clientMode = 0 /* Normal */;
14259
+ const demoCameraState = {
14260
+ mode: 0 /* FirstPerson */,
14261
+ thirdPersonDistance: 80,
14262
+ thirdPersonOffset: { x: 0, y: 0, z: 0 },
14263
+ freeCameraOrigin: { x: 0, y: 0, z: 0 },
14264
+ freeCameraAngles: { x: 0, y: 0, z: 0 },
14265
+ followEntityId: -1
14266
+ };
13927
14267
  const menuSystem = new MenuSystem();
13928
14268
  const loadingScreen = new LoadingScreen();
13929
14269
  const errorDialog = new ErrorDialog();
@@ -14138,6 +14478,8 @@ function createClient(imports) {
14138
14478
  },
14139
14479
  handleInput(key, down) {
14140
14480
  if (isDemoPlaying) {
14481
+ if (demoCameraState.mode === 2 /* Free */) {
14482
+ }
14141
14483
  if (demoControls.handleInput(key, down)) {
14142
14484
  return true;
14143
14485
  }
@@ -14223,18 +14565,51 @@ function createClient(imports) {
14223
14565
  lastRenderTime = now;
14224
14566
  demoPlayback.update(dt);
14225
14567
  lastRendered = demoHandler.getPredictionState(demoPlayback.getCurrentTime());
14226
- const frameDuration = 100;
14227
14568
  renderEntities = demoHandler.getRenderableEntities(1, configStrings);
14228
14569
  if (demoHandler.latestFrame && demoHandler.latestFrame.packetEntities) {
14229
14570
  currentPacketEntities = demoHandler.latestFrame.packetEntities.entities;
14230
14571
  }
14231
14572
  if (lastRendered) {
14232
14573
  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;
14574
+ if (demoCameraState.mode === 0 /* FirstPerson */) {
14575
+ if (demoCamera) {
14576
+ lastRendered.origin = demoCamera.origin;
14577
+ lastRendered.viewAngles = demoCamera.angles;
14578
+ if (demoCamera.fov) {
14579
+ lastRendered.fov = demoCamera.fov;
14580
+ }
14581
+ }
14582
+ } else if (demoCameraState.mode === 1 /* ThirdPerson */) {
14583
+ if (demoCamera) {
14584
+ const vectors = angleVectors(demoCamera.angles);
14585
+ const forward = vectors.forward;
14586
+ const dist2 = demoCameraState.thirdPersonDistance;
14587
+ const camOrigin = { ...demoCamera.origin };
14588
+ camOrigin.x -= forward.x * dist2;
14589
+ camOrigin.y -= forward.y * dist2;
14590
+ camOrigin.z -= forward.z * dist2;
14591
+ const traceStart = demoCamera.origin;
14592
+ const traceEnd = camOrigin;
14593
+ const mins = { x: -4, y: -4, z: -4 };
14594
+ const maxs = { x: 4, y: 4, z: 4 };
14595
+ const trace = imports.engine.trace(traceStart, traceEnd, mins, maxs);
14596
+ if (trace.fraction < 1) {
14597
+ lastRendered.origin = trace.endpos;
14598
+ } else {
14599
+ lastRendered.origin = camOrigin;
14600
+ }
14601
+ lastRendered.viewAngles = demoCamera.angles;
14602
+ }
14603
+ } else if (demoCameraState.mode === 2 /* Free */) {
14604
+ lastRendered.origin = demoCameraState.freeCameraOrigin;
14605
+ lastRendered.viewAngles = demoCameraState.freeCameraAngles;
14606
+ } else if (demoCameraState.mode === 3 /* Follow */) {
14607
+ if (demoCameraState.followEntityId !== -1) {
14608
+ const ent = renderEntities.find((e) => e.id === demoCameraState.followEntityId);
14609
+ if (ent) {
14610
+ const mat = ent.transform;
14611
+ lastRendered.origin = { x: mat[12], y: mat[13], z: mat[14] };
14612
+ }
14238
14613
  }
14239
14614
  }
14240
14615
  }
@@ -14274,8 +14649,10 @@ function createClient(imports) {
14274
14649
  const { origin, viewAngles } = lastRendered;
14275
14650
  camera = new Camera();
14276
14651
  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]);
14652
+ if (!isDemoPlaying || demoCameraState.mode === 0 /* FirstPerson */) {
14653
+ const viewOffset = lastView?.offset ?? { x: 0, y: 0, z: 0 };
14654
+ vec3_exports.add(camera.position, camera.position, [viewOffset.x, viewOffset.y, viewOffset.z]);
14655
+ }
14279
14656
  const effectAngles = lastView?.angles ?? { x: 0, y: 0, z: 0 };
14280
14657
  camera.angles = vec3_exports.fromValues(viewAngles.x + effectAngles.x, viewAngles.y + effectAngles.y, viewAngles.z + effectAngles.z);
14281
14658
  if (isDemoPlaying && lastRendered.fov) {
@@ -14508,7 +14885,21 @@ function createClient(imports) {
14508
14885
  },
14509
14886
  demoHandler,
14510
14887
  multiplayer,
14511
- configStrings
14888
+ configStrings,
14889
+ // Demo Camera API
14890
+ setDemoCameraMode(mode) {
14891
+ demoCameraState.mode = mode;
14892
+ },
14893
+ setDemoThirdPersonDistance(dist2) {
14894
+ demoCameraState.thirdPersonDistance = dist2;
14895
+ },
14896
+ setDemoThirdPersonOffset(offset) {
14897
+ demoCameraState.thirdPersonOffset = offset;
14898
+ },
14899
+ setDemoFreeCamera(origin, angles) {
14900
+ demoCameraState.freeCameraOrigin = origin;
14901
+ demoCameraState.freeCameraAngles = angles;
14902
+ }
14512
14903
  };
14513
14904
  return clientExports;
14514
14905
  }
@@ -14517,6 +14908,7 @@ function createClient(imports) {
14517
14908
  ClientConfigStrings,
14518
14909
  ClientMode,
14519
14910
  ClientPrediction,
14911
+ DemoCameraMode,
14520
14912
  GameSession,
14521
14913
  InputAction,
14522
14914
  InputBindings,