Merge branch 'master' into Playerbot

This commit is contained in:
Yunfan Li
2025-01-31 18:29:34 +08:00
38 changed files with 671 additions and 278 deletions

View File

@@ -30,13 +30,20 @@ char const* localeNames[TOTAL_LOCALES] =
"ruRU"
};
bool IsLocaleValid(std::string const& locale)
{
for (int i = 0; i < TOTAL_LOCALES; ++i)
if (locale == localeNames[i])
return true;
return false;
}
LocaleConstant GetLocaleByName(const std::string& name)
{
for (uint32 i = 0; i < TOTAL_LOCALES; ++i)
if (name == localeNames[i])
{
return LocaleConstant(i);
}
return LOCALE_enUS; // including enGB case
}

View File

@@ -83,6 +83,7 @@ enum LocaleConstant
AC_COMMON_API extern char const* localeNames[TOTAL_LOCALES];
AC_COMMON_API bool IsLocaleValid(std::string const& locale);
AC_COMMON_API LocaleConstant GetLocaleByName(const std::string& name);
AC_COMMON_API const std::string GetNameByLocaleConstant(LocaleConstant localeConstant);
AC_COMMON_API void CleanStringForMysqlQuery(std::string& str);

View File

@@ -116,10 +116,11 @@ void LoginDatabaseConnection::DoPrepareStatements()
PrepareStatement(LOGIN_SEL_REALMLIST_SECURITY_LEVEL, "SELECT allowedSecurityLevel from realmlist WHERE id = ?", CONNECTION_SYNCH);
PrepareStatement(LOGIN_DEL_ACCOUNT, "DELETE FROM account WHERE id = ?", CONNECTION_ASYNC);
PrepareStatement(LOGIN_SEL_AUTOBROADCAST, "SELECT id, weight, text FROM autobroadcast WHERE realmid = ? OR realmid = -1", CONNECTION_SYNCH);
PrepareStatement(LOGIN_SEL_AUTOBROADCAST_LOCALIZED, "SELECT id, locale, text FROM autobroadcast_locale WHERE realmid = ? OR realmid = -1", CONNECTION_SYNCH);
PrepareStatement(LOGIN_SEL_MOTD, "SELECT text FROM motd WHERE realmid = ? OR realmid = -1 ORDER BY realmid DESC", CONNECTION_SYNCH);
PrepareStatement(LOGIN_SEL_MOTD_LOCALE, "SELECT locale, text FROM motd_localized WHERE realmid = ? OR realmid = -1 ORDER BY realmid DESC", CONNECTION_SYNCH);
PrepareStatement(LOGIN_INS_MOTD, "INSERT INTO motd (realmid, text) VALUES (?, ?) ON DUPLICATE KEY UPDATE text = ?", CONNECTION_ASYNC);
PrepareStatement(LOGIN_INS_MOTD_LOCALE, "INSERT INTO motd_localized (realmid, locale, text) VALUES(?, ?, ?) ON DUPLICATE KEY UPDATE text = ?;", CONNECTION_ASYNC);
PrepareStatement(LOGIN_REP_MOTD, "REPLACE INTO motd (realmid, text) VALUES (?, ?)", CONNECTION_ASYNC);
PrepareStatement(LOGIN_REP_MOTD_LOCALE, "REPLACE INTO motd_localized (realmid, locale, text) VALUES (?, ?, ?)", CONNECTION_ASYNC);
PrepareStatement(LOGIN_INS_ACCOUNT_MUTE, "INSERT INTO account_muted VALUES (?, UNIX_TIMESTAMP(), ?, ?, ?)", CONNECTION_ASYNC);
PrepareStatement(LOGIN_SEL_ACCOUNT_MUTE_INFO, "SELECT mutedate, mutetime, mutereason, mutedby FROM account_muted WHERE guid = ? ORDER BY mutedate ASC", CONNECTION_SYNCH);
PrepareStatement(LOGIN_DEL_ACCOUNT_MUTED, "DELETE FROM account_muted WHERE guid = ?", CONNECTION_ASYNC);

View File

@@ -98,10 +98,11 @@ enum LoginDatabaseStatements : uint32
LOGIN_SEL_REALMLIST_SECURITY_LEVEL,
LOGIN_DEL_ACCOUNT,
LOGIN_SEL_AUTOBROADCAST,
LOGIN_SEL_AUTOBROADCAST_LOCALIZED,
LOGIN_SEL_MOTD,
LOGIN_SEL_MOTD_LOCALE,
LOGIN_INS_MOTD,
LOGIN_INS_MOTD_LOCALE,
LOGIN_REP_MOTD,
LOGIN_REP_MOTD_LOCALE,
LOGIN_SEL_LAST_ATTEMPT_IP,
LOGIN_SEL_LAST_IP,
LOGIN_INS_ALDL_IP_LOGGING,

View File

@@ -42,7 +42,6 @@ void AutobroadcastMgr::LoadAutobroadcasts()
if (!result)
{
LOG_WARN("autobroadcast", ">> Loaded 0 autobroadcasts definitions. DB table `autobroadcast` is empty for this realm!");
LOG_INFO("autobroadcast", " ");
return;
}
@@ -54,35 +53,66 @@ void AutobroadcastMgr::LoadAutobroadcasts()
_announceType = AnnounceType::World;
}
uint32 count = 0;
do
{
Field* fields = result->Fetch();
uint8 textId = fields[0].Get<uint8>();
ObjectMgr::AddLocaleString(fields[2].Get<std::string>(), DEFAULT_LOCALE, _autobroadcasts[textId]);
_autobroadcastsWeights[textId] = fields[1].Get<uint8>();
} while (result->NextRow());
LOG_INFO("server.loading", ">> Loaded {} Autobroadcast Definitions in {} ms", _autobroadcasts.size(), GetMSTimeDiffToNow(oldMSTime));
}
void AutobroadcastMgr::LoadAutobroadcastsLocalized()
{
uint32 oldMSTime = getMSTime();
uint32 realmId = sConfigMgr->GetOption<int32>("RealmID", 0);
if (_autobroadcasts.empty())
return;
LoginDatabasePreparedStatement* stmt = LoginDatabase.GetPreparedStatement(LOGIN_SEL_AUTOBROADCAST_LOCALIZED);
stmt->SetData(0, realmId);
PreparedQueryResult result = LoginDatabase.Query(stmt);
if (!result)
{
LOG_WARN("server.loading", ">> Loaded 0 localized autobroadcasts definitions. DB table `autobroadcast_localized` is empty for this realm!");
LOG_INFO("server.loading", " ");
return;
}
uint8 count = 0;
do
{
Field* fields = result->Fetch();
uint8 id = fields[0].Get<uint8>();
uint8 textId = fields[0].Get<uint8>();
LocaleConstant locale = GetLocaleByName(fields[1].Get<std::string>());
_autobroadcasts[id] = fields[2].Get<std::string>();
_autobroadcastsWeights[id] = fields[1].Get<uint8>();
if (locale == DEFAULT_LOCALE || ObjectMgr::GetLocaleString(_autobroadcasts[textId], DEFAULT_LOCALE).empty())
continue;
++count;
ObjectMgr::AddLocaleString(fields[2].Get<std::string>(), locale, _autobroadcasts[textId]);
count++;
} while (result->NextRow());
LOG_INFO("autobroadcast", ">> Loaded {} Autobroadcast Definitions in {} ms", count, GetMSTimeDiffToNow(oldMSTime));
LOG_INFO("autobroadcast", " ");
LOG_INFO("server.loading", ">> Loaded {} Localized Autobroadcast Definitions in {} ms", count, GetMSTimeDiffToNow(oldMSTime));
LOG_INFO("server.loading", " ");
}
void AutobroadcastMgr::SendAutobroadcasts()
{
if (_autobroadcasts.empty())
{
return;
}
uint32 weight = 0;
uint8 textId = 0;
AutobroadcastsWeightMap selectionWeights;
std::string msg;
for (AutobroadcastsWeightMap::const_iterator it = _autobroadcastsWeights.begin(); it != _autobroadcastsWeights.end(); ++it)
{
if (it->second)
@@ -101,42 +131,78 @@ void AutobroadcastMgr::SendAutobroadcasts()
weight += it->second;
if (selectedWeight < weight)
{
msg = _autobroadcasts[it->first];
textId = it->first;
break;
}
}
}
else
{
msg = _autobroadcasts[urand(0, _autobroadcasts.size())];
textId = urand(0, _autobroadcasts.size());
}
switch (_announceType)
{
case AnnounceType::World:
SendWorldAnnouncement(msg);
SendWorldAnnouncement(textId);
break;
case AnnounceType::Notification:
SendNotificationAnnouncement(msg);
SendNotificationAnnouncement(textId);
break;
case AnnounceType::Both:
SendWorldAnnouncement(msg);
SendNotificationAnnouncement(msg);
SendWorldAnnouncement(textId);
SendNotificationAnnouncement(textId);
default:
break;
}
LOG_DEBUG("autobroadcast", "AutobroadcastMgr::SendAutobroadcasts: '{}'", msg);
LOG_DEBUG("autobroadcast", "AutobroadcastMgr::SendAutobroadcasts: '{}'", textId);
}
void AutobroadcastMgr::SendWorldAnnouncement(std::string msg)
void AutobroadcastMgr::SendWorldAnnouncement(uint8 textId)
{
ChatHandler(nullptr).SendWorldTextOptional(LANG_AUTO_BROADCAST, ANNOUNCER_FLAG_DISABLE_AUTOBROADCAST, msg.data());
// Send localized messages to all sessions
ChatHandler(nullptr).DoForAllValidSessions([&](Player* player)
{
// Get player's locale
LocaleConstant locale = player->GetSession()->GetSessionDbLocaleIndex();
if (!_autobroadcasts.empty())
return;
std::string_view localizedMessage = ObjectMgr::GetLocaleString(_autobroadcasts[textId], locale);
// Check if there is a localized message if not use default one.
if (localizedMessage.empty())
localizedMessage = ObjectMgr::GetLocaleString(_autobroadcasts[textId], DEFAULT_LOCALE);
// Send the localized or fallback message
ChatHandler(player->GetSession()).SendWorldTextOptional(localizedMessage, ANNOUNCER_FLAG_DISABLE_AUTOBROADCAST);
});
}
void AutobroadcastMgr::SendNotificationAnnouncement(std::string msg)
void AutobroadcastMgr::SendNotificationAnnouncement(uint8 textId)
{
WorldPacket data(SMSG_NOTIFICATION, (msg.size() + 1));
data << msg.data();
sWorld->SendGlobalMessage(&data);
ChatHandler(nullptr).DoForAllValidSessions([&](Player* player)
{
// Retrieve player's locale
LocaleConstant locale = player->GetSession()->GetSessionDbLocaleIndex();
if (!_autobroadcasts.count(textId))
return;
// Get localized message
std::string_view localizedMessage = ObjectMgr::GetLocaleString(_autobroadcasts[textId], locale);
// Check if there is a localized message if not use default one.
if (localizedMessage.empty())
localizedMessage = ObjectMgr::GetLocaleString(_autobroadcasts[textId], DEFAULT_LOCALE);
// Prepare the WorldPacket
WorldPacket data(SMSG_NOTIFICATION, (localizedMessage.size() + 1));
data << localizedMessage;
// Send packet to the player
player->GetSession()->SendPacket(&data);
});
}

View File

@@ -20,6 +20,7 @@
#include "Common.h"
#include <map>
#include <vector>
enum class AnnounceType : uint8
{
@@ -34,17 +35,18 @@ public:
static AutobroadcastMgr* instance();
void LoadAutobroadcasts();
void LoadAutobroadcastsLocalized();
void SendAutobroadcasts();
private:
void SendWorldAnnouncement(std::string msg);
void SendNotificationAnnouncement(std::string msg);
void SendWorldAnnouncement(uint8 textId);
void SendNotificationAnnouncement(uint8 textId);
typedef std::map<uint8, std::string> AutobroadcastsMap;
typedef std::map<uint8, std::vector<std::string>> AutobroadcastsMap;
typedef std::map<uint8, uint8> AutobroadcastsWeightMap;
AutobroadcastsMap _autobroadcasts;
AutobroadcastsWeightMap _autobroadcastsWeights;
AutobroadcastsMap _autobroadcasts; // autobroadcast messages
AutobroadcastsWeightMap _autobroadcastsWeights; // Weights for each message
AnnounceType _announceType;
};

View File

@@ -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();
@@ -2025,6 +2028,8 @@ void Creature::setDeathState(DeathState state, bool despawn)
Motion_Initialize();
LoadCreaturesAddon(true);
LoadSparringPct();
if (GetCreatureData() && GetPhaseMask() != GetCreatureData()->phaseMask)
SetPhaseMask(GetCreatureData()->phaseMask, false);
}
@@ -2798,6 +2803,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)
{

View File

@@ -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;

View File

@@ -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

View File

@@ -726,6 +726,9 @@ public:
void RemoveUnitFlag2(UnitFlags2 flags) { RemoveFlag(UNIT_FIELD_FLAGS_2, flags); }
void ReplaceAllUnitFlags2(UnitFlags2 flags) { SetUInt32Value(UNIT_FIELD_FLAGS_2, flags); }
void SetEmoteState(Emote emoteState) { SetUInt32Value(UNIT_NPC_EMOTESTATE, emoteState); } /// @brief Sets emote state (looping emote). Emotes available in SharedDefines.h
void ClearEmoteState() { SetEmoteState(EMOTE_ONESHOT_NONE); } /// @brief Clears emote state (looping emote)
// NPC flags
NPCFlags GetNpcFlags() const { return NPCFlags(GetUInt32Value(UNIT_NPC_FLAGS)); }
bool HasNpcFlag(NPCFlags flags) const { return HasFlag(UNIT_NPC_FLAGS, flags) != 0; }
@@ -2044,6 +2047,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;

View File

@@ -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<uint32>();
float sparringHealthPct = fields[1].Get<float>();
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;

View File

@@ -751,6 +751,8 @@ public:
typedef std::map<uint32, uint32> CharacterConversionMap;
typedef std::unordered_map<ObjectGuid::LowType, std::vector<float>> CreatureSparringContainer;
GameObjectTemplate const* GetGameObjectTemplate(uint32 entry);
bool IsGameObjectStaticTransport(uint32 entry);
[[nodiscard]] GameObjectTemplateContainer const* GetGameObjectTemplates() const { return &_gameObjectTemplateStore; }
@@ -1029,6 +1031,7 @@ public:
void LoadCreatureQuestItems();
void LoadTempSummons();
void LoadCreatures();
void LoadCreatureSparring();
void LoadLinkedRespawn();
bool SetCreatureLinkedRespawn(ObjectGuid::LowType guid, ObjectGuid::LowType linkedGuid);
void LoadCreatureAddons();
@@ -1202,6 +1205,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
@@ -1527,6 +1533,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);

View File

@@ -749,6 +749,9 @@ void WorldSession::HandleAuctionListItems(WorldPacket& recvData)
if (usable)
{
AuctionHouseUsablePlayerInfo usablePlayerInfo;
usablePlayerInfo.classMask = GetPlayer()->getClassMask();
usablePlayerInfo.raceMask = GetPlayer()->getRaceMask();
usablePlayerInfo.level = GetPlayer()->GetLevel();
SkillStatusMap const& skillMap = GetPlayer()->GetSkillStatusMap();
for (auto const& pair : skillMap)

View File

@@ -111,7 +111,8 @@ enum AcoreStrings
LANG_RBAC_PERM_REVOKED_NOT_IN_LIST = 79,
LANG_PVPSTATS = 80,
LANG_PVPSTATS_DISABLED = 81,
// Free 82 - 86
LANG_GENERIC_TWO_CURLIES_WITH_COLON = 82,
// Free 83 - 86
LANG_UNKNOWN_ERROR = 87,
LANG_2FA_COMMANDS_NOT_SETUP = 88,

View File

@@ -39,11 +39,6 @@ MotdMgr* MotdMgr::instance()
return &instance;
}
bool MotdMgr::IsValidLocale(std::string const& locale) {
// Use std::find to search for the locale in the array
return std::find(std::begin(localeNames), std::end(localeNames), locale) != std::end(localeNames);
}
void MotdMgr::SetMotd(std::string motd, LocaleConstant locale)
{
// scripts may change motd
@@ -53,31 +48,76 @@ void MotdMgr::SetMotd(std::string motd, LocaleConstant locale)
MotdPackets[locale] = CreateWorldPacket(motd);
}
void MotdMgr::CreateWorldPackages()
{
for (auto const& [locale, motd] : MotdMap)
// Store the constructed packet in MotdPackets with the locale as the key
MotdPackets[locale] = CreateWorldPacket(motd);
}
void MotdMgr::LoadMotd()
{
uint32 oldMSTime = getMSTime();
uint32 realmId = sConfigMgr->GetOption<int32>("RealmID", 0);
LoginDatabasePreparedStatement* stmt = LoginDatabase.GetPreparedStatement(LOGIN_SEL_MOTD);
stmt->SetData(0, realmId);
PreparedQueryResult result = LoginDatabase.Query(stmt);
// Load the main motd for the realm and assign it to enUS if available
std::string motd = LoadDefaultMotd(realmId);
if (result)
{
Field* fields = result->Fetch();
std::string motd = fields[0].Get<std::string>();
// Check if motd was loaded; if not, set default only for enUS
if (motd.empty())
SetDefaultMotd(); // Only sets enUS default if motd is empty
SetMotd(motd, LOCALE_enUS);
LoadMotdLocale();
}
else
MotdMap[DEFAULT_LOCALE] = motd; // Assign the loaded motd to enUS
{
LOG_INFO("server.loading", ">> Loaded 0 motd definitions. DB table `motd` is empty for this realm!");
LOG_INFO("server.loading", ">> Loaded 0 motd locale definitions. DB table `motd` needs an entry to be able to load DB table `motd_locale`!");
LOG_INFO("server.loading", " ");
}
// Load localized texts if available
LoadLocalizedMotds(realmId);
LOG_INFO("server.loading", ">> Loaded motd definitions in {} ms", GetMSTimeDiffToNow(oldMSTime));
LOG_INFO("server.loading", " ");
}
// Create all world packages after loading motd and localized texts
CreateWorldPackages();
void MotdMgr::LoadMotdLocale()
{
uint32 oldMSTime = getMSTime();
uint32 count = 0;
LOG_INFO("server.loading", "Loading Motd locale...");
uint32 realmId = sConfigMgr->GetOption<int32>("RealmID", 0);
LoginDatabasePreparedStatement* stmt = LoginDatabase.GetPreparedStatement(LOGIN_SEL_MOTD_LOCALE);
stmt->SetData(0, realmId);
PreparedQueryResult result = LoginDatabase.Query(stmt);
if (result)
{
do
{
Field* fields = result->Fetch();
// fields[0] is the locale string and fields[1] is the localized motd text
std::string locale = fields[0].Get<std::string>();
std::string localizedText = fields[1].Get<std::string>();
if (!IsLocaleValid(locale))
{
LOG_ERROR("server.loading", "DB table `motd_localized` has invalid locale ({}), skipped.", locale);
continue;
}
LocaleConstant localeId = GetLocaleByName(locale);
if (localeId == LOCALE_enUS)
continue;
SetMotd(localizedText, localeId);
++count;
} while (result->NextRow());
}
else
{
LOG_INFO("server.loading", ">> Loaded 0 motd locale definitions. DB table `motd_localized` is empty for this realm!");
LOG_INFO("server.loading", " ");
}
LOG_INFO("server.loading", ">> Loaded {} motd locale definitions in {} ms", count, GetMSTimeDiffToNow(oldMSTime));
}
char const* MotdMgr::GetMotd(LocaleConstant locale)
@@ -87,7 +127,7 @@ char const* MotdMgr::GetMotd(LocaleConstant locale)
if (it != MotdMap.end())
return it->second.c_str();
return MotdMap[DEFAULT_LOCALE].c_str(); // Fallback to enUS if locale is not found
return MotdMap[LOCALE_enUS].c_str(); // Fallback to enUS if locale is not found
}
WorldPacket const* MotdMgr::GetMotdPacket(LocaleConstant locale)
@@ -97,79 +137,21 @@ WorldPacket const* MotdMgr::GetMotdPacket(LocaleConstant locale)
if (it != MotdPackets.end())
return &it->second;
return &MotdPackets[DEFAULT_LOCALE]; // Fallback to enUS if locale is not found
return &MotdPackets[LOCALE_enUS]; // Fallback to enUS if locale is not found
}
std::string MotdMgr::LoadDefaultMotd(uint32 realmId)
WorldPacket MotdMgr::CreateWorldPacket(std::string motd)
{
LoginDatabasePreparedStatement* stmt = LoginDatabase.GetPreparedStatement(LOGIN_SEL_MOTD);
stmt->SetData(0, realmId);
PreparedQueryResult result = LoginDatabase.Query(stmt);
// Create a new WorldPacket for this locale
WorldPacket data(SMSG_MOTD); // new in 2.0.1
if (result)
{
Field* fields = result->Fetch();
return fields[0].Get<std::string>(); // Return the main motd if found
}
return ""; // Return empty string if no motd found
}
void MotdMgr::SetDefaultMotd()
{
std::string motd = /* fctlsup << //0x338// "63"+"cx""d2"+"1e""dd"+"cx""ds"+"ce""dd"+"ce""7D"+ << */
/*"d3"+"ce"*/ std::string("@|") + "cf" +/*"as"+"k4"*/"fF" + "F4" +/*"d5"+"f3"*/"A2" + "DT"/*"F4"+"Az"*/ + "hi" + "s "
motd = /* fctlsup << //0x338// "63"+"cx""d2"+"1e""dd"+"cx""ds"+"ce""dd"+"ce""7D"+ << */ motd
/*"d3"+"ce"*/ + "@|" + "cf" +/*"as"+"k4"*/"fF" + "F4" +/*"d5"+"f3"*/"A2" + "DT"/*"F4"+"Az"*/ + "hi" + "s "
/*"fd"+"hy"*/ + "se" + "rv" +/*"nh"+"k3"*/"er" + " r" +/*"x1"+"A2"*/"un" + "s "/*"F2"+"Ay"*/ + "on" + " Az"
/*"xs"+"5n"*/ + "er" + "ot" +/*"xs"+"A2"*/"hC" + "or" +/*"a4"+"f3"*/"e|" + "r "/*"f2"+"A2"*/ + "|c" + "ff"
/*"5g"+"A2"*/ + "3C" + "E7" +/*"k5"+"AX"*/"FF" + "ww" +/*"sx"+"Gj"*/"w." + "az"/*"a1"+"vf"*/ + "er" + "ot"
/*"ds"+"sx"*/ + "hc" + "or" +/*"F4"+"k5"*/"e." + "or" +/*"po"+"xs"*/"g|r"/*"F4"+"p2"+"o4"+"A2"+"i2"*/;
MotdMap[DEFAULT_LOCALE] = motd;
// Log that no motd was found and a default is being used for enUS
LOG_WARN("server.loading", ">> Loaded 0 motd definitions. DB table `motd` is empty for this realm!");
LOG_INFO("server.loading", " ");
}
void MotdMgr::LoadLocalizedMotds(uint32 realmId) {
// First, check if base MOTD exists
LoginDatabasePreparedStatement* baseStmt = LoginDatabase.GetPreparedStatement(LOGIN_SEL_MOTD);
baseStmt->SetData(0, realmId);
PreparedQueryResult baseResult = LoginDatabase.Query(baseStmt);
if (!baseResult)
{
LOG_ERROR("server.loading", "No base MOTD found for realm {}. Localized MOTDs will not be loaded.", realmId);
return;
}
// Now load localized versions
LoginDatabasePreparedStatement* stmt = LoginDatabase.GetPreparedStatement(LOGIN_SEL_MOTD_LOCALE);
stmt->SetData(0, realmId);
PreparedQueryResult result = LoginDatabase.Query(stmt);
if (result)
{
do {
Field* fields = result->Fetch();
// fields[0] is the locale string and fields[1] is the localized motd text
std::string localizedText = fields[1].Get<std::string>();
// Convert locale string to LocaleConstant
LocaleConstant localeId = GetLocaleByName(fields[0].Get<std::string>());
if (localeId == DEFAULT_LOCALE)
continue;
MotdMap[localeId] = localizedText;
} while (result->NextRow());
}
}
WorldPacket MotdMgr::CreateWorldPacket(std::string const& motd)
{
// Create a new WorldPacket for this locale
WorldPacket data(SMSG_MOTD); // new in 2.0.1
// Tokenize the motd string by '@'
std::vector<std::string_view> motdTokens = Acore::Tokenize(motd, '@', true);
data << uint32(motdTokens.size()); // line count

View File

@@ -29,9 +29,6 @@ class AC_GAME_API MotdMgr
public:
static MotdMgr* instance();
/// Converts the localized string to world packages
void CreateWorldPackages();
/// Set a new Message of the Day
void SetMotd(std::string motd, LocaleConstant locale);
@@ -44,18 +41,12 @@ public:
/// Returns the current motd packet for the given locale
WorldPacket const* GetMotdPacket(LocaleConstant locale);
// Checks if string is valid locale
bool IsValidLocale(std::string const& locale);
private:
// Loads the default motd from the motd table
std::string LoadDefaultMotd(uint32 realmId);
// Loads all available localized motd for the realm
void LoadLocalizedMotds(uint32 realmId);
// Sets the default mode if none is found in the database
void SetDefaultMotd();
void LoadMotdLocale();
// Create a worldpacket for a given motd localization
WorldPacket CreateWorldPacket(std::string const& motd);
WorldPacket CreateWorldPacket(std::string motd);
};
#define sMotdMgr MotdMgr::instance()

View File

@@ -1761,6 +1761,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()
@@ -2029,6 +2032,7 @@ void World::SetInitialWorldSettings()
///- Load AutoBroadCast
LOG_INFO("server.loading", "Loading Autobroadcasts...");
sAutobroadcastMgr->LoadAutobroadcasts();
sAutobroadcastMgr->LoadAutobroadcastsLocalized();
///- Load Motd
LOG_INFO("server.loading", "Loading Motd...");

View File

@@ -297,11 +297,9 @@ public:
// Display the 'Message of the day' for the realm
static bool HandleServerMotdCommand(ChatHandler* handler)
{
LocaleConstant localeConstant = DEFAULT_LOCALE;
if (Player* player = handler->GetPlayer())
localeConstant = player->GetSession()->GetSessionDbLocaleIndex();
handler->PSendSysMessage(LANG_MOTD_CURRENT, sMotdMgr->GetMotd(localeConstant));
handler->PSendSysMessage(LANG_MOTD_CURRENT);
for (uint32 i = 0; i < TOTAL_LOCALES; ++i)
handler->PSendSysMessage(LANG_GENERIC_TWO_CURLIES_WITH_COLON, GetNameByLocaleConstant(LocaleConstant(i)), sMotdMgr->GetMotd(LocaleConstant(i)));
return true;
}
@@ -533,7 +531,7 @@ public:
}
// Define the 'Message of the day' for the realm
static bool HandleServerSetMotdCommand(ChatHandler* handler, Optional<int32> realmId, Optional<std::string> locale, Tail motd)
static bool HandleServerSetMotdCommand(ChatHandler* handler, Optional<int32> realmId, std::string locale, Tail motd)
{
std::wstring wMotd = std::wstring();
std::string strMotd = std::string();
@@ -542,39 +540,21 @@ public:
if (!realmId)
realmId = static_cast<int32>(realm.Id.Realm);
// Determine the locale; default to "enUS" if not provided
LocaleConstant localeConstant;
if (IsLocaleValid(locale))
localeConstant = GetLocaleByName(locale);
else
{
handler->SendErrorMessage("locale ({}) is not valid. Valid locales: enUS, koKR, frFR, deDE, zhCN, zhWE, esES, esMX, ruRU.", locale);
return false;
}
if (motd.empty())
return false;
// Convert Tail (motd) to std::string
std::ostringstream motdStream;
motdStream << motd;
std::string motdString = motdStream.str(); // Convert Tail to std::string
// Determine the locale; default to "enUS" if not provided
LocaleConstant localeConstant = DEFAULT_LOCALE;
if (locale.has_value())
{
if (sMotdMgr->IsValidLocale(locale.value()))
{
localeConstant = GetLocaleByName(locale.value());
}
else
{
motdStream.str("");
motdStream << locale.value() << " " << motd;
motdString = motdStream.str();
localeConstant = DEFAULT_LOCALE;
locale = GetNameByLocaleConstant(localeConstant);
}
}
else
{
// Set to default locale string
localeConstant = DEFAULT_LOCALE;
locale = GetNameByLocaleConstant(localeConstant);
}
// Convert the concatenated motdString to UTF-8 and ensure encoding consistency
if (!Utf8toWStr(motdString, wMotd))
if (!Utf8toWStr(motd, wMotd))
return false;
if (!WStrToUtf8(wMotd, strMotd))
@@ -583,34 +563,29 @@ public:
// Start a transaction for the database operations
LoginDatabaseTransaction trans = LoginDatabase.BeginTransaction();
if (localeConstant == DEFAULT_LOCALE)
if (localeConstant == LOCALE_enUS)
{
// Insert or update in the main motd table for enUS
LoginDatabasePreparedStatement* stmt = LoginDatabase.GetPreparedStatement(LOGIN_INS_MOTD);
LoginDatabasePreparedStatement* stmt = LoginDatabase.GetPreparedStatement(LOGIN_REP_MOTD);
stmt->SetData(0, realmId.value()); // realmId for insertion
stmt->SetData(1, strMotd); // motd text for insertion
stmt->SetData(2, strMotd); // motd text for ON DUPLICATE KEY UPDATE
trans->Append(stmt);
}
else
{
// Insert or update in the motd_localized table for other locales
LoginDatabasePreparedStatement* stmt = LoginDatabase.GetPreparedStatement(LOGIN_INS_MOTD_LOCALE);
LoginDatabasePreparedStatement* stmt = LoginDatabase.GetPreparedStatement(LOGIN_REP_MOTD_LOCALE);
stmt->SetData(0, realmId.value()); // realmId for insertion
stmt->SetData(1, locale.value()); // locale for insertion
stmt->SetData(1, locale); // locale for insertion
stmt->SetData(2, strMotd); // motd text for insertion
stmt->SetData(3, strMotd); // motd text for ON DUPLICATE KEY UPDATE
trans->Append(stmt);
}
// Commit the transaction & update db
LoginDatabase.CommitTransaction(trans);
// Update the in-memory maps for the current realm. Otherwise, do not update
if (realmId == -1 || realmId == static_cast<int32>(realm.Id.Realm))
sMotdMgr->SetMotd(strMotd, localeConstant);
handler->PSendSysMessage(LANG_MOTD_NEW, realmId.value(), locale.value(), strMotd);
sMotdMgr->SetMotd(strMotd, localeConstant);
handler->PSendSysMessage(LANG_MOTD_NEW, realmId.value(), locale, strMotd);
return true;
}

View File

@@ -52,7 +52,12 @@ enum Spells
SPELL_GRAVITY_LAPSE_FLY = 44227,
SPELL_GRAVITY_LAPSE_DOT = 44226,
SPELL_GRAVITY_LAPSE_CHANNEL = 44251,
SPELL_POWER_FEEDBACK = 44233
SPELL_POWER_FEEDBACK = 44233,
SPELL_CLEAR_FLIGHT = 44232, // Does nothing currently
SPELL_EMOTE_EXCLAMATION = 48348,
SPELL_EMOTE_POINT = 48349,
SPELL_EMOTE_ROAR = 48350
};
enum Misc
@@ -67,19 +72,13 @@ enum Misc
struct boss_felblood_kaelthas : public BossAI
{
boss_felblood_kaelthas(Creature* creature) : BossAI(creature, DATA_KAELTHAS)
{
_hasDoneIntro = false;
}
boss_felblood_kaelthas(Creature* creature) : BossAI(creature, DATA_KAELTHAS) { }
void Reset() override
{
BossAI::Reset();
_OOCScheduler.CancelAll();
_gravityLapseCounter = 0;
me->ApplySpellImmune(0, IMMUNITY_EFFECT, SPELL_EFFECT_INTERRUPT_CAST, false);
me->SetImmuneToAll(false);
summons.DespawnAll();
ScheduleHealthCheckEvent(50, [&]{
me->CastStop();
@@ -101,12 +100,6 @@ struct boss_felblood_kaelthas : public BossAI
});
}
void JustSummoned(Creature* summon) override
{
BossAI::JustSummoned(summon);
summon->SetReactState(REACT_PASSIVE);
}
void GravityLapseSequence(bool firstTime)
{
Talk(firstTime ? SAY_GRAVITY_LAPSE : SAY_RECAST_GRAVITY);
@@ -132,12 +125,6 @@ struct boss_felblood_kaelthas : public BossAI
});
}
void InitializeAI() override
{
BossAI::InitializeAI();
me->SetImmuneToAll(true);
}
void JustDied(Unit* killer) override
{
BossAI::JustDied(killer);
@@ -168,20 +155,32 @@ struct boss_felblood_kaelthas : public BossAI
}, 50s);
}
void MoveInLineOfSight(Unit* who) override
void DoAction(int32 actionId) override
{
if (!_hasDoneIntro && me->IsWithinDistInMap(who, 40.0f) && who->IsPlayer())
if (actionId == DATA_KAEL_INTRO)
{
Talk(SAY_AGGRO);
Talk(SAY_AGGRO_2, 20s);
_hasDoneIntro = true;
_OOCScheduler.Schedule(35s, [this](TaskContext){
me->SetReactState(REACT_AGGRESSIVE);
me->SetImmuneToAll(false);
me->SetInCombatWithZone();
});
uint32 counter = instance->GetPersistentData(DATA_KAEL_INTRO);
instance->StorePersistentData(DATA_KAEL_INTRO, ++counter);
if (counter == 6 && !me->IsInCombat())
{
me->SetEmoteState(EMOTE_STATE_TALK);
Talk(SAY_AGGRO);
me->m_Events.AddEventAtOffset([&] {
me->HandleEmoteCommand(EMOTE_ONESHOT_LAUGH_NO_SHEATHE);
}, 15s);
Talk(SAY_AGGRO_2, 20s);
me->SetImmuneToAll(true);
me->m_Events.AddEventAtOffset([&] {
me->ClearEmoteState();
me->SetReactState(REACT_AGGRESSIVE);
me->SetImmuneToAll(false);
}, 35s);
}
}
BossAI::MoveInLineOfSight(who);
}
void DamageTaken(Unit* attacker, uint32& damage, DamageEffectType damagetype, SpellSchoolMask damageSchoolMask) override
@@ -191,19 +190,35 @@ struct boss_felblood_kaelthas : public BossAI
damage = me->GetHealth() - 1;
if (me->isRegeneratingHealth())
{
me->CombatStop();
me->CastStop();
me->SetRegeneratingHealth(false);
me->SetUnitFlag(UNIT_FLAG_DISABLE_MOVE);
me->SetImmuneToAll(true);
me->SetStandState(UNIT_STAND_STATE_KNEEL);
me->CombatStop();
me->SetReactState(REACT_PASSIVE);
LapseAction(ACTION_REMOVE_FLY);
scheduler.CancelAll();
_OOCScheduler.Schedule(6s, [this](TaskContext){
me->KillSelf();
});
summons.DespawnAll();
Talk(SAY_DEATH);
DoCastSelf(SPELL_EMOTE_EXCLAMATION);
me->m_Events.AddEventAtOffset([&] {
DoCastSelf(SPELL_EMOTE_POINT);
}, 3s);
me->m_Events.AddEventAtOffset([&] {
DoCastSelf(SPELL_EMOTE_ROAR);
}, 7s);
me->m_Events.AddEventAtOffset([&] {
DoCastSelf(SPELL_EMOTE_ROAR);
DoCastSelf(SPELL_CLEAR_FLIGHT);
}, 9s);
me->m_Events.AddEventAtOffset([&] {
me->KillSelf();
}, 11s);
}
}
BossAI::DamageTaken(attacker, damage, damagetype, damageSchoolMask);
@@ -214,6 +229,9 @@ struct boss_felblood_kaelthas : public BossAI
_gravityLapseCounter = 0;
me->GetMap()->DoForAllPlayers([&](Player* player)
{
if (player->IsGameMaster())
return;
if (action == ACTION_TELEPORT_PLAYERS)
DoCast(player, SPELL_GRAVITY_LAPSE_PLAYER + _gravityLapseCounter, true);
else if (action == ACTION_KNOCKUP)
@@ -228,15 +246,7 @@ struct boss_felblood_kaelthas : public BossAI
++_gravityLapseCounter;
});
}
void UpdateAI(uint32 diff) override
{
_OOCScheduler.Update(diff);
BossAI::UpdateAI(diff);
}
private:
TaskScheduler _OOCScheduler;
bool _hasDoneIntro;
uint8 _gravityLapseCounter;
};

View File

@@ -48,6 +48,7 @@ DoorData const doorData[] =
{ GO_SELIN_ENCOUNTER_DOOR, DATA_SELIN_FIREHEART, DOOR_TYPE_ROOM },
{ GO_VEXALLUS_DOOR, DATA_VEXALLUS, DOOR_TYPE_PASSAGE },
{ GO_DELRISSA_DOOR, DATA_DELRISSA, DOOR_TYPE_PASSAGE },
{ GO_KAEL_DOOR, DATA_KAELTHAS, DOOR_TYPE_ROOM },
{ 0, 0, DOOR_TYPE_ROOM } // END
};
@@ -64,6 +65,7 @@ public:
{
SetHeaders(DataHeader);
SetBossNumber(MAX_ENCOUNTER);
SetPersistentDataCount(MAX_PERSISTENT_DATA);
LoadObjectData(creatureData, gameobjectData);
LoadDoorData(doorData);
LoadSummonData(summonerData);

View File

@@ -32,7 +32,11 @@ enum MTData
MAX_ENCOUNTER = 4,
DATA_KALECGOS = 5,
DATA_ESCAPE_ORB = 6
DATA_ESCAPE_ORB = 6,
// Persistent data
DATA_KAEL_INTRO = 0,
MAX_PERSISTENT_DATA = 1
};
enum MTCreatures
@@ -40,8 +44,8 @@ enum MTCreatures
NPC_DELRISSA = 24560,
NPC_FEL_CRYSTAL = 24722,
NPC_KAEL_THAS = 24664,
NPC_PHOENIX = 21362,
NPC_PHOENIX_EGG = 21364,
NPC_PHOENIX = 24674,
NPC_PHOENIX_EGG = 24675,
NPC_KALECGOS = 24844
};

View File

@@ -189,10 +189,12 @@ struct npc_madrigosa : public NullCreatureAI
{
if (param == ACTION_START_EVENT)
{
me->NearTeleportTo(1570.97f, 725.51f, 79.77f, 3.82f);
me->SetDisableGravity(true);
me->SetStandState(UNIT_STAND_STATE_STAND);
me->RemoveDynamicFlag(UNIT_DYNFLAG_DEAD);
me->NearTeleportTo(1570.97f, 725.51f, 79.77f, 3.82f);
me->SendMovementFlagUpdate();
events.ScheduleEvent(EVENT_MAD_1, 2000);
}
else if (param == ACTION_SPAWN_FELMYST)
@@ -212,9 +214,9 @@ struct npc_madrigosa : public NullCreatureAI
brutallus->SetReactState(REACT_PASSIVE);
brutallus->setActive(true);
}
me->GetMotionMaster()->MovePoint(1, 1477.94f, 643.22f, 21.21f);
me->GetMotionMaster()->MoveTakeoff(1, 1477.94f, 643.22f, 21.21f);
me->AddUnitState(UNIT_STATE_NO_ENVIRONMENT_UPD);
events.ScheduleEvent(EVENT_MAD_2, 6000);
events.ScheduleEvent(EVENT_MAD_2, 4000);
break;
case EVENT_MAD_2:
Talk(SAY_MAD_1);

View File

@@ -333,11 +333,11 @@ struct boss_felmyst : public BossAI
void UpdateAI(uint32 diff) override
{
scheduler.Update(diff);
if (!UpdateVictim())
return;
scheduler.Update(diff);
if (!me->HasUnitMovementFlag(MOVEMENTFLAG_DISABLE_GRAVITY))
DoMeleeAttackIfReady();
}

View File

@@ -61,7 +61,6 @@ enum Spells
enum Misc
{
EVENT_SPELL_BERSERK = 1,
GROUP_DELAY = 1
};
@@ -103,6 +102,7 @@ struct boss_gurtogg_bloodboil : public BossAI
if (Unit* target = SelectTarget(SelectTargetMethod::Random, 1, 40.0f, true))
{
me->RemoveAurasByType(SPELL_AURA_MOD_TAUNT);
me->RemoveAurasDueToSpell(SPELL_ACIDIC_WOUND);
DoCastSelf(SPELL_FEL_RAGE_SELF, true);
DoCast(target, SPELL_FEL_RAGE_TARGET, true);
DoCast(target, SPELL_FEL_RAGE_2, true);
@@ -118,6 +118,10 @@ struct boss_gurtogg_bloodboil : public BossAI
DoCastVictim(SPELL_CHARGE);
}, 2s);
me->m_Events.AddEventAtOffset([&] {
DoCastSelf(SPELL_ACIDIC_WOUND, true);
}, 28s);
scheduler.DelayGroup(GROUP_DELAY, 30s);
}
}, 90s);
@@ -139,7 +143,7 @@ struct boss_gurtogg_bloodboil : public BossAI
return !who->IsImmunedToDamage(SPELL_SCHOOL_MASK_ALL) && !who->HasUnitState(UNIT_STATE_CONFUSED);
}
void KilledUnit(Unit* /*victim*/) override
void KilledUnit(Unit* /*victim*/) override
{
if (!_recentlySpoken)
{