Compare commits
| Author | SHA1 | Date | |
|---|---|---|---|
|
|
c245a199b6 | ||
|
|
eca6b5db9b |
File diff suppressed because it is too large
Load Diff
@@ -27,6 +27,7 @@
|
|||||||
}
|
}
|
||||||
],
|
],
|
||||||
"environmentMode": "detect",
|
"environmentMode": "detect",
|
||||||
|
"coordinateAlignment": "source",
|
||||||
"exceptions": [],
|
"exceptions": [],
|
||||||
"expansion": "wrath",
|
"expansion": "wrath",
|
||||||
"extraction": {
|
"extraction": {
|
||||||
|
|||||||
@@ -8,6 +8,7 @@
|
|||||||
"clientBuild": "3.3.5a-Ascension",
|
"clientBuild": "3.3.5a-Ascension",
|
||||||
"mapDirectory": "World/Maps/TempestKeepArcane",
|
"mapDirectory": "World/Maps/TempestKeepArcane",
|
||||||
"environmentMode": "global-wmo",
|
"environmentMode": "global-wmo",
|
||||||
|
"coordinateAlignment": "source",
|
||||||
"difficultyVariants": [{"id": "normal", "difficulty": "normal", "spawnMask": 1, "levelRange": [65, 70]}],
|
"difficultyVariants": [{"id": "normal", "difficulty": "normal", "spawnMask": 1, "levelRange": [65, 70]}],
|
||||||
"clientSource": {"rootEnvironmentVariable": "WOW_CLIENT_ROOT", "relativeDefault": "../wow335a"},
|
"clientSource": {"rootEnvironmentVariable": "WOW_CLIENT_ROOT", "relativeDefault": "../wow335a"},
|
||||||
"serverSource": {"rootEnvironmentVariable": "WOW_SERVER_DATA", "adapter": "normalized-json", "readOnly": true},
|
"serverSource": {"rootEnvironmentVariable": "WOW_SERVER_DATA", "adapter": "normalized-json", "readOnly": true},
|
||||||
|
|||||||
@@ -8,6 +8,7 @@
|
|||||||
"clientBuild": "3.3.5a-Ascension",
|
"clientBuild": "3.3.5a-Ascension",
|
||||||
"mapDirectory": "World/Maps/AuchindounDraenei",
|
"mapDirectory": "World/Maps/AuchindounDraenei",
|
||||||
"environmentMode": "global-wmo",
|
"environmentMode": "global-wmo",
|
||||||
|
"coordinateAlignment": "source",
|
||||||
"difficultyVariants": [{"id": "normal", "difficulty": "normal", "spawnMask": 1, "levelRange": [65, 70]}],
|
"difficultyVariants": [{"id": "normal", "difficulty": "normal", "spawnMask": 1, "levelRange": [65, 70]}],
|
||||||
"clientSource": {"rootEnvironmentVariable": "WOW_CLIENT_ROOT", "relativeDefault": "../wow335a"},
|
"clientSource": {"rootEnvironmentVariable": "WOW_CLIENT_ROOT", "relativeDefault": "../wow335a"},
|
||||||
"serverSource": {"rootEnvironmentVariable": "WOW_SERVER_DATA", "adapter": "normalized-json", "readOnly": true},
|
"serverSource": {"rootEnvironmentVariable": "WOW_SERVER_DATA", "adapter": "normalized-json", "readOnly": true},
|
||||||
|
|||||||
@@ -27,6 +27,7 @@
|
|||||||
}
|
}
|
||||||
],
|
],
|
||||||
"environmentMode": "detect",
|
"environmentMode": "detect",
|
||||||
|
"coordinateAlignment": "source",
|
||||||
"exceptions": [],
|
"exceptions": [],
|
||||||
"expansion": "wrath",
|
"expansion": "wrath",
|
||||||
"extraction": {
|
"extraction": {
|
||||||
|
|||||||
@@ -27,6 +27,7 @@
|
|||||||
}
|
}
|
||||||
],
|
],
|
||||||
"environmentMode": "detect",
|
"environmentMode": "detect",
|
||||||
|
"coordinateAlignment": "source",
|
||||||
"exceptions": [],
|
"exceptions": [],
|
||||||
"expansion": "burning-crusade",
|
"expansion": "burning-crusade",
|
||||||
"extraction": {
|
"extraction": {
|
||||||
|
|||||||
@@ -8,6 +8,7 @@
|
|||||||
"clientBuild": "3.3.5a-Ascension",
|
"clientBuild": "3.3.5a-Ascension",
|
||||||
"mapDirectory": "World/Maps/Blackfathom",
|
"mapDirectory": "World/Maps/Blackfathom",
|
||||||
"environmentMode": "global-wmo",
|
"environmentMode": "global-wmo",
|
||||||
|
"coordinateAlignment": "source",
|
||||||
"difficultyVariants": [{"id": "normal", "difficulty": "normal", "spawnMask": 1, "levelRange": [20, 30]}],
|
"difficultyVariants": [{"id": "normal", "difficulty": "normal", "spawnMask": 1, "levelRange": [20, 30]}],
|
||||||
"clientSource": {"rootEnvironmentVariable": "WOW_CLIENT_ROOT", "relativeDefault": "../wow335a"},
|
"clientSource": {"rootEnvironmentVariable": "WOW_CLIENT_ROOT", "relativeDefault": "../wow335a"},
|
||||||
"serverSource": {"rootEnvironmentVariable": "WOW_SERVER_DATA", "adapter": "normalized-json", "readOnly": true},
|
"serverSource": {"rootEnvironmentVariable": "WOW_SERVER_DATA", "adapter": "normalized-json", "readOnly": true},
|
||||||
|
|||||||
@@ -8,6 +8,7 @@
|
|||||||
"clientBuild": "3.3.5a-Ascension",
|
"clientBuild": "3.3.5a-Ascension",
|
||||||
"mapDirectory": "World/Maps/BlackrockDepths",
|
"mapDirectory": "World/Maps/BlackrockDepths",
|
||||||
"environmentMode": "global-wmo",
|
"environmentMode": "global-wmo",
|
||||||
|
"coordinateAlignment": "source",
|
||||||
"difficultyVariants": [{"id": "normal", "difficulty": "normal", "spawnMask": 1, "levelRange": [50, 60]}],
|
"difficultyVariants": [{"id": "normal", "difficulty": "normal", "spawnMask": 1, "levelRange": [50, 60]}],
|
||||||
"clientSource": {"rootEnvironmentVariable": "WOW_CLIENT_ROOT", "relativeDefault": "../wow335a"},
|
"clientSource": {"rootEnvironmentVariable": "WOW_CLIENT_ROOT", "relativeDefault": "../wow335a"},
|
||||||
"serverSource": {"rootEnvironmentVariable": "WOW_SERVER_DATA", "adapter": "normalized-json", "readOnly": true},
|
"serverSource": {"rootEnvironmentVariable": "WOW_SERVER_DATA", "adapter": "normalized-json", "readOnly": true},
|
||||||
|
|||||||
@@ -8,6 +8,7 @@
|
|||||||
"clientBuild": "3.3.5a-Ascension",
|
"clientBuild": "3.3.5a-Ascension",
|
||||||
"mapDirectory": "World/Maps/BlackRockSpire",
|
"mapDirectory": "World/Maps/BlackRockSpire",
|
||||||
"environmentMode": "global-wmo",
|
"environmentMode": "global-wmo",
|
||||||
|
"coordinateAlignment": "source",
|
||||||
"difficultyVariants": [{"id": "normal", "difficulty": "normal", "spawnMask": 1, "levelRange": [55, 60]}],
|
"difficultyVariants": [{"id": "normal", "difficulty": "normal", "spawnMask": 1, "levelRange": [55, 60]}],
|
||||||
"clientSource": {"rootEnvironmentVariable": "WOW_CLIENT_ROOT", "relativeDefault": "../wow335a"},
|
"clientSource": {"rootEnvironmentVariable": "WOW_CLIENT_ROOT", "relativeDefault": "../wow335a"},
|
||||||
"serverSource": {"rootEnvironmentVariable": "WOW_SERVER_DATA", "adapter": "normalized-json", "readOnly": true},
|
"serverSource": {"rootEnvironmentVariable": "WOW_SERVER_DATA", "adapter": "normalized-json", "readOnly": true},
|
||||||
|
|||||||
@@ -8,6 +8,7 @@
|
|||||||
"clientBuild": "3.3.5a-Ascension",
|
"clientBuild": "3.3.5a-Ascension",
|
||||||
"mapDirectory": "World/Maps/HellfireDemon",
|
"mapDirectory": "World/Maps/HellfireDemon",
|
||||||
"environmentMode": "global-wmo",
|
"environmentMode": "global-wmo",
|
||||||
|
"coordinateAlignment": "source",
|
||||||
"difficultyVariants": [{"id": "normal", "difficulty": "normal", "spawnMask": 1, "levelRange": [58, 70]}],
|
"difficultyVariants": [{"id": "normal", "difficulty": "normal", "spawnMask": 1, "levelRange": [58, 70]}],
|
||||||
"clientSource": {"rootEnvironmentVariable": "WOW_CLIENT_ROOT", "relativeDefault": "../wow335a"},
|
"clientSource": {"rootEnvironmentVariable": "WOW_CLIENT_ROOT", "relativeDefault": "../wow335a"},
|
||||||
"serverSource": {"rootEnvironmentVariable": "WOW_SERVER_DATA", "adapter": "normalized-json", "readOnly": true},
|
"serverSource": {"rootEnvironmentVariable": "WOW_SERVER_DATA", "adapter": "normalized-json", "readOnly": true},
|
||||||
|
|||||||
@@ -8,6 +8,7 @@
|
|||||||
"clientBuild": "3.3.5a-Ascension",
|
"clientBuild": "3.3.5a-Ascension",
|
||||||
"mapDirectory": "World/Maps/TempestKeepAtrium",
|
"mapDirectory": "World/Maps/TempestKeepAtrium",
|
||||||
"environmentMode": "global-wmo",
|
"environmentMode": "global-wmo",
|
||||||
|
"coordinateAlignment": "source",
|
||||||
"difficultyVariants": [{"id": "normal", "difficulty": "normal", "spawnMask": 1, "levelRange": [65, 70]}],
|
"difficultyVariants": [{"id": "normal", "difficulty": "normal", "spawnMask": 1, "levelRange": [65, 70]}],
|
||||||
"clientSource": {"rootEnvironmentVariable": "WOW_CLIENT_ROOT", "relativeDefault": "../wow335a"},
|
"clientSource": {"rootEnvironmentVariable": "WOW_CLIENT_ROOT", "relativeDefault": "../wow335a"},
|
||||||
"serverSource": {"rootEnvironmentVariable": "WOW_SERVER_DATA", "adapter": "normalized-json", "readOnly": true},
|
"serverSource": {"rootEnvironmentVariable": "WOW_SERVER_DATA", "adapter": "normalized-json", "readOnly": true},
|
||||||
|
|||||||
@@ -27,6 +27,7 @@
|
|||||||
}
|
}
|
||||||
],
|
],
|
||||||
"environmentMode": "detect",
|
"environmentMode": "detect",
|
||||||
|
"coordinateAlignment": "source",
|
||||||
"exceptions": [],
|
"exceptions": [],
|
||||||
"expansion": "wrath",
|
"expansion": "wrath",
|
||||||
"extraction": {
|
"extraction": {
|
||||||
|
|||||||
@@ -8,6 +8,8 @@
|
|||||||
"clientBuild": "3.3.5a-Ascension",
|
"clientBuild": "3.3.5a-Ascension",
|
||||||
"mapDirectory": "World/Maps/DireMaul",
|
"mapDirectory": "World/Maps/DireMaul",
|
||||||
"environmentMode": "global-wmo",
|
"environmentMode": "global-wmo",
|
||||||
|
"coordinateAlignment": "source-transform",
|
||||||
|
"coordinateTransform": {"origin": [0, 0, 0], "yawDegrees": 180},
|
||||||
"difficultyVariants": [{"id": "normal", "difficulty": "normal", "spawnMask": 1, "levelRange": [50, 60]}],
|
"difficultyVariants": [{"id": "normal", "difficulty": "normal", "spawnMask": 1, "levelRange": [50, 60]}],
|
||||||
"clientSource": {"rootEnvironmentVariable": "WOW_CLIENT_ROOT", "relativeDefault": "../wow335a"},
|
"clientSource": {"rootEnvironmentVariable": "WOW_CLIENT_ROOT", "relativeDefault": "../wow335a"},
|
||||||
"serverSource": {"rootEnvironmentVariable": "WOW_SERVER_DATA", "adapter": "normalized-json", "readOnly": true},
|
"serverSource": {"rootEnvironmentVariable": "WOW_SERVER_DATA", "adapter": "normalized-json", "readOnly": true},
|
||||||
|
|||||||
@@ -27,6 +27,7 @@
|
|||||||
}
|
}
|
||||||
],
|
],
|
||||||
"environmentMode": "detect",
|
"environmentMode": "detect",
|
||||||
|
"coordinateAlignment": "source",
|
||||||
"exceptions": [],
|
"exceptions": [],
|
||||||
"expansion": "wrath",
|
"expansion": "wrath",
|
||||||
"extraction": {
|
"extraction": {
|
||||||
|
|||||||
@@ -27,6 +27,7 @@
|
|||||||
}
|
}
|
||||||
],
|
],
|
||||||
"environmentMode": "detect",
|
"environmentMode": "detect",
|
||||||
|
"coordinateAlignment": "source",
|
||||||
"exceptions": [],
|
"exceptions": [],
|
||||||
"expansion": "burning-crusade",
|
"expansion": "burning-crusade",
|
||||||
"extraction": {
|
"extraction": {
|
||||||
|
|||||||
@@ -27,6 +27,7 @@
|
|||||||
}
|
}
|
||||||
],
|
],
|
||||||
"environmentMode": "detect",
|
"environmentMode": "detect",
|
||||||
|
"coordinateAlignment": "source",
|
||||||
"exceptions": [],
|
"exceptions": [],
|
||||||
"expansion": "wrath",
|
"expansion": "wrath",
|
||||||
"extraction": {
|
"extraction": {
|
||||||
|
|||||||
@@ -8,6 +8,7 @@
|
|||||||
"clientBuild": "3.3.5a-Ascension",
|
"clientBuild": "3.3.5a-Ascension",
|
||||||
"mapDirectory": "World/Maps/GnomeragonInstance",
|
"mapDirectory": "World/Maps/GnomeragonInstance",
|
||||||
"environmentMode": "global-wmo",
|
"environmentMode": "global-wmo",
|
||||||
|
"coordinateAlignment": "source",
|
||||||
"difficultyVariants": [{"id": "normal", "difficulty": "normal", "spawnMask": 1, "levelRange": [25, 40]}],
|
"difficultyVariants": [{"id": "normal", "difficulty": "normal", "spawnMask": 1, "levelRange": [25, 40]}],
|
||||||
"clientSource": {"rootEnvironmentVariable": "WOW_CLIENT_ROOT", "relativeDefault": "../wow335a"},
|
"clientSource": {"rootEnvironmentVariable": "WOW_CLIENT_ROOT", "relativeDefault": "../wow335a"},
|
||||||
"serverSource": {"rootEnvironmentVariable": "WOW_SERVER_DATA", "adapter": "normalized-json", "readOnly": true},
|
"serverSource": {"rootEnvironmentVariable": "WOW_SERVER_DATA", "adapter": "normalized-json", "readOnly": true},
|
||||||
|
|||||||
@@ -27,6 +27,7 @@
|
|||||||
}
|
}
|
||||||
],
|
],
|
||||||
"environmentMode": "detect",
|
"environmentMode": "detect",
|
||||||
|
"coordinateAlignment": "source",
|
||||||
"exceptions": [],
|
"exceptions": [],
|
||||||
"expansion": "wrath",
|
"expansion": "wrath",
|
||||||
"extraction": {
|
"extraction": {
|
||||||
|
|||||||
@@ -27,6 +27,7 @@
|
|||||||
}
|
}
|
||||||
],
|
],
|
||||||
"environmentMode": "detect",
|
"environmentMode": "detect",
|
||||||
|
"coordinateAlignment": "source",
|
||||||
"exceptions": [],
|
"exceptions": [],
|
||||||
"expansion": "wrath",
|
"expansion": "wrath",
|
||||||
"extraction": {
|
"extraction": {
|
||||||
|
|||||||
@@ -27,6 +27,7 @@
|
|||||||
}
|
}
|
||||||
],
|
],
|
||||||
"environmentMode": "detect",
|
"environmentMode": "detect",
|
||||||
|
"coordinateAlignment": "source",
|
||||||
"exceptions": [],
|
"exceptions": [],
|
||||||
"expansion": "wrath",
|
"expansion": "wrath",
|
||||||
"extraction": {
|
"extraction": {
|
||||||
|
|||||||
@@ -27,6 +27,7 @@
|
|||||||
}
|
}
|
||||||
],
|
],
|
||||||
"environmentMode": "detect",
|
"environmentMode": "detect",
|
||||||
|
"coordinateAlignment": "source",
|
||||||
"exceptions": [],
|
"exceptions": [],
|
||||||
"expansion": "wrath",
|
"expansion": "wrath",
|
||||||
"extraction": {
|
"extraction": {
|
||||||
|
|||||||
@@ -8,6 +8,8 @@
|
|||||||
"clientBuild": "3.3.5a-Ascension",
|
"clientBuild": "3.3.5a-Ascension",
|
||||||
"mapDirectory": "World/Maps/HellfireRampart",
|
"mapDirectory": "World/Maps/HellfireRampart",
|
||||||
"environmentMode": "global-wmo",
|
"environmentMode": "global-wmo",
|
||||||
|
"coordinateAlignment": "source-transform",
|
||||||
|
"coordinateTransform": {"origin": [1262.4427, -60.8902, 1622.5397], "yawDegrees": 262.143},
|
||||||
"difficultyVariants": [{"id": "normal", "difficulty": "normal", "spawnMask": 1, "levelRange": [58, 70]}],
|
"difficultyVariants": [{"id": "normal", "difficulty": "normal", "spawnMask": 1, "levelRange": [58, 70]}],
|
||||||
"clientSource": {"rootEnvironmentVariable": "WOW_CLIENT_ROOT", "relativeDefault": "../wow335a"},
|
"clientSource": {"rootEnvironmentVariable": "WOW_CLIENT_ROOT", "relativeDefault": "../wow335a"},
|
||||||
"serverSource": {"rootEnvironmentVariable": "WOW_SERVER_DATA", "adapter": "normalized-json", "readOnly": true},
|
"serverSource": {"rootEnvironmentVariable": "WOW_SERVER_DATA", "adapter": "normalized-json", "readOnly": true},
|
||||||
|
|||||||
@@ -8,6 +8,7 @@
|
|||||||
"clientBuild": "3.3.5a-Ascension",
|
"clientBuild": "3.3.5a-Ascension",
|
||||||
"mapDirectory": "World/Maps/Sunwell5ManFix",
|
"mapDirectory": "World/Maps/Sunwell5ManFix",
|
||||||
"environmentMode": "global-wmo",
|
"environmentMode": "global-wmo",
|
||||||
|
"coordinateAlignment": "source",
|
||||||
"difficultyVariants": [{"id": "normal", "difficulty": "normal", "spawnMask": 1, "levelRange": [70, 70]}],
|
"difficultyVariants": [{"id": "normal", "difficulty": "normal", "spawnMask": 1, "levelRange": [70, 70]}],
|
||||||
"clientSource": {"rootEnvironmentVariable": "WOW_CLIENT_ROOT", "relativeDefault": "../wow335a"},
|
"clientSource": {"rootEnvironmentVariable": "WOW_CLIENT_ROOT", "relativeDefault": "../wow335a"},
|
||||||
"serverSource": {"rootEnvironmentVariable": "WOW_SERVER_DATA", "adapter": "normalized-json", "readOnly": true},
|
"serverSource": {"rootEnvironmentVariable": "WOW_SERVER_DATA", "adapter": "normalized-json", "readOnly": true},
|
||||||
|
|||||||
@@ -8,6 +8,7 @@
|
|||||||
"clientBuild": "3.3.5a-Ascension",
|
"clientBuild": "3.3.5a-Ascension",
|
||||||
"mapDirectory": "World/Maps/AuchindounEthereal",
|
"mapDirectory": "World/Maps/AuchindounEthereal",
|
||||||
"environmentMode": "global-wmo",
|
"environmentMode": "global-wmo",
|
||||||
|
"coordinateAlignment": "source",
|
||||||
"difficultyVariants": [{"id": "normal", "difficulty": "normal", "spawnMask": 1, "levelRange": [65, 70]}],
|
"difficultyVariants": [{"id": "normal", "difficulty": "normal", "spawnMask": 1, "levelRange": [65, 70]}],
|
||||||
"clientSource": {"rootEnvironmentVariable": "WOW_CLIENT_ROOT", "relativeDefault": "../wow335a"},
|
"clientSource": {"rootEnvironmentVariable": "WOW_CLIENT_ROOT", "relativeDefault": "../wow335a"},
|
||||||
"serverSource": {"rootEnvironmentVariable": "WOW_SERVER_DATA", "adapter": "normalized-json", "readOnly": true},
|
"serverSource": {"rootEnvironmentVariable": "WOW_SERVER_DATA", "adapter": "normalized-json", "readOnly": true},
|
||||||
|
|||||||
@@ -8,6 +8,7 @@
|
|||||||
"clientBuild": "3.3.5a-Ascension",
|
"clientBuild": "3.3.5a-Ascension",
|
||||||
"mapDirectory": "World/Maps/Mauradon",
|
"mapDirectory": "World/Maps/Mauradon",
|
||||||
"environmentMode": "global-wmo",
|
"environmentMode": "global-wmo",
|
||||||
|
"coordinateAlignment": "source",
|
||||||
"difficultyVariants": [{"id": "normal", "difficulty": "normal", "spawnMask": 1, "levelRange": [40, 55]}],
|
"difficultyVariants": [{"id": "normal", "difficulty": "normal", "spawnMask": 1, "levelRange": [40, 55]}],
|
||||||
"clientSource": {"rootEnvironmentVariable": "WOW_CLIENT_ROOT", "relativeDefault": "../wow335a"},
|
"clientSource": {"rootEnvironmentVariable": "WOW_CLIENT_ROOT", "relativeDefault": "../wow335a"},
|
||||||
"serverSource": {"rootEnvironmentVariable": "WOW_SERVER_DATA", "adapter": "normalized-json", "readOnly": true},
|
"serverSource": {"rootEnvironmentVariable": "WOW_SERVER_DATA", "adapter": "normalized-json", "readOnly": true},
|
||||||
|
|||||||
@@ -8,6 +8,7 @@
|
|||||||
"clientBuild": "3.3.5a-Ascension",
|
"clientBuild": "3.3.5a-Ascension",
|
||||||
"mapDirectory": "World/Maps/TempestKeepFactory",
|
"mapDirectory": "World/Maps/TempestKeepFactory",
|
||||||
"environmentMode": "global-wmo",
|
"environmentMode": "global-wmo",
|
||||||
|
"coordinateAlignment": "source",
|
||||||
"difficultyVariants": [{"id": "normal", "difficulty": "normal", "spawnMask": 1, "levelRange": [65, 70]}],
|
"difficultyVariants": [{"id": "normal", "difficulty": "normal", "spawnMask": 1, "levelRange": [65, 70]}],
|
||||||
"clientSource": {"rootEnvironmentVariable": "WOW_CLIENT_ROOT", "relativeDefault": "../wow335a"},
|
"clientSource": {"rootEnvironmentVariable": "WOW_CLIENT_ROOT", "relativeDefault": "../wow335a"},
|
||||||
"serverSource": {"rootEnvironmentVariable": "WOW_SERVER_DATA", "adapter": "normalized-json", "readOnly": true},
|
"serverSource": {"rootEnvironmentVariable": "WOW_SERVER_DATA", "adapter": "normalized-json", "readOnly": true},
|
||||||
|
|||||||
@@ -27,6 +27,7 @@
|
|||||||
}
|
}
|
||||||
],
|
],
|
||||||
"environmentMode": "detect",
|
"environmentMode": "detect",
|
||||||
|
"coordinateAlignment": "source",
|
||||||
"exceptions": [],
|
"exceptions": [],
|
||||||
"expansion": "wrath",
|
"expansion": "wrath",
|
||||||
"extraction": {
|
"extraction": {
|
||||||
|
|||||||
@@ -8,6 +8,7 @@
|
|||||||
"clientBuild": "3.3.5a-Ascension",
|
"clientBuild": "3.3.5a-Ascension",
|
||||||
"mapDirectory": "World/Maps/OrgrimmarInstance",
|
"mapDirectory": "World/Maps/OrgrimmarInstance",
|
||||||
"environmentMode": "global-wmo",
|
"environmentMode": "global-wmo",
|
||||||
|
"globalWmoAlignment": "source",
|
||||||
"difficultyVariants": [{"id": "normal", "difficulty": "normal", "spawnMask": 1, "levelRange": [13, 25]}],
|
"difficultyVariants": [{"id": "normal", "difficulty": "normal", "spawnMask": 1, "levelRange": [13, 25]}],
|
||||||
"clientSource": {"rootEnvironmentVariable": "WOW_CLIENT_ROOT", "relativeDefault": "../wow335a"},
|
"clientSource": {"rootEnvironmentVariable": "WOW_CLIENT_ROOT", "relativeDefault": "../wow335a"},
|
||||||
"serverSource": {"rootEnvironmentVariable": "WOW_SERVER_DATA", "adapter": "normalized-json", "readOnly": true},
|
"serverSource": {"rootEnvironmentVariable": "WOW_SERVER_DATA", "adapter": "normalized-json", "readOnly": true},
|
||||||
|
|||||||
@@ -8,6 +8,8 @@
|
|||||||
"clientBuild": "3.3.5a-Ascension",
|
"clientBuild": "3.3.5a-Ascension",
|
||||||
"mapDirectory": "World/Maps/RazorfenDowns",
|
"mapDirectory": "World/Maps/RazorfenDowns",
|
||||||
"environmentMode": "global-wmo",
|
"environmentMode": "global-wmo",
|
||||||
|
"coordinateAlignment": "source-transform",
|
||||||
|
"coordinateTransform": {"origin": [-2402.2809, 62, 1208.3219], "yawDegrees": 0},
|
||||||
"difficultyVariants": [{"id": "normal", "difficulty": "normal", "spawnMask": 1, "levelRange": [35, 50]}],
|
"difficultyVariants": [{"id": "normal", "difficulty": "normal", "spawnMask": 1, "levelRange": [35, 50]}],
|
||||||
"clientSource": {"rootEnvironmentVariable": "WOW_CLIENT_ROOT", "relativeDefault": "../wow335a"},
|
"clientSource": {"rootEnvironmentVariable": "WOW_CLIENT_ROOT", "relativeDefault": "../wow335a"},
|
||||||
"serverSource": {"rootEnvironmentVariable": "WOW_SERVER_DATA", "adapter": "normalized-json", "readOnly": true},
|
"serverSource": {"rootEnvironmentVariable": "WOW_SERVER_DATA", "adapter": "normalized-json", "readOnly": true},
|
||||||
|
|||||||
@@ -8,7 +8,8 @@
|
|||||||
"clientBuild": "3.3.5a-Ascension",
|
"clientBuild": "3.3.5a-Ascension",
|
||||||
"mapDirectory": "World/Maps/RazorfenKraulInstance",
|
"mapDirectory": "World/Maps/RazorfenKraulInstance",
|
||||||
"environmentMode": "global-wmo",
|
"environmentMode": "global-wmo",
|
||||||
"globalWmoSourceBasis": "flip-x",
|
"coordinateAlignment": "source-transform",
|
||||||
|
"coordinateTransform": {"origin": [-1952.1803, 87.684, 1587.8923], "yawDegrees": -133},
|
||||||
"difficultyVariants": [{"id": "normal", "difficulty": "normal", "spawnMask": 1, "levelRange": [25, 40]}],
|
"difficultyVariants": [{"id": "normal", "difficulty": "normal", "spawnMask": 1, "levelRange": [25, 40]}],
|
||||||
"clientSource": {"rootEnvironmentVariable": "WOW_CLIENT_ROOT", "relativeDefault": "../wow335a"},
|
"clientSource": {"rootEnvironmentVariable": "WOW_CLIENT_ROOT", "relativeDefault": "../wow335a"},
|
||||||
"serverSource": {"rootEnvironmentVariable": "WOW_SERVER_DATA", "adapter": "normalized-json", "readOnly": true},
|
"serverSource": {"rootEnvironmentVariable": "WOW_SERVER_DATA", "adapter": "normalized-json", "readOnly": true},
|
||||||
|
|||||||
@@ -8,6 +8,8 @@
|
|||||||
"clientBuild": "3.3.5a",
|
"clientBuild": "3.3.5a",
|
||||||
"mapDirectory": "World/Maps/MonasteryInstances",
|
"mapDirectory": "World/Maps/MonasteryInstances",
|
||||||
"environmentMode": "detect",
|
"environmentMode": "detect",
|
||||||
|
"coordinateAlignment": "source",
|
||||||
|
"adtSourceBasis": "z-y-negative-x",
|
||||||
"difficultyVariants": [
|
"difficultyVariants": [
|
||||||
{ "id": "normal", "difficulty": "normal", "spawnMask": 1, "levelRange": [26, 45] }
|
{ "id": "normal", "difficulty": "normal", "spawnMask": 1, "levelRange": [26, 45] }
|
||||||
],
|
],
|
||||||
|
|||||||
@@ -8,6 +8,8 @@
|
|||||||
"clientBuild": "3.3.5a-Ascension",
|
"clientBuild": "3.3.5a-Ascension",
|
||||||
"mapDirectory": "World/Maps/SchoolofNecromancy",
|
"mapDirectory": "World/Maps/SchoolofNecromancy",
|
||||||
"environmentMode": "global-wmo",
|
"environmentMode": "global-wmo",
|
||||||
|
"coordinateAlignment": "source-transform",
|
||||||
|
"coordinateTransform": {"origin": [-141.694, 148.0683, 105.6784], "yawDegrees": 1},
|
||||||
"difficultyVariants": [{"id": "normal", "difficulty": "normal", "spawnMask": 1, "levelRange": [55, 60]}],
|
"difficultyVariants": [{"id": "normal", "difficulty": "normal", "spawnMask": 1, "levelRange": [55, 60]}],
|
||||||
"clientSource": {"rootEnvironmentVariable": "WOW_CLIENT_ROOT", "relativeDefault": "../wow335a"},
|
"clientSource": {"rootEnvironmentVariable": "WOW_CLIENT_ROOT", "relativeDefault": "../wow335a"},
|
||||||
"serverSource": {"rootEnvironmentVariable": "WOW_SERVER_DATA", "adapter": "normalized-json", "readOnly": true},
|
"serverSource": {"rootEnvironmentVariable": "WOW_SERVER_DATA", "adapter": "normalized-json", "readOnly": true},
|
||||||
|
|||||||
@@ -8,6 +8,7 @@
|
|||||||
"clientBuild": "3.3.5a-Ascension",
|
"clientBuild": "3.3.5a-Ascension",
|
||||||
"mapDirectory": "World/Maps/AuchindounDemon",
|
"mapDirectory": "World/Maps/AuchindounDemon",
|
||||||
"environmentMode": "global-wmo",
|
"environmentMode": "global-wmo",
|
||||||
|
"coordinateAlignment": "source",
|
||||||
"difficultyVariants": [{"id": "normal", "difficulty": "normal", "spawnMask": 1, "levelRange": [65, 70]}],
|
"difficultyVariants": [{"id": "normal", "difficulty": "normal", "spawnMask": 1, "levelRange": [65, 70]}],
|
||||||
"clientSource": {"rootEnvironmentVariable": "WOW_CLIENT_ROOT", "relativeDefault": "../wow335a"},
|
"clientSource": {"rootEnvironmentVariable": "WOW_CLIENT_ROOT", "relativeDefault": "../wow335a"},
|
||||||
"serverSource": {"rootEnvironmentVariable": "WOW_SERVER_DATA", "adapter": "normalized-json", "readOnly": true},
|
"serverSource": {"rootEnvironmentVariable": "WOW_SERVER_DATA", "adapter": "normalized-json", "readOnly": true},
|
||||||
|
|||||||
@@ -8,6 +8,7 @@
|
|||||||
"clientBuild": "3.3.5a-Ascension",
|
"clientBuild": "3.3.5a-Ascension",
|
||||||
"mapDirectory": "World/Maps/AuchindounShadow",
|
"mapDirectory": "World/Maps/AuchindounShadow",
|
||||||
"environmentMode": "global-wmo",
|
"environmentMode": "global-wmo",
|
||||||
|
"coordinateAlignment": "source",
|
||||||
"difficultyVariants": [{"id": "normal", "difficulty": "normal", "spawnMask": 1, "levelRange": [65, 70]}],
|
"difficultyVariants": [{"id": "normal", "difficulty": "normal", "spawnMask": 1, "levelRange": [65, 70]}],
|
||||||
"clientSource": {"rootEnvironmentVariable": "WOW_CLIENT_ROOT", "relativeDefault": "../wow335a"},
|
"clientSource": {"rootEnvironmentVariable": "WOW_CLIENT_ROOT", "relativeDefault": "../wow335a"},
|
||||||
"serverSource": {"rootEnvironmentVariable": "WOW_SERVER_DATA", "adapter": "normalized-json", "readOnly": true},
|
"serverSource": {"rootEnvironmentVariable": "WOW_SERVER_DATA", "adapter": "normalized-json", "readOnly": true},
|
||||||
|
|||||||
@@ -7,12 +7,14 @@
|
|||||||
"expansion": "classic",
|
"expansion": "classic",
|
||||||
"clientBuild": "3.3.5a-Ascension",
|
"clientBuild": "3.3.5a-Ascension",
|
||||||
"mapDirectory": "World/Maps/Shadowfang",
|
"mapDirectory": "World/Maps/Shadowfang",
|
||||||
"environmentMode": "global-wmo",
|
"environmentMode": "detect",
|
||||||
|
"coordinateAlignment": "source",
|
||||||
|
"adtSourceBasis": "z-y-negative-x",
|
||||||
"difficultyVariants": [{"id": "normal", "difficulty": "normal", "spawnMask": 1, "levelRange": [18, 30]}],
|
"difficultyVariants": [{"id": "normal", "difficulty": "normal", "spawnMask": 1, "levelRange": [18, 30]}],
|
||||||
"clientSource": {"rootEnvironmentVariable": "WOW_CLIENT_ROOT", "relativeDefault": "../wow335a"},
|
"clientSource": {"rootEnvironmentVariable": "WOW_CLIENT_ROOT", "relativeDefault": "../wow335a"},
|
||||||
"serverSource": {"rootEnvironmentVariable": "WOW_SERVER_DATA", "adapter": "normalized-json", "readOnly": true},
|
"serverSource": {"rootEnvironmentVariable": "WOW_SERVER_DATA", "adapter": "normalized-json", "readOnly": true},
|
||||||
"extraction": {"rootWmo": "World/wmo/Dungeon/LD_ShadowFang/LD_ShadowFang.wmo", "doodadSets": ["Set_$DefaultGlobal"]},
|
"extraction": {"adtTiles": "all-present"},
|
||||||
"navigation": {"cellSize": 0.2, "cellHeight": 0.1, "agentHeight": 1.8, "agentRadius": 0.36, "agentMaxClimb": 0.5, "agentMaxSlope": 50},
|
"navigation": {"cellSize": 0.2, "cellHeight": 0.1, "tileSize": 256, "retainTileIntermediates": false, "agentHeight": 1.8, "agentRadius": 0.36, "agentMaxClimb": 0.5, "agentMaxSlope": 50},
|
||||||
"review": {"entrance": "required", "bossOrder": "required", "optionalEncounters": "required", "presentation": "required", "offMeshLinks": "required"},
|
"review": {"entrance": "required", "bossOrder": "required", "optionalEncounters": "required", "presentation": "required", "offMeshLinks": "required"},
|
||||||
"exceptions": []
|
"exceptions": []
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -8,6 +8,7 @@
|
|||||||
"clientBuild": "3.3.5a-Ascension",
|
"clientBuild": "3.3.5a-Ascension",
|
||||||
"mapDirectory": "World/Maps/HellfireMilitary",
|
"mapDirectory": "World/Maps/HellfireMilitary",
|
||||||
"environmentMode": "global-wmo",
|
"environmentMode": "global-wmo",
|
||||||
|
"coordinateAlignment": "source",
|
||||||
"difficultyVariants": [{"id": "normal", "difficulty": "normal", "spawnMask": 1, "levelRange": [60, 70]}],
|
"difficultyVariants": [{"id": "normal", "difficulty": "normal", "spawnMask": 1, "levelRange": [60, 70]}],
|
||||||
"clientSource": {"rootEnvironmentVariable": "WOW_CLIENT_ROOT", "relativeDefault": "../wow335a"},
|
"clientSource": {"rootEnvironmentVariable": "WOW_CLIENT_ROOT", "relativeDefault": "../wow335a"},
|
||||||
"serverSource": {"rootEnvironmentVariable": "WOW_SERVER_DATA", "adapter": "normalized-json", "readOnly": true},
|
"serverSource": {"rootEnvironmentVariable": "WOW_SERVER_DATA", "adapter": "normalized-json", "readOnly": true},
|
||||||
|
|||||||
@@ -8,6 +8,7 @@
|
|||||||
"clientBuild": "3.3.5a-Ascension",
|
"clientBuild": "3.3.5a-Ascension",
|
||||||
"mapDirectory": "World/Maps/CoilfangDraenei",
|
"mapDirectory": "World/Maps/CoilfangDraenei",
|
||||||
"environmentMode": "global-wmo",
|
"environmentMode": "global-wmo",
|
||||||
|
"coordinateAlignment": "source",
|
||||||
"difficultyVariants": [{"id": "normal", "difficulty": "normal", "spawnMask": 1, "levelRange": [60, 70]}],
|
"difficultyVariants": [{"id": "normal", "difficulty": "normal", "spawnMask": 1, "levelRange": [60, 70]}],
|
||||||
"clientSource": {"rootEnvironmentVariable": "WOW_CLIENT_ROOT", "relativeDefault": "../wow335a"},
|
"clientSource": {"rootEnvironmentVariable": "WOW_CLIENT_ROOT", "relativeDefault": "../wow335a"},
|
||||||
"serverSource": {"rootEnvironmentVariable": "WOW_SERVER_DATA", "adapter": "normalized-json", "readOnly": true},
|
"serverSource": {"rootEnvironmentVariable": "WOW_SERVER_DATA", "adapter": "normalized-json", "readOnly": true},
|
||||||
|
|||||||
@@ -8,6 +8,7 @@
|
|||||||
"clientBuild": "3.3.5a-Ascension",
|
"clientBuild": "3.3.5a-Ascension",
|
||||||
"mapDirectory": "World/Maps/CoilfangPumping",
|
"mapDirectory": "World/Maps/CoilfangPumping",
|
||||||
"environmentMode": "global-wmo",
|
"environmentMode": "global-wmo",
|
||||||
|
"coordinateAlignment": "source",
|
||||||
"difficultyVariants": [{"id": "normal", "difficulty": "normal", "spawnMask": 1, "levelRange": [60, 70]}],
|
"difficultyVariants": [{"id": "normal", "difficulty": "normal", "spawnMask": 1, "levelRange": [60, 70]}],
|
||||||
"clientSource": {"rootEnvironmentVariable": "WOW_CLIENT_ROOT", "relativeDefault": "../wow335a"},
|
"clientSource": {"rootEnvironmentVariable": "WOW_CLIENT_ROOT", "relativeDefault": "../wow335a"},
|
||||||
"serverSource": {"rootEnvironmentVariable": "WOW_SERVER_DATA", "adapter": "normalized-json", "readOnly": true},
|
"serverSource": {"rootEnvironmentVariable": "WOW_SERVER_DATA", "adapter": "normalized-json", "readOnly": true},
|
||||||
|
|||||||
@@ -8,6 +8,8 @@
|
|||||||
"clientBuild": "3.3.5a-Ascension",
|
"clientBuild": "3.3.5a-Ascension",
|
||||||
"mapDirectory": "World/Maps/Stratholme",
|
"mapDirectory": "World/Maps/Stratholme",
|
||||||
"environmentMode": "global-wmo",
|
"environmentMode": "global-wmo",
|
||||||
|
"coordinateAlignment": "source-transform",
|
||||||
|
"coordinateTransform": {"origin": [-3449.6608, 144.3848, -3379.6966], "yawDegrees": 0},
|
||||||
"difficultyVariants": [{"id": "normal", "difficulty": "normal", "spawnMask": 1, "levelRange": [55, 60]}],
|
"difficultyVariants": [{"id": "normal", "difficulty": "normal", "spawnMask": 1, "levelRange": [55, 60]}],
|
||||||
"clientSource": {"rootEnvironmentVariable": "WOW_CLIENT_ROOT", "relativeDefault": "../wow335a"},
|
"clientSource": {"rootEnvironmentVariable": "WOW_CLIENT_ROOT", "relativeDefault": "../wow335a"},
|
||||||
"serverSource": {"rootEnvironmentVariable": "WOW_SERVER_DATA", "adapter": "normalized-json", "readOnly": true},
|
"serverSource": {"rootEnvironmentVariable": "WOW_SERVER_DATA", "adapter": "normalized-json", "readOnly": true},
|
||||||
|
|||||||
@@ -8,6 +8,7 @@
|
|||||||
"clientBuild": "3.3.5a-Ascension",
|
"clientBuild": "3.3.5a-Ascension",
|
||||||
"mapDirectory": "World/Maps/SunkenTemple",
|
"mapDirectory": "World/Maps/SunkenTemple",
|
||||||
"environmentMode": "global-wmo",
|
"environmentMode": "global-wmo",
|
||||||
|
"coordinateAlignment": "source",
|
||||||
"difficultyVariants": [{"id": "normal", "difficulty": "normal", "spawnMask": 1, "levelRange": [45, 55]}],
|
"difficultyVariants": [{"id": "normal", "difficulty": "normal", "spawnMask": 1, "levelRange": [45, 55]}],
|
||||||
"clientSource": {"rootEnvironmentVariable": "WOW_CLIENT_ROOT", "relativeDefault": "../wow335a"},
|
"clientSource": {"rootEnvironmentVariable": "WOW_CLIENT_ROOT", "relativeDefault": "../wow335a"},
|
||||||
"serverSource": {"rootEnvironmentVariable": "WOW_SERVER_DATA", "adapter": "normalized-json", "readOnly": true},
|
"serverSource": {"rootEnvironmentVariable": "WOW_SERVER_DATA", "adapter": "normalized-json", "readOnly": true},
|
||||||
|
|||||||
@@ -27,6 +27,7 @@
|
|||||||
}
|
}
|
||||||
],
|
],
|
||||||
"environmentMode": "detect",
|
"environmentMode": "detect",
|
||||||
|
"coordinateAlignment": "source",
|
||||||
"exceptions": [],
|
"exceptions": [],
|
||||||
"expansion": "wrath",
|
"expansion": "wrath",
|
||||||
"extraction": {
|
"extraction": {
|
||||||
|
|||||||
@@ -27,6 +27,7 @@
|
|||||||
}
|
}
|
||||||
],
|
],
|
||||||
"environmentMode": "detect",
|
"environmentMode": "detect",
|
||||||
|
"coordinateAlignment": "source",
|
||||||
"exceptions": [],
|
"exceptions": [],
|
||||||
"expansion": "wrath",
|
"expansion": "wrath",
|
||||||
"extraction": {
|
"extraction": {
|
||||||
|
|||||||
@@ -27,6 +27,7 @@
|
|||||||
}
|
}
|
||||||
],
|
],
|
||||||
"environmentMode": "detect",
|
"environmentMode": "detect",
|
||||||
|
"coordinateAlignment": "source",
|
||||||
"exceptions": [],
|
"exceptions": [],
|
||||||
"expansion": "wrath",
|
"expansion": "wrath",
|
||||||
"extraction": {
|
"extraction": {
|
||||||
|
|||||||
@@ -8,6 +8,7 @@
|
|||||||
"clientBuild": "3.3.5a-Ascension",
|
"clientBuild": "3.3.5a-Ascension",
|
||||||
"mapDirectory": "World/Maps/Uldaman",
|
"mapDirectory": "World/Maps/Uldaman",
|
||||||
"environmentMode": "global-wmo",
|
"environmentMode": "global-wmo",
|
||||||
|
"coordinateAlignment": "source",
|
||||||
"difficultyVariants": [
|
"difficultyVariants": [
|
||||||
{
|
{
|
||||||
"id": "normal",
|
"id": "normal",
|
||||||
|
|||||||
@@ -8,6 +8,7 @@
|
|||||||
"clientBuild": "3.3.5a-Ascension",
|
"clientBuild": "3.3.5a-Ascension",
|
||||||
"mapDirectory": "World/Maps/CoilfangMarsh",
|
"mapDirectory": "World/Maps/CoilfangMarsh",
|
||||||
"environmentMode": "global-wmo",
|
"environmentMode": "global-wmo",
|
||||||
|
"coordinateAlignment": "source",
|
||||||
"difficultyVariants": [{"id": "normal", "difficulty": "normal", "spawnMask": 1, "levelRange": [60, 70]}],
|
"difficultyVariants": [{"id": "normal", "difficulty": "normal", "spawnMask": 1, "levelRange": [60, 70]}],
|
||||||
"clientSource": {"rootEnvironmentVariable": "WOW_CLIENT_ROOT", "relativeDefault": "../wow335a"},
|
"clientSource": {"rootEnvironmentVariable": "WOW_CLIENT_ROOT", "relativeDefault": "../wow335a"},
|
||||||
"serverSource": {"rootEnvironmentVariable": "WOW_SERVER_DATA", "adapter": "normalized-json", "readOnly": true},
|
"serverSource": {"rootEnvironmentVariable": "WOW_SERVER_DATA", "adapter": "normalized-json", "readOnly": true},
|
||||||
|
|||||||
@@ -27,6 +27,7 @@
|
|||||||
}
|
}
|
||||||
],
|
],
|
||||||
"environmentMode": "detect",
|
"environmentMode": "detect",
|
||||||
|
"coordinateAlignment": "source",
|
||||||
"exceptions": [],
|
"exceptions": [],
|
||||||
"expansion": "wrath",
|
"expansion": "wrath",
|
||||||
"extraction": {
|
"extraction": {
|
||||||
|
|||||||
@@ -27,6 +27,7 @@
|
|||||||
}
|
}
|
||||||
],
|
],
|
||||||
"environmentMode": "detect",
|
"environmentMode": "detect",
|
||||||
|
"coordinateAlignment": "source",
|
||||||
"exceptions": [],
|
"exceptions": [],
|
||||||
"expansion": "wrath",
|
"expansion": "wrath",
|
||||||
"extraction": {
|
"extraction": {
|
||||||
|
|||||||
@@ -27,6 +27,7 @@
|
|||||||
}
|
}
|
||||||
],
|
],
|
||||||
"environmentMode": "detect",
|
"environmentMode": "detect",
|
||||||
|
"coordinateAlignment": "source",
|
||||||
"exceptions": [],
|
"exceptions": [],
|
||||||
"expansion": "wrath",
|
"expansion": "wrath",
|
||||||
"extraction": {
|
"extraction": {
|
||||||
|
|||||||
@@ -8,6 +8,8 @@
|
|||||||
"clientBuild": "3.3.5a-Ascension",
|
"clientBuild": "3.3.5a-Ascension",
|
||||||
"mapDirectory": "World/Maps/TanarisInstance",
|
"mapDirectory": "World/Maps/TanarisInstance",
|
||||||
"environmentMode": "detect",
|
"environmentMode": "detect",
|
||||||
|
"coordinateAlignment": "source",
|
||||||
|
"adtSourceBasis": "z-y-negative-x",
|
||||||
"difficultyVariants": [
|
"difficultyVariants": [
|
||||||
{ "id": "normal", "difficulty": "normal", "spawnMask": 1, "levelRange": [40, 54] }
|
{ "id": "normal", "difficulty": "normal", "spawnMask": 1, "levelRange": [40, 54] }
|
||||||
],
|
],
|
||||||
|
|||||||
Generated
+2
-2
@@ -1,12 +1,12 @@
|
|||||||
{
|
{
|
||||||
"name": "healer-man",
|
"name": "healer-man",
|
||||||
"version": "0.1.6",
|
"version": "0.1.8",
|
||||||
"lockfileVersion": 3,
|
"lockfileVersion": 3,
|
||||||
"requires": true,
|
"requires": true,
|
||||||
"packages": {
|
"packages": {
|
||||||
"": {
|
"": {
|
||||||
"name": "healer-man",
|
"name": "healer-man",
|
||||||
"version": "0.1.6",
|
"version": "0.1.8",
|
||||||
"dependencies": {
|
"dependencies": {
|
||||||
"@capacitor/android": "8.4.1",
|
"@capacitor/android": "8.4.1",
|
||||||
"@capacitor/core": "8.4.1",
|
"@capacitor/core": "8.4.1",
|
||||||
|
|||||||
+5
-3
@@ -1,7 +1,7 @@
|
|||||||
{
|
{
|
||||||
"name": "healer-man",
|
"name": "healer-man",
|
||||||
"private": true,
|
"private": true,
|
||||||
"version": "0.1.6",
|
"version": "0.1.8",
|
||||||
"type": "module",
|
"type": "module",
|
||||||
"scripts": {
|
"scripts": {
|
||||||
"dev": "vite --host 0.0.0.0",
|
"dev": "vite --host 0.0.0.0",
|
||||||
@@ -20,7 +20,8 @@
|
|||||||
"coa:sync:live": "node scripts/sync-ascension-coa.mjs --tooltips",
|
"coa:sync:live": "node scripts/sync-ascension-coa.mjs --tooltips",
|
||||||
"coa:icons": "node scripts/export-spell-icons.mjs",
|
"coa:icons": "node scripts/export-spell-icons.mjs",
|
||||||
"ascension:talents": "node scripts/generate-wow335-talents.mjs ../wow335a/Interface/AddOns/PlayerBotManager/PBM/PBM_TalentData.lua ../LadiksMPQEditor/client-current/area-52/patch-D/DBFilesClient/Spell.dbc src/game/wow335Talents.generated.ts ../LadiksMPQEditor/client-current/area-52/patch-D/DBFilesClient/Talent.dbc",
|
"ascension:talents": "node scripts/generate-wow335-talents.mjs ../wow335a/Interface/AddOns/PlayerBotManager/PBM/PBM_TalentData.lua ../LadiksMPQEditor/client-current/area-52/patch-D/DBFilesClient/Spell.dbc src/game/wow335Talents.generated.ts ../LadiksMPQEditor/client-current/area-52/patch-D/DBFilesClient/Talent.dbc",
|
||||||
"ascension:sync": "npm run coa:sync && npm run wow335:abilities && npm run ascension:talents && npm run coa:icons",
|
"catalogs:shard": "vite-node scripts/generate-catalog-shards.ts",
|
||||||
|
"ascension:sync": "npm run coa:sync && npm run wow335:abilities && npm run ascension:talents && npm run coa:icons && npm run catalogs:shard",
|
||||||
"manastorm:import": "node scripts/manastorm-import/cli.mjs",
|
"manastorm:import": "node scripts/manastorm-import/cli.mjs",
|
||||||
"manastorm:forensics": "node scripts/manastorm-import/forensics-cli.mjs",
|
"manastorm:forensics": "node scripts/manastorm-import/forensics-cli.mjs",
|
||||||
"manastorm:sync": "npm run manastorm:import && npm run manastorm:forensics && npm run coa:icons",
|
"manastorm:sync": "npm run manastorm:import && npm run manastorm:forensics && npm run coa:icons",
|
||||||
@@ -91,7 +92,8 @@
|
|||||||
"assets:ktx2:creatures": "node scripts/asset-pipeline/compress-creature-ktx2.mjs",
|
"assets:ktx2:creatures": "node scripts/asset-pipeline/compress-creature-ktx2.mjs",
|
||||||
"assets:ktx2:audit": "node scripts/asset-pipeline/audit-ktx2.mjs --visual-only --require-all",
|
"assets:ktx2:audit": "node scripts/asset-pipeline/audit-ktx2.mjs --visual-only --require-all",
|
||||||
"assets:ktx2:refresh-metadata": "node scripts/asset-pipeline/refresh-ktx2-metadata.mjs",
|
"assets:ktx2:refresh-metadata": "node scripts/asset-pipeline/refresh-ktx2-metadata.mjs",
|
||||||
"prebuild": "npm run loot:generate && npm run assets:ktx2:audit",
|
"assets:performance:audit": "node scripts/asset-pipeline/audit-environment-performance.mjs",
|
||||||
|
"prebuild": "npm run loot:generate && npm run assets:ktx2:audit && npm run assets:performance:audit",
|
||||||
"build": "node --max-old-space-size=8192 node_modules/typescript/bin/tsc -b && node --max-old-space-size=8192 node_modules/vite/bin/vite.js build",
|
"build": "node --max-old-space-size=8192 node_modules/typescript/bin/tsc -b && node --max-old-space-size=8192 node_modules/vite/bin/vite.js build",
|
||||||
"build:android:web": "npm run prebuild && npm run android:prepare && node --max-old-space-size=8192 node_modules/typescript/bin/tsc -b && node --max-old-space-size=8192 node_modules/vite/bin/vite.js build --mode android",
|
"build:android:web": "npm run prebuild && npm run android:prepare && node --max-old-space-size=8192 node_modules/typescript/bin/tsc -b && node --max-old-space-size=8192 node_modules/vite/bin/vite.js build --mode android",
|
||||||
"android:sync": "npm run build:android:web && cap sync android",
|
"android:sync": "npm run build:android:web && cap sync android",
|
||||||
|
|||||||
@@ -13,8 +13,8 @@
|
|||||||
{
|
{
|
||||||
"id": "visual",
|
"id": "visual",
|
||||||
"url": "/assets/game/manastorm/230-blackrockdepths/visual.glb",
|
"url": "/assets/game/manastorm/230-blackrockdepths/visual.glb",
|
||||||
"checksum": "42d57961c31f54ecc7bc78829751110fc89294e6318336b8f260e132e478d13d",
|
"checksum": "7e4c7c75a898f5000fcb686dcfc6d3c1c4e675dc3dac5f7a8d48e53372b9f364",
|
||||||
"byteLength": 10266900,
|
"byteLength": 7068884,
|
||||||
"triangleCount": 248302,
|
"triangleCount": 248302,
|
||||||
"compression": "meshopt+ktx2"
|
"compression": "meshopt+ktx2"
|
||||||
}
|
}
|
||||||
|
|||||||
File diff suppressed because it is too large
Load Diff
@@ -7,8 +7,8 @@
|
|||||||
{
|
{
|
||||||
"id": "visual",
|
"id": "visual",
|
||||||
"url": "/assets/game/manastorm/230-blackrockdepths/visual.glb",
|
"url": "/assets/game/manastorm/230-blackrockdepths/visual.glb",
|
||||||
"checksum": "42d57961c31f54ecc7bc78829751110fc89294e6318336b8f260e132e478d13d",
|
"checksum": "7e4c7c75a898f5000fcb686dcfc6d3c1c4e675dc3dac5f7a8d48e53372b9f364",
|
||||||
"byteLength": 10266900,
|
"byteLength": 7068884,
|
||||||
"triangleCount": 248302,
|
"triangleCount": 248302,
|
||||||
"compression": "meshopt+ktx2"
|
"compression": "meshopt+ktx2"
|
||||||
}
|
}
|
||||||
|
|||||||
Binary file not shown.
Binary file not shown.
Binary file not shown.
Binary file not shown.
Binary file not shown.
Binary file not shown.
Binary file not shown.
Binary file not shown.
Binary file not shown.
Binary file not shown.
Binary file not shown.
Binary file not shown.
Binary file not shown.
Binary file not shown.
Binary file not shown.
Binary file not shown.
Binary file not shown.
Binary file not shown.
Binary file not shown.
Binary file not shown.
Binary file not shown.
Binary file not shown.
Binary file not shown.
Binary file not shown.
Binary file not shown.
Binary file not shown.
Binary file not shown.
Binary file not shown.
File diff suppressed because it is too large
Load Diff
Binary file not shown.
@@ -1,4 +1,4 @@
|
|||||||
{
|
{
|
||||||
"versionName": "0.1.6",
|
"versionName": "0.1.8",
|
||||||
"versionCode": 1006
|
"versionCode": 1008
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -0,0 +1,121 @@
|
|||||||
|
#!/usr/bin/env node
|
||||||
|
import { access, mkdir, readFile, readdir, stat, writeFile } from "node:fs/promises";
|
||||||
|
import path from "node:path";
|
||||||
|
import { fileURLToPath } from "node:url";
|
||||||
|
import { analyzeGlbFile } from "./glb-performance.mjs";
|
||||||
|
|
||||||
|
const projectRoot = path.resolve(path.dirname(fileURLToPath(import.meta.url)), "../..");
|
||||||
|
const baselineFile = path.join(
|
||||||
|
projectRoot,
|
||||||
|
"scripts/asset-pipeline/environment-performance-baseline.json",
|
||||||
|
);
|
||||||
|
const defaultRoots = [
|
||||||
|
path.join(projectRoot, "public/assets/game/manastorm"),
|
||||||
|
path.join(projectRoot, "src/assets/game/dungeons"),
|
||||||
|
];
|
||||||
|
const defaultThresholds = {
|
||||||
|
maxEstimatedDrawCalls: 1_500,
|
||||||
|
maxUnbatchedDrawCallSavings: 250,
|
||||||
|
};
|
||||||
|
|
||||||
|
async function exists(file) {
|
||||||
|
try {
|
||||||
|
await access(file);
|
||||||
|
return true;
|
||||||
|
} catch {
|
||||||
|
return false;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
async function collectVisualGlbs(root, files = []) {
|
||||||
|
if (!await exists(root)) return files;
|
||||||
|
const entry = await stat(root);
|
||||||
|
if (entry.isFile()) {
|
||||||
|
if (/(?:^|[-_])visual\.glb$/i.test(path.basename(root))) files.push(root);
|
||||||
|
return files;
|
||||||
|
}
|
||||||
|
for (const child of await readdir(root, { withFileTypes: true })) {
|
||||||
|
const target = path.join(root, child.name);
|
||||||
|
if (child.isDirectory()) await collectVisualGlbs(target, files);
|
||||||
|
else if (/(?:^|[-_])visual\.glb$/i.test(child.name)) files.push(target);
|
||||||
|
}
|
||||||
|
return files;
|
||||||
|
}
|
||||||
|
|
||||||
|
function projectPath(file) {
|
||||||
|
return path.relative(projectRoot, file).split(path.sep).join("/");
|
||||||
|
}
|
||||||
|
|
||||||
|
function exceeds(report, limits) {
|
||||||
|
return report.estimatedDrawCalls > limits.maxEstimatedDrawCalls
|
||||||
|
|| report.unbatchedDrawCallSavings > limits.maxUnbatchedDrawCallSavings;
|
||||||
|
}
|
||||||
|
|
||||||
|
const args = process.argv.slice(2);
|
||||||
|
const writeBaseline = args.includes("--write-baseline");
|
||||||
|
const roots = args.filter((argument) => argument !== "--write-baseline")
|
||||||
|
.map((argument) => path.resolve(projectRoot, argument));
|
||||||
|
const files = [...new Set((await Promise.all(
|
||||||
|
(roots.length ? roots : defaultRoots).map((root) => collectVisualGlbs(root)),
|
||||||
|
)).flat())].sort();
|
||||||
|
const reports = await Promise.all(files.map(async (file) => ({
|
||||||
|
file: projectPath(file),
|
||||||
|
byteLength: (await stat(file)).size,
|
||||||
|
...(await analyzeGlbFile(file)),
|
||||||
|
})));
|
||||||
|
|
||||||
|
if (writeBaseline) {
|
||||||
|
const baseline = {
|
||||||
|
schemaVersion: 1,
|
||||||
|
thresholds: defaultThresholds,
|
||||||
|
exceptions: Object.fromEntries(
|
||||||
|
reports
|
||||||
|
.filter((report) => exceeds(report, defaultThresholds))
|
||||||
|
.map((report) => [report.file, {
|
||||||
|
maxEstimatedDrawCalls: Math.max(defaultThresholds.maxEstimatedDrawCalls, report.estimatedDrawCalls),
|
||||||
|
maxUnbatchedDrawCallSavings: Math.max(
|
||||||
|
defaultThresholds.maxUnbatchedDrawCallSavings,
|
||||||
|
report.unbatchedDrawCallSavings,
|
||||||
|
),
|
||||||
|
}]),
|
||||||
|
),
|
||||||
|
};
|
||||||
|
await mkdir(path.dirname(baselineFile), { recursive: true });
|
||||||
|
await writeFile(baselineFile, `${JSON.stringify(baseline, null, 2)}\n`, "utf8");
|
||||||
|
console.log(`Wrote ${Object.keys(baseline.exceptions).length} performance exceptions to ${projectPath(baselineFile)}.`);
|
||||||
|
process.exit(0);
|
||||||
|
}
|
||||||
|
|
||||||
|
if (!await exists(baselineFile)) {
|
||||||
|
throw new Error(`Missing ${projectPath(baselineFile)}. Run this script with --write-baseline.`);
|
||||||
|
}
|
||||||
|
const baseline = JSON.parse(await readFile(baselineFile, "utf8"));
|
||||||
|
const thresholds = { ...defaultThresholds, ...baseline.thresholds };
|
||||||
|
const regressions = reports.filter((report) => {
|
||||||
|
const limits = { ...thresholds, ...baseline.exceptions?.[report.file] };
|
||||||
|
return exceeds(report, limits);
|
||||||
|
});
|
||||||
|
const debt = reports.filter((report) => exceeds(report, thresholds));
|
||||||
|
const top = [...reports]
|
||||||
|
.sort((left, right) => right.estimatedDrawCalls - left.estimatedDrawCalls)
|
||||||
|
.slice(0, 12)
|
||||||
|
.map((report) => ({
|
||||||
|
asset: report.file,
|
||||||
|
calls: report.estimatedDrawCalls,
|
||||||
|
avoidable: report.unbatchedDrawCallSavings,
|
||||||
|
gpuBatches: report.gpuInstanceBatches,
|
||||||
|
nodes: report.nodes,
|
||||||
|
}));
|
||||||
|
|
||||||
|
console.table(top);
|
||||||
|
console.log(JSON.stringify({
|
||||||
|
status: regressions.length ? "blocked" : "green",
|
||||||
|
assets: reports.length,
|
||||||
|
knownBudgetExceptions: debt.length,
|
||||||
|
regressions: regressions.map((report) => ({
|
||||||
|
file: report.file,
|
||||||
|
estimatedDrawCalls: report.estimatedDrawCalls,
|
||||||
|
unbatchedDrawCallSavings: report.unbatchedDrawCallSavings,
|
||||||
|
})),
|
||||||
|
}, null, 2));
|
||||||
|
if (regressions.length) process.exitCode = 1;
|
||||||
@@ -0,0 +1,249 @@
|
|||||||
|
{
|
||||||
|
"schemaVersion": 1,
|
||||||
|
"thresholds": {
|
||||||
|
"maxEstimatedDrawCalls": 1500,
|
||||||
|
"maxUnbatchedDrawCallSavings": 250
|
||||||
|
},
|
||||||
|
"exceptions": {
|
||||||
|
"public/assets/game/manastorm/109-sunkentemple/visual.glb": {
|
||||||
|
"maxEstimatedDrawCalls": 1500,
|
||||||
|
"maxUnbatchedDrawCallSavings": 320
|
||||||
|
},
|
||||||
|
"public/assets/game/manastorm/129-razorfendowns/visual.glb": {
|
||||||
|
"maxEstimatedDrawCalls": 1507,
|
||||||
|
"maxUnbatchedDrawCallSavings": 1406
|
||||||
|
},
|
||||||
|
"public/assets/game/manastorm/1771-custom-marsh/visual.glb": {
|
||||||
|
"maxEstimatedDrawCalls": 2493,
|
||||||
|
"maxUnbatchedDrawCallSavings": 2114
|
||||||
|
},
|
||||||
|
"public/assets/game/manastorm/1774-custom-tropical/visual.glb": {
|
||||||
|
"maxEstimatedDrawCalls": 1500,
|
||||||
|
"maxUnbatchedDrawCallSavings": 910
|
||||||
|
},
|
||||||
|
"public/assets/game/manastorm/1775-custom-tuskarr/visual.glb": {
|
||||||
|
"maxEstimatedDrawCalls": 1500,
|
||||||
|
"maxUnbatchedDrawCallSavings": 500
|
||||||
|
},
|
||||||
|
"public/assets/game/manastorm/1776-custom-vrykul1/visual.glb": {
|
||||||
|
"maxEstimatedDrawCalls": 2223,
|
||||||
|
"maxUnbatchedDrawCallSavings": 1818
|
||||||
|
},
|
||||||
|
"public/assets/game/manastorm/1781-forgottenmine/visual.glb": {
|
||||||
|
"maxEstimatedDrawCalls": 5756,
|
||||||
|
"maxUnbatchedDrawCallSavings": 3818
|
||||||
|
},
|
||||||
|
"public/assets/game/manastorm/1785-valsharaharena/visual.glb": {
|
||||||
|
"maxEstimatedDrawCalls": 1500,
|
||||||
|
"maxUnbatchedDrawCallSavings": 392
|
||||||
|
},
|
||||||
|
"public/assets/game/manastorm/1787-baradinholdarena/visual.glb": {
|
||||||
|
"maxEstimatedDrawCalls": 10462,
|
||||||
|
"maxUnbatchedDrawCallSavings": 9362
|
||||||
|
},
|
||||||
|
"public/assets/game/manastorm/1794-twistingnether/visual.glb": {
|
||||||
|
"maxEstimatedDrawCalls": 3077,
|
||||||
|
"maxUnbatchedDrawCallSavings": 2557
|
||||||
|
},
|
||||||
|
"public/assets/game/manastorm/209-tanarisinstance/visual.glb": {
|
||||||
|
"maxEstimatedDrawCalls": 4579,
|
||||||
|
"maxUnbatchedDrawCallSavings": 3547
|
||||||
|
},
|
||||||
|
"public/assets/game/manastorm/229-blackrockspire/visual.glb": {
|
||||||
|
"maxEstimatedDrawCalls": 3972,
|
||||||
|
"maxUnbatchedDrawCallSavings": 3598
|
||||||
|
},
|
||||||
|
"public/assets/game/manastorm/269-cavernsoftime/visual.glb": {
|
||||||
|
"maxEstimatedDrawCalls": 5446,
|
||||||
|
"maxUnbatchedDrawCallSavings": 5289
|
||||||
|
},
|
||||||
|
"public/assets/game/manastorm/289-schoolofnecromancy/visual.glb": {
|
||||||
|
"maxEstimatedDrawCalls": 5140,
|
||||||
|
"maxUnbatchedDrawCallSavings": 4790
|
||||||
|
},
|
||||||
|
"public/assets/game/manastorm/309-zul-gurub/visual.glb": {
|
||||||
|
"maxEstimatedDrawCalls": 17377,
|
||||||
|
"maxUnbatchedDrawCallSavings": 15605
|
||||||
|
},
|
||||||
|
"public/assets/game/manastorm/329-stratholme/visual.glb": {
|
||||||
|
"maxEstimatedDrawCalls": 2954,
|
||||||
|
"maxUnbatchedDrawCallSavings": 2725
|
||||||
|
},
|
||||||
|
"public/assets/game/manastorm/34-stormwindjail/visual.glb": {
|
||||||
|
"maxEstimatedDrawCalls": 1500,
|
||||||
|
"maxUnbatchedDrawCallSavings": 487
|
||||||
|
},
|
||||||
|
"public/assets/game/manastorm/349-mauradon/visual.glb": {
|
||||||
|
"maxEstimatedDrawCalls": 5624,
|
||||||
|
"maxUnbatchedDrawCallSavings": 5352
|
||||||
|
},
|
||||||
|
"public/assets/game/manastorm/36-deadminesinstance/visual.glb": {
|
||||||
|
"maxEstimatedDrawCalls": 1500,
|
||||||
|
"maxUnbatchedDrawCallSavings": 309
|
||||||
|
},
|
||||||
|
"public/assets/game/manastorm/389-orgrimmarinstance/visual.glb": {
|
||||||
|
"maxEstimatedDrawCalls": 1500,
|
||||||
|
"maxUnbatchedDrawCallSavings": 881
|
||||||
|
},
|
||||||
|
"public/assets/game/manastorm/429-diremaul/visual.glb": {
|
||||||
|
"maxEstimatedDrawCalls": 5021,
|
||||||
|
"maxUnbatchedDrawCallSavings": 4636
|
||||||
|
},
|
||||||
|
"public/assets/game/manastorm/469-blackwinglair/visual.glb": {
|
||||||
|
"maxEstimatedDrawCalls": 1500,
|
||||||
|
"maxUnbatchedDrawCallSavings": 452
|
||||||
|
},
|
||||||
|
"public/assets/game/manastorm/47-razorfenkraulinstance/visual.glb": {
|
||||||
|
"maxEstimatedDrawCalls": 1500,
|
||||||
|
"maxUnbatchedDrawCallSavings": 897
|
||||||
|
},
|
||||||
|
"public/assets/game/manastorm/48-blackfathom/visual.glb": {
|
||||||
|
"maxEstimatedDrawCalls": 2164,
|
||||||
|
"maxUnbatchedDrawCallSavings": 1950
|
||||||
|
},
|
||||||
|
"public/assets/game/manastorm/509-ahnqiraj/visual.glb": {
|
||||||
|
"maxEstimatedDrawCalls": 1500,
|
||||||
|
"maxUnbatchedDrawCallSavings": 600
|
||||||
|
},
|
||||||
|
"public/assets/game/manastorm/531-ahnqirajtemple/visual.glb": {
|
||||||
|
"maxEstimatedDrawCalls": 1500,
|
||||||
|
"maxUnbatchedDrawCallSavings": 953
|
||||||
|
},
|
||||||
|
"public/assets/game/manastorm/532-karazahn/visual.glb": {
|
||||||
|
"maxEstimatedDrawCalls": 8540,
|
||||||
|
"maxUnbatchedDrawCallSavings": 7892
|
||||||
|
},
|
||||||
|
"public/assets/game/manastorm/533-stratholme-raid/visual.glb": {
|
||||||
|
"maxEstimatedDrawCalls": 4165,
|
||||||
|
"maxUnbatchedDrawCallSavings": 4014
|
||||||
|
},
|
||||||
|
"public/assets/game/manastorm/540-hellfiremilitary/visual.glb": {
|
||||||
|
"maxEstimatedDrawCalls": 1500,
|
||||||
|
"maxUnbatchedDrawCallSavings": 1150
|
||||||
|
},
|
||||||
|
"public/assets/game/manastorm/542-hellfiredemon/visual.glb": {
|
||||||
|
"maxEstimatedDrawCalls": 1500,
|
||||||
|
"maxUnbatchedDrawCallSavings": 444
|
||||||
|
},
|
||||||
|
"public/assets/game/manastorm/543-hellfirerampart/visual.glb": {
|
||||||
|
"maxEstimatedDrawCalls": 1500,
|
||||||
|
"maxUnbatchedDrawCallSavings": 296
|
||||||
|
},
|
||||||
|
"public/assets/game/manastorm/545-coilfangpumping/visual.glb": {
|
||||||
|
"maxEstimatedDrawCalls": 1500,
|
||||||
|
"maxUnbatchedDrawCallSavings": 674
|
||||||
|
},
|
||||||
|
"public/assets/game/manastorm/546-coilfangmarsh/visual.glb": {
|
||||||
|
"maxEstimatedDrawCalls": 3104,
|
||||||
|
"maxUnbatchedDrawCallSavings": 3033
|
||||||
|
},
|
||||||
|
"public/assets/game/manastorm/547-coilfangdraenei/visual.glb": {
|
||||||
|
"maxEstimatedDrawCalls": 1535,
|
||||||
|
"maxUnbatchedDrawCallSavings": 1455
|
||||||
|
},
|
||||||
|
"public/assets/game/manastorm/548-coilfangraid/visual.glb": {
|
||||||
|
"maxEstimatedDrawCalls": 1916,
|
||||||
|
"maxUnbatchedDrawCallSavings": 1683
|
||||||
|
},
|
||||||
|
"public/assets/game/manastorm/550-tempestkeepraid/visual.glb": {
|
||||||
|
"maxEstimatedDrawCalls": 1500,
|
||||||
|
"maxUnbatchedDrawCallSavings": 420
|
||||||
|
},
|
||||||
|
"public/assets/game/manastorm/552-tempestkeeparcane/visual.glb": {
|
||||||
|
"maxEstimatedDrawCalls": 1500,
|
||||||
|
"maxUnbatchedDrawCallSavings": 999
|
||||||
|
},
|
||||||
|
"public/assets/game/manastorm/553-tempestkeepatrium/visual.glb": {
|
||||||
|
"maxEstimatedDrawCalls": 1500,
|
||||||
|
"maxUnbatchedDrawCallSavings": 1115
|
||||||
|
},
|
||||||
|
"public/assets/game/manastorm/554-tempestkeepfactory/visual.glb": {
|
||||||
|
"maxEstimatedDrawCalls": 1500,
|
||||||
|
"maxUnbatchedDrawCallSavings": 872
|
||||||
|
},
|
||||||
|
"public/assets/game/manastorm/555-auchindounshadow/visual.glb": {
|
||||||
|
"maxEstimatedDrawCalls": 2624,
|
||||||
|
"maxUnbatchedDrawCallSavings": 2516
|
||||||
|
},
|
||||||
|
"public/assets/game/manastorm/556-auchindoundemon/visual.glb": {
|
||||||
|
"maxEstimatedDrawCalls": 2095,
|
||||||
|
"maxUnbatchedDrawCallSavings": 1987
|
||||||
|
},
|
||||||
|
"public/assets/game/manastorm/557-auchindounethereal/visual.glb": {
|
||||||
|
"maxEstimatedDrawCalls": 3164,
|
||||||
|
"maxUnbatchedDrawCallSavings": 3026
|
||||||
|
},
|
||||||
|
"public/assets/game/manastorm/558-auchindoundraenei/visual.glb": {
|
||||||
|
"maxEstimatedDrawCalls": 2613,
|
||||||
|
"maxUnbatchedDrawCallSavings": 2523
|
||||||
|
},
|
||||||
|
"public/assets/game/manastorm/560-hillsbradpast/visual.glb": {
|
||||||
|
"maxEstimatedDrawCalls": 28259,
|
||||||
|
"maxUnbatchedDrawCallSavings": 24733
|
||||||
|
},
|
||||||
|
"public/assets/game/manastorm/568-zulaman/visual.glb": {
|
||||||
|
"maxEstimatedDrawCalls": 4307,
|
||||||
|
"maxUnbatchedDrawCallSavings": 3850
|
||||||
|
},
|
||||||
|
"public/assets/game/manastorm/574-valgarde70/visual.glb": {
|
||||||
|
"maxEstimatedDrawCalls": 8271,
|
||||||
|
"maxUnbatchedDrawCallSavings": 7072
|
||||||
|
},
|
||||||
|
"public/assets/game/manastorm/575-utgardepinnacle/visual.glb": {
|
||||||
|
"maxEstimatedDrawCalls": 16277,
|
||||||
|
"maxUnbatchedDrawCallSavings": 14018
|
||||||
|
},
|
||||||
|
"public/assets/game/manastorm/576-nexus70/visual.glb": {
|
||||||
|
"maxEstimatedDrawCalls": 1500,
|
||||||
|
"maxUnbatchedDrawCallSavings": 396
|
||||||
|
},
|
||||||
|
"public/assets/game/manastorm/578-nexus80/visual.glb": {
|
||||||
|
"maxEstimatedDrawCalls": 2283,
|
||||||
|
"maxUnbatchedDrawCallSavings": 1896
|
||||||
|
},
|
||||||
|
"public/assets/game/manastorm/585-sunwell5manfix/visual.glb": {
|
||||||
|
"maxEstimatedDrawCalls": 1810,
|
||||||
|
"maxUnbatchedDrawCallSavings": 1618
|
||||||
|
},
|
||||||
|
"public/assets/game/manastorm/595-stratholmecot/visual.glb": {
|
||||||
|
"maxEstimatedDrawCalls": 4353,
|
||||||
|
"maxUnbatchedDrawCallSavings": 3386
|
||||||
|
},
|
||||||
|
"public/assets/game/manastorm/600-draktheronkeep/visual.glb": {
|
||||||
|
"maxEstimatedDrawCalls": 3518,
|
||||||
|
"maxUnbatchedDrawCallSavings": 2876
|
||||||
|
},
|
||||||
|
"public/assets/game/manastorm/602-ulduar80/visual.glb": {
|
||||||
|
"maxEstimatedDrawCalls": 1973,
|
||||||
|
"maxUnbatchedDrawCallSavings": 1295
|
||||||
|
},
|
||||||
|
"public/assets/game/manastorm/619-azjol-lowercity/visual.glb": {
|
||||||
|
"maxEstimatedDrawCalls": 1596,
|
||||||
|
"maxUnbatchedDrawCallSavings": 1243
|
||||||
|
},
|
||||||
|
"public/assets/game/manastorm/658-quarryoftears/visual.glb": {
|
||||||
|
"maxEstimatedDrawCalls": 1500,
|
||||||
|
"maxUnbatchedDrawCallSavings": 618
|
||||||
|
},
|
||||||
|
"public/assets/game/manastorm/668-hallsofreflection/visual.glb": {
|
||||||
|
"maxEstimatedDrawCalls": 5464,
|
||||||
|
"maxUnbatchedDrawCallSavings": 4606
|
||||||
|
},
|
||||||
|
"public/assets/game/manastorm/70-uldaman/visual.glb": {
|
||||||
|
"maxEstimatedDrawCalls": 3268,
|
||||||
|
"maxUnbatchedDrawCallSavings": 3115
|
||||||
|
},
|
||||||
|
"public/assets/game/manastorm/821-tinketechshowdown/visual.glb": {
|
||||||
|
"maxEstimatedDrawCalls": 2585,
|
||||||
|
"maxUnbatchedDrawCallSavings": 2379
|
||||||
|
},
|
||||||
|
"public/assets/game/manastorm/90-gnomeragoninstance/visual.glb": {
|
||||||
|
"maxEstimatedDrawCalls": 1500,
|
||||||
|
"maxUnbatchedDrawCallSavings": 1051
|
||||||
|
},
|
||||||
|
"public/assets/game/manastorm/908-alvaencounter/visual.glb": {
|
||||||
|
"maxEstimatedDrawCalls": 1500,
|
||||||
|
"maxUnbatchedDrawCallSavings": 1096
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
@@ -0,0 +1,98 @@
|
|||||||
|
import { open } from "node:fs/promises";
|
||||||
|
|
||||||
|
const GLB_MAGIC = 0x46546c67;
|
||||||
|
const JSON_CHUNK_TYPE = 0x4e4f534a;
|
||||||
|
const GPU_INSTANCING_EXTENSION = "EXT_mesh_gpu_instancing";
|
||||||
|
|
||||||
|
function primitiveCount(json, meshIndex) {
|
||||||
|
return json.meshes?.[meshIndex]?.primitives?.length ?? 0;
|
||||||
|
}
|
||||||
|
|
||||||
|
function gpuInstanceCount(json, node) {
|
||||||
|
const attributes = node.extensions?.[GPU_INSTANCING_EXTENSION]?.attributes;
|
||||||
|
if (!attributes || typeof attributes !== "object") return 0;
|
||||||
|
return Math.max(
|
||||||
|
0,
|
||||||
|
...Object.values(attributes).map((accessorIndex) => (
|
||||||
|
json.accessors?.[accessorIndex]?.count ?? 0
|
||||||
|
)),
|
||||||
|
);
|
||||||
|
}
|
||||||
|
|
||||||
|
export function analyzeGlbJson(json) {
|
||||||
|
const repeatedMeshes = new Map();
|
||||||
|
let estimatedDrawCalls = 0;
|
||||||
|
let expandedPrimitiveInstances = 0;
|
||||||
|
let meshNodes = 0;
|
||||||
|
let gpuInstanceBatches = 0;
|
||||||
|
let gpuInstances = 0;
|
||||||
|
|
||||||
|
for (const node of json.nodes ?? []) {
|
||||||
|
if (!Number.isInteger(node.mesh)) continue;
|
||||||
|
meshNodes += 1;
|
||||||
|
const primitives = primitiveCount(json, node.mesh);
|
||||||
|
const instances = gpuInstanceCount(json, node);
|
||||||
|
estimatedDrawCalls += primitives;
|
||||||
|
expandedPrimitiveInstances += primitives * Math.max(1, instances);
|
||||||
|
if (instances > 0) {
|
||||||
|
gpuInstanceBatches += 1;
|
||||||
|
gpuInstances += instances;
|
||||||
|
continue;
|
||||||
|
}
|
||||||
|
repeatedMeshes.set(node.mesh, (repeatedMeshes.get(node.mesh) ?? 0) + 1);
|
||||||
|
}
|
||||||
|
|
||||||
|
let eligibleRepeatedMeshes = 0;
|
||||||
|
let eligibleRepeatedNodes = 0;
|
||||||
|
let unbatchedDrawCallSavings = 0;
|
||||||
|
for (const [meshIndex, count] of repeatedMeshes) {
|
||||||
|
if (count < 3) continue;
|
||||||
|
eligibleRepeatedMeshes += 1;
|
||||||
|
eligibleRepeatedNodes += count;
|
||||||
|
unbatchedDrawCallSavings += (count - 1) * primitiveCount(json, meshIndex);
|
||||||
|
}
|
||||||
|
|
||||||
|
const materials = json.materials ?? [];
|
||||||
|
return {
|
||||||
|
nodes: json.nodes?.length ?? 0,
|
||||||
|
meshNodes,
|
||||||
|
meshes: json.meshes?.length ?? 0,
|
||||||
|
materials: materials.length,
|
||||||
|
textures: json.textures?.length ?? 0,
|
||||||
|
doubleSidedMaterials: materials.filter((material) => material.doubleSided === true).length,
|
||||||
|
blendedMaterials: materials.filter((material) => material.alphaMode === "BLEND").length,
|
||||||
|
estimatedDrawCalls,
|
||||||
|
expandedPrimitiveInstances,
|
||||||
|
gpuInstanceBatches,
|
||||||
|
gpuInstances,
|
||||||
|
eligibleRepeatedMeshes,
|
||||||
|
eligibleRepeatedNodes,
|
||||||
|
unbatchedDrawCallSavings,
|
||||||
|
usesGpuInstancing: (json.extensionsUsed ?? []).includes(GPU_INSTANCING_EXTENSION),
|
||||||
|
};
|
||||||
|
}
|
||||||
|
|
||||||
|
export async function readGlbJson(file) {
|
||||||
|
const handle = await open(file, "r");
|
||||||
|
try {
|
||||||
|
const header = Buffer.alloc(20);
|
||||||
|
const { bytesRead } = await handle.read(header, 0, header.length, 0);
|
||||||
|
if (bytesRead !== header.length
|
||||||
|
|| header.readUInt32LE(0) !== GLB_MAGIC
|
||||||
|
|| header.readUInt32LE(4) !== 2
|
||||||
|
|| header.readUInt32LE(16) !== JSON_CHUNK_TYPE) {
|
||||||
|
throw new Error(`${file} is not a supported GLB 2.0 file.`);
|
||||||
|
}
|
||||||
|
const jsonLength = header.readUInt32LE(12);
|
||||||
|
const jsonBuffer = Buffer.alloc(jsonLength);
|
||||||
|
const jsonRead = await handle.read(jsonBuffer, 0, jsonLength, 20);
|
||||||
|
if (jsonRead.bytesRead !== jsonLength) throw new Error(`${file} has a truncated JSON chunk.`);
|
||||||
|
return JSON.parse(jsonBuffer.toString("utf8"));
|
||||||
|
} finally {
|
||||||
|
await handle.close();
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
export async function analyzeGlbFile(file) {
|
||||||
|
return analyzeGlbJson(await readGlbJson(file));
|
||||||
|
}
|
||||||
@@ -0,0 +1,54 @@
|
|||||||
|
import assert from "node:assert/strict";
|
||||||
|
import test from "node:test";
|
||||||
|
import { analyzeGlbJson } from "./glb-performance.mjs";
|
||||||
|
|
||||||
|
test("counts repeated mesh placements as avoidable draw calls", () => {
|
||||||
|
const report = analyzeGlbJson({
|
||||||
|
meshes: [
|
||||||
|
{ primitives: [{}, {}] },
|
||||||
|
{ primitives: [{}] },
|
||||||
|
],
|
||||||
|
nodes: [
|
||||||
|
{ mesh: 0 },
|
||||||
|
{ mesh: 0 },
|
||||||
|
{ mesh: 0 },
|
||||||
|
{ mesh: 1 },
|
||||||
|
],
|
||||||
|
materials: [
|
||||||
|
{ doubleSided: true, alphaMode: "BLEND" },
|
||||||
|
{},
|
||||||
|
],
|
||||||
|
textures: [{}, {}],
|
||||||
|
});
|
||||||
|
|
||||||
|
assert.equal(report.estimatedDrawCalls, 7);
|
||||||
|
assert.equal(report.expandedPrimitiveInstances, 7);
|
||||||
|
assert.equal(report.eligibleRepeatedMeshes, 1);
|
||||||
|
assert.equal(report.eligibleRepeatedNodes, 3);
|
||||||
|
assert.equal(report.unbatchedDrawCallSavings, 4);
|
||||||
|
assert.equal(report.doubleSidedMaterials, 1);
|
||||||
|
assert.equal(report.blendedMaterials, 1);
|
||||||
|
});
|
||||||
|
|
||||||
|
test("counts one draw-call set for an EXT_mesh_gpu_instancing batch", () => {
|
||||||
|
const report = analyzeGlbJson({
|
||||||
|
extensionsUsed: ["EXT_mesh_gpu_instancing"],
|
||||||
|
accessors: [{ count: 12 }],
|
||||||
|
meshes: [{ primitives: [{}, {}, {}] }],
|
||||||
|
nodes: [{
|
||||||
|
mesh: 0,
|
||||||
|
extensions: {
|
||||||
|
EXT_mesh_gpu_instancing: {
|
||||||
|
attributes: { TRANSLATION: 0 },
|
||||||
|
},
|
||||||
|
},
|
||||||
|
}],
|
||||||
|
});
|
||||||
|
|
||||||
|
assert.equal(report.estimatedDrawCalls, 3);
|
||||||
|
assert.equal(report.expandedPrimitiveInstances, 36);
|
||||||
|
assert.equal(report.gpuInstanceBatches, 1);
|
||||||
|
assert.equal(report.gpuInstances, 12);
|
||||||
|
assert.equal(report.unbatchedDrawCallSavings, 0);
|
||||||
|
assert.equal(report.usesGpuInstancing, true);
|
||||||
|
});
|
||||||
@@ -0,0 +1,122 @@
|
|||||||
|
#!/usr/bin/env node
|
||||||
|
|
||||||
|
import path from "node:path";
|
||||||
|
import { fileURLToPath } from "node:url";
|
||||||
|
import { NodeIO } from "@gltf-transform/core";
|
||||||
|
import { ALL_EXTENSIONS } from "@gltf-transform/extensions";
|
||||||
|
import { BufferAttribute, BufferGeometry, Vector3 } from "three";
|
||||||
|
import { MeshBVH } from "three-mesh-bvh";
|
||||||
|
import { MeshoptDecoder } from "meshoptimizer";
|
||||||
|
|
||||||
|
const projectRoot = path.resolve(path.dirname(fileURLToPath(import.meta.url)), "../..");
|
||||||
|
const campaign = await import("../../src/game/generated/dungeonCampaignCatalog.json", {
|
||||||
|
with: { type: "json" },
|
||||||
|
}).then((module) => module.default);
|
||||||
|
const useCollision = process.argv.includes("--collision");
|
||||||
|
const requestedSlugs = new Set(process.argv.slice(2).filter((argument) => argument !== "--collision"));
|
||||||
|
const definitions = campaign.definitions.filter((definition) => (
|
||||||
|
!requestedSlugs.size || requestedSlugs.has(definition.id)
|
||||||
|
));
|
||||||
|
|
||||||
|
if (!definitions.length) {
|
||||||
|
throw new Error("No matching dungeon definitions. Pass one or more dungeon slugs.");
|
||||||
|
}
|
||||||
|
|
||||||
|
await MeshoptDecoder.ready;
|
||||||
|
const io = new NodeIO()
|
||||||
|
.registerExtensions(ALL_EXTENSIONS)
|
||||||
|
.registerDependencies({ "meshopt.decoder": MeshoptDecoder });
|
||||||
|
const point = new Vector3();
|
||||||
|
|
||||||
|
function normalizerFor(accessor, array) {
|
||||||
|
if (!accessor.getNormalized()) return (value) => value;
|
||||||
|
if (array instanceof Int16Array) return (value) => Math.max(value / 32_767, -1);
|
||||||
|
if (array instanceof Uint16Array) return (value) => value / 65_535;
|
||||||
|
if (array instanceof Int8Array) return (value) => Math.max(value / 127, -1);
|
||||||
|
if (array instanceof Uint8Array) return (value) => value / 255;
|
||||||
|
return (value) => value;
|
||||||
|
}
|
||||||
|
|
||||||
|
async function geometryTrees(file) {
|
||||||
|
const document = await io.read(file);
|
||||||
|
const trees = [];
|
||||||
|
for (const node of document.getRoot().listNodes()) {
|
||||||
|
const mesh = node.getMesh();
|
||||||
|
if (!mesh) continue;
|
||||||
|
const matrix = node.getWorldMatrix();
|
||||||
|
for (const primitive of mesh.listPrimitives()) {
|
||||||
|
const accessor = primitive.getAttribute("POSITION");
|
||||||
|
const source = accessor?.getArray();
|
||||||
|
if (!accessor || !source) continue;
|
||||||
|
const normalize = normalizerFor(accessor, source);
|
||||||
|
const positions = new Float32Array(source.length);
|
||||||
|
for (let offset = 0; offset < source.length; offset += 3) {
|
||||||
|
const x = normalize(source[offset]);
|
||||||
|
const y = normalize(source[offset + 1]);
|
||||||
|
const z = normalize(source[offset + 2]);
|
||||||
|
positions[offset] = matrix[0] * x + matrix[4] * y + matrix[8] * z + matrix[12];
|
||||||
|
positions[offset + 1] = matrix[1] * x + matrix[5] * y + matrix[9] * z + matrix[13];
|
||||||
|
positions[offset + 2] = matrix[2] * x + matrix[6] * y + matrix[10] * z + matrix[14];
|
||||||
|
}
|
||||||
|
const geometry = new BufferGeometry();
|
||||||
|
geometry.setAttribute("position", new BufferAttribute(positions, 3));
|
||||||
|
const indices = primitive.getIndices()?.getArray();
|
||||||
|
if (indices) geometry.setIndex(new BufferAttribute(indices, 1));
|
||||||
|
trees.push(new MeshBVH(geometry));
|
||||||
|
}
|
||||||
|
}
|
||||||
|
return trees;
|
||||||
|
}
|
||||||
|
|
||||||
|
function nearestDistance(trees, position) {
|
||||||
|
point.fromArray(position);
|
||||||
|
let distance = Infinity;
|
||||||
|
for (const tree of trees) {
|
||||||
|
const result = tree.closestPointToPoint(point);
|
||||||
|
distance = Math.min(distance, result?.distance ?? Infinity);
|
||||||
|
}
|
||||||
|
return distance;
|
||||||
|
}
|
||||||
|
|
||||||
|
function percentile(sorted, proportion) {
|
||||||
|
return sorted[Math.min(sorted.length - 1, Math.floor(sorted.length * proportion))] ?? 0;
|
||||||
|
}
|
||||||
|
|
||||||
|
const results = [];
|
||||||
|
for (const definition of definitions) {
|
||||||
|
const geometryUrls = useCollision
|
||||||
|
? definition.assets.collision.map((chunk) => chunk.url)
|
||||||
|
: [definition.assets.navigation.url];
|
||||||
|
const trees = (await Promise.all(geometryUrls.map((url) => (
|
||||||
|
geometryTrees(path.join(projectRoot, "public", url.replace(/^\//, "")))
|
||||||
|
)))).flat();
|
||||||
|
const samples = [
|
||||||
|
{ id: "entrance", position: definition.entrance.footPosition },
|
||||||
|
...definition.staticSpawns.map((spawn) => ({ id: spawn.id, position: spawn.position })),
|
||||||
|
...definition.roamingPacks.flatMap((pack) => (
|
||||||
|
pack.waypoints.map((position, index) => ({ id: `${pack.id}:${index}`, position }))
|
||||||
|
)),
|
||||||
|
];
|
||||||
|
const distances = samples
|
||||||
|
.map((sample) => ({ ...sample, distance: nearestDistance(trees, sample.position) }))
|
||||||
|
.sort((left, right) => left.distance - right.distance);
|
||||||
|
const values = distances.map((sample) => sample.distance);
|
||||||
|
results.push({
|
||||||
|
dungeonId: definition.id,
|
||||||
|
geometry: useCollision ? "collision" : "navigation",
|
||||||
|
coordinateAlignment: definition.provenance.coordinateAlignment,
|
||||||
|
coordinateTransform: definition.provenance.coordinateTransform,
|
||||||
|
samples: values.length,
|
||||||
|
withinOneMeter: values.filter((distance) => distance <= 1).length,
|
||||||
|
medianDistance: percentile(values, 0.5),
|
||||||
|
p90Distance: percentile(values, 0.9),
|
||||||
|
maximumDistance: values.at(-1) ?? 0,
|
||||||
|
furthest: distances.slice(-5).reverse().map(({ id, position, distance }) => ({
|
||||||
|
id,
|
||||||
|
position,
|
||||||
|
distance,
|
||||||
|
})),
|
||||||
|
});
|
||||||
|
}
|
||||||
|
|
||||||
|
console.log(JSON.stringify(results, null, 2));
|
||||||
@@ -175,6 +175,10 @@ function interpolate(left, right, alpha) {
|
|||||||
return left.map((value, axis) => rounded(value + (right[axis] - value) * alpha));
|
return left.map((value, axis) => rounded(value + (right[axis] - value) * alpha));
|
||||||
}
|
}
|
||||||
|
|
||||||
|
function horizontalDistanceSquared(left, right) {
|
||||||
|
return (left[0] - right[0]) ** 2 + (left[2] - right[2]) ** 2;
|
||||||
|
}
|
||||||
|
|
||||||
function normalizedName(value) {
|
function normalizedName(value) {
|
||||||
return String(value ?? "").toLowerCase().replace(/[^a-z0-9]+/g, "");
|
return String(value ?? "").toLowerCase().replace(/[^a-z0-9]+/g, "");
|
||||||
}
|
}
|
||||||
@@ -201,7 +205,7 @@ const ADT_SOURCE_BASES = new Set([
|
|||||||
"z-y-negative-x",
|
"z-y-negative-x",
|
||||||
]);
|
]);
|
||||||
const GLOBAL_WMO_SOURCE_BASES = new Set(["draft", "flip-x"]);
|
const GLOBAL_WMO_SOURCE_BASES = new Set(["draft", "flip-x"]);
|
||||||
const GLOBAL_WMO_ALIGNMENTS = new Set(["player-anchor", "source"]);
|
const COORDINATE_ALIGNMENTS = new Set(["player-anchor", "source", "source-transform"]);
|
||||||
|
|
||||||
function sourceBasisFor(recipe, environmentMode) {
|
function sourceBasisFor(recipe, environmentMode) {
|
||||||
if (environmentMode !== "adt-hybrid") {
|
if (environmentMode !== "adt-hybrid") {
|
||||||
@@ -222,14 +226,42 @@ function sourceBasisFor(recipe, environmentMode) {
|
|||||||
}
|
}
|
||||||
|
|
||||||
function coordinateAlignmentFor(recipe, environmentMode) {
|
function coordinateAlignmentFor(recipe, environmentMode) {
|
||||||
if (environmentMode !== "global-wmo") return "player-anchor";
|
const legacyGlobalWmoAlignment = environmentMode === "global-wmo"
|
||||||
const alignment = recipe.globalWmoAlignment ?? "player-anchor";
|
? recipe.globalWmoAlignment
|
||||||
if (!GLOBAL_WMO_ALIGNMENTS.has(alignment)) {
|
: undefined;
|
||||||
throw new Error(`${recipe.slug}: unsupported global WMO alignment ${alignment}.`);
|
if (
|
||||||
|
recipe.coordinateAlignment
|
||||||
|
&& legacyGlobalWmoAlignment
|
||||||
|
&& recipe.coordinateAlignment !== legacyGlobalWmoAlignment
|
||||||
|
) {
|
||||||
|
throw new Error(`${recipe.slug}: coordinate alignment overrides disagree.`);
|
||||||
|
}
|
||||||
|
const alignment = recipe.coordinateAlignment ?? legacyGlobalWmoAlignment ?? "player-anchor";
|
||||||
|
if (!COORDINATE_ALIGNMENTS.has(alignment)) {
|
||||||
|
throw new Error(`${recipe.slug}: unsupported coordinate alignment ${alignment}.`);
|
||||||
}
|
}
|
||||||
return alignment;
|
return alignment;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
function sourceTransformFor(recipe, alignment) {
|
||||||
|
const transform = recipe.coordinateTransform;
|
||||||
|
if (alignment !== "source-transform") {
|
||||||
|
if (transform) {
|
||||||
|
throw new Error(`${recipe.slug}: coordinateTransform requires source-transform alignment.`);
|
||||||
|
}
|
||||||
|
return undefined;
|
||||||
|
}
|
||||||
|
if (!transform || !Array.isArray(transform.origin) || transform.origin.length !== 3) {
|
||||||
|
throw new Error(`${recipe.slug}: source-transform alignment requires a three-value coordinateTransform.origin.`);
|
||||||
|
}
|
||||||
|
const origin = transform.origin.map(Number);
|
||||||
|
const yawDegrees = Number(transform.yawDegrees ?? 0);
|
||||||
|
if (!origin.every(Number.isFinite) || !Number.isFinite(yawDegrees)) {
|
||||||
|
throw new Error(`${recipe.slug}: coordinateTransform values must be finite numbers.`);
|
||||||
|
}
|
||||||
|
return { origin, yawDegrees };
|
||||||
|
}
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* Runtime drafts retain the original pipeline convention. wow.export ADT
|
* Runtime drafts retain the original pipeline convention. wow.export ADT
|
||||||
* packages have an additional horizontal basis change baked into their GLBs.
|
* packages have an additional horizontal basis change baked into their GLBs.
|
||||||
@@ -248,10 +280,39 @@ function sourceToPackageBasis(position, environmentMode, sourceBasis) {
|
|||||||
: point;
|
: point;
|
||||||
}
|
}
|
||||||
|
|
||||||
function coordinateMapper(draft, environmentMode, playerAnchor, sourceBasis, alignment) {
|
function rotateAroundY(position, yawDegrees) {
|
||||||
|
const radians = yawDegrees * Math.PI / 180;
|
||||||
|
const cosine = Math.cos(radians);
|
||||||
|
const sine = Math.sin(radians);
|
||||||
|
return vector([
|
||||||
|
position[0] * cosine - position[2] * sine,
|
||||||
|
position[1],
|
||||||
|
position[0] * sine + position[2] * cosine,
|
||||||
|
]);
|
||||||
|
}
|
||||||
|
|
||||||
|
function coordinateMapper(
|
||||||
|
draft,
|
||||||
|
environmentMode,
|
||||||
|
playerAnchor,
|
||||||
|
sourceBasis,
|
||||||
|
alignment,
|
||||||
|
sourceTransform,
|
||||||
|
) {
|
||||||
if (alignment === "source") {
|
if (alignment === "source") {
|
||||||
return (position) => sourceToPackageBasis(position, environmentMode, sourceBasis);
|
return (position) => sourceToPackageBasis(position, environmentMode, sourceBasis);
|
||||||
}
|
}
|
||||||
|
if (alignment === "source-transform") {
|
||||||
|
const basisOrigin = sourceToPackageBasis(
|
||||||
|
sourceTransform.origin,
|
||||||
|
environmentMode,
|
||||||
|
sourceBasis,
|
||||||
|
);
|
||||||
|
return (position) => rotateAroundY(
|
||||||
|
subtract(sourceToPackageBasis(position, environmentMode, sourceBasis), basisOrigin),
|
||||||
|
sourceTransform.yawDegrees,
|
||||||
|
);
|
||||||
|
}
|
||||||
const sourceEntrance = draft?.entrance
|
const sourceEntrance = draft?.entrance
|
||||||
?? draft?.spawns?.[0]?.position
|
?? draft?.spawns?.[0]?.position
|
||||||
?? playerAnchor.position;
|
?? playerAnchor.position;
|
||||||
@@ -778,6 +839,7 @@ async function compile() {
|
|||||||
const environmentMode = inferEnvironmentMode(recipe, assetPackage);
|
const environmentMode = inferEnvironmentMode(recipe, assetPackage);
|
||||||
const sourceBasis = sourceBasisFor(recipe, environmentMode);
|
const sourceBasis = sourceBasisFor(recipe, environmentMode);
|
||||||
const coordinateAlignment = coordinateAlignmentFor(recipe, environmentMode);
|
const coordinateAlignment = coordinateAlignmentFor(recipe, environmentMode);
|
||||||
|
const sourceTransform = sourceTransformFor(recipe, coordinateAlignment);
|
||||||
const playerAnchor = assetPackage.anchors.find((anchor) => anchor.kind === "player");
|
const playerAnchor = assetPackage.anchors.find((anchor) => anchor.kind === "player");
|
||||||
const trashAnchor = assetPackage.anchors.find((anchor) => anchor.kind === "trash");
|
const trashAnchor = assetPackage.anchors.find((anchor) => anchor.kind === "trash");
|
||||||
const bossAnchor = assetPackage.anchors.find((anchor) => anchor.kind === "boss");
|
const bossAnchor = assetPackage.anchors.find((anchor) => anchor.kind === "boss");
|
||||||
@@ -788,7 +850,7 @@ async function compile() {
|
|||||||
trashAnchor.position[0] - playerAnchor.position[0],
|
trashAnchor.position[0] - playerAnchor.position[0],
|
||||||
trashAnchor.position[2] - playerAnchor.position[2],
|
trashAnchor.position[2] - playerAnchor.position[2],
|
||||||
);
|
);
|
||||||
const entranceYaw = Number.isFinite(playerAnchor.yaw)
|
let entranceYaw = Number.isFinite(playerAnchor.yaw)
|
||||||
? playerAnchor.yaw
|
? playerAnchor.yaw
|
||||||
: inferredEntranceYaw;
|
: inferredEntranceYaw;
|
||||||
|
|
||||||
@@ -809,6 +871,7 @@ async function compile() {
|
|||||||
playerAnchor,
|
playerAnchor,
|
||||||
sourceBasis,
|
sourceBasis,
|
||||||
coordinateAlignment,
|
coordinateAlignment,
|
||||||
|
sourceTransform,
|
||||||
);
|
);
|
||||||
const entrancePosition = mapPosition(
|
const entrancePosition = mapPosition(
|
||||||
draft?.entrance
|
draft?.entrance
|
||||||
@@ -896,16 +959,49 @@ async function compile() {
|
|||||||
const entry = Number(spawn.entityId.replace("entry-", ""));
|
const entry = Number(spawn.entityId.replace("entry-", ""));
|
||||||
if (!bossCandidatesByEntry.has(entry)) bossCandidatesByEntry.set(entry, spawn);
|
if (!bossCandidatesByEntry.has(entry)) bossCandidatesByEntry.set(entry, spawn);
|
||||||
}
|
}
|
||||||
|
const mappedNativeRoutePositions = coordinateAlignment !== "player-anchor"
|
||||||
|
? (draft?.spawns ?? [])
|
||||||
|
.map((spawn) => mapPosition(spawn.position))
|
||||||
|
.filter((position) => position.every(Number.isFinite))
|
||||||
|
.sort((left, right) => (
|
||||||
|
horizontalDistanceSquared(left, entrancePosition)
|
||||||
|
- horizontalDistanceSquared(right, entrancePosition)
|
||||||
|
))
|
||||||
|
: [];
|
||||||
|
const entranceDirectionTarget = mappedNativeRoutePositions.find((position) => (
|
||||||
|
Math.abs(position[1] - entrancePosition[1]) <= 3
|
||||||
|
&& horizontalDistanceSquared(position, entrancePosition) >= 16
|
||||||
|
));
|
||||||
|
if (entranceDirectionTarget) {
|
||||||
|
entranceYaw = Math.atan2(
|
||||||
|
entranceDirectionTarget[0] - entrancePosition[0],
|
||||||
|
entranceDirectionTarget[2] - entrancePosition[2],
|
||||||
|
);
|
||||||
|
}
|
||||||
|
const entranceLevelRoutePositions = mappedNativeRoutePositions.filter(
|
||||||
|
(position) => Math.abs(position[1] - entrancePosition[1]) <= 2.5,
|
||||||
|
);
|
||||||
|
const nativeRoutePositions = (
|
||||||
|
bossCandidatesByEntry.size === 0
|
||||||
|
&& entranceLevelRoutePositions.length >= encounters.length
|
||||||
|
)
|
||||||
|
? entranceLevelRoutePositions
|
||||||
|
: mappedNativeRoutePositions;
|
||||||
const usedBossSpawnIds = new Set();
|
const usedBossSpawnIds = new Set();
|
||||||
const bossObjectives = encounters.map((encounter, index) => {
|
const bossObjectives = encounters.map((encounter, index) => {
|
||||||
const candidate = bossCandidatesByEntry.get(Number(encounter.creatureId));
|
const candidate = bossCandidatesByEntry.get(Number(encounter.creatureId));
|
||||||
const alpha = encounters.length <= 1 ? 1 : index / (encounters.length - 1);
|
const alpha = encounters.length <= 1 ? 1 : index / (encounters.length - 1);
|
||||||
const routePosition = interpolate(trashAnchor.position, bossAnchor.position, 0.18 + alpha * 0.82);
|
const routeAlpha = 0.18 + alpha * 0.82;
|
||||||
|
const nativeRoutePosition = nativeRoutePositions.length
|
||||||
|
? nativeRoutePositions[Math.round(routeAlpha * (nativeRoutePositions.length - 1))]
|
||||||
|
: undefined;
|
||||||
|
const routePosition = nativeRoutePosition
|
||||||
|
?? interpolate(trashAnchor.position, bossAnchor.position, routeAlpha);
|
||||||
const position = encounter.eventOnly
|
const position = encounter.eventOnly
|
||||||
? [-151.27, -102.82, 252.26]
|
? [-151.27, -102.82, 252.26]
|
||||||
: candidate
|
: candidate
|
||||||
? mapPosition(candidate.position)
|
? mapPosition(candidate.position)
|
||||||
: offsetPosition(routePosition, index % 5, 2.4);
|
: nativeRoutePosition ?? offsetPosition(routePosition, index % 5, 2.4);
|
||||||
const id = encounter.eventOnly
|
const id = encounter.eventOnly
|
||||||
? "mutanus-spawn"
|
? "mutanus-spawn"
|
||||||
: candidate?.id ?? `encounter-${encounter.encounterId}`;
|
: candidate?.id ?? `encounter-${encounter.encounterId}`;
|
||||||
@@ -1000,8 +1096,9 @@ async function compile() {
|
|||||||
|
|
||||||
const allPoints = [
|
const allPoints = [
|
||||||
entrancePosition,
|
entrancePosition,
|
||||||
trashAnchor.position,
|
...(coordinateAlignment === "player-anchor" || syntheticTrash
|
||||||
bossAnchor.position,
|
? [trashAnchor.position, bossAnchor.position]
|
||||||
|
: []),
|
||||||
...staticSpawns.map((spawn) => spawn.position),
|
...staticSpawns.map((spawn) => spawn.position),
|
||||||
...roamingPacks.flatMap((pack) => pack.waypoints),
|
...roamingPacks.flatMap((pack) => pack.waypoints),
|
||||||
];
|
];
|
||||||
@@ -1032,6 +1129,8 @@ async function compile() {
|
|||||||
? ["Trash identities and placements are procedural; boss identities come from the installed Ascension encounter catalog."]
|
? ["Trash identities and placements are procedural; boss identities come from the installed Ascension encounter catalog."]
|
||||||
: coordinateAlignment === "source"
|
: coordinateAlignment === "source"
|
||||||
? [`Server positions use the packaged ${environmentMode} environment's native world coordinates.`]
|
? [`Server positions use the packaged ${environmentMode} environment's native world coordinates.`]
|
||||||
|
: coordinateAlignment === "source-transform"
|
||||||
|
? [`Server positions use the packaged ${environmentMode} environment's authoritative WDT placement transform.`]
|
||||||
: [`Server positions were aligned to the packaged ${environmentMode} environment at its reviewed player anchor.`]),
|
: [`Server positions were aligned to the packaged ${environmentMode} environment at its reviewed player anchor.`]),
|
||||||
];
|
];
|
||||||
|
|
||||||
@@ -1078,6 +1177,7 @@ async function compile() {
|
|||||||
serverGameObjects: draft?.gameObjects ?? [],
|
serverGameObjects: draft?.gameObjects ?? [],
|
||||||
provenance: {
|
provenance: {
|
||||||
clientBuild: recipe.clientBuild,
|
clientBuild: recipe.clientBuild,
|
||||||
|
coordinateAlignment,
|
||||||
clientArchiveHashes: assetPackage.source.checksums ?? {},
|
clientArchiveHashes: assetPackage.source.checksums ?? {},
|
||||||
databaseAdapter: databaseKind,
|
databaseAdapter: databaseKind,
|
||||||
databaseRevision: draft?.spawns?.length
|
databaseRevision: draft?.spawns?.length
|
||||||
@@ -1090,6 +1190,8 @@ async function compile() {
|
|||||||
? sourceBasis === "z-y-negative-x"
|
? sourceBasis === "z-y-negative-x"
|
||||||
? "three(x,y,z)=(wow.y,wow.z,wow.x)"
|
? "three(x,y,z)=(wow.y,wow.z,wow.x)"
|
||||||
: "three(x,y,z)=(-wow.y,wow.z,wow.x)"
|
: "three(x,y,z)=(-wow.y,wow.z,wow.x)"
|
||||||
|
: coordinateAlignment === "source-transform"
|
||||||
|
? `three(x,y,z)=rotate-y(${sourceBasis === "flip-x" ? "(-draft.x,draft.y,draft.z)" : "draft(x,y,z)"}-[${sourceTransform.origin.join(",")}],${sourceTransform.yawDegrees}deg)`
|
||||||
: coordinateAlignment === "source"
|
: coordinateAlignment === "source"
|
||||||
? sourceBasis === "flip-x"
|
? sourceBasis === "flip-x"
|
||||||
? "three(x,y,z)=(-draft.x,draft.y,draft.z)"
|
? "three(x,y,z)=(-draft.x,draft.y,draft.z)"
|
||||||
|
|||||||
@@ -0,0 +1,106 @@
|
|||||||
|
import {
|
||||||
|
mkdirSync,
|
||||||
|
readFileSync,
|
||||||
|
readdirSync,
|
||||||
|
unlinkSync,
|
||||||
|
writeFileSync,
|
||||||
|
} from "node:fs";
|
||||||
|
import { basename, resolve } from "node:path";
|
||||||
|
import {
|
||||||
|
CLASSES,
|
||||||
|
COA_CLASS_IDS,
|
||||||
|
type ClassId,
|
||||||
|
type CoaClassId,
|
||||||
|
} from "../src/app/characterCatalog";
|
||||||
|
import { TALENT_NODES, TALENT_TREES } from "../src/game/talentCatalog";
|
||||||
|
|
||||||
|
const projectRoot = resolve(import.meta.dirname, "..");
|
||||||
|
const talentOutputDirectory = resolve(projectRoot, "src/game/generated/talentUi");
|
||||||
|
const coaAbilityOutputDirectory = resolve(projectRoot, "src/game/generated/coaAbilities");
|
||||||
|
|
||||||
|
function writeJson(file: string, value: unknown): void {
|
||||||
|
writeFileSync(file, `${JSON.stringify(value)}\n`, "utf8");
|
||||||
|
console.log(`Wrote ${file}`);
|
||||||
|
}
|
||||||
|
|
||||||
|
function removeStaleJson(directory: string, expectedNames: ReadonlySet<string>): void {
|
||||||
|
for (const name of readdirSync(directory)) {
|
||||||
|
if (!name.endsWith(".json") || expectedNames.has(name)) continue;
|
||||||
|
unlinkSync(resolve(directory, name));
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
mkdirSync(talentOutputDirectory, { recursive: true });
|
||||||
|
const talentClassIds = new Set<ClassId>(TALENT_TREES.map((tree) => tree.classId));
|
||||||
|
const talentFileNames = new Set([...talentClassIds].map((classId) => `${classId}.json`));
|
||||||
|
removeStaleJson(talentOutputDirectory, talentFileNames);
|
||||||
|
|
||||||
|
for (const classId of talentClassIds) {
|
||||||
|
const trees = TALENT_TREES.filter((tree) => tree.classId === classId).map((tree) => ({
|
||||||
|
id: tree.id,
|
||||||
|
classId: tree.classId,
|
||||||
|
name: tree.name,
|
||||||
|
icon: tree.icon,
|
||||||
|
background: tree.background,
|
||||||
|
description: tree.description,
|
||||||
|
system: tree.system,
|
||||||
|
}));
|
||||||
|
const treeIds = new Set(trees.map((tree) => tree.id));
|
||||||
|
const nodes = TALENT_NODES.filter((node) => treeIds.has(node.treeId)).map((node) => ({
|
||||||
|
id: node.id,
|
||||||
|
treeId: node.treeId,
|
||||||
|
index: node.index,
|
||||||
|
prerequisiteId: node.prerequisiteId,
|
||||||
|
prerequisiteRank: node.prerequisiteRank,
|
||||||
|
prerequisites: node.prerequisites,
|
||||||
|
column: node.column,
|
||||||
|
row: node.row,
|
||||||
|
icon: node.icon,
|
||||||
|
maxRank: node.maxRank,
|
||||||
|
name: node.name,
|
||||||
|
description: node.description,
|
||||||
|
rankDescriptions: node.rankDescriptions,
|
||||||
|
system: node.system,
|
||||||
|
entryType: node.entryType,
|
||||||
|
abilityEssenceCost: node.abilityEssenceCost,
|
||||||
|
talentEssenceCost: node.talentEssenceCost,
|
||||||
|
requiredClassPoints: node.requiredClassPoints,
|
||||||
|
requiredSpecPoints: node.requiredSpecPoints,
|
||||||
|
requiredLevel: node.requiredLevel,
|
||||||
|
isPassive: node.isPassive,
|
||||||
|
}));
|
||||||
|
writeJson(resolve(talentOutputDirectory, `${classId}.json`), {
|
||||||
|
schemaVersion: 1,
|
||||||
|
classId,
|
||||||
|
trees,
|
||||||
|
nodes,
|
||||||
|
});
|
||||||
|
}
|
||||||
|
|
||||||
|
interface CoaAbilitySnapshot {
|
||||||
|
readonly schemaVersion: 1;
|
||||||
|
readonly abilitiesByClass: Readonly<Record<CoaClassId, readonly unknown[]>>;
|
||||||
|
readonly resourcesByClass: Readonly<Record<CoaClassId, unknown>>;
|
||||||
|
readonly triggeredAbilitiesBySpellId: Readonly<Record<string, { readonly classId?: ClassId }>>;
|
||||||
|
}
|
||||||
|
|
||||||
|
const coaAbilityInput = resolve(projectRoot, "src/game/generated/coaAbilityRuntimeCatalog.json");
|
||||||
|
const coaSnapshot = JSON.parse(readFileSync(coaAbilityInput, "utf8")) as CoaAbilitySnapshot;
|
||||||
|
mkdirSync(coaAbilityOutputDirectory, { recursive: true });
|
||||||
|
const coaAbilityFileNames = new Set(COA_CLASS_IDS.map((classId) => `${classId}.json`));
|
||||||
|
removeStaleJson(coaAbilityOutputDirectory, coaAbilityFileNames);
|
||||||
|
|
||||||
|
for (const classId of COA_CLASS_IDS) {
|
||||||
|
const triggeredAbilitiesBySpellId = Object.fromEntries(Object.entries(
|
||||||
|
coaSnapshot.triggeredAbilitiesBySpellId,
|
||||||
|
).filter(([, ability]) => ability.classId === classId));
|
||||||
|
writeJson(resolve(coaAbilityOutputDirectory, `${classId}.json`), {
|
||||||
|
schemaVersion: 1,
|
||||||
|
classId,
|
||||||
|
abilities: coaSnapshot.abilitiesByClass[classId] ?? [],
|
||||||
|
resource: coaSnapshot.resourcesByClass[classId],
|
||||||
|
triggeredAbilitiesBySpellId,
|
||||||
|
});
|
||||||
|
}
|
||||||
|
|
||||||
|
console.log(`Generated ${talentFileNames.size} talent UI shards and ${coaAbilityFileNames.size} Conquest ability shards from ${basename(coaAbilityInput)}.`);
|
||||||
@@ -404,15 +404,23 @@ async function optimizeStage(slug) {
|
|||||||
const source = path.join(staging, entry.path);
|
const source = path.join(staging, entry.path);
|
||||||
const outputName = optimizedChunkName(role, index, entries.length);
|
const outputName = optimizedChunkName(role, index, entries.length);
|
||||||
const output = path.join(staging, outputName);
|
const output = path.join(staging, outputName);
|
||||||
await run(process.execPath, [
|
|
||||||
cli,
|
|
||||||
"meshopt",
|
|
||||||
source,
|
|
||||||
output,
|
|
||||||
"--level",
|
|
||||||
"high",
|
|
||||||
]);
|
|
||||||
if (role === "visual") {
|
if (role === "visual") {
|
||||||
|
await run(process.execPath, [
|
||||||
|
cli,
|
||||||
|
"optimize",
|
||||||
|
source,
|
||||||
|
output,
|
||||||
|
"--compress",
|
||||||
|
"meshopt",
|
||||||
|
"--meshopt-level",
|
||||||
|
"high",
|
||||||
|
"--instance",
|
||||||
|
"true",
|
||||||
|
"--instance-min",
|
||||||
|
"3",
|
||||||
|
"--texture-size",
|
||||||
|
"2048",
|
||||||
|
]);
|
||||||
await run(process.execPath, [
|
await run(process.execPath, [
|
||||||
path.join(projectRoot, "scripts/asset-pipeline/compress-ktx2.mjs"),
|
path.join(projectRoot, "scripts/asset-pipeline/compress-ktx2.mjs"),
|
||||||
output,
|
output,
|
||||||
@@ -421,6 +429,15 @@ async function optimizeStage(slug) {
|
|||||||
"--jobs",
|
"--jobs",
|
||||||
"2",
|
"2",
|
||||||
]);
|
]);
|
||||||
|
} else {
|
||||||
|
await run(process.execPath, [
|
||||||
|
cli,
|
||||||
|
"meshopt",
|
||||||
|
source,
|
||||||
|
output,
|
||||||
|
"--level",
|
||||||
|
"high",
|
||||||
|
]);
|
||||||
}
|
}
|
||||||
const geometry = await inspectGlb(output);
|
const geometry = await inspectGlb(output);
|
||||||
const descriptor = {
|
const descriptor = {
|
||||||
|
|||||||
+38
-2
@@ -1,5 +1,7 @@
|
|||||||
import { lazy, Suspense, useEffect } from "react";
|
import { lazy, Suspense, useEffect, useMemo, useState, type ReactNode } from "react";
|
||||||
|
import { BASE_CLASS_IDS, classById } from "./app/characterCatalog";
|
||||||
import { useShellStore } from "./app/shellStore";
|
import { useShellStore } from "./app/shellStore";
|
||||||
|
import { preloadAbilityCatalogsForClasses } from "./game/abilityCatalog";
|
||||||
import { useManastormAdminStore } from "./game/manastormAdminStore";
|
import { useManastormAdminStore } from "./game/manastormAdminStore";
|
||||||
import { useAuthoritativeDualScreenSync, useForcedThorDisplays } from "./platform/useThorDualScreen";
|
import { useAuthoritativeDualScreenSync, useForcedThorDisplays } from "./platform/useThorDualScreen";
|
||||||
import { LoginScreen } from "./ui/LoginScreen";
|
import { LoginScreen } from "./ui/LoginScreen";
|
||||||
@@ -18,6 +20,39 @@ function ShellTransition() {
|
|||||||
return <div className="world-transition"><span>HM</span><strong>Opening expedition</strong><small>Preparing the next screen...</small></div>;
|
return <div className="world-transition"><span>HM</span><strong>Opening expedition</strong><small>Preparing the next screen...</small></div>;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
function RuntimeCatalogGate({ children }: { readonly children: ReactNode }) {
|
||||||
|
const character = useShellStore((state) => (
|
||||||
|
state.characters.find((candidate) => candidate.id === state.selectedCharacterId) ?? null
|
||||||
|
));
|
||||||
|
const classIds = useMemo(() => {
|
||||||
|
if (!character) return [];
|
||||||
|
const definition = classById(character.classId);
|
||||||
|
if (definition.mode === "conquest") return [...BASE_CLASS_IDS, character.classId];
|
||||||
|
return [character.classId, ...(character.secondaryClassId ? [character.secondaryClassId] : [])];
|
||||||
|
}, [character]);
|
||||||
|
const requestKey = classIds.join(":");
|
||||||
|
const [loadedKey, setLoadedKey] = useState("");
|
||||||
|
const [failedKey, setFailedKey] = useState("");
|
||||||
|
|
||||||
|
useEffect(() => {
|
||||||
|
if (!requestKey) return;
|
||||||
|
let active = true;
|
||||||
|
setFailedKey("");
|
||||||
|
void preloadAbilityCatalogsForClasses(classIds).then(() => {
|
||||||
|
if (active) setLoadedKey(requestKey);
|
||||||
|
}).catch(() => {
|
||||||
|
if (active) setFailedKey(requestKey);
|
||||||
|
});
|
||||||
|
return () => { active = false; };
|
||||||
|
}, [classIds, requestKey]);
|
||||||
|
|
||||||
|
if (!requestKey || loadedKey === requestKey) return children;
|
||||||
|
if (failedKey === requestKey) {
|
||||||
|
return <div className="world-transition"><span>!</span><strong>Catalog unavailable</strong><small>Reload the app to retry loading combat data.</small></div>;
|
||||||
|
}
|
||||||
|
return <ShellTransition />;
|
||||||
|
}
|
||||||
|
|
||||||
export default function App() {
|
export default function App() {
|
||||||
useForcedThorDisplays();
|
useForcedThorDisplays();
|
||||||
useAuthoritativeDualScreenSync();
|
useAuthoritativeDualScreenSync();
|
||||||
@@ -39,5 +74,6 @@ export default function App() {
|
|||||||
else if (phase === "manastorms") screen = <Suspense fallback={<ShellTransition />}><ManastormSelectScreen /></Suspense>;
|
else if (phase === "manastorms") screen = <Suspense fallback={<ShellTransition />}><ManastormSelectScreen /></Suspense>;
|
||||||
else if (phase === "gm-admin") screen = <Suspense fallback={<ShellTransition />}><GmAdminScreen /></Suspense>;
|
else if (phase === "gm-admin") screen = <Suspense fallback={<ShellTransition />}><GmAdminScreen /></Suspense>;
|
||||||
else if (phase === "game") screen = <Suspense fallback={<ShellTransition />}><GameRuntime /></Suspense>;
|
else if (phase === "game") screen = <Suspense fallback={<ShellTransition />}><GameRuntime /></Suspense>;
|
||||||
return <><OnlineGroupCoordinator />{screen}</>;
|
const requiresRuntimeCatalog = !["login", "characters", "create-character"].includes(phase);
|
||||||
|
return <><OnlineGroupCoordinator />{requiresRuntimeCatalog ? <RuntimeCatalogGate>{screen}</RuntimeCatalogGate> : screen}</>;
|
||||||
}
|
}
|
||||||
|
|||||||
+141
-41
@@ -1,13 +1,18 @@
|
|||||||
import type { ClassId } from "../app/characterCatalog";
|
|
||||||
import {
|
import {
|
||||||
COA_RUNTIME_ABILITIES_BY_CLASS as COA_ABILITIES_BY_CLASS,
|
BASE_CLASS_IDS,
|
||||||
COA_RUNTIME_CLASS_RESOURCES as COA_CLASS_RESOURCES,
|
COA_CLASS_IDS,
|
||||||
} from "./coaAbilityRuntimeCatalog";
|
ROM_CLASS_IDS,
|
||||||
import { WOW335_ABILITIES_BY_CLASS } from "./wow335AbilityCatalog";
|
type BaseClassId,
|
||||||
import { ROM_ALL_ABILITIES, ROM_ELITE_ABILITIES, ROM_ORDINARY_ABILITIES_BY_CLASS, type RomSkillGroup } from "./romAbilityCatalog";
|
type ClassId,
|
||||||
|
type CoaClassId,
|
||||||
|
type RomClassId,
|
||||||
|
} from "../app/characterCatalog";
|
||||||
|
import { loadCoaAbilityCatalogShard } from "./coaAbilityCatalogLoader";
|
||||||
import { registerAbilityAnimationLookup } from "./abilityAnimationLookup";
|
import { registerAbilityAnimationLookup } from "./abilityAnimationLookup";
|
||||||
import type { AuraDefinition, DamageSchool, DispelCategory } from "./combatAuras";
|
import type { AuraDefinition, DamageSchool, DispelCategory } from "./combatAuras";
|
||||||
|
|
||||||
|
export type RomSkillGroup = "primary" | "general" | "elite" | "passive";
|
||||||
|
|
||||||
export type ResourceType =
|
export type ResourceType =
|
||||||
| "mana"
|
| "mana"
|
||||||
| "rage"
|
| "rage"
|
||||||
@@ -301,7 +306,7 @@ function defineAbility(definition: AbilityDefinitionInput): AbilityDefinition {
|
|||||||
};
|
};
|
||||||
}
|
}
|
||||||
|
|
||||||
export const CLASS_RESOURCES: Readonly<Record<ClassId, ClassResourceProfile>> = {
|
const classResources: Partial<Record<ClassId, ClassResourceProfile>> = {
|
||||||
// The prototype has no auto-attack swing timer yet, so a small passive Rage
|
// The prototype has no auto-attack swing timer yet, so a small passive Rage
|
||||||
// gain stands in for white-hit generation and keeps the full kit usable.
|
// gain stands in for white-hit generation and keeps the full kit usable.
|
||||||
warrior: { type: "rage", name: "Rage", color: "#b74439", maximum: 100, initial: 0, regenerationPerSecond: 5 },
|
warrior: { type: "rage", name: "Rage", color: "#b74439", maximum: 100, initial: 0, regenerationPerSecond: 5 },
|
||||||
@@ -314,7 +319,6 @@ export const CLASS_RESOURCES: Readonly<Record<ClassId, ClassResourceProfile>> =
|
|||||||
mage: { type: "mana", name: "Mana", color: "#397dcc", maximum: 100, initial: 100, regenerationPerSecond: 4 },
|
mage: { type: "mana", name: "Mana", color: "#397dcc", maximum: 100, initial: 100, regenerationPerSecond: 4 },
|
||||||
warlock: { type: "mana", name: "Mana", color: "#397dcc", maximum: 100, initial: 100, regenerationPerSecond: 4 },
|
warlock: { type: "mana", name: "Mana", color: "#397dcc", maximum: 100, initial: 100, regenerationPerSecond: 4 },
|
||||||
druid: { type: "mana", name: "Mana", color: "#397dcc", maximum: 100, initial: 100, regenerationPerSecond: 4 },
|
druid: { type: "mana", name: "Mana", color: "#397dcc", maximum: 100, initial: 100, regenerationPerSecond: 4 },
|
||||||
...COA_CLASS_RESOURCES,
|
|
||||||
"rom-warrior": { type: "rage", name: "Rage", color: "#b74439", maximum: 100, initial: 0, regenerationPerSecond: 0 },
|
"rom-warrior": { type: "rage", name: "Rage", color: "#b74439", maximum: 100, initial: 0, regenerationPerSecond: 0 },
|
||||||
"rom-scout": { type: "focus", name: "Focus", color: "#55b995", maximum: 100, initial: 100, regenerationPerSecond: 6 },
|
"rom-scout": { type: "focus", name: "Focus", color: "#55b995", maximum: 100, initial: 100, regenerationPerSecond: 6 },
|
||||||
"rom-rogue": { type: "energy", name: "Energy", color: "#d8c43f", maximum: 100, initial: 100, regenerationPerSecond: 10 },
|
"rom-rogue": { type: "energy", name: "Energy", color: "#d8c43f", maximum: 100, initial: 100, regenerationPerSecond: 10 },
|
||||||
@@ -327,6 +331,9 @@ export const CLASS_RESOURCES: Readonly<Record<ClassId, ClassResourceProfile>> =
|
|||||||
"rom-champion": { type: "rage", name: "Rage", color: "#bb604c", maximum: 100, initial: 0, regenerationPerSecond: 0 },
|
"rom-champion": { type: "rage", name: "Rage", color: "#bb604c", maximum: 100, initial: 0, regenerationPerSecond: 0 },
|
||||||
};
|
};
|
||||||
|
|
||||||
|
/** Populated per mode/class by preloadAbilityCatalogsForClasses before gameplay mounts. */
|
||||||
|
export const CLASS_RESOURCES = classResources as Readonly<Record<ClassId, ClassResourceProfile>>;
|
||||||
|
|
||||||
const WARRIOR = [
|
const WARRIOR = [
|
||||||
defineAbility({
|
defineAbility({
|
||||||
id: "warrior-heroic-strike", classId: "warrior", dbcSpellId: 78, name: "Heroic Strike",
|
id: "warrior-heroic-strike", classId: "warrior", dbcSpellId: 78, name: "Heroic Strike",
|
||||||
@@ -789,48 +796,131 @@ function mergeWow335ClassicAbilities(
|
|||||||
];
|
];
|
||||||
}
|
}
|
||||||
|
|
||||||
export const ABILITIES_BY_CLASS: Readonly<Record<ClassId, readonly AbilityDefinition[]>> = {
|
const CLASSIC_CORE_ABILITIES: Readonly<Record<BaseClassId, readonly AbilityDefinition[]>> = {
|
||||||
warrior: mergeWow335ClassicAbilities(WARRIOR, WOW335_ABILITIES_BY_CLASS.warrior),
|
warrior: WARRIOR,
|
||||||
paladin: mergeWow335ClassicAbilities(PALADIN, WOW335_ABILITIES_BY_CLASS.paladin),
|
paladin: PALADIN,
|
||||||
hunter: mergeWow335ClassicAbilities(HUNTER, WOW335_ABILITIES_BY_CLASS.hunter),
|
hunter: HUNTER,
|
||||||
rogue: mergeWow335ClassicAbilities(ROGUE, WOW335_ABILITIES_BY_CLASS.rogue),
|
rogue: ROGUE,
|
||||||
priest: mergeWow335ClassicAbilities(PRIEST, WOW335_ABILITIES_BY_CLASS.priest),
|
priest: PRIEST,
|
||||||
"death-knight": mergeWow335ClassicAbilities(DEATH_KNIGHT, WOW335_ABILITIES_BY_CLASS["death-knight"]),
|
"death-knight": DEATH_KNIGHT,
|
||||||
shaman: mergeWow335ClassicAbilities(SHAMAN, WOW335_ABILITIES_BY_CLASS.shaman),
|
shaman: SHAMAN,
|
||||||
mage: mergeWow335ClassicAbilities(MAGE, WOW335_ABILITIES_BY_CLASS.mage),
|
mage: MAGE,
|
||||||
warlock: mergeWow335ClassicAbilities(WARLOCK, WOW335_ABILITIES_BY_CLASS.warlock),
|
warlock: WARLOCK,
|
||||||
druid: mergeWow335ClassicAbilities(DRUID, WOW335_ABILITIES_BY_CLASS.druid),
|
druid: DRUID,
|
||||||
...COA_ABILITIES_BY_CLASS,
|
|
||||||
"rom-warrior": ROM_ORDINARY_ABILITIES_BY_CLASS["rom-warrior"],
|
|
||||||
"rom-scout": ROM_ORDINARY_ABILITIES_BY_CLASS["rom-scout"],
|
|
||||||
"rom-rogue": ROM_ORDINARY_ABILITIES_BY_CLASS["rom-rogue"],
|
|
||||||
"rom-mage": ROM_ORDINARY_ABILITIES_BY_CLASS["rom-mage"],
|
|
||||||
"rom-priest": ROM_ORDINARY_ABILITIES_BY_CLASS["rom-priest"],
|
|
||||||
"rom-knight": ROM_ORDINARY_ABILITIES_BY_CLASS["rom-knight"],
|
|
||||||
"rom-warden": ROM_ORDINARY_ABILITIES_BY_CLASS["rom-warden"],
|
|
||||||
"rom-druid": ROM_ORDINARY_ABILITIES_BY_CLASS["rom-druid"],
|
|
||||||
"rom-warlock": ROM_ORDINARY_ABILITIES_BY_CLASS["rom-warlock"],
|
|
||||||
"rom-champion": ROM_ORDINARY_ABILITIES_BY_CLASS["rom-champion"],
|
|
||||||
};
|
};
|
||||||
|
const abilitiesByClass: Partial<Record<ClassId, readonly AbilityDefinition[]>> = {
|
||||||
|
...CLASSIC_CORE_ABILITIES,
|
||||||
|
};
|
||||||
|
const abilityCatalog: Record<string, AbilityDefinition> = {};
|
||||||
|
let romEliteAbilities: readonly AbilityDefinition[] = [];
|
||||||
|
let romTriggeredAbilityResolver: ((spellId: number) => AbilityDefinition | null) | null = null;
|
||||||
|
const coaTriggeredAbilities = new Map<number, AbilityDefinition>();
|
||||||
|
let classicLoad: Promise<void> | null = null;
|
||||||
|
let romLoad: Promise<void> | null = null;
|
||||||
|
const coaLoads = new Map<CoaClassId, Promise<void>>();
|
||||||
|
|
||||||
export const ABILITY_CATALOG: Readonly<Record<string, AbilityDefinition>> = Object.freeze(
|
/** Live read-only views retained for catalog consumers and validation tests. */
|
||||||
Object.fromEntries([...Object.values(ABILITIES_BY_CLASS).flat(), ...ROM_ALL_ABILITIES].map((ability) => [ability.id, ability])),
|
export const ABILITIES_BY_CLASS = abilitiesByClass as Readonly<Record<ClassId, readonly AbilityDefinition[]>>;
|
||||||
);
|
export const ABILITY_CATALOG = abilityCatalog as Readonly<Record<string, AbilityDefinition>>;
|
||||||
|
|
||||||
|
function registerClassAbilities(classId: ClassId, abilities: readonly AbilityDefinition[]): void {
|
||||||
|
for (const previous of abilitiesByClass[classId] ?? []) delete abilityCatalog[previous.id];
|
||||||
|
abilitiesByClass[classId] = abilities;
|
||||||
|
for (const ability of abilities) abilityCatalog[ability.id] = ability;
|
||||||
|
}
|
||||||
|
|
||||||
|
for (const classId of BASE_CLASS_IDS) {
|
||||||
|
registerClassAbilities(classId, CLASSIC_CORE_ABILITIES[classId]);
|
||||||
|
}
|
||||||
|
|
||||||
|
async function preloadClassicAbilityCatalog(): Promise<void> {
|
||||||
|
if (classicLoad) return classicLoad;
|
||||||
|
classicLoad = import("./wow335AbilityCatalog").then(({ WOW335_ABILITIES_BY_CLASS }) => {
|
||||||
|
for (const classId of BASE_CLASS_IDS) {
|
||||||
|
registerClassAbilities(
|
||||||
|
classId,
|
||||||
|
mergeWow335ClassicAbilities(CLASSIC_CORE_ABILITIES[classId], WOW335_ABILITIES_BY_CLASS[classId]),
|
||||||
|
);
|
||||||
|
}
|
||||||
|
}).catch((error: unknown) => {
|
||||||
|
classicLoad = null;
|
||||||
|
throw error;
|
||||||
|
});
|
||||||
|
return classicLoad;
|
||||||
|
}
|
||||||
|
|
||||||
|
async function preloadRomAbilityCatalog(): Promise<void> {
|
||||||
|
if (romLoad) return romLoad;
|
||||||
|
romLoad = import("./romAbilityCatalog").then((catalog) => {
|
||||||
|
for (const classId of ROM_CLASS_IDS) {
|
||||||
|
registerClassAbilities(classId, catalog.ROM_ORDINARY_ABILITIES_BY_CLASS[classId]);
|
||||||
|
}
|
||||||
|
romEliteAbilities = catalog.ROM_ELITE_ABILITIES;
|
||||||
|
romTriggeredAbilityResolver = catalog.romTriggeredAbilityBySpellId;
|
||||||
|
for (const ability of catalog.ROM_ALL_ABILITIES) abilityCatalog[ability.id] = ability;
|
||||||
|
}).catch((error: unknown) => {
|
||||||
|
romLoad = null;
|
||||||
|
throw error;
|
||||||
|
});
|
||||||
|
return romLoad;
|
||||||
|
}
|
||||||
|
|
||||||
|
async function preloadCoaAbilityCatalog(classId: CoaClassId): Promise<void> {
|
||||||
|
const cached = coaLoads.get(classId);
|
||||||
|
if (cached) return cached;
|
||||||
|
const pending = loadCoaAbilityCatalogShard(classId).then((shard) => {
|
||||||
|
registerClassAbilities(classId, shard.abilities);
|
||||||
|
classResources[classId] = shard.resource;
|
||||||
|
for (const [spellId, ability] of Object.entries(shard.triggeredAbilitiesBySpellId)) {
|
||||||
|
coaTriggeredAbilities.set(Number(spellId), ability);
|
||||||
|
}
|
||||||
|
}).catch((error: unknown) => {
|
||||||
|
coaLoads.delete(classId);
|
||||||
|
throw error;
|
||||||
|
});
|
||||||
|
coaLoads.set(classId, pending);
|
||||||
|
return pending;
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Loads only the executable catalogs needed by the selected character and its
|
||||||
|
* party mode. Callers gate gameplay mounting on this promise so combat APIs can
|
||||||
|
* stay synchronous inside the simulation loop.
|
||||||
|
*/
|
||||||
|
export async function preloadAbilityCatalogsForClasses(classIds: readonly ClassId[]): Promise<void> {
|
||||||
|
const requested = new Set(classIds);
|
||||||
|
const loads: Promise<void>[] = [];
|
||||||
|
if (BASE_CLASS_IDS.some((classId) => requested.has(classId))) loads.push(preloadClassicAbilityCatalog());
|
||||||
|
if (ROM_CLASS_IDS.some((classId) => requested.has(classId))) loads.push(preloadRomAbilityCatalog());
|
||||||
|
for (const classId of COA_CLASS_IDS) {
|
||||||
|
if (requested.has(classId)) loads.push(preloadCoaAbilityCatalog(classId));
|
||||||
|
}
|
||||||
|
await Promise.all(loads);
|
||||||
|
}
|
||||||
|
|
||||||
|
export function triggeredAbilityBySpellId(
|
||||||
|
spellId: number,
|
||||||
|
includeCoa = true,
|
||||||
|
): AbilityDefinition | null {
|
||||||
|
return romTriggeredAbilityResolver?.(spellId)
|
||||||
|
?? (includeCoa ? coaTriggeredAbilities.get(spellId) : undefined)
|
||||||
|
?? null;
|
||||||
|
}
|
||||||
|
|
||||||
export function abilitiesForClass(classId: ClassId): readonly AbilityDefinition[] {
|
export function abilitiesForClass(classId: ClassId): readonly AbilityDefinition[] {
|
||||||
return ABILITIES_BY_CLASS[classId];
|
return abilitiesByClass[classId] ?? [];
|
||||||
}
|
}
|
||||||
|
|
||||||
export function abilitiesForCharacter(
|
export function abilitiesForCharacter(
|
||||||
classId: ClassId,
|
classId: ClassId,
|
||||||
secondaryClassId: ClassId | null | undefined,
|
secondaryClassId: ClassId | null | undefined,
|
||||||
): readonly AbilityDefinition[] {
|
): readonly AbilityDefinition[] {
|
||||||
if (!classId.startsWith("rom-") || !secondaryClassId?.startsWith("rom-")) return ABILITIES_BY_CLASS[classId];
|
if (!classId.startsWith("rom-") || !secondaryClassId?.startsWith("rom-")) return abilitiesForClass(classId);
|
||||||
const primary = ABILITIES_BY_CLASS[classId];
|
const primary = abilitiesForClass(classId);
|
||||||
const secondaryGeneral = ABILITIES_BY_CLASS[secondaryClassId].filter((ability) => (
|
const secondaryGeneral = abilitiesForClass(secondaryClassId).filter((ability) => (
|
||||||
ability.romSkillGroup === "general" || (ability.romSkillGroup === "passive" && ability.id.includes("-general-"))
|
ability.romSkillGroup === "general" || (ability.romSkillGroup === "passive" && ability.id.includes("-general-"))
|
||||||
));
|
));
|
||||||
const elites = ROM_ELITE_ABILITIES.filter((ability) => ability.classId === classId && ability.secondaryClassId === secondaryClassId);
|
const elites = romEliteAbilities.filter((ability) => ability.classId === classId && ability.secondaryClassId === secondaryClassId);
|
||||||
return [...primary, ...secondaryGeneral, ...elites];
|
return [...primary, ...secondaryGeneral, ...elites];
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -885,7 +975,7 @@ export function abilitiesUnlockedBetweenLevels(
|
|||||||
previousLevel: number,
|
previousLevel: number,
|
||||||
currentLevel: number,
|
currentLevel: number,
|
||||||
): readonly AbilityDefinition[] {
|
): readonly AbilityDefinition[] {
|
||||||
return ABILITIES_BY_CLASS[classId].filter((ability) => (
|
return abilitiesForClass(classId).filter((ability) => (
|
||||||
ability.unlockLevel > previousLevel && ability.unlockLevel <= currentLevel
|
ability.unlockLevel > previousLevel && ability.unlockLevel <= currentLevel
|
||||||
));
|
));
|
||||||
}
|
}
|
||||||
@@ -920,7 +1010,7 @@ export function defaultActionBarForClass(classId: ClassId): readonly string[] {
|
|||||||
|| effect.kind === "rune"
|
|| effect.kind === "rune"
|
||||||
|| effect.kind === "trigger-spell"
|
|| effect.kind === "trigger-spell"
|
||||||
)) ? 0 : 1;
|
)) ? 0 : 1;
|
||||||
const ordered = ABILITIES_BY_CLASS[classId]
|
const ordered = abilitiesForClass(classId)
|
||||||
.map((ability, index) => ({ ability, index }))
|
.map((ability, index) => ({ ability, index }))
|
||||||
.sort((left, right) => (
|
.sort((left, right) => (
|
||||||
left.ability.unlockLevel - right.ability.unlockLevel
|
left.ability.unlockLevel - right.ability.unlockLevel
|
||||||
@@ -962,5 +1052,15 @@ export function defaultActionBarForCharacter(classId: ClassId, secondaryClassId?
|
|||||||
}
|
}
|
||||||
|
|
||||||
export function resourceProfileForClass(classId: ClassId): ClassResourceProfile {
|
export function resourceProfileForClass(classId: ClassId): ClassResourceProfile {
|
||||||
return CLASS_RESOURCES[classId];
|
const profile = classResources[classId];
|
||||||
|
if (!profile) throw new Error(`Ability catalog for ${classId} was used before it was loaded.`);
|
||||||
|
return profile;
|
||||||
|
}
|
||||||
|
|
||||||
|
if (import.meta.env.MODE === "test") {
|
||||||
|
await preloadAbilityCatalogsForClasses([
|
||||||
|
...BASE_CLASS_IDS,
|
||||||
|
...COA_CLASS_IDS,
|
||||||
|
...ROM_CLASS_IDS,
|
||||||
|
]);
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -0,0 +1,18 @@
|
|||||||
|
import { describe, expect, it } from "vitest";
|
||||||
|
import { loadCoaAbilityCatalogShard } from "./coaAbilityCatalogLoader";
|
||||||
|
import {
|
||||||
|
COA_RUNTIME_ABILITIES_BY_CLASS,
|
||||||
|
COA_RUNTIME_CLASS_RESOURCES,
|
||||||
|
} from "./coaAbilityRuntimeCatalog";
|
||||||
|
|
||||||
|
describe("Conquest ability catalog shards", () => {
|
||||||
|
it.each(["barbarian", "runemaster"] as const)("loads only the %s executable catalog", async (classId) => {
|
||||||
|
const shard = await loadCoaAbilityCatalogShard(classId);
|
||||||
|
expect(shard.classId).toBe(classId);
|
||||||
|
expect(shard.abilities).toEqual(COA_RUNTIME_ABILITIES_BY_CLASS[classId]);
|
||||||
|
expect(shard.resource).toEqual(COA_RUNTIME_CLASS_RESOURCES[classId]);
|
||||||
|
expect(Object.values(shard.triggeredAbilitiesBySpellId).every((ability) => (
|
||||||
|
ability.classId === classId
|
||||||
|
))).toBe(true);
|
||||||
|
});
|
||||||
|
});
|
||||||
@@ -0,0 +1,36 @@
|
|||||||
|
import type { CoaClassId } from "../app/characterCatalog";
|
||||||
|
import type { AbilityDefinition, ClassResourceProfile } from "./abilityCatalog";
|
||||||
|
|
||||||
|
export interface CoaAbilityCatalogShard {
|
||||||
|
readonly schemaVersion: 1;
|
||||||
|
readonly classId: CoaClassId;
|
||||||
|
readonly abilities: readonly AbilityDefinition[];
|
||||||
|
readonly resource: ClassResourceProfile;
|
||||||
|
readonly triggeredAbilitiesBySpellId: Readonly<Record<number, AbilityDefinition>>;
|
||||||
|
}
|
||||||
|
|
||||||
|
interface CoaAbilityCatalogShardModule {
|
||||||
|
readonly default: CoaAbilityCatalogShard;
|
||||||
|
}
|
||||||
|
|
||||||
|
const shardLoaders = import.meta.glob<CoaAbilityCatalogShardModule>("./generated/coaAbilities/*.json");
|
||||||
|
const loadCache = new Map<CoaClassId, Promise<CoaAbilityCatalogShard>>();
|
||||||
|
|
||||||
|
export function loadCoaAbilityCatalogShard(classId: CoaClassId): Promise<CoaAbilityCatalogShard> {
|
||||||
|
const cached = loadCache.get(classId);
|
||||||
|
if (cached) return cached;
|
||||||
|
const key = `./generated/coaAbilities/${classId}.json`;
|
||||||
|
const loader = shardLoaders[key];
|
||||||
|
if (!loader) return Promise.reject(new Error(`No Conquest ability catalog exists for ${classId}.`));
|
||||||
|
const pending = loader().then((module) => {
|
||||||
|
if (module.default.schemaVersion !== 1 || module.default.classId !== classId) {
|
||||||
|
throw new Error(`Conquest ability catalog ${classId} has incompatible metadata.`);
|
||||||
|
}
|
||||||
|
return module.default;
|
||||||
|
}).catch((error: unknown) => {
|
||||||
|
loadCache.delete(classId);
|
||||||
|
throw error;
|
||||||
|
});
|
||||||
|
loadCache.set(classId, pending);
|
||||||
|
return pending;
|
||||||
|
}
|
||||||
@@ -16,6 +16,7 @@ import {
|
|||||||
defaultActionBarForCharacter,
|
defaultActionBarForCharacter,
|
||||||
isAbilityUnlocked,
|
isAbilityUnlocked,
|
||||||
resourceProfileForClass,
|
resourceProfileForClass,
|
||||||
|
triggeredAbilityBySpellId,
|
||||||
type AbilityAmountScaling,
|
type AbilityAmountScaling,
|
||||||
type AbilityDefinition,
|
type AbilityDefinition,
|
||||||
type AbilityEffect,
|
type AbilityEffect,
|
||||||
@@ -57,8 +58,6 @@ import {
|
|||||||
spendDeathKnightRunes,
|
spendDeathKnightRunes,
|
||||||
} from "./combatResources";
|
} from "./combatResources";
|
||||||
import { resolveHealing, type HealingResolution } from "./combatHealing";
|
import { resolveHealing, type HealingResolution } from "./combatHealing";
|
||||||
import { romTriggeredAbilityBySpellId } from "./romAbilityCatalog";
|
|
||||||
import { coaTriggeredAbilityBySpellId } from "./coaAbilityRuntimeCatalog";
|
|
||||||
import {
|
import {
|
||||||
awardExperience,
|
awardExperience,
|
||||||
healerManExperienceAward,
|
healerManExperienceAward,
|
||||||
@@ -2578,7 +2577,7 @@ function executeTriggeredSpellInWork(
|
|||||||
const catalogAbility = work.abilities.find((candidate) => (
|
const catalogAbility = work.abilities.find((candidate) => (
|
||||||
candidate.dbcSpellId === spellId
|
candidate.dbcSpellId === spellId
|
||||||
|| candidate.ranks?.some((rank) => rank.spellId === spellId)
|
|| candidate.ranks?.some((rank) => rank.spellId === spellId)
|
||||||
)) ?? romTriggeredAbilityBySpellId(spellId) ?? (allowCoaHidden ? coaTriggeredAbilityBySpellId(spellId) : null);
|
)) ?? triggeredAbilityBySpellId(spellId, allowCoaHidden);
|
||||||
if (!catalogAbility) return;
|
if (!catalogAbility) return;
|
||||||
const ability = abilityAtLevel(catalogAbility, work.progression.level);
|
const ability = abilityAtLevel(catalogAbility, work.progression.level);
|
||||||
const source = playerThreatSource({ talentModifiers: work.talentModifiers }, ability.id, true);
|
const source = playerThreatSource({ talentModifiers: work.talentModifiers }, ability.id, true);
|
||||||
|
|||||||
@@ -5,6 +5,61 @@ import generated from "./generated/dungeonCampaignCatalog.json";
|
|||||||
import manastormPackages from "./generated/manastormAssetPackages.json";
|
import manastormPackages from "./generated/manastormAssetPackages.json";
|
||||||
import { DUNGEON_DEFINITIONS, requireDungeonDefinition } from "./dungeonRegistry";
|
import { DUNGEON_DEFINITIONS, requireDungeonDefinition } from "./dungeonRegistry";
|
||||||
|
|
||||||
|
const SOURCE_NATIVE_DUNGEON_IDS = [
|
||||||
|
"ahnkahet-old-kingdom",
|
||||||
|
"arcatraz",
|
||||||
|
"auchenai-crypts",
|
||||||
|
"azjol-nerub",
|
||||||
|
"black-morass",
|
||||||
|
"blackfathom-deeps",
|
||||||
|
"blackrock-depths",
|
||||||
|
"blackrock-spire",
|
||||||
|
"blood-furnace",
|
||||||
|
"botanica",
|
||||||
|
"culling-of-stratholme",
|
||||||
|
"draktharon-keep",
|
||||||
|
"escape-from-durnholde",
|
||||||
|
"forge-of-souls",
|
||||||
|
"gnomeregan",
|
||||||
|
"gundrak",
|
||||||
|
"halls-of-lightning",
|
||||||
|
"halls-of-reflection",
|
||||||
|
"halls-of-stone",
|
||||||
|
"magisters-terrace",
|
||||||
|
"mana-tombs",
|
||||||
|
"maraudon",
|
||||||
|
"mechanar",
|
||||||
|
"pit-of-saron",
|
||||||
|
"ragefire-chasm",
|
||||||
|
"scarlet-monastery",
|
||||||
|
"sethekk-halls",
|
||||||
|
"shadow-labyrinth",
|
||||||
|
"shadowfang-keep",
|
||||||
|
"shattered-halls",
|
||||||
|
"slave-pens",
|
||||||
|
"steamvault",
|
||||||
|
"stormwind-stockade",
|
||||||
|
"sunken-temple",
|
||||||
|
"the-nexus",
|
||||||
|
"the-oculus",
|
||||||
|
"trial-of-the-champion",
|
||||||
|
"uldaman",
|
||||||
|
"underbog",
|
||||||
|
"utgarde-keep",
|
||||||
|
"utgarde-pinnacle",
|
||||||
|
"violet-hold",
|
||||||
|
"zulfarrak",
|
||||||
|
] as const;
|
||||||
|
|
||||||
|
const WDT_PLACEMENT_DUNGEON_IDS = [
|
||||||
|
"dire-maul",
|
||||||
|
"hellfire-ramparts",
|
||||||
|
"razorfen-downs",
|
||||||
|
"razorfen-kraul",
|
||||||
|
"scholomance",
|
||||||
|
"stratholme",
|
||||||
|
] as const;
|
||||||
|
|
||||||
describe("generated dungeon campaign catalog", () => {
|
describe("generated dungeon campaign catalog", () => {
|
||||||
it("covers only reviewed five-player instances with loadable assets and combat content", () => {
|
it("covers only reviewed five-player instances with loadable assets and combat content", () => {
|
||||||
expect(generated.definitions).toHaveLength(54);
|
expect(generated.definitions).toHaveLength(54);
|
||||||
@@ -240,17 +295,124 @@ describe("generated dungeon campaign catalog", () => {
|
|||||||
expect(targorr?.position).toEqual([-159.582, -25.6062, 1.2531]);
|
expect(targorr?.position).toEqual([-159.582, -25.6062, 1.2531]);
|
||||||
});
|
});
|
||||||
|
|
||||||
|
it("starts Ragefire Chasm in the dungeon instead of the Orgrimmar portal annex", () => {
|
||||||
|
const ragefire = requireDungeonDefinition("ragefire-chasm");
|
||||||
|
const firstSpawn = ragefire.staticSpawns.find(
|
||||||
|
(spawn) => spawn.id === "creature-38041",
|
||||||
|
);
|
||||||
|
const jergosh = ragefire.staticSpawns.find(
|
||||||
|
(spawn) => spawn.id === "creature-48740",
|
||||||
|
);
|
||||||
|
|
||||||
|
expect(ragefire.environmentMode).toBe("global-wmo");
|
||||||
|
expect(ragefire.provenance.coordinateTransform)
|
||||||
|
.toBe("three(x,y,z)=draft(x,y,z)");
|
||||||
|
expect(ragefire.entrance.footPosition).toEqual([-3.81, -17.84, -14.82]);
|
||||||
|
expect(firstSpawn?.position).toEqual([45.0298, -21.2917, -27.7645]);
|
||||||
|
expect(jergosh?.position).toEqual([376.811, -21.801, 209.224]);
|
||||||
|
});
|
||||||
|
|
||||||
|
it("keeps every navigation-audited dungeon in its native source coordinates", () => {
|
||||||
|
const sourceAligned = generated.definitions
|
||||||
|
.filter((dungeon) => dungeon.provenance.coordinateAlignment === "source")
|
||||||
|
.map((dungeon) => dungeon.id)
|
||||||
|
.sort();
|
||||||
|
|
||||||
|
expect(sourceAligned).toEqual([...SOURCE_NATIVE_DUNGEON_IDS].sort());
|
||||||
|
for (const dungeonId of SOURCE_NATIVE_DUNGEON_IDS) {
|
||||||
|
const dungeon = requireDungeonDefinition(dungeonId);
|
||||||
|
expect(dungeon.provenance.coordinateAlignment, dungeonId).toBe("source");
|
||||||
|
expect(dungeon.provenance.coordinateTransform, dungeonId)
|
||||||
|
.not.toContain("package-anchor-translation");
|
||||||
|
expect(
|
||||||
|
dungeon.validation.warnings.some((warning) => (
|
||||||
|
warning.includes("native world coordinates")
|
||||||
|
)),
|
||||||
|
dungeonId,
|
||||||
|
).toBe(true);
|
||||||
|
}
|
||||||
|
});
|
||||||
|
|
||||||
|
it("pins representative global-WMO and ADT entrances to their source basis", () => {
|
||||||
|
const expectedEntrances = {
|
||||||
|
"blackfathom-deeps": [151.89, -39.87, 106.96],
|
||||||
|
"blackrock-depths": [-456.929, -68.0896, 34.0923],
|
||||||
|
"blood-furnace": [3.9967, -44.8009, 14.6363],
|
||||||
|
"the-nexus": [-145.87, -16.636, -10.554],
|
||||||
|
"violet-hold": [803.93, 44.364, 1808.82],
|
||||||
|
"black-morass": [7034.7, 32.5619, -1496.24],
|
||||||
|
"culling-of-stratholme": [556.92, 36.69, 1431.1],
|
||||||
|
} as const;
|
||||||
|
|
||||||
|
for (const [dungeonId, expected] of Object.entries(expectedEntrances)) {
|
||||||
|
expect(requireDungeonDefinition(dungeonId).entrance.footPosition, dungeonId)
|
||||||
|
.toEqual(expected);
|
||||||
|
}
|
||||||
|
});
|
||||||
|
|
||||||
|
it("keeps scripted Trial of the Champion objectives on the native arena floor", () => {
|
||||||
|
const trial = requireDungeonDefinition("trial-of-the-champion");
|
||||||
|
const [entranceX, , entranceZ] = trial.entrance.footPosition;
|
||||||
|
|
||||||
|
expect(trial.objectiveOrder).toHaveLength(5);
|
||||||
|
for (const objective of trial.objectiveOrder) {
|
||||||
|
expect(objective.position[1], objective.name).toBeGreaterThanOrEqual(411);
|
||||||
|
expect(objective.position[1], objective.name).toBeLessThanOrEqual(413);
|
||||||
|
expect(
|
||||||
|
Math.hypot(objective.position[0] - entranceX, objective.position[2] - entranceZ),
|
||||||
|
objective.name,
|
||||||
|
).toBeLessThan(150);
|
||||||
|
}
|
||||||
|
});
|
||||||
|
|
||||||
|
it("uses client WDT placement records for rotated or translated root WMOs", () => {
|
||||||
|
for (const dungeonId of WDT_PLACEMENT_DUNGEON_IDS) {
|
||||||
|
expect(
|
||||||
|
requireDungeonDefinition(dungeonId).provenance.coordinateAlignment,
|
||||||
|
dungeonId,
|
||||||
|
).toBe("source-transform");
|
||||||
|
expect(
|
||||||
|
requireDungeonDefinition(dungeonId).provenance.coordinateTransform,
|
||||||
|
dungeonId,
|
||||||
|
).toContain("rotate-y");
|
||||||
|
expect(
|
||||||
|
requireDungeonDefinition(dungeonId).provenance.coordinateTransform,
|
||||||
|
dungeonId,
|
||||||
|
).not.toContain("package-anchor-translation");
|
||||||
|
}
|
||||||
|
});
|
||||||
|
|
||||||
|
it("pins every map found by the portal-map coordinate audit to its dungeon entrance", () => {
|
||||||
|
const expectedEntrances = {
|
||||||
|
arcatraz: [1.2316, -0.2043, 0.0143],
|
||||||
|
"dire-maul": [44.4499, -2.712, 154.822],
|
||||||
|
"hellfire-ramparts": [5.7204, 129.1393, -94.4661],
|
||||||
|
"razorfen-downs": [-190.2691, -10.71, -100.8219],
|
||||||
|
"razorfen-kraul": [-37.901, -5.684, 22.7908],
|
||||||
|
"scarlet-monastery": [1053.48, 18.6775, 1688.99],
|
||||||
|
scholomance: [-55.0407, -13.1583, 20.4141],
|
||||||
|
"shadowfang-keep": [2109.18, 76.8898, -229.135],
|
||||||
|
stratholme: [-143.4892, -5.8848, -266.8634],
|
||||||
|
zulfarrak: [841.59, 8.93, 1213.52],
|
||||||
|
} as const;
|
||||||
|
|
||||||
|
for (const [dungeonId, expected] of Object.entries(expectedEntrances)) {
|
||||||
|
expect(requireDungeonDefinition(dungeonId).entrance.footPosition, dungeonId)
|
||||||
|
.toEqual(expected);
|
||||||
|
}
|
||||||
|
});
|
||||||
|
|
||||||
it("starts Razorfen Kraul inside the portal and maps its first pack into the WMO", () => {
|
it("starts Razorfen Kraul inside the portal and maps its first pack into the WMO", () => {
|
||||||
const razorfenKraul = requireDungeonDefinition("razorfen-kraul");
|
const razorfenKraul = requireDungeonDefinition("razorfen-kraul");
|
||||||
const firstPackSpawn = razorfenKraul.staticSpawns.find(
|
const firstPackSpawn = razorfenKraul.staticSpawns.find(
|
||||||
(spawn) => spawn.id === "creature-87284",
|
(spawn) => spawn.id === "creature-87284",
|
||||||
);
|
);
|
||||||
|
|
||||||
expect(razorfenKraul.entrance.footPosition).toEqual([-33.81, -5.2481, 21.522]);
|
expect(razorfenKraul.entrance.footPosition).toEqual([-37.901, -5.684, 22.7908]);
|
||||||
expect(firstPackSpawn).toBeTruthy();
|
expect(firstPackSpawn).toBeTruthy();
|
||||||
expect(firstPackSpawn!.position[0]).toBeGreaterThan(razorfenKraul.entrance.footPosition[0]);
|
expect(firstPackSpawn!.position[0]).toBeGreaterThan(razorfenKraul.entrance.footPosition[0]);
|
||||||
expect(firstPackSpawn!.position[0]).toBeLessThan(100);
|
expect(firstPackSpawn!.position[0]).toBeLessThan(100);
|
||||||
expect(razorfenKraul.provenance.coordinateTransform)
|
expect(razorfenKraul.provenance.coordinateTransform)
|
||||||
.toBe("three(x,y,z)=(-draft.x,draft.y,draft.z)+package-anchor-translation");
|
.toBe("three(x,y,z)=rotate-y(draft(x,y,z)-[-1952.1803,87.684,1587.8923],-133deg)");
|
||||||
});
|
});
|
||||||
});
|
});
|
||||||
|
|||||||
@@ -191,6 +191,7 @@ export interface DungeonPresentation {
|
|||||||
|
|
||||||
export interface DungeonSourceProvenance {
|
export interface DungeonSourceProvenance {
|
||||||
readonly clientBuild: string;
|
readonly clientBuild: string;
|
||||||
|
readonly coordinateAlignment?: "player-anchor" | "source" | "source-transform";
|
||||||
readonly clientArchiveHashes: Readonly<Record<string, string>>;
|
readonly clientArchiveHashes: Readonly<Record<string, string>>;
|
||||||
readonly databaseAdapter: string;
|
readonly databaseAdapter: string;
|
||||||
readonly databaseRevision?: string;
|
readonly databaseRevision?: string;
|
||||||
|
|||||||
Some files were not shown because too many files have changed in this diff Show More
Reference in New Issue
Block a user