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
@@ -20,7 +20,23 @@ var __toCommonJS = (mod) => __copyProps(__defProp({}, "__esModule", { value: tru
20
20
  // src/index.ts
21
21
  var index_exports = {};
22
22
  __export(index_exports, {
23
+ ATTN_IDLE: () => ATTN_IDLE,
24
+ ATTN_LOOP_NONE: () => ATTN_LOOP_NONE,
25
+ ATTN_NONE: () => ATTN_NONE,
26
+ ATTN_NORM: () => ATTN_NORM,
27
+ ATTN_STATIC: () => ATTN_STATIC,
28
+ AssetDependencyError: () => AssetDependencyError,
29
+ AssetDependencyTracker: () => AssetDependencyTracker,
30
+ AssetManager: () => AssetManager,
31
+ AudioApi: () => AudioApi,
32
+ AudioContextController: () => AudioContextController,
33
+ AudioRegistry: () => AudioRegistry,
34
+ AudioRegistryError: () => AudioRegistryError,
35
+ AudioSystem: () => AudioSystem,
36
+ BSP_SURFACE_FRAGMENT_SOURCE: () => BSP_SURFACE_FRAGMENT_SOURCE,
37
+ BSP_SURFACE_VERTEX_SOURCE: () => BSP_SURFACE_VERTEX_SOURCE,
23
38
  BSP_VERTEX_LAYOUT: () => BSP_VERTEX_LAYOUT,
39
+ BspSurfacePipeline: () => BspSurfacePipeline,
24
40
  ConfigStringRegistry: () => ConfigStringRegistry,
25
41
  Cvar: () => Cvar,
26
42
  CvarRegistry: () => CvarRegistry,
@@ -30,27 +46,79 @@ __export(index_exports, {
30
46
  Framebuffer: () => Framebuffer,
31
47
  IndexBuffer: () => IndexBuffer,
32
48
  LruCache: () => LruCache,
49
+ MAX_SOUND_CHANNELS: () => MAX_SOUND_CHANNELS,
50
+ MD2_FRAGMENT_SHADER: () => MD2_FRAGMENT_SHADER,
51
+ MD2_VERTEX_SHADER: () => MD2_VERTEX_SHADER,
33
52
  Md2Loader: () => Md2Loader,
53
+ Md2MeshBuffers: () => Md2MeshBuffers,
34
54
  Md2ParseError: () => Md2ParseError,
55
+ Md2Pipeline: () => Md2Pipeline,
56
+ Md3Loader: () => Md3Loader,
57
+ Md3ParseError: () => Md3ParseError,
58
+ MusicSystem: () => MusicSystem,
35
59
  PakArchive: () => PakArchive,
60
+ PakIndexStore: () => PakIndexStore,
36
61
  PakIngestionError: () => PakIngestionError,
37
62
  PakParseError: () => PakParseError,
63
+ PakValidationError: () => PakValidationError,
64
+ PakValidator: () => PakValidator,
65
+ RERELEASE_KNOWN_PAKS: () => RERELEASE_KNOWN_PAKS,
66
+ SKYBOX_FRAGMENT_SHADER: () => SKYBOX_FRAGMENT_SHADER,
67
+ SKYBOX_VERTEX_SHADER: () => SKYBOX_VERTEX_SHADER,
68
+ SOUND_FULLVOLUME: () => SOUND_FULLVOLUME,
69
+ SOUND_LOOP_ATTENUATE: () => SOUND_LOOP_ATTENUATE,
38
70
  ShaderProgram: () => ShaderProgram,
71
+ SkyboxPipeline: () => SkyboxPipeline,
72
+ SoundChannel: () => SoundChannel,
73
+ SoundPrecache: () => SoundPrecache,
74
+ SoundRegistry: () => SoundRegistry,
39
75
  Texture2D: () => Texture2D,
76
+ TextureCache: () => TextureCache,
77
+ TextureCubeMap: () => TextureCubeMap,
40
78
  VertexArray: () => VertexArray,
41
79
  VertexBuffer: () => VertexBuffer,
42
80
  VirtualFileSystem: () => VirtualFileSystem,
81
+ advanceAnimation: () => advanceAnimation,
82
+ applySurfaceState: () => applySurfaceState,
83
+ attenuationToDistanceMultiplier: () => attenuationToDistanceMultiplier,
84
+ boxIntersectsFrustum: () => boxIntersectsFrustum,
43
85
  buildBspGeometry: () => buildBspGeometry,
86
+ buildMd2Geometry: () => buildMd2Geometry,
87
+ buildMd2VertexData: () => buildMd2VertexData,
88
+ calculateMaxAudibleDistance: () => calculateMaxAudibleDistance,
44
89
  calculatePakChecksum: () => calculatePakChecksum,
90
+ computeFrameBlend: () => computeFrameBlend,
91
+ computeSkyScroll: () => computeSkyScroll,
92
+ createAnimationState: () => createAnimationState,
93
+ createAudioGraph: () => createAudioGraph,
45
94
  createEngine: () => createEngine,
46
95
  createEngineRuntime: () => createEngineRuntime,
96
+ createInitialChannels: () => createInitialChannels,
47
97
  createProgramFromSources: () => createProgramFromSources,
48
98
  createWebGLContext: () => createWebGLContext,
99
+ decodeOgg: () => decodeOgg,
100
+ deriveSurfaceRenderState: () => deriveSurfaceRenderState,
101
+ extractFrustumPlanes: () => extractFrustumPlanes,
49
102
  filesToPakSources: () => filesToPakSources,
103
+ findLeafForPoint: () => findLeafForPoint,
104
+ gatherVisibleFaces: () => gatherVisibleFaces,
50
105
  groupMd2Animations: () => groupMd2Animations,
51
106
  ingestPakFiles: () => ingestPakFiles,
52
107
  ingestPaks: () => ingestPaks,
108
+ interpolateVec3: () => interpolateVec3,
53
109
  parseMd2: () => parseMd2,
110
+ parseMd3: () => parseMd3,
111
+ parsePcx: () => parsePcx,
112
+ parseWal: () => parseWal,
113
+ parseWalTexture: () => parseWalTexture,
114
+ parseWav: () => parseWav,
115
+ pcxToRgba: () => pcxToRgba,
116
+ pickChannel: () => pickChannel,
117
+ preparePcxTexture: () => preparePcxTexture,
118
+ removeViewTranslation: () => removeViewTranslation,
119
+ resolveLightStyles: () => resolveLightStyles,
120
+ spatializeOrigin: () => spatializeOrigin,
121
+ walToRgba: () => walToRgba,
54
122
  wireDropTarget: () => wireDropTarget,
55
123
  wireFileInput: () => wireFileInput
56
124
  });
@@ -188,7 +256,27 @@ var EngineHost = class {
188
256
  };
189
257
 
190
258
  // ../shared/dist/esm/index.js
259
+ var ZERO_VEC3 = { x: 0, y: 0, z: 0 };
191
260
  var DEG_TO_RAD = Math.PI / 180;
261
+ function subtractVec3(a, b) {
262
+ return { x: a.x - b.x, y: a.y - b.y, z: a.z - b.z };
263
+ }
264
+ function scaleVec3(a, scalar) {
265
+ return { x: a.x * scalar, y: a.y * scalar, z: a.z * scalar };
266
+ }
267
+ function dotVec3(a, b) {
268
+ return a.x * b.x + a.y * b.y + a.z * b.z;
269
+ }
270
+ function lengthSquaredVec3(a) {
271
+ return dotVec3(a, a);
272
+ }
273
+ function lengthVec3(a) {
274
+ return Math.sqrt(lengthSquaredVec3(a));
275
+ }
276
+ function normalizeVec3(a) {
277
+ const len = lengthVec3(a);
278
+ return len === 0 ? a : scaleVec3(a, 1 / len);
279
+ }
192
280
  var DEG2RAD_FACTOR = Math.PI / 180;
193
281
  var RAD2DEG_FACTOR = 180 / Math.PI;
194
282
  var CONTENTS_SOLID = 1 << 0;
@@ -244,6 +332,7 @@ var MASK_NAV_SOLID = CONTENTS_SOLID | CONTENTS_PLAYERCLIP | CONTENTS_WINDOW;
244
332
  var MASK_LADDER_NAV_SOLID = CONTENTS_SOLID | CONTENTS_WINDOW;
245
333
  var MASK_WALK_NAV_SOLID = CONTENTS_SOLID | CONTENTS_PLAYERCLIP | CONTENTS_WINDOW | CONTENTS_MONSTERCLIP;
246
334
  var MASK_PROJECTILE = MASK_SHOT | CONTENTS_PROJECTILECLIP;
335
+ var MAX_CHECKCOUNT = Number.MAX_SAFE_INTEGER - 1;
247
336
  var CvarFlags = /* @__PURE__ */ ((CvarFlags2) => {
248
337
  CvarFlags2[CvarFlags2["None"] = 0] = "None";
249
338
  CvarFlags2[CvarFlags2["Archive"] = 1] = "Archive";
@@ -349,6 +438,14 @@ var ConfigStringRegistry = class {
349
438
  soundIndex(path) {
350
439
  return this.register(path, ConfigStringIndex.Sounds, MAX_SOUNDS, "soundCursor");
351
440
  }
441
+ findSoundIndex(path) {
442
+ for (let i = ConfigStringIndex.Sounds; i < ConfigStringIndex.Sounds + MAX_SOUNDS; i += 1) {
443
+ if (this.values.get(i) === path) {
444
+ return i;
445
+ }
446
+ }
447
+ return void 0;
448
+ }
352
449
  imageIndex(path) {
353
450
  return this.register(path, ConfigStringIndex.Images, MAX_IMAGES, "imageCursor");
354
451
  }
@@ -717,6 +814,72 @@ var VirtualFileSystem = class {
717
814
  }
718
815
  };
719
816
 
817
+ // src/assets/pakValidation.ts
818
+ var RERELEASE_KNOWN_PAKS = Object.freeze([
819
+ // Base campaign
820
+ { name: "pak0.pak", checksum: 2378051181, description: "Base game assets" },
821
+ { name: "pak0.pak@baseq2", checksum: 2378051181, description: "Base game assets (baseq2)" },
822
+ // Mission packs bundled with the rerelease
823
+ { name: "pak0.pak@rogue", checksum: 3373211245, description: "Ground Zero (rogue) mission pack" },
824
+ { name: "pak0.pak@xatrix", checksum: 1358269824, description: "The Reckoning (xatrix) mission pack" }
825
+ ]);
826
+ var PakValidationError = class extends Error {
827
+ constructor(result) {
828
+ super(
829
+ result.status === "unknown" ? `Unknown PAK not allowed: ${result.name}` : `PAK checksum mismatch for ${result.name}`
830
+ );
831
+ this.result = result;
832
+ this.name = "PakValidationError";
833
+ }
834
+ };
835
+ var PakValidator = class {
836
+ constructor(knownPaks = RERELEASE_KNOWN_PAKS) {
837
+ this.known = /* @__PURE__ */ new Map();
838
+ knownPaks.forEach((pak) => this.known.set(this.normalizePakName(pak.name), pak));
839
+ }
840
+ validateArchive(archive, nameOverride) {
841
+ const pakName = this.normalizePakName(nameOverride ?? ("name" in archive ? archive.name : "unknown"));
842
+ const checksum = archive.checksum;
843
+ const size = "size" in archive ? archive.size : void 0;
844
+ const known = this.known.get(pakName);
845
+ if (!known) {
846
+ return { name: pakName, checksum, status: "unknown", size };
847
+ }
848
+ if (known.checksum !== checksum) {
849
+ return {
850
+ name: pakName,
851
+ checksum,
852
+ expectedChecksum: known.checksum,
853
+ status: "mismatch",
854
+ size,
855
+ description: known.description
856
+ };
857
+ }
858
+ return {
859
+ name: pakName,
860
+ checksum,
861
+ expectedChecksum: known.checksum,
862
+ status: "valid",
863
+ size,
864
+ description: known.description
865
+ };
866
+ }
867
+ assertValid(archive, nameOverride) {
868
+ const outcome = this.validateArchive(archive, nameOverride);
869
+ if (outcome.status === "mismatch") {
870
+ throw new PakValidationError(outcome);
871
+ }
872
+ return outcome;
873
+ }
874
+ normalizePakName(name) {
875
+ const normalized = normalizePath(name);
876
+ const parts = normalized.split("/");
877
+ const filename = parts.pop() ?? normalized;
878
+ const directory = parts.pop();
879
+ return directory ? `${filename}@${directory}` : filename;
880
+ }
881
+ };
882
+
720
883
  // src/assets/ingestion.ts
721
884
  var PakIngestionError = class extends Error {
722
885
  constructor(file, cause) {
@@ -808,18 +971,47 @@ async function toArrayBuffer(source, onProgress) {
808
971
  }
809
972
  async function ingestPaks(vfs, sources, onProgressOrOptions) {
810
973
  const options = typeof onProgressOrOptions === "function" ? { onProgress: onProgressOrOptions } : onProgressOrOptions ?? {};
974
+ const shouldPersist = options.persistIndexes ?? Boolean(options.pakIndexStore);
975
+ const enforceValidation = options.enforceValidation ?? Boolean(options.validator);
976
+ const allowUnknownPaks = options.allowUnknownPaks ?? true;
977
+ const stopOnError = options.stopOnError ?? false;
811
978
  const results = [];
812
979
  for (const source of sources) {
813
980
  try {
814
981
  const buffer = await toArrayBuffer(source, options.onProgress);
815
982
  const archive = PakArchive.fromArrayBuffer(source.name, buffer);
983
+ const validation = options.validator?.validateArchive(archive);
984
+ if (validation) {
985
+ options.onValidationResult?.(validation);
986
+ const isMismatch = validation.status === "mismatch";
987
+ const isUnknown = validation.status === "unknown";
988
+ if (isMismatch && enforceValidation || isUnknown && !allowUnknownPaks) {
989
+ const validationError = new PakValidationError(validation);
990
+ options.onError?.(source.name, validationError);
991
+ if (stopOnError) {
992
+ throw new PakIngestionError(source.name, validationError);
993
+ }
994
+ results.push({ archive, mounted: false, validation });
995
+ continue;
996
+ }
997
+ }
816
998
  vfs.mountPak(archive);
999
+ if (shouldPersist && options.pakIndexStore) {
1000
+ try {
1001
+ await options.pakIndexStore.persist(archive);
1002
+ } catch (error) {
1003
+ options.onError?.(source.name, error);
1004
+ if (stopOnError) {
1005
+ throw new PakIngestionError(source.name, error);
1006
+ }
1007
+ }
1008
+ }
817
1009
  options.onProgress?.({ file: source.name, loadedBytes: buffer.byteLength, totalBytes: buffer.byteLength, state: "parsed" });
818
- results.push({ archive, mounted: true });
1010
+ results.push({ archive, mounted: true, validation });
819
1011
  } catch (error) {
820
1012
  options.onProgress?.({ file: source.name, loadedBytes: 0, totalBytes: 0, state: "error" });
821
1013
  options.onError?.(source.name, error);
822
- if (options.stopOnError) {
1014
+ if (stopOnError) {
823
1015
  throw new PakIngestionError(source.name, error);
824
1016
  }
825
1017
  }
@@ -1298,6 +1490,1393 @@ function groupMd2Animations(frames) {
1298
1490
  return animations;
1299
1491
  }
1300
1492
 
1493
+ // src/assets/md3.ts
1494
+ var MD3_IDENT = 860898377;
1495
+ var MD3_VERSION = 15;
1496
+ var Md3ParseError = class extends Error {
1497
+ constructor(message) {
1498
+ super(message);
1499
+ this.name = "Md3ParseError";
1500
+ }
1501
+ };
1502
+ function readString(view, offset, length) {
1503
+ const bytes = new Uint8Array(view.buffer, view.byteOffset + offset, length);
1504
+ const decoded = new TextDecoder("utf-8").decode(bytes);
1505
+ return decoded.replace(/\0.*$/, "").trim();
1506
+ }
1507
+ function decodeLatLngNormal(latLng) {
1508
+ const lat = (latLng >> 8 & 255) * (2 * Math.PI / 255);
1509
+ const lng = (latLng & 255) * (2 * Math.PI / 255);
1510
+ const sinLng = Math.sin(lng);
1511
+ return {
1512
+ x: Math.cos(lat) * sinLng,
1513
+ y: Math.sin(lat) * sinLng,
1514
+ z: Math.cos(lng)
1515
+ };
1516
+ }
1517
+ function validateOffset(name, offset, size, bufferLength) {
1518
+ if (offset < 0 || offset + size > bufferLength) {
1519
+ throw new Md3ParseError(`${name} exceeds buffer bounds`);
1520
+ }
1521
+ }
1522
+ function parseHeader2(view) {
1523
+ const ident = view.getInt32(0, true);
1524
+ if (ident !== MD3_IDENT) {
1525
+ throw new Md3ParseError(`Invalid MD3 ident: ${ident}`);
1526
+ }
1527
+ const version = view.getInt32(4, true);
1528
+ if (version !== MD3_VERSION) {
1529
+ throw new Md3ParseError(`Unsupported MD3 version: ${version}`);
1530
+ }
1531
+ const name = readString(view, 8, 64);
1532
+ const flags = view.getInt32(72, true);
1533
+ const numFrames = view.getInt32(76, true);
1534
+ const numTags = view.getInt32(80, true);
1535
+ const numSurfaces = view.getInt32(84, true);
1536
+ const numSkins = view.getInt32(88, true);
1537
+ const ofsFrames = view.getInt32(92, true);
1538
+ const ofsTags = view.getInt32(96, true);
1539
+ const ofsSurfaces = view.getInt32(100, true);
1540
+ const ofsEnd = view.getInt32(104, true);
1541
+ if (numFrames <= 0 || numSurfaces < 0 || numTags < 0) {
1542
+ throw new Md3ParseError("Invalid MD3 counts");
1543
+ }
1544
+ return {
1545
+ ident,
1546
+ version,
1547
+ name,
1548
+ flags,
1549
+ numFrames,
1550
+ numTags,
1551
+ numSurfaces,
1552
+ numSkins,
1553
+ ofsFrames,
1554
+ ofsTags,
1555
+ ofsSurfaces,
1556
+ ofsEnd
1557
+ };
1558
+ }
1559
+ function parseFrames2(view, header) {
1560
+ const frames = [];
1561
+ const frameSize = 56;
1562
+ validateOffset("Frames", header.ofsFrames, header.numFrames * frameSize, view.byteLength);
1563
+ for (let i = 0; i < header.numFrames; i += 1) {
1564
+ const base = header.ofsFrames + i * frameSize;
1565
+ frames.push({
1566
+ minBounds: {
1567
+ x: view.getFloat32(base, true),
1568
+ y: view.getFloat32(base + 4, true),
1569
+ z: view.getFloat32(base + 8, true)
1570
+ },
1571
+ maxBounds: {
1572
+ x: view.getFloat32(base + 12, true),
1573
+ y: view.getFloat32(base + 16, true),
1574
+ z: view.getFloat32(base + 20, true)
1575
+ },
1576
+ localOrigin: {
1577
+ x: view.getFloat32(base + 24, true),
1578
+ y: view.getFloat32(base + 28, true),
1579
+ z: view.getFloat32(base + 32, true)
1580
+ },
1581
+ radius: view.getFloat32(base + 36, true),
1582
+ name: readString(view, base + 40, 16)
1583
+ });
1584
+ }
1585
+ return frames;
1586
+ }
1587
+ function parseTags(view, header) {
1588
+ const tags = [];
1589
+ const tagSize = 112;
1590
+ const totalSize = header.numFrames * header.numTags * tagSize;
1591
+ validateOffset("Tags", header.ofsTags, totalSize, view.byteLength);
1592
+ for (let frame = 0; frame < header.numFrames; frame += 1) {
1593
+ const frameTags = [];
1594
+ for (let tagIndex = 0; tagIndex < header.numTags; tagIndex += 1) {
1595
+ const base = header.ofsTags + (frame * header.numTags + tagIndex) * tagSize;
1596
+ const originOffset = base + 64;
1597
+ const axisOffset = originOffset + 12;
1598
+ frameTags.push({
1599
+ name: readString(view, base, 64),
1600
+ origin: {
1601
+ x: view.getFloat32(originOffset, true),
1602
+ y: view.getFloat32(originOffset + 4, true),
1603
+ z: view.getFloat32(originOffset + 8, true)
1604
+ },
1605
+ axis: [
1606
+ {
1607
+ x: view.getFloat32(axisOffset, true),
1608
+ y: view.getFloat32(axisOffset + 4, true),
1609
+ z: view.getFloat32(axisOffset + 8, true)
1610
+ },
1611
+ {
1612
+ x: view.getFloat32(axisOffset + 12, true),
1613
+ y: view.getFloat32(axisOffset + 16, true),
1614
+ z: view.getFloat32(axisOffset + 20, true)
1615
+ },
1616
+ {
1617
+ x: view.getFloat32(axisOffset + 24, true),
1618
+ y: view.getFloat32(axisOffset + 28, true),
1619
+ z: view.getFloat32(axisOffset + 32, true)
1620
+ }
1621
+ ]
1622
+ });
1623
+ }
1624
+ tags.push(frameTags);
1625
+ }
1626
+ return tags;
1627
+ }
1628
+ function parseSurface(view, offset) {
1629
+ const ident = view.getInt32(offset, true);
1630
+ if (ident !== MD3_IDENT) {
1631
+ throw new Md3ParseError(`Invalid surface ident at ${offset}: ${ident}`);
1632
+ }
1633
+ const name = readString(view, offset + 4, 64);
1634
+ const flags = view.getInt32(offset + 68, true);
1635
+ const numFrames = view.getInt32(offset + 72, true);
1636
+ const numShaders = view.getInt32(offset + 76, true);
1637
+ const numVerts = view.getInt32(offset + 80, true);
1638
+ const numTriangles = view.getInt32(offset + 84, true);
1639
+ const ofsTriangles = view.getInt32(offset + 88, true);
1640
+ const ofsShaders = view.getInt32(offset + 92, true);
1641
+ const ofsSt = view.getInt32(offset + 96, true);
1642
+ const ofsXyzNormals = view.getInt32(offset + 100, true);
1643
+ const ofsEnd = view.getInt32(offset + 104, true);
1644
+ if (numFrames <= 0 || numVerts <= 0 || numTriangles <= 0) {
1645
+ throw new Md3ParseError(`Invalid surface counts for ${name}`);
1646
+ }
1647
+ const surfaceSize = ofsEnd;
1648
+ validateOffset(`Surface ${name}`, offset, surfaceSize, view.byteLength);
1649
+ const triangles = [];
1650
+ const triangleStart = offset + ofsTriangles;
1651
+ for (let i = 0; i < numTriangles; i += 1) {
1652
+ const base = triangleStart + i * 12;
1653
+ triangles.push({
1654
+ indices: [view.getInt32(base, true), view.getInt32(base + 4, true), view.getInt32(base + 8, true)]
1655
+ });
1656
+ }
1657
+ const shaders = [];
1658
+ const shaderStart = offset + ofsShaders;
1659
+ for (let i = 0; i < numShaders; i += 1) {
1660
+ const base = shaderStart + i * 68;
1661
+ shaders.push({ name: readString(view, base, 64), shaderIndex: view.getInt32(base + 64, true) });
1662
+ }
1663
+ const texCoords = [];
1664
+ const stStart = offset + ofsSt;
1665
+ for (let i = 0; i < numVerts; i += 1) {
1666
+ const base = stStart + i * 8;
1667
+ texCoords.push({ s: view.getFloat32(base, true), t: view.getFloat32(base + 4, true) });
1668
+ }
1669
+ const vertices = [];
1670
+ const xyzStart = offset + ofsXyzNormals;
1671
+ for (let frame = 0; frame < numFrames; frame += 1) {
1672
+ const frameVertices = [];
1673
+ for (let i = 0; i < numVerts; i += 1) {
1674
+ const base = xyzStart + (frame * numVerts + i) * 8;
1675
+ const x = view.getInt16(base, true) / 64;
1676
+ const y = view.getInt16(base + 2, true) / 64;
1677
+ const z = view.getInt16(base + 4, true) / 64;
1678
+ const latLng = view.getUint16(base + 6, true);
1679
+ frameVertices.push({ position: { x, y, z }, latLng, normal: decodeLatLngNormal(latLng) });
1680
+ }
1681
+ vertices.push(frameVertices);
1682
+ }
1683
+ return {
1684
+ surface: { name, flags, numFrames, shaders, triangles, texCoords, vertices },
1685
+ nextOffset: offset + ofsEnd
1686
+ };
1687
+ }
1688
+ function parseMd3(buffer) {
1689
+ if (buffer.byteLength < 108) {
1690
+ throw new Md3ParseError("MD3 buffer too small for header");
1691
+ }
1692
+ const view = new DataView(buffer);
1693
+ const header = parseHeader2(view);
1694
+ validateOffset("MD3 end", header.ofsEnd, 0, buffer.byteLength);
1695
+ const frames = parseFrames2(view, header);
1696
+ const tags = parseTags(view, header);
1697
+ const surfaces = [];
1698
+ let surfaceOffset = header.ofsSurfaces;
1699
+ for (let i = 0; i < header.numSurfaces; i += 1) {
1700
+ const { surface, nextOffset } = parseSurface(view, surfaceOffset);
1701
+ surfaces.push(surface);
1702
+ surfaceOffset = nextOffset;
1703
+ }
1704
+ if (surfaceOffset !== header.ofsEnd) {
1705
+ throw new Md3ParseError("Surface parsing did not reach ofsEnd");
1706
+ }
1707
+ return { header, frames, tags, surfaces };
1708
+ }
1709
+ var Md3Loader = class {
1710
+ constructor(vfs) {
1711
+ this.vfs = vfs;
1712
+ }
1713
+ async load(path) {
1714
+ const data = await this.vfs.readFile(path);
1715
+ return parseMd3(data.slice().buffer);
1716
+ }
1717
+ };
1718
+
1719
+ // src/assets/animation.ts
1720
+ function advanceAnimation(state, deltaSeconds) {
1721
+ const duration = (state.sequence.end - state.sequence.start + 1) / state.sequence.fps;
1722
+ const loop = state.sequence.loop !== false;
1723
+ let time = state.time + deltaSeconds;
1724
+ if (loop) {
1725
+ time = (time % duration + duration) % duration;
1726
+ } else if (time > duration) {
1727
+ time = duration;
1728
+ }
1729
+ return { ...state, time: Math.max(0, Math.min(time, duration)) };
1730
+ }
1731
+ function computeFrameBlend(state) {
1732
+ const totalFrames = state.sequence.end - state.sequence.start + 1;
1733
+ const frameDuration = 1 / state.sequence.fps;
1734
+ const loop = state.sequence.loop !== false;
1735
+ const framePosition = state.time / frameDuration;
1736
+ if (!loop && framePosition >= totalFrames) {
1737
+ return { frame: state.sequence.end, nextFrame: state.sequence.end, lerp: 0 };
1738
+ }
1739
+ const normalizedPosition = loop ? framePosition % totalFrames : Math.min(framePosition, totalFrames - 1);
1740
+ const baseFrame = Math.floor(normalizedPosition);
1741
+ const frame = state.sequence.start + baseFrame;
1742
+ const nextFrame = baseFrame + 1 >= totalFrames ? loop ? state.sequence.start : state.sequence.end : frame + 1;
1743
+ const lerp2 = !loop && baseFrame >= totalFrames - 1 ? 0 : normalizedPosition - baseFrame;
1744
+ return { frame, nextFrame, lerp: lerp2 };
1745
+ }
1746
+ function createAnimationState(sequence) {
1747
+ return { sequence, time: 0 };
1748
+ }
1749
+ function interpolateVec3(a, b, t) {
1750
+ return {
1751
+ x: a.x + (b.x - a.x) * t,
1752
+ y: a.y + (b.y - a.y) * t,
1753
+ z: a.z + (b.z - a.z) * t
1754
+ };
1755
+ }
1756
+
1757
+ // src/assets/wal.ts
1758
+ var WalParseError = class extends Error {
1759
+ constructor(message) {
1760
+ super(message);
1761
+ this.name = "WalParseError";
1762
+ }
1763
+ };
1764
+ function parseWal(buffer) {
1765
+ if (buffer.byteLength < 100) {
1766
+ throw new WalParseError("WAL buffer too small");
1767
+ }
1768
+ const view = new DataView(buffer);
1769
+ const nameBytes = new Uint8Array(buffer, 0, 32);
1770
+ const name = new TextDecoder("utf-8").decode(nameBytes).replace(/\0.*$/, "").trim();
1771
+ const width = view.getInt32(32, true);
1772
+ const height = view.getInt32(36, true);
1773
+ const offsets = [view.getInt32(40, true), view.getInt32(44, true), view.getInt32(48, true), view.getInt32(52, true)];
1774
+ const animNameBytes = new Uint8Array(buffer, 56, 32);
1775
+ const animName = new TextDecoder("utf-8").decode(animNameBytes).replace(/\0.*$/, "").trim();
1776
+ const flags = view.getInt32(88, true);
1777
+ const contents = view.getInt32(92, true);
1778
+ const value = view.getInt32(96, true);
1779
+ if (width <= 0 || height <= 0) {
1780
+ throw new WalParseError("Invalid WAL dimensions");
1781
+ }
1782
+ const mipmaps = [];
1783
+ let currentWidth = width;
1784
+ let currentHeight = height;
1785
+ for (let level = 0; level < offsets.length; level += 1) {
1786
+ const offset = offsets[level];
1787
+ const expectedSize = Math.max(1, currentWidth * currentHeight | 0);
1788
+ if (offset <= 0 || offset + expectedSize > buffer.byteLength) {
1789
+ throw new WalParseError(`Invalid WAL mip offset for level ${level}`);
1790
+ }
1791
+ const data = new Uint8Array(buffer, offset, expectedSize);
1792
+ mipmaps.push({ level, width: currentWidth, height: currentHeight, data });
1793
+ currentWidth = Math.max(1, currentWidth >> 1);
1794
+ currentHeight = Math.max(1, currentHeight >> 1);
1795
+ }
1796
+ return { name, width, height, mipmaps, animName, flags, contents, value };
1797
+ }
1798
+
1799
+ // src/assets/pcx.ts
1800
+ var PcxParseError = class extends Error {
1801
+ constructor(message) {
1802
+ super(message);
1803
+ this.name = "PcxParseError";
1804
+ }
1805
+ };
1806
+ function parsePcx(buffer) {
1807
+ if (buffer.byteLength < 128) {
1808
+ throw new PcxParseError("PCX buffer too small for header");
1809
+ }
1810
+ const view = new DataView(buffer);
1811
+ const manufacturer = view.getUint8(0);
1812
+ const encoding = view.getUint8(2);
1813
+ const bitsPerPixel = view.getUint8(3);
1814
+ const xMin = view.getUint16(4, true);
1815
+ const yMin = view.getUint16(6, true);
1816
+ const xMax = view.getUint16(8, true);
1817
+ const yMax = view.getUint16(10, true);
1818
+ if (manufacturer !== 10 || encoding !== 1) {
1819
+ throw new PcxParseError("Unsupported PCX encoding");
1820
+ }
1821
+ if (bitsPerPixel !== 8) {
1822
+ throw new PcxParseError("Only 8bpp PCX files are supported");
1823
+ }
1824
+ const width = xMax - xMin + 1;
1825
+ const height = yMax - yMin + 1;
1826
+ const bytesPerLine = view.getUint16(66, true);
1827
+ const paletteMarkerOffset = buffer.byteLength - 769;
1828
+ if (paletteMarkerOffset < 128 || new DataView(buffer, paletteMarkerOffset, 1).getUint8(0) !== 12) {
1829
+ throw new PcxParseError("Missing PCX palette");
1830
+ }
1831
+ const palette = new Uint8Array(buffer, paletteMarkerOffset + 1, 768);
1832
+ const encoded = new Uint8Array(buffer, 128, paletteMarkerOffset - 128);
1833
+ const pixels = new Uint8Array(width * height);
1834
+ let srcIndex = 0;
1835
+ let dstIndex = 0;
1836
+ for (let y = 0; y < height; y += 1) {
1837
+ let written = 0;
1838
+ while (written < bytesPerLine && srcIndex < encoded.length) {
1839
+ let count = 1;
1840
+ let value = encoded[srcIndex++];
1841
+ if ((value & 192) === 192) {
1842
+ count = value & 63;
1843
+ if (srcIndex >= encoded.length) {
1844
+ throw new PcxParseError("Unexpected end of PCX RLE data");
1845
+ }
1846
+ value = encoded[srcIndex++];
1847
+ }
1848
+ for (let i = 0; i < count && written < bytesPerLine; i += 1) {
1849
+ if (written < width) {
1850
+ pixels[dstIndex++] = value;
1851
+ }
1852
+ written += 1;
1853
+ }
1854
+ }
1855
+ }
1856
+ return { width, height, bitsPerPixel, pixels, palette };
1857
+ }
1858
+ function pcxToRgba(image) {
1859
+ const rgba = new Uint8Array(image.width * image.height * 4);
1860
+ for (let i = 0; i < image.pixels.length; i += 1) {
1861
+ const colorIndex = image.pixels[i];
1862
+ const paletteIndex = colorIndex * 3;
1863
+ const rgbaIndex = i * 4;
1864
+ rgba[rgbaIndex] = image.palette[paletteIndex];
1865
+ rgba[rgbaIndex + 1] = image.palette[paletteIndex + 1];
1866
+ rgba[rgbaIndex + 2] = image.palette[paletteIndex + 2];
1867
+ rgba[rgbaIndex + 3] = colorIndex === 255 ? 0 : 255;
1868
+ }
1869
+ return rgba;
1870
+ }
1871
+
1872
+ // src/assets/texture.ts
1873
+ var TextureCache = class {
1874
+ constructor(options = {}) {
1875
+ this.cache = new LruCache(options.capacity ?? 128);
1876
+ }
1877
+ get size() {
1878
+ return this.cache.size;
1879
+ }
1880
+ get(key) {
1881
+ return this.cache.get(key.toLowerCase());
1882
+ }
1883
+ set(key, texture) {
1884
+ this.cache.set(key.toLowerCase(), texture);
1885
+ }
1886
+ clear() {
1887
+ this.cache.clear();
1888
+ }
1889
+ };
1890
+ function walToRgba(wal, palette) {
1891
+ const levels = [];
1892
+ for (const mip of wal.mipmaps) {
1893
+ const rgba = new Uint8Array(mip.width * mip.height * 4);
1894
+ for (let i = 0; i < mip.data.length; i += 1) {
1895
+ const colorIndex = mip.data[i];
1896
+ const paletteIndex = colorIndex * 3;
1897
+ const outIndex = i * 4;
1898
+ rgba[outIndex] = palette[paletteIndex];
1899
+ rgba[outIndex + 1] = palette[paletteIndex + 1];
1900
+ rgba[outIndex + 2] = palette[paletteIndex + 2];
1901
+ rgba[outIndex + 3] = colorIndex === 255 ? 0 : 255;
1902
+ }
1903
+ levels.push({ level: mip.level, width: mip.width, height: mip.height, rgba });
1904
+ }
1905
+ return { width: wal.width, height: wal.height, levels, source: "wal" };
1906
+ }
1907
+ function preparePcxTexture(pcx) {
1908
+ const rgba = pcxToRgba(pcx);
1909
+ const level = { level: 0, width: pcx.width, height: pcx.height, rgba };
1910
+ return { width: pcx.width, height: pcx.height, levels: [level], source: "pcx" };
1911
+ }
1912
+ function parseWalTexture(buffer, palette) {
1913
+ return walToRgba(parseWal(buffer), palette);
1914
+ }
1915
+
1916
+ // src/assets/wav.ts
1917
+ var WavParseError = class extends Error {
1918
+ constructor(message) {
1919
+ super(message);
1920
+ this.name = "WavParseError";
1921
+ }
1922
+ };
1923
+ function readString2(view, offset, length) {
1924
+ return new TextDecoder("ascii").decode(new Uint8Array(view.buffer, view.byteOffset + offset, length));
1925
+ }
1926
+ function parseWav(buffer) {
1927
+ if (buffer.byteLength < 44) {
1928
+ throw new WavParseError("WAV buffer too small");
1929
+ }
1930
+ const view = new DataView(buffer);
1931
+ if (readString2(view, 0, 4) !== "RIFF" || readString2(view, 8, 4) !== "WAVE") {
1932
+ throw new WavParseError("Invalid WAV header");
1933
+ }
1934
+ let offset = 12;
1935
+ let fmtOffset = -1;
1936
+ let dataOffset = -1;
1937
+ let fmtSize = 0;
1938
+ let dataSize = 0;
1939
+ while (offset + 8 <= buffer.byteLength) {
1940
+ const chunkId = readString2(view, offset, 4);
1941
+ const chunkSize = view.getUint32(offset + 4, true);
1942
+ const chunkDataOffset = offset + 8;
1943
+ if (chunkId === "fmt ") {
1944
+ fmtOffset = chunkDataOffset;
1945
+ fmtSize = chunkSize;
1946
+ } else if (chunkId === "data") {
1947
+ dataOffset = chunkDataOffset;
1948
+ dataSize = chunkSize;
1949
+ }
1950
+ offset = chunkDataOffset + chunkSize;
1951
+ }
1952
+ if (fmtOffset === -1 || dataOffset === -1) {
1953
+ throw new WavParseError("Missing fmt or data chunk");
1954
+ }
1955
+ const audioFormat = view.getUint16(fmtOffset, true);
1956
+ const channels = view.getUint16(fmtOffset + 2, true);
1957
+ const sampleRate = view.getUint32(fmtOffset + 4, true);
1958
+ const bitsPerSample = view.getUint16(fmtOffset + 14, true);
1959
+ if (audioFormat !== 1) {
1960
+ throw new WavParseError("Only PCM WAV is supported");
1961
+ }
1962
+ const bytesPerSample = bitsPerSample / 8;
1963
+ const frameCount = dataSize / (bytesPerSample * channels);
1964
+ const samples = new Float32Array(frameCount * channels);
1965
+ for (let frame = 0; frame < frameCount; frame += 1) {
1966
+ for (let ch = 0; ch < channels; ch += 1) {
1967
+ const sampleIndex = frame * channels + ch;
1968
+ const byteOffset = dataOffset + sampleIndex * bytesPerSample;
1969
+ let value = 0;
1970
+ if (bitsPerSample === 8) {
1971
+ value = view.getUint8(byteOffset);
1972
+ samples[sampleIndex] = (value - 128) / 128;
1973
+ } else if (bitsPerSample === 16) {
1974
+ value = view.getInt16(byteOffset, true);
1975
+ samples[sampleIndex] = value / 32768;
1976
+ } else if (bitsPerSample === 24) {
1977
+ const b0 = view.getUint8(byteOffset);
1978
+ const b1 = view.getUint8(byteOffset + 1);
1979
+ const b2 = view.getInt8(byteOffset + 2);
1980
+ value = b0 | b1 << 8 | b2 << 16;
1981
+ samples[sampleIndex] = value / 8388608;
1982
+ } else {
1983
+ throw new WavParseError(`Unsupported bitsPerSample: ${bitsPerSample}`);
1984
+ }
1985
+ }
1986
+ }
1987
+ return { sampleRate, channels, bitsPerSample, samples };
1988
+ }
1989
+
1990
+ // src/assets/ogg.ts
1991
+ var import_ogg_vorbis = require("@wasm-audio-decoders/ogg-vorbis");
1992
+ var OggDecodeError = class extends Error {
1993
+ constructor(message) {
1994
+ super(message);
1995
+ this.name = "OggDecodeError";
1996
+ }
1997
+ };
1998
+ async function decodeOgg(buffer, decoder = new import_ogg_vorbis.OggVorbisDecoder()) {
1999
+ await decoder.ready;
2000
+ const result = await decoder.decode(new Uint8Array(buffer));
2001
+ const errors = result.errors;
2002
+ if (errors && errors.length > 0) {
2003
+ throw new OggDecodeError(errors.map((err) => err.message).join("; "));
2004
+ }
2005
+ return {
2006
+ sampleRate: result.sampleRate,
2007
+ channels: result.channelData.length,
2008
+ bitDepth: result.bitDepth,
2009
+ channelData: result.channelData
2010
+ };
2011
+ }
2012
+
2013
+ // src/assets/audio.ts
2014
+ var AudioRegistryError = class extends Error {
2015
+ constructor(message) {
2016
+ super(message);
2017
+ this.name = "AudioRegistryError";
2018
+ }
2019
+ };
2020
+ var AudioRegistry = class {
2021
+ constructor(vfs, options = {}) {
2022
+ this.vfs = vfs;
2023
+ this.refCounts = /* @__PURE__ */ new Map();
2024
+ this.cache = new LruCache(options.cacheSize ?? 64);
2025
+ }
2026
+ get size() {
2027
+ return this.cache.size;
2028
+ }
2029
+ async load(path) {
2030
+ const normalized = path.toLowerCase();
2031
+ const cached = this.cache.get(normalized);
2032
+ if (cached) {
2033
+ this.refCounts.set(normalized, (this.refCounts.get(normalized) ?? 0) + 1);
2034
+ return cached;
2035
+ }
2036
+ const data = await this.vfs.readFile(path);
2037
+ const arrayBuffer = data.slice().buffer;
2038
+ const audio = await this.decodeByExtension(path, arrayBuffer);
2039
+ this.cache.set(normalized, audio);
2040
+ this.refCounts.set(normalized, 1);
2041
+ return audio;
2042
+ }
2043
+ release(path) {
2044
+ const normalized = path.toLowerCase();
2045
+ const count = this.refCounts.get(normalized) ?? 0;
2046
+ if (count <= 1) {
2047
+ this.cache.delete(normalized);
2048
+ this.refCounts.delete(normalized);
2049
+ } else {
2050
+ this.refCounts.set(normalized, count - 1);
2051
+ }
2052
+ }
2053
+ clearAll() {
2054
+ this.cache.clear();
2055
+ this.refCounts.clear();
2056
+ }
2057
+ async decodeByExtension(path, buffer) {
2058
+ const lower = path.toLowerCase();
2059
+ if (lower.endsWith(".wav")) {
2060
+ const wav = parseWav(buffer);
2061
+ const channels = wav.channels;
2062
+ const channelData = Array.from({ length: channels }, () => new Float32Array(wav.samples.length / channels));
2063
+ for (let i = 0; i < wav.samples.length; i += 1) {
2064
+ channelData[i % channels][Math.floor(i / channels)] = wav.samples[i];
2065
+ }
2066
+ return { sampleRate: wav.sampleRate, channels, bitDepth: wav.bitsPerSample, channelData };
2067
+ }
2068
+ if (lower.endsWith(".ogg") || lower.endsWith(".oga")) {
2069
+ return decodeOgg(buffer);
2070
+ }
2071
+ throw new AudioRegistryError(`Unsupported audio format: ${path}`);
2072
+ }
2073
+ };
2074
+
2075
+ // src/assets/pakIndexStore.ts
2076
+ var DEFAULT_DB_NAME = "quake2ts-pak-indexes";
2077
+ var DEFAULT_STORE_NAME = "pak-indexes";
2078
+ function getIndexedDb() {
2079
+ if (typeof indexedDB !== "undefined") {
2080
+ return indexedDB;
2081
+ }
2082
+ if (typeof window !== "undefined" && "indexedDB" in window) {
2083
+ return window.indexedDB;
2084
+ }
2085
+ if (typeof globalThis !== "undefined" && "indexedDB" in globalThis) {
2086
+ return globalThis.indexedDB;
2087
+ }
2088
+ return void 0;
2089
+ }
2090
+ function openDatabase(dbName, storeName) {
2091
+ const idb = getIndexedDb();
2092
+ if (!idb) {
2093
+ return Promise.reject(new Error("IndexedDB is not available in this environment"));
2094
+ }
2095
+ return new Promise((resolve, reject) => {
2096
+ const request = idb.open(dbName, 1);
2097
+ request.onupgradeneeded = () => {
2098
+ const { result } = request;
2099
+ if (!result.objectStoreNames.contains(storeName)) {
2100
+ result.createObjectStore(storeName, { keyPath: "key" });
2101
+ }
2102
+ };
2103
+ request.onerror = () => reject(request.error ?? new Error("Unknown IndexedDB error"));
2104
+ request.onsuccess = () => resolve(request.result);
2105
+ });
2106
+ }
2107
+ function runTransaction(db, storeName, mode, runner) {
2108
+ return new Promise((resolve, reject) => {
2109
+ const transaction = db.transaction(storeName, mode);
2110
+ const store = transaction.objectStore(storeName);
2111
+ const request = runner(store);
2112
+ request.onsuccess = () => resolve(request.result);
2113
+ request.onerror = () => reject(request.error ?? new Error("IndexedDB transaction error"));
2114
+ });
2115
+ }
2116
+ function buildKey(name, checksum) {
2117
+ return `${normalizePath(name)}:${checksum.toString(16)}`;
2118
+ }
2119
+ function cloneEntries(entries) {
2120
+ return entries.map((entry) => ({ ...entry }));
2121
+ }
2122
+ var PakIndexStore = class {
2123
+ constructor(dbName = DEFAULT_DB_NAME, storeName = DEFAULT_STORE_NAME) {
2124
+ this.dbName = dbName;
2125
+ this.storeName = storeName;
2126
+ }
2127
+ get isSupported() {
2128
+ return Boolean(getIndexedDb());
2129
+ }
2130
+ async persist(archive) {
2131
+ if (!this.isSupported) {
2132
+ return void 0;
2133
+ }
2134
+ const validation = archive.validate();
2135
+ const record = {
2136
+ ...validation,
2137
+ key: buildKey(archive.name, validation.checksum),
2138
+ name: archive.name,
2139
+ size: archive.size,
2140
+ persistedAt: Date.now(),
2141
+ entries: cloneEntries(validation.entries)
2142
+ };
2143
+ const db = await openDatabase(this.dbName, this.storeName);
2144
+ await runTransaction(db, this.storeName, "readwrite", (store) => store.put(record));
2145
+ db.close();
2146
+ return record;
2147
+ }
2148
+ async find(name, checksum) {
2149
+ if (!this.isSupported) {
2150
+ return void 0;
2151
+ }
2152
+ const db = await openDatabase(this.dbName, this.storeName);
2153
+ const key = checksum !== void 0 ? buildKey(name, checksum) : void 0;
2154
+ const record = await runTransaction(db, this.storeName, "readonly", (store) => {
2155
+ if (key) {
2156
+ return store.get(key);
2157
+ }
2158
+ return store.getAll();
2159
+ });
2160
+ db.close();
2161
+ if (!record) {
2162
+ return void 0;
2163
+ }
2164
+ if (Array.isArray(record)) {
2165
+ const normalized = normalizePath(name);
2166
+ const matches = record.filter((candidate) => normalizePath(candidate.name) === normalized);
2167
+ if (matches.length === 0) {
2168
+ return void 0;
2169
+ }
2170
+ return matches.sort((a, b) => b.persistedAt - a.persistedAt)[0];
2171
+ }
2172
+ return record;
2173
+ }
2174
+ async remove(name, checksum) {
2175
+ if (!this.isSupported) {
2176
+ return false;
2177
+ }
2178
+ const db = await openDatabase(this.dbName, this.storeName);
2179
+ const key = checksum !== void 0 ? buildKey(name, checksum) : void 0;
2180
+ const result = await runTransaction(db, this.storeName, "readwrite", (store) => {
2181
+ if (key) {
2182
+ return store.delete(key);
2183
+ }
2184
+ const prefix = `${normalizePath(name)}:`;
2185
+ return store.delete(IDBKeyRange.bound(prefix, `${prefix}\uFFFF`, false, true));
2186
+ });
2187
+ db.close();
2188
+ return typeof result === "number" ? result > 0 : true;
2189
+ }
2190
+ async clear() {
2191
+ if (!this.isSupported) {
2192
+ return;
2193
+ }
2194
+ const db = await openDatabase(this.dbName, this.storeName);
2195
+ await runTransaction(db, this.storeName, "readwrite", (store) => store.clear());
2196
+ db.close();
2197
+ }
2198
+ async list() {
2199
+ if (!this.isSupported) {
2200
+ return [];
2201
+ }
2202
+ const db = await openDatabase(this.dbName, this.storeName);
2203
+ const result = await runTransaction(db, this.storeName, "readonly", (store) => store.getAll());
2204
+ db.close();
2205
+ return result.sort((a, b) => b.persistedAt - a.persistedAt);
2206
+ }
2207
+ };
2208
+
2209
+ // src/assets/manager.ts
2210
+ var AssetDependencyError = class extends Error {
2211
+ constructor(missing, message) {
2212
+ super(message ?? `Missing dependencies: ${missing.join(", ")}`);
2213
+ this.missing = missing;
2214
+ this.name = "AssetDependencyError";
2215
+ }
2216
+ };
2217
+ var AssetDependencyTracker = class {
2218
+ constructor() {
2219
+ this.nodes = /* @__PURE__ */ new Map();
2220
+ }
2221
+ register(assetKey, dependencies = []) {
2222
+ const node = this.nodes.get(assetKey) ?? { dependencies: /* @__PURE__ */ new Set(), loaded: false };
2223
+ dependencies.forEach((dependency) => node.dependencies.add(dependency));
2224
+ this.nodes.set(assetKey, node);
2225
+ dependencies.forEach((dependency) => {
2226
+ if (!this.nodes.has(dependency)) {
2227
+ this.nodes.set(dependency, { dependencies: /* @__PURE__ */ new Set(), loaded: false });
2228
+ }
2229
+ });
2230
+ }
2231
+ markLoaded(assetKey) {
2232
+ const node = this.nodes.get(assetKey) ?? { dependencies: /* @__PURE__ */ new Set(), loaded: false };
2233
+ const missing = this.getMissingDependencies(assetKey, node);
2234
+ if (missing.length > 0) {
2235
+ throw new AssetDependencyError(missing, `Asset ${assetKey} is missing dependencies: ${missing.join(", ")}`);
2236
+ }
2237
+ node.loaded = true;
2238
+ this.nodes.set(assetKey, node);
2239
+ }
2240
+ markUnloaded(assetKey) {
2241
+ const node = this.nodes.get(assetKey);
2242
+ if (node) {
2243
+ node.loaded = false;
2244
+ }
2245
+ }
2246
+ isLoaded(assetKey) {
2247
+ return this.nodes.get(assetKey)?.loaded ?? false;
2248
+ }
2249
+ missingDependencies(assetKey) {
2250
+ const node = this.nodes.get(assetKey);
2251
+ if (!node) {
2252
+ return [];
2253
+ }
2254
+ return this.getMissingDependencies(assetKey, node);
2255
+ }
2256
+ reset() {
2257
+ this.nodes.clear();
2258
+ }
2259
+ getMissingDependencies(assetKey, node) {
2260
+ const missing = [];
2261
+ for (const dependency of node.dependencies) {
2262
+ if (!this.nodes.get(dependency)?.loaded) {
2263
+ missing.push(dependency);
2264
+ }
2265
+ }
2266
+ return missing;
2267
+ }
2268
+ };
2269
+ var AssetManager = class {
2270
+ constructor(vfs, options = {}) {
2271
+ this.vfs = vfs;
2272
+ this.textures = new TextureCache({ capacity: options.textureCacheCapacity ?? 128 });
2273
+ this.audio = new AudioRegistry(vfs, { cacheSize: options.audioCacheSize ?? 64 });
2274
+ this.dependencyTracker = options.dependencyTracker ?? new AssetDependencyTracker();
2275
+ this.md2 = new Md2Loader(vfs);
2276
+ this.md3 = new Md3Loader(vfs);
2277
+ }
2278
+ isAssetLoaded(type, path) {
2279
+ return this.dependencyTracker.isLoaded(this.makeKey(type, path));
2280
+ }
2281
+ registerTexture(path, texture) {
2282
+ this.textures.set(path, texture);
2283
+ const key = this.makeKey("texture", path);
2284
+ this.dependencyTracker.register(key);
2285
+ this.dependencyTracker.markLoaded(key);
2286
+ }
2287
+ async loadSound(path) {
2288
+ const audio = await this.audio.load(path);
2289
+ const key = this.makeKey("sound", path);
2290
+ this.dependencyTracker.register(key);
2291
+ this.dependencyTracker.markLoaded(key);
2292
+ return audio;
2293
+ }
2294
+ async loadMd2Model(path, textureDependencies = []) {
2295
+ const modelKey = this.makeKey("model", path);
2296
+ const dependencyKeys = textureDependencies.map((dep) => this.makeKey("texture", dep));
2297
+ this.dependencyTracker.register(modelKey, dependencyKeys);
2298
+ const missing = this.dependencyTracker.missingDependencies(modelKey);
2299
+ if (missing.length > 0) {
2300
+ throw new AssetDependencyError(missing, `Asset ${modelKey} is missing dependencies: ${missing.join(", ")}`);
2301
+ }
2302
+ const model = await this.md2.load(path);
2303
+ this.dependencyTracker.markLoaded(modelKey);
2304
+ return model;
2305
+ }
2306
+ async loadMd3Model(path, textureDependencies = []) {
2307
+ const modelKey = this.makeKey("model", path);
2308
+ const dependencyKeys = textureDependencies.map((dep) => this.makeKey("texture", dep));
2309
+ this.dependencyTracker.register(modelKey, dependencyKeys);
2310
+ const missing = this.dependencyTracker.missingDependencies(modelKey);
2311
+ if (missing.length > 0) {
2312
+ throw new AssetDependencyError(missing, `Asset ${modelKey} is missing dependencies: ${missing.join(", ")}`);
2313
+ }
2314
+ const model = await this.md3.load(path);
2315
+ this.dependencyTracker.markLoaded(modelKey);
2316
+ return model;
2317
+ }
2318
+ resetForLevelChange() {
2319
+ this.textures.clear();
2320
+ this.audio.clearAll();
2321
+ this.dependencyTracker.reset();
2322
+ }
2323
+ makeKey(type, path) {
2324
+ return `${type}:${normalizePath(path)}`;
2325
+ }
2326
+ };
2327
+
2328
+ // src/audio/constants.ts
2329
+ var MAX_SOUND_CHANNELS = 32;
2330
+ var SoundChannel = /* @__PURE__ */ ((SoundChannel2) => {
2331
+ SoundChannel2[SoundChannel2["Auto"] = 0] = "Auto";
2332
+ SoundChannel2[SoundChannel2["Weapon"] = 1] = "Weapon";
2333
+ SoundChannel2[SoundChannel2["Voice"] = 2] = "Voice";
2334
+ SoundChannel2[SoundChannel2["Item"] = 3] = "Item";
2335
+ SoundChannel2[SoundChannel2["Body"] = 4] = "Body";
2336
+ SoundChannel2[SoundChannel2["Aux"] = 5] = "Aux";
2337
+ SoundChannel2[SoundChannel2["Footstep"] = 6] = "Footstep";
2338
+ SoundChannel2[SoundChannel2["Aux3"] = 7] = "Aux3";
2339
+ SoundChannel2[SoundChannel2["NoPhsAdd"] = 8] = "NoPhsAdd";
2340
+ SoundChannel2[SoundChannel2["Reliable"] = 16] = "Reliable";
2341
+ SoundChannel2[SoundChannel2["ForcePos"] = 32] = "ForcePos";
2342
+ return SoundChannel2;
2343
+ })(SoundChannel || {});
2344
+ var ATTN_LOOP_NONE = -1;
2345
+ var ATTN_NONE = 0;
2346
+ var ATTN_NORM = 1;
2347
+ var ATTN_IDLE = 2;
2348
+ var ATTN_STATIC = 3;
2349
+ var SOUND_FULLVOLUME = 80;
2350
+ var SOUND_LOOP_ATTENUATE = 3e-3;
2351
+ function attenuationToDistanceMultiplier(attenuation) {
2352
+ return attenuation === ATTN_STATIC ? attenuation * 1e-3 : attenuation * 5e-4;
2353
+ }
2354
+ function calculateMaxAudibleDistance(attenuation) {
2355
+ const distMult = attenuationToDistanceMultiplier(attenuation);
2356
+ return distMult <= 0 ? Number.POSITIVE_INFINITY : SOUND_FULLVOLUME + 1 / distMult;
2357
+ }
2358
+
2359
+ // src/audio/context.ts
2360
+ var AudioContextController = class {
2361
+ constructor(factory) {
2362
+ this.factory = factory;
2363
+ }
2364
+ getContext() {
2365
+ if (!this.context) {
2366
+ this.context = this.factory();
2367
+ }
2368
+ return this.context;
2369
+ }
2370
+ async resume() {
2371
+ const ctx = this.getContext();
2372
+ if (ctx.state === "suspended") {
2373
+ await ctx.resume();
2374
+ }
2375
+ }
2376
+ getState() {
2377
+ return this.context?.state ?? "suspended";
2378
+ }
2379
+ };
2380
+ function createAudioGraph(controller) {
2381
+ const context = controller.getContext();
2382
+ const master = context.createGain();
2383
+ master.gain.value = 1;
2384
+ const compressor = context.createDynamicsCompressor();
2385
+ const filter = context.createBiquadFilter?.();
2386
+ if (filter) {
2387
+ filter.type = "lowpass";
2388
+ filter.frequency.value = 2e4;
2389
+ filter.Q.value = 0.707;
2390
+ master.connect(filter);
2391
+ filter.connect(compressor);
2392
+ } else {
2393
+ master.connect(compressor);
2394
+ }
2395
+ compressor.connect(context.destination);
2396
+ return { context, master, compressor, filter };
2397
+ }
2398
+
2399
+ // src/audio/registry.ts
2400
+ var SoundRegistry = class {
2401
+ constructor(configStrings = new ConfigStringRegistry()) {
2402
+ this.configStrings = configStrings;
2403
+ this.buffers = /* @__PURE__ */ new Map();
2404
+ }
2405
+ registerName(name) {
2406
+ return this.configStrings.soundIndex(name);
2407
+ }
2408
+ register(name, buffer) {
2409
+ const index = this.registerName(name);
2410
+ this.buffers.set(index, buffer);
2411
+ return index;
2412
+ }
2413
+ find(name) {
2414
+ return this.configStrings.findSoundIndex(name);
2415
+ }
2416
+ get(index) {
2417
+ return this.buffers.get(index);
2418
+ }
2419
+ has(index) {
2420
+ return this.buffers.has(index);
2421
+ }
2422
+ };
2423
+
2424
+ // src/audio/precache.ts
2425
+ var SoundPrecache = class {
2426
+ constructor(options) {
2427
+ this.vfs = options.vfs;
2428
+ this.registry = options.registry;
2429
+ this.contextController = options.context;
2430
+ this.soundRoot = options.soundRoot ?? "sound/";
2431
+ this.decodeAudio = options.decodeAudio ?? ((context, data) => {
2432
+ if (!context.decodeAudioData) {
2433
+ throw new Error("decodeAudioData is not available on the provided audio context");
2434
+ }
2435
+ return context.decodeAudioData(data);
2436
+ });
2437
+ }
2438
+ async precache(paths) {
2439
+ const unique = [...new Set(paths.map((p) => this.normalize(p)))];
2440
+ const report = { loaded: [], skipped: [], missing: [], errors: {} };
2441
+ const context = this.contextController.getContext();
2442
+ for (const path of unique) {
2443
+ try {
2444
+ const existingIndex = this.registry.find(path);
2445
+ if (existingIndex !== void 0 && this.registry.has(existingIndex)) {
2446
+ report.skipped.push(path);
2447
+ continue;
2448
+ }
2449
+ const stat = this.vfs.stat(path);
2450
+ if (!stat) {
2451
+ report.missing.push(path);
2452
+ continue;
2453
+ }
2454
+ const bytes = await this.vfs.readFile(path);
2455
+ const copy = bytes.slice().buffer;
2456
+ const buffer = await this.decodeAudio(context, copy);
2457
+ this.registry.register(path, buffer);
2458
+ report.loaded.push(path);
2459
+ } catch (error) {
2460
+ const err = error instanceof Error ? error : new Error(String(error));
2461
+ report.errors[path] = err;
2462
+ }
2463
+ }
2464
+ return report;
2465
+ }
2466
+ normalize(path) {
2467
+ const normalized = normalizePath(path.replace(/^\//, ""));
2468
+ if (normalized.startsWith(this.soundRoot)) {
2469
+ return normalized;
2470
+ }
2471
+ return normalizePath(`${this.soundRoot}${normalized}`);
2472
+ }
2473
+ };
2474
+
2475
+ // src/audio/channels.ts
2476
+ var CHANNEL_MASK = 7;
2477
+ var baseChannel = (entchannel) => entchannel & CHANNEL_MASK;
2478
+ function createInitialChannels(playerEntity) {
2479
+ return Array.from({ length: MAX_SOUND_CHANNELS }, () => ({
2480
+ entnum: 0,
2481
+ entchannel: 0 /* Auto */,
2482
+ endTimeMs: 0,
2483
+ isPlayer: false,
2484
+ active: false
2485
+ })).map((channel) => ({ ...channel, isPlayer: channel.entnum === playerEntity }));
2486
+ }
2487
+ function pickChannel(channels, entnum, entchannel, context) {
2488
+ if (entchannel < 0) {
2489
+ throw new Error("pickChannel: entchannel must be non-negative");
2490
+ }
2491
+ const normalizedEntchannel = baseChannel(entchannel);
2492
+ let firstToDie = -1;
2493
+ let lifeLeft = Number.POSITIVE_INFINITY;
2494
+ for (let i = 0; i < channels.length; i += 1) {
2495
+ const channel = channels[i];
2496
+ const channelBase = baseChannel(channel.entchannel);
2497
+ if (normalizedEntchannel !== 0 /* Auto */ && channel.entnum === entnum && channelBase === normalizedEntchannel) {
2498
+ firstToDie = i;
2499
+ break;
2500
+ }
2501
+ if (channel.active && channel.entnum === context.playerEntity && entnum !== context.playerEntity) {
2502
+ continue;
2503
+ }
2504
+ const remainingLife = channel.endTimeMs - context.nowMs;
2505
+ if (firstToDie === -1 || remainingLife < lifeLeft) {
2506
+ lifeLeft = remainingLife;
2507
+ firstToDie = i;
2508
+ }
2509
+ }
2510
+ return firstToDie === -1 ? void 0 : firstToDie;
2511
+ }
2512
+
2513
+ // src/audio/spatialization.ts
2514
+ function spatializeOrigin(origin, listener, masterVolume, attenuation, isListenerSound) {
2515
+ if (isListenerSound) {
2516
+ return { left: masterVolume, right: masterVolume, distanceComponent: 0 };
2517
+ }
2518
+ const sourceVec = subtractVec3(origin, listener.origin);
2519
+ const distance = lengthVec3(sourceVec);
2520
+ const normalized = normalizeVec3(sourceVec);
2521
+ let dist = distance - SOUND_FULLVOLUME;
2522
+ if (dist < 0) dist = 0;
2523
+ dist *= attenuationToDistanceMultiplier(attenuation);
2524
+ const dot = dotVec3(listener.right, normalized);
2525
+ const mono = listener.mono ?? false;
2526
+ const rscale = mono || attenuation === 0 ? 1 : 0.5 * (1 + dot);
2527
+ const lscale = mono || attenuation === 0 ? 1 : 0.5 * (1 - dot);
2528
+ const right = Math.max(0, Math.floor(masterVolume * (1 - dist) * rscale));
2529
+ const left = Math.max(0, Math.floor(masterVolume * (1 - dist) * lscale));
2530
+ return { left, right, distanceComponent: dist };
2531
+ }
2532
+
2533
+ // src/audio/system.ts
2534
+ var AudioSystem = class {
2535
+ constructor(options) {
2536
+ this.activeSources = /* @__PURE__ */ new Map();
2537
+ this.contextController = options.context;
2538
+ this.registry = options.registry;
2539
+ this.playerEntity = options.playerEntity;
2540
+ this.channels = createInitialChannels(options.playerEntity);
2541
+ this.listener = options.listener ?? { origin: ZERO_VEC3, right: { x: 1, y: 0, z: 0 } };
2542
+ this.sfxVolume = options.sfxVolume ?? 1;
2543
+ this.masterVolume = options.masterVolume ?? 1;
2544
+ this.resolveOcclusion = options.resolveOcclusion;
2545
+ this.graph = createAudioGraph(this.contextController);
2546
+ this.graph.master.gain.value = this.masterVolume;
2547
+ }
2548
+ setListener(listener) {
2549
+ this.listener = listener;
2550
+ }
2551
+ setMasterVolume(volume) {
2552
+ this.masterVolume = volume;
2553
+ this.graph.master.gain.value = volume;
2554
+ }
2555
+ setSfxVolume(volume) {
2556
+ this.sfxVolume = volume;
2557
+ }
2558
+ async ensureRunning() {
2559
+ await this.contextController.resume();
2560
+ }
2561
+ play(request) {
2562
+ const buffer = this.registry.get(request.soundIndex);
2563
+ if (!buffer) return void 0;
2564
+ const ctx = this.graph.context;
2565
+ const nowMs = ctx.currentTime * 1e3;
2566
+ const channelIndex = pickChannel(this.channels, request.entity, request.channel, {
2567
+ nowMs,
2568
+ playerEntity: this.playerEntity
2569
+ });
2570
+ if (channelIndex === void 0) return void 0;
2571
+ const existing = this.activeSources.get(channelIndex);
2572
+ if (existing) {
2573
+ existing.source.onended = null;
2574
+ existing.source.stop();
2575
+ this.activeSources.delete(channelIndex);
2576
+ }
2577
+ const source = ctx.createBufferSource();
2578
+ source.buffer = buffer;
2579
+ source.loop = request.looping ?? false;
2580
+ const origin = request.origin ?? this.listener.origin;
2581
+ const gain = ctx.createGain();
2582
+ const panner = this.createPanner(ctx, request.attenuation);
2583
+ const occlusion = this.resolveOcclusion?.(this.listener, origin, request.attenuation);
2584
+ const occlusionScale = clamp01(occlusion?.gainScale ?? 1);
2585
+ const occlusionFilter = this.resolveOcclusion ? this.createOcclusionFilter(ctx, occlusion?.lowpassHz ?? 2e4) : void 0;
2586
+ this.applyOriginToPanner(panner, origin);
2587
+ const isListenerSound = request.entity === this.playerEntity;
2588
+ const spatial = spatializeOrigin(origin, this.listener, request.volume, request.attenuation, isListenerSound);
2589
+ const attenuationScale = request.volume === 0 ? 0 : Math.max(spatial.left, spatial.right) / Math.max(1, request.volume);
2590
+ const gainValue = attenuationScale * (request.volume / 255) * this.masterVolume * this.sfxVolume;
2591
+ gain.gain.value = gainValue * occlusionScale;
2592
+ const startTimeSec = ctx.currentTime + (request.timeOffsetMs ?? 0) / 1e3;
2593
+ const endTimeMs = (request.looping ? Number.POSITIVE_INFINITY : buffer.duration * 1e3) + startTimeSec * 1e3;
2594
+ source.connect(panner);
2595
+ if (occlusionFilter) {
2596
+ panner.connect(occlusionFilter);
2597
+ occlusionFilter.connect(gain);
2598
+ } else {
2599
+ panner.connect(gain);
2600
+ }
2601
+ gain.connect(this.graph.master);
2602
+ source.start(startTimeSec);
2603
+ source.onended = () => {
2604
+ this.channels[channelIndex].active = false;
2605
+ this.activeSources.delete(channelIndex);
2606
+ };
2607
+ const active = {
2608
+ channelIndex,
2609
+ entnum: request.entity,
2610
+ entchannel: baseChannel(request.channel),
2611
+ endTimeMs,
2612
+ source,
2613
+ panner,
2614
+ gain,
2615
+ baseGain: gainValue,
2616
+ origin,
2617
+ attenuation: request.attenuation,
2618
+ occlusion: occlusionFilter ? { scale: occlusionScale, lowpassHz: occlusion?.lowpassHz, filter: occlusionFilter } : occlusion ? { scale: occlusionScale, lowpassHz: occlusion.lowpassHz } : void 0
2619
+ };
2620
+ this.channels[channelIndex] = {
2621
+ entnum: request.entity,
2622
+ entchannel: baseChannel(request.channel),
2623
+ endTimeMs,
2624
+ isPlayer: request.entity === this.playerEntity,
2625
+ active: true
2626
+ };
2627
+ this.activeSources.set(channelIndex, active);
2628
+ return active;
2629
+ }
2630
+ stop(channelIndex) {
2631
+ const active = this.activeSources.get(channelIndex);
2632
+ if (!active) return;
2633
+ active.source.stop();
2634
+ this.channels[channelIndex].active = false;
2635
+ this.activeSources.delete(channelIndex);
2636
+ }
2637
+ stopEntitySounds(entnum) {
2638
+ for (const [index, active] of [...this.activeSources.entries()]) {
2639
+ if (active.entnum !== entnum) continue;
2640
+ active.source.stop();
2641
+ this.channels[index].active = false;
2642
+ this.activeSources.delete(index);
2643
+ }
2644
+ }
2645
+ updateEntityPosition(entnum, origin) {
2646
+ for (const active of this.activeSources.values()) {
2647
+ if (active.entnum !== entnum) continue;
2648
+ this.applyOriginToPanner(active.panner, origin);
2649
+ active.origin = origin;
2650
+ if (this.resolveOcclusion) {
2651
+ const occlusion = this.resolveOcclusion(this.listener, origin, active.attenuation);
2652
+ this.applyOcclusion(active, occlusion);
2653
+ }
2654
+ }
2655
+ }
2656
+ positionedSound(origin, soundIndex, volume, attenuation) {
2657
+ return this.play({
2658
+ entity: 0,
2659
+ channel: 0 /* Auto */,
2660
+ soundIndex,
2661
+ volume,
2662
+ attenuation,
2663
+ origin
2664
+ });
2665
+ }
2666
+ ambientSound(origin, soundIndex, volume) {
2667
+ return this.play({
2668
+ entity: 0,
2669
+ channel: 0 /* Auto */,
2670
+ soundIndex,
2671
+ volume,
2672
+ attenuation: ATTN_NONE,
2673
+ origin,
2674
+ looping: true
2675
+ });
2676
+ }
2677
+ getChannelState(index) {
2678
+ return this.channels[index];
2679
+ }
2680
+ getDiagnostics() {
2681
+ return {
2682
+ activeChannels: this.activeSources.size,
2683
+ masterVolume: this.masterVolume,
2684
+ sfxVolume: this.sfxVolume,
2685
+ channels: [...this.channels],
2686
+ activeSounds: [...this.activeSources.values()].map((sound) => ({
2687
+ entnum: sound.entnum,
2688
+ entchannel: sound.entchannel,
2689
+ channelIndex: sound.channelIndex,
2690
+ origin: sound.origin,
2691
+ gain: sound.gain.gain.value,
2692
+ baseGain: sound.baseGain,
2693
+ attenuation: sound.attenuation,
2694
+ maxDistance: sound.panner.maxDistance,
2695
+ distanceModel: sound.panner.distanceModel,
2696
+ occlusion: sound.occlusion ? { scale: sound.occlusion.scale, lowpassHz: sound.occlusion.lowpassHz } : void 0
2697
+ }))
2698
+ };
2699
+ }
2700
+ setUnderwater(enabled, cutoffHz = 400) {
2701
+ const filter = this.graph.filter;
2702
+ if (!filter) return;
2703
+ filter.type = "lowpass";
2704
+ filter.Q.value = 0.707;
2705
+ filter.frequency.value = enabled ? cutoffHz : 2e4;
2706
+ }
2707
+ createPanner(context, attenuation) {
2708
+ const panner = context.createPanner ? context.createPanner() : Object.assign(context.createGain(), {
2709
+ positionX: { value: this.listener.origin.x },
2710
+ positionY: { value: this.listener.origin.y },
2711
+ positionZ: { value: this.listener.origin.z }
2712
+ });
2713
+ return this.configurePanner(panner, attenuation);
2714
+ }
2715
+ configurePanner(panner, attenuation) {
2716
+ const distMult = attenuationToDistanceMultiplier(attenuation);
2717
+ panner.refDistance = SOUND_FULLVOLUME;
2718
+ panner.maxDistance = calculateMaxAudibleDistance(attenuation);
2719
+ panner.rolloffFactor = distMult;
2720
+ panner.distanceModel = attenuation === 0 ? "linear" : "inverse";
2721
+ panner.positionX.value = this.listener.origin.x;
2722
+ panner.positionY.value = this.listener.origin.y;
2723
+ panner.positionZ.value = this.listener.origin.z;
2724
+ return panner;
2725
+ }
2726
+ applyOriginToPanner(panner, origin) {
2727
+ panner.positionX.value = origin.x;
2728
+ panner.positionY.value = origin.y;
2729
+ panner.positionZ.value = origin.z;
2730
+ }
2731
+ createOcclusionFilter(context, cutoffHz) {
2732
+ if (!context.createBiquadFilter) return void 0;
2733
+ const filter = context.createBiquadFilter();
2734
+ filter.type = "lowpass";
2735
+ filter.Q.value = 0.707;
2736
+ filter.frequency.value = clamp(cutoffHz, 10, 2e4);
2737
+ return filter;
2738
+ }
2739
+ applyOcclusion(active, occlusion) {
2740
+ const scale = clamp01(occlusion?.gainScale ?? 1);
2741
+ active.gain.gain.value = active.baseGain * scale;
2742
+ if (active.occlusion?.filter) {
2743
+ const cutoff = occlusion?.lowpassHz ?? 2e4;
2744
+ active.occlusion.filter.frequency.value = clamp(cutoff, 10, 2e4);
2745
+ }
2746
+ if (active.occlusion) {
2747
+ active.occlusion.scale = scale;
2748
+ active.occlusion.lowpassHz = occlusion?.lowpassHz;
2749
+ } else if (occlusion) {
2750
+ active.occlusion = { scale, lowpassHz: occlusion.lowpassHz };
2751
+ }
2752
+ }
2753
+ };
2754
+ var clamp = (value, min, max) => Math.min(max, Math.max(min, value));
2755
+ var clamp01 = (value) => clamp(value, 0, 1);
2756
+
2757
+ // src/audio/music.ts
2758
+ var MusicSystem = class {
2759
+ constructor(options) {
2760
+ this.createElement = options.createElement;
2761
+ this.resolveSource = options.resolveSource ?? (async (path) => path);
2762
+ this.volume = options.volume ?? 1;
2763
+ }
2764
+ async play(track, { loop = true, restart = false } = {}) {
2765
+ if (this.track === track && this.element) {
2766
+ this.element.loop = loop;
2767
+ this.element.volume = this.volume;
2768
+ if (restart) {
2769
+ this.element.currentTime = 0;
2770
+ }
2771
+ if (this.element.paused || restart) {
2772
+ await this.element.play();
2773
+ }
2774
+ return;
2775
+ }
2776
+ const src = await this.resolveSource(track);
2777
+ const element = this.createElement();
2778
+ element.src = src;
2779
+ element.loop = loop;
2780
+ element.volume = this.volume;
2781
+ element.currentTime = 0;
2782
+ element.load();
2783
+ await element.play();
2784
+ this.element = element;
2785
+ this.track = track;
2786
+ }
2787
+ pause() {
2788
+ if (!this.element || this.element.paused) return;
2789
+ this.element.pause();
2790
+ }
2791
+ async resume() {
2792
+ if (!this.element || !this.element.paused) return;
2793
+ await this.element.play();
2794
+ }
2795
+ stop() {
2796
+ if (!this.element) return;
2797
+ this.element.pause();
2798
+ this.element.currentTime = 0;
2799
+ this.element = void 0;
2800
+ this.track = void 0;
2801
+ }
2802
+ setVolume(volume) {
2803
+ this.volume = volume;
2804
+ if (this.element) {
2805
+ this.element.volume = volume;
2806
+ }
2807
+ }
2808
+ getState() {
2809
+ const playing = Boolean(this.element && !this.element.paused && !this.element.ended);
2810
+ const paused = Boolean(this.element?.paused);
2811
+ return { track: this.track, paused, playing, volume: this.volume };
2812
+ }
2813
+ };
2814
+
2815
+ // src/audio/api.ts
2816
+ var AudioApi = class {
2817
+ constructor(options) {
2818
+ this.registry = options.registry;
2819
+ this.system = options.system;
2820
+ this.music = options.music;
2821
+ }
2822
+ soundindex(name) {
2823
+ return this.registry.registerName(name);
2824
+ }
2825
+ sound(entity, channel, soundindex, volume, attenuation, timeofs) {
2826
+ this.system.play({
2827
+ entity,
2828
+ channel,
2829
+ soundIndex: soundindex,
2830
+ volume,
2831
+ attenuation,
2832
+ timeOffsetMs: timeofs
2833
+ });
2834
+ }
2835
+ positioned_sound(origin, soundindex, volume, attenuation) {
2836
+ this.system.positionedSound(origin, soundindex, volume, attenuation);
2837
+ }
2838
+ loop_sound(entity, channel, soundindex, volume, attenuation) {
2839
+ this.system.play({
2840
+ entity,
2841
+ channel,
2842
+ soundIndex: soundindex,
2843
+ volume,
2844
+ attenuation,
2845
+ looping: true
2846
+ });
2847
+ }
2848
+ stop_entity_sounds(entnum) {
2849
+ this.system.stopEntitySounds(entnum);
2850
+ }
2851
+ set_listener(listener) {
2852
+ this.system.setListener(listener);
2853
+ }
2854
+ play_music(track, loop = true) {
2855
+ if (!this.music) {
2856
+ return Promise.resolve();
2857
+ }
2858
+ return this.music.play(track, { loop });
2859
+ }
2860
+ pause_music() {
2861
+ this.music?.pause();
2862
+ }
2863
+ resume_music() {
2864
+ return this.music?.resume() ?? Promise.resolve();
2865
+ }
2866
+ stop_music() {
2867
+ this.music?.stop();
2868
+ }
2869
+ set_music_volume(volume) {
2870
+ this.music?.setVolume(volume);
2871
+ }
2872
+ play_ambient(origin, soundindex, volume) {
2873
+ this.system.ambientSound(origin, soundindex, volume);
2874
+ }
2875
+ play_channel(request) {
2876
+ this.system.play({ ...request });
2877
+ }
2878
+ };
2879
+
1301
2880
  // src/render/context.ts
1302
2881
  function configureDefaultGLState(gl) {
1303
2882
  gl.enable(gl.DEPTH_TEST);
@@ -1559,6 +3138,43 @@ var Texture2D = class {
1559
3138
  this.gl.deleteTexture(this.texture);
1560
3139
  }
1561
3140
  };
3141
+ var TextureCubeMap = class {
3142
+ constructor(gl) {
3143
+ this.gl = gl;
3144
+ this.target = gl.TEXTURE_CUBE_MAP;
3145
+ const texture = gl.createTexture();
3146
+ if (!texture) {
3147
+ throw new Error("Failed to allocate cubemap texture");
3148
+ }
3149
+ this.texture = texture;
3150
+ }
3151
+ bind(unit = 0) {
3152
+ this.gl.activeTexture(this.gl.TEXTURE0 + unit);
3153
+ this.gl.bindTexture(this.target, this.texture);
3154
+ }
3155
+ setParameters(params) {
3156
+ this.bind();
3157
+ if (params.wrapS !== void 0) {
3158
+ this.gl.texParameteri(this.target, this.gl.TEXTURE_WRAP_S, params.wrapS);
3159
+ }
3160
+ if (params.wrapT !== void 0) {
3161
+ this.gl.texParameteri(this.target, this.gl.TEXTURE_WRAP_T, params.wrapT);
3162
+ }
3163
+ if (params.minFilter !== void 0) {
3164
+ this.gl.texParameteri(this.target, this.gl.TEXTURE_MIN_FILTER, params.minFilter);
3165
+ }
3166
+ if (params.magFilter !== void 0) {
3167
+ this.gl.texParameteri(this.target, this.gl.TEXTURE_MAG_FILTER, params.magFilter);
3168
+ }
3169
+ }
3170
+ uploadFace(faceTarget, level, internalFormat, width, height, border, format, type, data) {
3171
+ this.bind();
3172
+ this.gl.texImage2D(faceTarget, level, internalFormat, width, height, border, format, type, data);
3173
+ }
3174
+ dispose() {
3175
+ this.gl.deleteTexture(this.texture);
3176
+ }
3177
+ };
1562
3178
  var Framebuffer = class {
1563
3179
  constructor(gl) {
1564
3180
  this.gl = gl;
@@ -1771,6 +3387,728 @@ function buildBspGeometry(gl, surfaces, options = {}) {
1771
3387
  return { surfaces: results, lightmaps };
1772
3388
  }
1773
3389
 
3390
+ // src/render/culling.ts
3391
+ function normalizePlane(plane) {
3392
+ const { normal, distance } = plane;
3393
+ const length = Math.sqrt(normal.x * normal.x + normal.y * normal.y + normal.z * normal.z);
3394
+ if (length === 0) {
3395
+ return plane;
3396
+ }
3397
+ const inv = 1 / length;
3398
+ return {
3399
+ normal: { x: normal.x * inv, y: normal.y * inv, z: normal.z * inv },
3400
+ distance: distance * inv
3401
+ };
3402
+ }
3403
+ function extractFrustumPlanes(matrix) {
3404
+ if (matrix.length !== 16) {
3405
+ throw new Error("View-projection matrix must contain 16 elements");
3406
+ }
3407
+ const m00 = matrix[0];
3408
+ const m01 = matrix[4];
3409
+ const m02 = matrix[8];
3410
+ const m03 = matrix[12];
3411
+ const m10 = matrix[1];
3412
+ const m11 = matrix[5];
3413
+ const m12 = matrix[9];
3414
+ const m13 = matrix[13];
3415
+ const m20 = matrix[2];
3416
+ const m21 = matrix[6];
3417
+ const m22 = matrix[10];
3418
+ const m23 = matrix[14];
3419
+ const m30 = matrix[3];
3420
+ const m31 = matrix[7];
3421
+ const m32 = matrix[11];
3422
+ const m33 = matrix[15];
3423
+ const planes = [
3424
+ // Left
3425
+ normalizePlane({ normal: { x: m30 + m00, y: m31 + m01, z: m32 + m02 }, distance: m33 + m03 }),
3426
+ // Right
3427
+ normalizePlane({ normal: { x: m30 - m00, y: m31 - m01, z: m32 - m02 }, distance: m33 - m03 }),
3428
+ // Bottom
3429
+ normalizePlane({ normal: { x: m30 + m10, y: m31 + m11, z: m32 + m12 }, distance: m33 + m13 }),
3430
+ // Top
3431
+ normalizePlane({ normal: { x: m30 - m10, y: m31 - m11, z: m32 - m12 }, distance: m33 - m13 }),
3432
+ // Near
3433
+ normalizePlane({ normal: { x: m30 + m20, y: m31 + m21, z: m32 + m22 }, distance: m33 + m23 }),
3434
+ // Far
3435
+ normalizePlane({ normal: { x: m30 - m20, y: m31 - m21, z: m32 - m22 }, distance: m33 - m23 })
3436
+ ];
3437
+ return planes;
3438
+ }
3439
+ function planeDistance(plane, point) {
3440
+ return plane.normal.x * point.x + plane.normal.y * point.y + plane.normal.z * point.z + plane.distance;
3441
+ }
3442
+ function boxIntersectsFrustum(mins, maxs, planes) {
3443
+ for (const plane of planes) {
3444
+ const x = plane.normal.x >= 0 ? maxs.x : mins.x;
3445
+ const y = plane.normal.y >= 0 ? maxs.y : mins.y;
3446
+ const z = plane.normal.z >= 0 ? maxs.z : mins.z;
3447
+ if (planeDistance(plane, { x, y, z }) < 0) {
3448
+ return false;
3449
+ }
3450
+ }
3451
+ return true;
3452
+ }
3453
+
3454
+ // src/render/bspTraversal.ts
3455
+ function childIsLeaf(index) {
3456
+ return index < 0;
3457
+ }
3458
+ function childLeafIndex(index) {
3459
+ return -index - 1;
3460
+ }
3461
+ function distanceToPlane(plane, point) {
3462
+ return plane.normal[0] * point.x + plane.normal[1] * point.y + plane.normal[2] * point.z - plane.dist;
3463
+ }
3464
+ function isClusterVisible(visibility, fromCluster, testCluster) {
3465
+ if (!visibility) {
3466
+ return true;
3467
+ }
3468
+ if (fromCluster < 0 || testCluster < 0) {
3469
+ return true;
3470
+ }
3471
+ const rowBytes = Math.ceil(visibility.numClusters / 8);
3472
+ const row = visibility.clusters[fromCluster].pvs;
3473
+ const byteIndex = Math.floor(testCluster / 8);
3474
+ const bit = 1 << testCluster % 8;
3475
+ if (byteIndex < 0 || byteIndex >= rowBytes) {
3476
+ return false;
3477
+ }
3478
+ return (row[byteIndex] & bit) !== 0;
3479
+ }
3480
+ function leafIntersectsFrustum(leaf, planes) {
3481
+ const mins = { x: leaf.mins[0], y: leaf.mins[1], z: leaf.mins[2] };
3482
+ const maxs = { x: leaf.maxs[0], y: leaf.maxs[1], z: leaf.maxs[2] };
3483
+ return boxIntersectsFrustum(mins, maxs, planes);
3484
+ }
3485
+ function findLeafForPoint(map, point) {
3486
+ let nodeIndex = 0;
3487
+ while (nodeIndex >= 0) {
3488
+ const node = map.nodes[nodeIndex];
3489
+ const plane = map.planes[node.planeIndex];
3490
+ const dist = distanceToPlane(plane, point);
3491
+ const side = dist >= 0 ? 0 : 1;
3492
+ const child = node.children[side];
3493
+ if (childIsLeaf(child)) {
3494
+ return childLeafIndex(child);
3495
+ }
3496
+ nodeIndex = child;
3497
+ }
3498
+ return -1;
3499
+ }
3500
+ function collectFacesFromLeaf(map, leafIndex) {
3501
+ const leaf = map.leafs[leafIndex];
3502
+ const faces = [];
3503
+ for (let i = 0; i < leaf.numLeafFaces; i += 1) {
3504
+ faces.push(map.leafLists.leafFaces[leafIndex][i]);
3505
+ }
3506
+ return faces;
3507
+ }
3508
+ function traverse(map, nodeIndex, camera, frustum, viewCluster, visibleFaces, visitedFaces) {
3509
+ if (childIsLeaf(nodeIndex)) {
3510
+ const leafIndex = childLeafIndex(nodeIndex);
3511
+ const leaf = map.leafs[leafIndex];
3512
+ if (!isClusterVisible(map.visibility, viewCluster, leaf.cluster)) {
3513
+ return;
3514
+ }
3515
+ if (!leafIntersectsFrustum(leaf, frustum)) {
3516
+ return;
3517
+ }
3518
+ const center = {
3519
+ x: (leaf.mins[0] + leaf.maxs[0]) * 0.5,
3520
+ y: (leaf.mins[1] + leaf.maxs[1]) * 0.5,
3521
+ z: (leaf.mins[2] + leaf.maxs[2]) * 0.5
3522
+ };
3523
+ const dx = center.x - camera.x;
3524
+ const dy = center.y - camera.y;
3525
+ const dz = center.z - camera.z;
3526
+ const leafSortKey = -(dx * dx + dy * dy + dz * dz);
3527
+ for (const faceIndex of collectFacesFromLeaf(map, leafIndex)) {
3528
+ if (visitedFaces.has(faceIndex)) {
3529
+ continue;
3530
+ }
3531
+ visitedFaces.add(faceIndex);
3532
+ visibleFaces.push({ faceIndex, leafIndex, sortKey: leafSortKey });
3533
+ }
3534
+ return;
3535
+ }
3536
+ const node = map.nodes[nodeIndex];
3537
+ const plane = map.planes[node.planeIndex];
3538
+ const dist = distanceToPlane(plane, camera);
3539
+ const nearChild = dist >= 0 ? node.children[0] : node.children[1];
3540
+ const farChild = dist >= 0 ? node.children[1] : node.children[0];
3541
+ if (boxIntersectsFrustum(
3542
+ { x: node.mins[0], y: node.mins[1], z: node.mins[2] },
3543
+ { x: node.maxs[0], y: node.maxs[1], z: node.maxs[2] },
3544
+ frustum
3545
+ )) {
3546
+ traverse(map, nearChild, camera, frustum, viewCluster, visibleFaces, visitedFaces);
3547
+ traverse(map, farChild, camera, frustum, viewCluster, visibleFaces, visitedFaces);
3548
+ }
3549
+ }
3550
+ function gatherVisibleFaces(map, cameraPosition, frustum) {
3551
+ const viewLeaf = findLeafForPoint(map, cameraPosition);
3552
+ const viewCluster = viewLeaf >= 0 ? map.leafs[viewLeaf].cluster : -1;
3553
+ const visibleFaces = [];
3554
+ const visitedFaces = /* @__PURE__ */ new Set();
3555
+ traverse(map, 0, cameraPosition, frustum, viewCluster, visibleFaces, visitedFaces);
3556
+ return visibleFaces;
3557
+ }
3558
+
3559
+ // src/render/bspPipeline.ts
3560
+ var BSP_SURFACE_VERTEX_SOURCE = `#version 300 es
3561
+ precision highp float;
3562
+
3563
+ layout(location = 0) in vec3 a_position;
3564
+ layout(location = 1) in vec2 a_texCoord;
3565
+ layout(location = 2) in vec2 a_lightmapCoord;
3566
+
3567
+ uniform mat4 u_modelViewProjection;
3568
+ uniform vec2 u_texScroll;
3569
+ uniform vec2 u_lightmapScroll;
3570
+
3571
+ out vec2 v_texCoord;
3572
+ out vec2 v_lightmapCoord;
3573
+
3574
+ vec2 applyScroll(vec2 uv, vec2 scroll) {
3575
+ return uv + scroll;
3576
+ }
3577
+
3578
+ void main() {
3579
+ v_texCoord = applyScroll(a_texCoord, u_texScroll);
3580
+ v_lightmapCoord = applyScroll(a_lightmapCoord, u_lightmapScroll);
3581
+ gl_Position = u_modelViewProjection * vec4(a_position, 1.0);
3582
+ }`;
3583
+ var BSP_SURFACE_FRAGMENT_SOURCE = `#version 300 es
3584
+ precision highp float;
3585
+
3586
+ in vec2 v_texCoord;
3587
+ in vec2 v_lightmapCoord;
3588
+
3589
+ uniform sampler2D u_diffuseMap;
3590
+ uniform sampler2D u_lightmapAtlas;
3591
+ uniform vec4 u_lightStyleFactors;
3592
+ uniform float u_alpha;
3593
+ uniform bool u_applyLightmap;
3594
+ uniform bool u_warp;
3595
+ uniform float u_time;
3596
+
3597
+ out vec4 o_color;
3598
+
3599
+ vec2 warpCoords(vec2 uv) {
3600
+ // Quake II warp applies a subtle sinusoidal offset; we mirror the rerelease scale.
3601
+ if (!u_warp) {
3602
+ return uv;
3603
+ }
3604
+ float s = uv.x + sin(uv.y * 0.125 + u_time) * 0.125;
3605
+ float t = uv.y + sin(uv.x * 0.125 + u_time) * 0.125;
3606
+ return vec2(s, t);
3607
+ }
3608
+
3609
+ void main() {
3610
+ vec2 warpedTex = warpCoords(v_texCoord);
3611
+ vec4 base = texture(u_diffuseMap, warpedTex);
3612
+
3613
+ if (u_applyLightmap) {
3614
+ vec3 light = texture(u_lightmapAtlas, warpCoords(v_lightmapCoord)).rgb;
3615
+ float styleScale = dot(u_lightStyleFactors, vec4(1.0));
3616
+ base.rgb *= light * styleScale;
3617
+ }
3618
+
3619
+ o_color = vec4(base.rgb, base.a * u_alpha);
3620
+ }`;
3621
+ var DEFAULT_STYLE_INDICES = [0, 255, 255, 255];
3622
+ function resolveLightStyles(styleIndices = DEFAULT_STYLE_INDICES, styleValues = []) {
3623
+ const factors = new Float32Array(4);
3624
+ for (let i = 0; i < 4; i += 1) {
3625
+ const styleIndex = styleIndices[i] ?? 255;
3626
+ if (styleIndex === 255) {
3627
+ factors[i] = 0;
3628
+ continue;
3629
+ }
3630
+ const value = styleValues[styleIndex];
3631
+ factors[i] = value !== void 0 ? value : 1;
3632
+ }
3633
+ return factors;
3634
+ }
3635
+ function computeFlowOffset(timeSeconds) {
3636
+ const cycle = timeSeconds * 0.25 % 1;
3637
+ return [-cycle, 0];
3638
+ }
3639
+ function deriveSurfaceRenderState(surfaceFlags = SURF_NONE, timeSeconds = 0) {
3640
+ const flowing = (surfaceFlags & SURF_FLOWING) !== 0;
3641
+ const warp = (surfaceFlags & SURF_WARP) !== 0;
3642
+ const sky = (surfaceFlags & SURF_SKY) !== 0;
3643
+ const trans33 = (surfaceFlags & SURF_TRANS33) !== 0;
3644
+ const trans66 = (surfaceFlags & SURF_TRANS66) !== 0;
3645
+ const alpha = trans33 ? 0.33 : trans66 ? 0.66 : 1;
3646
+ const blend = trans33 || trans66;
3647
+ const depthWrite = !blend && !sky;
3648
+ const flowOffset = flowing ? computeFlowOffset(timeSeconds) : [0, 0];
3649
+ return {
3650
+ alpha,
3651
+ blend,
3652
+ depthWrite,
3653
+ warp,
3654
+ flowOffset,
3655
+ sky
3656
+ };
3657
+ }
3658
+ var BspSurfacePipeline = class {
3659
+ constructor(gl) {
3660
+ this.gl = gl;
3661
+ this.program = ShaderProgram.create(
3662
+ gl,
3663
+ { vertex: BSP_SURFACE_VERTEX_SOURCE, fragment: BSP_SURFACE_FRAGMENT_SOURCE },
3664
+ { a_position: 0, a_texCoord: 1, a_lightmapCoord: 2 }
3665
+ );
3666
+ this.uniformMvp = this.program.getUniformLocation("u_modelViewProjection");
3667
+ this.uniformTexScroll = this.program.getUniformLocation("u_texScroll");
3668
+ this.uniformLmScroll = this.program.getUniformLocation("u_lightmapScroll");
3669
+ this.uniformLightStyles = this.program.getUniformLocation("u_lightStyleFactors");
3670
+ this.uniformAlpha = this.program.getUniformLocation("u_alpha");
3671
+ this.uniformApplyLightmap = this.program.getUniformLocation("u_applyLightmap");
3672
+ this.uniformWarp = this.program.getUniformLocation("u_warp");
3673
+ this.uniformDiffuse = this.program.getUniformLocation("u_diffuseMap");
3674
+ this.uniformLightmap = this.program.getUniformLocation("u_lightmapAtlas");
3675
+ this.uniformTime = this.program.getUniformLocation("u_time");
3676
+ }
3677
+ bind(options) {
3678
+ const {
3679
+ modelViewProjection,
3680
+ styleIndices = DEFAULT_STYLE_INDICES,
3681
+ styleValues = [],
3682
+ diffuseSampler = 0,
3683
+ lightmapSampler = 1,
3684
+ surfaceFlags = SURF_NONE,
3685
+ timeSeconds = 0
3686
+ } = options;
3687
+ const state = deriveSurfaceRenderState(surfaceFlags, timeSeconds);
3688
+ const styles = resolveLightStyles(styleIndices, styleValues);
3689
+ this.program.use();
3690
+ this.gl.uniformMatrix4fv(this.uniformMvp, false, modelViewProjection);
3691
+ this.gl.uniform2f(this.uniformTexScroll, state.flowOffset[0], state.flowOffset[1]);
3692
+ this.gl.uniform2f(this.uniformLmScroll, state.flowOffset[0], state.flowOffset[1]);
3693
+ this.gl.uniform4fv(this.uniformLightStyles, styles);
3694
+ this.gl.uniform1f(this.uniformAlpha, state.alpha);
3695
+ this.gl.uniform1i(this.uniformApplyLightmap, state.sky ? 0 : 1);
3696
+ this.gl.uniform1i(this.uniformWarp, state.warp ? 1 : 0);
3697
+ this.gl.uniform1f(this.uniformTime, timeSeconds);
3698
+ this.gl.uniform1i(this.uniformDiffuse, diffuseSampler);
3699
+ this.gl.uniform1i(this.uniformLightmap, lightmapSampler);
3700
+ return state;
3701
+ }
3702
+ dispose() {
3703
+ this.program.dispose();
3704
+ }
3705
+ };
3706
+ function applySurfaceState(gl, state) {
3707
+ gl.depthMask(state.depthWrite);
3708
+ if (state.blend) {
3709
+ gl.enable(gl.BLEND);
3710
+ gl.blendFunc(gl.SRC_ALPHA, gl.ONE_MINUS_SRC_ALPHA);
3711
+ } else {
3712
+ gl.disable(gl.BLEND);
3713
+ }
3714
+ }
3715
+
3716
+ // src/render/skybox.ts
3717
+ var SKYBOX_POSITIONS = new Float32Array([
3718
+ // Front
3719
+ -1,
3720
+ -1,
3721
+ 1,
3722
+ 1,
3723
+ -1,
3724
+ 1,
3725
+ 1,
3726
+ 1,
3727
+ 1,
3728
+ -1,
3729
+ -1,
3730
+ 1,
3731
+ 1,
3732
+ 1,
3733
+ 1,
3734
+ -1,
3735
+ 1,
3736
+ 1,
3737
+ // Back
3738
+ -1,
3739
+ -1,
3740
+ -1,
3741
+ -1,
3742
+ 1,
3743
+ -1,
3744
+ 1,
3745
+ 1,
3746
+ -1,
3747
+ -1,
3748
+ -1,
3749
+ -1,
3750
+ 1,
3751
+ 1,
3752
+ -1,
3753
+ 1,
3754
+ -1,
3755
+ -1,
3756
+ // Left
3757
+ -1,
3758
+ -1,
3759
+ -1,
3760
+ -1,
3761
+ -1,
3762
+ 1,
3763
+ -1,
3764
+ 1,
3765
+ 1,
3766
+ -1,
3767
+ -1,
3768
+ -1,
3769
+ -1,
3770
+ 1,
3771
+ 1,
3772
+ -1,
3773
+ 1,
3774
+ -1,
3775
+ // Right
3776
+ 1,
3777
+ -1,
3778
+ -1,
3779
+ 1,
3780
+ 1,
3781
+ -1,
3782
+ 1,
3783
+ 1,
3784
+ 1,
3785
+ 1,
3786
+ -1,
3787
+ -1,
3788
+ 1,
3789
+ 1,
3790
+ 1,
3791
+ 1,
3792
+ -1,
3793
+ 1,
3794
+ // Top
3795
+ -1,
3796
+ 1,
3797
+ -1,
3798
+ -1,
3799
+ 1,
3800
+ 1,
3801
+ 1,
3802
+ 1,
3803
+ 1,
3804
+ -1,
3805
+ 1,
3806
+ -1,
3807
+ 1,
3808
+ 1,
3809
+ 1,
3810
+ 1,
3811
+ 1,
3812
+ -1,
3813
+ // Bottom
3814
+ -1,
3815
+ -1,
3816
+ -1,
3817
+ 1,
3818
+ -1,
3819
+ -1,
3820
+ 1,
3821
+ -1,
3822
+ 1,
3823
+ -1,
3824
+ -1,
3825
+ -1,
3826
+ 1,
3827
+ -1,
3828
+ 1,
3829
+ -1,
3830
+ -1,
3831
+ 1
3832
+ ]);
3833
+ var SKYBOX_VERTEX_SHADER = `#version 300 es
3834
+ precision highp float;
3835
+
3836
+ layout(location = 0) in vec3 a_position;
3837
+
3838
+ uniform mat4 u_viewProjectionNoTranslation;
3839
+ uniform vec2 u_scroll;
3840
+
3841
+ out vec3 v_direction;
3842
+
3843
+ void main() {
3844
+ vec3 dir = normalize(a_position);
3845
+ dir.xy += u_scroll;
3846
+ v_direction = dir;
3847
+ gl_Position = u_viewProjectionNoTranslation * vec4(a_position, 1.0);
3848
+ }`;
3849
+ var SKYBOX_FRAGMENT_SHADER = `#version 300 es
3850
+ precision highp float;
3851
+
3852
+ in vec3 v_direction;
3853
+ uniform samplerCube u_skybox;
3854
+
3855
+ out vec4 o_color;
3856
+
3857
+ void main() {
3858
+ o_color = texture(u_skybox, v_direction);
3859
+ }`;
3860
+ var SkyboxPipeline = class {
3861
+ constructor(gl) {
3862
+ this.gl = gl;
3863
+ this.program = ShaderProgram.create(
3864
+ gl,
3865
+ { vertex: SKYBOX_VERTEX_SHADER, fragment: SKYBOX_FRAGMENT_SHADER },
3866
+ { a_position: 0 }
3867
+ );
3868
+ this.vao = new VertexArray(gl);
3869
+ this.vbo = new VertexBuffer(gl, gl.STATIC_DRAW);
3870
+ this.vbo.upload(SKYBOX_POSITIONS, gl.STATIC_DRAW);
3871
+ const layout = [{ index: 0, size: 3, type: gl.FLOAT, stride: 12, offset: 0 }];
3872
+ this.vao.configureAttributes(layout, this.vbo);
3873
+ this.uniformViewProj = this.program.getUniformLocation("u_viewProjectionNoTranslation");
3874
+ this.uniformScroll = this.program.getUniformLocation("u_scroll");
3875
+ this.uniformSampler = this.program.getUniformLocation("u_skybox");
3876
+ this.cubemap = new TextureCubeMap(gl);
3877
+ this.cubemap.setParameters({
3878
+ minFilter: gl.LINEAR,
3879
+ magFilter: gl.LINEAR,
3880
+ wrapS: gl.CLAMP_TO_EDGE,
3881
+ wrapT: gl.CLAMP_TO_EDGE
3882
+ });
3883
+ }
3884
+ bind(options) {
3885
+ const { viewProjection, scroll, textureUnit = 0 } = options;
3886
+ this.program.use();
3887
+ this.gl.depthMask(false);
3888
+ this.gl.uniformMatrix4fv(this.uniformViewProj, false, viewProjection);
3889
+ this.gl.uniform2f(this.uniformScroll, scroll[0], scroll[1]);
3890
+ this.gl.uniform1i(this.uniformSampler, textureUnit);
3891
+ this.cubemap.bind(textureUnit);
3892
+ this.vao.bind();
3893
+ }
3894
+ draw() {
3895
+ this.gl.drawArrays(this.gl.TRIANGLES, 0, SKYBOX_POSITIONS.length / 3);
3896
+ }
3897
+ dispose() {
3898
+ this.vbo.dispose();
3899
+ this.vao.dispose();
3900
+ this.cubemap.dispose();
3901
+ this.program.dispose();
3902
+ }
3903
+ };
3904
+ function removeViewTranslation(viewMatrix) {
3905
+ const noTranslation = viewMatrix.slice();
3906
+ noTranslation[12] = 0;
3907
+ noTranslation[13] = 0;
3908
+ noTranslation[14] = 0;
3909
+ return noTranslation;
3910
+ }
3911
+ function computeSkyScroll(timeSeconds, scrollSpeeds = [0.01, 0.02]) {
3912
+ const [sx, sy] = scrollSpeeds;
3913
+ return [sx * timeSeconds, sy * timeSeconds];
3914
+ }
3915
+
3916
+ // src/render/md2Pipeline.ts
3917
+ var MD2_VERTEX_SHADER = `#version 300 es
3918
+ precision highp float;
3919
+
3920
+ layout(location = 0) in vec3 a_position;
3921
+ layout(location = 1) in vec3 a_normal;
3922
+ layout(location = 2) in vec2 a_texCoord;
3923
+
3924
+ uniform mat4 u_modelViewProjection;
3925
+ uniform vec3 u_lightDir;
3926
+
3927
+ out vec2 v_texCoord;
3928
+ out float v_light;
3929
+
3930
+ void main() {
3931
+ vec3 normal = normalize(a_normal);
3932
+ v_light = max(dot(normal, normalize(u_lightDir)), 0.0);
3933
+ v_texCoord = a_texCoord;
3934
+ gl_Position = u_modelViewProjection * vec4(a_position, 1.0);
3935
+ }`;
3936
+ var MD2_FRAGMENT_SHADER = `#version 300 es
3937
+ precision highp float;
3938
+
3939
+ in vec2 v_texCoord;
3940
+ in float v_light;
3941
+
3942
+ uniform sampler2D u_diffuseMap;
3943
+ uniform vec4 u_tint;
3944
+
3945
+ out vec4 o_color;
3946
+
3947
+ void main() {
3948
+ vec4 albedo = texture(u_diffuseMap, v_texCoord) * u_tint;
3949
+ o_color = vec4(albedo.rgb * v_light, albedo.a);
3950
+ }`;
3951
+ function normalizeVec32(v) {
3952
+ const lengthSq = v.x * v.x + v.y * v.y + v.z * v.z;
3953
+ if (lengthSq <= 0) {
3954
+ return { x: 0, y: 0, z: 1 };
3955
+ }
3956
+ const inv = 1 / Math.sqrt(lengthSq);
3957
+ return { x: v.x * inv, y: v.y * inv, z: v.z * inv };
3958
+ }
3959
+ function lerp(a, b, t) {
3960
+ return a + (b - a) * t;
3961
+ }
3962
+ function lerpVec3(a, b, t) {
3963
+ return {
3964
+ x: lerp(a.x, b.x, t),
3965
+ y: lerp(a.y, b.y, t),
3966
+ z: lerp(a.z, b.z, t)
3967
+ };
3968
+ }
3969
+ function normalizeUv(s, t, header) {
3970
+ return [s / header.skinWidth, 1 - t / header.skinHeight];
3971
+ }
3972
+ function buildMd2Geometry(model) {
3973
+ if (model.glCommands.length === 0) {
3974
+ const vertices2 = [];
3975
+ const indices2 = [];
3976
+ model.triangles.forEach((triangle) => {
3977
+ const baseIndex = vertices2.length;
3978
+ for (let i = 0; i < 3; i += 1) {
3979
+ const vertexIndex = triangle.vertexIndices[i];
3980
+ const texCoordIndex = triangle.texCoordIndices[i];
3981
+ const texCoord = model.texCoords[texCoordIndex];
3982
+ vertices2.push({
3983
+ vertexIndex,
3984
+ texCoord: normalizeUv(texCoord.s, texCoord.t, model.header)
3985
+ });
3986
+ }
3987
+ indices2.push(baseIndex, baseIndex + 1, baseIndex + 2);
3988
+ });
3989
+ return { vertices: vertices2, indices: new Uint16Array(indices2) };
3990
+ }
3991
+ const vertices = [];
3992
+ const indices = [];
3993
+ for (const command of model.glCommands) {
3994
+ const start = vertices.length;
3995
+ vertices.push(
3996
+ ...command.vertices.map((vertex) => ({
3997
+ vertexIndex: vertex.vertexIndex,
3998
+ texCoord: [vertex.s, 1 - vertex.t]
3999
+ }))
4000
+ );
4001
+ if (command.mode === "strip") {
4002
+ for (let i = 0; i < command.vertices.length - 2; i += 1) {
4003
+ const even = i % 2 === 0;
4004
+ const a = start + i + (even ? 0 : 1);
4005
+ const b = start + i + (even ? 1 : 0);
4006
+ const c = start + i + 2;
4007
+ indices.push(a, b, c);
4008
+ }
4009
+ } else {
4010
+ for (let i = 1; i < command.vertices.length - 1; i += 1) {
4011
+ indices.push(start, start + i, start + i + 1);
4012
+ }
4013
+ }
4014
+ }
4015
+ return { vertices, indices: new Uint16Array(indices) };
4016
+ }
4017
+ function buildMd2VertexData(model, geometry, blend) {
4018
+ const { currentFrame, nextFrame, lerp: lerp2 } = blend;
4019
+ const frameA = model.frames[currentFrame];
4020
+ const frameB = model.frames[nextFrame];
4021
+ if (!frameA || !frameB) {
4022
+ throw new Error("Requested MD2 frames are out of range");
4023
+ }
4024
+ const data = new Float32Array(geometry.vertices.length * 8);
4025
+ geometry.vertices.forEach((vertex, index) => {
4026
+ const vA = frameA.vertices[vertex.vertexIndex];
4027
+ const vB = frameB.vertices[vertex.vertexIndex];
4028
+ if (!vA || !vB) {
4029
+ throw new Error("MD2 vertex index out of range for frame");
4030
+ }
4031
+ const position = lerpVec3(vA.position, vB.position, lerp2);
4032
+ const normal = normalizeVec32(lerpVec3(vA.normal, vB.normal, lerp2));
4033
+ const base = index * 8;
4034
+ data[base] = position.x;
4035
+ data[base + 1] = position.y;
4036
+ data[base + 2] = position.z;
4037
+ data[base + 3] = normal.x;
4038
+ data[base + 4] = normal.y;
4039
+ data[base + 5] = normal.z;
4040
+ data[base + 6] = vertex.texCoord[0];
4041
+ data[base + 7] = vertex.texCoord[1];
4042
+ });
4043
+ return data;
4044
+ }
4045
+ var Md2MeshBuffers = class {
4046
+ constructor(gl, model, blend) {
4047
+ this.gl = gl;
4048
+ this.geometry = buildMd2Geometry(model);
4049
+ this.vertexBuffer = new VertexBuffer(gl, gl.STATIC_DRAW);
4050
+ this.indexBuffer = new IndexBuffer(gl, gl.STATIC_DRAW);
4051
+ this.vertexArray = new VertexArray(gl);
4052
+ this.indexCount = this.geometry.indices.length;
4053
+ this.vertexArray.configureAttributes(
4054
+ [
4055
+ { index: 0, size: 3, type: gl.FLOAT, stride: 32, offset: 0 },
4056
+ { index: 1, size: 3, type: gl.FLOAT, stride: 32, offset: 12 },
4057
+ { index: 2, size: 2, type: gl.FLOAT, stride: 32, offset: 24 }
4058
+ ],
4059
+ this.vertexBuffer
4060
+ );
4061
+ this.vertexArray.bind();
4062
+ this.indexBuffer.bind();
4063
+ this.indexBuffer.upload(this.geometry.indices, gl.STATIC_DRAW);
4064
+ this.update(model, blend);
4065
+ }
4066
+ update(model, blend) {
4067
+ const data = buildMd2VertexData(model, this.geometry, blend);
4068
+ this.vertexBuffer.upload(data, this.gl.STATIC_DRAW);
4069
+ }
4070
+ bind() {
4071
+ this.vertexArray.bind();
4072
+ this.indexBuffer.bind();
4073
+ }
4074
+ dispose() {
4075
+ this.vertexBuffer.dispose();
4076
+ this.indexBuffer.dispose();
4077
+ this.vertexArray.dispose();
4078
+ }
4079
+ };
4080
+ var Md2Pipeline = class {
4081
+ constructor(gl) {
4082
+ this.gl = gl;
4083
+ this.program = ShaderProgram.create(
4084
+ gl,
4085
+ { vertex: MD2_VERTEX_SHADER, fragment: MD2_FRAGMENT_SHADER },
4086
+ { a_position: 0, a_normal: 1, a_texCoord: 2 }
4087
+ );
4088
+ this.uniformMvp = this.program.getUniformLocation("u_modelViewProjection");
4089
+ this.uniformLightDir = this.program.getUniformLocation("u_lightDir");
4090
+ this.uniformTint = this.program.getUniformLocation("u_tint");
4091
+ this.uniformDiffuse = this.program.getUniformLocation("u_diffuseMap");
4092
+ }
4093
+ bind(options) {
4094
+ const { modelViewProjection, lightDirection = [0, 0, 1], tint = [1, 1, 1, 1], diffuseSampler = 0 } = options;
4095
+ const lightVec = new Float32Array(lightDirection);
4096
+ const tintVec = new Float32Array(tint);
4097
+ this.program.use();
4098
+ this.gl.uniformMatrix4fv(this.uniformMvp, false, modelViewProjection);
4099
+ this.gl.uniform3fv(this.uniformLightDir, lightVec);
4100
+ this.gl.uniform4fv(this.uniformTint, tintVec);
4101
+ this.gl.uniform1i(this.uniformDiffuse, diffuseSampler);
4102
+ }
4103
+ draw(mesh) {
4104
+ mesh.bind();
4105
+ this.gl.drawElements(this.gl.TRIANGLES, mesh.indexCount, this.gl.UNSIGNED_SHORT, 0);
4106
+ }
4107
+ dispose() {
4108
+ this.program.dispose();
4109
+ }
4110
+ };
4111
+
1774
4112
  // src/index.ts
1775
4113
  function createEngine(imports) {
1776
4114
  return {
@@ -1786,7 +4124,23 @@ function createEngine(imports) {
1786
4124
  }
1787
4125
  // Annotate the CommonJS export names for ESM import in node:
1788
4126
  0 && (module.exports = {
4127
+ ATTN_IDLE,
4128
+ ATTN_LOOP_NONE,
4129
+ ATTN_NONE,
4130
+ ATTN_NORM,
4131
+ ATTN_STATIC,
4132
+ AssetDependencyError,
4133
+ AssetDependencyTracker,
4134
+ AssetManager,
4135
+ AudioApi,
4136
+ AudioContextController,
4137
+ AudioRegistry,
4138
+ AudioRegistryError,
4139
+ AudioSystem,
4140
+ BSP_SURFACE_FRAGMENT_SOURCE,
4141
+ BSP_SURFACE_VERTEX_SOURCE,
1789
4142
  BSP_VERTEX_LAYOUT,
4143
+ BspSurfacePipeline,
1790
4144
  ConfigStringRegistry,
1791
4145
  Cvar,
1792
4146
  CvarRegistry,
@@ -1796,27 +4150,79 @@ function createEngine(imports) {
1796
4150
  Framebuffer,
1797
4151
  IndexBuffer,
1798
4152
  LruCache,
4153
+ MAX_SOUND_CHANNELS,
4154
+ MD2_FRAGMENT_SHADER,
4155
+ MD2_VERTEX_SHADER,
1799
4156
  Md2Loader,
4157
+ Md2MeshBuffers,
1800
4158
  Md2ParseError,
4159
+ Md2Pipeline,
4160
+ Md3Loader,
4161
+ Md3ParseError,
4162
+ MusicSystem,
1801
4163
  PakArchive,
4164
+ PakIndexStore,
1802
4165
  PakIngestionError,
1803
4166
  PakParseError,
4167
+ PakValidationError,
4168
+ PakValidator,
4169
+ RERELEASE_KNOWN_PAKS,
4170
+ SKYBOX_FRAGMENT_SHADER,
4171
+ SKYBOX_VERTEX_SHADER,
4172
+ SOUND_FULLVOLUME,
4173
+ SOUND_LOOP_ATTENUATE,
1804
4174
  ShaderProgram,
4175
+ SkyboxPipeline,
4176
+ SoundChannel,
4177
+ SoundPrecache,
4178
+ SoundRegistry,
1805
4179
  Texture2D,
4180
+ TextureCache,
4181
+ TextureCubeMap,
1806
4182
  VertexArray,
1807
4183
  VertexBuffer,
1808
4184
  VirtualFileSystem,
4185
+ advanceAnimation,
4186
+ applySurfaceState,
4187
+ attenuationToDistanceMultiplier,
4188
+ boxIntersectsFrustum,
1809
4189
  buildBspGeometry,
4190
+ buildMd2Geometry,
4191
+ buildMd2VertexData,
4192
+ calculateMaxAudibleDistance,
1810
4193
  calculatePakChecksum,
4194
+ computeFrameBlend,
4195
+ computeSkyScroll,
4196
+ createAnimationState,
4197
+ createAudioGraph,
1811
4198
  createEngine,
1812
4199
  createEngineRuntime,
4200
+ createInitialChannels,
1813
4201
  createProgramFromSources,
1814
4202
  createWebGLContext,
4203
+ decodeOgg,
4204
+ deriveSurfaceRenderState,
4205
+ extractFrustumPlanes,
1815
4206
  filesToPakSources,
4207
+ findLeafForPoint,
4208
+ gatherVisibleFaces,
1816
4209
  groupMd2Animations,
1817
4210
  ingestPakFiles,
1818
4211
  ingestPaks,
4212
+ interpolateVec3,
1819
4213
  parseMd2,
4214
+ parseMd3,
4215
+ parsePcx,
4216
+ parseWal,
4217
+ parseWalTexture,
4218
+ parseWav,
4219
+ pcxToRgba,
4220
+ pickChannel,
4221
+ preparePcxTexture,
4222
+ removeViewTranslation,
4223
+ resolveLightStyles,
4224
+ spatializeOrigin,
4225
+ walToRgba,
1820
4226
  wireDropTarget,
1821
4227
  wireFileInput
1822
4228
  });