diff --git a/data/sql/updates/pending_db_world/rev_1724592400426298600.sql b/data/sql/updates/pending_db_world/rev_1724592400426298600.sql new file mode 100644 index 000000000..038ea6a83 --- /dev/null +++ b/data/sql/updates/pending_db_world/rev_1724592400426298600.sql @@ -0,0 +1,9 @@ +-- +DROP TABLE IF EXISTS `creature_sparring`; +CREATE TABLE `creature_sparring` ( + `GUID` int unsigned NOT NULL, + `SparringPCT` float NOT NULL, + PRIMARY KEY (`GUID`), + FOREIGN KEY (`GUID`) REFERENCES creature(`guid`), + CONSTRAINT `creature_sparring_chk_1` CHECK (`SparringPCT` BETWEEN 0 AND 100) +) ENGINE=InnoDB DEFAULT CHARSET=utf8mb4 COLLATE=utf8mb4_unicode_ci; diff --git a/src/server/game/Entities/Creature/Creature.cpp b/src/server/game/Entities/Creature/Creature.cpp index 7abedcae7..ba92fa80a 100644 --- a/src/server/game/Entities/Creature/Creature.cpp +++ b/src/server/game/Entities/Creature/Creature.cpp @@ -273,7 +273,7 @@ Creature::Creature(bool isWorldObject): Unit(isWorldObject), MovableMapObject(), m_transportCheckTimer(1000), lootPickPocketRestoreTime(0), m_combatPulseTime(0), m_combatPulseDelay(0), m_reactState(REACT_AGGRESSIVE), m_defaultMovementType(IDLE_MOTION_TYPE), m_spawnId(0), m_equipmentId(0), m_originalEquipmentId(0), m_AlreadyCallAssistance(false), m_AlreadySearchedAssistance(false), m_regenHealth(true), m_regenPower(true), m_AI_locked(false), m_meleeDamageSchoolMask(SPELL_SCHOOL_MASK_NORMAL), m_originalEntry(0), m_moveInLineOfSightDisabled(false), m_moveInLineOfSightStrictlyDisabled(false), - m_homePosition(), m_transportHomePosition(), m_creatureInfo(nullptr), m_creatureData(nullptr), m_detectionDistance(20.0f), m_waypointID(0), m_path_id(0), m_formation(nullptr), m_lastLeashExtensionTime(nullptr), m_cannotReachTimer(0), + m_homePosition(), m_transportHomePosition(), m_creatureInfo(nullptr), m_creatureData(nullptr), m_detectionDistance(20.0f),_sparringPct(0.0f), m_waypointID(0), m_path_id(0), m_formation(nullptr), m_lastLeashExtensionTime(nullptr), m_cannotReachTimer(0), _isMissingSwimmingFlagOutOfCombat(false), m_assistanceTimer(0), _playerDamageReq(0), _damagedByPlayer(false), _isCombatMovementAllowed(true) { m_regenTimer = CREATURE_REGEN_INTERVAL; @@ -608,6 +608,8 @@ bool Creature::UpdateEntry(uint32 Entry, const CreatureData* data, bool changele SetCanModifyStats(true); UpdateAllStats(); + LoadSparringPct(); + // checked and error show at loading templates if (FactionTemplateEntry const* factionTemplate = sFactionTemplateStore.LookupEntry(cInfo->faction)) { @@ -1189,6 +1191,7 @@ bool Creature::Create(ObjectGuid::LowType guidlow, Map* map, uint32 phaseMask, u } LoadCreaturesAddon(); + LoadSparringPct(); //! Need to be called after LoadCreaturesAddon - MOVEMENTFLAG_HOVER is set there m_positionZ += GetHoverHeight(); @@ -2024,6 +2027,8 @@ void Creature::setDeathState(DeathState state, bool despawn) Motion_Initialize(); LoadCreaturesAddon(true); + LoadSparringPct(); + if (GetCreatureData() && GetPhaseMask() != GetCreatureData()->phaseMask) SetPhaseMask(GetCreatureData()->phaseMask, false); } @@ -2797,6 +2802,18 @@ bool Creature::LoadCreaturesAddon(bool reload) return true; } +void Creature::LoadSparringPct() +{ + ObjectGuid::LowType spawnId = GetSpawnId(); + auto const& sparringData = sObjectMgr->GetSparringData(); + + auto itr = sparringData.find(spawnId); + if (itr != sparringData.end() && !itr->second.empty()) + { + _sparringPct = itr->second[0]; + } +} + /// Send a message to LocalDefense channel for players opposition team in the zone void Creature::SendZoneUnderAttackMessage(Player* attacker) { diff --git a/src/server/game/Entities/Creature/Creature.h b/src/server/game/Entities/Creature/Creature.h index 17f69f801..b9632748b 100644 --- a/src/server/game/Entities/Creature/Creature.h +++ b/src/server/game/Entities/Creature/Creature.h @@ -187,6 +187,9 @@ public: void UpdateAttackPowerAndDamage(bool ranged = false) override; void CalculateMinMaxDamage(WeaponAttackType attType, bool normalized, bool addTotalPct, float& minDamage, float& maxDamage, uint8 damageIndex) override; + void LoadSparringPct(); + [[nodiscard]] float GetSparringPct() const { return _sparringPct; } + bool HasWeapon(WeaponAttackType type) const override; bool HasWeaponForAttack(WeaponAttackType type) const override { return (Unit::HasWeaponForAttack(type) && HasWeapon(type)); } void SetCanDualWield(bool value) override; @@ -483,6 +486,8 @@ protected: float m_detectionDistance; uint16 m_LootMode; // bitmask, default LOOT_MODE_DEFAULT, determines what loot will be lootable + float _sparringPct; + [[nodiscard]] bool IsInvisibleDueToDespawn() const override; bool CanAlwaysSee(WorldObject const* obj) const override; bool IsAlwaysDetectableFor(WorldObject const* seer) const override; diff --git a/src/server/game/Entities/Unit/Unit.cpp b/src/server/game/Entities/Unit/Unit.cpp index 25f1c2893..a09adbe81 100644 --- a/src/server/game/Entities/Unit/Unit.cpp +++ b/src/server/game/Entities/Unit/Unit.cpp @@ -1032,6 +1032,17 @@ uint32 Unit::DealDamage(Unit* attacker, Unit* victim, uint32 damage, CleanDamage } } + // Sparring + if (victim->CanSparringWith(attacker)) + { + if (damage >= victim->GetHealth()) + damage = 0; + + uint32 sparringHealth = victim->GetHealth() * (victim->ToCreature()->GetSparringPct() / 100); + if (victim->GetHealth() - damage <= sparringHealth) + damage = 0; + } + if (health <= damage) { LOG_DEBUG("entities.unit", "DealDamage: victim just died"); @@ -2635,6 +2646,10 @@ void Unit::AttackerStateUpdate(Unit* victim, WeaponAttackType attType /*= BASE_A Unit::DealDamageMods(victim, damageInfo.damages[i].damage, &damageInfo.damages[i].absorb); } + // Related to sparring system. Allow attack animations even if there are no damages + if (victim->CanSparringWith(damageInfo.attacker)) + damageInfo.HitInfo |= HITINFO_FAKE_DAMAGE; + SendAttackStateUpdate(&damageInfo); //TriggerAurasProcOnEvent(damageInfo); @@ -3954,6 +3969,24 @@ void Unit::_UpdateAutoRepeatSpell() } } +bool Unit::CanSparringWith(Unit const* attacker) const +{ + if (!IsCreature() || IsCharmedOwnedByPlayerOrPlayer()) + return false; + + if (!attacker) + return false; + + if (!attacker->IsCreature() || attacker->IsCharmedOwnedByPlayerOrPlayer()) + return false; + + if (Creature const* creature = ToCreature()) + if (!creature->GetSparringPct()) + return false; + + return true; +} + void Unit::SetCurrentCastedSpell(Spell* pSpell) { ASSERT(pSpell); // nullptr may be never passed here, use InterruptSpell or InterruptNonMeleeSpells diff --git a/src/server/game/Entities/Unit/Unit.h b/src/server/game/Entities/Unit/Unit.h index 39898f578..3bbc8f188 100644 --- a/src/server/game/Entities/Unit/Unit.h +++ b/src/server/game/Entities/Unit/Unit.h @@ -2038,6 +2038,8 @@ protected: void _UpdateAutoRepeatSpell(); + bool CanSparringWith(Unit const* attacker) const; ///@brief: Check if unit is eligible for sparring damages. Work only if attacker and victim are creatures. + bool IsAlwaysVisibleFor(WorldObject const* seer) const override; bool IsAlwaysDetectableFor(WorldObject const* seer) const override; diff --git a/src/server/game/Globals/ObjectMgr.cpp b/src/server/game/Globals/ObjectMgr.cpp index f2bc1fe4b..1dc3eeed3 100644 --- a/src/server/game/Globals/ObjectMgr.cpp +++ b/src/server/game/Globals/ObjectMgr.cpp @@ -2298,6 +2298,42 @@ void ObjectMgr::LoadCreatures() LOG_INFO("server.loading", " "); } +void ObjectMgr::LoadCreatureSparring() +{ + uint32 oldMSTime = getMSTime(); + + QueryResult result = WorldDatabase.Query("SELECT GUID, SparringPCT FROM creature_sparring"); + + if (!result) + { + LOG_WARN("server.loading", ">> Loaded 0 sparring data. DB table `creature_sparring` is empty."); + LOG_INFO("server.loading", " "); + return; + } + + uint32 count = 0; + do + { + Field* fields = result->Fetch(); + + ObjectGuid::LowType spawnId = fields[0].Get(); + float sparringHealthPct = fields[1].Get(); + + if (!sObjectMgr->GetCreatureData(spawnId)) + { + LOG_ERROR("sql.sql", "Entry {} has a record in `creature_sparring` but doesn't exist in `creatures` table"); + continue; + } + + _creatureSparringStore[spawnId].push_back(sparringHealthPct); + + ++count; + } while (result->NextRow()); + + LOG_INFO("server.loading", ">> Loaded {} sparring data in {} ms", count, GetMSTimeDiffToNow(oldMSTime)); + LOG_INFO("server.loading", " "); +} + void ObjectMgr::AddCreatureToGrid(ObjectGuid::LowType guid, CreatureData const* data) { uint8 mask = data->spawnMask; diff --git a/src/server/game/Globals/ObjectMgr.h b/src/server/game/Globals/ObjectMgr.h index 2b45f3ce1..ef1b38314 100644 --- a/src/server/game/Globals/ObjectMgr.h +++ b/src/server/game/Globals/ObjectMgr.h @@ -751,6 +751,8 @@ public: typedef std::map CharacterConversionMap; + typedef std::unordered_map> CreatureSparringContainer; + GameObjectTemplate const* GetGameObjectTemplate(uint32 entry); bool IsGameObjectStaticTransport(uint32 entry); [[nodiscard]] GameObjectTemplateContainer const* GetGameObjectTemplates() const { return &_gameObjectTemplateStore; } @@ -1028,6 +1030,7 @@ public: void LoadCreatureQuestItems(); void LoadTempSummons(); void LoadCreatures(); + void LoadCreatureSparring(); void LoadLinkedRespawn(); bool SetCreatureLinkedRespawn(ObjectGuid::LowType guid, ObjectGuid::LowType linkedGuid); void LoadCreatureAddons(); @@ -1201,6 +1204,9 @@ public: if (itr == _creatureDataStore.end()) return nullptr; return &itr->second; } + + [[nodiscard]] CreatureSparringContainer const& GetSparringData() const { return _creatureSparringStore; } + CreatureData& NewOrExistCreatureData(ObjectGuid::LowType spawnId) { return _creatureDataStore[spawnId]; } void DeleteCreatureData(ObjectGuid::LowType spawnId); [[nodiscard]] ObjectGuid GetLinkedRespawnGuid(ObjectGuid guid) const @@ -1526,6 +1532,8 @@ private: PageTextContainer _pageTextStore; InstanceTemplateContainer _instanceTemplateStore; + CreatureSparringContainer _creatureSparringStore; + private: void LoadScripts(ScriptsType type); void LoadQuestRelationsHelper(QuestRelations& map, std::string const& table, bool starter, bool go); diff --git a/src/server/game/World/World.cpp b/src/server/game/World/World.cpp index 3e7dd681b..2e81b71b0 100644 --- a/src/server/game/World/World.cpp +++ b/src/server/game/World/World.cpp @@ -1760,6 +1760,9 @@ void World::SetInitialWorldSettings() LOG_INFO("server.loading", "Loading Creature Data..."); sObjectMgr->LoadCreatures(); + LOG_INFO("server.loading", "Loading Creature sparring..."); + sObjectMgr->LoadCreatureSparring(); + LOG_INFO("server.loading", "Loading Temporary Summon Data..."); sObjectMgr->LoadTempSummons(); // must be after LoadCreatureTemplates() and LoadGameObjectTemplates()