diff --git a/sql/updates/world/2022_01_20_00_world_creature_sparring.sql b/sql/updates/world/2022_01_20_00_world_creature_sparring.sql new file mode 100644 index 00000000..444ed862 --- /dev/null +++ b/sql/updates/world/2022_01_20_00_world_creature_sparring.sql @@ -0,0 +1,13 @@ +DROP TABLE IF EXISTS `creature_sparring_template`; +CREATE TABLE `creature_sparring_template` ( + `creature_id` mediumint(8) unsigned NOT NULL, + `health_limit_pct` float DEFAULT "0", + `comment` varchar(255) DEFAULT "", + PRIMARY KEY (`creature_id`) +) ENGINE=MyISAM DEFAULT CHARSET=utf8; + +DELETE FROM `creature_sparring_template` WHERE `creature_id` IN (49422, 49423, 49428); +INSERT INTO `creature_sparring_template`(`creature_id`, `health_limit_pct`, `comment`) VALUES +(49422,85,"Rotbrain Berserker"), +(49423,85,"Rotbrain Magus"), +(49428,85,"Deathguard Protector"); diff --git a/src/server/game/Entities/Creature/Creature.cpp b/src/server/game/Entities/Creature/Creature.cpp index b45fbf93..658d4d2b 100644 --- a/src/server/game/Entities/Creature/Creature.cpp +++ b/src/server/game/Entities/Creature/Creature.cpp @@ -2947,3 +2947,8 @@ void Creature::RecalculateDynamicHealth(uint32 newHealth) SetHealth(newHealth - damage); } } + +float Creature::GetSparringHealthLimit() const +{ + return sObjectMgr->GetSparringHealthLimitFor(GetEntry()); +} diff --git a/src/server/game/Entities/Creature/Creature.h b/src/server/game/Entities/Creature/Creature.h index 261b0114..aa945a6a 100644 --- a/src/server/game/Entities/Creature/Creature.h +++ b/src/server/game/Entities/Creature/Creature.h @@ -565,6 +565,8 @@ class Creature : public Unit, public GridObject, public MapObject CreatureData const* GetCreatureData() const { return m_creatureData; } CreatureAddon const* GetCreatureAddon() const; + float GetSparringHealthLimit() const; + std::string GetAIName() const; std::string GetScriptName() const; uint32 GetScriptId() const; @@ -832,4 +834,6 @@ class ForcedDespawnDelayEvent : public BasicEvent Creature& m_owner; }; +typedef std::unordered_map CreatureSparringTemplateMap; + #endif diff --git a/src/server/game/Entities/Unit/Unit.cpp b/src/server/game/Entities/Unit/Unit.cpp index fbf1efcf..d0b2e8a5 100644 --- a/src/server/game/Entities/Unit/Unit.cpp +++ b/src/server/game/Entities/Unit/Unit.cpp @@ -783,6 +783,22 @@ void Unit::DealDamageMods(Unit* victim, uint32 &damage, uint32* absorb) uint32 Unit::DealDamage(Unit* victim, uint32 damage, CleanDamage const* cleanDamage, DamageEffectType damagetype, SpellSchoolMask damageSchoolMask, SpellInfo const* spellProto, bool durabilityLoss) { + if (Creature* target = victim->ToCreature()) + { + if (GetTypeId() == TYPEID_UNIT && !IsCharmedOwnedByPlayerOrPlayer()) + { + float sparringLimitPct = target->GetSparringHealthLimit(); + + if (sparringLimitPct != 0.0f) + { + if (damage >= target->GetHealth()) // First check: if we have a sparring limit we will never allow creatures to kill the sparring victim + damage = target->GetHealth() - 1; + else if (target->GetHealthPct() <= sparringLimitPct) // Second check: stop incomming damage when we have surpassed the health limit + damage = 0; + } + } + } + if (GetTypeId() == TYPEID_PLAYER && ToPlayer()->GetSession()->GetSecurity() < SEC_GAMEMASTER && damage >= sWorld->getIntConfig(CONFIG_DAMAGE_LOG_MIN_DAMAGE)) { float takenMod = victim->GetTotalAuraModifier(SPELL_AURA_MOD_DAMAGE_PERCENT_TAKEN); @@ -2221,6 +2237,22 @@ void Unit::CalcAbsorbResist(Unit* victim, SpellSchoolMask schoolMask, DamageEffe uint32 splitAbsorb = dmgInfo.GetAbsorbedSplit(); DealDamageMods(caster, splitted, &splitAbsorb); + if (Creature* target = dmgInfo.GetVictim()->ToCreature()) + { + if (GetTypeId() == TypeID::TYPEID_UNIT && !IsCharmedOwnedByPlayerOrPlayer()) + { + float sparringLimitPct = target->GetSparringHealthLimit(); + + if (sparringLimitPct != 0.0f) + { + if (target->GetHealthPct() <= sparringLimitPct) + { + dmgInfo.ModifyDamage(dmgInfo.GetDamage() * -1); + } + } + } + } + SendSpellNonMeleeDamageLog(caster, (*itr)->GetSpellInfo()->Id, splitted, schoolMask, splitAbsorb, 0, false, 0, false); CleanDamage cleanDamage = CleanDamage(splitted, 0, BASE_ATTACK, MELEE_HIT_NORMAL); @@ -2359,6 +2391,23 @@ void Unit::AttackerStateUpdate(Unit* victim, bool ignoreLos, WeaponAttackType at CalculateMeleeDamage(victim, 0, &damageInfo, attType); // Send log damage message to client DealDamageMods(victim, damageInfo.damage, &damageInfo.absorb); + + if (Creature* target = victim->ToCreature()) + { + if (GetTypeId() == TypeID::TYPEID_UNIT && !IsCharmedOwnedByPlayerOrPlayer()) + { + float sparringLimitPct = target->GetSparringHealthLimit(); + + if (sparringLimitPct != 0.0f) + { + if (target->GetHealthPct() <= sparringLimitPct) + { + damageInfo.HitInfo |= HITINFO_FAKE_DAMAGE; + } + } + } + } + SendAttackStateUpdate(&damageInfo); DealMeleeDamage(&damageInfo, true); diff --git a/src/server/game/Globals/ObjectMgr.cpp b/src/server/game/Globals/ObjectMgr.cpp index 4cb557c3..48e138fd 100644 --- a/src/server/game/Globals/ObjectMgr.cpp +++ b/src/server/game/Globals/ObjectMgr.cpp @@ -704,6 +704,51 @@ void ObjectMgr::LoadCreatureDifficultyModifiers() TC_LOG_INFO("server.loading", ">> Loaded %u creature difficulty modifiers in %u ms", count, GetMSTimeDiffToNow(oldMSTime)); } +void ObjectMgr::LoadCreatureSparringTemplate() +{ + uint32 oldMSTime = getMSTime(); + + // 0 1 + QueryResult result = WorldDatabase.Query("SELECT creature_id, health_limit_pct FROM creature_sparring_template"); + + if (!result) + { + TC_LOG_INFO("server.loading", ">> Loaded 0 creature template sparring definitions. DB table `creature_sparring_template` is empty."); + return; + } + + uint32 count = 0; + do + { + Field* fields = result->Fetch(); + + uint32 entry = fields[0].GetUInt32(); + float healthPct = fields[1].GetFloat(); + + if (!sObjectMgr->GetCreatureTemplate(entry)) + { + TC_LOG_ERROR("sql.sql", "Creature template (Entry: %u) does not exist but has a record in `creature_sparring_template`", entry); + continue; + } + + if (healthPct > 100.0f) + { + TC_LOG_ERROR("sql.sql", "Sparring entry (Entry: %u) exceeds the health percentage limit. Setting to 100.", entry); + healthPct = 100.0f; + } + + if (healthPct <= 0.0f) + { + TC_LOG_ERROR("sql.sql", "Sparring entry (Entry: %u) has a negative or too small health percentage. Setting to 0.1.", entry); + healthPct = 0.1f; + } + + _creatureSparringTemplateStore[entry] = healthPct; + } while (result->NextRow()); + + TC_LOG_INFO("server.loading", ">> Loaded %u creature sparring templates in %u ms", count, GetMSTimeDiffToNow(oldMSTime)); +} + void ObjectMgr::CheckCreatureTemplate(CreatureTemplate const* cInfo) { if (!cInfo) diff --git a/src/server/game/Globals/ObjectMgr.h b/src/server/game/Globals/ObjectMgr.h index 955d5f39..43692c2a 100644 --- a/src/server/game/Globals/ObjectMgr.h +++ b/src/server/game/Globals/ObjectMgr.h @@ -1108,6 +1108,7 @@ class ObjectMgr void LoadGraveyardOrientations(); void LoadCreatureTemplates(); void LoadCreatureTemplateAddons(); + void LoadCreatureSparringTemplate(); void LoadCreatureDifficultyModifiers(); void CheckCreatureTemplate(CreatureTemplate const* cInfo); void LoadTempSummons(); @@ -1378,8 +1379,20 @@ class ObjectMgr return &itr->second; } GameObjectData& NewGOData(uint32 guid) { return _gameObjectDataStore[guid]; } + void DeleteGOData(uint32 guid); + float GetSparringHealthLimitFor(uint32 entry) const + { + auto itr = _creatureSparringTemplateStore.find(entry); + + if (itr != _creatureSparringTemplateStore.end()) + { + return itr->second; + } + return 0.0f; + } + TrinityStringLocale const* GetTrinityStringLocale(int32 entry) const { TrinityStringLocaleContainer::const_iterator itr = _trinityStringLocaleStore.find(entry); @@ -1792,6 +1805,7 @@ class ObjectMgr CreatureAddonContainer _creatureAddonStore; GameObjectAddonContainer _gameObjectAddonStore; CreatureAddonContainer _creatureTemplateAddonStore; + CreatureSparringTemplateMap _creatureSparringTemplateStore; EquipmentInfoContainer _equipmentInfoStore; LinkedRespawnContainer _linkedRespawnStore; CreatureLocaleContainer _creatureLocaleStore; diff --git a/src/server/game/Spells/Spell.cpp b/src/server/game/Spells/Spell.cpp index 920c0977..b4f2979c 100644 --- a/src/server/game/Spells/Spell.cpp +++ b/src/server/game/Spells/Spell.cpp @@ -2811,6 +2811,22 @@ void Spell::DoAllEffectOnTarget(TargetInfo* target) caster->CalculateSpellDamageTaken(&damageInfo, m_damage * m_spellValue->Multiplier, m_spellInfo, m_attackType, target->crit, procVictim, createProcExtendMask(&damageInfo, missInfo)); caster->DealDamageMods(damageInfo.target, damageInfo.damage, &damageInfo.absorb); + if (Creature* target = damageInfo.target->ToCreature()) + { + if (caster->GetTypeId() == TYPEID_UNIT && !caster->IsCharmedOwnedByPlayerOrPlayer()) + { + float sparringLimitPct = target->GetSparringHealthLimit(); + + if (sparringLimitPct != 0.0f) + { + if (target->GetHealthPct() <= sparringLimitPct) + { + damageInfo.damage = 0; + } + } + } + } + // Send log damage message to client caster->SendSpellNonMeleeDamageLog(&damageInfo); caster->DealSpellDamage(&damageInfo, true); diff --git a/src/server/game/World/World.cpp b/src/server/game/World/World.cpp index 5a652138..0139c32a 100644 --- a/src/server/game/World/World.cpp +++ b/src/server/game/World/World.cpp @@ -568,7 +568,7 @@ void World::LoadConfigSettings(bool reload) m_bool_configs[CONFIG_ALLOW_TWO_SIDE_INTERACTION_GROUP] = sConfigMgr->GetBoolDefault("AllowTwoSide.Interaction.Group", false); m_bool_configs[CONFIG_ALLOW_TWO_SIDE_INTERACTION_GUILD] = sConfigMgr->GetBoolDefault("AllowTwoSide.Interaction.Guild", false); m_bool_configs[CONFIG_ALLOW_TWO_SIDE_INTERACTION_AUCTION] = sConfigMgr->GetBoolDefault("AllowTwoSide.Interaction.Auction", false); - m_bool_configs[CONFIG_ALLOW_TWO_SIDE_TRADE] = sConfigMgr->GetBoolDefault("AllowTwoSide.trade", false); + m_bool_configs[CONFIG_ALLOW_TWO_SIDE_TRADE] = sConfigMgr->GetBoolDefault("AllowTwoSide.Trade", false); m_int_configs[CONFIG_STRICT_PLAYER_NAMES] = sConfigMgr->GetIntDefault ("StrictPlayerNames", 0); m_int_configs[CONFIG_STRICT_CHARTER_NAMES] = sConfigMgr->GetIntDefault ("StrictCharterNames", 0); m_int_configs[CONFIG_STRICT_PET_NAMES] = sConfigMgr->GetIntDefault ("StrictPetNames", 0); @@ -1890,6 +1890,9 @@ void World::SetInitialWorldSettings() TC_LOG_INFO("server.loading", "Loading GameObject Addon Data..."); sObjectMgr->LoadGameObjectAddons(); // must be after LoadGameObjectTemplate() and LoadGameobjects() + TC_LOG_INFO("server.loading", "Loading Creature Sparring Data..."); + sObjectMgr->LoadCreatureSparringTemplate(); + TC_LOG_INFO("server.loading", "Loading Creature Linked Respawn..."); sObjectMgr->LoadLinkedRespawn(); // must be after LoadCreatures(), LoadGameObjects()