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.
- package/README.md +425 -0
- package/apps/viewer/dist/browser/index.global.js +1 -1
- package/apps/viewer/dist/browser/index.global.js.map +1 -1
- package/apps/viewer/dist/cjs/index.cjs +2097 -295
- package/apps/viewer/dist/cjs/index.cjs.map +1 -1
- package/apps/viewer/dist/esm/index.js +2097 -295
- package/apps/viewer/dist/esm/index.js.map +1 -1
- package/apps/viewer/dist/tsconfig.tsbuildinfo +1 -1
- package/apps/viewer/dist/types/index.d.ts +1 -1
- package/package.json +1 -1
- package/packages/client/dist/browser/index.global.js +1 -1
- package/packages/client/dist/browser/index.global.js.map +1 -1
- package/packages/client/dist/cjs/index.cjs +1200 -13
- package/packages/client/dist/cjs/index.cjs.map +1 -1
- package/packages/client/dist/esm/index.js +1186 -12
- package/packages/client/dist/esm/index.js.map +1 -1
- package/packages/client/dist/tsconfig.tsbuildinfo +1 -1
- package/packages/client/dist/types/index.d.ts +14 -6
- package/packages/client/dist/types/index.d.ts.map +1 -1
- package/packages/client/dist/types/input/bindings.d.ts +18 -0
- package/packages/client/dist/types/input/bindings.d.ts.map +1 -0
- package/packages/client/dist/types/input/command-buffer.d.ts +15 -0
- package/packages/client/dist/types/input/command-buffer.d.ts.map +1 -0
- package/packages/client/dist/types/input/controller.d.ts +125 -0
- package/packages/client/dist/types/input/controller.d.ts.map +1 -0
- package/packages/client/dist/types/prediction.d.ts +38 -0
- package/packages/client/dist/types/prediction.d.ts.map +1 -0
- package/packages/client/dist/types/view-effects.d.ts +41 -0
- package/packages/client/dist/types/view-effects.d.ts.map +1 -0
- package/packages/engine/dist/browser/index.global.js +257 -1
- package/packages/engine/dist/browser/index.global.js.map +1 -1
- package/packages/engine/dist/cjs/index.cjs +2408 -2
- package/packages/engine/dist/cjs/index.cjs.map +1 -1
- package/packages/engine/dist/esm/index.js +2340 -2
- package/packages/engine/dist/esm/index.js.map +1 -1
- package/packages/engine/dist/tsconfig.tsbuildinfo +1 -1
- package/packages/engine/dist/types/assets/animation.d.ts +33 -0
- package/packages/engine/dist/types/assets/animation.d.ts.map +1 -0
- package/packages/engine/dist/types/assets/audio.d.ts +21 -0
- package/packages/engine/dist/types/assets/audio.d.ts.map +1 -0
- package/packages/engine/dist/types/assets/bsp.d.ts +1 -1
- package/packages/engine/dist/types/assets/bsp.d.ts.map +1 -1
- package/packages/engine/dist/types/assets/ingestion.d.ts +31 -0
- package/packages/engine/dist/types/assets/ingestion.d.ts.map +1 -1
- package/packages/engine/dist/types/assets/manager.d.ts +43 -0
- package/packages/engine/dist/types/assets/manager.d.ts.map +1 -0
- package/packages/engine/dist/types/assets/md3.d.ts +69 -0
- package/packages/engine/dist/types/assets/md3.d.ts.map +1 -0
- package/packages/engine/dist/types/assets/ogg.d.ts +12 -0
- package/packages/engine/dist/types/assets/ogg.d.ts.map +1 -0
- package/packages/engine/dist/types/assets/pakIndexStore.d.ts +19 -0
- package/packages/engine/dist/types/assets/pakIndexStore.d.ts.map +1 -0
- package/packages/engine/dist/types/assets/pakValidation.d.ts +28 -0
- package/packages/engine/dist/types/assets/pakValidation.d.ts.map +1 -0
- package/packages/engine/dist/types/assets/pcx.d.ts +13 -0
- package/packages/engine/dist/types/assets/pcx.d.ts.map +1 -0
- package/packages/engine/dist/types/assets/texture.d.ts +29 -0
- package/packages/engine/dist/types/assets/texture.d.ts.map +1 -0
- package/packages/engine/dist/types/assets/wal.d.ts +21 -0
- package/packages/engine/dist/types/assets/wal.d.ts.map +1 -0
- package/packages/engine/dist/types/assets/wav.d.ts +11 -0
- package/packages/engine/dist/types/assets/wav.d.ts.map +1 -0
- package/packages/engine/dist/types/audio/api.d.ts +29 -0
- package/packages/engine/dist/types/audio/api.d.ts.map +1 -0
- package/packages/engine/dist/types/audio/channels.d.ts +15 -0
- package/packages/engine/dist/types/audio/channels.d.ts.map +1 -0
- package/packages/engine/dist/types/audio/constants.d.ts +24 -0
- package/packages/engine/dist/types/audio/constants.d.ts.map +1 -0
- package/packages/engine/dist/types/audio/context.d.ts +67 -0
- package/packages/engine/dist/types/audio/context.d.ts.map +1 -0
- package/packages/engine/dist/types/audio/music.d.ts +42 -0
- package/packages/engine/dist/types/audio/music.d.ts.map +1 -0
- package/packages/engine/dist/types/audio/precache.d.ts +28 -0
- package/packages/engine/dist/types/audio/precache.d.ts.map +1 -0
- package/packages/engine/dist/types/audio/registry.d.ts +13 -0
- package/packages/engine/dist/types/audio/registry.d.ts.map +1 -0
- package/packages/engine/dist/types/audio/spatialization.d.ts +14 -0
- package/packages/engine/dist/types/audio/spatialization.d.ts.map +1 -0
- package/packages/engine/dist/types/audio/system.d.ts +101 -0
- package/packages/engine/dist/types/audio/system.d.ts.map +1 -0
- package/packages/engine/dist/types/configstrings.d.ts +1 -0
- package/packages/engine/dist/types/configstrings.d.ts.map +1 -1
- package/packages/engine/dist/types/index.d.ts +26 -1
- package/packages/engine/dist/types/index.d.ts.map +1 -1
- package/packages/engine/dist/types/render/bspPipeline.d.ts +42 -0
- package/packages/engine/dist/types/render/bspPipeline.d.ts.map +1 -0
- package/packages/engine/dist/types/render/bspTraversal.d.ts +11 -0
- package/packages/engine/dist/types/render/bspTraversal.d.ts.map +1 -0
- package/packages/engine/dist/types/render/culling.d.ts +8 -0
- package/packages/engine/dist/types/render/culling.d.ts.map +1 -0
- package/packages/engine/dist/types/render/md2Pipeline.d.ts +51 -0
- package/packages/engine/dist/types/render/md2Pipeline.d.ts.map +1 -0
- package/packages/engine/dist/types/render/resources.d.ts +10 -0
- package/packages/engine/dist/types/render/resources.d.ts.map +1 -1
- package/packages/engine/dist/types/render/skybox.d.ts +26 -0
- package/packages/engine/dist/types/render/skybox.d.ts.map +1 -0
- package/packages/game/dist/browser/index.global.js +1 -1
- package/packages/game/dist/browser/index.global.js.map +1 -1
- package/packages/game/dist/cjs/index.cjs +2926 -116
- package/packages/game/dist/cjs/index.cjs.map +1 -1
- package/packages/game/dist/esm/index.js +2863 -115
- package/packages/game/dist/esm/index.js.map +1 -1
- package/packages/game/dist/tsconfig.tsbuildinfo +1 -1
- package/packages/game/dist/types/ai/constants.d.ts +13 -0
- package/packages/game/dist/types/ai/constants.d.ts.map +1 -0
- package/packages/game/dist/types/ai/index.d.ts +4 -0
- package/packages/game/dist/types/ai/index.d.ts.map +1 -0
- package/packages/game/dist/types/ai/movement.d.ts +20 -0
- package/packages/game/dist/types/ai/movement.d.ts.map +1 -0
- package/packages/game/dist/types/ai/perception.d.ts +21 -0
- package/packages/game/dist/types/ai/perception.d.ts.map +1 -0
- package/packages/game/dist/types/checksum.d.ts +3 -0
- package/packages/game/dist/types/checksum.d.ts.map +1 -0
- package/packages/game/dist/types/combat/armor.d.ts +39 -0
- package/packages/game/dist/types/combat/armor.d.ts.map +1 -0
- package/packages/game/dist/types/combat/damage.d.ts +52 -0
- package/packages/game/dist/types/combat/damage.d.ts.map +1 -0
- package/packages/game/dist/types/combat/damageFlags.d.ts +15 -0
- package/packages/game/dist/types/combat/damageFlags.d.ts.map +1 -0
- package/packages/game/dist/types/combat/damageMods.d.ts +79 -0
- package/packages/game/dist/types/combat/damageMods.d.ts.map +1 -0
- package/packages/game/dist/types/combat/index.d.ts +6 -0
- package/packages/game/dist/types/combat/index.d.ts.map +1 -0
- package/packages/game/dist/types/combat/specialDamage.d.ts +88 -0
- package/packages/game/dist/types/combat/specialDamage.d.ts.map +1 -0
- package/packages/game/dist/types/entities/entity.d.ts +46 -2
- package/packages/game/dist/types/entities/entity.d.ts.map +1 -1
- package/packages/game/dist/types/entities/index.d.ts +6 -2
- package/packages/game/dist/types/entities/index.d.ts.map +1 -1
- package/packages/game/dist/types/entities/pool.d.ts +9 -0
- package/packages/game/dist/types/entities/pool.d.ts.map +1 -1
- package/packages/game/dist/types/entities/spawn.d.ts +27 -0
- package/packages/game/dist/types/entities/spawn.d.ts.map +1 -0
- package/packages/game/dist/types/entities/system.d.ts +32 -1
- package/packages/game/dist/types/entities/system.d.ts.map +1 -1
- package/packages/game/dist/types/entities/thinkScheduler.d.ts +6 -0
- package/packages/game/dist/types/entities/thinkScheduler.d.ts.map +1 -1
- package/packages/game/dist/types/entities/triggers.d.ts +3 -0
- package/packages/game/dist/types/entities/triggers.d.ts.map +1 -0
- package/packages/game/dist/types/entities/utils.d.ts +4 -0
- package/packages/game/dist/types/entities/utils.d.ts.map +1 -0
- package/packages/game/dist/types/index.d.ts +5 -0
- package/packages/game/dist/types/index.d.ts.map +1 -1
- package/packages/game/dist/types/inventory/ammo.d.ts +17 -0
- package/packages/game/dist/types/inventory/ammo.d.ts.map +1 -0
- package/packages/game/dist/types/inventory/index.d.ts +2 -0
- package/packages/game/dist/types/inventory/index.d.ts.map +1 -0
- package/packages/game/dist/types/level.d.ts +1 -0
- package/packages/game/dist/types/level.d.ts.map +1 -1
- package/packages/game/dist/types/save/index.d.ts +4 -0
- package/packages/game/dist/types/save/index.d.ts.map +1 -0
- package/packages/game/dist/types/save/rerelease.d.ts +25 -0
- package/packages/game/dist/types/save/rerelease.d.ts.map +1 -0
- package/packages/game/dist/types/save/save.d.ts +49 -0
- package/packages/game/dist/types/save/save.d.ts.map +1 -0
- package/packages/game/dist/types/save/storage.d.ts +37 -0
- package/packages/game/dist/types/save/storage.d.ts.map +1 -0
- package/packages/shared/dist/browser/index.global.js +1 -1
- package/packages/shared/dist/browser/index.global.js.map +1 -1
- package/packages/shared/dist/cjs/index.cjs +638 -9
- package/packages/shared/dist/cjs/index.cjs.map +1 -1
- package/packages/shared/dist/esm/index.js +616 -9
- package/packages/shared/dist/esm/index.js.map +1 -1
- package/packages/shared/dist/tsconfig.tsbuildinfo +1 -1
- package/packages/shared/dist/types/bsp/collision.d.ts +56 -0
- package/packages/shared/dist/types/bsp/collision.d.ts.map +1 -1
- package/packages/shared/dist/types/bsp/contents.d.ts +1 -0
- package/packages/shared/dist/types/bsp/contents.d.ts.map +1 -1
- package/packages/shared/dist/types/index.d.ts +2 -0
- package/packages/shared/dist/types/index.d.ts.map +1 -1
- package/packages/shared/dist/types/math/random.d.ts +11 -0
- package/packages/shared/dist/types/math/random.d.ts.map +1 -1
- package/packages/shared/dist/types/protocol/contracts.d.ts +17 -0
- package/packages/shared/dist/types/protocol/contracts.d.ts.map +1 -0
- package/packages/shared/dist/types/protocol/usercmd.d.ts +30 -0
- package/packages/shared/dist/types/protocol/usercmd.d.ts.map +1 -0
- 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 (
|
|
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
|
});
|