mirror of
https://github.com/araxiaonline/mod-mythic-plus.git
synced 2026-06-13 03:02:24 -04:00
Support for World Boss encounters
This commit is contained in:
@@ -132,3 +132,41 @@ MythicPlus.DiminishingThreshold.Ascendant = 40000
|
||||
MythicPlus.ElementalMeleeReducer = 0.50
|
||||
MythicPlus.NormalEnemyReducer = 0.50
|
||||
MythicPlus.NonCreatureSpellReducer = 0.50
|
||||
|
||||
##########################################################
|
||||
#
|
||||
# World Boss Settings
|
||||
# - These settings control Mythic+ scaling for world bosses in open-world zones
|
||||
# - World bosses work differently than dungeons:
|
||||
# * Multiple groups can engage the same boss
|
||||
# * Boss scales to the highest difficulty of participating groups
|
||||
# * Proximity-based group detection (players within range)
|
||||
# * Summoned creatures (adds) are automatically scaled
|
||||
#
|
||||
##########################################################
|
||||
|
||||
# Enable Mythic+ scaling for world bosses
|
||||
# Default: 0 (disabled)
|
||||
MythicPlus.WorldBoss.Enabled = 0
|
||||
|
||||
# Comma-separated list of zone IDs where world boss scaling is enabled
|
||||
# Examples: 16 (Azshara), 490 (Moonglade), 28 (Western Plaguelands), 139 (Eastern Plaguelands)
|
||||
# Leave empty to allow all zones
|
||||
# Default: empty (all zones allowed when enabled)
|
||||
MythicPlus.WorldBoss.EnabledZones = 16,28,139,490
|
||||
|
||||
# Comma-separated list of specific world boss creature entries to enable
|
||||
# Examples: 6109 (Azuregos), 14889 (Emeriss), 14890 (Taerar), 14887 (Ysondre), 14888 (Lethon)
|
||||
# Leave empty to allow all world bosses in enabled zones
|
||||
# Default: empty (all world bosses in enabled zones)
|
||||
MythicPlus.WorldBoss.EnabledBosses = 6109,14889,14890,14887,14888
|
||||
|
||||
# Proximity range in yards for detecting groups near world bosses
|
||||
# Groups within this range will be registered to the encounter
|
||||
# Default: 100 yards
|
||||
MythicPlus.WorldBoss.ProximityRange = 100
|
||||
|
||||
# Scale boss difficulty based on number of participating groups
|
||||
# If enabled, boss gets additional scaling per group beyond the first
|
||||
# Default: 0 (disabled - uses highest difficulty only)
|
||||
MythicPlus.WorldBoss.ScaleWithGroupCount = 0
|
||||
|
||||
@@ -1,3 +1,4 @@
|
||||
#include <algorithm>
|
||||
#include "CharacterDatabase.h"
|
||||
#include "MpDataStore.h"
|
||||
#include "Chat.h"
|
||||
@@ -5,6 +6,68 @@
|
||||
#include "MpLogger.h"
|
||||
#include "Chat.h"
|
||||
|
||||
void MpWorldBossEncounter::AddGroup(ObjectGuid groupGuid, MpDifficulty groupDifficulty)
|
||||
{
|
||||
if (!groupGuid)
|
||||
return;
|
||||
|
||||
// Store the highest difficulty among participating groups
|
||||
auto it = participatingGroups.find(groupGuid);
|
||||
if (it == participatingGroups.end() || it->second < groupDifficulty) {
|
||||
participatingGroups[groupGuid] = groupDifficulty;
|
||||
if (groupDifficulty > difficulty) {
|
||||
difficulty = groupDifficulty;
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
void MpWorldBossEncounter::AddScaledCreature(ObjectGuid creatureGuid)
|
||||
{
|
||||
if (creatureGuid) {
|
||||
scaledCreatures.insert(creatureGuid);
|
||||
}
|
||||
}
|
||||
|
||||
void MpDataStore::AddWorldBossEncounter(ObjectGuid bossGuid, uint32 bossEntry, uint32 zoneId)
|
||||
{
|
||||
if (!bossGuid || !bossEntry) {
|
||||
return;
|
||||
}
|
||||
|
||||
MpWorldBossEncounter encounter(bossGuid, bossEntry, zoneId);
|
||||
encounter.isActive = true;
|
||||
(*_worldBossEncounters)[bossGuid] = encounter;
|
||||
}
|
||||
|
||||
MpWorldBossEncounter* MpDataStore::GetWorldBossEncounter(ObjectGuid bossGuid)
|
||||
{
|
||||
if (!_worldBossEncounters->contains(bossGuid)) {
|
||||
return nullptr;
|
||||
}
|
||||
|
||||
return &(*_worldBossEncounters)[bossGuid];
|
||||
}
|
||||
|
||||
MpWorldBossEncounter* MpDataStore::GetWorldBossEncounterByZone(uint32 zoneId)
|
||||
{
|
||||
for (auto& [guid, encounter] : *_worldBossEncounters) {
|
||||
if (encounter.zoneId == zoneId && encounter.isActive) {
|
||||
return &encounter;
|
||||
}
|
||||
}
|
||||
|
||||
return nullptr;
|
||||
}
|
||||
|
||||
void MpDataStore::RemoveWorldBossEncounter(ObjectGuid bossGuid)
|
||||
{
|
||||
if (!bossGuid) {
|
||||
return;
|
||||
}
|
||||
|
||||
_worldBossEncounters->erase(bossGuid);
|
||||
}
|
||||
|
||||
// Adds an entry for the group difficult to memory and updats database
|
||||
void MpDataStore::AddGroupData(Group *group, MpGroupData groupData) {
|
||||
if(!group) {
|
||||
@@ -314,6 +377,48 @@ int32 MpDataStore::LoadScaleFactors() {
|
||||
return int32(_scaleFactors->size());
|
||||
}
|
||||
|
||||
int32 MpDataStore::LoadWorldBossConfig() {
|
||||
// 0 1 2 3 4 5 6 7 8
|
||||
QueryResult result = WorldDatabase.Query("SELECT entry, name, zoneId, enabled, melee_bonus, spell_bonus, heal_bonus, hp_bonus, difficulty FROM mp_world_boss_config WHERE enabled = 1");
|
||||
|
||||
if (!result) {
|
||||
MpLogger::warn("No world boss configuration found in database (table may not exist or is empty)");
|
||||
return 0;
|
||||
}
|
||||
|
||||
int32 count = 0;
|
||||
do {
|
||||
Field* fields = result->Fetch();
|
||||
uint32 entry = fields[0].Get<uint32>();
|
||||
std::string name = fields[1].Get<std::string>();
|
||||
uint32 zoneId = fields[2].Get<uint32>();
|
||||
bool enabled = fields[3].Get<bool>();
|
||||
float meleeBonus = fields[4].Get<float>();
|
||||
float spellBonus = fields[5].Get<float>();
|
||||
float healBonus = fields[6].Get<float>();
|
||||
float hpBonus = fields[7].Get<float>();
|
||||
int32 difficulty = fields[8].Get<int32>();
|
||||
|
||||
// Store scale factors for this world boss using its zone ID and difficulty
|
||||
MpScaleFactor scaleFactor = {
|
||||
.meleeBonus = meleeBonus,
|
||||
.spellBonus = spellBonus,
|
||||
.healBonus = healBonus,
|
||||
.healthBonus = hpBonus
|
||||
};
|
||||
|
||||
// Use zone ID as the "map" for world bosses
|
||||
_scaleFactors->emplace(GetScaleFactorKey(zoneId, difficulty), scaleFactor);
|
||||
|
||||
MpLogger::debug("Loaded world boss config: {} (entry: {}, zone: {}, difficulty: {})",
|
||||
name, entry, zoneId, difficulty);
|
||||
|
||||
count++;
|
||||
} while (result->NextRow());
|
||||
|
||||
return count;
|
||||
}
|
||||
|
||||
void MpDataStore::LoadPlayerHealthAvg() {
|
||||
_playerHealthAvg.clear();
|
||||
|
||||
|
||||
@@ -10,6 +10,7 @@
|
||||
#include "ObjectGuid.h"
|
||||
|
||||
#include <unordered_map>
|
||||
#include <unordered_set>
|
||||
#include <map>
|
||||
#include <string>
|
||||
#include <vector>
|
||||
@@ -25,6 +26,26 @@ enum MpDifficulty
|
||||
MP_DIFFICULTY_ASCENDANT = 5
|
||||
};
|
||||
|
||||
struct MpWorldBossEncounter
|
||||
{
|
||||
ObjectGuid bossGuid;
|
||||
uint32 bossEntry = 0;
|
||||
uint32 zoneId = 0;
|
||||
bool isActive = false;
|
||||
MpDifficulty difficulty = MP_DIFFICULTY_MYTHIC;
|
||||
MpDifficulty scaledDifficulty = MP_DIFFICULTY_NORMAL;
|
||||
std::unordered_map<ObjectGuid, MpDifficulty> participatingGroups;
|
||||
std::unordered_set<ObjectGuid> scaledCreatures;
|
||||
|
||||
MpWorldBossEncounter() = default;
|
||||
|
||||
MpWorldBossEncounter(ObjectGuid guid, uint32 entry, uint32 zone)
|
||||
: bossGuid(guid), bossEntry(entry), zoneId(zone), isActive(true) {}
|
||||
|
||||
void AddGroup(ObjectGuid groupGuid, MpDifficulty groupDifficulty);
|
||||
void AddScaledCreature(ObjectGuid creatureGuid);
|
||||
};
|
||||
|
||||
class MpDataStore;
|
||||
|
||||
struct MpPlayerInstanceData
|
||||
@@ -304,7 +325,8 @@ private:
|
||||
_instanceData(std::make_unique<std::map<std::pair<uint32, uint32>, MpInstanceData>>()),
|
||||
_groupData(std::make_unique<std::unordered_map<ObjectGuid, MpGroupData>>()),
|
||||
_instanceCreatureData(std::make_unique<std::unordered_map<ObjectGuid, MpCreatureData>>()),
|
||||
_scaleFactors(std::make_unique<std::map<std::pair<int32, int32>,MpScaleFactor>>())
|
||||
_scaleFactors(std::make_unique<std::map<std::pair<int32, int32>,MpScaleFactor>>()),
|
||||
_worldBossEncounters(std::make_unique<std::unordered_map<ObjectGuid, MpWorldBossEncounter>>())
|
||||
{
|
||||
_playerData->reserve(32);
|
||||
_groupData->reserve(32);
|
||||
@@ -333,6 +355,9 @@ private:
|
||||
// use to mimic pattern normals scale to heroic (loaded at server start)
|
||||
std::unique_ptr<std::map<std::pair<int32,int32>,MpScaleFactor>> _scaleFactors; // {mapId,difficulty}
|
||||
|
||||
// Active world boss encounters keyed by boss GUID
|
||||
std::unique_ptr<std::unordered_map<ObjectGuid, MpWorldBossEncounter>> _worldBossEncounters;
|
||||
|
||||
// Player mapping of level to average amount of health for that level, this is used for scaling against
|
||||
// percentages to more consistently scale damage from spells and healing from creatures.
|
||||
std::unordered_map<uint32, uint32> _playerHealthAvg; // level -> avg health
|
||||
@@ -405,6 +430,11 @@ public:
|
||||
// Retrieves the average players hp pool for a player level
|
||||
uint32 GetPlayerHealthAvg(uint32 level) const;
|
||||
|
||||
// Debug method to get map sizes
|
||||
std::tuple<size_t, size_t, size_t> GetMapSizes() const {
|
||||
return {_instanceData->size(), _groupData->size(), _instanceCreatureData->size()};
|
||||
}
|
||||
|
||||
// Individual Creature Scaling Multipliers
|
||||
// void AddCreatureOverride(uint32 entry, CreatureOverride* override);
|
||||
// MpMultipliers* GetCreatureOverride(uint32 entry);
|
||||
@@ -419,6 +449,15 @@ public:
|
||||
// Used at initial server load
|
||||
int32 LoadScaleFactors();
|
||||
|
||||
// Load world boss scaling configuration from database
|
||||
int32 LoadWorldBossConfig();
|
||||
|
||||
// World boss encounter management
|
||||
void AddWorldBossEncounter(ObjectGuid bossGuid, uint32 bossEntry, uint32 zoneId);
|
||||
MpWorldBossEncounter* GetWorldBossEncounter(ObjectGuid bossGuid);
|
||||
MpWorldBossEncounter* GetWorldBossEncounterByZone(uint32 zoneId);
|
||||
void RemoveWorldBossEncounter(ObjectGuid bossGuid);
|
||||
|
||||
// Load the player health average from the database
|
||||
void LoadPlayerHealthAvg();
|
||||
|
||||
|
||||
@@ -8,6 +8,9 @@
|
||||
#include "WorldPacket.h"
|
||||
#include "UpdateMask.h"
|
||||
#include "MpScriptAI.h"
|
||||
#include "GridNotifiers.h"
|
||||
#include "GridNotifiersImpl.h"
|
||||
#include "CellImpl.h"
|
||||
|
||||
#include <algorithm>
|
||||
#include <cmath>
|
||||
@@ -17,17 +20,167 @@ const uint32 HEADLESS_HORSEMAN = 23682;
|
||||
|
||||
bool MythicPlus::IsMapEligible(Map* map)
|
||||
{
|
||||
if (!Enabled) {
|
||||
if (!Enabled || !map) {
|
||||
return false;
|
||||
}
|
||||
|
||||
// Existing dungeon support
|
||||
if (map->IsDungeon()) {
|
||||
return true;
|
||||
}
|
||||
|
||||
// World boss encounters happen on outdoor maps; allow them when the feature is enabled
|
||||
if (EnableWorldBoss && !map->Instanceable()) {
|
||||
return true;
|
||||
}
|
||||
|
||||
return false;
|
||||
}
|
||||
|
||||
bool MythicPlus::IsWorldBossZone(uint32 zoneId)
|
||||
{
|
||||
return std::find(enabledWorldBossZones.begin(), enabledWorldBossZones.end(), zoneId) != enabledWorldBossZones.end();
|
||||
}
|
||||
|
||||
bool MythicPlus::IsWorldBossEnabled(uint32 creatureEntry)
|
||||
{
|
||||
// If no specific bosses are configured, all world bosses in enabled zones are allowed
|
||||
if (enabledWorldBosses.empty()) {
|
||||
return true;
|
||||
}
|
||||
|
||||
return std::find(enabledWorldBosses.begin(), enabledWorldBosses.end(), creatureEntry) != enabledWorldBosses.end();
|
||||
}
|
||||
|
||||
void MythicPlus::ScanForNearbyGroups(Creature* worldBoss)
|
||||
{
|
||||
if (!worldBoss || !worldBoss->isWorldBoss()) {
|
||||
return;
|
||||
}
|
||||
|
||||
Map* map = worldBoss->GetMap();
|
||||
if (!map) {
|
||||
return;
|
||||
}
|
||||
|
||||
MpWorldBossEncounter* encounter = sMpDataStore->GetWorldBossEncounter(worldBoss->GetGUID());
|
||||
if (!encounter) {
|
||||
MpLogger::warn("ScanForNearbyGroups: No encounter found for world boss {}", worldBoss->GetName());
|
||||
return;
|
||||
}
|
||||
|
||||
// Get all players in range
|
||||
std::list<Player*> playersInRange;
|
||||
Acore::AnyPlayerInObjectRangeCheck checker(worldBoss, worldBossProximityRange);
|
||||
Acore::PlayerListSearcher<Acore::AnyPlayerInObjectRangeCheck> searcher(worldBoss, playersInRange, checker);
|
||||
Cell::VisitWorldObjects(worldBoss, searcher, worldBossProximityRange);
|
||||
|
||||
// Process each player's group
|
||||
for (Player* player : playersInRange) {
|
||||
Group* group = player->GetGroup();
|
||||
if (!group) {
|
||||
continue;
|
||||
}
|
||||
|
||||
MpGroupData* groupData = sMpDataStore->GetGroupData(group->GetGUID());
|
||||
if (!groupData) {
|
||||
continue; // Group doesn't have Mythic+ difficulty set
|
||||
}
|
||||
|
||||
// Add group to encounter
|
||||
encounter->AddGroup(group->GetGUID(), groupData->difficulty);
|
||||
|
||||
MpLogger::debug("World boss {}: Added group {} with difficulty {}",
|
||||
worldBoss->GetName(),
|
||||
group->GetGUID().ToString(),
|
||||
static_cast<int>(groupData->difficulty));
|
||||
}
|
||||
}
|
||||
|
||||
void MythicPlus::HandleWorldBossEncounter(Creature* worldBoss)
|
||||
{
|
||||
if (!worldBoss || !worldBoss->isWorldBoss()) {
|
||||
return;
|
||||
}
|
||||
|
||||
Map* map = worldBoss->GetMap();
|
||||
if (!map) {
|
||||
return;
|
||||
}
|
||||
|
||||
ObjectGuid bossGuid = worldBoss->GetGUID();
|
||||
uint32 bossEntry = worldBoss->GetEntry();
|
||||
uint32 zoneId = worldBoss->GetZoneId();
|
||||
if (!zoneId) {
|
||||
zoneId = worldBoss->GetAreaId();
|
||||
}
|
||||
if (!zoneId) {
|
||||
zoneId = map->GetId();
|
||||
}
|
||||
|
||||
// Check if encounter already exists
|
||||
MpWorldBossEncounter* encounter = sMpDataStore->GetWorldBossEncounter(bossGuid);
|
||||
|
||||
if (!encounter) {
|
||||
// Create new encounter
|
||||
sMpDataStore->AddWorldBossEncounter(bossGuid, bossEntry, zoneId);
|
||||
encounter = sMpDataStore->GetWorldBossEncounter(bossGuid);
|
||||
|
||||
if (!encounter) {
|
||||
MpLogger::error("Failed to create world boss encounter for {}", worldBoss->GetName());
|
||||
return;
|
||||
}
|
||||
|
||||
MpLogger::info("Created world boss encounter: {} in zone {}", worldBoss->GetName(), zoneId);
|
||||
}
|
||||
|
||||
// Scan for nearby groups
|
||||
ScanForNearbyGroups(worldBoss);
|
||||
|
||||
// If no groups are participating, don't scale
|
||||
if (encounter->participatingGroups.empty()) {
|
||||
MpLogger::debug("World boss {}: No groups in range with Mythic+ difficulty", worldBoss->GetName());
|
||||
return;
|
||||
}
|
||||
|
||||
// Get the highest difficulty from participating groups
|
||||
MpDifficulty difficulty = encounter->difficulty;
|
||||
|
||||
// Get appropriate multipliers based on difficulty
|
||||
MpMultipliers* multipliers = nullptr;
|
||||
switch (difficulty) {
|
||||
case MP_DIFFICULTY_MYTHIC:
|
||||
multipliers = &mythicBossModifiers;
|
||||
break;
|
||||
case MP_DIFFICULTY_LEGENDARY:
|
||||
multipliers = &legendaryBossModifiers;
|
||||
break;
|
||||
case MP_DIFFICULTY_ASCENDANT:
|
||||
multipliers = &ascendantBossModifiers;
|
||||
break;
|
||||
default:
|
||||
MpLogger::warn("World boss {}: Invalid difficulty {}", worldBoss->GetName(), static_cast<int>(difficulty));
|
||||
return;
|
||||
}
|
||||
|
||||
bool alreadyScaled = encounter->scaledCreatures.contains(bossGuid) && encounter->scaledDifficulty == difficulty;
|
||||
if (alreadyScaled) {
|
||||
return;
|
||||
}
|
||||
|
||||
// Scale the world boss
|
||||
ScaleCreature(multipliers->avgLevel, worldBoss, multipliers, difficulty);
|
||||
|
||||
// Track this creature as scaled
|
||||
encounter->AddScaledCreature(bossGuid);
|
||||
encounter->scaledDifficulty = difficulty;
|
||||
|
||||
MpLogger::info("Scaled world boss {} to difficulty {} for {} groups",
|
||||
worldBoss->GetName(),
|
||||
static_cast<int>(difficulty),
|
||||
encounter->participatingGroups.size());
|
||||
}
|
||||
|
||||
bool MythicPlus::IsDifficultySet(Player const* player)
|
||||
{
|
||||
Group const* group = player->GetGroup();
|
||||
@@ -130,6 +283,18 @@ bool MythicPlus::IsCreatureEligible(Creature* creature)
|
||||
return true;
|
||||
}
|
||||
|
||||
// NEW: World boss eligibility check
|
||||
if (EnableWorldBoss && creature->isWorldBoss()) {
|
||||
uint32 zoneId = creature->GetZoneId();
|
||||
if (!zoneId) {
|
||||
zoneId = creature->GetAreaId();
|
||||
}
|
||||
|
||||
if (zoneId && IsWorldBossZone(zoneId) && IsWorldBossEnabled(creature->GetEntry())) {
|
||||
return true;
|
||||
}
|
||||
}
|
||||
|
||||
// Check if the creature is a pet or summon controlled by a player
|
||||
if ((creature->IsHunterPet() || creature->IsPet() || creature->IsSummon()) && creature->IsControlledByPlayer()) {
|
||||
return false;
|
||||
@@ -251,12 +416,9 @@ void MythicPlus::ScaleCreature(uint8 level, Creature* creature, MpMultipliers* m
|
||||
CreatureTemplate const* cInfo = creature->GetCreatureTemplate();
|
||||
uint32 mapId = creature->GetMapId();
|
||||
|
||||
// get the map difficulty from the map instance to see if it is a heroic or normal set instance
|
||||
InstanceMap *instanceMap = creature->GetMap()->ToInstanceMap();
|
||||
if (!instanceMap) {
|
||||
MpLogger::error("Invalid instance map ScaleCreature()");
|
||||
return;
|
||||
}
|
||||
// When inside an instanced map we can access additional info (heroic, 25-man, etc.)
|
||||
// but world bosses run on outdoor maps, so tolerate the lack of InstanceMap here.
|
||||
InstanceMap* instanceMap = creature->GetMap()->ToInstanceMap();
|
||||
|
||||
creature->SetLevel(level);
|
||||
CreatureBaseStats const* stats = sObjectMgr->GetCreatureBaseStats(
|
||||
@@ -291,13 +453,17 @@ void MythicPlus::ScaleCreature(uint8 level, Creature* creature, MpMultipliers* m
|
||||
creature->SetModifierValue(UNIT_MOD_MANA, BASE_VALUE, (float)mana * 3.0f);
|
||||
}
|
||||
|
||||
MpInstanceData *instanceData = sMpDataStore->GetInstanceData(creature->GetMapId(), creature->GetInstanceId());
|
||||
MpInstanceData* instanceData = sMpDataStore->GetInstanceData(creature->GetMapId(), creature->GetInstanceId());
|
||||
|
||||
// Handle new melee/range scaling with simple formula (for simplicity range will just be 80% of melee bonus)
|
||||
float meleeMultiplier = sMpDataStore->GetMeleeScaleFactor(creature->GetMapId(), instanceData->difficulty);
|
||||
float meleeMultiplier = multipliers->melee;
|
||||
|
||||
if (instanceData) {
|
||||
meleeMultiplier = sMpDataStore->GetMeleeScaleFactor(creature->GetMapId(), instanceData->difficulty);
|
||||
}
|
||||
|
||||
// Since Heroic Scaling can get out of hand. Reduce the instance multiplier by way too much 10%
|
||||
if(instanceMap->IsHeroic() || instanceMap->Is25ManRaid()) {
|
||||
if(instanceMap && (instanceMap->IsHeroic() || instanceMap->Is25ManRaid())) {
|
||||
// if the enemy is a boss reduce it by less
|
||||
meleeMultiplier *= 0.9f;
|
||||
}
|
||||
@@ -394,13 +560,31 @@ int32 MythicPlus::ScaleDamageSpell(SpellInfo const * spellInfo, uint32 damage, M
|
||||
return damage;
|
||||
}
|
||||
|
||||
MpInstanceData *instanceData = sMpDataStore->GetInstanceData(creature->GetMapId(), creature->GetInstanceId());
|
||||
if (!instanceData) {
|
||||
MpLogger::debug("No instance data found for spell scaling, using original damage");
|
||||
return damage;
|
||||
}
|
||||
MpInstanceData* instanceData = sMpDataStore->GetInstanceData(creature->GetMapId(), creature->GetInstanceId());
|
||||
float scaleFactor = 1.0f;
|
||||
|
||||
float scaleFactor = sMpDataStore->GetSpellScaleFactor(creature->GetMapId(), instanceData->difficulty);
|
||||
if (instanceData) {
|
||||
scaleFactor = sMpDataStore->GetSpellScaleFactor(creature->GetMapId(), instanceData->difficulty);
|
||||
} else {
|
||||
MpWorldBossEncounter* encounter = sMpDataStore->GetWorldBossEncounter(creature->GetGUID());
|
||||
if (!encounter) {
|
||||
uint32 zoneId = creature->GetZoneId();
|
||||
if (!zoneId) {
|
||||
zoneId = creature->GetAreaId();
|
||||
}
|
||||
if (zoneId) {
|
||||
encounter = sMpDataStore->GetWorldBossEncounterByZone(zoneId);
|
||||
}
|
||||
}
|
||||
|
||||
if (!encounter) {
|
||||
MpLogger::debug("No scaling context found for spell damage (map {}), leaving damage unchanged", creature->GetMapId());
|
||||
return damage;
|
||||
}
|
||||
|
||||
uint32 scaleKey = encounter->zoneId ? encounter->zoneId : creature->GetMapId();
|
||||
scaleFactor = sMpDataStore->GetSpellScaleFactor(scaleKey, encounter->difficulty);
|
||||
}
|
||||
|
||||
MpLogger::debug("DAMAGE SPELL: >> ScaleFactor: {} DamageMultiplier: {}", scaleFactor, damageMultiplier);
|
||||
|
||||
|
||||
@@ -82,6 +82,11 @@ public:
|
||||
float normalEnemyReducer;
|
||||
float nonCreatureSpellReducer;
|
||||
|
||||
// World Boss Settings
|
||||
bool EnableWorldBoss;
|
||||
std::vector<uint32> enabledWorldBossZones;
|
||||
std::vector<uint32> enabledWorldBosses;
|
||||
uint32 worldBossProximityRange;
|
||||
|
||||
enum MP_UNIT_EVENT_TYPE
|
||||
{
|
||||
@@ -95,6 +100,18 @@ public:
|
||||
// Map is eligible for mythic+ scaling
|
||||
bool IsMapEligible(Map* map);
|
||||
|
||||
// Check if a zone is enabled for world boss scaling
|
||||
bool IsWorldBossZone(uint32 zoneId);
|
||||
|
||||
// Check if a specific world boss is enabled for scaling
|
||||
bool IsWorldBossEnabled(uint32 creatureEntry);
|
||||
|
||||
// Scan for groups near a world boss and register them to the encounter
|
||||
void ScanForNearbyGroups(Creature* worldBoss);
|
||||
|
||||
// Handle world boss encounter creation and scaling
|
||||
void HandleWorldBossEncounter(Creature* worldBoss);
|
||||
|
||||
// If a player difficulty is set that is eligible for mythic+ scaling
|
||||
bool IsDifficultySet(Player const* player);
|
||||
|
||||
|
||||
@@ -66,6 +66,14 @@ public:
|
||||
}
|
||||
m_creatureUpdateTimers[creatureGuid] = 0;
|
||||
|
||||
// NEW: Handle world boss encounters (non-instanced)
|
||||
if (sMythicPlus->EnableWorldBoss && creature->isWorldBoss()) {
|
||||
// Only scan when boss is in combat to avoid unnecessary overhead
|
||||
if (creature->IsInCombat()) {
|
||||
sMythicPlus->HandleWorldBossEncounter(creature);
|
||||
}
|
||||
return; // World bosses don't use instance data
|
||||
}
|
||||
|
||||
auto instanceData = sMpDataStore->GetInstanceData(creature->GetMapId(), creature->GetInstanceId());
|
||||
// no instance data yet means dont scale.
|
||||
@@ -97,8 +105,6 @@ public:
|
||||
sMythicPlus->AddScaledCreature(creature, instanceData);
|
||||
}
|
||||
}
|
||||
|
||||
|
||||
}
|
||||
|
||||
// When a new creature is added into a mythic+ map add it to the list of creatures to scale later.
|
||||
@@ -110,6 +116,46 @@ public:
|
||||
}
|
||||
|
||||
if (!sMythicPlus->IsCreatureEligible(creature)) {
|
||||
// NEW: Check if this is a summon/add for a world boss encounter
|
||||
if (sMythicPlus->EnableWorldBoss && creature->IsSummon()) {
|
||||
// Ignore invisible triggers / non-combat helpers
|
||||
if (creature->IsTrigger() || creature->HasUnitFlag(UNIT_FLAG_NOT_SELECTABLE) || creature->IsCritter()) {
|
||||
return;
|
||||
}
|
||||
|
||||
// Check if there's an active world boss encounter in this zone
|
||||
uint32 zoneId = creature->GetZoneId();
|
||||
if (!zoneId) {
|
||||
zoneId = creature->GetAreaId();
|
||||
}
|
||||
MpWorldBossEncounter* encounter = sMpDataStore->GetWorldBossEncounterByZone(zoneId);
|
||||
if (encounter && encounter->isActive) {
|
||||
// This is likely an add for the world boss - scale it
|
||||
MpDifficulty difficulty = encounter->difficulty;
|
||||
MpMultipliers* multipliers = nullptr;
|
||||
|
||||
switch (difficulty) {
|
||||
case MP_DIFFICULTY_MYTHIC:
|
||||
multipliers = &sMythicPlus->mythicDungeonModifiers;
|
||||
break;
|
||||
case MP_DIFFICULTY_LEGENDARY:
|
||||
multipliers = &sMythicPlus->legendaryDungeonModifiers;
|
||||
break;
|
||||
case MP_DIFFICULTY_ASCENDANT:
|
||||
multipliers = &sMythicPlus->ascendantDungeonModifiers;
|
||||
break;
|
||||
default:
|
||||
return;
|
||||
}
|
||||
|
||||
sMythicPlus->ScaleCreature(multipliers->avgLevel, creature, multipliers, difficulty);
|
||||
encounter->AddScaledCreature(creature->GetGUID());
|
||||
|
||||
MpLogger::debug("Scaled world boss add: {} for encounter in zone {}",
|
||||
creature->GetName(), map->GetId());
|
||||
return;
|
||||
}
|
||||
}
|
||||
return;
|
||||
}
|
||||
|
||||
@@ -126,6 +172,18 @@ public:
|
||||
// Cleanup the creature from custom data used for mythic+ mod
|
||||
void OnCreatureRemoveWorld(Creature* creature) override
|
||||
{
|
||||
// NEW: Clean up world boss encounters
|
||||
if (sMythicPlus->EnableWorldBoss && creature->isWorldBoss()) {
|
||||
MpWorldBossEncounter* encounter = sMpDataStore->GetWorldBossEncounter(creature->GetGUID());
|
||||
if (encounter) {
|
||||
encounter->isActive = false;
|
||||
MpLogger::info("World boss {} removed - encounter marked inactive", creature->GetName());
|
||||
}
|
||||
}
|
||||
|
||||
// Remove any throttling state for this creature
|
||||
m_creatureUpdateTimers.erase(creature->GetGUID());
|
||||
|
||||
sMythicPlus->RemoveCreature(creature);
|
||||
}
|
||||
|
||||
|
||||
@@ -39,6 +39,7 @@ public:
|
||||
{"mp", commandTableMain},
|
||||
{"mythicplus", commandTableMain},
|
||||
{"mp debug", HandleDebug, SEC_PLAYER, Console::No},
|
||||
{"mp debug maps", HandleDebugMaps, SEC_PLAYER, Console::No},
|
||||
{"mp reload", HandleReload, SEC_GAMEMASTER, Console::No},
|
||||
{"advancement", HandleAdvancement, SEC_PLAYER, Console::No}
|
||||
};
|
||||
@@ -105,6 +106,18 @@ public:
|
||||
|
||||
}
|
||||
|
||||
static bool HandleDebugMaps(ChatHandler* handler)
|
||||
{
|
||||
auto [instanceDataSize, groupDataSize, creatureDataSize] = sMpDataStore->GetMapSizes();
|
||||
|
||||
handler->PSendSysMessage("=== MpDataStore Map Sizes ===");
|
||||
handler->PSendSysMessage("Instance Data (_instanceData): {}", instanceDataSize);
|
||||
handler->PSendSysMessage("Group Data (_groupData): {}", groupDataSize);
|
||||
handler->PSendSysMessage("Creature Data (_instanceCreatureData): {}", creatureDataSize);
|
||||
|
||||
return true;
|
||||
}
|
||||
|
||||
// sets the difficluty for the group
|
||||
static bool HandleSetDifficulty(ChatHandler* handler, const std::vector<std::string>& args)
|
||||
{
|
||||
|
||||
@@ -42,43 +42,28 @@ public:
|
||||
return;
|
||||
}
|
||||
|
||||
// get the item to scale up
|
||||
// get the original item template
|
||||
ItemTemplate const* origItem = sObjectMgr->GetItemTemplate(LootStoreItem->itemid);
|
||||
if (!origItem) {
|
||||
|
||||
// If there is not a scaled up item and the item is a below quality green then set an invalid item_id so it is not added to loot
|
||||
ItemTemplate const* nonMythicItem = sObjectMgr->GetItemTemplate(LootStoreItem->itemid);
|
||||
if (nonMythicItem->Quality < 2) {
|
||||
LootStoreItem->itemid = 0;
|
||||
return;
|
||||
}
|
||||
|
||||
// otherwise roll a chance to see a shadowy remains item is provided instead only if there is not already a shadowy remains item on the corpse
|
||||
bool hasShadowyRemains = false;
|
||||
for (auto& item : loot.items) {
|
||||
if(item.itemid == MpConstants::SHADOWY_REMAINS) {
|
||||
hasShadowyRemains = true;
|
||||
break;
|
||||
}
|
||||
}
|
||||
|
||||
if (!hasShadowyRemains) {
|
||||
LootStoreItem->itemid = MpConstants::SHADOWY_REMAINS;
|
||||
return;
|
||||
} else {
|
||||
LootStoreItem->itemid = 0;
|
||||
return;
|
||||
}
|
||||
// Original item doesn't exist in database - this shouldn't happen
|
||||
MpLogger::warn("Original item template not found for itemid {} in OnBeforeDropAddItem()", LootStoreItem->itemid);
|
||||
LootStoreItem->itemid = 0;
|
||||
return;
|
||||
}
|
||||
|
||||
// Calculate the scaled mythic+ item ID
|
||||
uint32 newItemId = origItem->ItemId + mythicSettings->itemOffset;
|
||||
ItemTemplate const* newItemTempl = sObjectMgr->GetItemTemplate(newItemId);
|
||||
|
||||
if(!newItemTempl) {
|
||||
MpLogger::warn("New Loot Item not found for itemid {} original item: {} ({})", newItemId, origItem->Name1, origItem->ItemId);
|
||||
|
||||
// Scaled item doesn't exist - keep the original item
|
||||
// LootStoreItem->itemid is already set to the original, so just return
|
||||
return;
|
||||
}
|
||||
|
||||
// Scaled item exists - use it
|
||||
LootStoreItem->itemid = newItemId;
|
||||
|
||||
// Revalidate the LootStoreItem to ensure consistency
|
||||
|
||||
@@ -152,12 +152,19 @@ private:
|
||||
#endif
|
||||
|
||||
Creature* creatureCaster = attacker->ToCreature();
|
||||
MpCreatureData* creatureData = sMpDataStore->GetCreatureData(creatureCaster->GetGUID());
|
||||
|
||||
if (!creatureCaster) {
|
||||
MpLogger::error("Creature caster is null in map {}", attacker ? attacker->GetMap()->GetId() : 0);
|
||||
return;
|
||||
}
|
||||
|
||||
// Only scale spells for creatures that Mythic+ tracks (instances or world-boss encounters)
|
||||
Map* casterMap = creatureCaster->GetMap();
|
||||
if (!casterMap || !sMythicPlus->IsMapEligible(casterMap) || !sMythicPlus->IsCreatureEligible(creatureCaster)) {
|
||||
return;
|
||||
}
|
||||
|
||||
MpCreatureData* creatureData = sMpDataStore->GetCreatureData(creatureCaster->GetGUID());
|
||||
if (!creatureData) {
|
||||
MpLogger::error("Failed to get creature data for {} in map {}", creatureCaster->GetName(), attacker ? attacker->GetMap()->GetId() : 0);
|
||||
return;
|
||||
|
||||
@@ -7,6 +7,8 @@
|
||||
#include "ScriptMgr.h"
|
||||
#include "MpEventHandlers.cpp"
|
||||
|
||||
#include <sstream>
|
||||
|
||||
class MythicPlus_WorldScript : public WorldScript
|
||||
{
|
||||
public:
|
||||
@@ -100,6 +102,46 @@ public:
|
||||
sMythicPlus->elementalMeleeReducer = sConfigMgr->GetOption<float>("MythicPlus.ElementalMeleeReducer", 0.50f);
|
||||
sMythicPlus->normalEnemyReducer = sConfigMgr->GetOption<float>("MythicPlus.NormalEnemyReducer", 0.50f);
|
||||
sMythicPlus->nonCreatureSpellReducer = sConfigMgr->GetOption<float>("MythicPlus.NonCreatureSpellReducer", 0.50f);
|
||||
|
||||
// World Boss Settings
|
||||
sMythicPlus->EnableWorldBoss = sConfigMgr->GetOption<bool>("MythicPlus.WorldBoss.Enabled", true);
|
||||
sMythicPlus->worldBossProximityRange = sConfigMgr->GetOption<uint32>("MythicPlus.WorldBoss.ProximityRange", 100);
|
||||
|
||||
// Parse enabled zones (comma-separated list)
|
||||
std::string zonesStr = sConfigMgr->GetOption<std::string>("MythicPlus.WorldBoss.EnabledZones", "");
|
||||
if (!zonesStr.empty()) {
|
||||
std::istringstream zoneStream(zonesStr);
|
||||
std::string zone;
|
||||
while (std::getline(zoneStream, zone, ',')) {
|
||||
// Trim whitespace
|
||||
zone.erase(0, zone.find_first_not_of(" \t"));
|
||||
zone.erase(zone.find_last_not_of(" \t") + 1);
|
||||
if (!zone.empty()) {
|
||||
sMythicPlus->enabledWorldBossZones.push_back(std::stoul(zone));
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
// Parse enabled bosses (comma-separated list)
|
||||
std::string bossesStr = sConfigMgr->GetOption<std::string>("MythicPlus.WorldBoss.EnabledBosses", "");
|
||||
if (!bossesStr.empty()) {
|
||||
std::istringstream bossStream(bossesStr);
|
||||
std::string boss;
|
||||
while (std::getline(bossStream, boss, ',')) {
|
||||
// Trim whitespace
|
||||
boss.erase(0, boss.find_first_not_of(" \t"));
|
||||
boss.erase(boss.find_last_not_of(" \t") + 1);
|
||||
if (!boss.empty()) {
|
||||
sMythicPlus->enabledWorldBosses.push_back(std::stoul(boss));
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
if (sMythicPlus->EnableWorldBoss) {
|
||||
MpLogger::info("World Boss scaling enabled for {} zones and {} specific bosses",
|
||||
sMythicPlus->enabledWorldBossZones.size(),
|
||||
sMythicPlus->enabledWorldBosses.size());
|
||||
}
|
||||
}
|
||||
|
||||
void OnStartup() override
|
||||
@@ -107,6 +149,12 @@ public:
|
||||
int32 size = sMpDataStore->LoadScaleFactors();
|
||||
MpLogger::info("Loaded {} Mythic+ Scaling Factors from database...", size);
|
||||
|
||||
// Load world boss configuration if enabled
|
||||
if (sMythicPlus->EnableWorldBoss) {
|
||||
size = sMpDataStore->LoadWorldBossConfig();
|
||||
MpLogger::info("Loaded {} World Boss configurations from database...", size);
|
||||
}
|
||||
|
||||
size = sAdvancementMgr->LoadAdvancementRanks();
|
||||
MpLogger::info("Loaded {} advancement ranks...", size);
|
||||
|
||||
|
||||
Reference in New Issue
Block a user