Release Healer Man 0.1.14

This commit is contained in:
phenom committed 2026-09-05 11:40:50 -04:00
1 parent 67d7a8ebe7
commit 7b41b102fa
147 files changed
+169892 -1342

No files matched your search

+4
View File
@@ -159,6 +159,7 @@ describe("local profile repository", () => {
rosters: { "offline-roster": [] },
presentationSettingsByOwner: {
"offline-roster": {
vfxMode: "original",
vfxQuality: "low",
vfxEnabled: { player: true, party: false, enemy: true },
audioEnabled: { player: true, party: true, enemy: false },
@@ -172,12 +173,15 @@ describe("local profile repository", () => {
});
expect(JSON.parse(legacyV3Storage.getItem(PROFILE_STORAGE_KEYS.database)!)
.presentationSettingsByOwner["offline-roster"].vfxMode).toBe("generated");
expect(JSON.parse(legacyV3Storage.getItem(PROFILE_STORAGE_KEYS.database)!)
.presentationSettingsByOwner["offline-roster"].originalMediaEnabled).toBe(false);
const storage = new MemoryStorage();
const firstOwner = (await createLocalAccount("EffectsOne", "eight-plus", storage)).session!.ownerId;
const secondOwner = (await createLocalAccount("EffectsTwo", "eight-plus", storage)).session!.ownerId;
const customized = updateCombatPresentationSettings(firstOwner, {
...DEFAULT_COMBAT_PRESENTATION_SETTINGS,
originalMediaEnabled: true,
vfxMode: "original",
vfxQuality: "low",
vfxEnabled: { player: true, party: false, enemy: true },
+7 -1
View File
@@ -216,7 +216,13 @@ function readDatabase(storage: StorageLike | null = browserLocalStorage()): Prof
const storedMode = settings && typeof settings === "object" && !Array.isArray(settings)
? (settings as { vfxMode?: unknown }).vfxMode
: undefined;
if (storedMode !== normalized.vfxMode) presentationSettingsMigrated = true;
const storedOriginalMediaEnabled = settings && typeof settings === "object" && !Array.isArray(settings)
? (settings as { originalMediaEnabled?: unknown }).originalMediaEnabled
: undefined;
if (
storedMode !== normalized.vfxMode
|| storedOriginalMediaEnabled !== normalized.originalMediaEnabled
) presentationSettingsMigrated = true;
return [ownerId, normalized];
}),
);
@@ -36,7 +36,7 @@
"runeWakerIdsAreNamespaced": true,
"serverEntryPolicy": "RuneWaker numeric ids are never passed to AzerothCore serverEntry.",
"sourceSpellCatalog": "scripts/runewaker-pipeline/snapshots/runewaker-npc-spell-catalog.json",
"sourceSpellCatalogSha256": "ad29efc1d4906fdd4f2ac7a69316cac32d8fcbaea674678f050265c86e0c506c",
"sourceSpellCatalogSha256": "c47e67013e68ecf9c7ae30ef4b2058a850505385739dfe8c32ce8874e6ed077c",
"sourceSpellPolicy": "Only positively classified direct-HP-damage families execute; coefficients remain conservative HealerMan values.",
"status": "green",
"recipe": "scripts/runewaker-pipeline/recipes/aeternal-circle-population.json",
@@ -36,7 +36,7 @@
"runeWakerIdsAreNamespaced": true,
"serverEntryPolicy": "RuneWaker numeric ids are never passed to AzerothCore serverEntry.",
"sourceSpellCatalog": "scripts/runewaker-pipeline/snapshots/runewaker-npc-spell-catalog.json",
"sourceSpellCatalogSha256": "ad29efc1d4906fdd4f2ac7a69316cac32d8fcbaea674678f050265c86e0c506c",
"sourceSpellCatalogSha256": "c47e67013e68ecf9c7ae30ef4b2058a850505385739dfe8c32ce8874e6ed077c",
"sourceSpellPolicy": "Only positively classified direct-HP-damage families execute; coefficients remain conservative HealerMan values.",
"status": "green",
"recipe": "scripts/runewaker-pipeline/recipes/barren-caves-population.json",
@@ -36,7 +36,7 @@
"runeWakerIdsAreNamespaced": true,
"serverEntryPolicy": "RuneWaker numeric ids are never passed to AzerothCore serverEntry.",
"sourceSpellCatalog": "scripts/runewaker-pipeline/snapshots/runewaker-npc-spell-catalog.json",
"sourceSpellCatalogSha256": "ad29efc1d4906fdd4f2ac7a69316cac32d8fcbaea674678f050265c86e0c506c",
"sourceSpellCatalogSha256": "c47e67013e68ecf9c7ae30ef4b2058a850505385739dfe8c32ce8874e6ed077c",
"sourceSpellPolicy": "Only positively classified direct-HP-damage families execute; coefficients remain conservative HealerMan values.",
"status": "green",
"recipe": "scripts/runewaker-pipeline/recipes/cave-water-dragon-population.json",
@@ -36,7 +36,7 @@
"runeWakerIdsAreNamespaced": true,
"serverEntryPolicy": "RuneWaker numeric ids are never passed to AzerothCore serverEntry.",
"sourceSpellCatalog": "scripts/runewaker-pipeline/snapshots/runewaker-npc-spell-catalog.json",
"sourceSpellCatalogSha256": "ad29efc1d4906fdd4f2ac7a69316cac32d8fcbaea674678f050265c86e0c506c",
"sourceSpellCatalogSha256": "c47e67013e68ecf9c7ae30ef4b2058a850505385739dfe8c32ce8874e6ed077c",
"sourceSpellPolicy": "Only positively classified direct-HP-damage families execute; coefficients remain conservative HealerMan values.",
"status": "green",
"recipe": "scripts/runewaker-pipeline/recipes/demon-stronghold-125-population.json",
@@ -36,7 +36,7 @@
"runeWakerIdsAreNamespaced": true,
"serverEntryPolicy": "RuneWaker numeric ids are never passed to AzerothCore serverEntry.",
"sourceSpellCatalog": "scripts/runewaker-pipeline/snapshots/runewaker-npc-spell-catalog.json",
"sourceSpellCatalogSha256": "ad29efc1d4906fdd4f2ac7a69316cac32d8fcbaea674678f050265c86e0c506c",
"sourceSpellCatalogSha256": "c47e67013e68ecf9c7ae30ef4b2058a850505385739dfe8c32ce8874e6ed077c",
"sourceSpellPolicy": "Only positively classified direct-HP-damage families execute; coefficients remain conservative HealerMan values.",
"status": "green",
"recipe": "scripts/runewaker-pipeline/recipes/demon-stronghold-population.json",
@@ -36,7 +36,7 @@
"runeWakerIdsAreNamespaced": true,
"serverEntryPolicy": "RuneWaker numeric ids are never passed to AzerothCore serverEntry.",
"sourceSpellCatalog": "scripts/runewaker-pipeline/snapshots/runewaker-npc-spell-catalog.json",
"sourceSpellCatalogSha256": "ad29efc1d4906fdd4f2ac7a69316cac32d8fcbaea674678f050265c86e0c506c",
"sourceSpellCatalogSha256": "c47e67013e68ecf9c7ae30ef4b2058a850505385739dfe8c32ce8874e6ed077c",
"sourceSpellPolicy": "Only positively classified direct-HP-damage families execute; coefficients remain conservative HealerMan values.",
"status": "green",
"recipe": "scripts/runewaker-pipeline/recipes/dungeon-of-dalanis-population.json",
@@ -1,18 +1,11 @@
{
"schemaVersion": 2,
"schemaVersion": 1,
"dungeonId": "forsaken-abbey",
"zoneId": 102,
"sourceRowCount": 251,
"importedSpawnCount": 150,
"excludedRowCount": 101,
"bossSpawnCount": 8,
"objectiveOrder": {
"authoritative": false,
"kind": "healerMan-distance-derived",
"evidence": [
"scripts/runewaker-pipeline/build-population.mjs"
]
},
"sourceSnapshot": "scripts/runewaker-pipeline/snapshots/forsaken-abbey-zone-102.json",
"sourceContentSha256": "13ef6edd3df6e71389aa92d01201889148c0794cf9ecaafdbaf90d3145d2bf7b",
"sourceHashes": {
@@ -26,7 +19,7 @@
"Resource/data/string_enus.db": "4fce021ba9ed110a213e08d3228b2de57cec66ddbbc8cdcf5753204874d9ab75"
}
},
"coordinateTransform": "threeLocal=(inverse(WDB placement)*server)*(meters,meters,-meters)",
"coordinateTransform": "threeLocal=(inverse(WDB placement)*server)*(0.1,0.1,-0.1)",
"navmeshProjection": {
"maximumAllowedDistance": 2,
"maximumObservedDistance": 2.5965,
@@ -35,9 +28,6 @@
},
"runeWakerIdsAreNamespaced": true,
"serverEntryPolicy": "RuneWaker numeric ids are never passed to AzerothCore serverEntry.",
"sourceSpellCatalog": "scripts/runewaker-pipeline/snapshots/runewaker-npc-spell-catalog.json",
"sourceSpellCatalogSha256": "ad29efc1d4906fdd4f2ac7a69316cac32d8fcbaea674678f050265c86e0c506c",
"sourceSpellPolicy": "Only positively classified direct-HP-damage families execute; coefficients remain conservative HealerMan values.",
"status": "green",
"recipe": "scripts/runewaker-pipeline/recipes/forsaken-abbey-population.json",
"actorManifest": "public/assets/creatures/forsaken-abbey/manifest.json",
@@ -36,7 +36,7 @@
"runeWakerIdsAreNamespaced": true,
"serverEntryPolicy": "RuneWaker numeric ids are never passed to AzerothCore serverEntry.",
"sourceSpellCatalog": "scripts/runewaker-pipeline/snapshots/runewaker-npc-spell-catalog.json",
"sourceSpellCatalogSha256": "ad29efc1d4906fdd4f2ac7a69316cac32d8fcbaea674678f050265c86e0c506c",
"sourceSpellCatalogSha256": "c47e67013e68ecf9c7ae30ef4b2058a850505385739dfe8c32ce8874e6ed077c",
"sourceSpellPolicy": "Only positively classified direct-HP-damage families execute; coefficients remain conservative HealerMan values.",
"status": "green",
"recipe": "scripts/runewaker-pipeline/recipes/grafu-castle-population.json",
@@ -36,7 +36,7 @@
"runeWakerIdsAreNamespaced": true,
"serverEntryPolicy": "RuneWaker numeric ids are never passed to AzerothCore serverEntry.",
"sourceSpellCatalog": "scripts/runewaker-pipeline/snapshots/runewaker-npc-spell-catalog.json",
"sourceSpellCatalogSha256": "ad29efc1d4906fdd4f2ac7a69316cac32d8fcbaea674678f050265c86e0c506c",
"sourceSpellCatalogSha256": "c47e67013e68ecf9c7ae30ef4b2058a850505385739dfe8c32ce8874e6ed077c",
"sourceSpellPolicy": "Only positively classified direct-HP-damage families execute; coefficients remain conservative HealerMan values.",
"status": "green",
"recipe": "scripts/runewaker-pipeline/recipes/hall-of-survivors-population.json",
@@ -36,7 +36,7 @@
"runeWakerIdsAreNamespaced": true,
"serverEntryPolicy": "RuneWaker numeric ids are never passed to AzerothCore serverEntry.",
"sourceSpellCatalog": "scripts/runewaker-pipeline/snapshots/runewaker-npc-spell-catalog.json",
"sourceSpellCatalogSha256": "ad29efc1d4906fdd4f2ac7a69316cac32d8fcbaea674678f050265c86e0c506c",
"sourceSpellCatalogSha256": "c47e67013e68ecf9c7ae30ef4b2058a850505385739dfe8c32ce8874e6ed077c",
"sourceSpellPolicy": "Only positively classified direct-HP-damage families execute; coefficients remain conservative HealerMan values.",
"status": "green",
"recipe": "scripts/runewaker-pipeline/recipes/hoto-population.json",
@@ -36,7 +36,7 @@
"runeWakerIdsAreNamespaced": true,
"serverEntryPolicy": "RuneWaker numeric ids are never passed to AzerothCore serverEntry.",
"sourceSpellCatalog": "scripts/runewaker-pipeline/snapshots/runewaker-npc-spell-catalog.json",
"sourceSpellCatalogSha256": "ad29efc1d4906fdd4f2ac7a69316cac32d8fcbaea674678f050265c86e0c506c",
"sourceSpellCatalogSha256": "c47e67013e68ecf9c7ae30ef4b2058a850505385739dfe8c32ce8874e6ed077c",
"sourceSpellPolicy": "Only positively classified direct-HP-damage families execute; coefficients remain conservative HealerMan values.",
"status": "green",
"recipe": "scripts/runewaker-pipeline/recipes/ice-dwarf-kingdom-population.json",
@@ -36,7 +36,7 @@
"runeWakerIdsAreNamespaced": true,
"serverEntryPolicy": "RuneWaker numeric ids are never passed to AzerothCore serverEntry.",
"sourceSpellCatalog": "scripts/runewaker-pipeline/snapshots/runewaker-npc-spell-catalog.json",
"sourceSpellCatalogSha256": "ad29efc1d4906fdd4f2ac7a69316cac32d8fcbaea674678f050265c86e0c506c",
"sourceSpellCatalogSha256": "c47e67013e68ecf9c7ae30ef4b2058a850505385739dfe8c32ce8874e6ed077c",
"sourceSpellPolicy": "Only positively classified direct-HP-damage families execute; coefficients remain conservative HealerMan values.",
"status": "green",
"recipe": "scripts/runewaker-pipeline/recipes/kal-turok-hive-population.json",
@@ -36,7 +36,7 @@
"runeWakerIdsAreNamespaced": true,
"serverEntryPolicy": "RuneWaker numeric ids are never passed to AzerothCore serverEntry.",
"sourceSpellCatalog": "scripts/runewaker-pipeline/snapshots/runewaker-npc-spell-catalog.json",
"sourceSpellCatalogSha256": "ad29efc1d4906fdd4f2ac7a69316cac32d8fcbaea674678f050265c86e0c506c",
"sourceSpellCatalogSha256": "c47e67013e68ecf9c7ae30ef4b2058a850505385739dfe8c32ce8874e6ed077c",
"sourceSpellPolicy": "Only positively classified direct-HP-damage families execute; coefficients remain conservative HealerMan values.",
"status": "green",
"recipe": "scripts/runewaker-pipeline/recipes/kalins-shrine-population.json",
@@ -36,7 +36,7 @@
"runeWakerIdsAreNamespaced": true,
"serverEntryPolicy": "RuneWaker numeric ids are never passed to AzerothCore serverEntry.",
"sourceSpellCatalog": "scripts/runewaker-pipeline/snapshots/runewaker-npc-spell-catalog.json",
"sourceSpellCatalogSha256": "ad29efc1d4906fdd4f2ac7a69316cac32d8fcbaea674678f050265c86e0c506c",
"sourceSpellCatalogSha256": "c47e67013e68ecf9c7ae30ef4b2058a850505385739dfe8c32ce8874e6ed077c",
"sourceSpellPolicy": "Only positively classified direct-HP-damage families execute; coefficients remain conservative HealerMan values.",
"status": "green",
"recipe": "scripts/runewaker-pipeline/recipes/kawaks-tomb-population.json",
@@ -36,7 +36,7 @@
"runeWakerIdsAreNamespaced": true,
"serverEntryPolicy": "RuneWaker numeric ids are never passed to AzerothCore serverEntry.",
"sourceSpellCatalog": "scripts/runewaker-pipeline/snapshots/runewaker-npc-spell-catalog.json",
"sourceSpellCatalogSha256": "ad29efc1d4906fdd4f2ac7a69316cac32d8fcbaea674678f050265c86e0c506c",
"sourceSpellCatalogSha256": "c47e67013e68ecf9c7ae30ef4b2058a850505385739dfe8c32ce8874e6ed077c",
"sourceSpellPolicy": "Only positively classified direct-HP-damage families execute; coefficients remain conservative HealerMan values.",
"status": "green",
"recipe": "scripts/runewaker-pipeline/recipes/kulech-bones-nest-population.json",
@@ -36,7 +36,7 @@
"runeWakerIdsAreNamespaced": true,
"serverEntryPolicy": "RuneWaker numeric ids are never passed to AzerothCore serverEntry.",
"sourceSpellCatalog": "scripts/runewaker-pipeline/snapshots/runewaker-npc-spell-catalog.json",
"sourceSpellCatalogSha256": "ad29efc1d4906fdd4f2ac7a69316cac32d8fcbaea674678f050265c86e0c506c",
"sourceSpellCatalogSha256": "c47e67013e68ecf9c7ae30ef4b2058a850505385739dfe8c32ce8874e6ed077c",
"sourceSpellPolicy": "Only positively classified direct-HP-damage families execute; coefficients remain conservative HealerMan values.",
"status": "green",
"recipe": "scripts/runewaker-pipeline/recipes/lair-demon-dragon-population.json",
@@ -36,7 +36,7 @@
"runeWakerIdsAreNamespaced": true,
"serverEntryPolicy": "RuneWaker numeric ids are never passed to AzerothCore serverEntry.",
"sourceSpellCatalog": "scripts/runewaker-pipeline/snapshots/runewaker-npc-spell-catalog.json",
"sourceSpellCatalogSha256": "ad29efc1d4906fdd4f2ac7a69316cac32d8fcbaea674678f050265c86e0c506c",
"sourceSpellCatalogSha256": "c47e67013e68ecf9c7ae30ef4b2058a850505385739dfe8c32ce8874e6ed077c",
"sourceSpellPolicy": "Only positively classified direct-HP-damage families execute; coefficients remain conservative HealerMan values.",
"status": "green",
"recipe": "scripts/runewaker-pipeline/recipes/mystic-altar-population.json",
@@ -36,7 +36,7 @@
"runeWakerIdsAreNamespaced": true,
"serverEntryPolicy": "RuneWaker numeric ids are never passed to AzerothCore serverEntry.",
"sourceSpellCatalog": "scripts/runewaker-pipeline/snapshots/runewaker-npc-spell-catalog.json",
"sourceSpellCatalogSha256": "ad29efc1d4906fdd4f2ac7a69316cac32d8fcbaea674678f050265c86e0c506c",
"sourceSpellCatalogSha256": "c47e67013e68ecf9c7ae30ef4b2058a850505385739dfe8c32ce8874e6ed077c",
"sourceSpellPolicy": "Only positively classified direct-HP-damage families execute; coefficients remain conservative HealerMan values.",
"status": "green",
"recipe": "scripts/runewaker-pipeline/recipes/paspers-shrine-population.json",
@@ -36,7 +36,7 @@
"runeWakerIdsAreNamespaced": true,
"serverEntryPolicy": "RuneWaker numeric ids are never passed to AzerothCore serverEntry.",
"sourceSpellCatalog": "scripts/runewaker-pipeline/snapshots/runewaker-npc-spell-catalog.json",
"sourceSpellCatalogSha256": "ad29efc1d4906fdd4f2ac7a69316cac32d8fcbaea674678f050265c86e0c506c",
"sourceSpellCatalogSha256": "c47e67013e68ecf9c7ae30ef4b2058a850505385739dfe8c32ce8874e6ed077c",
"sourceSpellPolicy": "Only positively classified direct-HP-damage families execute; coefficients remain conservative HealerMan values.",
"status": "green",
"recipe": "scripts/runewaker-pipeline/recipes/raksha-temple-population.json",
@@ -36,7 +36,7 @@
"runeWakerIdsAreNamespaced": true,
"serverEntryPolicy": "RuneWaker numeric ids are never passed to AzerothCore serverEntry.",
"sourceSpellCatalog": "scripts/runewaker-pipeline/snapshots/runewaker-npc-spell-catalog.json",
"sourceSpellCatalogSha256": "ad29efc1d4906fdd4f2ac7a69316cac32d8fcbaea674678f050265c86e0c506c",
"sourceSpellCatalogSha256": "c47e67013e68ecf9c7ae30ef4b2058a850505385739dfe8c32ce8874e6ed077c",
"sourceSpellPolicy": "Only positively classified direct-HP-damage families execute; coefficients remain conservative HealerMan values.",
"status": "green",
"recipe": "scripts/runewaker-pipeline/recipes/sand-vortex-population.json",
@@ -36,7 +36,7 @@
"runeWakerIdsAreNamespaced": true,
"serverEntryPolicy": "RuneWaker numeric ids are never passed to AzerothCore serverEntry.",
"sourceSpellCatalog": "scripts/runewaker-pipeline/snapshots/runewaker-npc-spell-catalog.json",
"sourceSpellCatalogSha256": "ad29efc1d4906fdd4f2ac7a69316cac32d8fcbaea674678f050265c86e0c506c",
"sourceSpellCatalogSha256": "c47e67013e68ecf9c7ae30ef4b2058a850505385739dfe8c32ce8874e6ed077c",
"sourceSpellPolicy": "Only positively classified direct-HP-damage families execute; coefficients remain conservative HealerMan values.",
"status": "green",
"recipe": "scripts/runewaker-pipeline/recipes/sardo-castle-population.json",
@@ -36,7 +36,7 @@
"runeWakerIdsAreNamespaced": true,
"serverEntryPolicy": "RuneWaker numeric ids are never passed to AzerothCore serverEntry.",
"sourceSpellCatalog": "scripts/runewaker-pipeline/snapshots/runewaker-npc-spell-catalog.json",
"sourceSpellCatalogSha256": "ad29efc1d4906fdd4f2ac7a69316cac32d8fcbaea674678f050265c86e0c506c",
"sourceSpellCatalogSha256": "c47e67013e68ecf9c7ae30ef4b2058a850505385739dfe8c32ce8874e6ed077c",
"sourceSpellPolicy": "Only positively classified direct-HP-damage families execute; coefficients remain conservative HealerMan values.",
"status": "green",
"recipe": "scripts/runewaker-pipeline/recipes/the-origin-population.json",
@@ -36,7 +36,7 @@
"runeWakerIdsAreNamespaced": true,
"serverEntryPolicy": "RuneWaker numeric ids are never passed to AzerothCore serverEntry.",
"sourceSpellCatalog": "scripts/runewaker-pipeline/snapshots/runewaker-npc-spell-catalog.json",
"sourceSpellCatalogSha256": "ad29efc1d4906fdd4f2ac7a69316cac32d8fcbaea674678f050265c86e0c506c",
"sourceSpellCatalogSha256": "c47e67013e68ecf9c7ae30ef4b2058a850505385739dfe8c32ce8874e6ed077c",
"sourceSpellPolicy": "Only positively classified direct-HP-damage families execute; coefficients remain conservative HealerMan values.",
"status": "green",
"recipe": "scripts/runewaker-pipeline/recipes/tomb-seven-heroes-population.json",
@@ -36,7 +36,7 @@
"runeWakerIdsAreNamespaced": true,
"serverEntryPolicy": "RuneWaker numeric ids are never passed to AzerothCore serverEntry.",
"sourceSpellCatalog": "scripts/runewaker-pipeline/snapshots/runewaker-npc-spell-catalog.json",
"sourceSpellCatalogSha256": "ad29efc1d4906fdd4f2ac7a69316cac32d8fcbaea674678f050265c86e0c506c",
"sourceSpellCatalogSha256": "c47e67013e68ecf9c7ae30ef4b2058a850505385739dfe8c32ce8874e6ed077c",
"sourceSpellPolicy": "Only positively classified direct-HP-damage families execute; coefficients remain conservative HealerMan values.",
"status": "green",
"recipe": "scripts/runewaker-pipeline/recipes/treasure-trove-population.json",
@@ -36,7 +36,7 @@
"runeWakerIdsAreNamespaced": true,
"serverEntryPolicy": "RuneWaker numeric ids are never passed to AzerothCore serverEntry.",
"sourceSpellCatalog": "scripts/runewaker-pipeline/snapshots/runewaker-npc-spell-catalog.json",
"sourceSpellCatalogSha256": "ad29efc1d4906fdd4f2ac7a69316cac32d8fcbaea674678f050265c86e0c506c",
"sourceSpellCatalogSha256": "c47e67013e68ecf9c7ae30ef4b2058a850505385739dfe8c32ce8874e6ed077c",
"sourceSpellPolicy": "Only positively classified direct-HP-damage families execute; coefficients remain conservative HealerMan values.",
"status": "green",
"recipe": "scripts/runewaker-pipeline/recipes/warnorken-arena-population.json",
@@ -36,7 +36,7 @@
"runeWakerIdsAreNamespaced": true,
"serverEntryPolicy": "RuneWaker numeric ids are never passed to AzerothCore serverEntry.",
"sourceSpellCatalog": "scripts/runewaker-pipeline/snapshots/runewaker-npc-spell-catalog.json",
"sourceSpellCatalogSha256": "ad29efc1d4906fdd4f2ac7a69316cac32d8fcbaea674678f050265c86e0c506c",
"sourceSpellCatalogSha256": "c47e67013e68ecf9c7ae30ef4b2058a850505385739dfe8c32ce8874e6ed077c",
"sourceSpellPolicy": "Only positively classified direct-HP-damage families execute; coefficients remain conservative HealerMan values.",
"status": "green",
"recipe": "scripts/runewaker-pipeline/recipes/ystra-labyrinth-boss1-population.json",
@@ -36,7 +36,7 @@
"runeWakerIdsAreNamespaced": true,
"serverEntryPolicy": "RuneWaker numeric ids are never passed to AzerothCore serverEntry.",
"sourceSpellCatalog": "scripts/runewaker-pipeline/snapshots/runewaker-npc-spell-catalog.json",
"sourceSpellCatalogSha256": "ad29efc1d4906fdd4f2ac7a69316cac32d8fcbaea674678f050265c86e0c506c",
"sourceSpellCatalogSha256": "c47e67013e68ecf9c7ae30ef4b2058a850505385739dfe8c32ce8874e6ed077c",
"sourceSpellPolicy": "Only positively classified direct-HP-damage families execute; coefficients remain conservative HealerMan values.",
"status": "green",
"recipe": "scripts/runewaker-pipeline/recipes/ystra-labyrinth-boss2-population.json",
@@ -36,7 +36,7 @@
"runeWakerIdsAreNamespaced": true,
"serverEntryPolicy": "RuneWaker numeric ids are never passed to AzerothCore serverEntry.",
"sourceSpellCatalog": "scripts/runewaker-pipeline/snapshots/runewaker-npc-spell-catalog.json",
"sourceSpellCatalogSha256": "ad29efc1d4906fdd4f2ac7a69316cac32d8fcbaea674678f050265c86e0c506c",
"sourceSpellCatalogSha256": "c47e67013e68ecf9c7ae30ef4b2058a850505385739dfe8c32ce8874e6ed077c",
"sourceSpellPolicy": "Only positively classified direct-HP-damage families execute; coefficients remain conservative HealerMan values.",
"status": "green",
"recipe": "scripts/runewaker-pipeline/recipes/ystra-labyrinth-boss3-population.json",
@@ -36,7 +36,7 @@
"runeWakerIdsAreNamespaced": true,
"serverEntryPolicy": "RuneWaker numeric ids are never passed to AzerothCore serverEntry.",
"sourceSpellCatalog": "scripts/runewaker-pipeline/snapshots/runewaker-npc-spell-catalog.json",
"sourceSpellCatalogSha256": "ad29efc1d4906fdd4f2ac7a69316cac32d8fcbaea674678f050265c86e0c506c",
"sourceSpellCatalogSha256": "c47e67013e68ecf9c7ae30ef4b2058a850505385739dfe8c32ce8874e6ed077c",
"sourceSpellPolicy": "Only positively classified direct-HP-damage families execute; coefficients remain conservative HealerMan values.",
"status": "green",
"recipe": "scripts/runewaker-pipeline/recipes/zurhidon-stronghold-124-population.json",
@@ -36,7 +36,7 @@
"runeWakerIdsAreNamespaced": true,
"serverEntryPolicy": "RuneWaker numeric ids are never passed to AzerothCore serverEntry.",
"sourceSpellCatalog": "scripts/runewaker-pipeline/snapshots/runewaker-npc-spell-catalog.json",
"sourceSpellCatalogSha256": "ad29efc1d4906fdd4f2ac7a69316cac32d8fcbaea674678f050265c86e0c506c",
"sourceSpellCatalogSha256": "c47e67013e68ecf9c7ae30ef4b2058a850505385739dfe8c32ce8874e6ed077c",
"sourceSpellPolicy": "Only positively classified direct-HP-damage families execute; coefficients remain conservative HealerMan values.",
"status": "green",
"recipe": "scripts/runewaker-pipeline/recipes/zurhidon-stronghold-population.json",
+3 -1
View File
@@ -36,6 +36,7 @@ import type {
} from "../app/characterCatalog";
import type { InventoryItem } from "../game/lootTypes";
import { resolveContentUrl } from "../content/contentManager";
import { useContentManagerSnapshot } from "../content/useContentManager";
import {
selectCharacterBaseClip,
selectCharacterEventClip,
@@ -144,6 +145,7 @@ function useManifestEntry(identity: AvatarIdentity | null | undefined): {
manifest: AvatarManifest | null;
entry: AvatarManifestEntry | null;
} {
const contentRevision = useContentManagerSnapshot().installedRevision;
const [state, setState] = useState<{ manifest: AvatarManifest; entry: AvatarManifestEntry } | null>(null);
const key = identity ? avatarManifestKey(identity.raceId, identity.gender) : "";
const raceId = identity?.raceId;
@@ -162,7 +164,7 @@ function useManifestEntry(identity: AvatarIdentity | null | undefined): {
if (!cancelled) console.warn("Playable-character manifest could not be loaded", error);
});
return () => { cancelled = true; };
}, [gender, key, raceId]);
}, [contentRevision, gender, key, raceId]);
return { manifest: state?.manifest ?? null, entry: state?.entry ?? null };
}
+2
View File
@@ -2,6 +2,7 @@ import { useGLTF } from "@react-three/drei";
import { resolveContentUrl } from "../content/contentManager";
import type { AvatarIdentity } from "./CharacterModel";
import {
availableAvatarManifestUrls,
getAvatarEntry,
loadAvatarManifest,
} from "./characterManifest";
@@ -16,6 +17,7 @@ const requestedModels = new Set<string>();
export async function preloadAvatarModels(
identities: readonly (AvatarIdentity | null | undefined)[],
): Promise<void> {
if (!availableAvatarManifestUrls().length) return;
const manifest = await loadAvatarManifest();
for (const identity of identities) {
if (!identity) continue;
+28
View File
@@ -1,5 +1,7 @@
import { afterEach, describe, expect, it, vi } from "vitest";
import {
availableAvatarManifestUrls,
getAvatarEntry,
loadAvatarManifestSources,
type AvatarManifest,
type AvatarManifestEntry,
@@ -33,6 +35,32 @@ afterEach(() => {
});
describe("playable-character manifest loading", () => {
it("does not expose optional character manifests until their packs are usable", () => {
expect(availableAvatarManifestUrls(() => false)).toEqual([]);
expect(availableAvatarManifestUrls((packId) => packId === "core:character-basic")).toEqual([
"/assets/characters/wow335a-v1/manifest.json",
]);
});
it("uses the matching basic human when an extra race pack is unavailable", async () => {
const loaded = await loadAvatarManifestSources(
["/assets/characters/wow335a-v1/manifest.json"],
async () => ({
ok: true,
status: 200,
json: async () => manifest("3.3.5", {
"human:male": { raceId: "human", gender: "male", model: "/base-human.glb" },
"orc:male": { raceId: "orc", gender: "male", model: "/orc.glb" },
}),
}),
);
const fallback = getAvatarEntry(loaded, "orc", "male", (packId) => packId === "core:character-basic");
expect(fallback?.model).toBe("/base-human.glb");
expect(fallback?.counts).toEqual({ skinColor: 1, face: 1, hairStyle: 1, hairColor: 1, feature: 1 });
expect(getAvatarEntry(loaded, "orc", "male", () => true)?.model).toBe("/orc.glb");
});
it("keeps valid base characters available when an optional pack fails", async () => {
vi.spyOn(console, "warn").mockImplementation(() => undefined);
const base = manifest("3.3.5", {
+108 -20
View File
@@ -1,5 +1,8 @@
import type { AppearanceCounts, GenderId, RaceId } from "../app/characterCatalog";
import { resolveContentUrl } from "../content/contentManager";
import {
contentPackIsUsable,
resolveContentUrl,
} from "../content/contentManager";
export type TextureTriplet = readonly [string, string, string];
export type TextureRegion = readonly [number, number, number, number];
@@ -73,6 +76,8 @@ export interface AvatarManifestEntry {
featureOptions: readonly AvatarFeatureOption[];
attachmentPoints?: Readonly<Record<string, AvatarAttachmentPoint>>;
rendererStrategy?: RunewakerModularRendererStrategy;
/** Runtime ownership; generated manifests may omit it. */
contentPackId?: string;
}
export interface AvatarManifest {
@@ -97,12 +102,24 @@ export interface AvatarManifest {
export type CharacterManifest = AvatarManifest;
export const AVATAR_MANIFEST_URL = "/assets/characters/wow335a-v1/manifest.json";
export const AVATAR_MANIFEST_URLS = [
AVATAR_MANIFEST_URL,
"/assets/characters/ascension-v1/manifest.json",
"/assets/characters/runewaker-v1/manifest.json",
export const BASIC_CHARACTER_CONTENT_PACK_ID = "core:character-basic";
export const WOW_CHARACTER_CONTENT_PACK_ID = "characters:wow335a-v1";
export const AVATAR_MANIFEST_SOURCES = [
{
packIds: [BASIC_CHARACTER_CONTENT_PACK_ID, WOW_CHARACTER_CONTENT_PACK_ID],
url: AVATAR_MANIFEST_URL,
},
{
packIds: ["characters:ascension-v1"],
url: "/assets/characters/ascension-v1/manifest.json",
},
{
packIds: ["characters:runewaker-v1"],
url: "/assets/characters/runewaker-v1/manifest.json",
},
] as const;
let manifestPromise: Promise<AvatarManifest> | null = null;
export const AVATAR_MANIFEST_URLS = AVATAR_MANIFEST_SOURCES.map((source) => source.url);
let manifestRequest: { readonly key: string; readonly promise: Promise<AvatarManifest> } | null = null;
interface AvatarManifestResponse {
readonly ok: boolean;
@@ -114,12 +131,37 @@ export function avatarManifestKey(raceId: RaceId, gender: GenderId): string {
return `${raceId}:${gender}`;
}
function mergeAvatarManifests(manifests: readonly AvatarManifest[]): AvatarManifest {
const [baseManifest, ...extensions] = manifests;
export function availableAvatarManifestUrls(
isUsable: (packId: string) => boolean = contentPackIsUsable,
): readonly string[] {
return AVATAR_MANIFEST_SOURCES
.filter((source) => source.packIds.some((packId) => isUsable(packId)))
.map((source) => source.url);
}
function contentPackForAvatarEntry(url: string, entry: AvatarManifestEntry): string {
if (url === AVATAR_MANIFEST_URL) {
return entry.raceId === "human"
? BASIC_CHARACTER_CONTENT_PACK_ID
: WOW_CHARACTER_CONTENT_PACK_ID;
}
return AVATAR_MANIFEST_SOURCES.find((source) => source.url === url)?.packIds[0]
?? WOW_CHARACTER_CONTENT_PACK_ID;
}
function mergeAvatarManifests(
sources: readonly { readonly url: string; readonly manifest: AvatarManifest }[],
): AvatarManifest {
const [{ manifest: baseManifest }, ...extensions] = sources;
return {
...baseManifest,
sourceBuild: manifests.map((manifest) => manifest.sourceBuild).join(" + "),
entries: Object.assign({}, baseManifest.entries, ...extensions.map((manifest) => manifest.entries)),
sourceBuild: sources.map(({ manifest }) => manifest.sourceBuild).join(" + "),
entries: Object.assign({}, ...sources.map(({ manifest, url }) => Object.fromEntries(
Object.entries(manifest.entries).map(([key, entry]) => [
key,
{ ...entry, contentPackId: contentPackForAvatarEntry(url, entry) },
]),
))),
};
}
@@ -138,10 +180,10 @@ export async function loadAvatarManifestSources(
if (manifest.version !== 1 || !manifest.entries) {
throw new Error(`Avatar manifest is malformed: ${url}`);
}
return manifest;
return { url, manifest };
}));
const manifests = results.flatMap((result) => result.status === "fulfilled" ? [result.value] : []);
if (!manifests.length) {
const sources = results.flatMap((result) => result.status === "fulfilled" ? [result.value] : []);
if (!sources.length) {
const firstFailure = results.find((result) => result.status === "rejected");
throw firstFailure?.status === "rejected"
? firstFailure.reason
@@ -152,27 +194,73 @@ export async function loadAvatarManifestSources(
console.warn(`Optional playable-character pack could not be loaded: ${urls[index]}`, result.reason);
}
});
return mergeAvatarManifests(manifests);
return mergeAvatarManifests(sources);
}
export async function loadAvatarManifest(): Promise<AvatarManifest> {
if (!manifestPromise) {
manifestPromise = loadAvatarManifestSources(
AVATAR_MANIFEST_URLS,
const urls = availableAvatarManifestUrls();
if (!urls.length) throw new Error("No optional playable-character packs are installed");
const key = urls.join("|");
if (manifestRequest?.key !== key) {
const promise = loadAvatarManifestSources(
urls,
(url) => fetch(resolveContentUrl(url), { cache: "force-cache" }),
)
.catch((error) => {
manifestPromise = null;
if (manifestRequest?.promise === promise) manifestRequest = null;
throw error;
});
manifestRequest = { key, promise };
}
return manifestPromise;
return manifestRequest.promise;
}
function firstOrEmpty<T>(values: readonly T[]): readonly T[] {
return values.length ? [values[0]] : [];
}
function basicHumanEntry(entry: AvatarManifestEntry): AvatarManifestEntry {
const skin = entry.skinOptions?.[0];
const face = entry.faceOptions?.[0];
const hair = entry.hairOptions?.[0];
const feature = entry.featureOptions?.[0];
return {
...entry,
contentPackId: BASIC_CHARACTER_CONTENT_PACK_ID,
counts: { skinColor: 1, face: 1, hairStyle: 1, hairColor: 1, feature: 1 },
dbcIndices: {
skinColor: firstOrEmpty(entry.dbcIndices?.skinColor ?? []),
face: firstOrEmpty(entry.dbcIndices?.face ?? []),
hairStyle: firstOrEmpty(entry.dbcIndices?.hairStyle ?? []),
hairColor: firstOrEmpty(entry.dbcIndices?.hairColor ?? []),
feature: firstOrEmpty(entry.dbcIndices?.feature ?? []),
},
skinOptions: skin ? [skin] : [],
faceOptions: face ? [{ ...face, bySkin: firstOrEmpty(face.bySkin) }] : [],
hairOptions: hair ? [{ ...hair, byColor: firstOrEmpty(hair.byColor) }] : [],
featureOptions: feature ? [{ ...feature, byColor: firstOrEmpty(feature.byColor) }] : [],
};
}
export function getAvatarEntry(
manifest: AvatarManifest,
raceId: RaceId,
gender: GenderId,
isUsable: (packId: string) => boolean = contentPackIsUsable,
): AvatarManifestEntry | null {
return manifest.entries[avatarManifestKey(raceId, gender)] ?? null;
const requested = manifest.entries[avatarManifestKey(raceId, gender)] ?? null;
if (requested) {
const requestedPackUsable = !requested.contentPackId || isUsable(requested.contentPackId);
if (requested.raceId === "human") {
if (requestedPackUsable && isUsable(WOW_CHARACTER_CONTENT_PACK_ID)) return requested;
} else if (requestedPackUsable) {
return requested;
}
}
const basicHuman = manifest.entries[avatarManifestKey("human", gender)]
?? manifest.entries[avatarManifestKey("human", "male")]
?? null;
return basicHuman && (!basicHuman.contentPackId || isUsable(basicHuman.contentPackId))
? basicHumanEntry(basicHuman)
: null;
}
+24
View File
@@ -0,0 +1,24 @@
import { readFileSync } from "node:fs";
import { describe, expect, it } from "vitest";
import { BROWSER_CONTENT_CACHE_NAME } from "./contentManager";
const workerSource = readFileSync(
new URL("../../public/content-cache-sw.js", import.meta.url),
"utf8",
);
describe("browser content cache worker", () => {
it("serves only cached same-origin asset requests from the active content cache", () => {
expect(workerSource).toContain(`const CONTENT_CACHE_NAME = "${BROWSER_CONTENT_CACHE_NAME}";`);
expect(workerSource).toContain('request.method !== "GET"');
expect(workerSource).toContain("url.origin !== self.location.origin");
expect(workerSource).toContain("MANAGED_ASSET_PREFIXES.some");
expect(workerSource).toContain('"/assets/game/"');
expect(workerSource).toContain("if (cached) return cached");
expect(workerSource).toContain("BUNDLED_ASSET_PATHS.has(url.pathname)");
expect(workerSource).toContain("BUNDLED_ASSET_PREFIXES.some");
expect(workerSource).not.toContain('"/assets/characters/wow335a-v1/textures/character/human/",');
expect(workerSource).toContain('status: 404');
expect(workerSource).not.toContain("cached ?? fetch(request)");
});
});
+150
View File
@@ -0,0 +1,150 @@
import { createHash } from "node:crypto";
import { Capacitor } from "@capacitor/core";
import { afterEach, describe, expect, it, vi } from "vitest";
import {
BROWSER_CONTENT_CACHE_NAME,
checkForContentUpdates,
contentPackHasUpdate,
contentPackIsBundled,
contentPackIsInstalled,
contentPackIsUsable,
getContentManagerSnapshot,
installContentPack,
removeContentPack,
} from "./contentManager";
import type { ContentManifest } from "./contentManifest";
class MemoryCache {
readonly responses = new Map<string, Response>();
async match(request: RequestInfo | URL): Promise<Response | undefined> {
return this.responses.get(this.key(request))?.clone();
}
async put(request: RequestInfo | URL, response: Response): Promise<void> {
this.responses.set(this.key(request), response.clone());
}
async delete(request: RequestInfo | URL): Promise<boolean> {
return this.responses.delete(this.key(request));
}
private key(request: RequestInfo | URL): string {
if (request instanceof Request) return request.url;
return new URL(String(request), "http://localhost").toString();
}
}
class MemoryCacheStorage {
readonly stores = new Map<string, MemoryCache>();
async open(name: string): Promise<Cache> {
const cache = this.stores.get(name) ?? new MemoryCache();
this.stores.set(name, cache);
return cache as unknown as Cache;
}
async delete(name: string): Promise<boolean> {
return this.stores.delete(name);
}
}
function sha256(bytes: Uint8Array): string {
return createHash("sha256").update(bytes).digest("hex");
}
function contentManifest(version: string, objectName: string, bytes: Uint8Array): ContentManifest {
return {
schemaVersion: 1,
version,
createdAt: "2026-09-04T00:00:00.000Z",
minimumAppVersion: "0.1.0",
packs: [{
id: "characters:test",
label: "Test characters",
bytes: bytes.byteLength,
fileCount: 1,
bundledOnAndroid: false,
dependencies: [],
}],
files: {
"/assets/characters/test.glb": {
packId: "characters:test",
url: `/content/objects/${objectName}`,
sha256: sha256(bytes),
bytes: bytes.byteLength,
},
},
};
}
afterEach(() => {
vi.restoreAllMocks();
vi.unstubAllGlobals();
});
describe("browser content packs", () => {
it("downloads, updates, and removes verified optional content", async () => {
const firstBytes = new TextEncoder().encode("first-model");
const updatedBytes = new TextEncoder().encode("updated-model");
let manifest = contentManifest("content-one", "first.glb", firstBytes);
let corruptUpdate = false;
const storage = new MemoryCacheStorage();
vi.spyOn(Capacitor, "isNativePlatform").mockReturnValue(false);
vi.stubGlobal("caches", storage as unknown as CacheStorage);
vi.stubGlobal("fetch", vi.fn(async (input: RequestInfo | URL) => {
const url = String(input);
if (url.endsWith("/content/manifest.json")) return Response.json(manifest);
if (url.endsWith("/first.glb")) return new Response(firstBytes);
if (url.endsWith("/updated.glb")) {
return new Response(corruptUpdate ? new Uint8Array(updatedBytes.byteLength).fill(7) : updatedBytes);
}
return new Response(null, { status: 404 });
}));
await checkForContentUpdates();
expect(contentPackIsInstalled("characters:test")).toBe(false);
expect(contentPackIsUsable("characters:test")).toBe(false);
expect(contentPackHasUpdate("characters:test")).toBe(false);
await installContentPack("characters:test");
expect(contentPackIsInstalled("characters:test")).toBe(true);
expect(contentPackIsUsable("characters:test")).toBe(true);
expect(contentPackHasUpdate("characters:test")).toBe(false);
expect(getContentManagerSnapshot()).toMatchObject({
installingPackId: null,
downloadedBytes: firstBytes.byteLength,
totalBytes: firstBytes.byteLength,
error: "",
});
const active = storage.stores.get(BROWSER_CONTENT_CACHE_NAME);
expect(await (await active?.match("http://localhost/assets/characters/test.glb"))?.text()).toBe("first-model");
manifest = contentManifest("content-two", "updated.glb", updatedBytes);
await checkForContentUpdates();
expect(contentPackHasUpdate("characters:test")).toBe(true);
corruptUpdate = true;
await expect(installContentPack("characters:test")).rejects.toThrow(/verification/i);
expect(await (await active?.match("http://localhost/assets/characters/test.glb"))?.text()).toBe("first-model");
corruptUpdate = false;
await installContentPack("characters:test");
expect(contentPackHasUpdate("characters:test")).toBe(false);
expect(await (await active?.match("http://localhost/assets/characters/test.glb"))?.text()).toBe("updated-model");
await removeContentPack("characters:test");
expect(contentPackIsInstalled("characters:test")).toBe(false);
expect(contentPackIsUsable("characters:test")).toBe(false);
expect(await active?.match("http://localhost/assets/characters/test.glb")).toBeUndefined();
});
it("keeps default browser content installed and non-removable", async () => {
vi.spyOn(Capacitor, "isNativePlatform").mockReturnValue(false);
expect(contentPackIsBundled("core:ui")).toBe(true);
expect(contentPackIsUsable("core:ui")).toBe(true);
await expect(removeContentPack("core:ui")).resolves.toBeUndefined();
expect(contentPackIsUsable("core:ui")).toBe(true);
});
});
+194 -24
View File
@@ -11,7 +11,23 @@ import { NativeContentStorage } from "./nativeContentStorage";
const CONTENT_STATE_KEY = "healer-man.content-state.v1";
const PRODUCTION_ORIGIN = "https://iwanttoheal.phenomrom.com";
const STATIC_ANDROID_BUNDLED_PACKS = new Set(["core:ui", "core:equipment", "dungeon:wailing-caverns"]);
const STATIC_ANDROID_BUNDLED_PACKS = new Set([
"core:ui",
"core:equipment",
"core:character-basic",
"core:runtime",
"dungeon:wailing-caverns",
]);
const STATIC_BROWSER_BUNDLED_PACKS = new Set([
"core:ui",
"core:equipment",
"core:character-basic",
"core:runtime",
"dungeon:wailing-caverns",
]);
export const BROWSER_CONTENT_CACHE_NAME = "healer-man-content-v1";
export const BROWSER_CONTENT_WORKER_URL = "/content-cache-sw.js";
export interface InstalledContentFile {
readonly packId: string;
@@ -29,6 +45,7 @@ interface PersistedContentState {
export interface ContentManagerSnapshot {
readonly revision: number;
readonly installedRevision: number;
readonly checking: boolean;
readonly manifest: ContentManifest | null;
readonly bundledManifest: ContentManifest | null;
@@ -42,6 +59,7 @@ const EMPTY_STATE: PersistedContentState = { version: 1, packVersions: {}, files
let installedState = readInstalledState();
let snapshot: ContentManagerSnapshot = {
revision: 0,
installedRevision: 0,
checking: false,
manifest: null,
bundledManifest: null,
@@ -87,13 +105,45 @@ function absoluteContentUrl(value: string): string {
return new URL(value, `${contentOrigin() || "http://localhost"}/`).toString();
}
function browserCacheRequest(logicalPath: string): Request {
const appOrigin = typeof window === "undefined" ? "http://localhost" : window.location.origin;
return new Request(new URL(logicalPath, `${appOrigin}/`).toString(), { method: "GET" });
}
function browserCacheStorage(): CacheStorage | null {
return typeof caches === "undefined" ? null : caches;
}
async function registerBrowserContentWorker(): Promise<void> {
if (Capacitor.isNativePlatform()
|| import.meta.env.DEV
|| typeof navigator === "undefined"
|| !("serviceWorker" in navigator)) return;
try {
await navigator.serviceWorker.register(BROWSER_CONTENT_WORKER_URL, {
scope: "/",
updateViaCache: "none",
});
await navigator.serviceWorker.ready;
} catch (error) {
console.warn("Browser content cache worker could not be registered.", error);
}
}
function extensionForEntry(entry: ContentManifestFile): string {
const match = /(?:\.[A-Za-z0-9]+)(?:\?.*)?$/.exec(entry.url);
return match?.[0].split("?")[0].toLowerCase() ?? ".bin";
}
function bundledFiles(): Readonly<Record<string, { readonly sha256: string }>> {
return snapshot.bundledManifest?.files ?? {};
if (snapshot.bundledManifest) return snapshot.bundledManifest.files;
if (!snapshot.manifest) return {};
const bundledPacks = Capacitor.isNativePlatform()
? STATIC_ANDROID_BUNDLED_PACKS
: STATIC_BROWSER_BUNDLED_PACKS;
return Object.fromEntries(Object.entries(snapshot.manifest.files).filter(([, entry]) => (
bundledPacks.has(entry.packId)
)));
}
function activeFileIdentities(): Readonly<Record<string, { readonly sha256: string }>> {
@@ -117,21 +167,31 @@ export function resolveContentUrl(logicalUrl: string): string {
return installed?.fileUri ? Capacitor.convertFileSrc(installed.fileUri) : logicalUrl;
}
export function contentPackIsBundled(packId: string): boolean {
if (!Capacitor.isNativePlatform()) return STATIC_BROWSER_BUNDLED_PACKS.has(packId);
return STATIC_ANDROID_BUNDLED_PACKS.has(packId)
|| Boolean(snapshot.bundledManifest?.packs.some((pack) => pack.id === packId));
}
export function contentPackIsInstalled(packId: string): boolean {
return contentPackIsBundled(packId) || Boolean(installedState.packVersions[packId]);
}
export function contentPackIsUsable(packId: string): boolean {
if (!Capacitor.isNativePlatform()) return true;
const installed = STATIC_ANDROID_BUNDLED_PACKS.has(packId)
|| snapshot.bundledManifest?.packs.some((pack) => pack.id === packId)
|| Boolean(installedState.packVersions[packId]);
if (!installed) return false;
if (contentPackIsBundled(packId)) return true;
if (!installedState.packVersions[packId]) return false;
const pack = snapshot.manifest?.packs.find((candidate) => candidate.id === packId);
return pack?.dependencies.every((dependency) => contentPackIsUsable(dependency)) ?? true;
}
export function contentPackHasUpdate(packId: string): boolean {
if (!snapshot.manifest) return false;
if (!snapshot.manifest || !contentPackIsInstalled(packId)) return false;
const pack = snapshot.manifest.packs.find((candidate) => candidate.id === packId);
return changedFilesForPack(snapshot.manifest, packId, activeFileIdentities()).length > 0
|| Boolean(pack?.dependencies.some((dependency) => contentPackHasUpdate(dependency)));
|| Boolean(!contentPackIsBundled(packId)
&& pack?.dependencies.some((dependency) => (
!contentPackIsUsable(dependency) || contentPackHasUpdate(dependency)
)));
}
export async function checkForContentUpdates(): Promise<ContentManifest | null> {
@@ -149,7 +209,7 @@ export async function checkForContentUpdates(): Promise<ContentManifest | null>
}
async function loadBundledManifest(): Promise<void> {
if (!Capacitor.isNativePlatform()) return;
if (!Capacitor.isNativePlatform() && import.meta.env.VITE_CONTENT_MODE !== "downloadable") return;
try {
const response = await fetch("/content/bootstrap-manifest.json", { cache: "force-cache" });
if (!response.ok) return;
@@ -162,13 +222,102 @@ async function loadBundledManifest(): Promise<void> {
export function initializeContentSystem(): Promise<void> {
if (!initialization) {
initialization = (async () => {
await loadBundledManifest();
await Promise.all([loadBundledManifest(), registerBrowserContentWorker()]);
if (typeof navigator === "undefined" || navigator.onLine !== false) void checkForContentUpdates();
})();
}
return initialization;
}
async function sha256Bytes(bytes: ArrayBuffer): Promise<string> {
if (!globalThis.crypto?.subtle) {
throw new Error("This browser cannot verify downloaded content.");
}
const digest = await globalThis.crypto.subtle.digest("SHA-256", bytes);
return [...new Uint8Array(digest)].map((value) => value.toString(16).padStart(2, "0")).join("");
}
async function installBrowserContentPack(
packId: string,
manifest: ContentManifest,
changes: ReturnType<typeof changedFilesForPack>,
): Promise<void> {
const storage = browserCacheStorage();
if (!storage) throw new Error("Offline content storage is unavailable in this browser.");
const totalBytes = changes.reduce((total, [, entry]) => total + entry.bytes, 0);
const stagingName = `${BROWSER_CONTENT_CACHE_NAME}-staging-${encodeURIComponent(packId)}`;
await storage.delete(stagingName);
const staging = await storage.open(stagingName);
publish({ installingPackId: packId, downloadedBytes: 0, totalBytes, error: "" });
let completedBytes = 0;
try {
for (const [logicalPath, entry] of changes) {
const response = await fetch(absoluteContentUrl(entry.url), { cache: "no-store" });
if (!response.ok) throw new Error(`Content request failed (${response.status}): ${logicalPath}`);
const bytes = await response.arrayBuffer();
if (bytes.byteLength !== entry.bytes) {
throw new Error(`Downloaded content has the wrong size: ${logicalPath}`);
}
if (await sha256Bytes(bytes) !== entry.sha256) {
throw new Error(`Downloaded content failed verification: ${logicalPath}`);
}
const headers = new Headers();
const contentType = response.headers.get("content-type");
if (contentType) headers.set("content-type", contentType);
await staging.put(browserCacheRequest(logicalPath), new Response(bytes, {
headers,
status: 200,
}));
completedBytes += entry.bytes;
publish({ downloadedBytes: completedBytes });
}
const active = await storage.open(BROWSER_CONTENT_CACHE_NAME);
for (const [logicalPath] of changes) {
const cached = await staging.match(browserCacheRequest(logicalPath));
if (!cached) throw new Error(`Verified content was not staged: ${logicalPath}`);
await active.put(browserCacheRequest(logicalPath), cached);
}
const pendingFiles = { ...installedState.files };
const latestPaths = new Set(filesForContentPack(manifest, packId).map(([logicalPath]) => logicalPath));
for (const [logicalPath, installed] of Object.entries(pendingFiles)) {
if (installed.packId !== packId || latestPaths.has(logicalPath)) continue;
await active.delete(browserCacheRequest(logicalPath));
delete pendingFiles[logicalPath];
}
for (const [logicalPath, entry] of filesForContentPack(manifest, packId)) {
pendingFiles[logicalPath] = {
packId,
sha256: entry.sha256,
bytes: entry.bytes,
extension: extensionForEntry(entry),
fileUri: absoluteContentUrl(logicalPath),
};
}
installedState = {
version: 1,
files: pendingFiles,
packVersions: { ...installedState.packVersions, [packId]: manifest.version },
};
persistInstalledState();
publish({
installingPackId: null,
downloadedBytes: totalBytes,
totalBytes,
error: "",
installedRevision: snapshot.installedRevision + 1,
});
} catch (error) {
publish({ installingPackId: null, error: error instanceof Error ? error.message : "Content download failed." });
throw error;
} finally {
await storage.delete(stagingName).catch(() => false);
}
}
export async function installContentPack(packId: string): Promise<void> {
const manifest = snapshot.manifest ?? await checkForContentUpdates();
if (!manifest) throw new Error(snapshot.error || "Content server is unavailable.");
@@ -177,15 +326,20 @@ export async function installContentPack(packId: string): Promise<void> {
}
const pack = manifest.packs.find((candidate) => candidate.id === packId);
if (!pack) throw new Error(`Unknown content pack: ${packId}`);
if (!Capacitor.isNativePlatform()) return;
for (const dependency of pack.dependencies) {
if (!contentPackIsUsable(dependency) || contentPackHasUpdate(dependency)) {
await installContentPack(dependency);
if (!contentPackIsBundled(packId)) {
for (const dependency of pack.dependencies) {
if (!contentPackIsUsable(dependency) || contentPackHasUpdate(dependency)) {
await installContentPack(dependency);
}
}
}
const changes = changedFilesForPack(manifest, packId, activeFileIdentities());
if (!Capacitor.isNativePlatform()) {
await installBrowserContentPack(packId, manifest, changes);
return;
}
const totalBytes = changes.reduce((total, [, entry]) => total + entry.bytes, 0);
const storage = await NativeContentStorage.getFreeBytes();
const safetyReserve = 128 * 1024 * 1024;
@@ -232,7 +386,13 @@ export async function installContentPack(packId: string): Promise<void> {
packVersions: { ...installedState.packVersions, [packId]: manifest.version },
};
persistInstalledState();
publish({ installingPackId: null, downloadedBytes: totalBytes, totalBytes, error: "" });
publish({
installingPackId: null,
downloadedBytes: totalBytes,
totalBytes,
error: "",
installedRevision: snapshot.installedRevision + 1,
});
} catch (error) {
publish({ installingPackId: null, error: error instanceof Error ? error.message : "Content download failed." });
throw error;
@@ -242,19 +402,29 @@ export async function installContentPack(packId: string): Promise<void> {
}
export async function removeContentPack(packId: string): Promise<void> {
if (!Capacitor.isNativePlatform()) return;
if (contentPackIsBundled(packId)) return;
const remainingFiles = { ...installedState.files };
const targetFiles = Object.entries(remainingFiles).filter(([, entry]) => entry.packId === packId);
for (const [logicalPath, entry] of targetFiles) {
const shared = Object.entries(remainingFiles).some(([otherPath, other]) => (
otherPath !== logicalPath && other.sha256 === entry.sha256 && other.extension === entry.extension && other.packId !== packId
));
if (!shared) await NativeContentStorage.remove({ sha256: entry.sha256, extension: entry.extension });
delete remainingFiles[logicalPath];
if (Capacitor.isNativePlatform()) {
for (const [logicalPath, entry] of targetFiles) {
const shared = Object.entries(remainingFiles).some(([otherPath, other]) => (
otherPath !== logicalPath && other.sha256 === entry.sha256 && other.extension === entry.extension && other.packId !== packId
));
if (!shared) await NativeContentStorage.remove({ sha256: entry.sha256, extension: entry.extension });
delete remainingFiles[logicalPath];
}
} else {
const storage = browserCacheStorage();
if (!storage) throw new Error("Offline content storage is unavailable in this browser.");
const active = await storage.open(BROWSER_CONTENT_CACHE_NAME);
for (const [logicalPath] of targetFiles) {
await active.delete(browserCacheRequest(logicalPath));
delete remainingFiles[logicalPath];
}
}
const packVersions = { ...installedState.packVersions };
delete packVersions[packId];
installedState = { version: 1, files: remainingFiles, packVersions };
persistInstalledState();
publish({ error: "" });
publish({ error: "", installedRevision: snapshot.installedRevision + 1 });
}
+38 -1
View File
@@ -1,10 +1,20 @@
import { afterEach, describe, expect, it, vi } from "vitest";
const combatMedia = vi.hoisted(() => ({ ready: false }));
vi.mock("./combatMediaPack", async (importOriginal) => ({
...(await importOriginal<typeof import("./combatMediaPack")>()),
combatMediaPackReady: () => combatMedia.ready,
}));
import {
DEFAULT_COMBAT_PRESENTATION_SETTINGS,
type CombatPresentationEvent,
} from "./combatPresentation";
import {
combatAudioOutputEnabled,
authenticCombatAudioEnabled,
combatMediaPreloadEnabled,
configureCombatAudio,
masterAudioOutputEnabled,
playCombatAudio,
@@ -28,7 +38,10 @@ const fixtureEvent: CombatPresentationEvent = {
targetPosition: [1, 0, 0],
};
afterEach(() => vi.unstubAllGlobals());
afterEach(() => {
combatMedia.ready = false;
vi.unstubAllGlobals();
});
describe("combat audio output policy", () => {
it("does not create or fetch audio while the master volume is disabled", async () => {
@@ -50,6 +63,30 @@ describe("combat audio output policy", () => {
expect(masterAudioOutputEnabled(0.6)).toBe(true);
expect(combatAudioOutputEnabled(DEFAULT_COMBAT_PRESENTATION_SETTINGS, 0)).toBe(false);
expect(combatAudioOutputEnabled(DEFAULT_COMBAT_PRESENTATION_SETTINGS, 0.6)).toBe(true);
expect(authenticCombatAudioEnabled(DEFAULT_COMBAT_PRESENTATION_SETTINGS)).toBe(false);
combatMedia.ready = true;
expect(authenticCombatAudioEnabled({
...DEFAULT_COMBAT_PRESENTATION_SETTINGS,
originalMediaEnabled: true,
})).toBe(true);
expect(combatMediaPreloadEnabled(DEFAULT_COMBAT_PRESENTATION_SETTINGS, 0.6)).toBe(false);
expect(combatMediaPreloadEnabled({
...DEFAULT_COMBAT_PRESENTATION_SETTINGS,
originalMediaEnabled: true,
}, 0.6)).toBe(true);
});
it("does not fetch packaged sound effects before the player downloads them", async () => {
const AudioContextConstructor = vi.fn();
const fetch = vi.fn();
vi.stubGlobal("window", { AudioContext: AudioContextConstructor });
vi.stubGlobal("fetch", fetch);
configureCombatAudio(DEFAULT_COMBAT_PRESENTATION_SETTINGS, 0.6);
await expect(playMasterAudioCue("/assets/game/combat-effects/audio/fixture.wav")).resolves.toBeNull();
expect(AudioContextConstructor).not.toHaveBeenCalled();
expect(fetch).not.toHaveBeenCalled();
});
it("does not initialize audio for a zero-volume actor group", () => {
+22 -2
View File
@@ -6,6 +6,10 @@ import {
type CombatPresentationSettings,
type CombatPresentationSchool,
} from "./combatPresentation";
import {
combatMediaPackReady,
fetchOptionalMediaAsset,
} from "./combatMediaPack";
const MAX_COMBAT_VOICES = 32;
@@ -37,6 +41,19 @@ export function combatAudioOutputEnabled(
));
}
export function authenticCombatAudioEnabled(settings: CombatPresentationSettings): boolean {
return settings.originalMediaEnabled && combatMediaPackReady();
}
export function combatMediaPreloadEnabled(
settings: CombatPresentationSettings,
masterVolume: number,
): boolean {
return authenticCombatAudioEnabled(settings) && (
settings.vfxQuality !== "off" || combatAudioOutputEnabled(settings, masterVolume)
);
}
function combatAudioGroupOutputEnabled(
settings: CombatPresentationSettings,
masterVolume: number,
@@ -185,6 +202,7 @@ function playSynthesizedCombatCue(event: CombatPresentationEvent): void {
}
function weightedSound(event: CombatPresentationEvent): CombatEffectSoundAsset | null {
if (!configuredSettings || !authenticCombatAudioEnabled(configuredSettings)) return null;
const sounds = combatEffectManifestEntry(event.sourceSpellId, event.source)?.sounds ?? [];
const playable = sounds.filter((sound) => sound.url);
if (!playable.length) return null;
@@ -201,7 +219,7 @@ async function decodeAudio(url: string): Promise<AudioBuffer | null> {
const audioContext = ensureContext();
if (!audioContext) return null;
if (!decodedBuffers.has(url)) {
decodedBuffers.set(url, fetch(url)
decodedBuffers.set(url, fetchOptionalMediaAsset(url)
.then((response) => response.ok ? response.arrayBuffer() : Promise.reject(new Error(String(response.status))))
.then((data) => audioContext.decodeAudioData(data))
.catch(() => null));
@@ -213,7 +231,9 @@ export async function playMasterAudioCue(
url: string,
volume = 1,
): Promise<(() => void) | null> {
if (!masterAudioOutputEnabled(configuredMasterVolume)) return null;
if (!configuredSettings
|| !authenticCombatAudioEnabled(configuredSettings)
|| !masterAudioOutputEnabled(configuredMasterVolume)) return null;
const audioContext = ensureContext();
const buffer = await decodeAudio(url);
if (
+3
View File
@@ -116,6 +116,9 @@ describe("named aura combat runtime", () => {
school: "fire",
interruptible: true,
targetActorId: PLAYER_AGGRO_ID,
sourcePosition: [0, 0, 0],
targetPosition: [1, 0, 0],
sourceYaw: Math.PI / 2,
startedAt: 1_000,
completesAt: 6_000,
},
+77
View File
@@ -0,0 +1,77 @@
import { beforeEach, describe, expect, it, vi } from "vitest";
const content = vi.hoisted(() => ({
usable: false,
install: vi.fn(async () => undefined),
remove: vi.fn(async () => undefined),
listener: null as (() => void) | null,
snapshot: {
downloadedBytes: 4,
totalBytes: 8,
manifest: {
packs: [{ id: "core:combat-media", fileCount: 12 }],
},
},
}));
vi.mock("../content/contentManager", () => ({
contentPackIsUsable: () => content.usable,
getContentManagerSnapshot: () => content.snapshot,
installContentPack: content.install,
removeContentPack: content.remove,
resolveContentUrl: (url: string) => `resolved:${url}`,
subscribeContentManager: (listener: () => void) => {
content.listener = listener;
return () => { content.listener = null; };
},
}));
import {
combatMediaPackReady,
downloadCombatMediaPack,
fetchCombatMediaAsset,
removeCombatMediaPack,
} from "./combatMediaPack";
beforeEach(() => {
content.usable = false;
content.install.mockClear();
content.remove.mockClear();
vi.unstubAllGlobals();
});
describe("optional combat media pack", () => {
it("uses the shared verified content-pack installer only after an explicit request", async () => {
const progress = vi.fn();
await expect(downloadCombatMediaPack(progress)).resolves.toEqual({ fileCount: 12 });
expect(content.install).toHaveBeenCalledWith("core:combat-media");
expect(progress).toHaveBeenLastCalledWith({ completed: 4, total: 8 });
});
it("does not fetch source media while its pack is offloaded", async () => {
const fetch = vi.fn();
vi.stubGlobal("fetch", fetch);
expect(combatMediaPackReady()).toBe(false);
await expect(fetchCombatMediaAsset("/assets/game/combat-effects/audio/shield.wav"))
.rejects.toThrow("not installed");
expect(fetch).not.toHaveBeenCalled();
});
it("resolves installed media through the shared content router and removes the same pack", async () => {
content.usable = true;
const response = new Response(new Uint8Array([1, 2, 3]));
const fetch = vi.fn(async () => response);
vi.stubGlobal("fetch", fetch);
await expect(fetchCombatMediaAsset("/assets/game/combat-effects/audio/shield.wav"))
.resolves.toBe(response);
expect(fetch).toHaveBeenCalledWith(
"resolved:/assets/game/combat-effects/audio/shield.wav",
{ cache: "no-store" },
);
await removeCombatMediaPack();
expect(content.remove).toHaveBeenCalledWith("core:combat-media");
});
});
+70
View File
@@ -0,0 +1,70 @@
import {
contentPackIsUsable,
getContentManagerSnapshot,
installContentPack,
removeContentPack,
resolveContentUrl,
subscribeContentManager,
} from "../content/contentManager";
export const COMBAT_MEDIA_MANIFEST_URL = "/assets/game/combat-effects/manifest.json";
export const COMBAT_MEDIA_CONTENT_PACK_ID = "core:combat-media";
export interface CombatMediaDownloadProgress {
readonly completed: number;
readonly total: number;
}
export interface CombatMediaDownloadResult {
readonly fileCount: number;
}
type ProgressListener = (progress: CombatMediaDownloadProgress) => void;
function isCombatMediaUrl(value: unknown): value is string {
return typeof value === "string" && value.startsWith("/assets/game/combat-effects/");
}
/** Callers must enforce the player's original-media opt-in before fetching. */
export async function fetchCombatMediaAsset(url: string): Promise<Response> {
if (!isCombatMediaUrl(url)) throw new Error(`Unsupported combat media URL: ${url}`);
return fetchOptionalMediaAsset(url);
}
/** Fetch source media only after its shared optional pack has been enabled. */
export async function fetchOptionalMediaAsset(url: string): Promise<Response> {
if (!combatMediaPackReady()) throw new Error("Original spell media is not installed.");
return fetch(resolveContentUrl(url), { cache: "no-store" });
}
/** Download and verify every file owned by the original combat-media pack. */
export async function downloadCombatMediaPack(
onProgress?: ProgressListener,
): Promise<CombatMediaDownloadResult> {
const publishProgress = () => {
const state = getContentManagerSnapshot();
onProgress?.({
completed: state.downloadedBytes,
total: Math.max(1, state.totalBytes),
});
};
const unsubscribe = subscribeContentManager(publishProgress);
try {
await installContentPack(COMBAT_MEDIA_CONTENT_PACK_ID);
publishProgress();
const pack = getContentManagerSnapshot().manifest?.packs.find((candidate) => (
candidate.id === COMBAT_MEDIA_CONTENT_PACK_ID
));
return { fileCount: pack?.fileCount ?? 0 };
} finally {
unsubscribe();
}
}
export async function removeCombatMediaPack(): Promise<void> {
await removeContentPack(COMBAT_MEDIA_CONTENT_PACK_ID);
}
export function combatMediaPackReady(): boolean {
return contentPackIsUsable(COMBAT_MEDIA_CONTENT_PACK_ID);
}
+6
View File
@@ -23,6 +23,7 @@ describe("combat presentation", () => {
audioEnabled: { enemy: false },
audioVolume: { player: 2, party: -1, enemy: 0.35 },
})).toEqual({
originalMediaEnabled: false,
vfxMode: "generated",
vfxQuality: "low",
vfxEnabled: { player: false, party: true, enemy: true },
@@ -31,8 +32,13 @@ describe("combat presentation", () => {
});
expect(normalizeCombatPresentationSettings({
...DEFAULT_COMBAT_PRESENTATION_SETTINGS,
originalMediaEnabled: true,
vfxMode: "original",
}).vfxMode).toBe("original");
expect(normalizeCombatPresentationSettings({
...DEFAULT_COMBAT_PRESENTATION_SETTINGS,
vfxMode: "original",
}).vfxMode).toBe("generated");
expect(normalizeCombatPresentationSettings({
...DEFAULT_COMBAT_PRESENTATION_SETTINGS,
vfxMode: "hybrid",
+22 -3
View File
@@ -1,4 +1,9 @@
import type { AbilityDefinition, AbilityEffect } from "./abilityCatalog";
import {
COMBAT_MEDIA_MANIFEST_URL,
combatMediaPackReady,
fetchCombatMediaAsset,
} from "./combatMediaPack";
export type CombatActorGroup = "player" | "party" | "enemy";
export type CombatVfxMode = "generated" | "original";
@@ -24,6 +29,7 @@ export type CombatPresentationSource = "wow335" | "ascension" | "runewaker" | "f
export type CombatPresentationPosition = readonly [number, number, number];
export interface CombatPresentationSettings {
readonly originalMediaEnabled: boolean;
readonly vfxMode: CombatVfxMode;
readonly vfxQuality: CombatVfxQuality;
readonly vfxEnabled: Readonly<Record<CombatActorGroup, boolean>>;
@@ -34,6 +40,7 @@ export interface CombatPresentationSettings {
const ACTOR_GROUPS: readonly CombatActorGroup[] = ["player", "party", "enemy"];
export const DEFAULT_COMBAT_PRESENTATION_SETTINGS: CombatPresentationSettings = Object.freeze({
originalMediaEnabled: false,
vfxMode: "generated",
vfxQuality: "high",
vfxEnabled: Object.freeze({ player: true, party: true, enemy: true }),
@@ -80,13 +87,20 @@ export function normalizeCombatPresentationSettings(
const record = value && typeof value === "object" && !Array.isArray(value)
? value as Partial<Record<keyof CombatPresentationSettings, unknown>>
: {};
const vfxMode = record.vfxMode === "generated" || record.vfxMode === "original"
const originalMediaEnabled = typeof record.originalMediaEnabled === "boolean"
? record.originalMediaEnabled
: fallback.originalMediaEnabled;
const requestedVfxMode = record.vfxMode === "generated" || record.vfxMode === "original"
? record.vfxMode
: fallback.vfxMode;
const vfxMode = requestedVfxMode === "original" && !originalMediaEnabled
? "generated"
: requestedVfxMode;
const vfxQuality = record.vfxQuality === "off" || record.vfxQuality === "low" || record.vfxQuality === "high"
? record.vfxQuality
: fallback.vfxQuality;
return Object.freeze({
originalMediaEnabled,
vfxMode,
vfxQuality,
vfxEnabled: booleanGroups(record.vfxEnabled, fallback.vfxEnabled),
@@ -191,7 +205,6 @@ interface GeneratedCombatEffectManifest {
readonly entries: readonly CombatEffectManifestEntry[];
}
const COMBAT_EFFECT_MANIFEST_URL = "/assets/game/combat-effects/manifest.json";
const manifestByKey = new Map<string, CombatEffectManifestEntry>();
const manifestBySpellId = new Map<number, CombatEffectManifestEntry>();
let manifestRequest: Promise<void> | null = null;
@@ -207,13 +220,19 @@ function installCombatEffectManifest(manifest: GeneratedCombatEffectManifest): v
export function preloadCombatEffectManifest(): Promise<void> {
if (manifestRequest) return manifestRequest;
if (typeof fetch === "undefined") return Promise.resolve();
manifestRequest = fetch(COMBAT_EFFECT_MANIFEST_URL)
manifestRequest = fetchCombatMediaAsset(COMBAT_MEDIA_MANIFEST_URL)
.then((response) => response.ok ? response.json() : Promise.reject(new Error(String(response.status))))
.then((manifest) => installCombatEffectManifest(manifest as GeneratedCombatEffectManifest))
.catch(() => undefined);
return manifestRequest;
}
export function effectiveCombatVfxMode(settings: CombatPresentationSettings): CombatVfxMode {
return settings.originalMediaEnabled && combatMediaPackReady()
? settings.vfxMode
: "generated";
}
export function combatEffectManifestEntry(
spellId: number | undefined,
source?: CombatPresentationSource,
+40
View File
@@ -83,12 +83,14 @@ import {
} from "./talentModifiers";
import {
getMobPosition,
getMobRuntimeTransform,
mobRuntimeDistanceSquared,
nearbyMobRuntimeIds,
removeMobPosition,
setMobPosition,
type CombatPosition,
} from "./mobRuntimeRegistry";
import { enemyTelegraphContainsPosition } from "./enemyTelegraphGeometry";
import {
getPartyRuntimePosition,
partyRuntimeDistanceSquared,
@@ -299,6 +301,9 @@ export interface EnemyActiveCast {
readonly school: EnemyDamageSchool;
readonly interruptible: boolean;
readonly targetActorId: AggroActorId | null;
readonly sourcePosition: CombatPosition;
readonly targetPosition: CombatPosition;
readonly sourceYaw: number;
readonly startedAt: number;
readonly completesAt: number;
}
@@ -6163,6 +6168,7 @@ export const useCombatStore = create<CombatState>((set, get) => ({
);
if (selectedAttack && startingEnemyCast) {
const attack = selectedAttack;
const sourceTransform = getMobRuntimeTransform(id);
enemyActiveCast = {
attackId: attack.id,
name: attack.name,
@@ -6170,6 +6176,10 @@ export const useCombatStore = create<CombatState>((set, get) => ({
school: attack.school,
interruptible: attack.interruptible !== false,
targetActorId: resolution.targetActorId,
sourcePosition: [...position],
targetPosition: [...targetPosition],
sourceYaw: sourceTransform?.yaw
?? Math.atan2(targetPosition[0] - position[0], targetPosition[2] - position[2]),
startedAt: now,
completesAt: now + (attack.castTimeMs ?? 0),
};
@@ -6187,6 +6197,7 @@ export const useCombatStore = create<CombatState>((set, get) => ({
);
} else if (selectedAttack && !cannotAttack) {
const attack = selectedAttack!;
const resolvedEnemyCast = enemyActiveCast;
enemyActiveCast = null;
const effect = attack.effect ?? {
kind: "damage" as const,
@@ -6198,10 +6209,24 @@ export const useCombatStore = create<CombatState>((set, get) => ({
stableCombatUnit(`${id}:${attack.id}:${attackRevision}:target`) * eligibleActors.length,
)]
: resolution.targetActorId;
const telegraphPlacement = resolvedEnemyCast
? {
sourcePosition: resolvedEnemyCast.sourcePosition,
targetPosition: resolvedEnemyCast.targetPosition,
sourceYaw: resolvedEnemyCast.sourceYaw,
}
: {
sourcePosition: position,
targetPosition,
sourceYaw: Math.atan2(targetPosition[0] - position[0], targetPosition[2] - position[2]),
};
const unvalidatedTargets = attack.target === "nearby-party"
? eligibleActors.filter((actorId) => {
const candidate = actorPosition(runtimeState, actorId);
if (!candidate) return false;
if (attack.telegraph) {
return enemyTelegraphContainsPosition(attack.telegraph, telegraphPlacement, candidate);
}
const radius = attack.radius ?? Math.max(1, attack.range * 0.35);
const targetDx = candidate[0] - position[0];
const targetDy = candidate[1] - position[1];
@@ -6298,6 +6323,21 @@ export const useCombatStore = create<CombatState>((set, get) => ({
targetId: id,
now,
}).auras];
} else if (effect.kind === "target-aura") {
for (const actorId of targets) {
namedAuras = [...applyCombatAura(namedAuras, {
definition: {
id: `enemy-debuff:${id}:${attack.id}:${effect.aura}`,
name: `${attack.name}: ${effect.aura}`,
disposition: "debuff",
durationMs: effect.durationMs > 0 ? effect.durationMs : null,
dispelCategory: "magic",
},
sourceId: id,
targetId: actorId,
now,
}).auras];
}
} else if (effect.kind === "control") {
for (const actorId of targets) {
if (hasControlImmunity(namedAuras, actorId, effect.mechanic)) continue;
+44
View File
@@ -167,6 +167,7 @@ describe("generated dungeon campaign catalog", () => {
"Fear",
"Magic's Bane",
"Corrupt Soul",
"Soulstorm",
]);
expect(attackNames(culling!, "Mal'Ganis")).toEqual([
"Basic Attack",
@@ -204,6 +205,49 @@ describe("generated dungeon campaign catalog", () => {
});
});
it("compiles Archaedas's timed and health-based encounter mechanics", () => {
const uldaman = generated.definitions.find((dungeon) => dungeon.id === "uldaman")!;
const archaedas = Object.values(uldaman.entities).find(
(entity) => entity.name === "Archaedas",
) as unknown as {
readonly combat: {
readonly mechanicIds: readonly string[];
readonly attacks: ReadonlyArray<{
readonly id: string;
readonly initialCooldownMs?: readonly [number, number];
readonly repeatCooldownMs?: readonly [number, number];
readonly condition?: { readonly kind: string; readonly maximum?: number };
readonly effect?: { readonly kind: string; readonly creatureEntry?: number; readonly count?: number };
readonly telegraph?: { readonly shape: string; readonly anchor: string; readonly radius?: number };
}>;
};
};
expect(archaedas.combat.mechanicIds).toEqual([
"archaedas-awaken-earthen-custodian",
"archaedas-ground-tremor",
"archaedas-awaken-earthen-guardians",
"archaedas-awaken-vault-warders",
]);
expect(archaedas.combat.attacks.find((attack) => attack.id === "archaedas-ground-tremor"))
.toMatchObject({
initialCooldownMs: [0, 10_000],
repeatCooldownMs: [14_000, 20_000],
telegraph: { shape: "circle", anchor: "source", radius: 18 },
});
expect(archaedas.combat.attacks.find(
(attack) => attack.id === "archaedas-awaken-earthen-guardians",
)).toMatchObject({
condition: { kind: "self-health-percent", maximum: 70 },
effect: { kind: "summon", creatureEntry: 7076, count: 6 },
});
expect(archaedas.combat.attacks.find(
(attack) => attack.id === "archaedas-awaken-vault-warders",
)).toMatchObject({
condition: { kind: "self-health-percent", maximum: 40 },
effect: { kind: "summon", creatureEntry: 10120, count: 2 },
});
});
it("does not leak outdoor zones, arenas, or raids into the dungeon browser", () => {
const ids = new Set(generated.definitions.map((dungeon) => dungeon.id));
for (const excluded of [
+114
View File
@@ -0,0 +1,114 @@
import { beforeEach, describe, expect, it } from "vitest";
import { encounterTimeline } from "./encounterTimeline";
import { useCombatStore } from "./combatStore";
describe("encounter mechanic timeline", () => {
beforeEach(() => {
useCombatStore.getState().initializeCharacter({ classId: "priest", level: 40 });
useCombatStore.getState().registerMob("boss", {
name: "Timeline Boss",
boss: true,
maxHealth: 1_000,
attacks: [{
id: "timed-quake",
name: "Timed Quake",
delivery: "area",
target: "nearby-party",
school: "nature",
animation: "cast",
range: 20,
radius: 10,
cooldownMs: 15_000,
repeatCooldownMs: [12_000, 18_000],
damageMultiplier: 1,
}, {
id: "health-adds",
name: "Awaken Adds",
delivery: "area",
target: "self",
school: "physical",
animation: "cast",
range: 0,
cooldownMs: 1_000,
maximumCasts: 1,
damageMultiplier: 0,
effect: { kind: "summon", creatureEntry: 1, count: 2 },
condition: { kind: "self-health-percent", minimum: 0, maximum: 70 },
}],
});
useCombatStore.setState((state) => ({
mobs: {
...state.mobs,
boss: {
...state.mobs.boss,
engaged: true,
abilityReadyAt: { "timed-quake": 16_000, "health-adds": 1_000 },
},
},
}));
});
it("shows both timer countdowns and pending health thresholds", () => {
const model = encounterTimeline(useCombatStore.getState().mobs, 10_000);
expect(model?.bossName).toBe("Timeline Boss");
expect(model?.rows.map((row) => [row.name, row.kind, row.value])).toEqual([
["Timed Quake", "timer", "6.0s"],
["Awaken Adds", "health", "70% HP"],
]);
});
it("promotes an active cast and hides a completed one-shot threshold", () => {
useCombatStore.setState((state) => ({
mobs: {
...state.mobs,
boss: {
...state.mobs.boss,
abilityCastCounts: { "health-adds": 1 },
activeCast: {
attackId: "timed-quake",
name: "Timed Quake",
school: "nature",
interruptible: true,
targetActorId: "__player__",
sourcePosition: [0, 0, 0],
targetPosition: [0, 0, 5],
sourceYaw: 0,
startedAt: 10_000,
completesAt: 12_000,
},
},
},
}));
expect(encounterTimeline(useCombatStore.getState().mobs, 11_250)?.rows).toMatchObject([{
name: "Timed Quake",
kind: "casting",
value: "0.8s",
}]);
});
it("keeps every pending health threshold visible beyond the countdown row limit", () => {
const boss = useCombatStore.getState().mobs.boss;
const extraTimers = Array.from({ length: 5 }, (_, index) => ({
...boss.attacks[0],
id: `extra-timer-${index}`,
name: `Extra Timer ${index}`,
}));
useCombatStore.setState((state) => ({
mobs: {
...state.mobs,
boss: {
...state.mobs.boss,
attacks: [...state.mobs.boss.attacks, ...extraTimers],
abilityReadyAt: {
...state.mobs.boss.abilityReadyAt,
...Object.fromEntries(extraTimers.map((attack, index) => [attack.id, 11_000 + index * 1_000])),
},
},
},
}));
const rows = encounterTimeline(useCombatStore.getState().mobs, 10_000, 4)?.rows ?? [];
expect(rows.filter((row) => row.kind === "timer")).toHaveLength(4);
expect(rows.filter((row) => row.kind === "health").map((row) => row.value)).toEqual(["70% HP"]);
});
});
+99
View File
@@ -0,0 +1,99 @@
import type { MobCombatState } from "./combatStore";
import type { EnemyDamageSchool, EnemyTelegraphShape } from "./enemyAttacks";
export type EncounterTimelineKind = "casting" | "timer" | "health";
export interface EncounterTimelineRow {
readonly id: string;
readonly name: string;
readonly kind: EncounterTimelineKind;
readonly value: string;
readonly progress: number;
readonly sortAt: number;
readonly school: EnemyDamageSchool;
readonly telegraphShape?: EnemyTelegraphShape;
}
export interface EncounterTimelineModel {
readonly bossId: string;
readonly bossName: string;
readonly rows: readonly EncounterTimelineRow[];
}
function clampedProgress(value: number): number {
return Math.min(1, Math.max(0, value));
}
export function encounterTimeline(
mobs: Readonly<Record<string, MobCombatState>>,
now: number,
limit = 4,
): EncounterTimelineModel | null {
const boss = Object.values(mobs)
.filter((mob) => mob.boss && mob.engaged && !mob.dead && !mob.despawned)
.sort((left, right) => (
Number(Boolean(right.activeCast)) - Number(Boolean(left.activeCast))
|| left.id.localeCompare(right.id)
))[0];
if (!boss) return null;
const healthPercent = (boss.health / Math.max(1, boss.maxHealth)) * 100;
const rows = boss.attacks.flatMap<EncounterTimelineRow>((attack) => {
if (attack.id === "basic-attack") return [];
const castCount = boss.abilityCastCounts[attack.id] ?? 0;
if (attack.maximumCasts && castCount >= attack.maximumCasts) return [];
const common = {
id: attack.id,
name: attack.name,
school: attack.school,
...(attack.telegraph ? { telegraphShape: attack.telegraph.shape } : {}),
};
if (boss.activeCast?.attackId === attack.id) {
const duration = Math.max(1, boss.activeCast.completesAt - boss.activeCast.startedAt);
const remaining = Math.max(0, boss.activeCast.completesAt - now);
return [{
...common,
kind: "casting",
value: `${(remaining / 1_000).toFixed(1)}s`,
progress: clampedProgress(remaining / duration),
sortAt: -1,
}];
}
if (attack.condition?.kind === "self-health-percent") {
const threshold = attack.condition.maximum;
return [{
...common,
kind: "health",
value: `${Math.round(threshold)}% HP`,
progress: clampedProgress(healthPercent / Math.max(1, threshold)),
sortAt: 1_000_000 + (100 - threshold) * 1_000,
}];
}
const readyAt = boss.abilityReadyAt[attack.id];
if (!Number.isFinite(readyAt)) return [];
const remaining = Math.max(0, (readyAt ?? now) - now);
const cooldown = Math.max(
1,
attack.repeatCooldownMs?.[1] ?? attack.initialCooldownMs?.[1] ?? attack.cooldownMs,
);
return [{
...common,
kind: "timer",
value: `${(remaining / 1_000).toFixed(1)}s`,
progress: clampedProgress(remaining / cooldown),
sortAt: remaining,
}];
}).sort((left, right) => left.sortAt - right.sortAt || left.id.localeCompare(right.id));
const alwaysVisibleHealthRows = rows.filter((row) => row.kind === "health");
const countdownRows = rows
.filter((row) => row.kind !== "health")
.slice(0, Math.max(1, limit));
const visibleRows = [...countdownRows, ...alwaysVisibleHealthRows];
return visibleRows.length ? {
bossId: boss.id,
bossName: boss.name,
rows: visibleRows,
} : null;
}
+56
View File
@@ -0,0 +1,56 @@
import { describe, expect, it } from "vitest";
import { normalizeEnemyAttack, type EnemyAttackDefinition } from "./enemyAttacks";
function attack(overrides: Partial<EnemyAttackDefinition>): EnemyAttackDefinition {
return {
id: "test-special",
name: "Test Special",
delivery: "area",
target: "nearby-party",
school: "physical",
animation: "attack",
range: 12,
cooldownMs: 10_000,
damageMultiplier: 1,
...overrides,
};
}
describe("enemy attack telegraphs", () => {
it("infers a warned cone for legacy frontal mechanics", () => {
const normalized = normalizeEnemyAttack(attack({ name: "Stone Cleave" }));
expect(normalized.telegraph).toMatchObject({
shape: "cone",
anchor: "source",
warningMs: 1_650,
});
expect(normalized.castTimeMs).toBe(1_650);
});
it("preserves authored geometry and extends too-short cast warnings", () => {
const normalized = normalizeEnemyAttack(attack({
name: "Falling Ruin",
castTimeMs: 500,
telegraph: {
shape: "circle",
anchor: "target",
warningMs: 2_200,
radius: 7,
},
}));
expect(normalized.castTimeMs).toBe(2_200);
expect(normalized.telegraph).toEqual({
shape: "circle",
anchor: "target",
warningMs: 2_200,
radius: 7,
});
});
it("does not telegraph basic attacks or beneficial mechanics", () => {
expect(normalizeEnemyAttack(attack({ id: "basic-attack" })).telegraph).toBeUndefined();
expect(normalizeEnemyAttack(attack({
effect: { kind: "heal", multiplier: 1 },
})).telegraph).toBeUndefined();
});
});
+106 -2
View File
@@ -17,6 +17,21 @@ export type EnemyDamageSchool =
export type EnemyControlMechanic = "fear" | "sleep" | "root" | "stun" | "silence";
export type EnemyTelegraphShape = "circle" | "cone" | "line";
export type EnemyTelegraphAnchor = "source" | "target";
/** World-space warning geometry shared by simulation and Three presentation. */
export interface EnemyAttackTelegraph {
readonly shape: EnemyTelegraphShape;
readonly anchor: EnemyTelegraphAnchor;
/** Minimum warning time before the mechanic resolves. */
readonly warningMs: number;
readonly radius?: number;
readonly length?: number;
readonly width?: number;
readonly angleDegrees?: number;
}
export type EnemyAbilityEffect =
| { readonly kind: "damage"; readonly multiplier: number }
| { readonly kind: "heal"; readonly multiplier: number }
@@ -26,6 +41,11 @@ export type EnemyAbilityEffect =
readonly durationMs: number;
readonly magnitude?: number;
}
| {
readonly kind: "target-aura";
readonly aura: string;
readonly durationMs: number;
}
| {
readonly kind: "control";
readonly mechanic: EnemyControlMechanic;
@@ -83,6 +103,7 @@ export interface EnemyAttackDefinition {
readonly maximumCasts?: number;
readonly damageMultiplier: number;
readonly radius?: number;
readonly telegraph?: EnemyAttackTelegraph;
readonly effect?: EnemyAbilityEffect;
readonly condition?: EnemyAbilityCondition;
readonly source?: EnemyAbilitySource;
@@ -96,6 +117,8 @@ export interface EnemyCombatDefinition {
readonly moveSpeed?: number;
readonly aggroRange?: number;
readonly leashRange?: number;
/** Stable special-attack ids available for future encounter loadout randomization. */
readonly mechanicIds?: readonly string[];
readonly attacks: readonly EnemyAttackDefinition[];
}
@@ -125,14 +148,94 @@ function normalizeCooldownRange(
return Object.freeze([Math.round(lower), Math.round(upper)]);
}
function harmfulEnemyAttack(value: EnemyAttackDefinition): boolean {
return !value.effect
|| value.effect.kind === "damage"
|| value.effect.kind === "control"
|| value.effect.kind === "target-aura";
}
/**
* Legacy/imported encounters receive safe defaults while authored mechanics can
* override every dimension in the modular mechanic catalog.
*/
export function inferEnemyAttackTelegraph(
value: EnemyAttackDefinition,
): EnemyAttackTelegraph | undefined {
if (value.id === "basic-attack" || !harmfulEnemyAttack(value)) return undefined;
const radius = value.radius ?? Math.max(3, value.range * 0.35);
if (/\b(?:breath|cleave|frontal|sweep|cone)\b/i.test(value.name)) {
return {
shape: "cone",
anchor: "source",
warningMs: 1_650,
length: Math.max(5.5, value.range),
angleDegrees: /breath/i.test(value.name) ? 70 : 95,
};
}
if (/\b(?:beam|charge|line|lance)\b/i.test(value.name)) {
return {
shape: "line",
anchor: "source",
warningMs: 1_650,
length: Math.max(7, value.range),
width: Math.max(2, radius * 0.45),
};
}
if (value.delivery === "area" || value.radius !== undefined) {
const targetAnchored = /\b(?:blizzard|rain|barrage|volley|eruption|ground|meteor)\b/i
.test(value.name) && value.target !== "self";
return {
shape: "circle",
anchor: targetAnchored ? "target" : "source",
warningMs: 1_650,
radius,
};
}
return undefined;
}
function normalizeTelegraph(value: EnemyAttackTelegraph): EnemyAttackTelegraph {
const warningMs = Math.round(finiteAtLeast(value.warningMs, 1_650, 250));
if (value.shape === "circle") {
return Object.freeze({
shape: value.shape,
anchor: value.anchor,
warningMs,
radius: finiteAtLeast(value.radius, 4, 0.5),
});
}
if (value.shape === "cone") {
return Object.freeze({
shape: value.shape,
anchor: "source",
warningMs,
length: finiteAtLeast(value.length, 8, 1),
angleDegrees: Math.min(180, finiteAtLeast(value.angleDegrees, 90, 5)),
});
}
return Object.freeze({
shape: value.shape,
anchor: "source",
warningMs,
length: finiteAtLeast(value.length, 8, 1),
width: finiteAtLeast(value.width, 3, 0.5),
});
}
export function normalizeEnemyAttack(
value: EnemyAttackDefinition,
fallback: EnemyAttackDefinition = DEFAULT_ENEMY_ATTACK,
): EnemyAttackDefinition {
const cooldownMs = Math.round(finiteAtLeast(value.cooldownMs, fallback.cooldownMs, 1));
const telegraph = value.telegraph ?? inferEnemyAttackTelegraph(value);
const inferredCastTimeMs = value.castTimeMs === undefined && value.animation === "cast"
? (fallback.castTimeMs ?? 1_500)
: (fallback.castTimeMs ?? 0);
: telegraph?.warningMs ?? fallback.castTimeMs ?? 0;
const castTimeMs = Math.max(
Math.round(finiteAtLeast(value.castTimeMs, inferredCastTimeMs, 0)),
telegraph?.warningMs ?? 0,
);
return Object.freeze({
id: value.id.trim() || fallback.id,
name: value.name.trim() || fallback.name,
@@ -143,7 +246,7 @@ export function normalizeEnemyAttack(
target: value.target,
school: value.school,
animation: value.animation,
castTimeMs: Math.round(finiteAtLeast(value.castTimeMs, inferredCastTimeMs, 0)),
castTimeMs,
interruptible: value.interruptible ?? fallback.interruptible ?? true,
range: finiteAtLeast(value.range, fallback.range, 0),
cooldownMs,
@@ -160,6 +263,7 @@ export function normalizeEnemyAttack(
...(value.radius !== undefined
? { radius: finiteAtLeast(value.radius, 0, 0) }
: {}),
...(telegraph ? { telegraph: normalizeTelegraph(telegraph) } : {}),
...(value.effect ? { effect: value.effect } : {}),
...(value.condition ? { condition: value.condition } : {}),
...(value.source ? { source: value.source } : {}),
+37
View File
@@ -1,4 +1,5 @@
import { afterEach, beforeEach, describe, expect, it } from "vitest";
import { PLAYER_AGGRO_ID } from "./aggro";
import {
deterministicEnemyCooldown,
useCombatStore,
@@ -171,6 +172,42 @@ describe("server-backed enemy ability runtime", () => {
expect(summonIds.every((id) => getMobPosition(id) === null)).toBe(true);
});
it("applies a modular target aura as a dispellable player debuff", () => {
useCombatStore.getState().registerMob("runewaker-debuffer", {
name: "RuneWaker Debuffer",
maxHealth: 1_000,
attacks: [{
id: "conductivity",
name: "Conductivity",
delivery: "projectile",
target: "primary",
school: "arcane",
animation: "cast",
castTimeMs: 1_000,
range: 20,
cooldownMs: 10_000,
initialCooldownMs: [0, 0],
repeatCooldownMs: [10_000, 10_000],
damageMultiplier: 0,
effect: { kind: "target-aura", aura: "conductivity", durationMs: 6_000 },
}],
});
setMobPosition("runewaker-debuffer", [2, 0, 0]);
useCombatStore.getState().damageMob("runewaker-debuffer", 1, 1_000);
useCombatStore.getState().advanceMobCombat(1_000, [0, 0, 0]);
useCombatStore.getState().advanceMobCombat(2_000, [0, 0, 0]);
expect(useCombatStore.getState().auras).toContainEqual(expect.objectContaining({
sourceId: "runewaker-debuffer",
targetId: PLAYER_AGGRO_ID,
expiresAt: 8_000,
definition: expect.objectContaining({
disposition: "debuff",
dispelCategory: "magic",
}),
}));
});
it("honors encounter-state conditions before selecting an ability", () => {
useWailingEncounterStore.getState().reset();
useCombatStore.getState().registerMob("event-caster", {
+47
View File
@@ -0,0 +1,47 @@
import { describe, expect, it } from "vitest";
import { enemyTelegraphContainsPosition } from "./enemyTelegraphGeometry";
const placement = {
sourcePosition: [0, 0, 0] as const,
targetPosition: [0, 0, 10] as const,
sourceYaw: 0,
};
describe("enemy telegraph gameplay geometry", () => {
it("matches source and target anchored circles", () => {
expect(enemyTelegraphContainsPosition({
shape: "circle", anchor: "source", warningMs: 1_000, radius: 4,
}, placement, [3, 0, 0])).toBe(true);
expect(enemyTelegraphContainsPosition({
shape: "circle", anchor: "target", warningMs: 1_000, radius: 3,
}, placement, [0, 0, 8])).toBe(true);
expect(enemyTelegraphContainsPosition({
shape: "circle", anchor: "target", warningMs: 1_000, radius: 3,
}, placement, [0, 0, 4])).toBe(false);
});
it("keeps cone hits in front and within the visible angle", () => {
const cone = {
shape: "cone" as const,
anchor: "source" as const,
warningMs: 1_000,
length: 10,
angleDegrees: 90,
};
expect(enemyTelegraphContainsPosition(cone, placement, [2, 0, 7])).toBe(true);
expect(enemyTelegraphContainsPosition(cone, placement, [8, 0, 2])).toBe(false);
expect(enemyTelegraphContainsPosition(cone, placement, [0, 0, -2])).toBe(false);
});
it("uses the visible line width for collision", () => {
const line = {
shape: "line" as const,
anchor: "source" as const,
warningMs: 1_000,
length: 12,
width: 4,
};
expect(enemyTelegraphContainsPosition(line, placement, [1.9, 0, 8])).toBe(true);
expect(enemyTelegraphContainsPosition(line, placement, [2.1, 0, 8])).toBe(false);
});
});
+51
View File
@@ -0,0 +1,51 @@
import type { EnemyAttackTelegraph } from "./enemyAttacks";
import type { CombatPosition } from "./mobRuntimeRegistry";
export interface EnemyTelegraphPlacement {
readonly sourcePosition: CombatPosition;
readonly targetPosition: CombatPosition;
readonly sourceYaw: number;
}
export function enemyTelegraphYaw(placement: EnemyTelegraphPlacement): number {
const dx = placement.targetPosition[0] - placement.sourcePosition[0];
const dz = placement.targetPosition[2] - placement.sourcePosition[2];
return dx * dx + dz * dz > 0.0001 ? Math.atan2(dx, dz) : placement.sourceYaw;
}
export function enemyTelegraphOrigin(
telegraph: EnemyAttackTelegraph,
placement: EnemyTelegraphPlacement,
): CombatPosition {
return telegraph.anchor === "target"
? placement.targetPosition
: placement.sourcePosition;
}
/** Matches the visible warning mesh in the X/Z combat plane. */
export function enemyTelegraphContainsPosition(
telegraph: EnemyAttackTelegraph,
placement: EnemyTelegraphPlacement,
candidate: CombatPosition,
): boolean {
const origin = enemyTelegraphOrigin(telegraph, placement);
const dx = candidate[0] - origin[0];
const dz = candidate[2] - origin[2];
if (telegraph.shape === "circle") {
const radius = telegraph.radius ?? 0;
return dx * dx + dz * dz <= radius * radius;
}
const yaw = enemyTelegraphYaw(placement);
const forwardX = Math.sin(yaw);
const forwardZ = Math.cos(yaw);
const forward = dx * forwardX + dz * forwardZ;
const side = Math.abs(dx * forwardZ - dz * forwardX);
const length = telegraph.length ?? 0;
if (forward < 0 || forward > length) return false;
if (telegraph.shape === "line") return side <= (telegraph.width ?? 0) / 2;
const distance = Math.hypot(dx, dz);
if (distance <= 0.0001) return true;
const halfAngle = ((telegraph.angleDegrees ?? 0) * Math.PI) / 360;
return Math.acos(Math.min(1, Math.max(-1, forward / distance))) <= halfAngle;
}
@@ -16,7 +16,7 @@ export const AETERNAL_CIRCLE_ENTITIES = {
"leashRange": 24,
"attacks": [
{
"id": "rw-aeternal-circle-basic-attack",
"id": "basic-attack",
"name": "Basic Attack",
"delivery": "melee",
"target": "primary",
@@ -56,7 +56,7 @@ export const AETERNAL_CIRCLE_ENTITIES = {
"leashRange": 24,
"attacks": [
{
"id": "rw-aeternal-circle-basic-attack",
"id": "basic-attack",
"name": "Basic Attack",
"delivery": "melee",
"target": "primary",
@@ -96,7 +96,7 @@ export const AETERNAL_CIRCLE_ENTITIES = {
"leashRange": 24,
"attacks": [
{
"id": "rw-aeternal-circle-basic-attack",
"id": "basic-attack",
"name": "Basic Attack",
"delivery": "melee",
"target": "primary",
@@ -156,7 +156,7 @@ export const AETERNAL_CIRCLE_ENTITIES = {
"leashRange": 24,
"attacks": [
{
"id": "rw-aeternal-circle-basic-attack",
"id": "basic-attack",
"name": "Basic Attack",
"delivery": "melee",
"target": "primary",
@@ -196,7 +196,7 @@ export const AETERNAL_CIRCLE_ENTITIES = {
"leashRange": 24,
"attacks": [
{
"id": "rw-aeternal-circle-basic-attack",
"id": "basic-attack",
"name": "Basic Attack",
"delivery": "melee",
"target": "primary",
@@ -256,7 +256,7 @@ export const AETERNAL_CIRCLE_ENTITIES = {
"leashRange": 24,
"attacks": [
{
"id": "rw-aeternal-circle-basic-attack",
"id": "basic-attack",
"name": "Basic Attack",
"delivery": "melee",
"target": "primary",
@@ -336,7 +336,7 @@ export const AETERNAL_CIRCLE_ENTITIES = {
"leashRange": 24,
"attacks": [
{
"id": "rw-aeternal-circle-basic-attack",
"id": "basic-attack",
"name": "Basic Attack",
"delivery": "melee",
"target": "primary",
@@ -416,7 +416,7 @@ export const AETERNAL_CIRCLE_ENTITIES = {
"leashRange": 24,
"attacks": [
{
"id": "rw-aeternal-circle-basic-attack",
"id": "basic-attack",
"name": "Basic Attack",
"delivery": "melee",
"target": "primary",
@@ -496,7 +496,7 @@ export const AETERNAL_CIRCLE_ENTITIES = {
"leashRange": 24,
"attacks": [
{
"id": "rw-aeternal-circle-basic-attack",
"id": "basic-attack",
"name": "Basic Attack",
"delivery": "melee",
"target": "primary",
@@ -556,7 +556,7 @@ export const AETERNAL_CIRCLE_ENTITIES = {
"leashRange": 24,
"attacks": [
{
"id": "rw-aeternal-circle-basic-attack",
"id": "basic-attack",
"name": "Basic Attack",
"delivery": "melee",
"target": "primary",
@@ -596,7 +596,7 @@ export const AETERNAL_CIRCLE_ENTITIES = {
"leashRange": 24,
"attacks": [
{
"id": "rw-aeternal-circle-basic-attack",
"id": "basic-attack",
"name": "Basic Attack",
"delivery": "melee",
"target": "primary",
@@ -656,7 +656,7 @@ export const AETERNAL_CIRCLE_ENTITIES = {
"leashRange": 24,
"attacks": [
{
"id": "rw-aeternal-circle-basic-attack",
"id": "basic-attack",
"name": "Basic Attack",
"delivery": "melee",
"target": "primary",
@@ -696,7 +696,7 @@ export const AETERNAL_CIRCLE_ENTITIES = {
"leashRange": 24,
"attacks": [
{
"id": "rw-aeternal-circle-basic-attack",
"id": "basic-attack",
"name": "Basic Attack",
"delivery": "melee",
"target": "primary",
@@ -756,7 +756,7 @@ export const AETERNAL_CIRCLE_ENTITIES = {
"leashRange": 24,
"attacks": [
{
"id": "rw-aeternal-circle-basic-attack",
"id": "basic-attack",
"name": "Basic Attack",
"delivery": "melee",
"target": "primary",
@@ -836,7 +836,7 @@ export const AETERNAL_CIRCLE_ENTITIES = {
"leashRange": 24,
"attacks": [
{
"id": "rw-aeternal-circle-basic-attack",
"id": "basic-attack",
"name": "Basic Attack",
"delivery": "melee",
"target": "primary",
@@ -876,7 +876,7 @@ export const AETERNAL_CIRCLE_ENTITIES = {
"leashRange": 24,
"attacks": [
{
"id": "rw-aeternal-circle-basic-attack",
"id": "basic-attack",
"name": "Basic Attack",
"delivery": "melee",
"target": "primary",
@@ -2303,6 +2303,6 @@ export const AETERNAL_CIRCLE_POPULATION_PROVENANCE = {
"runeWakerIdsAreNamespaced": true,
"serverEntryPolicy": "RuneWaker numeric ids are never passed to AzerothCore serverEntry.",
"sourceSpellCatalog": "scripts/runewaker-pipeline/snapshots/runewaker-npc-spell-catalog.json",
"sourceSpellCatalogSha256": "ad29efc1d4906fdd4f2ac7a69316cac32d8fcbaea674678f050265c86e0c506c",
"sourceSpellCatalogSha256": "c47e67013e68ecf9c7ae30ef4b2058a850505385739dfe8c32ce8874e6ed077c",
"sourceSpellPolicy": "Only positively classified direct-HP-damage families execute; coefficients remain conservative HealerMan values."
} as const;
File diff suppressed because it is too large. Load diff
@@ -16,7 +16,7 @@ export const BARREN_CAVES_ENTITIES = {
"leashRange": 24,
"attacks": [
{
"id": "rw-barren-caves-basic-attack",
"id": "basic-attack",
"name": "Basic Attack",
"delivery": "melee",
"target": "primary",
@@ -76,7 +76,7 @@ export const BARREN_CAVES_ENTITIES = {
"leashRange": 24,
"attacks": [
{
"id": "rw-barren-caves-basic-attack",
"id": "basic-attack",
"name": "Basic Attack",
"delivery": "melee",
"target": "primary",
@@ -136,7 +136,7 @@ export const BARREN_CAVES_ENTITIES = {
"leashRange": 24,
"attacks": [
{
"id": "rw-barren-caves-basic-attack",
"id": "basic-attack",
"name": "Basic Attack",
"delivery": "melee",
"target": "primary",
@@ -176,7 +176,7 @@ export const BARREN_CAVES_ENTITIES = {
"leashRange": 24,
"attacks": [
{
"id": "rw-barren-caves-basic-attack",
"id": "basic-attack",
"name": "Basic Attack",
"delivery": "melee",
"target": "primary",
@@ -216,7 +216,7 @@ export const BARREN_CAVES_ENTITIES = {
"leashRange": 24,
"attacks": [
{
"id": "rw-barren-caves-basic-attack",
"id": "basic-attack",
"name": "Basic Attack",
"delivery": "melee",
"target": "primary",
@@ -296,7 +296,7 @@ export const BARREN_CAVES_ENTITIES = {
"leashRange": 24,
"attacks": [
{
"id": "rw-barren-caves-basic-attack",
"id": "basic-attack",
"name": "Basic Attack",
"delivery": "melee",
"target": "primary",
@@ -355,9 +355,14 @@ export const BARREN_CAVES_ENTITIES = {
"damageMultiplier": 1.3,
"moveSpeed": 2.25,
"leashRange": 32,
"mechanicIds": [
"rw-barren-caves-101462-spell-492292",
"rw-barren-caves-101462-spell-492293",
"rw-barren-caves-101462-control-492294"
],
"attacks": [
{
"id": "rw-barren-caves-basic-attack",
"id": "basic-attack",
"name": "Basic Attack",
"delivery": "melee",
"target": "primary",
@@ -368,13 +373,14 @@ export const BARREN_CAVES_ENTITIES = {
"damageMultiplier": 1
},
{
"id": "rw-barren-caves-spell-492292",
"id": "rw-barren-caves-101462-spell-492292",
"name": "Trample",
"spellId": 492292,
"delivery": "projectile",
"target": "primary",
"delivery": "area",
"target": "nearby-party",
"school": "physical",
"animation": "cast",
"castTimeMs": 1800,
"range": 10,
"cooldownMs": 20000,
"initialCooldownMs": [
@@ -385,16 +391,24 @@ export const BARREN_CAVES_ENTITIES = {
20000,
20000
],
"damageMultiplier": 0.85
"damageMultiplier": 0.85,
"radius": 5,
"telegraph": {
"shape": "circle",
"anchor": "source",
"warningMs": 1800,
"radius": 5
}
},
{
"id": "rw-barren-caves-spell-492293",
"id": "rw-barren-caves-101462-spell-492293",
"name": "Bash",
"spellId": 492293,
"delivery": "melee",
"target": "primary",
"delivery": "area",
"target": "nearby-party",
"school": "physical",
"animation": "attack",
"castTimeMs": 1800,
"range": 5,
"cooldownMs": 20000,
"initialCooldownMs": [
@@ -405,7 +419,46 @@ export const BARREN_CAVES_ENTITIES = {
20000,
20000
],
"damageMultiplier": 0.85
"damageMultiplier": 0.85,
"radius": 5,
"telegraph": {
"shape": "circle",
"anchor": "source",
"warningMs": 1800,
"radius": 5
}
},
{
"id": "rw-barren-caves-101462-control-492294",
"name": "Gulp",
"delivery": "area",
"target": "nearby-party",
"school": "shadow",
"animation": "cast",
"castTimeMs": 1800,
"range": 15,
"cooldownMs": 20000,
"initialCooldownMs": [
20000,
20000
],
"repeatCooldownMs": [
20000,
20000
],
"damageMultiplier": 0,
"radius": 5,
"telegraph": {
"shape": "circle",
"anchor": "source",
"warningMs": 1800,
"radius": 5
},
"effect": {
"kind": "control",
"mechanic": "stun",
"durationMs": 3000
}
}
]
},
@@ -1009,6 +1062,6 @@ export const BARREN_CAVES_POPULATION_PROVENANCE = {
"runeWakerIdsAreNamespaced": true,
"serverEntryPolicy": "RuneWaker numeric ids are never passed to AzerothCore serverEntry.",
"sourceSpellCatalog": "scripts/runewaker-pipeline/snapshots/runewaker-npc-spell-catalog.json",
"sourceSpellCatalogSha256": "ad29efc1d4906fdd4f2ac7a69316cac32d8fcbaea674678f050265c86e0c506c",
"sourceSpellCatalogSha256": "c47e67013e68ecf9c7ae30ef4b2058a850505385739dfe8c32ce8874e6ed077c",
"sourceSpellPolicy": "Only positively classified direct-HP-damage families execute; coefficients remain conservative HealerMan values."
} as const;
@@ -16,7 +16,7 @@ export const CAVE_WATER_DRAGON_ENTITIES = {
"leashRange": 24,
"attacks": [
{
"id": "rw-cave-water-dragon-basic-attack",
"id": "basic-attack",
"name": "Basic Attack",
"delivery": "melee",
"target": "primary",
@@ -56,7 +56,7 @@ export const CAVE_WATER_DRAGON_ENTITIES = {
"leashRange": 24,
"attacks": [
{
"id": "rw-cave-water-dragon-basic-attack",
"id": "basic-attack",
"name": "Basic Attack",
"delivery": "melee",
"target": "primary",
@@ -116,7 +116,7 @@ export const CAVE_WATER_DRAGON_ENTITIES = {
"leashRange": 24,
"attacks": [
{
"id": "rw-cave-water-dragon-basic-attack",
"id": "basic-attack",
"name": "Basic Attack",
"delivery": "melee",
"target": "primary",
@@ -176,7 +176,7 @@ export const CAVE_WATER_DRAGON_ENTITIES = {
"leashRange": 24,
"attacks": [
{
"id": "rw-cave-water-dragon-basic-attack",
"id": "basic-attack",
"name": "Basic Attack",
"delivery": "melee",
"target": "primary",
@@ -216,7 +216,7 @@ export const CAVE_WATER_DRAGON_ENTITIES = {
"leashRange": 24,
"attacks": [
{
"id": "rw-cave-water-dragon-basic-attack",
"id": "basic-attack",
"name": "Basic Attack",
"delivery": "melee",
"target": "primary",
@@ -255,9 +255,18 @@ export const CAVE_WATER_DRAGON_ENTITIES = {
"damageMultiplier": 1.3,
"moveSpeed": 2.25,
"leashRange": 32,
"mechanicIds": [
"rw-cave-water-dragon-102623-spell-494249",
"rw-cave-water-dragon-102623-spell-494250",
"rw-cave-water-dragon-102623-spell-494253",
"rw-cave-water-dragon-102623-spell-494254",
"rw-cave-water-dragon-102623-debuff-494297",
"rw-cave-water-dragon-102623-debuff-494298",
"rw-cave-water-dragon-102623-phase-45"
],
"attacks": [
{
"id": "rw-cave-water-dragon-basic-attack",
"id": "basic-attack",
"name": "Basic Attack",
"delivery": "melee",
"target": "primary",
@@ -266,6 +275,193 @@ export const CAVE_WATER_DRAGON_ENTITIES = {
"range": 4.5,
"cooldownMs": 1900,
"damageMultiplier": 1
},
{
"id": "rw-cave-water-dragon-102623-spell-494249",
"name": "Manastorms Duo Lv5",
"spellId": 494249,
"delivery": "projectile",
"target": "primary",
"school": "arcane",
"animation": "cast",
"castTimeMs": 0,
"range": 30,
"cooldownMs": 6500,
"initialCooldownMs": [
6500,
6500
],
"repeatCooldownMs": [
6500,
6500
],
"damageMultiplier": 0.85
},
{
"id": "rw-cave-water-dragon-102623-spell-494250",
"name": "Manastorms Duo Lv5",
"spellId": 494250,
"delivery": "projectile",
"target": "primary",
"school": "arcane",
"animation": "cast",
"castTimeMs": 0,
"range": 30,
"cooldownMs": 6500,
"initialCooldownMs": [
6500,
6500
],
"repeatCooldownMs": [
6500,
6500
],
"damageMultiplier": 0.85
},
{
"id": "rw-cave-water-dragon-102623-spell-494253",
"name": "Water Dragon Bomb",
"spellId": 494253,
"delivery": "projectile",
"target": "primary",
"school": "frost",
"animation": "cast",
"castTimeMs": 2000,
"range": 30,
"cooldownMs": 6500,
"initialCooldownMs": [
6500,
6500
],
"repeatCooldownMs": [
6500,
6500
],
"damageMultiplier": 0.85
},
{
"id": "rw-cave-water-dragon-102623-spell-494254",
"name": "Dragon Attack",
"spellId": 494254,
"delivery": "area",
"target": "nearby-party",
"school": "physical",
"animation": "cast",
"castTimeMs": 1800,
"range": 15,
"cooldownMs": 6500,
"initialCooldownMs": [
6500,
6500
],
"repeatCooldownMs": [
6500,
6500
],
"damageMultiplier": 0.85,
"radius": 10,
"telegraph": {
"shape": "circle",
"anchor": "source",
"warningMs": 1800,
"radius": 10
}
},
{
"id": "rw-cave-water-dragon-102623-debuff-494297",
"name": "Law of the Dragons",
"delivery": "area",
"target": "nearby-party",
"school": "arcane",
"animation": "cast",
"castTimeMs": 2000,
"range": 30,
"cooldownMs": 12000,
"initialCooldownMs": [
12000,
12000
],
"repeatCooldownMs": [
12000,
12000
],
"damageMultiplier": 0,
"radius": 15,
"telegraph": {
"shape": "circle",
"anchor": "source",
"warningMs": 1800,
"radius": 15
},
"effect": {
"kind": "target-aura",
"aura": "confusion",
"durationMs": 6000
}
},
{
"id": "rw-cave-water-dragon-102623-debuff-494298",
"name": "Law of the Dragons",
"delivery": "area",
"target": "nearby-party",
"school": "arcane",
"animation": "cast",
"castTimeMs": 2000,
"range": 30,
"cooldownMs": 12000,
"initialCooldownMs": [
12000,
12000
],
"repeatCooldownMs": [
12000,
12000
],
"damageMultiplier": 0,
"radius": 15,
"telegraph": {
"shape": "circle",
"anchor": "source",
"warningMs": 1800,
"radius": 15
},
"effect": {
"kind": "target-aura",
"aura": "confusion",
"durationMs": 6000
}
},
{
"id": "rw-cave-water-dragon-102623-phase-45",
"name": "Phase Change",
"delivery": "area",
"target": "self",
"school": "arcane",
"animation": "cast",
"castTimeMs": 1200,
"interruptible": false,
"range": 0,
"cooldownMs": 1000,
"initialCooldownMs": [
0,
0
],
"repeatCooldownMs": [
1000,
1000
],
"maximumCasts": 1,
"damageMultiplier": 0,
"effect": {
"kind": "aura",
"aura": "phase-45",
"durationMs": 5000
},
"condition": {
"kind": "self-health-percent",
"minimum": 0,
"maximum": 45
}
}
]
},
@@ -296,9 +492,15 @@ export const CAVE_WATER_DRAGON_ENTITIES = {
"damageMultiplier": 1.3,
"moveSpeed": 2.25,
"leashRange": 32,
"mechanicIds": [
"rw-cave-water-dragon-104161-spell-493996",
"rw-cave-water-dragon-104161-spell-493999",
"rw-cave-water-dragon-104161-spell-494000",
"rw-cave-water-dragon-104161-aura-494386"
],
"attacks": [
{
"id": "rw-cave-water-dragon-basic-attack",
"id": "basic-attack",
"name": "Basic Attack",
"delivery": "melee",
"target": "primary",
@@ -307,6 +509,115 @@ export const CAVE_WATER_DRAGON_ENTITIES = {
"range": 4.5,
"cooldownMs": 1900,
"damageMultiplier": 1
},
{
"id": "rw-cave-water-dragon-104161-spell-493996",
"name": "Water Spirit Attack",
"spellId": 493996,
"delivery": "area",
"target": "nearby-party",
"school": "frost",
"animation": "cast",
"castTimeMs": 2000,
"range": 15,
"cooldownMs": 6500,
"initialCooldownMs": [
6500,
6500
],
"repeatCooldownMs": [
6500,
6500
],
"damageMultiplier": 0.85,
"radius": 15,
"telegraph": {
"shape": "circle",
"anchor": "source",
"warningMs": 1800,
"radius": 15
}
},
{
"id": "rw-cave-water-dragon-104161-spell-493999",
"name": "Wave Bomb",
"spellId": 493999,
"delivery": "area",
"target": "nearby-party",
"school": "frost",
"animation": "cast",
"castTimeMs": 2000,
"range": 20,
"cooldownMs": 6500,
"initialCooldownMs": [
6500,
6500
],
"repeatCooldownMs": [
6500,
6500
],
"damageMultiplier": 0.85,
"radius": 15,
"telegraph": {
"shape": "circle",
"anchor": "source",
"warningMs": 1800,
"radius": 15
}
},
{
"id": "rw-cave-water-dragon-104161-spell-494000",
"name": "Wave Strike",
"spellId": 494000,
"delivery": "area",
"target": "nearby-party",
"school": "frost",
"animation": "cast",
"castTimeMs": 2000,
"range": 20,
"cooldownMs": 6500,
"initialCooldownMs": [
6500,
6500
],
"repeatCooldownMs": [
6500,
6500
],
"damageMultiplier": 0.85,
"radius": 15,
"telegraph": {
"shape": "circle",
"anchor": "source",
"warningMs": 1800,
"radius": 15
}
},
{
"id": "rw-cave-water-dragon-104161-aura-494386",
"name": "Soul of the Water Dragon",
"delivery": "area",
"target": "self",
"school": "arcane",
"animation": "cast",
"castTimeMs": 1200,
"range": 0,
"cooldownMs": 15000,
"initialCooldownMs": [
15000,
15000
],
"repeatCooldownMs": [
15000,
15000
],
"damageMultiplier": 0,
"effect": {
"kind": "aura",
"aura": "soul-of-the-water-dragon",
"durationMs": 7500
}
}
]
},
@@ -1458,6 +1769,6 @@ export const CAVE_WATER_DRAGON_POPULATION_PROVENANCE = {
"runeWakerIdsAreNamespaced": true,
"serverEntryPolicy": "RuneWaker numeric ids are never passed to AzerothCore serverEntry.",
"sourceSpellCatalog": "scripts/runewaker-pipeline/snapshots/runewaker-npc-spell-catalog.json",
"sourceSpellCatalogSha256": "ad29efc1d4906fdd4f2ac7a69316cac32d8fcbaea674678f050265c86e0c506c",
"sourceSpellCatalogSha256": "c47e67013e68ecf9c7ae30ef4b2058a850505385739dfe8c32ce8874e6ed077c",
"sourceSpellPolicy": "Only positively classified direct-HP-damage families execute; coefficients remain conservative HealerMan values."
} as const;
@@ -48,6 +48,6 @@ export const DEMON_STRONGHOLD_125_POPULATION_PROVENANCE = {
"runeWakerIdsAreNamespaced": true,
"serverEntryPolicy": "RuneWaker numeric ids are never passed to AzerothCore serverEntry.",
"sourceSpellCatalog": "scripts/runewaker-pipeline/snapshots/runewaker-npc-spell-catalog.json",
"sourceSpellCatalogSha256": "ad29efc1d4906fdd4f2ac7a69316cac32d8fcbaea674678f050265c86e0c506c",
"sourceSpellCatalogSha256": "c47e67013e68ecf9c7ae30ef4b2058a850505385739dfe8c32ce8874e6ed077c",
"sourceSpellPolicy": "Only positively classified direct-HP-damage families execute; coefficients remain conservative HealerMan values."
} as const;
@@ -16,7 +16,7 @@ export const DEMON_STRONGHOLD_ENTITIES = {
"leashRange": 24,
"attacks": [
{
"id": "rw-demon-stronghold-basic-attack",
"id": "basic-attack",
"name": "Basic Attack",
"delivery": "melee",
"target": "primary",
@@ -76,7 +76,7 @@ export const DEMON_STRONGHOLD_ENTITIES = {
"leashRange": 24,
"attacks": [
{
"id": "rw-demon-stronghold-basic-attack",
"id": "basic-attack",
"name": "Basic Attack",
"delivery": "melee",
"target": "primary",
@@ -116,7 +116,7 @@ export const DEMON_STRONGHOLD_ENTITIES = {
"leashRange": 24,
"attacks": [
{
"id": "rw-demon-stronghold-basic-attack",
"id": "basic-attack",
"name": "Basic Attack",
"delivery": "melee",
"target": "primary",
@@ -156,7 +156,7 @@ export const DEMON_STRONGHOLD_ENTITIES = {
"leashRange": 24,
"attacks": [
{
"id": "rw-demon-stronghold-basic-attack",
"id": "basic-attack",
"name": "Basic Attack",
"delivery": "melee",
"target": "primary",
@@ -236,7 +236,7 @@ export const DEMON_STRONGHOLD_ENTITIES = {
"leashRange": 24,
"attacks": [
{
"id": "rw-demon-stronghold-basic-attack",
"id": "basic-attack",
"name": "Basic Attack",
"delivery": "melee",
"target": "primary",
@@ -336,7 +336,7 @@ export const DEMON_STRONGHOLD_ENTITIES = {
"leashRange": 24,
"attacks": [
{
"id": "rw-demon-stronghold-basic-attack",
"id": "basic-attack",
"name": "Basic Attack",
"delivery": "melee",
"target": "primary",
@@ -396,7 +396,7 @@ export const DEMON_STRONGHOLD_ENTITIES = {
"leashRange": 24,
"attacks": [
{
"id": "rw-demon-stronghold-basic-attack",
"id": "basic-attack",
"name": "Basic Attack",
"delivery": "melee",
"target": "primary",
@@ -436,7 +436,7 @@ export const DEMON_STRONGHOLD_ENTITIES = {
"leashRange": 24,
"attacks": [
{
"id": "rw-demon-stronghold-basic-attack",
"id": "basic-attack",
"name": "Basic Attack",
"delivery": "melee",
"target": "primary",
@@ -536,7 +536,7 @@ export const DEMON_STRONGHOLD_ENTITIES = {
"leashRange": 24,
"attacks": [
{
"id": "rw-demon-stronghold-basic-attack",
"id": "basic-attack",
"name": "Basic Attack",
"delivery": "melee",
"target": "primary",
@@ -616,7 +616,7 @@ export const DEMON_STRONGHOLD_ENTITIES = {
"leashRange": 24,
"attacks": [
{
"id": "rw-demon-stronghold-basic-attack",
"id": "basic-attack",
"name": "Basic Attack",
"delivery": "melee",
"target": "primary",
@@ -656,7 +656,7 @@ export const DEMON_STRONGHOLD_ENTITIES = {
"leashRange": 24,
"attacks": [
{
"id": "rw-demon-stronghold-basic-attack",
"id": "basic-attack",
"name": "Basic Attack",
"delivery": "melee",
"target": "primary",
@@ -696,7 +696,7 @@ export const DEMON_STRONGHOLD_ENTITIES = {
"leashRange": 24,
"attacks": [
{
"id": "rw-demon-stronghold-basic-attack",
"id": "basic-attack",
"name": "Basic Attack",
"delivery": "melee",
"target": "primary",
@@ -736,7 +736,7 @@ export const DEMON_STRONGHOLD_ENTITIES = {
"leashRange": 24,
"attacks": [
{
"id": "rw-demon-stronghold-basic-attack",
"id": "basic-attack",
"name": "Basic Attack",
"delivery": "melee",
"target": "primary",
@@ -776,7 +776,7 @@ export const DEMON_STRONGHOLD_ENTITIES = {
"leashRange": 24,
"attacks": [
{
"id": "rw-demon-stronghold-basic-attack",
"id": "basic-attack",
"name": "Basic Attack",
"delivery": "melee",
"target": "primary",
@@ -816,7 +816,7 @@ export const DEMON_STRONGHOLD_ENTITIES = {
"leashRange": 24,
"attacks": [
{
"id": "rw-demon-stronghold-basic-attack",
"id": "basic-attack",
"name": "Basic Attack",
"delivery": "melee",
"target": "primary",
@@ -855,9 +855,15 @@ export const DEMON_STRONGHOLD_ENTITIES = {
"damageMultiplier": 1.3,
"moveSpeed": 2.25,
"leashRange": 32,
"mechanicIds": [
"rw-demon-stronghold-102701-aura-494871",
"rw-demon-stronghold-102701-aura-494873",
"rw-demon-stronghold-102701-spell-494875",
"rw-demon-stronghold-102701-spell-494876"
],
"attacks": [
{
"id": "rw-demon-stronghold-basic-attack",
"id": "basic-attack",
"name": "Basic Attack",
"delivery": "melee",
"target": "primary",
@@ -866,6 +872,105 @@ export const DEMON_STRONGHOLD_ENTITIES = {
"range": 4.5,
"cooldownMs": 1900,
"damageMultiplier": 1
},
{
"id": "rw-demon-stronghold-102701-aura-494871",
"name": "Hurricane",
"delivery": "area",
"target": "self",
"school": "arcane",
"animation": "cast",
"castTimeMs": 2000,
"range": 0,
"cooldownMs": 15000,
"initialCooldownMs": [
15000,
15000
],
"repeatCooldownMs": [
15000,
15000
],
"damageMultiplier": 0,
"effect": {
"kind": "aura",
"aura": "hurricane",
"durationMs": 7500
}
},
{
"id": "rw-demon-stronghold-102701-aura-494873",
"name": "Struggle",
"delivery": "area",
"target": "self",
"school": "arcane",
"animation": "cast",
"castTimeMs": 10000,
"range": 0,
"cooldownMs": 15000,
"initialCooldownMs": [
15000,
15000
],
"repeatCooldownMs": [
15000,
15000
],
"damageMultiplier": 0,
"effect": {
"kind": "aura",
"aura": "struggle",
"durationMs": 7500
}
},
{
"id": "rw-demon-stronghold-102701-spell-494875",
"name": "Double Cyclone",
"spellId": 494875,
"delivery": "area",
"target": "nearby-party",
"school": "arcane",
"animation": "cast",
"castTimeMs": 3000,
"range": 15,
"cooldownMs": 6500,
"initialCooldownMs": [
6500,
6500
],
"repeatCooldownMs": [
6500,
6500
],
"damageMultiplier": 0.85,
"radius": 15,
"telegraph": {
"shape": "circle",
"anchor": "source",
"warningMs": 1800,
"radius": 15
}
},
{
"id": "rw-demon-stronghold-102701-spell-494876",
"name": "Titanic Attack",
"spellId": 494876,
"delivery": "projectile",
"target": "primary",
"school": "physical",
"animation": "cast",
"castTimeMs": 2000,
"range": 15,
"cooldownMs": 6500,
"initialCooldownMs": [
6500,
6500
],
"repeatCooldownMs": [
6500,
6500
],
"damageMultiplier": 0.85
}
]
},
@@ -2954,6 +3059,6 @@ export const DEMON_STRONGHOLD_POPULATION_PROVENANCE = {
"runeWakerIdsAreNamespaced": true,
"serverEntryPolicy": "RuneWaker numeric ids are never passed to AzerothCore serverEntry.",
"sourceSpellCatalog": "scripts/runewaker-pipeline/snapshots/runewaker-npc-spell-catalog.json",
"sourceSpellCatalogSha256": "ad29efc1d4906fdd4f2ac7a69316cac32d8fcbaea674678f050265c86e0c506c",
"sourceSpellCatalogSha256": "c47e67013e68ecf9c7ae30ef4b2058a850505385739dfe8c32ce8874e6ed077c",
"sourceSpellPolicy": "Only positively classified direct-HP-damage families execute; coefficients remain conservative HealerMan values."
} as const;
@@ -1,2 +1,2 @@
// Generated by npm run dungeon:catalog. Do not edit.
export default "H4sIAAAAAAACCuy9bY/jOJIu+lcMnwOcWcBmMYLvBZwP/bKzM/dO7/Tt2jODi0FhQUlUpqecVl7ZWdXVjf7vF0FKtmzJaTntrsrKqf0w22WmRDIUEYyXJ4K/Ttf5bbjzfwv1elGtpq9hNl1XD3Uepq9/neb+7t4vblY/+NWiDOvN9PW0eFjdhGo1v1/ch+ViFV61P7R/y/65rlbT2TSs3i/qanUXVpsfff7O34T19PU/pus6f3Xj78Krm7AKtd+E4lW5eB9+XPqPof4+vevPd/dVvVm3bxp45M6v/HpT1XffrNdh+/74wOQPm0DP+/rjpPTLZebzd/82fUsLyquH1SbU3/mNX1Y309ePvvmnh9VmcReaP27X4n8JdbW5/a6qw3fV3d2CSCILV0AhM2VQBAsuV7lwvNS5BzAGrSiyMrNB7D/+JlL5L1X+bnghg3+6Jcl9WC63+/h16td5WKUPOGXs1V98sXi3/uHH/+ffi8Wmql/ly0VYbeb5Q12H1eaVr4OfK3x17zf57fz7V99/+8fFMqy/i3/16g29mxVZTh/xvspv0zv/FPwy1D/41fzNpqr9TXjVssD8Q1W/a/lg/So+M09THn31b7/Npnn1PtCLpq//8WvLV38uiBq3vqg+lH51M38Xwv10Nr3z9zQiROczrqevDf1zs9gsiLcEse7Gbxb5m3v/YbWevgbuZtO68neL1Q3xyHr6GuWOv9uvkVd1mP42O1gD8cCHxaqYrzdV/s4XobMMub8M1V0G9JbhxOEq+MhFFMEXd4tVWHfm1o+QALFHAtMngRg5+we/WC5WN/Pcvw/1qrMGefAZ7N4a+OEaUOHhGszIJdT+l6ouw2r+rvYPy84KzGOMYEdQAXDkErKlz9+VfnNb3c2LEO67dLD7q9B7dDA9OnDorWIsJzwsC3/nV7u5DT+LAoLjkylws6ruQh1uutO7g+nl45IoTG/rAsZK4sPqXVjNSasvO0IYhfv4EkSP+lIdLkGey4VF9aErB4DuMQZQPTZ0svcRxgrjOvf1MmzmdxWdT6H+2FmGdY/oI9HjQ0Fcd7AMPXIZvzwsS1/X/t1ueuSPTY8wQh2APEcY6yp/N1/fL+oON+Dhp4C9VZieQCite4oRz15FEe43tx2OQHEgFyC6y9ADR5Tsfww1miduq2V151d5lxKH3GBPaAZSVE9dwKYmxbi86x6Oh1/CdedXvdNBip5qoDeMmv/O1/6hqFbdo/nR3fe0gsKeVkA+Wi3chHJRh3l+69d3nTXYxzRT3z6A/hcYKw8FTX+3fzT2JAH3JKGnlYzqSYIa+wHWt36zCXUo5rd+uewIgpIHgrAnB6Jvpzj7ZNWYLauqmJcP9cp3BUFJfGQJ0FOLINyTl3AblsvICjV5QfVmjxIH1tLe54AeJax68kG93gR/994/LDfd6dV5VBhQB2OttYdVEeqsuunOrh8RhQFzVcOTzdX10r8P8/uw2iO+eWT3OCCKvbNJjKW+r3O/qf0vndkVPrZ7d/rTj5XDrNr41SL33bnFY6ey65vI8GTuvwv5rV/5uju7PIvrrHyySZCcxfnSZx/rxWpz212EOo/57NMVwDpsbsO7dz01qPRjwq8HzIEne6wUuZhvqrtsb/7H+D/SeH9+cE82BvxDfhtWfjHP64/3expQ2ceIIE/7y8KMpsHNgqzj9XwT6nr/OLCPKsK+zyh7ilCfYx/O76rar7u2oXaPLGDgOHqyVRDWub8P87Iml/WhXt1Wy274QunHzua+ahC2/0HGLuVhc+PrIhzEcZR5TD0MSCbg0w+lZgX3i9XK510PUpnH9MMYJTWWCpvbMF+Fnx+6QmH0Y75jTzOgeLqKpOmr/GG5P799bPNjPsHYgzF/WMY4UlXOhxwG5R5V0/1wEjw5nha1c1pHtdpbgjuLE4x5spVS1P7d5tbX1epAJjTnZ51Wzj3Zife//LNazlehfsi688Nj+rHnORn+VH7YfoXl4uZ2s1qsbrqrwLOYYYgtxwaWbh5WxV4kQ/PHArv9ya15cjzv/aKiaA5p5u789pFP0HPc3FPFwN+u3vnbsJlXy2L+brG6KaqO+6rh0XhO32YQ4sk2Q1nVNyEKZPXQtZu0wMdMhh4zyp7xasfqxnrhl7QCUpL5rb+7X3TjCVrxR4jR+yTqqZ/kfrGJZCDF0J39MYYQfWnQ9snO41Yq61AuQ77ZJ4N+1ITrL+TJKnqzWL0L9Sbkt/P1bfWBIq67ZVg80FHQXUafM/Vjq7ivqzwUD7Vfzts8YZ8/6+qXsJrXwee33eC3/cTLqOqbarMJqzllgzqRX2N/b4K8nU2LUC5WC2KIdUzV9TPGi+Gs3WaxWdKL32xHJv93L5+3LG/atG9B77eUqv353rfpzHzp1+tFvp9R/qEqIiMVm/ntx6xekBItFmW5yB+Wm49/8/XCrzZptXFtq6q+88u9P+r+uib6/ODX7+JuluF9WP7kVzEzCXYm+NuGDBRa+X77hj/vvdm/94ulzxbLRXx3889ob97XYR1W9CVoR79Ol1Xe/PcAaTa34Y729tf7sKLM6rKYhJ/vQ7FoJHL9cHfna5rij3R8Tja3dfVwczs5eNNsUoQy+M1ksVlPthwyqStyj2YTvyom9JUmm9swoYjVx4lfLt4HNp1Nl5UvFqubH8J6HbOz0x/rcO/rxermcBLG6O8fVvmtrzeh+KYO/j99XP3/aX+b9HdIjHVTVw8rot//4JYXnD5vmVLZebWs6jjgAYFPZ9NV8HVKfJb0H6j5b7Opv8sopfwXosHeY95m0pFds1htKCdOX4MzxN9m09twt1jf34Y6bB9bv/v4XfukNS63FEJJi9v+zh3PuDt8o5a/zWL8NapKv+yvpMDcuGL/OZhNq7Jchw2xFs6Qz+bm7W8kEJtAJ9E6vuFhUz1sfoyh1Shz0//+Fv4nYRjSCNGYfn47m2bLsCr2/xLjX8aBzh/6gjjofdj/Wxn/th3b/nlc0D0t5cOi2NxSkJ4T+dIWBVmq9/TQ6iYZBHm12lQPdXzpDygnyNXkO6snAHYCEicozQS5nYCByRsh5MTARHI9AdTT2fQHYSco3OQ74G4CVkzA4AS1naDk8bE3QsqJ0xPhzMSY6VsCD3gCfkRqvV+sH/xyJ+vNv2fTcrEMDTum39jNkhjjoV5OX09fpTck2MUW9fFKiPlOkb3aey6/Dfm79cPd9PVU5DJg4YKzgcAd4AnlEQQEoQCd1VwFb2VWWCEQweXccyO8B2kzV3pjSI4XvwSylJw1UstZNERWN8vwHUnq9LV0yIGT3smr5XKRlGG7xe1Pc875/k73hs7f8NDjnX1zV+Y65zygB1eW0uZQZLLIrAtZcByU4FnhLBgshTOZlgFcYYTnipeF0byzb5BO93atlRKaTr/eNuH4NuGybUJ/m1qDNaC8F76wXmPmjFXcAXijQTulNDfCclGgsKhECcCzHHOEkmORO9XdpqZoy+ht4vFt4mXbxP42jQKFslQ2A6N9gCxTWYG68EaZQuSGF2iUyApjjc2dUtzLzCuvyjwIrnzW2SZytGdsUxzfprhsm6K/zUyA17JEiSrnRa5UaQBQIqIw3JWuKJQP3ilecq28VCZ4m5fATebKkPuw9zXtOduUx7cpL9um7G8TMpCldzJDmxtZWs4DbYWjtNKWyktjpeCG+zLLi8y5LMtDKDOdO5tnKNS+bOIZ21THt6ku26YaYFpwTmbeGuXLzOSIFkvMNfhMSSu4A64CWu+VK2xWZAGzEoo8U7r0PofgOtuUTp/zNfXxberLtqn728SyhBJzg4XgptAauFZZ8KYseY5K0OIBICAP0mnIJGRCYGm5QZlpB7KzTWXlOds0x7dpLtum6W+zyJQ3nKPPvcid92BLMDxoHoQrNYpcWlAh1y5ojTKoUqIJeeaKQgjnMtvZppZcnrFNe3yb9rJt2oEDpfBZoYWQvNCobaa1R+O9DwY5VyorA2RYhKC9U4BBYs6tLrEUpdXS2e42BVfnyKY7vk132TZdf5tWccsDlmDyXFuygXyweVkI64PI8pBnRikRQDqjdCh5UFxaZ70zUhhfdg8UpfCMcxOOW0FwmRUEA1aQ45gJWwSUIstKEXwZiiCdLbISTZlxLaCQ4HhRClMi5JzzQkJJpgFCZnbbFE5Z3pdNo4Vzg9s8bgXBZVYQDFlBaHNhM2VlrvPCegxo0YFzygZrJHhe6kJlpQ82aCkNF7km9HPBQwiye6A4C+dYQXDcCoLLrCAYsIJUrp1x5Cy7TOvcGG4hZCYYHbDMJGgMMoCWPCAvueAoMGAuM8+d1KZzoCAIMaRptbFycJvHrSC4zAqCASvIQR5MnjtTGIVaZcgLcFoorRVA4bh1VqEDL/Lcl4UN0ijNy5znWgReio4KkgIk9G130lzDn/O4GQSXmUEwYAZpsmezTJKBIHRRZii01nnhvRWl5WC1Ba+dFegKlXsM2kmhfamx5NYZv/ucaMSAcGojrBrc5nEzCC4zg2DADOKZsJgJ8kc0Wh8ynkuvcy68kmB9yDEIIzELRYGZxcDzzJXCFybzJS9N1wwCZ844UeC4GQSXmUEwYAbZEHJeuMBLrpzTBhVmVsjcaVUKMhtUyCDjZaZtFlyOHp1KZqFUDsRum0ILPbBNIx0fFs7jZhBcZgbBgBmkbK6tDUpYyLzKMVjaKLfKF9rkQVqUhROIeemVlJrnWkEITkqJGUiDO6bVVg6pWmtxeJvHzSC4zAyCATPI2MJa7V1hHSg0PM8kyCC58JkSufUSkOcuK02JLkcltXdaYcBCC5VDKbbbVJw72bf2nAEcPjiPm0FwmRkEA2aQKAoLkMtCS+Ho0MyMFVmeq6wwDrhDKYALb3JbFNZTHCUrlC/KAnNuSreLDgmrxcDXNJoLMbRNPG4G4WVmEA6YQTLjvvDKKY5C0ukitPFGidLnqlDKW2O9w1BwoZwurc5kIaSCwpRZiZZ3XBRjnTzDPsDjZhBeZgbhgBlUeFdiWeRLine truncated
export default "H4sIAAAAAAACCuy9a5PbyJUo+FcY3N24MxFkKs/J10lF3A/9GI+96x57W147NhwdN0AgWcURH7UgS2p1h/77xskESJAASbBISaWy/EHuQhLIzJPn/crfh+v8Piyyv4dyPVsth69hNFyvHss8DF//PsyzxUM2u1v+lC1n07DeDF8Pi8flXVgtxw+zhzCfLcOr+kH9W/Hf69VyOBqG5btZuVouwnLz1yx/m92F9fD1P4frMn91ly3Cq7uwDGW2CcWr6exd+Os8+xDKH9O3/rR4WJWbdf2ljlcW2TJbb1bl4rv1Omy/H18Y/Nsm8PtZ+WEwzebzSZa//ffhL7ygfPW43ITyh2yTzVd3w9cnv/zz43IzW4Tqx/Vast9Cudrc/7Aqww+rxWLGINGFL6DQE+NQBQKfm1x5ObV5BuAckiom0wkFtf/6mwjlP6/yt90L6fzpFiQPYT7f7uP3YbbOwzId4FCIV3/Oitnb9U9//b//o5htVuWrfD4Ly804fyzLsNy8ysqQjQ2+esg2+f34x1c/fv+H2Tysf4i/evWGvy2KSc6H+LDK79M3/xiyeSh/ypbjN5tVmd2FVzUKjN+vyrc1HqxfxXfGacqjn/74cTTMV+8Cf2j4+p+/13j1p4KhcZ8Vq/fTbHk3fhvCw3A0XGQPPKJU4xjXw9eO/9zMNjPGLcWou8k2s/zNQ/Z+uR6+BulHw3KVLWbLO8aR9fA16h1+16eRr8ow/Dg6WAPjwPvZshivN6v8bVaExjL0/jJMcxnQWoZXh6uQPRdRhKxYzJZh3ZjbngABYgsErg0C1XP299lsPlvejfPsXSiXjTXog2OgvTXIwzWgwcM1uJ5LKLPfVuU0LMdvy+xx3liBO4UI1AMKgD2XMJln+dtptrlfLcZFCA9NOND+KuweHFwLDhJaq+iLCY/zIltky93cTl4EASXxyRC4W64WoQx3zen9wfT6NCUq19q6gr6U+Lh8G5Zj5urzBhFG4j6+BNWCvjaHS9CXYmGxet+kA0B/CgFMCw29bh1CX2Jc51k5D5vxYsXyKZQfGssgf4IfqRYeKsa6g2XYnsv47XE+zcoye7ubHuWp6RF6sAPQlxBjucrfjtcPs7KBDXh4FLC3CtciCGNtizHixasowsPmvoERqA7oAlRzGbZDROn2YZjeOHG/mq8W2TJvQuIQG+gMZ2BG9dQFbEpmjPNFUzgenoRvzm9a0kGrFmvgL/Saf5GV2WOxWjZF88ndt7iCwRZXQNmbLdyF6awM4/w+Wy8aa6BTnKmtH0D7BPrSQ8HTL/ZFY4sScI8SWlzJmRYlmL4HsL7PNptQhmJ8n83nDUIw+oAQ9uhAtfUUT09mjZP5alWMp4/lMmsSgtF4YgnQYoug/JOXcB/m84gKJVtB5WYPEgfa0t5xQAsSZJ4sqNebkC3eZY/zTXN6cxkUOthBX23tcVmEcrK6a85uT5BCh7pq4cnq6nqevQvjh7DcA747sXvsIMWWbFJ9oZ+VebYps98asxs8tXt//uj70uFktcmWszxrzq1OSWXfVpHhydi/CPl9tszK5uz6Iqwj/WSVIBmL43k2+VDOlpv75iLMZchHT2cA67C5D2/fttigsaeI33aoA0+2WNlzMd6sFpO9+U/hf4Tx/vzgn6wMZI/5fVhms3FefnjY44CGTgFBn7eXlesNg7sZa8fr8SaU5b44oJOMsG0z6hYjtJfoh+PFqszWTd3Q+hML6BBHT9YKwjrPHsJ4WrLJ+lgu71fzpvvC2FOyuc0aFLUPpO9SHjd3WVmEAz+OcafYQwdlAj5dKFUreJgtl1netCCNO8Uf+jCpvlDY3IfxMvz62CQKZ0/Zji3OgOrpLJKnX+WP8/356dTm+xxBX8GYP86jH2k1HXcZDMafZNNtdxI82Z8WuXNax2q5twR/ESY492QtpSizt5v7rFwtD2jCSnmRtPL+yUZ89tt/r+bjZSgfJ8354RR/bFlOTj4VH7anMJ/d3W+Ws+VdcxV4ETJ0oWVfx9Ld47LY82RYecqx256c3JP9ee9mK/bmMGduzk8njqBluPmnkkF2v3yb3YfNeDUvxm9ny7ti1TBfLZz057R1BqWerDNMV+VdiAS5emzqTVbhKZWhhYy6pbxSX95YzrI5r4CZZH6fLR5mTX+CNfIEMFpHYp56JA+zTQQDM4bm7KcQQrWpwdKTjcctVZZhOg/5Zh8M9qQK117Ik1n0ZrZ8G8pNyO/H6/vVe/a47pZBeMCjoLmMNmbaU6t4KFd5KB7LbD6u44Rt/CxXv4XluAxZft90ftNnXsaqvFttNmE55mhQw/Pr6FMD5JfRsAjT2XLGCLGOobp2xHjWHbXbzDZz/vCb7cjg/2rF8+bTuzrsW/D3iUO1vz5kdTgzn2fr9Szfjyj/tCoiIhWb8f2HSTljJlrMptNZ/jjffPh7Vs6y5SatNq5tuSoX2XzvR82na4bPT9n6bdzNPLwL85+zZYxMAo2U/KUCA7tWftx+4U97X87eZbN5NpnNZ/Hb1Z9R33wowzos+SR4R78P56u8+u8O0Gzuw4L39peHsOTI6rwYhF8fQjGrKHL9uFhkJU/xBxafg819uXq8ux8cfGk0KMI0ZJvBbLMebDFkUK7YPBoNsmUx4FMabO7DgD1WHwbZfPYuiOFoOF9lxWx591NYr2N0dvjXMjxk5Wx5dziJEPz7x2V+n5WbUHxXhuy/srj6/6d+NmjvkBHrrlw9Lhl+/5skWUg+3mkKZeer+aqMAxkgyOFouAxZmQKfU/4PtPLjaJgtJhxS/jPDYO+1jCbas14zW244Js6nIQXix9HwPixm64f7UIbta+u3H36o3yTnc2IXSlrc9rn0ciL94Ret/jiK/tfIKrN5eyUF5s4X++/BaLiaTtdhw6iFI5SjsfvlIxPEJrAkWscvPG5Wj5u/RtdqpLnh//oe/nfOYUgjDGN+/MtoOJmHZbH/S4y/jAONH2YFY9C7sP9bHX9bj21/Hhf0wEt5Pys29+yklwy+tEXFmuoDv7S8SwpBvlpuVo9l/OhPqAcozeAHsgMAGoDGAWo3QEkDcDB4o5QeOBhoaQeAdjga/qRogMoPfgDpB0BqAA4HaGmAWsbX3iitB94OlHcD54a/cPJAxokfEVrvZuvHbL6j9erv0XA6m4cKHdMzcTdnxHgs58PXw1fpCyntYpv18Uqp8Y6Rvdp7L78P+dv142L4eqhyHbDwwVPg5A7IOMsjKAjKAHqy0oSM9KQgpRDB5zKTTmUZaJr4aeYc0/Hst8CakienrR5FRWR5Nw8/MKUOX2uPEiTznXw1n88SM6y3uH00llLu73Rv6PINd73e2Lf009zmUgbMwE+nmnIoJrqYkA+T4CUYJSeFJ3A4Vd5NrA7gC6cyaeS0cFY29g3a29aurTHKsvRrbROObxOu2ya0t2ktkAOTZSorKLM48Y6M9ACZs2C9MVY6RVIVqAiNmgLISY45wlRikXvT3KZlb0vvbeLxbeJ128T2Np0Bg3pqaALOZgEmEzMp0BaZM65QuZMFOqMmhSNHuTdGZnqSmcxM86CkySaNbaJEumCb6vg21XXbVO1tThRkVk9Ro8llkRszdQCoEVE56ae+KEwWMm/kVFqTaeNCRvkUpJv4acizsHeadMk29fFt6uu2qdvbhAnoaeb1BCl3ekpSBt6KRE2apibTjrSSTmbTSV5MvJ9M8hCmE5t7yieozD5t4gXbNMe3aa7bpulAWvBeTzJyJptOXI5IOMXcQjYxmpT0IE1AyjLjC5oUk4CTKRT5xNhpluUQfGOb2ttLTtMe36a9bpu2vU2cTmGKucNCSVdYC9KaScjcdCpzNIoXDwABZdDewkTDRCmcknSoJ9aDbmzTkL5km+74Nt1123TtbRYTkzkpMcszlfssA5qCk8HKoPzUoso1gQm59cFa1MFMNbqQT3xRKOX9hBrbtFrqC7ZJx7dJ122TOgRKkU0Kq5SWhUVLE2szdFmWBYdSGjOZBphgEYLNvAEMGnNJdopTNSWrPTW3qaS5hDb98W3667bp29skI0kGnILLc0usA2WB8mmhKAtqkod84oxRAbR3xoapDEZq8pR5p5XLpk2BYgxeIDfhuBYE12lB0KEFeYkTRUVArSaTqQrZNBRBeyomU3TTibQKCg1eFlPlpgi5lLLQMGXVAGHidttU3pBs06azyvvObR7XguA6LQi6tCCkXNHEkM5tXlCGAQk9eG8okNOQyaktzGSaBQpWaydVbjn7uZAhBN0UKJ7gEi0IjmtBcJ0WBB1akMmtd56NZT+xNndOEoSJC84GnE40WAw6gNUyoJxKJVFhwFxPMum1dQ2BgqBUF6e1jnTnNo9rQXCdFgQdWpCHPLg8965wBq2ZoCzAW2WsNQCFl+TJoIdM5Xk2LShoZ6yc5jK3KsiparAgrUBDW3dnztV9nMfVILhODYIONciyPjuZaFYQlC2mE1TW2rzIMlJTkkCWILOeFPrC5BkG67Wy2dTiVJJ32e440akO4rROkenc5nE1CK5Tg6BDDZITRThRbI9YpCxMZK4zm0uVGQ2UhRyDchonoShwQhhkPvFTlRVukk3l1DXVIPDuAokCx9UguE4Ngg41iELIZeGDnErjvXVocEJK596aqWK1wYQJTOR0YmkSfI4ZepPUQm08qN02lVW2Y5tOe9lNnMfVILhODYIONchQbomCUQSTzOQYiDcqyWSFdXnQhLrwCjGfZkZrK3NrIASvtcYJaIc7pLWku1gtEXZv87gaBNepQdChBjkqiGzmC/Jg0Ml8okEHLVU2MSqnTAPK3E+mboo+R6Nt5q3BgIVVJoep2m7TSOl1W9vzDrBbcB5Xg+A6NQg61CBVFASQ68Jq5VloThypSZ6bSeE8SI9agVSZy6koKGM/yqQwWTEtMJdu6nfeIUVWdZyms1Kprm3icTUIr1ODsEMN0hOZFZnxRqLSLF2UdZkzaprlpjAmI0eZx1BIZbydkp3oQmkDhZtOpkiyYaI Line truncated
@@ -1 +1 @@
{"schemaVersion":1,"source":{"campaignManifest":"dungeon-pipeline/dungeon-campaign.json","environmentPackages":["src/game/generated/fivePlayerDungeonImports.json","src/game/generated/manastormAssetPackages.json (temporary fallback)"],"encounterCatalog":"src/game/generated/manastormRuntimeCatalog.json","azerothCoreCommit":"4d9d1d4b5723e819c5c390f6ca1177283dbfb8e3","azerothCoreSourceLock":"src/game/generated/azerothCoreSourceLock.json","spellCatalog":{"ascension":"../LadiksMPQEditor/client-current/area-52/patch-D/DBFilesClient/Spell.dbc","epoch":"../HealerMan-Storage/pipeline-work/dungeons/epoch-client/DBFilesClient/Spell.dbc"}},"coverage":[{"dungeonId":"shadowfang-keep","mapId":33,"encounters":7,"entities":31,"staticSpawns":109,"roamingPacks":24,"source":"azerothcore"},{"dungeonId":"stormwind-stockade","mapId":34,"encounters":5,"entities":11,"staticSpawns":93,"roamingPacks":0,"source":"azerothcore"},{"dungeonId":"deadmines","mapId":36,"encounters":7,"entities":22,"staticSpawns":179,"roamingPacks":23,"source":"azerothcore"},{"dungeonId":"wailing-caverns","mapId":43,"encounters":8,"entities":20,"staticSpawns":252,"roamingPacks":7,"source":"azerothcore"},{"dungeonId":"razorfen-kraul","mapId":47,"encounters":7,"entities":38,"staticSpawns":179,"roamingPacks":12,"source":"azerothcore"},{"dungeonId":"blackfathom-deeps","mapId":48,"encounters":6,"entities":27,"staticSpawns":201,"roamingPacks":10,"source":"azerothcore"},{"dungeonId":"uldaman","mapId":70,"encounters":7,"entities":38,"staticSpawns":302,"roamingPacks":12,"source":"azerothcore"},{"dungeonId":"gnomeregan","mapId":90,"encounters":4,"entities":31,"staticSpawns":371,"roamingPacks":31,"source":"azerothcore"},{"dungeonId":"sunken-temple","mapId":109,"encounters":4,"entities":33,"staticSpawns":245,"roamingPacks":4,"source":"azerothcore"},{"dungeonId":"razorfen-downs","mapId":129,"encounters":6,"entities":25,"staticSpawns":194,"roamingPacks":13,"source":"azerothcore"},{"dungeonId":"scarlet-monastery","mapId":189,"encounters":5,"entities":37,"staticSpawns":310,"roamingPacks":16,"source":"azerothcore"},{"dungeonId":"zulfarrak","mapId":209,"encounters":5,"entities":21,"staticSpawns":252,"roamingPacks":14,"source":"azerothcore"},{"dungeonId":"blackrock-spire","mapId":229,"encounters":15,"entities":78,"staticSpawns":566,"roamingPacks":22,"source":"azerothcore"},{"dungeonId":"blackrock-depths","mapId":230,"encounters":13,"entities":61,"staticSpawns":1040,"roamingPacks":15,"source":"azerothcore"},{"dungeonId":"scholomance","mapId":289,"encounters":8,"entities":38,"staticSpawns":327,"roamingPacks":15,"source":"azerothcore"},{"dungeonId":"stratholme","mapId":329,"encounters":9,"entities":50,"staticSpawns":431,"roamingPacks":29,"source":"azerothcore"},{"dungeonId":"maraudon","mapId":349,"encounters":8,"entities":33,"staticSpawns":525,"roamingPacks":20,"source":"azerothcore"},{"dungeonId":"ragefire-chasm","mapId":389,"encounters":4,"entities":11,"staticSpawns":127,"roamingPacks":4,"source":"azerothcore"},{"dungeonId":"dire-maul","mapId":429,"encounters":12,"entities":75,"staticSpawns":756,"roamingPacks":59,"source":"azerothcore"},{"dungeonId":"shattered-halls","mapId":540,"encounters":3,"entities":32,"staticSpawns":198,"roamingPacks":13,"source":"azerothcore"},{"dungeonId":"blood-furnace","mapId":542,"encounters":3,"entities":17,"staticSpawns":139,"roamingPacks":13,"source":"azerothcore"},{"dungeonId":"hellfire-ramparts","mapId":543,"encounters":2,"entities":12,"staticSpawns":85,"roamingPacks":12,"source":"azerothcore"},{"dungeonId":"steamvault","mapId":545,"encounters":3,"entities":17,"staticSpawns":127,"roamingPacks":17,"source":"azerothcore"},{"dungeonId":"underbog","mapId":546,"encounters":4,"entities":22,"staticSpawns":161,"roamingPacks":23,"source":"azerothcore"},{"dungeonId":"slave-pens","mapId":547,"encounters":3,"entities":21,"staticSpawns":122,"roamingPacks":32,"source":"azerothcore"},{"dungeonId":"arcatraz","mapId":552,"encounters":4,"entities":29,"staticSpawns":85,"roamingPacks":9,"source":"azerothcore"},{"dungeonId":"botanica","mapId":553,"encounters":5,"entities":29,"staticSpawns":171,"roamingPacks":13,"source":"azerothcore"},{"dungeonId":"mechanar","mapId":554,"encounters":3,"entities":17,"staticSpawns":84,"roamingPacks":14,"source":"azerothcore"},{"dungeonId":"shadow-labyrinth","mapId":555,"encounters":4,"entities":22,"staticSpawns":189,"roamingPacks":13,"source":"azerothcore"},{"dungeonId":"sethekk-halls","mapId":556,"encounters":2,"entities":16,"staticSpawns":104,"roamingPacks":24,"source":"azerothcore"},{"dungeonId":"mana-tombs","mapId":557,"encounters":3,"entities":14,"staticSpawns":119,"roamingPacks":15,"source":"azerothcore"},{"dungeonId":"auchenai-crypts","mapId":558,"encounters":2,"entities":14,"staticSpawns":93,"roamingPacks":37,"source":"azerothcore"},{"dungeonId":"magisters-terrace","mapId":585,"encounters":3,"entities":18,"staticSpawns":147,"roamingPacks":6,"source":"azerothcore"},{"dungeonId":"black-morass","mapId":269,"encounters":3,"entitiesLine truncated
{"schemaVersion":1,"source":{"campaignManifest":"dungeon-pipeline/dungeon-campaign.json","environmentPackages":["src/game/generated/fivePlayerDungeonImports.json","src/game/generated/manastormAssetPackages.json (temporary fallback)"],"encounterCatalog":"src/game/generated/manastormRuntimeCatalog.json","azerothCoreCommit":"4d9d1d4b5723e819c5c390f6ca1177283dbfb8e3","azerothCoreSourceLock":"src/game/generated/azerothCoreSourceLock.json","spellCatalog":{"ascension":"../LadiksMPQEditor/client-current/area-52/patch-D/DBFilesClient/Spell.dbc","epoch":"../HealerMan-Storage/pipeline-work/dungeons/epoch-client/DBFilesClient/Spell.dbc"}},"coverage":[{"dungeonId":"shadowfang-keep","mapId":33,"encounters":7,"entities":31,"staticSpawns":109,"roamingPacks":24,"source":"azerothcore"},{"dungeonId":"stormwind-stockade","mapId":34,"encounters":5,"entities":11,"staticSpawns":93,"roamingPacks":0,"source":"azerothcore"},{"dungeonId":"deadmines","mapId":36,"encounters":7,"entities":22,"staticSpawns":179,"roamingPacks":23,"source":"azerothcore"},{"dungeonId":"wailing-caverns","mapId":43,"encounters":8,"entities":20,"staticSpawns":252,"roamingPacks":7,"source":"azerothcore"},{"dungeonId":"razorfen-kraul","mapId":47,"encounters":7,"entities":38,"staticSpawns":179,"roamingPacks":12,"source":"azerothcore"},{"dungeonId":"blackfathom-deeps","mapId":48,"encounters":6,"entities":27,"staticSpawns":201,"roamingPacks":10,"source":"azerothcore"},{"dungeonId":"uldaman","mapId":70,"encounters":7,"entities":38,"staticSpawns":302,"roamingPacks":12,"source":"azerothcore"},{"dungeonId":"gnomeregan","mapId":90,"encounters":4,"entities":31,"staticSpawns":371,"roamingPacks":31,"source":"azerothcore"},{"dungeonId":"sunken-temple","mapId":109,"encounters":4,"entities":33,"staticSpawns":245,"roamingPacks":4,"source":"azerothcore"},{"dungeonId":"razorfen-downs","mapId":129,"encounters":6,"entities":25,"staticSpawns":194,"roamingPacks":13,"source":"azerothcore"},{"dungeonId":"scarlet-monastery","mapId":189,"encounters":5,"entities":37,"staticSpawns":310,"roamingPacks":16,"source":"azerothcore"},{"dungeonId":"zulfarrak","mapId":209,"encounters":5,"entities":21,"staticSpawns":252,"roamingPacks":14,"source":"azerothcore"},{"dungeonId":"blackrock-spire","mapId":229,"encounters":15,"entities":78,"staticSpawns":566,"roamingPacks":22,"source":"azerothcore"},{"dungeonId":"blackrock-depths","mapId":230,"encounters":13,"entities":61,"staticSpawns":1040,"roamingPacks":15,"source":"azerothcore"},{"dungeonId":"scholomance","mapId":289,"encounters":8,"entities":38,"staticSpawns":327,"roamingPacks":15,"source":"azerothcore"},{"dungeonId":"stratholme","mapId":329,"encounters":9,"entities":50,"staticSpawns":431,"roamingPacks":29,"source":"azerothcore"},{"dungeonId":"maraudon","mapId":349,"encounters":8,"entities":33,"staticSpawns":525,"roamingPacks":20,"source":"azerothcore"},{"dungeonId":"ragefire-chasm","mapId":389,"encounters":4,"entities":11,"staticSpawns":127,"roamingPacks":4,"source":"azerothcore"},{"dungeonId":"dire-maul","mapId":429,"encounters":12,"entities":75,"staticSpawns":756,"roamingPacks":59,"source":"azerothcore"},{"dungeonId":"shattered-halls","mapId":540,"encounters":3,"entities":32,"staticSpawns":198,"roamingPacks":13,"source":"azerothcore"},{"dungeonId":"blood-furnace","mapId":542,"encounters":3,"entities":17,"staticSpawns":139,"roamingPacks":13,"source":"azerothcore"},{"dungeonId":"hellfire-ramparts","mapId":543,"encounters":2,"entities":12,"staticSpawns":85,"roamingPacks":12,"source":"azerothcore"},{"dungeonId":"steamvault","mapId":545,"encounters":3,"entities":17,"staticSpawns":127,"roamingPacks":17,"source":"azerothcore"},{"dungeonId":"underbog","mapId":546,"encounters":4,"entities":22,"staticSpawns":161,"roamingPacks":23,"source":"azerothcore"},{"dungeonId":"slave-pens","mapId":547,"encounters":3,"entities":21,"staticSpawns":122,"roamingPacks":32,"source":"azerothcore"},{"dungeonId":"arcatraz","mapId":552,"encounters":4,"entities":29,"staticSpawns":85,"roamingPacks":9,"source":"azerothcore"},{"dungeonId":"botanica","mapId":553,"encounters":5,"entities":29,"staticSpawns":171,"roamingPacks":13,"source":"azerothcore"},{"dungeonId":"mechanar","mapId":554,"encounters":3,"entities":17,"staticSpawns":84,"roamingPacks":14,"source":"azerothcore"},{"dungeonId":"shadow-labyrinth","mapId":555,"encounters":4,"entities":22,"staticSpawns":189,"roamingPacks":13,"source":"azerothcore"},{"dungeonId":"sethekk-halls","mapId":556,"encounters":2,"entities":16,"staticSpawns":104,"roamingPacks":24,"source":"azerothcore"},{"dungeonId":"mana-tombs","mapId":557,"encounters":3,"entities":14,"staticSpawns":119,"roamingPacks":15,"source":"azerothcore"},{"dungeonId":"auchenai-crypts","mapId":558,"encounters":2,"entities":14,"staticSpawns":93,"roamingPacks":37,"source":"azerothcore"},{"dungeonId":"magisters-terrace","mapId":585,"encounters":3,"entities":18,"staticSpawns":147,"roamingPacks":6,"source":"azerothcore"},{"dungeonId":"black-morass","mapId":269,"encounters":3,"entitiesLine truncated
@@ -16,7 +16,7 @@ export const DUNGEON_OF_DALANIS_ENTITIES = {
"leashRange": 24,
"attacks": [
{
"id": "rw-dungeon-of-dalanis-basic-attack",
"id": "basic-attack",
"name": "Basic Attack",
"delivery": "melee",
"target": "primary",
@@ -56,7 +56,7 @@ export const DUNGEON_OF_DALANIS_ENTITIES = {
"leashRange": 24,
"attacks": [
{
"id": "rw-dungeon-of-dalanis-basic-attack",
"id": "basic-attack",
"name": "Basic Attack",
"delivery": "melee",
"target": "primary",
@@ -96,7 +96,7 @@ export const DUNGEON_OF_DALANIS_ENTITIES = {
"leashRange": 24,
"attacks": [
{
"id": "rw-dungeon-of-dalanis-basic-attack",
"id": "basic-attack",
"name": "Basic Attack",
"delivery": "melee",
"target": "primary",
@@ -136,7 +136,7 @@ export const DUNGEON_OF_DALANIS_ENTITIES = {
"leashRange": 24,
"attacks": [
{
"id": "rw-dungeon-of-dalanis-basic-attack",
"id": "basic-attack",
"name": "Basic Attack",
"delivery": "melee",
"target": "primary",
@@ -196,7 +196,7 @@ export const DUNGEON_OF_DALANIS_ENTITIES = {
"leashRange": 24,
"attacks": [
{
"id": "rw-dungeon-of-dalanis-basic-attack",
"id": "basic-attack",
"name": "Basic Attack",
"delivery": "melee",
"target": "primary",
@@ -236,7 +236,7 @@ export const DUNGEON_OF_DALANIS_ENTITIES = {
"leashRange": 24,
"attacks": [
{
"id": "rw-dungeon-of-dalanis-basic-attack",
"id": "basic-attack",
"name": "Corrosive Slam",
"delivery": "melee",
"target": "primary",
@@ -296,7 +296,7 @@ export const DUNGEON_OF_DALANIS_ENTITIES = {
"leashRange": 24,
"attacks": [
{
"id": "rw-dungeon-of-dalanis-basic-attack",
"id": "basic-attack",
"name": "Basic Attack",
"delivery": "melee",
"target": "primary",
@@ -336,7 +336,7 @@ export const DUNGEON_OF_DALANIS_ENTITIES = {
"leashRange": 24,
"attacks": [
{
"id": "rw-dungeon-of-dalanis-basic-attack",
"id": "basic-attack",
"name": "Basic Attack",
"delivery": "melee",
"target": "primary",
@@ -376,7 +376,7 @@ export const DUNGEON_OF_DALANIS_ENTITIES = {
"leashRange": 24,
"attacks": [
{
"id": "rw-dungeon-of-dalanis-basic-attack",
"id": "basic-attack",
"name": "Basic Attack",
"delivery": "melee",
"target": "primary",
@@ -416,7 +416,7 @@ export const DUNGEON_OF_DALANIS_ENTITIES = {
"leashRange": 24,
"attacks": [
{
"id": "rw-dungeon-of-dalanis-basic-attack",
"id": "basic-attack",
"name": "Basic Attack",
"delivery": "melee",
"target": "primary",
@@ -476,7 +476,7 @@ export const DUNGEON_OF_DALANIS_ENTITIES = {
"leashRange": 24,
"attacks": [
{
"id": "rw-dungeon-of-dalanis-basic-attack",
"id": "basic-attack",
"name": "Basic Attack",
"delivery": "melee",
"target": "primary",
@@ -536,7 +536,7 @@ export const DUNGEON_OF_DALANIS_ENTITIES = {
"leashRange": 24,
"attacks": [
{
"id": "rw-dungeon-of-dalanis-basic-attack",
"id": "basic-attack",
"name": "Basic Attack",
"delivery": "melee",
"target": "primary",
@@ -576,7 +576,7 @@ export const DUNGEON_OF_DALANIS_ENTITIES = {
"leashRange": 24,
"attacks": [
{
"id": "rw-dungeon-of-dalanis-basic-attack",
"id": "basic-attack",
"name": "Basic Attack",
"delivery": "melee",
"target": "primary",
@@ -636,7 +636,7 @@ export const DUNGEON_OF_DALANIS_ENTITIES = {
"leashRange": 24,
"attacks": [
{
"id": "rw-dungeon-of-dalanis-basic-attack",
"id": "basic-attack",
"name": "Basic Attack",
"delivery": "melee",
"target": "primary",
@@ -676,7 +676,7 @@ export const DUNGEON_OF_DALANIS_ENTITIES = {
"leashRange": 24,
"attacks": [
{
"id": "rw-dungeon-of-dalanis-basic-attack",
"id": "basic-attack",
"name": "Basic Attack",
"delivery": "melee",
"target": "primary",
@@ -716,7 +716,7 @@ export const DUNGEON_OF_DALANIS_ENTITIES = {
"leashRange": 24,
"attacks": [
{
"id": "rw-dungeon-of-dalanis-basic-attack",
"id": "basic-attack",
"name": "Basic Attack",
"delivery": "melee",
"target": "primary",
@@ -756,7 +756,7 @@ export const DUNGEON_OF_DALANIS_ENTITIES = {
"leashRange": 24,
"attacks": [
{
"id": "rw-dungeon-of-dalanis-basic-attack",
"id": "basic-attack",
"name": "Basic Attack",
"delivery": "melee",
"target": "primary",
@@ -1567,6 +1567,6 @@ export const DUNGEON_OF_DALANIS_POPULATION_PROVENANCE = {
"runeWakerIdsAreNamespaced": true,
"serverEntryPolicy": "RuneWaker numeric ids are never passed to AzerothCore serverEntry.",
"sourceSpellCatalog": "scripts/runewaker-pipeline/snapshots/runewaker-npc-spell-catalog.json",
"sourceSpellCatalogSha256": "ad29efc1d4906fdd4f2ac7a69316cac32d8fcbaea674678f050265c86e0c506c",
"sourceSpellCatalogSha256": "c47e67013e68ecf9c7ae30ef4b2058a850505385739dfe8c32ce8874e6ed077c",
"sourceSpellPolicy": "Only positively classified direct-HP-damage families execute; coefficients remain conservative HealerMan values."
} as const;
@@ -1,4 +1,4 @@
/* Generated by scripts/runewaker-pipeline/build-population.mjs. */
/* This file is generated by scripts/runewaker-pipeline/build-forsaken-population.mjs. */
import type { DungeonBossObjective } from "../dungeonTypes";
import type { PopulationDefinitionMap, StaticMobSpawnDefinition } from "../mobPopulation";
@@ -16,7 +16,7 @@ export const FORSAKEN_ABBEY_ENTITIES = {
"leashRange": 24,
"attacks": [
{
"id": "rw-fa-basic-attack",
"id": "basic-attack",
"name": "Corrosive Slam",
"delivery": "melee",
"target": "primary",
@@ -56,7 +56,7 @@ export const FORSAKEN_ABBEY_ENTITIES = {
"leashRange": 24,
"attacks": [
{
"id": "rw-fa-basic-attack",
"id": "basic-attack",
"name": "Basic Attack",
"delivery": "melee",
"target": "primary",
@@ -96,7 +96,7 @@ export const FORSAKEN_ABBEY_ENTITIES = {
"leashRange": 24,
"attacks": [
{
"id": "rw-fa-basic-attack",
"id": "basic-attack",
"name": "Basic Attack",
"delivery": "melee",
"target": "primary",
@@ -136,7 +136,7 @@ export const FORSAKEN_ABBEY_ENTITIES = {
"leashRange": 24,
"attacks": [
{
"id": "rw-fa-basic-attack",
"id": "basic-attack",
"name": "Basic Attack",
"delivery": "melee",
"target": "primary",
@@ -175,9 +175,12 @@ export const FORSAKEN_ABBEY_ENTITIES = {
"damageMultiplier": 1.3,
"moveSpeed": 2.25,
"leashRange": 32,
"mechanicIds": [
"rw-fa-100070-source-gap-burst"
],
"attacks": [
{
"id": "rw-fa-basic-attack",
"id": "basic-attack",
"name": "Basic Attack",
"delivery": "melee",
"target": "primary",
@@ -186,6 +189,33 @@ export const FORSAKEN_ABBEY_ENTITIES = {
"range": 4.5,
"cooldownMs": 1900,
"damageMultiplier": 1
},
{
"id": "rw-fa-100070-source-gap-burst",
"name": "Arcane Burst",
"delivery": "area",
"target": "nearby-party",
"school": "arcane",
"animation": "cast",
"castTimeMs": 2000,
"range": 12,
"cooldownMs": 12000,
"initialCooldownMs": [
8000,
8000
],
"repeatCooldownMs": [
12000,
12000
],
"damageMultiplier": 0.82,
"radius": 8,
"telegraph": {
"shape": "circle",
"anchor": "source",
"warningMs": 2000,
"radius": 8
}
}
]
},
@@ -217,7 +247,7 @@ export const FORSAKEN_ABBEY_ENTITIES = {
"leashRange": 24,
"attacks": [
{
"id": "rw-fa-basic-attack",
"id": "basic-attack",
"name": "Basic Attack",
"delivery": "melee",
"target": "primary",
@@ -257,7 +287,7 @@ export const FORSAKEN_ABBEY_ENTITIES = {
"leashRange": 24,
"attacks": [
{
"id": "rw-fa-basic-attack",
"id": "basic-attack",
"name": "Basic Attack",
"delivery": "melee",
"target": "primary",
@@ -297,7 +327,7 @@ export const FORSAKEN_ABBEY_ENTITIES = {
"leashRange": 24,
"attacks": [
{
"id": "rw-fa-basic-attack",
"id": "basic-attack",
"name": "Corrosive Slam",
"delivery": "melee",
"target": "primary",
@@ -336,9 +366,12 @@ export const FORSAKEN_ABBEY_ENTITIES = {
"damageMultiplier": 1.3,
"moveSpeed": 2.25,
"leashRange": 32,
"mechanicIds": [
"rw-fa-100561-source-gap-sweep"
],
"attacks": [
{
"id": "rw-fa-basic-attack",
"id": "basic-attack",
"name": "Corrosive Slam",
"delivery": "melee",
"target": "primary",
@@ -347,6 +380,33 @@ export const FORSAKEN_ABBEY_ENTITIES = {
"range": 4.5,
"cooldownMs": 1900,
"damageMultiplier": 1
},
{
"id": "rw-fa-100561-source-gap-sweep",
"name": "Corrosive Sweep",
"delivery": "area",
"target": "nearby-party",
"school": "nature",
"animation": "attack",
"castTimeMs": 1800,
"range": 7,
"cooldownMs": 10000,
"initialCooldownMs": [
7000,
7000
],
"repeatCooldownMs": [
10000,
10000
],
"damageMultiplier": 1.1,
"telegraph": {
"shape": "cone",
"anchor": "source",
"warningMs": 1800,
"length": 7,
"angleDegrees": 100
}
}
]
},
@@ -378,7 +438,7 @@ export const FORSAKEN_ABBEY_ENTITIES = {
"leashRange": 24,
"attacks": [
{
"id": "rw-fa-basic-attack",
"id": "basic-attack",
"name": "Basic Attack",
"delivery": "melee",
"target": "primary",
@@ -417,9 +477,12 @@ export const FORSAKEN_ABBEY_ENTITIES = {
"damageMultiplier": 1.3,
"moveSpeed": 2.25,
"leashRange": 32,
"mechanicIds": [
"rw-fa-100562-source-gap-sweep"
],
"attacks": [
{
"id": "rw-fa-basic-attack",
"id": "basic-attack",
"name": "Basic Attack",
"delivery": "melee",
"target": "primary",
@@ -428,6 +491,33 @@ export const FORSAKEN_ABBEY_ENTITIES = {
"range": 4.5,
"cooldownMs": 1900,
"damageMultiplier": 1
},
{
"id": "rw-fa-100562-source-gap-sweep",
"name": "Heavy Sweep",
"delivery": "area",
"target": "nearby-party",
"school": "physical",
"animation": "attack",
"castTimeMs": 1800,
"range": 7,
"cooldownMs": 10000,
"initialCooldownMs": [
7000,
7000
],
"repeatCooldownMs": [
10000,
10000
],
"damageMultiplier": 1.1,
"telegraph": {
"shape": "cone",
"anchor": "source",
"warningMs": 1800,
"length": 7,
"angleDegrees": 100
}
}
]
},
@@ -459,7 +549,7 @@ export const FORSAKEN_ABBEY_ENTITIES = {
"leashRange": 24,
"attacks": [
{
"id": "rw-fa-basic-attack",
"id": "basic-attack",
"name": "Basic Attack",
"delivery": "melee",
"target": "primary",
@@ -498,9 +588,12 @@ export const FORSAKEN_ABBEY_ENTITIES = {
"damageMultiplier": 1.3,
"moveSpeed": 2.25,
"leashRange": 32,
"mechanicIds": [
"rw-fa-100563-source-gap-sweep"
],
"attacks": [
{
"id": "rw-fa-basic-attack",
"id": "basic-attack",
"name": "Basic Attack",
"delivery": "melee",
"target": "primary",
@@ -509,6 +602,33 @@ export const FORSAKEN_ABBEY_ENTITIES = {
"range": 4.5,
"cooldownMs": 1900,
"damageMultiplier": 1
},
{
"id": "rw-fa-100563-source-gap-sweep",
"name": "Heavy Sweep",
"delivery": "area",
"target": "nearby-party",
"school": "physical",
"animation": "attack",
"castTimeMs": 1800,
"range": 7,
"cooldownMs": 10000,
"initialCooldownMs": [
7000,
7000
],
"repeatCooldownMs": [
10000,
10000
],
"damageMultiplier": 1.1,
"telegraph": {
"shape": "cone",
"anchor": "source",
"warningMs": 1800,
"length": 7,
"angleDegrees": 100
}
}
]
},
@@ -539,9 +659,18 @@ export const FORSAKEN_ABBEY_ENTITIES = {
"damageMultiplier": 1.3,
"moveSpeed": 2.25,
"leashRange": 32,
"mechanicIds": [
"rw-fa-100074-spell-490471",
"rw-fa-100074-spell-490489",
"rw-fa-100074-control-490636",
"rw-fa-100074-summon-100070-at-50",
"rw-fa-100074-summon-100070-at-25",
"rw-fa-100074-phase-50",
"rw-fa-100074-phase-25"
],
"attacks": [
{
"id": "rw-fa-basic-attack",
"id": "basic-attack",
"name": "Basic Attack",
"delivery": "melee",
"target": "primary",
@@ -550,6 +679,215 @@ export const FORSAKEN_ABBEY_ENTITIES = {
"range": 4.5,
"cooldownMs": 1900,
"damageMultiplier": 1
},
{
"id": "rw-fa-100074-spell-490471",
"name": "Fireball",
"spellId": 490471,
"delivery": "projectile",
"target": "primary",
"school": "fire",
"animation": "cast",
"castTimeMs": 2000,
"range": 22.5,
"cooldownMs": 3000,
"initialCooldownMs": [
3000,
3000
],
"repeatCooldownMs": [
3000,
3000
],
"damageMultiplier": 0.85
},
{
"id": "rw-fa-100074-spell-490489",
"name": "Dark Power Release",
"spellId": 490489,
"delivery": "area",
"target": "nearby-party",
"school": "fire",
"animation": "cast",
"castTimeMs": 5000,
"range": 15,
"cooldownMs": 20000,
"initialCooldownMs": [
20000,
20000
],
"repeatCooldownMs": [
20000,
20000
],
"damageMultiplier": 0.85,
"radius": 15,
"telegraph": {
"shape": "circle",
"anchor": "source",
"warningMs": 1800,
"radius": 15
}
},
{
"id": "rw-fa-100074-control-490636",
"name": "Group Catch",
"delivery": "area",
"target": "nearby-party",
"school": "shadow",
"animation": "cast",
"castTimeMs": 1800,
"range": 10,
"cooldownMs": 15000,
"initialCooldownMs": [
15000,
15000
],
"repeatCooldownMs": [
15000,
15000
],
"damageMultiplier": 0,
"radius": 5,
"telegraph": {
"shape": "circle",
"anchor": "source",
"warningMs": 1800,
"radius": 5
},
"effect": {
"kind": "control",
"mechanic": "root",
"durationMs": 3000
}
},
{
"id": "rw-fa-100074-summon-100070-at-50",
"name": "Reinforcements at 50%",
"delivery": "area",
"target": "self",
"school": "physical",
"animation": "cast",
"castTimeMs": 1800,
"interruptible": false,
"range": 0,
"cooldownMs": 1000,
"initialCooldownMs": [
0,
0
],
"repeatCooldownMs": [
1000,
1000
],
"maximumCasts": 1,
"damageMultiplier": 0,
"effect": {
"kind": "summon",
"creatureEntry": 100070,
"count": 2
},
"condition": {
"kind": "self-health-percent",
"minimum": 0,
"maximum": 50
}
},
{
"id": "rw-fa-100074-summon-100070-at-25",
"name": "Reinforcements at 25%",
"delivery": "area",
"target": "self",
"school": "physical",
"animation": "cast",
"castTimeMs": 1800,
"interruptible": false,
"range": 0,
"cooldownMs": 1000,
"initialCooldownMs": [
0,
0
],
"repeatCooldownMs": [
1000,
1000
],
"maximumCasts": 1,
"damageMultiplier": 0,
"effect": {
"kind": "summon",
"creatureEntry": 100070,
"count": 2
},
"condition": {
"kind": "self-health-percent",
"minimum": 0,
"maximum": 25
}
},
{
"id": "rw-fa-100074-phase-50",
"name": "Phase Change",
"delivery": "area",
"target": "self",
"school": "arcane",
"animation": "cast",
"castTimeMs": 1200,
"interruptible": false,
"range": 0,
"cooldownMs": 1000,
"initialCooldownMs": [
0,
0
],
"repeatCooldownMs": [
1000,
1000
],
"maximumCasts": 1,
"damageMultiplier": 0,
"effect": {
"kind": "aura",
"aura": "phase-50",
"durationMs": 5000
},
"condition": {
"kind": "self-health-percent",
"minimum": 0,
"maximum": 50
}
},
{
"id": "rw-fa-100074-phase-25",
"name": "Phase Change",
"delivery": "area",
"target": "self",
"school": "arcane",
"animation": "cast",
"castTimeMs": 1200,
"interruptible": false,
"range": 0,
"cooldownMs": 1000,
"initialCooldownMs": [
0,
0
],
"repeatCooldownMs": [
1000,
1000
],
"maximumCasts": 1,
"damageMultiplier": 0,
"effect": {
"kind": "aura",
"aura": "phase-25",
"durationMs": 5000
},
"condition": {
"kind": "self-health-percent",
"minimum": 0,
"maximum": 25
}
}
]
},
@@ -581,7 +919,7 @@ export const FORSAKEN_ABBEY_ENTITIES = {
"leashRange": 24,
"attacks": [
{
"id": "rw-fa-basic-attack",
"id": "basic-attack",
"name": "Basic Attack",
"delivery": "melee",
"target": "primary",
@@ -2341,20 +2679,13 @@ export const FORSAKEN_ABBEY_BOSSES = [
export const FORSAKEN_ABBEY_OBJECTIVE_ORDER = FORSAKEN_ABBEY_BOSSES;
export const FORSAKEN_ABBEY_POPULATION_PROVENANCE = {
"schemaVersion": 2,
"schemaVersion": 1,
"dungeonId": "forsaken-abbey",
"zoneId": 102,
"sourceRowCount": 251,
"importedSpawnCount": 150,
"excludedRowCount": 101,
"bossSpawnCount": 8,
"objectiveOrder": {
"authoritative": false,
"kind": "healerMan-distance-derived",
"evidence": [
"scripts/runewaker-pipeline/build-population.mjs"
]
},
"sourceSnapshot": "scripts/runewaker-pipeline/snapshots/forsaken-abbey-zone-102.json",
"sourceContentSha256": "13ef6edd3df6e71389aa92d01201889148c0794cf9ecaafdbaf90d3145d2bf7b",
"sourceHashes": {
@@ -2368,7 +2699,7 @@ export const FORSAKEN_ABBEY_POPULATION_PROVENANCE = {
"Resource/data/string_enus.db": "4fce021ba9ed110a213e08d3228b2de57cec66ddbbc8cdcf5753204874d9ab75"
}
},
"coordinateTransform": "threeLocal=(inverse(WDB placement)*server)*(meters,meters,-meters)",
"coordinateTransform": "threeLocal=(inverse(WDB placement)*server)*(0.1,0.1,-0.1)",
"navmeshProjection": {
"maximumAllowedDistance": 2,
"maximumObservedDistance": 2.5965,
@@ -2376,8 +2707,5 @@ export const FORSAKEN_ABBEY_POPULATION_PROVENANCE = {
"deferredRowsBeyondCombatLimit": 1
},
"runeWakerIdsAreNamespaced": true,
"serverEntryPolicy": "RuneWaker numeric ids are never passed to AzerothCore serverEntry.",
"sourceSpellCatalog": "scripts/runewaker-pipeline/snapshots/runewaker-npc-spell-catalog.json",
"sourceSpellCatalogSha256": "ad29efc1d4906fdd4f2ac7a69316cac32d8fcbaea674678f050265c86e0c506c",
"sourceSpellPolicy": "Only positively classified direct-HP-damage families execute; coefficients remain conservative HealerMan values."
"serverEntryPolicy": "RuneWaker numeric ids are never passed to AzerothCore serverEntry."
} as const;
@@ -16,7 +16,7 @@ export const GRAFU_CASTLE_ENTITIES = {
"leashRange": 24,
"attacks": [
{
"id": "rw-grafu-castle-basic-attack",
"id": "basic-attack",
"name": "Basic Attack",
"delivery": "melee",
"target": "primary",
@@ -56,7 +56,7 @@ export const GRAFU_CASTLE_ENTITIES = {
"leashRange": 24,
"attacks": [
{
"id": "rw-grafu-castle-basic-attack",
"id": "basic-attack",
"name": "Basic Attack",
"delivery": "melee",
"target": "primary",
@@ -96,7 +96,7 @@ export const GRAFU_CASTLE_ENTITIES = {
"leashRange": 24,
"attacks": [
{
"id": "rw-grafu-castle-basic-attack",
"id": "basic-attack",
"name": "Basic Attack",
"delivery": "melee",
"target": "primary",
@@ -176,7 +176,7 @@ export const GRAFU_CASTLE_ENTITIES = {
"leashRange": 24,
"attacks": [
{
"id": "rw-grafu-castle-basic-attack",
"id": "basic-attack",
"name": "Basic Attack",
"delivery": "melee",
"target": "primary",
@@ -256,7 +256,7 @@ export const GRAFU_CASTLE_ENTITIES = {
"leashRange": 24,
"attacks": [
{
"id": "rw-grafu-castle-basic-attack",
"id": "basic-attack",
"name": "Basic Attack",
"delivery": "melee",
"target": "primary",
@@ -336,7 +336,7 @@ export const GRAFU_CASTLE_ENTITIES = {
"leashRange": 24,
"attacks": [
{
"id": "rw-grafu-castle-basic-attack",
"id": "basic-attack",
"name": "Basic Attack",
"delivery": "melee",
"target": "primary",
@@ -416,7 +416,7 @@ export const GRAFU_CASTLE_ENTITIES = {
"leashRange": 24,
"attacks": [
{
"id": "rw-grafu-castle-basic-attack",
"id": "basic-attack",
"name": "Basic Attack",
"delivery": "melee",
"target": "primary",
@@ -456,7 +456,7 @@ export const GRAFU_CASTLE_ENTITIES = {
"leashRange": 24,
"attacks": [
{
"id": "rw-grafu-castle-basic-attack",
"id": "basic-attack",
"name": "Basic Attack",
"delivery": "melee",
"target": "primary",
@@ -536,7 +536,7 @@ export const GRAFU_CASTLE_ENTITIES = {
"leashRange": 24,
"attacks": [
{
"id": "rw-grafu-castle-basic-attack",
"id": "basic-attack",
"name": "Basic Attack",
"delivery": "melee",
"target": "primary",
@@ -576,7 +576,7 @@ export const GRAFU_CASTLE_ENTITIES = {
"leashRange": 24,
"attacks": [
{
"id": "rw-grafu-castle-basic-attack",
"id": "basic-attack",
"name": "Basic Attack",
"delivery": "melee",
"target": "primary",
@@ -616,7 +616,7 @@ export const GRAFU_CASTLE_ENTITIES = {
"leashRange": 24,
"attacks": [
{
"id": "rw-grafu-castle-basic-attack",
"id": "basic-attack",
"name": "Basic Attack",
"delivery": "melee",
"target": "primary",
@@ -1306,6 +1306,6 @@ export const GRAFU_CASTLE_POPULATION_PROVENANCE = {
"runeWakerIdsAreNamespaced": true,
"serverEntryPolicy": "RuneWaker numeric ids are never passed to AzerothCore serverEntry.",
"sourceSpellCatalog": "scripts/runewaker-pipeline/snapshots/runewaker-npc-spell-catalog.json",
"sourceSpellCatalogSha256": "ad29efc1d4906fdd4f2ac7a69316cac32d8fcbaea674678f050265c86e0c506c",
"sourceSpellCatalogSha256": "c47e67013e68ecf9c7ae30ef4b2058a850505385739dfe8c32ce8874e6ed077c",
"sourceSpellPolicy": "Only positively classified direct-HP-damage families execute; coefficients remain conservative HealerMan values."
} as const;
File diff suppressed because it is too large. Load diff
+618 -50
View File
@@ -16,7 +16,7 @@ export const HOTO_ENTITIES = {
"leashRange": 24,
"attacks": [
{
"id": "rw-hoto-basic-attack",
"id": "basic-attack",
"name": "Basic Attack",
"delivery": "melee",
"target": "primary",
@@ -76,7 +76,7 @@ export const HOTO_ENTITIES = {
"leashRange": 24,
"attacks": [
{
"id": "rw-hoto-basic-attack",
"id": "basic-attack",
"name": "Basic Attack",
"delivery": "melee",
"target": "primary",
@@ -156,7 +156,7 @@ export const HOTO_ENTITIES = {
"leashRange": 24,
"attacks": [
{
"id": "rw-hoto-basic-attack",
"id": "basic-attack",
"name": "Basic Attack",
"delivery": "melee",
"target": "primary",
@@ -196,7 +196,7 @@ export const HOTO_ENTITIES = {
"leashRange": 24,
"attacks": [
{
"id": "rw-hoto-basic-attack",
"id": "basic-attack",
"name": "Basic Attack",
"delivery": "melee",
"target": "primary",
@@ -256,7 +256,7 @@ export const HOTO_ENTITIES = {
"leashRange": 24,
"attacks": [
{
"id": "rw-hoto-basic-attack",
"id": "basic-attack",
"name": "Basic Attack",
"delivery": "melee",
"target": "primary",
@@ -336,7 +336,7 @@ export const HOTO_ENTITIES = {
"leashRange": 24,
"attacks": [
{
"id": "rw-hoto-basic-attack",
"id": "basic-attack",
"name": "Basic Attack",
"delivery": "melee",
"target": "primary",
@@ -376,7 +376,7 @@ export const HOTO_ENTITIES = {
"leashRange": 24,
"attacks": [
{
"id": "rw-hoto-basic-attack",
"id": "basic-attack",
"name": "Basic Attack",
"delivery": "melee",
"target": "primary",
@@ -456,7 +456,7 @@ export const HOTO_ENTITIES = {
"leashRange": 24,
"attacks": [
{
"id": "rw-hoto-basic-attack",
"id": "basic-attack",
"name": "Basic Attack",
"delivery": "melee",
"target": "primary",
@@ -536,7 +536,7 @@ export const HOTO_ENTITIES = {
"leashRange": 24,
"attacks": [
{
"id": "rw-hoto-basic-attack",
"id": "basic-attack",
"name": "Basic Attack",
"delivery": "melee",
"target": "primary",
@@ -596,7 +596,7 @@ export const HOTO_ENTITIES = {
"leashRange": 24,
"attacks": [
{
"id": "rw-hoto-basic-attack",
"id": "basic-attack",
"name": "Basic Attack",
"delivery": "melee",
"target": "primary",
@@ -656,7 +656,7 @@ export const HOTO_ENTITIES = {
"leashRange": 24,
"attacks": [
{
"id": "rw-hoto-basic-attack",
"id": "basic-attack",
"name": "Basic Attack",
"delivery": "melee",
"target": "primary",
@@ -756,7 +756,7 @@ export const HOTO_ENTITIES = {
"leashRange": 24,
"attacks": [
{
"id": "rw-hoto-basic-attack",
"id": "basic-attack",
"name": "Basic Attack",
"delivery": "melee",
"target": "primary",
@@ -816,7 +816,7 @@ export const HOTO_ENTITIES = {
"leashRange": 24,
"attacks": [
{
"id": "rw-hoto-basic-attack",
"id": "basic-attack",
"name": "Basic Attack",
"delivery": "melee",
"target": "primary",
@@ -856,7 +856,7 @@ export const HOTO_ENTITIES = {
"leashRange": 24,
"attacks": [
{
"id": "rw-hoto-basic-attack",
"id": "basic-attack",
"name": "Basic Attack",
"delivery": "melee",
"target": "primary",
@@ -936,7 +936,7 @@ export const HOTO_ENTITIES = {
"leashRange": 24,
"attacks": [
{
"id": "rw-hoto-basic-attack",
"id": "basic-attack",
"name": "Basic Attack",
"delivery": "melee",
"target": "primary",
@@ -976,7 +976,7 @@ export const HOTO_ENTITIES = {
"leashRange": 24,
"attacks": [
{
"id": "rw-hoto-basic-attack",
"id": "basic-attack",
"name": "Basic Attack",
"delivery": "melee",
"target": "primary",
@@ -1016,7 +1016,7 @@ export const HOTO_ENTITIES = {
"leashRange": 24,
"attacks": [
{
"id": "rw-hoto-basic-attack",
"id": "basic-attack",
"name": "Basic Attack",
"delivery": "melee",
"target": "primary",
@@ -1055,9 +1055,15 @@ export const HOTO_ENTITIES = {
"damageMultiplier": 1.3,
"moveSpeed": 2.25,
"leashRange": 32,
"mechanicIds": [
"rw-hoto-102021-aura-492669",
"rw-hoto-102021-spell-493215",
"rw-hoto-102021-spell-493307",
"rw-hoto-102021-aura-493308"
],
"attacks": [
{
"id": "rw-hoto-basic-attack",
"id": "basic-attack",
"name": "Basic Attack",
"delivery": "melee",
"target": "primary",
@@ -1068,13 +1074,67 @@ export const HOTO_ENTITIES = {
"damageMultiplier": 1
},
{
"id": "rw-hoto-spell-493307",
"id": "rw-hoto-102021-aura-492669",
"name": "Blue Explosion",
"delivery": "area",
"target": "self",
"school": "arcane",
"animation": "cast",
"castTimeMs": 1200,
"range": 0,
"cooldownMs": 15000,
"initialCooldownMs": [
15000,
15000
],
"repeatCooldownMs": [
15000,
15000
],
"damageMultiplier": 0,
"effect": {
"kind": "aura",
"aura": "blue-explosion",
"durationMs": 7500
}
},
{
"id": "rw-hoto-102021-spell-493215",
"name": "Water Gulp",
"spellId": 493215,
"delivery": "area",
"target": "nearby-party",
"school": "frost",
"animation": "cast",
"castTimeMs": 1800,
"range": 30,
"cooldownMs": 6500,
"initialCooldownMs": [
6500,
6500
],
"repeatCooldownMs": [
6500,
6500
],
"damageMultiplier": 0.85,
"radius": 15,
"telegraph": {
"shape": "circle",
"anchor": "source",
"warningMs": 1800,
"radius": 15
}
},
{
"id": "rw-hoto-102021-spell-493307",
"name": "Water Arrow",
"spellId": 493307,
"delivery": "projectile",
"target": "primary",
"school": "frost",
"animation": "cast",
"castTimeMs": 0,
"range": 30,
"cooldownMs": 10000,
"initialCooldownMs": [
@@ -1086,6 +1146,31 @@ export const HOTO_ENTITIES = {
10000
],
"damageMultiplier": 0.85
},
{
"id": "rw-hoto-102021-aura-493308",
"name": "Water Arrow Volley",
"delivery": "area",
"target": "self",
"school": "arcane",
"animation": "cast",
"castTimeMs": 1200,
"range": 0,
"cooldownMs": 25000,
"initialCooldownMs": [
25000,
25000
],
"repeatCooldownMs": [
25000,
25000
],
"damageMultiplier": 0,
"effect": {
"kind": "aura",
"aura": "water-arrow-volley",
"durationMs": 12000
}
}
]
},
@@ -1116,9 +1201,13 @@ export const HOTO_ENTITIES = {
"damageMultiplier": 1.3,
"moveSpeed": 2.25,
"leashRange": 32,
"mechanicIds": [
"rw-hoto-102046-spell-493237",
"rw-hoto-102046-spell-493239"
],
"attacks": [
{
"id": "rw-hoto-basic-attack",
"id": "basic-attack",
"name": "Basic Attack",
"delivery": "melee",
"target": "primary",
@@ -1129,13 +1218,42 @@ export const HOTO_ENTITIES = {
"damageMultiplier": 1
},
{
"id": "rw-hoto-spell-493239",
"id": "rw-hoto-102046-spell-493237",
"name": "Splinter Magic",
"spellId": 493237,
"delivery": "area",
"target": "nearby-party",
"school": "physical",
"animation": "attack",
"castTimeMs": 2000,
"range": 15,
"cooldownMs": 15000,
"initialCooldownMs": [
15000,
15000
],
"repeatCooldownMs": [
15000,
15000
],
"damageMultiplier": 0.85,
"radius": 15,
"telegraph": {
"shape": "circle",
"anchor": "source",
"warningMs": 1800,
"radius": 15
}
},
{
"id": "rw-hoto-102046-spell-493239",
"name": "Giant Claw Attack",
"spellId": 493239,
"delivery": "melee",
"target": "primary",
"school": "physical",
"animation": "attack",
"castTimeMs": 0,
"range": 5,
"cooldownMs": 10000,
"initialCooldownMs": [
@@ -1147,26 +1265,6 @@ export const HOTO_ENTITIES = {
10000
],
"damageMultiplier": 0.85
},
{
"id": "rw-hoto-spell-493237",
"name": "Splinter Magic",
"spellId": 493237,
"delivery": "melee",
"target": "primary",
"school": "physical",
"animation": "attack",
"range": 5,
"cooldownMs": 15000,
"initialCooldownMs": [
15000,
15000
],
"repeatCooldownMs": [
15000,
15000
],
"damageMultiplier": 0.85
}
]
},
@@ -1197,9 +1295,22 @@ export const HOTO_ENTITIES = {
"damageMultiplier": 1.3,
"moveSpeed": 2.25,
"leashRange": 32,
"mechanicIds": [
"rw-hoto-102040-aura-493207",
"rw-hoto-102040-aura-493208",
"rw-hoto-102040-debuff-493209",
"rw-hoto-102040-spell-493210",
"rw-hoto-102040-aura-493211",
"rw-hoto-102040-aura-493212",
"rw-hoto-102040-debuff-493305",
"rw-hoto-102040-aura-493525",
"rw-hoto-102040-aura-493578",
"rw-hoto-102040-spell-493580",
"rw-hoto-102040-aura-493595"
],
"attacks": [
{
"id": "rw-hoto-basic-attack",
"id": "basic-attack",
"name": "Basic Attack",
"delivery": "melee",
"target": "primary",
@@ -1208,6 +1319,280 @@ export const HOTO_ENTITIES = {
"range": 4.5,
"cooldownMs": 1900,
"damageMultiplier": 1
},
{
"id": "rw-hoto-102040-aura-493207",
"name": "Manastorms Duo Lv5",
"delivery": "area",
"target": "self",
"school": "arcane",
"animation": "cast",
"castTimeMs": 1200,
"range": 0,
"cooldownMs": 45000,
"initialCooldownMs": [
45000,
45000
],
"repeatCooldownMs": [
45000,
45000
],
"damageMultiplier": 0,
"effect": {
"kind": "aura",
"aura": "manastorms-duo-lv5",
"durationMs": 12000
}
},
{
"id": "rw-hoto-102040-aura-493208",
"name": "Snake Lady's Seduction",
"delivery": "area",
"target": "self",
"school": "arcane",
"animation": "cast",
"castTimeMs": 1200,
"range": 0,
"cooldownMs": 18000,
"initialCooldownMs": [
18000,
18000
],
"repeatCooldownMs": [
18000,
18000
],
"damageMultiplier": 0,
"effect": {
"kind": "aura",
"aura": "petrify",
"durationMs": 9000
}
},
{
"id": "rw-hoto-102040-debuff-493209",
"name": "Rock Binding Magic",
"delivery": "projectile",
"target": "primary",
"school": "arcane",
"animation": "cast",
"castTimeMs": 1800,
"range": 30,
"cooldownMs": 18000,
"initialCooldownMs": [
18000,
18000
],
"repeatCooldownMs": [
18000,
18000
],
"damageMultiplier": 0,
"effect": {
"kind": "target-aura",
"aura": "rock-bind",
"durationMs": 9000
}
},
{
"id": "rw-hoto-102040-spell-493210",
"name": "Rock Shattering Wave",
"spellId": 493210,
"delivery": "area",
"target": "nearby-party",
"school": "nature",
"animation": "cast",
"castTimeMs": 1800,
"range": 30,
"cooldownMs": 13000,
"initialCooldownMs": [
13000,
13000
],
"repeatCooldownMs": [
13000,
13000
],
"damageMultiplier": 0.85,
"radius": 15,
"telegraph": {
"shape": "circle",
"anchor": "source",
"warningMs": 1800,
"radius": 15
}
},
{
"id": "rw-hoto-102040-aura-493211",
"name": "Manastorms Duo Lv5",
"delivery": "area",
"target": "self",
"school": "arcane",
"animation": "cast",
"castTimeMs": 1200,
"range": 0,
"cooldownMs": 45000,
"initialCooldownMs": [
45000,
45000
],
"repeatCooldownMs": [
45000,
45000
],
"damageMultiplier": 0,
"effect": {
"kind": "aura",
"aura": "fortification",
"durationMs": 12000
}
},
{
"id": "rw-hoto-102040-aura-493212",
"name": "Summon Sound Shell",
"delivery": "area",
"target": "self",
"school": "arcane",
"animation": "cast",
"castTimeMs": 1200,
"range": 0,
"cooldownMs": 40000,
"initialCooldownMs": [
40000,
40000
],
"repeatCooldownMs": [
40000,
40000
],
"damageMultiplier": 0,
"effect": {
"kind": "aura",
"aura": "summon-sound-shell",
"durationMs": 12000
}
},
{
"id": "rw-hoto-102040-debuff-493305",
"name": "Magic Fortress",
"delivery": "projectile",
"target": "primary",
"school": "arcane",
"animation": "cast",
"castTimeMs": 1800,
"range": 30,
"cooldownMs": 30000,
"initialCooldownMs": [
30000,
30000
],
"repeatCooldownMs": [
30000,
30000
],
"damageMultiplier": 0,
"effect": {
"kind": "target-aura",
"aura": "magic-fortress",
"durationMs": 10000
}
},
{
"id": "rw-hoto-102040-aura-493525",
"name": "Manastorms Duo Lv5",
"delivery": "area",
"target": "self",
"school": "arcane",
"animation": "cast",
"castTimeMs": 10000,
"range": 0,
"cooldownMs": 15000,
"initialCooldownMs": [
15000,
15000
],
"repeatCooldownMs": [
15000,
15000
],
"damageMultiplier": 0,
"effect": {
"kind": "aura",
"aura": "manastorms-duo-lv5",
"durationMs": 7500
}
},
{
"id": "rw-hoto-102040-aura-493578",
"name": "Fatal Snake Woman Scent",
"delivery": "area",
"target": "self",
"school": "arcane",
"animation": "cast",
"castTimeMs": 1200,
"range": 0,
"cooldownMs": 25000,
"initialCooldownMs": [
25000,
25000
],
"repeatCooldownMs": [
25000,
25000
],
"damageMultiplier": 0,
"effect": {
"kind": "aura",
"aura": "fatal-snake-woman-scent",
"durationMs": 12000
}
},
{
"id": "rw-hoto-102040-spell-493580",
"name": "Snake Staff Attack",
"spellId": 493580,
"delivery": "melee",
"target": "primary",
"school": "physical",
"animation": "attack",
"castTimeMs": 0,
"range": 5,
"cooldownMs": 10000,
"initialCooldownMs": [
10000,
10000
],
"repeatCooldownMs": [
10000,
10000
],
"damageMultiplier": 0.85
},
{
"id": "rw-hoto-102040-aura-493595",
"name": "Manastorms Duo Lv5",
"delivery": "area",
"target": "self",
"school": "arcane",
"animation": "cast",
"castTimeMs": 1200,
"range": 0,
"cooldownMs": 35000,
"initialCooldownMs": [
35000,
35000
],
"repeatCooldownMs": [
35000,
35000
],
"damageMultiplier": 0,
"effect": {
"kind": "aura",
"aura": "manastorms-duo-lv5",
"durationMs": 12000
}
}
]
},
@@ -1239,7 +1624,7 @@ export const HOTO_ENTITIES = {
"leashRange": 24,
"attacks": [
{
"id": "rw-hoto-basic-attack",
"id": "basic-attack",
"name": "Basic Attack",
"delivery": "melee",
"target": "primary",
@@ -1279,7 +1664,7 @@ export const HOTO_ENTITIES = {
"leashRange": 24,
"attacks": [
{
"id": "rw-hoto-basic-attack",
"id": "basic-attack",
"name": "Basic Attack",
"delivery": "melee",
"target": "primary",
@@ -1358,9 +1743,14 @@ export const HOTO_ENTITIES = {
"damageMultiplier": 1.3,
"moveSpeed": 2.25,
"leashRange": 32,
"mechanicIds": [
"rw-hoto-102041-spell-493231",
"rw-hoto-102041-spell-493232",
"rw-hoto-102041-aura-493233"
],
"attacks": [
{
"id": "rw-hoto-basic-attack",
"id": "basic-attack",
"name": "Basic Attack",
"delivery": "melee",
"target": "primary",
@@ -1371,13 +1761,14 @@ export const HOTO_ENTITIES = {
"damageMultiplier": 1
},
{
"id": "rw-hoto-spell-493231",
"id": "rw-hoto-102041-spell-493231",
"name": "Big Ripper",
"spellId": 493231,
"delivery": "melee",
"target": "primary",
"school": "physical",
"animation": "attack",
"castTimeMs": 0,
"range": 5,
"cooldownMs": 10000,
"initialCooldownMs": [
@@ -1389,6 +1780,52 @@ export const HOTO_ENTITIES = {
10000
],
"damageMultiplier": 0.85
},
{
"id": "rw-hoto-102041-spell-493232",
"name": "Scaly Surprise",
"spellId": 493232,
"delivery": "projectile",
"target": "primary",
"school": "physical",
"animation": "cast",
"castTimeMs": 0,
"range": 30,
"cooldownMs": 10000,
"initialCooldownMs": [
10000,
10000
],
"repeatCooldownMs": [
10000,
10000
],
"damageMultiplier": 0.85
},
{
"id": "rw-hoto-102041-aura-493233",
"name": "Disperse Poison",
"delivery": "area",
"target": "self",
"school": "arcane",
"animation": "cast",
"castTimeMs": 2000,
"range": 0,
"cooldownMs": 15000,
"initialCooldownMs": [
15000,
15000
],
"repeatCooldownMs": [
15000,
15000
],
"damageMultiplier": 0,
"effect": {
"kind": "aura",
"aura": "disperse-poison",
"durationMs": 7500
}
}
]
},
@@ -1419,9 +1856,16 @@ export const HOTO_ENTITIES = {
"damageMultiplier": 1.3,
"moveSpeed": 2.25,
"leashRange": 32,
"mechanicIds": [
"rw-hoto-102063-aura-493187",
"rw-hoto-102063-aura-493188",
"rw-hoto-102063-spell-493189",
"rw-hoto-102063-spell-493221",
"rw-hoto-102063-aura-493492"
],
"attacks": [
{
"id": "rw-hoto-basic-attack",
"id": "basic-attack",
"name": "Basic Attack",
"delivery": "melee",
"target": "primary",
@@ -1430,6 +1874,130 @@ export const HOTO_ENTITIES = {
"range": 4.5,
"cooldownMs": 1900,
"damageMultiplier": 1
},
{
"id": "rw-hoto-102063-aura-493187",
"name": "Geba's Rage",
"delivery": "area",
"target": "self",
"school": "arcane",
"animation": "cast",
"castTimeMs": 1500,
"range": 0,
"cooldownMs": 15000,
"initialCooldownMs": [
15000,
15000
],
"repeatCooldownMs": [
15000,
15000
],
"damageMultiplier": 0,
"effect": {
"kind": "aura",
"aura": "geba-s-rage",
"durationMs": 7500
}
},
{
"id": "rw-hoto-102063-aura-493188",
"name": "Sonic Roar",
"delivery": "area",
"target": "self",
"school": "arcane",
"animation": "cast",
"castTimeMs": 1200,
"range": 0,
"cooldownMs": 15000,
"initialCooldownMs": [
15000,
15000
],
"repeatCooldownMs": [
15000,
15000
],
"damageMultiplier": 0,
"effect": {
"kind": "aura",
"aura": "sonic-roar",
"durationMs": 7500
}
},
{
"id": "rw-hoto-102063-spell-493189",
"name": "Geba's Fury",
"spellId": 493189,
"delivery": "projectile",
"target": "primary",
"school": "physical",
"animation": "cast",
"castTimeMs": 0,
"range": 30,
"cooldownMs": 6500,
"initialCooldownMs": [
6500,
6500
],
"repeatCooldownMs": [
6500,
6500
],
"damageMultiplier": 0.85
},
{
"id": "rw-hoto-102063-spell-493221",
"name": "Whirl Counterattack",
"spellId": 493221,
"delivery": "area",
"target": "nearby-party",
"school": "physical",
"animation": "attack",
"castTimeMs": 1800,
"range": 5,
"cooldownMs": 6500,
"initialCooldownMs": [
6500,
6500
],
"repeatCooldownMs": [
6500,
6500
],
"damageMultiplier": 0.85,
"radius": 5,
"telegraph": {
"shape": "circle",
"anchor": "source",
"warningMs": 1800,
"radius": 5
}
},
{
"id": "rw-hoto-102063-aura-493492",
"name": "Geba's Magic",
"delivery": "area",
"target": "self",
"school": "arcane",
"animation": "cast",
"castTimeMs": 2000,
"range": 0,
"cooldownMs": 15000,
"initialCooldownMs": [
15000,
15000
],
"repeatCooldownMs": [
15000,
15000
],
"damageMultiplier": 0,
"effect": {
"kind": "aura",
"aura": "geba-s-magic",
"durationMs": 7500
}
}
]
},
@@ -1461,7 +2029,7 @@ export const HOTO_ENTITIES = {
"leashRange": 24,
"attacks": [
{
"id": "rw-hoto-basic-attack",
"id": "basic-attack",
"name": "Basic Attack",
"delivery": "melee",
"target": "primary",
@@ -1541,7 +2109,7 @@ export const HOTO_ENTITIES = {
"leashRange": 24,
"attacks": [
{
"id": "rw-hoto-basic-attack",
"id": "basic-attack",
"name": "Basic Attack",
"delivery": "melee",
"target": "primary",
@@ -5078,6 +5646,6 @@ export const HOTO_POPULATION_PROVENANCE = {
"runeWakerIdsAreNamespaced": true,
"serverEntryPolicy": "RuneWaker numeric ids are never passed to AzerothCore serverEntry.",
"sourceSpellCatalog": "scripts/runewaker-pipeline/snapshots/runewaker-npc-spell-catalog.json",
"sourceSpellCatalogSha256": "ad29efc1d4906fdd4f2ac7a69316cac32d8fcbaea674678f050265c86e0c506c",
"sourceSpellCatalogSha256": "c47e67013e68ecf9c7ae30ef4b2058a850505385739dfe8c32ce8874e6ed077c",
"sourceSpellPolicy": "Only positively classified direct-HP-damage families execute; coefficients remain conservative HealerMan values."
} as const;
@@ -16,7 +16,7 @@ export const ICE_DWARF_KINGDOM_ENTITIES = {
"leashRange": 24,
"attacks": [
{
"id": "rw-ice-dwarf-kingdom-basic-attack",
"id": "basic-attack",
"name": "Basic Attack",
"delivery": "melee",
"target": "primary",
@@ -96,7 +96,7 @@ export const ICE_DWARF_KINGDOM_ENTITIES = {
"leashRange": 24,
"attacks": [
{
"id": "rw-ice-dwarf-kingdom-basic-attack",
"id": "basic-attack",
"name": "Basic Attack",
"delivery": "melee",
"target": "primary",
@@ -136,7 +136,7 @@ export const ICE_DWARF_KINGDOM_ENTITIES = {
"leashRange": 24,
"attacks": [
{
"id": "rw-ice-dwarf-kingdom-basic-attack",
"id": "basic-attack",
"name": "Basic Attack",
"delivery": "melee",
"target": "primary",
@@ -196,7 +196,7 @@ export const ICE_DWARF_KINGDOM_ENTITIES = {
"leashRange": 24,
"attacks": [
{
"id": "rw-ice-dwarf-kingdom-basic-attack",
"id": "basic-attack",
"name": "Basic Attack",
"delivery": "melee",
"target": "primary",
@@ -256,7 +256,7 @@ export const ICE_DWARF_KINGDOM_ENTITIES = {
"leashRange": 24,
"attacks": [
{
"id": "rw-ice-dwarf-kingdom-basic-attack",
"id": "basic-attack",
"name": "Basic Attack",
"delivery": "melee",
"target": "primary",
@@ -316,7 +316,7 @@ export const ICE_DWARF_KINGDOM_ENTITIES = {
"leashRange": 24,
"attacks": [
{
"id": "rw-ice-dwarf-kingdom-basic-attack",
"id": "basic-attack",
"name": "Basic Attack",
"delivery": "melee",
"target": "primary",
@@ -356,7 +356,7 @@ export const ICE_DWARF_KINGDOM_ENTITIES = {
"leashRange": 24,
"attacks": [
{
"id": "rw-ice-dwarf-kingdom-basic-attack",
"id": "basic-attack",
"name": "Basic Attack",
"delivery": "melee",
"target": "primary",
@@ -436,7 +436,7 @@ export const ICE_DWARF_KINGDOM_ENTITIES = {
"leashRange": 24,
"attacks": [
{
"id": "rw-ice-dwarf-kingdom-basic-attack",
"id": "basic-attack",
"name": "Basic Attack",
"delivery": "melee",
"target": "primary",
@@ -496,7 +496,7 @@ export const ICE_DWARF_KINGDOM_ENTITIES = {
"leashRange": 24,
"attacks": [
{
"id": "rw-ice-dwarf-kingdom-basic-attack",
"id": "basic-attack",
"name": "Basic Attack",
"delivery": "melee",
"target": "primary",
@@ -556,7 +556,7 @@ export const ICE_DWARF_KINGDOM_ENTITIES = {
"leashRange": 24,
"attacks": [
{
"id": "rw-ice-dwarf-kingdom-basic-attack",
"id": "basic-attack",
"name": "Basic Attack",
"delivery": "melee",
"target": "primary",
@@ -596,7 +596,7 @@ export const ICE_DWARF_KINGDOM_ENTITIES = {
"leashRange": 24,
"attacks": [
{
"id": "rw-ice-dwarf-kingdom-basic-attack",
"id": "basic-attack",
"name": "Basic Attack",
"delivery": "melee",
"target": "primary",
@@ -676,7 +676,7 @@ export const ICE_DWARF_KINGDOM_ENTITIES = {
"leashRange": 24,
"attacks": [
{
"id": "rw-ice-dwarf-kingdom-basic-attack",
"id": "basic-attack",
"name": "Basic Attack",
"delivery": "melee",
"target": "primary",
@@ -756,7 +756,7 @@ export const ICE_DWARF_KINGDOM_ENTITIES = {
"leashRange": 24,
"attacks": [
{
"id": "rw-ice-dwarf-kingdom-basic-attack",
"id": "basic-attack",
"name": "Basic Attack",
"delivery": "melee",
"target": "primary",
@@ -836,7 +836,7 @@ export const ICE_DWARF_KINGDOM_ENTITIES = {
"leashRange": 24,
"attacks": [
{
"id": "rw-ice-dwarf-kingdom-basic-attack",
"id": "basic-attack",
"name": "Basic Attack",
"delivery": "melee",
"target": "primary",
@@ -876,7 +876,7 @@ export const ICE_DWARF_KINGDOM_ENTITIES = {
"leashRange": 24,
"attacks": [
{
"id": "rw-ice-dwarf-kingdom-basic-attack",
"id": "basic-attack",
"name": "Basic Attack",
"delivery": "melee",
"target": "primary",
@@ -916,7 +916,7 @@ export const ICE_DWARF_KINGDOM_ENTITIES = {
"leashRange": 24,
"attacks": [
{
"id": "rw-ice-dwarf-kingdom-basic-attack",
"id": "basic-attack",
"name": "Basic Attack",
"delivery": "melee",
"target": "primary",
@@ -956,7 +956,7 @@ export const ICE_DWARF_KINGDOM_ENTITIES = {
"leashRange": 24,
"attacks": [
{
"id": "rw-ice-dwarf-kingdom-basic-attack",
"id": "basic-attack",
"name": "Basic Attack",
"delivery": "melee",
"target": "primary",
@@ -996,7 +996,7 @@ export const ICE_DWARF_KINGDOM_ENTITIES = {
"leashRange": 24,
"attacks": [
{
"id": "rw-ice-dwarf-kingdom-basic-attack",
"id": "basic-attack",
"name": "Basic Attack",
"delivery": "melee",
"target": "primary",
@@ -1036,7 +1036,7 @@ export const ICE_DWARF_KINGDOM_ENTITIES = {
"leashRange": 24,
"attacks": [
{
"id": "rw-ice-dwarf-kingdom-basic-attack",
"id": "basic-attack",
"name": "Basic Attack",
"delivery": "melee",
"target": "primary",
@@ -1115,9 +1115,12 @@ export const ICE_DWARF_KINGDOM_ENTITIES = {
"damageMultiplier": 1.3,
"moveSpeed": 2.25,
"leashRange": 32,
"mechanicIds": [
"rw-ice-dwarf-kingdom-101881-debuff-492657"
],
"attacks": [
{
"id": "rw-ice-dwarf-kingdom-basic-attack",
"id": "basic-attack",
"name": "Basic Attack",
"delivery": "melee",
"target": "primary",
@@ -1126,6 +1129,31 @@ export const ICE_DWARF_KINGDOM_ENTITIES = {
"range": 4.5,
"cooldownMs": 1900,
"damageMultiplier": 1
},
{
"id": "rw-ice-dwarf-kingdom-101881-debuff-492657",
"name": "Conductivity",
"delivery": "projectile",
"target": "primary",
"school": "arcane",
"animation": "cast",
"castTimeMs": 1800,
"range": 30,
"cooldownMs": 6000,
"initialCooldownMs": [
6000,
6000
],
"repeatCooldownMs": [
6000,
6000
],
"damageMultiplier": 0,
"effect": {
"kind": "target-aura",
"aura": "conductivity",
"durationMs": 4000
}
}
]
},
@@ -1156,9 +1184,17 @@ export const ICE_DWARF_KINGDOM_ENTITIES = {
"damageMultiplier": 1.3,
"moveSpeed": 2.25,
"leashRange": 32,
"mechanicIds": [
"rw-ice-dwarf-kingdom-101883-debuff-492533",
"rw-ice-dwarf-kingdom-101883-spell-492587",
"rw-ice-dwarf-kingdom-101883-spell-492601",
"rw-ice-dwarf-kingdom-101883-debuff-492602",
"rw-ice-dwarf-kingdom-101883-debuff-493085",
"rw-ice-dwarf-kingdom-101883-debuff-493241"
],
"attacks": [
{
"id": "rw-ice-dwarf-kingdom-basic-attack",
"id": "basic-attack",
"name": "Basic Attack",
"delivery": "melee",
"target": "primary",
@@ -1167,6 +1203,155 @@ export const ICE_DWARF_KINGDOM_ENTITIES = {
"range": 4.5,
"cooldownMs": 1900,
"damageMultiplier": 1
},
{
"id": "rw-ice-dwarf-kingdom-101883-debuff-492533",
"name": "Runic Decline",
"delivery": "area",
"target": "nearby-party",
"school": "arcane",
"animation": "cast",
"castTimeMs": 1800,
"range": 20,
"cooldownMs": 12000,
"initialCooldownMs": [
12000,
12000
],
"repeatCooldownMs": [
12000,
12000
],
"damageMultiplier": 0,
"radius": 3,
"telegraph": {
"shape": "circle",
"anchor": "source",
"warningMs": 1800,
"radius": 3
},
"effect": {
"kind": "target-aura",
"aura": "runic-decline",
"durationMs": 6000
}
},
{
"id": "rw-ice-dwarf-kingdom-101883-spell-492587",
"name": "Energy Burn",
"spellId": 492587,
"delivery": "melee",
"target": "primary",
"school": "shadow",
"animation": "cast",
"castTimeMs": 0,
"range": 4.5,
"cooldownMs": 6500,
"initialCooldownMs": [
6500,
6500
],
"repeatCooldownMs": [
6500,
6500
],
"damageMultiplier": 0.85
},
{
"id": "rw-ice-dwarf-kingdom-101883-spell-492601",
"name": "Triple Slash",
"spellId": 492601,
"delivery": "projectile",
"target": "primary",
"school": "shadow",
"animation": "cast",
"castTimeMs": 2000,
"range": 7.5,
"cooldownMs": 20000,
"initialCooldownMs": [
20000,
20000
],
"repeatCooldownMs": [
20000,
20000
],
"damageMultiplier": 0.85
},
{
"id": "rw-ice-dwarf-kingdom-101883-debuff-492602",
"name": "Slipping Ice Block",
"delivery": "projectile",
"target": "primary",
"school": "arcane",
"animation": "cast",
"castTimeMs": 1800,
"range": 25,
"cooldownMs": 20000,
"initialCooldownMs": [
20000,
20000
],
"repeatCooldownMs": [
20000,
20000
],
"damageMultiplier": 0,
"effect": {
"kind": "target-aura",
"aura": "slipping-ice-block",
"durationMs": 10000
}
},
{
"id": "rw-ice-dwarf-kingdom-101883-debuff-493085",
"name": "RuneWaker Spell 493085",
"delivery": "projectile",
"target": "primary",
"school": "arcane",
"animation": "cast",
"castTimeMs": 1800,
"range": 20,
"cooldownMs": 12000,
"initialCooldownMs": [
12000,
12000
],
"repeatCooldownMs": [
12000,
12000
],
"damageMultiplier": 0,
"effect": {
"kind": "target-aura",
"aura": "repair",
"durationMs": 6000
}
},
{
"id": "rw-ice-dwarf-kingdom-101883-debuff-493241",
"name": "Manastorms Duo Lv5",
"delivery": "projectile",
"target": "primary",
"school": "arcane",
"animation": "cast",
"castTimeMs": 1800,
"range": 30,
"cooldownMs": 15000,
"initialCooldownMs": [
15000,
15000
],
"repeatCooldownMs": [
15000,
15000
],
"damageMultiplier": 0,
"effect": {
"kind": "target-aura",
"aura": "food-preservative",
"durationMs": 7500
}
}
]
},
@@ -1197,9 +1382,13 @@ export const ICE_DWARF_KINGDOM_ENTITIES = {
"damageMultiplier": 1.3,
"moveSpeed": 2.25,
"leashRange": 32,
"mechanicIds": [
"rw-ice-dwarf-kingdom-101884-spell-492759",
"rw-ice-dwarf-kingdom-101884-spell-492760"
],
"attacks": [
{
"id": "rw-ice-dwarf-kingdom-basic-attack",
"id": "basic-attack",
"name": "Basic Attack",
"delivery": "melee",
"target": "primary",
@@ -1210,13 +1399,14 @@ export const ICE_DWARF_KINGDOM_ENTITIES = {
"damageMultiplier": 1
},
{
"id": "rw-ice-dwarf-kingdom-spell-492759",
"id": "rw-ice-dwarf-kingdom-101884-spell-492759",
"name": "Raging Flames",
"spellId": 492759,
"delivery": "melee",
"target": "primary",
"delivery": "area",
"target": "nearby-party",
"school": "fire",
"animation": "cast",
"castTimeMs": 1800,
"range": 5,
"cooldownMs": 15000,
"initialCooldownMs": [
@@ -1227,16 +1417,24 @@ export const ICE_DWARF_KINGDOM_ENTITIES = {
15000,
15000
],
"damageMultiplier": 0.85
"damageMultiplier": 0.85,
"radius": 3,
"telegraph": {
"shape": "circle",
"anchor": "source",
"warningMs": 1800,
"radius": 3
}
},
{
"id": "rw-ice-dwarf-kingdom-spell-492760",
"id": "rw-ice-dwarf-kingdom-101884-spell-492760",
"name": "Bash",
"spellId": 492760,
"delivery": "melee",
"target": "primary",
"school": "physical",
"animation": "attack",
"castTimeMs": 0,
"range": 5,
"cooldownMs": 6500,
"initialCooldownMs": [
@@ -1278,9 +1476,14 @@ export const ICE_DWARF_KINGDOM_ENTITIES = {
"damageMultiplier": 1.3,
"moveSpeed": 2.25,
"leashRange": 32,
"mechanicIds": [
"rw-ice-dwarf-kingdom-101268-spell-492821",
"rw-ice-dwarf-kingdom-101268-debuff-492822",
"rw-ice-dwarf-kingdom-101268-spell-492841"
],
"attacks": [
{
"id": "rw-ice-dwarf-kingdom-basic-attack",
"id": "basic-attack",
"name": "Basic Attack",
"delivery": "melee",
"target": "primary",
@@ -1291,34 +1494,15 @@ export const ICE_DWARF_KINGDOM_ENTITIES = {
"damageMultiplier": 1
},
{
"id": "rw-ice-dwarf-kingdom-spell-492841",
"name": "Boulder Split",
"spellId": 492841,
"delivery": "projectile",
"target": "primary",
"school": "nature",
"animation": "cast",
"range": 10,
"cooldownMs": 20000,
"initialCooldownMs": [
20000,
20000
],
"repeatCooldownMs": [
20000,
20000
],
"damageMultiplier": 0.85
},
{
"id": "rw-ice-dwarf-kingdom-spell-492821",
"id": "rw-ice-dwarf-kingdom-101268-spell-492821",
"name": "Terrain Clear",
"spellId": 492821,
"delivery": "projectile",
"target": "primary",
"delivery": "area",
"target": "nearby-party",
"school": "nature",
"animation": "cast",
"range": 6,
"castTimeMs": 5000,
"range": 14,
"cooldownMs": 3000,
"initialCooldownMs": [
3000,
@@ -1328,7 +1512,74 @@ export const ICE_DWARF_KINGDOM_ENTITIES = {
3000,
3000
],
"damageMultiplier": 0.85
"damageMultiplier": 0.85,
"radius": 14,
"telegraph": {
"shape": "circle",
"anchor": "source",
"warningMs": 1800,
"radius": 14
}
},
{
"id": "rw-ice-dwarf-kingdom-101268-debuff-492822",
"name": "Dust",
"delivery": "area",
"target": "nearby-party",
"school": "arcane",
"animation": "cast",
"castTimeMs": 1800,
"range": 30,
"cooldownMs": 6000,
"initialCooldownMs": [
6000,
6000
],
"repeatCooldownMs": [
6000,
6000
],
"damageMultiplier": 0,
"radius": 15,
"telegraph": {
"shape": "circle",
"anchor": "source",
"warningMs": 1800,
"radius": 15
},
"effect": {
"kind": "target-aura",
"aura": "dust",
"durationMs": 4000
}
},
{
"id": "rw-ice-dwarf-kingdom-101268-spell-492841",
"name": "Boulder Split",
"spellId": 492841,
"delivery": "area",
"target": "nearby-party",
"school": "nature",
"animation": "cast",
"castTimeMs": 1800,
"range": 14,
"cooldownMs": 20000,
"initialCooldownMs": [
20000,
20000
],
"repeatCooldownMs": [
20000,
20000
],
"damageMultiplier": 0.85,
"radius": 14,
"telegraph": {
"shape": "circle",
"anchor": "source",
"warningMs": 1800,
"radius": 14
}
}
]
},
@@ -1359,9 +1610,15 @@ export const ICE_DWARF_KINGDOM_ENTITIES = {
"damageMultiplier": 1.3,
"moveSpeed": 2.25,
"leashRange": 32,
"mechanicIds": [
"rw-ice-dwarf-kingdom-101584-spell-492847",
"rw-ice-dwarf-kingdom-101584-debuff-492848",
"rw-ice-dwarf-kingdom-101584-aura-492850",
"rw-ice-dwarf-kingdom-101584-debuff-492853"
],
"attacks": [
{
"id": "rw-ice-dwarf-kingdom-basic-attack",
"id": "basic-attack",
"name": "Basic Attack",
"delivery": "melee",
"target": "primary",
@@ -1370,6 +1627,123 @@ export const ICE_DWARF_KINGDOM_ENTITIES = {
"range": 4.5,
"cooldownMs": 1900,
"damageMultiplier": 1
},
{
"id": "rw-ice-dwarf-kingdom-101584-spell-492847",
"name": "Ancient Fire",
"spellId": 492847,
"delivery": "area",
"target": "nearby-party",
"school": "fire",
"animation": "cast",
"castTimeMs": 1800,
"range": 15,
"cooldownMs": 10000,
"initialCooldownMs": [
10000,
10000
],
"repeatCooldownMs": [
10000,
10000
],
"damageMultiplier": 0.85,
"radius": 15,
"telegraph": {
"shape": "circle",
"anchor": "source",
"warningMs": 1800,
"radius": 15
}
},
{
"id": "rw-ice-dwarf-kingdom-101584-debuff-492848",
"name": "Fire Chaos",
"delivery": "area",
"target": "nearby-party",
"school": "arcane",
"animation": "cast",
"castTimeMs": 1800,
"range": 30,
"cooldownMs": 12000,
"initialCooldownMs": [
12000,
12000
],
"repeatCooldownMs": [
12000,
12000
],
"damageMultiplier": 0,
"radius": 7,
"telegraph": {
"shape": "circle",
"anchor": "source",
"warningMs": 1800,
"radius": 7
},
"effect": {
"kind": "target-aura",
"aura": "fire-chaos",
"durationMs": 6000
}
},
{
"id": "rw-ice-dwarf-kingdom-101584-aura-492850",
"name": "Hellish Rage",
"delivery": "area",
"target": "self",
"school": "arcane",
"animation": "cast",
"castTimeMs": 1200,
"range": 0,
"cooldownMs": 20000,
"initialCooldownMs": [
20000,
20000
],
"repeatCooldownMs": [
20000,
20000
],
"damageMultiplier": 0,
"effect": {
"kind": "aura",
"aura": "hellish-rage",
"durationMs": 10000
}
},
{
"id": "rw-ice-dwarf-kingdom-101584-debuff-492853",
"name": "RuneWaker Spell 492853",
"delivery": "area",
"target": "nearby-party",
"school": "arcane",
"animation": "cast",
"castTimeMs": 1800,
"range": 30,
"cooldownMs": 12000,
"initialCooldownMs": [
12000,
12000
],
"repeatCooldownMs": [
12000,
12000
],
"damageMultiplier": 0,
"radius": 15,
"telegraph": {
"shape": "circle",
"anchor": "source",
"warningMs": 1800,
"radius": 15
},
"effect": {
"kind": "target-aura",
"aura": "manastorms-duo-lv5",
"durationMs": 6000
}
}
]
},
@@ -1401,7 +1775,7 @@ export const ICE_DWARF_KINGDOM_ENTITIES = {
"leashRange": 24,
"attacks": [
{
"id": "rw-ice-dwarf-kingdom-basic-attack",
"id": "basic-attack",
"name": "Basic Attack",
"delivery": "melee",
"target": "primary",
@@ -1501,7 +1875,7 @@ export const ICE_DWARF_KINGDOM_ENTITIES = {
"leashRange": 24,
"attacks": [
{
"id": "rw-ice-dwarf-kingdom-basic-attack",
"id": "basic-attack",
"name": "Basic Attack",
"delivery": "melee",
"target": "primary",
@@ -1581,7 +1955,7 @@ export const ICE_DWARF_KINGDOM_ENTITIES = {
"leashRange": 24,
"attacks": [
{
"id": "rw-ice-dwarf-kingdom-basic-attack",
"id": "basic-attack",
"name": "Basic Attack",
"delivery": "melee",
"target": "primary",
@@ -1681,7 +2055,7 @@ export const ICE_DWARF_KINGDOM_ENTITIES = {
"leashRange": 24,
"attacks": [
{
"id": "rw-ice-dwarf-kingdom-basic-attack",
"id": "basic-attack",
"name": "Basic Attack",
"delivery": "melee",
"target": "primary",
@@ -1741,7 +2115,7 @@ export const ICE_DWARF_KINGDOM_ENTITIES = {
"leashRange": 24,
"attacks": [
{
"id": "rw-ice-dwarf-kingdom-basic-attack",
"id": "basic-attack",
"name": "Basic Attack",
"delivery": "melee",
"target": "primary",
@@ -1801,7 +2175,7 @@ export const ICE_DWARF_KINGDOM_ENTITIES = {
"leashRange": 24,
"attacks": [
{
"id": "rw-ice-dwarf-kingdom-basic-attack",
"id": "basic-attack",
"name": "Basic Attack",
"delivery": "melee",
"target": "primary",
@@ -1881,7 +2255,7 @@ export const ICE_DWARF_KINGDOM_ENTITIES = {
"leashRange": 24,
"attacks": [
{
"id": "rw-ice-dwarf-kingdom-basic-attack",
"id": "basic-attack",
"name": "Basic Attack",
"delivery": "melee",
"target": "primary",
@@ -1961,7 +2335,7 @@ export const ICE_DWARF_KINGDOM_ENTITIES = {
"leashRange": 24,
"attacks": [
{
"id": "rw-ice-dwarf-kingdom-basic-attack",
"id": "basic-attack",
"name": "Basic Attack",
"delivery": "melee",
"target": "primary",
@@ -8690,6 +9064,6 @@ export const ICE_DWARF_KINGDOM_POPULATION_PROVENANCE = {
"runeWakerIdsAreNamespaced": true,
"serverEntryPolicy": "RuneWaker numeric ids are never passed to AzerothCore serverEntry.",
"sourceSpellCatalog": "scripts/runewaker-pipeline/snapshots/runewaker-npc-spell-catalog.json",
"sourceSpellCatalogSha256": "ad29efc1d4906fdd4f2ac7a69316cac32d8fcbaea674678f050265c86e0c506c",
"sourceSpellCatalogSha256": "c47e67013e68ecf9c7ae30ef4b2058a850505385739dfe8c32ce8874e6ed077c",
"sourceSpellPolicy": "Only positively classified direct-HP-damage families execute; coefficients remain conservative HealerMan values."
} as const;
@@ -16,7 +16,7 @@ export const KAL_TUROK_HIVE_ENTITIES = {
"leashRange": 24,
"attacks": [
{
"id": "rw-kal-turok-hive-basic-attack",
"id": "basic-attack",
"name": "Basic Attack",
"delivery": "melee",
"target": "primary",
@@ -181,6 +181,6 @@ export const KAL_TUROK_HIVE_POPULATION_PROVENANCE = {
"runeWakerIdsAreNamespaced": true,
"serverEntryPolicy": "RuneWaker numeric ids are never passed to AzerothCore serverEntry.",
"sourceSpellCatalog": "scripts/runewaker-pipeline/snapshots/runewaker-npc-spell-catalog.json",
"sourceSpellCatalogSha256": "ad29efc1d4906fdd4f2ac7a69316cac32d8fcbaea674678f050265c86e0c506c",
"sourceSpellCatalogSha256": "c47e67013e68ecf9c7ae30ef4b2058a850505385739dfe8c32ce8874e6ed077c",
"sourceSpellPolicy": "Only positively classified direct-HP-damage families execute; coefficients remain conservative HealerMan values."
} as const;
@@ -16,7 +16,7 @@ export const KALINS_SHRINE_ENTITIES = {
"leashRange": 24,
"attacks": [
{
"id": "rw-kalins-shrine-basic-attack",
"id": "basic-attack",
"name": "Basic Attack",
"delivery": "melee",
"target": "primary",
@@ -56,7 +56,7 @@ export const KALINS_SHRINE_ENTITIES = {
"leashRange": 24,
"attacks": [
{
"id": "rw-kalins-shrine-basic-attack",
"id": "basic-attack",
"name": "Basic Attack",
"delivery": "melee",
"target": "primary",
@@ -96,7 +96,7 @@ export const KALINS_SHRINE_ENTITIES = {
"leashRange": 24,
"attacks": [
{
"id": "rw-kalins-shrine-basic-attack",
"id": "basic-attack",
"name": "Basic Attack",
"delivery": "melee",
"target": "primary",
@@ -156,7 +156,7 @@ export const KALINS_SHRINE_ENTITIES = {
"leashRange": 24,
"attacks": [
{
"id": "rw-kalins-shrine-basic-attack",
"id": "basic-attack",
"name": "Basic Attack",
"delivery": "melee",
"target": "primary",
@@ -216,7 +216,7 @@ export const KALINS_SHRINE_ENTITIES = {
"leashRange": 24,
"attacks": [
{
"id": "rw-kalins-shrine-basic-attack",
"id": "basic-attack",
"name": "Basic Attack",
"delivery": "melee",
"target": "primary",
@@ -276,7 +276,7 @@ export const KALINS_SHRINE_ENTITIES = {
"leashRange": 24,
"attacks": [
{
"id": "rw-kalins-shrine-basic-attack",
"id": "basic-attack",
"name": "Basic Attack",
"delivery": "melee",
"target": "primary",
@@ -356,7 +356,7 @@ export const KALINS_SHRINE_ENTITIES = {
"leashRange": 24,
"attacks": [
{
"id": "rw-kalins-shrine-basic-attack",
"id": "basic-attack",
"name": "Basic Attack",
"delivery": "melee",
"target": "primary",
@@ -436,7 +436,7 @@ export const KALINS_SHRINE_ENTITIES = {
"leashRange": 24,
"attacks": [
{
"id": "rw-kalins-shrine-basic-attack",
"id": "basic-attack",
"name": "Basic Attack",
"delivery": "melee",
"target": "primary",
@@ -516,7 +516,7 @@ export const KALINS_SHRINE_ENTITIES = {
"leashRange": 24,
"attacks": [
{
"id": "rw-kalins-shrine-basic-attack",
"id": "basic-attack",
"name": "Basic Attack",
"delivery": "melee",
"target": "primary",
@@ -595,9 +595,15 @@ export const KALINS_SHRINE_ENTITIES = {
"damageMultiplier": 1.3,
"moveSpeed": 2.25,
"leashRange": 32,
"mechanicIds": [
"rw-kalins-shrine-101507-spell-492371",
"rw-kalins-shrine-101507-spell-492372",
"rw-kalins-shrine-101507-spell-492373",
"rw-kalins-shrine-101507-aura-492374"
],
"attacks": [
{
"id": "rw-kalins-shrine-basic-attack",
"id": "basic-attack",
"name": "Basic Attack",
"delivery": "melee",
"target": "primary",
@@ -608,13 +614,42 @@ export const KALINS_SHRINE_ENTITIES = {
"damageMultiplier": 1
},
{
"id": "rw-kalins-shrine-spell-492372",
"id": "rw-kalins-shrine-101507-spell-492371",
"name": "Power Build-Up Strike",
"spellId": 492371,
"delivery": "area",
"target": "nearby-party",
"school": "physical",
"animation": "attack",
"castTimeMs": 4000,
"range": 5,
"cooldownMs": 8000,
"initialCooldownMs": [
8000,
8000
],
"repeatCooldownMs": [
8000,
8000
],
"damageMultiplier": 0.85,
"radius": 5,
"telegraph": {
"shape": "circle",
"anchor": "source",
"warningMs": 1800,
"radius": 5
}
},
{
"id": "rw-kalins-shrine-101507-spell-492372",
"name": "Earth Shattering Attack",
"spellId": 492372,
"delivery": "projectile",
"target": "primary",
"school": "physical",
"animation": "cast",
"castTimeMs": 2500,
"range": 25,
"cooldownMs": 20000,
"initialCooldownMs": [
@@ -628,13 +663,14 @@ export const KALINS_SHRINE_ENTITIES = {
"damageMultiplier": 0.85
},
{
"id": "rw-kalins-shrine-spell-492373",
"id": "rw-kalins-shrine-101507-spell-492373",
"name": "Hit on the Head",
"spellId": 492373,
"delivery": "melee",
"target": "primary",
"school": "physical",
"animation": "attack",
"castTimeMs": 0,
"range": 5,
"cooldownMs": 20000,
"initialCooldownMs": [
@@ -648,24 +684,29 @@ export const KALINS_SHRINE_ENTITIES = {
"damageMultiplier": 0.85
},
{
"id": "rw-kalins-shrine-spell-492371",
"name": "Power Build-Up Strike",
"spellId": 492371,
"delivery": "melee",
"target": "primary",
"school": "physical",
"animation": "attack",
"range": 5,
"cooldownMs": 8000,
"id": "rw-kalins-shrine-101507-aura-492374",
"name": "Steel Body Protection",
"delivery": "area",
"target": "self",
"school": "arcane",
"animation": "cast",
"castTimeMs": 1200,
"range": 0,
"cooldownMs": 45000,
"initialCooldownMs": [
8000,
8000
45000,
45000
],
"repeatCooldownMs": [
8000,
8000
45000,
45000
],
"damageMultiplier": 0.85
"damageMultiplier": 0,
"effect": {
"kind": "aura",
"aura": "steel-reinforcement",
"durationMs": 12000
}
}
]
},
@@ -696,9 +737,16 @@ export const KALINS_SHRINE_ENTITIES = {
"damageMultiplier": 1.3,
"moveSpeed": 2.25,
"leashRange": 32,
"mechanicIds": [
"rw-kalins-shrine-101502-spell-492375",
"rw-kalins-shrine-101502-debuff-492376",
"rw-kalins-shrine-101502-debuff-492377",
"rw-kalins-shrine-101502-aura-492536",
"rw-kalins-shrine-101502-debuff-492778"
],
"attacks": [
{
"id": "rw-kalins-shrine-basic-attack",
"id": "basic-attack",
"name": "Basic Attack",
"delivery": "melee",
"target": "primary",
@@ -709,13 +757,14 @@ export const KALINS_SHRINE_ENTITIES = {
"damageMultiplier": 1
},
{
"id": "rw-kalins-shrine-spell-492375",
"id": "rw-kalins-shrine-101502-spell-492375",
"name": "Icy Water Cannon",
"spellId": 492375,
"delivery": "projectile",
"target": "primary",
"school": "frost",
"animation": "cast",
"castTimeMs": 2000,
"range": 20,
"cooldownMs": 5000,
"initialCooldownMs": [
@@ -727,6 +776,113 @@ export const KALINS_SHRINE_ENTITIES = {
5000
],
"damageMultiplier": 0.85
},
{
"id": "rw-kalins-shrine-101502-debuff-492376",
"name": "Spray Mucus",
"delivery": "projectile",
"target": "primary",
"school": "arcane",
"animation": "cast",
"castTimeMs": 2000,
"range": 20,
"cooldownMs": 6000,
"initialCooldownMs": [
6000,
6000
],
"repeatCooldownMs": [
6000,
6000
],
"damageMultiplier": 0,
"effect": {
"kind": "target-aura",
"aura": "sticky",
"durationMs": 4000
}
},
{
"id": "rw-kalins-shrine-101502-debuff-492377",
"name": "Strange Egg Parasites",
"delivery": "projectile",
"target": "primary",
"school": "arcane",
"animation": "cast",
"castTimeMs": 1800,
"range": 30,
"cooldownMs": 20000,
"initialCooldownMs": [
20000,
20000
],
"repeatCooldownMs": [
20000,
20000
],
"damageMultiplier": 0,
"effect": {
"kind": "target-aura",
"aura": "strange-egg-parasites",
"durationMs": 10000
}
},
{
"id": "rw-kalins-shrine-101502-aura-492536",
"name": "Strange Egg Parasites",
"delivery": "area",
"target": "self",
"school": "arcane",
"animation": "cast",
"castTimeMs": 1200,
"range": 0,
"cooldownMs": 15000,
"initialCooldownMs": [
15000,
15000
],
"repeatCooldownMs": [
15000,
15000
],
"damageMultiplier": 0,
"effect": {
"kind": "aura",
"aura": "strange-egg-parasites",
"durationMs": 7500
}
},
{
"id": "rw-kalins-shrine-101502-debuff-492778",
"name": "Strange Egg Parasites",
"delivery": "area",
"target": "nearby-party",
"school": "arcane",
"animation": "cast",
"castTimeMs": 1800,
"range": 30,
"cooldownMs": 10000,
"initialCooldownMs": [
10000,
10000
],
"repeatCooldownMs": [
10000,
10000
],
"damageMultiplier": 0,
"radius": 15,
"telegraph": {
"shape": "circle",
"anchor": "source",
"warningMs": 1800,
"radius": 15
},
"effect": {
"kind": "target-aura",
"aura": "strange-egg-parasites",
"durationMs": 5000
}
}
]
},
@@ -757,9 +913,16 @@ export const KALINS_SHRINE_ENTITIES = {
"damageMultiplier": 1.3,
"moveSpeed": 2.25,
"leashRange": 32,
"mechanicIds": [
"rw-kalins-shrine-101503-aura-490656",
"rw-kalins-shrine-101503-aura-492569",
"rw-kalins-shrine-101503-aura-492570",
"rw-kalins-shrine-101503-aura-492571",
"rw-kalins-shrine-101503-control-492682"
],
"attacks": [
{
"id": "rw-kalins-shrine-basic-attack",
"id": "basic-attack",
"name": "Basic Attack",
"delivery": "melee",
"target": "primary",
@@ -768,6 +931,138 @@ export const KALINS_SHRINE_ENTITIES = {
"range": 4.5,
"cooldownMs": 1900,
"damageMultiplier": 1
},
{
"id": "rw-kalins-shrine-101503-aura-490656",
"name": "Wave Armor",
"delivery": "area",
"target": "self",
"school": "arcane",
"animation": "cast",
"castTimeMs": 1200,
"range": 0,
"cooldownMs": 45000,
"initialCooldownMs": [
45000,
45000
],
"repeatCooldownMs": [
45000,
45000
],
"damageMultiplier": 0,
"effect": {
"kind": "aura",
"aura": "wave-armor",
"durationMs": 12000
}
},
{
"id": "rw-kalins-shrine-101503-aura-492569",
"name": "Kiss of Blood Clot",
"delivery": "area",
"target": "self",
"school": "arcane",
"animation": "cast",
"castTimeMs": 1200,
"range": 0,
"cooldownMs": 15000,
"initialCooldownMs": [
15000,
15000
],
"repeatCooldownMs": [
15000,
15000
],
"damageMultiplier": 0,
"effect": {
"kind": "aura",
"aura": "kiss-of-blood-clot",
"durationMs": 7500
}
},
{
"id": "rw-kalins-shrine-101503-aura-492570",
"name": "Lament of Dejection",
"delivery": "area",
"target": "self",
"school": "arcane",
"animation": "cast",
"castTimeMs": 1200,
"range": 0,
"cooldownMs": 15000,
"initialCooldownMs": [
15000,
15000
],
"repeatCooldownMs": [
15000,
15000
],
"damageMultiplier": 0,
"effect": {
"kind": "aura",
"aura": "lament-of-dejection",
"durationMs": 7500
}
},
{
"id": "rw-kalins-shrine-101503-aura-492571",
"name": "Confusion Curse",
"delivery": "area",
"target": "self",
"school": "arcane",
"animation": "cast",
"castTimeMs": 1200,
"range": 0,
"cooldownMs": 10000,
"initialCooldownMs": [
10000,
10000
],
"repeatCooldownMs": [
10000,
10000
],
"damageMultiplier": 0,
"effect": {
"kind": "aura",
"aura": "confusion-curse",
"durationMs": 5000
}
},
{
"id": "rw-kalins-shrine-101503-control-492682",
"name": "Witch's Song",
"delivery": "area",
"target": "nearby-party",
"school": "shadow",
"animation": "cast",
"castTimeMs": 1800,
"range": 4.5,
"cooldownMs": 25000,
"initialCooldownMs": [
25000,
25000
],
"repeatCooldownMs": [
25000,
25000
],
"damageMultiplier": 0,
"radius": 15,
"telegraph": {
"shape": "circle",
"anchor": "source",
"warningMs": 1800,
"radius": 15
},
"effect": {
"kind": "control",
"mechanic": "fear",
"durationMs": 3000
}
}
]
},
@@ -798,9 +1093,15 @@ export const KALINS_SHRINE_ENTITIES = {
"damageMultiplier": 1.3,
"moveSpeed": 2.25,
"leashRange": 32,
"mechanicIds": [
"rw-kalins-shrine-101500-spell-490477",
"rw-kalins-shrine-101500-spell-492367",
"rw-kalins-shrine-101500-debuff-492383",
"rw-kalins-shrine-101500-aura-492384"
],
"attacks": [
{
"id": "rw-kalins-shrine-basic-attack",
"id": "basic-attack",
"name": "Basic Attack",
"delivery": "melee",
"target": "primary",
@@ -811,33 +1112,14 @@ export const KALINS_SHRINE_ENTITIES = {
"damageMultiplier": 1
},
{
"id": "rw-kalins-shrine-spell-492367",
"name": "Thorny Road",
"spellId": 492367,
"delivery": "projectile",
"target": "primary",
"school": "physical",
"animation": "cast",
"range": 25,
"cooldownMs": 15000,
"initialCooldownMs": [
15000,
15000
],
"repeatCooldownMs": [
15000,
15000
],
"damageMultiplier": 0.85
},
{
"id": "rw-kalins-shrine-spell-490477",
"id": "rw-kalins-shrine-101500-spell-490477",
"name": "Slash",
"spellId": 490477,
"delivery": "melee",
"target": "primary",
"school": "physical",
"animation": "attack",
"castTimeMs": 0,
"range": 5,
"cooldownMs": 10000,
"initialCooldownMs": [
@@ -849,6 +1131,84 @@ export const KALINS_SHRINE_ENTITIES = {
10000
],
"damageMultiplier": 0.85
},
{
"id": "rw-kalins-shrine-101500-spell-492367",
"name": "Thorny Road",
"spellId": 492367,
"delivery": "area",
"target": "nearby-party",
"school": "physical",
"animation": "cast",
"castTimeMs": 1800,
"range": 25,
"cooldownMs": 15000,
"initialCooldownMs": [
15000,
15000
],
"repeatCooldownMs": [
15000,
15000
],
"damageMultiplier": 0.85,
"radius": 15,
"telegraph": {
"shape": "circle",
"anchor": "source",
"warningMs": 1800,
"radius": 15
}
},
{
"id": "rw-kalins-shrine-101500-debuff-492383",
"name": "Wine Spirit's Invitation",
"delivery": "projectile",
"target": "primary",
"school": "arcane",
"animation": "cast",
"castTimeMs": 2000,
"range": 25,
"cooldownMs": 20000,
"initialCooldownMs": [
20000,
20000
],
"repeatCooldownMs": [
20000,
20000
],
"damageMultiplier": 0,
"effect": {
"kind": "target-aura",
"aura": "slumber",
"durationMs": 10000
}
},
{
"id": "rw-kalins-shrine-101500-aura-492384",
"name": "Spiritual Divine Praise",
"delivery": "area",
"target": "self",
"school": "arcane",
"animation": "cast",
"castTimeMs": 1200,
"range": 0,
"cooldownMs": 20000,
"initialCooldownMs": [
20000,
20000
],
"repeatCooldownMs": [
20000,
20000
],
"damageMultiplier": 0,
"effect": {
"kind": "aura",
"aura": "heroic-blessing",
"durationMs": 10000
}
}
]
},
@@ -879,9 +1239,16 @@ export const KALINS_SHRINE_ENTITIES = {
"damageMultiplier": 1.3,
"moveSpeed": 2.25,
"leashRange": 32,
"mechanicIds": [
"rw-kalins-shrine-101501-spell-492379",
"rw-kalins-shrine-101501-debuff-492380",
"rw-kalins-shrine-101501-spell-492381",
"rw-kalins-shrine-101501-spell-492382",
"rw-kalins-shrine-101501-aura-492573"
],
"attacks": [
{
"id": "rw-kalins-shrine-basic-attack",
"id": "basic-attack",
"name": "Basic Attack",
"delivery": "melee",
"target": "primary",
@@ -890,6 +1257,147 @@ export const KALINS_SHRINE_ENTITIES = {
"range": 4.5,
"cooldownMs": 1900,
"damageMultiplier": 1
},
{
"id": "rw-kalins-shrine-101501-spell-492379",
"name": "High Speed Pressing",
"spellId": 492379,
"delivery": "area",
"target": "nearby-party",
"school": "arcane",
"animation": "cast",
"castTimeMs": 1800,
"range": 10,
"cooldownMs": 10000,
"initialCooldownMs": [
10000,
10000
],
"repeatCooldownMs": [
10000,
10000
],
"damageMultiplier": 0.85,
"radius": 10,
"telegraph": {
"shape": "circle",
"anchor": "source",
"warningMs": 1800,
"radius": 10
}
},
{
"id": "rw-kalins-shrine-101501-debuff-492380",
"name": "Bite to Pieces",
"delivery": "area",
"target": "nearby-party",
"school": "arcane",
"animation": "cast",
"castTimeMs": 1800,
"range": 10,
"cooldownMs": 10000,
"initialCooldownMs": [
10000,
10000
],
"repeatCooldownMs": [
10000,
10000
],
"damageMultiplier": 0,
"radius": 10,
"telegraph": {
"shape": "circle",
"anchor": "source",
"warningMs": 1800,
"radius": 10
},
"effect": {
"kind": "target-aura",
"aura": "bleed",
"durationMs": 5000
}
},
{
"id": "rw-kalins-shrine-101501-spell-492381",
"name": "Water Arrow Volley",
"spellId": 492381,
"delivery": "area",
"target": "nearby-party",
"school": "frost",
"animation": "cast",
"castTimeMs": 1800,
"range": 25,
"cooldownMs": 10000,
"initialCooldownMs": [
10000,
10000
],
"repeatCooldownMs": [
10000,
10000
],
"damageMultiplier": 0.85,
"radius": 5,
"telegraph": {
"shape": "circle",
"anchor": "source",
"warningMs": 1800,
"radius": 5
}
},
{
"id": "rw-kalins-shrine-101501-spell-492382",
"name": "Lake Master's Fury",
"spellId": 492382,
"delivery": "area",
"target": "nearby-party",
"school": "arcane",
"animation": "cast",
"castTimeMs": 2500,
"range": 15,
"cooldownMs": 10000,
"initialCooldownMs": [
10000,
10000
],
"repeatCooldownMs": [
10000,
10000
],
"damageMultiplier": 0.85,
"radius": 15,
"telegraph": {
"shape": "circle",
"anchor": "source",
"warningMs": 1800,
"radius": 15
}
},
{
"id": "rw-kalins-shrine-101501-aura-492573",
"name": "Stone Skin",
"delivery": "area",
"target": "self",
"school": "arcane",
"animation": "cast",
"castTimeMs": 35000,
"range": 0,
"cooldownMs": 15000,
"initialCooldownMs": [
15000,
15000
],
"repeatCooldownMs": [
15000,
15000
],
"damageMultiplier": 0,
"effect": {
"kind": "aura",
"aura": "stone-skin",
"durationMs": 7500
}
}
]
},
@@ -2816,6 +3324,6 @@ export const KALINS_SHRINE_POPULATION_PROVENANCE = {
"runeWakerIdsAreNamespaced": true,
"serverEntryPolicy": "RuneWaker numeric ids are never passed to AzerothCore serverEntry.",
"sourceSpellCatalog": "scripts/runewaker-pipeline/snapshots/runewaker-npc-spell-catalog.json",
"sourceSpellCatalogSha256": "ad29efc1d4906fdd4f2ac7a69316cac32d8fcbaea674678f050265c86e0c506c",
"sourceSpellCatalogSha256": "c47e67013e68ecf9c7ae30ef4b2058a850505385739dfe8c32ce8874e6ed077c",
"sourceSpellPolicy": "Only positively classified direct-HP-damage families execute; coefficients remain conservative HealerMan values."
} as const;
@@ -16,7 +16,7 @@ export const KAWAKS_TOMB_ENTITIES = {
"leashRange": 24,
"attacks": [
{
"id": "rw-kawaks-tomb-basic-attack",
"id": "basic-attack",
"name": "Basic Attack",
"delivery": "melee",
"target": "primary",
@@ -76,7 +76,7 @@ export const KAWAKS_TOMB_ENTITIES = {
"leashRange": 24,
"attacks": [
{
"id": "rw-kawaks-tomb-basic-attack",
"id": "basic-attack",
"name": "Basic Attack",
"delivery": "melee",
"target": "primary",
@@ -136,7 +136,7 @@ export const KAWAKS_TOMB_ENTITIES = {
"leashRange": 24,
"attacks": [
{
"id": "rw-kawaks-tomb-basic-attack",
"id": "basic-attack",
"name": "Basic Attack",
"delivery": "melee",
"target": "primary",
@@ -196,7 +196,7 @@ export const KAWAKS_TOMB_ENTITIES = {
"leashRange": 24,
"attacks": [
{
"id": "rw-kawaks-tomb-basic-attack",
"id": "basic-attack",
"name": "Basic Attack",
"delivery": "melee",
"target": "primary",
@@ -236,7 +236,7 @@ export const KAWAKS_TOMB_ENTITIES = {
"leashRange": 24,
"attacks": [
{
"id": "rw-kawaks-tomb-basic-attack",
"id": "basic-attack",
"name": "Basic Attack",
"delivery": "melee",
"target": "primary",
@@ -276,7 +276,7 @@ export const KAWAKS_TOMB_ENTITIES = {
"leashRange": 24,
"attacks": [
{
"id": "rw-kawaks-tomb-basic-attack",
"id": "basic-attack",
"name": "Basic Attack",
"delivery": "melee",
"target": "primary",
@@ -316,7 +316,7 @@ export const KAWAKS_TOMB_ENTITIES = {
"leashRange": 24,
"attacks": [
{
"id": "rw-kawaks-tomb-basic-attack",
"id": "basic-attack",
"name": "Basic Attack",
"delivery": "melee",
"target": "primary",
@@ -355,9 +355,17 @@ export const KAWAKS_TOMB_ENTITIES = {
"damageMultiplier": 1.3,
"moveSpeed": 2.25,
"leashRange": 32,
"mechanicIds": [
"rw-kawaks-tomb-104228-spell-496027",
"rw-kawaks-tomb-104228-spell-496028",
"rw-kawaks-tomb-104228-spell-496030",
"rw-kawaks-tomb-104228-spell-496031",
"rw-kawaks-tomb-104228-debuff-496036",
"rw-kawaks-tomb-104228-phase-50"
],
"attacks": [
{
"id": "rw-kawaks-tomb-basic-attack",
"id": "basic-attack",
"name": "Basic Attack",
"delivery": "melee",
"target": "primary",
@@ -366,6 +374,175 @@ export const KAWAKS_TOMB_ENTITIES = {
"range": 4.5,
"cooldownMs": 1900,
"damageMultiplier": 1
},
{
"id": "rw-kawaks-tomb-104228-spell-496027",
"name": "Shake Heaven and Earth",
"spellId": 496027,
"delivery": "area",
"target": "nearby-party",
"school": "arcane",
"animation": "cast",
"castTimeMs": 2000,
"range": 30,
"cooldownMs": 6500,
"initialCooldownMs": [
6500,
6500
],
"repeatCooldownMs": [
6500,
6500
],
"damageMultiplier": 0.85,
"radius": 15,
"telegraph": {
"shape": "circle",
"anchor": "source",
"warningMs": 1800,
"radius": 15
}
},
{
"id": "rw-kawaks-tomb-104228-spell-496028",
"name": "Wing Poison",
"spellId": 496028,
"delivery": "area",
"target": "nearby-party",
"school": "arcane",
"animation": "cast",
"castTimeMs": 1800,
"range": 30,
"cooldownMs": 6500,
"initialCooldownMs": [
6500,
6500
],
"repeatCooldownMs": [
6500,
6500
],
"damageMultiplier": 0.85,
"radius": 15,
"telegraph": {
"shape": "circle",
"anchor": "source",
"warningMs": 1800,
"radius": 15
}
},
{
"id": "rw-kawaks-tomb-104228-spell-496030",
"name": "Holy Stone Floor",
"spellId": 496030,
"delivery": "projectile",
"target": "primary",
"school": "arcane",
"animation": "cast",
"castTimeMs": 0,
"range": 30,
"cooldownMs": 6500,
"initialCooldownMs": [
6500,
6500
],
"repeatCooldownMs": [
6500,
6500
],
"damageMultiplier": 0.85
},
{
"id": "rw-kawaks-tomb-104228-spell-496031",
"name": "Double Horned Spike",
"spellId": 496031,
"delivery": "area",
"target": "nearby-party",
"school": "physical",
"animation": "cast",
"castTimeMs": 2000,
"range": 15,
"cooldownMs": 6500,
"initialCooldownMs": [
6500,
6500
],
"repeatCooldownMs": [
6500,
6500
],
"damageMultiplier": 0.85,
"radius": 15,
"telegraph": {
"shape": "circle",
"anchor": "source",
"warningMs": 1800,
"radius": 15
}
},
{
"id": "rw-kawaks-tomb-104228-debuff-496036",
"name": "Pesticide",
"delivery": "area",
"target": "nearby-party",
"school": "arcane",
"animation": "cast",
"castTimeMs": 5000,
"range": 30,
"cooldownMs": 12000,
"initialCooldownMs": [
12000,
12000
],
"repeatCooldownMs": [
12000,
12000
],
"damageMultiplier": 0,
"radius": 15,
"telegraph": {
"shape": "circle",
"anchor": "source",
"warningMs": 1800,
"radius": 15
},
"effect": {
"kind": "target-aura",
"aura": "pesticide",
"durationMs": 6000
}
},
{
"id": "rw-kawaks-tomb-104228-phase-50",
"name": "Phase Change",
"delivery": "area",
"target": "self",
"school": "arcane",
"animation": "cast",
"castTimeMs": 1200,
"interruptible": false,
"range": 0,
"cooldownMs": 1000,
"initialCooldownMs": [
0,
0
],
"repeatCooldownMs": [
1000,
1000
],
"maximumCasts": 1,
"damageMultiplier": 0,
"effect": {
"kind": "aura",
"aura": "phase-50",
"durationMs": 5000
},
"condition": {
"kind": "self-health-percent",
"minimum": 0,
"maximum": 50
}
}
]
},
@@ -397,7 +574,7 @@ export const KAWAKS_TOMB_ENTITIES = {
"leashRange": 24,
"attacks": [
{
"id": "rw-kawaks-tomb-basic-attack",
"id": "basic-attack",
"name": "Basic Attack",
"delivery": "melee",
"target": "primary",
@@ -437,7 +614,7 @@ export const KAWAKS_TOMB_ENTITIES = {
"leashRange": 24,
"attacks": [
{
"id": "rw-kawaks-tomb-basic-attack",
"id": "basic-attack",
"name": "Basic Attack",
"delivery": "melee",
"target": "primary",
@@ -477,7 +654,7 @@ export const KAWAKS_TOMB_ENTITIES = {
"leashRange": 24,
"attacks": [
{
"id": "rw-kawaks-tomb-basic-attack",
"id": "basic-attack",
"name": "Basic Attack",
"delivery": "melee",
"target": "primary",
@@ -517,7 +694,7 @@ export const KAWAKS_TOMB_ENTITIES = {
"leashRange": 24,
"attacks": [
{
"id": "rw-kawaks-tomb-basic-attack",
"id": "basic-attack",
"name": "Basic Attack",
"delivery": "melee",
"target": "primary",
@@ -557,7 +734,7 @@ export const KAWAKS_TOMB_ENTITIES = {
"leashRange": 24,
"attacks": [
{
"id": "rw-kawaks-tomb-basic-attack",
"id": "basic-attack",
"name": "Basic Attack",
"delivery": "melee",
"target": "primary",
@@ -617,7 +794,7 @@ export const KAWAKS_TOMB_ENTITIES = {
"leashRange": 24,
"attacks": [
{
"id": "rw-kawaks-tomb-basic-attack",
"id": "basic-attack",
"name": "Basic Attack",
"delivery": "melee",
"target": "primary",
@@ -677,7 +854,7 @@ export const KAWAKS_TOMB_ENTITIES = {
"leashRange": 24,
"attacks": [
{
"id": "rw-kawaks-tomb-basic-attack",
"id": "basic-attack",
"name": "Basic Attack",
"delivery": "melee",
"target": "primary",
@@ -717,7 +894,7 @@ export const KAWAKS_TOMB_ENTITIES = {
"leashRange": 24,
"attacks": [
{
"id": "rw-kawaks-tomb-basic-attack",
"id": "basic-attack",
"name": "Basic Attack",
"delivery": "melee",
"target": "primary",
@@ -797,7 +974,7 @@ export const KAWAKS_TOMB_ENTITIES = {
"leashRange": 24,
"attacks": [
{
"id": "rw-kawaks-tomb-basic-attack",
"id": "basic-attack",
"name": "Basic Attack",
"delivery": "melee",
"target": "primary",
@@ -837,7 +1014,7 @@ export const KAWAKS_TOMB_ENTITIES = {
"leashRange": 24,
"attacks": [
{
"id": "rw-kawaks-tomb-basic-attack",
"id": "basic-attack",
"name": "Basic Attack",
"delivery": "melee",
"target": "primary",
@@ -877,7 +1054,7 @@ export const KAWAKS_TOMB_ENTITIES = {
"leashRange": 24,
"attacks": [
{
"id": "rw-kawaks-tomb-basic-attack",
"id": "basic-attack",
"name": "Basic Attack",
"delivery": "melee",
"target": "primary",
@@ -917,7 +1094,7 @@ export const KAWAKS_TOMB_ENTITIES = {
"leashRange": 24,
"attacks": [
{
"id": "rw-kawaks-tomb-basic-attack",
"id": "basic-attack",
"name": "Basic Attack",
"delivery": "melee",
"target": "primary",
@@ -2288,6 +2465,6 @@ export const KAWAKS_TOMB_POPULATION_PROVENANCE = {
"runeWakerIdsAreNamespaced": true,
"serverEntryPolicy": "RuneWaker numeric ids are never passed to AzerothCore serverEntry.",
"sourceSpellCatalog": "scripts/runewaker-pipeline/snapshots/runewaker-npc-spell-catalog.json",
"sourceSpellCatalogSha256": "ad29efc1d4906fdd4f2ac7a69316cac32d8fcbaea674678f050265c86e0c506c",
"sourceSpellCatalogSha256": "c47e67013e68ecf9c7ae30ef4b2058a850505385739dfe8c32ce8874e6ed077c",
"sourceSpellPolicy": "Only positively classified direct-HP-damage families execute; coefficients remain conservative HealerMan values."
} as const;
@@ -16,7 +16,7 @@ export const KULECH_BONES_NEST_ENTITIES = {
"leashRange": 24,
"attacks": [
{
"id": "rw-kulech-bones-nest-basic-attack",
"id": "basic-attack",
"name": "Basic Attack",
"delivery": "melee",
"target": "primary",
@@ -76,7 +76,7 @@ export const KULECH_BONES_NEST_ENTITIES = {
"leashRange": 24,
"attacks": [
{
"id": "rw-kulech-bones-nest-basic-attack",
"id": "basic-attack",
"name": "Basic Attack",
"delivery": "melee",
"target": "primary",
@@ -136,7 +136,7 @@ export const KULECH_BONES_NEST_ENTITIES = {
"leashRange": 24,
"attacks": [
{
"id": "rw-kulech-bones-nest-basic-attack",
"id": "basic-attack",
"name": "Basic Attack",
"delivery": "melee",
"target": "primary",
@@ -195,9 +195,17 @@ export const KULECH_BONES_NEST_ENTITIES = {
"damageMultiplier": 1.3,
"moveSpeed": 2.25,
"leashRange": 32,
"mechanicIds": [
"rw-kulech-bones-nest-107306-spell-496328",
"rw-kulech-bones-nest-107306-spell-496404",
"rw-kulech-bones-nest-107306-spell-496405",
"rw-kulech-bones-nest-107306-aura-496406",
"rw-kulech-bones-nest-107306-aura-496407",
"rw-kulech-bones-nest-107306-spell-496409"
],
"attacks": [
{
"id": "rw-kulech-bones-nest-basic-attack",
"id": "basic-attack",
"name": "Basic Attack",
"delivery": "melee",
"target": "primary",
@@ -206,6 +214,154 @@ export const KULECH_BONES_NEST_ENTITIES = {
"range": 4.5,
"cooldownMs": 1900,
"damageMultiplier": 1
},
{
"id": "rw-kulech-bones-nest-107306-spell-496328",
"name": "Stone Sickle Chop",
"spellId": 496328,
"delivery": "area",
"target": "nearby-party",
"school": "physical",
"animation": "cast",
"castTimeMs": 1800,
"range": 7.5,
"cooldownMs": 6500,
"initialCooldownMs": [
6500,
6500
],
"repeatCooldownMs": [
6500,
6500
],
"damageMultiplier": 0.85,
"radius": 7.5,
"telegraph": {
"shape": "circle",
"anchor": "source",
"warningMs": 1800,
"radius": 7.5
}
},
{
"id": "rw-kulech-bones-nest-107306-spell-496404",
"name": "Soul Killer Potion",
"spellId": 496404,
"delivery": "projectile",
"target": "primary",
"school": "arcane",
"animation": "cast",
"castTimeMs": 0,
"range": 30,
"cooldownMs": 6500,
"initialCooldownMs": [
6500,
6500
],
"repeatCooldownMs": [
6500,
6500
],
"damageMultiplier": 0.85
},
{
"id": "rw-kulech-bones-nest-107306-spell-496405",
"name": "Wind Sickle Shadow",
"spellId": 496405,
"delivery": "area",
"target": "nearby-party",
"school": "arcane",
"animation": "cast",
"castTimeMs": 1800,
"range": 30,
"cooldownMs": 6500,
"initialCooldownMs": [
6500,
6500
],
"repeatCooldownMs": [
6500,
6500
],
"damageMultiplier": 0.85,
"radius": 15,
"telegraph": {
"shape": "circle",
"anchor": "source",
"warningMs": 1800,
"radius": 15
}
},
{
"id": "rw-kulech-bones-nest-107306-aura-496406",
"name": "Awaken",
"delivery": "area",
"target": "self",
"school": "arcane",
"animation": "cast",
"castTimeMs": 1200,
"range": 0,
"cooldownMs": 15000,
"initialCooldownMs": [
15000,
15000
],
"repeatCooldownMs": [
15000,
15000
],
"damageMultiplier": 0,
"effect": {
"kind": "aura",
"aura": "awaken",
"durationMs": 7500
}
},
{
"id": "rw-kulech-bones-nest-107306-aura-496407",
"name": "Corrosive Mucus",
"delivery": "area",
"target": "self",
"school": "arcane",
"animation": "cast",
"castTimeMs": 1200,
"range": 0,
"cooldownMs": 15000,
"initialCooldownMs": [
15000,
15000
],
"repeatCooldownMs": [
15000,
15000
],
"damageMultiplier": 0,
"effect": {
"kind": "aura",
"aura": "acid-irritation",
"durationMs": 7500
}
},
{
"id": "rw-kulech-bones-nest-107306-spell-496409",
"name": "Wind Sickle",
"spellId": 496409,
"delivery": "projectile",
"target": "primary",
"school": "arcane",
"animation": "cast",
"castTimeMs": 1000,
"range": 30,
"cooldownMs": 6500,
"initialCooldownMs": [
6500,
6500
],
"repeatCooldownMs": [
6500,
6500
],
"damageMultiplier": 0.85
}
]
},
@@ -237,7 +393,7 @@ export const KULECH_BONES_NEST_ENTITIES = {
"leashRange": 24,
"attacks": [
{
"id": "rw-kulech-bones-nest-basic-attack",
"id": "basic-attack",
"name": "Basic Attack",
"delivery": "melee",
"target": "primary",
@@ -317,7 +473,7 @@ export const KULECH_BONES_NEST_ENTITIES = {
"leashRange": 24,
"attacks": [
{
"id": "rw-kulech-bones-nest-basic-attack",
"id": "basic-attack",
"name": "Basic Attack",
"delivery": "melee",
"target": "primary",
@@ -377,7 +533,7 @@ export const KULECH_BONES_NEST_ENTITIES = {
"leashRange": 24,
"attacks": [
{
"id": "rw-kulech-bones-nest-basic-attack",
"id": "basic-attack",
"name": "Basic Attack",
"delivery": "melee",
"target": "primary",
@@ -457,7 +613,7 @@ export const KULECH_BONES_NEST_ENTITIES = {
"leashRange": 24,
"attacks": [
{
"id": "rw-kulech-bones-nest-basic-attack",
"id": "basic-attack",
"name": "Basic Attack",
"delivery": "melee",
"target": "primary",
@@ -537,7 +693,7 @@ export const KULECH_BONES_NEST_ENTITIES = {
"leashRange": 24,
"attacks": [
{
"id": "rw-kulech-bones-nest-basic-attack",
"id": "basic-attack",
"name": "Basic Attack",
"delivery": "melee",
"target": "primary",
@@ -577,7 +733,7 @@ export const KULECH_BONES_NEST_ENTITIES = {
"leashRange": 24,
"attacks": [
{
"id": "rw-kulech-bones-nest-basic-attack",
"id": "basic-attack",
"name": "Basic Attack",
"delivery": "melee",
"target": "primary",
@@ -657,7 +813,7 @@ export const KULECH_BONES_NEST_ENTITIES = {
"leashRange": 24,
"attacks": [
{
"id": "rw-kulech-bones-nest-basic-attack",
"id": "basic-attack",
"name": "Basic Attack",
"delivery": "melee",
"target": "primary",
@@ -717,7 +873,7 @@ export const KULECH_BONES_NEST_ENTITIES = {
"leashRange": 24,
"attacks": [
{
"id": "rw-kulech-bones-nest-basic-attack",
"id": "basic-attack",
"name": "Basic Attack",
"delivery": "melee",
"target": "primary",
@@ -777,7 +933,7 @@ export const KULECH_BONES_NEST_ENTITIES = {
"leashRange": 24,
"attacks": [
{
"id": "rw-kulech-bones-nest-basic-attack",
"id": "basic-attack",
"name": "Basic Attack",
"delivery": "melee",
"target": "primary",
@@ -837,7 +993,7 @@ export const KULECH_BONES_NEST_ENTITIES = {
"leashRange": 24,
"attacks": [
{
"id": "rw-kulech-bones-nest-basic-attack",
"id": "basic-attack",
"name": "Basic Attack",
"delivery": "melee",
"target": "primary",
@@ -897,7 +1053,7 @@ export const KULECH_BONES_NEST_ENTITIES = {
"leashRange": 24,
"attacks": [
{
"id": "rw-kulech-bones-nest-basic-attack",
"id": "basic-attack",
"name": "Basic Attack",
"delivery": "melee",
"target": "primary",
@@ -957,7 +1113,7 @@ export const KULECH_BONES_NEST_ENTITIES = {
"leashRange": 24,
"attacks": [
{
"id": "rw-kulech-bones-nest-basic-attack",
"id": "basic-attack",
"name": "Basic Attack",
"delivery": "melee",
"target": "primary",
@@ -2755,6 +2911,6 @@ export const KULECH_BONES_NEST_POPULATION_PROVENANCE = {
"runeWakerIdsAreNamespaced": true,
"serverEntryPolicy": "RuneWaker numeric ids are never passed to AzerothCore serverEntry.",
"sourceSpellCatalog": "scripts/runewaker-pipeline/snapshots/runewaker-npc-spell-catalog.json",
"sourceSpellCatalogSha256": "ad29efc1d4906fdd4f2ac7a69316cac32d8fcbaea674678f050265c86e0c506c",
"sourceSpellCatalogSha256": "c47e67013e68ecf9c7ae30ef4b2058a850505385739dfe8c32ce8874e6ed077c",
"sourceSpellPolicy": "Only positively classified direct-HP-damage families execute; coefficients remain conservative HealerMan values."
} as const;
@@ -16,7 +16,7 @@ export const LAIR_DEMON_DRAGON_ENTITIES = {
"leashRange": 24,
"attacks": [
{
"id": "rw-lair-demon-dragon-basic-attack",
"id": "basic-attack",
"name": "Basic Attack",
"delivery": "melee",
"target": "primary",
@@ -96,7 +96,7 @@ export const LAIR_DEMON_DRAGON_ENTITIES = {
"leashRange": 24,
"attacks": [
{
"id": "rw-lair-demon-dragon-basic-attack",
"id": "basic-attack",
"name": "Basic Attack",
"delivery": "melee",
"target": "primary",
@@ -156,7 +156,7 @@ export const LAIR_DEMON_DRAGON_ENTITIES = {
"leashRange": 24,
"attacks": [
{
"id": "rw-lair-demon-dragon-basic-attack",
"id": "basic-attack",
"name": "Basic Attack",
"delivery": "melee",
"target": "primary",
@@ -196,7 +196,7 @@ export const LAIR_DEMON_DRAGON_ENTITIES = {
"leashRange": 24,
"attacks": [
{
"id": "rw-lair-demon-dragon-basic-attack",
"id": "basic-attack",
"name": "Basic Attack",
"delivery": "melee",
"target": "primary",
@@ -256,7 +256,7 @@ export const LAIR_DEMON_DRAGON_ENTITIES = {
"leashRange": 24,
"attacks": [
{
"id": "rw-lair-demon-dragon-basic-attack",
"id": "basic-attack",
"name": "Basic Attack",
"delivery": "melee",
"target": "primary",
@@ -316,7 +316,7 @@ export const LAIR_DEMON_DRAGON_ENTITIES = {
"leashRange": 24,
"attacks": [
{
"id": "rw-lair-demon-dragon-basic-attack",
"id": "basic-attack",
"name": "Basic Attack",
"delivery": "melee",
"target": "primary",
@@ -376,7 +376,7 @@ export const LAIR_DEMON_DRAGON_ENTITIES = {
"leashRange": 24,
"attacks": [
{
"id": "rw-lair-demon-dragon-basic-attack",
"id": "basic-attack",
"name": "Basic Attack",
"delivery": "melee",
"target": "primary",
@@ -416,7 +416,7 @@ export const LAIR_DEMON_DRAGON_ENTITIES = {
"leashRange": 24,
"attacks": [
{
"id": "rw-lair-demon-dragon-basic-attack",
"id": "basic-attack",
"name": "Basic Attack",
"delivery": "melee",
"target": "primary",
@@ -456,7 +456,7 @@ export const LAIR_DEMON_DRAGON_ENTITIES = {
"leashRange": 24,
"attacks": [
{
"id": "rw-lair-demon-dragon-basic-attack",
"id": "basic-attack",
"name": "Basic Attack",
"delivery": "melee",
"target": "primary",
@@ -496,7 +496,7 @@ export const LAIR_DEMON_DRAGON_ENTITIES = {
"leashRange": 24,
"attacks": [
{
"id": "rw-lair-demon-dragon-basic-attack",
"id": "basic-attack",
"name": "Basic Attack",
"delivery": "melee",
"target": "primary",
@@ -556,7 +556,7 @@ export const LAIR_DEMON_DRAGON_ENTITIES = {
"leashRange": 24,
"attacks": [
{
"id": "rw-lair-demon-dragon-basic-attack",
"id": "basic-attack",
"name": "Basic Attack",
"delivery": "melee",
"target": "primary",
@@ -616,7 +616,7 @@ export const LAIR_DEMON_DRAGON_ENTITIES = {
"leashRange": 24,
"attacks": [
{
"id": "rw-lair-demon-dragon-basic-attack",
"id": "basic-attack",
"name": "Basic Attack",
"delivery": "melee",
"target": "primary",
@@ -656,7 +656,7 @@ export const LAIR_DEMON_DRAGON_ENTITIES = {
"leashRange": 24,
"attacks": [
{
"id": "rw-lair-demon-dragon-basic-attack",
"id": "basic-attack",
"name": "Basic Attack",
"delivery": "melee",
"target": "primary",
@@ -716,7 +716,7 @@ export const LAIR_DEMON_DRAGON_ENTITIES = {
"leashRange": 24,
"attacks": [
{
"id": "rw-lair-demon-dragon-basic-attack",
"id": "basic-attack",
"name": "Basic Attack",
"delivery": "melee",
"target": "primary",
@@ -1923,6 +1923,6 @@ export const LAIR_DEMON_DRAGON_POPULATION_PROVENANCE = {
"runeWakerIdsAreNamespaced": true,
"serverEntryPolicy": "RuneWaker numeric ids are never passed to AzerothCore serverEntry.",
"sourceSpellCatalog": "scripts/runewaker-pipeline/snapshots/runewaker-npc-spell-catalog.json",
"sourceSpellCatalogSha256": "ad29efc1d4906fdd4f2ac7a69316cac32d8fcbaea674678f050265c86e0c506c",
"sourceSpellCatalogSha256": "c47e67013e68ecf9c7ae30ef4b2058a850505385739dfe8c32ce8874e6ed077c",
"sourceSpellPolicy": "Only positively classified direct-HP-damage families execute; coefficients remain conservative HealerMan values."
} as const;
@@ -15,9 +15,18 @@ export const MYSTIC_ALTAR_ENTITIES = {
"damageMultiplier": 1.3,
"moveSpeed": 2.25,
"leashRange": 32,
"mechanicIds": [
"rw-mystic-altar-100611-spell-490476",
"rw-mystic-altar-100611-debuff-490480",
"rw-mystic-altar-100611-spell-490489",
"rw-mystic-altar-100611-control-491087",
"rw-mystic-altar-100611-aura-491095",
"rw-mystic-altar-100611-aura-491096",
"rw-mystic-altar-100611-spell-491104"
],
"attacks": [
{
"id": "rw-mystic-altar-basic-attack",
"id": "basic-attack",
"name": "Basic Attack",
"delivery": "melee",
"target": "primary",
@@ -28,13 +37,14 @@ export const MYSTIC_ALTAR_ENTITIES = {
"damageMultiplier": 1
},
{
"id": "rw-mystic-altar-spell-490476",
"id": "rw-mystic-altar-100611-spell-490476",
"name": "Bloodthirst",
"spellId": 490476,
"delivery": "melee",
"target": "primary",
"school": "arcane",
"animation": "cast",
"castTimeMs": 0,
"range": 5,
"cooldownMs": 20000,
"initialCooldownMs": [
@@ -48,34 +58,40 @@ export const MYSTIC_ALTAR_ENTITIES = {
"damageMultiplier": 0.85
},
{
"id": "rw-mystic-altar-spell-490489",
"name": "Dark Power Release",
"spellId": 490489,
"delivery": "melee",
"id": "rw-mystic-altar-100611-debuff-490480",
"name": "Poisoned",
"delivery": "projectile",
"target": "primary",
"school": "fire",
"school": "arcane",
"animation": "cast",
"range": 4.5,
"cooldownMs": 20000,
"castTimeMs": 2000,
"range": 10,
"cooldownMs": 10000,
"initialCooldownMs": [
20000,
20000
10000,
10000
],
"repeatCooldownMs": [
20000,
20000
10000,
10000
],
"damageMultiplier": 0.85
"damageMultiplier": 0,
"effect": {
"kind": "target-aura",
"aura": "poisoned",
"durationMs": 5000
}
},
{
"id": "rw-mystic-altar-spell-491104",
"name": "Sound of the Underworld",
"spellId": 491104,
"delivery": "melee",
"target": "primary",
"school": "shadow",
"id": "rw-mystic-altar-100611-spell-490489",
"name": "Dark Power Release",
"spellId": 490489,
"delivery": "area",
"target": "nearby-party",
"school": "fire",
"animation": "cast",
"range": 4.5,
"castTimeMs": 5000,
"range": 15,
"cooldownMs": 20000,
"initialCooldownMs": [
20000,
@@ -85,7 +101,124 @@ export const MYSTIC_ALTAR_ENTITIES = {
20000,
20000
],
"damageMultiplier": 0.85
"damageMultiplier": 0.85,
"radius": 15,
"telegraph": {
"shape": "circle",
"anchor": "source",
"warningMs": 1800,
"radius": 15
}
},
{
"id": "rw-mystic-altar-100611-control-491087",
"name": "Dreadful Howl",
"delivery": "area",
"target": "nearby-party",
"school": "shadow",
"animation": "cast",
"castTimeMs": 1800,
"range": 4.5,
"cooldownMs": 25000,
"initialCooldownMs": [
25000,
25000
],
"repeatCooldownMs": [
25000,
25000
],
"damageMultiplier": 0,
"radius": 15,
"telegraph": {
"shape": "circle",
"anchor": "source",
"warningMs": 1800,
"radius": 15
},
"effect": {
"kind": "control",
"mechanic": "fear",
"durationMs": 3000
}
},
{
"id": "rw-mystic-altar-100611-aura-491095",
"name": "Rampart",
"delivery": "area",
"target": "self",
"school": "arcane",
"animation": "cast",
"castTimeMs": 1200,
"range": 0,
"cooldownMs": 45000,
"initialCooldownMs": [
45000,
45000
],
"repeatCooldownMs": [
45000,
45000
],
"damageMultiplier": 0,
"effect": {
"kind": "aura",
"aura": "rampart",
"durationMs": 12000
}
},
{
"id": "rw-mystic-altar-100611-aura-491096",
"name": "Mana Insulation",
"delivery": "area",
"target": "self",
"school": "arcane",
"animation": "cast",
"castTimeMs": 1200,
"range": 0,
"cooldownMs": 45000,
"initialCooldownMs": [
45000,
45000
],
"repeatCooldownMs": [
45000,
45000
],
"damageMultiplier": 0,
"effect": {
"kind": "aura",
"aura": "mana-insulation",
"durationMs": 12000
}
},
{
"id": "rw-mystic-altar-100611-spell-491104",
"name": "Sound of the Underworld",
"spellId": 491104,
"delivery": "area",
"target": "nearby-party",
"school": "shadow",
"animation": "cast",
"castTimeMs": 5000,
"range": 15,
"cooldownMs": 20000,
"initialCooldownMs": [
20000,
20000
],
"repeatCooldownMs": [
20000,
20000
],
"damageMultiplier": 0.85,
"radius": 15,
"telegraph": {
"shape": "circle",
"anchor": "source",
"warningMs": 1800,
"radius": 15
}
}
]
},
@@ -117,7 +250,7 @@ export const MYSTIC_ALTAR_ENTITIES = {
"leashRange": 24,
"attacks": [
{
"id": "rw-mystic-altar-basic-attack",
"id": "basic-attack",
"name": "Basic Attack",
"delivery": "melee",
"target": "primary",
@@ -177,7 +310,7 @@ export const MYSTIC_ALTAR_ENTITIES = {
"leashRange": 24,
"attacks": [
{
"id": "rw-mystic-altar-basic-attack",
"id": "basic-attack",
"name": "Basic Attack",
"delivery": "melee",
"target": "primary",
@@ -237,7 +370,7 @@ export const MYSTIC_ALTAR_ENTITIES = {
"leashRange": 24,
"attacks": [
{
"id": "rw-mystic-altar-basic-attack",
"id": "basic-attack",
"name": "Basic Attack",
"delivery": "melee",
"target": "primary",
@@ -316,9 +449,19 @@ export const MYSTIC_ALTAR_ENTITIES = {
"damageMultiplier": 1.3,
"moveSpeed": 2.25,
"leashRange": 32,
"mechanicIds": [
"rw-mystic-altar-100154-spell-490489",
"rw-mystic-altar-100154-spell-490633",
"rw-mystic-altar-100154-control-490636",
"rw-mystic-altar-100154-spell-490637",
"rw-mystic-altar-100154-aura-490652",
"rw-mystic-altar-100154-control-491086",
"rw-mystic-altar-100154-debuff-491097",
"rw-mystic-altar-100154-debuff-491101"
],
"attacks": [
{
"id": "rw-mystic-altar-basic-attack",
"id": "basic-attack",
"name": "Basic Attack",
"delivery": "melee",
"target": "primary",
@@ -329,54 +472,15 @@ export const MYSTIC_ALTAR_ENTITIES = {
"damageMultiplier": 1
},
{
"id": "rw-mystic-altar-spell-490637",
"name": "Silence",
"spellId": 490637,
"delivery": "projectile",
"target": "primary",
"school": "arcane",
"animation": "cast",
"range": 18,
"cooldownMs": 15000,
"initialCooldownMs": [
15000,
15000
],
"repeatCooldownMs": [
15000,
15000
],
"damageMultiplier": 0.85
},
{
"id": "rw-mystic-altar-spell-490633",
"name": "Thunderstorm",
"spellId": 490633,
"delivery": "projectile",
"target": "primary",
"school": "nature",
"animation": "cast",
"range": 18,
"cooldownMs": 15000,
"initialCooldownMs": [
15000,
15000
],
"repeatCooldownMs": [
15000,
15000
],
"damageMultiplier": 0.85
},
{
"id": "rw-mystic-altar-spell-490489",
"id": "rw-mystic-altar-100154-spell-490489",
"name": "Dark Power Release",
"spellId": 490489,
"delivery": "melee",
"target": "primary",
"delivery": "area",
"target": "nearby-party",
"school": "fire",
"animation": "cast",
"range": 4.5,
"castTimeMs": 5000,
"range": 15,
"cooldownMs": 20000,
"initialCooldownMs": [
20000,
@@ -386,7 +490,202 @@ export const MYSTIC_ALTAR_ENTITIES = {
20000,
20000
],
"damageMultiplier": 0.85,
"radius": 15,
"telegraph": {
"shape": "circle",
"anchor": "source",
"warningMs": 1800,
"radius": 15
}
},
{
"id": "rw-mystic-altar-100154-spell-490633",
"name": "Thunderstorm",
"spellId": 490633,
"delivery": "area",
"target": "nearby-party",
"school": "nature",
"animation": "cast",
"castTimeMs": 2000,
"range": 18,
"cooldownMs": 15000,
"initialCooldownMs": [
15000,
15000
],
"repeatCooldownMs": [
15000,
15000
],
"damageMultiplier": 0.85,
"radius": 5,
"telegraph": {
"shape": "circle",
"anchor": "source",
"warningMs": 1800,
"radius": 5
}
},
{
"id": "rw-mystic-altar-100154-control-490636",
"name": "Group Catch",
"delivery": "area",
"target": "nearby-party",
"school": "shadow",
"animation": "cast",
"castTimeMs": 1800,
"range": 10,
"cooldownMs": 15000,
"initialCooldownMs": [
15000,
15000
],
"repeatCooldownMs": [
15000,
15000
],
"damageMultiplier": 0,
"radius": 5,
"telegraph": {
"shape": "circle",
"anchor": "source",
"warningMs": 1800,
"radius": 5
},
"effect": {
"kind": "control",
"mechanic": "root",
"durationMs": 3000
}
},
{
"id": "rw-mystic-altar-100154-spell-490637",
"name": "Silence",
"spellId": 490637,
"delivery": "projectile",
"target": "primary",
"school": "arcane",
"animation": "cast",
"castTimeMs": 1000,
"range": 18,
"cooldownMs": 15000,
"initialCooldownMs": [
15000,
15000
],
"repeatCooldownMs": [
15000,
15000
],
"damageMultiplier": 0.85
},
{
"id": "rw-mystic-altar-100154-aura-490652",
"name": "Life Source",
"delivery": "area",
"target": "self",
"school": "arcane",
"animation": "cast",
"castTimeMs": 8000,
"range": 0,
"cooldownMs": 45000,
"initialCooldownMs": [
45000,
45000
],
"repeatCooldownMs": [
45000,
45000
],
"damageMultiplier": 0,
"effect": {
"kind": "aura",
"aura": "life-source",
"durationMs": 12000
}
},
{
"id": "rw-mystic-altar-100154-control-491086",
"name": "Dreadful Cry",
"delivery": "projectile",
"target": "primary",
"school": "shadow",
"animation": "cast",
"castTimeMs": 1800,
"range": 12,
"cooldownMs": 25000,
"initialCooldownMs": [
25000,
25000
],
"repeatCooldownMs": [
25000,
25000
],
"damageMultiplier": 0,
"effect": {
"kind": "control",
"mechanic": "fear",
"durationMs": 3000
}
},
{
"id": "rw-mystic-altar-100154-debuff-491097",
"name": "Life Sandglass",
"delivery": "projectile",
"target": "primary",
"school": "arcane",
"animation": "cast",
"castTimeMs": 1800,
"range": 25,
"cooldownMs": 12000,
"initialCooldownMs": [
12000,
12000
],
"repeatCooldownMs": [
12000,
12000
],
"damageMultiplier": 0,
"effect": {
"kind": "target-aura",
"aura": "life-sandglass",
"durationMs": 6000
}
},
{
"id": "rw-mystic-altar-100154-debuff-491101",
"name": "Life Sandglass",
"delivery": "area",
"target": "nearby-party",
"school": "arcane",
"animation": "cast",
"castTimeMs": 1800,
"range": 15,
"cooldownMs": 12000,
"initialCooldownMs": [
12000,
12000
],
"repeatCooldownMs": [
12000,
12000
],
"damageMultiplier": 0,
"radius": 15,
"telegraph": {
"shape": "circle",
"anchor": "source",
"warningMs": 1800,
"radius": 15
},
"effect": {
"kind": "target-aura",
"aura": "life-sandglass",
"durationMs": 6000
}
}
]
},
@@ -418,7 +717,7 @@ export const MYSTIC_ALTAR_ENTITIES = {
"leashRange": 24,
"attacks": [
{
"id": "rw-mystic-altar-basic-attack",
"id": "basic-attack",
"name": "Basic Attack",
"delivery": "melee",
"target": "primary",
@@ -478,7 +777,7 @@ export const MYSTIC_ALTAR_ENTITIES = {
"leashRange": 24,
"attacks": [
{
"id": "rw-mystic-altar-basic-attack",
"id": "basic-attack",
"name": "Basic Attack",
"delivery": "melee",
"target": "primary",
@@ -578,7 +877,7 @@ export const MYSTIC_ALTAR_ENTITIES = {
"leashRange": 24,
"attacks": [
{
"id": "rw-mystic-altar-basic-attack",
"id": "basic-attack",
"name": "Basic Attack",
"delivery": "melee",
"target": "primary",
@@ -678,7 +977,7 @@ export const MYSTIC_ALTAR_ENTITIES = {
"leashRange": 24,
"attacks": [
{
"id": "rw-mystic-altar-basic-attack",
"id": "basic-attack",
"name": "Basic Attack",
"delivery": "melee",
"target": "primary",
@@ -738,7 +1037,7 @@ export const MYSTIC_ALTAR_ENTITIES = {
"leashRange": 24,
"attacks": [
{
"id": "rw-mystic-altar-basic-attack",
"id": "basic-attack",
"name": "Basic Attack",
"delivery": "melee",
"target": "primary",
@@ -838,7 +1137,7 @@ export const MYSTIC_ALTAR_ENTITIES = {
"leashRange": 24,
"attacks": [
{
"id": "rw-mystic-altar-basic-attack",
"id": "basic-attack",
"name": "Basic Attack",
"delivery": "melee",
"target": "primary",
@@ -877,9 +1176,19 @@ export const MYSTIC_ALTAR_ENTITIES = {
"damageMultiplier": 1.3,
"moveSpeed": 2.25,
"leashRange": 32,
"mechanicIds": [
"rw-mystic-altar-100138-spell-490477",
"rw-mystic-altar-100138-spell-490478",
"rw-mystic-altar-100138-spell-490520",
"rw-mystic-altar-100138-spell-490522",
"rw-mystic-altar-100138-aura-490523",
"rw-mystic-altar-100138-spell-490525",
"rw-mystic-altar-100138-control-490636",
"rw-mystic-altar-100138-aura-491095"
],
"attacks": [
{
"id": "rw-mystic-altar-basic-attack",
"id": "basic-attack",
"name": "Basic Attack",
"delivery": "melee",
"target": "primary",
@@ -890,13 +1199,14 @@ export const MYSTIC_ALTAR_ENTITIES = {
"damageMultiplier": 1
},
{
"id": "rw-mystic-altar-spell-490477",
"id": "rw-mystic-altar-100138-spell-490477",
"name": "Slash",
"spellId": 490477,
"delivery": "melee",
"target": "primary",
"school": "physical",
"animation": "attack",
"castTimeMs": 0,
"range": 5,
"cooldownMs": 10000,
"initialCooldownMs": [
@@ -910,73 +1220,14 @@ export const MYSTIC_ALTAR_ENTITIES = {
"damageMultiplier": 0.85
},
{
"id": "rw-mystic-altar-spell-490520",
"name": "Whirlwind",
"spellId": 490520,
"delivery": "melee",
"target": "primary",
"school": "physical",
"animation": "attack",
"range": 4.5,
"cooldownMs": 20000,
"initialCooldownMs": [
20000,
20000
],
"repeatCooldownMs": [
20000,
20000
],
"damageMultiplier": 0.85
},
{
"id": "rw-mystic-altar-spell-490525",
"name": "Blasting Cyclone",
"spellId": 490525,
"delivery": "melee",
"target": "primary",
"school": "physical",
"animation": "attack",
"range": 4.5,
"cooldownMs": 20000,
"initialCooldownMs": [
20000,
20000
],
"repeatCooldownMs": [
20000,
20000
],
"damageMultiplier": 0.85
},
{
"id": "rw-mystic-altar-spell-490522",
"name": "Formation Breaker",
"spellId": 490522,
"delivery": "melee",
"target": "primary",
"school": "physical",
"animation": "attack",
"range": 4.5,
"cooldownMs": 20000,
"initialCooldownMs": [
20000,
20000
],
"repeatCooldownMs": [
20000,
20000
],
"damageMultiplier": 0.85
},
{
"id": "rw-mystic-altar-spell-490478",
"id": "rw-mystic-altar-100138-spell-490478",
"name": "Slash",
"spellId": 490478,
"delivery": "melee",
"target": "primary",
"school": "physical",
"animation": "attack",
"castTimeMs": 0,
"range": 5,
"cooldownMs": 6000,
"initialCooldownMs": [
@@ -988,6 +1239,172 @@ export const MYSTIC_ALTAR_ENTITIES = {
6000
],
"damageMultiplier": 0.85
},
{
"id": "rw-mystic-altar-100138-spell-490520",
"name": "Whirlwind",
"spellId": 490520,
"delivery": "area",
"target": "nearby-party",
"school": "physical",
"animation": "attack",
"castTimeMs": 1800,
"range": 5,
"cooldownMs": 20000,
"initialCooldownMs": [
20000,
20000
],
"repeatCooldownMs": [
20000,
20000
],
"damageMultiplier": 0.85,
"radius": 5,
"telegraph": {
"shape": "circle",
"anchor": "source",
"warningMs": 1800,
"radius": 5
}
},
{
"id": "rw-mystic-altar-100138-spell-490522",
"name": "Formation Breaker",
"spellId": 490522,
"delivery": "area",
"target": "nearby-party",
"school": "physical",
"animation": "attack",
"castTimeMs": 3000,
"range": 7.5,
"cooldownMs": 20000,
"initialCooldownMs": [
20000,
20000
],
"repeatCooldownMs": [
20000,
20000
],
"damageMultiplier": 0.85,
"radius": 7.5,
"telegraph": {
"shape": "circle",
"anchor": "source",
"warningMs": 1800,
"radius": 7.5
}
},
{
"id": "rw-mystic-altar-100138-aura-490523",
"name": "Frenzy",
"delivery": "area",
"target": "self",
"school": "arcane",
"animation": "cast",
"castTimeMs": 1200,
"range": 0,
"cooldownMs": 45000,
"initialCooldownMs": [
45000,
45000
],
"repeatCooldownMs": [
45000,
45000
],
"damageMultiplier": 0,
"effect": {
"kind": "aura",
"aura": "frenzy",
"durationMs": 12000
}
},
{
"id": "rw-mystic-altar-100138-spell-490525",
"name": "Blasting Cyclone",
"spellId": 490525,
"delivery": "area",
"target": "nearby-party",
"school": "physical",
"animation": "attack",
"castTimeMs": 1800,
"range": 8,
"cooldownMs": 20000,
"initialCooldownMs": [
20000,
20000
],
"repeatCooldownMs": [
20000,
20000
],
"damageMultiplier": 0.85,
"radius": 8,
"telegraph": {
"shape": "circle",
"anchor": "source",
"warningMs": 1800,
"radius": 8
}
},
{
"id": "rw-mystic-altar-100138-control-490636",
"name": "Group Catch",
"delivery": "area",
"target": "nearby-party",
"school": "shadow",
"animation": "cast",
"castTimeMs": 1800,
"range": 10,
"cooldownMs": 15000,
"initialCooldownMs": [
15000,
15000
],
"repeatCooldownMs": [
15000,
15000
],
"damageMultiplier": 0,
"radius": 5,
"telegraph": {
"shape": "circle",
"anchor": "source",
"warningMs": 1800,
"radius": 5
},
"effect": {
"kind": "control",
"mechanic": "root",
"durationMs": 3000
}
},
{
"id": "rw-mystic-altar-100138-aura-491095",
"name": "Rampart",
"delivery": "area",
"target": "self",
"school": "arcane",
"animation": "cast",
"castTimeMs": 1200,
"range": 0,
"cooldownMs": 45000,
"initialCooldownMs": [
45000,
45000
],
"repeatCooldownMs": [
45000,
45000
],
"damageMultiplier": 0,
"effect": {
"kind": "aura",
"aura": "rampart",
"durationMs": 12000
}
}
]
},
@@ -3842,6 +4259,6 @@ export const MYSTIC_ALTAR_POPULATION_PROVENANCE = {
"runeWakerIdsAreNamespaced": true,
"serverEntryPolicy": "RuneWaker numeric ids are never passed to AzerothCore serverEntry.",
"sourceSpellCatalog": "scripts/runewaker-pipeline/snapshots/runewaker-npc-spell-catalog.json",
"sourceSpellCatalogSha256": "ad29efc1d4906fdd4f2ac7a69316cac32d8fcbaea674678f050265c86e0c506c",
"sourceSpellCatalogSha256": "c47e67013e68ecf9c7ae30ef4b2058a850505385739dfe8c32ce8874e6ed077c",
"sourceSpellPolicy": "Only positively classified direct-HP-damage families execute; coefficients remain conservative HealerMan values."
} as const;
File diff suppressed because it is too large. Load diff
@@ -16,7 +16,7 @@ export const RAKSHA_TEMPLE_ENTITIES = {
"leashRange": 24,
"attacks": [
{
"id": "rw-raksha-temple-basic-attack",
"id": "basic-attack",
"name": "Basic Attack",
"delivery": "melee",
"target": "primary",
@@ -56,7 +56,7 @@ export const RAKSHA_TEMPLE_ENTITIES = {
"leashRange": 24,
"attacks": [
{
"id": "rw-raksha-temple-basic-attack",
"id": "basic-attack",
"name": "Basic Attack",
"delivery": "melee",
"target": "primary",
@@ -96,7 +96,7 @@ export const RAKSHA_TEMPLE_ENTITIES = {
"leashRange": 24,
"attacks": [
{
"id": "rw-raksha-temple-basic-attack",
"id": "basic-attack",
"name": "Basic Attack",
"delivery": "melee",
"target": "primary",
@@ -136,7 +136,7 @@ export const RAKSHA_TEMPLE_ENTITIES = {
"leashRange": 24,
"attacks": [
{
"id": "rw-raksha-temple-basic-attack",
"id": "basic-attack",
"name": "Basic Attack",
"delivery": "melee",
"target": "primary",
@@ -176,7 +176,7 @@ export const RAKSHA_TEMPLE_ENTITIES = {
"leashRange": 24,
"attacks": [
{
"id": "rw-raksha-temple-basic-attack",
"id": "basic-attack",
"name": "Basic Attack",
"delivery": "melee",
"target": "primary",
@@ -216,7 +216,7 @@ export const RAKSHA_TEMPLE_ENTITIES = {
"leashRange": 24,
"attacks": [
{
"id": "rw-raksha-temple-basic-attack",
"id": "basic-attack",
"name": "Basic Attack",
"delivery": "melee",
"target": "primary",
@@ -276,7 +276,7 @@ export const RAKSHA_TEMPLE_ENTITIES = {
"leashRange": 24,
"attacks": [
{
"id": "rw-raksha-temple-basic-attack",
"id": "basic-attack",
"name": "Basic Attack",
"delivery": "melee",
"target": "primary",
@@ -336,7 +336,7 @@ export const RAKSHA_TEMPLE_ENTITIES = {
"leashRange": 24,
"attacks": [
{
"id": "rw-raksha-temple-basic-attack",
"id": "basic-attack",
"name": "Basic Attack",
"delivery": "melee",
"target": "primary",
@@ -376,7 +376,7 @@ export const RAKSHA_TEMPLE_ENTITIES = {
"leashRange": 24,
"attacks": [
{
"id": "rw-raksha-temple-basic-attack",
"id": "basic-attack",
"name": "Basic Attack",
"delivery": "melee",
"target": "primary",
@@ -436,7 +436,7 @@ export const RAKSHA_TEMPLE_ENTITIES = {
"leashRange": 24,
"attacks": [
{
"id": "rw-raksha-temple-basic-attack",
"id": "basic-attack",
"name": "Basic Attack",
"delivery": "melee",
"target": "primary",
@@ -516,7 +516,7 @@ export const RAKSHA_TEMPLE_ENTITIES = {
"leashRange": 24,
"attacks": [
{
"id": "rw-raksha-temple-basic-attack",
"id": "basic-attack",
"name": "Basic Attack",
"delivery": "melee",
"target": "primary",
@@ -556,7 +556,7 @@ export const RAKSHA_TEMPLE_ENTITIES = {
"leashRange": 24,
"attacks": [
{
"id": "rw-raksha-temple-basic-attack",
"id": "basic-attack",
"name": "Basic Attack",
"delivery": "melee",
"target": "primary",
@@ -596,7 +596,7 @@ export const RAKSHA_TEMPLE_ENTITIES = {
"leashRange": 24,
"attacks": [
{
"id": "rw-raksha-temple-basic-attack",
"id": "basic-attack",
"name": "Basic Attack",
"delivery": "melee",
"target": "primary",
@@ -656,7 +656,7 @@ export const RAKSHA_TEMPLE_ENTITIES = {
"leashRange": 24,
"attacks": [
{
"id": "rw-raksha-temple-basic-attack",
"id": "basic-attack",
"name": "Basic Attack",
"delivery": "melee",
"target": "primary",
@@ -716,7 +716,7 @@ export const RAKSHA_TEMPLE_ENTITIES = {
"leashRange": 24,
"attacks": [
{
"id": "rw-raksha-temple-basic-attack",
"id": "basic-attack",
"name": "Basic Attack",
"delivery": "melee",
"target": "primary",
@@ -756,7 +756,7 @@ export const RAKSHA_TEMPLE_ENTITIES = {
"leashRange": 24,
"attacks": [
{
"id": "rw-raksha-temple-basic-attack",
"id": "basic-attack",
"name": "Basic Attack",
"delivery": "melee",
"target": "primary",
@@ -816,7 +816,7 @@ export const RAKSHA_TEMPLE_ENTITIES = {
"leashRange": 24,
"attacks": [
{
"id": "rw-raksha-temple-basic-attack",
"id": "basic-attack",
"name": "Basic Attack",
"delivery": "melee",
"target": "primary",
@@ -896,7 +896,7 @@ export const RAKSHA_TEMPLE_ENTITIES = {
"leashRange": 24,
"attacks": [
{
"id": "rw-raksha-temple-basic-attack",
"id": "basic-attack",
"name": "Basic Attack",
"delivery": "melee",
"target": "primary",
@@ -936,7 +936,7 @@ export const RAKSHA_TEMPLE_ENTITIES = {
"leashRange": 24,
"attacks": [
{
"id": "rw-raksha-temple-basic-attack",
"id": "basic-attack",
"name": "Basic Attack",
"delivery": "melee",
"target": "primary",
@@ -976,7 +976,7 @@ export const RAKSHA_TEMPLE_ENTITIES = {
"leashRange": 24,
"attacks": [
{
"id": "rw-raksha-temple-basic-attack",
"id": "basic-attack",
"name": "Basic Attack",
"delivery": "melee",
"target": "primary",
@@ -1036,7 +1036,7 @@ export const RAKSHA_TEMPLE_ENTITIES = {
"leashRange": 24,
"attacks": [
{
"id": "rw-raksha-temple-basic-attack",
"id": "basic-attack",
"name": "Basic Attack",
"delivery": "melee",
"target": "primary",
@@ -1076,7 +1076,7 @@ export const RAKSHA_TEMPLE_ENTITIES = {
"leashRange": 24,
"attacks": [
{
"id": "rw-raksha-temple-basic-attack",
"id": "basic-attack",
"name": "Basic Attack",
"delivery": "melee",
"target": "primary",
@@ -1116,7 +1116,7 @@ export const RAKSHA_TEMPLE_ENTITIES = {
"leashRange": 24,
"attacks": [
{
"id": "rw-raksha-temple-basic-attack",
"id": "basic-attack",
"name": "Basic Attack",
"delivery": "melee",
"target": "primary",
@@ -1196,7 +1196,7 @@ export const RAKSHA_TEMPLE_ENTITIES = {
"leashRange": 24,
"attacks": [
{
"id": "rw-raksha-temple-basic-attack",
"id": "basic-attack",
"name": "Basic Attack",
"delivery": "melee",
"target": "primary",
@@ -1236,7 +1236,7 @@ export const RAKSHA_TEMPLE_ENTITIES = {
"leashRange": 24,
"attacks": [
{
"id": "rw-raksha-temple-basic-attack",
"id": "basic-attack",
"name": "Basic Attack",
"delivery": "melee",
"target": "primary",
@@ -1296,7 +1296,7 @@ export const RAKSHA_TEMPLE_ENTITIES = {
"leashRange": 24,
"attacks": [
{
"id": "rw-raksha-temple-basic-attack",
"id": "basic-attack",
"name": "Basic Attack",
"delivery": "melee",
"target": "primary",
@@ -1336,7 +1336,7 @@ export const RAKSHA_TEMPLE_ENTITIES = {
"leashRange": 24,
"attacks": [
{
"id": "rw-raksha-temple-basic-attack",
"id": "basic-attack",
"name": "Basic Attack",
"delivery": "melee",
"target": "primary",
@@ -1396,7 +1396,7 @@ export const RAKSHA_TEMPLE_ENTITIES = {
"leashRange": 24,
"attacks": [
{
"id": "rw-raksha-temple-basic-attack",
"id": "basic-attack",
"name": "Basic Attack",
"delivery": "melee",
"target": "primary",
@@ -1476,7 +1476,7 @@ export const RAKSHA_TEMPLE_ENTITIES = {
"leashRange": 24,
"attacks": [
{
"id": "rw-raksha-temple-basic-attack",
"id": "basic-attack",
"name": "Basic Attack",
"delivery": "melee",
"target": "primary",
@@ -1516,7 +1516,7 @@ export const RAKSHA_TEMPLE_ENTITIES = {
"leashRange": 24,
"attacks": [
{
"id": "rw-raksha-temple-basic-attack",
"id": "basic-attack",
"name": "Basic Attack",
"delivery": "melee",
"target": "primary",
@@ -1556,7 +1556,7 @@ export const RAKSHA_TEMPLE_ENTITIES = {
"leashRange": 24,
"attacks": [
{
"id": "rw-raksha-temple-basic-attack",
"id": "basic-attack",
"name": "Basic Attack",
"delivery": "melee",
"target": "primary",
@@ -1616,7 +1616,7 @@ export const RAKSHA_TEMPLE_ENTITIES = {
"leashRange": 24,
"attacks": [
{
"id": "rw-raksha-temple-basic-attack",
"id": "basic-attack",
"name": "Basic Attack",
"delivery": "melee",
"target": "primary",
@@ -1676,7 +1676,7 @@ export const RAKSHA_TEMPLE_ENTITIES = {
"leashRange": 24,
"attacks": [
{
"id": "rw-raksha-temple-basic-attack",
"id": "basic-attack",
"name": "Basic Attack",
"delivery": "melee",
"target": "primary",
@@ -1716,7 +1716,7 @@ export const RAKSHA_TEMPLE_ENTITIES = {
"leashRange": 24,
"attacks": [
{
"id": "rw-raksha-temple-basic-attack",
"id": "basic-attack",
"name": "Basic Attack",
"delivery": "melee",
"target": "primary",
@@ -1756,7 +1756,7 @@ export const RAKSHA_TEMPLE_ENTITIES = {
"leashRange": 24,
"attacks": [
{
"id": "rw-raksha-temple-basic-attack",
"id": "basic-attack",
"name": "Basic Attack",
"delivery": "melee",
"target": "primary",
@@ -1816,7 +1816,7 @@ export const RAKSHA_TEMPLE_ENTITIES = {
"leashRange": 24,
"attacks": [
{
"id": "rw-raksha-temple-basic-attack",
"id": "basic-attack",
"name": "Basic Attack",
"delivery": "melee",
"target": "primary",
@@ -1856,7 +1856,7 @@ export const RAKSHA_TEMPLE_ENTITIES = {
"leashRange": 24,
"attacks": [
{
"id": "rw-raksha-temple-basic-attack",
"id": "basic-attack",
"name": "Basic Attack",
"delivery": "melee",
"target": "primary",
@@ -1916,7 +1916,7 @@ export const RAKSHA_TEMPLE_ENTITIES = {
"leashRange": 24,
"attacks": [
{
"id": "rw-raksha-temple-basic-attack",
"id": "basic-attack",
"name": "Basic Attack",
"delivery": "melee",
"target": "primary",
@@ -1956,7 +1956,7 @@ export const RAKSHA_TEMPLE_ENTITIES = {
"leashRange": 24,
"attacks": [
{
"id": "rw-raksha-temple-basic-attack",
"id": "basic-attack",
"name": "Basic Attack",
"delivery": "melee",
"target": "primary",
@@ -1995,9 +1995,15 @@ export const RAKSHA_TEMPLE_ENTITIES = {
"damageMultiplier": 1.3,
"moveSpeed": 2.25,
"leashRange": 32,
"mechanicIds": [
"rw-raksha-temple-103946-spell-495545",
"rw-raksha-temple-103946-spell-495591",
"rw-raksha-temple-103946-spell-495996",
"rw-raksha-temple-103946-spell-496112"
],
"attacks": [
{
"id": "rw-raksha-temple-basic-attack",
"id": "basic-attack",
"name": "Basic Attack",
"delivery": "melee",
"target": "primary",
@@ -2006,6 +2012,111 @@ export const RAKSHA_TEMPLE_ENTITIES = {
"range": 4.5,
"cooldownMs": 1900,
"damageMultiplier": 1
},
{
"id": "rw-raksha-temple-103946-spell-495545",
"name": "Soul Fire",
"spellId": 495545,
"delivery": "area",
"target": "nearby-party",
"school": "fire",
"animation": "cast",
"castTimeMs": 2000,
"range": 7.5,
"cooldownMs": 6500,
"initialCooldownMs": [
6500,
6500
],
"repeatCooldownMs": [
6500,
6500
],
"damageMultiplier": 0.85,
"radius": 7.5,
"telegraph": {
"shape": "circle",
"anchor": "source",
"warningMs": 1800,
"radius": 7.5
}
},
{
"id": "rw-raksha-temple-103946-spell-495591",
"name": "Void Fire",
"spellId": 495591,
"delivery": "area",
"target": "nearby-party",
"school": "fire",
"animation": "cast",
"castTimeMs": 2000,
"range": 30,
"cooldownMs": 6500,
"initialCooldownMs": [
6500,
6500
],
"repeatCooldownMs": [
6500,
6500
],
"damageMultiplier": 0.85,
"radius": 15,
"telegraph": {
"shape": "circle",
"anchor": "source",
"warningMs": 1800,
"radius": 15
}
},
{
"id": "rw-raksha-temple-103946-spell-495996",
"name": "Totem Burst",
"spellId": 495996,
"delivery": "area",
"target": "nearby-party",
"school": "arcane",
"animation": "cast",
"castTimeMs": 1800,
"range": 30,
"cooldownMs": 6500,
"initialCooldownMs": [
6500,
6500
],
"repeatCooldownMs": [
6500,
6500
],
"damageMultiplier": 0.85,
"radius": 15,
"telegraph": {
"shape": "circle",
"anchor": "source",
"warningMs": 1800,
"radius": 15
}
},
{
"id": "rw-raksha-temple-103946-spell-496112",
"name": "RuneWaker Spell 496112",
"spellId": 496112,
"delivery": "melee",
"target": "primary",
"school": "arcane",
"animation": "cast",
"castTimeMs": 0,
"range": 4.5,
"cooldownMs": 6500,
"initialCooldownMs": [
6500,
6500
],
"repeatCooldownMs": [
6500,
6500
],
"damageMultiplier": 0.85
}
]
},
@@ -3434,6 +3545,6 @@ export const RAKSHA_TEMPLE_POPULATION_PROVENANCE = {
"runeWakerIdsAreNamespaced": true,
"serverEntryPolicy": "RuneWaker numeric ids are never passed to AzerothCore serverEntry.",
"sourceSpellCatalog": "scripts/runewaker-pipeline/snapshots/runewaker-npc-spell-catalog.json",
"sourceSpellCatalogSha256": "ad29efc1d4906fdd4f2ac7a69316cac32d8fcbaea674678f050265c86e0c506c",
"sourceSpellCatalogSha256": "c47e67013e68ecf9c7ae30ef4b2058a850505385739dfe8c32ce8874e6ed077c",
"sourceSpellPolicy": "Only positively classified direct-HP-damage families execute; coefficients remain conservative HealerMan values."
} as const;
@@ -102,8 +102,8 @@ export const RUNEWAKER_DUNGEON_DEFINITIONS = [
{
"id": "visual-000",
"fileName": "kalins-shrine-visual.glb",
"checksum": "0cfdb047990a4870d18e67fcf96170c3cd575fbb7f0739784dcf15edd5c8bddf",
"size": 14175516,
"checksum": "769accb48e23c9f80e28697e3e8454ef4148243a69ce247ed06ba4eb35bb0da7",
"size": 8747072,
"triangleCount": 202382
}
],
@@ -216,7 +216,7 @@ export const RUNEWAKER_DUNGEON_DEFINITIONS = [
"Secondary WDB doodads/effects remain in the audit; this pass exports the primary ROS hierarchy.",
"Collision initially includes decorative primary-ROS geometry.",
"Original GR2 actor animations are recovered where source graphs and validation permit; scripted encounter mechanics remain evidence-driven per-instance work.",
"Mechanics candidates are source-audited; complex doors, summons, phases, patrol scripts, and loot remain review-required."
"Boss combat kits are compiled from RuneWaker spell/script evidence; complex doors, patrol scripts, and loot remain review-required."
]
}
} as const satisfies DungeonDefinition,
@@ -291,8 +291,8 @@ export const RUNEWAKER_DUNGEON_DEFINITIONS = [
{
"id": "visual-000",
"fileName": "hoto-visual.glb",
"checksum": "4c69138c8bf6a6b1055f337a7c39204b977a6169905ab735e1742a139c76ad64",
"size": 16626184,
"checksum": "0d12b9c6dc477704d2833c81f76113d340da7f07f5885a40bafd1321524e455e",
"size": 6667208,
"triangleCount": 189855
}
],
@@ -405,7 +405,7 @@ export const RUNEWAKER_DUNGEON_DEFINITIONS = [
"Secondary WDB doodads/effects remain in the audit; this pass exports the primary ROS hierarchy.",
"Collision initially includes decorative primary-ROS geometry.",
"Original GR2 actor animations are recovered where source graphs and validation permit; scripted encounter mechanics remain evidence-driven per-instance work.",
"Mechanics candidates are source-audited; complex doors, summons, phases, patrol scripts, and loot remain review-required."
"Boss combat kits are compiled from RuneWaker spell/script evidence; complex doors, patrol scripts, and loot remain review-required."
]
}
} as const satisfies DungeonDefinition,
@@ -480,8 +480,8 @@ export const RUNEWAKER_DUNGEON_DEFINITIONS = [
{
"id": "visual-000",
"fileName": "mystic-altar-visual.glb",
"checksum": "9f395e5670c79fe306c2a5440125885c4c58b36d677f2bde74667d3779d1eab5",
"size": 6453276,
"checksum": "765dd1b15bbe79fe1ffc2104e10caece19acaf86c919f0327419cb3609dbf600",
"size": 5552872,
"triangleCount": 174521
}
],
@@ -588,7 +588,7 @@ export const RUNEWAKER_DUNGEON_DEFINITIONS = [
"Collision initially includes decorative primary-ROS geometry.",
"Original GR2 actor animations are recovered where source graphs and validation permit; scripted encounter mechanics remain evidence-driven per-instance work.",
"Source-anchor support pads preserve combat and entrance coordinates where modular WDB sections or primary-ROS collision leave navmesh gaps; full secondary placement assembly remains a visual follow-up.",
"Mechanics candidates are source-audited; complex doors, summons, phases, patrol scripts, and loot remain review-required."
"Boss combat kits are compiled from RuneWaker spell/script evidence; complex doors, patrol scripts, and loot remain review-required."
]
}
} as const satisfies DungeonDefinition,
@@ -663,8 +663,8 @@ export const RUNEWAKER_DUNGEON_DEFINITIONS = [
{
"id": "visual-000",
"fileName": "kal-turok-hive-visual.glb",
"checksum": "7a593b8b0b001d410213c09cf75f3b1edc3fb8543f412d2a5bb18e411506b0da",
"size": 5593304,
"checksum": "012867deefc4dc5a18c3e1a507309aa48f119cf4c2e88968fdd2787936ed7c24",
"size": 1777616,
"triangleCount": 24026
}
],
@@ -763,7 +763,7 @@ export const RUNEWAKER_DUNGEON_DEFINITIONS = [
"Secondary WDB doodads/effects remain in the audit; this pass exports the primary ROS hierarchy.",
"Collision initially includes decorative primary-ROS geometry.",
"Original GR2 actor animations are recovered where source graphs and validation permit; scripted encounter mechanics remain evidence-driven per-instance work.",
"Mechanics candidates are source-audited; complex doors, summons, phases, patrol scripts, and loot remain review-required."
"Boss combat kits are compiled from RuneWaker spell/script evidence; complex doors, patrol scripts, and loot remain review-required."
]
}
} as const satisfies DungeonDefinition,
@@ -838,8 +838,8 @@ export const RUNEWAKER_DUNGEON_DEFINITIONS = [
{
"id": "visual-000",
"fileName": "paspers-shrine-visual.glb",
"checksum": "564dc92bae483dd0be8fd0086215d4e2e6bd4dda45f2094a55f4045de8301f5f",
"size": 6262596,
"checksum": "82ea6cafd19c0a323c994795699223a07193807da29b300eb12dac41146f1034",
"size": 5625952,
"triangleCount": 169935
}
],
@@ -945,7 +945,7 @@ export const RUNEWAKER_DUNGEON_DEFINITIONS = [
"Secondary WDB doodads/effects remain in the audit; this pass exports the primary ROS hierarchy.",
"Collision initially includes decorative primary-ROS geometry.",
"Original GR2 actor animations are recovered where source graphs and validation permit; scripted encounter mechanics remain evidence-driven per-instance work.",
"Mechanics candidates are source-audited; complex doors, summons, phases, patrol scripts, and loot remain review-required."
"Boss combat kits are compiled from RuneWaker spell/script evidence; complex doors, patrol scripts, and loot remain review-required."
]
}
} as const satisfies DungeonDefinition,
@@ -1020,8 +1020,8 @@ export const RUNEWAKER_DUNGEON_DEFINITIONS = [
{
"id": "visual-000",
"fileName": "treasure-trove-visual.glb",
"checksum": "eb8f26f1594d8e10a060444899e91744c8c5f48af720b873504031d860d9f446",
"size": 10623004,
"checksum": "f4c4dc12df65f8afc3c696308db17cb42b6a5894086dbe870dae2e21e1258448",
"size": 7433280,
"triangleCount": 182387
}
],
@@ -1135,7 +1135,7 @@ export const RUNEWAKER_DUNGEON_DEFINITIONS = [
"Collision initially includes decorative primary-ROS geometry.",
"Original GR2 actor animations are recovered where source graphs and validation permit; scripted encounter mechanics remain evidence-driven per-instance work.",
"Source-anchor support pads preserve combat and entrance coordinates where modular WDB sections or primary-ROS collision leave navmesh gaps; full secondary placement assembly remains a visual follow-up.",
"Mechanics candidates are source-audited; complex doors, summons, phases, patrol scripts, and loot remain review-required."
"Boss combat kits are compiled from RuneWaker spell/script evidence; complex doors, patrol scripts, and loot remain review-required."
]
}
} as const satisfies DungeonDefinition,
@@ -1210,8 +1210,8 @@ export const RUNEWAKER_DUNGEON_DEFINITIONS = [
{
"id": "visual-000",
"fileName": "ystra-labyrinth-boss1-visual.glb",
"checksum": "54febe2d5e0740da790f6bdbad73e4f383ed9b633da9c096a6687ae28081eeaa",
"size": 8036460,
"checksum": "c8ca72a5fdf487fa71a1aecd1e66ea821183f476540be9a89444b409be957c25",
"size": 2709352,
"triangleCount": 64284
}
],
@@ -1324,7 +1324,7 @@ export const RUNEWAKER_DUNGEON_DEFINITIONS = [
"Secondary WDB doodads/effects remain in the audit; this pass exports the primary ROS hierarchy.",
"Collision initially includes decorative primary-ROS geometry.",
"Original GR2 actor animations are recovered where source graphs and validation permit; scripted encounter mechanics remain evidence-driven per-instance work.",
"Mechanics candidates are source-audited; complex doors, summons, phases, patrol scripts, and loot remain review-required."
"Boss combat kits are compiled from RuneWaker spell/script evidence; complex doors, patrol scripts, and loot remain review-required."
]
}
} as const satisfies DungeonDefinition,
@@ -1399,8 +1399,8 @@ export const RUNEWAKER_DUNGEON_DEFINITIONS = [
{
"id": "visual-000",
"fileName": "barren-caves-visual.glb",
"checksum": "71e264d5e757e2402c45e7c291b03af243c9defdd89d78ec736a3024d126d482",
"size": 1852500,
"checksum": "d0f733de4b26c0f432c5e160614765f320bf1753a829be269740b340e7415a12",
"size": 1710152,
"triangleCount": 47084
}
],
@@ -1499,7 +1499,7 @@ export const RUNEWAKER_DUNGEON_DEFINITIONS = [
"Secondary WDB doodads/effects remain in the audit; this pass exports the primary ROS hierarchy.",
"Collision initially includes decorative primary-ROS geometry.",
"Original GR2 actor animations are recovered where source graphs and validation permit; scripted encounter mechanics remain evidence-driven per-instance work.",
"Mechanics candidates are source-audited; complex doors, summons, phases, patrol scripts, and loot remain review-required."
"Boss combat kits are compiled from RuneWaker spell/script evidence; complex doors, patrol scripts, and loot remain review-required."
]
}
} as const satisfies DungeonDefinition,
@@ -1574,8 +1574,8 @@ export const RUNEWAKER_DUNGEON_DEFINITIONS = [
{
"id": "visual-000",
"fileName": "ystra-labyrinth-boss2-visual.glb",
"checksum": "12c32e7c72cb1a6ed8693999d9d9a99bfeeee28ce32f2415852d8c9a9f59cf6c",
"size": 7954300,
"checksum": "d52524621220e16d844badc24ee745f4374798031b2b0bc7236decbe3b9588ab",
"size": 2682688,
"triangleCount": 21428
}
],
@@ -1674,7 +1674,7 @@ export const RUNEWAKER_DUNGEON_DEFINITIONS = [
"Secondary WDB doodads/effects remain in the audit; this pass exports the primary ROS hierarchy.",
"Collision initially includes decorative primary-ROS geometry.",
"Original GR2 actor animations are recovered where source graphs and validation permit; scripted encounter mechanics remain evidence-driven per-instance work.",
"Mechanics candidates are source-audited; complex doors, summons, phases, patrol scripts, and loot remain review-required."
"Boss combat kits are compiled from RuneWaker spell/script evidence; complex doors, patrol scripts, and loot remain review-required."
]
}
} as const satisfies DungeonDefinition,
@@ -1749,8 +1749,8 @@ export const RUNEWAKER_DUNGEON_DEFINITIONS = [
{
"id": "visual-000",
"fileName": "ystra-labyrinth-boss3-visual.glb",
"checksum": "12c32e7c72cb1a6ed8693999d9d9a99bfeeee28ce32f2415852d8c9a9f59cf6c",
"size": 7954300,
"checksum": "d52524621220e16d844badc24ee745f4374798031b2b0bc7236decbe3b9588ab",
"size": 2682688,
"triangleCount": 21428
}
],
@@ -1849,7 +1849,7 @@ export const RUNEWAKER_DUNGEON_DEFINITIONS = [
"Secondary WDB doodads/effects remain in the audit; this pass exports the primary ROS hierarchy.",
"Collision initially includes decorative primary-ROS geometry.",
"Original GR2 actor animations are recovered where source graphs and validation permit; scripted encounter mechanics remain evidence-driven per-instance work.",
"Mechanics candidates are source-audited; complex doors, summons, phases, patrol scripts, and loot remain review-required."
"Boss combat kits are compiled from RuneWaker spell/script evidence; complex doors, patrol scripts, and loot remain review-required."
]
}
} as const satisfies DungeonDefinition,
@@ -1924,8 +1924,8 @@ export const RUNEWAKER_DUNGEON_DEFINITIONS = [
{
"id": "visual-000",
"fileName": "ice-dwarf-kingdom-visual.glb",
"checksum": "a0f4ce4572d6fe491c362a8494623b5b4f5a255c1a63c9a4b5509af633f05c72",
"size": 11348084,
"checksum": "a6b902e85bb6026f18281d77efa235ec880ffc2fc13ea6bfdc431d24af768e00",
"size": 9880192,
"triangleCount": 285517
}
],
@@ -2045,7 +2045,7 @@ export const RUNEWAKER_DUNGEON_DEFINITIONS = [
"Secondary WDB doodads/effects remain in the audit; this pass exports the primary ROS hierarchy.",
"Collision initially includes decorative primary-ROS geometry.",
"Original GR2 actor animations are recovered where source graphs and validation permit; scripted encounter mechanics remain evidence-driven per-instance work.",
"Mechanics candidates are source-audited; complex doors, summons, phases, patrol scripts, and loot remain review-required."
"Boss combat kits are compiled from RuneWaker spell/script evidence; complex doors, patrol scripts, and loot remain review-required."
]
}
} as const satisfies DungeonDefinition,
@@ -2120,8 +2120,8 @@ export const RUNEWAKER_DUNGEON_DEFINITIONS = [
{
"id": "visual-000",
"fileName": "the-origin-visual.glb",
"checksum": "d6e1b8aaa69d067068ea29ad38544466a3a5fab7e0a532ca15da60cbfceac51b",
"size": 643248,
"checksum": "a6db29db31f88a45bd082d27130725d70b2fa1db82030530750492a54b3f9bf8",
"size": 251132,
"triangleCount": 2664
}
],
@@ -2221,7 +2221,7 @@ export const RUNEWAKER_DUNGEON_DEFINITIONS = [
"Collision initially includes decorative primary-ROS geometry.",
"Original GR2 actor animations are recovered where source graphs and validation permit; scripted encounter mechanics remain evidence-driven per-instance work.",
"Source-anchor support pads preserve combat and entrance coordinates where modular WDB sections or primary-ROS collision leave navmesh gaps; full secondary placement assembly remains a visual follow-up.",
"Mechanics candidates are source-audited; complex doors, summons, phases, patrol scripts, and loot remain review-required."
"Boss combat kits are compiled from RuneWaker spell/script evidence; complex doors, patrol scripts, and loot remain review-required."
]
}
} as const satisfies DungeonDefinition,
@@ -2296,8 +2296,8 @@ export const RUNEWAKER_DUNGEON_DEFINITIONS = [
{
"id": "visual-000",
"fileName": "hall-of-survivors-visual.glb",
"checksum": "c22b5ae6b4a836a8861d90c8db249ed813caab8dfa6eb1311bc0730fd62e1150",
"size": 31415356,
"checksum": "18b9d116da599159719ad76eeee45a4d89056ee8c61cc4f21cab9a1d9228fff7",
"size": 15216768,
"triangleCount": 380192
}
],
@@ -2424,7 +2424,7 @@ export const RUNEWAKER_DUNGEON_DEFINITIONS = [
"Secondary WDB doodads/effects remain in the audit; this pass exports the primary ROS hierarchy.",
"Collision initially includes decorative primary-ROS geometry.",
"Original GR2 actor animations are recovered where source graphs and validation permit; scripted encounter mechanics remain evidence-driven per-instance work.",
"Mechanics candidates are source-audited; complex doors, summons, phases, patrol scripts, and loot remain review-required."
"Boss combat kits are compiled from RuneWaker spell/script evidence; complex doors, patrol scripts, and loot remain review-required."
]
}
} as const satisfies DungeonDefinition,
@@ -2499,8 +2499,8 @@ export const RUNEWAKER_DUNGEON_DEFINITIONS = [
{
"id": "visual-000",
"fileName": "cave-water-dragon-visual.glb",
"checksum": "56b9565c53ae8c644fb7ca7d070c02f39656e3ff30c407f1e04f28874566b781",
"size": 13081352,
"checksum": "cc57c0285de77690c4a1cdabaecd8b192791cbd05aca38aa21ed96462688ca31",
"size": 8893112,
"triangleCount": 249082
}
],
@@ -2613,7 +2613,7 @@ export const RUNEWAKER_DUNGEON_DEFINITIONS = [
"Secondary WDB doodads/effects remain in the audit; this pass exports the primary ROS hierarchy.",
"Collision initially includes decorative primary-ROS geometry.",
"Original GR2 actor animations are recovered where source graphs and validation permit; scripted encounter mechanics remain evidence-driven per-instance work.",
"Mechanics candidates are source-audited; complex doors, summons, phases, patrol scripts, and loot remain review-required."
"Boss combat kits are compiled from RuneWaker spell/script evidence; complex doors, patrol scripts, and loot remain review-required."
]
}
} as const satisfies DungeonDefinition,
@@ -2688,8 +2688,8 @@ export const RUNEWAKER_DUNGEON_DEFINITIONS = [
{
"id": "visual-000",
"fileName": "lair-demon-dragon-visual.glb",
"checksum": "4e3e15f5c5c3d02fab6a5eefa37962e614012cb2d255c56c841dadfd983db101",
"size": 11971136,
"checksum": "d28481504802203e800985836f2b0907d67f6d6d0495c67500914e749d771ffe",
"size": 4551300,
"triangleCount": 64688
}
],
@@ -2788,7 +2788,7 @@ export const RUNEWAKER_DUNGEON_DEFINITIONS = [
"Secondary WDB doodads/effects remain in the audit; this pass exports the primary ROS hierarchy.",
"Collision initially includes decorative primary-ROS geometry.",
"Original GR2 actor animations are recovered where source graphs and validation permit; scripted encounter mechanics remain evidence-driven per-instance work.",
"Mechanics candidates are source-audited; complex doors, summons, phases, patrol scripts, and loot remain review-required."
"Boss combat kits are compiled from RuneWaker spell/script evidence; complex doors, patrol scripts, and loot remain review-required."
]
}
} as const satisfies DungeonDefinition,
@@ -2863,8 +2863,8 @@ export const RUNEWAKER_DUNGEON_DEFINITIONS = [
{
"id": "visual-000",
"fileName": "zurhidon-stronghold-visual.glb",
"checksum": "ac533caeaf367039a48527d33695d9f28dfaffa6c29eac7b304e93334e5bb8d3",
"size": 38128108,
"checksum": "a2d450946ca9687395cf65815b931c7817d9f41041afeb36ba1a351ec24104c4",
"size": 17189492,
"triangleCount": 364765
}
],
@@ -2991,7 +2991,7 @@ export const RUNEWAKER_DUNGEON_DEFINITIONS = [
"Secondary WDB doodads/effects remain in the audit; this pass exports the primary ROS hierarchy.",
"Collision initially includes decorative primary-ROS geometry.",
"Original GR2 actor animations are recovered where source graphs and validation permit; scripted encounter mechanics remain evidence-driven per-instance work.",
"Mechanics candidates are source-audited; complex doors, summons, phases, patrol scripts, and loot remain review-required."
"Boss combat kits are compiled from RuneWaker spell/script evidence; complex doors, patrol scripts, and loot remain review-required."
]
}
} as const satisfies DungeonDefinition,
@@ -3167,7 +3167,7 @@ export const RUNEWAKER_DUNGEON_DEFINITIONS = [
"Collision initially includes decorative primary-ROS geometry.",
"Original GR2 actor animations are recovered where source graphs and validation permit; scripted encounter mechanics remain evidence-driven per-instance work.",
"The source WDB has no terrain ROS; a flat playable arena is derived from source descriptor bounds and must be visually reviewed.",
"Mechanics candidates are source-audited; complex doors, summons, phases, patrol scripts, and loot remain review-required."
"Boss combat kits are compiled from RuneWaker spell/script evidence; complex doors, patrol scripts, and loot remain review-required."
]
}
} as const satisfies DungeonDefinition,
@@ -3242,8 +3242,8 @@ export const RUNEWAKER_DUNGEON_DEFINITIONS = [
{
"id": "visual-000",
"fileName": "demon-stronghold-visual.glb",
"checksum": "27d9026c01418e26959c2c24fb3929fc50a7a29c63922ef7223a7cf82aceb46f",
"size": 37609808,
"checksum": "9368984b3c183a8db6e8b7e18b7818edcf41b8edd45ffeca1d1df28aadc592a2",
"size": 15166984,
"triangleCount": 261609
}
],
@@ -3357,7 +3357,7 @@ export const RUNEWAKER_DUNGEON_DEFINITIONS = [
"Collision initially includes decorative primary-ROS geometry.",
"Original GR2 actor animations are recovered where source graphs and validation permit; scripted encounter mechanics remain evidence-driven per-instance work.",
"Source-anchor support pads preserve combat and entrance coordinates where modular WDB sections or primary-ROS collision leave navmesh gaps; full secondary placement assembly remains a visual follow-up.",
"Mechanics candidates are source-audited; complex doors, summons, phases, patrol scripts, and loot remain review-required."
"Boss combat kits are compiled from RuneWaker spell/script evidence; complex doors, patrol scripts, and loot remain review-required."
]
}
} as const satisfies DungeonDefinition,
@@ -3432,8 +3432,8 @@ export const RUNEWAKER_DUNGEON_DEFINITIONS = [
{
"id": "visual-000",
"fileName": "zurhidon-stronghold-124-visual.glb",
"checksum": "ac533caeaf367039a48527d33695d9f28dfaffa6c29eac7b304e93334e5bb8d3",
"size": 38128108,
"checksum": "a2d450946ca9687395cf65815b931c7817d9f41041afeb36ba1a351ec24104c4",
"size": 17189492,
"triangleCount": 364765
}
],
@@ -3560,7 +3560,7 @@ export const RUNEWAKER_DUNGEON_DEFINITIONS = [
"Secondary WDB doodads/effects remain in the audit; this pass exports the primary ROS hierarchy.",
"Collision initially includes decorative primary-ROS geometry.",
"Original GR2 actor animations are recovered where source graphs and validation permit; scripted encounter mechanics remain evidence-driven per-instance work.",
"Mechanics candidates are source-audited; complex doors, summons, phases, patrol scripts, and loot remain review-required."
"Boss combat kits are compiled from RuneWaker spell/script evidence; complex doors, patrol scripts, and loot remain review-required."
]
}
} as const satisfies DungeonDefinition,
@@ -3635,8 +3635,8 @@ export const RUNEWAKER_DUNGEON_DEFINITIONS = [
{
"id": "visual-000",
"fileName": "demon-stronghold-125-visual.glb",
"checksum": "e648750047d8bbcf77b3f1a7d1c79144e4dcf45bd209bc54f070777d1e55541c",
"size": 37597956,
"checksum": "ecbacfe274e7eecb104c5807eedf3cc51cd4b7a628967c6331fc93b86396b842",
"size": 15134500,
"triangleCount": 259377
}
],
@@ -3749,7 +3749,7 @@ export const RUNEWAKER_DUNGEON_DEFINITIONS = [
"Secondary WDB doodads/effects remain in the audit; this pass exports the primary ROS hierarchy.",
"Collision initially includes decorative primary-ROS geometry.",
"Original GR2 actor animations are recovered where source graphs and validation permit; scripted encounter mechanics remain evidence-driven per-instance work.",
"Mechanics candidates are source-audited; complex doors, summons, phases, patrol scripts, and loot remain review-required."
"Boss combat kits are compiled from RuneWaker spell/script evidence; complex doors, patrol scripts, and loot remain review-required."
]
}
} as const satisfies DungeonDefinition,
@@ -3824,8 +3824,8 @@ export const RUNEWAKER_DUNGEON_DEFINITIONS = [
{
"id": "visual-000",
"fileName": "dungeon-of-dalanis-visual.glb",
"checksum": "9dbf0d01dd3d6ba029a2647561076ccd049c29ea9391a45ade5c6259c7c11b9c",
"size": 14905224,
"checksum": "4c584ef34779e516f8e6ff3a4907eaab1c7c8d49d4c302de66e17b09222d8829",
"size": 11936864,
"triangleCount": 288949
}
],
@@ -3945,7 +3945,7 @@ export const RUNEWAKER_DUNGEON_DEFINITIONS = [
"Secondary WDB doodads/effects remain in the audit; this pass exports the primary ROS hierarchy.",
"Collision initially includes decorative primary-ROS geometry.",
"Original GR2 actor animations are recovered where source graphs and validation permit; scripted encounter mechanics remain evidence-driven per-instance work.",
"Mechanics candidates are source-audited; complex doors, summons, phases, patrol scripts, and loot remain review-required."
"Boss combat kits are compiled from RuneWaker spell/script evidence; complex doors, patrol scripts, and loot remain review-required."
]
}
} as const satisfies DungeonDefinition,
@@ -4020,8 +4020,8 @@ export const RUNEWAKER_DUNGEON_DEFINITIONS = [
{
"id": "visual-000",
"fileName": "warnorken-arena-visual.glb",
"checksum": "52d94f8fabf0c86fb24c57d5be36a490b26703b0acf84f83139adcd7df9710df",
"size": 10114952,
"checksum": "c452a843c2e4123d4fe0d20da41558f5b5a1906548b3d091dc691ffc42ed19d9",
"size": 3752136,
"triangleCount": 60751
}
],
@@ -4120,7 +4120,7 @@ export const RUNEWAKER_DUNGEON_DEFINITIONS = [
"Secondary WDB doodads/effects remain in the audit; this pass exports the primary ROS hierarchy.",
"Collision initially includes decorative primary-ROS geometry.",
"Original GR2 actor animations are recovered where source graphs and validation permit; scripted encounter mechanics remain evidence-driven per-instance work.",
"Mechanics candidates are source-audited; complex doors, summons, phases, patrol scripts, and loot remain review-required."
"Boss combat kits are compiled from RuneWaker spell/script evidence; complex doors, patrol scripts, and loot remain review-required."
]
}
} as const satisfies DungeonDefinition,
@@ -4195,8 +4195,8 @@ export const RUNEWAKER_DUNGEON_DEFINITIONS = [
{
"id": "visual-000",
"fileName": "raksha-temple-visual.glb",
"checksum": "91a2197f45cd7e63829c35a3748efc2311c7d5a40e6737a33f42d21163dc4b1d",
"size": 3274336,
"checksum": "3439e64f2a49d0545651bac35276867af83c3eb57b3c859f767de0c49c3dce66",
"size": 937772,
"triangleCount": 4302
}
],
@@ -4296,7 +4296,7 @@ export const RUNEWAKER_DUNGEON_DEFINITIONS = [
"Collision initially includes decorative primary-ROS geometry.",
"Original GR2 actor animations are recovered where source graphs and validation permit; scripted encounter mechanics remain evidence-driven per-instance work.",
"Source-anchor support pads preserve combat and entrance coordinates where modular WDB sections or primary-ROS collision leave navmesh gaps; full secondary placement assembly remains a visual follow-up.",
"Mechanics candidates are source-audited; complex doors, summons, phases, patrol scripts, and loot remain review-required."
"Boss combat kits are compiled from RuneWaker spell/script evidence; complex doors, patrol scripts, and loot remain review-required."
]
}
} as const satisfies DungeonDefinition,
@@ -4371,8 +4371,8 @@ export const RUNEWAKER_DUNGEON_DEFINITIONS = [
{
"id": "visual-000",
"fileName": "kawaks-tomb-visual.glb",
"checksum": "6a55e61218a56a0f21566555ad0b7bb88b29b52882317231e836cc76b359ba14",
"size": 21954824,
"checksum": "de54d223725e2c3dfb0c47a97c208a212ef9423860bd0ac390f1482b302d3a32",
"size": 8963188,
"triangleCount": 161613
}
],
@@ -4478,7 +4478,7 @@ export const RUNEWAKER_DUNGEON_DEFINITIONS = [
"Secondary WDB doodads/effects remain in the audit; this pass exports the primary ROS hierarchy.",
"Collision initially includes decorative primary-ROS geometry.",
"Original GR2 actor animations are recovered where source graphs and validation permit; scripted encounter mechanics remain evidence-driven per-instance work.",
"Mechanics candidates are source-audited; complex doors, summons, phases, patrol scripts, and loot remain review-required."
"Boss combat kits are compiled from RuneWaker spell/script evidence; complex doors, patrol scripts, and loot remain review-required."
]
}
} as const satisfies DungeonDefinition,
@@ -4553,8 +4553,8 @@ export const RUNEWAKER_DUNGEON_DEFINITIONS = [
{
"id": "visual-000",
"fileName": "grafu-castle-visual.glb",
"checksum": "d5561b8b427edd6dca0a675dabaf40b251430ad5511ab66217254bde45a7eda0",
"size": 21509296,
"checksum": "ba4552484ef9cddc1c00607d0dfd4c8fb0bae5b3cd3f992d215a580dac1e96ed",
"size": 11149716,
"triangleCount": 207925
}
],
@@ -4667,7 +4667,7 @@ export const RUNEWAKER_DUNGEON_DEFINITIONS = [
"Secondary WDB doodads/effects remain in the audit; this pass exports the primary ROS hierarchy.",
"Collision initially includes decorative primary-ROS geometry.",
"Original GR2 actor animations are recovered where source graphs and validation permit; scripted encounter mechanics remain evidence-driven per-instance work.",
"Mechanics candidates are source-audited; complex doors, summons, phases, patrol scripts, and loot remain review-required."
"Boss combat kits are compiled from RuneWaker spell/script evidence; complex doors, patrol scripts, and loot remain review-required."
]
}
} as const satisfies DungeonDefinition,
@@ -4742,8 +4742,8 @@ export const RUNEWAKER_DUNGEON_DEFINITIONS = [
{
"id": "visual-000",
"fileName": "sardo-castle-visual.glb",
"checksum": "e819c1afb60fd81f7e79f0e3fea0b3bf4e64f57eb41526761bf41ccce3876a51",
"size": 17572084,
"checksum": "16d759af9f8a3f98281e89362c7fb52dceff6543bbd837a7c6f89015920cdd94",
"size": 7756568,
"triangleCount": 147586
}
],
@@ -4849,7 +4849,7 @@ export const RUNEWAKER_DUNGEON_DEFINITIONS = [
"Secondary WDB doodads/effects remain in the audit; this pass exports the primary ROS hierarchy.",
"Collision initially includes decorative primary-ROS geometry.",
"Original GR2 actor animations are recovered where source graphs and validation permit; scripted encounter mechanics remain evidence-driven per-instance work.",
"Mechanics candidates are source-audited; complex doors, summons, phases, patrol scripts, and loot remain review-required."
"Boss combat kits are compiled from RuneWaker spell/script evidence; complex doors, patrol scripts, and loot remain review-required."
]
}
} as const satisfies DungeonDefinition,
@@ -4924,8 +4924,8 @@ export const RUNEWAKER_DUNGEON_DEFINITIONS = [
{
"id": "visual-000",
"fileName": "tomb-seven-heroes-visual.glb",
"checksum": "3659d1675240704a6f5e43cb59a2ffffc503c979adfe92afcc93ff4e18a0413c",
"size": 26962236,
"checksum": "d9f39da6b61c9f841c5d075e4985bc67e533ce422313fdf38d5f6c99dbb304c1",
"size": 10910804,
"triangleCount": 155422
}
],
@@ -5031,7 +5031,7 @@ export const RUNEWAKER_DUNGEON_DEFINITIONS = [
"Secondary WDB doodads/effects remain in the audit; this pass exports the primary ROS hierarchy.",
"Collision initially includes decorative primary-ROS geometry.",
"Original GR2 actor animations are recovered where source graphs and validation permit; scripted encounter mechanics remain evidence-driven per-instance work.",
"Mechanics candidates are source-audited; complex doors, summons, phases, patrol scripts, and loot remain review-required."
"Boss combat kits are compiled from RuneWaker spell/script evidence; complex doors, patrol scripts, and loot remain review-required."
]
}
} as const satisfies DungeonDefinition,
@@ -5106,8 +5106,8 @@ export const RUNEWAKER_DUNGEON_DEFINITIONS = [
{
"id": "visual-000",
"fileName": "aeternal-circle-visual.glb",
"checksum": "d182e54b99a9875600c89efea194d9e86aa508c800a4e2b4d6ac7420158906aa",
"size": 12997820,
"checksum": "42fdcff1b25b58688943a030f69fd14ea839247a7baabbc31b5f6f7faaf49785",
"size": 4963912,
"triangleCount": 61639
}
],
@@ -5207,7 +5207,7 @@ export const RUNEWAKER_DUNGEON_DEFINITIONS = [
"Collision initially includes decorative primary-ROS geometry.",
"Original GR2 actor animations are recovered where source graphs and validation permit; scripted encounter mechanics remain evidence-driven per-instance work.",
"Source-anchor support pads preserve combat and entrance coordinates where modular WDB sections or primary-ROS collision leave navmesh gaps; full secondary placement assembly remains a visual follow-up.",
"Mechanics candidates are source-audited; complex doors, summons, phases, patrol scripts, and loot remain review-required."
"Boss combat kits are compiled from RuneWaker spell/script evidence; complex doors, patrol scripts, and loot remain review-required."
]
}
} as const satisfies DungeonDefinition,
@@ -5282,8 +5282,8 @@ export const RUNEWAKER_DUNGEON_DEFINITIONS = [
{
"id": "visual-000",
"fileName": "kulech-bones-nest-visual.glb",
"checksum": "a29116b276c7057ef30a6d7faacc8c7ff4430cea658b9bd9eb1edb86228f770c",
"size": 36555996,
"checksum": "2d912dcf7c0c8e59e36362fd947df1d5554c5dd5060590e1abb112e4c39f606f",
"size": 12146892,
"triangleCount": 168192
}
],
@@ -5390,7 +5390,7 @@ export const RUNEWAKER_DUNGEON_DEFINITIONS = [
"Collision initially includes decorative primary-ROS geometry.",
"Original GR2 actor animations are recovered where source graphs and validation permit; scripted encounter mechanics remain evidence-driven per-instance work.",
"1 runtime/dynamic actor containers use HealerMan procedural visuals because their ROS files expose no static triangles.",
"Mechanics candidates are source-audited; complex doors, summons, phases, patrol scripts, and loot remain review-required."
"Boss combat kits are compiled from RuneWaker spell/script evidence; complex doors, patrol scripts, and loot remain review-required."
]
}
} as const satisfies DungeonDefinition
@@ -15,9 +15,15 @@ export const SAND_VORTEX_ENTITIES = {
"damageMultiplier": 1.3,
"moveSpeed": 2.25,
"leashRange": 32,
"mechanicIds": [
"rw-sand-vortex-100645-spell-494642",
"rw-sand-vortex-100645-spell-494643",
"rw-sand-vortex-100645-debuff-494644",
"rw-sand-vortex-100645-aura-494649"
],
"attacks": [
{
"id": "rw-sand-vortex-basic-attack",
"id": "basic-attack",
"name": "Basic Attack",
"delivery": "melee",
"target": "primary",
@@ -26,6 +32,119 @@ export const SAND_VORTEX_ENTITIES = {
"range": 4.5,
"cooldownMs": 1900,
"damageMultiplier": 1
},
{
"id": "rw-sand-vortex-100645-spell-494642",
"name": "Viscous Quicksand",
"spellId": 494642,
"delivery": "area",
"target": "nearby-party",
"school": "nature",
"animation": "cast",
"castTimeMs": 1800,
"range": 4.5,
"cooldownMs": 6500,
"initialCooldownMs": [
6500,
6500
],
"repeatCooldownMs": [
6500,
6500
],
"damageMultiplier": 0.85,
"radius": 3,
"telegraph": {
"shape": "circle",
"anchor": "source",
"warningMs": 1800,
"radius": 3
}
},
{
"id": "rw-sand-vortex-100645-spell-494643",
"name": "Sand Fountain",
"spellId": 494643,
"delivery": "area",
"target": "nearby-party",
"school": "nature",
"animation": "cast",
"castTimeMs": 2000,
"range": 30,
"cooldownMs": 6500,
"initialCooldownMs": [
6500,
6500
],
"repeatCooldownMs": [
6500,
6500
],
"damageMultiplier": 0.85,
"radius": 8,
"telegraph": {
"shape": "circle",
"anchor": "source",
"warningMs": 1800,
"radius": 8
}
},
{
"id": "rw-sand-vortex-100645-debuff-494644",
"name": "Sandy Ties",
"delivery": "area",
"target": "nearby-party",
"school": "arcane",
"animation": "cast",
"castTimeMs": 1800,
"range": 30,
"cooldownMs": 12000,
"initialCooldownMs": [
12000,
12000
],
"repeatCooldownMs": [
12000,
12000
],
"damageMultiplier": 0,
"radius": 15,
"telegraph": {
"shape": "circle",
"anchor": "source",
"warningMs": 1800,
"radius": 15
},
"effect": {
"kind": "target-aura",
"aura": "sandy-ties",
"durationMs": 6000
}
},
{
"id": "rw-sand-vortex-100645-aura-494649",
"name": "Corrosive Sand",
"delivery": "area",
"target": "self",
"school": "arcane",
"animation": "cast",
"castTimeMs": 1200,
"range": 0,
"cooldownMs": 15000,
"initialCooldownMs": [
15000,
15000
],
"repeatCooldownMs": [
15000,
15000
],
"damageMultiplier": 0,
"effect": {
"kind": "aura",
"aura": "corrosive-sand",
"durationMs": 7500
}
}
]
},
@@ -112,6 +231,6 @@ export const SAND_VORTEX_POPULATION_PROVENANCE = {
"runeWakerIdsAreNamespaced": true,
"serverEntryPolicy": "RuneWaker numeric ids are never passed to AzerothCore serverEntry.",
"sourceSpellCatalog": "scripts/runewaker-pipeline/snapshots/runewaker-npc-spell-catalog.json",
"sourceSpellCatalogSha256": "ad29efc1d4906fdd4f2ac7a69316cac32d8fcbaea674678f050265c86e0c506c",
"sourceSpellCatalogSha256": "c47e67013e68ecf9c7ae30ef4b2058a850505385739dfe8c32ce8874e6ed077c",
"sourceSpellPolicy": "Only positively classified direct-HP-damage families execute; coefficients remain conservative HealerMan values."
} as const;
@@ -16,7 +16,7 @@ export const SARDO_CASTLE_ENTITIES = {
"leashRange": 24,
"attacks": [
{
"id": "rw-sardo-castle-basic-attack",
"id": "basic-attack",
"name": "Basic Attack",
"delivery": "melee",
"target": "primary",
@@ -56,7 +56,7 @@ export const SARDO_CASTLE_ENTITIES = {
"leashRange": 24,
"attacks": [
{
"id": "rw-sardo-castle-basic-attack",
"id": "basic-attack",
"name": "Basic Attack",
"delivery": "melee",
"target": "primary",
@@ -96,7 +96,7 @@ export const SARDO_CASTLE_ENTITIES = {
"leashRange": 24,
"attacks": [
{
"id": "rw-sardo-castle-basic-attack",
"id": "basic-attack",
"name": "Basic Attack",
"delivery": "melee",
"target": "primary",
@@ -136,7 +136,7 @@ export const SARDO_CASTLE_ENTITIES = {
"leashRange": 24,
"attacks": [
{
"id": "rw-sardo-castle-basic-attack",
"id": "basic-attack",
"name": "Basic Attack",
"delivery": "melee",
"target": "primary",
@@ -216,7 +216,7 @@ export const SARDO_CASTLE_ENTITIES = {
"leashRange": 24,
"attacks": [
{
"id": "rw-sardo-castle-basic-attack",
"id": "basic-attack",
"name": "Basic Attack",
"delivery": "melee",
"target": "primary",
@@ -296,7 +296,7 @@ export const SARDO_CASTLE_ENTITIES = {
"leashRange": 24,
"attacks": [
{
"id": "rw-sardo-castle-basic-attack",
"id": "basic-attack",
"name": "Basic Attack",
"delivery": "melee",
"target": "primary",
@@ -376,7 +376,7 @@ export const SARDO_CASTLE_ENTITIES = {
"leashRange": 24,
"attacks": [
{
"id": "rw-sardo-castle-basic-attack",
"id": "basic-attack",
"name": "Basic Attack",
"delivery": "melee",
"target": "primary",
@@ -456,7 +456,7 @@ export const SARDO_CASTLE_ENTITIES = {
"leashRange": 24,
"attacks": [
{
"id": "rw-sardo-castle-basic-attack",
"id": "basic-attack",
"name": "Basic Attack",
"delivery": "melee",
"target": "primary",
@@ -516,7 +516,7 @@ export const SARDO_CASTLE_ENTITIES = {
"leashRange": 24,
"attacks": [
{
"id": "rw-sardo-castle-basic-attack",
"id": "basic-attack",
"name": "Basic Attack",
"delivery": "melee",
"target": "primary",
@@ -556,7 +556,7 @@ export const SARDO_CASTLE_ENTITIES = {
"leashRange": 24,
"attacks": [
{
"id": "rw-sardo-castle-basic-attack",
"id": "basic-attack",
"name": "Basic Attack",
"delivery": "melee",
"target": "primary",
@@ -616,7 +616,7 @@ export const SARDO_CASTLE_ENTITIES = {
"leashRange": 24,
"attacks": [
{
"id": "rw-sardo-castle-basic-attack",
"id": "basic-attack",
"name": "Basic Attack",
"delivery": "melee",
"target": "primary",
@@ -656,7 +656,7 @@ export const SARDO_CASTLE_ENTITIES = {
"leashRange": 24,
"attacks": [
{
"id": "rw-sardo-castle-basic-attack",
"id": "basic-attack",
"name": "Basic Attack",
"delivery": "melee",
"target": "primary",
@@ -695,9 +695,14 @@ export const SARDO_CASTLE_ENTITIES = {
"damageMultiplier": 1.3,
"moveSpeed": 2.25,
"leashRange": 32,
"mechanicIds": [
"rw-sardo-castle-105701-spell-497652",
"rw-sardo-castle-105701-spell-497653",
"rw-sardo-castle-105701-aura-497654"
],
"attacks": [
{
"id": "rw-sardo-castle-basic-attack",
"id": "basic-attack",
"name": "Basic Attack",
"delivery": "melee",
"target": "primary",
@@ -706,6 +711,87 @@ export const SARDO_CASTLE_ENTITIES = {
"range": 4.5,
"cooldownMs": 1900,
"damageMultiplier": 1
},
{
"id": "rw-sardo-castle-105701-spell-497652",
"name": "Energy Release",
"spellId": 497652,
"delivery": "area",
"target": "nearby-party",
"school": "arcane",
"animation": "cast",
"castTimeMs": 1800,
"range": 15,
"cooldownMs": 6500,
"initialCooldownMs": [
6500,
6500
],
"repeatCooldownMs": [
6500,
6500
],
"damageMultiplier": 0.85,
"radius": 15,
"telegraph": {
"shape": "circle",
"anchor": "source",
"warningMs": 1800,
"radius": 15
}
},
{
"id": "rw-sardo-castle-105701-spell-497653",
"name": "Falling Flame",
"spellId": 497653,
"delivery": "area",
"target": "nearby-party",
"school": "fire",
"animation": "cast",
"castTimeMs": 1800,
"range": 25,
"cooldownMs": 6500,
"initialCooldownMs": [
6500,
6500
],
"repeatCooldownMs": [
6500,
6500
],
"damageMultiplier": 0.85,
"radius": 15,
"telegraph": {
"shape": "circle",
"anchor": "source",
"warningMs": 1800,
"radius": 15
}
},
{
"id": "rw-sardo-castle-105701-aura-497654",
"name": "Dark Healing",
"delivery": "area",
"target": "self",
"school": "arcane",
"animation": "cast",
"castTimeMs": 2000,
"range": 0,
"cooldownMs": 15000,
"initialCooldownMs": [
15000,
15000
],
"repeatCooldownMs": [
15000,
15000
],
"damageMultiplier": 0,
"effect": {
"kind": "aura",
"aura": "dark-healing",
"durationMs": 7500
}
}
]
},
@@ -737,7 +823,7 @@ export const SARDO_CASTLE_ENTITIES = {
"leashRange": 24,
"attacks": [
{
"id": "rw-sardo-castle-basic-attack",
"id": "basic-attack",
"name": "Basic Attack",
"delivery": "melee",
"target": "primary",
@@ -817,7 +903,7 @@ export const SARDO_CASTLE_ENTITIES = {
"leashRange": 24,
"attacks": [
{
"id": "rw-sardo-castle-basic-attack",
"id": "basic-attack",
"name": "Basic Attack",
"delivery": "melee",
"target": "primary",
@@ -857,7 +943,7 @@ export const SARDO_CASTLE_ENTITIES = {
"leashRange": 24,
"attacks": [
{
"id": "rw-sardo-castle-basic-attack",
"id": "basic-attack",
"name": "Basic Attack",
"delivery": "melee",
"target": "primary",
@@ -1740,6 +1826,6 @@ export const SARDO_CASTLE_POPULATION_PROVENANCE = {
"runeWakerIdsAreNamespaced": true,
"serverEntryPolicy": "RuneWaker numeric ids are never passed to AzerothCore serverEntry.",
"sourceSpellCatalog": "scripts/runewaker-pipeline/snapshots/runewaker-npc-spell-catalog.json",
"sourceSpellCatalogSha256": "ad29efc1d4906fdd4f2ac7a69316cac32d8fcbaea674678f050265c86e0c506c",
"sourceSpellCatalogSha256": "c47e67013e68ecf9c7ae30ef4b2058a850505385739dfe8c32ce8874e6ed077c",
"sourceSpellPolicy": "Only positively classified direct-HP-damage families execute; coefficients remain conservative HealerMan values."
} as const;
@@ -16,7 +16,7 @@ export const THE_ORIGIN_ENTITIES = {
"leashRange": 24,
"attacks": [
{
"id": "rw-the-origin-basic-attack",
"id": "basic-attack",
"name": "Basic Attack",
"delivery": "melee",
"target": "primary",
@@ -96,7 +96,7 @@ export const THE_ORIGIN_ENTITIES = {
"leashRange": 24,
"attacks": [
{
"id": "rw-the-origin-basic-attack",
"id": "basic-attack",
"name": "Basic Attack",
"delivery": "melee",
"target": "primary",
@@ -176,7 +176,7 @@ export const THE_ORIGIN_ENTITIES = {
"leashRange": 24,
"attacks": [
{
"id": "rw-the-origin-basic-attack",
"id": "basic-attack",
"name": "Basic Attack",
"delivery": "melee",
"target": "primary",
@@ -216,7 +216,7 @@ export const THE_ORIGIN_ENTITIES = {
"leashRange": 24,
"attacks": [
{
"id": "rw-the-origin-basic-attack",
"id": "basic-attack",
"name": "Basic Attack",
"delivery": "melee",
"target": "primary",
@@ -296,7 +296,7 @@ export const THE_ORIGIN_ENTITIES = {
"leashRange": 24,
"attacks": [
{
"id": "rw-the-origin-basic-attack",
"id": "basic-attack",
"name": "Basic Attack",
"delivery": "melee",
"target": "primary",
@@ -376,7 +376,7 @@ export const THE_ORIGIN_ENTITIES = {
"leashRange": 24,
"attacks": [
{
"id": "rw-the-origin-basic-attack",
"id": "basic-attack",
"name": "Basic Attack",
"delivery": "melee",
"target": "primary",
@@ -436,7 +436,7 @@ export const THE_ORIGIN_ENTITIES = {
"leashRange": 24,
"attacks": [
{
"id": "rw-the-origin-basic-attack",
"id": "basic-attack",
"name": "Basic Attack",
"delivery": "melee",
"target": "primary",
@@ -496,7 +496,7 @@ export const THE_ORIGIN_ENTITIES = {
"leashRange": 24,
"attacks": [
{
"id": "rw-the-origin-basic-attack",
"id": "basic-attack",
"name": "Basic Attack",
"delivery": "melee",
"target": "primary",
@@ -576,7 +576,7 @@ export const THE_ORIGIN_ENTITIES = {
"leashRange": 24,
"attacks": [
{
"id": "rw-the-origin-basic-attack",
"id": "basic-attack",
"name": "Basic Attack",
"delivery": "melee",
"target": "primary",
@@ -616,7 +616,7 @@ export const THE_ORIGIN_ENTITIES = {
"leashRange": 24,
"attacks": [
{
"id": "rw-the-origin-basic-attack",
"id": "basic-attack",
"name": "Basic Attack",
"delivery": "melee",
"target": "primary",
@@ -696,7 +696,7 @@ export const THE_ORIGIN_ENTITIES = {
"leashRange": 24,
"attacks": [
{
"id": "rw-the-origin-basic-attack",
"id": "basic-attack",
"name": "Basic Attack",
"delivery": "melee",
"target": "primary",
@@ -756,7 +756,7 @@ export const THE_ORIGIN_ENTITIES = {
"leashRange": 24,
"attacks": [
{
"id": "rw-the-origin-basic-attack",
"id": "basic-attack",
"name": "Basic Attack",
"delivery": "melee",
"target": "primary",
@@ -836,7 +836,7 @@ export const THE_ORIGIN_ENTITIES = {
"leashRange": 24,
"attacks": [
{
"id": "rw-the-origin-basic-attack",
"id": "basic-attack",
"name": "Basic Attack",
"delivery": "melee",
"target": "primary",
@@ -916,7 +916,7 @@ export const THE_ORIGIN_ENTITIES = {
"leashRange": 24,
"attacks": [
{
"id": "rw-the-origin-basic-attack",
"id": "basic-attack",
"name": "Basic Attack",
"delivery": "melee",
"target": "primary",
@@ -956,7 +956,7 @@ export const THE_ORIGIN_ENTITIES = {
"leashRange": 24,
"attacks": [
{
"id": "rw-the-origin-basic-attack",
"id": "basic-attack",
"name": "Basic Attack",
"delivery": "melee",
"target": "primary",
@@ -996,7 +996,7 @@ export const THE_ORIGIN_ENTITIES = {
"leashRange": 24,
"attacks": [
{
"id": "rw-the-origin-basic-attack",
"id": "basic-attack",
"name": "Basic Attack",
"delivery": "melee",
"target": "primary",
@@ -1076,7 +1076,7 @@ export const THE_ORIGIN_ENTITIES = {
"leashRange": 24,
"attacks": [
{
"id": "rw-the-origin-basic-attack",
"id": "basic-attack",
"name": "Basic Attack",
"delivery": "melee",
"target": "primary",
@@ -1156,7 +1156,7 @@ export const THE_ORIGIN_ENTITIES = {
"leashRange": 24,
"attacks": [
{
"id": "rw-the-origin-basic-attack",
"id": "basic-attack",
"name": "Basic Attack",
"delivery": "melee",
"target": "primary",
@@ -1256,7 +1256,7 @@ export const THE_ORIGIN_ENTITIES = {
"leashRange": 24,
"attacks": [
{
"id": "rw-the-origin-basic-attack",
"id": "basic-attack",
"name": "Basic Attack",
"delivery": "melee",
"target": "primary",
@@ -1296,7 +1296,7 @@ export const THE_ORIGIN_ENTITIES = {
"leashRange": 24,
"attacks": [
{
"id": "rw-the-origin-basic-attack",
"id": "basic-attack",
"name": "Basic Attack",
"delivery": "melee",
"target": "primary",
@@ -1396,7 +1396,7 @@ export const THE_ORIGIN_ENTITIES = {
"leashRange": 24,
"attacks": [
{
"id": "rw-the-origin-basic-attack",
"id": "basic-attack",
"name": "Basic Attack",
"delivery": "melee",
"target": "primary",
@@ -1436,7 +1436,7 @@ export const THE_ORIGIN_ENTITIES = {
"leashRange": 24,
"attacks": [
{
"id": "rw-the-origin-basic-attack",
"id": "basic-attack",
"name": "Basic Attack",
"delivery": "melee",
"target": "primary",
@@ -1476,7 +1476,7 @@ export const THE_ORIGIN_ENTITIES = {
"leashRange": 24,
"attacks": [
{
"id": "rw-the-origin-basic-attack",
"id": "basic-attack",
"name": "Basic Attack",
"delivery": "melee",
"target": "primary",
@@ -1515,9 +1515,14 @@ export const THE_ORIGIN_ENTITIES = {
"damageMultiplier": 1.3,
"moveSpeed": 2.25,
"leashRange": 32,
"mechanicIds": [
"rw-the-origin-102349-spell-493663",
"rw-the-origin-102349-spell-493664",
"rw-the-origin-102349-aura-493665"
],
"attacks": [
{
"id": "rw-the-origin-basic-attack",
"id": "basic-attack",
"name": "Basic Attack",
"delivery": "melee",
"target": "primary",
@@ -1526,6 +1531,73 @@ export const THE_ORIGIN_ENTITIES = {
"range": 4.5,
"cooldownMs": 1900,
"damageMultiplier": 1
},
{
"id": "rw-the-origin-102349-spell-493663",
"name": "Spike Puncture",
"spellId": 493663,
"delivery": "melee",
"target": "primary",
"school": "physical",
"animation": "attack",
"castTimeMs": 0,
"range": 5,
"cooldownMs": 6500,
"initialCooldownMs": [
6500,
6500
],
"repeatCooldownMs": [
6500,
6500
],
"damageMultiplier": 0.85
},
{
"id": "rw-the-origin-102349-spell-493664",
"name": "Earth Rupture",
"spellId": 493664,
"delivery": "projectile",
"target": "primary",
"school": "physical",
"animation": "cast",
"castTimeMs": 0,
"range": 30,
"cooldownMs": 6500,
"initialCooldownMs": [
6500,
6500
],
"repeatCooldownMs": [
6500,
6500
],
"damageMultiplier": 0.85
},
{
"id": "rw-the-origin-102349-aura-493665",
"name": "King Bug Shock",
"delivery": "area",
"target": "self",
"school": "arcane",
"animation": "cast",
"castTimeMs": 2000,
"range": 0,
"cooldownMs": 15000,
"initialCooldownMs": [
15000,
15000
],
"repeatCooldownMs": [
15000,
15000
],
"damageMultiplier": 0,
"effect": {
"kind": "aura",
"aura": "king-bug-shock",
"durationMs": 7500
}
}
]
},
@@ -1557,7 +1629,7 @@ export const THE_ORIGIN_ENTITIES = {
"leashRange": 24,
"attacks": [
{
"id": "rw-the-origin-basic-attack",
"id": "basic-attack",
"name": "Basic Attack",
"delivery": "melee",
"target": "primary",
@@ -1596,9 +1668,17 @@ export const THE_ORIGIN_ENTITIES = {
"damageMultiplier": 1.3,
"moveSpeed": 2.25,
"leashRange": 32,
"mechanicIds": [
"rw-the-origin-102307-spell-493590",
"rw-the-origin-102307-aura-493592",
"rw-the-origin-102307-spell-493649",
"rw-the-origin-102307-aura-493650",
"rw-the-origin-102307-aura-493651",
"rw-the-origin-102307-spell-493652"
],
"attacks": [
{
"id": "rw-the-origin-basic-attack",
"id": "basic-attack",
"name": "Basic Attack",
"delivery": "melee",
"target": "primary",
@@ -1607,6 +1687,151 @@ export const THE_ORIGIN_ENTITIES = {
"range": 4.5,
"cooldownMs": 1900,
"damageMultiplier": 1
},
{
"id": "rw-the-origin-102307-spell-493590",
"name": "Power of the Ents",
"spellId": 493590,
"delivery": "projectile",
"target": "primary",
"school": "physical",
"animation": "cast",
"castTimeMs": 0,
"range": 10,
"cooldownMs": 6500,
"initialCooldownMs": [
6500,
6500
],
"repeatCooldownMs": [
6500,
6500
],
"damageMultiplier": 0.85
},
{
"id": "rw-the-origin-102307-aura-493592",
"name": "Manastorms Duo Lv5",
"delivery": "area",
"target": "self",
"school": "arcane",
"animation": "cast",
"castTimeMs": 1200,
"range": 0,
"cooldownMs": 15000,
"initialCooldownMs": [
15000,
15000
],
"repeatCooldownMs": [
15000,
15000
],
"damageMultiplier": 0,
"effect": {
"kind": "aura",
"aura": "manastorms-duo-lv5",
"durationMs": 7500
}
},
{
"id": "rw-the-origin-102307-spell-493649",
"name": "Giant Tree Heavy Hammer",
"spellId": 493649,
"delivery": "melee",
"target": "primary",
"school": "physical",
"animation": "attack",
"castTimeMs": 0,
"range": 5,
"cooldownMs": 6500,
"initialCooldownMs": [
6500,
6500
],
"repeatCooldownMs": [
6500,
6500
],
"damageMultiplier": 0.85
},
{
"id": "rw-the-origin-102307-aura-493650",
"name": "Impaling Thorns",
"delivery": "area",
"target": "self",
"school": "arcane",
"animation": "cast",
"castTimeMs": 1200,
"range": 0,
"cooldownMs": 15000,
"initialCooldownMs": [
15000,
15000
],
"repeatCooldownMs": [
15000,
15000
],
"damageMultiplier": 0,
"effect": {
"kind": "aura",
"aura": "impaling-thorns",
"durationMs": 7500
}
},
{
"id": "rw-the-origin-102307-aura-493651",
"name": "Entangling Tree Vines",
"delivery": "area",
"target": "self",
"school": "arcane",
"animation": "cast",
"castTimeMs": 1200,
"range": 0,
"cooldownMs": 15000,
"initialCooldownMs": [
15000,
15000
],
"repeatCooldownMs": [
15000,
15000
],
"damageMultiplier": 0,
"effect": {
"kind": "aura",
"aura": "armor-penetrating-tree-vine",
"durationMs": 7500
}
},
{
"id": "rw-the-origin-102307-spell-493652",
"name": "Giant Wood Thicket",
"spellId": 493652,
"delivery": "area",
"target": "nearby-party",
"school": "arcane",
"animation": "cast",
"castTimeMs": 1800,
"range": 15,
"cooldownMs": 6500,
"initialCooldownMs": [
6500,
6500
],
"repeatCooldownMs": [
6500,
6500
],
"damageMultiplier": 0.85,
"radius": 15,
"telegraph": {
"shape": "circle",
"anchor": "source",
"warningMs": 1800,
"radius": 15
}
}
]
},
@@ -3759,6 +3984,6 @@ export const THE_ORIGIN_POPULATION_PROVENANCE = {
"runeWakerIdsAreNamespaced": true,
"serverEntryPolicy": "RuneWaker numeric ids are never passed to AzerothCore serverEntry.",
"sourceSpellCatalog": "scripts/runewaker-pipeline/snapshots/runewaker-npc-spell-catalog.json",
"sourceSpellCatalogSha256": "ad29efc1d4906fdd4f2ac7a69316cac32d8fcbaea674678f050265c86e0c506c",
"sourceSpellCatalogSha256": "c47e67013e68ecf9c7ae30ef4b2058a850505385739dfe8c32ce8874e6ed077c",
"sourceSpellPolicy": "Only positively classified direct-HP-damage families execute; coefficients remain conservative HealerMan values."
} as const;
@@ -16,7 +16,7 @@ export const TOMB_SEVEN_HEROES_ENTITIES = {
"leashRange": 24,
"attacks": [
{
"id": "rw-tomb-seven-heroes-basic-attack",
"id": "basic-attack",
"name": "Basic Attack",
"delivery": "melee",
"target": "primary",
@@ -116,7 +116,7 @@ export const TOMB_SEVEN_HEROES_ENTITIES = {
"leashRange": 24,
"attacks": [
{
"id": "rw-tomb-seven-heroes-basic-attack",
"id": "basic-attack",
"name": "Basic Attack",
"delivery": "melee",
"target": "primary",
@@ -196,7 +196,7 @@ export const TOMB_SEVEN_HEROES_ENTITIES = {
"leashRange": 24,
"attacks": [
{
"id": "rw-tomb-seven-heroes-basic-attack",
"id": "basic-attack",
"name": "Basic Attack",
"delivery": "melee",
"target": "primary",
@@ -276,7 +276,7 @@ export const TOMB_SEVEN_HEROES_ENTITIES = {
"leashRange": 24,
"attacks": [
{
"id": "rw-tomb-seven-heroes-basic-attack",
"id": "basic-attack",
"name": "Basic Attack",
"delivery": "melee",
"target": "primary",
@@ -356,7 +356,7 @@ export const TOMB_SEVEN_HEROES_ENTITIES = {
"leashRange": 24,
"attacks": [
{
"id": "rw-tomb-seven-heroes-basic-attack",
"id": "basic-attack",
"name": "Basic Attack",
"delivery": "melee",
"target": "primary",
@@ -436,7 +436,7 @@ export const TOMB_SEVEN_HEROES_ENTITIES = {
"leashRange": 24,
"attacks": [
{
"id": "rw-tomb-seven-heroes-basic-attack",
"id": "basic-attack",
"name": "Basic Attack",
"delivery": "melee",
"target": "primary",
@@ -516,7 +516,7 @@ export const TOMB_SEVEN_HEROES_ENTITIES = {
"leashRange": 24,
"attacks": [
{
"id": "rw-tomb-seven-heroes-basic-attack",
"id": "basic-attack",
"name": "Basic Attack",
"delivery": "melee",
"target": "primary",
@@ -556,7 +556,7 @@ export const TOMB_SEVEN_HEROES_ENTITIES = {
"leashRange": 24,
"attacks": [
{
"id": "rw-tomb-seven-heroes-basic-attack",
"id": "basic-attack",
"name": "Basic Attack",
"delivery": "melee",
"target": "primary",
@@ -656,7 +656,7 @@ export const TOMB_SEVEN_HEROES_ENTITIES = {
"leashRange": 24,
"attacks": [
{
"id": "rw-tomb-seven-heroes-basic-attack",
"id": "basic-attack",
"name": "Basic Attack",
"delivery": "melee",
"target": "primary",
@@ -736,7 +736,7 @@ export const TOMB_SEVEN_HEROES_ENTITIES = {
"leashRange": 24,
"attacks": [
{
"id": "rw-tomb-seven-heroes-basic-attack",
"id": "basic-attack",
"name": "Basic Attack",
"delivery": "melee",
"target": "primary",
@@ -776,7 +776,7 @@ export const TOMB_SEVEN_HEROES_ENTITIES = {
"leashRange": 24,
"attacks": [
{
"id": "rw-tomb-seven-heroes-basic-attack",
"id": "basic-attack",
"name": "Basic Attack",
"delivery": "melee",
"target": "primary",
@@ -856,7 +856,7 @@ export const TOMB_SEVEN_HEROES_ENTITIES = {
"leashRange": 24,
"attacks": [
{
"id": "rw-tomb-seven-heroes-basic-attack",
"id": "basic-attack",
"name": "Basic Attack",
"delivery": "melee",
"target": "primary",
@@ -896,7 +896,7 @@ export const TOMB_SEVEN_HEROES_ENTITIES = {
"leashRange": 24,
"attacks": [
{
"id": "rw-tomb-seven-heroes-basic-attack",
"id": "basic-attack",
"name": "Basic Attack",
"delivery": "melee",
"target": "primary",
@@ -1016,7 +1016,7 @@ export const TOMB_SEVEN_HEROES_ENTITIES = {
"leashRange": 24,
"attacks": [
{
"id": "rw-tomb-seven-heroes-basic-attack",
"id": "basic-attack",
"name": "Basic Attack",
"delivery": "melee",
"target": "primary",
@@ -1076,7 +1076,7 @@ export const TOMB_SEVEN_HEROES_ENTITIES = {
"leashRange": 24,
"attacks": [
{
"id": "rw-tomb-seven-heroes-basic-attack",
"id": "basic-attack",
"name": "Basic Attack",
"delivery": "melee",
"target": "primary",
@@ -1136,7 +1136,7 @@ export const TOMB_SEVEN_HEROES_ENTITIES = {
"leashRange": 24,
"attacks": [
{
"id": "rw-tomb-seven-heroes-basic-attack",
"id": "basic-attack",
"name": "Basic Attack",
"delivery": "melee",
"target": "primary",
@@ -1196,7 +1196,7 @@ export const TOMB_SEVEN_HEROES_ENTITIES = {
"leashRange": 24,
"attacks": [
{
"id": "rw-tomb-seven-heroes-basic-attack",
"id": "basic-attack",
"name": "Basic Attack",
"delivery": "melee",
"target": "primary",
@@ -1236,7 +1236,7 @@ export const TOMB_SEVEN_HEROES_ENTITIES = {
"leashRange": 24,
"attacks": [
{
"id": "rw-tomb-seven-heroes-basic-attack",
"id": "basic-attack",
"name": "Basic Attack",
"delivery": "melee",
"target": "primary",
@@ -1296,7 +1296,7 @@ export const TOMB_SEVEN_HEROES_ENTITIES = {
"leashRange": 24,
"attacks": [
{
"id": "rw-tomb-seven-heroes-basic-attack",
"id": "basic-attack",
"name": "Basic Attack",
"delivery": "melee",
"target": "primary",
@@ -1376,7 +1376,7 @@ export const TOMB_SEVEN_HEROES_ENTITIES = {
"leashRange": 24,
"attacks": [
{
"id": "rw-tomb-seven-heroes-basic-attack",
"id": "basic-attack",
"name": "Basic Attack",
"delivery": "melee",
"target": "primary",
@@ -1436,7 +1436,7 @@ export const TOMB_SEVEN_HEROES_ENTITIES = {
"leashRange": 24,
"attacks": [
{
"id": "rw-tomb-seven-heroes-basic-attack",
"id": "basic-attack",
"name": "Basic Attack",
"delivery": "melee",
"target": "primary",
@@ -1496,7 +1496,7 @@ export const TOMB_SEVEN_HEROES_ENTITIES = {
"leashRange": 24,
"attacks": [
{
"id": "rw-tomb-seven-heroes-basic-attack",
"id": "basic-attack",
"name": "Basic Attack",
"delivery": "melee",
"target": "primary",
@@ -1536,7 +1536,7 @@ export const TOMB_SEVEN_HEROES_ENTITIES = {
"leashRange": 24,
"attacks": [
{
"id": "rw-tomb-seven-heroes-basic-attack",
"id": "basic-attack",
"name": "Basic Attack",
"delivery": "melee",
"target": "primary",
@@ -1576,7 +1576,7 @@ export const TOMB_SEVEN_HEROES_ENTITIES = {
"leashRange": 24,
"attacks": [
{
"id": "rw-tomb-seven-heroes-basic-attack",
"id": "basic-attack",
"name": "Basic Attack",
"delivery": "melee",
"target": "primary",
@@ -1616,7 +1616,7 @@ export const TOMB_SEVEN_HEROES_ENTITIES = {
"leashRange": 24,
"attacks": [
{
"id": "rw-tomb-seven-heroes-basic-attack",
"id": "basic-attack",
"name": "Basic Attack",
"delivery": "melee",
"target": "primary",
@@ -4330,6 +4330,6 @@ export const TOMB_SEVEN_HEROES_POPULATION_PROVENANCE = {
"runeWakerIdsAreNamespaced": true,
"serverEntryPolicy": "RuneWaker numeric ids are never passed to AzerothCore serverEntry.",
"sourceSpellCatalog": "scripts/runewaker-pipeline/snapshots/runewaker-npc-spell-catalog.json",
"sourceSpellCatalogSha256": "ad29efc1d4906fdd4f2ac7a69316cac32d8fcbaea674678f050265c86e0c506c",
"sourceSpellCatalogSha256": "c47e67013e68ecf9c7ae30ef4b2058a850505385739dfe8c32ce8874e6ed077c",
"sourceSpellPolicy": "Only positively classified direct-HP-damage families execute; coefficients remain conservative HealerMan values."
} as const;
@@ -16,7 +16,7 @@ export const TREASURE_TROVE_ENTITIES = {
"leashRange": 24,
"attacks": [
{
"id": "rw-treasure-trove-basic-attack",
"id": "basic-attack",
"name": "Basic Attack",
"delivery": "melee",
"target": "primary",
@@ -75,9 +75,15 @@ export const TREASURE_TROVE_ENTITIES = {
"damageMultiplier": 1.3,
"moveSpeed": 2.25,
"leashRange": 32,
"mechanicIds": [
"rw-treasure-trove-101533-spell-490477",
"rw-treasure-trove-101533-aura-492431",
"rw-treasure-trove-101533-debuff-492433",
"rw-treasure-trove-101533-spell-492434"
],
"attacks": [
{
"id": "rw-treasure-trove-basic-attack",
"id": "basic-attack",
"name": "Basic Attack",
"delivery": "melee",
"target": "primary",
@@ -88,33 +94,14 @@ export const TREASURE_TROVE_ENTITIES = {
"damageMultiplier": 1
},
{
"id": "rw-treasure-trove-spell-492434",
"name": "Boulder Strike",
"spellId": 492434,
"delivery": "projectile",
"target": "primary",
"school": "physical",
"animation": "cast",
"range": 30,
"cooldownMs": 20000,
"initialCooldownMs": [
20000,
20000
],
"repeatCooldownMs": [
20000,
20000
],
"damageMultiplier": 0.85
},
{
"id": "rw-treasure-trove-spell-490477",
"id": "rw-treasure-trove-101533-spell-490477",
"name": "Slash",
"spellId": 490477,
"delivery": "melee",
"target": "primary",
"school": "physical",
"animation": "attack",
"castTimeMs": 0,
"range": 5,
"cooldownMs": 10000,
"initialCooldownMs": [
@@ -126,6 +113,77 @@ export const TREASURE_TROVE_ENTITIES = {
10000
],
"damageMultiplier": 0.85
},
{
"id": "rw-treasure-trove-101533-aura-492431",
"name": "Strengthen Body",
"delivery": "area",
"target": "self",
"school": "arcane",
"animation": "cast",
"castTimeMs": 1200,
"range": 0,
"cooldownMs": 10000,
"initialCooldownMs": [
10000,
10000
],
"repeatCooldownMs": [
10000,
10000
],
"damageMultiplier": 0,
"effect": {
"kind": "aura",
"aura": "strengthen-body",
"durationMs": 5000
}
},
{
"id": "rw-treasure-trove-101533-debuff-492433",
"name": "Armor Damaging Attack",
"delivery": "projectile",
"target": "primary",
"school": "arcane",
"animation": "cast",
"castTimeMs": 1800,
"range": 5,
"cooldownMs": 20000,
"initialCooldownMs": [
20000,
20000
],
"repeatCooldownMs": [
20000,
20000
],
"damageMultiplier": 0,
"effect": {
"kind": "target-aura",
"aura": "armor-damage",
"durationMs": 10000
}
},
{
"id": "rw-treasure-trove-101533-spell-492434",
"name": "Boulder Strike",
"spellId": 492434,
"delivery": "projectile",
"target": "primary",
"school": "physical",
"animation": "cast",
"castTimeMs": 0,
"range": 30,
"cooldownMs": 20000,
"initialCooldownMs": [
20000,
20000
],
"repeatCooldownMs": [
20000,
20000
],
"damageMultiplier": 0.85
}
]
},
@@ -156,9 +214,15 @@ export const TREASURE_TROVE_ENTITIES = {
"damageMultiplier": 1.3,
"moveSpeed": 2.25,
"leashRange": 32,
"mechanicIds": [
"rw-treasure-trove-101535-debuff-492435",
"rw-treasure-trove-101535-aura-492436",
"rw-treasure-trove-101535-spell-492438",
"rw-treasure-trove-101535-aura-492991"
],
"attacks": [
{
"id": "rw-treasure-trove-basic-attack",
"id": "basic-attack",
"name": "Basic Attack",
"delivery": "melee",
"target": "primary",
@@ -169,14 +233,65 @@ export const TREASURE_TROVE_ENTITIES = {
"damageMultiplier": 1
},
{
"id": "rw-treasure-trove-spell-492438",
"id": "rw-treasure-trove-101535-debuff-492435",
"name": "Open Wound",
"delivery": "projectile",
"target": "primary",
"school": "arcane",
"animation": "cast",
"castTimeMs": 1800,
"range": 30,
"cooldownMs": 20000,
"initialCooldownMs": [
20000,
20000
],
"repeatCooldownMs": [
20000,
20000
],
"damageMultiplier": 0,
"effect": {
"kind": "target-aura",
"aura": "absorbed-heal",
"durationMs": 10000
}
},
{
"id": "rw-treasure-trove-101535-aura-492436",
"name": "Queen's Summons",
"delivery": "area",
"target": "self",
"school": "arcane",
"animation": "cast",
"castTimeMs": 1200,
"range": 0,
"cooldownMs": 30000,
"initialCooldownMs": [
30000,
30000
],
"repeatCooldownMs": [
30000,
30000
],
"damageMultiplier": 0,
"effect": {
"kind": "aura",
"aura": "queen-s-summons",
"durationMs": 12000
}
},
{
"id": "rw-treasure-trove-101535-spell-492438",
"name": "Snow's Dance",
"spellId": 492438,
"delivery": "melee",
"target": "primary",
"delivery": "area",
"target": "nearby-party",
"school": "physical",
"animation": "attack",
"range": 4.5,
"castTimeMs": 1800,
"range": 5,
"cooldownMs": 15000,
"initialCooldownMs": [
15000,
@@ -186,7 +301,39 @@ export const TREASURE_TROVE_ENTITIES = {
15000,
15000
],
"damageMultiplier": 0.85
"damageMultiplier": 0.85,
"radius": 5,
"telegraph": {
"shape": "circle",
"anchor": "source",
"warningMs": 1800,
"radius": 5
}
},
{
"id": "rw-treasure-trove-101535-aura-492991",
"name": "Rose Flower Demon Vine",
"delivery": "area",
"target": "self",
"school": "arcane",
"animation": "cast",
"castTimeMs": 1200,
"range": 0,
"cooldownMs": 30000,
"initialCooldownMs": [
30000,
30000
],
"repeatCooldownMs": [
30000,
30000
],
"damageMultiplier": 0,
"effect": {
"kind": "aura",
"aura": "rose-flower-demon-vine",
"durationMs": 12000
}
}
]
},
@@ -218,7 +365,7 @@ export const TREASURE_TROVE_ENTITIES = {
"leashRange": 24,
"attacks": [
{
"id": "rw-treasure-trove-basic-attack",
"id": "basic-attack",
"name": "Basic Attack",
"delivery": "melee",
"target": "primary",
@@ -318,7 +465,7 @@ export const TREASURE_TROVE_ENTITIES = {
"leashRange": 24,
"attacks": [
{
"id": "rw-treasure-trove-basic-attack",
"id": "basic-attack",
"name": "Basic Attack",
"delivery": "melee",
"target": "primary",
@@ -418,7 +565,7 @@ export const TREASURE_TROVE_ENTITIES = {
"leashRange": 24,
"attacks": [
{
"id": "rw-treasure-trove-basic-attack",
"id": "basic-attack",
"name": "Basic Attack",
"delivery": "melee",
"target": "primary",
@@ -518,7 +665,7 @@ export const TREASURE_TROVE_ENTITIES = {
"leashRange": 24,
"attacks": [
{
"id": "rw-treasure-trove-basic-attack",
"id": "basic-attack",
"name": "Basic Attack",
"delivery": "melee",
"target": "primary",
@@ -598,7 +745,7 @@ export const TREASURE_TROVE_ENTITIES = {
"leashRange": 24,
"attacks": [
{
"id": "rw-treasure-trove-basic-attack",
"id": "basic-attack",
"name": "Basic Attack",
"delivery": "melee",
"target": "primary",
@@ -698,7 +845,7 @@ export const TREASURE_TROVE_ENTITIES = {
"leashRange": 24,
"attacks": [
{
"id": "rw-treasure-trove-basic-attack",
"id": "basic-attack",
"name": "Basic Attack",
"delivery": "melee",
"target": "primary",
@@ -778,7 +925,7 @@ export const TREASURE_TROVE_ENTITIES = {
"leashRange": 24,
"attacks": [
{
"id": "rw-treasure-trove-basic-attack",
"id": "basic-attack",
"name": "Basic Attack",
"delivery": "melee",
"target": "primary",
@@ -878,7 +1025,7 @@ export const TREASURE_TROVE_ENTITIES = {
"leashRange": 24,
"attacks": [
{
"id": "rw-treasure-trove-basic-attack",
"id": "basic-attack",
"name": "Basic Attack",
"delivery": "melee",
"target": "primary",
@@ -938,7 +1085,7 @@ export const TREASURE_TROVE_ENTITIES = {
"leashRange": 24,
"attacks": [
{
"id": "rw-treasure-trove-basic-attack",
"id": "basic-attack",
"name": "Basic Attack",
"delivery": "melee",
"target": "primary",
@@ -998,7 +1145,7 @@ export const TREASURE_TROVE_ENTITIES = {
"leashRange": 24,
"attacks": [
{
"id": "rw-treasure-trove-basic-attack",
"id": "basic-attack",
"name": "Basic Attack",
"delivery": "melee",
"target": "primary",
@@ -1078,7 +1225,7 @@ export const TREASURE_TROVE_ENTITIES = {
"leashRange": 24,
"attacks": [
{
"id": "rw-treasure-trove-basic-attack",
"id": "basic-attack",
"name": "Basic Attack",
"delivery": "melee",
"target": "primary",
@@ -1138,7 +1285,7 @@ export const TREASURE_TROVE_ENTITIES = {
"leashRange": 24,
"attacks": [
{
"id": "rw-treasure-trove-basic-attack",
"id": "basic-attack",
"name": "Basic Attack",
"delivery": "melee",
"target": "primary",
@@ -1217,9 +1364,15 @@ export const TREASURE_TROVE_ENTITIES = {
"damageMultiplier": 1.3,
"moveSpeed": 2.25,
"leashRange": 32,
"mechanicIds": [
"rw-treasure-trove-101534-spell-490477",
"rw-treasure-trove-101534-spell-492428",
"rw-treasure-trove-101534-debuff-492429",
"rw-treasure-trove-101534-spell-492430"
],
"attacks": [
{
"id": "rw-treasure-trove-basic-attack",
"id": "basic-attack",
"name": "Basic Attack",
"delivery": "melee",
"target": "primary",
@@ -1230,13 +1383,14 @@ export const TREASURE_TROVE_ENTITIES = {
"damageMultiplier": 1
},
{
"id": "rw-treasure-trove-spell-490477",
"id": "rw-treasure-trove-101534-spell-490477",
"name": "Slash",
"spellId": 490477,
"delivery": "melee",
"target": "primary",
"school": "physical",
"animation": "attack",
"castTimeMs": 0,
"range": 5,
"cooldownMs": 10000,
"initialCooldownMs": [
@@ -1248,6 +1402,87 @@ export const TREASURE_TROVE_ENTITIES = {
10000
],
"damageMultiplier": 0.85
},
{
"id": "rw-treasure-trove-101534-spell-492428",
"name": "Dizzy Chop",
"spellId": 492428,
"delivery": "melee",
"target": "primary",
"school": "physical",
"animation": "attack",
"castTimeMs": 0,
"range": 5,
"cooldownMs": 20000,
"initialCooldownMs": [
20000,
20000
],
"repeatCooldownMs": [
20000,
20000
],
"damageMultiplier": 0.85
},
{
"id": "rw-treasure-trove-101534-debuff-492429",
"name": "Focused Gaze",
"delivery": "area",
"target": "nearby-party",
"school": "arcane",
"animation": "cast",
"castTimeMs": 1800,
"range": 30,
"cooldownMs": 15000,
"initialCooldownMs": [
15000,
15000
],
"repeatCooldownMs": [
15000,
15000
],
"damageMultiplier": 0,
"radius": 15,
"telegraph": {
"shape": "circle",
"anchor": "source",
"warningMs": 1800,
"radius": 15
},
"effect": {
"kind": "target-aura",
"aura": "weak-spot-exposure",
"durationMs": 7500
}
},
{
"id": "rw-treasure-trove-101534-spell-492430",
"name": "Aimed Throw",
"spellId": 492430,
"delivery": "area",
"target": "nearby-party",
"school": "physical",
"animation": "cast",
"castTimeMs": 1800,
"range": 30,
"cooldownMs": 6500,
"initialCooldownMs": [
6500,
6500
],
"repeatCooldownMs": [
6500,
6500
],
"damageMultiplier": 0.85,
"radius": 15,
"telegraph": {
"shape": "circle",
"anchor": "source",
"warningMs": 1800,
"radius": 15
}
}
]
},
@@ -1279,7 +1514,7 @@ export const TREASURE_TROVE_ENTITIES = {
"leashRange": 24,
"attacks": [
{
"id": "rw-treasure-trove-basic-attack",
"id": "basic-attack",
"name": "Basic Attack",
"delivery": "melee",
"target": "primary",
@@ -1319,7 +1554,7 @@ export const TREASURE_TROVE_ENTITIES = {
"leashRange": 24,
"attacks": [
{
"id": "rw-treasure-trove-basic-attack",
"id": "basic-attack",
"name": "Basic Attack",
"delivery": "melee",
"target": "primary",
@@ -1379,7 +1614,7 @@ export const TREASURE_TROVE_ENTITIES = {
"leashRange": 24,
"attacks": [
{
"id": "rw-treasure-trove-basic-attack",
"id": "basic-attack",
"name": "Basic Attack",
"delivery": "melee",
"target": "primary",
@@ -1439,7 +1674,7 @@ export const TREASURE_TROVE_ENTITIES = {
"leashRange": 24,
"attacks": [
{
"id": "rw-treasure-trove-basic-attack",
"id": "basic-attack",
"name": "Basic Attack",
"delivery": "melee",
"target": "primary",
@@ -1499,7 +1734,7 @@ export const TREASURE_TROVE_ENTITIES = {
"leashRange": 24,
"attacks": [
{
"id": "rw-treasure-trove-basic-attack",
"id": "basic-attack",
"name": "Basic Attack",
"delivery": "melee",
"target": "primary",
@@ -7068,6 +7303,6 @@ export const TREASURE_TROVE_POPULATION_PROVENANCE = {
"runeWakerIdsAreNamespaced": true,
"serverEntryPolicy": "RuneWaker numeric ids are never passed to AzerothCore serverEntry.",
"sourceSpellCatalog": "scripts/runewaker-pipeline/snapshots/runewaker-npc-spell-catalog.json",
"sourceSpellCatalogSha256": "ad29efc1d4906fdd4f2ac7a69316cac32d8fcbaea674678f050265c86e0c506c",
"sourceSpellCatalogSha256": "c47e67013e68ecf9c7ae30ef4b2058a850505385739dfe8c32ce8874e6ed077c",
"sourceSpellPolicy": "Only positively classified direct-HP-damage families execute; coefficients remain conservative HealerMan values."
} as const;
@@ -16,7 +16,7 @@ export const WARNORKEN_ARENA_ENTITIES = {
"leashRange": 24,
"attacks": [
{
"id": "rw-warnorken-arena-basic-attack",
"id": "basic-attack",
"name": "Basic Attack",
"delivery": "melee",
"target": "primary",
@@ -101,6 +101,6 @@ export const WARNORKEN_ARENA_POPULATION_PROVENANCE = {
"runeWakerIdsAreNamespaced": true,
"serverEntryPolicy": "RuneWaker numeric ids are never passed to AzerothCore serverEntry.",
"sourceSpellCatalog": "scripts/runewaker-pipeline/snapshots/runewaker-npc-spell-catalog.json",
"sourceSpellCatalogSha256": "ad29efc1d4906fdd4f2ac7a69316cac32d8fcbaea674678f050265c86e0c506c",
"sourceSpellCatalogSha256": "c47e67013e68ecf9c7ae30ef4b2058a850505385739dfe8c32ce8874e6ed077c",
"sourceSpellPolicy": "Only positively classified direct-HP-damage families execute; coefficients remain conservative HealerMan values."
} as const;
@@ -15,9 +15,19 @@ export const YSTRA_LABYRINTH_BOSS1_ENTITIES = {
"damageMultiplier": 1.3,
"moveSpeed": 2.25,
"leashRange": 32,
"mechanicIds": [
"rw-ystra-labyrinth-boss1-102195-spell-490634",
"rw-ystra-labyrinth-boss1-102195-aura-491008",
"rw-ystra-labyrinth-boss1-102195-spell-492307",
"rw-ystra-labyrinth-boss1-102195-spell-492308",
"rw-ystra-labyrinth-boss1-102195-phase-80",
"rw-ystra-labyrinth-boss1-102195-phase-60",
"rw-ystra-labyrinth-boss1-102195-phase-40",
"rw-ystra-labyrinth-boss1-102195-phase-20"
],
"attacks": [
{
"id": "rw-ystra-labyrinth-boss1-basic-attack",
"id": "basic-attack",
"name": "Basic Attack",
"delivery": "melee",
"target": "primary",
@@ -28,13 +38,14 @@ export const YSTRA_LABYRINTH_BOSS1_ENTITIES = {
"damageMultiplier": 1
},
{
"id": "rw-ystra-labyrinth-boss1-spell-490634",
"id": "rw-ystra-labyrinth-boss1-102195-spell-490634",
"name": "Meteor Shower",
"spellId": 490634,
"delivery": "projectile",
"target": "primary",
"school": "arcane",
"animation": "cast",
"castTimeMs": 2000,
"range": 25,
"cooldownMs": 20000,
"initialCooldownMs": [
@@ -46,6 +57,215 @@ export const YSTRA_LABYRINTH_BOSS1_ENTITIES = {
20000
],
"damageMultiplier": 0.85
},
{
"id": "rw-ystra-labyrinth-boss1-102195-aura-491008",
"name": "Transport Magic",
"delivery": "area",
"target": "self",
"school": "arcane",
"animation": "cast",
"castTimeMs": 1200,
"range": 0,
"cooldownMs": 15000,
"initialCooldownMs": [
15000,
15000
],
"repeatCooldownMs": [
15000,
15000
],
"damageMultiplier": 0,
"effect": {
"kind": "aura",
"aura": "transport-magic",
"durationMs": 7500
}
},
{
"id": "rw-ystra-labyrinth-boss1-102195-spell-492307",
"name": "Guard Bomb",
"spellId": 492307,
"delivery": "area",
"target": "nearby-party",
"school": "fire",
"animation": "cast",
"castTimeMs": 2000,
"range": 5,
"cooldownMs": 6500,
"initialCooldownMs": [
6500,
6500
],
"repeatCooldownMs": [
6500,
6500
],
"damageMultiplier": 0.85,
"radius": 5,
"telegraph": {
"shape": "circle",
"anchor": "source",
"warningMs": 1800,
"radius": 5
}
},
{
"id": "rw-ystra-labyrinth-boss1-102195-spell-492308",
"name": "Guard Bomb",
"spellId": 492308,
"delivery": "area",
"target": "nearby-party",
"school": "fire",
"animation": "cast",
"castTimeMs": 2000,
"range": 5,
"cooldownMs": 6500,
"initialCooldownMs": [
6500,
6500
],
"repeatCooldownMs": [
6500,
6500
],
"damageMultiplier": 0.85,
"radius": 5,
"telegraph": {
"shape": "circle",
"anchor": "source",
"warningMs": 1800,
"radius": 5
}
},
{
"id": "rw-ystra-labyrinth-boss1-102195-phase-80",
"name": "Phase Change",
"delivery": "area",
"target": "self",
"school": "arcane",
"animation": "cast",
"castTimeMs": 1200,
"interruptible": false,
"range": 0,
"cooldownMs": 1000,
"initialCooldownMs": [
0,
0
],
"repeatCooldownMs": [
1000,
1000
],
"maximumCasts": 1,
"damageMultiplier": 0,
"effect": {
"kind": "aura",
"aura": "phase-80",
"durationMs": 5000
},
"condition": {
"kind": "self-health-percent",
"minimum": 0,
"maximum": 80
}
},
{
"id": "rw-ystra-labyrinth-boss1-102195-phase-60",
"name": "Phase Change",
"delivery": "area",
"target": "self",
"school": "arcane",
"animation": "cast",
"castTimeMs": 1200,
"interruptible": false,
"range": 0,
"cooldownMs": 1000,
"initialCooldownMs": [
0,
0
],
"repeatCooldownMs": [
1000,
1000
],
"maximumCasts": 1,
"damageMultiplier": 0,
"effect": {
"kind": "aura",
"aura": "phase-60",
"durationMs": 5000
},
"condition": {
"kind": "self-health-percent",
"minimum": 0,
"maximum": 60
}
},
{
"id": "rw-ystra-labyrinth-boss1-102195-phase-40",
"name": "Phase Change",
"delivery": "area",
"target": "self",
"school": "arcane",
"animation": "cast",
"castTimeMs": 1200,
"interruptible": false,
"range": 0,
"cooldownMs": 1000,
"initialCooldownMs": [
0,
0
],
"repeatCooldownMs": [
1000,
1000
],
"maximumCasts": 1,
"damageMultiplier": 0,
"effect": {
"kind": "aura",
"aura": "phase-40",
"durationMs": 5000
},
"condition": {
"kind": "self-health-percent",
"minimum": 0,
"maximum": 40
}
},
{
"id": "rw-ystra-labyrinth-boss1-102195-phase-20",
"name": "Phase Change",
"delivery": "area",
"target": "self",
"school": "arcane",
"animation": "cast",
"castTimeMs": 1200,
"interruptible": false,
"range": 0,
"cooldownMs": 1000,
"initialCooldownMs": [
0,
0
],
"repeatCooldownMs": [
1000,
1000
],
"maximumCasts": 1,
"damageMultiplier": 0,
"effect": {
"kind": "aura",
"aura": "phase-20",
"durationMs": 5000
},
"condition": {
"kind": "self-health-percent",
"minimum": 0,
"maximum": 20
}
}
]
},
@@ -76,9 +296,18 @@ export const YSTRA_LABYRINTH_BOSS1_ENTITIES = {
"damageMultiplier": 1.3,
"moveSpeed": 2.25,
"leashRange": 32,
"mechanicIds": [
"rw-ystra-labyrinth-boss1-101432-spell-490539",
"rw-ystra-labyrinth-boss1-101432-aura-491008",
"rw-ystra-labyrinth-boss1-101432-spell-492310",
"rw-ystra-labyrinth-boss1-101432-spell-492312",
"rw-ystra-labyrinth-boss1-101432-aura-492313",
"rw-ystra-labyrinth-boss1-101432-spell-492314",
"rw-ystra-labyrinth-boss1-101432-spell-492334"
],
"attacks": [
{
"id": "rw-ystra-labyrinth-boss1-basic-attack",
"id": "basic-attack",
"name": "Basic Attack",
"delivery": "melee",
"target": "primary",
@@ -89,13 +318,67 @@ export const YSTRA_LABYRINTH_BOSS1_ENTITIES = {
"damageMultiplier": 1
},
{
"id": "rw-ystra-labyrinth-boss1-spell-492310",
"id": "rw-ystra-labyrinth-boss1-101432-spell-490539",
"name": "Giant Whirlwind",
"spellId": 490539,
"delivery": "area",
"target": "nearby-party",
"school": "physical",
"animation": "attack",
"castTimeMs": 1800,
"range": 7.5,
"cooldownMs": 6500,
"initialCooldownMs": [
6500,
6500
],
"repeatCooldownMs": [
6500,
6500
],
"damageMultiplier": 0.85,
"radius": 7.5,
"telegraph": {
"shape": "circle",
"anchor": "source",
"warningMs": 1800,
"radius": 7.5
}
},
{
"id": "rw-ystra-labyrinth-boss1-101432-aura-491008",
"name": "Transport Magic",
"delivery": "area",
"target": "self",
"school": "arcane",
"animation": "cast",
"castTimeMs": 1200,
"range": 0,
"cooldownMs": 15000,
"initialCooldownMs": [
15000,
15000
],
"repeatCooldownMs": [
15000,
15000
],
"damageMultiplier": 0,
"effect": {
"kind": "aura",
"aura": "transport-magic",
"durationMs": 7500
}
},
{
"id": "rw-ystra-labyrinth-boss1-101432-spell-492310",
"name": "Drop Magic",
"spellId": 492310,
"delivery": "projectile",
"target": "primary",
"school": "arcane",
"animation": "cast",
"castTimeMs": 0,
"range": 30,
"cooldownMs": 5000,
"initialCooldownMs": [
@@ -107,6 +390,115 @@ export const YSTRA_LABYRINTH_BOSS1_ENTITIES = {
5000
],
"damageMultiplier": 0.85
},
{
"id": "rw-ystra-labyrinth-boss1-101432-spell-492312",
"name": "Energy Overload",
"spellId": 492312,
"delivery": "area",
"target": "nearby-party",
"school": "shadow",
"animation": "cast",
"castTimeMs": 1800,
"range": 7.5,
"cooldownMs": 6500,
"initialCooldownMs": [
6500,
6500
],
"repeatCooldownMs": [
6500,
6500
],
"damageMultiplier": 0.85,
"radius": 7.5,
"telegraph": {
"shape": "circle",
"anchor": "source",
"warningMs": 1800,
"radius": 7.5
}
},
{
"id": "rw-ystra-labyrinth-boss1-101432-aura-492313",
"name": "Magic Focus",
"delivery": "area",
"target": "self",
"school": "arcane",
"animation": "cast",
"castTimeMs": 1200,
"range": 0,
"cooldownMs": 15000,
"initialCooldownMs": [
15000,
15000
],
"repeatCooldownMs": [
15000,
15000
],
"damageMultiplier": 0,
"effect": {
"kind": "aura",
"aura": "magic-focus",
"durationMs": 7500
}
},
{
"id": "rw-ystra-labyrinth-boss1-101432-spell-492314",
"name": "Magic Explosion",
"spellId": 492314,
"delivery": "area",
"target": "nearby-party",
"school": "shadow",
"animation": "cast",
"castTimeMs": 1800,
"range": 7.5,
"cooldownMs": 6500,
"initialCooldownMs": [
6500,
6500
],
"repeatCooldownMs": [
6500,
6500
],
"damageMultiplier": 0.85,
"radius": 7.5,
"telegraph": {
"shape": "circle",
"anchor": "source",
"warningMs": 1800,
"radius": 7.5
}
},
{
"id": "rw-ystra-labyrinth-boss1-101432-spell-492334",
"name": "Manastorms Duo Lv5",
"spellId": 492334,
"delivery": "area",
"target": "nearby-party",
"school": "shadow",
"animation": "cast",
"castTimeMs": 1800,
"range": 30,
"cooldownMs": 6500,
"initialCooldownMs": [
6500,
6500
],
"repeatCooldownMs": [
6500,
6500
],
"damageMultiplier": 0.85,
"radius": 15,
"telegraph": {
"shape": "circle",
"anchor": "source",
"warningMs": 1800,
"radius": 15
}
}
]
},
@@ -137,9 +529,18 @@ export const YSTRA_LABYRINTH_BOSS1_ENTITIES = {
"damageMultiplier": 1.3,
"moveSpeed": 2.25,
"leashRange": 32,
"mechanicIds": [
"rw-ystra-labyrinth-boss1-101433-spell-490524",
"rw-ystra-labyrinth-boss1-101433-aura-491008",
"rw-ystra-labyrinth-boss1-101433-aura-492256",
"rw-ystra-labyrinth-boss1-101433-aura-492322",
"rw-ystra-labyrinth-boss1-101433-spell-492323",
"rw-ystra-labyrinth-boss1-101433-aura-492324",
"rw-ystra-labyrinth-boss1-101433-spell-492327"
],
"attacks": [
{
"id": "rw-ystra-labyrinth-boss1-basic-attack",
"id": "basic-attack",
"name": "Basic Attack",
"delivery": "melee",
"target": "primary",
@@ -148,6 +549,183 @@ export const YSTRA_LABYRINTH_BOSS1_ENTITIES = {
"range": 4.5,
"cooldownMs": 1900,
"damageMultiplier": 1
},
{
"id": "rw-ystra-labyrinth-boss1-101433-spell-490524",
"name": "Clash",
"spellId": 490524,
"delivery": "projectile",
"target": "primary",
"school": "arcane",
"animation": "cast",
"castTimeMs": 0,
"range": 6,
"cooldownMs": 10000,
"initialCooldownMs": [
10000,
10000
],
"repeatCooldownMs": [
10000,
10000
],
"damageMultiplier": 0.85
},
{
"id": "rw-ystra-labyrinth-boss1-101433-aura-491008",
"name": "Transport Magic",
"delivery": "area",
"target": "self",
"school": "arcane",
"animation": "cast",
"castTimeMs": 1200,
"range": 0,
"cooldownMs": 15000,
"initialCooldownMs": [
15000,
15000
],
"repeatCooldownMs": [
15000,
15000
],
"damageMultiplier": 0,
"effect": {
"kind": "aura",
"aura": "transport-magic",
"durationMs": 7500
}
},
{
"id": "rw-ystra-labyrinth-boss1-101433-aura-492256",
"name": "Fog of Petrification",
"delivery": "area",
"target": "self",
"school": "arcane",
"animation": "cast",
"castTimeMs": 1200,
"range": 0,
"cooldownMs": 8000,
"initialCooldownMs": [
8000,
8000
],
"repeatCooldownMs": [
8000,
8000
],
"damageMultiplier": 0,
"effect": {
"kind": "aura",
"aura": "fog-of-petrification",
"durationMs": 4000
}
},
{
"id": "rw-ystra-labyrinth-boss1-101433-aura-492322",
"name": "Cursed Poison Fog",
"delivery": "area",
"target": "self",
"school": "arcane",
"animation": "cast",
"castTimeMs": 1200,
"range": 0,
"cooldownMs": 8000,
"initialCooldownMs": [
8000,
8000
],
"repeatCooldownMs": [
8000,
8000
],
"damageMultiplier": 0,
"effect": {
"kind": "aura",
"aura": "cursed-posion",
"durationMs": 4000
}
},
{
"id": "rw-ystra-labyrinth-boss1-101433-spell-492323",
"name": "Potent Poison Swipe",
"spellId": 492323,
"delivery": "area",
"target": "nearby-party",
"school": "physical",
"animation": "attack",
"castTimeMs": 1800,
"range": 5,
"cooldownMs": 6500,
"initialCooldownMs": [
6500,
6500
],
"repeatCooldownMs": [
6500,
6500
],
"damageMultiplier": 0.85,
"radius": 5,
"telegraph": {
"shape": "circle",
"anchor": "source",
"warningMs": 1800,
"radius": 5
}
},
{
"id": "rw-ystra-labyrinth-boss1-101433-aura-492324",
"name": "Demon's Roar",
"delivery": "area",
"target": "self",
"school": "arcane",
"animation": "cast",
"castTimeMs": 1200,
"range": 0,
"cooldownMs": 15000,
"initialCooldownMs": [
15000,
15000
],
"repeatCooldownMs": [
15000,
15000
],
"damageMultiplier": 0,
"effect": {
"kind": "aura",
"aura": "demon-s-roar",
"durationMs": 7500
}
},
{
"id": "rw-ystra-labyrinth-boss1-101433-spell-492327",
"name": "Charge Assault",
"spellId": 492327,
"delivery": "area",
"target": "nearby-party",
"school": "physical",
"animation": "cast",
"castTimeMs": 1800,
"range": 30,
"cooldownMs": 6500,
"initialCooldownMs": [
6500,
6500
],
"repeatCooldownMs": [
6500,
6500
],
"damageMultiplier": 0.85,
"telegraph": {
"shape": "line",
"anchor": "source",
"warningMs": 1800,
"length": 30,
"width": 2
}
}
]
},
@@ -274,6 +852,6 @@ export const YSTRA_LABYRINTH_BOSS1_POPULATION_PROVENANCE = {
"runeWakerIdsAreNamespaced": true,
"serverEntryPolicy": "RuneWaker numeric ids are never passed to AzerothCore serverEntry.",
"sourceSpellCatalog": "scripts/runewaker-pipeline/snapshots/runewaker-npc-spell-catalog.json",
"sourceSpellCatalogSha256": "ad29efc1d4906fdd4f2ac7a69316cac32d8fcbaea674678f050265c86e0c506c",
"sourceSpellCatalogSha256": "c47e67013e68ecf9c7ae30ef4b2058a850505385739dfe8c32ce8874e6ed077c",
"sourceSpellPolicy": "Only positively classified direct-HP-damage families execute; coefficients remain conservative HealerMan values."
} as const;
@@ -15,9 +15,18 @@ export const YSTRA_LABYRINTH_BOSS2_ENTITIES = {
"damageMultiplier": 1.3,
"moveSpeed": 2.25,
"leashRange": 32,
"mechanicIds": [
"rw-ystra-labyrinth-boss2-102196-spell-490539",
"rw-ystra-labyrinth-boss2-102196-aura-491008",
"rw-ystra-labyrinth-boss2-102196-spell-492310",
"rw-ystra-labyrinth-boss2-102196-spell-492312",
"rw-ystra-labyrinth-boss2-102196-aura-492313",
"rw-ystra-labyrinth-boss2-102196-spell-492314",
"rw-ystra-labyrinth-boss2-102196-spell-492334"
],
"attacks": [
{
"id": "rw-ystra-labyrinth-boss2-basic-attack",
"id": "basic-attack",
"name": "Basic Attack",
"delivery": "melee",
"target": "primary",
@@ -28,13 +37,67 @@ export const YSTRA_LABYRINTH_BOSS2_ENTITIES = {
"damageMultiplier": 1
},
{
"id": "rw-ystra-labyrinth-boss2-spell-492310",
"id": "rw-ystra-labyrinth-boss2-102196-spell-490539",
"name": "Giant Whirlwind",
"spellId": 490539,
"delivery": "area",
"target": "nearby-party",
"school": "physical",
"animation": "attack",
"castTimeMs": 1800,
"range": 7.5,
"cooldownMs": 6500,
"initialCooldownMs": [
6500,
6500
],
"repeatCooldownMs": [
6500,
6500
],
"damageMultiplier": 0.85,
"radius": 7.5,
"telegraph": {
"shape": "circle",
"anchor": "source",
"warningMs": 1800,
"radius": 7.5
}
},
{
"id": "rw-ystra-labyrinth-boss2-102196-aura-491008",
"name": "Transport Magic",
"delivery": "area",
"target": "self",
"school": "arcane",
"animation": "cast",
"castTimeMs": 1200,
"range": 0,
"cooldownMs": 15000,
"initialCooldownMs": [
15000,
15000
],
"repeatCooldownMs": [
15000,
15000
],
"damageMultiplier": 0,
"effect": {
"kind": "aura",
"aura": "transport-magic",
"durationMs": 7500
}
},
{
"id": "rw-ystra-labyrinth-boss2-102196-spell-492310",
"name": "Drop Magic",
"spellId": 492310,
"delivery": "projectile",
"target": "primary",
"school": "arcane",
"animation": "cast",
"castTimeMs": 0,
"range": 30,
"cooldownMs": 5000,
"initialCooldownMs": [
@@ -46,6 +109,115 @@ export const YSTRA_LABYRINTH_BOSS2_ENTITIES = {
5000
],
"damageMultiplier": 0.85
},
{
"id": "rw-ystra-labyrinth-boss2-102196-spell-492312",
"name": "Energy Overload",
"spellId": 492312,
"delivery": "area",
"target": "nearby-party",
"school": "shadow",
"animation": "cast",
"castTimeMs": 1800,
"range": 7.5,
"cooldownMs": 6500,
"initialCooldownMs": [
6500,
6500
],
"repeatCooldownMs": [
6500,
6500
],
"damageMultiplier": 0.85,
"radius": 7.5,
"telegraph": {
"shape": "circle",
"anchor": "source",
"warningMs": 1800,
"radius": 7.5
}
},
{
"id": "rw-ystra-labyrinth-boss2-102196-aura-492313",
"name": "Magic Focus",
"delivery": "area",
"target": "self",
"school": "arcane",
"animation": "cast",
"castTimeMs": 1200,
"range": 0,
"cooldownMs": 15000,
"initialCooldownMs": [
15000,
15000
],
"repeatCooldownMs": [
15000,
15000
],
"damageMultiplier": 0,
"effect": {
"kind": "aura",
"aura": "magic-focus",
"durationMs": 7500
}
},
{
"id": "rw-ystra-labyrinth-boss2-102196-spell-492314",
"name": "Magic Explosion",
"spellId": 492314,
"delivery": "area",
"target": "nearby-party",
"school": "shadow",
"animation": "cast",
"castTimeMs": 1800,
"range": 7.5,
"cooldownMs": 6500,
"initialCooldownMs": [
6500,
6500
],
"repeatCooldownMs": [
6500,
6500
],
"damageMultiplier": 0.85,
"radius": 7.5,
"telegraph": {
"shape": "circle",
"anchor": "source",
"warningMs": 1800,
"radius": 7.5
}
},
{
"id": "rw-ystra-labyrinth-boss2-102196-spell-492334",
"name": "Manastorms Duo Lv5",
"spellId": 492334,
"delivery": "area",
"target": "nearby-party",
"school": "shadow",
"animation": "cast",
"castTimeMs": 1800,
"range": 30,
"cooldownMs": 6500,
"initialCooldownMs": [
6500,
6500
],
"repeatCooldownMs": [
6500,
6500
],
"damageMultiplier": 0.85,
"radius": 15,
"telegraph": {
"shape": "circle",
"anchor": "source",
"warningMs": 1800,
"radius": 15
}
}
]
},
@@ -132,6 +304,6 @@ export const YSTRA_LABYRINTH_BOSS2_POPULATION_PROVENANCE = {
"runeWakerIdsAreNamespaced": true,
"serverEntryPolicy": "RuneWaker numeric ids are never passed to AzerothCore serverEntry.",
"sourceSpellCatalog": "scripts/runewaker-pipeline/snapshots/runewaker-npc-spell-catalog.json",
"sourceSpellCatalogSha256": "ad29efc1d4906fdd4f2ac7a69316cac32d8fcbaea674678f050265c86e0c506c",
"sourceSpellCatalogSha256": "c47e67013e68ecf9c7ae30ef4b2058a850505385739dfe8c32ce8874e6ed077c",
"sourceSpellPolicy": "Only positively classified direct-HP-damage families execute; coefficients remain conservative HealerMan values."
} as const;
@@ -15,9 +15,18 @@ export const YSTRA_LABYRINTH_BOSS3_ENTITIES = {
"damageMultiplier": 1.3,
"moveSpeed": 2.25,
"leashRange": 32,
"mechanicIds": [
"rw-ystra-labyrinth-boss3-102197-spell-490524",
"rw-ystra-labyrinth-boss3-102197-aura-491008",
"rw-ystra-labyrinth-boss3-102197-aura-492256",
"rw-ystra-labyrinth-boss3-102197-aura-492322",
"rw-ystra-labyrinth-boss3-102197-spell-492323",
"rw-ystra-labyrinth-boss3-102197-aura-492324",
"rw-ystra-labyrinth-boss3-102197-spell-492327"
],
"attacks": [
{
"id": "rw-ystra-labyrinth-boss3-basic-attack",
"id": "basic-attack",
"name": "Basic Attack",
"delivery": "melee",
"target": "primary",
@@ -26,6 +35,183 @@ export const YSTRA_LABYRINTH_BOSS3_ENTITIES = {
"range": 4.5,
"cooldownMs": 1900,
"damageMultiplier": 1
},
{
"id": "rw-ystra-labyrinth-boss3-102197-spell-490524",
"name": "Clash",
"spellId": 490524,
"delivery": "projectile",
"target": "primary",
"school": "arcane",
"animation": "cast",
"castTimeMs": 0,
"range": 6,
"cooldownMs": 10000,
"initialCooldownMs": [
10000,
10000
],
"repeatCooldownMs": [
10000,
10000
],
"damageMultiplier": 0.85
},
{
"id": "rw-ystra-labyrinth-boss3-102197-aura-491008",
"name": "Transport Magic",
"delivery": "area",
"target": "self",
"school": "arcane",
"animation": "cast",
"castTimeMs": 1200,
"range": 0,
"cooldownMs": 15000,
"initialCooldownMs": [
15000,
15000
],
"repeatCooldownMs": [
15000,
15000
],
"damageMultiplier": 0,
"effect": {
"kind": "aura",
"aura": "transport-magic",
"durationMs": 7500
}
},
{
"id": "rw-ystra-labyrinth-boss3-102197-aura-492256",
"name": "Fog of Petrification",
"delivery": "area",
"target": "self",
"school": "arcane",
"animation": "cast",
"castTimeMs": 1200,
"range": 0,
"cooldownMs": 8000,
"initialCooldownMs": [
8000,
8000
],
"repeatCooldownMs": [
8000,
8000
],
"damageMultiplier": 0,
"effect": {
"kind": "aura",
"aura": "fog-of-petrification",
"durationMs": 4000
}
},
{
"id": "rw-ystra-labyrinth-boss3-102197-aura-492322",
"name": "Cursed Poison Fog",
"delivery": "area",
"target": "self",
"school": "arcane",
"animation": "cast",
"castTimeMs": 1200,
"range": 0,
"cooldownMs": 8000,
"initialCooldownMs": [
8000,
8000
],
"repeatCooldownMs": [
8000,
8000
],
"damageMultiplier": 0,
"effect": {
"kind": "aura",
"aura": "cursed-posion",
"durationMs": 4000
}
},
{
"id": "rw-ystra-labyrinth-boss3-102197-spell-492323",
"name": "Potent Poison Swipe",
"spellId": 492323,
"delivery": "area",
"target": "nearby-party",
"school": "physical",
"animation": "attack",
"castTimeMs": 1800,
"range": 5,
"cooldownMs": 6500,
"initialCooldownMs": [
6500,
6500
],
"repeatCooldownMs": [
6500,
6500
],
"damageMultiplier": 0.85,
"radius": 5,
"telegraph": {
"shape": "circle",
"anchor": "source",
"warningMs": 1800,
"radius": 5
}
},
{
"id": "rw-ystra-labyrinth-boss3-102197-aura-492324",
"name": "Demon's Roar",
"delivery": "area",
"target": "self",
"school": "arcane",
"animation": "cast",
"castTimeMs": 1200,
"range": 0,
"cooldownMs": 15000,
"initialCooldownMs": [
15000,
15000
],
"repeatCooldownMs": [
15000,
15000
],
"damageMultiplier": 0,
"effect": {
"kind": "aura",
"aura": "demon-s-roar",
"durationMs": 7500
}
},
{
"id": "rw-ystra-labyrinth-boss3-102197-spell-492327",
"name": "Charge Assault",
"spellId": 492327,
"delivery": "area",
"target": "nearby-party",
"school": "physical",
"animation": "cast",
"castTimeMs": 1800,
"range": 30,
"cooldownMs": 6500,
"initialCooldownMs": [
6500,
6500
],
"repeatCooldownMs": [
6500,
6500
],
"damageMultiplier": 0.85,
"telegraph": {
"shape": "line",
"anchor": "source",
"warningMs": 1800,
"length": 30,
"width": 2
}
}
]
},
@@ -112,6 +298,6 @@ export const YSTRA_LABYRINTH_BOSS3_POPULATION_PROVENANCE = {
"runeWakerIdsAreNamespaced": true,
"serverEntryPolicy": "RuneWaker numeric ids are never passed to AzerothCore serverEntry.",
"sourceSpellCatalog": "scripts/runewaker-pipeline/snapshots/runewaker-npc-spell-catalog.json",
"sourceSpellCatalogSha256": "ad29efc1d4906fdd4f2ac7a69316cac32d8fcbaea674678f050265c86e0c506c",
"sourceSpellCatalogSha256": "c47e67013e68ecf9c7ae30ef4b2058a850505385739dfe8c32ce8874e6ed077c",
"sourceSpellPolicy": "Only positively classified direct-HP-damage families execute; coefficients remain conservative HealerMan values."
} as const;
@@ -48,6 +48,6 @@ export const ZURHIDON_STRONGHOLD_124_POPULATION_PROVENANCE = {
"runeWakerIdsAreNamespaced": true,
"serverEntryPolicy": "RuneWaker numeric ids are never passed to AzerothCore serverEntry.",
"sourceSpellCatalog": "scripts/runewaker-pipeline/snapshots/runewaker-npc-spell-catalog.json",
"sourceSpellCatalogSha256": "ad29efc1d4906fdd4f2ac7a69316cac32d8fcbaea674678f050265c86e0c506c",
"sourceSpellCatalogSha256": "c47e67013e68ecf9c7ae30ef4b2058a850505385739dfe8c32ce8874e6ed077c",
"sourceSpellPolicy": "Only positively classified direct-HP-damage families execute; coefficients remain conservative HealerMan values."
} as const;
@@ -16,7 +16,7 @@ export const ZURHIDON_STRONGHOLD_ENTITIES = {
"leashRange": 24,
"attacks": [
{
"id": "rw-zurhidon-stronghold-basic-attack",
"id": "basic-attack",
"name": "Basic Attack",
"delivery": "melee",
"target": "primary",
@@ -76,7 +76,7 @@ export const ZURHIDON_STRONGHOLD_ENTITIES = {
"leashRange": 24,
"attacks": [
{
"id": "rw-zurhidon-stronghold-basic-attack",
"id": "basic-attack",
"name": "Basic Attack",
"delivery": "melee",
"target": "primary",
@@ -136,7 +136,7 @@ export const ZURHIDON_STRONGHOLD_ENTITIES = {
"leashRange": 24,
"attacks": [
{
"id": "rw-zurhidon-stronghold-basic-attack",
"id": "basic-attack",
"name": "Basic Attack",
"delivery": "melee",
"target": "primary",
@@ -176,7 +176,7 @@ export const ZURHIDON_STRONGHOLD_ENTITIES = {
"leashRange": 24,
"attacks": [
{
"id": "rw-zurhidon-stronghold-basic-attack",
"id": "basic-attack",
"name": "Basic Attack",
"delivery": "melee",
"target": "primary",
@@ -256,7 +256,7 @@ export const ZURHIDON_STRONGHOLD_ENTITIES = {
"leashRange": 24,
"attacks": [
{
"id": "rw-zurhidon-stronghold-basic-attack",
"id": "basic-attack",
"name": "Basic Attack",
"delivery": "melee",
"target": "primary",
@@ -336,7 +336,7 @@ export const ZURHIDON_STRONGHOLD_ENTITIES = {
"leashRange": 24,
"attacks": [
{
"id": "rw-zurhidon-stronghold-basic-attack",
"id": "basic-attack",
"name": "Basic Attack",
"delivery": "melee",
"target": "primary",
@@ -416,7 +416,7 @@ export const ZURHIDON_STRONGHOLD_ENTITIES = {
"leashRange": 24,
"attacks": [
{
"id": "rw-zurhidon-stronghold-basic-attack",
"id": "basic-attack",
"name": "Basic Attack",
"delivery": "melee",
"target": "primary",
@@ -456,7 +456,7 @@ export const ZURHIDON_STRONGHOLD_ENTITIES = {
"leashRange": 24,
"attacks": [
{
"id": "rw-zurhidon-stronghold-basic-attack",
"id": "basic-attack",
"name": "Basic Attack",
"delivery": "melee",
"target": "primary",
@@ -536,7 +536,7 @@ export const ZURHIDON_STRONGHOLD_ENTITIES = {
"leashRange": 24,
"attacks": [
{
"id": "rw-zurhidon-stronghold-basic-attack",
"id": "basic-attack",
"name": "Basic Attack",
"delivery": "melee",
"target": "primary",
@@ -576,7 +576,7 @@ export const ZURHIDON_STRONGHOLD_ENTITIES = {
"leashRange": 24,
"attacks": [
{
"id": "rw-zurhidon-stronghold-basic-attack",
"id": "basic-attack",
"name": "Basic Attack",
"delivery": "melee",
"target": "primary",
@@ -616,7 +616,7 @@ export const ZURHIDON_STRONGHOLD_ENTITIES = {
"leashRange": 24,
"attacks": [
{
"id": "rw-zurhidon-stronghold-basic-attack",
"id": "basic-attack",
"name": "Basic Attack",
"delivery": "melee",
"target": "primary",
@@ -656,7 +656,7 @@ export const ZURHIDON_STRONGHOLD_ENTITIES = {
"leashRange": 24,
"attacks": [
{
"id": "rw-zurhidon-stronghold-basic-attack",
"id": "basic-attack",
"name": "Basic Attack",
"delivery": "melee",
"target": "primary",
@@ -696,7 +696,7 @@ export const ZURHIDON_STRONGHOLD_ENTITIES = {
"leashRange": 24,
"attacks": [
{
"id": "rw-zurhidon-stronghold-basic-attack",
"id": "basic-attack",
"name": "Basic Attack",
"delivery": "melee",
"target": "primary",
@@ -736,7 +736,7 @@ export const ZURHIDON_STRONGHOLD_ENTITIES = {
"leashRange": 24,
"attacks": [
{
"id": "rw-zurhidon-stronghold-basic-attack",
"id": "basic-attack",
"name": "Basic Attack",
"delivery": "melee",
"target": "primary",
@@ -836,7 +836,7 @@ export const ZURHIDON_STRONGHOLD_ENTITIES = {
"leashRange": 24,
"attacks": [
{
"id": "rw-zurhidon-stronghold-basic-attack",
"id": "basic-attack",
"name": "Basic Attack",
"delivery": "melee",
"target": "primary",
@@ -876,7 +876,7 @@ export const ZURHIDON_STRONGHOLD_ENTITIES = {
"leashRange": 24,
"attacks": [
{
"id": "rw-zurhidon-stronghold-basic-attack",
"id": "basic-attack",
"name": "Basic Attack",
"delivery": "melee",
"target": "primary",
@@ -936,7 +936,7 @@ export const ZURHIDON_STRONGHOLD_ENTITIES = {
"leashRange": 24,
"attacks": [
{
"id": "rw-zurhidon-stronghold-basic-attack",
"id": "basic-attack",
"name": "Basic Attack",
"delivery": "melee",
"target": "primary",
@@ -1016,7 +1016,7 @@ export const ZURHIDON_STRONGHOLD_ENTITIES = {
"leashRange": 24,
"attacks": [
{
"id": "rw-zurhidon-stronghold-basic-attack",
"id": "basic-attack",
"name": "Basic Attack",
"delivery": "melee",
"target": "primary",
@@ -1096,7 +1096,7 @@ export const ZURHIDON_STRONGHOLD_ENTITIES = {
"leashRange": 24,
"attacks": [
{
"id": "rw-zurhidon-stronghold-basic-attack",
"id": "basic-attack",
"name": "Basic Attack",
"delivery": "melee",
"target": "primary",
@@ -1136,7 +1136,7 @@ export const ZURHIDON_STRONGHOLD_ENTITIES = {
"leashRange": 24,
"attacks": [
{
"id": "rw-zurhidon-stronghold-basic-attack",
"id": "basic-attack",
"name": "Basic Attack",
"delivery": "melee",
"target": "primary",
@@ -1196,7 +1196,7 @@ export const ZURHIDON_STRONGHOLD_ENTITIES = {
"leashRange": 24,
"attacks": [
{
"id": "rw-zurhidon-stronghold-basic-attack",
"id": "basic-attack",
"name": "Basic Attack",
"delivery": "melee",
"target": "primary",
@@ -1276,7 +1276,7 @@ export const ZURHIDON_STRONGHOLD_ENTITIES = {
"leashRange": 24,
"attacks": [
{
"id": "rw-zurhidon-stronghold-basic-attack",
"id": "basic-attack",
"name": "Basic Attack",
"delivery": "melee",
"target": "primary",
@@ -1316,7 +1316,7 @@ export const ZURHIDON_STRONGHOLD_ENTITIES = {
"leashRange": 24,
"attacks": [
{
"id": "rw-zurhidon-stronghold-basic-attack",
"id": "basic-attack",
"name": "Basic Attack",
"delivery": "melee",
"target": "primary",
@@ -1376,7 +1376,7 @@ export const ZURHIDON_STRONGHOLD_ENTITIES = {
"leashRange": 24,
"attacks": [
{
"id": "rw-zurhidon-stronghold-basic-attack",
"id": "basic-attack",
"name": "Basic Attack",
"delivery": "melee",
"target": "primary",
@@ -1436,7 +1436,7 @@ export const ZURHIDON_STRONGHOLD_ENTITIES = {
"leashRange": 24,
"attacks": [
{
"id": "rw-zurhidon-stronghold-basic-attack",
"id": "basic-attack",
"name": "Basic Attack",
"delivery": "melee",
"target": "primary",
@@ -3193,6 +3193,6 @@ export const ZURHIDON_STRONGHOLD_POPULATION_PROVENANCE = {
"runeWakerIdsAreNamespaced": true,
"serverEntryPolicy": "RuneWaker numeric ids are never passed to AzerothCore serverEntry.",
"sourceSpellCatalog": "scripts/runewaker-pipeline/snapshots/runewaker-npc-spell-catalog.json",
"sourceSpellCatalogSha256": "ad29efc1d4906fdd4f2ac7a69316cac32d8fcbaea674678f050265c86e0c506c",
"sourceSpellCatalogSha256": "c47e67013e68ecf9c7ae30ef4b2058a850505385739dfe8c32ce8874e6ed077c",
"sourceSpellPolicy": "Only positively classified direct-HP-damage families execute; coefficients remain conservative HealerMan values."
} as const;
+20 -1
View File
@@ -1,6 +1,10 @@
import { readFileSync } from "node:fs";
import path from "node:path";
import { fileURLToPath } from "node:url";
import { afterEach, describe, expect, it, vi } from "vitest";
import {
MANASTORM_DEFAULT_PORTAL_READY_CUE_ID,
MANASTORM_PORTAL_AUDIO_ROOT,
MANASTORM_PORTAL_AUDIO_CUES,
manastormCueForStatusTransition,
manastormPortalAudioCue,
@@ -10,6 +14,8 @@ import {
} from "./manastormClientCues";
import { useManastormStore } from "./manastormStore";
const projectDirectory = fileURLToPath(new URL("../..", import.meta.url));
describe("Manastorm imported client cues", () => {
afterEach(() => {
stopManastormClientCue();
@@ -25,10 +31,23 @@ describe("Manastorm imported client cues", () => {
.toHaveLength(3);
for (const cue of MANASTORM_PORTAL_AUDIO_CUES) {
expect(cue.fileName).toMatch(/^Manastorm_BossKill_.+\.wav$/);
expect(cue.url).toContain(cue.fileName);
expect(cue.url).toBe(`${MANASTORM_PORTAL_AUDIO_ROOT}/${cue.fileName}`);
const audio = readFileSync(path.join(projectDirectory, "public", cue.url.slice(1)));
expect(audio.subarray(0, 4).toString("ascii")).toBe("RIFF");
expect(audio.subarray(8, 12).toString("ascii")).toBe("WAVE");
}
});
it("keeps optional portal audio out of Vite source imports", () => {
const source = readFileSync(
new URL("./manastormClientCues.ts", import.meta.url),
"utf8",
);
expect(source).not.toContain("LadiksMPQEditor");
expect(source).not.toMatch(/new URL\s*\(/);
});
it("maps only an actual portal-ready state edge to the automatic cue", () => {
expect(manastormCueForStatusTransition("awaiting-loot", "portal-ready"))
.toBe(MANASTORM_DEFAULT_PORTAL_READY_CUE_ID);
+13 -26
View File
@@ -1,3 +1,5 @@
import { playMasterAudioCue } from "./combatAudio";
export type ManastormPortalCueSignal = "portal-coming-up" | "portal-ready";
export interface ManastormPortalAudioCue {
@@ -24,26 +26,12 @@ export interface ManastormAudioHandle {
export type ManastormAudioFactory = (url: string) => ManastormAudioHandle;
const genericPortalComingUpUrl = new URL(
"../../../LadiksMPQEditor/patch-O/sound/Manastorm/Manastorm_BossKill_GenericPortalComingUp.wav",
import.meta.url,
).href;
const singlePortalComingUpUrl = new URL(
"../../../LadiksMPQEditor/patch-O/sound/Manastorm/Manastorm_BossKill_1PortalComingUp.wav",
import.meta.url,
).href;
const alternateSinglePortalComingUpUrl = new URL(
"../../../LadiksMPQEditor/patch-O/sound/Manastorm/Manastorm_BossKill_1PortalComingUp_2.wav",
import.meta.url,
).href;
const genericPortalsReadyUrl = new URL(
"../../../LadiksMPQEditor/patch-O/sound/Manastorm/Manastorm_BossKill_GenericPortalsReady.wav",
import.meta.url,
).href;
const alternateGenericPortalsReadyUrl = new URL(
"../../../LadiksMPQEditor/patch-O/sound/Manastorm/Manastorm_BossKill_GenericPortalsReady_2.wav",
import.meta.url,
).href;
export const MANASTORM_PORTAL_AUDIO_ROOT =
"/assets/game/combat-effects/audio/manastorm";
function packagedPortalAudioUrl(fileName: string): string {
return `${MANASTORM_PORTAL_AUDIO_ROOT}/${fileName}`;
}
export const MANASTORM_PORTAL_AUDIO_CUES = [
{
@@ -52,7 +40,7 @@ export const MANASTORM_PORTAL_AUDIO_CUES = [
variant: "generic",
alternate: false,
fileName: "Manastorm_BossKill_GenericPortalComingUp.wav",
url: genericPortalComingUpUrl,
url: packagedPortalAudioUrl("Manastorm_BossKill_GenericPortalComingUp.wav"),
},
{
id: "single-portal-coming-up",
@@ -60,7 +48,7 @@ export const MANASTORM_PORTAL_AUDIO_CUES = [
variant: "single",
alternate: false,
fileName: "Manastorm_BossKill_1PortalComingUp.wav",
url: singlePortalComingUpUrl,
url: packagedPortalAudioUrl("Manastorm_BossKill_1PortalComingUp.wav"),
},
{
id: "single-portal-coming-up-alt",
@@ -68,7 +56,7 @@ export const MANASTORM_PORTAL_AUDIO_CUES = [
variant: "single",
alternate: true,
fileName: "Manastorm_BossKill_1PortalComingUp_2.wav",
url: alternateSinglePortalComingUpUrl,
url: packagedPortalAudioUrl("Manastorm_BossKill_1PortalComingUp_2.wav"),
},
{
id: "generic-portals-ready",
@@ -76,7 +64,7 @@ export const MANASTORM_PORTAL_AUDIO_CUES = [
variant: "generic",
alternate: false,
fileName: "Manastorm_BossKill_GenericPortalsReady.wav",
url: genericPortalsReadyUrl,
url: packagedPortalAudioUrl("Manastorm_BossKill_GenericPortalsReady.wav"),
},
{
id: "generic-portals-ready-alt",
@@ -84,7 +72,7 @@ export const MANASTORM_PORTAL_AUDIO_CUES = [
variant: "generic",
alternate: true,
fileName: "Manastorm_BossKill_GenericPortalsReady_2.wav",
url: alternateGenericPortalsReadyUrl,
url: packagedPortalAudioUrl("Manastorm_BossKill_GenericPortalsReady_2.wav"),
},
] as const satisfies readonly ManastormPortalAudioCue[];
@@ -167,4 +155,3 @@ export function stopManastormClientCue(): void {
activeAudio?.pause();
activeAudio = null;
}
import { playMasterAudioCue } from "./combatAudio";
+3
View File
@@ -41,6 +41,9 @@ describe("catalog-driven party rotations", () => {
school: "shadow",
interruptible: true,
targetActorId: "__player__",
sourcePosition: [0, 0, 0],
targetPosition: [1, 0, 0],
sourceYaw: Math.PI / 2,
startedAt: 1_000,
completesAt: 5_000,
},
+75
View File
@@ -0,0 +1,75 @@
import { readFileSync } from "node:fs";
import { describe, expect, it } from "vitest";
import { DUNGEON_DEFINITIONS } from "./dungeonRegistry";
import { normalizeEnemyAttacks } from "./enemyAttacks";
const catalog = JSON.parse(readFileSync(
"dungeon-pipeline/boss-mechanics.json",
"utf8",
)) as {
readonly bosses: Readonly<Record<string, {
readonly source?: { readonly kind?: string };
readonly mechanics: readonly { readonly id: string }[];
}>>;
};
describe("modular RuneWaker boss mechanics", () => {
const dungeons = DUNGEON_DEFINITIONS.filter(
(definition) => definition.expansion === "runewaker",
);
const bosses = dungeons.flatMap((dungeon) => (
Object.values(dungeon.entities)
.filter((entity) => entity.kind === "boss")
.map((entity) => ({ dungeon, entity }))
));
it("gives every packaged boss a catalog-backed special-mechanic loadout", () => {
expect(bosses).toHaveLength(56);
for (const { dungeon, entity } of bosses) {
const label = `${dungeon.id}:${entity.id}`;
const authored = catalog.bosses[entity.id];
expect(authored, label).toBeDefined();
expect(authored.mechanics.length, label).toBeGreaterThan(0);
expect(entity.combat?.mechanicIds, label).toEqual(
authored.mechanics.map((mechanic) => mechanic.id),
);
expect(entity.combat?.attacks[0]?.id, label).toBe("basic-attack");
expect(entity.combat?.attacks.length, label).toBeGreaterThan(1);
}
});
it("normalizes every harmful spatial mechanic with matching warning geometry", () => {
let telegraphed = 0;
let healthTriggered = 0;
for (const { dungeon, entity } of bosses) {
const label = `${dungeon.id}:${entity.id}`;
const attacks = normalizeEnemyAttacks(entity.combat?.attacks);
for (const attack of attacks.slice(1)) {
if (attack.condition?.kind === "self-health-percent") healthTriggered += 1;
if (attack.telegraph) {
telegraphed += 1;
expect(attack.castTimeMs, label).toBeGreaterThanOrEqual(attack.telegraph.warningMs);
if (attack.telegraph.shape === "circle") {
expect(attack.telegraph.radius, label).toBeGreaterThan(0);
} else {
expect(attack.telegraph.length, label).toBeGreaterThan(0);
}
}
}
}
expect(telegraphed).toBeGreaterThanOrEqual(100);
expect(healthTriggered).toBeGreaterThanOrEqual(25);
});
it("keeps source-gap fallbacks explicit instead of presenting them as recovered mechanics", () => {
const sourceGaps = bosses.filter(({ entity }) => (
catalog.bosses[entity.id]?.source?.kind === "healer-man-source-gap"
));
expect(sourceGaps.map(({ entity }) => entity.id).sort()).toEqual([
"rw-fa-100070",
"rw-fa-100561",
"rw-fa-100562",
"rw-fa-100563",
]);
});
});
+3 -3
View File
@@ -86,14 +86,14 @@ describe("portable RuneWaker source-spell mappings", () => {
expect(source?.runtime).toBeDefined();
expect(attack).toMatchObject({
name: source?.name,
delivery: source?.runtime?.delivery,
target: source?.runtime?.target,
school: source?.runtime?.school,
animation: source?.runtime?.animation,
range: source?.runtime?.range,
cooldownMs: source?.runtime?.cooldownMs,
damageMultiplier: source?.runtime?.damageMultiplier,
});
expect(["melee", "projectile", "area"]).toContain(attack.delivery);
expect(["primary", "nearby-party", "random-party"]).toContain(attack.target);
expect(attack.range).toBeGreaterThanOrEqual(source?.runtime?.range ?? 0);
}
}
}
+4 -1
View File
@@ -18,6 +18,9 @@ const campaignIds = new Set(campaign.dungeons.map((dungeon) => dungeon.slug));
const basicOnlyBossExceptions = new Set([
"wailing-caverns:kresh",
"wailing-caverns:deviate-faerie-dragon",
"blackrock-spire:entry-9218",
"blackrock-spire:entry-9219",
"blackrock-spire:entry-10376",
]);
const noTrashExceptions = new Set(["trial-of-the-champion"]);
const animationCache = new Map<string, Promise<readonly { readonly name: string }[]>>();
@@ -171,4 +174,4 @@ describe("WoW dungeon combat and animation coverage", () => {
}
}
}, 120_000);
});
});
+7 -7
View File
@@ -13,6 +13,7 @@ import { useCombatStore } from "../game/combatStore";
import {
combatEffectStyle,
preloadCombatEffectManifest,
effectiveCombatVfxMode,
subscribeCombatPresentation,
type CombatPresentationEvent,
type CombatVfxQuality,
@@ -20,6 +21,7 @@ import {
import {
combatAudioOutputEnabled,
configureCombatAudio,
combatMediaPreloadEnabled,
playCombatAudio,
resetCombatAudio,
setCombatAudioListener,
@@ -254,17 +256,18 @@ export function CombatEffects({ active, safeGraphics = false }: CombatEffectsPro
const camera = useThree((state) => state.camera);
const forward = useMemo(() => new Vector3(), []);
const audioOutputEnabled = combatAudioOutputEnabled(settings, masterVolume);
const vfxMode = effectiveCombatVfxMode(settings);
useEffect(() => {
if (!import.meta.env.DEV) return;
document.documentElement.dataset[COMBAT_EFFECT_DATASET_KEY] = JSON.stringify({
mode: settings.vfxMode,
mode: vfxMode,
quality: effectiveQuality,
systems: effects.length,
activeLights: 0,
allocatedLights: 0,
});
}, [effectiveQuality, effects.length, settings.vfxMode]);
}, [effectiveQuality, effects.length, vfxMode]);
useEffect(() => () => {
if (import.meta.env.DEV) delete document.documentElement.dataset[COMBAT_EFFECT_DATASET_KEY];
@@ -274,10 +277,7 @@ export function CombatEffects({ active, safeGraphics = false }: CombatEffectsPro
settingsRef.current = settings;
qualityRef.current = safeGraphics && settings.vfxQuality === "high" ? "low" : settings.vfxQuality;
configureCombatAudio(settings, masterVolume);
if (
qualityRef.current !== "off"
|| combatAudioOutputEnabled(settings, masterVolume)
) void preloadCombatEffectManifest();
if (combatMediaPreloadEnabled(settings, masterVolume)) void preloadCombatEffectManifest();
setEffects((current) => capCombatEffects(current.filter(({ event }) => (
qualityRef.current !== "off" && settings.vfxEnabled[event.actorGroup]
)), qualityRef.current));
@@ -333,7 +333,7 @@ export function CombatEffects({ active, safeGraphics = false }: CombatEffectsPro
return (
<group name="combat-effects">
{effects.map((effect) => (
settings.vfxMode === "original" ? (
vfxMode === "original" ? (
<OriginalCombatEffect
key={`original:${effect.key}`}
event={effect.event}
+96 -1
View File
@@ -1,12 +1,107 @@
import { Fragment, useMemo } from "react";
import { DoubleSide } from "three";
import { useCombatStore } from "../game/combatStore";
import { useCombatStore, type EnemyActiveCast } from "../game/combatStore";
import type { EnemyAttackDefinition, EnemyDamageSchool } from "../game/enemyAttacks";
import {
enemyTelegraphOrigin,
enemyTelegraphYaw,
} from "../game/enemyTelegraphGeometry";
const TELEGRAPH_COLORS: Readonly<Record<EnemyDamageSchool, string>> = Object.freeze({
physical: "#f2b45e",
arcane: "#c68cff",
fire: "#ff684e",
frost: "#75cfff",
nature: "#8ddf68",
shadow: "#bd79e8",
holy: "#ffe47a",
});
function EnemyCastTelegraph({
cast,
attack,
}: {
readonly cast: EnemyActiveCast;
readonly attack: EnemyAttackDefinition;
}) {
const telegraph = attack.telegraph;
if (!telegraph) return null;
const placement = {
sourcePosition: cast.sourcePosition,
targetPosition: cast.targetPosition,
sourceYaw: cast.sourceYaw,
};
const origin = enemyTelegraphOrigin(telegraph, placement);
const position = [origin[0], origin[1] + 0.055, origin[2]] as const;
const color = TELEGRAPH_COLORS[attack.school];
if (telegraph.shape === "circle") {
const radius = telegraph.radius ?? 1;
return (
<group position={position} name={`enemy-telegraph-${cast.attackId}`}>
<mesh rotation={[-Math.PI / 2, 0, 0]} renderOrder={6}>
<circleGeometry args={[radius, 64]} />
<meshBasicMaterial color={color} transparent opacity={0.18} depthWrite={false} side={DoubleSide} />
</mesh>
<mesh position={[0, 0.008, 0]} rotation={[-Math.PI / 2, 0, 0]} renderOrder={7}>
<ringGeometry args={[Math.max(0.1, radius - 0.18), radius, 64]} />
<meshBasicMaterial color={color} transparent opacity={0.9} depthWrite={false} side={DoubleSide} />
</mesh>
</group>
);
}
const yaw = enemyTelegraphYaw(placement);
if (telegraph.shape === "cone") {
const length = telegraph.length ?? 1;
const angle = ((telegraph.angleDegrees ?? 90) * Math.PI) / 180;
const start = -Math.PI / 2 - angle / 2;
return (
<group position={position} rotation={[0, yaw, 0]} name={`enemy-telegraph-${cast.attackId}`}>
<mesh rotation={[-Math.PI / 2, 0, 0]} renderOrder={6}>
<circleGeometry args={[length, 48, start, angle]} />
<meshBasicMaterial color={color} transparent opacity={0.2} depthWrite={false} side={DoubleSide} />
</mesh>
<mesh position={[0, 0.008, 0]} rotation={[-Math.PI / 2, 0, 0]} renderOrder={7}>
<ringGeometry args={[Math.max(0.1, length - 0.18), length, 48, 1, start, angle]} />
<meshBasicMaterial color={color} transparent opacity={0.92} depthWrite={false} side={DoubleSide} />
</mesh>
</group>
);
}
const length = telegraph.length ?? 1;
const width = telegraph.width ?? 1;
return (
<group position={position} rotation={[0, yaw, 0]} name={`enemy-telegraph-${cast.attackId}`}>
<mesh position={[0, 0, length / 2]} rotation={[Math.PI / 2, 0, 0]} renderOrder={6}>
<planeGeometry args={[width + 0.22, length + 0.22]} />
<meshBasicMaterial color={color} transparent opacity={0.85} depthWrite={false} side={DoubleSide} />
</mesh>
<mesh position={[0, 0.009, length / 2]} rotation={[Math.PI / 2, 0, 0]} renderOrder={7}>
<planeGeometry args={[width, length]} />
<meshBasicMaterial color={color} transparent opacity={0.2} depthWrite={false} side={DoubleSide} />
</mesh>
</group>
);
}
/** Presentation-only rings; placement and ticking remain in the combat runtime. */
export function GroundEffectTelegraphs() {
const effects = useCombatStore((state) => state.areaEffects);
const pending = useCombatStore((state) => state.pendingGroundTarget);
const mobs = useCombatStore((state) => state.mobs);
const enemyCasts = useMemo(() => Object.values(mobs).flatMap((mob) => {
if (!mob.engaged || mob.dead || !mob.activeCast) return [];
const attack = mob.attacks.find((candidate) => candidate.id === mob.activeCast?.attackId);
return attack?.telegraph ? [{ mobId: mob.id, cast: mob.activeCast, attack }] : [];
}), [mobs]);
return (
<group name="combat-ground-effects">
{enemyCasts.map(({ mobId, cast, attack }) => (
<Fragment key={`${mobId}:${cast.attackId}:${cast.startedAt}`}>
<EnemyCastTelegraph cast={cast} attack={attack} />
</Fragment>
))}
{pending ? (
<mesh
name="ground-target-reticle"
+100
View File
@@ -39,6 +39,7 @@ import {
} from "../game/partyRuntimeRegistry";
import { PLAYER_AGGRO_ID } from "../game/aggro";
import {
getMobRuntimeTransform,
registerMobRuntime,
updateMobRuntimePosition,
} from "../game/mobRuntimeRegistry";
@@ -1450,6 +1451,100 @@ function StaticPopulationCluster({
);
}
function SummonedPopulationMember({
instanceId,
entity,
active,
useOriginalModels,
}: {
readonly instanceId: string;
readonly entity: PopulationEntityDefinition;
readonly active: boolean;
readonly useOriginalModels: boolean;
}) {
const groupRef = useRef<Group>(null);
const initial = getMobRuntimeTransform(instanceId)?.position ?? [0, 0, 0];
const positionRef = useRef<MutableMobVector3>([...initial]);
useFrame((_, delta) => {
const mob = useCombatStore.getState().mobs[instanceId];
if (!mob || mob.dead || mob.despawned) return;
const player = useGameStore.getState().playerPosition;
const targetPosition = mobTargetPosition(mob.targetActorId, player);
const position = positionRef.current;
const now = Date.now();
const movementLocked = mob.statuses.some((status) => status.endsAt > now
&& MOVEMENT_LOCKING_MOB_STATUSES.has(status.kind));
const feared = mob.statuses.some((status) => status.endsAt > now && status.kind === "fear");
const slow = mob.statuses.find((status) => status.endsAt > now && status.kind === "slow")?.magnitude ?? 0;
const maximumStep = Math.min(delta, 0.05) * mob.moveSpeed * Math.max(0.1, 1 - slow);
if (!movementLocked && feared) {
stepFearedMobAway(
position,
targetPosition,
mob.homePosition ?? initial,
maximumStep * 1.1,
mob.leashRange,
position,
);
} else if (!movementLocked && mob.combatPhase === "chasing") {
stepToward(position, targetPosition, maximumStep);
}
const facingSign = feared && !movementLocked ? -1 : 1;
const yaw = Math.atan2(
(targetPosition[0] - position[0]) * facingSign,
(targetPosition[2] - position[2]) * facingSign,
);
if (groupRef.current) {
groupRef.current.position.set(position[0], position[1], position[2]);
groupRef.current.rotation.y = yaw;
}
updateMobRuntimePosition(instanceId, position[0], position[1], position[2], yaw);
});
return (
<group ref={groupRef} position={initial} name={`summoned-mob-${instanceId}`}>
<ProxyMob
definition={entity}
instanceId={instanceId}
active={active}
moving
showLabel={false}
useOriginalModels={useOriginalModels}
/>
</group>
);
}
function SummonedEnemyPopulation({
entities,
active,
useOriginalModels,
}: {
readonly entities: PopulationDefinitionMap;
readonly active: boolean;
readonly useOriginalModels: boolean;
}) {
const mobs = useCombatStore((state) => state.mobs);
const summons = useMemo(() => Object.values(mobs).flatMap((mob) => {
if (!mob.summonedBy || mob.serverEntry === null || mob.despawned) return [];
const entity = entities[`entry-${mob.serverEntry}`]
?? Object.values(entities).find((candidate) => candidate.combat?.serverEntry === mob.serverEntry);
return entity ? [{ instanceId: mob.id, entity }] : [];
}), [entities, mobs]);
return (
<group name="summoned-enemy-population">
{summons.map(({ instanceId, entity }) => (
<SummonedPopulationMember
key={instanceId}
instanceId={instanceId}
entity={entity}
active={active}
useOriginalModels={useOriginalModels}
/>
))}
</group>
);
}
export interface MobPopulationProps {
readonly entities: PopulationDefinitionMap;
readonly roamingPacks: readonly MobPackDefinition[];
@@ -1512,6 +1607,11 @@ export function MobPopulation({
useOriginalModels={useOriginalModels}
/>
))}
<SummonedEnemyPopulation
entities={entities}
active={active}
useOriginalModels={useOriginalModels}
/>
</group>
);
}
+7 -1
View File
@@ -1,6 +1,6 @@
import { Html } from "@react-three/drei";
import { useFrame } from "@react-three/fiber";
import { useMemo, useRef, type CSSProperties } from "react";
import { useEffect, useMemo, useRef, type CSSProperties } from "react";
import type { Group } from "three";
import {
classById,
@@ -17,6 +17,7 @@ import { ProceduralAvatar } from "../avatar/ProceduralAvatar";
import { useCombatStore } from "../game/combatStore";
import { onlinePlayerActorId } from "../game/onlineSessionRuntime";
import { usePartyStore } from "../game/partyStore";
import { registerCombatEffectAnchor } from "./combatEffectAnchorRegistry";
const labelStyle: CSSProperties = {
color: "#d8f6ff",
@@ -60,6 +61,11 @@ function RemoteOnlinePlayer({ player, active }: {
const actorId = onlinePlayerActorId(player.accountId);
const selected = useCombatStore((state) => state.selectedFriendlyActorId === actorId);
useEffect(() => {
const root = rootRef.current;
return root ? registerCombatEffectAnchor(actorId, root) : undefined;
}, [actorId]);
useFrame((_, delta) => {
const root = rootRef.current;
if (!root) return;
+48 -15
View File
@@ -1,5 +1,5 @@
import { memo, useEffect, useMemo, useRef, useState, type MutableRefObject } from "react";
import { useFrame } from "@react-three/fiber";
import { useFrame, useThree } from "@react-three/fiber";
import {
AdditiveBlending,
BufferAttribute,
@@ -15,6 +15,7 @@ import {
Texture,
TextureLoader,
Vector2,
Vector3,
} from "three";
import {
combatOriginalPhaseAssets,
@@ -23,6 +24,7 @@ import {
type CombatPresentationEvent,
type CombatVfxQuality,
} from "../game/combatPresentation";
import { fetchCombatMediaAsset } from "../game/combatMediaPack";
import { abilityById } from "../game/abilityCatalog";
import { PLAYER_AURA_ENTITY_ID, useCombatStore } from "../game/combatStore";
import { getMobPosition } from "../game/mobRuntimeRegistry";
@@ -44,6 +46,7 @@ import {
parseOriginalCombatMeshGraph,
type OriginalCombatMeshGraph,
} from "./originalCombatMeshRuntime";
import { updateCombatEffectAnchorPosition } from "./combatEffectAnchorRegistry";
type OriginalSourceEffectGraph = OriginalCombatEffectGraph | OriginalCombatMeshGraph;
@@ -201,7 +204,7 @@ async function loadGraph(url: string): Promise<OriginalSourceEffectGraph> {
// Graph filenames are keyed to the immutable source M2, while the decoded
// browser schema can improve between releases. Revalidate the small JSON so
// an older cached decoder payload cannot fail the current strict parser.
const request = fetch(url, { cache: "no-cache" })
const request = fetchCombatMediaAsset(url)
.then((response) => response.ok
? response.json()
: Promise.reject(new Error(`Original effect graph request failed (${response.status}): ${url}`)))
@@ -218,11 +221,18 @@ async function loadGraph(url: string): Promise<OriginalSourceEffectGraph> {
async function loadTexture(url: string): Promise<Texture> {
const existing = textureRequestCache.get(url);
if (existing) return existing;
const request = textureLoader.loadAsync(url).then((texture) => {
texture.colorSpace = SRGBColorSpace;
texture.name = `original-combat-effect:${url}`;
texture.needsUpdate = true;
return texture;
const request = fetchCombatMediaAsset(url).then(async (response) => {
if (!response.ok) throw new Error(`Original effect texture request failed (${response.status}): ${url}`);
const objectUrl = URL.createObjectURL(await response.blob());
try {
const texture = await textureLoader.loadAsync(objectUrl);
texture.colorSpace = SRGBColorSpace;
texture.name = `original-combat-effect:${url}`;
texture.needsUpdate = true;
return texture;
} finally {
URL.revokeObjectURL(objectUrl);
}
});
textureRequestCache.set(url, request);
request.catch(() => textureRequestCache.delete(url));
@@ -446,7 +456,8 @@ const OriginalStaticMesh = memo(function OriginalStaticMesh({
readonly active: boolean;
readonly playbackRef: MutableRefObject<{ elapsedMs: number; decaying: boolean }>;
}) {
const billboardRef = useRef<Group>(null);
const camera = useThree((state) => state.camera);
const sourcePlaneRef = useRef<Group>(null);
const geometry = useMemo(() => {
const next = new BufferGeometry();
const sourcePositions = graph.geometry.positions;
@@ -457,7 +468,7 @@ const OriginalStaticMesh = memo(function OriginalStaticMesh({
const positions = new Float32Array(sourcePositions.length);
const normals = new Float32Array(sourceNormals.length);
for (let index = 0; index < sourcePositions.length; index += 3) {
// Turn WoW's Z-up source plane into an upright, camera-facing local XY plane.
// Turn WoW's Z-up source plane into an upright local XY plane.
positions[index] = -sourcePositions[index + 1];
positions[index + 1] = sourcePositions[index + 2];
positions[index + 2] = sourcePositions[index] - xCenter;
@@ -501,9 +512,14 @@ const OriginalStaticMesh = memo(function OriginalStaticMesh({
for (const material of materials) material.dispose();
}, [geometry, materials]);
useFrame(({ camera }) => {
useEffect(() => {
// WoW authored this mesh as a billboard. Resolve that orientation once at
// spawn so it stays readable without rotating whenever the camera moves.
sourcePlaneRef.current?.lookAt(camera.position);
}, [camera]);
useFrame(() => {
if (!active) return;
if (billboardRef.current) billboardRef.current.lookAt(camera.position);
const playback = originalCombatMeshPlaybackFrame(
graph,
playbackRef.current.elapsedMs,
@@ -517,8 +533,22 @@ const OriginalStaticMesh = memo(function OriginalStaticMesh({
});
return (
<group ref={billboardRef}>
<mesh geometry={geometry} material={materials} renderOrder={20} frustumCulled={false} />
<group ref={sourcePlaneRef}>
<mesh
name="original-combat-static-source-plane"
geometry={geometry}
material={materials}
renderOrder={20}
frustumCulled={false}
/>
<mesh
name="original-combat-static-cross-plane"
geometry={geometry}
material={materials}
rotation={[0, Math.PI / 2, 0]}
renderOrder={20}
frustumCulled={false}
/>
</group>
);
});
@@ -533,6 +563,7 @@ function LoadedOriginalCombatEffect({
const elapsedRef = useRef(0);
const doneRef = useRef(false);
const groupRef = useRef<Group>(null);
const liveAnchorPosition = useMemo(() => new Vector3(), []);
const auraDecayStartedAtRef = useRef<number | null>(null);
const staticMeshPlaybackRef = useRef({ elapsedMs: 0, decaying: false });
const particleBudgets = useMemo(() => {
@@ -617,8 +648,10 @@ function LoadedOriginalCombatEffect({
);
} else {
const actorId = originalCombatAnchorActorId(event);
const live = liveActorPosition(actorId);
if (live) groupRef.current.position.set(live[0], live[1], live[2]);
if (!updateCombatEffectAnchorPosition(actorId, groupRef.current, liveAnchorPosition)) {
const live = liveActorPosition(actorId);
if (live) groupRef.current.position.set(live[0], live[1], live[2]);
}
}
}
const terminalDuration = projectile
Loaded 100 of 147 files, more files were not shown because too many files have changed in this diff. Show more