quake2ts 0.0.7 → 0.0.39

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 (177) hide show
  1. package/README.md +425 -0
  2. package/apps/viewer/dist/browser/index.global.js +1 -1
  3. package/apps/viewer/dist/browser/index.global.js.map +1 -1
  4. package/apps/viewer/dist/cjs/index.cjs +2097 -295
  5. package/apps/viewer/dist/cjs/index.cjs.map +1 -1
  6. package/apps/viewer/dist/esm/index.js +2097 -295
  7. package/apps/viewer/dist/esm/index.js.map +1 -1
  8. package/apps/viewer/dist/tsconfig.tsbuildinfo +1 -1
  9. package/apps/viewer/dist/types/index.d.ts +1 -1
  10. package/package.json +1 -1
  11. package/packages/client/dist/browser/index.global.js +1 -1
  12. package/packages/client/dist/browser/index.global.js.map +1 -1
  13. package/packages/client/dist/cjs/index.cjs +1200 -13
  14. package/packages/client/dist/cjs/index.cjs.map +1 -1
  15. package/packages/client/dist/esm/index.js +1186 -12
  16. package/packages/client/dist/esm/index.js.map +1 -1
  17. package/packages/client/dist/tsconfig.tsbuildinfo +1 -1
  18. package/packages/client/dist/types/index.d.ts +14 -6
  19. package/packages/client/dist/types/index.d.ts.map +1 -1
  20. package/packages/client/dist/types/input/bindings.d.ts +18 -0
  21. package/packages/client/dist/types/input/bindings.d.ts.map +1 -0
  22. package/packages/client/dist/types/input/command-buffer.d.ts +15 -0
  23. package/packages/client/dist/types/input/command-buffer.d.ts.map +1 -0
  24. package/packages/client/dist/types/input/controller.d.ts +125 -0
  25. package/packages/client/dist/types/input/controller.d.ts.map +1 -0
  26. package/packages/client/dist/types/prediction.d.ts +38 -0
  27. package/packages/client/dist/types/prediction.d.ts.map +1 -0
  28. package/packages/client/dist/types/view-effects.d.ts +41 -0
  29. package/packages/client/dist/types/view-effects.d.ts.map +1 -0
  30. package/packages/engine/dist/browser/index.global.js +257 -1
  31. package/packages/engine/dist/browser/index.global.js.map +1 -1
  32. package/packages/engine/dist/cjs/index.cjs +2408 -2
  33. package/packages/engine/dist/cjs/index.cjs.map +1 -1
  34. package/packages/engine/dist/esm/index.js +2340 -2
  35. package/packages/engine/dist/esm/index.js.map +1 -1
  36. package/packages/engine/dist/tsconfig.tsbuildinfo +1 -1
  37. package/packages/engine/dist/types/assets/animation.d.ts +33 -0
  38. package/packages/engine/dist/types/assets/animation.d.ts.map +1 -0
  39. package/packages/engine/dist/types/assets/audio.d.ts +21 -0
  40. package/packages/engine/dist/types/assets/audio.d.ts.map +1 -0
  41. package/packages/engine/dist/types/assets/bsp.d.ts +1 -1
  42. package/packages/engine/dist/types/assets/bsp.d.ts.map +1 -1
  43. package/packages/engine/dist/types/assets/ingestion.d.ts +31 -0
  44. package/packages/engine/dist/types/assets/ingestion.d.ts.map +1 -1
  45. package/packages/engine/dist/types/assets/manager.d.ts +43 -0
  46. package/packages/engine/dist/types/assets/manager.d.ts.map +1 -0
  47. package/packages/engine/dist/types/assets/md3.d.ts +69 -0
  48. package/packages/engine/dist/types/assets/md3.d.ts.map +1 -0
  49. package/packages/engine/dist/types/assets/ogg.d.ts +12 -0
  50. package/packages/engine/dist/types/assets/ogg.d.ts.map +1 -0
  51. package/packages/engine/dist/types/assets/pakIndexStore.d.ts +19 -0
  52. package/packages/engine/dist/types/assets/pakIndexStore.d.ts.map +1 -0
  53. package/packages/engine/dist/types/assets/pakValidation.d.ts +28 -0
  54. package/packages/engine/dist/types/assets/pakValidation.d.ts.map +1 -0
  55. package/packages/engine/dist/types/assets/pcx.d.ts +13 -0
  56. package/packages/engine/dist/types/assets/pcx.d.ts.map +1 -0
  57. package/packages/engine/dist/types/assets/texture.d.ts +29 -0
  58. package/packages/engine/dist/types/assets/texture.d.ts.map +1 -0
  59. package/packages/engine/dist/types/assets/wal.d.ts +21 -0
  60. package/packages/engine/dist/types/assets/wal.d.ts.map +1 -0
  61. package/packages/engine/dist/types/assets/wav.d.ts +11 -0
  62. package/packages/engine/dist/types/assets/wav.d.ts.map +1 -0
  63. package/packages/engine/dist/types/audio/api.d.ts +29 -0
  64. package/packages/engine/dist/types/audio/api.d.ts.map +1 -0
  65. package/packages/engine/dist/types/audio/channels.d.ts +15 -0
  66. package/packages/engine/dist/types/audio/channels.d.ts.map +1 -0
  67. package/packages/engine/dist/types/audio/constants.d.ts +24 -0
  68. package/packages/engine/dist/types/audio/constants.d.ts.map +1 -0
  69. package/packages/engine/dist/types/audio/context.d.ts +67 -0
  70. package/packages/engine/dist/types/audio/context.d.ts.map +1 -0
  71. package/packages/engine/dist/types/audio/music.d.ts +42 -0
  72. package/packages/engine/dist/types/audio/music.d.ts.map +1 -0
  73. package/packages/engine/dist/types/audio/precache.d.ts +28 -0
  74. package/packages/engine/dist/types/audio/precache.d.ts.map +1 -0
  75. package/packages/engine/dist/types/audio/registry.d.ts +13 -0
  76. package/packages/engine/dist/types/audio/registry.d.ts.map +1 -0
  77. package/packages/engine/dist/types/audio/spatialization.d.ts +14 -0
  78. package/packages/engine/dist/types/audio/spatialization.d.ts.map +1 -0
  79. package/packages/engine/dist/types/audio/system.d.ts +101 -0
  80. package/packages/engine/dist/types/audio/system.d.ts.map +1 -0
  81. package/packages/engine/dist/types/configstrings.d.ts +1 -0
  82. package/packages/engine/dist/types/configstrings.d.ts.map +1 -1
  83. package/packages/engine/dist/types/index.d.ts +26 -1
  84. package/packages/engine/dist/types/index.d.ts.map +1 -1
  85. package/packages/engine/dist/types/render/bspPipeline.d.ts +42 -0
  86. package/packages/engine/dist/types/render/bspPipeline.d.ts.map +1 -0
  87. package/packages/engine/dist/types/render/bspTraversal.d.ts +11 -0
  88. package/packages/engine/dist/types/render/bspTraversal.d.ts.map +1 -0
  89. package/packages/engine/dist/types/render/culling.d.ts +8 -0
  90. package/packages/engine/dist/types/render/culling.d.ts.map +1 -0
  91. package/packages/engine/dist/types/render/md2Pipeline.d.ts +51 -0
  92. package/packages/engine/dist/types/render/md2Pipeline.d.ts.map +1 -0
  93. package/packages/engine/dist/types/render/resources.d.ts +10 -0
  94. package/packages/engine/dist/types/render/resources.d.ts.map +1 -1
  95. package/packages/engine/dist/types/render/skybox.d.ts +26 -0
  96. package/packages/engine/dist/types/render/skybox.d.ts.map +1 -0
  97. package/packages/game/dist/browser/index.global.js +1 -1
  98. package/packages/game/dist/browser/index.global.js.map +1 -1
  99. package/packages/game/dist/cjs/index.cjs +2926 -116
  100. package/packages/game/dist/cjs/index.cjs.map +1 -1
  101. package/packages/game/dist/esm/index.js +2863 -115
  102. package/packages/game/dist/esm/index.js.map +1 -1
  103. package/packages/game/dist/tsconfig.tsbuildinfo +1 -1
  104. package/packages/game/dist/types/ai/constants.d.ts +13 -0
  105. package/packages/game/dist/types/ai/constants.d.ts.map +1 -0
  106. package/packages/game/dist/types/ai/index.d.ts +4 -0
  107. package/packages/game/dist/types/ai/index.d.ts.map +1 -0
  108. package/packages/game/dist/types/ai/movement.d.ts +20 -0
  109. package/packages/game/dist/types/ai/movement.d.ts.map +1 -0
  110. package/packages/game/dist/types/ai/perception.d.ts +21 -0
  111. package/packages/game/dist/types/ai/perception.d.ts.map +1 -0
  112. package/packages/game/dist/types/checksum.d.ts +3 -0
  113. package/packages/game/dist/types/checksum.d.ts.map +1 -0
  114. package/packages/game/dist/types/combat/armor.d.ts +39 -0
  115. package/packages/game/dist/types/combat/armor.d.ts.map +1 -0
  116. package/packages/game/dist/types/combat/damage.d.ts +52 -0
  117. package/packages/game/dist/types/combat/damage.d.ts.map +1 -0
  118. package/packages/game/dist/types/combat/damageFlags.d.ts +15 -0
  119. package/packages/game/dist/types/combat/damageFlags.d.ts.map +1 -0
  120. package/packages/game/dist/types/combat/damageMods.d.ts +79 -0
  121. package/packages/game/dist/types/combat/damageMods.d.ts.map +1 -0
  122. package/packages/game/dist/types/combat/index.d.ts +6 -0
  123. package/packages/game/dist/types/combat/index.d.ts.map +1 -0
  124. package/packages/game/dist/types/combat/specialDamage.d.ts +88 -0
  125. package/packages/game/dist/types/combat/specialDamage.d.ts.map +1 -0
  126. package/packages/game/dist/types/entities/entity.d.ts +46 -2
  127. package/packages/game/dist/types/entities/entity.d.ts.map +1 -1
  128. package/packages/game/dist/types/entities/index.d.ts +6 -2
  129. package/packages/game/dist/types/entities/index.d.ts.map +1 -1
  130. package/packages/game/dist/types/entities/pool.d.ts +9 -0
  131. package/packages/game/dist/types/entities/pool.d.ts.map +1 -1
  132. package/packages/game/dist/types/entities/spawn.d.ts +27 -0
  133. package/packages/game/dist/types/entities/spawn.d.ts.map +1 -0
  134. package/packages/game/dist/types/entities/system.d.ts +32 -1
  135. package/packages/game/dist/types/entities/system.d.ts.map +1 -1
  136. package/packages/game/dist/types/entities/thinkScheduler.d.ts +6 -0
  137. package/packages/game/dist/types/entities/thinkScheduler.d.ts.map +1 -1
  138. package/packages/game/dist/types/entities/triggers.d.ts +3 -0
  139. package/packages/game/dist/types/entities/triggers.d.ts.map +1 -0
  140. package/packages/game/dist/types/entities/utils.d.ts +4 -0
  141. package/packages/game/dist/types/entities/utils.d.ts.map +1 -0
  142. package/packages/game/dist/types/index.d.ts +5 -0
  143. package/packages/game/dist/types/index.d.ts.map +1 -1
  144. package/packages/game/dist/types/inventory/ammo.d.ts +17 -0
  145. package/packages/game/dist/types/inventory/ammo.d.ts.map +1 -0
  146. package/packages/game/dist/types/inventory/index.d.ts +2 -0
  147. package/packages/game/dist/types/inventory/index.d.ts.map +1 -0
  148. package/packages/game/dist/types/level.d.ts +1 -0
  149. package/packages/game/dist/types/level.d.ts.map +1 -1
  150. package/packages/game/dist/types/save/index.d.ts +4 -0
  151. package/packages/game/dist/types/save/index.d.ts.map +1 -0
  152. package/packages/game/dist/types/save/rerelease.d.ts +25 -0
  153. package/packages/game/dist/types/save/rerelease.d.ts.map +1 -0
  154. package/packages/game/dist/types/save/save.d.ts +49 -0
  155. package/packages/game/dist/types/save/save.d.ts.map +1 -0
  156. package/packages/game/dist/types/save/storage.d.ts +37 -0
  157. package/packages/game/dist/types/save/storage.d.ts.map +1 -0
  158. package/packages/shared/dist/browser/index.global.js +1 -1
  159. package/packages/shared/dist/browser/index.global.js.map +1 -1
  160. package/packages/shared/dist/cjs/index.cjs +638 -9
  161. package/packages/shared/dist/cjs/index.cjs.map +1 -1
  162. package/packages/shared/dist/esm/index.js +616 -9
  163. package/packages/shared/dist/esm/index.js.map +1 -1
  164. package/packages/shared/dist/tsconfig.tsbuildinfo +1 -1
  165. package/packages/shared/dist/types/bsp/collision.d.ts +56 -0
  166. package/packages/shared/dist/types/bsp/collision.d.ts.map +1 -1
  167. package/packages/shared/dist/types/bsp/contents.d.ts +1 -0
  168. package/packages/shared/dist/types/bsp/contents.d.ts.map +1 -1
  169. package/packages/shared/dist/types/index.d.ts +2 -0
  170. package/packages/shared/dist/types/index.d.ts.map +1 -1
  171. package/packages/shared/dist/types/math/random.d.ts +11 -0
  172. package/packages/shared/dist/types/math/random.d.ts.map +1 -1
  173. package/packages/shared/dist/types/protocol/contracts.d.ts +17 -0
  174. package/packages/shared/dist/types/protocol/contracts.d.ts.map +1 -0
  175. package/packages/shared/dist/types/protocol/usercmd.d.ts +30 -0
  176. package/packages/shared/dist/types/protocol/usercmd.d.ts.map +1 -0
  177. package/packages/tools/dist/tsconfig.tsbuildinfo +1 -1
@@ -130,7 +130,27 @@ var EngineHost = class {
130
130
  };
131
131
 
132
132
  // ../shared/dist/esm/index.js
133
+ var ZERO_VEC3 = { x: 0, y: 0, z: 0 };
133
134
  var DEG_TO_RAD = Math.PI / 180;
135
+ function subtractVec3(a, b) {
136
+ return { x: a.x - b.x, y: a.y - b.y, z: a.z - b.z };
137
+ }
138
+ function scaleVec3(a, scalar) {
139
+ return { x: a.x * scalar, y: a.y * scalar, z: a.z * scalar };
140
+ }
141
+ function dotVec3(a, b) {
142
+ return a.x * b.x + a.y * b.y + a.z * b.z;
143
+ }
144
+ function lengthSquaredVec3(a) {
145
+ return dotVec3(a, a);
146
+ }
147
+ function lengthVec3(a) {
148
+ return Math.sqrt(lengthSquaredVec3(a));
149
+ }
150
+ function normalizeVec3(a) {
151
+ const len = lengthVec3(a);
152
+ return len === 0 ? a : scaleVec3(a, 1 / len);
153
+ }
134
154
  var DEG2RAD_FACTOR = Math.PI / 180;
135
155
  var RAD2DEG_FACTOR = 180 / Math.PI;
136
156
  var CONTENTS_SOLID = 1 << 0;
@@ -186,6 +206,7 @@ var MASK_NAV_SOLID = CONTENTS_SOLID | CONTENTS_PLAYERCLIP | CONTENTS_WINDOW;
186
206
  var MASK_LADDER_NAV_SOLID = CONTENTS_SOLID | CONTENTS_WINDOW;
187
207
  var MASK_WALK_NAV_SOLID = CONTENTS_SOLID | CONTENTS_PLAYERCLIP | CONTENTS_WINDOW | CONTENTS_MONSTERCLIP;
188
208
  var MASK_PROJECTILE = MASK_SHOT | CONTENTS_PROJECTILECLIP;
209
+ var MAX_CHECKCOUNT = Number.MAX_SAFE_INTEGER - 1;
189
210
  var CvarFlags = /* @__PURE__ */ ((CvarFlags2) => {
190
211
  CvarFlags2[CvarFlags2["None"] = 0] = "None";
191
212
  CvarFlags2[CvarFlags2["Archive"] = 1] = "Archive";
@@ -291,6 +312,14 @@ var ConfigStringRegistry = class {
291
312
  soundIndex(path) {
292
313
  return this.register(path, ConfigStringIndex.Sounds, MAX_SOUNDS, "soundCursor");
293
314
  }
315
+ findSoundIndex(path) {
316
+ for (let i = ConfigStringIndex.Sounds; i < ConfigStringIndex.Sounds + MAX_SOUNDS; i += 1) {
317
+ if (this.values.get(i) === path) {
318
+ return i;
319
+ }
320
+ }
321
+ return void 0;
322
+ }
294
323
  imageIndex(path) {
295
324
  return this.register(path, ConfigStringIndex.Images, MAX_IMAGES, "imageCursor");
296
325
  }
@@ -659,6 +688,72 @@ var VirtualFileSystem = class {
659
688
  }
660
689
  };
661
690
 
691
+ // src/assets/pakValidation.ts
692
+ var RERELEASE_KNOWN_PAKS = Object.freeze([
693
+ // Base campaign
694
+ { name: "pak0.pak", checksum: 2378051181, description: "Base game assets" },
695
+ { name: "pak0.pak@baseq2", checksum: 2378051181, description: "Base game assets (baseq2)" },
696
+ // Mission packs bundled with the rerelease
697
+ { name: "pak0.pak@rogue", checksum: 3373211245, description: "Ground Zero (rogue) mission pack" },
698
+ { name: "pak0.pak@xatrix", checksum: 1358269824, description: "The Reckoning (xatrix) mission pack" }
699
+ ]);
700
+ var PakValidationError = class extends Error {
701
+ constructor(result) {
702
+ super(
703
+ result.status === "unknown" ? `Unknown PAK not allowed: ${result.name}` : `PAK checksum mismatch for ${result.name}`
704
+ );
705
+ this.result = result;
706
+ this.name = "PakValidationError";
707
+ }
708
+ };
709
+ var PakValidator = class {
710
+ constructor(knownPaks = RERELEASE_KNOWN_PAKS) {
711
+ this.known = /* @__PURE__ */ new Map();
712
+ knownPaks.forEach((pak) => this.known.set(this.normalizePakName(pak.name), pak));
713
+ }
714
+ validateArchive(archive, nameOverride) {
715
+ const pakName = this.normalizePakName(nameOverride ?? ("name" in archive ? archive.name : "unknown"));
716
+ const checksum = archive.checksum;
717
+ const size = "size" in archive ? archive.size : void 0;
718
+ const known = this.known.get(pakName);
719
+ if (!known) {
720
+ return { name: pakName, checksum, status: "unknown", size };
721
+ }
722
+ if (known.checksum !== checksum) {
723
+ return {
724
+ name: pakName,
725
+ checksum,
726
+ expectedChecksum: known.checksum,
727
+ status: "mismatch",
728
+ size,
729
+ description: known.description
730
+ };
731
+ }
732
+ return {
733
+ name: pakName,
734
+ checksum,
735
+ expectedChecksum: known.checksum,
736
+ status: "valid",
737
+ size,
738
+ description: known.description
739
+ };
740
+ }
741
+ assertValid(archive, nameOverride) {
742
+ const outcome = this.validateArchive(archive, nameOverride);
743
+ if (outcome.status === "mismatch") {
744
+ throw new PakValidationError(outcome);
745
+ }
746
+ return outcome;
747
+ }
748
+ normalizePakName(name) {
749
+ const normalized = normalizePath(name);
750
+ const parts = normalized.split("/");
751
+ const filename = parts.pop() ?? normalized;
752
+ const directory = parts.pop();
753
+ return directory ? `${filename}@${directory}` : filename;
754
+ }
755
+ };
756
+
662
757
  // src/assets/ingestion.ts
663
758
  var PakIngestionError = class extends Error {
664
759
  constructor(file, cause) {
@@ -750,18 +845,47 @@ async function toArrayBuffer(source, onProgress) {
750
845
  }
751
846
  async function ingestPaks(vfs, sources, onProgressOrOptions) {
752
847
  const options = typeof onProgressOrOptions === "function" ? { onProgress: onProgressOrOptions } : onProgressOrOptions ?? {};
848
+ const shouldPersist = options.persistIndexes ?? Boolean(options.pakIndexStore);
849
+ const enforceValidation = options.enforceValidation ?? Boolean(options.validator);
850
+ const allowUnknownPaks = options.allowUnknownPaks ?? true;
851
+ const stopOnError = options.stopOnError ?? false;
753
852
  const results = [];
754
853
  for (const source of sources) {
755
854
  try {
756
855
  const buffer = await toArrayBuffer(source, options.onProgress);
757
856
  const archive = PakArchive.fromArrayBuffer(source.name, buffer);
857
+ const validation = options.validator?.validateArchive(archive);
858
+ if (validation) {
859
+ options.onValidationResult?.(validation);
860
+ const isMismatch = validation.status === "mismatch";
861
+ const isUnknown = validation.status === "unknown";
862
+ if (isMismatch && enforceValidation || isUnknown && !allowUnknownPaks) {
863
+ const validationError = new PakValidationError(validation);
864
+ options.onError?.(source.name, validationError);
865
+ if (stopOnError) {
866
+ throw new PakIngestionError(source.name, validationError);
867
+ }
868
+ results.push({ archive, mounted: false, validation });
869
+ continue;
870
+ }
871
+ }
758
872
  vfs.mountPak(archive);
873
+ if (shouldPersist && options.pakIndexStore) {
874
+ try {
875
+ await options.pakIndexStore.persist(archive);
876
+ } catch (error) {
877
+ options.onError?.(source.name, error);
878
+ if (stopOnError) {
879
+ throw new PakIngestionError(source.name, error);
880
+ }
881
+ }
882
+ }
759
883
  options.onProgress?.({ file: source.name, loadedBytes: buffer.byteLength, totalBytes: buffer.byteLength, state: "parsed" });
760
- results.push({ archive, mounted: true });
884
+ results.push({ archive, mounted: true, validation });
761
885
  } catch (error) {
762
886
  options.onProgress?.({ file: source.name, loadedBytes: 0, totalBytes: 0, state: "error" });
763
887
  options.onError?.(source.name, error);
764
- if (options.stopOnError) {
888
+ if (stopOnError) {
765
889
  throw new PakIngestionError(source.name, error);
766
890
  }
767
891
  }
@@ -1240,6 +1364,1393 @@ function groupMd2Animations(frames) {
1240
1364
  return animations;
1241
1365
  }
1242
1366
 
1367
+ // src/assets/md3.ts
1368
+ var MD3_IDENT = 860898377;
1369
+ var MD3_VERSION = 15;
1370
+ var Md3ParseError = class extends Error {
1371
+ constructor(message) {
1372
+ super(message);
1373
+ this.name = "Md3ParseError";
1374
+ }
1375
+ };
1376
+ function readString(view, offset, length) {
1377
+ const bytes = new Uint8Array(view.buffer, view.byteOffset + offset, length);
1378
+ const decoded = new TextDecoder("utf-8").decode(bytes);
1379
+ return decoded.replace(/\0.*$/, "").trim();
1380
+ }
1381
+ function decodeLatLngNormal(latLng) {
1382
+ const lat = (latLng >> 8 & 255) * (2 * Math.PI / 255);
1383
+ const lng = (latLng & 255) * (2 * Math.PI / 255);
1384
+ const sinLng = Math.sin(lng);
1385
+ return {
1386
+ x: Math.cos(lat) * sinLng,
1387
+ y: Math.sin(lat) * sinLng,
1388
+ z: Math.cos(lng)
1389
+ };
1390
+ }
1391
+ function validateOffset(name, offset, size, bufferLength) {
1392
+ if (offset < 0 || offset + size > bufferLength) {
1393
+ throw new Md3ParseError(`${name} exceeds buffer bounds`);
1394
+ }
1395
+ }
1396
+ function parseHeader2(view) {
1397
+ const ident = view.getInt32(0, true);
1398
+ if (ident !== MD3_IDENT) {
1399
+ throw new Md3ParseError(`Invalid MD3 ident: ${ident}`);
1400
+ }
1401
+ const version = view.getInt32(4, true);
1402
+ if (version !== MD3_VERSION) {
1403
+ throw new Md3ParseError(`Unsupported MD3 version: ${version}`);
1404
+ }
1405
+ const name = readString(view, 8, 64);
1406
+ const flags = view.getInt32(72, true);
1407
+ const numFrames = view.getInt32(76, true);
1408
+ const numTags = view.getInt32(80, true);
1409
+ const numSurfaces = view.getInt32(84, true);
1410
+ const numSkins = view.getInt32(88, true);
1411
+ const ofsFrames = view.getInt32(92, true);
1412
+ const ofsTags = view.getInt32(96, true);
1413
+ const ofsSurfaces = view.getInt32(100, true);
1414
+ const ofsEnd = view.getInt32(104, true);
1415
+ if (numFrames <= 0 || numSurfaces < 0 || numTags < 0) {
1416
+ throw new Md3ParseError("Invalid MD3 counts");
1417
+ }
1418
+ return {
1419
+ ident,
1420
+ version,
1421
+ name,
1422
+ flags,
1423
+ numFrames,
1424
+ numTags,
1425
+ numSurfaces,
1426
+ numSkins,
1427
+ ofsFrames,
1428
+ ofsTags,
1429
+ ofsSurfaces,
1430
+ ofsEnd
1431
+ };
1432
+ }
1433
+ function parseFrames2(view, header) {
1434
+ const frames = [];
1435
+ const frameSize = 56;
1436
+ validateOffset("Frames", header.ofsFrames, header.numFrames * frameSize, view.byteLength);
1437
+ for (let i = 0; i < header.numFrames; i += 1) {
1438
+ const base = header.ofsFrames + i * frameSize;
1439
+ frames.push({
1440
+ minBounds: {
1441
+ x: view.getFloat32(base, true),
1442
+ y: view.getFloat32(base + 4, true),
1443
+ z: view.getFloat32(base + 8, true)
1444
+ },
1445
+ maxBounds: {
1446
+ x: view.getFloat32(base + 12, true),
1447
+ y: view.getFloat32(base + 16, true),
1448
+ z: view.getFloat32(base + 20, true)
1449
+ },
1450
+ localOrigin: {
1451
+ x: view.getFloat32(base + 24, true),
1452
+ y: view.getFloat32(base + 28, true),
1453
+ z: view.getFloat32(base + 32, true)
1454
+ },
1455
+ radius: view.getFloat32(base + 36, true),
1456
+ name: readString(view, base + 40, 16)
1457
+ });
1458
+ }
1459
+ return frames;
1460
+ }
1461
+ function parseTags(view, header) {
1462
+ const tags = [];
1463
+ const tagSize = 112;
1464
+ const totalSize = header.numFrames * header.numTags * tagSize;
1465
+ validateOffset("Tags", header.ofsTags, totalSize, view.byteLength);
1466
+ for (let frame = 0; frame < header.numFrames; frame += 1) {
1467
+ const frameTags = [];
1468
+ for (let tagIndex = 0; tagIndex < header.numTags; tagIndex += 1) {
1469
+ const base = header.ofsTags + (frame * header.numTags + tagIndex) * tagSize;
1470
+ const originOffset = base + 64;
1471
+ const axisOffset = originOffset + 12;
1472
+ frameTags.push({
1473
+ name: readString(view, base, 64),
1474
+ origin: {
1475
+ x: view.getFloat32(originOffset, true),
1476
+ y: view.getFloat32(originOffset + 4, true),
1477
+ z: view.getFloat32(originOffset + 8, true)
1478
+ },
1479
+ axis: [
1480
+ {
1481
+ x: view.getFloat32(axisOffset, true),
1482
+ y: view.getFloat32(axisOffset + 4, true),
1483
+ z: view.getFloat32(axisOffset + 8, true)
1484
+ },
1485
+ {
1486
+ x: view.getFloat32(axisOffset + 12, true),
1487
+ y: view.getFloat32(axisOffset + 16, true),
1488
+ z: view.getFloat32(axisOffset + 20, true)
1489
+ },
1490
+ {
1491
+ x: view.getFloat32(axisOffset + 24, true),
1492
+ y: view.getFloat32(axisOffset + 28, true),
1493
+ z: view.getFloat32(axisOffset + 32, true)
1494
+ }
1495
+ ]
1496
+ });
1497
+ }
1498
+ tags.push(frameTags);
1499
+ }
1500
+ return tags;
1501
+ }
1502
+ function parseSurface(view, offset) {
1503
+ const ident = view.getInt32(offset, true);
1504
+ if (ident !== MD3_IDENT) {
1505
+ throw new Md3ParseError(`Invalid surface ident at ${offset}: ${ident}`);
1506
+ }
1507
+ const name = readString(view, offset + 4, 64);
1508
+ const flags = view.getInt32(offset + 68, true);
1509
+ const numFrames = view.getInt32(offset + 72, true);
1510
+ const numShaders = view.getInt32(offset + 76, true);
1511
+ const numVerts = view.getInt32(offset + 80, true);
1512
+ const numTriangles = view.getInt32(offset + 84, true);
1513
+ const ofsTriangles = view.getInt32(offset + 88, true);
1514
+ const ofsShaders = view.getInt32(offset + 92, true);
1515
+ const ofsSt = view.getInt32(offset + 96, true);
1516
+ const ofsXyzNormals = view.getInt32(offset + 100, true);
1517
+ const ofsEnd = view.getInt32(offset + 104, true);
1518
+ if (numFrames <= 0 || numVerts <= 0 || numTriangles <= 0) {
1519
+ throw new Md3ParseError(`Invalid surface counts for ${name}`);
1520
+ }
1521
+ const surfaceSize = ofsEnd;
1522
+ validateOffset(`Surface ${name}`, offset, surfaceSize, view.byteLength);
1523
+ const triangles = [];
1524
+ const triangleStart = offset + ofsTriangles;
1525
+ for (let i = 0; i < numTriangles; i += 1) {
1526
+ const base = triangleStart + i * 12;
1527
+ triangles.push({
1528
+ indices: [view.getInt32(base, true), view.getInt32(base + 4, true), view.getInt32(base + 8, true)]
1529
+ });
1530
+ }
1531
+ const shaders = [];
1532
+ const shaderStart = offset + ofsShaders;
1533
+ for (let i = 0; i < numShaders; i += 1) {
1534
+ const base = shaderStart + i * 68;
1535
+ shaders.push({ name: readString(view, base, 64), shaderIndex: view.getInt32(base + 64, true) });
1536
+ }
1537
+ const texCoords = [];
1538
+ const stStart = offset + ofsSt;
1539
+ for (let i = 0; i < numVerts; i += 1) {
1540
+ const base = stStart + i * 8;
1541
+ texCoords.push({ s: view.getFloat32(base, true), t: view.getFloat32(base + 4, true) });
1542
+ }
1543
+ const vertices = [];
1544
+ const xyzStart = offset + ofsXyzNormals;
1545
+ for (let frame = 0; frame < numFrames; frame += 1) {
1546
+ const frameVertices = [];
1547
+ for (let i = 0; i < numVerts; i += 1) {
1548
+ const base = xyzStart + (frame * numVerts + i) * 8;
1549
+ const x = view.getInt16(base, true) / 64;
1550
+ const y = view.getInt16(base + 2, true) / 64;
1551
+ const z = view.getInt16(base + 4, true) / 64;
1552
+ const latLng = view.getUint16(base + 6, true);
1553
+ frameVertices.push({ position: { x, y, z }, latLng, normal: decodeLatLngNormal(latLng) });
1554
+ }
1555
+ vertices.push(frameVertices);
1556
+ }
1557
+ return {
1558
+ surface: { name, flags, numFrames, shaders, triangles, texCoords, vertices },
1559
+ nextOffset: offset + ofsEnd
1560
+ };
1561
+ }
1562
+ function parseMd3(buffer) {
1563
+ if (buffer.byteLength < 108) {
1564
+ throw new Md3ParseError("MD3 buffer too small for header");
1565
+ }
1566
+ const view = new DataView(buffer);
1567
+ const header = parseHeader2(view);
1568
+ validateOffset("MD3 end", header.ofsEnd, 0, buffer.byteLength);
1569
+ const frames = parseFrames2(view, header);
1570
+ const tags = parseTags(view, header);
1571
+ const surfaces = [];
1572
+ let surfaceOffset = header.ofsSurfaces;
1573
+ for (let i = 0; i < header.numSurfaces; i += 1) {
1574
+ const { surface, nextOffset } = parseSurface(view, surfaceOffset);
1575
+ surfaces.push(surface);
1576
+ surfaceOffset = nextOffset;
1577
+ }
1578
+ if (surfaceOffset !== header.ofsEnd) {
1579
+ throw new Md3ParseError("Surface parsing did not reach ofsEnd");
1580
+ }
1581
+ return { header, frames, tags, surfaces };
1582
+ }
1583
+ var Md3Loader = class {
1584
+ constructor(vfs) {
1585
+ this.vfs = vfs;
1586
+ }
1587
+ async load(path) {
1588
+ const data = await this.vfs.readFile(path);
1589
+ return parseMd3(data.slice().buffer);
1590
+ }
1591
+ };
1592
+
1593
+ // src/assets/animation.ts
1594
+ function advanceAnimation(state, deltaSeconds) {
1595
+ const duration = (state.sequence.end - state.sequence.start + 1) / state.sequence.fps;
1596
+ const loop = state.sequence.loop !== false;
1597
+ let time = state.time + deltaSeconds;
1598
+ if (loop) {
1599
+ time = (time % duration + duration) % duration;
1600
+ } else if (time > duration) {
1601
+ time = duration;
1602
+ }
1603
+ return { ...state, time: Math.max(0, Math.min(time, duration)) };
1604
+ }
1605
+ function computeFrameBlend(state) {
1606
+ const totalFrames = state.sequence.end - state.sequence.start + 1;
1607
+ const frameDuration = 1 / state.sequence.fps;
1608
+ const loop = state.sequence.loop !== false;
1609
+ const framePosition = state.time / frameDuration;
1610
+ if (!loop && framePosition >= totalFrames) {
1611
+ return { frame: state.sequence.end, nextFrame: state.sequence.end, lerp: 0 };
1612
+ }
1613
+ const normalizedPosition = loop ? framePosition % totalFrames : Math.min(framePosition, totalFrames - 1);
1614
+ const baseFrame = Math.floor(normalizedPosition);
1615
+ const frame = state.sequence.start + baseFrame;
1616
+ const nextFrame = baseFrame + 1 >= totalFrames ? loop ? state.sequence.start : state.sequence.end : frame + 1;
1617
+ const lerp2 = !loop && baseFrame >= totalFrames - 1 ? 0 : normalizedPosition - baseFrame;
1618
+ return { frame, nextFrame, lerp: lerp2 };
1619
+ }
1620
+ function createAnimationState(sequence) {
1621
+ return { sequence, time: 0 };
1622
+ }
1623
+ function interpolateVec3(a, b, t) {
1624
+ return {
1625
+ x: a.x + (b.x - a.x) * t,
1626
+ y: a.y + (b.y - a.y) * t,
1627
+ z: a.z + (b.z - a.z) * t
1628
+ };
1629
+ }
1630
+
1631
+ // src/assets/wal.ts
1632
+ var WalParseError = class extends Error {
1633
+ constructor(message) {
1634
+ super(message);
1635
+ this.name = "WalParseError";
1636
+ }
1637
+ };
1638
+ function parseWal(buffer) {
1639
+ if (buffer.byteLength < 100) {
1640
+ throw new WalParseError("WAL buffer too small");
1641
+ }
1642
+ const view = new DataView(buffer);
1643
+ const nameBytes = new Uint8Array(buffer, 0, 32);
1644
+ const name = new TextDecoder("utf-8").decode(nameBytes).replace(/\0.*$/, "").trim();
1645
+ const width = view.getInt32(32, true);
1646
+ const height = view.getInt32(36, true);
1647
+ const offsets = [view.getInt32(40, true), view.getInt32(44, true), view.getInt32(48, true), view.getInt32(52, true)];
1648
+ const animNameBytes = new Uint8Array(buffer, 56, 32);
1649
+ const animName = new TextDecoder("utf-8").decode(animNameBytes).replace(/\0.*$/, "").trim();
1650
+ const flags = view.getInt32(88, true);
1651
+ const contents = view.getInt32(92, true);
1652
+ const value = view.getInt32(96, true);
1653
+ if (width <= 0 || height <= 0) {
1654
+ throw new WalParseError("Invalid WAL dimensions");
1655
+ }
1656
+ const mipmaps = [];
1657
+ let currentWidth = width;
1658
+ let currentHeight = height;
1659
+ for (let level = 0; level < offsets.length; level += 1) {
1660
+ const offset = offsets[level];
1661
+ const expectedSize = Math.max(1, currentWidth * currentHeight | 0);
1662
+ if (offset <= 0 || offset + expectedSize > buffer.byteLength) {
1663
+ throw new WalParseError(`Invalid WAL mip offset for level ${level}`);
1664
+ }
1665
+ const data = new Uint8Array(buffer, offset, expectedSize);
1666
+ mipmaps.push({ level, width: currentWidth, height: currentHeight, data });
1667
+ currentWidth = Math.max(1, currentWidth >> 1);
1668
+ currentHeight = Math.max(1, currentHeight >> 1);
1669
+ }
1670
+ return { name, width, height, mipmaps, animName, flags, contents, value };
1671
+ }
1672
+
1673
+ // src/assets/pcx.ts
1674
+ var PcxParseError = class extends Error {
1675
+ constructor(message) {
1676
+ super(message);
1677
+ this.name = "PcxParseError";
1678
+ }
1679
+ };
1680
+ function parsePcx(buffer) {
1681
+ if (buffer.byteLength < 128) {
1682
+ throw new PcxParseError("PCX buffer too small for header");
1683
+ }
1684
+ const view = new DataView(buffer);
1685
+ const manufacturer = view.getUint8(0);
1686
+ const encoding = view.getUint8(2);
1687
+ const bitsPerPixel = view.getUint8(3);
1688
+ const xMin = view.getUint16(4, true);
1689
+ const yMin = view.getUint16(6, true);
1690
+ const xMax = view.getUint16(8, true);
1691
+ const yMax = view.getUint16(10, true);
1692
+ if (manufacturer !== 10 || encoding !== 1) {
1693
+ throw new PcxParseError("Unsupported PCX encoding");
1694
+ }
1695
+ if (bitsPerPixel !== 8) {
1696
+ throw new PcxParseError("Only 8bpp PCX files are supported");
1697
+ }
1698
+ const width = xMax - xMin + 1;
1699
+ const height = yMax - yMin + 1;
1700
+ const bytesPerLine = view.getUint16(66, true);
1701
+ const paletteMarkerOffset = buffer.byteLength - 769;
1702
+ if (paletteMarkerOffset < 128 || new DataView(buffer, paletteMarkerOffset, 1).getUint8(0) !== 12) {
1703
+ throw new PcxParseError("Missing PCX palette");
1704
+ }
1705
+ const palette = new Uint8Array(buffer, paletteMarkerOffset + 1, 768);
1706
+ const encoded = new Uint8Array(buffer, 128, paletteMarkerOffset - 128);
1707
+ const pixels = new Uint8Array(width * height);
1708
+ let srcIndex = 0;
1709
+ let dstIndex = 0;
1710
+ for (let y = 0; y < height; y += 1) {
1711
+ let written = 0;
1712
+ while (written < bytesPerLine && srcIndex < encoded.length) {
1713
+ let count = 1;
1714
+ let value = encoded[srcIndex++];
1715
+ if ((value & 192) === 192) {
1716
+ count = value & 63;
1717
+ if (srcIndex >= encoded.length) {
1718
+ throw new PcxParseError("Unexpected end of PCX RLE data");
1719
+ }
1720
+ value = encoded[srcIndex++];
1721
+ }
1722
+ for (let i = 0; i < count && written < bytesPerLine; i += 1) {
1723
+ if (written < width) {
1724
+ pixels[dstIndex++] = value;
1725
+ }
1726
+ written += 1;
1727
+ }
1728
+ }
1729
+ }
1730
+ return { width, height, bitsPerPixel, pixels, palette };
1731
+ }
1732
+ function pcxToRgba(image) {
1733
+ const rgba = new Uint8Array(image.width * image.height * 4);
1734
+ for (let i = 0; i < image.pixels.length; i += 1) {
1735
+ const colorIndex = image.pixels[i];
1736
+ const paletteIndex = colorIndex * 3;
1737
+ const rgbaIndex = i * 4;
1738
+ rgba[rgbaIndex] = image.palette[paletteIndex];
1739
+ rgba[rgbaIndex + 1] = image.palette[paletteIndex + 1];
1740
+ rgba[rgbaIndex + 2] = image.palette[paletteIndex + 2];
1741
+ rgba[rgbaIndex + 3] = colorIndex === 255 ? 0 : 255;
1742
+ }
1743
+ return rgba;
1744
+ }
1745
+
1746
+ // src/assets/texture.ts
1747
+ var TextureCache = class {
1748
+ constructor(options = {}) {
1749
+ this.cache = new LruCache(options.capacity ?? 128);
1750
+ }
1751
+ get size() {
1752
+ return this.cache.size;
1753
+ }
1754
+ get(key) {
1755
+ return this.cache.get(key.toLowerCase());
1756
+ }
1757
+ set(key, texture) {
1758
+ this.cache.set(key.toLowerCase(), texture);
1759
+ }
1760
+ clear() {
1761
+ this.cache.clear();
1762
+ }
1763
+ };
1764
+ function walToRgba(wal, palette) {
1765
+ const levels = [];
1766
+ for (const mip of wal.mipmaps) {
1767
+ const rgba = new Uint8Array(mip.width * mip.height * 4);
1768
+ for (let i = 0; i < mip.data.length; i += 1) {
1769
+ const colorIndex = mip.data[i];
1770
+ const paletteIndex = colorIndex * 3;
1771
+ const outIndex = i * 4;
1772
+ rgba[outIndex] = palette[paletteIndex];
1773
+ rgba[outIndex + 1] = palette[paletteIndex + 1];
1774
+ rgba[outIndex + 2] = palette[paletteIndex + 2];
1775
+ rgba[outIndex + 3] = colorIndex === 255 ? 0 : 255;
1776
+ }
1777
+ levels.push({ level: mip.level, width: mip.width, height: mip.height, rgba });
1778
+ }
1779
+ return { width: wal.width, height: wal.height, levels, source: "wal" };
1780
+ }
1781
+ function preparePcxTexture(pcx) {
1782
+ const rgba = pcxToRgba(pcx);
1783
+ const level = { level: 0, width: pcx.width, height: pcx.height, rgba };
1784
+ return { width: pcx.width, height: pcx.height, levels: [level], source: "pcx" };
1785
+ }
1786
+ function parseWalTexture(buffer, palette) {
1787
+ return walToRgba(parseWal(buffer), palette);
1788
+ }
1789
+
1790
+ // src/assets/wav.ts
1791
+ var WavParseError = class extends Error {
1792
+ constructor(message) {
1793
+ super(message);
1794
+ this.name = "WavParseError";
1795
+ }
1796
+ };
1797
+ function readString2(view, offset, length) {
1798
+ return new TextDecoder("ascii").decode(new Uint8Array(view.buffer, view.byteOffset + offset, length));
1799
+ }
1800
+ function parseWav(buffer) {
1801
+ if (buffer.byteLength < 44) {
1802
+ throw new WavParseError("WAV buffer too small");
1803
+ }
1804
+ const view = new DataView(buffer);
1805
+ if (readString2(view, 0, 4) !== "RIFF" || readString2(view, 8, 4) !== "WAVE") {
1806
+ throw new WavParseError("Invalid WAV header");
1807
+ }
1808
+ let offset = 12;
1809
+ let fmtOffset = -1;
1810
+ let dataOffset = -1;
1811
+ let fmtSize = 0;
1812
+ let dataSize = 0;
1813
+ while (offset + 8 <= buffer.byteLength) {
1814
+ const chunkId = readString2(view, offset, 4);
1815
+ const chunkSize = view.getUint32(offset + 4, true);
1816
+ const chunkDataOffset = offset + 8;
1817
+ if (chunkId === "fmt ") {
1818
+ fmtOffset = chunkDataOffset;
1819
+ fmtSize = chunkSize;
1820
+ } else if (chunkId === "data") {
1821
+ dataOffset = chunkDataOffset;
1822
+ dataSize = chunkSize;
1823
+ }
1824
+ offset = chunkDataOffset + chunkSize;
1825
+ }
1826
+ if (fmtOffset === -1 || dataOffset === -1) {
1827
+ throw new WavParseError("Missing fmt or data chunk");
1828
+ }
1829
+ const audioFormat = view.getUint16(fmtOffset, true);
1830
+ const channels = view.getUint16(fmtOffset + 2, true);
1831
+ const sampleRate = view.getUint32(fmtOffset + 4, true);
1832
+ const bitsPerSample = view.getUint16(fmtOffset + 14, true);
1833
+ if (audioFormat !== 1) {
1834
+ throw new WavParseError("Only PCM WAV is supported");
1835
+ }
1836
+ const bytesPerSample = bitsPerSample / 8;
1837
+ const frameCount = dataSize / (bytesPerSample * channels);
1838
+ const samples = new Float32Array(frameCount * channels);
1839
+ for (let frame = 0; frame < frameCount; frame += 1) {
1840
+ for (let ch = 0; ch < channels; ch += 1) {
1841
+ const sampleIndex = frame * channels + ch;
1842
+ const byteOffset = dataOffset + sampleIndex * bytesPerSample;
1843
+ let value = 0;
1844
+ if (bitsPerSample === 8) {
1845
+ value = view.getUint8(byteOffset);
1846
+ samples[sampleIndex] = (value - 128) / 128;
1847
+ } else if (bitsPerSample === 16) {
1848
+ value = view.getInt16(byteOffset, true);
1849
+ samples[sampleIndex] = value / 32768;
1850
+ } else if (bitsPerSample === 24) {
1851
+ const b0 = view.getUint8(byteOffset);
1852
+ const b1 = view.getUint8(byteOffset + 1);
1853
+ const b2 = view.getInt8(byteOffset + 2);
1854
+ value = b0 | b1 << 8 | b2 << 16;
1855
+ samples[sampleIndex] = value / 8388608;
1856
+ } else {
1857
+ throw new WavParseError(`Unsupported bitsPerSample: ${bitsPerSample}`);
1858
+ }
1859
+ }
1860
+ }
1861
+ return { sampleRate, channels, bitsPerSample, samples };
1862
+ }
1863
+
1864
+ // src/assets/ogg.ts
1865
+ import { OggVorbisDecoder } from "@wasm-audio-decoders/ogg-vorbis";
1866
+ var OggDecodeError = class extends Error {
1867
+ constructor(message) {
1868
+ super(message);
1869
+ this.name = "OggDecodeError";
1870
+ }
1871
+ };
1872
+ async function decodeOgg(buffer, decoder = new OggVorbisDecoder()) {
1873
+ await decoder.ready;
1874
+ const result = await decoder.decode(new Uint8Array(buffer));
1875
+ const errors = result.errors;
1876
+ if (errors && errors.length > 0) {
1877
+ throw new OggDecodeError(errors.map((err) => err.message).join("; "));
1878
+ }
1879
+ return {
1880
+ sampleRate: result.sampleRate,
1881
+ channels: result.channelData.length,
1882
+ bitDepth: result.bitDepth,
1883
+ channelData: result.channelData
1884
+ };
1885
+ }
1886
+
1887
+ // src/assets/audio.ts
1888
+ var AudioRegistryError = class extends Error {
1889
+ constructor(message) {
1890
+ super(message);
1891
+ this.name = "AudioRegistryError";
1892
+ }
1893
+ };
1894
+ var AudioRegistry = class {
1895
+ constructor(vfs, options = {}) {
1896
+ this.vfs = vfs;
1897
+ this.refCounts = /* @__PURE__ */ new Map();
1898
+ this.cache = new LruCache(options.cacheSize ?? 64);
1899
+ }
1900
+ get size() {
1901
+ return this.cache.size;
1902
+ }
1903
+ async load(path) {
1904
+ const normalized = path.toLowerCase();
1905
+ const cached = this.cache.get(normalized);
1906
+ if (cached) {
1907
+ this.refCounts.set(normalized, (this.refCounts.get(normalized) ?? 0) + 1);
1908
+ return cached;
1909
+ }
1910
+ const data = await this.vfs.readFile(path);
1911
+ const arrayBuffer = data.slice().buffer;
1912
+ const audio = await this.decodeByExtension(path, arrayBuffer);
1913
+ this.cache.set(normalized, audio);
1914
+ this.refCounts.set(normalized, 1);
1915
+ return audio;
1916
+ }
1917
+ release(path) {
1918
+ const normalized = path.toLowerCase();
1919
+ const count = this.refCounts.get(normalized) ?? 0;
1920
+ if (count <= 1) {
1921
+ this.cache.delete(normalized);
1922
+ this.refCounts.delete(normalized);
1923
+ } else {
1924
+ this.refCounts.set(normalized, count - 1);
1925
+ }
1926
+ }
1927
+ clearAll() {
1928
+ this.cache.clear();
1929
+ this.refCounts.clear();
1930
+ }
1931
+ async decodeByExtension(path, buffer) {
1932
+ const lower = path.toLowerCase();
1933
+ if (lower.endsWith(".wav")) {
1934
+ const wav = parseWav(buffer);
1935
+ const channels = wav.channels;
1936
+ const channelData = Array.from({ length: channels }, () => new Float32Array(wav.samples.length / channels));
1937
+ for (let i = 0; i < wav.samples.length; i += 1) {
1938
+ channelData[i % channels][Math.floor(i / channels)] = wav.samples[i];
1939
+ }
1940
+ return { sampleRate: wav.sampleRate, channels, bitDepth: wav.bitsPerSample, channelData };
1941
+ }
1942
+ if (lower.endsWith(".ogg") || lower.endsWith(".oga")) {
1943
+ return decodeOgg(buffer);
1944
+ }
1945
+ throw new AudioRegistryError(`Unsupported audio format: ${path}`);
1946
+ }
1947
+ };
1948
+
1949
+ // src/assets/pakIndexStore.ts
1950
+ var DEFAULT_DB_NAME = "quake2ts-pak-indexes";
1951
+ var DEFAULT_STORE_NAME = "pak-indexes";
1952
+ function getIndexedDb() {
1953
+ if (typeof indexedDB !== "undefined") {
1954
+ return indexedDB;
1955
+ }
1956
+ if (typeof window !== "undefined" && "indexedDB" in window) {
1957
+ return window.indexedDB;
1958
+ }
1959
+ if (typeof globalThis !== "undefined" && "indexedDB" in globalThis) {
1960
+ return globalThis.indexedDB;
1961
+ }
1962
+ return void 0;
1963
+ }
1964
+ function openDatabase(dbName, storeName) {
1965
+ const idb = getIndexedDb();
1966
+ if (!idb) {
1967
+ return Promise.reject(new Error("IndexedDB is not available in this environment"));
1968
+ }
1969
+ return new Promise((resolve, reject) => {
1970
+ const request = idb.open(dbName, 1);
1971
+ request.onupgradeneeded = () => {
1972
+ const { result } = request;
1973
+ if (!result.objectStoreNames.contains(storeName)) {
1974
+ result.createObjectStore(storeName, { keyPath: "key" });
1975
+ }
1976
+ };
1977
+ request.onerror = () => reject(request.error ?? new Error("Unknown IndexedDB error"));
1978
+ request.onsuccess = () => resolve(request.result);
1979
+ });
1980
+ }
1981
+ function runTransaction(db, storeName, mode, runner) {
1982
+ return new Promise((resolve, reject) => {
1983
+ const transaction = db.transaction(storeName, mode);
1984
+ const store = transaction.objectStore(storeName);
1985
+ const request = runner(store);
1986
+ request.onsuccess = () => resolve(request.result);
1987
+ request.onerror = () => reject(request.error ?? new Error("IndexedDB transaction error"));
1988
+ });
1989
+ }
1990
+ function buildKey(name, checksum) {
1991
+ return `${normalizePath(name)}:${checksum.toString(16)}`;
1992
+ }
1993
+ function cloneEntries(entries) {
1994
+ return entries.map((entry) => ({ ...entry }));
1995
+ }
1996
+ var PakIndexStore = class {
1997
+ constructor(dbName = DEFAULT_DB_NAME, storeName = DEFAULT_STORE_NAME) {
1998
+ this.dbName = dbName;
1999
+ this.storeName = storeName;
2000
+ }
2001
+ get isSupported() {
2002
+ return Boolean(getIndexedDb());
2003
+ }
2004
+ async persist(archive) {
2005
+ if (!this.isSupported) {
2006
+ return void 0;
2007
+ }
2008
+ const validation = archive.validate();
2009
+ const record = {
2010
+ ...validation,
2011
+ key: buildKey(archive.name, validation.checksum),
2012
+ name: archive.name,
2013
+ size: archive.size,
2014
+ persistedAt: Date.now(),
2015
+ entries: cloneEntries(validation.entries)
2016
+ };
2017
+ const db = await openDatabase(this.dbName, this.storeName);
2018
+ await runTransaction(db, this.storeName, "readwrite", (store) => store.put(record));
2019
+ db.close();
2020
+ return record;
2021
+ }
2022
+ async find(name, checksum) {
2023
+ if (!this.isSupported) {
2024
+ return void 0;
2025
+ }
2026
+ const db = await openDatabase(this.dbName, this.storeName);
2027
+ const key = checksum !== void 0 ? buildKey(name, checksum) : void 0;
2028
+ const record = await runTransaction(db, this.storeName, "readonly", (store) => {
2029
+ if (key) {
2030
+ return store.get(key);
2031
+ }
2032
+ return store.getAll();
2033
+ });
2034
+ db.close();
2035
+ if (!record) {
2036
+ return void 0;
2037
+ }
2038
+ if (Array.isArray(record)) {
2039
+ const normalized = normalizePath(name);
2040
+ const matches = record.filter((candidate) => normalizePath(candidate.name) === normalized);
2041
+ if (matches.length === 0) {
2042
+ return void 0;
2043
+ }
2044
+ return matches.sort((a, b) => b.persistedAt - a.persistedAt)[0];
2045
+ }
2046
+ return record;
2047
+ }
2048
+ async remove(name, checksum) {
2049
+ if (!this.isSupported) {
2050
+ return false;
2051
+ }
2052
+ const db = await openDatabase(this.dbName, this.storeName);
2053
+ const key = checksum !== void 0 ? buildKey(name, checksum) : void 0;
2054
+ const result = await runTransaction(db, this.storeName, "readwrite", (store) => {
2055
+ if (key) {
2056
+ return store.delete(key);
2057
+ }
2058
+ const prefix = `${normalizePath(name)}:`;
2059
+ return store.delete(IDBKeyRange.bound(prefix, `${prefix}\uFFFF`, false, true));
2060
+ });
2061
+ db.close();
2062
+ return typeof result === "number" ? result > 0 : true;
2063
+ }
2064
+ async clear() {
2065
+ if (!this.isSupported) {
2066
+ return;
2067
+ }
2068
+ const db = await openDatabase(this.dbName, this.storeName);
2069
+ await runTransaction(db, this.storeName, "readwrite", (store) => store.clear());
2070
+ db.close();
2071
+ }
2072
+ async list() {
2073
+ if (!this.isSupported) {
2074
+ return [];
2075
+ }
2076
+ const db = await openDatabase(this.dbName, this.storeName);
2077
+ const result = await runTransaction(db, this.storeName, "readonly", (store) => store.getAll());
2078
+ db.close();
2079
+ return result.sort((a, b) => b.persistedAt - a.persistedAt);
2080
+ }
2081
+ };
2082
+
2083
+ // src/assets/manager.ts
2084
+ var AssetDependencyError = class extends Error {
2085
+ constructor(missing, message) {
2086
+ super(message ?? `Missing dependencies: ${missing.join(", ")}`);
2087
+ this.missing = missing;
2088
+ this.name = "AssetDependencyError";
2089
+ }
2090
+ };
2091
+ var AssetDependencyTracker = class {
2092
+ constructor() {
2093
+ this.nodes = /* @__PURE__ */ new Map();
2094
+ }
2095
+ register(assetKey, dependencies = []) {
2096
+ const node = this.nodes.get(assetKey) ?? { dependencies: /* @__PURE__ */ new Set(), loaded: false };
2097
+ dependencies.forEach((dependency) => node.dependencies.add(dependency));
2098
+ this.nodes.set(assetKey, node);
2099
+ dependencies.forEach((dependency) => {
2100
+ if (!this.nodes.has(dependency)) {
2101
+ this.nodes.set(dependency, { dependencies: /* @__PURE__ */ new Set(), loaded: false });
2102
+ }
2103
+ });
2104
+ }
2105
+ markLoaded(assetKey) {
2106
+ const node = this.nodes.get(assetKey) ?? { dependencies: /* @__PURE__ */ new Set(), loaded: false };
2107
+ const missing = this.getMissingDependencies(assetKey, node);
2108
+ if (missing.length > 0) {
2109
+ throw new AssetDependencyError(missing, `Asset ${assetKey} is missing dependencies: ${missing.join(", ")}`);
2110
+ }
2111
+ node.loaded = true;
2112
+ this.nodes.set(assetKey, node);
2113
+ }
2114
+ markUnloaded(assetKey) {
2115
+ const node = this.nodes.get(assetKey);
2116
+ if (node) {
2117
+ node.loaded = false;
2118
+ }
2119
+ }
2120
+ isLoaded(assetKey) {
2121
+ return this.nodes.get(assetKey)?.loaded ?? false;
2122
+ }
2123
+ missingDependencies(assetKey) {
2124
+ const node = this.nodes.get(assetKey);
2125
+ if (!node) {
2126
+ return [];
2127
+ }
2128
+ return this.getMissingDependencies(assetKey, node);
2129
+ }
2130
+ reset() {
2131
+ this.nodes.clear();
2132
+ }
2133
+ getMissingDependencies(assetKey, node) {
2134
+ const missing = [];
2135
+ for (const dependency of node.dependencies) {
2136
+ if (!this.nodes.get(dependency)?.loaded) {
2137
+ missing.push(dependency);
2138
+ }
2139
+ }
2140
+ return missing;
2141
+ }
2142
+ };
2143
+ var AssetManager = class {
2144
+ constructor(vfs, options = {}) {
2145
+ this.vfs = vfs;
2146
+ this.textures = new TextureCache({ capacity: options.textureCacheCapacity ?? 128 });
2147
+ this.audio = new AudioRegistry(vfs, { cacheSize: options.audioCacheSize ?? 64 });
2148
+ this.dependencyTracker = options.dependencyTracker ?? new AssetDependencyTracker();
2149
+ this.md2 = new Md2Loader(vfs);
2150
+ this.md3 = new Md3Loader(vfs);
2151
+ }
2152
+ isAssetLoaded(type, path) {
2153
+ return this.dependencyTracker.isLoaded(this.makeKey(type, path));
2154
+ }
2155
+ registerTexture(path, texture) {
2156
+ this.textures.set(path, texture);
2157
+ const key = this.makeKey("texture", path);
2158
+ this.dependencyTracker.register(key);
2159
+ this.dependencyTracker.markLoaded(key);
2160
+ }
2161
+ async loadSound(path) {
2162
+ const audio = await this.audio.load(path);
2163
+ const key = this.makeKey("sound", path);
2164
+ this.dependencyTracker.register(key);
2165
+ this.dependencyTracker.markLoaded(key);
2166
+ return audio;
2167
+ }
2168
+ async loadMd2Model(path, textureDependencies = []) {
2169
+ const modelKey = this.makeKey("model", path);
2170
+ const dependencyKeys = textureDependencies.map((dep) => this.makeKey("texture", dep));
2171
+ this.dependencyTracker.register(modelKey, dependencyKeys);
2172
+ const missing = this.dependencyTracker.missingDependencies(modelKey);
2173
+ if (missing.length > 0) {
2174
+ throw new AssetDependencyError(missing, `Asset ${modelKey} is missing dependencies: ${missing.join(", ")}`);
2175
+ }
2176
+ const model = await this.md2.load(path);
2177
+ this.dependencyTracker.markLoaded(modelKey);
2178
+ return model;
2179
+ }
2180
+ async loadMd3Model(path, textureDependencies = []) {
2181
+ const modelKey = this.makeKey("model", path);
2182
+ const dependencyKeys = textureDependencies.map((dep) => this.makeKey("texture", dep));
2183
+ this.dependencyTracker.register(modelKey, dependencyKeys);
2184
+ const missing = this.dependencyTracker.missingDependencies(modelKey);
2185
+ if (missing.length > 0) {
2186
+ throw new AssetDependencyError(missing, `Asset ${modelKey} is missing dependencies: ${missing.join(", ")}`);
2187
+ }
2188
+ const model = await this.md3.load(path);
2189
+ this.dependencyTracker.markLoaded(modelKey);
2190
+ return model;
2191
+ }
2192
+ resetForLevelChange() {
2193
+ this.textures.clear();
2194
+ this.audio.clearAll();
2195
+ this.dependencyTracker.reset();
2196
+ }
2197
+ makeKey(type, path) {
2198
+ return `${type}:${normalizePath(path)}`;
2199
+ }
2200
+ };
2201
+
2202
+ // src/audio/constants.ts
2203
+ var MAX_SOUND_CHANNELS = 32;
2204
+ var SoundChannel = /* @__PURE__ */ ((SoundChannel2) => {
2205
+ SoundChannel2[SoundChannel2["Auto"] = 0] = "Auto";
2206
+ SoundChannel2[SoundChannel2["Weapon"] = 1] = "Weapon";
2207
+ SoundChannel2[SoundChannel2["Voice"] = 2] = "Voice";
2208
+ SoundChannel2[SoundChannel2["Item"] = 3] = "Item";
2209
+ SoundChannel2[SoundChannel2["Body"] = 4] = "Body";
2210
+ SoundChannel2[SoundChannel2["Aux"] = 5] = "Aux";
2211
+ SoundChannel2[SoundChannel2["Footstep"] = 6] = "Footstep";
2212
+ SoundChannel2[SoundChannel2["Aux3"] = 7] = "Aux3";
2213
+ SoundChannel2[SoundChannel2["NoPhsAdd"] = 8] = "NoPhsAdd";
2214
+ SoundChannel2[SoundChannel2["Reliable"] = 16] = "Reliable";
2215
+ SoundChannel2[SoundChannel2["ForcePos"] = 32] = "ForcePos";
2216
+ return SoundChannel2;
2217
+ })(SoundChannel || {});
2218
+ var ATTN_LOOP_NONE = -1;
2219
+ var ATTN_NONE = 0;
2220
+ var ATTN_NORM = 1;
2221
+ var ATTN_IDLE = 2;
2222
+ var ATTN_STATIC = 3;
2223
+ var SOUND_FULLVOLUME = 80;
2224
+ var SOUND_LOOP_ATTENUATE = 3e-3;
2225
+ function attenuationToDistanceMultiplier(attenuation) {
2226
+ return attenuation === ATTN_STATIC ? attenuation * 1e-3 : attenuation * 5e-4;
2227
+ }
2228
+ function calculateMaxAudibleDistance(attenuation) {
2229
+ const distMult = attenuationToDistanceMultiplier(attenuation);
2230
+ return distMult <= 0 ? Number.POSITIVE_INFINITY : SOUND_FULLVOLUME + 1 / distMult;
2231
+ }
2232
+
2233
+ // src/audio/context.ts
2234
+ var AudioContextController = class {
2235
+ constructor(factory) {
2236
+ this.factory = factory;
2237
+ }
2238
+ getContext() {
2239
+ if (!this.context) {
2240
+ this.context = this.factory();
2241
+ }
2242
+ return this.context;
2243
+ }
2244
+ async resume() {
2245
+ const ctx = this.getContext();
2246
+ if (ctx.state === "suspended") {
2247
+ await ctx.resume();
2248
+ }
2249
+ }
2250
+ getState() {
2251
+ return this.context?.state ?? "suspended";
2252
+ }
2253
+ };
2254
+ function createAudioGraph(controller) {
2255
+ const context = controller.getContext();
2256
+ const master = context.createGain();
2257
+ master.gain.value = 1;
2258
+ const compressor = context.createDynamicsCompressor();
2259
+ const filter = context.createBiquadFilter?.();
2260
+ if (filter) {
2261
+ filter.type = "lowpass";
2262
+ filter.frequency.value = 2e4;
2263
+ filter.Q.value = 0.707;
2264
+ master.connect(filter);
2265
+ filter.connect(compressor);
2266
+ } else {
2267
+ master.connect(compressor);
2268
+ }
2269
+ compressor.connect(context.destination);
2270
+ return { context, master, compressor, filter };
2271
+ }
2272
+
2273
+ // src/audio/registry.ts
2274
+ var SoundRegistry = class {
2275
+ constructor(configStrings = new ConfigStringRegistry()) {
2276
+ this.configStrings = configStrings;
2277
+ this.buffers = /* @__PURE__ */ new Map();
2278
+ }
2279
+ registerName(name) {
2280
+ return this.configStrings.soundIndex(name);
2281
+ }
2282
+ register(name, buffer) {
2283
+ const index = this.registerName(name);
2284
+ this.buffers.set(index, buffer);
2285
+ return index;
2286
+ }
2287
+ find(name) {
2288
+ return this.configStrings.findSoundIndex(name);
2289
+ }
2290
+ get(index) {
2291
+ return this.buffers.get(index);
2292
+ }
2293
+ has(index) {
2294
+ return this.buffers.has(index);
2295
+ }
2296
+ };
2297
+
2298
+ // src/audio/precache.ts
2299
+ var SoundPrecache = class {
2300
+ constructor(options) {
2301
+ this.vfs = options.vfs;
2302
+ this.registry = options.registry;
2303
+ this.contextController = options.context;
2304
+ this.soundRoot = options.soundRoot ?? "sound/";
2305
+ this.decodeAudio = options.decodeAudio ?? ((context, data) => {
2306
+ if (!context.decodeAudioData) {
2307
+ throw new Error("decodeAudioData is not available on the provided audio context");
2308
+ }
2309
+ return context.decodeAudioData(data);
2310
+ });
2311
+ }
2312
+ async precache(paths) {
2313
+ const unique = [...new Set(paths.map((p) => this.normalize(p)))];
2314
+ const report = { loaded: [], skipped: [], missing: [], errors: {} };
2315
+ const context = this.contextController.getContext();
2316
+ for (const path of unique) {
2317
+ try {
2318
+ const existingIndex = this.registry.find(path);
2319
+ if (existingIndex !== void 0 && this.registry.has(existingIndex)) {
2320
+ report.skipped.push(path);
2321
+ continue;
2322
+ }
2323
+ const stat = this.vfs.stat(path);
2324
+ if (!stat) {
2325
+ report.missing.push(path);
2326
+ continue;
2327
+ }
2328
+ const bytes = await this.vfs.readFile(path);
2329
+ const copy = bytes.slice().buffer;
2330
+ const buffer = await this.decodeAudio(context, copy);
2331
+ this.registry.register(path, buffer);
2332
+ report.loaded.push(path);
2333
+ } catch (error) {
2334
+ const err = error instanceof Error ? error : new Error(String(error));
2335
+ report.errors[path] = err;
2336
+ }
2337
+ }
2338
+ return report;
2339
+ }
2340
+ normalize(path) {
2341
+ const normalized = normalizePath(path.replace(/^\//, ""));
2342
+ if (normalized.startsWith(this.soundRoot)) {
2343
+ return normalized;
2344
+ }
2345
+ return normalizePath(`${this.soundRoot}${normalized}`);
2346
+ }
2347
+ };
2348
+
2349
+ // src/audio/channels.ts
2350
+ var CHANNEL_MASK = 7;
2351
+ var baseChannel = (entchannel) => entchannel & CHANNEL_MASK;
2352
+ function createInitialChannels(playerEntity) {
2353
+ return Array.from({ length: MAX_SOUND_CHANNELS }, () => ({
2354
+ entnum: 0,
2355
+ entchannel: 0 /* Auto */,
2356
+ endTimeMs: 0,
2357
+ isPlayer: false,
2358
+ active: false
2359
+ })).map((channel) => ({ ...channel, isPlayer: channel.entnum === playerEntity }));
2360
+ }
2361
+ function pickChannel(channels, entnum, entchannel, context) {
2362
+ if (entchannel < 0) {
2363
+ throw new Error("pickChannel: entchannel must be non-negative");
2364
+ }
2365
+ const normalizedEntchannel = baseChannel(entchannel);
2366
+ let firstToDie = -1;
2367
+ let lifeLeft = Number.POSITIVE_INFINITY;
2368
+ for (let i = 0; i < channels.length; i += 1) {
2369
+ const channel = channels[i];
2370
+ const channelBase = baseChannel(channel.entchannel);
2371
+ if (normalizedEntchannel !== 0 /* Auto */ && channel.entnum === entnum && channelBase === normalizedEntchannel) {
2372
+ firstToDie = i;
2373
+ break;
2374
+ }
2375
+ if (channel.active && channel.entnum === context.playerEntity && entnum !== context.playerEntity) {
2376
+ continue;
2377
+ }
2378
+ const remainingLife = channel.endTimeMs - context.nowMs;
2379
+ if (firstToDie === -1 || remainingLife < lifeLeft) {
2380
+ lifeLeft = remainingLife;
2381
+ firstToDie = i;
2382
+ }
2383
+ }
2384
+ return firstToDie === -1 ? void 0 : firstToDie;
2385
+ }
2386
+
2387
+ // src/audio/spatialization.ts
2388
+ function spatializeOrigin(origin, listener, masterVolume, attenuation, isListenerSound) {
2389
+ if (isListenerSound) {
2390
+ return { left: masterVolume, right: masterVolume, distanceComponent: 0 };
2391
+ }
2392
+ const sourceVec = subtractVec3(origin, listener.origin);
2393
+ const distance = lengthVec3(sourceVec);
2394
+ const normalized = normalizeVec3(sourceVec);
2395
+ let dist = distance - SOUND_FULLVOLUME;
2396
+ if (dist < 0) dist = 0;
2397
+ dist *= attenuationToDistanceMultiplier(attenuation);
2398
+ const dot = dotVec3(listener.right, normalized);
2399
+ const mono = listener.mono ?? false;
2400
+ const rscale = mono || attenuation === 0 ? 1 : 0.5 * (1 + dot);
2401
+ const lscale = mono || attenuation === 0 ? 1 : 0.5 * (1 - dot);
2402
+ const right = Math.max(0, Math.floor(masterVolume * (1 - dist) * rscale));
2403
+ const left = Math.max(0, Math.floor(masterVolume * (1 - dist) * lscale));
2404
+ return { left, right, distanceComponent: dist };
2405
+ }
2406
+
2407
+ // src/audio/system.ts
2408
+ var AudioSystem = class {
2409
+ constructor(options) {
2410
+ this.activeSources = /* @__PURE__ */ new Map();
2411
+ this.contextController = options.context;
2412
+ this.registry = options.registry;
2413
+ this.playerEntity = options.playerEntity;
2414
+ this.channels = createInitialChannels(options.playerEntity);
2415
+ this.listener = options.listener ?? { origin: ZERO_VEC3, right: { x: 1, y: 0, z: 0 } };
2416
+ this.sfxVolume = options.sfxVolume ?? 1;
2417
+ this.masterVolume = options.masterVolume ?? 1;
2418
+ this.resolveOcclusion = options.resolveOcclusion;
2419
+ this.graph = createAudioGraph(this.contextController);
2420
+ this.graph.master.gain.value = this.masterVolume;
2421
+ }
2422
+ setListener(listener) {
2423
+ this.listener = listener;
2424
+ }
2425
+ setMasterVolume(volume) {
2426
+ this.masterVolume = volume;
2427
+ this.graph.master.gain.value = volume;
2428
+ }
2429
+ setSfxVolume(volume) {
2430
+ this.sfxVolume = volume;
2431
+ }
2432
+ async ensureRunning() {
2433
+ await this.contextController.resume();
2434
+ }
2435
+ play(request) {
2436
+ const buffer = this.registry.get(request.soundIndex);
2437
+ if (!buffer) return void 0;
2438
+ const ctx = this.graph.context;
2439
+ const nowMs = ctx.currentTime * 1e3;
2440
+ const channelIndex = pickChannel(this.channels, request.entity, request.channel, {
2441
+ nowMs,
2442
+ playerEntity: this.playerEntity
2443
+ });
2444
+ if (channelIndex === void 0) return void 0;
2445
+ const existing = this.activeSources.get(channelIndex);
2446
+ if (existing) {
2447
+ existing.source.onended = null;
2448
+ existing.source.stop();
2449
+ this.activeSources.delete(channelIndex);
2450
+ }
2451
+ const source = ctx.createBufferSource();
2452
+ source.buffer = buffer;
2453
+ source.loop = request.looping ?? false;
2454
+ const origin = request.origin ?? this.listener.origin;
2455
+ const gain = ctx.createGain();
2456
+ const panner = this.createPanner(ctx, request.attenuation);
2457
+ const occlusion = this.resolveOcclusion?.(this.listener, origin, request.attenuation);
2458
+ const occlusionScale = clamp01(occlusion?.gainScale ?? 1);
2459
+ const occlusionFilter = this.resolveOcclusion ? this.createOcclusionFilter(ctx, occlusion?.lowpassHz ?? 2e4) : void 0;
2460
+ this.applyOriginToPanner(panner, origin);
2461
+ const isListenerSound = request.entity === this.playerEntity;
2462
+ const spatial = spatializeOrigin(origin, this.listener, request.volume, request.attenuation, isListenerSound);
2463
+ const attenuationScale = request.volume === 0 ? 0 : Math.max(spatial.left, spatial.right) / Math.max(1, request.volume);
2464
+ const gainValue = attenuationScale * (request.volume / 255) * this.masterVolume * this.sfxVolume;
2465
+ gain.gain.value = gainValue * occlusionScale;
2466
+ const startTimeSec = ctx.currentTime + (request.timeOffsetMs ?? 0) / 1e3;
2467
+ const endTimeMs = (request.looping ? Number.POSITIVE_INFINITY : buffer.duration * 1e3) + startTimeSec * 1e3;
2468
+ source.connect(panner);
2469
+ if (occlusionFilter) {
2470
+ panner.connect(occlusionFilter);
2471
+ occlusionFilter.connect(gain);
2472
+ } else {
2473
+ panner.connect(gain);
2474
+ }
2475
+ gain.connect(this.graph.master);
2476
+ source.start(startTimeSec);
2477
+ source.onended = () => {
2478
+ this.channels[channelIndex].active = false;
2479
+ this.activeSources.delete(channelIndex);
2480
+ };
2481
+ const active = {
2482
+ channelIndex,
2483
+ entnum: request.entity,
2484
+ entchannel: baseChannel(request.channel),
2485
+ endTimeMs,
2486
+ source,
2487
+ panner,
2488
+ gain,
2489
+ baseGain: gainValue,
2490
+ origin,
2491
+ attenuation: request.attenuation,
2492
+ occlusion: occlusionFilter ? { scale: occlusionScale, lowpassHz: occlusion?.lowpassHz, filter: occlusionFilter } : occlusion ? { scale: occlusionScale, lowpassHz: occlusion.lowpassHz } : void 0
2493
+ };
2494
+ this.channels[channelIndex] = {
2495
+ entnum: request.entity,
2496
+ entchannel: baseChannel(request.channel),
2497
+ endTimeMs,
2498
+ isPlayer: request.entity === this.playerEntity,
2499
+ active: true
2500
+ };
2501
+ this.activeSources.set(channelIndex, active);
2502
+ return active;
2503
+ }
2504
+ stop(channelIndex) {
2505
+ const active = this.activeSources.get(channelIndex);
2506
+ if (!active) return;
2507
+ active.source.stop();
2508
+ this.channels[channelIndex].active = false;
2509
+ this.activeSources.delete(channelIndex);
2510
+ }
2511
+ stopEntitySounds(entnum) {
2512
+ for (const [index, active] of [...this.activeSources.entries()]) {
2513
+ if (active.entnum !== entnum) continue;
2514
+ active.source.stop();
2515
+ this.channels[index].active = false;
2516
+ this.activeSources.delete(index);
2517
+ }
2518
+ }
2519
+ updateEntityPosition(entnum, origin) {
2520
+ for (const active of this.activeSources.values()) {
2521
+ if (active.entnum !== entnum) continue;
2522
+ this.applyOriginToPanner(active.panner, origin);
2523
+ active.origin = origin;
2524
+ if (this.resolveOcclusion) {
2525
+ const occlusion = this.resolveOcclusion(this.listener, origin, active.attenuation);
2526
+ this.applyOcclusion(active, occlusion);
2527
+ }
2528
+ }
2529
+ }
2530
+ positionedSound(origin, soundIndex, volume, attenuation) {
2531
+ return this.play({
2532
+ entity: 0,
2533
+ channel: 0 /* Auto */,
2534
+ soundIndex,
2535
+ volume,
2536
+ attenuation,
2537
+ origin
2538
+ });
2539
+ }
2540
+ ambientSound(origin, soundIndex, volume) {
2541
+ return this.play({
2542
+ entity: 0,
2543
+ channel: 0 /* Auto */,
2544
+ soundIndex,
2545
+ volume,
2546
+ attenuation: ATTN_NONE,
2547
+ origin,
2548
+ looping: true
2549
+ });
2550
+ }
2551
+ getChannelState(index) {
2552
+ return this.channels[index];
2553
+ }
2554
+ getDiagnostics() {
2555
+ return {
2556
+ activeChannels: this.activeSources.size,
2557
+ masterVolume: this.masterVolume,
2558
+ sfxVolume: this.sfxVolume,
2559
+ channels: [...this.channels],
2560
+ activeSounds: [...this.activeSources.values()].map((sound) => ({
2561
+ entnum: sound.entnum,
2562
+ entchannel: sound.entchannel,
2563
+ channelIndex: sound.channelIndex,
2564
+ origin: sound.origin,
2565
+ gain: sound.gain.gain.value,
2566
+ baseGain: sound.baseGain,
2567
+ attenuation: sound.attenuation,
2568
+ maxDistance: sound.panner.maxDistance,
2569
+ distanceModel: sound.panner.distanceModel,
2570
+ occlusion: sound.occlusion ? { scale: sound.occlusion.scale, lowpassHz: sound.occlusion.lowpassHz } : void 0
2571
+ }))
2572
+ };
2573
+ }
2574
+ setUnderwater(enabled, cutoffHz = 400) {
2575
+ const filter = this.graph.filter;
2576
+ if (!filter) return;
2577
+ filter.type = "lowpass";
2578
+ filter.Q.value = 0.707;
2579
+ filter.frequency.value = enabled ? cutoffHz : 2e4;
2580
+ }
2581
+ createPanner(context, attenuation) {
2582
+ const panner = context.createPanner ? context.createPanner() : Object.assign(context.createGain(), {
2583
+ positionX: { value: this.listener.origin.x },
2584
+ positionY: { value: this.listener.origin.y },
2585
+ positionZ: { value: this.listener.origin.z }
2586
+ });
2587
+ return this.configurePanner(panner, attenuation);
2588
+ }
2589
+ configurePanner(panner, attenuation) {
2590
+ const distMult = attenuationToDistanceMultiplier(attenuation);
2591
+ panner.refDistance = SOUND_FULLVOLUME;
2592
+ panner.maxDistance = calculateMaxAudibleDistance(attenuation);
2593
+ panner.rolloffFactor = distMult;
2594
+ panner.distanceModel = attenuation === 0 ? "linear" : "inverse";
2595
+ panner.positionX.value = this.listener.origin.x;
2596
+ panner.positionY.value = this.listener.origin.y;
2597
+ panner.positionZ.value = this.listener.origin.z;
2598
+ return panner;
2599
+ }
2600
+ applyOriginToPanner(panner, origin) {
2601
+ panner.positionX.value = origin.x;
2602
+ panner.positionY.value = origin.y;
2603
+ panner.positionZ.value = origin.z;
2604
+ }
2605
+ createOcclusionFilter(context, cutoffHz) {
2606
+ if (!context.createBiquadFilter) return void 0;
2607
+ const filter = context.createBiquadFilter();
2608
+ filter.type = "lowpass";
2609
+ filter.Q.value = 0.707;
2610
+ filter.frequency.value = clamp(cutoffHz, 10, 2e4);
2611
+ return filter;
2612
+ }
2613
+ applyOcclusion(active, occlusion) {
2614
+ const scale = clamp01(occlusion?.gainScale ?? 1);
2615
+ active.gain.gain.value = active.baseGain * scale;
2616
+ if (active.occlusion?.filter) {
2617
+ const cutoff = occlusion?.lowpassHz ?? 2e4;
2618
+ active.occlusion.filter.frequency.value = clamp(cutoff, 10, 2e4);
2619
+ }
2620
+ if (active.occlusion) {
2621
+ active.occlusion.scale = scale;
2622
+ active.occlusion.lowpassHz = occlusion?.lowpassHz;
2623
+ } else if (occlusion) {
2624
+ active.occlusion = { scale, lowpassHz: occlusion.lowpassHz };
2625
+ }
2626
+ }
2627
+ };
2628
+ var clamp = (value, min, max) => Math.min(max, Math.max(min, value));
2629
+ var clamp01 = (value) => clamp(value, 0, 1);
2630
+
2631
+ // src/audio/music.ts
2632
+ var MusicSystem = class {
2633
+ constructor(options) {
2634
+ this.createElement = options.createElement;
2635
+ this.resolveSource = options.resolveSource ?? (async (path) => path);
2636
+ this.volume = options.volume ?? 1;
2637
+ }
2638
+ async play(track, { loop = true, restart = false } = {}) {
2639
+ if (this.track === track && this.element) {
2640
+ this.element.loop = loop;
2641
+ this.element.volume = this.volume;
2642
+ if (restart) {
2643
+ this.element.currentTime = 0;
2644
+ }
2645
+ if (this.element.paused || restart) {
2646
+ await this.element.play();
2647
+ }
2648
+ return;
2649
+ }
2650
+ const src = await this.resolveSource(track);
2651
+ const element = this.createElement();
2652
+ element.src = src;
2653
+ element.loop = loop;
2654
+ element.volume = this.volume;
2655
+ element.currentTime = 0;
2656
+ element.load();
2657
+ await element.play();
2658
+ this.element = element;
2659
+ this.track = track;
2660
+ }
2661
+ pause() {
2662
+ if (!this.element || this.element.paused) return;
2663
+ this.element.pause();
2664
+ }
2665
+ async resume() {
2666
+ if (!this.element || !this.element.paused) return;
2667
+ await this.element.play();
2668
+ }
2669
+ stop() {
2670
+ if (!this.element) return;
2671
+ this.element.pause();
2672
+ this.element.currentTime = 0;
2673
+ this.element = void 0;
2674
+ this.track = void 0;
2675
+ }
2676
+ setVolume(volume) {
2677
+ this.volume = volume;
2678
+ if (this.element) {
2679
+ this.element.volume = volume;
2680
+ }
2681
+ }
2682
+ getState() {
2683
+ const playing = Boolean(this.element && !this.element.paused && !this.element.ended);
2684
+ const paused = Boolean(this.element?.paused);
2685
+ return { track: this.track, paused, playing, volume: this.volume };
2686
+ }
2687
+ };
2688
+
2689
+ // src/audio/api.ts
2690
+ var AudioApi = class {
2691
+ constructor(options) {
2692
+ this.registry = options.registry;
2693
+ this.system = options.system;
2694
+ this.music = options.music;
2695
+ }
2696
+ soundindex(name) {
2697
+ return this.registry.registerName(name);
2698
+ }
2699
+ sound(entity, channel, soundindex, volume, attenuation, timeofs) {
2700
+ this.system.play({
2701
+ entity,
2702
+ channel,
2703
+ soundIndex: soundindex,
2704
+ volume,
2705
+ attenuation,
2706
+ timeOffsetMs: timeofs
2707
+ });
2708
+ }
2709
+ positioned_sound(origin, soundindex, volume, attenuation) {
2710
+ this.system.positionedSound(origin, soundindex, volume, attenuation);
2711
+ }
2712
+ loop_sound(entity, channel, soundindex, volume, attenuation) {
2713
+ this.system.play({
2714
+ entity,
2715
+ channel,
2716
+ soundIndex: soundindex,
2717
+ volume,
2718
+ attenuation,
2719
+ looping: true
2720
+ });
2721
+ }
2722
+ stop_entity_sounds(entnum) {
2723
+ this.system.stopEntitySounds(entnum);
2724
+ }
2725
+ set_listener(listener) {
2726
+ this.system.setListener(listener);
2727
+ }
2728
+ play_music(track, loop = true) {
2729
+ if (!this.music) {
2730
+ return Promise.resolve();
2731
+ }
2732
+ return this.music.play(track, { loop });
2733
+ }
2734
+ pause_music() {
2735
+ this.music?.pause();
2736
+ }
2737
+ resume_music() {
2738
+ return this.music?.resume() ?? Promise.resolve();
2739
+ }
2740
+ stop_music() {
2741
+ this.music?.stop();
2742
+ }
2743
+ set_music_volume(volume) {
2744
+ this.music?.setVolume(volume);
2745
+ }
2746
+ play_ambient(origin, soundindex, volume) {
2747
+ this.system.ambientSound(origin, soundindex, volume);
2748
+ }
2749
+ play_channel(request) {
2750
+ this.system.play({ ...request });
2751
+ }
2752
+ };
2753
+
1243
2754
  // src/render/context.ts
1244
2755
  function configureDefaultGLState(gl) {
1245
2756
  gl.enable(gl.DEPTH_TEST);
@@ -1501,6 +3012,43 @@ var Texture2D = class {
1501
3012
  this.gl.deleteTexture(this.texture);
1502
3013
  }
1503
3014
  };
3015
+ var TextureCubeMap = class {
3016
+ constructor(gl) {
3017
+ this.gl = gl;
3018
+ this.target = gl.TEXTURE_CUBE_MAP;
3019
+ const texture = gl.createTexture();
3020
+ if (!texture) {
3021
+ throw new Error("Failed to allocate cubemap texture");
3022
+ }
3023
+ this.texture = texture;
3024
+ }
3025
+ bind(unit = 0) {
3026
+ this.gl.activeTexture(this.gl.TEXTURE0 + unit);
3027
+ this.gl.bindTexture(this.target, this.texture);
3028
+ }
3029
+ setParameters(params) {
3030
+ this.bind();
3031
+ if (params.wrapS !== void 0) {
3032
+ this.gl.texParameteri(this.target, this.gl.TEXTURE_WRAP_S, params.wrapS);
3033
+ }
3034
+ if (params.wrapT !== void 0) {
3035
+ this.gl.texParameteri(this.target, this.gl.TEXTURE_WRAP_T, params.wrapT);
3036
+ }
3037
+ if (params.minFilter !== void 0) {
3038
+ this.gl.texParameteri(this.target, this.gl.TEXTURE_MIN_FILTER, params.minFilter);
3039
+ }
3040
+ if (params.magFilter !== void 0) {
3041
+ this.gl.texParameteri(this.target, this.gl.TEXTURE_MAG_FILTER, params.magFilter);
3042
+ }
3043
+ }
3044
+ uploadFace(faceTarget, level, internalFormat, width, height, border, format, type, data) {
3045
+ this.bind();
3046
+ this.gl.texImage2D(faceTarget, level, internalFormat, width, height, border, format, type, data);
3047
+ }
3048
+ dispose() {
3049
+ this.gl.deleteTexture(this.texture);
3050
+ }
3051
+ };
1504
3052
  var Framebuffer = class {
1505
3053
  constructor(gl) {
1506
3054
  this.gl = gl;
@@ -1713,6 +3261,728 @@ function buildBspGeometry(gl, surfaces, options = {}) {
1713
3261
  return { surfaces: results, lightmaps };
1714
3262
  }
1715
3263
 
3264
+ // src/render/culling.ts
3265
+ function normalizePlane(plane) {
3266
+ const { normal, distance } = plane;
3267
+ const length = Math.sqrt(normal.x * normal.x + normal.y * normal.y + normal.z * normal.z);
3268
+ if (length === 0) {
3269
+ return plane;
3270
+ }
3271
+ const inv = 1 / length;
3272
+ return {
3273
+ normal: { x: normal.x * inv, y: normal.y * inv, z: normal.z * inv },
3274
+ distance: distance * inv
3275
+ };
3276
+ }
3277
+ function extractFrustumPlanes(matrix) {
3278
+ if (matrix.length !== 16) {
3279
+ throw new Error("View-projection matrix must contain 16 elements");
3280
+ }
3281
+ const m00 = matrix[0];
3282
+ const m01 = matrix[4];
3283
+ const m02 = matrix[8];
3284
+ const m03 = matrix[12];
3285
+ const m10 = matrix[1];
3286
+ const m11 = matrix[5];
3287
+ const m12 = matrix[9];
3288
+ const m13 = matrix[13];
3289
+ const m20 = matrix[2];
3290
+ const m21 = matrix[6];
3291
+ const m22 = matrix[10];
3292
+ const m23 = matrix[14];
3293
+ const m30 = matrix[3];
3294
+ const m31 = matrix[7];
3295
+ const m32 = matrix[11];
3296
+ const m33 = matrix[15];
3297
+ const planes = [
3298
+ // Left
3299
+ normalizePlane({ normal: { x: m30 + m00, y: m31 + m01, z: m32 + m02 }, distance: m33 + m03 }),
3300
+ // Right
3301
+ normalizePlane({ normal: { x: m30 - m00, y: m31 - m01, z: m32 - m02 }, distance: m33 - m03 }),
3302
+ // Bottom
3303
+ normalizePlane({ normal: { x: m30 + m10, y: m31 + m11, z: m32 + m12 }, distance: m33 + m13 }),
3304
+ // Top
3305
+ normalizePlane({ normal: { x: m30 - m10, y: m31 - m11, z: m32 - m12 }, distance: m33 - m13 }),
3306
+ // Near
3307
+ normalizePlane({ normal: { x: m30 + m20, y: m31 + m21, z: m32 + m22 }, distance: m33 + m23 }),
3308
+ // Far
3309
+ normalizePlane({ normal: { x: m30 - m20, y: m31 - m21, z: m32 - m22 }, distance: m33 - m23 })
3310
+ ];
3311
+ return planes;
3312
+ }
3313
+ function planeDistance(plane, point) {
3314
+ return plane.normal.x * point.x + plane.normal.y * point.y + plane.normal.z * point.z + plane.distance;
3315
+ }
3316
+ function boxIntersectsFrustum(mins, maxs, planes) {
3317
+ for (const plane of planes) {
3318
+ const x = plane.normal.x >= 0 ? maxs.x : mins.x;
3319
+ const y = plane.normal.y >= 0 ? maxs.y : mins.y;
3320
+ const z = plane.normal.z >= 0 ? maxs.z : mins.z;
3321
+ if (planeDistance(plane, { x, y, z }) < 0) {
3322
+ return false;
3323
+ }
3324
+ }
3325
+ return true;
3326
+ }
3327
+
3328
+ // src/render/bspTraversal.ts
3329
+ function childIsLeaf(index) {
3330
+ return index < 0;
3331
+ }
3332
+ function childLeafIndex(index) {
3333
+ return -index - 1;
3334
+ }
3335
+ function distanceToPlane(plane, point) {
3336
+ return plane.normal[0] * point.x + plane.normal[1] * point.y + plane.normal[2] * point.z - plane.dist;
3337
+ }
3338
+ function isClusterVisible(visibility, fromCluster, testCluster) {
3339
+ if (!visibility) {
3340
+ return true;
3341
+ }
3342
+ if (fromCluster < 0 || testCluster < 0) {
3343
+ return true;
3344
+ }
3345
+ const rowBytes = Math.ceil(visibility.numClusters / 8);
3346
+ const row = visibility.clusters[fromCluster].pvs;
3347
+ const byteIndex = Math.floor(testCluster / 8);
3348
+ const bit = 1 << testCluster % 8;
3349
+ if (byteIndex < 0 || byteIndex >= rowBytes) {
3350
+ return false;
3351
+ }
3352
+ return (row[byteIndex] & bit) !== 0;
3353
+ }
3354
+ function leafIntersectsFrustum(leaf, planes) {
3355
+ const mins = { x: leaf.mins[0], y: leaf.mins[1], z: leaf.mins[2] };
3356
+ const maxs = { x: leaf.maxs[0], y: leaf.maxs[1], z: leaf.maxs[2] };
3357
+ return boxIntersectsFrustum(mins, maxs, planes);
3358
+ }
3359
+ function findLeafForPoint(map, point) {
3360
+ let nodeIndex = 0;
3361
+ while (nodeIndex >= 0) {
3362
+ const node = map.nodes[nodeIndex];
3363
+ const plane = map.planes[node.planeIndex];
3364
+ const dist = distanceToPlane(plane, point);
3365
+ const side = dist >= 0 ? 0 : 1;
3366
+ const child = node.children[side];
3367
+ if (childIsLeaf(child)) {
3368
+ return childLeafIndex(child);
3369
+ }
3370
+ nodeIndex = child;
3371
+ }
3372
+ return -1;
3373
+ }
3374
+ function collectFacesFromLeaf(map, leafIndex) {
3375
+ const leaf = map.leafs[leafIndex];
3376
+ const faces = [];
3377
+ for (let i = 0; i < leaf.numLeafFaces; i += 1) {
3378
+ faces.push(map.leafLists.leafFaces[leafIndex][i]);
3379
+ }
3380
+ return faces;
3381
+ }
3382
+ function traverse(map, nodeIndex, camera, frustum, viewCluster, visibleFaces, visitedFaces) {
3383
+ if (childIsLeaf(nodeIndex)) {
3384
+ const leafIndex = childLeafIndex(nodeIndex);
3385
+ const leaf = map.leafs[leafIndex];
3386
+ if (!isClusterVisible(map.visibility, viewCluster, leaf.cluster)) {
3387
+ return;
3388
+ }
3389
+ if (!leafIntersectsFrustum(leaf, frustum)) {
3390
+ return;
3391
+ }
3392
+ const center = {
3393
+ x: (leaf.mins[0] + leaf.maxs[0]) * 0.5,
3394
+ y: (leaf.mins[1] + leaf.maxs[1]) * 0.5,
3395
+ z: (leaf.mins[2] + leaf.maxs[2]) * 0.5
3396
+ };
3397
+ const dx = center.x - camera.x;
3398
+ const dy = center.y - camera.y;
3399
+ const dz = center.z - camera.z;
3400
+ const leafSortKey = -(dx * dx + dy * dy + dz * dz);
3401
+ for (const faceIndex of collectFacesFromLeaf(map, leafIndex)) {
3402
+ if (visitedFaces.has(faceIndex)) {
3403
+ continue;
3404
+ }
3405
+ visitedFaces.add(faceIndex);
3406
+ visibleFaces.push({ faceIndex, leafIndex, sortKey: leafSortKey });
3407
+ }
3408
+ return;
3409
+ }
3410
+ const node = map.nodes[nodeIndex];
3411
+ const plane = map.planes[node.planeIndex];
3412
+ const dist = distanceToPlane(plane, camera);
3413
+ const nearChild = dist >= 0 ? node.children[0] : node.children[1];
3414
+ const farChild = dist >= 0 ? node.children[1] : node.children[0];
3415
+ if (boxIntersectsFrustum(
3416
+ { x: node.mins[0], y: node.mins[1], z: node.mins[2] },
3417
+ { x: node.maxs[0], y: node.maxs[1], z: node.maxs[2] },
3418
+ frustum
3419
+ )) {
3420
+ traverse(map, nearChild, camera, frustum, viewCluster, visibleFaces, visitedFaces);
3421
+ traverse(map, farChild, camera, frustum, viewCluster, visibleFaces, visitedFaces);
3422
+ }
3423
+ }
3424
+ function gatherVisibleFaces(map, cameraPosition, frustum) {
3425
+ const viewLeaf = findLeafForPoint(map, cameraPosition);
3426
+ const viewCluster = viewLeaf >= 0 ? map.leafs[viewLeaf].cluster : -1;
3427
+ const visibleFaces = [];
3428
+ const visitedFaces = /* @__PURE__ */ new Set();
3429
+ traverse(map, 0, cameraPosition, frustum, viewCluster, visibleFaces, visitedFaces);
3430
+ return visibleFaces;
3431
+ }
3432
+
3433
+ // src/render/bspPipeline.ts
3434
+ var BSP_SURFACE_VERTEX_SOURCE = `#version 300 es
3435
+ precision highp float;
3436
+
3437
+ layout(location = 0) in vec3 a_position;
3438
+ layout(location = 1) in vec2 a_texCoord;
3439
+ layout(location = 2) in vec2 a_lightmapCoord;
3440
+
3441
+ uniform mat4 u_modelViewProjection;
3442
+ uniform vec2 u_texScroll;
3443
+ uniform vec2 u_lightmapScroll;
3444
+
3445
+ out vec2 v_texCoord;
3446
+ out vec2 v_lightmapCoord;
3447
+
3448
+ vec2 applyScroll(vec2 uv, vec2 scroll) {
3449
+ return uv + scroll;
3450
+ }
3451
+
3452
+ void main() {
3453
+ v_texCoord = applyScroll(a_texCoord, u_texScroll);
3454
+ v_lightmapCoord = applyScroll(a_lightmapCoord, u_lightmapScroll);
3455
+ gl_Position = u_modelViewProjection * vec4(a_position, 1.0);
3456
+ }`;
3457
+ var BSP_SURFACE_FRAGMENT_SOURCE = `#version 300 es
3458
+ precision highp float;
3459
+
3460
+ in vec2 v_texCoord;
3461
+ in vec2 v_lightmapCoord;
3462
+
3463
+ uniform sampler2D u_diffuseMap;
3464
+ uniform sampler2D u_lightmapAtlas;
3465
+ uniform vec4 u_lightStyleFactors;
3466
+ uniform float u_alpha;
3467
+ uniform bool u_applyLightmap;
3468
+ uniform bool u_warp;
3469
+ uniform float u_time;
3470
+
3471
+ out vec4 o_color;
3472
+
3473
+ vec2 warpCoords(vec2 uv) {
3474
+ // Quake II warp applies a subtle sinusoidal offset; we mirror the rerelease scale.
3475
+ if (!u_warp) {
3476
+ return uv;
3477
+ }
3478
+ float s = uv.x + sin(uv.y * 0.125 + u_time) * 0.125;
3479
+ float t = uv.y + sin(uv.x * 0.125 + u_time) * 0.125;
3480
+ return vec2(s, t);
3481
+ }
3482
+
3483
+ void main() {
3484
+ vec2 warpedTex = warpCoords(v_texCoord);
3485
+ vec4 base = texture(u_diffuseMap, warpedTex);
3486
+
3487
+ if (u_applyLightmap) {
3488
+ vec3 light = texture(u_lightmapAtlas, warpCoords(v_lightmapCoord)).rgb;
3489
+ float styleScale = dot(u_lightStyleFactors, vec4(1.0));
3490
+ base.rgb *= light * styleScale;
3491
+ }
3492
+
3493
+ o_color = vec4(base.rgb, base.a * u_alpha);
3494
+ }`;
3495
+ var DEFAULT_STYLE_INDICES = [0, 255, 255, 255];
3496
+ function resolveLightStyles(styleIndices = DEFAULT_STYLE_INDICES, styleValues = []) {
3497
+ const factors = new Float32Array(4);
3498
+ for (let i = 0; i < 4; i += 1) {
3499
+ const styleIndex = styleIndices[i] ?? 255;
3500
+ if (styleIndex === 255) {
3501
+ factors[i] = 0;
3502
+ continue;
3503
+ }
3504
+ const value = styleValues[styleIndex];
3505
+ factors[i] = value !== void 0 ? value : 1;
3506
+ }
3507
+ return factors;
3508
+ }
3509
+ function computeFlowOffset(timeSeconds) {
3510
+ const cycle = timeSeconds * 0.25 % 1;
3511
+ return [-cycle, 0];
3512
+ }
3513
+ function deriveSurfaceRenderState(surfaceFlags = SURF_NONE, timeSeconds = 0) {
3514
+ const flowing = (surfaceFlags & SURF_FLOWING) !== 0;
3515
+ const warp = (surfaceFlags & SURF_WARP) !== 0;
3516
+ const sky = (surfaceFlags & SURF_SKY) !== 0;
3517
+ const trans33 = (surfaceFlags & SURF_TRANS33) !== 0;
3518
+ const trans66 = (surfaceFlags & SURF_TRANS66) !== 0;
3519
+ const alpha = trans33 ? 0.33 : trans66 ? 0.66 : 1;
3520
+ const blend = trans33 || trans66;
3521
+ const depthWrite = !blend && !sky;
3522
+ const flowOffset = flowing ? computeFlowOffset(timeSeconds) : [0, 0];
3523
+ return {
3524
+ alpha,
3525
+ blend,
3526
+ depthWrite,
3527
+ warp,
3528
+ flowOffset,
3529
+ sky
3530
+ };
3531
+ }
3532
+ var BspSurfacePipeline = class {
3533
+ constructor(gl) {
3534
+ this.gl = gl;
3535
+ this.program = ShaderProgram.create(
3536
+ gl,
3537
+ { vertex: BSP_SURFACE_VERTEX_SOURCE, fragment: BSP_SURFACE_FRAGMENT_SOURCE },
3538
+ { a_position: 0, a_texCoord: 1, a_lightmapCoord: 2 }
3539
+ );
3540
+ this.uniformMvp = this.program.getUniformLocation("u_modelViewProjection");
3541
+ this.uniformTexScroll = this.program.getUniformLocation("u_texScroll");
3542
+ this.uniformLmScroll = this.program.getUniformLocation("u_lightmapScroll");
3543
+ this.uniformLightStyles = this.program.getUniformLocation("u_lightStyleFactors");
3544
+ this.uniformAlpha = this.program.getUniformLocation("u_alpha");
3545
+ this.uniformApplyLightmap = this.program.getUniformLocation("u_applyLightmap");
3546
+ this.uniformWarp = this.program.getUniformLocation("u_warp");
3547
+ this.uniformDiffuse = this.program.getUniformLocation("u_diffuseMap");
3548
+ this.uniformLightmap = this.program.getUniformLocation("u_lightmapAtlas");
3549
+ this.uniformTime = this.program.getUniformLocation("u_time");
3550
+ }
3551
+ bind(options) {
3552
+ const {
3553
+ modelViewProjection,
3554
+ styleIndices = DEFAULT_STYLE_INDICES,
3555
+ styleValues = [],
3556
+ diffuseSampler = 0,
3557
+ lightmapSampler = 1,
3558
+ surfaceFlags = SURF_NONE,
3559
+ timeSeconds = 0
3560
+ } = options;
3561
+ const state = deriveSurfaceRenderState(surfaceFlags, timeSeconds);
3562
+ const styles = resolveLightStyles(styleIndices, styleValues);
3563
+ this.program.use();
3564
+ this.gl.uniformMatrix4fv(this.uniformMvp, false, modelViewProjection);
3565
+ this.gl.uniform2f(this.uniformTexScroll, state.flowOffset[0], state.flowOffset[1]);
3566
+ this.gl.uniform2f(this.uniformLmScroll, state.flowOffset[0], state.flowOffset[1]);
3567
+ this.gl.uniform4fv(this.uniformLightStyles, styles);
3568
+ this.gl.uniform1f(this.uniformAlpha, state.alpha);
3569
+ this.gl.uniform1i(this.uniformApplyLightmap, state.sky ? 0 : 1);
3570
+ this.gl.uniform1i(this.uniformWarp, state.warp ? 1 : 0);
3571
+ this.gl.uniform1f(this.uniformTime, timeSeconds);
3572
+ this.gl.uniform1i(this.uniformDiffuse, diffuseSampler);
3573
+ this.gl.uniform1i(this.uniformLightmap, lightmapSampler);
3574
+ return state;
3575
+ }
3576
+ dispose() {
3577
+ this.program.dispose();
3578
+ }
3579
+ };
3580
+ function applySurfaceState(gl, state) {
3581
+ gl.depthMask(state.depthWrite);
3582
+ if (state.blend) {
3583
+ gl.enable(gl.BLEND);
3584
+ gl.blendFunc(gl.SRC_ALPHA, gl.ONE_MINUS_SRC_ALPHA);
3585
+ } else {
3586
+ gl.disable(gl.BLEND);
3587
+ }
3588
+ }
3589
+
3590
+ // src/render/skybox.ts
3591
+ var SKYBOX_POSITIONS = new Float32Array([
3592
+ // Front
3593
+ -1,
3594
+ -1,
3595
+ 1,
3596
+ 1,
3597
+ -1,
3598
+ 1,
3599
+ 1,
3600
+ 1,
3601
+ 1,
3602
+ -1,
3603
+ -1,
3604
+ 1,
3605
+ 1,
3606
+ 1,
3607
+ 1,
3608
+ -1,
3609
+ 1,
3610
+ 1,
3611
+ // Back
3612
+ -1,
3613
+ -1,
3614
+ -1,
3615
+ -1,
3616
+ 1,
3617
+ -1,
3618
+ 1,
3619
+ 1,
3620
+ -1,
3621
+ -1,
3622
+ -1,
3623
+ -1,
3624
+ 1,
3625
+ 1,
3626
+ -1,
3627
+ 1,
3628
+ -1,
3629
+ -1,
3630
+ // Left
3631
+ -1,
3632
+ -1,
3633
+ -1,
3634
+ -1,
3635
+ -1,
3636
+ 1,
3637
+ -1,
3638
+ 1,
3639
+ 1,
3640
+ -1,
3641
+ -1,
3642
+ -1,
3643
+ -1,
3644
+ 1,
3645
+ 1,
3646
+ -1,
3647
+ 1,
3648
+ -1,
3649
+ // Right
3650
+ 1,
3651
+ -1,
3652
+ -1,
3653
+ 1,
3654
+ 1,
3655
+ -1,
3656
+ 1,
3657
+ 1,
3658
+ 1,
3659
+ 1,
3660
+ -1,
3661
+ -1,
3662
+ 1,
3663
+ 1,
3664
+ 1,
3665
+ 1,
3666
+ -1,
3667
+ 1,
3668
+ // Top
3669
+ -1,
3670
+ 1,
3671
+ -1,
3672
+ -1,
3673
+ 1,
3674
+ 1,
3675
+ 1,
3676
+ 1,
3677
+ 1,
3678
+ -1,
3679
+ 1,
3680
+ -1,
3681
+ 1,
3682
+ 1,
3683
+ 1,
3684
+ 1,
3685
+ 1,
3686
+ -1,
3687
+ // Bottom
3688
+ -1,
3689
+ -1,
3690
+ -1,
3691
+ 1,
3692
+ -1,
3693
+ -1,
3694
+ 1,
3695
+ -1,
3696
+ 1,
3697
+ -1,
3698
+ -1,
3699
+ -1,
3700
+ 1,
3701
+ -1,
3702
+ 1,
3703
+ -1,
3704
+ -1,
3705
+ 1
3706
+ ]);
3707
+ var SKYBOX_VERTEX_SHADER = `#version 300 es
3708
+ precision highp float;
3709
+
3710
+ layout(location = 0) in vec3 a_position;
3711
+
3712
+ uniform mat4 u_viewProjectionNoTranslation;
3713
+ uniform vec2 u_scroll;
3714
+
3715
+ out vec3 v_direction;
3716
+
3717
+ void main() {
3718
+ vec3 dir = normalize(a_position);
3719
+ dir.xy += u_scroll;
3720
+ v_direction = dir;
3721
+ gl_Position = u_viewProjectionNoTranslation * vec4(a_position, 1.0);
3722
+ }`;
3723
+ var SKYBOX_FRAGMENT_SHADER = `#version 300 es
3724
+ precision highp float;
3725
+
3726
+ in vec3 v_direction;
3727
+ uniform samplerCube u_skybox;
3728
+
3729
+ out vec4 o_color;
3730
+
3731
+ void main() {
3732
+ o_color = texture(u_skybox, v_direction);
3733
+ }`;
3734
+ var SkyboxPipeline = class {
3735
+ constructor(gl) {
3736
+ this.gl = gl;
3737
+ this.program = ShaderProgram.create(
3738
+ gl,
3739
+ { vertex: SKYBOX_VERTEX_SHADER, fragment: SKYBOX_FRAGMENT_SHADER },
3740
+ { a_position: 0 }
3741
+ );
3742
+ this.vao = new VertexArray(gl);
3743
+ this.vbo = new VertexBuffer(gl, gl.STATIC_DRAW);
3744
+ this.vbo.upload(SKYBOX_POSITIONS, gl.STATIC_DRAW);
3745
+ const layout = [{ index: 0, size: 3, type: gl.FLOAT, stride: 12, offset: 0 }];
3746
+ this.vao.configureAttributes(layout, this.vbo);
3747
+ this.uniformViewProj = this.program.getUniformLocation("u_viewProjectionNoTranslation");
3748
+ this.uniformScroll = this.program.getUniformLocation("u_scroll");
3749
+ this.uniformSampler = this.program.getUniformLocation("u_skybox");
3750
+ this.cubemap = new TextureCubeMap(gl);
3751
+ this.cubemap.setParameters({
3752
+ minFilter: gl.LINEAR,
3753
+ magFilter: gl.LINEAR,
3754
+ wrapS: gl.CLAMP_TO_EDGE,
3755
+ wrapT: gl.CLAMP_TO_EDGE
3756
+ });
3757
+ }
3758
+ bind(options) {
3759
+ const { viewProjection, scroll, textureUnit = 0 } = options;
3760
+ this.program.use();
3761
+ this.gl.depthMask(false);
3762
+ this.gl.uniformMatrix4fv(this.uniformViewProj, false, viewProjection);
3763
+ this.gl.uniform2f(this.uniformScroll, scroll[0], scroll[1]);
3764
+ this.gl.uniform1i(this.uniformSampler, textureUnit);
3765
+ this.cubemap.bind(textureUnit);
3766
+ this.vao.bind();
3767
+ }
3768
+ draw() {
3769
+ this.gl.drawArrays(this.gl.TRIANGLES, 0, SKYBOX_POSITIONS.length / 3);
3770
+ }
3771
+ dispose() {
3772
+ this.vbo.dispose();
3773
+ this.vao.dispose();
3774
+ this.cubemap.dispose();
3775
+ this.program.dispose();
3776
+ }
3777
+ };
3778
+ function removeViewTranslation(viewMatrix) {
3779
+ const noTranslation = viewMatrix.slice();
3780
+ noTranslation[12] = 0;
3781
+ noTranslation[13] = 0;
3782
+ noTranslation[14] = 0;
3783
+ return noTranslation;
3784
+ }
3785
+ function computeSkyScroll(timeSeconds, scrollSpeeds = [0.01, 0.02]) {
3786
+ const [sx, sy] = scrollSpeeds;
3787
+ return [sx * timeSeconds, sy * timeSeconds];
3788
+ }
3789
+
3790
+ // src/render/md2Pipeline.ts
3791
+ var MD2_VERTEX_SHADER = `#version 300 es
3792
+ precision highp float;
3793
+
3794
+ layout(location = 0) in vec3 a_position;
3795
+ layout(location = 1) in vec3 a_normal;
3796
+ layout(location = 2) in vec2 a_texCoord;
3797
+
3798
+ uniform mat4 u_modelViewProjection;
3799
+ uniform vec3 u_lightDir;
3800
+
3801
+ out vec2 v_texCoord;
3802
+ out float v_light;
3803
+
3804
+ void main() {
3805
+ vec3 normal = normalize(a_normal);
3806
+ v_light = max(dot(normal, normalize(u_lightDir)), 0.0);
3807
+ v_texCoord = a_texCoord;
3808
+ gl_Position = u_modelViewProjection * vec4(a_position, 1.0);
3809
+ }`;
3810
+ var MD2_FRAGMENT_SHADER = `#version 300 es
3811
+ precision highp float;
3812
+
3813
+ in vec2 v_texCoord;
3814
+ in float v_light;
3815
+
3816
+ uniform sampler2D u_diffuseMap;
3817
+ uniform vec4 u_tint;
3818
+
3819
+ out vec4 o_color;
3820
+
3821
+ void main() {
3822
+ vec4 albedo = texture(u_diffuseMap, v_texCoord) * u_tint;
3823
+ o_color = vec4(albedo.rgb * v_light, albedo.a);
3824
+ }`;
3825
+ function normalizeVec32(v) {
3826
+ const lengthSq = v.x * v.x + v.y * v.y + v.z * v.z;
3827
+ if (lengthSq <= 0) {
3828
+ return { x: 0, y: 0, z: 1 };
3829
+ }
3830
+ const inv = 1 / Math.sqrt(lengthSq);
3831
+ return { x: v.x * inv, y: v.y * inv, z: v.z * inv };
3832
+ }
3833
+ function lerp(a, b, t) {
3834
+ return a + (b - a) * t;
3835
+ }
3836
+ function lerpVec3(a, b, t) {
3837
+ return {
3838
+ x: lerp(a.x, b.x, t),
3839
+ y: lerp(a.y, b.y, t),
3840
+ z: lerp(a.z, b.z, t)
3841
+ };
3842
+ }
3843
+ function normalizeUv(s, t, header) {
3844
+ return [s / header.skinWidth, 1 - t / header.skinHeight];
3845
+ }
3846
+ function buildMd2Geometry(model) {
3847
+ if (model.glCommands.length === 0) {
3848
+ const vertices2 = [];
3849
+ const indices2 = [];
3850
+ model.triangles.forEach((triangle) => {
3851
+ const baseIndex = vertices2.length;
3852
+ for (let i = 0; i < 3; i += 1) {
3853
+ const vertexIndex = triangle.vertexIndices[i];
3854
+ const texCoordIndex = triangle.texCoordIndices[i];
3855
+ const texCoord = model.texCoords[texCoordIndex];
3856
+ vertices2.push({
3857
+ vertexIndex,
3858
+ texCoord: normalizeUv(texCoord.s, texCoord.t, model.header)
3859
+ });
3860
+ }
3861
+ indices2.push(baseIndex, baseIndex + 1, baseIndex + 2);
3862
+ });
3863
+ return { vertices: vertices2, indices: new Uint16Array(indices2) };
3864
+ }
3865
+ const vertices = [];
3866
+ const indices = [];
3867
+ for (const command of model.glCommands) {
3868
+ const start = vertices.length;
3869
+ vertices.push(
3870
+ ...command.vertices.map((vertex) => ({
3871
+ vertexIndex: vertex.vertexIndex,
3872
+ texCoord: [vertex.s, 1 - vertex.t]
3873
+ }))
3874
+ );
3875
+ if (command.mode === "strip") {
3876
+ for (let i = 0; i < command.vertices.length - 2; i += 1) {
3877
+ const even = i % 2 === 0;
3878
+ const a = start + i + (even ? 0 : 1);
3879
+ const b = start + i + (even ? 1 : 0);
3880
+ const c = start + i + 2;
3881
+ indices.push(a, b, c);
3882
+ }
3883
+ } else {
3884
+ for (let i = 1; i < command.vertices.length - 1; i += 1) {
3885
+ indices.push(start, start + i, start + i + 1);
3886
+ }
3887
+ }
3888
+ }
3889
+ return { vertices, indices: new Uint16Array(indices) };
3890
+ }
3891
+ function buildMd2VertexData(model, geometry, blend) {
3892
+ const { currentFrame, nextFrame, lerp: lerp2 } = blend;
3893
+ const frameA = model.frames[currentFrame];
3894
+ const frameB = model.frames[nextFrame];
3895
+ if (!frameA || !frameB) {
3896
+ throw new Error("Requested MD2 frames are out of range");
3897
+ }
3898
+ const data = new Float32Array(geometry.vertices.length * 8);
3899
+ geometry.vertices.forEach((vertex, index) => {
3900
+ const vA = frameA.vertices[vertex.vertexIndex];
3901
+ const vB = frameB.vertices[vertex.vertexIndex];
3902
+ if (!vA || !vB) {
3903
+ throw new Error("MD2 vertex index out of range for frame");
3904
+ }
3905
+ const position = lerpVec3(vA.position, vB.position, lerp2);
3906
+ const normal = normalizeVec32(lerpVec3(vA.normal, vB.normal, lerp2));
3907
+ const base = index * 8;
3908
+ data[base] = position.x;
3909
+ data[base + 1] = position.y;
3910
+ data[base + 2] = position.z;
3911
+ data[base + 3] = normal.x;
3912
+ data[base + 4] = normal.y;
3913
+ data[base + 5] = normal.z;
3914
+ data[base + 6] = vertex.texCoord[0];
3915
+ data[base + 7] = vertex.texCoord[1];
3916
+ });
3917
+ return data;
3918
+ }
3919
+ var Md2MeshBuffers = class {
3920
+ constructor(gl, model, blend) {
3921
+ this.gl = gl;
3922
+ this.geometry = buildMd2Geometry(model);
3923
+ this.vertexBuffer = new VertexBuffer(gl, gl.STATIC_DRAW);
3924
+ this.indexBuffer = new IndexBuffer(gl, gl.STATIC_DRAW);
3925
+ this.vertexArray = new VertexArray(gl);
3926
+ this.indexCount = this.geometry.indices.length;
3927
+ this.vertexArray.configureAttributes(
3928
+ [
3929
+ { index: 0, size: 3, type: gl.FLOAT, stride: 32, offset: 0 },
3930
+ { index: 1, size: 3, type: gl.FLOAT, stride: 32, offset: 12 },
3931
+ { index: 2, size: 2, type: gl.FLOAT, stride: 32, offset: 24 }
3932
+ ],
3933
+ this.vertexBuffer
3934
+ );
3935
+ this.vertexArray.bind();
3936
+ this.indexBuffer.bind();
3937
+ this.indexBuffer.upload(this.geometry.indices, gl.STATIC_DRAW);
3938
+ this.update(model, blend);
3939
+ }
3940
+ update(model, blend) {
3941
+ const data = buildMd2VertexData(model, this.geometry, blend);
3942
+ this.vertexBuffer.upload(data, this.gl.STATIC_DRAW);
3943
+ }
3944
+ bind() {
3945
+ this.vertexArray.bind();
3946
+ this.indexBuffer.bind();
3947
+ }
3948
+ dispose() {
3949
+ this.vertexBuffer.dispose();
3950
+ this.indexBuffer.dispose();
3951
+ this.vertexArray.dispose();
3952
+ }
3953
+ };
3954
+ var Md2Pipeline = class {
3955
+ constructor(gl) {
3956
+ this.gl = gl;
3957
+ this.program = ShaderProgram.create(
3958
+ gl,
3959
+ { vertex: MD2_VERTEX_SHADER, fragment: MD2_FRAGMENT_SHADER },
3960
+ { a_position: 0, a_normal: 1, a_texCoord: 2 }
3961
+ );
3962
+ this.uniformMvp = this.program.getUniformLocation("u_modelViewProjection");
3963
+ this.uniformLightDir = this.program.getUniformLocation("u_lightDir");
3964
+ this.uniformTint = this.program.getUniformLocation("u_tint");
3965
+ this.uniformDiffuse = this.program.getUniformLocation("u_diffuseMap");
3966
+ }
3967
+ bind(options) {
3968
+ const { modelViewProjection, lightDirection = [0, 0, 1], tint = [1, 1, 1, 1], diffuseSampler = 0 } = options;
3969
+ const lightVec = new Float32Array(lightDirection);
3970
+ const tintVec = new Float32Array(tint);
3971
+ this.program.use();
3972
+ this.gl.uniformMatrix4fv(this.uniformMvp, false, modelViewProjection);
3973
+ this.gl.uniform3fv(this.uniformLightDir, lightVec);
3974
+ this.gl.uniform4fv(this.uniformTint, tintVec);
3975
+ this.gl.uniform1i(this.uniformDiffuse, diffuseSampler);
3976
+ }
3977
+ draw(mesh) {
3978
+ mesh.bind();
3979
+ this.gl.drawElements(this.gl.TRIANGLES, mesh.indexCount, this.gl.UNSIGNED_SHORT, 0);
3980
+ }
3981
+ dispose() {
3982
+ this.program.dispose();
3983
+ }
3984
+ };
3985
+
1716
3986
  // src/index.ts
1717
3987
  function createEngine(imports) {
1718
3988
  return {
@@ -1727,7 +3997,23 @@ function createEngine(imports) {
1727
3997
  };
1728
3998
  }
1729
3999
  export {
4000
+ ATTN_IDLE,
4001
+ ATTN_LOOP_NONE,
4002
+ ATTN_NONE,
4003
+ ATTN_NORM,
4004
+ ATTN_STATIC,
4005
+ AssetDependencyError,
4006
+ AssetDependencyTracker,
4007
+ AssetManager,
4008
+ AudioApi,
4009
+ AudioContextController,
4010
+ AudioRegistry,
4011
+ AudioRegistryError,
4012
+ AudioSystem,
4013
+ BSP_SURFACE_FRAGMENT_SOURCE,
4014
+ BSP_SURFACE_VERTEX_SOURCE,
1730
4015
  BSP_VERTEX_LAYOUT,
4016
+ BspSurfacePipeline,
1731
4017
  ConfigStringRegistry,
1732
4018
  Cvar,
1733
4019
  CvarRegistry,
@@ -1737,27 +4023,79 @@ export {
1737
4023
  Framebuffer,
1738
4024
  IndexBuffer,
1739
4025
  LruCache,
4026
+ MAX_SOUND_CHANNELS,
4027
+ MD2_FRAGMENT_SHADER,
4028
+ MD2_VERTEX_SHADER,
1740
4029
  Md2Loader,
4030
+ Md2MeshBuffers,
1741
4031
  Md2ParseError,
4032
+ Md2Pipeline,
4033
+ Md3Loader,
4034
+ Md3ParseError,
4035
+ MusicSystem,
1742
4036
  PakArchive,
4037
+ PakIndexStore,
1743
4038
  PakIngestionError,
1744
4039
  PakParseError,
4040
+ PakValidationError,
4041
+ PakValidator,
4042
+ RERELEASE_KNOWN_PAKS,
4043
+ SKYBOX_FRAGMENT_SHADER,
4044
+ SKYBOX_VERTEX_SHADER,
4045
+ SOUND_FULLVOLUME,
4046
+ SOUND_LOOP_ATTENUATE,
1745
4047
  ShaderProgram,
4048
+ SkyboxPipeline,
4049
+ SoundChannel,
4050
+ SoundPrecache,
4051
+ SoundRegistry,
1746
4052
  Texture2D,
4053
+ TextureCache,
4054
+ TextureCubeMap,
1747
4055
  VertexArray,
1748
4056
  VertexBuffer,
1749
4057
  VirtualFileSystem,
4058
+ advanceAnimation,
4059
+ applySurfaceState,
4060
+ attenuationToDistanceMultiplier,
4061
+ boxIntersectsFrustum,
1750
4062
  buildBspGeometry,
4063
+ buildMd2Geometry,
4064
+ buildMd2VertexData,
4065
+ calculateMaxAudibleDistance,
1751
4066
  calculatePakChecksum,
4067
+ computeFrameBlend,
4068
+ computeSkyScroll,
4069
+ createAnimationState,
4070
+ createAudioGraph,
1752
4071
  createEngine,
1753
4072
  createEngineRuntime,
4073
+ createInitialChannels,
1754
4074
  createProgramFromSources,
1755
4075
  createWebGLContext,
4076
+ decodeOgg,
4077
+ deriveSurfaceRenderState,
4078
+ extractFrustumPlanes,
1756
4079
  filesToPakSources,
4080
+ findLeafForPoint,
4081
+ gatherVisibleFaces,
1757
4082
  groupMd2Animations,
1758
4083
  ingestPakFiles,
1759
4084
  ingestPaks,
4085
+ interpolateVec3,
1760
4086
  parseMd2,
4087
+ parseMd3,
4088
+ parsePcx,
4089
+ parseWal,
4090
+ parseWalTexture,
4091
+ parseWav,
4092
+ pcxToRgba,
4093
+ pickChannel,
4094
+ preparePcxTexture,
4095
+ removeViewTranslation,
4096
+ resolveLightStyles,
4097
+ spatializeOrigin,
4098
+ walToRgba,
1761
4099
  wireDropTarget,
1762
4100
  wireFileInput
1763
4101
  };