From 50a5d5e451d67cf5eb48a1d55bb1acec5aa5ef4a Mon Sep 17 00:00:00 2001 From: KJack Date: Thu, 19 Oct 2023 11:52:30 -0400 Subject: [PATCH] World damage/healing, destructible buildings, multiplier rewrite, logging improvements (#157) * Only skip critters that are flavor-only (<= level 5) * Refactor to break out some logic into separate functions. * Refactor more repeated operations into separate functions - Add worldHealthMultiplier, intended to be used to scale the health of attackable game objects in the instance * Continued refinement of non-creature-based damage and healing * Rework damage handling, improve coverage, debug logging - tear out and re-implement all the damage handling to ensure more cases are handled - use the map's damage modifier when damage happens where the source isn't a creature - better handle when the source of damage no longer exists (logged out or despawned) - extensive (optional) logging under the `module.AutoBalance.Damage` logging prefix - WIP implementattion of game object damage handling (destructible buildings) - fix a long-standing error in the class name for some logging * World damage scaling is working, WIP - need to apply statmodifiers still - improved logging * - many, many logging improvements - fix a bug with the enable check case statements (stupid case breaks) - remove antiquated battleground checks - should never scale in battlegrounds - better handle "special" creatures (totems, triggers, critters) - allow combat critters to scale correctly - allow intentionally-low-level triggers and critters to remain low level - workaround an issue with summons showing the wrong level client-side (level change ramp up) - further improve GM and user commands - erase map AB info from maps when they are created, preventing issues with instance ID re-use - better handle instances with no non-GM characters (disable scaling temporarily) * Further work on summoned creatures, still WIP - removed unnecessary `std::string`s * WIP * Rewrite multiplier calculation, reduce required processing - majory re-wrote the multiplicer calculations to increase accuracy and clarity - take advantage of new `OnBeforeCreatureSelectLevel` hook to set the initial level of creatures before they are added to the world - separated level-scaled and non-level-scaled multipliers for tracking and display - move creature relevancy check to new function `isCreatureRelevant` - rename many variables to standardize and clarify - add option to force a map stats update, to be used when a player enters the map - players leaving the map are no longer included in the map's stats - sweeping improvements in logging of the multiplier calculation process (`AutoBalance.StatGeneration`) - update `.ab creaturestat` to show new scaled modifiers in an understandable way * Optimize map updates to only occur when necessary. WIP. * Create documentation for Info classes, add player lists * Reworking config tracking, WIP * Rewrite player tracking to remove kludges, many improvements - replace kludgy player count detection with an internal player list (more features coming Soon) - skip scaling on player-owned summons, totems, etc - skip scaling on flavor critters but not on combat critters - separate map and global config times, reducing the work to refresh if only a map config changes - centralize detection of "non-relevant" creatures, use a `skipMe` tag to speed up processing - correctly handle a GM leaving the instance who was previously a non-GM when they entered - temporarily disable in-combat checking - improve logging format to produce the ID needed for `.go creature ` - standardize logging format * Performance improvements, fix statmodifier selection, logging - moved enums to top the file, added `Relevance` enum - improve performance of `isCreatureRelevant` but saving the decision from previous runs - fix issues with summon detection to (hopefully) cover all edge cases - consolidate and fix StatModifier case selection, add debugs - more logging improvements all around * Recount players on a config reload. * Improve player enter/leave behavior. * Improve handling of empty instances, properly scale trigger creatures, improve `.ab mapstat` * Check that the target for `.ab creaturestat` is in a dungeon. * Move boss detection to separate function - boss summons now also count as a "boss" * Do not modify spells that hurt the player but are intended to - Dark Runes, etc - logging improvements * Fix world multipliers not getting reloaded. Logging improvements. * Additional debug for boss detection * Fix percent-based damage auras, map multipliers honor level scaling - percent-based damage auras will now ignore level scaling (per-player scaling only) - map multipliers (damage/healing and health) will now honor the level scaling setting - use a custom struct to store and move around world multipliers - properly remove LevelScalingEndGameBoost until it can be fixed (#156) - creatures summoned by a boss will now be scaled like bosses - further refinement of trigger handling logic - fix active creature not being decremented on creature removal - fix incorrect announcing of GMs entering/exiting the instance - improvements for in-game commands - continued log improvements * World multipliers now honor stat modifiers - logging improvements * Re-enable the processing of map stats when the instance is empty * Combat locking: WIP * Combat locking - better implementation. WIP, needs notifications. * Re-implement combat locking * Complete migration from skipMe to isCreatureRelevant * Code and log cleanup * Handle a combatLockMinPlayers of 0 * Update bug report template to request AB-specific information * Update README.md with commands, loggers, and min AC version * Add logging settings to .conf.dist file. Small logging change. * Replace uint with uint8. * Rename `GetCurrentTime()` and remove case ranges to make Windows happy * Code cleanup. Replace case statements. Remove `entry`, which is unneeded. * (Hopefully) final code cleanup, comment refinement * Fix enemy totems not level scaling. Update logging statement. * Resolve issue with heroic dungeons that don't have LFG levels defined * Fix dungeon difficulty normal, add error logging if no LFG found * Fix incorrect heroic settings. - fix display issue with active creatures * Fix syntax error. - that'll teach me to commit before I build * Handle cloned summons, improve creaturestat command - track summoner as a part of AutoBalanceCreatureInfo - detect when a summon is a clone and use the correct current health instead of scaling again - add summon information to `.ab creaturestat` - as always, logging improvements * Handle shared damage auras, add never modify spells - spells with SPELL_AURA_SHARE_DAMAGE_PCT effects will now correctly be unmodified - added `spellIdsToNeverModify` and correctly skip modification of them - add "Twin Empathy" (1177) to the `spellIdsToNeverModify` list - convert `_IsAuraPercentDamage` to `_isAuraWithEffectType` since this probably isn't the last I'll need it - logging changes, naturally * Move initial scaling from first OnAllCreatureUpdate to Creature_SelectLevel * Tell user to move logging settings to `worldserver.conf`, since they don't work well in module config * Fix combat locking. * Small update to spellIdsToNeverModify * Move warning to debug. --- .github/ISSUE_TEMPLATE/bug_report.yml | 10 + README.md | 25 +- conf/AutoBalance.conf.dist | 27 +- src/AutoBalance.cpp | 6219 +++++++++++++++++++------ 4 files changed, 4827 insertions(+), 1454 deletions(-) diff --git a/.github/ISSUE_TEMPLATE/bug_report.yml b/.github/ISSUE_TEMPLATE/bug_report.yml index 5610d2b..d08667e 100644 --- a/.github/ISSUE_TEMPLATE/bug_report.yml +++ b/.github/ISSUE_TEMPLATE/bug_report.yml @@ -43,6 +43,16 @@ body: None validations: required: false + - type: textarea + id: abcommands + attributes: + label: AutoBalance Debug Commands + description: | + Please include text or an image of the `.ab mapstat` command. If your issue is concerning scaling of creatures, please also include the `.ab creaturestat` command while targeting the problematic creature. + placeholder: | + None + validations: + required: false - type: textarea id: commit attributes: diff --git a/README.md b/README.md index b4f520f..49bcc60 100644 --- a/README.md +++ b/README.md @@ -1,11 +1,34 @@ # ![logo](https://raw.githubusercontent.com/azerothcore/azerothcore.github.io/master/images/logo-github.png) AzerothCore -## AutoBalanceModule + +## AutoBalance + - Latest build status with azerothcore: [![Build Status](https://github.com/azerothcore/mod-autobalance/workflows/core-build/badge.svg?branch=master&event=push)](https://github.com/azerothcore/mod-autobalance) This module is intended to scale based on number of players, instance mobs and bosses' health, mana, and damage. +**NOTE:** This module requires at least [this commit](https://github.com/azerothcore/azerothcore-wotlk/commit/f127e583aae3cfa51a77d056c1892a7de07ffb52) of AzerothCore in order to work correctly. Older versions are not supported. + All settings are well-described in the configuration file. +**PLEASE** include the output from the `.ab mapstat` and `.ab creaturestat` commands (while targeting a problematic creature) when reporting issues. This will help us to quickly identify the problem and provide a solution. + +## In-game Commands +| Command | Permission | Description | +| :------ | :--------- | :---------- | +| `.ab mapstat` | All Players | Displays AB-calcualted settings for the current map, including player count, difficulty, world modifiers, and others. | +| `.ab creaturestat` | All Players | Displays AB-calculated settings for the targeted dungeon creature including level scaling, difficulty, modifiers, and boss status. | +| `.ab setoffset` | Game Masters | Sets the server-wide player difficulty offset. Instances will be scaled as though they had this many more/less players than they really do. | +| `.ab getoffset` | All Players | Gets the current server-wide player difficulty offset. Instances will be scaled as though they had this many more/less players than they really do. | +| `.reload config` | Game Masters | Reloads all your configuration files, including `AutoBalance.conf`. This lets you update AutoBalance settings without restarting your worldserver. This module is designed to contiue to work as expected when this command is issued. | + +## Logger Names +| Logger | Description | +| :----- | ----------- | +| `Logger.module.AutoBalance` | Main logger, verbose debug logs. Map detection, list management, creature adjustments, multiplier, modifiers. Catch-all. | +| `Logger.module.AutoBalance_CombatLocking` | Debug logs related to the combat locking/unlocking mechanism for maps. | +| `Logger.module.AutoBalance_DamageHealingCC` | Debug logs for the spell/melee/CC modifications that are made in real-time. | +| `Logger.module.AutoBalance_StatGeneration` | Detailed debug logs that show all the calculation steps in how different multipliers are derived. | + ## References - [Interactive Inflection Point Spreadsheet](https://docs.google.com/spreadsheets/d/100cmKIJIjCZ-ncWd0K9ykO8KUgwFTcwg4h2nfE_UeCc/copy) - [InflectionPoint Curve Examples](https://i.imgur.com/x42UnUR.png) diff --git a/conf/AutoBalance.conf.dist b/conf/AutoBalance.conf.dist index 318ebea..c40167e 100644 --- a/conf/AutoBalance.conf.dist +++ b/conf/AutoBalance.conf.dist @@ -1,4 +1,18 @@ [worldserver] +########################## +# +# Logging +# +# Add these lines to your worldserver.conf file to enable logging for AutoBalance. +# +# 4 = Info (Default), 5 = Debug +# +########################## +# Logger.module.AutoBalance=4,Console Server +# Logger.module.AutoBalance_CombatLocking=4,Console Server +# Logger.module.AutoBalance_DamageHealingCC=4,Console Server +# Logger.module.AutoBalance_StatGeneration=4,Console Server + ########################## # # Enable / Disable Settings @@ -300,6 +314,8 @@ AutoBalance.playerCountDifficultyOffset=0 # Health | Mana | Armor | Damage # Adjusts the StatModifier for the appropriate stat. Affected by the Global StatModifier above. # +# NOTE: "Damage" affects both creature damage and world damage. +# # Default: 1.0 # # Boss.Global | Boss.Health | Boss.Mana | Boss.Armor | Boss.Damage @@ -807,12 +823,19 @@ AutoBalance.LevelScaling.DynamicLevel.DistanceCheck.PerInstance="189 500" # # AutoBalance.LevelScaling.LevelEndGameBoost +# +# NOTICE: This setting is currently not implemented pending a rewrite. +# Enabling it here has NO effect. +# +# See: https://github.com/azerothcore/mod-autobalance/issues/156 +# +# Old description: # End game creatures have an exponential (not linear) regression # that is not correctly handled by db values. Keep this enabled # to have stats as near possible to the official ones. # -# Default: 1 (1 = ON, 0 = OFF) -AutoBalance.LevelScaling.EndGameBoost = 1 +# Default: 0 (1 = ON, 0 = OFF) +AutoBalance.LevelScaling.EndGameBoost = 0 # setting to 1 does not do anything ########################## # diff --git a/src/AutoBalance.cpp b/src/AutoBalance.cpp index 7f463da..39258b8 100644 --- a/src/AutoBalance.cpp +++ b/src/AutoBalance.cpp @@ -44,6 +44,7 @@ #include "ScriptMgrMacros.h" #include "Group.h" #include "Log.h" +#include "SharedDefines.h" #include #if AC_COMPILER == AC_COMPILER_GNU @@ -52,6 +53,32 @@ using namespace Acore::ChatCommands; +enum ScalingMethod { + AUTOBALANCE_SCALING_FIXED, + AUTOBALANCE_SCALING_DYNAMIC +}; + +enum BaseValueType { + AUTOBALANCE_HEALTH, + AUTOBALANCE_DAMAGE_HEALING +}; + +enum Relevance { + AUTOBALANCE_RELEVANCE_FALSE, + AUTOBALANCE_RELEVANCE_TRUE, + AUTOBALANCE_RELEVANCE_UNCHECKED +}; + +enum Damage_Healing_Debug_Phase { + AUTOBALANCE_DAMAGE_HEALING_DEBUG_PHASE_BEFORE, + AUTOBALANCE_DAMAGE_HEALING_DEBUG_PHASE_AFTER +}; + +struct World_Multipliers { + float scaled = 1.0f; + float unscaled = 1.0f; +}; + ABScriptMgr* ABScriptMgr::instance() { static ABScriptMgr instance; @@ -109,32 +136,46 @@ ABModuleScript::ABModuleScript(const char* name) ScriptRegistry::AddScript(this); } - class AutoBalanceCreatureInfo : public DataMap::Base { public: AutoBalanceCreatureInfo() {} - uint64_t configTime; + uint64_t mapConfigTime = 1; // the last map config time that this creature was updated + + uint32 instancePlayerCount = 0; // the number of players this creature has been scaled for + uint8 selectedLevel = 0; // the level that this creature should be set to + + float DamageMultiplier = 1.0f; // per-player damage multiplier (no level scaling) + float ScaledDamageMultiplier = 1.0f; // per-player and level scaling damage multiplier - uint32 instancePlayerCount = 0; - uint8 selectedLevel = 0; - // this is used to detect creatures that update their entry - uint32 entry = 0; - float DamageMultiplier = 1.0f; - float HealthMultiplier = 1.0f; - float ManaMultiplier = 1.0f; - float ArmorMultiplier = 1.0f; - float CCDurationMultiplier = 1.0f; + float HealthMultiplier = 1.0f; // per-player health multiplier (no level scaling) + float ScaledHealthMultiplier = 1.0f; // per-player and level scaling health multiplier - float XPModifier = 1.0f; - float MoneyModifier = 1.0f; + float ManaMultiplier = 1.0f; // per-player mana multiplier (no level scaling) + float ScaledManaMultiplier = 1.0f; // per-player and level scaling mana multiplier - uint8 UnmodifiedLevel = 0; + float ArmorMultiplier = 1.0f; // per-player armor multiplier (no level scaling) + float ScaledArmorMultiplier = 1.0f; // per-player and level scaling armor multiplier - bool isActive = false; - bool wasAliveNowDead = false; - bool isInCreatureList = false; + float CCDurationMultiplier = 1.0f; // per-player crowd control duration multiplier (level scaling doesn't affect this) + + float XPModifier = 1.0f; // per-player XP modifier (level scaling provided by normal XP distribution) + float MoneyModifier = 1.0f; // per-player money modifier (no level scaling) + + uint8 UnmodifiedLevel = 0; // original level of the creature as determined by the game + + bool isActive = false; // whether or not the current creature is affecting map stats. May change as conditions change. + bool wasAliveNowDead = false; // whether or not the creature was alive and is now dead + bool isInCreatureList = false; // whether or not the creature is in the map's creature list + bool isBrandNew = false; // whether or not the creature is brand new to the map (hasn't been added to the world yet) + bool neverLevelScale = false; // whether or not the creature should never be level scaled (can still be player scaled) + + // creature->IsSummon() // whether or not the creature is a summon + Creature* summoner = nullptr; // the creature that summoned this creature + bool isCloneOfSummoner = false; // whether or not the creature is a clone of its summoner + + Relevance relevance = AUTOBALANCE_RELEVANCE_UNCHECKED; // whether or not the creature is relevant for scaling }; class AutoBalanceMapInfo : public DataMap::Base @@ -142,31 +183,47 @@ class AutoBalanceMapInfo : public DataMap::Base public: AutoBalanceMapInfo() {} - uint64_t configTime; + uint64_t globalConfigTime = 1; // the last global config time that this map was updated + uint64_t mapConfigTime = 1; // the last map config time that this map was updated - uint32 playerCount = 0; - uint32 adjustedPlayerCount = 0; - uint32 minPlayers = 1; + uint8 playerCount = 0; // the actual number of non-GM players in the map + uint8 adjustedPlayerCount = 0; // the currently difficulty level expressed as number of players + uint8 minPlayers = 1; // will be set by the config - uint8 mapLevel = 0; - uint8 lowestPlayerLevel = 0; - uint8 highestPlayerLevel = 0; + uint8 mapLevel = 0; // calculated from the avgCreatureLevel + uint8 lowestPlayerLevel = 0; // the lowest-level player in the map + uint8 highestPlayerLevel = 0; // the highest-level player in the map + + uint8 lfgMinLevel = 0; // the minimum level for the map according to LFG + uint8 lfgTargetLevel = 0; // the target level for the map according to LFG + uint8 lfgMaxLevel = 0; // the maximum level for the map according to LFG + + uint8 worldMultiplierTargetLevel = 0; // the level of the pseudo-creature that the world modifiers scale to + float worldDamageHealingMultiplier = 1.0f; // the damage/healing multiplier for the world (where source isn't an enemy creature) + float scaledWorldDamageHealingMultiplier = 1.0f; // the damage/healing multiplier for the world (where source isn't an enemy creature) + float worldHealthMultiplier = 1.0f; // the "health" multiplier for any destructible buildings in the map - uint8 lfgMinLevel = 0; - uint8 lfgTargetLevel = 80; - uint8 lfgMaxLevel = 80; + bool enabled = false; // should AutoBalance make any changes to this map or its creatures? - bool enabled = false; + std::vector allMapCreatures; // all creatures in the map, active and non-active + std::vector allMapPlayers; // all players that are currently in the map - std::vector allMapCreatures; - uint8 highestCreatureLevel = 0; - uint8 lowestCreatureLevel = 0; - float avgCreatureLevel; - uint32 activeCreatureCount = 0; + bool combatLocked = false; // whether or not the map is combat locked + bool combatLockTripped = false; // set to true when combat locking was needed during this current combat (some tried to leave) + uint8 combatLockMinPlayers = 0; // the instance cannot be set to less than this number of players until combat ends - bool isLevelScalingEnabled; - int levelScalingSkipHigherLevels, levelScalingSkipLowerLevels; - int levelScalingDynamicCeiling, levelScalingDynamicFloor; + uint8 highestCreatureLevel = 0; // the highest-level creature in the map + uint8 lowestCreatureLevel = 0; // the lowest-level creature in the map + float avgCreatureLevel = 0; // the average level of all active creatures in the map (continuously updated) + uint32 activeCreatureCount = 0; // the number of creatures in the map that are included in the map's stats (not necessarily alive) + + bool isLevelScalingEnabled = false; // whether level scaling is enabled on this map + uint8 levelScalingSkipHigherLevels; // used to determine if this map should scale or not + uint8 levelScalingSkipLowerLevels; // used to determine if this map should scale or not + uint8 levelScalingDynamicCeiling; // how many levels MORE than the highestPlayerLevel creature should be scaled to + uint8 levelScalingDynamicFloor; // how many levels LESS than the highestPlayerLevel creature should be scaled to + + uint8 prevMapLevel = 0; // used to reduce calculations when they are not necessary }; class AutoBalanceStatModifiers : public DataMap::Base @@ -181,8 +238,6 @@ class AutoBalanceStatModifiers : public DataMap::Base float armor; float damage; float ccduration; - - std::time_t configTime; }; class AutoBalanceInflectionPointSettings : public DataMap::Base @@ -208,12 +263,49 @@ class AutoBalanceLevelScalingDynamicLevelSettings: public DataMap::Base int floor; }; -enum ScalingMethod { - AUTOBALANCE_SCALING_FIXED, - AUTOBALANCE_SCALING_DYNAMIC +uint64_t GetCurrentConfigTime() +{ + return std::chrono::duration_cast(std::chrono::system_clock::now().time_since_epoch()).count(); +} + +// spell IDs that spend player health +// player abilities don't actually appear to be caught by `ModifySpellDamageTaken`, +// but I'm leaving them here in case they ever DO get caught by it +static std::list spellIdsThatSpendPlayerHealth = +{ + 45529, // Blood Tap + 2687, // Bloodrage + 27869, // Dark Rune + 16666, // Demonic Rune + 755, // Health Funnel (Rank 1) + 3698, // Health Funnel (Rank 2) + 3699, // Health Funnel (Rank 3) + 3700, // Health Funnel (Rank 4) + 11693, // Health Funnel (Rank 5) + 11694, // Health Funnel (Rank 6) + 11695, // Health Funnel (Rank 7) + 27259, // Health Funnel (Rank 8) + 47856, // Health Funnel (Rank 9) + 1454, // Life Tap (Rank 1) + 1455, // Life Tap (Rank 2) + 1456, // Life Tap (Rank 3) + 11687, // Life Tap (Rank 4) + 11688, // Life Tap (Rank 5) + 11689, // Life Tap (Rank 6) + 27222, // Life Tap (Rank 7) + 57946, // Life Tap (Rank 8) + 29858, // Soulshatter + 55213 // Unholy Frenzy +}; + +static std::list spellIdsToNeverModify = +{ + 1177, // Twin Empathy (AQ40 Twin Emperors, only in `spell_dbc`) }; -// The map values correspond with the .AutoBalance.XX.Name entries in the configuration file. +// spacer used for logging +std::string SPACER = "------------------------------------------------"; + static std::map forcedCreatureIds; static std::list disabledDungeonIds; @@ -228,8 +320,7 @@ static std::map statModifierBossOverrides; static std::map statModifierCreatureOverrides; static std::map levelScalingDynamicLevelOverrides; static std::map levelScalingDistanceCheckOverrides; -// cheaphack for difficulty server-wide. -// Another value TODO in player class for the party leader's value to determine dungeon difficulty. + static int8 PlayerCountDifficultyOffset; static bool LevelScaling; static int8 LevelScalingSkipHigherLevels, LevelScalingSkipLowerLevels; @@ -246,8 +337,8 @@ static ScalingMethod RewardScalingMethod; static bool RewardScalingXP, RewardScalingMoney; static float RewardScalingXPModifier, RewardScalingMoneyModifier; -// Track the last time the config was reloaded -static uint64_t lastConfigTime = std::chrono::duration_cast(std::chrono::system_clock::now().time_since_epoch()).count(); +// Track the initial config time +static uint64_t globalConfigTime = GetCurrentConfigTime(); // Enable.* static bool EnableGlobal; @@ -460,7 +551,6 @@ std::map LoadDistanceCheckOverrides(std::string dungeonIdString) return overrideMap; } - bool isDungeonInDisabledDungeonIds(uint32 dungeonId) { return (std::find(disabledDungeonIds.begin(), disabledDungeonIds.end(), dungeonId) != disabledDungeonIds.end()); @@ -475,7 +565,6 @@ bool isDungeonInMinPlayerMap(uint32 dungeonId, bool isHeroic) } } - bool hasDungeonOverride(uint32 dungeonId) { return (dungeonOverrides.find(dungeonId) != dungeonOverrides.end()); @@ -513,430 +602,1861 @@ bool hasLevelScalingDistanceCheckOverride(uint32 dungeonId) bool ShouldMapBeEnabled(Map* map) { - if (map->IsDungeon() || map->IsRaid()) + if (map->IsDungeon()) { + // get the map's info + AutoBalanceMapInfo *mapABInfo = map->CustomData.GetDefault("AutoBalanceMapInfo"); + // if globally disabled, return false if (!EnableGlobal) { + LOG_DEBUG("module.AutoBalance", "AutoBalance::ShouldMapBeEnabled: {} ({}{}) - Not enabled because EnableGlobal is false", + map->GetMapName(), + map->GetId(), + map->GetInstanceId() ? "-" + std::to_string(map->GetInstanceId()) : "" + ); return false; } - // get the current instance map - auto instanceMap = ((InstanceMap*)sMapMgr->FindMap(map->GetId(), map->GetInstanceId())); + InstanceMap* instanceMap = map->ToInstanceMap(); // if there wasn't one, then we're not in an instance if (!instanceMap) { + LOG_DEBUG("module.AutoBalance", "AutoBalance::ShouldMapBeEnabled: {} ({}{}) - Not enabled for the base map without an Instance ID.", + map->GetMapName(), + map->GetId(), + map->GetInstanceId() ? "-" + std::to_string(map->GetInstanceId()) : "" + ); return false; } - // get the max player count for the instance - auto maxPlayerCount = instanceMap->GetMaxPlayers(); - // if the player count is less than 1, then we're not in an instance - if (maxPlayerCount < 1) + if (instanceMap->GetMaxPlayers() < 1) + { + LOG_DEBUG("module.AutoBalance", "AutoBalance::ShouldMapBeEnabled: {} ({}{}, {}-player {}) - Not enabled because GetMaxPlayers < 1", + map->GetMapName(), + map->GetId(), + map->GetInstanceId() ? "-" + std::to_string(map->GetInstanceId()) : "", + instanceMap->GetMaxPlayers(), + instanceMap->IsHeroic() ? "Heroic" : "Normal" + ); + return false; + } + + // if the map has no players in the player list, then disabled + if (!mapABInfo->playerCount) { + LOG_DEBUG("module.AutoBalance", "AutoBalance::ShouldMapBeEnabled: {} ({}{}, {}-player {}) - Not enabled because there are no non-GM players in the map.", + map->GetMapName(), + map->GetId(), + map->GetInstanceId() ? "-" + std::to_string(map->GetInstanceId()) : "", + instanceMap->GetMaxPlayers(), + instanceMap->IsHeroic() ? "Heroic" : "Normal" + ); + return false; } // if the Dungeon is disabled via configuration, do not enable it if (isDungeonInDisabledDungeonIds(map->GetId())) { + LOG_DEBUG("module.AutoBalance", "AutoBalance::ShouldMapBeEnabled: {} ({}{}, {}-player {}) - Not enabled because the map ID is disabled via configuration.", + map->GetMapName(), + map->GetId(), + map->GetInstanceId() ? "-" + std::to_string(map->GetInstanceId()) : "", + instanceMap->GetMaxPlayers(), + instanceMap->IsHeroic() ? "Heroic" : "Normal" + ); + return false; } // use the configuration variables to determine if this instance type/size should have scaling enabled + bool sizeDifficultyEnabled; if (instanceMap->IsHeroic()) { - switch (maxPlayerCount) + //LOG_DEBUG("module.AutoBalance", "AutoBalance::ShouldMapBeEnabled: Heroic Enables - 5:{} 10:{} 25:{} Other:{}", + // Enable5MHeroic, Enable10MHeroic, Enable25MHeroic, EnableOtherHeroic); + + if (instanceMap->GetMaxPlayers() <= 5) + { + sizeDifficultyEnabled = Enable5MHeroic; + } + else if (instanceMap->GetMaxPlayers() <= 10) + { + sizeDifficultyEnabled = Enable10MHeroic; + } + else if (instanceMap->GetMaxPlayers() <= 25) + { + sizeDifficultyEnabled = Enable25MHeroic; + } + else { - case 5: - return Enable5MHeroic; - case 10: - return Enable10MHeroic; - case 25: - return Enable25MHeroic; - default: - return EnableOtherHeroic; + sizeDifficultyEnabled = EnableOtherHeroic; } } else { - switch (maxPlayerCount) + //LOG_DEBUG("module.AutoBalance", "AutoBalance::ShouldMapBeEnabled: Normal Enables - 5:{} 10:{} 15:{} 20:{} 25:{} 40:{} Other:{}", + // Enable5M, Enable10M, Enable15M, Enable20M, Enable25M, Enable40M, EnableOtherNormal); + if (instanceMap->GetMaxPlayers() <= 5) + { + sizeDifficultyEnabled = Enable5M; + } + else if (instanceMap->GetMaxPlayers() <= 10) + { + sizeDifficultyEnabled = Enable10M; + } + else if (instanceMap->GetMaxPlayers() <= 15) + { + sizeDifficultyEnabled = Enable15M; + } + else if (instanceMap->GetMaxPlayers() <= 20) + { + sizeDifficultyEnabled = Enable20M; + } + else if (instanceMap->GetMaxPlayers() <= 25) + { + sizeDifficultyEnabled = Enable25M; + } + else if (instanceMap->GetMaxPlayers() <= 40) + { + sizeDifficultyEnabled = Enable40M; + } + else { - case 5: - return Enable5M; - case 10: - return Enable10M; - case 15: - return Enable15M; - case 20: - return Enable20M; - case 25: - return Enable25M; - case 40: - return Enable40M; - default: - return EnableOtherNormal; + sizeDifficultyEnabled = EnableOtherNormal; } } + + if (sizeDifficultyEnabled) + { + LOG_DEBUG("module.AutoBalance", "AutoBalance::ShouldMapBeEnabled: Map {} ({}{}, {}-player {}) | Enabled for AutoBalancing.", + map->GetMapName(), + map->GetId(), + map->GetInstanceId() ? "-" + std::to_string(map->GetInstanceId()) : "", + instanceMap->GetMaxPlayers(), + instanceMap->IsHeroic() ? "Heroic" : "Normal" + ); + } + else + { + LOG_DEBUG("module.AutoBalance", "AutoBalance::ShouldMapBeEnabled: Map {} ({}{}, {}-player {}) | Not enabled because its size and difficulty are disabled via configuration.", + map->GetMapName(), + map->GetId(), + map->GetInstanceId() ? "-" + std::to_string(map->GetInstanceId()) : "", + instanceMap->GetMaxPlayers(), + instanceMap->IsHeroic() ? "Heroic" : "Normal" + ); + } + + return sizeDifficultyEnabled; } else { + LOG_DEBUG("module.AutoBalance", "AutoBalance::ShouldMapBeEnabled: Map {} ({}{}) | Not enabled because the map is not an instance.", + map->GetMapName(), + map->GetId(), + map->GetInstanceId() ? "-" + std::to_string(map->GetInstanceId()) : "" + ); + return false; + // we're not in a dungeon or a raid, we never scale return false; } } -void LoadMapSettings(Map* map) +float getBaseExpansionValueForLevel(const float baseValues[3], uint8 targetLevel) { - // Load (or create) the map's info - AutoBalanceMapInfo *mapABInfo=map->CustomData.GetDefault("AutoBalanceMapInfo"); - - // create an InstanceMap object - InstanceMap* instanceMap = map->ToInstanceMap(); - - //check for null pointer - if (!map) - { - return; - } - - if (!map->IsDungeon() && !map->IsRaid()) - { - return; - } + // the database holds multiple base values depending on the expansion + // this function returns the correct base value for the given level and + // smooths the transition between expansions - LOG_DEBUG("module.AutoBalance", "LoadMapSettings: Loading settings for map {}.", map->GetMapName()); + float vanillaValue = baseValues[0]; + float bcValue = baseValues[1]; + float wotlkValue = baseValues[2]; - // determine the minumum player count - if (isDungeonInMinPlayerMap(map->GetId(), instanceMap->IsHeroic())) - { - mapABInfo->minPlayers = instanceMap->IsHeroic() ? minPlayersPerHeroicDungeonIdMap[map->GetId()] : minPlayersPerDungeonIdMap[map->GetId()]; - } - else if (instanceMap->IsHeroic()) - { - mapABInfo->minPlayers = minPlayersHeroic; - } - else - { - mapABInfo->minPlayers = minPlayersNormal; - } + float returnValue; - // if the minPlayers value we determined is less than the max number of players in this map, adjust down - if (mapABInfo->minPlayers > instanceMap->GetMaxPlayers()) + // vanilla + if (targetLevel <= 60) { - LOG_DEBUG("module.AutoBalance", "LoadMapSettings: Your settings tried to set a minimum player count of {} which is greater than {}'s max player count of {}. Adjusting down.", - mapABInfo->minPlayers, - map->GetMapName(), - instanceMap->GetMaxPlayers() - ); - - mapABInfo->minPlayers = instanceMap->GetMaxPlayers(); + returnValue = vanillaValue; + //LOG_DEBUG("module.AutoBalance", "AutoBalance::getBaseExpansionValueForLevel: Returning Vanilla = {}", returnValue); } - - LOG_DEBUG("module.AutoBalance", "LoadMapSettings: Map {} has a minimum player count of {}.", map->GetMapName(), mapABInfo->minPlayers); - - // - // Dynamic Level Scaling Floor and Ceiling - // - - // 5-player normal dungeons - if (instanceMap->GetMaxPlayers() <= 5 && !instanceMap->IsHeroic()) + // transition from vanilla to BC + else if (targetLevel < 63) { - mapABInfo->levelScalingDynamicCeiling = LevelScalingDynamicLevelCeilingDungeons; - mapABInfo->levelScalingDynamicFloor = LevelScalingDynamicLevelFloorDungeons; + float vanillaMultiplier = (63 - targetLevel) / 3.0; + float bcMultiplier = 1.0f - vanillaMultiplier; + returnValue = (vanillaValue * vanillaMultiplier) + (bcValue * bcMultiplier); + //LOG_DEBUG("module.AutoBalance", "AutoBalance::getBaseExpansionValueForLevel: Returning Vanilla/BC = {}", returnValue); } - // 5-player heroic dungeons - else if (instanceMap->GetMaxPlayers() <= 5 && instanceMap->IsHeroic()) - { - mapABInfo->levelScalingDynamicCeiling = LevelScalingDynamicLevelCeilingHeroicDungeons; - mapABInfo->levelScalingDynamicFloor = LevelScalingDynamicLevelFloorHeroicDungeons; - } - // Normal raids - else if (instanceMap->GetMaxPlayers() > 5 && !instanceMap->IsHeroic()) + // BC + else if (targetLevel <= 70) { - mapABInfo->levelScalingDynamicCeiling = LevelScalingDynamicLevelCeilingRaids; - mapABInfo->levelScalingDynamicFloor = LevelScalingDynamicLevelFloorRaids; + returnValue = bcValue; + //LOG_DEBUG("module.AutoBalance", "AutoBalance::getBaseExpansionValueForLevel: Returning BC = {}", returnValue); } - // Heroic raids - else if (instanceMap->GetMaxPlayers() > 5 && instanceMap->IsHeroic()) + // transition from BC to WotLK + else if (targetLevel < 73) { - mapABInfo->levelScalingDynamicCeiling = LevelScalingDynamicLevelCeilingHeroicRaids; - mapABInfo->levelScalingDynamicFloor = LevelScalingDynamicLevelFloorHeroicRaids; + float bcMultiplier = (73 - targetLevel) / 3.0f; + float wotlkMultiplier = 1.0f - bcMultiplier; + + returnValue = (bcValue * bcMultiplier) + (wotlkValue * wotlkMultiplier); + //LOG_DEBUG("module.AutoBalance", "AutoBalance::getBaseExpansionValueForLevel: Returning BC/WotLK = {}", returnValue); } - // something went wrong + // WotLK else { - LOG_ERROR("module.AutoBalance", "AutoBalance_AllCreatureScript::ModifyCreatureAttributes: Unable to determine dynamic scaling floor and ceiling for instance {}.", instanceMap->GetMapName()); - mapABInfo->levelScalingDynamicCeiling = 3; - mapABInfo->levelScalingDynamicFloor = 5; + returnValue = wotlkValue; + //LOG_DEBUG("module.AutoBalance", "AutoBalance::getBaseExpansionValueForLevel: Returning WotLK = {}", returnValue); } - // - // Level Scaling Skip Levels - // - - // Load the global settings into the map - mapABInfo->levelScalingSkipHigherLevels = LevelScalingSkipHigherLevels; - mapABInfo->levelScalingSkipLowerLevels = LevelScalingSkipLowerLevels; + return returnValue; +} - // - // Per-instance overrides, if applicable - // - if (hasDynamicLevelOverride(map->GetId())) +uint32 getBaseExpansionValueForLevel(const uint32 baseValues[3], uint8 targetLevel) +{ + // convert baseValues from an array of uint32 to an array of float + float floatBaseValues[3]; + for (int i = 0; i < 3; i++) { - AutoBalanceLevelScalingDynamicLevelSettings* myDynamicLevelSettings = &levelScalingDynamicLevelOverrides[map->GetId()]; - - // LevelScaling.SkipHigherLevels - if (myDynamicLevelSettings->skipHigher != -1) - mapABInfo->levelScalingSkipHigherLevels = myDynamicLevelSettings->skipHigher; - - // LevelScaling.SkipLowerLevels - if (myDynamicLevelSettings->skipLower != -1) - mapABInfo->levelScalingSkipLowerLevels = myDynamicLevelSettings->skipLower; - - // LevelScaling.DynamicLevelCeiling - if (myDynamicLevelSettings->ceiling != -1) - mapABInfo->levelScalingDynamicCeiling = myDynamicLevelSettings->ceiling; - - // LevelScaling.DynamicLevelFloor - if (myDynamicLevelSettings->floor != -1) - mapABInfo->levelScalingDynamicFloor = myDynamicLevelSettings->floor; + floatBaseValues[i] = (float)baseValues[i]; } + + // return the result + return getBaseExpansionValueForLevel(floatBaseValues, targetLevel); } -void AddCreatureToMapData(Creature* creature, bool addToCreatureList = true, Player* playerToExcludeFromChecks = nullptr, bool forceRecalculation = false) +bool isBossOrBossSummon(Creature* creature, bool log = false) { - // make sure we have a creature and that it's assigned to a map - if (!creature || !creature->GetMap()) - return; + // no creature? not a boss + if (!creature) + { + LOG_INFO("module.AutoBalance", "AutoBalance::isBossOrBossSummon: Creature is null."); + return false; + } - // if this isn't a dungeon or a battleground, skip - if (!(creature->GetMap()->IsDungeon() || creature->GetMap()->IsBattleground())) - return; + // if this creature is a boss, return true + if (creature->IsDungeonBoss() || creature->isWorldBoss()) + { + if (log) + { + LOG_DEBUG("module.AutoBalance", "AutoBalance::isBossOrBossSummon: {} ({}{}) is a boss.", + creature->GetName(), + creature->GetEntry(), + creature->GetInstanceId() ? "-" + std::to_string(creature->GetInstanceId()) : "" + ); + } + + return true; + } - // get AutoBalance data - InstanceMap* instanceMap = ((InstanceMap*)sMapMgr->FindMap(creature->GetMapId(), creature->GetInstanceId())); - AutoBalanceMapInfo *mapABInfo=instanceMap->CustomData.GetDefault("AutoBalanceMapInfo"); - AutoBalanceCreatureInfo *creatureABInfo=creature->CustomData.GetDefault("AutoBalanceCreatureInfo"); - // store the creature's original level if this is the first time seeing it - if (creatureABInfo->UnmodifiedLevel == 0) + // if this creature is a summon of a boss, return true + if ( + creature->IsSummon() && + creature->ToTempSummon() && + creature->ToTempSummon()->GetSummoner() && + creature->ToTempSummon()->GetSummoner()->ToCreature() + ) { - // handle summoned creatures - if (creature->IsSummon()) + Creature* summoner = creature->ToTempSummon()->GetSummoner()->ToCreature(); + + if (summoner) { - LOG_DEBUG("module.AutoBalance", "AutoBalance_AllCreature::AddCreatureToMapData(): Creature {} ({}->\?\?) is a summon.", creature->GetName(), creature->GetLevel()); - if (creature->ToTempSummon() && - creature->ToTempSummon()->GetSummoner() && - creature->ToTempSummon()->GetSummoner()->ToCreature()) + if (summoner->IsDungeonBoss() || summoner->isWorldBoss()) { - Creature* summoner = creature->ToTempSummon()->GetSummoner()->ToCreature(); - if (!summoner) + if (log) { - creatureABInfo->UnmodifiedLevel = mapABInfo->avgCreatureLevel; - LOG_DEBUG("module.AutoBalance", "AutoBalance_AllCreature::AddCreatureToMapData(): Summoned creature {} ({}) is not owned by a summoner.", creature->GetName(), creatureABInfo->UnmodifiedLevel); + LOG_DEBUG("module.AutoBalance", "AutoBalance::isBossOrBossSummon: {} ({}) is a summon of boss {}({}).", + creature->GetName(), + creature->GetEntry(), + summoner->GetName(), + summoner->GetEntry() + ); } - else - { - Creature* summonerCreature = summoner->ToCreature(); - AutoBalanceCreatureInfo *summonerABInfo=summonerCreature->CustomData.GetDefault("AutoBalanceCreatureInfo"); - if (summonerABInfo->UnmodifiedLevel > 0) - { - creatureABInfo->UnmodifiedLevel = summonerABInfo->UnmodifiedLevel; - LOG_DEBUG("module.AutoBalance", "AutoBalance_AllCreature::AddCreatureToMapData(): Summoned creature {} ({}) owned by {} ({}->{})", creature->GetName(), creatureABInfo->UnmodifiedLevel, summonerCreature->GetName(), summonerABInfo->UnmodifiedLevel, summonerCreature->GetLevel()); - } - else - { - creatureABInfo->UnmodifiedLevel = summonerCreature->GetLevel(); - LOG_DEBUG("module.AutoBalance", "AutoBalance_AllCreature::AddCreatureToMapData(): Summoned creature {} ({}) owned by {} ({})", creature->GetName(), creatureABInfo->UnmodifiedLevel, summonerCreature->GetName(), summonerCreature->GetLevel()); - } - } + return true; } else { - creatureABInfo->UnmodifiedLevel = mapABInfo->avgCreatureLevel; - LOG_DEBUG("module.AutoBalance", "AutoBalance_AllCreature::AddCreatureToMapData(): Summoned creature {} ({}) does not have a summoner.", creature->GetName(), creatureABInfo->UnmodifiedLevel); + if (log) + { + LOG_DEBUG("module.AutoBalance", "AutoBalance::isBossOrBossSummon: {} ({}) is a summon of {}({}).", + creature->GetName(), + creature->GetEntry(), + summoner->GetName(), + summoner->GetEntry() + ); + } + return false; } - - // if this is a summon, we shouldn't track it in any list and it does not contribute to the average level - LOG_DEBUG("module.AutoBalance", "AutoBalance_AllCreature::AddCreatureToMapData(): Summoned creature {} ({}) will not affect the map's stats.", creature->GetName(), creatureABInfo->UnmodifiedLevel); - return; - - } - // creature isn't a summon, just store their unmodified level - else - { - creatureABInfo->UnmodifiedLevel = creature->GetLevel(); - LOG_DEBUG("module.AutoBalance", "AutoBalance_AllCreature::AddCreatureToMapData(): {} ({})", creature->GetName(), creatureABInfo->UnmodifiedLevel); } } - // if this is a creature controlled by the player, skip - if (((creature->IsHunterPet() || creature->IsPet() || creature->IsSummon()) && creature->IsControlledByPlayer())) + // not a boss + if (log) { - LOG_DEBUG("module.AutoBalance", "AutoBalance_AllCreature::AddCreatureToMapData(): {} ({}) is controlled by the player - skip.", creature->GetName(), creatureABInfo->UnmodifiedLevel); - return; + // LOG_DEBUG("module.AutoBalance", "AutoBalance::isBossOrBossSummon: {} ({}) is NOT a boss.", + // creature->GetName(), + // creature->GetEntry() + // ); } - // if this is a non-relevant creature, skip - if (creature->IsCritter() || creature->IsTotem() || creature->IsTrigger()) + return false; +} + +bool isCreatureRelevant(Creature* creature) { + // if the creature is gone, return false + if (!creature) { - LOG_DEBUG("module.AutoBalance", "AutoBalance_AllCreature::AddCreatureToMapData(): {} ({}) is a critter, totem, or trigger - skip.", creature->GetName(), creatureABInfo->UnmodifiedLevel); - return; + LOG_DEBUG("module.AutoBalance", "AutoBalance_AllCreatureScript::isCreatureRelevant: Creature is null."); + return false; } - // if the creature level is below 85% of the minimum LFG level, assume it's a flavor creature and shouldn't be tracked or modified - if (creatureABInfo->UnmodifiedLevel < ((float)mapABInfo->lfgMinLevel * .85f)) + // if this creature isn't assigned to a map, make no changes + if (!creature->GetMap() || !creature->GetMap()->IsDungeon()) { - LOG_DEBUG("module.AutoBalance", "AutoBalance_AllCreature::AddCreatureToMapData(): {} ({}) is below 85% of the LFG min level of {} and is NOT tracked.", creature->GetName(), creatureABInfo->UnmodifiedLevel, mapABInfo->lfgMinLevel); - return; + // executed every Creature update for every world creature, enable carefully + // LOG_DEBUG("module.AutoBalance", "AutoBalance_AllCreatureScript::isCreatureRelevant: Creature {} ({}) isn't in a dungeon.", + // creature->GetName(), + // creature->GetLevel() + // ); + return false; } - // if the creature level is above 125% of the maximum LFG level, assume it's a flavor creature or holiday boss and shouldn't be tracked or modified - if (creatureABInfo->UnmodifiedLevel > ((float)mapABInfo->lfgMaxLevel * 1.15f)) + // get the creature's info + AutoBalanceCreatureInfo *creatureABInfo=creature->CustomData.GetDefault("AutoBalanceCreatureInfo"); + + // if this creature has been already been evaluated, just return the previous evaluation + if (creatureABInfo->relevance == AUTOBALANCE_RELEVANCE_FALSE) { - LOG_DEBUG("module.AutoBalance", "AutoBalance_AllCreature::AddCreatureToMapData(): {} ({}) is above 115% of the LFG max level of {} and is NOT tracked.", creature->GetName(), creatureABInfo->UnmodifiedLevel, mapABInfo->lfgMaxLevel); - return; + return false; + } + else if (creatureABInfo->relevance == AUTOBALANCE_RELEVANCE_TRUE) + { + return true; } + // otherwise the value is AUTOBALANCE_RELEVANCE_UNCHECKED, so it needs checking - // is this creature already in the map's creature list? - bool isCreatureAlreadyInCreatureList = creatureABInfo->isInCreatureList; + LOG_DEBUG("module.AutoBalance", "AutoBalance_AllCreatureScript::isCreatureRelevant: Creature {} ({}) | Needs to be evaluated.", + creature->GetName(), + creatureABInfo->UnmodifiedLevel + ); - // add the creature to the map's creature list if configured to do so - if (addToCreatureList && !isCreatureAlreadyInCreatureList) + // get the creature's map's info + Map* creatureMap = creature->GetMap(); + AutoBalanceMapInfo *mapABInfo=creatureMap->CustomData.GetDefault("AutoBalanceMapInfo"); + InstanceMap* instanceMap = creatureMap->ToInstanceMap(); + + // if this creature is in the dungeon's base map, make no changes + if (!(instanceMap)) { - mapABInfo->allMapCreatures.push_back(creature); - creatureABInfo->isInCreatureList = true; - LOG_DEBUG("module.AutoBalance", "AutoBalance_AllCreature::AddCreatureToMapData(): {} ({}) is creature #{} in the creature list.", creature->GetName(), creatureABInfo->UnmodifiedLevel, mapABInfo->allMapCreatures.size()); + creatureABInfo->relevance = AUTOBALANCE_RELEVANCE_FALSE; + LOG_DEBUG("module.AutoBalance", "AutoBalance_AllCreatureScript::isCreatureRelevant: Creature {} ({}) | is in the base map, no changes. Marked for skip.", + creature->GetName(), + creatureABInfo->UnmodifiedLevel + ); + return false; } - // alter stats for the map if needed - bool isIncludedInMapStats = true; - - // if this creature was already in the creature list, don't consider it for map stats (again) - // exception for if forceRecalculation is true (used on player enter/exit to recalculate map stats) - if (isCreatureAlreadyInCreatureList && !forceRecalculation) + // if this is a pet or summon controlled by the player, make no changes + if ((creature->IsHunterPet() || creature->IsPet() || creature->IsSummon()) && creature->IsControlledByPlayer()) { - isIncludedInMapStats = false; + creatureABInfo->relevance = AUTOBALANCE_RELEVANCE_FALSE; + LOG_DEBUG("module.AutoBalance", "AutoBalance_AllCreatureScript::isCreatureRelevant: Creature {} ({}) | is a pet or summon controlled by the player, no changes. Marked for skip.", + creature->GetName(), + creatureABInfo->UnmodifiedLevel + ); + + return false; } - Map::PlayerList const &playerList = creature->GetMap()->GetPlayers(); - if (!playerList.IsEmpty()) + // if this is a player temporary summon (that isn't actively trying to kill the players), make no changes + if ( + creature->ToTempSummon() && + creature->ToTempSummon()->GetSummoner() && + creature->ToTempSummon()->GetSummoner()->ToPlayer() + ) { - // only do these additional checks if we still think they need to be applied to the map stats - if (isIncludedInMapStats) + // if this creature is hostile to any non-charmed player, it should be scaled + bool isHostileToAnyValidPlayer = false; + TempSummon* creatureTempSummon = creature->ToTempSummon(); + Player* summonerPlayer = creatureTempSummon->GetSummoner()->ToPlayer(); + + for (std::vector::const_iterator playerIterator = mapABInfo->allMapPlayers.begin(); playerIterator != mapABInfo->allMapPlayers.end(); ++playerIterator) { - // if the creature is vendor, trainer, or has gossip, don't use it to update map stats - if ((creature->IsVendor() || - creature->HasNpcFlag(UNIT_NPC_FLAG_GOSSIP) || - creature->HasNpcFlag(UNIT_NPC_FLAG_QUESTGIVER) || - creature->HasNpcFlag(UNIT_NPC_FLAG_TRAINER) || - creature->HasNpcFlag(UNIT_NPC_FLAG_TRAINER_PROFESSION) || - creature->HasNpcFlag(UNIT_NPC_FLAG_REPAIR) || - creature->HasUnitFlag(UNIT_FLAG_IMMUNE_TO_PC) || - creature->HasUnitFlag(UNIT_FLAG_NOT_SELECTABLE)) && - (!creature->IsDungeonBoss()) - ) - { - LOG_DEBUG("module.AutoBalance", "AutoBalance_AllCreature::AddCreatureToMapData(): {} ({}) is a a vendor, trainer, or is otherwise not attackable - do not include in map stats.", creature->GetName(), creatureABInfo->UnmodifiedLevel); - isIncludedInMapStats = false; - } - else + Player* thisPlayer = *playerIterator; + + // is this a valid player? + if (!thisPlayer->IsGameMaster() && + !thisPlayer->IsCharmed() && + !thisPlayer->IsHostileToPlayers() && + !thisPlayer->IsHostileTo(summonerPlayer) && + thisPlayer->IsAlive() + ) { - // if the creature is friendly to a player, don't use it to update map stats - for (Map::PlayerList::const_iterator playerIteration = playerList.begin(); playerIteration != playerList.end(); ++playerIteration) + // if this is a guardian and the owner is not hostile to this player, skip + if + ( + creatureTempSummon->IsGuardian() && + !thisPlayer->IsHostileTo(summonerPlayer) + ) { - Player* playerHandle = playerIteration->GetSource(); - - // if this player matches the player we're supposed to skip, skip - if (playerHandle == playerToExcludeFromChecks) - { - continue; - } - - // if the creature is friendly and not a boss - if (creature->IsFriendlyTo(playerHandle) && !creature->IsDungeonBoss()) - { - LOG_DEBUG("module.AutoBalance", "AutoBalance_AllCreature::AddCreatureToMapData(): {} ({}) is friendly to {} - do not include in map stats.", creature->GetName(), creatureABInfo->UnmodifiedLevel, playerHandle->GetName()); - isIncludedInMapStats = false; - break; - } + LOG_DEBUG("module.AutoBalance", "AutoBalance_AllCreatureScript::isCreatureRelevant: Creature {} ({}) | is a guardian of player {}, who is not hostile to valid player {}.", + creature->GetName(), + creatureABInfo->UnmodifiedLevel, + summonerPlayer->GetName(), + thisPlayer->GetName() + ); + + continue; + } + // special case for totems? + else if + ( + creature->IsTotem() && + !thisPlayer->IsHostileTo(summonerPlayer) + ) + { + LOG_DEBUG("module.AutoBalance", "AutoBalance_AllCreatureScript::isCreatureRelevant: Creature {} ({}) | is a totem of player {}, who is not hostile to valid player {}.", + creature->GetName(), + creatureABInfo->UnmodifiedLevel, + summonerPlayer->GetName(), + thisPlayer->GetName() + ); + + continue; } - // perform the distance check if an override is configured for this map - if (hasLevelScalingDistanceCheckOverride(instanceMap->GetId())) + // if the creature is hostile to this valid player, + // unfortunately, `creature->IsHostileTo(thisPlayer)` returns true for cases when it is not actually hostile + else if ( + thisPlayer->isTargetableForAttack(true, creature) + ) { - uint32 distance = levelScalingDistanceCheckOverrides[instanceMap->GetId()]; - bool isPlayerWithinDistance = false; + LOG_DEBUG("module.AutoBalance", "AutoBalance_AllCreatureScript::isCreatureRelevant: Creature {} ({}) | is a player temporary summon hostile to valid player {}.", + creature->GetName(), + creatureABInfo->UnmodifiedLevel, + thisPlayer->GetName() + ); - for (Map::PlayerList::const_iterator playerIteration = playerList.begin(); playerIteration != playerList.end(); ++playerIteration) - { - Player* playerHandle = playerIteration->GetSource(); + isHostileToAnyValidPlayer = true; + break; + } + } + } + + if (!isHostileToAnyValidPlayer) + { + // since no players are hostile to this creature, it should not be scaled + creatureABInfo->relevance = AUTOBALANCE_RELEVANCE_FALSE; + LOG_DEBUG("module.AutoBalance", "AutoBalance_AllCreatureScript::isCreatureRelevant: Creature {} ({}) | is player-summoned and non-hostile, no changes. Marked for skip.", + creature->GetName(), + creatureABInfo->UnmodifiedLevel + ); + + return false; + } + + } + + // if this is a flavor critter + // level and health checks for some nasty level 1 critters in some encounters + if ((creature->IsCritter() && creatureABInfo->UnmodifiedLevel <= 5 && creature->GetMaxHealth() < 100)) + { + creatureABInfo->relevance = AUTOBALANCE_RELEVANCE_FALSE; + LOG_DEBUG("module.AutoBalance", "AutoBalance_AllCreatureScript::isCreatureRelevant: Creature {} ({}) | is a non-relevant critter, no changes. Marked for skip.", + creature->GetName(), + creatureABInfo->UnmodifiedLevel + ); + + return false; + } + + // survived to here, creature is relevant + LOG_DEBUG("module.AutoBalance", "AutoBalance_AllCreatureScript::isCreatureRelevant: Creature {} ({}) | is relevant. Marked for processing.", + creature->GetName(), + creatureABInfo->UnmodifiedLevel + ); + creatureABInfo->relevance = AUTOBALANCE_RELEVANCE_TRUE; + return true; + +} + +AutoBalanceInflectionPointSettings getInflectionPointSettings (InstanceMap* instanceMap, bool isBoss = false) +{ + uint32 maxNumberOfPlayers = instanceMap->GetMaxPlayers(); + uint32 mapId = instanceMap->GetEntry()->MapID; + + float inflectionValue, curveFloor, curveCeiling; + + inflectionValue = (float)maxNumberOfPlayers; + + // + // Base Inflection Point + // + if (instanceMap->IsHeroic()) + { + if (maxNumberOfPlayers <= 5) + { + inflectionValue *= InflectionPointHeroic; + curveFloor = InflectionPointHeroicCurveFloor; + curveCeiling = InflectionPointHeroicCurveCeiling; + } + else if (maxNumberOfPlayers <= 10) + { + inflectionValue *= InflectionPointRaid10MHeroic; + curveFloor = InflectionPointRaid10MHeroicCurveFloor; + curveCeiling = InflectionPointRaid10MHeroicCurveCeiling; + } + else if (maxNumberOfPlayers <= 25) + { + inflectionValue *= InflectionPointRaid25MHeroic; + curveFloor = InflectionPointRaid25MHeroicCurveFloor; + curveCeiling = InflectionPointRaid25MHeroicCurveCeiling; + } + else + { + inflectionValue *= InflectionPointRaidHeroic; + curveFloor = InflectionPointRaidHeroicCurveFloor; + curveCeiling = InflectionPointRaidHeroicCurveCeiling; + } + } + else + { + if (maxNumberOfPlayers <= 5) + { + inflectionValue *= InflectionPoint; + curveFloor = InflectionPointCurveFloor; + curveCeiling = InflectionPointCurveCeiling; + } + else if (maxNumberOfPlayers <= 10) + { + inflectionValue *= InflectionPointRaid10M; + curveFloor = InflectionPointRaid10MCurveFloor; + curveCeiling = InflectionPointRaid10MCurveCeiling; + } + else if (maxNumberOfPlayers <= 15) + { + inflectionValue *= InflectionPointRaid15M; + curveFloor = InflectionPointRaid15MCurveFloor; + curveCeiling = InflectionPointRaid15MCurveCeiling; + } + else if (maxNumberOfPlayers <= 20) + { + inflectionValue *= InflectionPointRaid20M; + curveFloor = InflectionPointRaid20MCurveFloor; + curveCeiling = InflectionPointRaid20MCurveCeiling; + } + else if (maxNumberOfPlayers <= 25) + { + inflectionValue *= InflectionPointRaid25M; + curveFloor = InflectionPointRaid25MCurveFloor; + curveCeiling = InflectionPointRaid25MCurveCeiling; + } + else if (maxNumberOfPlayers <= 40) + { + inflectionValue *= InflectionPointRaid40M; + curveFloor = InflectionPointRaid40MCurveFloor; + curveCeiling = InflectionPointRaid40MCurveCeiling; + } + else + { + inflectionValue *= InflectionPointRaid; + curveFloor = InflectionPointRaidCurveFloor; + curveCeiling = InflectionPointRaidCurveCeiling; + } + } + + // Per map ID overrides alter the above settings, if set + if (hasDungeonOverride(mapId)) + { + AutoBalanceInflectionPointSettings* myInflectionPointOverrides = &dungeonOverrides[mapId]; + + // Alter the inflectionValue according to the override, if set + if (myInflectionPointOverrides->value != -1) + { + inflectionValue = (float)maxNumberOfPlayers; // Starting over + inflectionValue *= myInflectionPointOverrides->value; + } + + if (myInflectionPointOverrides->curveFloor != -1) { curveFloor = myInflectionPointOverrides->curveFloor; } + if (myInflectionPointOverrides->curveCeiling != -1) { curveCeiling = myInflectionPointOverrides->curveCeiling; } + } + + // + // Boss Inflection Point + // + if (isBoss) { + + float bossInflectionPointMultiplier; + + if (instanceMap->IsHeroic()) + { + if (maxNumberOfPlayers <= 5) + { + bossInflectionPointMultiplier = InflectionPointHeroicBoss; + } + else if (maxNumberOfPlayers <= 10) + { + bossInflectionPointMultiplier = InflectionPointRaid10MHeroicBoss; + } + else if (maxNumberOfPlayers <= 25) + { + bossInflectionPointMultiplier = InflectionPointRaid25MHeroicBoss; + } + else + { + bossInflectionPointMultiplier = InflectionPointRaidHeroicBoss; + } + } + else + { + if (maxNumberOfPlayers <= 5) + { + bossInflectionPointMultiplier = InflectionPointBoss; + } + else if (maxNumberOfPlayers <= 10) + { + bossInflectionPointMultiplier = InflectionPointRaid10MBoss; + } + else if (maxNumberOfPlayers <= 15) + { + bossInflectionPointMultiplier = InflectionPointRaid15MBoss; + } + else if (maxNumberOfPlayers <= 20) + { + bossInflectionPointMultiplier = InflectionPointRaid20MBoss; + } + else if (maxNumberOfPlayers <= 25) + { + bossInflectionPointMultiplier = InflectionPointRaid25MBoss; + } + else if (maxNumberOfPlayers <= 40) + { + bossInflectionPointMultiplier = InflectionPointRaid40MBoss; + } + else + { + bossInflectionPointMultiplier = InflectionPointRaidBoss; + } + } + + // Per map ID overrides alter the above settings, if set + if (hasBossOverride(mapId)) + { + AutoBalanceInflectionPointSettings* myBossOverrides = &bossOverrides[mapId]; + + // If set, alter the inflectionValue according to the override + if (myBossOverrides->value != -1) + { + inflectionValue *= myBossOverrides->value; + } + // Otherwise, calculate using the value determined by instance type + else + { + inflectionValue *= bossInflectionPointMultiplier; + } + } + // No override, use the value determined by the instance type + else + { + inflectionValue *= bossInflectionPointMultiplier; + } + } + + return AutoBalanceInflectionPointSettings(inflectionValue, curveFloor, curveCeiling); +} + +void getStatModifiersDebug(Map *map, Creature *creature, std::string message) +{ + // if we have a creature, include that in the output + if (creature) + { + // get the creature's info + AutoBalanceCreatureInfo *creatureABInfo=creature->CustomData.GetDefault("AutoBalanceCreatureInfo"); + + LOG_DEBUG("module.AutoBalance_StatGeneration", "AutoBalance::getStatModifiers: Map {} ({}{}) | Creature {} ({}{}) | {}", + map->GetMapName(), + map->GetId(), + map->GetInstanceId() ? "-" + std::to_string(map->GetInstanceId()) : "", + creature->GetName(), + creatureABInfo->UnmodifiedLevel, + creatureABInfo->selectedLevel ? "->" + std::to_string(creatureABInfo->selectedLevel) : "", + message + ); + } + // if no creature was provided, remove that from the output + else + { + LOG_DEBUG("module.AutoBalance_StatGeneration", "AutoBalance::getStatModifiers: Map {} ({}{}) | {}", + map->GetMapName(), + map->GetId(), + map->GetInstanceId() ? "-" + std::to_string(map->GetInstanceId()) : "", + message + ); + } +} + +AutoBalanceStatModifiers getStatModifiers (Map* map, Creature* creature = nullptr) +{ + // get the instance's InstanceMap + InstanceMap* instanceMap = map->ToInstanceMap(); + + // map variables + uint32 maxNumberOfPlayers = instanceMap->GetMaxPlayers(); + uint32 mapId = map->GetId(); + + // get the creature's info if a creature was specified + AutoBalanceCreatureInfo* creatureABInfo = nullptr; + if (creature) + { + creatureABInfo = creature->CustomData.GetDefault("AutoBalanceCreatureInfo"); + } + + // this will be the return value + AutoBalanceStatModifiers statModifiers; + + // Apply the per-instance-type modifiers first + // AutoBalance.StatModifier*(.Boss). + if (instanceMap->IsHeroic()) // heroic + { + if (maxNumberOfPlayers <= 5) + { + if (creature && isBossOrBossSummon(creature)) + { + statModifiers.global = StatModifierHeroic_Boss_Global; + statModifiers.health = StatModifierHeroic_Boss_Health; + statModifiers.mana = StatModifierHeroic_Boss_Mana; + statModifiers.armor = StatModifierHeroic_Boss_Armor; + statModifiers.damage = StatModifierHeroic_Boss_Damage; + statModifiers.ccduration = StatModifierHeroic_Boss_CCDuration; + + getStatModifiersDebug(map, creature, "1 to 5 Player Heroic Boss"); + } + else + { + statModifiers.global = StatModifierHeroic_Global; + statModifiers.health = StatModifierHeroic_Health; + statModifiers.mana = StatModifierHeroic_Mana; + statModifiers.armor = StatModifierHeroic_Armor; + statModifiers.damage = StatModifierHeroic_Damage; + statModifiers.ccduration = StatModifierHeroic_CCDuration; + + getStatModifiersDebug(map, creature, "1 to 5 Player Heroic"); + } + } + else if (maxNumberOfPlayers <= 10) + { + if (creature && isBossOrBossSummon(creature)) + { + statModifiers.global = StatModifierRaid10MHeroic_Boss_Global; + statModifiers.health = StatModifierRaid10MHeroic_Boss_Health; + statModifiers.mana = StatModifierRaid10MHeroic_Boss_Mana; + statModifiers.armor = StatModifierRaid10MHeroic_Boss_Armor; + statModifiers.damage = StatModifierRaid10MHeroic_Boss_Damage; + statModifiers.ccduration = StatModifierRaid10MHeroic_Boss_CCDuration; + + getStatModifiersDebug(map, creature, "10 Player Heroic Boss"); + } + else + { + statModifiers.global = StatModifierRaid10MHeroic_Global; + statModifiers.health = StatModifierRaid10MHeroic_Health; + statModifiers.mana = StatModifierRaid10MHeroic_Mana; + statModifiers.armor = StatModifierRaid10MHeroic_Armor; + statModifiers.damage = StatModifierRaid10MHeroic_Damage; + statModifiers.ccduration = StatModifierRaid10MHeroic_CCDuration; + + getStatModifiersDebug(map, creature, "10 Player Heroic"); + } + } + else if (maxNumberOfPlayers <= 25) + { + if (creature && isBossOrBossSummon(creature)) + { + statModifiers.global = StatModifierRaid25MHeroic_Boss_Global; + statModifiers.health = StatModifierRaid25MHeroic_Boss_Health; + statModifiers.mana = StatModifierRaid25MHeroic_Boss_Mana; + statModifiers.armor = StatModifierRaid25MHeroic_Boss_Armor; + statModifiers.damage = StatModifierRaid25MHeroic_Boss_Damage; + statModifiers.ccduration = StatModifierRaid25MHeroic_Boss_CCDuration; + + getStatModifiersDebug(map, creature, "25 Player Heroic Boss"); + } + else + { + statModifiers.global = StatModifierRaid25MHeroic_Global; + statModifiers.health = StatModifierRaid25MHeroic_Health; + statModifiers.mana = StatModifierRaid25MHeroic_Mana; + statModifiers.armor = StatModifierRaid25MHeroic_Armor; + statModifiers.damage = StatModifierRaid25MHeroic_Damage; + statModifiers.ccduration = StatModifierRaid25MHeroic_CCDuration; + + getStatModifiersDebug(map, creature, "25 Player Heroic"); + } + } + else + { + if (creature && isBossOrBossSummon(creature)) + { + statModifiers.global = StatModifierRaidHeroic_Boss_Global; + statModifiers.health = StatModifierRaidHeroic_Boss_Health; + statModifiers.mana = StatModifierRaidHeroic_Boss_Mana; + statModifiers.armor = StatModifierRaidHeroic_Boss_Armor; + statModifiers.damage = StatModifierRaidHeroic_Boss_Damage; + statModifiers.ccduration = StatModifierRaidHeroic_Boss_CCDuration; + + getStatModifiersDebug(map, creature, "?? Player Heroic Boss"); + } + else + { + statModifiers.global = StatModifierRaidHeroic_Global; + statModifiers.health = StatModifierRaidHeroic_Health; + statModifiers.mana = StatModifierRaidHeroic_Mana; + statModifiers.armor = StatModifierRaidHeroic_Armor; + statModifiers.damage = StatModifierRaidHeroic_Damage; + statModifiers.ccduration = StatModifierRaidHeroic_CCDuration; + + getStatModifiersDebug(map, creature, "?? Player Heroic"); + } + } + } + else // non-heroic + { + if (maxNumberOfPlayers <= 5) + { + if (creature && isBossOrBossSummon(creature)) + { + statModifiers.global = StatModifier_Boss_Global; + statModifiers.health = StatModifier_Boss_Health; + statModifiers.mana = StatModifier_Boss_Mana; + statModifiers.armor = StatModifier_Boss_Armor; + statModifiers.damage = StatModifier_Boss_Damage; + statModifiers.ccduration = StatModifier_Boss_CCDuration; + + getStatModifiersDebug(map, creature, "1 to 5 Player Normal Boss"); + } + else + { + statModifiers.global = StatModifier_Global; + statModifiers.health = StatModifier_Health; + statModifiers.mana = StatModifier_Mana; + statModifiers.armor = StatModifier_Armor; + statModifiers.damage = StatModifier_Damage; + statModifiers.ccduration = StatModifier_CCDuration; + + getStatModifiersDebug(map, creature, "1 to 5 Player Normal"); + } + } + else if (maxNumberOfPlayers <= 10) + { + if (creature && isBossOrBossSummon(creature)) + { + statModifiers.global = StatModifierRaid10M_Boss_Global; + statModifiers.health = StatModifierRaid10M_Boss_Health; + statModifiers.mana = StatModifierRaid10M_Boss_Mana; + statModifiers.armor = StatModifierRaid10M_Boss_Armor; + statModifiers.damage = StatModifierRaid10M_Boss_Damage; + statModifiers.ccduration = StatModifierRaid10M_Boss_CCDuration; + + getStatModifiersDebug(map, creature, "10 Player Normal Boss"); + } + else + { + statModifiers.global = StatModifierRaid10M_Global; + statModifiers.health = StatModifierRaid10M_Health; + statModifiers.mana = StatModifierRaid10M_Mana; + statModifiers.armor = StatModifierRaid10M_Armor; + statModifiers.damage = StatModifierRaid10M_Damage; + statModifiers.ccduration = StatModifierRaid10M_CCDuration; + + getStatModifiersDebug(map, creature, "10 Player Normal"); + } + } + else if (maxNumberOfPlayers <= 15) + { + if (creature && isBossOrBossSummon(creature)) + { + statModifiers.global = StatModifierRaid15M_Boss_Global; + statModifiers.health = StatModifierRaid15M_Boss_Health; + statModifiers.mana = StatModifierRaid15M_Boss_Mana; + statModifiers.armor = StatModifierRaid15M_Boss_Armor; + statModifiers.damage = StatModifierRaid15M_Boss_Damage; + statModifiers.ccduration = StatModifierRaid15M_Boss_CCDuration; + + getStatModifiersDebug(map, creature, "15 Player Normal Boss"); + } + else + { + statModifiers.global = StatModifierRaid15M_Global; + statModifiers.health = StatModifierRaid15M_Health; + statModifiers.mana = StatModifierRaid15M_Mana; + statModifiers.armor = StatModifierRaid15M_Armor; + statModifiers.damage = StatModifierRaid15M_Damage; + statModifiers.ccduration = StatModifierRaid15M_CCDuration; + + getStatModifiersDebug(map, creature, "15 Player Normal"); + } + } + else if (maxNumberOfPlayers <= 20) + { + if (creature && isBossOrBossSummon(creature)) + { + statModifiers.global = StatModifierRaid20M_Boss_Global; + statModifiers.health = StatModifierRaid20M_Boss_Health; + statModifiers.mana = StatModifierRaid20M_Boss_Mana; + statModifiers.armor = StatModifierRaid20M_Boss_Armor; + statModifiers.damage = StatModifierRaid20M_Boss_Damage; + statModifiers.ccduration = StatModifierRaid20M_Boss_CCDuration; + + getStatModifiersDebug(map, creature, "20 Player Normal Boss"); + } + else + { + statModifiers.global = StatModifierRaid20M_Global; + statModifiers.health = StatModifierRaid20M_Health; + statModifiers.mana = StatModifierRaid20M_Mana; + statModifiers.armor = StatModifierRaid20M_Armor; + statModifiers.damage = StatModifierRaid20M_Damage; + statModifiers.ccduration = StatModifierRaid20M_CCDuration; + + getStatModifiersDebug(map, creature, "20 Player Normal"); + } + } + else if (maxNumberOfPlayers <= 25) + { + if (creature && isBossOrBossSummon(creature)) + { + statModifiers.global = StatModifierRaid25M_Boss_Global; + statModifiers.health = StatModifierRaid25M_Boss_Health; + statModifiers.mana = StatModifierRaid25M_Boss_Mana; + statModifiers.armor = StatModifierRaid25M_Boss_Armor; + statModifiers.damage = StatModifierRaid25M_Boss_Damage; + statModifiers.ccduration = StatModifierRaid25M_Boss_CCDuration; + + getStatModifiersDebug(map, creature, "25 Player Normal Boss"); + } + else + { + statModifiers.global = StatModifierRaid25M_Global; + statModifiers.health = StatModifierRaid25M_Health; + statModifiers.mana = StatModifierRaid25M_Mana; + statModifiers.armor = StatModifierRaid25M_Armor; + statModifiers.damage = StatModifierRaid25M_Damage; + statModifiers.ccduration = StatModifierRaid25M_CCDuration; + + getStatModifiersDebug(map, creature, "25 Player Normal"); + } + } + else if (maxNumberOfPlayers <= 40) + { + if (creature && isBossOrBossSummon(creature)) + { + statModifiers.global = StatModifierRaid40M_Boss_Global; + statModifiers.health = StatModifierRaid40M_Boss_Health; + statModifiers.mana = StatModifierRaid40M_Boss_Mana; + statModifiers.armor = StatModifierRaid40M_Boss_Armor; + statModifiers.damage = StatModifierRaid40M_Boss_Damage; + statModifiers.ccduration = StatModifierRaid40M_Boss_CCDuration; + + getStatModifiersDebug(map, creature, "40 Player Normal Boss"); + } + else + { + statModifiers.global = StatModifierRaid40M_Global; + statModifiers.health = StatModifierRaid40M_Health; + statModifiers.mana = StatModifierRaid40M_Mana; + statModifiers.armor = StatModifierRaid40M_Armor; + statModifiers.damage = StatModifierRaid40M_Damage; + statModifiers.ccduration = StatModifierRaid40M_CCDuration; + + getStatModifiersDebug(map, creature, "40 Player Normal"); + } + } + else + { + if (creature && isBossOrBossSummon(creature)) + { + statModifiers.global = StatModifierRaid_Boss_Global; + statModifiers.health = StatModifierRaid_Boss_Health; + statModifiers.mana = StatModifierRaid_Boss_Mana; + statModifiers.armor = StatModifierRaid_Boss_Armor; + statModifiers.damage = StatModifierRaid_Boss_Damage; + statModifiers.ccduration = StatModifierRaid_Boss_CCDuration; + + getStatModifiersDebug(map, creature, "?? Player Normal Boss"); + } + else + { + statModifiers.global = StatModifierRaid_Global; + statModifiers.health = StatModifierRaid_Health; + statModifiers.mana = StatModifierRaid_Mana; + statModifiers.armor = StatModifierRaid_Armor; + statModifiers.damage = StatModifierRaid_Damage; + statModifiers.ccduration = StatModifierRaid_CCDuration; + + getStatModifiersDebug(map, creature, "?? Player Normal"); + } + } + } + + // Per-Map Overrides + // AutoBalance.StatModifier.Boss.PerInstance + if (creature && isBossOrBossSummon(creature) && hasStatModifierBossOverride(mapId)) + { + AutoBalanceStatModifiers* myStatModifierBossOverrides = &statModifierBossOverrides[mapId]; + + if (myStatModifierBossOverrides->global != -1) { statModifiers.global = myStatModifierBossOverrides->global; } + if (myStatModifierBossOverrides->health != -1) { statModifiers.health = myStatModifierBossOverrides->health; } + if (myStatModifierBossOverrides->mana != -1) { statModifiers.mana = myStatModifierBossOverrides->mana; } + if (myStatModifierBossOverrides->armor != -1) { statModifiers.armor = myStatModifierBossOverrides->armor; } + if (myStatModifierBossOverrides->damage != -1) { statModifiers.damage = myStatModifierBossOverrides->damage; } + if (myStatModifierBossOverrides->ccduration != -1) { statModifiers.ccduration = myStatModifierBossOverrides->ccduration; } + + getStatModifiersDebug(map, creature, "Boss Per-Instance Override"); + } + // AutoBalance.StatModifier.PerInstance + else if (hasStatModifierOverride(mapId)) + { + AutoBalanceStatModifiers* myStatModifierOverrides = &statModifierOverrides[mapId]; + + if (myStatModifierOverrides->global != -1) { statModifiers.global = myStatModifierOverrides->global; } + if (myStatModifierOverrides->health != -1) { statModifiers.health = myStatModifierOverrides->health; } + if (myStatModifierOverrides->mana != -1) { statModifiers.mana = myStatModifierOverrides->mana; } + if (myStatModifierOverrides->armor != -1) { statModifiers.armor = myStatModifierOverrides->armor; } + if (myStatModifierOverrides->damage != -1) { statModifiers.damage = myStatModifierOverrides->damage; } + if (myStatModifierOverrides->ccduration != -1) { statModifiers.ccduration = myStatModifierOverrides->ccduration; } + + getStatModifiersDebug(map, creature, "Per-Instance Override"); + } + + // Per-creature modifiers applied last + // AutoBalance.StatModifier.PerCreature + if (creature && hasStatModifierCreatureOverride(creature->GetEntry())) + { + AutoBalanceStatModifiers* myCreatureOverrides = &statModifierCreatureOverrides[creature->GetEntry()]; + + if (myCreatureOverrides->global != -1) { statModifiers.global = myCreatureOverrides->global; } + if (myCreatureOverrides->health != -1) { statModifiers.health = myCreatureOverrides->health; } + if (myCreatureOverrides->mana != -1) { statModifiers.mana = myCreatureOverrides->mana; } + if (myCreatureOverrides->armor != -1) { statModifiers.armor = myCreatureOverrides->armor; } + if (myCreatureOverrides->damage != -1) { statModifiers.damage = myCreatureOverrides->damage; } + if (myCreatureOverrides->ccduration != -1) { statModifiers.ccduration = myCreatureOverrides->ccduration; } + + getStatModifiersDebug(map, creature, "Per-Creature Override"); + } + + if (creature) + { + LOG_DEBUG("module.AutoBalance_StatGeneration", "AutoBalance::getStatModifiers: Map {} ({}{}) | Creature {} ({}{}) | Stat Modifiers = global: {} | health: {} | mana: {} | armor: {} | damage: {} | ccduration: {}", + map->GetMapName(), + map->GetId(), + map->GetInstanceId() ? "-" + std::to_string(map->GetInstanceId()) : "", + creature->GetName(), + creatureABInfo->UnmodifiedLevel, + creatureABInfo->selectedLevel ? "->" + std::to_string(creatureABInfo->selectedLevel) : "", + statModifiers.global, + statModifiers.health, + statModifiers.mana, + statModifiers.armor, + statModifiers.damage, + statModifiers.ccduration == -1 ? 1.0f : statModifiers.ccduration + ); + } + else + { + LOG_DEBUG("module.AutoBalance_StatGeneration", "AutoBalance::getStatModifiers: Map {} ({}{}) | Stat Modifiers = global: {} | health: {} | mana: {} | armor: {} | damage: {} | ccduration: {}", + map->GetMapName(), + map->GetId(), + map->GetInstanceId() ? "-" + std::to_string(map->GetInstanceId()) : "", + statModifiers.global, + statModifiers.health, + statModifiers.mana, + statModifiers.armor, + statModifiers.damage, + statModifiers.ccduration == -1 ? 1.0f : statModifiers.ccduration + ); + } + + return statModifiers; + +} + +float getDefaultMultiplier(Map* map, AutoBalanceInflectionPointSettings inflectionPointSettings) +{ + // You can visually see the effects of this function by using this spreadsheet: + // https://docs.google.com/spreadsheets/d/100cmKIJIjCZ-ncWd0K9ykO8KUgwFTcwg4h2nfE_UeCc/copy + + // get the max player count for the map + uint32 maxNumberOfPlayers = map->ToInstanceMap()->GetMaxPlayers(); + + // get the adjustedPlayerCount for this instance + AutoBalanceMapInfo *mapABInfo=map->CustomData.GetDefault("AutoBalanceMapInfo"); + float adjustedPlayerCount = mapABInfo->adjustedPlayerCount; + + // #maththings + float diff = ((float)maxNumberOfPlayers/5)*1.5f; + + // For math reasons that I do not understand, curveCeiling needs to be adjusted to bring the actual multiplier + // closer to the curveCeiling setting. Create an adjustment based on how much the ceiling should be changed at + // the max players multiplier. + float curveCeilingAdjustment = + inflectionPointSettings.curveCeiling / + (((tanh(((float)maxNumberOfPlayers - inflectionPointSettings.value) / diff) + 1.0f) / 2.0f) * + (inflectionPointSettings.curveCeiling - inflectionPointSettings.curveFloor) + inflectionPointSettings.curveFloor); + + // Adjust the multiplier based on the configured floor and ceiling values, plus the ceiling adjustment we just calculated + float defaultMultiplier = + ((tanh((adjustedPlayerCount - inflectionPointSettings.value) / diff) + 1.0f) / 2.0f) * + (inflectionPointSettings.curveCeiling * curveCeilingAdjustment - inflectionPointSettings.curveFloor) + + inflectionPointSettings.curveFloor; + + return defaultMultiplier; +} + +World_Multipliers getWorldMultiplier(Map* map, BaseValueType baseValueType) +{ + World_Multipliers worldMultipliers; + + // null check + if (!map) + { + return worldMultipliers; + } + + // if this isn't a dungeon, return defaults + if (!(map->IsDungeon())) + { + return worldMultipliers; + } + + // grab map data + AutoBalanceMapInfo *mapABInfo=map->CustomData.GetDefault("AutoBalanceMapInfo"); + + // if the map isn't enabled, return defaults + if (!mapABInfo->enabled) + { + return worldMultipliers; + } + + // if there are no players on the map, return defaults + if (mapABInfo->allMapPlayers.size() == 0) + { + return worldMultipliers; + } + + // if creatures haven't been counted yet, return defaults + if (mapABInfo->avgCreatureLevel == 0) + { + return worldMultipliers; + } + + // create some data variables + InstanceMap* instanceMap = map->ToInstanceMap(); + uint8 avgCreatureLevelRounded = (uint8)(mapABInfo->avgCreatureLevel + 0.5f); + + // get the inflection point settings for this map + AutoBalanceInflectionPointSettings inflectionPointSettings = getInflectionPointSettings(instanceMap); + + // Generate the default multiplier before level scaling + // This value is only based on the adjusted number of players in the instance + float worldMultiplier = 1.0f; + float defaultMultiplier = getDefaultMultiplier(map, inflectionPointSettings); + + LOG_DEBUG("module.AutoBalance", + "AutoBalance::getWorldMultiplier: Map {} ({}) {} | defaultMultiplier ({}) = getDefaultMultiplier(map, inflectionPointSettings)", + map->GetMapName(), + avgCreatureLevelRounded, + baseValueType == BaseValueType::AUTOBALANCE_HEALTH ? "health" : "damage", + defaultMultiplier + ); + + // multiply by the appropriate stat modifiers + AutoBalanceStatModifiers statModifiers = getStatModifiers(map); + + if (baseValueType == BaseValueType::AUTOBALANCE_HEALTH) // health + { + worldMultiplier = defaultMultiplier * statModifiers.global * statModifiers.health; + } + else // damage + { + worldMultiplier = defaultMultiplier * statModifiers.global * statModifiers.damage; + } + + LOG_DEBUG("module.AutoBalance", + "AutoBalance::getWorldMultiplier: Map {} ({}) {} | worldMultiplier ({}) = defaultMultiplier ({}) * statModifiers.global ({}) * statModifiers.{} ({})", + map->GetMapName(), + avgCreatureLevelRounded, + baseValueType == BaseValueType::AUTOBALANCE_HEALTH ? "health" : "damage", + worldMultiplier, + defaultMultiplier, + statModifiers.global, + baseValueType == BaseValueType::AUTOBALANCE_HEALTH ? "health" : "damage", + baseValueType == BaseValueType::AUTOBALANCE_HEALTH ? statModifiers.health : statModifiers.damage + ); + + // store the unscaled multiplier + worldMultipliers.unscaled = worldMultiplier; + + LOG_DEBUG("module.AutoBalance", "AutoBalance::getWorldMultiplier: Map {} ({}) {} | multiplier before level scaling = ({}).", + map->GetMapName(), + avgCreatureLevelRounded, + baseValueType == BaseValueType::AUTOBALANCE_HEALTH ? "health" : "damage", + worldMultiplier + ); + + // only scale based on level if level scaling is enabled and the instance's average creature level is not within the skip range + if (LevelScaling && + ( + (mapABInfo->avgCreatureLevel > mapABInfo->highestPlayerLevel + mapABInfo->levelScalingSkipHigherLevels || mapABInfo->levelScalingSkipHigherLevels == 0) || + (mapABInfo->avgCreatureLevel < mapABInfo->highestPlayerLevel - mapABInfo->levelScalingSkipLowerLevels || mapABInfo->levelScalingSkipLowerLevels == 0) + ) + ) + { + mapABInfo->worldMultiplierTargetLevel = mapABInfo->highestPlayerLevel; + LOG_DEBUG("module.AutoBalance", "AutoBalance::getWorldMultiplier: Map {} ({}) {} | level will be scaled to {}.", + map->GetMapName(), + avgCreatureLevelRounded, + baseValueType == BaseValueType::AUTOBALANCE_HEALTH ? "health" : "damage", + mapABInfo->worldMultiplierTargetLevel + ); + + // use creature base stats to determine how to level scale the multiplier (the map is a warrior!) + CreatureBaseStats const* origMapBaseStats = sObjectMgr->GetCreatureBaseStats(avgCreatureLevelRounded, Classes::CLASS_WARRIOR); + CreatureBaseStats const* adjustedMapBaseStats = sObjectMgr->GetCreatureBaseStats(mapABInfo->worldMultiplierTargetLevel, Classes::CLASS_WARRIOR); + + // Original Base Value + float originalBaseValue; + + if (baseValueType == BaseValueType::AUTOBALANCE_HEALTH) // health + { + originalBaseValue = getBaseExpansionValueForLevel( + origMapBaseStats->BaseHealth, + avgCreatureLevelRounded + ); + } + else // damage + { + originalBaseValue = getBaseExpansionValueForLevel( + origMapBaseStats->BaseDamage, + avgCreatureLevelRounded + ); + } + + LOG_DEBUG("module.AutoBalance", "AutoBalance::getWorldMultiplier: Map {} ({}) {} | base is {}.", + map->GetMapName(), + avgCreatureLevelRounded, + baseValueType == BaseValueType::AUTOBALANCE_HEALTH ? "health" : "damage", + originalBaseValue + ); + + // New Base Value + float newBaseValue; + + if (baseValueType == BaseValueType::AUTOBALANCE_HEALTH) // health + { + newBaseValue = getBaseExpansionValueForLevel( + adjustedMapBaseStats->BaseHealth, + mapABInfo->worldMultiplierTargetLevel + ); + } + else // damage + { + newBaseValue = getBaseExpansionValueForLevel( + adjustedMapBaseStats->BaseDamage, + mapABInfo->worldMultiplierTargetLevel + ); + } + + LOG_DEBUG("module.AutoBalance", "AutoBalance::getWorldMultiplier: Map {} ({}->{}) {} | base is {}.", + map->GetMapName(), + avgCreatureLevelRounded, + mapABInfo->worldMultiplierTargetLevel, + baseValueType == BaseValueType::AUTOBALANCE_HEALTH ? "health" : "damage", + newBaseValue + ); + + // update the world multiplier accordingly + worldMultiplier *= newBaseValue / originalBaseValue; + + LOG_DEBUG("module.AutoBalance", "AutoBalance::getWorldMultiplier: Map {} ({}->{}) {} | worldMultiplier ({}) = worldMultiplier ({}) * newBaseValue ({}) / originalBaseValue ({})", + map->GetMapName(), + mapABInfo->avgCreatureLevel, + mapABInfo->worldMultiplierTargetLevel, + baseValueType == BaseValueType::AUTOBALANCE_HEALTH ? "health" : "damage", + worldMultiplier, + worldMultiplier, + newBaseValue, + originalBaseValue + ); + + LOG_DEBUG("module.AutoBalance", "AutoBalance::getWorldMultiplier: Map {} ({}->{}) {} | multiplier after level scaling = ({}).", + map->GetMapName(), + avgCreatureLevelRounded, + mapABInfo->worldMultiplierTargetLevel, + baseValueType == BaseValueType::AUTOBALANCE_HEALTH ? "health" : "damage", + worldMultiplier + ); + } + else + { + mapABInfo->worldMultiplierTargetLevel = avgCreatureLevelRounded; + + // level scaling is disabled + if (!LevelScaling) + { + LOG_DEBUG("module.AutoBalance", "AutoBalance::getWorldMultiplier: Map {} ({}) | not level scaled due to level scaling being disabled. World multiplier target level set to avgCreatureLevel ({}).", + map->GetMapName(), + mapABInfo->worldMultiplierTargetLevel, + mapABInfo->worldMultiplierTargetLevel + ); + } + // inside the level skip range + else + { + LOG_DEBUG("module.AutoBalance", "AutoBalance::getWorldMultiplier: Map {} ({}) | not level scaled due to being inside the level skip range. World multiplier target level set to avgCreatureLevel ({}).", + map->GetMapName(), + mapABInfo->worldMultiplierTargetLevel, + mapABInfo->worldMultiplierTargetLevel + ); + } + + LOG_DEBUG("module.AutoBalance", "AutoBalance::getWorldMultiplier: Map {} ({}) {} | multiplier after level scaling = ({}).", + map->GetMapName(), + mapABInfo->worldMultiplierTargetLevel, + baseValueType == BaseValueType::AUTOBALANCE_HEALTH ? "health" : "damage", + worldMultiplier + ); + } + + // store the (potentially) level-scaled multiplier + worldMultipliers.scaled = worldMultiplier; + + return worldMultipliers; +} + +void LoadMapSettings(Map* map) +{ + // Load (or create) the map's info + AutoBalanceMapInfo *mapABInfo=map->CustomData.GetDefault("AutoBalanceMapInfo"); + + // create an InstanceMap object + InstanceMap* instanceMap = map->ToInstanceMap(); + + LOG_DEBUG("module.AutoBalance", "AutoBalance::LoadMapSettings: Map {} ({}{}, {}-player {}) | Loading settings.", + map->GetMapName(), + map->GetId(), + map->GetInstanceId() ? "-" + std::to_string(map->GetInstanceId()) : "", + instanceMap->GetMaxPlayers(), + instanceMap->IsHeroic() ? "Heroic" : "Normal" + ); + + // determine the minumum player count + if (isDungeonInMinPlayerMap(map->GetId(), instanceMap->IsHeroic())) + { + mapABInfo->minPlayers = instanceMap->IsHeroic() ? minPlayersPerHeroicDungeonIdMap[map->GetId()] : minPlayersPerDungeonIdMap[map->GetId()]; + } + else if (instanceMap->IsHeroic()) + { + mapABInfo->minPlayers = minPlayersHeroic; + } + else + { + mapABInfo->minPlayers = minPlayersNormal; + } + + // if the minPlayers value we determined is less than the max number of players in this map, adjust down + if (mapABInfo->minPlayers > instanceMap->GetMaxPlayers()) + { + LOG_WARN("module.AutoBalance", "AutoBalance::LoadMapSettings: Your settings tried to set a minimum player count of {} which is greater than {}'s max player count of {}. Adjusting down.", + mapABInfo->minPlayers, + map->GetMapName(), + instanceMap->GetMaxPlayers() + ); + + mapABInfo->minPlayers = instanceMap->GetMaxPlayers(); + } + + LOG_DEBUG("module.AutoBalance", "AutoBalance::LoadMapSettings: Map {} ({}{}, {}-player {}) | has a minimum player count of {}.", + map->GetMapName(), + map->GetId(), + map->GetInstanceId() ? "-" + std::to_string(map->GetInstanceId()) : "", + instanceMap->GetMaxPlayers(), + instanceMap->IsHeroic() ? "Heroic" : "Normal", + mapABInfo->minPlayers + ); + + // + // Dynamic Level Scaling Floor and Ceiling + // + + // 5-player normal dungeons + if (instanceMap->GetMaxPlayers() <= 5 && !instanceMap->IsHeroic()) + { + mapABInfo->levelScalingDynamicCeiling = LevelScalingDynamicLevelCeilingDungeons; + mapABInfo->levelScalingDynamicFloor = LevelScalingDynamicLevelFloorDungeons; + + } + // 5-player heroic dungeons + else if (instanceMap->GetMaxPlayers() <= 5 && instanceMap->IsHeroic()) + { + mapABInfo->levelScalingDynamicCeiling = LevelScalingDynamicLevelCeilingHeroicDungeons; + mapABInfo->levelScalingDynamicFloor = LevelScalingDynamicLevelFloorHeroicDungeons; + } + // Normal raids + else if (instanceMap->GetMaxPlayers() > 5 && !instanceMap->IsHeroic()) + { + mapABInfo->levelScalingDynamicCeiling = LevelScalingDynamicLevelCeilingRaids; + mapABInfo->levelScalingDynamicFloor = LevelScalingDynamicLevelFloorRaids; + } + // Heroic raids + else if (instanceMap->GetMaxPlayers() > 5 && instanceMap->IsHeroic()) + { + mapABInfo->levelScalingDynamicCeiling = LevelScalingDynamicLevelCeilingHeroicRaids; + mapABInfo->levelScalingDynamicFloor = LevelScalingDynamicLevelFloorHeroicRaids; + } + // something went wrong + else + { + LOG_ERROR("module.AutoBalance", "AutoBalance::LoadMapSettings: Map {} ({}{}, {}-player {}) | Unable to determine dynamic scaling floor and ceiling for instance.", + map->GetMapName(), + map->GetId(), + map->GetInstanceId() ? "-" + std::to_string(map->GetInstanceId()) : "", + instanceMap->GetMaxPlayers(), + instanceMap->IsHeroic() ? "Heroic" : "Normal" + ); + + mapABInfo->levelScalingDynamicCeiling = 3; + mapABInfo->levelScalingDynamicFloor = 5; + + } + + // + // Level Scaling Skip Levels + // + + // Load the global settings into the map + mapABInfo->levelScalingSkipHigherLevels = LevelScalingSkipHigherLevels; + mapABInfo->levelScalingSkipLowerLevels = LevelScalingSkipLowerLevels; + + // + // Per-instance overrides, if applicable + // + + if (hasDynamicLevelOverride(map->GetId())) + { + AutoBalanceLevelScalingDynamicLevelSettings* myDynamicLevelSettings = &levelScalingDynamicLevelOverrides[map->GetId()]; + + // LevelScaling.SkipHigherLevels + if (myDynamicLevelSettings->skipHigher != -1) + mapABInfo->levelScalingSkipHigherLevels = myDynamicLevelSettings->skipHigher; + + // LevelScaling.SkipLowerLevels + if (myDynamicLevelSettings->skipLower != -1) + mapABInfo->levelScalingSkipLowerLevels = myDynamicLevelSettings->skipLower; + + // LevelScaling.DynamicLevelCeiling + if (myDynamicLevelSettings->ceiling != -1) + mapABInfo->levelScalingDynamicCeiling = myDynamicLevelSettings->ceiling; + + // LevelScaling.DynamicLevelFloor + if (myDynamicLevelSettings->floor != -1) + mapABInfo->levelScalingDynamicFloor = myDynamicLevelSettings->floor; + } +} + +void AddCreatureToMapCreatureList(Creature* creature, bool addToCreatureList = true, bool forceRecalculation = false) +{ + // make sure we have a creature and that it's assigned to a map + if (!creature || !creature->GetMap()) + return; + + // if this isn't a dungeon, skip + if (!(creature->GetMap()->IsDungeon())) + return; + + // get AutoBalance data + Map* map = creature->GetMap(); + InstanceMap* instanceMap = map->ToInstanceMap(); + AutoBalanceMapInfo *mapABInfo=instanceMap->CustomData.GetDefault("AutoBalanceMapInfo"); + AutoBalanceCreatureInfo *creatureABInfo=creature->CustomData.GetDefault("AutoBalanceCreatureInfo"); + + // handle summoned creatures + if (creature->IsSummon()) + { + LOG_DEBUG("module.AutoBalance", "AutoBalance::AddCreatureToMapCreatureList: Creature {} ({}) | is a summon.", + creature->GetName(), + creatureABInfo->UnmodifiedLevel + ); + if (creature->ToTempSummon() && + creature->ToTempSummon()->GetSummoner() && + creature->ToTempSummon()->GetSummoner()->ToCreature()) + { + creatureABInfo->summoner = creature->ToTempSummon()->GetSummoner()->ToCreature(); + Creature* summoner = creatureABInfo->summoner; + + if (!summoner) + { + creatureABInfo->UnmodifiedLevel = mapABInfo->avgCreatureLevel; + LOG_DEBUG("module.AutoBalance", "AutoBalance::AddCreatureToMapCreatureList: Creature {} ({}) (summon) | is not owned by a summoner. Original level is {}.", + creature->GetName(), + creatureABInfo->UnmodifiedLevel + ); + } + else + { + AutoBalanceCreatureInfo *summonerABInfo=summoner->CustomData.GetDefault("AutoBalanceCreatureInfo"); - // if this player matches the player we're supposed to skip, skip - if (playerHandle == playerToExcludeFromChecks) - { - continue; - } + LOG_DEBUG("module.AutoBalance", "AutoBalance::AddCreatureToMapCreatureList: Creature {} ({}) (summon) | is owned by {} ({}).", + creature->GetName(), + creatureABInfo->UnmodifiedLevel, + summoner->GetName(), + summonerABInfo->UnmodifiedLevel + ); - if (playerHandle->IsWithinDist(creature, 500)) - { - LOG_DEBUG("module.AutoBalance", "AutoBalance_AllCreature::AddCreatureToMapData(): {} ({}) is in range ({} world units) of player {} and is considered active.", creature->GetName(), creatureABInfo->UnmodifiedLevel, distance, playerHandle->GetName()); - isPlayerWithinDistance = true; - break; - } - else - { - LOG_DEBUG("module.AutoBalance", "AutoBalance_AllCreature::AddCreatureToMapData(): {} ({}) is NOT in range ({} world units) of any player and is NOT considered active.", creature->GetName(), creature->GetLevel(), distance); - } + // if the creature or its summoner is a trigger + if (creature->IsTrigger() || summoner->IsTrigger()) + { + LOG_DEBUG("module.AutoBalance", "AutoBalance::AddCreatureToMapCreatureList: Creature {} ({}) (summon) | or their summoner is a trigger.", + creature->GetName(), + creatureABInfo->UnmodifiedLevel + ); + + // if the creature is within the expected level range, allow scaling + if ( + (creatureABInfo->UnmodifiedLevel >= (uint8)(((float)mapABInfo->lfgMinLevel * .85f) + 0.5f)) && + (creatureABInfo->UnmodifiedLevel <= (uint8)(((float)mapABInfo->lfgMaxLevel * 1.15f) + 0.5f)) + ) + { + LOG_DEBUG("module.AutoBalance", "AutoBalance::AddCreatureToMapCreatureList: Creature {} ({}) (summon) | original level is within the expected NPC level for this map ({} to {}). Level scaling is allowed.", + creature->GetName(), + creatureABInfo->UnmodifiedLevel, + (uint8)(((float)mapABInfo->lfgMinLevel * .85f) + 0.5f), + (uint8)(((float)mapABInfo->lfgMaxLevel * 1.15f) + 0.5f) + ); } - - // if no players were within the distance, don't include this creature in the map stats - if (!isPlayerWithinDistance) - isIncludedInMapStats = false; + else { + creatureABInfo->neverLevelScale = true; + LOG_DEBUG("module.AutoBalance", "AutoBalance::AddCreatureToMapCreatureList: Creature {} ({}) (summon) | original level is outside the expected NPC level for this map ({} to {}). It will keep its original level.", + creature->GetName(), + creatureABInfo->UnmodifiedLevel, + (uint8)(((float)mapABInfo->lfgMinLevel * .85f) + 0.5f), + (uint8)(((float)mapABInfo->lfgMaxLevel * 1.15f) + 0.5f) + ); + } + } + // if the creature is not a trigger, match the summoner's level + else + { + // match the summoner's level + creatureABInfo->UnmodifiedLevel = summonerABInfo->UnmodifiedLevel; + + LOG_DEBUG("module.AutoBalance", "AutoBalance::AddCreatureToMapCreatureList: Creature {} ({}) (summon) | original level will match summoner's level ({}).", + creature->GetName(), + creatureABInfo->UnmodifiedLevel, + summonerABInfo->UnmodifiedLevel + ); } } } + // summoned by a player + else if + ( + creature->ToTempSummon() && + creature->ToTempSummon()->GetSummoner() && + creature->ToTempSummon()->GetSummoner()->ToPlayer() + ) + { + Player* summoner = creature->ToTempSummon()->GetSummoner()->ToPlayer(); + + // is this creature relevant? + if (isCreatureRelevant(creature)) + { + LOG_DEBUG("module.AutoBalance", "AutoBalance::AddCreatureToMapCreatureList: Creature {} ({}) (summon) | is an enemy owned by player {} ({}). Summon original level set to ({}).", + creature->GetName(), + creatureABInfo->UnmodifiedLevel, + summoner->GetName(), + summoner->GetLevel(), + creatureABInfo->UnmodifiedLevel + ); + } + // summon is not relevant + else + { + uint8 newLevel = std::min(summoner->GetLevel(), creature->GetCreatureTemplate()->maxlevel); + LOG_DEBUG("module.AutoBalance", "AutoBalance::AddCreatureToMapCreatureList: Creature {} ({}) (summon) | is an ally owned by player {} ({}). Summon original level set to ({}) level ({}).", + creature->GetName(), + creatureABInfo->UnmodifiedLevel, + summoner->GetName(), + summoner->GetLevel(), + newLevel == summoner->GetLevel() ? "player's" : "creature template's max", + newLevel + ); + creatureABInfo->UnmodifiedLevel = newLevel; + } + } + // pets and totems + else if (creature->IsCreatedByPlayer() || creature->IsPet() || creature->IsHunterPet() || creature->IsTotem()) + { + LOG_DEBUG("module.AutoBalance", "AutoBalance::AddCreatureToMapCreatureList: Creature {} ({}) (summon) | is a {}. Original level set to ({}).", + creature->GetName(), + creatureABInfo->UnmodifiedLevel, + creature->IsCreatedByPlayer() ? "creature created by a player" : creature->IsPet() ? "pet" : creature->IsHunterPet() ? "hunter pet" : "totem", + creatureABInfo->UnmodifiedLevel + ); + } + else + { + LOG_DEBUG("module.AutoBalance", "AutoBalance::AddCreatureToMapCreatureList: Creature {} ({}) (summon) | does not have a summoner. Summon original level set to ({}).", + creature->GetName(), + creatureABInfo->UnmodifiedLevel, + creatureABInfo->UnmodifiedLevel + ); + } - if (isIncludedInMapStats) + // if this is a summon, we shouldn't track it in any list and it does not contribute to the average level + LOG_DEBUG("module.AutoBalance", "AutoBalance::AddCreatureToMapCreatureList: Creature {} ({}) (summon) | will not affect the map's stats.", creature->GetName(), creatureABInfo->UnmodifiedLevel); + return; + } + // handle "special" creatures + else if (creature->IsCritter() || creature->IsTotem() || creature->IsTrigger()) + { + // if this is an intentionally-low-level creature (below 85% of the minimum LFG level), leave it where it is + // if this is an intentionally-high-level creature (above 125% of the maximum LFG level), leave it where it is + if ( + (creatureABInfo->UnmodifiedLevel < (uint8)(((float)mapABInfo->lfgMinLevel * .85f) + 0.5f)) || + (creatureABInfo->UnmodifiedLevel > (uint8)(((float)mapABInfo->lfgMaxLevel * 1.15f) + 0.5f)) + ) + { + creatureABInfo->neverLevelScale = true; + LOG_DEBUG("module.AutoBalance", "AutoBalance::AddCreatureToMapCreatureList: Creature {} ({}) (summon) | is a {} and is outside the expected NPC level for this map ({} to {}). Keeping original level of {}.", + creature->GetName(), + creatureABInfo->UnmodifiedLevel, + creature->IsCritter() ? "critter" : creature->IsTotem() ? "totem" : "trigger", + (uint8)(((float)mapABInfo->lfgMinLevel * .85f) + 0.5f), + (uint8)(((float)mapABInfo->lfgMaxLevel * 1.15f) + 0.5f), + creatureABInfo->UnmodifiedLevel + ); + } + // otherwise, set it to the target level of the instance so it will get scaled properly + else { - // mark this creature as being considered in the map stats - creatureABInfo->isActive = true; + creatureABInfo->UnmodifiedLevel = mapABInfo->lfgTargetLevel; + LOG_DEBUG("module.AutoBalance", "AutoBalance::AddCreatureToMapCreatureList: Creature {} ({}) (summon) | is a {} and is within the expected NPC level for this map ({} to {}). Keeping original level of {}.", + creature->GetName(), + creatureABInfo->UnmodifiedLevel, + creature->IsCritter() ? "critter" : creature->IsTotem() ? "totem" : "trigger", + (uint8)(((float)mapABInfo->lfgMinLevel * .85f) + 0.5f), + (uint8)(((float)mapABInfo->lfgMaxLevel * 1.15f) + 0.5f), + creatureABInfo->UnmodifiedLevel + ); + + } + + } + // creature isn't a summon, just store their unmodified level + else + { + creatureABInfo->UnmodifiedLevel = creatureABInfo->UnmodifiedLevel; + LOG_DEBUG("module.AutoBalance", "AutoBalance::AddCreatureToMapCreatureList: Creature {} ({}) | Original level set to ({}).", + creature->GetName(), + creatureABInfo->UnmodifiedLevel, + creatureABInfo->UnmodifiedLevel + ); + } + + // if this is a creature controlled by the player, skip for stats + if (((creature->IsHunterPet() || creature->IsPet() || creature->IsSummon()) && creature->IsControlledByPlayer())) + { + LOG_DEBUG("module.AutoBalance", "AutoBalance::AddCreatureToMapCreatureList: Creature {} ({}) | is controlled by the player and will not affect the map's stats.", creature->GetName(), creatureABInfo->UnmodifiedLevel); + return; + } - // update the highest and lowest creature levels - if (creatureABInfo->UnmodifiedLevel > mapABInfo->highestCreatureLevel || mapABInfo->highestCreatureLevel == 0) - mapABInfo->highestCreatureLevel = creatureABInfo->UnmodifiedLevel; - if (creatureABInfo->UnmodifiedLevel < mapABInfo->lowestCreatureLevel || mapABInfo->lowestCreatureLevel == 0) - mapABInfo->lowestCreatureLevel = creatureABInfo->UnmodifiedLevel; + // if this is a non-relevant creature, skip for stats + if (creature->IsCritter() || creature->IsTotem() || creature->IsTrigger()) + { + LOG_DEBUG("module.AutoBalance", "AutoBalance::AddCreatureToMapCreatureList: Creature {} ({}) | is a {} and will not affect the map's stats.", + creature->GetName(), + creatureABInfo->UnmodifiedLevel, + creature->IsCritter() ? "critter" : creature->IsTotem() ? "totem" : "trigger" + ); + return; + } - // calculate the new average creature level - float creatureCount = mapABInfo->activeCreatureCount; - float newAvgCreatureLevel = (((float)mapABInfo->avgCreatureLevel * creatureCount) + (float)creatureABInfo->UnmodifiedLevel) / (creatureCount + 1.0f); - mapABInfo->avgCreatureLevel = newAvgCreatureLevel; + // if the creature level is below 85% of the minimum LFG level, assume it's a flavor creature and shouldn't be tracked + if (creatureABInfo->UnmodifiedLevel < (uint8)(((float)mapABInfo->lfgMinLevel * 0.85f) + 0.5f)) + { + LOG_DEBUG("module.AutoBalance", "AutoBalance::AddCreatureToMapCreatureList: Creature {} ({}) | is below 85% of the LFG min level of {} and will not affect the map's stats.", creature->GetName(), creatureABInfo->UnmodifiedLevel, mapABInfo->lfgMinLevel); + return; + } - // increment the active creature counter - mapABInfo->activeCreatureCount++; + // if the creature level is above 125% of the maximum LFG level, assume it's a flavor creature or holiday boss and shouldn't be tracked + if (creatureABInfo->UnmodifiedLevel > (uint8)(((float)mapABInfo->lfgMaxLevel * 1.15f) + 0.5f)) + { + LOG_DEBUG("module.AutoBalance", "AutoBalance::AddCreatureToMapCreatureList: Creature {} ({}) | is above 115% of the LFG max level of {} and will not affect the map's stats.", creature->GetName(), creatureABInfo->UnmodifiedLevel, mapABInfo->lfgMaxLevel); + return; + } - LOG_DEBUG("module.AutoBalance", "AutoBalance_AllCreature::AddCreatureToMapData(): {} ({}) is included in map stats, adjusting avgCreatureLevel to {}", creature->GetName(), creatureABInfo->UnmodifiedLevel, newAvgCreatureLevel); + // is this creature already in the map's creature list? + bool isCreatureAlreadyInCreatureList = creatureABInfo->isInCreatureList; - // reset the last config time so that the map data will get updated - lastConfigTime = std::chrono::duration_cast(std::chrono::system_clock::now().time_since_epoch()).count(); - LOG_DEBUG("module.AutoBalance", "AutoBalance_AllCreature::AddCreatureToMapData(): lastConfigTime reset to {}", lastConfigTime); - } - else if (isCreatureAlreadyInCreatureList) + // add the creature to the map's creature list if configured to do so + if (addToCreatureList && !isCreatureAlreadyInCreatureList) + { + mapABInfo->allMapCreatures.push_back(creature); + creatureABInfo->isInCreatureList = true; + LOG_DEBUG("module.AutoBalance", "AutoBalance::AddCreatureToMapCreatureList: Creature {} ({}) | is #{} in the creature list.", creature->GetName(), creatureABInfo->UnmodifiedLevel, mapABInfo->allMapCreatures.size()); + } + + // alter stats for the map if needed + bool isIncludedInMapStats = true; + + // if this creature was already in the creature list, don't consider it for map stats (again) + // exception for if forceRecalculation is true (used on player enter/exit to recalculate map stats) + if (isCreatureAlreadyInCreatureList && !forceRecalculation) + { + LOG_DEBUG("module.AutoBalance", "AutoBalance::AddCreatureToMapCreatureList: Creature {} ({}) | is already included in map stats.", creature->GetName(), creatureABInfo->UnmodifiedLevel); + + // ensure that this creature is marked active + creatureABInfo->isActive = true; + + // increment the active creature counter + mapABInfo->activeCreatureCount++; + + return; + } + + // only do these additional checks if we still think they need to be applied to the map stats + if (isIncludedInMapStats) + { + // if the creature is vendor, trainer, or has gossip, don't use it to update map stats + if ((creature->IsVendor() || + creature->HasNpcFlag(UNIT_NPC_FLAG_GOSSIP) || + creature->HasNpcFlag(UNIT_NPC_FLAG_QUESTGIVER) || + creature->HasNpcFlag(UNIT_NPC_FLAG_TRAINER) || + creature->HasNpcFlag(UNIT_NPC_FLAG_TRAINER_PROFESSION) || + creature->HasNpcFlag(UNIT_NPC_FLAG_REPAIR) || + creature->HasUnitFlag(UNIT_FLAG_IMMUNE_TO_PC) || + creature->HasUnitFlag(UNIT_FLAG_NOT_SELECTABLE)) && + (!isBossOrBossSummon(creature)) + ) { - LOG_DEBUG("module.AutoBalance", "AutoBalance_AllCreature::AddCreatureToMapData(): {} ({}) is already included in map stats.", creature->GetName(), creatureABInfo->UnmodifiedLevel); + LOG_DEBUG("module.AutoBalance", "AutoBalance::AddCreatureToMapCreatureList: Creature {} ({}) | is a a vendor, trainer, or is otherwise not attackable - do not include in map stats.", creature->GetName(), creatureABInfo->UnmodifiedLevel); + isIncludedInMapStats = false; } else { - LOG_DEBUG("module.AutoBalance", "AutoBalance_AllCreature::AddCreatureToMapData(): {} ({}) is NOT included in map stats.", creature->GetName(), creatureABInfo->UnmodifiedLevel); + // if the creature is friendly to a player, don't use it to update map stats + for (std::vector::const_iterator playerIterator = mapABInfo->allMapPlayers.begin(); playerIterator != mapABInfo->allMapPlayers.end(); ++playerIterator) + { + Player* thisPlayer = *playerIterator; + + // if this player is a Game Master, skip + if (thisPlayer->IsGameMaster()) + { + continue; + } + + // if the creature is friendly and not a boss + if (creature->IsFriendlyTo(thisPlayer) && !isBossOrBossSummon(creature)) + { + LOG_DEBUG("module.AutoBalance", "AutoBalance::AddCreatureToMapCreatureList: Creature {} ({}) | is friendly to {} - do not include in map stats.", + creature->GetName(), + creatureABInfo->UnmodifiedLevel, + thisPlayer->GetName() + ); + isIncludedInMapStats = false; + break; + } + } + + // perform the distance check if an override is configured for this map + if (hasLevelScalingDistanceCheckOverride(instanceMap->GetId())) + { + uint32 distance = levelScalingDistanceCheckOverrides[instanceMap->GetId()]; + bool isPlayerWithinDistance = false; + + for (std::vector::const_iterator playerIterator = mapABInfo->allMapPlayers.begin(); playerIterator != mapABInfo->allMapPlayers.end(); ++playerIterator) + { + Player* thisPlayer = *playerIterator; + + // if this player is a Game Master, skip + if (thisPlayer->IsGameMaster()) + { + continue; + } + + if (thisPlayer->IsWithinDist(creature, 500)) + { + LOG_DEBUG("module.AutoBalance", "AutoBalance::AddCreatureToMapCreatureList: Creature {} ({}) | is in range ({} world units) of player {} and is considered active.", creature->GetName(), creatureABInfo->UnmodifiedLevel, distance, thisPlayer->GetName()); + isPlayerWithinDistance = true; + break; + } + else + { + LOG_DEBUG("module.AutoBalance", "AutoBalance::AddCreatureToMapCreatureList: Creature {} ({}) | is NOT in range ({} world units) of any player and is NOT considered active.", + creature->GetName(), + creatureABInfo->UnmodifiedLevel, + distance + ); + } + } + + // if no players were within the distance, don't include this creature in the map stats + if (!isPlayerWithinDistance) + isIncludedInMapStats = false; + } + } + } + + if (isIncludedInMapStats) + { + // mark this creature as being considered in the map stats + creatureABInfo->isActive = true; + + // update the highest and lowest creature levels + if (creatureABInfo->UnmodifiedLevel > mapABInfo->highestCreatureLevel || mapABInfo->highestCreatureLevel == 0) + mapABInfo->highestCreatureLevel = creatureABInfo->UnmodifiedLevel; + if (creatureABInfo->UnmodifiedLevel < mapABInfo->lowestCreatureLevel || mapABInfo->lowestCreatureLevel == 0) + mapABInfo->lowestCreatureLevel = creatureABInfo->UnmodifiedLevel; + + // calculate the new average creature level + float creatureCount = mapABInfo->activeCreatureCount; + float oldAvgCreatureLevel = mapABInfo->avgCreatureLevel; + float newAvgCreatureLevel = (((float)mapABInfo->avgCreatureLevel * creatureCount) + (float)creatureABInfo->UnmodifiedLevel) / (creatureCount + 1.0f); + + mapABInfo->avgCreatureLevel = newAvgCreatureLevel; + + // increment the active creature counter + mapABInfo->activeCreatureCount++; + + LOG_DEBUG("module.AutoBalance", "AutoBalance::AddCreatureToMapCreatureList: Creature {} ({}) | is included in map stats (active), adjusting avgCreatureLevel to ({})", creature->GetName(), creatureABInfo->UnmodifiedLevel, newAvgCreatureLevel); + + // if the average creature level transitions from one whole number to the next, reset the map's config time so it will refresh + if (round(oldAvgCreatureLevel) != round(newAvgCreatureLevel)) + { + mapABInfo->mapConfigTime = 1; + LOG_DEBUG("module.AutoBalance", "AutoBalance::AddCreatureToMapCreatureList: {} ({}{}) | average creature level changes {}->{}. Force map update. {} ({}{}) map config set to ({}).", + instanceMap->GetMapName(), + instanceMap->GetId(), + instanceMap->GetInstanceId() ? "-" + std::to_string(instanceMap->GetInstanceId()) : "", + round(oldAvgCreatureLevel), + round(newAvgCreatureLevel), + map->GetMapName(), + map->GetId(), + map->GetInstanceId() ? "-" + std::to_string(map->GetInstanceId()) : "", + mapABInfo->mapConfigTime + ); } - LOG_DEBUG("module.AutoBalance", "AutoBalance_AllCreature::AddCreatureToMapData(): There are {} active creatures.", mapABInfo->activeCreatureCount); + LOG_DEBUG("module.AutoBalance", "AutoBalance::AddCreatureToMapCreatureList: There are ({}) creatures included (active) in map stats.", mapABInfo->activeCreatureCount); } } @@ -952,60 +2472,128 @@ void RemoveCreatureFromMapData(Creature* creature) { if (*creatureIteration == creature) { - LOG_DEBUG("module.AutoBalance", "AutoBalance_AllCreature::RemoveCreatureFromMapData(): {} ({}) is in the creature list and will be removed. There are {} creatures left.", creature->GetName(), creature->GetLevel(), mapABInfo->allMapCreatures.size() - 1); + LOG_DEBUG("module.AutoBalance", "AutoBalance::RemoveCreatureFromMapData: Creature {} ({}) | is in the creature list and will be removed. There are {} creatures left.", creature->GetName(), creature->GetLevel(), mapABInfo->allMapCreatures.size() - 1); mapABInfo->allMapCreatures.erase(creatureIteration); // mark this creature as removed AutoBalanceCreatureInfo *creatureABInfo=creature->CustomData.GetDefault("AutoBalanceCreatureInfo"); creatureABInfo->isInCreatureList = false; + + // decrement the active creature counter if they were considered active + if (creatureABInfo->isActive) + { + LOG_DEBUG("module.AutoBalance", "AutoBalance::RemoveCreatureFromMapData: Creature {} ({}) | is no longer active. There are {} active creatures left.", + creature->GetName(), + creature->GetLevel(), + mapABInfo->activeCreatureCount - 1 + ); + + if (mapABInfo->activeCreatureCount > 0) + { + mapABInfo->activeCreatureCount--; + } + else + { + LOG_DEBUG("module.AutoBalance", "AutoBalance::RemoveCreatureFromMapData: Map {} ({}{}) | activeCreatureCount is already 0. This should not happen.", + creature->GetMap()->GetMapName(), + creature->GetMap()->GetId(), + creature->GetMap()->GetInstanceId() ? "-" + std::to_string(creature->GetMap()->GetInstanceId()) : "" + ); + } + } + break; } } } } -void UpdateMapPlayerStats(Map* map, bool adjustPlayerCount = true) +void UpdateMapPlayerStats(Map* map) { + // if this isn't a dungeon instance, just bail out immediately + if (!map->IsDungeon() || !map->GetInstanceId()) + { + return; + } + // get the map's info AutoBalanceMapInfo *mapABInfo=map->CustomData.GetDefault("AutoBalanceMapInfo"); + InstanceMap* instanceMap = map->ToInstanceMap(); - // get the map's player list - Map::PlayerList const &playerList = map->GetPlayers(); - - // if there are players on the map - if (!playerList.IsEmpty()) + // remember some values + uint8 oldPlayerCount = mapABInfo->playerCount; + uint8 oldAdjustedPlayerCount = mapABInfo->adjustedPlayerCount; + + LOG_DEBUG("module.AutoBalance", "AutoBalance::UpdateMapPlayerStats: Map {} ({}{}) | oldPlayerCount = ({}), oldAdjustedPlayerCount = ({}).", + instanceMap->GetMapName(), + instanceMap->GetId(), + instanceMap->GetInstanceId() ? "-" + std::to_string(instanceMap->GetInstanceId()) : "", + oldPlayerCount, + oldAdjustedPlayerCount + ); + + // update the player count + mapABInfo->playerCount = mapABInfo->allMapPlayers.size(); + + LOG_DEBUG("module.AutoBalance", "AutoBalance::UpdateMapPlayerStats: Map {} ({}{}) | playerCount = ({}).", + instanceMap->GetMapName(), + instanceMap->GetId(), + instanceMap->GetInstanceId() ? "-" + std::to_string(instanceMap->GetInstanceId()) : "", + mapABInfo->playerCount + ); + + LOG_DEBUG("module.AutoBalance", "AutoBalance::UpdateMapPlayerStats: Map {} ({}{}) | combatLocked = ({}), combatLockMinPlayers = ({}).", + instanceMap->GetMapName(), + instanceMap->GetId(), + instanceMap->GetInstanceId() ? "-" + std::to_string(instanceMap->GetInstanceId()) : "", + mapABInfo->combatLocked, + mapABInfo->combatLockMinPlayers + ); + + uint8 adjustedPlayerCount = 0; + + // if combat is locked and the new player count is higher than the combat lock, update the combat lock + if + ( + mapABInfo->combatLocked && + mapABInfo->playerCount > oldPlayerCount && + mapABInfo->playerCount > mapABInfo->combatLockMinPlayers + ) { - uint8 highestPlayerLevel = 0; - uint8 lowestPlayerLevel = 0; + // start with the actual player count + adjustedPlayerCount = mapABInfo->playerCount; - // iterate through the players and update the highest and lowest player levels - for (Map::PlayerList::const_iterator playerIteration = playerList.begin(); playerIteration != playerList.end(); ++playerIteration) - { - Player* playerHandle = playerIteration->GetSource(); - if (playerHandle && !playerHandle->IsGameMaster()) - { - if (playerHandle->getLevel() > highestPlayerLevel || highestPlayerLevel == 0) - highestPlayerLevel = playerHandle->getLevel(); + // this is the new floor + mapABInfo->combatLockMinPlayers = mapABInfo->playerCount; - if (playerHandle->getLevel() < lowestPlayerLevel || lowestPlayerLevel == 0) - lowestPlayerLevel = playerHandle->getLevel(); - } - mapABInfo->highestPlayerLevel = highestPlayerLevel; - mapABInfo->lowestPlayerLevel = lowestPlayerLevel; - } + LOG_DEBUG("module.AutoBalance_CombatLocking", "AutoBalance::UpdateMapPlayerStats: Map {} ({}{}) | Combat is locked. Combat floor increased. New floor is ({}).", + instanceMap->GetMapName(), + instanceMap->GetId(), + instanceMap->GetInstanceId() ? "-" + std::to_string(instanceMap->GetInstanceId()) : "", + mapABInfo->combatLockMinPlayers + ); - LOG_DEBUG("module.AutoBalance", "UpdateMapPlayerStats(): Map {} player level range: {} - {}.", map->GetMapName(), mapABInfo->lowestPlayerLevel, mapABInfo->highestPlayerLevel); } - - // update the player count (unless we should specifically skip this step) - if (adjustPlayerCount) + // if combat is otherwise locked + else if (mapABInfo->combatLocked) + { + // start with the saved floor + adjustedPlayerCount = mapABInfo->combatLockMinPlayers ? mapABInfo->combatLockMinPlayers : mapABInfo->playerCount; + + LOG_DEBUG("module.AutoBalance_CombatLocking", "AutoBalance::UpdateMapPlayerStats: Map {} ({}{}) | Combat is locked. Combat floor is ({}).", + instanceMap->GetMapName(), + instanceMap->GetId(), + instanceMap->GetInstanceId() ? "-" + std::to_string(instanceMap->GetInstanceId()) : "", + mapABInfo->combatLockMinPlayers + ); + } + // if combat is not locked + else { - mapABInfo->playerCount = map->GetPlayersCountExceptGMs(); + // start with the actual player count + adjustedPlayerCount = mapABInfo->playerCount; } - // start with the real player count - uint32 adjustedPlayerCount = mapABInfo->playerCount; - // if the adjusted player count is below the min players setting, adjust it if (adjustedPlayerCount < mapABInfo->minPlayers) adjustedPlayerCount = mapABInfo->minPlayers; @@ -1015,24 +2603,279 @@ void UpdateMapPlayerStats(Map* map, bool adjustPlayerCount = true) // store the adjusted player count in the map's info mapABInfo->adjustedPlayerCount = adjustedPlayerCount; + + // if the adjustedPlayerCount changed, schedule this map for a reconfiguration + if (oldAdjustedPlayerCount != mapABInfo->adjustedPlayerCount) + { + mapABInfo->mapConfigTime = 1; + LOG_DEBUG("module.AutoBalance_CombatLocking", "AutoBalance::UpdateMapPlayerStats: Map {} ({}{}) | Player difficulty changes ({}->{}). Force map update. {} ({}{}) map config time set to ({}).", + instanceMap->GetMapName(), + instanceMap->GetId(), + instanceMap->GetInstanceId() ? "-" + std::to_string(instanceMap->GetInstanceId()) : "", + oldAdjustedPlayerCount, + mapABInfo->adjustedPlayerCount, + instanceMap->GetMapName(), + instanceMap->GetId(), + instanceMap->GetInstanceId() ? "-" + std::to_string(instanceMap->GetInstanceId()) : "", + mapABInfo->mapConfigTime + ); + } + + uint8 highestPlayerLevel = 0; + uint8 lowestPlayerLevel = 80; + + // iterate through the players and update the highest and lowest player levels + for (std::vector::const_iterator playerIterator = mapABInfo->allMapPlayers.begin(); playerIterator != mapABInfo->allMapPlayers.end(); ++playerIterator) + { + Player* thisPlayer = *playerIterator; + + if (thisPlayer && !thisPlayer->IsGameMaster()) + { + if (thisPlayer->getLevel() > highestPlayerLevel || highestPlayerLevel == 0) + { + highestPlayerLevel = thisPlayer->getLevel(); + } + + if (thisPlayer->getLevel() < lowestPlayerLevel || lowestPlayerLevel == 0) + { + lowestPlayerLevel = thisPlayer->getLevel(); + } + } + } + + mapABInfo->highestPlayerLevel = highestPlayerLevel; + mapABInfo->lowestPlayerLevel = lowestPlayerLevel; + + if (!highestPlayerLevel) + { + mapABInfo->highestPlayerLevel = mapABInfo->lfgTargetLevel; + mapABInfo->lowestPlayerLevel = mapABInfo->lfgTargetLevel; + + // no non-GM players on the map, disable it + mapABInfo->enabled = false; + LOG_DEBUG("module.AutoBalance", "AutoBalance::UpdateMapPlayerStats: Map {} ({}{}, {}-player {}) | has no non-GM players. Disabling (potentially temporarily).", + map->GetMapName(), + map->GetId(), + map->GetInstanceId() ? "-" + std::to_string(map->GetInstanceId()) : "", + instanceMap->GetMaxPlayers(), + instanceMap->IsHeroic() ? "Heroic" : "Normal" + ); + } + else + { + LOG_DEBUG("module.AutoBalance", "AutoBalance::UpdateMapPlayerStats: Map {} ({}{}, {}-player {}) | has {} player(s) with level range ({})-({}). Difficulty is {} player(s).", + map->GetMapName(), + map->GetId(), + map->GetInstanceId() ? "-" + std::to_string(map->GetInstanceId()) : "", + instanceMap->GetMaxPlayers(), + instanceMap->IsHeroic() ? "Heroic" : "Normal", + mapABInfo->playerCount, + mapABInfo->lowestPlayerLevel, + mapABInfo->highestPlayerLevel, + mapABInfo->adjustedPlayerCount + ); + } +} + +void AddPlayerToMap(Map* map, Player* player) +{ + // get map data + AutoBalanceMapInfo *mapABInfo=map->CustomData.GetDefault("AutoBalanceMapInfo"); + + + if (!player) + { + LOG_DEBUG("module.AutoBalance", "AutoBalance::AddPlayerToMap: Map {} ({}{}) | Player does not exist.", + map->GetMapName(), + map->GetId(), + map->GetInstanceId() ? "-" + std::to_string(map->GetInstanceId()) : "" + ); + return; + } + // player is a GM + else if (player->IsGameMaster()) + { + LOG_DEBUG("module.AutoBalance", "AutoBalance::AddPlayerToMap: Map {} ({}{}) | Game Master ({}) will not be added to the player list.", + map->GetMapName(), + map->GetId(), + map->GetInstanceId() ? "-" + std::to_string(map->GetInstanceId()) : "", + player->GetName() + ); + return; + } + // if this player is already in the map's player list, skip + else if (std::find(mapABInfo->allMapPlayers.begin(), mapABInfo->allMapPlayers.end(), player) != mapABInfo->allMapPlayers.end()) + { + LOG_DEBUG("module.AutoBalance", "AutoBalance::AddPlayerToMap: Player {} ({}) | is already in the map's player list.", + player->GetName(), + player->getLevel() + ); + return; + } + + // add the player to the map's player list + mapABInfo->allMapPlayers.push_back(player); + LOG_DEBUG("module.AutoBalance", "AutoBalance::AddPlayerToMap: Player {} ({}) | added to the map's player list.", player->GetName(), player->getLevel()); + + // update the map's player stats + UpdateMapPlayerStats(map); +} + +bool RemovePlayerFromMap(Map* map, Player* player) +{ + // get map data + AutoBalanceMapInfo *mapABInfo=map->CustomData.GetDefault("AutoBalanceMapInfo"); + + // if this player isn't in the map's player list, skip + if (std::find(mapABInfo->allMapPlayers.begin(), mapABInfo->allMapPlayers.end(), player) == mapABInfo->allMapPlayers.end()) + { + LOG_DEBUG("module.AutoBalance", "AutoBalance::RemovePlayerFromMap: Player {} ({}) | was not in the map's player list.", player->GetName(), player->GetLevel()); + return false; + } + + // remove the player from the map's player list + mapABInfo->allMapPlayers.erase(std::remove(mapABInfo->allMapPlayers.begin(), mapABInfo->allMapPlayers.end(), player), mapABInfo->allMapPlayers.end()); + LOG_DEBUG("module.AutoBalance", "AutoBalance::RemovePlayerFromMap: Player {} ({}) | removed from the map's player list.", player->GetName(), player->GetLevel()); + + // if the map is combat locked, schedule a map update for when combat ends + if (mapABInfo->combatLocked) + { + mapABInfo->combatLockTripped = true; + } + + // update the map's player stats + UpdateMapPlayerStats(map); + + return true; } -void UpdateMapDataIfNeeded(Map* map) +bool UpdateMapDataIfNeeded(Map* map, bool force = false) { // get map data AutoBalanceMapInfo *mapABInfo=map->CustomData.GetDefault("AutoBalanceMapInfo"); // if map needs update - if (mapABInfo->configTime != lastConfigTime) + if (force || mapABInfo->globalConfigTime < globalConfigTime || mapABInfo->mapConfigTime < mapABInfo->globalConfigTime) { - LOG_DEBUG("module.AutoBalance", "UpdateMapLevelIfNeeded(): Map {} config is out of date ({} != {}) and will be updated.", - map->GetMapName(), - mapABInfo->configTime, - lastConfigTime); + + LOG_DEBUG("module.AutoBalance", "AutoBalance::UpdateMapDataIfNeeded: Map {} ({}{}) | globalConfigTime = ({}) | mapConfigTime = ({})", + map->GetMapName(), + map->GetId(), + map->GetInstanceId() ? "-" + std::to_string(map->GetInstanceId()) : "", + mapABInfo->globalConfigTime, + mapABInfo->mapConfigTime + ); + + // update forced + if (force) + { + LOG_DEBUG("module.AutoBalance", "AutoBalance::UpdateMapDataIfNeeded: Map {} ({}{}) | Update forced.", + map->GetMapName(), + map->GetId(), + map->GetInstanceId() ? "-" + std::to_string(map->GetInstanceId()) : "", + mapABInfo->mapConfigTime + ); + } + + // some tracking variables + bool isGlobalConfigOutOfDate = mapABInfo->globalConfigTime < globalConfigTime; + bool isMapConfigOutOfDate = mapABInfo->mapConfigTime < globalConfigTime; + + // if this was triggered by a global config update, redetect players + if (isGlobalConfigOutOfDate) + { + LOG_DEBUG("module.AutoBalance", "AutoBalance::UpdateMapDataIfNeeded: Map {} ({}{}) | Global config is out of date ({} < {}) and will be updated.", + map->GetMapName(), + map->GetId(), + map->GetInstanceId() ? "-" + std::to_string(map->GetInstanceId()) : "", + mapABInfo->globalConfigTime, + globalConfigTime + ); + + LOG_DEBUG("module.AutoBalance", "AutoBalance::UpdateMapDataIfNeeded: Map {} ({}{}) | Will recount players in the map.", + map->GetMapName(), + map->GetId(), + map->GetInstanceId() ? "-" + std::to_string(map->GetInstanceId()) : "" + ); + + // clear the map's player list + mapABInfo->allMapPlayers.clear(); + + // reset the combat lock + mapABInfo->combatLockMinPlayers = 0; + + // get the map's player list + Map::PlayerList const &playerList = map->GetPlayers(); + + // re-count the players in the dungeon + for (Map::PlayerList::const_iterator playerIteration = playerList.begin(); playerIteration != playerList.end(); ++playerIteration) + { + Player* thisPlayer = playerIteration->GetSource(); + + // if the player is in combat, combat lock the map + if (thisPlayer->IsInCombat()) + { + mapABInfo->combatLocked = true; + LOG_DEBUG("module.AutoBalance_CombatLocking", "AutoBalance::UpdateMapDataIfNeeded: Map {} ({}{}) | Player {} is in combat. Map is combat locked.", + map->GetMapName(), + map->GetId(), + map->GetInstanceId() ? "-" + std::to_string(map->GetInstanceId()) : "", + thisPlayer->GetName() + ); + } + + // (conditionally) add the player to the map's player list + AddPlayerToMap(map, thisPlayer); + } + + // map's player count will be updated in UpdateMapPlayerStats below + } + + // map config is out of date + if (isMapConfigOutOfDate) + { + LOG_DEBUG("module.AutoBalance", "AutoBalance::UpdateMapDataIfNeeded: Map {} ({}{}) | Map config is out of date ({} < {}) and will be updated.", + map->GetMapName(), + map->GetId(), + map->GetInstanceId() ? "-" + std::to_string(map->GetInstanceId()) : "", + mapABInfo->mapConfigTime, + globalConfigTime + ); + } // should the map be enabled? - mapABInfo->enabled = ShouldMapBeEnabled(map); - LOG_DEBUG("module.AutoBalance", "UpdateMapLevelIfNeeded(): Map {} is {}.", map->GetMapName(), mapABInfo->enabled ? "enabled" : "disabled"); + bool newEnabled = ShouldMapBeEnabled(map); + + // if this is a transition between enabled states, reset the map's config time so it will refresh + if (mapABInfo->enabled != newEnabled) + { + mapABInfo->mapConfigTime = 1; + + LOG_DEBUG("module.AutoBalance", "AutoBalance::UpdateMapDataIfNeeded: Map {} ({}{}) | Enabled state transitions from {}->{}, map update forced. Map config time set to ({}).", + map->GetMapName(), + map->GetId(), + map->GetInstanceId() ? "-" + std::to_string(map->GetInstanceId()) : "", + mapABInfo->enabled ? "ENABLED" : "DISABLED", + newEnabled ? "ENABLED" : "DISABLED", + mapABInfo->mapConfigTime + ); + } + + // update the enabled state + mapABInfo->enabled = newEnabled; + + if (!mapABInfo->enabled) + { + // mark the config updated to prevent checking the disabled map repeatedly + mapABInfo->globalConfigTime = globalConfigTime; + mapABInfo->mapConfigTime = globalConfigTime; + + LOG_DEBUG("module.AutoBalance", "AutoBalance::UpdateMapDataIfNeeded: Map {} ({}{}) | is disabled.", + map->GetMapName(), + map->GetId(), + map->GetInstanceId() ? "-" + std::to_string(map->GetInstanceId()) : "" + ); + } // load the map's settings LoadMapSettings(map); @@ -1043,33 +2886,119 @@ void UpdateMapDataIfNeeded(Map* map) // if LevelScaling is disabled OR if the average creature level is inside the skip range, // set the map level to the average creature level, rounded to the nearest integer if (!LevelScaling || - !mapABInfo->enabled || ((mapABInfo->avgCreatureLevel <= mapABInfo->highestPlayerLevel + mapABInfo->levelScalingSkipHigherLevels && mapABInfo->levelScalingSkipHigherLevels != 0) && - (mapABInfo->avgCreatureLevel >= mapABInfo->highestPlayerLevel - mapABInfo->levelScalingSkipLowerLevels && mapABInfo->levelScalingSkipLowerLevels != 0)) - ) + (mapABInfo->avgCreatureLevel >= mapABInfo->highestPlayerLevel - mapABInfo->levelScalingSkipLowerLevels && mapABInfo->levelScalingSkipLowerLevels != 0)) + ) { + mapABInfo->prevMapLevel = mapABInfo->mapLevel; mapABInfo->mapLevel = (uint8)(mapABInfo->avgCreatureLevel + 0.5f); mapABInfo->isLevelScalingEnabled = false; - LOG_DEBUG("module.AutoBalance", "UpdateMapLevelIfNeeded(): Map {} scaling is disabled. Map level is now {} (original).", map->GetMapName(), mapABInfo->mapLevel); + + // only log if the mapLevel has changed + if (mapABInfo->prevMapLevel != mapABInfo->mapLevel) + { + LOG_DEBUG("module.AutoBalance", "AutoBalance::UpdateMapDataIfNeeded: Map {} ({}{}, {}-player {}) | Level scaling is disabled. Map level tracking stat updated {}{} (original level).", + map->GetMapName(), + map->GetId(), + map->GetInstanceId() ? "-" + std::to_string(map->GetInstanceId()) : "", + map->ToInstanceMap()->GetMaxPlayers(), + map->ToInstanceMap()->IsHeroic() ? "Heroic" : "Normal", + mapABInfo->mapLevel != mapABInfo->prevMapLevel ? std::to_string(mapABInfo->prevMapLevel) + "->" : "", + mapABInfo->mapLevel + ); + } + } // If the average creature level is lower than the highest player level, // set the map level to the average creature level, rounded to the nearest integer else if (mapABInfo->avgCreatureLevel <= mapABInfo->highestPlayerLevel) { + mapABInfo->prevMapLevel = mapABInfo->mapLevel; mapABInfo->mapLevel = (uint8)(mapABInfo->avgCreatureLevel + 0.5f); mapABInfo->isLevelScalingEnabled = true; - LOG_DEBUG("module.AutoBalance", "UpdateMapLevelIfNeeded(): Map {} scaling is enabled. Map level is now {} (average creature level).", map->GetMapName(), mapABInfo->mapLevel); + + // only log if the mapLevel has changed + if (mapABInfo->prevMapLevel != mapABInfo->mapLevel) + { + LOG_DEBUG("module.AutoBalance", "AutoBalance::UpdateMapDataIfNeeded: Map {} ({}{}, {}-player {}) | Level scaling is enabled. Map level updated ({}{}) (average creature level).", + map->GetMapName(), + map->GetId(), + map->GetInstanceId() ? "-" + std::to_string(map->GetInstanceId()) : "", + map->ToInstanceMap()->GetMaxPlayers(), + map->ToInstanceMap()->IsHeroic() ? "Heroic" : "Normal", + mapABInfo->mapLevel != mapABInfo->prevMapLevel ? std::to_string(mapABInfo->prevMapLevel) + "->" : "", + mapABInfo->mapLevel + ); + } } // caps at the highest player level else { - mapABInfo->mapLevel = mapABInfo->highestPlayerLevel; - mapABInfo->isLevelScalingEnabled = true; - LOG_DEBUG("module.AutoBalance", "UpdateMapLevelIfNeeded(): Map {} scaling is enabled. Map level is now {} (highest player level).", map->GetMapName(), mapABInfo->mapLevel); + mapABInfo->prevMapLevel = mapABInfo->mapLevel; + mapABInfo->mapLevel = mapABInfo->highestPlayerLevel; + mapABInfo->isLevelScalingEnabled = true; + + // only log if the mapLevel has changed + if (mapABInfo->prevMapLevel != mapABInfo->mapLevel) + { + LOG_DEBUG("module.AutoBalance", "AutoBalance::UpdateMapDataIfNeeded: Map {} ({}{}, {}-player {}) | Lcaling is enabled. Map level updated ({}{}) (highest player level).", + map->GetMapName(), + map->GetId(), + map->GetInstanceId() ? "-" + std::to_string(map->GetInstanceId()) : "", + map->ToInstanceMap()->GetMaxPlayers(), + map->ToInstanceMap()->IsHeroic() ? "Heroic" : "Normal", + mapABInfo->mapLevel != mapABInfo->prevMapLevel ? std::to_string(mapABInfo->prevMapLevel) + "->" : "", + mapABInfo->mapLevel + ); + } + } + + // World multipliers only need to be updated if the mapLevel has changed OR if the map config is out of date + if (mapABInfo->prevMapLevel != mapABInfo->mapLevel || isMapConfigOutOfDate) + { + // Update World Health multiplier + // Used for scaling damage against destructible game objects + World_Multipliers health = getWorldMultiplier(map, BaseValueType::AUTOBALANCE_HEALTH); + mapABInfo->worldHealthMultiplier = health.unscaled; + + // Update World Damage or Healing multiplier + // Used for scaling damage and healing between players and/or non-creatures + World_Multipliers damageHealing = getWorldMultiplier(map, BaseValueType::AUTOBALANCE_DAMAGE_HEALING); + mapABInfo->worldDamageHealingMultiplier = damageHealing.unscaled; + mapABInfo->scaledWorldDamageHealingMultiplier = damageHealing.scaled; } // mark the config updated - mapABInfo->configTime = lastConfigTime; + mapABInfo->globalConfigTime = globalConfigTime; + mapABInfo->mapConfigTime = GetCurrentConfigTime(); + + LOG_DEBUG("module.AutoBalance", "AutoBalance::UpdateMapDataIfNeeded: {} ({}{}) | Global config time set to ({}).", + map->GetMapName(), + map->GetId(), + map->GetInstanceId() ? "-" + std::to_string(map->GetInstanceId()) : "", + mapABInfo->globalConfigTime + ); + + LOG_DEBUG("module.AutoBalance", "AutoBalance::UpdateMapDataIfNeeded: {} ({}{}) | Map config time set to ({}).", + map->GetMapName(), + map->GetId(), + map->GetInstanceId() ? "-" + std::to_string(map->GetInstanceId()) : "", + mapABInfo->mapConfigTime + ); + + return true; + } + else + { + // LOG_DEBUG("module.AutoBalance", "AutoBalance::UpdateMapDataIfNeeded: Map {} ({}{}) global config is up to date ({} == {}).", + // map->GetMapName(), + // map->GetId(), + // map->GetInstanceId() ? "-" + std::to_string(map->GetInstanceId()) : "", + // mapABInfo->globalConfigTime, + // globalConfigTime + // ); + + return false; } } @@ -1109,10 +3038,9 @@ class AutoBalance_WorldScript : public WorldScript void OnBeforeConfigLoad(bool /*reload*/) override { SetInitialWorldSettings(); - lastConfigTime = std::chrono::duration_cast(std::chrono::system_clock::now().time_since_epoch()).count(); - } - void OnStartup() override - { + globalConfigTime = GetCurrentConfigTime(); + + LOG_INFO("module.AutoBalance", "AutoBalance::OnBeforeConfigLoad: Config loaded. Global config time set to ({}).", globalConfigTime); } void SetInitialWorldSettings() @@ -1199,9 +3127,9 @@ class AutoBalance_WorldScript : public WorldScript EnableOtherNormal = sConfigMgr->GetOption("AutoBalance.Enable.OtherNormal", sConfigMgr->GetOption("AutoBalance.enable", 1, false)); Enable5MHeroic = sConfigMgr->GetOption("AutoBalance.Enable.5MHeroic", sConfigMgr->GetOption("AutoBalance.enable", 1, false)); - Enable10MHeroic = sConfigMgr->GetOption("AutoBalance.Enable.5MHeroic", sConfigMgr->GetOption("AutoBalance.enable", 1, false)); - Enable25MHeroic = sConfigMgr->GetOption("AutoBalance.Enable.5MHeroic", sConfigMgr->GetOption("AutoBalance.enable", 1, false)); - EnableOtherHeroic = sConfigMgr->GetOption("AutoBalance.Enable.5MHeroic", sConfigMgr->GetOption("AutoBalance.enable", 1, false)); + Enable10MHeroic = sConfigMgr->GetOption("AutoBalance.Enable.10MHeroic", sConfigMgr->GetOption("AutoBalance.enable", 1, false)); + Enable25MHeroic = sConfigMgr->GetOption("AutoBalance.Enable.25MHeroic", sConfigMgr->GetOption("AutoBalance.enable", 1, false)); + EnableOtherHeroic = sConfigMgr->GetOption("AutoBalance.Enable.OtherHeroic", sConfigMgr->GetOption("AutoBalance.enable", 1, false)); // Deprecated setting warning if (sConfigMgr->GetOption("AutoBalance.DungeonsOnly", -1, false) != -1) @@ -1503,6 +3431,11 @@ class AutoBalance_WorldScript : public WorldScript if (sConfigMgr->GetOption("AutoBalance.LevelEndGameBoost", false, false)) LOG_WARN("server.loading", "mod-autobalance: deprecated value `AutoBalance.LevelEndGameBoost` defined in `AutoBalance.conf`. This variable will be removed in a future release. Please see `AutoBalance.conf.dist` for more details."); LevelScalingEndGameBoost = sConfigMgr->GetOption("AutoBalance.LevelScaling.EndGameBoost", sConfigMgr->GetOption("AutoBalance.LevelEndGameBoost", 1, false), true); + if (LevelScalingEndGameBoost) + { + LOG_WARN("server.loading", "mod-autobalance: `AutoBalance.LevelScaling.EndGameBoost` is enabled in the configuration, but is not currently implemented. No effect."); + LevelScalingEndGameBoost = 0; + } // RewardScaling.* // warn the console if deprecated values are detected @@ -1553,263 +3486,942 @@ class AutoBalance_PlayerScript : public PlayerScript } } - virtual void OnLevelChanged(Player* player, uint8 oldlevel) override - { - LOG_DEBUG("module.AutoBalance", "AutoBalance_PlayerScript::OnLevelChanged(): {} has leveled from {} to {}", player->GetName(), oldlevel, player->getLevel()); - if (!player || player->IsGameMaster()) - return; + virtual void OnLevelChanged(Player* player, uint8 oldlevel) override + { + LOG_DEBUG("module.AutoBalance", "AutoBalance:: {}", SPACER); + + LOG_DEBUG("module.AutoBalance", "AutoBalance_PlayerScript::OnLevelChanged: {} has leveled ({}->{})", player->GetName(), oldlevel, player->getLevel()); + if (!player || player->IsGameMaster()) + { + return; + } + + Map* map = player->GetMap(); + + if (!map || !map->IsDungeon()) + { + return; + } + + // update the map's player stats + UpdateMapPlayerStats(map); + + // schedule all creatures for an update + AutoBalanceMapInfo *mapABInfo=map->CustomData.GetDefault("AutoBalanceMapInfo"); + mapABInfo->mapConfigTime = GetCurrentConfigTime(); + } + + void OnGiveXP(Player* player, uint32& amount, Unit* victim, uint8 /*xpSource*/) override + { + Map* map = player->GetMap(); + + // If this isn't a dungeon, make no changes + if (!map->IsDungeon() || !map->GetInstanceId() || !victim) + { + return; + } + + AutoBalanceMapInfo *mapABInfo=map->CustomData.GetDefault("AutoBalanceMapInfo"); + + if (victim && RewardScalingXP && mapABInfo->enabled) + { + Map* map = player->GetMap(); + + AutoBalanceCreatureInfo *creatureABInfo=victim->CustomData.GetDefault("AutoBalanceCreatureInfo"); + + if (map->IsDungeon()) + { + if (RewardScalingMethod == AUTOBALANCE_SCALING_DYNAMIC) + { + LOG_DEBUG("module.AutoBalance", "AutoBalance_PlayerScript::OnGiveXP: Distributing XP from '{}' to '{}' in dynamic mode - {}->{}", + victim->GetName(), player->GetName(), amount, uint32(amount * creatureABInfo->XPModifier)); + amount = uint32(amount * creatureABInfo->XPModifier); + } + else if (RewardScalingMethod == AUTOBALANCE_SCALING_FIXED) + { + // Ensure that the players always get the same XP, even when entering the dungeon alone + auto maxPlayerCount = map->ToInstanceMap()->GetMaxPlayers(); + auto currentPlayerCount = mapABInfo->playerCount; + LOG_DEBUG("module.AutoBalance", "AutoBalance_PlayerScript::OnGiveXP: Distributing XP from '{}' to '{}' in fixed mode - {}->{}", + victim->GetName(), player->GetName(), amount, uint32(amount * creatureABInfo->XPModifier * ((float)currentPlayerCount / maxPlayerCount))); + amount = uint32(amount * creatureABInfo->XPModifier * ((float)currentPlayerCount / maxPlayerCount)); + } + } + } + } + + void OnBeforeLootMoney(Player* player, Loot* loot) override + { + Map* map = player->GetMap(); + + // If this isn't a dungeon, make no changes + if (!map->IsDungeon()) + return; + + AutoBalanceMapInfo *mapABInfo=map->CustomData.GetDefault("AutoBalanceMapInfo"); + ObjectGuid sourceGuid = loot->sourceWorldObjectGUID; + + if (mapABInfo->enabled && RewardScalingMoney) + { + // if the loot source is a creature, honor the modifiers for that creature + if (sourceGuid.IsCreature()) + { + Creature* sourceCreature = ObjectAccessor::GetCreature(*player, sourceGuid); + AutoBalanceCreatureInfo *creatureABInfo=sourceCreature->CustomData.GetDefault("AutoBalanceCreatureInfo"); + + // Dynamic Mode + if (RewardScalingMethod == AUTOBALANCE_SCALING_DYNAMIC) + { + LOG_DEBUG("module.AutoBalance", "AutoBalance_PlayerScript::OnBeforeLootMoney: Distributing money from '{}' in dynamic mode - {}->{}", + sourceCreature->GetName(), loot->gold, uint32(loot->gold * creatureABInfo->MoneyModifier)); + loot->gold = uint32(loot->gold * creatureABInfo->MoneyModifier); + } + // Fixed Mode + else if (RewardScalingMethod == AUTOBALANCE_SCALING_FIXED) + { + // Ensure that the players always get the same money, even when entering the dungeon alone + auto maxPlayerCount = map->ToInstanceMap()->GetMaxPlayers(); + auto currentPlayerCount = mapABInfo->playerCount; + LOG_DEBUG("module.AutoBalance", "AutoBalance_PlayerScript::OnBeforeLootMoney: Distributing money from '{}' in fixed mode - {}->{}", + sourceCreature->GetName(), loot->gold, uint32(loot->gold * creatureABInfo->MoneyModifier * ((float)currentPlayerCount / maxPlayerCount))); + loot->gold = uint32(loot->gold * creatureABInfo->MoneyModifier * ((float)currentPlayerCount / maxPlayerCount)); + } + } + // for all other loot sources, just distribute in Fixed mode as though the instance was full + else + { + auto maxPlayerCount = map->ToInstanceMap()->GetMaxPlayers(); + auto currentPlayerCount = mapABInfo->playerCount; + LOG_DEBUG("module.AutoBalance", "AutoBalance_PlayerScript::OnBeforeLootMoney: Distributing money from a non-creature in fixed mode - {}->{}", + loot->gold, uint32(loot->gold * ((float)currentPlayerCount / maxPlayerCount))); + loot->gold = uint32(loot->gold * ((float)currentPlayerCount / maxPlayerCount)); + } + } + } + + virtual void OnPlayerEnterCombat(Player* player, Unit* /*enemy*/) override + { + Map* map = player->GetMap(); + + // If this isn't a dungeon, no work to do + if (!map || !map->IsDungeon()) + { + return; + } + + LOG_DEBUG("module.AutoBalance_CombatLocking", "AutoBalance_PlayerScript::OnPlayerEnterCombat: {} enters combat.", player->GetName()); + + AutoBalanceMapInfo *mapABInfo = map->CustomData.GetDefault("AutoBalanceMapInfo"); + + // if this map isn't enabled, no work to do + if (!mapABInfo->enabled) + { + return; + } + + // lock the current map + if (!mapABInfo->combatLocked) + { + mapABInfo->combatLocked = true; + mapABInfo->combatLockMinPlayers = mapABInfo->playerCount; + + LOG_DEBUG("module.AutoBalance_CombatLocking", "AutoBalance_PlayerScript::OnPlayerEnterCombat: Map {} ({}{}) | Locking difficulty to no less than ({}) as {} enters combat.", + map->GetMapName(), + map->GetId(), + map->GetInstanceId() ? "-" + std::to_string(map->GetInstanceId()) : "", + mapABInfo->combatLockMinPlayers, + player->GetName() + ); + } + } + + virtual void OnPlayerLeaveCombat(Player* player) override + { + Map* map = player->GetMap(); + + // If this isn't a dungeon, no work to do + if (!map || !map->IsDungeon()) + { + return; + } + + // this hook can get called even if the player isn't in combat + // I believe this happens whenever AC attempts to remove combat, but it doesn't check to see if the player is in combat first + // unfortunately, `player->IsInCombat()` doesn't work here + LOG_DEBUG("module.AutoBalance_CombatLocking", "AutoBalance_PlayerScript::OnPlayerLeaveCombat: {} leaves (or wasn't in) combat.", player->GetName()); + + AutoBalanceMapInfo *mapABInfo=map->CustomData.GetDefault("AutoBalanceMapInfo"); + + // if this map isn't enabled, no work to do + if (!mapABInfo->enabled) + { + return; + } + + // check to see if any of the other players are in combat + bool anyPlayersInCombat = false; + for (auto player : mapABInfo->allMapPlayers) + { + if (player && player->IsInCombat()) + { + anyPlayersInCombat = true; + + LOG_DEBUG("module.AutoBalance_CombatLocking", "AutoBalance_PlayerScript::OnPlayerLeaveCombat: Map {} ({}{}) | Player {} (and potentially others) are still in combat.", + map->GetMapName(), + map->GetId(), + map->GetInstanceId() ? "-" + std::to_string(map->GetInstanceId()) : "", + player->GetName() + ); + + break; + } + } + + // if no players are in combat, unlock the map + if (!anyPlayersInCombat && mapABInfo->combatLocked) + { + mapABInfo->combatLocked = false; + mapABInfo->combatLockMinPlayers = 0; + + LOG_DEBUG("module.AutoBalance_CombatLocking", "AutoBalance_PlayerScript::OnPlayerLeaveCombat: Map {} ({}{}) | Unlocking difficulty as {} leaves combat.", + map->GetMapName(), + map->GetId(), + map->GetInstanceId() ? "-" + std::to_string(map->GetInstanceId()) : "", + player->GetName() + ); + + // if the combat lock needed to be used, notify the players of it lifting + if (mapABInfo->combatLockTripped) + { + for (auto player : mapABInfo->allMapPlayers) + { + if (player && player->GetSession()) + { + ChatHandler(player->GetSession()).PSendSysMessage("|cffc3dbff [AutoBalance]|r|cffFF8000 Combat has ended. Difficulty is no longer locked.|r"); + } + } + } + + // if the number of players changed while combat was in progress, schedule the map for an update + if (mapABInfo->combatLockTripped && mapABInfo->playerCount != mapABInfo->combatLockMinPlayers) + { + mapABInfo->mapConfigTime = 1; + LOG_DEBUG("module.AutoBalance_CombatLocking", "AutoBalance_PlayerScript::OnPlayerLeaveCombat: Map {} ({}{}) | Reset map config time to ({}).", + map->GetMapName(), + map->GetId(), + map->GetInstanceId() ? "-" + std::to_string(map->GetInstanceId()) : "", + mapABInfo->mapConfigTime + ); + + mapABInfo->combatLockTripped = false; + } + } + } +}; + +class AutoBalance_UnitScript : public UnitScript +{ + public: + AutoBalance_UnitScript() + : UnitScript("AutoBalance_UnitScript", true) + { + } + + void ModifyPeriodicDamageAurasTick(Unit* target, Unit* source, uint32& amount, SpellInfo const* spellInfo) override + { + // if the spell is negative (damage), we need to flip the sign + // if the spell is positive (healing or other) we keep it the same + int32 adjustedAmount = !spellInfo->IsPositive() ? amount * -1 : amount; + + // only debug if the source or target is a player + bool _debug_damage_and_healing = ((source && (source->GetTypeId() == TYPEID_PLAYER || source->IsControlledByPlayer())) || (target && target->GetTypeId() == TYPEID_PLAYER)); + _debug_damage_and_healing = (source && source->GetMap()->GetInstanceId()); + + if (_debug_damage_and_healing) _Debug_Output("ModifyPeriodicDamageAurasTick", target, source, adjustedAmount, AUTOBALANCE_DAMAGE_HEALING_DEBUG_PHASE_BEFORE, spellInfo->SpellName[0], spellInfo->Id); + + // set amount to the absolute value of the function call + // the provided amount doesn't indicate whether it's a positive or negative value + adjustedAmount = _Modify_Damage_Healing(target, source, adjustedAmount, spellInfo); + amount = abs(adjustedAmount); + + if (_debug_damage_and_healing) _Debug_Output("ModifyPeriodicDamageAurasTick", target, source, adjustedAmount, AUTOBALANCE_DAMAGE_HEALING_DEBUG_PHASE_AFTER, spellInfo->SpellName[0], spellInfo->Id); + } + + void ModifySpellDamageTaken(Unit* target, Unit* source, int32& amount, SpellInfo const* spellInfo) override + { + // if the spell is negative (damage), we need to flip the sign to negative + // if the spell is positive (healing or other) we keep it the same (positive) + int32 adjustedAmount = !spellInfo->IsPositive() ? amount * -1 : amount; + + // only debug if the source or target is a player + bool _debug_damage_and_healing = ((source && (source->GetTypeId() == TYPEID_PLAYER || source->IsControlledByPlayer())) || (target && target->GetTypeId() == TYPEID_PLAYER)); + _debug_damage_and_healing = (source && source->GetMap()->GetInstanceId()); + + if (_debug_damage_and_healing) _Debug_Output("ModifySpellDamageTaken", target, source, adjustedAmount, AUTOBALANCE_DAMAGE_HEALING_DEBUG_PHASE_BEFORE, spellInfo->SpellName[0], spellInfo->Id); + + // set amount to the absolute value of the function call + // the provided amount doesn't indicate whether it's a positive or negative value + adjustedAmount = _Modify_Damage_Healing(target, source, adjustedAmount, spellInfo); + amount = abs(adjustedAmount); + + if (_debug_damage_and_healing) _Debug_Output("ModifySpellDamageTaken", target, source, adjustedAmount, AUTOBALANCE_DAMAGE_HEALING_DEBUG_PHASE_AFTER, spellInfo->SpellName[0], spellInfo->Id); + } + + void ModifyMeleeDamage(Unit* target, Unit* source, uint32& amount) override + { + // melee damage is always negative, so we need to flip the sign to negative + int32 adjustedAmount = amount * -1; + + // only debug if the source or target is a player + bool _debug_damage_and_healing = ((source && (source->GetTypeId() == TYPEID_PLAYER || source->IsControlledByPlayer())) || (target && target->GetTypeId() == TYPEID_PLAYER)); + _debug_damage_and_healing = (source && source->GetMap()->GetInstanceId()); + + if (_debug_damage_and_healing) _Debug_Output("ModifyMeleeDamage", target, source, adjustedAmount, AUTOBALANCE_DAMAGE_HEALING_DEBUG_PHASE_BEFORE, "Melee"); + + // set amount to the absolute value of the function call + adjustedAmount = _Modify_Damage_Healing(target, source, adjustedAmount); + amount = abs(adjustedAmount); + + if (_debug_damage_and_healing) _Debug_Output("ModifyMeleeDamage", target, source, adjustedAmount, AUTOBALANCE_DAMAGE_HEALING_DEBUG_PHASE_AFTER, "Melee"); + } + + void ModifyHealReceived(Unit* target, Unit* source, uint32& amount, SpellInfo const* spellInfo) override + { + // healing is always positive, no need for any sign flip + + // only debug if the source or target is a player + bool _debug_damage_and_healing = ((source && (source->GetTypeId() == TYPEID_PLAYER || source->IsControlledByPlayer())) || (target && target->GetTypeId() == TYPEID_PLAYER)); + _debug_damage_and_healing = (source && source->GetMap()->GetInstanceId()); + + if (_debug_damage_and_healing) _Debug_Output("ModifyHealReceived", target, source, amount, AUTOBALANCE_DAMAGE_HEALING_DEBUG_PHASE_BEFORE, spellInfo->SpellName[0], spellInfo->Id); + + amount = _Modify_Damage_Healing(target, source, amount, spellInfo); + + if (_debug_damage_and_healing) _Debug_Output("ModifyHealReceived", target, source, amount, AUTOBALANCE_DAMAGE_HEALING_DEBUG_PHASE_AFTER, spellInfo->SpellName[0], spellInfo->Id); + } + + void OnAuraApply(Unit* unit, Aura* aura) override { + // only debug if the source or target is a player + bool _debug_damage_and_healing = (unit && unit->GetTypeId() == TYPEID_PLAYER); + _debug_damage_and_healing = (unit && unit->GetMap()->GetInstanceId()); + + // Only if this aura has a duration + if (aura && (aura->GetDuration() > 0 || aura->GetMaxDuration() > 0)) + { + uint32 auraDuration = _Modifier_CCDuration(unit, aura->GetCaster(), aura); + + // only update if we decided to change it + if (auraDuration != (float)aura->GetDuration()) + { + if (_debug_damage_and_healing) LOG_DEBUG("module.AutoBalance_DamageHealingCC", "AutoBalance_UnitScript::OnAuraApply(): Spell '{}' had it's duration adjusted ({}->{}).", + aura->GetSpellInfo()->SpellName[0], + aura->GetMaxDuration()/1000, + auraDuration/1000 + ); + + aura->SetMaxDuration(auraDuration); + aura->SetDuration(auraDuration); + } + } + } + + private: + [[maybe_unused]] bool _debug_damage_and_healing = false; // defaults to false, overwritten in each function + + void _Debug_Output(std::string function_name, Unit* target, Unit* source, int32 amount, Damage_Healing_Debug_Phase phase, std::string spell_name = "Unknown Spell", uint32 spell_id = 0) + { + if (phase == AUTOBALANCE_DAMAGE_HEALING_DEBUG_PHASE_BEFORE) + { + LOG_DEBUG("module.AutoBalance_DamageHealingCC", "AutoBalance:: {}", SPACER); + } + + if (target && source && amount) + { + LOG_DEBUG("module.AutoBalance_DamageHealingCC", "AutoBalance_UnitScript::{}: {}: {}{} {} {}{} with {}{} for ({})", + function_name, + phase ? "AFTER" : "BEFORE", + source->GetName(), + source->GetEntry() ? " (" + std::to_string(source->GetEntry()) + ")" : "", + amount > 0 ? "heals" : "damages", + target->GetName(), + target->GetEntry() ? " (" + std::to_string(target->GetEntry()) + ")" : "", + spell_name, + spell_id ? " (" + std::to_string(spell_id) + ")" : "", + amount + ); + } + else if (target && source) + { + LOG_DEBUG("module.AutoBalance_DamageHealingCC", "AutoBalance_UnitScript::{}: {}: {}{} damages {}{} with {}{} for (0)", + function_name, + phase ? "AFTER" : "BEFORE", + source->GetName(), + source->GetEntry() ? " (" + std::to_string(source->GetEntry()) + ")" : "", + target->GetName(), + target->GetEntry() ? " (" + std::to_string(target->GetEntry()) + ")" : "", + spell_name, + spell_id ? " (" + std::to_string(spell_id) + ")" : "" + ); + } + else if (target && amount) + { + LOG_DEBUG("module.AutoBalance_DamageHealingCC", "AutoBalance_UnitScript::{}: {}: ?? {} {}{} with {}{} for ({})", + function_name, + phase ? "AFTER" : "BEFORE", + amount > 0 ? "heals" : "damages", + target->GetName(), + target->GetEntry() ? " (" + std::to_string(target->GetEntry()) + ")" : "", + spell_name, + spell_id ? " (" + std::to_string(spell_id) + ")" : "", + amount + ); + } + else if (target) + { + LOG_DEBUG("module.AutoBalance_DamageHealingCC", "AutoBalance_UnitScript::{}: {}: ?? affects {}{} with {}{}", + function_name, + phase ? "AFTER" : "BEFORE", + target->GetName(), + target->GetEntry() ? " (" + std::to_string(target->GetEntry()) + ")" : "", + spell_name, + spell_id ? " (" + std::to_string(spell_id) + ")" : "" + ); + } + else + { + LOG_DEBUG("module.AutoBalance_DamageHealingCC", "AutoBalance_UnitScript::{}: {}: W? T? F? with {}{}", + function_name, + phase ? "AFTER" : "BEFORE", + spell_name, + spell_id ? " (" + std::to_string(spell_id) + ")" : "" + ); + } + } + + int32 _Modify_Damage_Healing(Unit* target, Unit* source, int32 amount, SpellInfo const* spellInfo = nullptr) + { + // + // Pre-flight Checks + // + + // only debug if the source or target is a player + bool _debug_damage_and_healing = ((source && (source->GetTypeId() == TYPEID_PLAYER || source->IsControlledByPlayer())) || (target && target->GetTypeId() == TYPEID_PLAYER)); + _debug_damage_and_healing = (source && source->GetMap()->GetInstanceId()); + + // check that we're enabled globally, else return the original value + if (!EnableGlobal) + { + if (_debug_damage_and_healing) + LOG_DEBUG("module.AutoBalance_DamageHealingCC", "AutoBalance_UnitScript::_Modify_Damage_Healing: EnableGlobal is false, returning original value of ({}).", amount); + + return amount; + } + + // if the source is gone (logged off? despawned?), use the same target and source. + // hacky, but better than crashing or having the damage go to 1.0x + if (!source) + { + if (_debug_damage_and_healing) + LOG_DEBUG("module.AutoBalance_DamageHealingCC", "AutoBalance_UnitScript::_Modify_Damage_Healing: Source is null, using target as source."); + + source = target; + } + + // make sure the source and target are in an instance, else return the original damage + if (!(source->GetMap()->IsDungeon() && target->GetMap()->IsDungeon())) + { + //if (_debug_damage_and_healing) + // LOG_DEBUG("module.AutoBalance_DamageHealingCC", "AutoBalance_UnitScript::_Modify_Damage_Healing: Not in an instance, returning original value of ({}).", amount); + + return amount; + } + + // make sure that the source is in the world, else return the original value + if (!source->IsInWorld()) + { + if (_debug_damage_and_healing) + LOG_DEBUG("module.AutoBalance_DamageHealingCC", "AutoBalance_UnitScript::_Modify_Damage_Healing: Source does not exist in the world, returning original value of ({}).", amount); + + return amount; + } + + // if the spell ID is in our "never modify" list, return the original value + if + ( + spellInfo && + spellInfo->Id && + std::find + ( + spellIdsToNeverModify.begin(), + spellIdsToNeverModify.end(), + spellInfo->Id + ) != spellIdsToNeverModify.end() + ) + { + if (_debug_damage_and_healing) + LOG_DEBUG("module.AutoBalance_DamageHealingCC", "AutoBalance_UnitScript::_Modify_Damage_Healing: Spell {}({}) is in the never modify list, returning original value of ({}).", + spellInfo->SpellName[0], + spellInfo->Id, + amount + ); + + return amount; + } + + // get the maps' info + AutoBalanceMapInfo *sourceMapABInfo = source->GetMap()->CustomData.GetDefault("AutoBalanceMapInfo"); + AutoBalanceMapInfo *targetMapABInfo = target->GetMap()->CustomData.GetDefault("AutoBalanceMapInfo"); + + // if either the target or the source's maps are not enabled, return the original damage + if (!sourceMapABInfo->enabled || !targetMapABInfo->enabled) + { + if (_debug_damage_and_healing) + LOG_DEBUG("module.AutoBalance_DamageHealingCC", "AutoBalance_UnitScript::_Modify_Damage_Healing: Source or Target's map is not enabled, returning original value of ({}).", amount); - Map* map = player->GetMap(); + return amount; + } - if (!map || !map->IsDungeon()) - return; + // + // Source and Target Checking + // - // first update the map's player stats - UpdateMapPlayerStats(map); + // if the source is a player and they are healing themselves, return the original value + if (source->GetTypeId() == TYPEID_PLAYER && source->GetGUID() == target->GetGUID() && amount >= 0) + { + if (_debug_damage_and_healing) + LOG_DEBUG("module.AutoBalance_DamageHealingCC", "AutoBalance_UnitScript::_Modify_Damage_Healing: Source is a player that is self-healing, returning original value of ({}).", amount); - // schedule all creatures for an update - lastConfigTime = std::chrono::duration_cast(std::chrono::system_clock::now().time_since_epoch()).count(); - } + return amount; + } + // if the source is a player and they are damaging themselves, log to debug but continue + else if (source->GetTypeId() == TYPEID_PLAYER && source->GetGUID() == target->GetGUID() && amount < 0) + { + // if the spell used is in our list of spells to ignore, return the original value + if + ( + spellInfo && + spellInfo->Id && + std::find + ( + spellIdsThatSpendPlayerHealth.begin(), + spellIdsThatSpendPlayerHealth.end(), + spellInfo->Id + ) != spellIdsThatSpendPlayerHealth.end() + ) + { + if (_debug_damage_and_healing) + LOG_DEBUG("module.AutoBalance_DamageHealingCC", "AutoBalance_UnitScript::_Modify_Damage_Healing: Source is a player that is self-damaging with a spell that is ignored, returning original value of ({}).", amount); - void OnGiveXP(Player* player, uint32& amount, Unit* victim, uint8 /*xpSource*/) override - { - Map* map = player->GetMap(); + return amount; + } - // If this isn't a dungeon, make no changes - if (!map->IsDungeon() || !victim) - return; + if (_debug_damage_and_healing) + LOG_DEBUG("module.AutoBalance_DamageHealingCC", "AutoBalance_UnitScript::_Modify_Damage_Healing: Source is a player that is self-damaging, continuing."); + } + // if the source is a player and they are damaging unit that is friendly, log to debug but continue + else if (source->GetTypeId() == TYPEID_PLAYER && target->IsFriendlyTo(source) && amount < 0) + { + if (_debug_damage_and_healing) + LOG_DEBUG("module.AutoBalance_DamageHealingCC", "AutoBalance_UnitScript::_Modify_Damage_Healing: Source is a player that is damaging a friendly unit, continuing."); + } + // if the source is a player under any other condition, return the original value + else if (source->GetTypeId() == TYPEID_PLAYER) + { + if (_debug_damage_and_healing) + LOG_DEBUG("module.AutoBalance_DamageHealingCC", "AutoBalance_UnitScript::_Modify_Damage_Healing: Source is an enemy player, returning original value of ({}).", amount); - AutoBalanceMapInfo *mapABInfo=map->CustomData.GetDefault("AutoBalanceMapInfo"); + return amount; + } + // if the creature is attacking itself with an aura with effect type SPELL_AURA_SHARE_DAMAGE_PCT, return the orginal damage + else if + ( + source->GetTypeId() == TYPEID_UNIT && + source->GetTypeId() != TYPEID_PLAYER && + source->GetGUID() == target->GetGUID() && + _isAuraWithEffectType(spellInfo, SPELL_AURA_SHARE_DAMAGE_PCT) + ) + { + if (_debug_damage_and_healing) + LOG_DEBUG("module.AutoBalance_DamageHealingCC", "AutoBalance_UnitScript::_Modify_Damage_Healing: Source is a creature that is self-damaging with an aura that shares damage, returning original value of ({}).", amount); - if (victim && RewardScalingXP && mapABInfo->enabled) + return amount; + } + + // if the source is under the control of the player, return the original damage + // noteably, this should NOT include mind control targets + if ((source->IsHunterPet() || source->IsPet() || source->IsSummon()) && source->IsControlledByPlayer()) { - Map* map = player->GetMap(); + if (_debug_damage_and_healing) + LOG_DEBUG("module.AutoBalance_DamageHealingCC", "AutoBalance_UnitScript::_Modify_Damage_Healing: Source is a player-controlled pet or summon, returning original value of ({}).", amount); - AutoBalanceCreatureInfo *creatureABInfo=victim->CustomData.GetDefault("AutoBalanceCreatureInfo"); + return amount; + } - if (map->IsDungeon()) + // + // Multiplier calculation + // + float damageMultiplier = 1.0f; + + // if the source is a player AND the target is that same player AND the value is damage (negative), use the map's multiplier + if (source->GetTypeId() == TYPEID_PLAYER && source->GetGUID() == target->GetGUID() && amount < 0) + { + // if this aura damages based on a percent of the player's max health, use the un-level-scaled multiplier + if (_isAuraWithEffectType(spellInfo, SPELL_AURA_PERIODIC_DAMAGE_PERCENT)) { - if (RewardScalingMethod == AUTOBALANCE_SCALING_DYNAMIC) + damageMultiplier = sourceMapABInfo->worldDamageHealingMultiplier; + if (_debug_damage_and_healing) { - LOG_DEBUG("module.AutoBalance", "AutoBalance_PlayerScript::OnGiveXP(): Distributing XP from '{}' to '{}' in dynamic mode - {}->{}", - victim->GetName(), player->GetName(), amount, uint32(amount * creatureABInfo->XPModifier)); - amount = uint32(amount * creatureABInfo->XPModifier); + LOG_DEBUG("module.AutoBalance_DamageHealingCC", "AutoBalance_UnitScript::_Modify_Damage_Healing: Spell damage based on percent of max health. Ignore level scaling."); + LOG_DEBUG("module.AutoBalance_DamageHealingCC", + "AutoBalance_UnitScript::_Modify_Damage_Healing: Source is a player and the target is that same player, using the map's (level-scaling ignored) multiplier: ({})", + damageMultiplier + ); } - else if (RewardScalingMethod == AUTOBALANCE_SCALING_FIXED) + } + else + { + damageMultiplier = sourceMapABInfo->scaledWorldDamageHealingMultiplier; + if (_debug_damage_and_healing) { - // Ensure that the players always get the same XP, even when entering the dungeon alone - auto maxPlayerCount = ((InstanceMap*)sMapMgr->FindMap(map->GetId(), map->GetInstanceId()))->GetMaxPlayers(); - auto currentPlayerCount = map->GetPlayersCountExceptGMs(); - LOG_DEBUG("module.AutoBalance", "AutoBalance_PlayerScript::OnGiveXP(): Distributing XP from '{}' to '{}' in fixed mode - {}->{}", - victim->GetName(), player->GetName(), amount, uint32(amount * creatureABInfo->XPModifier * ((float)currentPlayerCount / maxPlayerCount))); - amount = uint32(amount * creatureABInfo->XPModifier * ((float)currentPlayerCount / maxPlayerCount)); + LOG_DEBUG("module.AutoBalance_DamageHealingCC", + "AutoBalance_UnitScript::_Modify_Damage_Healing: Source is a player and the target is that same player, using the map's multiplier: ({})", + damageMultiplier + ); + } + } + } + // if the target is a player AND the value is healing (positive), use the map's damage multiplier + // (player to player healing was already eliminated in the Source and Target Checking section) + else if (target->GetTypeId() == TYPEID_PLAYER && amount >= 0) + { + damageMultiplier = targetMapABInfo->scaledWorldDamageHealingMultiplier; + if (_debug_damage_and_healing) + { + LOG_DEBUG("module.AutoBalance_DamageHealingCC", + "AutoBalance_UnitScript::_Modify_Damage_Healing: A non-player is healing a player, using the map's multiplier: ({})", + damageMultiplier + ); + } + } + // if the target is a player AND the source is not a creature, use the map's multiplier + else if (target->GetTypeId() == TYPEID_PLAYER && source->GetTypeId() != TYPEID_UNIT && amount < 0) + { + // if this aura damages based on a percent of the player's max health, use the un-level-scaled multiplier + if (_isAuraWithEffectType(spellInfo, SPELL_AURA_PERIODIC_DAMAGE_PERCENT)) + { + damageMultiplier = targetMapABInfo->worldDamageHealingMultiplier; + if (_debug_damage_and_healing) + { + LOG_DEBUG("module.AutoBalance_DamageHealingCC", "AutoBalance_UnitScript::_Modify_Damage_Healing: Spell damage based on percent of max health. Ignore level scaling."); + LOG_DEBUG("module.AutoBalance_DamageHealingCC", + "AutoBalance_UnitScript::_Modify_Damage_Healing: Target is a player and the source is not a creature, using the map's (level-scaling-ignored) multiplier: ({})", + damageMultiplier + ); + } + } + else + { + damageMultiplier = targetMapABInfo->scaledWorldDamageHealingMultiplier; + if (_debug_damage_and_healing) + { + LOG_DEBUG("module.AutoBalance_DamageHealingCC", + "AutoBalance_UnitScript::_Modify_Damage_Healing: Target is a player and the source is not a creature, using the map's multiplier: ({})", + damageMultiplier + ); + } + } + } + // otherwise, use the source creature's damage multiplier + else + { + // if this aura damages based on a percent of the player's max health, use the un-level-scaled multiplier + if (_isAuraWithEffectType(spellInfo, SPELL_AURA_PERIODIC_DAMAGE_PERCENT)) + { + damageMultiplier = source->CustomData.GetDefault("AutoBalanceCreatureInfo")->DamageMultiplier; + if (_debug_damage_and_healing) + { + LOG_DEBUG("module.AutoBalance_DamageHealingCC", "AutoBalance_UnitScript::_Modify_Damage_Healing: Spell damage based on percent of max health. Ignore level scaling."); + LOG_DEBUG("module.AutoBalance_DamageHealingCC", + "AutoBalance_UnitScript::_Modify_Damage_Healing: Using the source creature's (level-scaling ignored) damage multiplier: ({})", + damageMultiplier + ); + } + } + // non percent-based, used the normal multiplier + else + { + damageMultiplier = source->CustomData.GetDefault("AutoBalanceCreatureInfo")->ScaledDamageMultiplier; + if (_debug_damage_and_healing) + { + LOG_DEBUG("module.AutoBalance_DamageHealingCC", + "AutoBalance_UnitScript::_Modify_Damage_Healing: Using the source creature's damage multiplier: ({})", + damageMultiplier + ); } } } + + // we are good to go, return the original damage times the multiplier + if (_debug_damage_and_healing) + LOG_DEBUG("module.AutoBalance_DamageHealingCC", "AutoBalance_UnitScript::_Modify_Damage_Healing: Returning modified {}: ({}) * ({}) = ({})", + amount <= 0 ? "damage" : "healing", + amount, + damageMultiplier, + amount * damageMultiplier + ); + + return amount * damageMultiplier; } - void OnBeforeLootMoney(Player* player, Loot* loot) override + uint32 _Modifier_CCDuration(Unit* target, Unit* caster, Aura* aura) { - Map* map = player->GetMap(); + // store the original duration of the aura + float originalDuration = (float)aura->GetDuration(); - // If this isn't a dungeon, make no changes - if (!map->IsDungeon()) - return; + // check that we're enabled globally, else return the original duration + if (!EnableGlobal) + return originalDuration; + + // ensure that both the target and the caster are defined + if (!target || !caster) + return originalDuration; + + // if the aura wasn't cast just now, don't change it + if (aura->GetDuration() != aura->GetMaxDuration()) + return originalDuration; + + // if the target isn't a player or the caster is a player, return the original duration + if (!target->IsPlayer() || caster->IsPlayer()) + return originalDuration; + + // make sure we're in an instance, else return the original duration + if (!(target->GetMap()->IsDungeon() && caster->GetMap()->IsDungeon())) + return originalDuration; + + // get the current creature's CC duration multiplier + float ccDurationMultiplier = caster->CustomData.GetDefault("AutoBalanceCreatureInfo")->CCDurationMultiplier; + + // if it's the default of 1.0, return the original damage + if (ccDurationMultiplier == 1) + return originalDuration; + + // if the aura was cast by a pet or summon, return the original duration + if ((caster->IsHunterPet() || caster->IsPet() || caster->IsSummon()) && caster->IsControlledByPlayer()) + return originalDuration; + + // only if this aura is a CC + if ( + aura->HasEffectType(SPELL_AURA_MOD_CHARM) || + aura->HasEffectType(SPELL_AURA_MOD_CONFUSE) || + aura->HasEffectType(SPELL_AURA_MOD_DISARM) || + aura->HasEffectType(SPELL_AURA_MOD_FEAR) || + aura->HasEffectType(SPELL_AURA_MOD_PACIFY) || + aura->HasEffectType(SPELL_AURA_MOD_POSSESS) || + aura->HasEffectType(SPELL_AURA_MOD_SILENCE) || + aura->HasEffectType(SPELL_AURA_MOD_STUN) || + aura->HasEffectType(SPELL_AURA_MOD_SPEED_SLOW_ALL) + ) + { + return originalDuration * ccDurationMultiplier; + } + else + { + return originalDuration; + } + } - AutoBalanceMapInfo *mapABInfo=map->CustomData.GetDefault("AutoBalanceMapInfo"); - ObjectGuid sourceGuid = loot->sourceWorldObjectGUID; + bool _isAuraWithEffectType(SpellInfo const* spellInfo, AuraType auraType, bool log = false) + { + // if the spell is not defined, return false + if (!spellInfo) + { + if (log) { LOG_DEBUG("module.AutoBalance_DamageHealingCC", "AutoBalance_UnitScript::_isAuraWithEffectType: SpellInfo is null, returning false."); } + return false; + } - if (mapABInfo->enabled && RewardScalingMoney) + // if the spell doesn't have any effects, return false + if (!spellInfo->GetEffects().size()) { - // if the loot source is a creature, honor the modifiers for that creature - if (sourceGuid.IsCreature()) - { - Creature* sourceCreature = ObjectAccessor::GetCreature(*player, sourceGuid); - AutoBalanceCreatureInfo *creatureABInfo=sourceCreature->CustomData.GetDefault("AutoBalanceCreatureInfo"); + if (log) { LOG_DEBUG("module.AutoBalance_DamageHealingCC", "AutoBalance_UnitScript::_isAuraWithEffectType: SpellInfo has no effects, returning false."); } + return false; + } - // Dynamic Mode - if (RewardScalingMethod == AUTOBALANCE_SCALING_DYNAMIC) - { - LOG_DEBUG("module.AutoBalance", "AutoBalance_PlayerScript::OnBeforeLootMoney(): Distributing money from '{}' in dynamic mode - {}->{}", - sourceCreature->GetName(), loot->gold, uint32(loot->gold * creatureABInfo->MoneyModifier)); - loot->gold = uint32(loot->gold * creatureABInfo->MoneyModifier); - } - // Fixed Mode - else if (RewardScalingMethod == AUTOBALANCE_SCALING_FIXED) - { - // Ensure that the players always get the same money, even when entering the dungeon alone - auto maxPlayerCount = ((InstanceMap*)sMapMgr->FindMap(map->GetId(), map->GetInstanceId()))->GetMaxPlayers(); - auto currentPlayerCount = map->GetPlayersCountExceptGMs(); - LOG_DEBUG("module.AutoBalance", "AutoBalance_PlayerScript::OnBeforeLootMoney(): Distributing money from '{}' in fixed mode - {}->{}", - sourceCreature->GetName(), loot->gold, uint32(loot->gold * creatureABInfo->MoneyModifier * ((float)currentPlayerCount / maxPlayerCount))); - loot->gold = uint32(loot->gold * creatureABInfo->MoneyModifier * ((float)currentPlayerCount / maxPlayerCount)); - } + // iterate through the spell effects + for (SpellEffectInfo effect : spellInfo->GetEffects()) + { + // if the effect is not an aura, continue to next effect + if (!effect.IsAura()) + { + if (log) { LOG_DEBUG("module.AutoBalance_DamageHealingCC", "AutoBalance_UnitScript::_isAuraWithEffectType: SpellInfo has an effect that is not an aura, continuing to next effect."); } + continue; } - // for all other loot sources, just distribute in Fixed mode as though the instance was full - else + + if (effect.ApplyAuraName == auraType) { - auto maxPlayerCount = ((InstanceMap*)sMapMgr->FindMap(map->GetId(), map->GetInstanceId()))->GetMaxPlayers(); - auto currentPlayerCount = map->GetPlayersCountExceptGMs(); - LOG_DEBUG("module.AutoBalance", "AutoBalance_PlayerScript::OnBeforeLootMoney(): Distributing money from a non-creature in fixed mode - {}->{}", - loot->gold, uint32(loot->gold * ((float)currentPlayerCount / maxPlayerCount))); - loot->gold = uint32(loot->gold * ((float)currentPlayerCount / maxPlayerCount)); + // if the effect is an aura of the target type, return true + LOG_DEBUG("module.AutoBalance_DamageHealingCC", "AutoBalance_UnitScript::_isAuraWithEffectType: SpellInfo has an aura of the target type, returning true."); + return true; } } + + // if no aura effect of type auraType was found, return false + if (log) { LOG_DEBUG("module.AutoBalance_DamageHealingCC", "AutoBalance_UnitScript::_isAuraWithEffectType: SpellInfo has no aura of the target type, returning false."); } + return false; } }; -class AutoBalance_UnitScript : public UnitScript +class AutoBalance_GameObjectScript : public AllGameObjectScript { public: - AutoBalance_UnitScript() - : UnitScript("AutoBalance_UnitScript", true) - { - } + AutoBalance_GameObjectScript() + : AllGameObjectScript("AutoBalance_GameObjectScript") + {} - uint32 DealDamage(Unit* AttackerUnit, Unit *playerVictim, uint32 damage, DamageEffectType /*damagetype*/) override - { - return _Modifer_DealDamage(playerVictim, AttackerUnit, damage); - } + void OnGameObjectModifyHealth(GameObject* target, Unit* source, int32& amount, SpellInfo const* spellInfo) override + { + // uncomment to debug this hook + bool _debug_damage_and_healing = (source && target && (source->GetTypeId() == TYPEID_PLAYER || source->IsControlledByPlayer())); - void ModifyPeriodicDamageAurasTick(Unit* target, Unit* attacker, uint32& damage, SpellInfo const* /*spellInfo*/) override - { - damage = _Modifer_DealDamage(target, attacker, damage); - } + if (_debug_damage_and_healing) _Debug_Output("OnGameObjectModifyHealth", target, source, amount, "BEFORE:", spellInfo->SpellName[0], spellInfo->Id); - void ModifySpellDamageTaken(Unit* target, Unit* attacker, int32& damage, SpellInfo const* /*spellInfo*/) override - { - damage = _Modifer_DealDamage(target, attacker, damage); - } + // modify the amount + amount = _Modify_GameObject_Damage_Healing(target, source, amount); - void ModifyMeleeDamage(Unit* target, Unit* attacker, uint32& damage) override - { - damage = _Modifer_DealDamage(target, attacker, damage); - } + if (_debug_damage_and_healing) _Debug_Output("OnGameObjectModifyHealth", target, source, amount, "AFTER:", spellInfo->SpellName[0], spellInfo->Id); + } - void ModifyHealReceived(Unit* target, Unit* attacker, uint32& damage, SpellInfo const* /*spellInfo*/) override - { - damage = _Modifer_DealDamage(target, attacker, damage); - } + private: - void OnAuraApply(Unit* unit, Aura* aura) override { - // Only if this aura has a duration - if (aura->GetDuration() > 0 || aura->GetMaxDuration() > 0) - { - uint32 auraDuration = _Modifier_CCDuration(unit, aura->GetCaster(), aura); + [[maybe_unused]] bool _debug_damage_and_healing = false; // defaults to false, overwritten in each function - // only update if we decided to change it - if (auraDuration != (float)aura->GetDuration()) + void _Debug_Output(std::string function_name, GameObject* target, Unit* source, int32 amount, std::string prefix = "", std::string spell_name = "Unknown Spell", uint32 spell_id = 0) + { + if (target && source && amount) + { + LOG_DEBUG("module.AutoBalance_DamageHealingCC", "AutoBalance_GameObjectScript::{}: {} {} {} {} ({} - {})", + function_name, + prefix, + source->GetName(), + amount, + target->GetName(), + spell_name, + spell_id + ); + } + else if (target && source) + { + LOG_DEBUG("module.AutoBalance_DamageHealingCC", "AutoBalance_GameObjectScript::{}: {} {} 0 {} ({} - {})", + function_name, + prefix, + source->GetName(), + target->GetName(), + spell_name, + spell_id + ); + } + else if (target && amount) + { + LOG_DEBUG("module.AutoBalance_DamageHealingCC", "AutoBalance_GameObjectScript::{}: {} ?? {} {} ({} - {})", + function_name, + prefix, + amount, + target->GetName(), + spell_name, + spell_id + ); + } + else if (target) + { + LOG_DEBUG("module.AutoBalance_DamageHealingCC", "AutoBalance_GameObjectScript::{}: {} ?? ?? {} ({} - {})", + function_name, + prefix, + target->GetName(), + spell_name, + spell_id + ); + } + else { - LOG_DEBUG("module.AutoBalance", "AutoBalance_UnitScript::OnAuraApply(): Spell '{}' had it's duration adjusted ({}->{}).", aura->GetSpellInfo()->SpellName[0], aura->GetMaxDuration()/1000, auraDuration/1000); - aura->SetMaxDuration(auraDuration); - aura->SetDuration(auraDuration); + LOG_DEBUG("module.AutoBalance_DamageHealingCC", "AutoBalance_GameObjectScript::{}: {} W? T? F? ({} - {})", + function_name, + prefix, + spell_name, + spell_id + ); } } - } - uint32 _Modifer_DealDamage(Unit* target, Unit* attacker, uint32 damage) - { - // check that we're enabled globally, else return the original damage - if (!EnableGlobal) - return damage; + int32 _Modify_GameObject_Damage_Healing(GameObject* target, Unit* source, int32 amount) + { + // + // Pre-flight Checks + // - // make sure we have an attacker, that its not a player, and that the attacker is in the world, else return the original damage - if (!attacker || attacker->GetTypeId() == TYPEID_PLAYER || !attacker->IsInWorld()) - return damage; + // uncomment to debug this function + bool _debug_damage_and_healing = (source && target && (source->GetTypeId() == TYPEID_PLAYER || source->IsControlledByPlayer())); - // make sure we're in an instance, else return the original damage - if ( - !( - (target->GetMap()->IsDungeon() && attacker->GetMap()->IsDungeon()) || - (target->GetMap()->IsBattleground() && attacker->GetMap()->IsBattleground()) - ) - ) - return damage; + // check that we're enabled globally, else return the original value + if (!EnableGlobal) + { + if (_debug_damage_and_healing) LOG_DEBUG("module.AutoBalance_DamageHealingCC", "AutoBalance_GameObjectScript::_Modify_GameObject_Damage_Healing: EnableGlobal is false, returning original value of ({}).", amount); - // get the map's info to see if we're enabled - AutoBalanceMapInfo *targetMapInfo = target->GetMap()->CustomData.GetDefault("AutoBalanceMapInfo"); - AutoBalanceMapInfo *attackerMapInfo = attacker->GetMap()->CustomData.GetDefault("AutoBalanceMapInfo"); + return amount; + } - // if either the target or the attacker's maps are not enabled, return the original damage - if (!targetMapInfo->enabled || !attackerMapInfo->enabled) - return damage; + // make sure the target is in an instance, else return the original damage + if (!(target->GetMap()->IsDungeon())) + { + if (_debug_damage_and_healing) LOG_DEBUG("module.AutoBalance_DamageHealingCC", "AutoBalance_GameObjectScript::_Modify_GameObject_Damage_Healing: Target is not in an instance, returning original value of ({}).", amount); - // get the current creature's damage multiplier - float damageMultiplier = attacker->CustomData.GetDefault("AutoBalanceCreatureInfo")->DamageMultiplier; + return amount; + } - // if it's the default of 1.0, return the original damage - if (damageMultiplier == 1) - return damage; + // make sure the target is in the world, else return the original value + if (!target->IsInWorld()) + { + if (_debug_damage_and_healing) LOG_DEBUG("module.AutoBalance_DamageHealingCC", "AutoBalance_GameObjectScript::_Modify_GameObject_Damage_Healing: Target does not exist in the world, returning original value of ({}).", amount); - // if the attacker is under the control of the player, return the original damage - if ((attacker->IsHunterPet() || attacker->IsPet() || attacker->IsSummon()) && attacker->IsControlledByPlayer()) - return damage; + return amount; + } - // we are good to go, return the original damage times the multiplier - return damage * damageMultiplier; - } + // get the map's info + AutoBalanceMapInfo *targetMapABInfo = target->GetMap()->CustomData.GetDefault("AutoBalanceMapInfo"); - uint32 _Modifier_CCDuration(Unit* target, Unit* caster, Aura* aura) - { - // store the original duration of the aura - float originalDuration = (float)aura->GetDuration(); + // if the target's map is not enabled, return the original damage + if (!targetMapABInfo->enabled) + { + if (_debug_damage_and_healing) LOG_DEBUG("module.AutoBalance_DamageHealingCC", "AutoBalance_GameObjectScript::_Modify_GameObject_Damage_Healing: Target's map is not enabled, returning original value of ({}).", amount); - // check that we're enabled globally, else return the original duration - if (!EnableGlobal) - return originalDuration; + return amount; + } - // ensure that both the target and the caster are defined - if (!target || !caster) - return originalDuration; + // + // Multiplier calculation + // - // if the aura wasn't cast just now, don't change it - if (aura->GetDuration() != aura->GetMaxDuration()) - return originalDuration; + // calculate the new damage amount using the map's World Health Multiplier + int32 newAmount = _Calculate_Amount_For_GameObject(target, amount, targetMapABInfo->worldHealthMultiplier); - // if the target isn't a player or the caster is a player, return the original duration - if (!target->IsPlayer() || caster->IsPlayer()) - return originalDuration; + if (_debug_damage_and_healing) + LOG_DEBUG("module.AutoBalance_DamageHealingCC", "AutoBalance_GameObjectScript::_Modify_GameObject_Damage_Healing: Returning modified damage: ({}) -> ({})", amount, newAmount); - // make sure we're in an instance, else return the original duration - if ( - !( - (target->GetMap()->IsDungeon() && caster->GetMap()->IsDungeon()) || - (target->GetMap()->IsBattleground() && caster->GetMap()->IsBattleground()) - ) - ) - return originalDuration; + return newAmount; + } - // get the current creature's CC duration multiplier - float ccDurationMultiplier = caster->CustomData.GetDefault("AutoBalanceCreatureInfo")->CCDurationMultiplier; + int32 _Calculate_Amount_For_GameObject (GameObject* target, int32 amount, float multiplier) + { + // since it would be very complicated to reduce the real health of destructible game objects, instead we will + // adjust the damage to them as though their health were scaled. Damage will usually be dealt by vehicles and + // other non-player sources, so this effect shouldn't be as noticable as if we applied it to the player. + uint32 realMaxHealth = target->GetGOValue()->Building.MaxHealth; - // if it's the default of 1.0, return the original damage - if (ccDurationMultiplier == 1) - return originalDuration; + uint32 scaledMaxHealth = realMaxHealth * multiplier; + float percentDamageOfScaledMaxHealth = (float)amount / (float)scaledMaxHealth; - // if the aura was cast by a pet or summon, return the original duration - if ((caster->IsHunterPet() || caster->IsPet() || caster->IsSummon()) && caster->IsControlledByPlayer()) - return originalDuration; + uint32 scaledAmount = realMaxHealth * percentDamageOfScaledMaxHealth; - // only if this aura is a CC - if ( - aura->HasEffectType(SPELL_AURA_MOD_CHARM) || - aura->HasEffectType(SPELL_AURA_MOD_CONFUSE) || - aura->HasEffectType(SPELL_AURA_MOD_DISARM) || - aura->HasEffectType(SPELL_AURA_MOD_FEAR) || - aura->HasEffectType(SPELL_AURA_MOD_PACIFY) || - aura->HasEffectType(SPELL_AURA_MOD_POSSESS) || - aura->HasEffectType(SPELL_AURA_MOD_SILENCE) || - aura->HasEffectType(SPELL_AURA_MOD_STUN) || - aura->HasEffectType(SPELL_AURA_MOD_SPEED_SLOW_ALL) - ) - { - return originalDuration * ccDurationMultiplier; - } - else - { - return originalDuration; + return scaledAmount; } - } }; @@ -1823,40 +4435,120 @@ class AutoBalance_AllMapScript : public AllMapScript void OnCreateMap(Map* map) { - LOG_DEBUG("module.AutoBalance", "AutoBalance_AllMapScript::OnCreateMap(): {}", map->GetMapName()); + LOG_DEBUG("module.AutoBalance", "AutoBalance_AllMapScript::OnCreateMap(): Map {} ({}{})", + map->GetMapName(), + map->GetId(), + map->GetInstanceId() ? "-" + std::to_string(map->GetInstanceId()) : "" + ); - if (!map->IsDungeon() && !map->IsBattleground()) - return; + // clear out any previously-recorded data + map->CustomData.Erase("AutoBalanceMapInfo"); - // get the map's info AutoBalanceMapInfo *mapABInfo=map->CustomData.GetDefault("AutoBalanceMapInfo"); - // get the map's LFG stats - LFGDungeonEntry const* dungeon = GetLFGDungeon(map->GetId(), map->GetDifficulty()); - if (dungeon) { - mapABInfo->lfgMinLevel = dungeon->MinLevel; - mapABInfo->lfgMaxLevel = dungeon->MaxLevel; - mapABInfo->lfgTargetLevel = dungeon->TargetLevel; - } + if (map->IsDungeon()) + { + // get the map's LFG stats even if not enabled + LFGDungeonEntry const* dungeon = GetLFGDungeon(map->GetId(), map->GetDifficulty()); + if (dungeon) { + mapABInfo->lfgMinLevel = dungeon->MinLevel; + mapABInfo->lfgMaxLevel = dungeon->MaxLevel; + mapABInfo->lfgTargetLevel = dungeon->TargetLevel; + } + // if this is a heroic dungeon that isn't in LFG, get the stats from the non-heroic version + else if (map->IsHeroic()) + { + LFGDungeonEntry const* nonHeroicDungeon = nullptr; + if (map->GetDifficulty() == DUNGEON_DIFFICULTY_HEROIC) + { + nonHeroicDungeon = GetLFGDungeon(map->GetId(), DUNGEON_DIFFICULTY_NORMAL); + } + else if (map->GetDifficulty() == RAID_DIFFICULTY_10MAN_HEROIC) + { + nonHeroicDungeon = GetLFGDungeon(map->GetId(), RAID_DIFFICULTY_10MAN_NORMAL); + } + else if (map->GetDifficulty() == RAID_DIFFICULTY_25MAN_HEROIC) + { + nonHeroicDungeon = GetLFGDungeon(map->GetId(), RAID_DIFFICULTY_25MAN_NORMAL); + } + + LOG_DEBUG("module.AutoBalance", "AutoBalance_AllMapScript::OnCreateMap(): Map {} ({}{}) | is a Heroic dungeon that is not in LFG. Using non-heroic LFG levels.", + map->GetMapName(), + map->GetId(), + map->GetInstanceId() ? "-" + std::to_string(map->GetInstanceId()) : "" + ); + + if (nonHeroicDungeon) + { + mapABInfo->lfgMinLevel = nonHeroicDungeon->MinLevel; + mapABInfo->lfgMaxLevel = nonHeroicDungeon->MaxLevel; + mapABInfo->lfgTargetLevel = nonHeroicDungeon->TargetLevel; + } + else + { + LOG_ERROR("module.AutoBalance", "AutoBalance_AllMapScript::OnCreateMap(): Map {} ({}{}) | Could not determine LFG level ranges for this map. Level will bet set to 0.", + map->GetMapName(), + map->GetId(), + map->GetInstanceId() ? "-" + std::to_string(map->GetInstanceId()) : "" + ); + } + } - // load the map's settings - LoadMapSettings(map); + if (map->GetInstanceId()) + { + LOG_DEBUG("module.AutoBalance", "AutoBalance_AllMapScript::OnCreateMap(): Map {} ({}{}) | is an instance of a map. Loading initial map data.", + map->GetMapName(), + map->GetId(), + map->GetInstanceId() ? "-" + std::to_string(map->GetInstanceId()) : "" + ); + UpdateMapDataIfNeeded(map); + + // provide a concise summary of the map data we collected + LOG_DEBUG("module.AutoBalance", "AutoBalance_AllMapScript::OnCreateMap(): Map {} ({}{}) | LFG levels ({}-{}) (target {}). {} for AutoBalancing.", + map->GetMapName(), + map->GetId(), + map->GetInstanceId() ? "-" + std::to_string(map->GetInstanceId()) : "", + mapABInfo->lfgMinLevel ? std::to_string(mapABInfo->lfgMinLevel) : "?", + mapABInfo->lfgMaxLevel ? std::to_string(mapABInfo->lfgMaxLevel) : "?", + mapABInfo->lfgTargetLevel ? std::to_string(mapABInfo->lfgTargetLevel) : "?", + mapABInfo->enabled ? "Enabled" : "Disabled" + ); + } + else + { + LOG_DEBUG( + "module.AutoBalance", "AutoBalance_AllMapScript::OnCreateMap(): Map {} ({}) | is an instance base map.", + map->GetMapName(), + map->GetId() + ); + } + } } + // hook triggers after the player has already entered the world void OnPlayerEnterAll(Map* map, Player* player) { - LOG_DEBUG("module.AutoBalance", "AutoBalance_AllMapScript::OnPlayerEnterAll(): {}", map->GetMapName()); - if (!map->IsDungeon() && !map->IsBattleground()) + if (!EnableGlobal) return; - if (player->IsGameMaster()) + if (!map->IsDungeon()) return; + LOG_DEBUG("module.AutoBalance", "AutoBalance:: {}", SPACER); + + LOG_DEBUG("module.AutoBalance", "AutoBalance_AllMapScript::OnPlayerEnterAll: Player {}{} | enters {} ({}{})", + player->GetName(), + player->IsGameMaster() ? " (GM)" : "", + map->GetMapName(), + map->GetId(), + map->GetInstanceId() ? "-" + std::to_string(map->GetInstanceId()) : "" + ); + // get the map's info AutoBalanceMapInfo *mapABInfo=map->CustomData.GetDefault("AutoBalanceMapInfo"); - // Update the map's level if it is out of date - UpdateMapDataIfNeeded(map); + // add player to this map's player list + AddPlayerToMap(map, player); // recalculate the zone's level stats mapABInfo->highestCreatureLevel = 0; @@ -1864,53 +4556,60 @@ class AutoBalance_AllMapScript : public AllMapScript mapABInfo->avgCreatureLevel = 0; mapABInfo->activeCreatureCount = 0; + // Update the map's data, forced + UpdateMapDataIfNeeded(map, true); + // see which existing creatures are active for (std::vector::iterator creatureIterator = mapABInfo->allMapCreatures.begin(); creatureIterator != mapABInfo->allMapCreatures.end(); ++creatureIterator) { - AddCreatureToMapData(*creatureIterator, false, nullptr, true); + AddCreatureToMapCreatureList(*creatureIterator, false, true); } - // updates the player count, player levels for the map - UpdateMapPlayerStats(map); - + // Notify players of the change if (PlayerChangeNotify && mapABInfo->enabled) { if (map->GetEntry()->IsDungeon() && player) { - Map::PlayerList const &playerList = map->GetPlayers(); - if (!playerList.IsEmpty()) + if (mapABInfo->playerCount) { - for (Map::PlayerList::const_iterator playerIteration = playerList.begin(); playerIteration != playerList.end(); ++playerIteration) + for (std::vector::const_iterator playerIterator = mapABInfo->allMapPlayers.begin(); playerIterator != mapABInfo->allMapPlayers.end(); ++playerIterator) { - if (Player* playerHandle = playerIteration->GetSource()) + Player* thisPlayer = *playerIterator; + if (thisPlayer) { - ChatHandler chatHandle = ChatHandler(playerHandle->GetSession()); - auto instanceMap = ((InstanceMap*)sMapMgr->FindMap(map->GetId(), map->GetInstanceId())); + ChatHandler chatHandle = ChatHandler(thisPlayer->GetSession()); + InstanceMap* instanceMap = map->ToInstanceMap(); std::string instanceDifficulty; if (instanceMap->IsHeroic()) instanceDifficulty = "Heroic"; else instanceDifficulty = "Normal"; - Player* mapPlayer = playerIteration->GetSource(); - - if (mapPlayer && mapPlayer == player) // This is the player that entered + if (thisPlayer && thisPlayer == player) // This is the player that entered { - chatHandle.PSendSysMessage("|cffFF0000 [AutoBalance]|r|cffFF8000 Welcome to %s (%u-player %s). There are %u player(s) in this instance. Difficulty set to %u player(s).|r", + chatHandle.PSendSysMessage("|cffc3dbff [AutoBalance]|r|cffFF8000 Welcome to %s (%u-player %s). There are %u player(s) in this instance. Difficulty set to %u player(s).|r", map->GetMapName(), instanceMap->GetMaxPlayers(), instanceDifficulty, mapABInfo->playerCount, mapABInfo->adjustedPlayerCount ); + + // notify GMs that they won't be accounted for + if (player->IsGameMaster()) + { + chatHandle.PSendSysMessage("|cffc3dbff [AutoBalance]|r|cffFF8000 Your GM flag is turned on. AutoBalance will ignore you. Please turn GM off and exit/re-enter the instance if you'd like to be considering for AutoBalancing.|r"); + } } else { - chatHandle.PSendSysMessage("|cffFF0000 [AutoBalance]|r|cffFF8000 %s enters the instance. There are %u player(s) in this instance. Difficulty set to %u player(s).|r", - player->GetName().c_str(), - mapABInfo->playerCount, - mapABInfo->adjustedPlayerCount - ); + // announce non-GMs entering the instance only + if (!player->IsGameMaster()) + { + chatHandle.PSendSysMessage("|cffc3dbff [AutoBalance]|r|cffFF8000 %s enters the instance. There are %u player(s) in this instance. Difficulty set to %u player(s).|r", + player->GetName().c_str(), + mapABInfo->playerCount, + mapABInfo->adjustedPlayerCount + ); + } } - - } } } @@ -1918,17 +4617,39 @@ class AutoBalance_AllMapScript : public AllMapScript } } + // hook triggers just before the player left the world void OnPlayerLeaveAll(Map* map, Player* player) { - LOG_DEBUG("module.AutoBalance", "AutoBalance_AllMapScript::OnPlayerLeaveAll(): {}", map->GetMapName()); if (!EnableGlobal) return; + if (!map->IsDungeon()) + return; + + LOG_DEBUG("module.AutoBalance", "AutoBalance:: {}", SPACER); + + LOG_DEBUG("module.AutoBalance", "AutoBalance_AllMapScript::OnPlayerLeaveAll: Player {}{} | exits {} ({}{})", + player->GetName(), + player->IsGameMaster() ? " (GM)" : "", + map->GetMapName(), + map->GetId(), + map->GetInstanceId() ? "-" + std::to_string(map->GetInstanceId()) : "" + ); + // get the map's info AutoBalanceMapInfo *mapABInfo=map->CustomData.GetDefault("AutoBalanceMapInfo"); - // Update the map's level if it is out of date - UpdateMapDataIfNeeded(map); + // remove this player from this map's player list + bool playerWasRemoved = RemovePlayerFromMap(map, player); + + // report the number of players in the map + LOG_DEBUG("module.AutoBalance", "AutoBalance_AllMapScript::OnPlayerLeaveAll: There are {} player(s) left in the map.", mapABInfo->allMapPlayers.size()); + + // if a player was NOT removed, return now - stats don't need to be updated + if (!playerWasRemoved) + { + return; + } // recalculate the zone's level stats mapABInfo->highestCreatureLevel = 0; @@ -1939,75 +4660,54 @@ class AutoBalance_AllMapScript : public AllMapScript // see which existing creatures are active for (std::vector::iterator creatureIterator = mapABInfo->allMapCreatures.begin(); creatureIterator != mapABInfo->allMapCreatures.end(); ++creatureIterator) { - AddCreatureToMapData(*creatureIterator, false, player, true); - } - - bool areAnyPlayersInCombat = false; - - // updates the player count and levels for the map - if (map->GetEntry() && map->GetEntry()->IsDungeon()) - { - // determine if any players in the map are in combat - // if so, do not adjust the player count - Map::PlayerList const& mapPlayerList = map->GetPlayers(); - for (Map::PlayerList::const_iterator itr = mapPlayerList.begin(); itr != mapPlayerList.end(); ++itr) - { - if (Player* mapPlayer = itr->GetSource()) - { - if (mapPlayer->IsInCombat() && mapPlayer->GetMap() == map) - { - areAnyPlayersInCombat = true; - - // notify the player that they left the instance while combat was in progress - ChatHandler chatHandle = ChatHandler(player->GetSession()); - chatHandle.PSendSysMessage("|cffFF0000 [AutoBalance]|r|cffFF8000 You left the instance while combat was in progress. The instance player count is still %u.", mapABInfo->playerCount); - - break; - } - } - } - if (areAnyPlayersInCombat) - { - for (Map::PlayerList::const_iterator itr = mapPlayerList.begin(); itr != mapPlayerList.end(); ++itr) - { - if (Player* mapPlayer = itr->GetSource()) - { - // only for the players who are in the instance and did not leave - if (mapPlayer != player) - { - ChatHandler chatHandle = ChatHandler(mapPlayer->GetSession()); - chatHandle.PSendSysMessage("|cffFF0000 [AutoBalance]|r|cffFF8000 %s left the instance while combat was in progress. The instance player count is still %u.", player->GetName().c_str(), mapABInfo->playerCount); - } - } - } - } - else + AddCreatureToMapCreatureList(*creatureIterator, false, true); + } + + // Update the map's data, forced + UpdateMapDataIfNeeded(map, true); + + // updates the player count and levels for the map + if (map->GetEntry() && map->GetEntry()->IsDungeon()) + { { - mapABInfo->playerCount = map->GetPlayersCountExceptGMs() - 1; - LOG_DEBUG("module.AutoBalance", "AutoBalance_AllMapScript::OnPlayerLeaveAll(): Player left, new player count is {}", mapABInfo->playerCount); + mapABInfo->playerCount = mapABInfo->allMapPlayers.size(); + LOG_DEBUG("module.AutoBalance", "AutoBalance_AllMapScript::OnPlayerLeaveAll: Player {} left the instance.", + player->GetName(), + mapABInfo->playerCount, + mapABInfo->adjustedPlayerCount + ); } } - // updates the player count, player levels for the map (but don't re-count players) - UpdateMapPlayerStats(map, false); - - if (PlayerChangeNotify && !player->IsGameMaster() && !areAnyPlayersInCombat && mapABInfo->enabled) + // Notify remaining players in the instance that a player left + if (PlayerChangeNotify && mapABInfo->enabled) { - if (map->GetEntry()->IsDungeon() && player) + if (map->GetEntry()->IsDungeon() && player && !player->IsGameMaster()) { - Map::PlayerList const &playerList = map->GetPlayers(); - if (!playerList.IsEmpty()) + if (mapABInfo->playerCount) { - for (Map::PlayerList::const_iterator playerIteration = playerList.begin(); playerIteration != playerList.end(); ++playerIteration) + for (std::vector::const_iterator playerIterator = mapABInfo->allMapPlayers.begin(); playerIterator != mapABInfo->allMapPlayers.end(); ++playerIterator) { - Player* mapPlayer = playerIteration->GetSource(); - if (mapPlayer && mapPlayer != player) + Player* thisPlayer = *playerIterator; + if (thisPlayer && thisPlayer != player) { - ChatHandler chatHandle = ChatHandler(mapPlayer->GetSession()); - chatHandle.PSendSysMessage("|cffFF0000 [AutoBalance]|r|cffFF8000 %s left the instance. There are %u player(s) in this instance. Difficulty set to %u player(s).|r", - player->GetName().c_str(), - mapABInfo->playerCount, - mapABInfo->adjustedPlayerCount); + ChatHandler chatHandle = ChatHandler(thisPlayer->GetSession()); + + if (mapABInfo->combatLocked) + { + chatHandle.PSendSysMessage("|cffc3dbff [AutoBalance]|r|cffFF8000 %s left the instance while combat was in progress. Difficulty locked to no less than %u players until combat ends.|r", + player->GetName().c_str(), + mapABInfo->adjustedPlayerCount + ); + } + else + { + chatHandle.PSendSysMessage("|cffc3dbff [AutoBalance]|r|cffFF8000 %s left the instance. There are %u player(s) in this instance. Difficulty set to %u player(s).|r", + player->GetName().c_str(), + mapABInfo->playerCount, + mapABInfo->adjustedPlayerCount + ); + } } } } @@ -2024,111 +4724,310 @@ class AutoBalance_AllCreatureScript : public AllCreatureScript { } - void Creature_SelectLevel(const CreatureTemplate* /*creatureTemplate*/, Creature* creature) override + void OnBeforeCreatureSelectLevel(const CreatureTemplate* /*creatureTemplate*/, Creature* creature, uint8 &level) override { - if (creature->GetMap()->IsDungeon()) - LOG_DEBUG("module.AutoBalance", "AutoBalance_AllCreatureScript::Creature_SelectLevel(): {} ({})", creature->GetName(), creature->GetLevel()); + Map* creatureMap = creature->GetMap(); + + if (creatureMap && creatureMap->IsDungeon()) + { + LOG_DEBUG("module.AutoBalance", "AutoBalance:: {}", SPACER); + LOG_DEBUG("module.AutoBalance", "AutoBalance_AllCreatureScript::OnBeforeCreatureSelectLevel: Creature {} ({}) | Entry ID: ({}) | Spawn ID: ({})", + creature->GetName(), + level, + creature->GetEntry(), + creature->GetSpawnId() + ); + + // Create the new creature's AB info + AutoBalanceCreatureInfo *creatureABInfo=creature->CustomData.GetDefault("AutoBalanceCreatureInfo"); + + // mark this creature as brand new so that only the level will be modified before creation + creatureABInfo->isBrandNew = true; + + // if the creature already has a selectedLevel on it, we have already processed it and can re-use that value + if (creatureABInfo->selectedLevel) + { + LOG_DEBUG("module.AutoBalance", "AutoBalance_AllCreatureScript::OnBeforeCreatureSelectLevel: Creature {} ({}) | has already been processed, using level {}.", + creature->GetName(), + creatureABInfo->UnmodifiedLevel, + creatureABInfo->selectedLevel + ); + + level = creatureABInfo->selectedLevel; + return; + } + + // Update the map's data if it is out of date (just before changing the map's creature list) + UpdateMapDataIfNeeded(creature->GetMap()); + + Map* creatureMap = creature->GetMap(); + InstanceMap* instanceMap = creatureMap->ToInstanceMap(); + + LOG_DEBUG("module.AutoBalance", "AutoBalance_AllCreatureScript::OnBeforeCreatureSelectLevel: Creature {} ({}) | is in map {} ({}{}{}{})", + creature->GetName(), + level, + creatureMap->GetMapName(), + creatureMap->GetId(), + instanceMap ? "-" + std::to_string(instanceMap->GetInstanceId()) : "", + instanceMap ? ", " + std::to_string(instanceMap->GetMaxPlayers()) + "-player" : "", + instanceMap ? instanceMap->IsHeroic() ? " Heroic" : " Normal" : "" + ); + + // Set level originally intended for the creature + creatureABInfo->UnmodifiedLevel = level; + + // add the creature to the map's tracking list + AddCreatureToMapCreatureList(creature); + + // Update the map's data if it is out of date (just after changing the map's creature list) + UpdateMapDataIfNeeded(creature->GetMap()); + + // do an initial modification run of the creature, but don't update the level yet + ModifyCreatureAttributes(creature); + + if (isCreatureRelevant(creature)) + { + // set the new creature level + level = creatureABInfo->selectedLevel; + + LOG_DEBUG("module.AutoBalance", "AutoBalance_AllCreatureScript::OnBeforeCreatureSelectLevel: Creature {} ({}) | will spawn in as level ({}).", + creature->GetName(), + creatureABInfo->UnmodifiedLevel, + creatureABInfo->selectedLevel + ); + } + else + { + // don't change level value + LOG_DEBUG("module.AutoBalance", "AutoBalance_AllCreatureScript::OnBeforeCreatureSelectLevel: Creature {} ({}) | will spawn in at its original level ({}).", + creature->GetName(), + creatureABInfo->UnmodifiedLevel, + creatureABInfo->selectedLevel + ); + } + } + } + + void Creature_SelectLevel(const CreatureTemplate* /* cinfo */, Creature* creature) override + { + // ensure we're in a dungeon with a creature + if ( + !creature || + !creature->GetMap() || + !creature->GetMap()->IsDungeon() || + !creature->GetMap()->GetInstanceId() + ) + { + return; + } + + // get the creature's info + AutoBalanceCreatureInfo *creatureABInfo=creature->CustomData.GetDefault("AutoBalanceCreatureInfo"); + + // If the creature is brand new, it needs more processing + if (creatureABInfo->isBrandNew) + { + LOG_DEBUG("module.AutoBalance", "AutoBalance:: {}", SPACER); + + LOG_DEBUG("module.AutoBalance", "AutoBalance_AllCreatureScript::Creature_SelectLevel: Creature {} ({}) | Entry ID: ({}) | Spawn ID: ({})", + creature->GetName(), + creature->GetLevel(), + creature->GetEntry(), + creature->GetSpawnId() + ); + + if (creatureABInfo->isBrandNew) + { + LOG_DEBUG("module.AutoBalance", "AutoBalance_AllCreatureScript::Creature_SelectLevel: Creature {} ({}) | is no longer brand new.", + creature->GetName(), + creature->GetLevel() + ); + creatureABInfo->isBrandNew = false; + } + + // Update the map's data if it is out of date + UpdateMapDataIfNeeded(creature->GetMap()); - // add the creature to the map's tracking list - AddCreatureToMapData(creature); + ModifyCreatureAttributes(creature); - // do an initial modification of the creature - ModifyCreatureAttributes(creature); + AutoBalanceCreatureInfo *creatureABInfo=creature->CustomData.GetDefault("AutoBalanceCreatureInfo"); + if (creature->GetLevel() != creatureABInfo->selectedLevel && isCreatureRelevant(creature)) + { + LOG_DEBUG("module.AutoBalance", "AutoBalance_AllCreatureScript::Creature_SelectLevel: Creature {} ({}) | is set to level ({}).", + creature->GetName(), + creature->GetLevel(), + creatureABInfo->selectedLevel + ); + creature->SetLevel(creatureABInfo->selectedLevel); + } + } + else + { + LOG_ERROR("module.AutoBalance", "AutoBalance_AllCreatureScript::Creature_SelectLevel: Creature {} ({}) | is new to the instance but wasn't flagged as brand new. Please open an issue.", + creature->GetName(), + creature->GetLevel() + ); + } } void OnCreatureAddWorld(Creature* creature) override { if (creature->GetMap()->IsDungeon()) - LOG_DEBUG("module.AutoBalance", "AutoBalance_AllCreatureScript::OnCreatureAddWorld(): {} ({})", creature->GetName(), creature->GetLevel()); + { + Map* creatureMap = creature->GetMap(); + InstanceMap* instanceMap = creatureMap->ToInstanceMap(); + AutoBalanceCreatureInfo *creatureABInfo=creature->CustomData.GetDefault("AutoBalanceCreatureInfo"); + + // final check to be sure the creature is the right level + if (isCreatureRelevant(creature) && creature->GetLevel() != creatureABInfo->selectedLevel && !creature->IsSummon()) + { + LOG_DEBUG("module.AutoBalance", "AutoBalance_AllCreatureScript::OnCreatureAddWorld: Creature {} ({}) | is set to level ({}) just after being added to the world.", + creature->GetName(), + creature->GetLevel(), + creatureABInfo->selectedLevel + ); + creature->SetLevel(creatureABInfo->selectedLevel); + } + + LOG_DEBUG("module.AutoBalance", "AutoBalance_AllCreatureScript::OnCreatureAddWorld: Creature {} ({}) | added to map {} ({}{}{}{})", + creature->GetName(), + creature->GetLevel(), + creatureMap->GetMapName(), + creatureMap->GetId(), + instanceMap ? "-" + std::to_string(instanceMap->GetInstanceId()) : "", + instanceMap ? ", " + std::to_string(instanceMap->GetMaxPlayers()) + "-player" : "", + instanceMap ? instanceMap->IsHeroic() ? " Heroic" : " Normal" : "" + ); + } } void OnCreatureRemoveWorld(Creature* creature) override { if (creature->GetMap()->IsDungeon()) - LOG_DEBUG("module.AutoBalance", "AutoBalance_AllCreatureScript::OnCreatureRemoveWorld(): {} ({})", creature->GetName(), creature->GetLevel()); - - // remove the creature from the map's tracking list, if present - RemoveCreatureFromMapData(creature); + { + LOG_DEBUG("module.AutoBalance", "AutoBalance:: {}", SPACER); + + LOG_DEBUG("module.AutoBalance", "AutoBalance_AllCreatureScript::OnCreatureRemoveWorld: Creature {} ({}) | Entry ID: ({}) | Spawn ID: ({})", + creature->GetName(), + creature->GetLevel(), + creature->GetEntry(), + creature->GetSpawnId() + + ); + + InstanceMap* instanceMap = creature->GetMap()->ToInstanceMap(); + Map* map = sMapMgr->FindBaseMap(creature->GetMapId()); + + LOG_DEBUG("module.AutoBalance", "AutoBalance_AllCreatureScript::OnCreatureRemoveWorld: Creature {} ({}) | removed from map {} ({}{}{}{})", + creature->GetName(), + creature->GetLevel(), + map->GetMapName(), + map->GetId(), + instanceMap ? "-" + std::to_string(instanceMap->GetInstanceId()) : "", + instanceMap ? ", " + std::to_string(instanceMap->GetMaxPlayers()) + "-player" : "", + instanceMap ? instanceMap->IsHeroic() ? " Heroic" : " Normal" : "" + ); + + // remove the creature from the map's tracking list, if present + RemoveCreatureFromMapData(creature); + } } void OnAllCreatureUpdate(Creature* creature, uint32 /*diff*/) override { + // ensure we're in a dungeon with a creature + if ( + !creature || + !creature->GetMap() || + !creature->GetMap()->IsDungeon() || + !creature->GetMap()->GetInstanceId() + ) + { + return; + } + + // update map data before making creature changes + UpdateMapDataIfNeeded(creature->GetMap()); + // If the config is out of date and the creature was reset, run modify against it if (ResetCreatureIfNeeded(creature)) { - LOG_DEBUG("module.AutoBalance", "AutoBalance_AllCreatureScript::OnAllCreatureUpdate(): Creature {} ({}) is reset to its original stats.", creature->GetName(), creature->GetLevel()); + LOG_DEBUG("module.AutoBalance", "AutoBalance:: {}", SPACER); + + LOG_DEBUG("module.AutoBalance", "AutoBalance_AllCreatureScript::OnAllCreatureUpdate: Creature {} ({}) | Entry ID: ({}) | Spawn ID: ({})", + creature->GetName(), + creature->GetLevel(), + creature->GetEntry(), + creature->GetSpawnId() + ); - // Update the map's level if it is out of date + // Update the map's data if it is out of date UpdateMapDataIfNeeded(creature->GetMap()); ModifyCreatureAttributes(creature); + + AutoBalanceCreatureInfo *creatureABInfo=creature->CustomData.GetDefault("AutoBalanceCreatureInfo"); + + if (creature->GetLevel() != creatureABInfo->selectedLevel && isCreatureRelevant(creature)) + { + LOG_DEBUG("module.AutoBalance", "AutoBalance_AllCreatureScript::OnAllCreatureUpdate: Creature {} ({}) | is set to level ({}).", + creature->GetName(), + creature->GetLevel(), + creatureABInfo->selectedLevel + ); + creature->SetLevel(creatureABInfo->selectedLevel); + } } } // Reset the passed creature to stock if the config has changed bool ResetCreatureIfNeeded(Creature* creature) { - // make sure we have a creature and that it's assigned to a map - if (!creature || !creature->GetMap()) - return false; - - // if this isn't a dungeon or a battleground, make no changes - if (!(creature->GetMap()->IsDungeon() || creature->GetMap()->IsBattleground())) - return false; - - // if this is a pet or summon controlled by the player, make no changes - if ((creature->IsHunterPet() || creature->IsPet() || creature->IsSummon()) && creature->IsControlledByPlayer()) - return false; - - // if this is a non-relevant creature, skip - if (creature->IsCritter() || creature->IsTotem() || creature->IsTrigger()) - return false; - - // get (or create) the creature and map's info - AutoBalanceCreatureInfo *creatureABInfo=creature->CustomData.GetDefault("AutoBalanceCreatureInfo"); - AutoBalanceMapInfo *mapABInfo=creature->GetMap()->CustomData.GetDefault("AutoBalanceMapInfo"); - - // if this creature is below 85% of the minimum level for the map, make no changes - if (creatureABInfo->UnmodifiedLevel < (float)mapABInfo->lfgMinLevel * .85f) + // make sure we have a creature + if (!creature || !isCreatureRelevant(creature)) { - if (creatureABInfo->configTime == 0) - LOG_DEBUG("module.AutoBalance", "AutoBalance_AllCreatureScript::ResetCreatureIfNeeded(): {} ({}) is below 85% of the LFG min level for the map, do not reset or modify.", creature->GetName(), creatureABInfo->UnmodifiedLevel); - - creatureABInfo->configTime = lastConfigTime; return false; } - // if this creature is above 115% of the maximum level for the map, make no changes - if (creatureABInfo->UnmodifiedLevel > (float)mapABInfo->lfgMaxLevel * 1.15f) - { - if (creatureABInfo->configTime == 0) - LOG_DEBUG("module.AutoBalance", "AutoBalance_AllCreatureScript::ResetCreatureIfNeeded(): {} ({}) is above 115% of the LFG max level for the map, do not reset or modify.", creature->GetName(), creatureABInfo->UnmodifiedLevel); - - creatureABInfo->configTime = lastConfigTime; - return false; - } + // get (or create) map and creature info + AutoBalanceMapInfo *mapABInfo=creature->GetMap()->CustomData.GetDefault("AutoBalanceMapInfo"); + AutoBalanceCreatureInfo *creatureABInfo=creature->CustomData.GetDefault("AutoBalanceCreatureInfo"); - // if creature is dead and configTime is 0, skip - if (creature->isDead() && creatureABInfo->configTime == 0) + // if creature is dead and mapConfigTime is 0, skip for now + if (creature->isDead() && creatureABInfo->mapConfigTime == 1) { return false; } - // if the creature is dead but configTime is NOT 0, we set it to 0 so that it will be recalculated if revived + // if the creature is dead but mapConfigTime is NOT 0, we set it to 0 so that it will be recalculated if revived // also remember that this creature was once alive but is now dead else if (creature->isDead()) { - LOG_DEBUG("module.AutoBalance", "AutoBalance_AllCreatureScript::ResetCreatureIfNeeded(): {} ({}) is dead and configTime is not 0 - prime for reset if revived.", creature->GetName(), creature->GetLevel()); - creatureABInfo->configTime = 0; + LOG_DEBUG("module.AutoBalance", "AutoBalance:: {}", SPACER); + LOG_DEBUG("module.AutoBalance", "AutoBalance_AllCreatureScript::ResetCreatureIfNeeded: Creature {} ({}) | is dead and mapConfigTime is not 0 - prime for reset if revived.", creature->GetName(), creature->GetLevel()); + creatureABInfo->mapConfigTime = 1; creatureABInfo->wasAliveNowDead = true; return false; } // if the config is outdated, reset the creature - if (creatureABInfo->configTime != lastConfigTime) + if (creatureABInfo->mapConfigTime < mapABInfo->mapConfigTime) { - // before updating the creature, we should update the map data if needed - UpdateMapDataIfNeeded(creature->GetMap()); + LOG_DEBUG("module.AutoBalance", "AutoBalance:: {}", SPACER); + + LOG_DEBUG("module.AutoBalance", "AutoBalance_AllCreatureScript::ResetCreatureIfNeeded: Creature {} ({}) | Entry ID: ({}) | Spawn ID: ({})", + creature->GetName(), + creature->GetLevel(), + creature->GetEntry(), + creature->GetSpawnId() + ); + + LOG_DEBUG("module.AutoBalance", "AutoBalance_AllCreatureScript::ResetCreatureIfNeeded: Creature {} ({}) | Map config time is out of date ({} < {}). Resetting creature before modify.", + creature->GetName(), + creature->GetLevel(), + creatureABInfo->mapConfigTime, + mapABInfo->mapConfigTime + ); // retain some values uint8 unmodifiedLevel = creatureABInfo->UnmodifiedLevel; @@ -2138,41 +5037,63 @@ class AutoBalance_AllCreatureScript : public AllCreatureScript // reset AutoBalance modifiers creature->CustomData.Erase("AutoBalanceCreatureInfo"); - AutoBalanceCreatureInfo *creatureABInfo=creature->CustomData.GetDefault("AutoBalanceCreatureInfo"); + AutoBalanceCreatureInfo *creatureABInfo = creature->CustomData.GetDefault("AutoBalanceCreatureInfo"); // grab the creature's template and the original creature's stats CreatureTemplate const* creatureTemplate = creature->GetCreatureTemplate(); // set the creature's level - creature->SetLevel(unmodifiedLevel); - creatureABInfo->UnmodifiedLevel = unmodifiedLevel; + if (creature->GetLevel() != unmodifiedLevel) + { + LOG_DEBUG("module.AutoBalance", "AutoBalance_AllCreatureScript::ResetCreatureIfNeeded: Creature {} ({}) | is set to level ({}).", + creature->GetName(), + creature->GetLevel(), + unmodifiedLevel + ); + creature->SetLevel(unmodifiedLevel); + creature->UpdateAllStats(); + } + else + { + LOG_DEBUG("module.AutoBalance", "AutoBalance_AllCreatureScript::ResetCreatureIfNeeded: Creature {} ({}) | is already set to level ({}).", + creature->GetName(), + creature->GetLevel(), + unmodifiedLevel + ); + } // get the creature's base stats - CreatureBaseStats const* origCreatureStats = sObjectMgr->GetCreatureBaseStats(unmodifiedLevel, creatureTemplate->unit_class); + CreatureBaseStats const* origCreatureBaseStats = sObjectMgr->GetCreatureBaseStats(unmodifiedLevel, creatureTemplate->unit_class); // health float currentHealthPercent = (float)creature->GetHealth() / (float)creature->GetMaxHealth(); - creature->SetMaxHealth(origCreatureStats->GenerateHealth(creatureTemplate)); - creature->SetHealth((float)origCreatureStats->GenerateHealth(creatureTemplate) * currentHealthPercent); + creature->SetMaxHealth(origCreatureBaseStats->GenerateHealth(creatureTemplate)); + creature->SetHealth((float)origCreatureBaseStats->GenerateHealth(creatureTemplate) * currentHealthPercent); // mana if (creature->getPowerType() == POWER_MANA && creature->GetPower(POWER_MANA) >= 0 && creature->GetMaxPower(POWER_MANA) > 0) { float currentManaPercent = creature->GetPower(POWER_MANA) / creature->GetMaxPower(POWER_MANA); - creature->SetMaxPower(POWER_MANA, origCreatureStats->GenerateMana(creatureTemplate)); + creature->SetMaxPower(POWER_MANA, origCreatureBaseStats->GenerateMana(creatureTemplate)); creature->SetPower(POWER_MANA, creature->GetMaxPower(POWER_MANA) * currentManaPercent); } // armor - creature->SetArmor(origCreatureStats->GenerateArmor(creatureTemplate)); + creature->SetArmor(origCreatureBaseStats->GenerateArmor(creatureTemplate)); // restore the saved data + creatureABInfo->UnmodifiedLevel = unmodifiedLevel; creatureABInfo->isActive = isActive; creatureABInfo->wasAliveNowDead = wasAliveNowDead; creatureABInfo->isInCreatureList = isInCreatureList; // damage and ccduration are handled using AutoBalanceCreatureInfo data only + LOG_DEBUG("module.AutoBalance", "AutoBalance_AllCreatureScript::ResetCreatureIfNeeded: Creature {} ({}) is reset to its original stats.", + creature->GetName(), + creature->GetLevel() + ); + // return true to indicate that the creature was reset return true; } @@ -2184,830 +5105,908 @@ class AutoBalance_AllCreatureScript : public AllCreatureScript void ModifyCreatureAttributes(Creature* creature) { - // make sure we have a creature and that it's assigned to a map - if (!creature || !creature->GetMap()) - return; - - // if this isn't a dungeon or a battleground, make no changes - if (!(creature->GetMap()->IsDungeon() || creature->GetMap()->IsBattleground())) - return; - - // if this is a pet or summon controlled by the player, make no changes - if (((creature->IsHunterPet() || creature->IsPet() || creature->IsSummon()) && creature->IsControlledByPlayer())) - return; - - // if this is a non-relevant creature, make no changes - if (creature->IsCritter() || creature->IsTotem() || creature->IsTrigger()) + // make sure we have a creature + if (!creature) + { + LOG_DEBUG("module.AutoBalance", "AutoBalance_AllCreatureScript::ModifyCreatureAttributes: creature is null."); return; + } // grab creature and map data AutoBalanceCreatureInfo *creatureABInfo=creature->CustomData.GetDefault("AutoBalanceCreatureInfo"); - AutoBalanceMapInfo *mapABInfo=creature->GetMap()->CustomData.GetDefault("AutoBalanceMapInfo"); + Map* map = creature->GetMap(); + InstanceMap* instanceMap = map->ToInstanceMap(); + AutoBalanceMapInfo *mapABInfo=instanceMap->CustomData.GetDefault("AutoBalanceMapInfo"); // mark the creature as updated using the current settings if needed - if (creatureABInfo->configTime != lastConfigTime) - creatureABInfo->configTime = lastConfigTime; + // if this creature is brand new, do not update this so that it will be re-processed next OnCreatureUpdate + if (creatureABInfo->mapConfigTime < mapABInfo->mapConfigTime && !creatureABInfo->isBrandNew) + { + LOG_DEBUG("module.AutoBalance", "AutoBalance_AllCreatureScript::ModifyCreatureAttributes: Creature {} ({}) | Map config time set to ({}).", + creature->GetName(), + creature->GetLevel(), + mapABInfo->mapConfigTime + ); + creatureABInfo->mapConfigTime = mapABInfo->mapConfigTime; + } // check to make sure that the creature's map is enabled for scaling if (!mapABInfo->enabled) + { + LOG_DEBUG("module.AutoBalance", "AutoBalance_AllCreatureScript::ModifyCreatureAttributes: Creature {} ({}) | is in map {} ({}{}{}{}) that is not enabled, not changed.", + creature->GetName(), + creatureABInfo->UnmodifiedLevel, + map->GetMapName(), + map->GetId(), + instanceMap ? "-" + std::to_string(instanceMap->GetInstanceId()) : "", + instanceMap ? ", " + std::to_string(instanceMap->GetMaxPlayers()) + "-player" : "", + instanceMap ? instanceMap->IsHeroic() ? " Heroic" : " Normal" : "" + ); + + // return the creature back to their original level, if it's not already + creatureABInfo->selectedLevel = creatureABInfo->UnmodifiedLevel; + return; + } + + // if the creature isn't relevant, don't modify it + if (!isCreatureRelevant(creature)) + { + LOG_DEBUG("module.AutoBalance", "AutoBalance_AllCreatureScript::ModifyCreatureAttributes: Creature {} ({}) | is not relevant, not changed.", + creature->GetName(), + creatureABInfo->UnmodifiedLevel + ); + + // return the creature back to their original level, if it's not already + creatureABInfo->selectedLevel = creatureABInfo->UnmodifiedLevel; - // if this creature is below 85% of the minimum LFG level for the map, make no changes - if (creatureABInfo->UnmodifiedLevel < (float)mapABInfo->lfgMinLevel * .85f) return; + } + // if this creature is below 85% of the minimum LFG level for the map, make no changes // if this creature is above 115% of the maximum LFG level for the map, make no changes - if (creatureABInfo->UnmodifiedLevel > (float)mapABInfo->lfgMaxLevel * 1.15f) + // if this is a critter that is substantial enough to be considered a real enemy, still modify it + // if this is a trigger, still modify it + if ( + ( + (creatureABInfo->UnmodifiedLevel < (uint8)(((float)mapABInfo->lfgMinLevel * .85f) + 0.5f)) || + (creatureABInfo->UnmodifiedLevel > (uint8)(((float)mapABInfo->lfgMaxLevel * 1.15f) + 0.5f)) + ) && + ( + !(creature->IsCritter() && creatureABInfo->UnmodifiedLevel >= 5 && creature->GetMaxHealth() > 100) && + !creature->IsTrigger() + ) + ) + { + LOG_DEBUG("module.AutoBalance", "AutoBalance_AllCreatureScript::ModifyCreatureAttributes: Creature {} ({}) | is a {} outside of the expected NPC level range for the map ({} to {}), not modified.", + creature->GetName(), + creatureABInfo->UnmodifiedLevel, + creature->IsCritter() ? "critter" : "creature", + (uint8)(((float)mapABInfo->lfgMinLevel * .85f) + 0.5f), + (uint8)(((float)mapABInfo->lfgMaxLevel * 1.15f) + 0.5f) + ); + + creatureABInfo->selectedLevel = creatureABInfo->UnmodifiedLevel; + return; + } // if the creature was dead (but this function is being called because they are being revived), reset it and allow modifications if (creatureABInfo->wasAliveNowDead) { - LOG_DEBUG("module.AutoBalance", "AutoBalance_AllCreatureScript::ModifyCreatureAttributes(): {} ({}) was dead but appears to be alive now, reset wasAliveNowDead flag.", creature->GetName(), creatureABInfo->UnmodifiedLevel); + LOG_DEBUG("module.AutoBalance", "AutoBalance_AllCreatureScript::ModifyCreatureAttributes: Creature {} ({}) | was dead but appears to be alive now, reset wasAliveNowDead flag.", creature->GetName(), creatureABInfo->UnmodifiedLevel); // if the creature was dead, reset it creatureABInfo->wasAliveNowDead = false; } // if the creature is dead and wasn't marked as dead by this script, simply skip else if (creature->isDead()) { - LOG_DEBUG("module.AutoBalance", "AutoBalance_AllCreatureScript::ModifyCreatureAttributes(): {} ({}) is dead, do not modify.", creature->GetName(), creatureABInfo->UnmodifiedLevel); + LOG_DEBUG("module.AutoBalance", "AutoBalance_AllCreatureScript::ModifyCreatureAttributes: Creature {} ({}) | is dead, do not modify.", creature->GetName(), creatureABInfo->UnmodifiedLevel); return; } CreatureTemplate const *creatureTemplate = creature->GetCreatureTemplate(); - InstanceMap* instanceMap = ((InstanceMap*)sMapMgr->FindMap(creature->GetMapId(), creature->GetInstanceId())); - uint32 mapId = instanceMap->GetEntry()->MapID; - uint32 maxNumberOfPlayers = instanceMap->GetMaxPlayers(); - // check to see if the creature is in the forced num players list uint32 forcedNumPlayers = GetForcedNumPlayers(creatureTemplate->Entry); if (forcedNumPlayers == 0) + { + LOG_DEBUG("module.AutoBalance", "AutoBalance_AllCreatureScript::ModifyCreatureAttributes: Creature {} ({}) | is in the forced num players list with a value of 0, not changed.", creature->GetName(), creatureABInfo->UnmodifiedLevel); return; // forcedNumPlayers 0 means that the creature is contained in DisabledID -> no scaling + } // start with the map's adjusted player count uint32 adjustedPlayerCount = mapABInfo->adjustedPlayerCount; // if the forced value is set and the adjusted player count is above the forced value, change it to match if (forcedNumPlayers > 0 && adjustedPlayerCount > forcedNumPlayers) - adjustedPlayerCount = maxNumberOfPlayers; + { + LOG_DEBUG("module.AutoBalance", "AutoBalance_AllCreatureScript::ModifyCreatureAttributes: Creature {} ({}) | is in the forced num players list with a value of {}, adjusting adjustedPlayerCount to match.", creature->GetName(), creatureABInfo->UnmodifiedLevel, forcedNumPlayers); + adjustedPlayerCount = forcedNumPlayers; + } // store the current player count in the creature and map's data creatureABInfo->instancePlayerCount = adjustedPlayerCount; if (!creatureABInfo->instancePlayerCount) // no players in map, do not modify attributes + { + LOG_DEBUG("module.AutoBalance", "AutoBalance_AllCreatureScript::ModifyCreatureAttributes: Creature {} ({}) | is on a map with no players, not changed.", creature->GetName(), creatureABInfo->UnmodifiedLevel); return; + } if (!sABScriptMgr->OnBeforeModifyAttributes(creature, creatureABInfo->instancePlayerCount)) return; // only scale levels if level scaling is enabled and the instance's average creature level is not within the skip range - if (LevelScaling && - ((mapABInfo->avgCreatureLevel > mapABInfo->highestPlayerLevel + mapABInfo->levelScalingSkipHigherLevels || mapABInfo->levelScalingSkipHigherLevels == 0) || - (mapABInfo->avgCreatureLevel < mapABInfo->highestPlayerLevel - mapABInfo->levelScalingSkipLowerLevels || mapABInfo->levelScalingSkipLowerLevels == 0)) - ) + if + ( + LevelScaling && + ( + (mapABInfo->avgCreatureLevel > mapABInfo->highestPlayerLevel + mapABInfo->levelScalingSkipHigherLevels || mapABInfo->levelScalingSkipHigherLevels == 0) || + (mapABInfo->avgCreatureLevel < mapABInfo->highestPlayerLevel - mapABInfo->levelScalingSkipLowerLevels || mapABInfo->levelScalingSkipLowerLevels == 0) + ) && + !creatureABInfo->neverLevelScale + ) { uint8 selectedLevel; - // if we're using dynamic scaling, calculate the creature's level based relative to the highest player level in the map - if (LevelScalingMethod == AUTOBALANCE_SCALING_DYNAMIC) - { - LOG_DEBUG("module.AutoBalance", "AutoBalance_AllCreatureScript::ModifyCreatureAttributes: Creature {} ({}) dynamic scaling floor: {}, ceiling: {}.", creature->GetName(), creatureABInfo->UnmodifiedLevel, mapABInfo->levelScalingDynamicFloor, mapABInfo->levelScalingDynamicCeiling); - - // calculate the creature's new level - selectedLevel = (mapABInfo->highestPlayerLevel + mapABInfo->levelScalingDynamicCeiling) - (mapABInfo->highestCreatureLevel - creatureABInfo->UnmodifiedLevel); - - // check to be sure that the creature's new level is at least the dynamic scaling floor - if (selectedLevel < (mapABInfo->highestPlayerLevel - mapABInfo->levelScalingDynamicFloor)) - { - selectedLevel = mapABInfo->highestPlayerLevel - mapABInfo->levelScalingDynamicFloor; - } - - // check to be sure that the creature's new level is no higher than the dynamic scaling ceiling - if (selectedLevel > (mapABInfo->highestPlayerLevel + mapABInfo->levelScalingDynamicCeiling)) - { - selectedLevel = mapABInfo->highestPlayerLevel + mapABInfo->levelScalingDynamicCeiling; - } - - LOG_DEBUG("module.AutoBalance", "AutoBalance_AllCreatureScript::ModifyCreatureAttributes: Creature {} ({}) scaled to {} via dynamic scaling.", creature->GetName(), creatureABInfo->UnmodifiedLevel, selectedLevel); - } - // otherwise we're using "fixed" scaling and should use the highest player level in the map - else - { - selectedLevel = mapABInfo->highestPlayerLevel; - LOG_DEBUG("module.AutoBalance", "AutoBalance_AllCreatureScript::ModifyCreatureAttributes: Creature {} ({}) scaled to {} via fixed scaling.", creature->GetName(), creatureABInfo->UnmodifiedLevel, selectedLevel); - } - - creatureABInfo->selectedLevel = selectedLevel; - creature->SetLevel(creatureABInfo->selectedLevel); - } - else - { - LOG_DEBUG("module.AutoBalance", "AutoBalance_AllCreatureScript::ModifyCreatureAttributes: Creature {} ({}) not level scaled due to level scaling being disabled or the instance's average creature level being outside the skip range.", creature->GetName(), creatureABInfo->UnmodifiedLevel); - creatureABInfo->selectedLevel = creatureABInfo->UnmodifiedLevel; - } - - creatureABInfo->entry = creature->GetEntry(); - - CreatureBaseStats const* origCreatureStats = sObjectMgr->GetCreatureBaseStats(creatureABInfo->UnmodifiedLevel, creatureTemplate->unit_class); - CreatureBaseStats const* creatureStats = sObjectMgr->GetCreatureBaseStats(creatureABInfo->selectedLevel, creatureTemplate->unit_class); - - uint32 baseMana = origCreatureStats->GenerateMana(creatureTemplate); - uint32 scaledHealth = 0; - uint32 scaledMana = 0; - - // Note: InflectionPoint handle the number of players required to get 50% health. - // you'd adjust this to raise or lower the hp modifier for per additional player in a non-whole group. - // - // diff modify the rate of percentage increase between - // number of players. Generally the closer to the value of 1 you have this - // the less gradual the rate will be. For example in a 5 man it would take 3 - // total players to face a mob at full health. - // - // The +1 and /2 values raise the TanH function to a positive range and make - // sure the modifier never goes above the value or 1.0 or below 0. - // - // curveFloor and curveCeiling squishes the curve by adjusting the curve start and end points. - // This allows for better control over high and low player count scaling. - - float defaultMultiplier; - float curveFloor; - float curveCeiling; - - // - // Inflection Point - // - float inflectionValue = (float)maxNumberOfPlayers; - - if (instanceMap->IsHeroic()) - { - switch (maxNumberOfPlayers) - { - case 1: - case 2: - case 3: - case 4: - case 5: - inflectionValue *= InflectionPointHeroic; - curveFloor = InflectionPointHeroicCurveFloor; - curveCeiling = InflectionPointHeroicCurveCeiling; - break; - case 10: - inflectionValue *= InflectionPointRaid10MHeroic; - curveFloor = InflectionPointRaid10MHeroicCurveFloor; - curveCeiling = InflectionPointRaid10MHeroicCurveCeiling; - break; - case 25: - inflectionValue *= InflectionPointRaid25MHeroic; - curveFloor = InflectionPointRaid25MHeroicCurveFloor; - curveCeiling = InflectionPointRaid25MHeroicCurveCeiling; - break; - default: - inflectionValue *= InflectionPointRaidHeroic; - curveFloor = InflectionPointRaidHeroicCurveFloor; - curveCeiling = InflectionPointRaidHeroicCurveCeiling; - } - } - else - { - switch (maxNumberOfPlayers) - { - case 1: - case 2: - case 3: - case 4: - case 5: - inflectionValue *= InflectionPoint; - curveFloor = InflectionPointCurveFloor; - curveCeiling = InflectionPointCurveCeiling; - break; - case 10: - inflectionValue *= InflectionPointRaid10M; - curveFloor = InflectionPointRaid10MCurveFloor; - curveCeiling = InflectionPointRaid10MCurveCeiling; - break; - case 15: - inflectionValue *= InflectionPointRaid15M; - curveFloor = InflectionPointRaid15MCurveFloor; - curveCeiling = InflectionPointRaid15MCurveCeiling; - break; - case 20: - inflectionValue *= InflectionPointRaid20M; - curveFloor = InflectionPointRaid20MCurveFloor; - curveCeiling = InflectionPointRaid20MCurveCeiling; - break; - case 25: - inflectionValue *= InflectionPointRaid25M; - curveFloor = InflectionPointRaid25MCurveFloor; - curveCeiling = InflectionPointRaid25MCurveCeiling; - break; - case 40: - inflectionValue *= InflectionPointRaid40M; - curveFloor = InflectionPointRaid40MCurveFloor; - curveCeiling = InflectionPointRaid40MCurveCeiling; - break; - default: - inflectionValue *= InflectionPointRaid; - curveFloor = InflectionPointRaidCurveFloor; - curveCeiling = InflectionPointRaidCurveCeiling; - } - } - - // Per map ID overrides alter the above settings, if set - if (hasDungeonOverride(mapId)) - { - AutoBalanceInflectionPointSettings* myInflectionPointOverrides = &dungeonOverrides[mapId]; - - // Alter the inflectionValue according to the override, if set - if (myInflectionPointOverrides->value != -1) - { - inflectionValue = (float)maxNumberOfPlayers; // Starting over - inflectionValue *= myInflectionPointOverrides->value; - } - - if (myInflectionPointOverrides->curveFloor != -1) { curveFloor = myInflectionPointOverrides->curveFloor; } - if (myInflectionPointOverrides->curveCeiling != -1) { curveCeiling = myInflectionPointOverrides->curveCeiling; } - } - - // - // Boss Inflection Point - // - if (creature->IsDungeonBoss()) { - - float bossInflectionPointMultiplier; - - // Determine the correct boss inflection multiplier - if (instanceMap->IsHeroic()) - { - switch (maxNumberOfPlayers) - { - case 1: - case 2: - case 3: - case 4: - case 5: - bossInflectionPointMultiplier = InflectionPointHeroicBoss; - break; - case 10: - bossInflectionPointMultiplier = InflectionPointRaid10MHeroicBoss; - break; - case 25: - bossInflectionPointMultiplier = InflectionPointRaid25MHeroicBoss; - break; - default: - bossInflectionPointMultiplier = InflectionPointRaidHeroicBoss; - } - } - else - { - switch (maxNumberOfPlayers) - { - case 1: - case 2: - case 3: - case 4: - case 5: - bossInflectionPointMultiplier = InflectionPointBoss; - break; - case 10: - bossInflectionPointMultiplier = InflectionPointRaid10MBoss; - break; - case 15: - bossInflectionPointMultiplier = InflectionPointRaid15MBoss; - break; - case 20: - bossInflectionPointMultiplier = InflectionPointRaid20MBoss; - break; - case 25: - bossInflectionPointMultiplier = InflectionPointRaid25MBoss; - break; - case 40: - bossInflectionPointMultiplier = InflectionPointRaid40MBoss; - break; - default: - bossInflectionPointMultiplier = InflectionPointRaidBoss; - } - } + // handle "special" creatures + // note that these already passed a more complex check above + if ( + (creature->IsTotem() && creature->IsSummon() && creatureABInfo->summoner && creatureABInfo->summoner->IsPlayer()) || + ( + creature->IsCritter() && creatureABInfo->UnmodifiedLevel <= 5 && creature->GetMaxHealth() <= 100 + ) + ) + { + LOG_DEBUG("module.AutoBalance", "AutoBalance_AllCreatureScript::ModifyCreatureAttributes: Creature {} ({}) | is a {} that will not be level scaled, but will have modifiers set.", + creature->GetName(), + creatureABInfo->UnmodifiedLevel, + creature->IsTotem() ? "totem" : "critter" + ); - // Per map ID overrides alter the above settings, if set - if (hasBossOverride(mapId)) + selectedLevel = creatureABInfo->UnmodifiedLevel; + } + // if we're using dynamic scaling, calculate the creature's level based relative to the highest player level in the map + else if (LevelScalingMethod == AUTOBALANCE_SCALING_DYNAMIC) { - AutoBalanceInflectionPointSettings* myBossOverrides = &bossOverrides[mapId]; + // calculate the creature's new level + selectedLevel = (mapABInfo->highestPlayerLevel + mapABInfo->levelScalingDynamicCeiling) - (mapABInfo->highestCreatureLevel - creatureABInfo->UnmodifiedLevel); - // If set, alter the inflectionValue according to the override - if (myBossOverrides->value != -1) + // check to be sure that the creature's new level is at least the dynamic scaling floor + if (selectedLevel < (mapABInfo->highestPlayerLevel - mapABInfo->levelScalingDynamicFloor)) { - inflectionValue *= myBossOverrides->value; + selectedLevel = mapABInfo->highestPlayerLevel - mapABInfo->levelScalingDynamicFloor; } - // Otherwise, calculate using the value determined by instance type - else + + // check to be sure that the creature's new level is no higher than the dynamic scaling ceiling + if (selectedLevel > (mapABInfo->highestPlayerLevel + mapABInfo->levelScalingDynamicCeiling)) { - inflectionValue *= bossInflectionPointMultiplier; + selectedLevel = mapABInfo->highestPlayerLevel + mapABInfo->levelScalingDynamicCeiling; } + + LOG_DEBUG("module.AutoBalance", "AutoBalance_AllCreatureScript::ModifyCreatureAttributes: Creature {} ({}) | scaled to level ({}) via dynamic scaling.", + creature->GetName(), + creatureABInfo->UnmodifiedLevel, + selectedLevel + ); } - // No override, use the value determined by the instance type + // otherwise we're using "fixed" scaling and should use the highest player level in the map else { - inflectionValue *= bossInflectionPointMultiplier; + selectedLevel = mapABInfo->highestPlayerLevel; + LOG_DEBUG("module.AutoBalance", "AutoBalance_AllCreatureScript::ModifyCreatureAttributes: Creature {} ({}) | scaled to level ({}) via fixed scaling.", creature->GetName(), creatureABInfo->UnmodifiedLevel, selectedLevel); } - } - // - // Stat Modifiers - // + creatureABInfo->selectedLevel = selectedLevel; - // Calculate stat modifiers - float statMod_global, statMod_health, statMod_mana, statMod_armor, statMod_damage, statMod_ccDuration; - float statMod_boss_global, statMod_boss_health, statMod_boss_mana, statMod_boss_armor, statMod_boss_damage, statMod_boss_ccDuration; - - // Apply the per-instance-type modifiers first - if (instanceMap->IsHeroic()) - { - switch (maxNumberOfPlayers) - { - case 1: - case 2: - case 3: - case 4: - case 5: - statMod_global = StatModifierHeroic_Global; - statMod_health = StatModifierHeroic_Health; - statMod_mana = StatModifierHeroic_Mana; - statMod_armor = StatModifierHeroic_Armor; - statMod_damage = StatModifierHeroic_Damage; - statMod_ccDuration = StatModifierHeroic_CCDuration; - - statMod_boss_global = StatModifierHeroic_Boss_Global; - statMod_boss_health = StatModifierHeroic_Boss_Health; - statMod_boss_mana = StatModifierHeroic_Boss_Mana; - statMod_boss_armor = StatModifierHeroic_Boss_Armor; - statMod_boss_damage = StatModifierHeroic_Boss_Damage; - statMod_boss_ccDuration = StatModifierHeroic_Boss_CCDuration; - break; - case 10: - statMod_global = StatModifierRaid10MHeroic_Global; - statMod_health = StatModifierRaid10MHeroic_Health; - statMod_mana = StatModifierRaid10MHeroic_Mana; - statMod_armor = StatModifierRaid10MHeroic_Armor; - statMod_damage = StatModifierRaid10MHeroic_Damage; - statMod_ccDuration = StatModifierRaid10MHeroic_CCDuration; - - statMod_boss_global = StatModifierRaid10MHeroic_Boss_Global; - statMod_boss_health = StatModifierRaid10MHeroic_Boss_Health; - statMod_boss_mana = StatModifierRaid10MHeroic_Boss_Mana; - statMod_boss_armor = StatModifierRaid10MHeroic_Boss_Armor; - statMod_boss_damage = StatModifierRaid10MHeroic_Boss_Damage; - statMod_boss_ccDuration = StatModifierRaid10MHeroic_Boss_CCDuration; - break; - case 25: - statMod_global = StatModifierRaid25MHeroic_Global; - statMod_health = StatModifierRaid25MHeroic_Health; - statMod_mana = StatModifierRaid25MHeroic_Mana; - statMod_armor = StatModifierRaid25MHeroic_Armor; - statMod_damage = StatModifierRaid25MHeroic_Damage; - statMod_ccDuration = StatModifierRaid25MHeroic_CCDuration; - - statMod_boss_global = StatModifierRaid25MHeroic_Boss_Global; - statMod_boss_health = StatModifierRaid25MHeroic_Boss_Health; - statMod_boss_mana = StatModifierRaid25MHeroic_Boss_Mana; - statMod_boss_armor = StatModifierRaid25MHeroic_Boss_Armor; - statMod_boss_damage = StatModifierRaid25MHeroic_Boss_Damage; - statMod_boss_ccDuration = StatModifierRaid25MHeroic_Boss_CCDuration; - break; - default: - statMod_global = StatModifierRaidHeroic_Global; - statMod_health = StatModifierRaidHeroic_Health; - statMod_mana = StatModifierRaidHeroic_Mana; - statMod_armor = StatModifierRaidHeroic_Armor; - statMod_damage = StatModifierRaidHeroic_Damage; - statMod_ccDuration = StatModifierRaidHeroic_CCDuration; - - statMod_boss_global = StatModifierRaidHeroic_Global; - statMod_boss_health = StatModifierRaidHeroic_Health; - statMod_boss_mana = StatModifierRaidHeroic_Mana; - statMod_boss_armor = StatModifierRaidHeroic_Armor; - statMod_boss_damage = StatModifierRaidHeroic_Damage; - statMod_boss_ccDuration = StatModifierRaidHeroic_Boss_CCDuration; - } - } - else - { - switch (maxNumberOfPlayers) - { - case 1: - case 2: - case 3: - case 4: - case 5: - statMod_global = StatModifier_Global; - statMod_health = StatModifier_Health; - statMod_mana = StatModifier_Mana; - statMod_armor = StatModifier_Armor; - statMod_damage = StatModifier_Damage; - statMod_ccDuration = StatModifier_CCDuration; - - statMod_boss_global = StatModifier_Boss_Global; - statMod_boss_health = StatModifier_Boss_Health; - statMod_boss_mana = StatModifier_Boss_Mana; - statMod_boss_armor = StatModifier_Boss_Armor; - statMod_boss_damage = StatModifier_Boss_Damage; - statMod_boss_ccDuration = StatModifier_Boss_CCDuration; - break; - case 10: - statMod_global = StatModifierRaid10M_Global; - statMod_health = StatModifierRaid10M_Health; - statMod_mana = StatModifierRaid10M_Mana; - statMod_armor = StatModifierRaid10M_Armor; - statMod_damage = StatModifierRaid10M_Damage; - statMod_ccDuration = StatModifierRaid10M_CCDuration; - - statMod_boss_global = StatModifierRaid10M_Boss_Global; - statMod_boss_health = StatModifierRaid10M_Boss_Health; - statMod_boss_mana = StatModifierRaid10M_Boss_Mana; - statMod_boss_armor = StatModifierRaid10M_Boss_Armor; - statMod_boss_damage = StatModifierRaid10M_Boss_Damage; - statMod_boss_ccDuration = StatModifierRaid10M_Boss_CCDuration; - break; - case 15: - statMod_global = StatModifierRaid15M_Global; - statMod_health = StatModifierRaid15M_Health; - statMod_mana = StatModifierRaid15M_Mana; - statMod_armor = StatModifierRaid15M_Armor; - statMod_damage = StatModifierRaid15M_Damage; - statMod_ccDuration = StatModifierRaid15M_CCDuration; - - statMod_boss_global = StatModifierRaid15M_Boss_Global; - statMod_boss_health = StatModifierRaid15M_Boss_Health; - statMod_boss_mana = StatModifierRaid15M_Boss_Mana; - statMod_boss_armor = StatModifierRaid15M_Boss_Armor; - statMod_boss_damage = StatModifierRaid15M_Boss_Damage; - statMod_boss_ccDuration = StatModifierRaid15M_Boss_CCDuration; - break; - case 20: - statMod_global = StatModifierRaid20M_Global; - statMod_health = StatModifierRaid20M_Health; - statMod_mana = StatModifierRaid20M_Mana; - statMod_armor = StatModifierRaid20M_Armor; - statMod_damage = StatModifierRaid20M_Damage; - statMod_ccDuration = StatModifierRaid20M_CCDuration; - - statMod_boss_global = StatModifierRaid20M_Boss_Global; - statMod_boss_health = StatModifierRaid20M_Boss_Health; - statMod_boss_mana = StatModifierRaid20M_Boss_Mana; - statMod_boss_armor = StatModifierRaid20M_Boss_Armor; - statMod_boss_damage = StatModifierRaid20M_Boss_Damage; - statMod_boss_ccDuration = StatModifierRaid20M_Boss_CCDuration; - break; - case 25: - statMod_global = StatModifierRaid25M_Global; - statMod_health = StatModifierRaid25M_Health; - statMod_mana = StatModifierRaid25M_Mana; - statMod_armor = StatModifierRaid25M_Armor; - statMod_damage = StatModifierRaid25M_Damage; - statMod_ccDuration = StatModifierRaid25M_CCDuration; - - statMod_boss_global = StatModifierRaid25M_Boss_Global; - statMod_boss_health = StatModifierRaid25M_Boss_Health; - statMod_boss_mana = StatModifierRaid25M_Boss_Mana; - statMod_boss_armor = StatModifierRaid25M_Boss_Armor; - statMod_boss_damage = StatModifierRaid25M_Boss_Damage; - statMod_boss_ccDuration = StatModifierRaid25M_Boss_CCDuration; - break; - case 40: - statMod_global = StatModifierRaid40M_Global; - statMod_health = StatModifierRaid40M_Health; - statMod_mana = StatModifierRaid40M_Mana; - statMod_armor = StatModifierRaid40M_Armor; - statMod_damage = StatModifierRaid40M_Damage; - statMod_ccDuration = StatModifierRaid40M_CCDuration; - - statMod_boss_global = StatModifierRaid40M_Boss_Global; - statMod_boss_health = StatModifierRaid40M_Boss_Health; - statMod_boss_mana = StatModifierRaid40M_Boss_Mana; - statMod_boss_armor = StatModifierRaid40M_Boss_Armor; - statMod_boss_damage = StatModifierRaid40M_Boss_Damage; - statMod_boss_ccDuration = StatModifierRaid40M_Boss_CCDuration; - break; - default: - statMod_global = StatModifierRaid_Global; - statMod_health = StatModifierRaid_Health; - statMod_mana = StatModifierRaid_Mana; - statMod_armor = StatModifierRaid_Armor; - statMod_damage = StatModifierRaid_Damage; - statMod_ccDuration = StatModifierRaid_CCDuration; - - statMod_boss_global = StatModifierRaid_Boss_Global; - statMod_boss_health = StatModifierRaid_Boss_Health; - statMod_boss_mana = StatModifierRaid_Boss_Mana; - statMod_boss_armor = StatModifierRaid_Boss_Armor; - statMod_boss_damage = StatModifierRaid_Boss_Damage; - statMod_boss_ccDuration = StatModifierRaid_Boss_CCDuration; - } - } - - // Boss modifiers - if (creature->IsDungeonBoss()) - { - // Start with the settings determined above - // AutoBalance.StatModifier*.Boss. - if (creature->IsDungeonBoss()) - { - statMod_global = statMod_boss_global; - statMod_health = statMod_boss_health; - statMod_mana = statMod_boss_mana; - statMod_armor = statMod_boss_armor; - statMod_damage = statMod_boss_damage; - statMod_ccDuration = statMod_boss_ccDuration; - } - - // Per-instance boss overrides - // AutoBalance.StatModifier.Boss.PerInstance - if (creature->IsDungeonBoss() && hasStatModifierBossOverride(mapId)) - { - AutoBalanceStatModifiers* myStatModifierBossOverrides = &statModifierBossOverrides[mapId]; - - if (myStatModifierBossOverrides->global != -1) { statMod_global = myStatModifierBossOverrides->global; } - if (myStatModifierBossOverrides->health != -1) { statMod_health = myStatModifierBossOverrides->health; } - if (myStatModifierBossOverrides->mana != -1) { statMod_mana = myStatModifierBossOverrides->mana; } - if (myStatModifierBossOverrides->armor != -1) { statMod_armor = myStatModifierBossOverrides->armor; } - if (myStatModifierBossOverrides->damage != -1) { statMod_damage = myStatModifierBossOverrides->damage; } - if (myStatModifierBossOverrides->ccduration != -1) { statMod_ccDuration = myStatModifierBossOverrides->ccduration; } - } - } - // Non-boss modifiers - else - { - // Per-instance non-boss overrides - // AutoBalance.StatModifier.PerInstance - if (hasStatModifierOverride(mapId)) + if (creature->GetLevel() != selectedLevel) { - AutoBalanceStatModifiers* myStatModifierOverrides = &statModifierOverrides[mapId]; + if (!creatureABInfo->isBrandNew) + { + LOG_DEBUG("module.AutoBalance", "AutoBalance_AllCreatureScript::ModifyCreatureAttributes: Creature {} ({}) | is set to new selectedLevel ({}).", + creature->GetName(), + creatureABInfo->UnmodifiedLevel, + selectedLevel + ); - if (myStatModifierOverrides->global != -1) { statMod_global = myStatModifierOverrides->global; } - if (myStatModifierOverrides->health != -1) { statMod_health = myStatModifierOverrides->health; } - if (myStatModifierOverrides->mana != -1) { statMod_mana = myStatModifierOverrides->mana; } - if (myStatModifierOverrides->armor != -1) { statMod_armor = myStatModifierOverrides->armor; } - if (myStatModifierOverrides->damage != -1) { statMod_damage = myStatModifierOverrides->damage; } - if (myStatModifierOverrides->ccduration != -1) { statMod_ccDuration = myStatModifierOverrides->ccduration; } + creature->SetLevel(selectedLevel); + } } } + else if (!LevelScaling) + { + LOG_DEBUG("module.AutoBalance", "AutoBalance_AllCreatureScript::ModifyCreatureAttributes: Creature {} ({}) | not level scaled due to level scaling being disabled.", creature->GetName(), creatureABInfo->UnmodifiedLevel); + creatureABInfo->selectedLevel = creatureABInfo->UnmodifiedLevel; + } + else if (creatureABInfo->neverLevelScale) + { + LOG_DEBUG("module.AutoBalance", "AutoBalance_AllCreatureScript::ModifyCreatureAttributes: Creature {} ({}) | not level scaled due to being marked as multipliers only.", creature->GetName(), creatureABInfo->UnmodifiedLevel); + creatureABInfo->selectedLevel = creatureABInfo->UnmodifiedLevel; + } + else + { + LOG_DEBUG("module.AutoBalance", "AutoBalance_AllCreatureScript::ModifyCreatureAttributes: Creature {} ({}) | not level scaled due the instance's average creature level being inside the skip range.", creature->GetName(), creatureABInfo->UnmodifiedLevel); + creatureABInfo->selectedLevel = creatureABInfo->UnmodifiedLevel; + } - // Per-creature modifiers applied last - // AutoBalance.StatModifier.PerCreature - if (hasStatModifierCreatureOverride(creatureABInfo->entry)) + if (creatureABInfo->isBrandNew) { - AutoBalanceStatModifiers* myCreatureOverrides = &statModifierCreatureOverrides[creatureABInfo->entry]; + LOG_DEBUG("module.AutoBalance", "AutoBalance_AllCreatureScript::ModifyCreatureAttributes: Creature {} ({}) | is brand new, do not modify level or stats yet.", + creature->GetName(), + creatureABInfo->UnmodifiedLevel + ); - if (myCreatureOverrides->global != -1) { statMod_global = myCreatureOverrides->global; } - if (myCreatureOverrides->health != -1) { statMod_health = myCreatureOverrides->health; } - if (myCreatureOverrides->mana != -1) { statMod_mana = myCreatureOverrides->mana; } - if (myCreatureOverrides->armor != -1) { statMod_armor = myCreatureOverrides->armor; } - if (myCreatureOverrides->damage != -1) { statMod_damage = myCreatureOverrides->damage; } - if (myCreatureOverrides->ccduration != -1) { statMod_ccDuration = myCreatureOverrides->ccduration; } + return; } - // #maththings - float diff = ((float)maxNumberOfPlayers/5)*1.5f; + CreatureBaseStats const* origCreatureBaseStats = sObjectMgr->GetCreatureBaseStats(creatureABInfo->UnmodifiedLevel, creatureTemplate->unit_class); + CreatureBaseStats const* newCreatureBaseStats = sObjectMgr->GetCreatureBaseStats(creatureABInfo->selectedLevel, creatureTemplate->unit_class); - // For math reasons that I do not understand, curveCeiling needs to be adjusted to bring the actual multiplier - // closer to the curveCeiling setting. Create an adjustment based on how much the ceiling should be changed at - // the max players multiplier. - float curveCeilingAdjustment = curveCeiling / (((tanh(((float)maxNumberOfPlayers - inflectionValue) / diff) + 1.0f) / 2.0f) * (curveCeiling - curveFloor) + curveFloor); + // Inflection Point + AutoBalanceInflectionPointSettings inflectionPointSettings = getInflectionPointSettings(instanceMap, isBossOrBossSummon(creature)); - // Adjust the multiplier based on the configured floor and ceiling values, plus the ceiling adjustment we just calculated - defaultMultiplier = ((tanh(((float)creatureABInfo->instancePlayerCount - inflectionValue) / diff) + 1.0f) / 2.0f) * (curveCeiling * curveCeilingAdjustment - curveFloor) + curveFloor; + // Generate the default multiplier + float defaultMultiplier = getDefaultMultiplier(instanceMap, inflectionPointSettings); if (!sABScriptMgr->OnAfterDefaultMultiplier(creature, defaultMultiplier)) return; + // Stat Modifiers + AutoBalanceStatModifiers statModifiers = getStatModifiers(map, creature); + float statMod_global = statModifiers.global; + float statMod_health = statModifiers.health; + float statMod_mana = statModifiers.mana; + float statMod_armor = statModifiers.armor; + float statMod_damage = statModifiers.damage; + float statMod_ccDuration = statModifiers.ccduration; + + // Storage for the final values applied to the creature + uint32 newFinalHealth = 0; + uint32 newFinalMana = 0; + uint32 newFinalArmor = 0; + // // Health Scaling // + LOG_DEBUG("module.AutoBalance_StatGeneration", "AutoBalance_AllCreatureScript::ModifyCreatureAttributes: Creature {} ({}) | ---------- HEALTH MULTIPLIER ----------", + creature->GetName(), + creatureABInfo->selectedLevel + ); float healthMultiplier = defaultMultiplier * statMod_global * statMod_health; + float scaledHealthMultiplier; + + LOG_DEBUG("module.AutoBalance_StatGeneration", "AutoBalance_AllCreatureScript::ModifyCreatureAttributes: Creature {} ({}) | HealthMultiplier: ({}) = defaultMultiplier ({}) * statMod_global ({}) * statMod_health ({})", + creature->GetName(), + creatureABInfo->selectedLevel, + healthMultiplier, + defaultMultiplier, + statMod_global, + statMod_health + ); + // Can't be less than MinHPModifier if (healthMultiplier <= MinHPModifier) + { healthMultiplier = MinHPModifier; - float hpStatsRate = 1.0f; - float originalHealth = origCreatureStats->GenerateHealth(creatureTemplate); - - float newBaseHealth; + LOG_DEBUG("module.AutoBalance_StatGeneration", "AutoBalance_AllCreatureScript::ModifyCreatureAttributes: Creature {} ({}) | HealthMultiplier: ({}) - capped to MinHPModifier ({})", + creature->GetName(), + creatureABInfo->selectedLevel, + healthMultiplier, + MinHPModifier + ); + } - // The database holds multiple values for base health, one for each expansion - // This code will smooth transition between the different expansions based on the highest player level in the instance - // Only do this if level scaling is enabled + // set the non-level-scaled health multiplier on the creature's AB info + creatureABInfo->HealthMultiplier = healthMultiplier; - if (LevelScaling) + // only level scale health if level scaling is enabled and the creature level has been altered + if (LevelScaling && creatureABInfo->selectedLevel != creatureABInfo->UnmodifiedLevel) { - float vanillaHealth = creatureStats->BaseHealth[0]; - float bcHealth = creatureStats->BaseHealth[1]; - float wotlkHealth = creatureStats->BaseHealth[2]; - - // vanilla health - if (mapABInfo->highestPlayerLevel <= 60) - { - newBaseHealth = vanillaHealth; - } - // transition from vanilla to BC health - else if (mapABInfo->highestPlayerLevel < 63) - { - float vanillaMultiplier = (63 - mapABInfo->highestPlayerLevel) / 3.0f; - float bcMultiplier = 1.0f - vanillaMultiplier; - - newBaseHealth = (vanillaHealth * vanillaMultiplier) + (bcHealth * bcMultiplier); - } - // BC health - else if (mapABInfo->highestPlayerLevel <= 70) - { - newBaseHealth = bcHealth; - } - // transition from BC to WotLK health - else if (mapABInfo->highestPlayerLevel < 73) - { - float bcMultiplier = (73 - mapABInfo->highestPlayerLevel) / 3.0f; - float wotlkMultiplier = 1.0f - bcMultiplier; - - newBaseHealth = (bcHealth * bcMultiplier) + (wotlkHealth * wotlkMultiplier); - } - // WotLK health - else - { - newBaseHealth = wotlkHealth; - - // special increase for end-game content - if (LevelScalingEndGameBoost) - if (mapABInfo->highestPlayerLevel >= 75 && creatureABInfo->UnmodifiedLevel < 75) - { - newBaseHealth *= (float)(mapABInfo->highestPlayerLevel-70) * 0.3f; - } - } - + // the max health that the creature had before we did anything with it + float origHealth = origCreatureBaseStats->GenerateHealth(creatureTemplate); + LOG_DEBUG("module.AutoBalance_StatGeneration", "AutoBalance_AllCreatureScript::ModifyCreatureAttributes: Creature {} ({}) | origHealth ({}) = origCreatureBaseStats->GenerateHealth(creatureTemplate)", + creature->GetName(), + creatureABInfo->selectedLevel, + origHealth + ); + + // the base health of the new creature level for this creature's class + // uses a custom smoothing formula to smooth transitions between expansions + float newBaseHealth = getBaseExpansionValueForLevel(newCreatureBaseStats->BaseHealth, mapABInfo->highestPlayerLevel); + LOG_DEBUG("module.AutoBalance_StatGeneration", "AutoBalance_AllCreatureScript::ModifyCreatureAttributes: Creature {} ({}) | newBaseHealth ({}) = getBaseExpansionValueForLevel(newCreatureBaseStats->BaseHealth, mapABInfo->highestPlayerLevel ({}))", + creature->GetName(), + creatureABInfo->selectedLevel, + newBaseHealth, + mapABInfo->highestPlayerLevel + ); + + // the health of the creature at its new level (before per-player scaling) float newHealth = newBaseHealth * creatureTemplate->ModHealth; - hpStatsRate = newHealth / originalHealth; - - healthMultiplier *= hpStatsRate; + LOG_DEBUG("module.AutoBalance_StatGeneration", "AutoBalance_AllCreatureScript::ModifyCreatureAttributes: Creature {} ({}) | newHealth ({}) = newBaseHealth ({}) * creature ModHealth ({})", + creature->GetName(), + creatureABInfo->selectedLevel, + newHealth, + newBaseHealth, + creatureTemplate->ModHealth + ); + + // the multiplier that would need to be applied to the creature's original health to get the new level's health (before per-player scaling) + float newHealthMultiplier = newHealth / origHealth; + LOG_DEBUG("module.AutoBalance_StatGeneration", "AutoBalance_AllCreatureScript::ModifyCreatureAttributes: Creature {} ({}) | newHealthMultiplier ({}) = newHealth ({}) / origHealth ({})", + creature->GetName(), + creatureABInfo->selectedLevel, + newHealthMultiplier, + newHealth, + origHealth + ); + + // the multiplier that would need to be applied to the creature's original health to get the new level's health (after per-player scaling) + scaledHealthMultiplier = healthMultiplier * newHealthMultiplier; + LOG_DEBUG("module.AutoBalance_StatGeneration", "AutoBalance_AllCreatureScript::ModifyCreatureAttributes: Creature {} ({}) | scaledHealthMultiplier ({}) = healthMultiplier ({}) * newHealthMultiplier ({})", + creature->GetName(), + creatureABInfo->selectedLevel, + scaledHealthMultiplier, + healthMultiplier, + newHealthMultiplier + ); + + // the actual health value to be applied to the level-scaled and player-scaled creature + newFinalHealth = round(origHealth * scaledHealthMultiplier); + LOG_DEBUG("module.AutoBalance_StatGeneration", "AutoBalance_AllCreatureScript::ModifyCreatureAttributes: Creature {} ({}) | newFinalHealth ({}) = origHealth ({}) * scaledHealthMultiplier ({})", + creature->GetName(), + creatureABInfo->selectedLevel, + newFinalHealth, + origHealth, + scaledHealthMultiplier + ); + } + else + { + // the non-level-scaled health multiplier is the same as the level-scaled health multiplier + scaledHealthMultiplier = healthMultiplier; + LOG_DEBUG("module.AutoBalance_StatGeneration", "AutoBalance_AllCreatureScript::ModifyCreatureAttributes: Creature {} ({}) | scaledHealthMultiplier ({}) = healthMultiplier ({})", + creature->GetName(), + creatureABInfo->selectedLevel, + scaledHealthMultiplier, + healthMultiplier + ); + + // the original health of the creature + uint32 origHealth = origCreatureBaseStats->GenerateHealth(creatureTemplate); + LOG_DEBUG("module.AutoBalance_StatGeneration", "AutoBalance_AllCreatureScript::ModifyCreatureAttributes: Creature {} ({}) | origHealth ({}) = origCreatureBaseStats->GenerateHealth(creatureTemplate)", + creature->GetName(), + creatureABInfo->selectedLevel, + origHealth + ); + + // the actual health value to be applied to the player-scaled creature + newFinalHealth = round(origHealth * creatureABInfo->HealthMultiplier); + LOG_DEBUG("module.AutoBalance_StatGeneration", "AutoBalance_AllCreatureScript::ModifyCreatureAttributes: Creature {} ({}) | newFinalHealth ({}) = origHealth ({}) * HealthMultiplier ({})", + creature->GetName(), + creatureABInfo->selectedLevel, + newFinalHealth, + origHealth, + creatureABInfo->HealthMultiplier + ); } - - creatureABInfo->HealthMultiplier = healthMultiplier; - scaledHealth = round(originalHealth * creatureABInfo->HealthMultiplier); // // Mana Scaling // - float manaStatsRate = 1.0f; - float newMana = creatureStats->GenerateMana(creatureTemplate); - manaStatsRate = newMana/float(baseMana); + LOG_DEBUG("module.AutoBalance_StatGeneration", "AutoBalance_AllCreatureScript::ModifyCreatureAttributes: Creature {} ({}) | ---------- MANA MULTIPLIER ----------", + creature->GetName(), + creatureABInfo->selectedLevel + ); + + float manaMultiplier = defaultMultiplier * statMod_global * statMod_mana; + float scaledManaMultiplier; + + LOG_DEBUG("module.AutoBalance_StatGeneration", "AutoBalance_AllCreatureScript::ModifyCreatureAttributes: Creature {} ({}) | ManaMultiplier: ({}) = defaultMultiplier ({}) * statMod_global ({}) * statMod_mana ({})", + creature->GetName(), + creatureABInfo->selectedLevel, + manaMultiplier, + defaultMultiplier, + statMod_global, + statMod_mana + ); + + // Can't be less than MinManaModifier + if (manaMultiplier <= MinManaModifier) + { + manaMultiplier = MinManaModifier; + + LOG_DEBUG("module.AutoBalance_StatGeneration", "AutoBalance_AllCreatureScript::ModifyCreatureAttributes: Creature {} ({}) | ManaMultiplier: ({}) - capped to MinManaModifier ({})", + creature->GetName(), + creatureABInfo->selectedLevel, + manaMultiplier, + MinManaModifier + ); + } - // check to be sure that manaStatsRate is not nan - if (manaStatsRate != manaStatsRate) + // if the creature doesn't have mana, set the multiplier to 0.0 + if (!origCreatureBaseStats->GenerateMana(creatureTemplate)) { + manaMultiplier = 0.0f; creatureABInfo->ManaMultiplier = 0.0f; + scaledManaMultiplier = 0.0f; + + LOG_DEBUG("module.AutoBalance_StatGeneration", "AutoBalance_AllCreatureScript::ModifyCreatureAttributes: Creature {} ({}) | Creature doesn't have mana, multiplier set to ({})", + creature->GetName(), + creatureABInfo->selectedLevel, + creatureABInfo->ManaMultiplier + ); } + // if the creature has mana, continue calculations else { - creatureABInfo->ManaMultiplier = defaultMultiplier * manaStatsRate * statMod_global * statMod_mana; - - if (creatureABInfo->ManaMultiplier <= MinManaModifier) + // set the non-level-scaled mana multiplier on the creature's AB info + creatureABInfo->ManaMultiplier = manaMultiplier; + LOG_DEBUG("module.AutoBalance_StatGeneration", "AutoBalance_AllCreatureScript::ModifyCreatureAttributes: Creature {} ({}) | ManaMultiplier: ({})", + creature->GetName(), + creatureABInfo->selectedLevel, + creatureABInfo->ManaMultiplier + ); + + // only level scale mana if level scaling is enabled and the creature level has been altered + if (LevelScaling && creatureABInfo->selectedLevel != creatureABInfo->UnmodifiedLevel) + { + // the max mana that the creature had before we did anything with it + uint32 origMana = origCreatureBaseStats->GenerateMana(creatureTemplate); + LOG_DEBUG("module.AutoBalance_StatGeneration", "AutoBalance_AllCreatureScript::ModifyCreatureAttributes: Creature {} ({}) | origMana ({}) = origCreatureBaseStats->GenerateMana(creatureTemplate)", + creature->GetName(), + creatureABInfo->selectedLevel, + origMana + ); + + // the max mana that the creature would have at its new level + // there is no per-expansion adjustment for mana + uint32 newMana = newCreatureBaseStats->GenerateMana(creatureTemplate); + LOG_DEBUG("module.AutoBalance_StatGeneration", "AutoBalance_AllCreatureScript::ModifyCreatureAttributes: Creature {} ({}) | newMana ({}) = newCreatureBaseStats->GenerateMana(creatureTemplate)", + creature->GetName(), + creatureABInfo->selectedLevel, + newMana + ); + + // the multiplier that would need to be applied to the creature's original mana to get the new level's mana (before per-player scaling) + float newManaMultiplier = (float)newMana / (float)origMana; + LOG_DEBUG("module.AutoBalance_StatGeneration", "AutoBalance_AllCreatureScript::ModifyCreatureAttributes: Creature {} ({}) | newManaMultiplier ({}) = newMana ({}) / origMana ({})", + creature->GetName(), + creatureABInfo->selectedLevel, + newManaMultiplier, + newMana, + origMana + ); + + // the multiplier that would need to be applied to the creature's original mana to get the new level's mana (after per-player scaling) + scaledManaMultiplier = manaMultiplier * newManaMultiplier; + LOG_DEBUG("module.AutoBalance_StatGeneration", "AutoBalance_AllCreatureScript::ModifyCreatureAttributes: Creature {} ({}) | scaledManaMultiplier ({}) = manaMultiplier ({}) * newManaMultiplier ({})", + creature->GetName(), + creatureABInfo->selectedLevel, + scaledManaMultiplier, + manaMultiplier, + newManaMultiplier + ); + + // the actual mana value to be applied to the level-scaled and player-scaled creature + newFinalMana = round(origMana * scaledManaMultiplier); + LOG_DEBUG("module.AutoBalance_StatGeneration", "AutoBalance_AllCreatureScript::ModifyCreatureAttributes: Creature {} ({}) | newFinalMana ({}) = origMana ({}) * scaledManaMultiplier ({})", + creature->GetName(), + creatureABInfo->selectedLevel, + newFinalMana, + origMana, + scaledManaMultiplier + ); + } + else { - creatureABInfo->ManaMultiplier = MinManaModifier; + // scaled mana multiplier is the same as the non-level-scaled mana multiplier + scaledManaMultiplier = manaMultiplier; + LOG_DEBUG("module.AutoBalance_StatGeneration", "AutoBalance_AllCreatureScript::ModifyCreatureAttributes: Creature {} ({}) | scaledManaMultiplier ({}) = manaMultiplier ({})", + creature->GetName(), + creatureABInfo->selectedLevel, + scaledManaMultiplier, + manaMultiplier + ); + + // the original mana of the creature + uint32 origMana = origCreatureBaseStats->GenerateMana(creatureTemplate); + LOG_DEBUG("module.AutoBalance_StatGeneration", "AutoBalance_AllCreatureScript::ModifyCreatureAttributes: Creature {} ({}) | origMana ({}) = origCreatureBaseStats->GenerateMana(creatureTemplate)", + creature->GetName(), + creatureABInfo->selectedLevel, + origMana + ); + + // the actual mana value to be applied to the player-scaled creature + newFinalMana = round(origMana * creatureABInfo->ManaMultiplier); + LOG_DEBUG("module.AutoBalance_StatGeneration", "AutoBalance_AllCreatureScript::ModifyCreatureAttributes: Creature {} ({}) | newFinalMana ({}) = origMana ({}) * creatureABInfo->ManaMultiplier ({})", + creature->GetName(), + creatureABInfo->selectedLevel, + newFinalMana, + origMana, + creatureABInfo->ManaMultiplier + ); } } - scaledMana = round(baseMana * creatureABInfo->ManaMultiplier); - // // Armor Scaling // - creatureABInfo->ArmorMultiplier = defaultMultiplier * statMod_global * statMod_armor; - uint32 newBaseArmor = round(creatureABInfo->ArmorMultiplier * (LevelScaling ? creatureStats->GenerateArmor(creatureTemplate) : origCreatureStats->GenerateArmor(creatureTemplate))); + LOG_DEBUG("module.AutoBalance_StatGeneration", "AutoBalance_AllCreatureScript::ModifyCreatureAttributes: Creature {} ({}) | ---------- ARMOR MULTIPLIER ----------", + creature->GetName(), + creatureABInfo->selectedLevel + ); - // - // Damage Scaling - // - float damageMul = defaultMultiplier * statMod_global * statMod_damage; + float armorMultiplier = defaultMultiplier * statMod_global * statMod_armor; + float scaledArmorMultiplier; + + LOG_DEBUG("module.AutoBalance_StatGeneration", "AutoBalance_AllCreatureScript::ModifyCreatureAttributes: Creature {} ({}) | armorMultiplier: ({}) = defaultMultiplier ({}) * statMod_global ({}) * statMod_armor ({})", + creature->GetName(), + creatureABInfo->selectedLevel, + armorMultiplier, + defaultMultiplier, + statMod_global, + statMod_armor + ); - // Can not be less than MinDamageModifier - if (damageMul <= MinDamageModifier) + // set the non-level-scaled armor multiplier on the creature's AB info + creatureABInfo->ArmorMultiplier = armorMultiplier; + + // only level scale armor if level scaling is enabled and the creature level has been altered + if (LevelScaling && creatureABInfo->selectedLevel != creatureABInfo->UnmodifiedLevel) + { + // the armor that the creature had before we did anything with it + uint32 origArmor = origCreatureBaseStats->GenerateArmor(creatureTemplate); + LOG_DEBUG("module.AutoBalance_StatGeneration", "AutoBalance_AllCreatureScript::ModifyCreatureAttributes: Creature {} ({}) | origArmor ({}) = origCreatureBaseStats->GenerateArmor(creatureTemplate)", + creature->GetName(), + creatureABInfo->selectedLevel, + origArmor + ); + + // the armor that the creature would have at its new level + // there is no per-expansion adjustment for armor + uint32 newArmor = newCreatureBaseStats->GenerateArmor(creatureTemplate); + LOG_DEBUG("module.AutoBalance_StatGeneration", "AutoBalance_AllCreatureScript::ModifyCreatureAttributes: Creature {} ({}) | newArmor ({}) = newCreatureBaseStats->GenerateArmor(creatureTemplate)", + creature->GetName(), + creatureABInfo->selectedLevel, + newArmor + ); + + // the multiplier that would need to be applied to the creature's original armor to get the new level's armor (before per-player scaling) + float newArmorMultiplier = (float)newArmor / (float)origArmor; + LOG_DEBUG("module.AutoBalance_StatGeneration", "AutoBalance_AllCreatureScript::ModifyCreatureAttributes: Creature {} ({}) | newArmorMultiplier ({}) = newArmor ({}) / origArmor ({})", + creature->GetName(), + creatureABInfo->selectedLevel, + newArmorMultiplier, + newArmor, + origArmor + ); + + // the multiplier that would need to be applied to the creature's original armor to get the new level's armor (after per-player scaling) + scaledArmorMultiplier = armorMultiplier * newArmorMultiplier; + LOG_DEBUG("module.AutoBalance_StatGeneration", "AutoBalance_AllCreatureScript::ModifyCreatureAttributes: Creature {} ({}) | scaledArmorMultiplier ({}) = armorMultiplier ({}) * newArmorMultiplier ({})", + creature->GetName(), + creatureABInfo->selectedLevel, + scaledArmorMultiplier, + armorMultiplier, + newArmorMultiplier + ); + + // the actual armor value to be applied to the level-scaled and player-scaled creature + newFinalArmor = round(origArmor * scaledArmorMultiplier); + LOG_DEBUG("module.AutoBalance_StatGeneration", "AutoBalance_AllCreatureScript::ModifyCreatureAttributes: Creature {} ({}) | newFinalArmor ({}) = origArmor ({}) * scaledArmorMultiplier ({})", + creature->GetName(), + creatureABInfo->selectedLevel, + newFinalArmor, + origArmor, + scaledArmorMultiplier + ); + } + else { - damageMul = MinDamageModifier; + // Scaled armor multiplier is the same as the non-level-scaled armor multiplier + scaledArmorMultiplier = armorMultiplier; + LOG_DEBUG("module.AutoBalance_StatGeneration", "AutoBalance_AllCreatureScript::ModifyCreatureAttributes: Creature {} ({}) | scaledArmorMultiplier ({}) = armorMultiplier ({})", + creature->GetName(), + creatureABInfo->selectedLevel, + scaledArmorMultiplier, + armorMultiplier + ); + + // the original armor of the creature + uint32 origArmor = origCreatureBaseStats->GenerateArmor(creatureTemplate); + LOG_DEBUG("module.AutoBalance_StatGeneration", "AutoBalance_AllCreatureScript::ModifyCreatureAttributes: Creature {} ({}) | origArmor ({}) = origCreatureBaseStats->GenerateArmor(creatureTemplate)", + creature->GetName(), + creatureABInfo->selectedLevel, + origArmor + ); + + // the actual armor value to be applied to the player-scaled creature + newFinalArmor = round(origArmor * creatureABInfo->ArmorMultiplier); + LOG_DEBUG("module.AutoBalance_StatGeneration", "AutoBalance_AllCreatureScript::ModifyCreatureAttributes: Creature {} ({}) | newFinalArmor ({}) = origArmor ({}) * creatureABInfo->ArmorMultiplier ({})", + creature->GetName(), + creatureABInfo->selectedLevel, + newFinalArmor, + origArmor, + creatureABInfo->ArmorMultiplier + ); } - // Calculate the new base damage - float origDmgBase = origCreatureStats->GenerateBaseDamage(creatureTemplate); - float newDmgBase = 0; + // + // Damage Scaling + // + LOG_DEBUG("module.AutoBalance_StatGeneration", "AutoBalance_AllCreatureScript::ModifyCreatureAttributes: Creature {} ({}) | ---------- DAMAGE MULTIPLIER ----------", + creature->GetName(), + creatureABInfo->selectedLevel + ); - float vanillaDamage = creatureStats->BaseDamage[0]; - float bcDamage = creatureStats->BaseDamage[1]; - float wotlkDamage = creatureStats->BaseDamage[2]; + float damageMultiplier = defaultMultiplier * statMod_global * statMod_damage; + float scaledDamageMultiplier; - // The database holds multiple values for base damage, one for each expansion - // This code will smooth transition between the different expansions based on the highest player level in the instance - // Only do this if level scaling is enabled + LOG_DEBUG("module.AutoBalance_StatGeneration", "AutoBalance_AllCreatureScript::ModifyCreatureAttributes: Creature {} ({}) | DamageMultiplier: ({}) = defaultMultiplier ({}) * statMod_global ({}) * statMod_damage ({})", + creature->GetName(), + creatureABInfo->selectedLevel, + damageMultiplier, + defaultMultiplier, + statMod_global, + statMod_damage + ); - if (LevelScaling) - { - // vanilla damage - if (mapABInfo->highestPlayerLevel <= 60) - { - newDmgBase=vanillaDamage; - } - // transition from vanilla to BC damage - else if (mapABInfo->highestPlayerLevel < 63) - { - float vanillaMultiplier = (63 - mapABInfo->highestPlayerLevel) / 3.0; - float bcMultiplier = 1.0f - vanillaMultiplier; + // Can't be less than MinDamageModifier + if (damageMultiplier <= MinDamageModifier) + { + damageMultiplier = MinDamageModifier; + + LOG_DEBUG("module.AutoBalance_StatGeneration", "AutoBalance_AllCreatureScript::ModifyCreatureAttributes: Creature {} ({}) | DamageMultiplier: ({}) - capped to MinDamageModifier ({})", + creature->GetName(), + creatureABInfo->selectedLevel, + damageMultiplier, + MinDamageModifier + ); + } - newDmgBase=(vanillaDamage * vanillaMultiplier) + (bcDamage * bcMultiplier); - } - // BC damage - else if (mapABInfo->highestPlayerLevel <= 70) - { - newDmgBase=bcDamage; - } - // transition from BC to WotLK damage - else if (mapABInfo->highestPlayerLevel < 73) - { - float bcMultiplier = (73 - mapABInfo->highestPlayerLevel) / 3.0; - float wotlkMultiplier = 1.0f - bcMultiplier; + // set the non-level-scaled damage multiplier on the creature's AB info + creatureABInfo->DamageMultiplier = damageMultiplier; + LOG_DEBUG("module.AutoBalance_StatGeneration", "AutoBalance_AllCreatureScript::ModifyCreatureAttributes: Creature {} ({}) | DamageMultiplier: ({})", + creature->GetName(), + creatureABInfo->selectedLevel, + creatureABInfo->DamageMultiplier + ); - newDmgBase=(bcDamage * bcMultiplier) + (wotlkDamage * wotlkMultiplier); - } - // WotLK damage - else - { - newDmgBase=wotlkDamage; + // only level scale damage if level scaling is enabled and the creature level has been altered + if (LevelScaling && creatureABInfo->selectedLevel != creatureABInfo->UnmodifiedLevel) + { - // special increase for end-game content - if (LevelScalingEndGameBoost && maxNumberOfPlayers <= 5) { - if (mapABInfo->highestPlayerLevel >= 75 && creatureABInfo->UnmodifiedLevel < 75) - newDmgBase *= float(mapABInfo->highestPlayerLevel-70) * 0.3f; - } - } + // the original base damage of the creature + // note that we don't mess with the damage modifier here since it applied equally to the original and new levels + float origBaseDamage = origCreatureBaseStats->GenerateBaseDamage(creatureTemplate); + LOG_DEBUG("module.AutoBalance_StatGeneration", "AutoBalance_AllCreatureScript::ModifyCreatureAttributes: Creature {} ({}) | origBaseDamage ({}) = origCreatureBaseStats->GenerateBaseDamage(creatureTemplate)", + creature->GetName(), + creatureABInfo->selectedLevel, + origBaseDamage + ); + + // the base damage of the new creature level for this creature's class + // uses a custom smoothing formula to smooth transitions between expansions + float newBaseDamage = getBaseExpansionValueForLevel(newCreatureBaseStats->BaseDamage, mapABInfo->highestPlayerLevel); + LOG_DEBUG("module.AutoBalance_StatGeneration", "AutoBalance_AllCreatureScript::ModifyCreatureAttributes: Creature {} ({}) | newBaseDamage ({}) = getBaseExpansionValueForLevel(newCreatureBaseStats->BaseDamage, mapABInfo->highestPlayerLevel ({}))", + creature->GetName(), + creatureABInfo->selectedLevel, + newBaseDamage, + mapABInfo->highestPlayerLevel + ); + + // the multiplier that would need to be applied to the creature's original damage to get the new level's damage (before per-player scaling) + float newDamageMultiplier = newBaseDamage / origBaseDamage; + LOG_DEBUG("module.AutoBalance_StatGeneration", "AutoBalance_AllCreatureScript::ModifyCreatureAttributes: Creature {} ({}) | newDamageMultiplier ({}) = newBaseDamage ({}) / origBaseDamage ({})", + creature->GetName(), + creatureABInfo->selectedLevel, + newDamageMultiplier, + newBaseDamage, + origBaseDamage + ); + + // the actual multiplier that will be used to scale the creature's damage (after per-player scaling) + scaledDamageMultiplier = damageMultiplier * newDamageMultiplier; + LOG_DEBUG("module.AutoBalance_StatGeneration", "AutoBalance_AllCreatureScript::ModifyCreatureAttributes: Creature {} ({}) | scaledDamageMultiplier ({}) = damageMultiplier ({}) * newDamageMultiplier ({})", + creature->GetName(), + creatureABInfo->selectedLevel, + scaledDamageMultiplier, + damageMultiplier, + newDamageMultiplier + ); + } + else + { + // the scaled damage multiplier is the same as the non-level-scaled damage multiplier + scaledDamageMultiplier = damageMultiplier; + LOG_DEBUG("module.AutoBalance_StatGeneration", "AutoBalance_AllCreatureScript::ModifyCreatureAttributes: Creature {} ({}) | scaledDamageMultiplier ({}) = damageMultiplier ({})", + creature->GetName(), + creatureABInfo->selectedLevel, + scaledDamageMultiplier, + origCreatureBaseStats->GenerateBaseDamage(creatureTemplate), + damageMultiplier + ); - damageMul *= newDmgBase/origDmgBase; } // // Crowd Control Debuff Duration Scaling // - float ccDurationMul; + + LOG_DEBUG("module.AutoBalance_StatGeneration", "AutoBalance_AllCreatureScript::ModifyCreatureAttributes: Creature {} ({}) | ---------- CC DURATION MULTIPLIER ----------", + creature->GetName(), + creatureABInfo->selectedLevel + ); + + float ccDurationMultiplier; + if (statMod_ccDuration != -1.0f) { - ccDurationMul = defaultMultiplier * statMod_ccDuration; + // calculate CC Duration from the default multiplier and the config settings + ccDurationMultiplier = defaultMultiplier * statMod_ccDuration; // Min/Max checking - if (ccDurationMul < MinCCDurationModifier) + if (ccDurationMultiplier < MinCCDurationModifier) { - ccDurationMul = MinCCDurationModifier; + ccDurationMultiplier = MinCCDurationModifier; } - else if (ccDurationMul > MaxCCDurationModifier) + else if (ccDurationMultiplier > MaxCCDurationModifier) { - ccDurationMul = MaxCCDurationModifier; + ccDurationMultiplier = MaxCCDurationModifier; } + + LOG_DEBUG("module.AutoBalance_StatGeneration", "AutoBalance_AllCreatureScript::ModifyCreatureAttributes: Creature {} ({}) | ccDurationMultiplier: ({})", + creature->GetName(), + creatureABInfo->selectedLevel, + ccDurationMultiplier + ); } else { - ccDurationMul = 1.0f; + // the CC Duration will not be changed + ccDurationMultiplier = 1.0f; + LOG_DEBUG("module.AutoBalance_StatGeneration", "AutoBalance_AllCreatureScript::ModifyCreatureAttributes: Creature {} ({}) | Crowd Control Duration will not be changed.", + creature->GetName(), + creatureABInfo->selectedLevel + ); } + LOG_DEBUG("module.AutoBalance_StatGeneration", "AutoBalance_AllCreatureScript::ModifyCreatureAttributes: Creature {} ({}) | ccDurationMultiplier: ({}) = defaultMultiplier ({}) * statMod_ccDuration ({})", + creature->GetName(), + creatureABInfo->selectedLevel, + ccDurationMultiplier, + defaultMultiplier, + statMod_ccDuration + ); + // // Apply New Values // - if (!sABScriptMgr->OnBeforeUpdateStats(creature, scaledHealth, scaledMana, damageMul, newBaseArmor)) + if (!sABScriptMgr->OnBeforeUpdateStats(creature, newFinalHealth, newFinalMana, damageMultiplier, newFinalArmor)) return; uint32 prevMaxHealth = creature->GetMaxHealth(); - uint32 prevMaxPower = creature->GetMaxPower(POWER_MANA); + uint32 prevMaxPower = creature->GetMaxPower(Powers::POWER_MANA); uint32 prevHealth = creature->GetHealth(); - uint32 prevPower = creature->GetPower(POWER_MANA); + uint32 prevPower = creature->GetPower(Powers::POWER_MANA); uint32 prevPlayerDamageRequired = creature->GetPlayerDamageReq(); uint32 prevCreateHealth = creature->GetCreateHealth(); - Powers pType= creature->getPowerType(); + Powers pType = creature->getPowerType(); - creature->SetArmor(newBaseArmor); - creature->SetModifierValue(UNIT_MOD_ARMOR, BASE_VALUE, (float)newBaseArmor); - creature->SetCreateHealth(scaledHealth); - creature->SetMaxHealth(scaledHealth); + creature->SetArmor(newFinalArmor); + creature->SetModifierValue(UNIT_MOD_ARMOR, BASE_VALUE, (float)newFinalArmor); + creature->SetCreateHealth(newFinalHealth); + creature->SetMaxHealth(newFinalHealth); creature->ResetPlayerDamageReq(); - creature->SetCreateMana(scaledMana); - creature->SetMaxPower(POWER_MANA, scaledMana); + creature->SetCreateMana(newFinalMana); + creature->SetMaxPower(Powers::POWER_MANA, newFinalMana); creature->SetModifierValue(UNIT_MOD_ENERGY, BASE_VALUE, (float)100.0f); creature->SetModifierValue(UNIT_MOD_RAGE, BASE_VALUE, (float)100.0f); - creature->SetModifierValue(UNIT_MOD_HEALTH, BASE_VALUE, (float)scaledHealth); - creature->SetModifierValue(UNIT_MOD_MANA, BASE_VALUE, (float)scaledMana); - creatureABInfo->DamageMultiplier = damageMul; - creatureABInfo->CCDurationMultiplier = ccDurationMul; + creature->SetModifierValue(UNIT_MOD_HEALTH, BASE_VALUE, (float)newFinalHealth); + creature->SetModifierValue(UNIT_MOD_MANA, BASE_VALUE, (float)newFinalMana); + creatureABInfo->ScaledHealthMultiplier = scaledHealthMultiplier; + creatureABInfo->ScaledManaMultiplier = scaledManaMultiplier; + creatureABInfo->ScaledArmorMultiplier = scaledArmorMultiplier; + creatureABInfo->ScaledDamageMultiplier = scaledDamageMultiplier; + creatureABInfo->CCDurationMultiplier = ccDurationMultiplier; + + // adjust the current health as appropriate + uint32 scaledCurHealth = 0; + uint32 scaledCurPower = 0; + + // if this is a summon and it's a clone of its summoner, keep the health and mana values of the summon + // only do this once, when `_isSummonCloneOfSummoner(creature)` returns true but !creatureABInfo->isCloneOfSummoner is false + if + ( + creature->IsSummon() && + _isSummonCloneOfSummoner(creature) && + !creatureABInfo->isCloneOfSummoner + ) + { + creatureABInfo->isCloneOfSummoner = true; + LOG_DEBUG("module.AutoBalance_StatGeneration", "AutoBalance_AllCreatureScript::ModifyCreatureAttributes: Creature {} ({}) | Summon is a clone of its summoner, keeping health and mana values.", + creature->GetName(), + creatureABInfo->selectedLevel + ); + + if (prevHealth && prevMaxHealth) + { + scaledCurHealth = prevHealth; + LOG_DEBUG("module.AutoBalance_StatGeneration", "AutoBalance_AllCreatureScript::ModifyCreatureAttributes: Creature {} ({}) | scaledCurHealth ({}) = prevHealth ({})", + creature->GetName(), + creatureABInfo->selectedLevel, + scaledCurHealth, + prevHealth + ); + } + + if (prevPower && prevMaxPower) + { + scaledCurPower = prevPower; + LOG_DEBUG("module.AutoBalance_StatGeneration", "AutoBalance_AllCreatureScript::ModifyCreatureAttributes: Creature {} ({}) | scaledCurPower ({}) = prevPower ({})", + creature->GetName(), + creatureABInfo->selectedLevel, + scaledCurPower, + prevPower + ); + } + } + else + { + if (prevHealth && prevMaxHealth) + { + scaledCurHealth = float(newFinalHealth) / float(prevMaxHealth) * float(prevHealth); + LOG_DEBUG("module.AutoBalance_StatGeneration", "AutoBalance_AllCreatureScript::ModifyCreatureAttributes: Creature {} ({}) | scaledCurHealth ({}) = float(newFinalHealth) ({}) / float(prevMaxHealth) ({}) * float(prevHealth) ({})", + creature->GetName(), + creatureABInfo->selectedLevel, + scaledCurHealth, + newFinalHealth, + prevMaxHealth, + prevHealth + ); + } + else + { + scaledCurHealth = 0; + LOG_DEBUG("module.AutoBalance_StatGeneration", "AutoBalance_AllCreatureScript::ModifyCreatureAttributes: Creature {} ({}) | scaledCurHealth ({}) = 0", + creature->GetName(), + creatureABInfo->selectedLevel, + scaledCurHealth + ); + } - uint32 scaledCurHealth=prevHealth && prevMaxHealth ? float(scaledHealth)/float(prevMaxHealth)*float(prevHealth) : 0; - uint32 scaledCurPower=prevPower && prevMaxPower ? float(scaledMana)/float(prevMaxPower)*float(prevPower) : 0; + if (prevPower && prevMaxPower) + { + scaledCurPower = float(newFinalMana) / float(prevMaxPower) * float(prevPower); + LOG_DEBUG("module.AutoBalance_StatGeneration", "AutoBalance_AllCreatureScript::ModifyCreatureAttributes: Creature {} ({}) | scaledCurPower ({}) = float(newFinalMana) ({}) / float(prevMaxPower) ({}) * float(prevPower) ({})", + creature->GetName(), + creatureABInfo->selectedLevel, + scaledCurPower, + newFinalMana, + prevMaxPower, + prevPower + ); + } + else + { + scaledCurPower = 0; + LOG_DEBUG("module.AutoBalance_StatGeneration", "AutoBalance_AllCreatureScript::ModifyCreatureAttributes: Creature {} ({}) | scaledCurPower ({}) = 0", + creature->GetName(), + creatureABInfo->selectedLevel, + scaledCurPower + ); + } + } creature->SetHealth(scaledCurHealth); - if (pType == POWER_MANA) - creature->SetPower(POWER_MANA, scaledCurPower); + if (pType == Powers::POWER_MANA) + creature->SetPower(Powers::POWER_MANA, scaledCurPower); else creature->setPowerType(pType); // fix creatures with different power types @@ -3020,7 +6019,7 @@ class AutoBalance_AllCreatureScript : public AllCreatureScript else { // Scale the damage requirements similar to creature HP scaling - uint32 scaledPlayerDmgReq = float(prevPlayerDamageRequired) * float(scaledHealth) / float(prevCreateHealth); + uint32 scaledPlayerDmgReq = float(prevPlayerDamageRequired) * float(newFinalHealth) / float(prevCreateHealth); // Do some math creature->LowerPlayerDamageReq(playerDamageRequired - scaledPlayerDmgReq, true); } @@ -3029,10 +6028,35 @@ class AutoBalance_AllCreatureScript : public AllCreatureScript // Reward Scaling // + LOG_DEBUG("module.AutoBalance_StatGeneration", "AutoBalance_AllCreatureScript::ModifyCreatureAttributes: Creature {} ({}) | ---------- REWARD SCALING ----------", + creature->GetName(), + creatureABInfo->selectedLevel + ); + // calculate the average multiplier after level scaling is applied - float averageMultiplierAfterLevelScaling; - // use health and damage to calculate the average multiplier - averageMultiplierAfterLevelScaling = (creatureABInfo->HealthMultiplier + creatureABInfo->DamageMultiplier) / 2.0f; + float avgHealthDamageMultipliers; + + // only if one of the scaling options is enabled + if (RewardScalingXP || RewardScalingMoney) + { + // use health and damage to calculate the average multiplier + avgHealthDamageMultipliers = (scaledHealthMultiplier + scaledDamageMultiplier) / 2.0f; + LOG_DEBUG("module.AutoBalance_StatGeneration", "AutoBalance_AllCreatureScript::ModifyCreatureAttributes: Creature {} ({}) | avgHealthDamageMultipliers ({}) = (scaledHealthMultiplier ({}) + scaledDamageMultiplier ({})) / 2.0f", + creature->GetName(), + creatureABInfo->selectedLevel, + avgHealthDamageMultipliers, + scaledHealthMultiplier, + scaledDamageMultiplier + ); + } + else + { + // Reward scaling is disabled + LOG_DEBUG("module.AutoBalance_StatGeneration", "AutoBalance_AllCreatureScript::ModifyCreatureAttributes: Creature {} ({}) | Reward scaling is disabled.", + creature->GetName(), + creatureABInfo->selectedLevel + ); + } // XP Scaling if (RewardScalingXP) @@ -3040,29 +6064,252 @@ class AutoBalance_AllCreatureScript : public AllCreatureScript if (RewardScalingMethod == AUTOBALANCE_SCALING_FIXED) { creatureABInfo->XPModifier = RewardScalingXPModifier; + LOG_DEBUG("module.AutoBalance_StatGeneration", "AutoBalance_AllCreatureScript::ModifyCreatureAttributes: Creature {} ({}) | Fixed Mode: XPModifier ({}) = RewardScalingXPModifier ({})", + creature->GetName(), + creatureABInfo->selectedLevel, + creatureABInfo->XPModifier, + RewardScalingXPModifier + ); } else if (RewardScalingMethod == AUTOBALANCE_SCALING_DYNAMIC) { - creatureABInfo->XPModifier = averageMultiplierAfterLevelScaling * RewardScalingXPModifier; + creatureABInfo->XPModifier = avgHealthDamageMultipliers * RewardScalingXPModifier; + LOG_DEBUG("module.AutoBalance_StatGeneration", "AutoBalance_AllCreatureScript::ModifyCreatureAttributes: Creature {} ({}) | Dynamic Mode: XPModifier ({}) = avgHealthDamageMultipliers ({}) * RewardScalingXPModifier ({})", + creature->GetName(), + creatureABInfo->selectedLevel, + creatureABInfo->XPModifier, + avgHealthDamageMultipliers, + RewardScalingXPModifier + ); } } // Money Scaling if (RewardScalingMoney) { - //LOG_DEBUG("module.AutoBalance", "AutoBalance_AllCreatureScript::ModifyCreatureAttributes: Creature {} ({}) has an average post-level-scaling modifier of {}.", creature->GetName(), creature->GetLevel(), averageMultiplierAfterLevelScaling); if (RewardScalingMethod == AUTOBALANCE_SCALING_FIXED) { creatureABInfo->MoneyModifier = RewardScalingMoneyModifier; + LOG_DEBUG("module.AutoBalance_StatGeneration", "AutoBalance_AllCreatureScript::ModifyCreatureAttributes: Creature {} ({}) | Fixed Mode: MoneyModifier ({}) = RewardScalingMoneyModifier ({})", + creature->GetName(), + creatureABInfo->selectedLevel, + creatureABInfo->MoneyModifier, + RewardScalingMoneyModifier + ); } else if (RewardScalingMethod == AUTOBALANCE_SCALING_DYNAMIC) { - creatureABInfo->MoneyModifier = averageMultiplierAfterLevelScaling * RewardScalingMoneyModifier; + creatureABInfo->MoneyModifier = avgHealthDamageMultipliers * RewardScalingMoneyModifier; + LOG_DEBUG("module.AutoBalance_StatGeneration", "AutoBalance_AllCreatureScript::ModifyCreatureAttributes: Creature {} ({}) | Dynamic Mode: MoneyModifier ({}) = avgHealthDamageMultipliers ({}) * RewardScalingMoneyModifier ({})", + creature->GetName(), + creatureABInfo->selectedLevel, + creatureABInfo->MoneyModifier, + avgHealthDamageMultipliers, + RewardScalingMoneyModifier + ); } } + // update all stats creature->UpdateAllStats(); + + LOG_DEBUG("module.AutoBalance_StatGeneration", "AutoBalance_AllCreatureScript::ModifyCreatureAttributes: Creature {} ({}) | ---------- FINAL STATS ----------", + creature->GetName(), + creatureABInfo->selectedLevel + ); + + LOG_DEBUG("module.AutoBalance_StatGeneration", "AutoBalance_AllCreatureScript::ModifyCreatureAttributes: Creature {} ({}) | Health ({}/{} {:.1f}%) -> ({}/{} {:.1f}%)", + creature->GetName(), + creatureABInfo->selectedLevel, + prevHealth, + prevMaxHealth, + prevMaxHealth ? float(prevHealth) / float(prevMaxHealth) * 100.0f : 0.0f, + creature->GetHealth(), + creature->GetMaxHealth(), + creature->GetMaxHealth() ? float(creature->GetHealth()) / float(creature->GetMaxHealth()) * 100.0f : 0.0f + ); + + if (prevPower && prevMaxPower && pType == Powers::POWER_MANA) + { + LOG_DEBUG("module.AutoBalance_StatGeneration", "AutoBalance_AllCreatureScript::ModifyCreatureAttributes: Creature {} ({}) | Mana ({}/{} {:.1f}%) -> ({}/{} {:.1f}%)", + creature->GetName(), + creatureABInfo->selectedLevel, + prevPower, + prevMaxPower, + prevMaxPower ? float(prevPower) / float(prevMaxPower) * 100.0f : 0.0f, + creature->GetPower(Powers::POWER_MANA), + creature->GetMaxPower(Powers::POWER_MANA), + creature->GetMaxPower(Powers::POWER_MANA) ? float(creature->GetPower(Powers::POWER_MANA)) / float(creature->GetMaxPower(Powers::POWER_MANA)) * 100.0f : 0.0f + ); + } + + // debug log the new stat multipliers stored in CreatureABInfo in a compact, single-line format + if (creatureABInfo->UnmodifiedLevel != creatureABInfo->selectedLevel) + { + LOG_DEBUG("module.AutoBalance", "AutoBalance_AllCreatureScript::ModifyCreatureAttributes: Creature {} ({}->{}) | Multipliers: H:{:.3f}->{:.3f} M:{:.3f}->{:.3f} A:{:.3f}->{:.3f} D:{:.3f}->{:.3f} CC:{:.3f} XP:{:.3f} $:{:.3f}", + creature->GetName(), + creatureABInfo->UnmodifiedLevel, + creatureABInfo->selectedLevel, + creatureABInfo->HealthMultiplier, + creatureABInfo->ScaledHealthMultiplier, + creatureABInfo->ManaMultiplier, + creatureABInfo->ScaledManaMultiplier, + creatureABInfo->ArmorMultiplier, + creatureABInfo->ScaledArmorMultiplier, + creatureABInfo->DamageMultiplier, + creatureABInfo->ScaledDamageMultiplier, + creatureABInfo->CCDurationMultiplier, + creatureABInfo->XPModifier, + creatureABInfo->MoneyModifier + ); + } + else + { + LOG_DEBUG("module.AutoBalance", "AutoBalance_AllCreatureScript::ModifyCreatureAttributes: Creature {} ({}) | Multipliers: H:{:.3f} M:{:.3f} A:{:.3f} D:{:.3f} CC:{:.3f} XP:{:.3f} $:{:.3f}", + creature->GetName(), + creatureABInfo->UnmodifiedLevel, + creatureABInfo->HealthMultiplier, + creatureABInfo->ManaMultiplier, + creatureABInfo->ArmorMultiplier, + creatureABInfo->DamageMultiplier, + creatureABInfo->CCDurationMultiplier, + creatureABInfo->XPModifier, + creatureABInfo->MoneyModifier + ); + } + + + } + +private: + bool _isSummonCloneOfSummoner(Creature* summon) + { + // if the summon doesn't exist or isn't a summon + if (!summon || !summon->IsSummon()) + { + return false; + } + + // get the summon's info + AutoBalanceCreatureInfo* summonABInfo = summon->CustomData.GetDefault("AutoBalanceCreatureInfo"); + + // get the saved summoner + Creature* summoner = summonABInfo->summoner; + + // if the summoner doesn't exist + if (!summoner) + { + return false; + } + + // create a running score for this check + int8 score = 0; + + LOG_DEBUG("module.AutoBalance", "AutoBalance_AllCreatureScript::_isSummonCloneOfSummoner: Creature {} ({}) | Is this a clone of it's summoner {} ({})?", + summon->GetName(), + summonABInfo->selectedLevel, + summoner->GetName(), + summoner->GetLevel() + ); + + + // if the entry ID is the same, +2 + if (summon->GetEntry() == summoner->GetEntry()) + { + score += 2; + LOG_DEBUG("module.AutoBalance", "AutoBalance_AllCreatureScript::_isSummonCloneOfSummoner: Creature {} ({}) | Entry: ({}) == ({}) | score: +2 = ({})", + summon->GetName(), + summonABInfo->selectedLevel, + summon->GetEntry(), + summoner->GetEntry(), + score + ); + } + + // if the max health is the same, +3 + if (summon->GetMaxHealth() == summoner->GetMaxHealth()) + { + score += 3; + LOG_DEBUG("module.AutoBalance", "AutoBalance_AllCreatureScript::_isSummonCloneOfSummoner: Creature {} ({}) | MaxHealth: ({}) == ({}) | score: +3 = ({})", + summon->GetName(), + summonABInfo->selectedLevel, + summon->GetMaxHealth(), + summoner->GetMaxHealth(), + score + ); + } + + // if the type (humanoid, dragonkin, etc) is the same, +1 + if (summon->GetCreatureType() == summoner->GetCreatureType()) + { + score += 1; + LOG_DEBUG("module.AutoBalance", "AutoBalance_AllCreatureScript::_isSummonCloneOfSummoner: Creature {} ({}) | CreatureType: ({}) == ({}) | score: +1 = ({})", + summon->GetName(), + summonABInfo->selectedLevel, + summon->GetCreatureType(), + summoner->GetCreatureType(), + score + ); + } + + // if the name is the same, +2 + if (summon->GetName() == summoner->GetName()) + { + score += 2; + LOG_DEBUG("module.AutoBalance", "AutoBalance_AllCreatureScript::_isSummonCloneOfSummoner: Creature {} ({}) | Name: ({}) == ({}) | score: +2 = ({})", + summon->GetName(), + summonABInfo->selectedLevel, + summon->GetName(), + summoner->GetName(), + score + ); + } + // if the summoner's name is a part of the summon's name, +1 + else if (summon->GetName().find(summoner->GetName()) != std::string::npos) + { + score += 1; + LOG_DEBUG("module.AutoBalance", "AutoBalance_AllCreatureScript::_isSummonCloneOfSummoner: Creature {} ({}) | Name: ({}) contains ({}) | score: +1 = ({})", + summon->GetName(), + summonABInfo->selectedLevel, + summon->GetName(), + summoner->GetName(), + score + ); + } + + // if the display ID is the same, +1 + if (summon->GetDisplayId() == summoner->GetDisplayId()) + { + score += 1; + LOG_DEBUG("module.AutoBalance", "AutoBalance_AllCreatureScript::_isSummonCloneOfSummoner: Creature {} ({}) | DisplayId: ({}) == ({}) | score: +1 = ({})", + summon->GetName(), + summonABInfo->selectedLevel, + summon->GetDisplayId(), + summoner->GetDisplayId(), + score + ); + } + + // if the score is at least 5, consider this a clone + if (score >= 5) + { + LOG_DEBUG("module.AutoBalance", "AutoBalance_AllCreatureScript::_isSummonCloneOfSummoner: Creature {} ({}) | score ({}) >= 5 | true", + summon->GetName(), + summonABInfo->selectedLevel, + score + ); + return true; + } + else + { + LOG_DEBUG("module.AutoBalance", "AutoBalance_AllCreatureScript::_isSummonCloneOfSummoner: Creature {} ({}) | score ({}) < 5 | false", + summon->GetName(), + summonABInfo->selectedLevel, + score + ); + return false; + } } }; class AutoBalance_CommandScript : public CommandScript @@ -3104,7 +6351,7 @@ class AutoBalance_CommandScript : public CommandScript offseti = (uint32)atoi(offset); handler->PSendSysMessage("Changing Player Difficulty Offset to %i.", offseti); PlayerCountDifficultyOffset = offseti; - lastConfigTime = std::chrono::duration_cast(std::chrono::system_clock::now().time_since_epoch()).count(); + globalConfigTime = GetCurrentConfigTime(); return true; } else @@ -3120,26 +6367,38 @@ class AutoBalance_CommandScript : public CommandScript static bool HandleABMapStatsCommand(ChatHandler* handler, const char* /*args*/) { - Player *player; - player = handler->getSelectedPlayer() ? handler->getSelectedPlayer() : handler->GetPlayer(); + Player *player = handler->GetPlayer(); AutoBalanceMapInfo *mapABInfo=player->GetMap()->CustomData.GetDefault("AutoBalanceMapInfo"); - if (player->GetMap()->IsDungeon() || player->GetMap()->IsBattleground()) + if (player->GetMap()->IsDungeon()) { handler->PSendSysMessage("---"); - handler->PSendSysMessage("Map: ID %u | %s (%u-player %s)", - player->GetMapId(), + // Map basics + handler->PSendSysMessage("%s (%u-player %s) | ID %u-%u%s", player->GetMap()->GetMapName(), player->GetMap()->ToInstanceMap()->GetMaxPlayers(), player->GetMap()->ToInstanceMap()->IsHeroic() ? "Heroic" : "Normal", + player->GetMapId(), + player->GetInstanceId(), mapABInfo->enabled ? "" : " | AutoBalance DISABLED"); + + // if the map isn't enabled, don't display anything else + // if (!mapABInfo->enabled) { return true; } + + // Player stats handler->PSendSysMessage("Players on map: %u (Lvl %u - %u)", mapABInfo->playerCount, mapABInfo->lowestPlayerLevel, mapABInfo->highestPlayerLevel ); - if (mapABInfo->playerCount < mapABInfo->minPlayers && !PlayerCountDifficultyOffset) + + // Adjusted player count (multiple scenarios) + if (mapABInfo->combatLockTripped) + { + handler->PSendSysMessage("Adjusted Player Count: %u (Combat Locked)", mapABInfo->adjustedPlayerCount); + } + else if (mapABInfo->playerCount < mapABInfo->minPlayers && !PlayerCountDifficultyOffset) { handler->PSendSysMessage("Adjusted Player Count: %u (Map Minimum)", mapABInfo->adjustedPlayerCount); } @@ -3155,11 +6414,35 @@ class AutoBalance_CommandScript : public CommandScript { handler->PSendSysMessage("Adjusted Player Count: %u", mapABInfo->adjustedPlayerCount); } + + // LFG levels handler->PSendSysMessage("LFG Range: Lvl %u - %u (Target: Lvl %u)", mapABInfo->lfgMinLevel, mapABInfo->lfgMaxLevel, mapABInfo->lfgTargetLevel); + + // Calculated map level (creature average) handler->PSendSysMessage("Map Level: %u%s", (uint8)(mapABInfo->avgCreatureLevel+0.5f), - mapABInfo->isLevelScalingEnabled ? std::string("->") + std::to_string(mapABInfo->highestPlayerLevel) + std::string(" (Level Scaling Enabled)") : std::string(" (Level Scaling Disabled)") + mapABInfo->isLevelScalingEnabled && mapABInfo->enabled ? "->" + std::to_string(mapABInfo->highestPlayerLevel) + " (Level Scaling Enabled)" : " (Level Scaling Disabled)" ); + + // World Health Multiplier + handler->PSendSysMessage("World health multiplier: %.3f", mapABInfo->worldHealthMultiplier); + + // World Damage and Healing Multiplier + if (mapABInfo->worldDamageHealingMultiplier != mapABInfo->scaledWorldDamageHealingMultiplier) + { + handler->PSendSysMessage("World hostile damage and healing multiplier: %.3f -> %.3f", + mapABInfo->worldDamageHealingMultiplier, + mapABInfo->scaledWorldDamageHealingMultiplier + ); + } + else + { + handler->PSendSysMessage("World hostile damage and healing multiplier: %.3f", + mapABInfo->worldDamageHealingMultiplier + ); + } + + // Creature Stats handler->PSendSysMessage("Original Creature Level Range: %u - %u (Avg: %.2f)", mapABInfo->lowestCreatureLevel, mapABInfo->highestCreatureLevel, @@ -3174,8 +6457,8 @@ class AutoBalance_CommandScript : public CommandScript } else { - handler->PSendSysMessage("The target is not in a dungeon or battleground."); - return true; + handler->PSendSysMessage("This command can only be used in a dungeon or raid."); + return false; } } @@ -3189,23 +6472,56 @@ class AutoBalance_CommandScript : public CommandScript handler->SetSentErrorMessage(true); return false; } + else if (!target->GetMap()->IsDungeon()) + { + handler->PSendSysMessage("That target is not in an instance."); + handler->SetSentErrorMessage(true); + return false; + } - AutoBalanceCreatureInfo *creatureABInfo=target->CustomData.GetDefault("AutoBalanceCreatureInfo"); - AutoBalanceMapInfo *mapABInfo=target->GetMap()->CustomData.GetDefault("AutoBalanceMapInfo"); + AutoBalanceCreatureInfo *targetABInfo=target->CustomData.GetDefault("AutoBalanceCreatureInfo"); handler->PSendSysMessage("---"); handler->PSendSysMessage("%s (%u%s%s), %s", target->GetName(), - creatureABInfo->UnmodifiedLevel, - mapABInfo->isLevelScalingEnabled ? std::string("->") + std::to_string(creatureABInfo->selectedLevel) : "", - target->IsDungeonBoss() ? " | Boss" : "", - creatureABInfo->isActive ? "Active for Map Stats" : "Ignored for Map Stats"); - handler->PSendSysMessage("Health multiplier: %.3f", creatureABInfo->HealthMultiplier); - handler->PSendSysMessage("Mana multiplier: %.3f", creatureABInfo->ManaMultiplier); - handler->PSendSysMessage("Armor multiplier: %.3f", creatureABInfo->ArmorMultiplier); - handler->PSendSysMessage("Damage multiplier: %.3f", creatureABInfo->DamageMultiplier); - handler->PSendSysMessage("CC Duration multiplier: %.3f", creatureABInfo->CCDurationMultiplier); - handler->PSendSysMessage("XP multiplier: %.3f Money multiplier: %.3f", creatureABInfo->XPModifier, creatureABInfo->MoneyModifier); + targetABInfo->UnmodifiedLevel, + isCreatureRelevant(target) && targetABInfo->UnmodifiedLevel != target->GetLevel() ? "->" + std::to_string(targetABInfo->selectedLevel) : "", + isBossOrBossSummon(target) ? " | Boss" : "", + targetABInfo->isActive ? "Active for Map Stats" : "Ignored for Map Stats"); + handler->PSendSysMessage("Creature difficulty level: %u player(s)", targetABInfo->instancePlayerCount); + + // summon + if (target->IsSummon() && targetABInfo->summoner && targetABInfo->isCloneOfSummoner) + { + handler->PSendSysMessage("Clone of %s (%u)", targetABInfo->summoner->GetName(), targetABInfo->summoner->GetLevel()); + } + else if (target->IsSummon() && targetABInfo->summoner) + { + handler->PSendSysMessage("Summon of %s (%u)", targetABInfo->summoner->GetName(), targetABInfo->summoner->GetLevel()); + } + else if (target->IsSummon()) + { + handler->PSendSysMessage("Summon without a summoner."); + } + + // level scaled + if (targetABInfo->UnmodifiedLevel != target->GetLevel()) + { + handler->PSendSysMessage("Health multiplier: %.3f -> %.3f", targetABInfo->HealthMultiplier, targetABInfo->ScaledHealthMultiplier); + handler->PSendSysMessage("Mana multiplier: %.3f -> %.3f", targetABInfo->ManaMultiplier, targetABInfo->ScaledManaMultiplier); + handler->PSendSysMessage("Armor multiplier: %.3f-> %.3f", targetABInfo->ArmorMultiplier, targetABInfo->ScaledArmorMultiplier); + handler->PSendSysMessage("Damage multiplier: %.3f -> %.3f", targetABInfo->DamageMultiplier, targetABInfo->ScaledDamageMultiplier); + } + // not level scaled + else + { + handler->PSendSysMessage("Health multiplier: %.3f", targetABInfo->HealthMultiplier); + handler->PSendSysMessage("Mana multiplier: %.3f", targetABInfo->ManaMultiplier); + handler->PSendSysMessage("Armor multiplier: %.3f", targetABInfo->ArmorMultiplier); + handler->PSendSysMessage("Damage multiplier: %.3f", targetABInfo->DamageMultiplier); + } + handler->PSendSysMessage("CC Duration multiplier: %.3f", targetABInfo->CCDurationMultiplier); + handler->PSendSysMessage("XP multiplier: %.3f Money multiplier: %.3f", targetABInfo->XPModifier, targetABInfo->MoneyModifier); return true; @@ -3223,11 +6539,11 @@ class AutoBalance_GlobalScript : public GlobalScript { if (!rewardEnabled || !updated) return; - if (map->GetPlayersCountExceptGMs() < MinPlayerReward) - return; - AutoBalanceMapInfo *mapABInfo=map->CustomData.GetDefault("AutoBalanceMapInfo"); + if (mapABInfo->adjustedPlayerCount < MinPlayerReward) + return; + // skip if it's not a pre-wotlk dungeon/raid and if it's not scaled if (!LevelScaling || mapABInfo->mapLevel <= 70 || mapABInfo->lfgMinLevel <= 70 // skip when not in dungeon or not kill credit @@ -3263,6 +6579,7 @@ void AddAutoBalanceScripts() new AutoBalance_WorldScript(); new AutoBalance_PlayerScript(); new AutoBalance_UnitScript(); + new AutoBalance_GameObjectScript(); new AutoBalance_AllCreatureScript(); new AutoBalance_AllMapScript(); new AutoBalance_CommandScript();